feat: list sort respects useBalancedRanking toggle
The two existing sort sites (initial init + threshold cutoff) now read from the local toggle. A second effect re-sorts the list when the toggle flips, but only when no manual reorder is pinned to the snapshot — persisted manual reorders always win, matching prior behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -411,12 +411,15 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
const dedupedStartup = dedup(startup)
|
||||
const dedupedConcept = dedup(concept)
|
||||
|
||||
// Sort by balanced (juror-corrected) score descending, falling back to raw
|
||||
// avgGlobalScore when no balanced score is available, then compositeScore as
|
||||
// a final tiebreaker. The threshold cutoff line uses the same metric so the
|
||||
// cutoff lands in the correct spot regardless of which score type is used.
|
||||
const scoreFor = (projectId: string, raw: number | null | undefined) =>
|
||||
evalScores.balanced[projectId]?.balancedAverage ?? raw ?? 0
|
||||
// Sort by balanced (juror-corrected) score descending when the toggle is
|
||||
// on, otherwise by raw. compositeScore is the final tiebreaker. The
|
||||
// threshold cutoff line uses the same metric so the cutoff lands in the
|
||||
// right spot regardless of which score type is used.
|
||||
const scoreFor = (projectId: string, raw: number | null | undefined) => {
|
||||
const balanced = evalScores.balanced[projectId]?.balancedAverage
|
||||
if (useBalanced && balanced != null) return balanced
|
||||
return raw ?? 0
|
||||
}
|
||||
dedupedStartup.sort((a, b) =>
|
||||
scoreFor(b.projectId, b.avgGlobalScore) - scoreFor(a.projectId, a.avgGlobalScore)
|
||||
|| b.compositeScore - a.compositeScore)
|
||||
@@ -465,6 +468,49 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
}
|
||||
}, [snapshot, evalScores])
|
||||
|
||||
// ─── Re-sort on toggle flip (after init) ─────────────────────────────────
|
||||
// Only resorts when no server-side manual reorder is pinned for the snapshot;
|
||||
// persisted manual reorders always win regardless of the score being used.
|
||||
useEffect(() => {
|
||||
if (!initialized.current || !snapshot || !evalScores) return
|
||||
const reorders = (snapshot.reordersJson as Array<{
|
||||
category: 'STARTUP' | 'BUSINESS_CONCEPT'
|
||||
orderedProjectIds: string[]
|
||||
}> | null) ?? []
|
||||
const hasManualReorder =
|
||||
reorders.some((r) => r.category === 'STARTUP') ||
|
||||
reorders.some((r) => r.category === 'BUSINESS_CONCEPT')
|
||||
if (hasManualReorder) return
|
||||
const startup = (snapshot.startupRankingJson ?? []) as unknown as RankedProjectEntry[]
|
||||
const concept = (snapshot.conceptRankingJson ?? []) as unknown as RankedProjectEntry[]
|
||||
const dedup = (arr: RankedProjectEntry[]): RankedProjectEntry[] => {
|
||||
const seen = new Set<string>()
|
||||
return arr.filter((r) => {
|
||||
if (seen.has(r.projectId)) return false
|
||||
seen.add(r.projectId)
|
||||
return true
|
||||
})
|
||||
}
|
||||
const scoreFor = (projectId: string, raw: number | null | undefined) => {
|
||||
const balanced = evalScores.balanced[projectId]?.balancedAverage
|
||||
if (useBalanced && balanced != null) return balanced
|
||||
return raw ?? 0
|
||||
}
|
||||
const sortedStartup = dedup(startup).sort((a, b) =>
|
||||
scoreFor(b.projectId, b.avgGlobalScore) - scoreFor(a.projectId, a.avgGlobalScore)
|
||||
|| b.compositeScore - a.compositeScore)
|
||||
const sortedConcept = dedup(concept).sort((a, b) =>
|
||||
scoreFor(b.projectId, b.avgGlobalScore) - scoreFor(a.projectId, a.avgGlobalScore)
|
||||
|| b.compositeScore - a.compositeScore)
|
||||
setLocalOrder({
|
||||
STARTUP: sortedStartup.map((r) => r.projectId),
|
||||
BUSINESS_CONCEPT: sortedConcept.map((r) => r.projectId),
|
||||
})
|
||||
// Eslint disable: snapshot/evalScores are read but the resort should only
|
||||
// run on toggle flip, not on every snapshot/scores refetch.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [useBalanced])
|
||||
|
||||
// ─── numericCriteria from eval form ─────────────────────────────────────
|
||||
const numericCriteria = useMemo(() => {
|
||||
if (!evalForm?.criteriaJson) return []
|
||||
@@ -886,13 +932,15 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
: (evalConfig?.conceptAdvanceCount ?? 0))
|
||||
const threshold = evalConfig?.advanceScoreThreshold ?? 0
|
||||
|
||||
// Effective ranking score = balanced (juror-corrected) average,
|
||||
// falling back to raw avgGlobalScore. Both the sort and the
|
||||
// threshold check use this same value so the cutoff lands in
|
||||
// the right spot.
|
||||
// Effective ranking score respects the per-round
|
||||
// useBalancedRanking toggle. Both the sort and the threshold
|
||||
// check read from the same helper so the cutoff lands in the
|
||||
// right spot.
|
||||
const effectiveScore = (id: string) => {
|
||||
const e = rankingMap.get(id)
|
||||
return evalScores?.balanced[id]?.balancedAverage ?? e?.avgGlobalScore ?? 0
|
||||
const balanced = evalScores?.balanced[id]?.balancedAverage
|
||||
if (useBalanced && balanced != null) return balanced
|
||||
return e?.avgGlobalScore ?? 0
|
||||
}
|
||||
|
||||
let cutoffIndex = -1
|
||||
|
||||
Reference in New Issue
Block a user