/** * Typed API client — the single place the frontend talks to the backend. * Components import these functions and never touch fetch() directly, so * error handling and response typing live in exactly one file. * * All paths are relative (/api/...): the Vite dev server proxies them to * FastAPI locally, and nginx does the same inside Docker. The UI never * needs to know where the backend lives. */ /** Strict 3-class scores for default-model analyze/batch endpoints. */ export interface Scores { negative: number; neutral: number; positive: number; } export interface AnalyzeResult { label: string; scores: Scores; } /** Dynamic scores for compare — keys vary by model (binary vs 3-class). */ export type DynamicScores = Record; export interface TokenAttribution { token: string; attribution: number; } export interface ExplainResult extends AnalyzeResult { tokens: TokenAttribution[]; } export interface BatchItem extends AnalyzeResult { text: string; } export interface BatchResult { results: BatchItem[]; aggregates: { counts: Record; mean_scores: Scores }; } export interface ModelInfo { name: string; labels: string[]; max_tokens: number; device: string; description: string; } export interface ModelSummary { id: string; name: string; task: string; labels: string[]; domain: string; note: string; default: boolean; loaded: boolean; } export interface CompareItem { model_id: string; name: string; domain: string; label: string; scores: DynamicScores; confidence: number; latency_ms: number; note: string; } export type AiDetectionScores = Record; export interface AiDetectItem { model_id: string; name: string; domain: string; label: string; scores: AiDetectionScores; confidence: number; latency_ms: number; note: string; } export interface AiDetectCompareResponse { results: AiDetectItem[]; disagreement: boolean; warning: string; } async function request(path: string, init?: RequestInit): Promise { const res = await fetch(path, init); if (!res.ok) { // FastAPI puts human-readable errors in { detail } — surface that // instead of a bare status code whenever it's available. const body = await res.json().catch(() => null); const detail = typeof body?.detail === "string" ? body.detail : `Request failed (${res.status})`; throw new Error(detail); } return res.json() as Promise; } const postJson = (body: unknown): RequestInit => ({ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); export const analyze = (text: string) => request("/api/analyze", postJson({ text })); export const explainText = (text: string) => request("/api/explain", postJson({ text })); export const analyzeCsv = (file: File) => { const form = new FormData(); form.append("file", file); // Note: no Content-Type header — the browser sets multipart boundaries itself. return request("/api/analyze/csv", { method: "POST", body: form }); }; export const getModelInfo = () => request("/api/model"); export const getModels = (task?: "sentiment" | "ai_text_detection") => request<{ models: ModelSummary[] }>( task ? `/api/models?task=${task}` : "/api/models", ); export const compareModels = (text: string, model_ids?: string[]) => request<{ results: CompareItem[] }>("/api/compare", postJson({ text, model_ids })); export const compareAiDetectors = (text: string, model_ids?: string[]) => request("/api/ai-detect/compare", postJson({ text, model_ids }));