From 387f84c338a471418751cef032f538ef99c05d41 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 27 Apr 2026 13:20:21 +0200 Subject: [PATCH] feat: per-round balanced-scoring toggle in side sheet A Switch at the top of the project side panel writes useBalancedRanking onto Round.configJson via the existing round.update mutation. The flip is shared across all viewers because the value lives in the round's persisted config; hydration runs on every roundData refetch so the UI converges quickly when another admin flips it. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../admin/round/ranking-dashboard.tsx | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/src/components/admin/round/ranking-dashboard.tsx b/src/components/admin/round/ranking-dashboard.tsx index 4655c4d..32428da 100644 --- a/src/components/admin/round/ranking-dashboard.tsx +++ b/src/components/admin/round/ranking-dashboard.tsx @@ -37,6 +37,7 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { Slider } from '@/components/ui/slider' +import { Switch } from '@/components/ui/switch' import { Collapsible, CollapsibleContent, @@ -271,6 +272,7 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran const [localCriteriaText, setLocalCriteriaText] = useState('') const [localScoreWeight, setLocalScoreWeight] = useState(5) const [localPassRateWeight, setLocalPassRateWeight] = useState(5) + const [useBalanced, setUseBalanced] = useState(true) const weightsInitialized = useRef(false) // ─── Sensors ────────────────────────────────────────────────────────────── @@ -471,18 +473,32 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran }, [evalForm]) // ─── Init local weights + criteriaText from round config ────────────────── + // useBalanced is hydrated on every roundData refetch (it has its own toggle + // that persists immediately), so it sits outside the once-only guard. useEffect(() => { - if (!weightsInitialized.current && roundData?.configJson) { - const cfg = roundData.configJson as Record - const saved = (cfg.criteriaWeights ?? {}) as Record - setLocalWeights(saved) - setLocalCriteriaText((cfg.rankingCriteria as string) ?? '') - setLocalScoreWeight((cfg.scoreWeight as number) ?? 5) - setLocalPassRateWeight((cfg.passRateWeight as number) ?? 5) - weightsInitialized.current = true - } + if (!roundData?.configJson) return + const cfg = roundData.configJson as Record + setUseBalanced((cfg.useBalancedRanking as boolean | undefined) ?? true) + if (weightsInitialized.current) return + const saved = (cfg.criteriaWeights ?? {}) as Record + setLocalWeights(saved) + setLocalCriteriaText((cfg.rankingCriteria as string) ?? '') + setLocalScoreWeight((cfg.scoreWeight as number) ?? 5) + setLocalPassRateWeight((cfg.passRateWeight as number) ?? 5) + weightsInitialized.current = true }, [roundData]) + // ─── Persist the balanced-ranking toggle immediately ───────────────────── + const persistUseBalanced = (next: boolean) => { + setUseBalanced(next) + if (!roundData?.configJson) return + const cfg = roundData.configJson as Record + updateRoundMutation.mutate({ + id: roundId, + configJson: { ...cfg, useBalancedRanking: next }, + }) + } + // ─── Save weights + criteria text to round config ───────────────────────── const saveRankingConfig = () => { if (!roundData?.configJson) return @@ -1001,6 +1017,16 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran ) : projectDetail ? (
+ {/* Balanced-ranking toggle (per-round; persists across viewers) */} +
+
+ Use balanced scoring for ranking + + Corrects for per-juror grading style. Off uses raw averages. + +
+ +
{/* Stats summary */} {projectDetail.stats && (