/** * PromptLooper typed API client. * * - JWT token stored in memory (never localStorage) for security. * - Automatic Authorization header injection. * - Typed wrapper functions for every API endpoint group. * - WebSocket connection helper for real-time updates. */ // --------------------------------------------------------------------------- // Types — mirrors backend Pydantic schemas // --------------------------------------------------------------------------- export interface ProjectCreate { name: string; description?: string | null; } export interface ProjectUpdate { name?: string | null; description?: string | null; } export interface ProjectResponse { id: string; name: string; description: string | null; owner_id: string; created_at: string; updated_at: string; } export interface ProjectListResponse { items: ProjectResponse[]; total: number; } export interface ExperimentCreate { name: string; description?: string | null; sample_data?: Record | null; pipeline_stages?: Record | null; scoring_config?: Record | null; parameter_space?: Record | null; } export interface ExperimentUpdate { name?: string | null; description?: string | null; sample_data?: Record | null; pipeline_stages?: Record | null; scoring_config?: Record | null; parameter_space?: Record | null; status?: string | null; } export interface ExperimentResponse { id: string; project_id: string; name: string; description: string | null; sample_data: Record | null; pipeline_stages: Record | null; scoring_config: Record | null; parameter_space: Record | null; status: string; created_at: string; updated_at: string; } export interface ExperimentListResponse { items: ExperimentResponse[]; total: number; } export interface RunResponse { id: string; experiment_id: string; config_hash: string; config: Record; status: string; started_at: string | null; completed_at: string | null; duration_ms: number | null; tokens_in: number | null; tokens_out: number | null; cost_estimate: number | null; } export interface RunListResponse { items: RunResponse[]; total: number; } export interface StageResultResponse { id: string; run_id: string; stage_index: number; prompt_sent: string; response_raw: string; model_used: string; parameters: Record | null; tokens_in: number | null; tokens_out: number | null; latency_ms: number | null; } export interface ScoreResponse { id: string; run_id: string; scorer_name: string; value: number; scorer_metadata: Record | null; created_at: string; } export interface RunDetailResponse extends RunResponse { stage_results: StageResultResponse[]; scores: ScoreResponse[]; } export interface ScoreInput { scorer_name: string; value: number; metadata?: Record | null; } export interface EndpointCreate { name: string; url: string; api_key?: string | null; default_model?: string | null; } export interface EndpointUpdate { name?: string | null; url?: string | null; api_key?: string | null; default_model?: string | null; } export interface EndpointResponse { id: string; name: string; url: string; default_model: string | null; } export interface EndpointListResponse { items: EndpointResponse[]; total: number; } export interface WebhookCreate { event_type: string; url: string; headers?: Record | null; is_active?: boolean; } export interface WebhookUpdate { event_type?: string | null; url?: string | null; headers?: Record | null; is_active?: boolean | null; } export interface WebhookResponse { id: string; event_type: string; url: string; headers: Record | null; is_active: boolean; } export interface WebhookListResponse { items: WebhookResponse[]; total: number; } export interface SetupRequest { username: string; password: string; } export interface LoginRequest { username: string; password: string; } export interface TokenResponse { access_token: string; token_type: string; } export interface UserResponse { id: string; username: string; is_admin: boolean; created_at: string; } export interface HealthResponse { status: string; database: boolean; redis: boolean; } export interface ExportRunRow { run_id: string; experiment_id: string; config_hash: string; config: Record; status: string; duration_ms: number | null; tokens_in: number | null; tokens_out: number | null; cost_estimate: number | null; scores: Record; } export interface ExportResponse { experiment_id: string; experiment_name: string; rows: ExportRunRow[]; } // --------------------------------------------------------------------------- // API Error // --------------------------------------------------------------------------- export class ApiError extends Error { constructor( public status: number, public statusText: string, public body: unknown, ) { super(`API ${status}: ${statusText}`); this.name = "ApiError"; } } // --------------------------------------------------------------------------- // Token management (in-memory only) // --------------------------------------------------------------------------- let _accessToken: string | null = null; export function setToken(token: string | null): void { _accessToken = token; } export function getToken(): string | null { return _accessToken; } export function clearToken(): void { _accessToken = null; } // --------------------------------------------------------------------------- // Base fetch wrapper // --------------------------------------------------------------------------- const BASE_URL = ""; // Uses Vite proxy in dev; same origin in prod async function request( path: string, options: RequestInit = {}, ): Promise { const headers: Record = { ...(options.headers as Record | undefined), }; // Inject auth header if (_accessToken) { headers["Authorization"] = `Bearer ${_accessToken}`; } // Default content-type for requests with bodies if (options.body && !headers["Content-Type"]) { headers["Content-Type"] = "application/json"; } const response = await fetch(`${BASE_URL}${path}`, { ...options, headers, }); if (!response.ok) { let body: unknown; try { body = await response.json(); } catch { body = await response.text(); } throw new ApiError(response.status, response.statusText, body); } // 204 No Content if (response.status === 204) { return undefined as T; } return response.json() as Promise; } function get(path: string): Promise { return request(path, { method: "GET" }); } function post(path: string, body?: unknown): Promise { return request(path, { method: "POST", body: body != null ? JSON.stringify(body) : undefined, }); } function put(path: string, body?: unknown): Promise { return request(path, { method: "PUT", body: body != null ? JSON.stringify(body) : undefined, }); } function del(path: string): Promise { return request(path, { method: "DELETE" }); } // --------------------------------------------------------------------------- // Health // --------------------------------------------------------------------------- export const health = { check: () => get("/health"), }; // --------------------------------------------------------------------------- // Auth // --------------------------------------------------------------------------- export const auth = { setup: (data: SetupRequest) => post("/api/auth/setup", data), login: async (data: LoginRequest): Promise => { const resp = await post("/api/auth/login", data); setToken(resp.access_token); return resp; }, me: () => get("/api/auth/me"), logout: () => { clearToken(); }, }; // --------------------------------------------------------------------------- // Projects // --------------------------------------------------------------------------- export const projects = { list: () => get("/api/projects/"), create: (data: ProjectCreate) => post("/api/projects/", data), get: (id: string) => get(`/api/projects/${id}`), update: (id: string, data: ProjectUpdate) => put(`/api/projects/${id}`, data), delete: (id: string) => del(`/api/projects/${id}`), }; // --------------------------------------------------------------------------- // Experiments // --------------------------------------------------------------------------- export const experiments = { list: () => get("/api/experiments/"), create: (data: ExperimentCreate) => post("/api/experiments/", data), get: (id: string) => get(`/api/experiments/${id}`), update: (id: string, data: ExperimentUpdate) => put(`/api/experiments/${id}`, data), delete: (id: string) => del(`/api/experiments/${id}`), startSweep: (id: string) => post(`/api/experiments/${id}/sweep`), pause: (id: string) => post(`/api/experiments/${id}/pause`), resume: (id: string) => post(`/api/experiments/${id}/resume`), stop: (id: string) => post(`/api/experiments/${id}/stop`), }; // --------------------------------------------------------------------------- // Runs // --------------------------------------------------------------------------- export const runs = { list: (experimentId: string) => get(`/api/runs/experiments/${experimentId}/runs`), get: (runId: string) => get(`/api/runs/${runId}`), create: (data: Record) => post("/api/runs/", data), score: (runId: string, data: ScoreInput) => post(`/api/runs/${runId}/score`, data), leaderboard: (experimentId: string) => get( `/api/runs/experiments/${experimentId}/leaderboard`, ), }; // --------------------------------------------------------------------------- // Endpoints (LLM targets) // --------------------------------------------------------------------------- export const endpoints = { list: () => get("/api/endpoints/"), create: (data: EndpointCreate) => post("/api/endpoints/", data), update: (id: string, data: EndpointUpdate) => put(`/api/endpoints/${id}`, data), delete: (id: string) => del(`/api/endpoints/${id}`), test: (id: string) => post>(`/api/endpoints/${id}/test`), }; // --------------------------------------------------------------------------- // Export // --------------------------------------------------------------------------- export const exportApi = { best: (experimentId: string) => get>( `/api/export/experiments/${experimentId}/best`, ), env: (experimentId: string) => get(`/api/export/experiments/${experimentId}/env`), yaml: (experimentId: string) => get(`/api/export/experiments/${experimentId}/yaml`), report: (experimentId: string) => get(`/api/export/experiments/${experimentId}/report`), }; // --------------------------------------------------------------------------- // Webhooks // --------------------------------------------------------------------------- export const webhooks = { list: () => get("/api/webhooks/"), create: (data: WebhookCreate) => post("/api/webhooks/", data), delete: (id: string) => del(`/api/webhooks/${id}`), }; // --------------------------------------------------------------------------- // Admin // --------------------------------------------------------------------------- export const admin = { getSettings: () => get>("/api/admin/settings"), updateSettings: (data: Record) => put>("/api/admin/settings", data), getStats: () => get>("/api/admin/stats"), }; // --------------------------------------------------------------------------- // WebSocket helper // --------------------------------------------------------------------------- export type WsMessageHandler = (data: unknown) => void; export interface WsConnection { send: (data: unknown) => void; close: () => void; } /** * Connect to the real-time WebSocket endpoint. * * @param onMessage Called for each incoming message. * @param onClose Optional callback when connection closes. * @returns Object with `send()` and `close()` methods. */ export function connectWebSocket( onMessage: WsMessageHandler, onClose?: () => void, ): WsConnection { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = `${protocol}//${window.location.host}/ws`; const ws = new WebSocket(wsUrl); ws.onmessage = (event) => { try { const data: unknown = JSON.parse(event.data as string); onMessage(data); } catch { onMessage(event.data); } }; ws.onclose = () => { onClose?.(); }; return { send: (data: unknown) => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(data)); } }, close: () => { ws.close(); }, }; }