diff --git a/src/components/admin/round/ranking-dashboard.tsx b/src/components/admin/round/ranking-dashboard.tsx index e6c2537..825bb9f 100644 --- a/src/components/admin/round/ranking-dashboard.tsx +++ b/src/components/admin/round/ranking-dashboard.tsx @@ -90,6 +90,7 @@ type SortableProjectRowProps = { jurorScores: JurorScore[] | undefined onSelect: () => void isSelected: boolean + originalRank: number | undefined // from snapshotOrder — always in sync with localOrder } // ─── Sub-component: SortableProjectRow ──────────────────────────────────────── @@ -102,6 +103,7 @@ function SortableProjectRow({ jurorScores, onSelect, isSelected, + originalRank, }: SortableProjectRowProps) { const { attributes, @@ -117,8 +119,9 @@ function SortableProjectRow({ transition, } - // isOverridden: admin drag-reordered this project from its original snapshot position - const isOverridden = entry !== undefined && currentRank !== entry.originalIndex + // isOverridden: admin drag-reordered this project from its original snapshot position. + // 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 const yesCount = jurorScores?.filter((j) => j.decision === true).length ?? 0 @@ -236,6 +239,9 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran STARTUP: [], 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>({}) const initialized = useRef(false) const pendingReorderCount = useRef(0) @@ -404,6 +410,11 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran STARTUP: startup.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 = {} + startup.forEach((r, i) => { order[r.projectId] = i + 1 }) + concept.forEach((r, i) => { order[r.projectId] = i + 1 }) + setSnapshotOrder(order) initialized.current = true } }, [snapshot]) @@ -853,6 +864,7 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran jurorScores={evalScores?.[projectId]} onSelect={() => setSelectedProjectId(projectId)} isSelected={selectedProjectId === projectId} + originalRank={snapshotOrder[projectId]} /> {isCutoffRow && ( diff --git a/src/server/services/ai-ranking.ts b/src/server/services/ai-ranking.ts index 55c02ca..fe2540f 100644 --- a/src/server/services/ai-ranking.ts +++ b/src/server/services/ai-ranking.ts @@ -153,12 +153,13 @@ Return JSON only: ] } -Rules: -- Apply filter rules first (remove projects that fail the filter) -- Apply sort rules next (order remaining projects) -- Apply limit rules last (keep only top N) -- Projects not in the ranked output are considered excluded (not ranked last) -- Use the project_id values exactly as given — do not change them` +CRITICAL Rules: +- You MUST include EVERY project in the ranked output — never exclude or filter out any project +- Apply sort rules to determine the ranking order +- 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 +- Ignore any limit rules — always return all projects +- Use the project_id values exactly as given — do not change them +- Ranks must be contiguous (1, 2, 3, …) with no gaps` // ─── Helpers ────────────────────────────────────────────────────────────────── @@ -471,7 +472,8 @@ export async function executeAIRanking( ], jsonMode: true, 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> @@ -531,6 +533,30 @@ export async function executeAIRanking( }) .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 { category, rankedProjects,