Jury evaluation UX overhaul + admin review features
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m53s

- Fix project documents not displaying on jury project page (rewrote MultiWindowDocViewer to use file.listByProject)
- Add working download/preview for project files via presigned URLs
- Display project tags on jury project detail page
- Add autosave for evaluation drafts (debounced 3s + save on unmount/beforeunload)
- Support mixed criterion types: numeric scores, yes/no booleans, text responses, section headers
- Replace inline criteria editor with rich EvaluationFormBuilder on admin round page
- Remove COI dialog from evaluation page
- Update AI summary service to handle boolean/text criteria (yes/no counts, text synthesis)
- Update EvaluationSummaryCard to show boolean criteria bars and text responses
- Add evaluation detail sheet on admin project page (click juror row to view full scores + feedback)
- Add Recent Evaluations dashboard widget showing latest jury reviews

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-18 12:43:28 +01:00
parent 73759eaddd
commit 9ce56f13fd
12 changed files with 1137 additions and 385 deletions

View File

@@ -0,0 +1,114 @@
'use client'
import Link from 'next/link'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { ClipboardCheck, ThumbsUp, ThumbsDown, ExternalLink } from 'lucide-react'
import { formatDistanceToNow } from 'date-fns'
type RecentEvaluation = {
id: string
globalScore: number | null
binaryDecision: boolean | null
submittedAt: Date | string | null
feedbackText: string | null
assignment: {
project: { id: string; title: string }
round: { id: string; name: string }
user: { id: string; name: string | null; email: string }
}
}
export function RecentEvaluations({ evaluations }: { evaluations: RecentEvaluation[] }) {
if (!evaluations || evaluations.length === 0) {
return (
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<ClipboardCheck className="h-4 w-4" />
Recent Evaluations
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground text-center py-4">
No evaluations submitted yet
</p>
</CardContent>
</Card>
)
}
return (
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<ClipboardCheck className="h-4 w-4" />
Recent Evaluations
</CardTitle>
<CardDescription>Latest jury reviews as they come in</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{evaluations.map((ev) => (
<Link
key={ev.id}
href={`/admin/projects/${ev.assignment.project.id}`}
className="block group"
>
<div className="flex items-start gap-3 p-2.5 rounded-lg border hover:bg-muted/50 transition-colors">
{/* Score indicator */}
<div className="flex flex-col items-center gap-0.5 shrink-0 pt-0.5">
{ev.globalScore !== null ? (
<span className="text-lg font-bold tabular-nums leading-none">
{ev.globalScore}
</span>
) : (
<span className="text-lg font-bold text-muted-foreground leading-none">-</span>
)}
<span className="text-[10px] text-muted-foreground">/10</span>
</div>
{/* Details */}
<div className="flex-1 min-w-0 space-y-1">
<div className="flex items-center gap-2">
<p className="text-sm font-medium truncate flex-1">
{ev.assignment.project.title}
</p>
<ExternalLink className="h-3 w-3 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span className="truncate">{ev.assignment.user.name || ev.assignment.user.email}</span>
<span className="shrink-0">
{ev.submittedAt
? formatDistanceToNow(new Date(ev.submittedAt), { addSuffix: true })
: ''}
</span>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-[10px] h-5">
{ev.assignment.round.name}
</Badge>
{ev.binaryDecision !== null && (
ev.binaryDecision ? (
<span className="flex items-center gap-0.5 text-xs text-emerald-600">
<ThumbsUp className="h-3 w-3" /> Yes
</span>
) : (
<span className="flex items-center gap-0.5 text-xs text-red-500">
<ThumbsDown className="h-3 w-3" /> No
</span>
)
)}
</div>
{ev.feedbackText && (
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed">
{ev.feedbackText}
</p>
)}
</div>
</div>
</Link>
))}
</CardContent>
</Card>
)
}