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>
|
<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>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user