/** The AI contract additions (see contract.md): runs, review rows, the approve gate. */ export type Band = 'green' | 'amber' | 'red' | 'blank' | 'unscored' export type RunStatus = 'pending' | 'queued' | 'running' | 'done' | 'failed' export type Run = { job_id: string document: string status: RunStatus model: string pages_done: number pages_total: number summary: Record | null // null until the run is done approved: boolean error: string | null } export type ReviewRow = { row_id: number field: string value: string | null band: Band score: number | null reason: string | null verified: boolean corrected_value: string | null } /** One run on the cost page. total_cost is null when the run recorded no token usage (unpriced). */ export type RoiRun = { job_id: string document: string created_at: string // ISO, UTC model: string input_tokens: number | null output_tokens: number | null thinking_tokens: number | null total_cost: number | null // USD } export type Pricing = { source: string; as_of: string } export const BUSY: RunStatus[] = ['queued', 'running'] // polled every 1.5s while in one of these /** Legend order, label, and the colour token. Band colours are semantic: never on buttons or headings. */ export const BANDS: { band: Band; label: string; colour: string }[] = [ { band: 'green', label: 'Clean ≥0.95', colour: 'var(--clean)' }, { band: 'amber', label: 'Check 0.90–0.95', colour: 'var(--verify)' }, { band: 'red', label: 'Review <0.90', colour: 'var(--review)' }, { band: 'blank', label: 'Missing (required)', colour: 'var(--absent)' }, { band: 'unscored', label: 'Unscored', colour: 'var(--absent)' }, ] export const CAVEAT = 'Colour reflects print clarity, not correctness. A cleanly printed value can still be misread.' async function req(path: string, init?: RequestInit): Promise { const res = await fetch(API_BASE + path, { ...init, headers: { 'Content-Type': 'application/json' } }) if (!res.ok) { // The server says why it refused (the approve gate answers 409 with the count). const body = await res.json().catch(() => null) throw new Error(body?.detail ?? `${res.status} ${res.statusText}`) } return res.json() } export const ai = { runs: () => req<{ runs: Run[] }>('/runs'), run: (id: string) => req(`/runs/${id}`), rows: (id: string) => req<{ rows: ReviewRow[] }>(`/runs/${id}/review`), patchRow: (rowId: number, body: { corrected_value?: string | null; verified: boolean }) => req(`/review/${rowId}`, { method: 'PATCH', body: JSON.stringify(body) }), verifyRemaining: (id: string) => req<{ verified: number }>(`/runs/${id}/verify-remaining`, { method: 'POST' }), approve: (id: string) => req<{ ok: boolean }>(`/runs/${id}/approve`, { method: 'POST' }), roi: () => req<{ runs: RoiRun[]; pricing: Pricing }>('/roi'), documentUrl: (id: string) => `${API_BASE}/runs/${id}/document`, }