refactor(frontend): rename project to s2s-frontend
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { accountSchema, 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<T>(path: string, schema: z.ZodType<T>, init?: RequestInit): Promise<T> {
|
||||
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<T>(path: string, schema: z.ZodType<T>, body: unknown, signal?: AbortSignal) {
|
||||
return request(path, schema, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
export async function optionalRequest<T>(path: string, schema: z.ZodType<T>, signal: AbortSignal) {
|
||||
try {
|
||||
return await request(path, schema, { signal });
|
||||
} catch (error) {
|
||||
if (signal.aborted) {
|
||||
throw error;
|
||||
}
|
||||
// Profile, reports, and account features are optional in generic deployments.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getConfig(signal: AbortSignal) {
|
||||
return request("/api/config", configSchema, { signal });
|
||||
}
|
||||
export function getAccount(signal: AbortSignal) {
|
||||
return optionalRequest("/api/me", accountSchema, 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 endReservation(path: "/api/session/end" | "/api/queue/end", body: Record<string, string>) {
|
||||
const payload = JSON.stringify(body);
|
||||
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.";
|
||||
}
|
||||
Reference in New Issue
Block a user