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); }); }