feat: side panel shows raw + balanced averages, list drops delta

Removes the per-row '⇢ X.X' annotation from the ranking list — the
list view stays clean. The side panel's stats area gains a combined
Avg Score card that shows Raw and Balanced side-by-side, with the
active one (per the round's toggle) bolded and tagged 'used for
ranking'. Pass Rate and Evaluators move below into a 2-col grid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-04-27 13:23:29 +02:00
parent e12f26092a
commit 664a682585

View File

@@ -83,7 +83,6 @@ type SortableProjectRowProps = {
entry: (RankedProjectEntry & { originalIndex?: number }) | undefined entry: (RankedProjectEntry & { originalIndex?: number }) | undefined
projectInfo: ProjectInfo | undefined projectInfo: ProjectInfo | undefined
jurorScores: JurorScore[] | undefined jurorScores: JurorScore[] | undefined
balancedScore: number | null
onSelect: () => void onSelect: () => void
isSelected: boolean isSelected: boolean
originalRank: number | undefined // from snapshotOrder — always in sync with localOrder originalRank: number | undefined // from snapshotOrder — always in sync with localOrder
@@ -97,7 +96,6 @@ function SortableProjectRow({
entry, entry,
projectInfo, projectInfo,
jurorScores, jurorScores,
balancedScore,
onSelect, onSelect,
isSelected, isSelected,
originalRank, originalRank,
@@ -202,27 +200,6 @@ function SortableProjectRow({
</span> </span>
) : null} ) : null}
{/* Raw + balanced averages shown side by side */}
{entry?.avgGlobalScore !== null && entry?.avgGlobalScore !== undefined && jurorScores && jurorScores.length > 1 && (
<div className="flex items-center gap-1.5 text-xs" title="Raw juror average vs. juror-balanced average (z-score normalized per juror, rescaled to 1-10)">
<span className="font-medium text-muted-foreground">
{entry.avgGlobalScore.toFixed(1)}
</span>
{balancedScore != null && Math.abs(balancedScore - entry.avgGlobalScore) >= 0.05 && (
<span
className={cn(
'font-semibold tabular-nums rounded px-1.5 py-0.5 border',
balancedScore > entry.avgGlobalScore
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
: 'bg-amber-50 text-amber-700 border-amber-200',
)}
>
{balancedScore.toFixed(1)}
</span>
)}
</div>
)}
{/* Advance decision indicator */} {/* Advance decision indicator */}
<div className={cn( <div className={cn(
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium', 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium',
@@ -1000,7 +977,6 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
entry={rankingMap.get(projectId)} entry={rankingMap.get(projectId)}
projectInfo={projectInfoMap.get(projectId)} projectInfo={projectInfoMap.get(projectId)}
jurorScores={evalScores?.byProject[projectId]} jurorScores={evalScores?.byProject[projectId]}
balancedScore={evalScores?.balanced[projectId]?.balancedAverage ?? null}
onSelect={() => setSelectedProjectId(projectId)} onSelect={() => setSelectedProjectId(projectId)}
isSelected={selectedProjectId === projectId} isSelected={selectedProjectId === projectId}
originalRank={hasReorders ? snapshotOrder[projectId] : undefined} originalRank={hasReorders ? snapshotOrder[projectId] : undefined}
@@ -1075,31 +1051,50 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
</div> </div>
<Switch checked={useBalanced} onCheckedChange={persistUseBalanced} /> <Switch checked={useBalanced} onCheckedChange={persistUseBalanced} />
</div> </div>
{/* Stats summary */} {/* Stats summary: combined Avg card with Raw + Balanced side-by-side */}
{projectDetail.stats && ( {projectDetail.stats && (() => {
<div className="grid grid-cols-3 gap-3"> const raw = selectedProjectId
<div className="rounded-lg border p-3 text-center"> ? evalScores?.balanced[selectedProjectId]?.rawAverage ?? null
<p className="text-xs text-muted-foreground">Avg Score</p> : null
<p className="mt-1 text-lg font-semibold"> const balanced = selectedProjectId
{projectDetail.stats.averageGlobalScore?.toFixed(1) ?? '—'} ? evalScores?.balanced[selectedProjectId]?.balancedAverage ?? null
</p> : null
return (
<div className="space-y-3">
<div className="rounded-lg border p-3">
<p className="text-xs text-muted-foreground mb-2">Avg Score</p>
<div className="flex items-baseline gap-4 flex-wrap">
<div className={`flex items-baseline gap-1 ${useBalanced ? 'text-muted-foreground' : 'font-semibold'}`}>
<span className="text-xs">Raw</span>
<span className="text-lg tabular-nums">{raw != null ? raw.toFixed(1) : '—'}</span>
{!useBalanced && <span className="ml-1 text-[10px] text-muted-foreground"> used for ranking</span>}
</div>
<div className={`flex items-baseline gap-1 ${useBalanced ? 'font-semibold' : 'text-muted-foreground'}`}>
<span className="text-xs">Balanced</span>
<span className="text-lg tabular-nums">{balanced != null ? balanced.toFixed(1) : '—'}</span>
{useBalanced && <span className="ml-1 text-[10px] text-muted-foreground"> used for ranking</span>}
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="rounded-lg border p-3 text-center">
<p className="text-xs text-muted-foreground">Pass Rate</p>
<p className="mt-1 text-lg font-semibold">
{projectDetail.stats.totalEvaluations > 0
? `${Math.round((projectDetail.stats.yesVotes / projectDetail.stats.totalEvaluations) * 100)}%`
: '—'}
</p>
</div>
<div className="rounded-lg border p-3 text-center">
<p className="text-xs text-muted-foreground">Evaluators</p>
<p className="mt-1 text-lg font-semibold">
{projectDetail.stats.totalEvaluations}
</p>
</div>
</div>
</div> </div>
<div className="rounded-lg border p-3 text-center"> )
<p className="text-xs text-muted-foreground">Pass Rate</p> })()}
<p className="mt-1 text-lg font-semibold">
{projectDetail.stats.totalEvaluations > 0
? `${Math.round((projectDetail.stats.yesVotes / projectDetail.stats.totalEvaluations) * 100)}%`
: '—'}
</p>
</div>
<div className="rounded-lg border p-3 text-center">
<p className="text-xs text-muted-foreground">Evaluators</p>
<p className="mt-1 text-lg font-semibold">
{projectDetail.stats.totalEvaluations}
</p>
</div>
</div>
)}
{/* Per-juror evaluations */} {/* Per-juror evaluations */}
<div> <div>