feat: side panel adds country, description, and per-criterion scores
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m40s

Three side-panel additions on the ranking dashboard's project detail
sheet:

- Project country and team name as outline badges in the header,
  matching the row chips on the list view.
- Collapsible 'Description' box (closed by default) that reveals the
  full project description without leaving the panel.
- Expanding a juror row now shows their per-criterion scores in
  addition to the free-text feedback. Boolean criteria render with
  the form's trueLabel/falseLabel (or 'Yes'/'No' fallback); numeric
  criteria show the raw value next to the criterion label from the
  active form's criteriaJson.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-04-27 14:30:12 +02:00
parent 70f1f64ea3
commit e0103fa956

View File

@@ -1112,6 +1112,18 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
<SheetDescription> <SheetDescription>
{selectedProjectId ? `ID: …${selectedProjectId.slice(-8)}` : ''} {selectedProjectId ? `ID: …${selectedProjectId.slice(-8)}` : ''}
</SheetDescription> </SheetDescription>
<div className="flex flex-wrap items-center gap-2 mt-1">
{projectDetail?.project.country && (
<Badge variant="outline" className="gap-1 text-xs">
<CountryDisplay country={projectDetail.project.country} />
</Badge>
)}
{projectDetail?.project.teamName && (
<Badge variant="outline" className="gap-1 text-xs">
{projectDetail.project.teamName}
</Badge>
)}
</div>
{selectedProjectId && ( {selectedProjectId && (
<a <a
href={`/admin/projects/${selectedProjectId}`} href={`/admin/projects/${selectedProjectId}`}
@@ -1154,6 +1166,20 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
<Switch checked={useBalancedPassRate} onCheckedChange={persistUseBalancedPassRate} /> <Switch checked={useBalancedPassRate} onCheckedChange={persistUseBalancedPassRate} />
</div> </div>
</div> </div>
{/* Project description (collapsible) */}
{projectDetail.project.description && (
<Collapsible>
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg border p-3 text-left hover:bg-muted/50 transition-colors">
<span className="text-sm font-medium">Description</span>
<ChevronDown className="h-4 w-4 transition-transform data-[state=open]:rotate-180" />
</CollapsibleTrigger>
<CollapsibleContent className="mt-1 rounded-lg border bg-muted/30 p-3 text-sm whitespace-pre-wrap">
{projectDetail.project.description}
</CollapsibleContent>
</Collapsible>
)}
{/* Stats summary: combined Avg card with Raw + Balanced side-by-side */} {/* Stats summary: combined Avg card with Raw + Balanced side-by-side */}
{projectDetail.stats && (() => { {projectDetail.stats && (() => {
const raw = selectedProjectId const raw = selectedProjectId
@@ -1266,10 +1292,55 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
})()} })()}
</div> </div>
</div> </div>
{isExpanded && a.evaluation?.feedbackText && ( {isExpanded && (
<p className="mt-2 text-sm text-muted-foreground whitespace-pre-wrap border-t pt-2"> <div className="mt-2 space-y-2 border-t pt-2">
{a.evaluation.feedbackText} {/* Per-criterion scores */}
</p> {(() => {
const scores = a.evaluation?.criterionScoresJson as Record<string, unknown> | null
if (!scores || !evalForm?.criteriaJson) return null
const criteria = evalForm.criteriaJson as Array<{
id: string
label: string
type?: string
trueLabel?: string
falseLabel?: string
scale?: number | string
}>
const rendered = criteria
.map((c) => {
const v = scores[c.id]
if (v == null || v === '') return null
let display: string
if (typeof v === 'boolean') {
display = v ? (c.trueLabel ?? 'Yes') : (c.falseLabel ?? 'No')
} else if (typeof v === 'number') {
display = String(v)
} else {
display = String(v)
}
return { label: c.label, display, type: c.type ?? 'numeric' }
})
.filter((x): x is { label: string; display: string; type: string } => x != null)
if (rendered.length === 0) return null
return (
<div className="space-y-1">
{rendered.map((c, i) => (
<div key={i} className="flex items-start justify-between gap-3 text-xs">
<span className="text-muted-foreground flex-1">{c.label}</span>
<span className="font-medium tabular-nums">{c.display}</span>
</div>
))}
</div>
)
})()}
{/* Feedback text */}
{a.evaluation?.feedbackText && (
<p className="text-sm text-muted-foreground whitespace-pre-wrap">
{a.evaluation.feedbackText}
</p>
)}
</div>
)} )}
</div> </div>
) )