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
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:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user