SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
4.3 KB · 75 lines typescript
Raw Blame History
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