fix: ranking shows all reviewed projects, fix override badge sync issue
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m17s
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m17s
- AI ranking now includes ALL projects (never filters/excludes any) - Updated system prompt: filter criteria inform priority, not exclusion - Dynamic maxTokens scaling for large project pools (80 tokens/project) - Fallback: projects AI omits are appended sorted by composite score - Override badge uses snapshotOrder state (synced with localOrder in same useEffect) instead of rankingMap.originalIndex to prevent stale-render mismatch where all items incorrectly showed as overridden Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,7 @@ type SortableProjectRowProps = {
|
|||||||
jurorScores: JurorScore[] | undefined
|
jurorScores: JurorScore[] | undefined
|
||||||
onSelect: () => void
|
onSelect: () => void
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
|
originalRank: number | undefined // from snapshotOrder — always in sync with localOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Sub-component: SortableProjectRow ────────────────────────────────────────
|
// ─── Sub-component: SortableProjectRow ────────────────────────────────────────
|
||||||
@@ -102,6 +103,7 @@ function SortableProjectRow({
|
|||||||
jurorScores,
|
jurorScores,
|
||||||
onSelect,
|
onSelect,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
originalRank,
|
||||||
}: SortableProjectRowProps) {
|
}: SortableProjectRowProps) {
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
@@ -117,8 +119,9 @@ function SortableProjectRow({
|
|||||||
transition,
|
transition,
|
||||||
}
|
}
|
||||||
|
|
||||||
// isOverridden: admin drag-reordered this project from its original snapshot position
|
// isOverridden: admin drag-reordered this project from its original snapshot position.
|
||||||
const isOverridden = entry !== undefined && currentRank !== entry.originalIndex
|
// Uses snapshotOrder (set in same effect as localOrder) so they are always in sync.
|
||||||
|
const isOverridden = originalRank !== undefined && currentRank !== originalRank
|
||||||
|
|
||||||
// Compute yes count from juror scores
|
// Compute yes count from juror scores
|
||||||
const yesCount = jurorScores?.filter((j) => j.decision === true).length ?? 0
|
const yesCount = jurorScores?.filter((j) => j.decision === true).length ?? 0
|
||||||
@@ -236,6 +239,9 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
|||||||
STARTUP: [],
|
STARTUP: [],
|
||||||
BUSINESS_CONCEPT: [],
|
BUSINESS_CONCEPT: [],
|
||||||
})
|
})
|
||||||
|
// Track the original snapshot order (projectId → 1-based rank) for override detection.
|
||||||
|
// Updated in the same effect as localOrder so they are always in sync.
|
||||||
|
const [snapshotOrder, setSnapshotOrder] = useState<Record<string, number>>({})
|
||||||
const initialized = useRef(false)
|
const initialized = useRef(false)
|
||||||
const pendingReorderCount = useRef(0)
|
const pendingReorderCount = useRef(0)
|
||||||
|
|
||||||
@@ -404,6 +410,11 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
|||||||
STARTUP: startup.map((r) => r.projectId),
|
STARTUP: startup.map((r) => r.projectId),
|
||||||
BUSINESS_CONCEPT: concept.map((r) => r.projectId),
|
BUSINESS_CONCEPT: concept.map((r) => r.projectId),
|
||||||
})
|
})
|
||||||
|
// Track original order for override detection (same effect = always in sync)
|
||||||
|
const order: Record<string, number> = {}
|
||||||
|
startup.forEach((r, i) => { order[r.projectId] = i + 1 })
|
||||||
|
concept.forEach((r, i) => { order[r.projectId] = i + 1 })
|
||||||
|
setSnapshotOrder(order)
|
||||||
initialized.current = true
|
initialized.current = true
|
||||||
}
|
}
|
||||||
}, [snapshot])
|
}, [snapshot])
|
||||||
@@ -853,6 +864,7 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
|||||||
jurorScores={evalScores?.[projectId]}
|
jurorScores={evalScores?.[projectId]}
|
||||||
onSelect={() => setSelectedProjectId(projectId)}
|
onSelect={() => setSelectedProjectId(projectId)}
|
||||||
isSelected={selectedProjectId === projectId}
|
isSelected={selectedProjectId === projectId}
|
||||||
|
originalRank={snapshotOrder[projectId]}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
{isCutoffRow && (
|
{isCutoffRow && (
|
||||||
|
|||||||
@@ -153,12 +153,13 @@ Return JSON only:
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
Rules:
|
CRITICAL Rules:
|
||||||
- Apply filter rules first (remove projects that fail the filter)
|
- You MUST include EVERY project in the ranked output — never exclude or filter out any project
|
||||||
- Apply sort rules next (order remaining projects)
|
- Apply sort rules to determine the ranking order
|
||||||
- Apply limit rules last (keep only top N)
|
- If filter criteria exist, use them to inform ranking priority (projects meeting all criteria rank higher, those failing criteria rank lower) but still include ALL projects
|
||||||
- Projects not in the ranked output are considered excluded (not ranked last)
|
- Ignore any limit rules — always return all projects
|
||||||
- Use the project_id values exactly as given — do not change them`
|
- Use the project_id values exactly as given — do not change them
|
||||||
|
- Ranks must be contiguous (1, 2, 3, …) with no gaps`
|
||||||
|
|
||||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -471,7 +472,8 @@ export async function executeAIRanking(
|
|||||||
],
|
],
|
||||||
jsonMode: true,
|
jsonMode: true,
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
maxTokens: 2000,
|
// ~50 tokens per project entry; scale for large pools with generous buffer
|
||||||
|
maxTokens: Math.max(2000, projects.length * 80),
|
||||||
})
|
})
|
||||||
|
|
||||||
let response: Awaited<ReturnType<typeof openai.chat.completions.create>>
|
let response: Awaited<ReturnType<typeof openai.chat.completions.create>>
|
||||||
@@ -531,6 +533,30 @@ export async function executeAIRanking(
|
|||||||
})
|
})
|
||||||
.sort((a, b) => a.rank - b.rank)
|
.sort((a, b) => a.rank - b.rank)
|
||||||
|
|
||||||
|
// ─── Ensure ALL projects are included (AI may omit some due to token limits) ──
|
||||||
|
const rankedIds = new Set(rankedProjects.map((r) => r.projectId))
|
||||||
|
const unrankedProjects = projects
|
||||||
|
.filter((p) => !rankedIds.has(p.id))
|
||||||
|
.map((p) => ({
|
||||||
|
projectId: p.id,
|
||||||
|
rank: 0,
|
||||||
|
compositeScore: computeCompositeScore(p, maxEvaluatorCount, criteriaWeights, criterionDefs),
|
||||||
|
avgGlobalScore: p.avgGlobalScore,
|
||||||
|
normalizedAvgScore: p.normalizedAvgScore,
|
||||||
|
passRate: p.passRate,
|
||||||
|
evaluatorCount: p.evaluatorCount,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.compositeScore - a.compositeScore)
|
||||||
|
|
||||||
|
let nextRank = rankedProjects.length + 1
|
||||||
|
for (const proj of unrankedProjects) {
|
||||||
|
proj.rank = nextRank++
|
||||||
|
rankedProjects.push(proj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-normalize ranks to be contiguous (1, 2, 3, …)
|
||||||
|
rankedProjects.forEach((p, i) => { p.rank = i + 1 })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
category,
|
category,
|
||||||
rankedProjects,
|
rankedProjects,
|
||||||
|
|||||||
Reference in New Issue
Block a user