import type { ChatSummary, SettingsPayload, SettingsUpdate } from "./types"; export class ApiError extends Error { status: number; constructor(status: number, message: string) { super(message); this.status = status; this.name = "ApiError"; } } async function request( url: string, token: string, init?: RequestInit, ): Promise { const res = await fetch(url, { ...(init ?? {}), headers: { ...(init?.headers ?? {}), Authorization: `Bearer ${token}`, }, credentials: "same-origin", }); if (!res.ok) { throw new ApiError(res.status, `HTTP ${res.status}`); } return (await res.json()) as T; } function splitKey(key: string): { channel: string; chatId: string } { const idx = key.indexOf(":"); if (idx === -1) return { channel: "", chatId: key }; return { channel: key.slice(0, idx), chatId: key.slice(idx + 1) }; } export async function listSessions( token: string, base: string = "", ): Promise { type Row = { key: string; created_at: string | null; updated_at: string | null; preview?: string; }; const body = await request<{ sessions: Row[] }>( `${base}/api/sessions`, token, ); return body.sessions.map((s) => ({ key: s.key, ...splitKey(s.key), createdAt: s.created_at, updatedAt: s.updated_at, preview: s.preview ?? "", })); } /** Signed image URL attached to a historical user message. The server * emits these in place of raw on-disk paths so the client can render * previews without learning where media lives on disk. Each URL is a * self-authenticating ``/api/media/...`` route (see backend * ``_sign_media_path``) safe to drop into an ```` attribute. */ export interface SessionMediaUrl { url: string; name?: string; } export async function fetchSessionMessages( token: string, key: string, base: string = "", ): Promise<{ key: string; created_at: string | null; updated_at: string | null; messages: Array<{ role: string; content: string; timestamp?: string; tool_calls?: unknown; tool_call_id?: string; name?: string; /** Present on ``user`` turns that attached images. Paths have already * been stripped server-side; only the signed fetch URLs survive. */ media_urls?: SessionMediaUrl[]; }>; }> { return request( `${base}/api/sessions/${encodeURIComponent(key)}/messages`, token, ); } export async function deleteSession( token: string, key: string, base: string = "", ): Promise { const body = await request<{ deleted: boolean }>( `${base}/api/sessions/${encodeURIComponent(key)}/delete`, token, ); return body.deleted; } export async function fetchSettings( token: string, base: string = "", ): Promise { return request(`${base}/api/settings`, token); } export async function updateSettings( token: string, update: SettingsUpdate, base: string = "", ): Promise { const query = new URLSearchParams(); if (update.model !== undefined) query.set("model", update.model); if (update.provider !== undefined) query.set("provider", update.provider); return request(`${base}/api/settings/update?${query}`, token); }