/** * Tiny typed fetch client for the FastAPI backend. * * Each function maps 1:1 to a backend route in * `webapp/backend/routes/`. The base URL is empty by default so the * calls are relative — Vite's dev proxy forwards them to * http://localhost:8000 (see `vite.config.ts`), and a built bundle * served from the same FastAPI server gets them for free. Override * via `VITE_API_BASE` for split-host deployments. */ import type { EvaluateRequest, EvaluateResponse, HealthResponse, OptimizeCancelResponse, OptimizeJobResponse, OptimizeRequest, OptimizeResultResponse, PredictRequest, PredictResponse, RegistryListResponse, ScenarioListResponse, ShapExplainRequest, ShapLocalResponse, SweepRequest, SweepResponse, VersionResponse, } from "@/types/api"; const API_BASE = (import.meta.env.VITE_API_BASE ?? "") as string; export function apiUrl(path: string): string { return `${API_BASE}${path}`; } class ApiError extends Error { status: number; body: unknown; constructor(status: number, message: string, body: unknown) { super(message); this.name = "ApiError"; this.status = status; this.body = body; } } async function request(path: string, init: RequestInit = {}): Promise { const url = apiUrl(path); const headers = new Headers(init.headers); if (init.body && !headers.has("content-type")) { headers.set("content-type", "application/json"); } const response = await fetch(url, { ...init, headers }); const text = await response.text(); const body: unknown = text ? safeJson(text) : null; if (!response.ok) { const detail = (body as { detail?: string } | null)?.detail; throw new ApiError( response.status, detail ?? `HTTP ${response.status} on ${path}`, body, ); } return body as T; } function safeJson(text: string): unknown { try { return JSON.parse(text); } catch { return text; } } export const api = { healthz: () => request("/healthz"), version: () => request("/version"), listScenarios: () => request("/scenarios"), listRegistry: () => request("/registry"), predict: (req: PredictRequest) => request("/predict", { method: "POST", body: JSON.stringify(req), }), evaluate: (req: EvaluateRequest) => request("/evaluate", { method: "POST", body: JSON.stringify(req), }), sweep: (req: SweepRequest) => request("/sweep", { method: "POST", body: JSON.stringify(req), }), optimize: (req: OptimizeRequest) => request("/optimize", { method: "POST", body: JSON.stringify(req), }), optimizeResult: (pathOrJobId: string) => request( pathOrJobId.startsWith("/") ? pathOrJobId : `/optimize/${pathOrJobId}/result`, ), cancelOptimize: (pathOrJobId: string) => request( pathOrJobId.startsWith("/") ? pathOrJobId : `/optimize/${pathOrJobId}/cancel`, { method: "POST" }, ), shapExplain: (req: ShapExplainRequest) => request("/shap/explain", { method: "POST", body: JSON.stringify(req), }), }; export { ApiError };