feat: formula-based ranking with optional AI, configurable score/pass-rate weights
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m56s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m56s
Add scoreWeight and passRateWeight (0-10) to evaluation config for configurable composite score formula. When ranking criteria text is empty, triggerAutoRank uses pure formula ranking (no LLM calls). When criteria text is present, AI-assisted ranking runs as before. - Add FORMULA to RankingMode enum with migration - Extract fetchCategoryProjects helper, add formulaRank service - Update computeCompositeScore to accept configurable weights - Add score/pass-rate weight sliders to ranking dashboard UI - Mode-aware button labels (Calculator/formula vs Sparkles/AI) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Record<string, number>>({})
|
||||
const [localCriteriaText, setLocalCriteriaText] = useState<string>('')
|
||||
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<string, number>
|
||||
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<string, unknown>
|
||||
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
|
||||
<>
|
||||
<Loader2 className="h-10 w-10 text-blue-500 animate-spin" />
|
||||
<div>
|
||||
<p className="font-medium">AI ranking in progress…</p>
|
||||
<p className="font-medium">Ranking in progress…</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
This may take a minute. You can continue working — results will appear automatically.
|
||||
</p>
|
||||
@@ -643,8 +658,12 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
onClick={() => triggerRankMutation.mutate({ roundId })}
|
||||
disabled={triggerRankMutation.isPending}
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Run Ranking Now
|
||||
{isFormulaMode ? (
|
||||
<Calculator className="mr-2 h-4 w-4" />
|
||||
) : (
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{isFormulaMode ? 'Run Ranking Now' : 'Run AI Ranking Now'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -714,10 +733,15 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Ranking…
|
||||
</>
|
||||
) : isFormulaMode ? (
|
||||
<>
|
||||
<Calculator className="mr-2 h-4 w-4" />
|
||||
Run Ranking
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Run Ranking
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
Run AI Ranking
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -754,11 +778,48 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<CardContent className="space-y-5 pt-0">
|
||||
{/* Ranking criteria text */}
|
||||
{/* Score vs Pass Rate weights */}
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label>Formula Weights</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Control the balance between evaluation scores and yes/no pass rate in the composite ranking
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm w-40 flex-shrink-0">Score Weight</span>
|
||||
<Slider
|
||||
min={0}
|
||||
max={10}
|
||||
step={1}
|
||||
value={[localScoreWeight]}
|
||||
onValueChange={([v]) => setLocalScoreWeight(v)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-sm font-mono w-6 text-right">{localScoreWeight}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm w-40 flex-shrink-0">Pass Rate Weight</span>
|
||||
<Slider
|
||||
min={0}
|
||||
max={10}
|
||||
step={1}
|
||||
value={[localPassRateWeight]}
|
||||
onValueChange={([v]) => setLocalPassRateWeight(v)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-sm font-mono w-6 text-right">{localPassRateWeight}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ranking criteria text (optional — triggers AI mode) */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="rankingCriteria">Ranking Criteria (natural language)</Label>
|
||||
<Label htmlFor="rankingCriteria">AI Ranking Criteria (optional)</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
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).
|
||||
</p>
|
||||
<Textarea
|
||||
id="rankingCriteria"
|
||||
@@ -768,6 +829,15 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
onChange={(e) => setLocalCriteriaText(e.target.value)}
|
||||
className="resize-y"
|
||||
/>
|
||||
{isFormulaMode ? (
|
||||
<p className="text-xs text-emerald-600 flex items-center gap-1">
|
||||
<Calculator className="h-3 w-3" /> Formula mode — ranking uses weights only, no AI calls
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-amber-600 flex items-center gap-1">
|
||||
<Sparkles className="h-3 w-3" /> AI mode — criteria will be parsed and used for ranking (uses API credits)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Per-criterion weights */}
|
||||
@@ -823,7 +893,7 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
<Loader2 className="h-5 w-5 animate-spin text-blue-600 dark:text-blue-400 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-blue-900 dark:text-blue-200">
|
||||
AI ranking in progress…
|
||||
Ranking in progress…
|
||||
</p>
|
||||
<p className="text-xs text-blue-700 dark:text-blue-400">
|
||||
This may take a minute. You can continue working — results will appear automatically.
|
||||
|
||||
Reference in New Issue
Block a user