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
|
||||
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<Record<string, number>>({})
|
||||
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<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
|
||||
}
|
||||
}, [snapshot])
|
||||
@@ -853,6 +864,7 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
jurorScores={evalScores?.[projectId]}
|
||||
onSelect={() => setSelectedProjectId(projectId)}
|
||||
isSelected={selectedProjectId === projectId}
|
||||
originalRank={snapshotOrder[projectId]}
|
||||
/>
|
||||
</motion.div>
|
||||
{isCutoffRow && (
|
||||
|
||||
@@ -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<ReturnType<typeof openai.chat.completions.create>>
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user