Filtering UX: overview results, auto-clear on re-run, config save fix
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m25s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m25s
- Add filtering results summary card on round Overview tab with pass/fail/flag counts and color-coded progress bar (polls every 5s) - Auto-delete previous filtering results when re-running so new ones stream in - Rename BUSINESS_CONCEPT to "Concept" in filtering results to prevent overflow - Fix config save race condition where toggling switches (aiParseFiles, advance counts) would revert: pendingSaveRef cleared before refetch completed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -233,6 +233,12 @@ export default function RoundDetailPage() {
|
||||
)
|
||||
const roundAwards = awards?.filter((a) => a.evaluationRoundId === roundId) ?? []
|
||||
|
||||
// Filtering results stats (only for FILTERING rounds)
|
||||
const { data: filteringStats } = trpc.filtering.getResultStats.useQuery(
|
||||
{ roundId },
|
||||
{ enabled: round?.roundType === 'FILTERING', refetchInterval: 5_000 },
|
||||
)
|
||||
|
||||
// Sync config from server when no pending save
|
||||
if (round && !pendingSaveRef.current) {
|
||||
const roundConfig = (round.configJson as Record<string, unknown>) ?? {}
|
||||
@@ -245,8 +251,12 @@ export default function RoundDetailPage() {
|
||||
const updateMutation = trpc.round.update.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.round.getById.invalidate({ id: roundId })
|
||||
pendingSaveRef.current = false
|
||||
setAutosaveStatus('saved')
|
||||
// Keep pendingSaveRef locked briefly so the sync block doesn't
|
||||
// overwrite local config with stale cache before refetch completes
|
||||
setTimeout(() => {
|
||||
pendingSaveRef.current = false
|
||||
}, 500)
|
||||
setTimeout(() => setAutosaveStatus('idle'), 2000)
|
||||
},
|
||||
onError: (err) => {
|
||||
@@ -866,8 +876,81 @@ export default function RoundDetailPage() {
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
{/* Filtering Results Summary — only for FILTERING rounds with results */}
|
||||
{isFiltering && filteringStats && filteringStats.total > 0 && (
|
||||
<AnimatedCard index={1}>
|
||||
<Card className="border-l-4 border-l-purple-500">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-full bg-purple-50 p-2">
|
||||
<Shield className="h-5 w-5 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base">Filtering Results</CardTitle>
|
||||
<CardDescription>
|
||||
{filteringStats.total} projects evaluated
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setActiveTab('filtering')}
|
||||
>
|
||||
View Details
|
||||
<ArrowRight className="h-3.5 w-3.5 ml-1.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-3 gap-4 mb-4">
|
||||
<div className="text-center p-3 rounded-lg bg-emerald-50">
|
||||
<p className="text-2xl font-bold text-emerald-700">{filteringStats.passed}</p>
|
||||
<p className="text-xs text-emerald-600 font-medium">Passed</p>
|
||||
</div>
|
||||
<div className="text-center p-3 rounded-lg bg-red-50">
|
||||
<p className="text-2xl font-bold text-red-700">{filteringStats.filteredOut}</p>
|
||||
<p className="text-xs text-red-600 font-medium">Filtered Out</p>
|
||||
</div>
|
||||
<div className="text-center p-3 rounded-lg bg-amber-50">
|
||||
<p className="text-2xl font-bold text-amber-700">{filteringStats.flagged}</p>
|
||||
<p className="text-xs text-amber-600 font-medium">Flagged</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Progress bar showing pass rate */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Pass rate</span>
|
||||
<span>{Math.round((filteringStats.passed / filteringStats.total) * 100)}%</span>
|
||||
</div>
|
||||
<div className="h-2.5 rounded-full bg-muted overflow-hidden flex">
|
||||
<div
|
||||
className="bg-emerald-500 transition-all duration-500"
|
||||
style={{ width: `${(filteringStats.passed / filteringStats.total) * 100}%` }}
|
||||
/>
|
||||
<div
|
||||
className="bg-red-400 transition-all duration-500"
|
||||
style={{ width: `${(filteringStats.filteredOut / filteringStats.total) * 100}%` }}
|
||||
/>
|
||||
<div
|
||||
className="bg-amber-400 transition-all duration-500"
|
||||
style={{ width: `${(filteringStats.flagged / filteringStats.total) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
{filteringStats.overridden > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{filteringStats.overridden} result(s) manually overridden
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
)}
|
||||
|
||||
{/* Quick Actions — Grouped & styled */}
|
||||
<AnimatedCard index={1}>
|
||||
<AnimatedCard index={2}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Quick Actions</CardTitle>
|
||||
|
||||
Reference in New Issue
Block a user