Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ import React, { useEffect, useMemo, useState } from "react";
import { createRoot } from "react-dom/client";
import "./admin.css";

type BenchSummary = {
id: string;
status: string;
totalRounds: number;
completedRounds: number;
} | null;

type AdminSnapshot = {
isPaused: boolean;
isRunningRound: boolean;
done: boolean;
completedInMemory: number;
persistedRounds: number;
viewerCount: number;
bench?: BenchSummary;
};

type AdminResponse = { ok: true } & AdminSnapshot;
Expand Down Expand Up @@ -61,6 +69,7 @@ function App() {
const [pending, setPending] = useState<string | null>(null);
const [isResetOpen, setIsResetOpen] = useState(false);
const [resetText, setResetText] = useState("");
const [benchRoundsPerPairing, setBenchRoundsPerPairing] = useState(1);

useEffect(() => {
let mounted = true;
Expand Down Expand Up @@ -173,6 +182,53 @@ function App() {
}
}

async function onBenchStart() {
setError(null);
setPending("bench-start");
try {
const response = await fetch("/api/bench/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ roundsPerPairing: benchRoundsPerPairing }),
cache: "no-store",
});
if (!response.ok) {
const text = await response.text();
throw new Error(text || `Request failed (${response.status})`);
}
try {
const statusData = await requestAdminJson("/api/admin/status");
setSnapshot(statusData);
} catch {}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to start benchmark");
} finally {
setPending(null);
}
}

async function onBenchCancel() {
setError(null);
setPending("bench-cancel");
try {
const response = await fetch("/api/bench/cancel", {
method: "POST",
cache: "no-store",
});
if (!response.ok) {
throw new Error(await readErrorMessage(response));
}
try {
const statusData = await requestAdminJson("/api/admin/status");
setSnapshot(statusData);
} catch {}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to cancel benchmark");
} finally {
setPending(null);
}
}

async function onLogout() {
setError(null);
setPending("logout");
Expand Down Expand Up @@ -263,6 +319,7 @@ function App() {
<nav className="quick-links">
<a href="/">Live Game</a>
<a href="/history">History</a>
<a href="/bench">Bench</a>
<button className="link-button" onClick={onLogout} disabled={busy}>
Logout
</button>
Expand Down Expand Up @@ -327,6 +384,86 @@ function App() {
</section>
</main>

<section className="panel panel--main" style={{ marginTop: 24 }}>
<div className="panel-head">
<h2 style={{ fontFamily: "var(--serif)", fontSize: "clamp(24px, 4vw, 36px)", lineHeight: 1 }}>
Benchmark
</h2>
<p>
Run a round-robin benchmark where every model plays against every
other model.{" "}
<a href="/bench" style={{ color: "var(--accent)", textDecoration: "none" }}>
View results
</a>
</p>
</div>

{snapshot?.bench ? (
<div>
<div className="status-grid" style={{ marginBottom: 16 }}>
<StatusCard label="Status" value={snapshot.bench.status} />
<StatusCard
label="Progress"
value={`${snapshot.bench.completedRounds}/${snapshot.bench.totalRounds}`}
/>
</div>
<div className="actions" style={{ gridTemplateColumns: "repeat(2, minmax(0, 1fr))" }}>
<button
type="button"
className="btn btn--danger"
disabled={busy}
onClick={onBenchCancel}
>
{pending === "bench-cancel" ? "Cancelling..." : "Cancel Benchmark"}
</button>
<a
href="/bench"
className="btn"
style={{ textAlign: "center", textDecoration: "none", display: "inline-flex", alignItems: "center", justifyContent: "center" }}
>
View Live Results
</a>
</div>
</div>
) : (
<div className="actions" style={{ gridTemplateColumns: "auto 1fr auto" }}>
<select
value={benchRoundsPerPairing}
onChange={(e) => setBenchRoundsPerPairing(Number(e.target.value))}
style={{
background: "var(--bg)",
border: "1px solid var(--border)",
color: "var(--text)",
fontFamily: "var(--mono)",
fontSize: 12,
padding: "10px 12px",
outline: "none",
}}
disabled={busy}
>
<option value={1}>1 round/pairing</option>
<option value={3}>3 rounds/pairing</option>
<option value={5}>5 rounds/pairing</option>
</select>
<button
type="button"
className="btn btn--primary"
disabled={busy}
onClick={onBenchStart}
>
{pending === "bench-start" ? "Starting..." : "Start Benchmark"}
</button>
<a
href="/bench"
className="btn"
style={{ textAlign: "center", textDecoration: "none", display: "inline-flex", alignItems: "center", justifyContent: "center" }}
>
Past Results
</a>
</div>
)}
</section>

{isResetOpen && (
<div className="modal-backdrop" role="dialog" aria-modal="true">
<div className="modal">
Expand Down
144 changes: 144 additions & 0 deletions bench-db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { db } from "./db.ts";

db.exec(`
CREATE TABLE IF NOT EXISTS bench_runs (
id TEXT PRIMARY KEY,
status TEXT NOT NULL DEFAULT 'running',
config TEXT NOT NULL,
started_at INTEGER NOT NULL,
finished_at INTEGER,
total_rounds INTEGER NOT NULL,
completed_rounds INTEGER NOT NULL DEFAULT 0,
error TEXT
);
`);

db.exec(`
CREATE TABLE IF NOT EXISTS bench_rounds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
pairing_index INTEGER NOT NULL,
round_num INTEGER NOT NULL,
data TEXT NOT NULL,
FOREIGN KEY (run_id) REFERENCES bench_runs(id)
);
`);

export type BenchRunRow = {
id: string;
status: string;
config: string;
started_at: number;
finished_at: number | null;
total_rounds: number;
completed_rounds: number;
error: string | null;
};

export type BenchRoundRow = {
id: number;
run_id: string;
pairing_index: number;
round_num: number;
data: string;
};

export function saveBenchRun(run: {
id: string;
status: string;
config: object;
startedAt: number;
totalRounds: number;
}) {
db.prepare(
`INSERT INTO bench_runs (id, status, config, started_at, total_rounds, completed_rounds)
VALUES ($id, $status, $config, $started_at, $total_rounds, 0)`,
).run({
$id: run.id,
$status: run.status,
$config: JSON.stringify(run.config),
$started_at: run.startedAt,
$total_rounds: run.totalRounds,
});
}

export function updateBenchRunStatus(
id: string,
update: {
status?: string;
completedRounds?: number;
finishedAt?: number;
error?: string;
},
) {
const sets: string[] = [];
const params: Record<string, unknown> = { $id: id };

if (update.status !== undefined) {
sets.push("status = $status");
params.$status = update.status;
}
if (update.completedRounds !== undefined) {
sets.push("completed_rounds = $completed_rounds");
params.$completed_rounds = update.completedRounds;
}
if (update.finishedAt !== undefined) {
sets.push("finished_at = $finished_at");
params.$finished_at = update.finishedAt;
}
if (update.error !== undefined) {
sets.push("error = $error");
params.$error = update.error;
}

if (sets.length === 0) return;
db.prepare(`UPDATE bench_runs SET ${sets.join(", ")} WHERE id = $id`).run(
params,
);
}

export function saveBenchRound(round: {
runId: string;
pairingIndex: number;
roundNum: number;
data: object;
}) {
db.prepare(
`INSERT INTO bench_rounds (run_id, pairing_index, round_num, data)
VALUES ($run_id, $pairing_index, $round_num, $data)`,
).run({
$run_id: round.runId,
$pairing_index: round.pairingIndex,
$round_num: round.roundNum,
$data: JSON.stringify(round.data),
});
}

export function getBenchRuns(): BenchRunRow[] {
return db
.query("SELECT * FROM bench_runs ORDER BY started_at DESC")
.all() as BenchRunRow[];
}

export function getBenchRun(id: string): BenchRunRow | null {
return (
(db
.query("SELECT * FROM bench_runs WHERE id = $id")
.get({ $id: id }) as BenchRunRow | null) ?? null
);
}

export function getBenchRounds(runId: string): BenchRoundRow[] {
return db
.query(
"SELECT * FROM bench_rounds WHERE run_id = $run_id ORDER BY pairing_index ASC, round_num ASC",
)
.all({ $run_id: runId }) as BenchRoundRow[];
}

export function markStaleBenchRunsAsError() {
db.prepare(
`UPDATE bench_runs SET status = 'error', error = 'Server restarted', finished_at = $now
WHERE status = 'running'`,
).run({ $now: Date.now() });
}
Loading