import React, { useState, useEffect, useCallback } from "react";
import { render, Box, Text, useInput } from "ink";
import Spinner from "ink-spinner";
import { ProgressBar } from "../tui-components/ProgressBar.jsx";
import { StatusBadge } from "../tui-components/StatusBadge.jsx";
import { DataTable } from "../tui-components/DataTable.jsx";
import { HeaderSwr } from "../tui-components/HeaderSwr.jsx";
const TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled"]);
const RESULT_SCHEMA = [
{ key: "idx", header: "#", width: 5 },
{ key: "status", header: "Status", width: 10, formatter: (v) => (v === "pass" ? "✔" : "✖") },
{ key: "model", header: "Model", width: 22 },
{ key: "score", header: "Score", width: 8, formatter: (v) => (v != null ? String(v) : "-") },
{ key: "latencyMs", header: "ms", width: 8, formatter: (v) => (v != null ? String(v) : "-") },
];
function EvalWatchApp({ runId, suiteId, baseUrl, apiKey, onExit }) {
const [run, setRun] = useState(null);
const [results, setResults] = useState([]);
const [paused, setPaused] = useState(false);
const [done, setDone] = useState(false);
const [elapsed, setElapsed] = useState(0);
const fetchUrl = `${baseUrl ?? "http://localhost:20128"}/api/evals/${runId}`;
const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
useEffect(() => {
const id = setInterval(() => setElapsed((e) => e + 1), 1000);
return () => clearInterval(id);
}, []);
const fetcher = useCallback(async () => {
if (paused) return null;
const res = await fetch(fetchUrl, { headers });
if (!res.ok) return null;
return res.json();
}, [fetchUrl, paused]);
useEffect(() => {
if (!run) return;
if (TERMINAL_STATUSES.has(run.status)) {
setDone(true);
}
const samples = run.samples ?? run.results ?? [];
setResults(
samples.map((s, i) => ({
idx: i + 1,
status: s.pass ? "pass" : "fail",
model: s.model ?? "-",
score: s.score,
latencyMs: s.latencyMs,
}))
);
}, [run]);
useInput((input, key) => {
if (input === "q" || (key.ctrl && input === "c")) onExit?.();
if (input === "p") setPaused((p) => !p);
});
const total = run?.progress?.total ?? 0;
const completed = run?.progress?.completed ?? 0;
const passed = run?.progress?.passed ?? results.filter((r) => r.status === "pass").length;
const failed = run?.progress?.failed ?? results.filter((r) => r.status === "fail").length;
const pct = total > 0 ? Math.round((completed / total) * 100) : 0;
return (
Eval Watch — Run {runId}
{suiteId ? ` (suite: ${suiteId})` : ""}
{Math.floor(elapsed / 60)}:{String(elapsed % 60).padStart(2, "0")}
{paused ? " [PAUSED]" : ""}
{
if (data && data !== run) setRun(data);
return null;
}}
initial={null}
/>
{run ? (
<>
{run.status}
{completed}/{total || "?"} samples
{total > 0 && (
✔ {passed} passed
✖ {failed} failed
)}
{results.length > 0 && (
Recent results
)}
{done && (
{run.status === "completed"
? `✔ Eval completed — ${passed}/${total} passed`
: `✖ Eval ${run.status}`}
)}
>
) : (
Loading...
)}
[q] quit [p] {paused ? "resume" : "pause"}
);
}
export async function startEvalWatchTui({ runId, suiteId, baseUrl, apiKey }) {
return new Promise((resolve, reject) => {
function onExit() {
unmount();
resolve();
}
const { unmount, waitUntilExit } = render(
);
waitUntilExit().then(resolve).catch(reject);
});
}