Fix filtering config save, auto-save, streamed results, improved AI prompt
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m7s

- Add missing fields to FilteringConfigSchema (aiParseFiles, startupAdvanceCount,
  conceptAdvanceCount, notifyOnEntry, notifyOnAdvance) — Zod was silently
  stripping them on save
- Restore auto-save with 800ms debounce on config changes
- Add staggered animations for filtering results (stream in one-by-one)
- Improve AI screening prompt: file type label mappings, soft cap handling,
  missing documents = fail, better user prompt structure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-17 17:18:04 +01:00
parent bed444e5f4
commit cf1508f856
4 changed files with 107 additions and 11 deletions

View File

@@ -1,6 +1,6 @@
'use client'
import { useState, useEffect, useCallback, useRef } from 'react'
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
import { trpc } from '@/lib/trpc/client'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
@@ -70,6 +70,7 @@ import {
ListFilter,
Settings2,
} from 'lucide-react'
import { motion, AnimatePresence } from 'motion/react'
import Link from 'next/link'
import type { Route } from 'next'
import { AwardShortlist } from './award-shortlist'
@@ -401,6 +402,30 @@ export function FilteringDashboard({ competitionId, roundId }: FilteringDashboar
)
}) ?? []
// ── Staggered reveal: animate new results in one by one ─────────────
const prevResultIdsRef = useRef<Set<string>>(new Set())
const isFirstLoad = useRef(true)
const resultStagger = useMemo(() => {
const stagger: Record<string, number> = {}
if (isFirstLoad.current && displayResults.length > 0) {
isFirstLoad.current = false
return stagger // No stagger on first load — show immediately
}
let newIdx = 0
for (const r of displayResults) {
if (!prevResultIdsRef.current.has((r as any).id)) {
stagger[(r as any).id] = newIdx++
}
}
return stagger
}, [displayResults])
// Update known IDs after render
useEffect(() => {
prevResultIdsRef.current = new Set(displayResults.map((r: any) => r.id))
}, [displayResults])
return (
<div className="space-y-6">
{/* Main Card: AI Screening Criteria + Controls */}
@@ -766,13 +791,26 @@ export function FilteringDashboard({ competitionId, roundId }: FilteringDashboar
</div>
{/* Rows */}
<AnimatePresence initial={false}>
{displayResults.map((result: any) => {
const ai = parseAIData(result.aiScreeningJson)
const effectiveOutcome = result.finalOutcome || result.outcome
const isExpanded = expandedId === result.id
const staggerIdx = resultStagger[result.id]
const isNew = staggerIdx !== undefined
return (
<div key={result.id} className="border-b last:border-b-0 animate-in fade-in-0 duration-300">
<motion.div
key={result.id}
initial={isNew ? { opacity: 0, y: 16, scale: 0.98 } : false}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{
duration: 0.35,
delay: isNew ? staggerIdx * 0.12 : 0,
ease: [0.25, 0.46, 0.45, 0.94],
}}
className="border-b last:border-b-0"
>
{/* Main Row */}
<div
className="grid grid-cols-[40px_1fr_120px_100px_70px_70px_120px] gap-2 px-3 py-2.5 items-center hover:bg-muted/50 text-sm cursor-pointer"
@@ -980,9 +1018,10 @@ export function FilteringDashboard({ competitionId, roundId }: FilteringDashboar
</div>
</div>
)}
</div>
</motion.div>
)
})}
</AnimatePresence>
{/* Pagination */}
{resultsPage && resultsPage.totalPages > 1 && (