fix: build speed, observer AI details, round tracker empty state
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
- Disable typedRoutes and skip TS in build (run tsc separately) — build drops from ~9min to ~36s - Expand optimizePackageImports for sonner, date-fns, recharts, motion, zod - Docker: mount .next/cache as build cache for faster rebuilds - Observer filtering panel: fix AI reasoning extraction (nested under rule ID) and show confidence, quality score, spam risk, override reason - Round User Tracker: show empty state message instead of disappearing when selected round has no passed projects yet Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,9 +23,9 @@ COPY . .
|
||||
# Generate Prisma client
|
||||
RUN npx prisma generate
|
||||
|
||||
# Build Next.js
|
||||
# Build Next.js — mount .next/cache as a Docker build cache for faster rebuilds
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN npm run build
|
||||
RUN --mount=type=cache,target=/app/.next/cache npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
|
||||
@@ -2,10 +2,21 @@ import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
typedRoutes: true,
|
||||
serverExternalPackages: ['@prisma/client', 'minio'],
|
||||
typescript: {
|
||||
// We run tsc --noEmit separately before each push
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
experimental: {
|
||||
optimizePackageImports: ['lucide-react'],
|
||||
optimizePackageImports: [
|
||||
'lucide-react',
|
||||
'sonner',
|
||||
'date-fns',
|
||||
'recharts',
|
||||
'motion/react',
|
||||
'zod',
|
||||
'@radix-ui/react-icons',
|
||||
],
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
|
||||
@@ -57,23 +57,21 @@ export function RoundUserTracker({ editionId }: RoundUserTrackerProps) {
|
||||
const { rounds, byCategory } = data
|
||||
const effectiveRoundId = data.selectedRoundId
|
||||
|
||||
// Don't render if no rounds or no data
|
||||
if (!effectiveRoundId || rounds.length === 0) return null
|
||||
// Don't render if no rounds at all
|
||||
if (rounds.length === 0) return null
|
||||
|
||||
const totalProjects = byCategory.reduce((sum, c) => sum + c.total, 0)
|
||||
const totalActivated = byCategory.reduce((sum, c) => sum + c.accountsSet, 0)
|
||||
const totalPending = byCategory.reduce((sum, c) => sum + c.accountsNotSet, 0)
|
||||
|
||||
if (totalProjects === 0) return null
|
||||
|
||||
const selectedRound = rounds.find(r => r.id === effectiveRoundId)
|
||||
const selectedRound = effectiveRoundId ? rounds.find(r => r.id === effectiveRoundId) : undefined
|
||||
|
||||
const handleSendReminder = async (target: string, opts: { category?: 'STARTUP' | 'BUSINESS_CONCEPT' }) => {
|
||||
setSendingTarget(target)
|
||||
try {
|
||||
await sendReminders.mutateAsync({
|
||||
editionId,
|
||||
roundId: effectiveRoundId,
|
||||
roundId: effectiveRoundId!,
|
||||
category: opts.category,
|
||||
})
|
||||
} finally {
|
||||
@@ -89,13 +87,15 @@ export function RoundUserTracker({ editionId }: RoundUserTrackerProps) {
|
||||
<Users className="h-4 w-4 text-brand-blue" />
|
||||
Round User Tracker
|
||||
</CardTitle>
|
||||
<Badge variant="outline" className="text-xs shrink-0">
|
||||
{totalActivated}/{totalProjects} activated
|
||||
</Badge>
|
||||
{totalProjects > 0 && (
|
||||
<Badge variant="outline" className="text-xs shrink-0">
|
||||
{totalActivated}/{totalProjects} activated
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{/* Round selector */}
|
||||
<Select
|
||||
value={effectiveRoundId}
|
||||
value={effectiveRoundId ?? ''}
|
||||
onValueChange={(val) => setSelectedRoundId(val)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs mt-2">
|
||||
@@ -114,6 +114,15 @@ export function RoundUserTracker({ editionId }: RoundUserTrackerProps) {
|
||||
</Select>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{totalProjects === 0 ? (
|
||||
<div className="text-center py-4">
|
||||
<Users className="h-8 w-8 text-muted-foreground/30 mx-auto mb-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No projects have passed {selectedRound?.name ?? 'this round'} yet
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Subtitle showing round context */}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Projects that passed <span className="font-medium">{selectedRound?.name ?? 'this round'}</span> — account activation status
|
||||
@@ -192,6 +201,8 @@ export function RoundUserTracker({ editionId }: RoundUserTrackerProps) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -19,6 +19,29 @@ import {
|
||||
import { Filter, ChevronDown, ChevronUp, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type AIScreeningData = {
|
||||
meetsCriteria?: boolean
|
||||
confidence?: number
|
||||
reasoning?: string
|
||||
qualityScore?: number
|
||||
spamRisk?: boolean
|
||||
}
|
||||
|
||||
function parseAIData(json: unknown): AIScreeningData | null {
|
||||
if (!json || typeof json !== 'object') return null
|
||||
const obj = json as Record<string, unknown>
|
||||
// aiScreeningJson is nested under rule ID: { [ruleId]: { outcome, confidence, ... } }
|
||||
if (!('outcome' in obj) && !('reasoning' in obj)) {
|
||||
const keys = Object.keys(obj)
|
||||
if (keys.length > 0) {
|
||||
const inner = obj[keys[0]]
|
||||
if (inner && typeof inner === 'object') return inner as AIScreeningData
|
||||
}
|
||||
return null
|
||||
}
|
||||
return obj as unknown as AIScreeningData
|
||||
}
|
||||
|
||||
export function FilteringPanel({ roundId }: { roundId: string }) {
|
||||
const [outcomeFilter, setOutcomeFilter] = useState<string>('ALL')
|
||||
const [page, setPage] = useState(1)
|
||||
@@ -199,17 +222,39 @@ export function FilteringPanel({ roundId }: { roundId: string }) {
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{expandedId === r.id && (
|
||||
<div className="px-4 pb-3 pt-0">
|
||||
<div className="rounded bg-muted/50 p-3 text-xs leading-relaxed text-muted-foreground">
|
||||
{(() => {
|
||||
const screening = r.aiScreeningJson as Record<string, unknown> | null
|
||||
const reasoning = (screening?.reasoning ?? screening?.explanation ?? r.overrideReason ?? 'No details available') as string
|
||||
return reasoning
|
||||
})()}
|
||||
{expandedId === r.id && (() => {
|
||||
const ai = parseAIData(r.aiScreeningJson)
|
||||
return (
|
||||
<div className="px-4 pb-3 pt-0 space-y-2">
|
||||
{ai?.confidence != null && (
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
{ai.confidence != null && (
|
||||
<span className="text-muted-foreground">
|
||||
Confidence: <strong>{Math.round(ai.confidence * 100)}%</strong>
|
||||
</span>
|
||||
)}
|
||||
{ai.qualityScore != null && (
|
||||
<span className="text-muted-foreground">
|
||||
Quality: <strong>{ai.qualityScore}/10</strong>
|
||||
</span>
|
||||
)}
|
||||
{ai.spamRisk && (
|
||||
<Badge variant="destructive" className="text-[10px] px-1.5 py-0">Spam Risk</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded bg-muted/50 border p-3 text-xs leading-relaxed text-muted-foreground whitespace-pre-wrap">
|
||||
{ai?.reasoning || 'No AI reasoning available'}
|
||||
</div>
|
||||
{r.overrideReason && (
|
||||
<div className="rounded bg-amber-50 border border-amber-200 p-3 text-xs">
|
||||
<span className="font-medium text-amber-800">Override: </span>
|
||||
{r.overrideReason}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user