/**
* EvalRunCompare (#121) — side-by-side regression comparison of two dataset runs.
* Pick a baseline + current run; shows pass-rate / avg-score deltas, the set of
* cases that flipped (pass↔fail), the prompt/model variant per run, and a
* dataset-version-mismatch warning.
*/
import { useState } from 'react';
import { useApi } from '../../hooks/useApi';
import { getEvalRuns, getRunComparison } from '../../api/eval';
import type { EvalRun, RegressionReport } from '../../api/eval';
const pct = (n: number): string => `${n >= 0 ? '+' : ''}${(n * 100).toFixed(1)}%`;
const passRate = (r: EvalRun): number => (r.totalCases > 0 ? r.passedCases / r.totalCases : 0);
const errMsg = (e: unknown): string => (e instanceof Error ? e.message : String(e));
function runLabel(r: EvalRun): string {
const variant = [r.promptVersionId ? `prompt ${r.promptVersionId.slice(0, 8)}` : '', r.modelId ?? '']
.filter(Boolean)
.join(' · ');
return `${r.id.slice(0, 8)} · ${(passRate(r) * 100).toFixed(0)}% (${r.passedCases}/${r.totalCases})${variant ? ` · ${variant}` : ''}`;
}
function Stat({ label, value, good }: { label: string; value: string; good: boolean }) {
return (
);
}
function ReportView({ report }: { report: RegressionReport }) {
const tone = report.overallRegression
? 'bg-red-50 border-red-200 text-red-800'
: 'bg-green-50 border-green-200 text-green-800';
return (
{report.overallRegression ? '✗ Regression detected' : '✓ No regression'}
{report.datasetVersionMismatch && (
⚠ These runs are over different dataset versions — deltas may be misleading.
)}
= 0} />
= 0} />
Flipped cases ({report.flippedCases.length})
{report.flippedCases.length === 0 ? (
No cases changed pass/fail status.
) : (
{report.flippedCases.map((f) => (
-
{f.direction === 'pass_to_fail' ? '↓ pass→fail' : '↑ fail→pass'}
{f.testCaseId.slice(0, 8)}
{f.baselineScore.toFixed(2)} → {f.currentScore.toFixed(2)}
))}
)}
);
}
export function EvalRunCompare({ datasetId }: { datasetId: string }) {
const { data, loading } = useApi(() => getEvalRuns({ datasetId, limit: 50 }), [datasetId]);
const runs = (data?.runs ?? []).filter((r) => r.status === 'completed');
const [baselineId, setBaselineId] = useState('');
const [currentId, setCurrentId] = useState('');
const [report, setReport] = useState(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState(null);
async function compare() {
if (!currentId || !baselineId) return;
setBusy(true);
setError(null);
setReport(null);
try {
setReport(await getRunComparison(currentId, baselineId));
} catch (e) {
setError(errMsg(e));
} finally {
setBusy(false);
}
}
return (
Compare runs
{loading ? (
Loading runs…
) : runs.length < 2 ? (
Run this dataset at least twice to compare runs.
) : (
<>
{error && {error}
}
{report && }
>
)}
);
}