MAESTRO: Extract Leaderboard into standalone component with expand, sort, and animation

Extract the inline LeaderboardTable from LivePage into a standalone
Leaderboard component with click-to-expand detail rows, sortable
columns, smooth slide-in animation for new entries, and a subtle
glow effect on the best run. 29 tests added.
This commit is contained in:
John Lightner 2026-04-07 03:10:08 -05:00
parent b16454994e
commit cf49e9c888
4 changed files with 738 additions and 170 deletions

View file

@ -23,9 +23,11 @@ Build the React frontend: setup wizard, experiment builder, real-time observabil
- [x] Build the model selector component (frontend/src/components/ModelSelector.tsx). Dropdown grouped by endpoint. Each option shows model name + endpoint label. Include a "refresh models" button that calls the endpoint test API to refresh available models. Show a connectivity indicator (green dot = reachable, red = error).
<!-- Implemented ModelSelector with optgroup-based endpoint grouping, each option showing model name + endpoint label. Refresh button tests all endpoints in parallel via endpoints.test() API, updates connectivity indicators (green=reachable, red=error, yellow=testing, gray=unknown). Integrated into ExperimentPage PipelineStageCard replacing the inline select. 16 tests added. -->
- [ ] Implement the Live Observability page (frontend/src/pages/Live.tsx). This is the star of the show — the real-time dashboard during active sweeps. Layout: left column (60%) shows the activity timeline and current run details, right column (40%) shows the leaderboard and steering controls. Connect via WebSocket to /ws/experiments/{id}. Everything updates in real-time without page refresh.
- [x] Implement the Live Observability page (frontend/src/pages/Live.tsx). This is the star of the show — the real-time dashboard during active sweeps. Layout: left column (60%) shows the activity timeline and current run details, right column (40%) shows the leaderboard and steering controls. Connect via WebSocket to /ws/experiments/{id}. Everything updates in real-time without page refresh.
<!-- Implemented in LivePage.tsx. 60/40 grid layout with: Activity Timeline (color-coded event cards for run.started/completed/failed, new_best_found, cache_hit; event filter dropdown; auto-scroll toggle), Leaderboard (sortable columns, best-run amber highlight, status badges), Steering Controls (pause/resume/stop with confirmation dialogs, progress bar, token/cost/cache-rate stats), WebSocket connection with exponential backoff reconnect and connection status indicator. 35 tests added. App.test.tsx updated. -->
- [ ] Build the Leaderboard component (frontend/src/components/Leaderboard.tsx). Real-time ranked table of runs. Columns: rank, config summary (model + key params), individual scores, weighted total, status (completed/cached/running). Click a row to expand full details. Sortable by any column. New entries animate in smoothly. Highlight the current best with a subtle glow effect.
- [x] Build the Leaderboard component (frontend/src/components/Leaderboard.tsx). Real-time ranked table of runs. Columns: rank, config summary (model + key params), individual scores, weighted total, status (completed/cached/running). Click a row to expand full details. Sortable by any column. New entries animate in smoothly. Highlight the current best with a subtle glow effect.
<!-- Extracted from LivePage's inline LeaderboardTable into standalone component. Ranked table with sortable columns (individual scores + weighted total). Click-to-expand detail panel shows scores breakdown with visual bars, run ID, status, duration, token counts, and full config JSON. Best run highlighted with amber background + subtle glow shadow. New entries animate in with slide-in keyframe animation (auto-removed after 600ms). StatusBadge handles completed/running/failed/cached/unknown states. LivePage updated to import from new component. 29 tests added. -->
- [ ] Build the Activity Timeline component (frontend/src/components/Timeline.tsx). Chronological feed of events received via WebSocket. Each event is a card: run.started (blue), run.completed (green), new_best_found (gold), cache_hit (gray), run.failed (red). Include timestamps and key metrics. Auto-scroll to latest, with a "pause scroll" button. Filterable by event type.

View file

@ -0,0 +1,374 @@
import { render, screen, within, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest";
import Leaderboard from "./Leaderboard";
import type { LeaderboardRow, LeaderboardProps } from "./Leaderboard";
// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------
function makeRow(overrides: Partial<LeaderboardRow> = {}): LeaderboardRow {
return {
run_id: "r1",
config_summary: "gpt-4 t=0.7",
scores: { length: 0.85, quality: 0.9 },
weighted_total: 0.875,
status: "completed",
cached: false,
...overrides,
};
}
const ROW_A = makeRow({
run_id: "r1",
config_summary: "gpt-4 t=0.7",
scores: { length: 0.85, quality: 0.9 },
weighted_total: 0.875,
});
const ROW_B = makeRow({
run_id: "r2",
config_summary: "gpt-3.5 t=1.0",
scores: { length: 0.6, quality: 0.7 },
weighted_total: 0.65,
});
const ROW_C = makeRow({
run_id: "r3",
config_summary: "llama-3 t=0.5",
scores: { length: 0.95, quality: 0.8 },
weighted_total: 0.88,
cached: true,
});
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function renderLeaderboard(overrides: Partial<LeaderboardProps> = {}) {
const props: LeaderboardProps = {
rows: [ROW_A, ROW_B],
bestRunId: null,
...overrides,
};
return { ...render(<Leaderboard {...props} />), props };
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("Leaderboard", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
// -------------------------------------------------------------------------
// Basic rendering
// -------------------------------------------------------------------------
it("renders the leaderboard table", () => {
renderLeaderboard();
expect(screen.getByTestId("leaderboard-table")).toBeInTheDocument();
});
it("renders rows for each entry", () => {
renderLeaderboard();
const rows = screen.getAllByTestId("leaderboard-row");
expect(rows).toHaveLength(2);
});
it("shows rank numbers", () => {
renderLeaderboard();
const rows = screen.getAllByTestId("leaderboard-row");
// Default sort is by weighted_total descending, so ROW_A (0.875) is #1
expect(rows[0]).toHaveTextContent("1");
expect(rows[1]).toHaveTextContent("2");
});
it("shows config summary", () => {
renderLeaderboard();
expect(screen.getByText("gpt-4 t=0.7")).toBeInTheDocument();
expect(screen.getByText("gpt-3.5 t=1.0")).toBeInTheDocument();
});
it("shows individual score columns", () => {
renderLeaderboard();
const rows = screen.getAllByTestId("leaderboard-row");
// ROW_A scores: length=0.85, quality=0.9
expect(rows[0]).toHaveTextContent("0.850");
expect(rows[0]).toHaveTextContent("0.900");
});
it("shows weighted total", () => {
renderLeaderboard();
const rows = screen.getAllByTestId("leaderboard-row");
expect(rows[0]).toHaveTextContent("0.875");
});
it("shows status badges", () => {
renderLeaderboard({ rows: [ROW_A] });
expect(screen.getByText("completed")).toBeInTheDocument();
});
it("shows cached badge", () => {
renderLeaderboard({ rows: [ROW_C] });
expect(screen.getByText("cached")).toBeInTheDocument();
});
it("shows running status badge", () => {
renderLeaderboard({
rows: [makeRow({ run_id: "r-run", status: "running" })],
});
expect(screen.getByText("running")).toBeInTheDocument();
});
it("shows failed status badge", () => {
renderLeaderboard({
rows: [makeRow({ run_id: "r-fail", status: "failed" })],
});
expect(screen.getByText("failed")).toBeInTheDocument();
});
it("shows fallback status badge for unknown status", () => {
renderLeaderboard({
rows: [makeRow({ run_id: "r-q", status: "queued" })],
});
expect(screen.getByText("queued")).toBeInTheDocument();
});
it("shows 'No runs yet' when empty", () => {
renderLeaderboard({ rows: [] });
expect(screen.getByText("No runs yet")).toBeInTheDocument();
});
// -------------------------------------------------------------------------
// Sorting
// -------------------------------------------------------------------------
it("sorts by weighted total descending by default", () => {
renderLeaderboard();
const rows = screen.getAllByTestId("leaderboard-row");
// ROW_A (0.875) should be first, ROW_B (0.65) second
expect(rows[0]).toHaveTextContent("gpt-4 t=0.7");
expect(rows[1]).toHaveTextContent("gpt-3.5 t=1.0");
});
it("toggles sort direction on column click", async () => {
renderLeaderboard();
const user = userEvent.setup();
// Click "Total" to toggle to ascending
await user.click(screen.getByTestId("sort-total"));
const rows = screen.getAllByTestId("leaderboard-row");
// Now ascending: ROW_B (0.65) first, ROW_A (0.875) second
expect(rows[0]).toHaveTextContent("gpt-3.5 t=1.0");
expect(rows[1]).toHaveTextContent("gpt-4 t=0.7");
});
it("sorts by individual score column", async () => {
renderLeaderboard({ rows: [ROW_A, ROW_B, ROW_C] });
const user = userEvent.setup();
// Click "length" column header
await user.click(screen.getByTestId("sort-length"));
const rows = screen.getAllByTestId("leaderboard-row");
// Descending by length: ROW_C (0.95), ROW_A (0.85), ROW_B (0.6)
expect(rows[0]).toHaveTextContent("llama-3 t=0.5");
expect(rows[1]).toHaveTextContent("gpt-4 t=0.7");
expect(rows[2]).toHaveTextContent("gpt-3.5 t=1.0");
});
it("shows sort indicator arrow", () => {
renderLeaderboard();
// Default sort is weighted_total descending
expect(screen.getByTestId("sort-total")).toHaveTextContent("\u25BC");
});
// -------------------------------------------------------------------------
// Best run highlighting
// -------------------------------------------------------------------------
it("highlights the best run with glow effect", () => {
renderLeaderboard({ bestRunId: "r1" });
const rows = screen.getAllByTestId("leaderboard-row");
expect(rows[0].className).toContain("bg-amber");
expect(rows[0].className).toContain("shadow-");
});
it("does not highlight non-best rows", () => {
renderLeaderboard({ bestRunId: "r1" });
const rows = screen.getAllByTestId("leaderboard-row");
expect(rows[1].className).not.toContain("bg-amber");
});
// -------------------------------------------------------------------------
// Click to expand details
// -------------------------------------------------------------------------
it("expands row details on click", async () => {
renderLeaderboard();
const user = userEvent.setup();
expect(screen.queryByTestId("leaderboard-detail")).not.toBeInTheDocument();
const rows = screen.getAllByTestId("leaderboard-row");
await user.click(rows[0]);
expect(screen.getByTestId("leaderboard-detail")).toBeInTheDocument();
});
it("shows run ID in expanded details", async () => {
renderLeaderboard();
const user = userEvent.setup();
await user.click(screen.getAllByTestId("leaderboard-row")[0]);
const detail = screen.getByTestId("leaderboard-detail");
expect(detail).toHaveTextContent("r1");
});
it("shows scores breakdown in expanded details", async () => {
renderLeaderboard();
const user = userEvent.setup();
await user.click(screen.getAllByTestId("leaderboard-row")[0]);
const detail = screen.getByTestId("leaderboard-detail");
expect(detail).toHaveTextContent("length");
expect(detail).toHaveTextContent("quality");
expect(detail).toHaveTextContent("Weighted Total");
});
it("shows duration when available", async () => {
renderLeaderboard({
rows: [makeRow({ run_id: "r-dur", duration_ms: 2500 })],
});
const user = userEvent.setup();
await user.click(screen.getAllByTestId("leaderboard-row")[0]);
const detail = screen.getByTestId("leaderboard-detail");
expect(detail).toHaveTextContent("2.50s");
});
it("shows token counts when available", async () => {
renderLeaderboard({
rows: [makeRow({ run_id: "r-tok", tokens_in: 100, tokens_out: 200 })],
});
const user = userEvent.setup();
await user.click(screen.getAllByTestId("leaderboard-row")[0]);
const detail = screen.getByTestId("leaderboard-detail");
expect(detail).toHaveTextContent("100 in / 200 out");
});
it("shows config JSON when available", async () => {
renderLeaderboard({
rows: [
makeRow({
run_id: "r-cfg",
config: { model: "gpt-4", temperature: 0.7 },
}),
],
});
const user = userEvent.setup();
await user.click(screen.getAllByTestId("leaderboard-row")[0]);
const detail = screen.getByTestId("leaderboard-detail");
expect(detail).toHaveTextContent('"model": "gpt-4"');
});
it("collapses expanded row on second click", async () => {
renderLeaderboard();
const user = userEvent.setup();
const rows = screen.getAllByTestId("leaderboard-row");
await user.click(rows[0]);
expect(screen.getByTestId("leaderboard-detail")).toBeInTheDocument();
await user.click(rows[0]);
expect(screen.queryByTestId("leaderboard-detail")).not.toBeInTheDocument();
});
it("only expands one row at a time", async () => {
renderLeaderboard();
const user = userEvent.setup();
const rows = screen.getAllByTestId("leaderboard-row");
await user.click(rows[0]);
expect(screen.getAllByTestId("leaderboard-detail")).toHaveLength(1);
// Click a different row
await user.click(rows[1]);
expect(screen.getAllByTestId("leaderboard-detail")).toHaveLength(1);
// Detail should now show the second row's data
expect(screen.getByTestId("leaderboard-detail")).toHaveTextContent("r2");
});
// -------------------------------------------------------------------------
// onRowClick callback
// -------------------------------------------------------------------------
it("calls onRowClick with run_id when row is clicked", async () => {
const onRowClick = vi.fn();
renderLeaderboard({ onRowClick });
const user = userEvent.setup();
await user.click(screen.getAllByTestId("leaderboard-row")[0]);
expect(onRowClick).toHaveBeenCalledWith("r1");
});
// -------------------------------------------------------------------------
// New entry animation
// -------------------------------------------------------------------------
it("applies animation class to new entries", () => {
const { rerender } = render(
<Leaderboard rows={[ROW_A]} bestRunId={null} />,
);
// Add a new row
rerender(
<Leaderboard rows={[ROW_A, ROW_B]} bestRunId={null} />,
);
const rows = screen.getAllByTestId("leaderboard-row");
// The new row (ROW_B) should have the animation class
const hasAnimation = rows.some((r) =>
r.className.includes("animate-slide-in"),
);
expect(hasAnimation).toBe(true);
});
it("removes animation class after timeout", async () => {
vi.useFakeTimers();
const { rerender } = render(
<Leaderboard rows={[ROW_A]} bestRunId={null} />,
);
rerender(
<Leaderboard rows={[ROW_A, ROW_B]} bestRunId={null} />,
);
// Advance past animation cleanup timeout
act(() => {
vi.advanceTimersByTime(700);
});
const rows = screen.getAllByTestId("leaderboard-row");
const hasAnimation = rows.some((r) =>
r.className.includes("animate-slide-in"),
);
expect(hasAnimation).toBe(false);
vi.useRealTimers();
});
});

View file

@ -0,0 +1,357 @@
import { Fragment, useState, useRef, useEffect } from "react";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface LeaderboardRow {
run_id: string;
config_summary: string;
scores: Record<string, number>;
weighted_total: number;
status: string;
cached: boolean;
/** Optional full config for expanded detail view */
config?: Record<string, unknown>;
/** Optional timing information */
duration_ms?: number | null;
/** Optional token counts */
tokens_in?: number | null;
tokens_out?: number | null;
}
export interface LeaderboardProps {
rows: LeaderboardRow[];
bestRunId: string | null;
onRowClick?: (runId: string) => void;
}
// ---------------------------------------------------------------------------
// Status Badge
// ---------------------------------------------------------------------------
function StatusBadge({ status, cached }: { status: string; cached: boolean }) {
if (cached)
return (
<span className="inline-flex items-center rounded-full bg-slate-100 dark:bg-slate-700 px-2 py-0.5 text-xs text-slate-500 dark:text-slate-400">
cached
</span>
);
if (status === "completed")
return (
<span className="inline-flex items-center rounded-full bg-green-100 dark:bg-green-900/30 px-2 py-0.5 text-xs text-green-700 dark:text-green-400">
completed
</span>
);
if (status === "running")
return (
<span className="inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/30 px-2 py-0.5 text-xs text-blue-700 dark:text-blue-400">
running
</span>
);
if (status === "failed")
return (
<span className="inline-flex items-center rounded-full bg-red-100 dark:bg-red-900/30 px-2 py-0.5 text-xs text-red-700 dark:text-red-400">
failed
</span>
);
return (
<span className="inline-flex items-center rounded-full bg-slate-100 dark:bg-slate-700 px-2 py-0.5 text-xs text-slate-500 dark:text-slate-400">
{status}
</span>
);
}
// ---------------------------------------------------------------------------
// Score Bar (visual indicator for individual scores)
// ---------------------------------------------------------------------------
function ScoreBar({ value }: { value: number }) {
const pct = Math.max(0, Math.min(100, value * 100));
return (
<div className="flex items-center gap-1.5">
<span className="tabular-nums text-slate-700 dark:text-slate-300 w-12 text-right">
{value.toFixed(3)}
</span>
<div className="flex-1 h-1.5 rounded-full bg-slate-200 dark:bg-slate-700 min-w-[40px] max-w-[60px]">
<div
className="h-full rounded-full bg-indigo-500 transition-all duration-300"
style={{ width: `${pct}%` }}
/>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Expanded Row Detail
// ---------------------------------------------------------------------------
function RowDetail({ row }: { row: LeaderboardRow }) {
return (
<tr data-testid="leaderboard-detail">
<td
colSpan={100}
className="px-4 py-3 bg-slate-50 dark:bg-slate-800/80 border-t border-slate-100 dark:border-slate-700"
>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
{/* Scores breakdown */}
<div>
<h4 className="text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500 mb-2">
Scores
</h4>
<div className="space-y-1.5">
{Object.entries(row.scores).map(([name, value]) => (
<div key={name} className="flex items-center justify-between gap-2">
<span className="text-slate-600 dark:text-slate-400 capitalize">
{name}
</span>
<ScoreBar value={value} />
</div>
))}
<div className="flex items-center justify-between gap-2 pt-1 border-t border-slate-200 dark:border-slate-700">
<span className="font-medium text-slate-700 dark:text-slate-300">
Weighted Total
</span>
<span className="tabular-nums font-semibold text-slate-900 dark:text-white">
{row.weighted_total.toFixed(3)}
</span>
</div>
</div>
</div>
{/* Config & metadata */}
<div>
<h4 className="text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500 mb-2">
Details
</h4>
<dl className="space-y-1">
<div className="flex justify-between">
<dt className="text-slate-500 dark:text-slate-400">Run ID</dt>
<dd className="text-slate-700 dark:text-slate-300 font-mono text-xs">
{row.run_id}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-slate-500 dark:text-slate-400">Status</dt>
<dd>
<StatusBadge status={row.status} cached={row.cached} />
</dd>
</div>
{row.duration_ms != null && (
<div className="flex justify-between">
<dt className="text-slate-500 dark:text-slate-400">Duration</dt>
<dd className="text-slate-700 dark:text-slate-300 tabular-nums">
{(row.duration_ms / 1000).toFixed(2)}s
</dd>
</div>
)}
{(row.tokens_in != null || row.tokens_out != null) && (
<div className="flex justify-between">
<dt className="text-slate-500 dark:text-slate-400">Tokens</dt>
<dd className="text-slate-700 dark:text-slate-300 tabular-nums">
{row.tokens_in ?? 0} in / {row.tokens_out ?? 0} out
</dd>
</div>
)}
{row.config && (
<div className="mt-2">
<dt className="text-slate-500 dark:text-slate-400 mb-1">Config</dt>
<dd>
<pre className="text-xs bg-slate-100 dark:bg-slate-900 rounded p-2 overflow-x-auto text-slate-700 dark:text-slate-300">
{JSON.stringify(row.config, null, 2)}
</pre>
</dd>
</div>
)}
</dl>
</div>
</div>
</td>
</tr>
);
}
// ---------------------------------------------------------------------------
// Leaderboard Component
// ---------------------------------------------------------------------------
export default function Leaderboard({ rows, bestRunId, onRowClick }: LeaderboardProps) {
const [sortKey, setSortKey] = useState<string>("weighted_total");
const [sortAsc, setSortAsc] = useState(false);
const [expandedRunId, setExpandedRunId] = useState<string | null>(null);
const [newRunIds, setNewRunIds] = useState<Set<string>>(new Set());
const prevRowCountRef = useRef(rows.length);
// Track newly added rows for entry animation
useEffect(() => {
if (rows.length > prevRowCountRef.current) {
const existingIds = new Set(
rows.slice(0, prevRowCountRef.current).map((r) => r.run_id),
);
const added = rows
.filter((r) => !existingIds.has(r.run_id))
.map((r) => r.run_id);
if (added.length > 0) {
setNewRunIds((prev) => {
const next = new Set(prev);
added.forEach((id) => next.add(id));
return next;
});
// Remove animation class after transition completes
const timer = setTimeout(() => {
setNewRunIds((prev) => {
const next = new Set(prev);
added.forEach((id) => next.delete(id));
return next;
});
}, 600);
return () => clearTimeout(timer);
}
}
prevRowCountRef.current = rows.length;
}, [rows]);
function handleSort(key: string) {
if (key === sortKey) {
setSortAsc(!sortAsc);
} else {
setSortKey(key);
setSortAsc(false);
}
}
function handleRowClick(runId: string) {
setExpandedRunId((prev) => (prev === runId ? null : runId));
onRowClick?.(runId);
}
const sortedRows = [...rows].sort((a, b) => {
let aVal: number;
let bVal: number;
if (sortKey === "weighted_total") {
aVal = a.weighted_total;
bVal = b.weighted_total;
} else {
aVal = a.scores[sortKey] ?? 0;
bVal = b.scores[sortKey] ?? 0;
}
return sortAsc ? aVal - bVal : bVal - aVal;
});
// Collect all score keys across all rows
const scoreKeys = Array.from(
new Set(rows.flatMap((r) => Object.keys(r.scores))),
);
return (
<div className="overflow-x-auto rounded-lg border border-slate-200 dark:border-slate-700">
<table className="w-full text-sm" data-testid="leaderboard-table">
<thead>
<tr className="bg-slate-50 dark:bg-slate-800 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
<th className="px-3 py-2">#</th>
<th className="px-3 py-2">Config</th>
{scoreKeys.map((k) => (
<th
key={k}
className="px-3 py-2 cursor-pointer hover:text-indigo-600 dark:hover:text-indigo-400 select-none"
onClick={() => handleSort(k)}
data-testid={`sort-${k}`}
>
{k}
{sortKey === k ? (sortAsc ? " \u25B2" : " \u25BC") : ""}
</th>
))}
<th
className="px-3 py-2 cursor-pointer hover:text-indigo-600 dark:hover:text-indigo-400 select-none"
onClick={() => handleSort("weighted_total")}
data-testid="sort-total"
>
Total
{sortKey === "weighted_total"
? sortAsc
? " \u25B2"
: " \u25BC"
: ""}
</th>
<th className="px-3 py-2">Status</th>
</tr>
</thead>
<tbody>
{sortedRows.map((row, idx) => {
const isExpanded = expandedRunId === row.run_id;
const isBest = row.run_id === bestRunId;
const isNew = newRunIds.has(row.run_id);
return (
<Fragment key={row.run_id}>
<tr
data-testid="leaderboard-row"
onClick={() => handleRowClick(row.run_id)}
className={[
"border-t border-slate-100 dark:border-slate-700 transition-all duration-300 cursor-pointer",
isBest
? "bg-amber-50 dark:bg-amber-900/20 ring-1 ring-amber-300 dark:ring-amber-700 shadow-[0_0_8px_rgba(251,191,36,0.3)] dark:shadow-[0_0_8px_rgba(251,191,36,0.15)]"
: "hover:bg-slate-50 dark:hover:bg-slate-800/50",
isNew ? "animate-slide-in" : "",
].join(" ")}
>
<td className="px-3 py-2 font-medium text-slate-700 dark:text-slate-300">
{idx + 1}
</td>
<td className="px-3 py-2 text-slate-600 dark:text-slate-400 max-w-[180px] truncate">
{row.config_summary}
</td>
{scoreKeys.map((k) => (
<td
key={k}
className="px-3 py-2 tabular-nums text-slate-700 dark:text-slate-300"
>
{row.scores[k] != null ? row.scores[k].toFixed(3) : "\u2014"}
</td>
))}
<td className="px-3 py-2 font-semibold tabular-nums text-slate-900 dark:text-white">
{row.weighted_total.toFixed(3)}
</td>
<td className="px-3 py-2">
<StatusBadge status={row.status} cached={row.cached} />
</td>
</tr>
{isExpanded && <RowDetail row={row} />}
</Fragment>
);
})}
{sortedRows.length === 0 && (
<tr>
<td
colSpan={3 + scoreKeys.length}
className="px-3 py-6 text-center text-slate-400 dark:text-slate-500"
>
No runs yet
</td>
</tr>
)}
</tbody>
</table>
{/* Animation keyframes injected via style tag */}
<style>{`
@keyframes leaderboard-slide-in {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-slide-in {
animation: leaderboard-slide-in 0.4s ease-out;
}
`}</style>
</div>
);
}

View file

@ -11,6 +11,8 @@ import type {
RunResponse,
WsConnection,
} from "../api/client";
import Leaderboard from "../components/Leaderboard";
import type { LeaderboardRow } from "../components/Leaderboard";
// ---------------------------------------------------------------------------
// Types
@ -54,15 +56,6 @@ export interface TimelineEntry {
timestamp: Date;
}
export interface LeaderboardRow {
run_id: string;
config_summary: string;
scores: Record<string, number>;
weighted_total: number;
status: string;
cached: boolean;
}
type ConnectionStatus = "connecting" | "connected" | "disconnected";
// ---------------------------------------------------------------------------
@ -137,164 +130,6 @@ function TimelineCard({ entry }: { entry: TimelineEntry }) {
);
}
// ---------------------------------------------------------------------------
// Leaderboard Table
// ---------------------------------------------------------------------------
function LeaderboardTable({
rows,
bestRunId,
}: {
rows: LeaderboardRow[];
bestRunId: string | null;
}) {
const [sortKey, setSortKey] = useState<string>("weighted_total");
const [sortAsc, setSortAsc] = useState(false);
function handleSort(key: string) {
if (key === sortKey) {
setSortAsc(!sortAsc);
} else {
setSortKey(key);
setSortAsc(false);
}
}
const sortedRows = [...rows].sort((a, b) => {
let aVal: number;
let bVal: number;
if (sortKey === "weighted_total") {
aVal = a.weighted_total;
bVal = b.weighted_total;
} else {
aVal = a.scores[sortKey] ?? 0;
bVal = b.scores[sortKey] ?? 0;
}
return sortAsc ? aVal - bVal : bVal - aVal;
});
// Collect all score keys
const scoreKeys = Array.from(
new Set(rows.flatMap((r) => Object.keys(r.scores))),
);
return (
<div className="overflow-x-auto rounded-lg border border-slate-200 dark:border-slate-700">
<table className="w-full text-sm" data-testid="leaderboard-table">
<thead>
<tr className="bg-slate-50 dark:bg-slate-800 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">
<th className="px-3 py-2">#</th>
<th className="px-3 py-2">Config</th>
{scoreKeys.map((k) => (
<th
key={k}
className="px-3 py-2 cursor-pointer hover:text-indigo-600 dark:hover:text-indigo-400 select-none"
onClick={() => handleSort(k)}
>
{k}
{sortKey === k ? (sortAsc ? " \u25B2" : " \u25BC") : ""}
</th>
))}
<th
className="px-3 py-2 cursor-pointer hover:text-indigo-600 dark:hover:text-indigo-400 select-none"
onClick={() => handleSort("weighted_total")}
>
Total
{sortKey === "weighted_total"
? sortAsc
? " \u25B2"
: " \u25BC"
: ""}
</th>
<th className="px-3 py-2">Status</th>
</tr>
</thead>
<tbody>
{sortedRows.map((row, idx) => (
<tr
key={row.run_id}
data-testid="leaderboard-row"
className={`border-t border-slate-100 dark:border-slate-700 transition-colors ${
row.run_id === bestRunId
? "bg-amber-50 dark:bg-amber-900/20 ring-1 ring-amber-300 dark:ring-amber-700"
: "hover:bg-slate-50 dark:hover:bg-slate-800/50"
}`}
>
<td className="px-3 py-2 font-medium text-slate-700 dark:text-slate-300">
{idx + 1}
</td>
<td className="px-3 py-2 text-slate-600 dark:text-slate-400 max-w-[180px] truncate">
{row.config_summary}
</td>
{scoreKeys.map((k) => (
<td
key={k}
className="px-3 py-2 tabular-nums text-slate-700 dark:text-slate-300"
>
{row.scores[k] != null ? row.scores[k].toFixed(3) : "—"}
</td>
))}
<td className="px-3 py-2 font-semibold tabular-nums text-slate-900 dark:text-white">
{row.weighted_total.toFixed(3)}
</td>
<td className="px-3 py-2">
<StatusBadge status={row.status} cached={row.cached} />
</td>
</tr>
))}
{sortedRows.length === 0 && (
<tr>
<td
colSpan={3 + scoreKeys.length}
className="px-3 py-6 text-center text-slate-400 dark:text-slate-500"
>
No runs yet
</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
function StatusBadge({
status,
cached,
}: {
status: string;
cached: boolean;
}) {
if (cached)
return (
<span className="inline-flex items-center rounded-full bg-slate-100 dark:bg-slate-700 px-2 py-0.5 text-xs text-slate-500 dark:text-slate-400">
cached
</span>
);
if (status === "completed")
return (
<span className="inline-flex items-center rounded-full bg-green-100 dark:bg-green-900/30 px-2 py-0.5 text-xs text-green-700 dark:text-green-400">
completed
</span>
);
if (status === "running")
return (
<span className="inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/30 px-2 py-0.5 text-xs text-blue-700 dark:text-blue-400">
running
</span>
);
if (status === "failed")
return (
<span className="inline-flex items-center rounded-full bg-red-100 dark:bg-red-900/30 px-2 py-0.5 text-xs text-red-700 dark:text-red-400">
failed
</span>
);
return (
<span className="inline-flex items-center rounded-full bg-slate-100 dark:bg-slate-700 px-2 py-0.5 text-xs text-slate-500 dark:text-slate-400">
{status}
</span>
);
}
// ---------------------------------------------------------------------------
// Steering Controls
@ -935,7 +770,7 @@ export default function LivePage() {
</h2>
</div>
<div className="p-4">
<LeaderboardTable rows={leaderboard} bestRunId={bestRunId} />
<Leaderboard rows={leaderboard} bestRunId={bestRunId} />
</div>
</div>
</div>