Spaces:
Sleeping
Sleeping
| 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<T>(res: Response): Promise<T> { | |
| if (!res.ok) { | |
| const text = await res.text().catch(() => ""); | |
| throw new Error(`${res.status} ${res.statusText}: ${text}`); | |
| } | |
| return res.json() as Promise<T>; | |
| } | |
| export const api = { | |
| health: () => fetch(`${BASE}/api/health`).then((r) => json<HealthInfo>(r)), | |
| listDocuments: () => | |
| fetch(`${BASE}/api/documents`).then((r) => json<DocumentMeta[]>(r)), | |
| getDocument: (id: string) => | |
| fetch(`${BASE}/api/documents/${id}`).then((r) => json<DocumentDetail>(r)), | |
| upload: (file: File) => { | |
| const fd = new FormData(); | |
| fd.append("file", file); | |
| return fetch(`${BASE}/api/documents`, { method: "POST", body: fd }).then( | |
| (r) => json<DocumentMeta>(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<ExtractionResult>(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<Classification>(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)); | |
| } | |
| } | |