feat: side panel shows per-juror baseline and balanced contribution

Extends the ranking router's roundEvaluationScores response with
per-juror grading stats (mean, stddev, count) plus the round's overall
mean/stddev. The side-sheet juror rows render 'typical X.XX →
contributes Y.YY' next to each Score badge whenever balanced is on,
making the z-rescaling visible per individual rather than only as a
project-level number.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-04-27 13:24:34 +02:00
parent 664a682585
commit ee68f8af41
2 changed files with 36 additions and 1 deletions

View File

@@ -1136,6 +1136,28 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
</Badge> </Badge>
)} )}
<Badge variant="outline">Score: {a.evaluation?.globalScore?.toFixed(1) ?? '—'}</Badge> <Badge variant="outline">Score: {a.evaluation?.globalScore?.toFixed(1) ?? '—'}</Badge>
{useBalanced && (() => {
const userId = a.user?.id
const score = a.evaluation?.globalScore
if (!userId || score == null) return null
const stats = evalScores?.jurorStats?.[userId]
const overallMean = evalScores?.overallMean
const overallStddev = evalScores?.overallStddev
if (!stats || overallMean == null || overallStddev == null || overallStddev === 0) return null
const z = stats.stddev > 0
? (score - stats.mean) / stats.stddev
: (score - overallMean) / overallStddev
const contributesAs = overallMean + z * overallStddev
return (
<span
className="text-xs text-muted-foreground"
title={`Their typical score in this round; rescaled contribution after juror balancing`}
onClick={(e) => e.stopPropagation()}
>
typical {stats.mean.toFixed(2)} contributes {contributesAs.toFixed(2)}
</span>
)
})()}
</div> </div>
</div> </div>
{isExpanded && a.evaluation?.feedbackText && ( {isExpanded && a.evaluation?.feedbackText && (

View File

@@ -537,6 +537,19 @@ export const rankingRouter = router({
} }
} }
return { byProject, balanced } // Per-juror grading stats so the side panel can render each juror's
// personal baseline and rescaled contribution.
const jurorStats: Record<string, { mean: number; stddev: number; count: number }> = {}
for (const [userId, s] of balanceCtx.jurorStats.entries()) {
jurorStats[userId] = { mean: s.mean, stddev: s.stddev, count: s.count }
}
return {
byProject,
balanced,
jurorStats,
overallMean: balanceCtx.overallMean,
overallStddev: balanceCtx.overallStddev,
}
}), }),
}) })