'use client'; /** Browser-side fetches: same origin `/api/v1/*` (Next rewrite → FastAPI). Never import server `api.ts` in client components. */ import type { ChangeEvent, Claim, Page, ProvenanceDetail, SearchPayload, Suggestion } from './types'; export class ClientApiError extends Error { readonly status: number; constructor(status: number, path: string) { super(`API ${status} on ${path}`); this.name = 'ClientApiError'; this.status = status; } } async function get(path: string, signal?: AbortSignal): Promise { const res = await fetch(`/api/v1${path}`, { headers: { accept: 'application/json' }, signal }); if (!res.ok) throw new ClientApiError(res.status, path); return (await res.json()) as T; } const enc = encodeURIComponent; export const clientApi = { suggest: (q: string, signal?: AbortSignal) => get<{ items: Suggestion[] }>(`/search/suggest?q=${enc(q)}`, signal), search: (q: string, limit = 10, signal?: AbortSignal) => get(`/search?q=${enc(q)}&limit=${limit}`, signal), changes: (qs: string, signal?: AbortSignal) => get>(`/changes?${qs}`, signal), /** Events of one entity (watchlist): `/changes?entity=&limit=`. */ entityChanges: (slug: string, limit = 20, signal?: AbortSignal) => get>(`/changes?entity=${enc(slug)}&limit=${limit}`, signal), /** 1.1 — full evidence behind one displayed value. 404 until the API stream lands → callers fall back to the inline provenance. */ provenance: (slug: string, property: string, signal?: AbortSignal) => get(`/entities/${enc(slug)}/provenance/${enc(property)}`, signal), /** Claim history of one property (exists today): used by the evidence drawer for the claim id, validity and conflicts. */ history: (slug: string, property: string, signal?: AbortSignal) => get<{ items: Claim[] }>(`/entities/${enc(slug)}/history?property=${enc(property)}`, signal), /** Page-view beacon (1 req/s/IP server-side). Fire-and-forget. */ view: (path: string) => fetch('/api/v1/views', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path }), keepalive: true }).catch(() => undefined), }; // ---- D2 (intelligence) ---- import type { CostContextPayload, CostPayload, Price } from './types'; /** Browser-side calls used by the price terminal (lazy row sparklines), the calculator and provider pages. */ export const clientIntel = { /** Full price history of one model (optionally one provider). */ priceHistory: (model: string, provider?: string, signal?: AbortSignal) => get<{ items: Price[] }>(`/prices/history?model=${enc(model)}${provider ? `&provider=${enc(provider)}` : ''}`, signal), /** `/cost` — query string already built by the caller (`model=&input_tokens=…`). */ cost: (qs: string, signal?: AbortSignal) => get(`/cost?${qs}`, signal), /** `/cost/context?tokens=`. */ costContext: (qs: string, signal?: AbortSignal) => get(`/cost/context?${qs}`, signal), }; // ---- /D2 ---- // ---- D3 (temporal/graph/admin) ---- import type { GraphExploreMode, GraphExplorePayload } from './types'; /** Graph explorer: root change and progressive neighbourhood expansion (merged client-side). */ export function clientGraphExplore(node: string, mode: GraphExploreMode, depth: 1 | 2 = 1, limit = 150, signal?: AbortSignal): Promise { return get(`/graph/explore?node=${enc(node)}&mode=${enc(mode)}&depth=${depth}&limit=${limit}`, signal); } /** Same-origin GET of any public route for the /developers request builder (returns status + parsed body or text). */ export async function clientTry(path: string, signal?: AbortSignal): Promise<{ status: number; ms: number; body: unknown; headers: Record }> { const t0 = performance.now(); const res = await fetch(`/api/v1${path}`, { headers: { accept: 'application/json' }, signal }); const text = await res.text(); let body: unknown = text; try { body = JSON.parse(text); } catch { /* keep text */ } const headers: Record = {}; for (const k of ['x-api-version', 'etag', 'cache-control', 'content-type']) { const v = res.headers.get(k); if (v) headers[k] = v; } return { status: res.status, ms: Math.round(performance.now() - t0), body, headers }; } // ---- /D3 ----