HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1'use client';2/** Browser-side fetches: same origin `/api/v1/*` (Next rewrite → FastAPI). Never import server `api.ts` in client components. */3import type { ChangeEvent, Claim, Page, ProvenanceDetail, SearchPayload, Suggestion } from './types';45export class ClientApiError extends Error {6 readonly status: number;7 constructor(status: number, path: string) {8 super(`API ${status} on ${path}`);9 this.name = 'ClientApiError';10 this.status = status;11 }12}1314async function get<T>(path: string, signal?: AbortSignal): Promise<T> {15 const res = await fetch(`/api/v1${path}`, { headers: { accept: 'application/json' }, signal });16 if (!res.ok) throw new ClientApiError(res.status, path);17 return (await res.json()) as T;18}1920const enc = encodeURIComponent;2122export const clientApi = {23 suggest: (q: string, signal?: AbortSignal) => get<{ items: Suggestion[] }>(`/search/suggest?q=${enc(q)}`, signal),24 search: (q: string, limit = 10, signal?: AbortSignal) => get<SearchPayload>(`/search?q=${enc(q)}&limit=${limit}`, signal),25 changes: (qs: string, signal?: AbortSignal) => get<Page<ChangeEvent>>(`/changes?${qs}`, signal),26 /** Events of one entity (watchlist): `/changes?entity=<slug>&limit=`. */27 entityChanges: (slug: string, limit = 20, signal?: AbortSignal) => get<Page<ChangeEvent>>(`/changes?entity=${enc(slug)}&limit=${limit}`, signal),28 /** 1.1 — full evidence behind one displayed value. 404 until the API stream lands → callers fall back to the inline provenance. */29 provenance: (slug: string, property: string, signal?: AbortSignal) => get<ProvenanceDetail>(`/entities/${enc(slug)}/provenance/${enc(property)}`, signal),30 /** Claim history of one property (exists today): used by the evidence drawer for the claim id, validity and conflicts. */31 history: (slug: string, property: string, signal?: AbortSignal) => get<{ items: Claim[] }>(`/entities/${enc(slug)}/history?property=${enc(property)}`, signal),32 /** Page-view beacon (1 req/s/IP server-side). Fire-and-forget. */33 view: (path: string) =>34 fetch('/api/v1/views', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path }), keepalive: true }).catch(() => undefined),35};3637// ---- D2 (intelligence) ----38import type { CostContextPayload, CostPayload, Price } from './types';39/** Browser-side calls used by the price terminal (lazy row sparklines), the calculator and provider pages. */40export const clientIntel = {41 /** Full price history of one model (optionally one provider). */42 priceHistory: (model: string, provider?: string, signal?: AbortSignal) => get<{ items: Price[] }>(`/prices/history?model=${enc(model)}${provider ? `&provider=${enc(provider)}` : ''}`, signal),43 /** `/cost` — query string already built by the caller (`model=&input_tokens=…`). */44 cost: (qs: string, signal?: AbortSignal) => get<CostPayload>(`/cost?${qs}`, signal),45 /** `/cost/context?tokens=`. */46 costContext: (qs: string, signal?: AbortSignal) => get<CostContextPayload>(`/cost/context?${qs}`, signal),47};48// ---- /D2 ----4950// ---- D3 (temporal/graph/admin) ----51import type { GraphExploreMode, GraphExplorePayload } from './types';52/** Graph explorer: root change and progressive neighbourhood expansion (merged client-side). */53export function clientGraphExplore(node: string, mode: GraphExploreMode, depth: 1 | 2 = 1, limit = 150, signal?: AbortSignal): Promise<GraphExplorePayload> {54 return get<GraphExplorePayload>(`/graph/explore?node=${enc(node)}&mode=${enc(mode)}&depth=${depth}&limit=${limit}`, signal);55}56/** Same-origin GET of any public route for the /developers request builder (returns status + parsed body or text). */57export async function clientTry(path: string, signal?: AbortSignal): Promise<{ status: number; ms: number; body: unknown; headers: Record<string, string> }> {58 const t0 = performance.now();59 const res = await fetch(`/api/v1${path}`, { headers: { accept: 'application/json' }, signal });60 const text = await res.text();61 let body: unknown = text;62 try {63 body = JSON.parse(text);64 } catch {65 /* keep text */66 }67 const headers: Record<string, string> = {};68 for (const k of ['x-api-version', 'etag', 'cache-control', 'content-type']) {69 const v = res.headers.get(k);70 if (v) headers[k] = v;71 }72 return { status: res.status, ms: Math.round(performance.now() - t0), body, headers };73}74// ---- /D3 ----75