import type { Classification, DocumentDetail, DocumentMeta, ExtractionResult, HealthInfo, } from "./types"; // All requests go through Next's rewrite proxy to the FastAPI backend. const BASE = ""; async function json(res: Response): Promise { if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`${res.status} ${res.statusText}: ${text}`); } return res.json() as Promise; } export const api = { health: () => fetch(`${BASE}/api/health`).then((r) => json(r)), listDocuments: () => fetch(`${BASE}/api/documents`).then((r) => json(r)), getDocument: (id: string) => fetch(`${BASE}/api/documents/${id}`).then((r) => json(r)), upload: (file: File) => { const fd = new FormData(); fd.append("file", file); return fetch(`${BASE}/api/documents`, { method: "POST", body: fd }).then( (r) => json(r), ); }, deleteDocument: (id: string) => fetch(`${BASE}/api/documents/${id}`, { method: "DELETE" }).then((r) => json<{ ok: boolean }>(r), ), reExtract: (id: string, schema?: string, provider?: string) => { const q = new URLSearchParams(); if (schema) q.set("schema", schema); if (provider) q.set("provider", provider); return fetch(`${BASE}/api/documents/${id}/extract?${q}`, { method: "POST", }).then((r) => json(r)); }, reClassify: (id: string, provider?: string) => { const q = new URLSearchParams(); if (provider) q.set("provider", provider); return fetch(`${BASE}/api/documents/${id}/classify?${q}`, { method: "POST", }).then((r) => json(r)); }, reSummarize: (id: string, provider?: string) => { const q = new URLSearchParams(); if (provider) q.set("provider", provider); return fetch(`${BASE}/api/documents/${id}/summarize?${q}`, { method: "POST", }).then((r) => json<{ summary: string }>(r)); }, exportUrl: (id: string, format: string) => `${BASE}/api/documents/${id}/export?format=${format}`, schemas: () => fetch(`${BASE}/api/documents/-/schemas`).then((r) => json<{ schemas: string[] }>(r), ), }; // --------------------------- streaming chat --------------------------- export interface ChatStreamHandlers { onToken: (text: string) => void; onCitations: (citations: import("./types").Citation[]) => void; onDone: () => void; onError: (msg: string) => void; } /** * POST to /api/chat and parse the SSE stream manually (EventSource only * supports GET, so we read the response body as a stream ourselves). */ export async function streamChat( body: { document_id?: string | null; messages: { role: string; content: string }[]; provider?: string | null; }, handlers: ChatStreamHandlers, signal?: AbortSignal, ) { try { const res = await fetch(`${BASE}/api/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), signal, }); if (!res.ok || !res.body) { throw new Error(`Chat request failed: ${res.status}`); } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // SSE frames are separated by a blank line const frames = buffer.split("\n\n"); buffer = frames.pop() ?? ""; for (const frame of frames) { let event = "message"; let data = ""; for (const line of frame.split("\n")) { if (line.startsWith("event:")) event = line.slice(6).trim(); else if (line.startsWith("data:")) data += line.slice(5).trim(); } if (!data) continue; let parsed: any; try { parsed = JSON.parse(data); } catch { continue; } if (event === "token") handlers.onToken(parsed.text ?? ""); else if (event === "citations") handlers.onCitations(parsed.citations ?? []); else if (event === "done") handlers.onDone(); else if (event === "error") handlers.onError(parsed.message ?? "error"); } } handlers.onDone(); } catch (e: any) { if (e?.name !== "AbortError") handlers.onError(e?.message ?? String(e)); } }