feat: side panel adds country, description, and per-criterion scores
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m40s
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:
@@ -1112,6 +1112,18 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
<SheetDescription>
|
||||
{selectedProjectId ? `ID: …${selectedProjectId.slice(-8)}` : ''}
|
||||
</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 && (
|
||||
<a
|
||||
href={`/admin/projects/${selectedProjectId}`}
|
||||
@@ -1154,6 +1166,20 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
<Switch checked={useBalancedPassRate} onCheckedChange={persistUseBalancedPassRate} />
|
||||
</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 */}
|
||||
{projectDetail.stats && (() => {
|
||||
const raw = selectedProjectId
|
||||
@@ -1266,12 +1292,57 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded && a.evaluation?.feedbackText && (
|
||||
<p className="mt-2 text-sm text-muted-foreground whitespace-pre-wrap border-t pt-2">
|
||||
{isExpanded && (
|
||||
<div className="mt-2 space-y-2 border-t pt-2">
|
||||
{/* Per-criterion scores */}
|
||||
{(() => {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user