import type { ChatSummary, SettingsPayload, SettingsUpdate, SlashCommand } 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; title?: string; 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, title: s.title ?? "", 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 listSlashCommands( token: string, base: string = "", ): Promise { type Row = { command: string; title: string; description: string; icon: string; arg_hint?: string; }; const body = await request<{ commands: Row[] }>(`${base}/api/commands`, token); return body.commands.map((command) => ({ command: command.command, title: command.title, description: command.description, icon: command.icon, argHint: command.arg_hint ?? "", })); } 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); }