diff --git a/prisma/migrations/20260302100000_add_formula_ranking_mode/migration.sql b/prisma/migrations/20260302100000_add_formula_ranking_mode/migration.sql new file mode 100644 index 0000000..fd75fb4 --- /dev/null +++ b/prisma/migrations/20260302100000_add_formula_ranking_mode/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "RankingMode" ADD VALUE 'FORMULA'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 72ceb68..c4f4791 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1425,6 +1425,7 @@ enum RankingMode { PREVIEW // Parsed rules shown to admin (not yet applied) CONFIRMED // Admin confirmed rules, ranking applied QUICK // Quick-rank: parse + apply without preview + FORMULA // Formula-only: no LLM, pure math ranking } enum RankingSnapshotStatus { diff --git a/src/components/admin/round/ranking-dashboard.tsx b/src/components/admin/round/ranking-dashboard.tsx index 34ee06d..c835f0c 100644 --- a/src/components/admin/round/ranking-dashboard.tsx +++ b/src/components/admin/round/ranking-dashboard.tsx @@ -53,8 +53,10 @@ import { import { GripVertical, BarChart3, + Calculator, Loader2, RefreshCw, + Sparkles, Trophy, ExternalLink, ChevronDown, @@ -263,6 +265,8 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran const [weightsOpen, setWeightsOpen] = useState(false) const [localWeights, setLocalWeights] = useState>({}) const [localCriteriaText, setLocalCriteriaText] = useState('') + const [localScoreWeight, setLocalScoreWeight] = useState(5) + const [localPassRateWeight, setLocalPassRateWeight] = useState(5) const weightsInitialized = useRef(false) // ─── Sensors ────────────────────────────────────────────────────────────── @@ -476,6 +480,8 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran 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]) @@ -486,10 +492,19 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran const cfg = roundData.configJson as Record updateRoundMutation.mutate({ id: roundId, - configJson: { ...cfg, criteriaWeights: localWeights, rankingCriteria: localCriteriaText }, + configJson: { + ...cfg, + criteriaWeights: localWeights, + rankingCriteria: localCriteriaText, + scoreWeight: localScoreWeight, + passRateWeight: localPassRateWeight, + }, }) } + // Derive ranking mode from criteria text + const isFormulaMode = !localCriteriaText.trim() + // ─── sync advance dialog defaults from config ──────────────────────────── useEffect(() => { if (evalConfig) { @@ -621,7 +636,7 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran <>
-

AI ranking in progress…

+

Ranking in progress…

This may take a minute. You can continue working — results will appear automatically.

@@ -643,8 +658,12 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran onClick={() => triggerRankMutation.mutate({ roundId })} disabled={triggerRankMutation.isPending} > - - Run Ranking Now + {isFormulaMode ? ( + + ) : ( + + )} + {isFormulaMode ? 'Run Ranking Now' : 'Run AI Ranking Now'} )} @@ -714,10 +733,15 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran Ranking… + ) : isFormulaMode ? ( + <> + + Run Ranking + ) : ( <> - - Run Ranking + + Run AI Ranking )} @@ -754,11 +778,48 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran - {/* Ranking criteria text */} + {/* Score vs Pass Rate weights */} +
+
+ +

+ Control the balance between evaluation scores and yes/no pass rate in the composite ranking +

+
+
+
+ Score Weight + setLocalScoreWeight(v)} + className="flex-1" + /> + {localScoreWeight} +
+
+ Pass Rate Weight + setLocalPassRateWeight(v)} + className="flex-1" + /> + {localPassRateWeight} +
+
+
+ + {/* Ranking criteria text (optional — triggers AI mode) */}
- +

- Describe how projects should be ranked. The AI will parse this into rules. + Optional: describe special ranking criteria for AI-assisted ranking. + Leave empty for formula-based ranking (faster, no AI cost).