// api.js // ---------------------------------------------------------------------------- // Thin browser client for the FastAPI server in `server/visualizer_server.py`. // // - `getModes()` — list available inference modes (LLM + baselines). // - `getConfig()` — sectors/baselines/etc. for the empty-state shell. // - `startRun(opts)` — POST /api/runs and return the new {run_id, ...}. // - `streamRun(runId, on)` — open an EventSource and dispatch typed events. // - `getSnapshot(runId)` — fetch the full episode-so-far (used on reload). // // The visualizer assumes Vite's dev proxy maps /api/* to the backend (see // `vite.config.js`). In production builds the same path works as long as the // FastAPI server is reverse-proxied under the same origin as the static UI. // ---------------------------------------------------------------------------- const BASE = '/api'; async function jsonOrThrow(res) { if (!res.ok) { let message = `${res.status} ${res.statusText}`; try { const body = await res.json(); if (body?.detail) message = typeof body.detail === 'string' ? body.detail : JSON.stringify(body.detail); } catch { /* empty body — fall back to status */ } throw new Error(message); } return res.json(); } export async function getHealth() { return jsonOrThrow(await fetch(`${BASE}/health`)); } export async function getConfig() { return jsonOrThrow(await fetch(`${BASE}/config`)); } export async function getModes() { return jsonOrThrow(await fetch(`${BASE}/modes`)); } export async function listRuns() { return jsonOrThrow(await fetch(`${BASE}/runs`)); } export async function startRun({ mode, modelId = null, seed = 42, maxRounds = 20, temperature = 0.2, }) { const res = await fetch(`${BASE}/runs`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode, model_id: modelId, seed, max_rounds: maxRounds, temperature, }), }); return jsonOrThrow(res); } export async function getSnapshot(runId) { return jsonOrThrow(await fetch(`${BASE}/runs/${runId}/snapshot`)); } /** * Open an SSE connection to a live run. * * `handlers` may contain any subset of: * { onStart, onRound, onActivity, onSummary, onError, onDone, onConnectionError } * * `onActivity(data)` fires for each within-round action: * data.kind = 'round_start' | 'debate' | 'proposal' | 'vote' * * Returns an object with a `close()` method to tear the connection down. */ export function streamRun(runId, handlers = {}) { const url = `${BASE}/runs/${runId}/stream`; const source = new EventSource(url); const route = (type, ev) => { let data = {}; try { data = ev.data ? JSON.parse(ev.data) : {}; } catch (err) { handlers.onConnectionError?.(err); return; } switch (type) { case 'start': handlers.onStart?.(data); break; case 'activity': handlers.onActivity?.(data); break; case 'round': handlers.onRound?.(data); break; case 'summary': handlers.onSummary?.(data); break; case 'error': handlers.onError?.(data); break; case 'done': handlers.onDone?.(data); source.close(); break; default: break; } }; ['start', 'activity', 'round', 'summary', 'error', 'done'].forEach(type => { source.addEventListener(type, e => route(type, e)); }); source.onerror = err => { handlers.onConnectionError?.(err); }; return { close: () => source.close(), raw: source, }; }