Opening a report or podcast link now establishes an authenticated starting reference for the conversation. Users can select other accessible content or compare reports while retaining the original link, and TALKFIRST greetings can name the verified report or podcast. Release notes: - Added authenticated report and episode link resolution with bounded lookups and unavailable-content handling - Added explicit conversation selection without restricting searches or changing the reference during comparisons - Added contextual TALKFIRST greetings through both voice-proxy and HTTP reasoning paths - Preserved backend permission checks, conversation isolation, cancellation, and the existing speech protocol Reviewed-on: AquaPortal/s2s#2
103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
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<Settings>) {
|
|
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;
|
|
reportId: 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(),
|
|
reportId: (query.get("report_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";
|
|
}
|