import { z } from "zod"; import { configSchema, recordSchema, reportsSchema, toolsEnvelopeSchema } from "./contracts"; export class ApiError extends Error { readonly status: number; readonly body: unknown; constructor(status: number, body: unknown) { const data = recordSchema.safeParse(body); super(data.success && typeof data.data.detail === "string" ? data.data.detail : `Error del servidor (${status}).`); this.name = "ApiError"; this.status = status; this.body = body; } } export async function request(path: string, schema: z.ZodType, init?: RequestInit): Promise { const response = await fetch(path, init); const body: unknown = await response.json().catch(() => null); if (!response.ok) { throw new ApiError(response.status, body); } return schema.parse(body); } export function post(path: string, schema: z.ZodType, body: unknown, signal?: AbortSignal) { return request(path, schema, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), signal, }); } export async function optionalRequest(path: string, schema: z.ZodType, signal: AbortSignal) { try { return await request(path, schema, { signal }); } catch (error) { if (signal.aborted) { throw error; } // Profile and reports are optional in generic deployments. return null; } } export function getConfig(signal: AbortSignal) { return request("/api/config", configSchema, { signal }); } export async function getProfile(phone: string, signal: AbortSignal) { if (!phone) { return null; } const data = await optionalRequest( `/api/user/phone?${new URLSearchParams({ phoneNumber: phone })}`, recordSchema, signal, ); if (!data) { return null; } const result = recordSchema.safeParse(data.result); return result.success ? result.data : data; } export function getReports(phone: string, signal: AbortSignal) { const query = phone ? `?${new URLSearchParams({ phoneNumber: phone })}` : ""; return optionalRequest(`/api/user/reports${query}`, reportsSchema, signal); } export async function getTools(signal: AbortSignal, refresh = false) { const result = await request( `/api/apipilot/tools?include_unauthorized=true&refresh=${String(refresh)}`, toolsEnvelopeSchema, { signal }, ); if (!result.success || !result.data) { throw new Error("No se pudieron cargar las herramientas."); } return result.data.tools; } export const reasoningSchema = z.object({ answer: z.string().default(""), log_only: z.boolean().default(false) }); export const searchSchema = z.object({ answer: z.string().nullish(), results: z.array(z.object({ title: z.string(), snippet: z.string().default(""), url: z.string() })).default([]), }); /** Teardown must survive pagehide and must not hold up a new session. */ export function cancelQueue(queueId: string) { const path = "/api/queue/end"; const payload = JSON.stringify({ queueId }); if ( typeof navigator.sendBeacon === "function" && navigator.sendBeacon(path, new Blob([payload], { type: "application/json" })) ) { return; } void fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: payload, keepalive: true, }).catch(() => undefined); } export function errorMessage(error: unknown) { if (error instanceof z.ZodError) { return "El servidor devolvió una respuesta inesperada."; } if (error instanceof DOMException) { if (error.name === "NotAllowedError") { return "No se pudo acceder al micrófono. Revisa el permiso del navegador y vuelve a intentarlo."; } if (error.name === "NotFoundError") { return "No se encontró un micrófono. Conecta uno y vuelve a intentarlo."; } if (error.name === "NotReadableError") { return "El micrófono está ocupado o no está disponible."; } } return error instanceof Error ? error.message : "No se pudo completar la operación."; }