import { recordSchema, type ServerConfig } from "./contracts"; export const VOICES = ["Aiden", "Ryan", "Dylan", "Eric", "Ono_Anna", "Serena", "Sohee", "Uncle_Fu", "Vivian"]; export const DEFAULT_INSTRUCTIONS = "You are Vocero, a friendly voice assistant. Keep replies short, warm, and spoken. Avoid long monologues. Respond ALWAYS and ONLY in Spanish, regardless of the language used by the user or any other instruction. No responses in Italian, English, Portuguese, or any other language are allowed. Every spoken sentence, greeting, confirmation, clarification, and error message must be in Spanish. The comma sign ',' in numbers is used to separate thousands. The point sign '.' in numbers is used to separate decimals. If you don't know the answer, say so in Spanish instead of guessing."; export const GATE_OFF = -66; export const GATE_MAX = -3; export interface Settings { directUrl: string; voice: string; instructions: string; noiseGate: number; webSearch: boolean; searchKey: string; } function read(key: string) { try { return localStorage.getItem(`s2s.ws.${key}`); } catch { return null; } } export function loadSettings(): Settings { const threshold = read("noiseGate"); const number = threshold === null || threshold === "" ? -50 : Number(threshold); let webSearch = true; try { const data = recordSchema.parse(JSON.parse(read("tools") ?? "{}")); webSearch = typeof data.web_search === "boolean" ? data.web_search : true; } catch { /* Ignore unavailable or older storage. */ } return { directUrl: read("directUrl") ?? "", voice: read("voice") || "Aiden", instructions: read("instructions") || DEFAULT_INSTRUCTIONS, noiseGate: Number.isFinite(number) ? Math.min(GATE_MAX, Math.max(GATE_OFF, Math.round(number))) : -50, webSearch, searchKey: read("searchKey") ?? "", }; } export function saveSettings(settings: Partial) { try { for (const key of ["directUrl", "voice", "instructions", "noiseGate", "searchKey"] as const) { if (settings[key] !== undefined) { localStorage.setItem(`s2s.ws.${key}`, String(settings[key])); } } if (settings.webSearch !== undefined) { localStorage.setItem("s2s.ws.tools", JSON.stringify({ web_search: settings.webSearch })); } } catch { /* The current session still works when browser storage is disabled. */ } } export interface Caller { phoneNumber: string; episodeId: string; } export function callerFromLocation(search = window.location.search): Caller { const query = new URLSearchParams(search); return { phoneNumber: (query.get("phoneNumber") ?? "").trim(), episodeId: (query.get("episode_id") ?? "").trim() }; } export function realtimeUrl(raw: string, caller: Caller): string { let value = raw.trim(); if (!value) { throw new Error("Introduce la URL del servidor de voz en Configuración."); } if (/^https?:\/\//i.test(value)) { value = value.replace(/^http/i, "ws"); } if (!/^wss?:\/\//i.test(value)) { value = (/^(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/i.test(value) ? "ws://" : "wss://") + value; } const url = new URL(value); if (url.pathname === "/") { url.pathname = "/v1/realtime"; } if (caller.phoneNumber) { url.searchParams.set("phoneNumber", caller.phoneNumber); } if (caller.episodeId) { url.searchParams.set("episode_id", caller.episodeId); } return url.toString(); } export function connectionTarget(config: ServerConfig, settings: Settings, caller: Caller) { return config.lb || !config.allowDirect ? { sessionUrl: "/api/session" } : { directUrl: realtimeUrl(config.s2sUrl || settings.directUrl, caller) }; } export function isDebug(search = window.location.search) { const value = new URLSearchParams(search).get("debug")?.toLowerCase(); return value === "true" || value === "1"; }