"use client"; export class ApiError extends Error { constructor(public status: number, message: string, public issues?: unknown) { super(message); } } export async function api(url: string, init?: RequestInit & { json?: unknown }): Promise { const headers = new Headers(init?.headers); let body = init?.body; if (init?.json !== undefined) { headers.set("content-type", "application/json"); body = JSON.stringify(init.json); } const r = await fetch(url, { ...init, headers, body, credentials: "same-origin" }); if (r.status === 401) { if (typeof window !== "undefined" && !location.pathname.startsWith("/login")) location.href = `/login?next=${encodeURIComponent(location.pathname)}`; throw new ApiError(401, "Session expirée"); } const ct = r.headers.get("content-type") ?? ""; const data = ct.includes("application/json") ? await r.json() : await r.text(); if (!r.ok) throw new ApiError(r.status, (data as { error?: string })?.error ?? `HTTP ${r.status}`, (data as { issues?: unknown })?.issues); return data as T; } export const fetcher = (url: string) => api(url); /** Parses a text/event-stream response into {event, data} objects. */ export async function* readSSE(res: Response): AsyncGenerator<{ event: string; data: unknown }> { if (!res.ok) { const t = await res.text(); let msg = t; try { msg = JSON.parse(t).error ?? t; } catch { /* raw */ } throw new ApiError(res.status, msg); } const reader = res.body!.getReader(); const dec = new TextDecoder(); let buf = ""; while (true) { const { value, done } = await reader.read(); if (done) break; buf += dec.decode(value, { stream: true }); let idx: number; while ((idx = buf.indexOf("\n\n")) >= 0) { const chunk = buf.slice(0, idx); buf = buf.slice(idx + 2); let event = "message"; let data = ""; for (const line of chunk.split("\n")) { if (line.startsWith("event:")) event = line.slice(6).trim(); else if (line.startsWith("data:")) data += line.slice(5).trim(); } if (!data) continue; try { yield { event, data: JSON.parse(data) }; } catch { yield { event, data }; } } } } export function fmtTokens(n?: number | null) { if (n == null) return "—"; return n >= 1000 ? `${(n / 1000).toFixed(n >= 10_000 ? 0 : 1)}k` : String(n); } export function fmtUsd(v?: number | null, digits = 4) { if (v == null) return "—"; if (v === 0) return "0 $"; return v < 0.01 ? `${v.toFixed(digits)} $` : `${v.toFixed(2)} $`; } export function fmtMs(ms?: number | null) { if (ms == null) return "—"; return ms >= 1000 ? `${(ms / 1000).toFixed(1)} s` : `${ms} ms`; } export function relTime(d: string | Date) { const t = new Date(d).getTime(); const diff = Date.now() - t; const m = Math.round(diff / 60000); if (m < 1) return "à l'instant"; if (m < 60) return `${m} min`; const h = Math.round(m / 60); if (h < 24) return `${h} h`; const dd = Math.round(h / 24); if (dd < 7) return `${dd} j`; return new Date(d).toLocaleDateString("fr-CA", { day: "numeric", month: "short" }); }