refactor: standardize developer text and identifiers in English

This commit is contained in:
2026-10-05 10:13:25 -03:00
parent 6b8c27fd76
commit dfc1f663c7
20 changed files with 933 additions and 1142 deletions
+144 -163
View File
@@ -22,36 +22,35 @@ import { ChatView } from "./ui/chat.js?v=202609041120";
import { Account } from "./ui/account.js?v=202609041120";
// ---------------------------------------------------------------------------
// 🔎 DEBUG BANNER (always-on): emissione incondizionata in console per capire
// ESATTAMENTE quale versione di main.js è caricata nel browser.
// Se NON vedi questa riga in Console = stai ancora usando il file CACHATO VECCHIO.
// 🔎 DEBUG BANNER (always-on): identify the exact main.js version loaded.
// If this line is missing from the console, the browser is using an older cached file.
console.info(
"%c🟢 VOCERO main.js loaded. version=2026-09-24 23:12 (DEBUG FETCH WRAPPER+WS SEMPRE-ATTIVO; APIPILOT: demo flag=true + PREFETCH AUTOMATICO OGNI SESSIONE WS + PREFETCH DOPO LOGIN-BY-PHONE OK (refresh=true per invalidare cache server anonima); cache client per-utente; cambio phone = invalidate automatico; export chat; fix scroll).",
"%c🟢 VOCERO main.js loaded. version=2026-09-24 23:12 (ALWAYS-ON FETCH WRAPPER+WS DEBUG; APIPILOT: demo flag=true + AUTOMATIC PREFETCH FOR EVERY WS SESSION + PREFETCH AFTER PHONE LOGIN (refresh=true invalidates the anonymous server cache); per-user client cache; phone changes invalidate automatically; chat export; scroll fix).",
"background:#1e293b;color:#86efac;font-weight:700;padding:4px 8px;border-radius:4px;"
);
console.info(
" URL attuale location.href=", location.href,
" Current URL location.href=", location.href,
"\n location.search=", location.search || "(empty)",
"\n DEBUG (imported from dom.js) =", DEBUG,
"\n typeof window.DEBUG =", typeof window.DEBUG
);
// eslint-disable-next-line no-debugger
// debugger; // ← scommenta per fermarti QUI all'avvio se DevTools è aperto
// debugger; // Uncomment to pause here at startup when DevTools is open.
// ---------------------------------------------------------------------------
// 🔎 FETCH WRAPPER (SEMANTIC DEBUG LOG) — attivo SEMPRE (non solo in DEBUG)
// Ogni chiamata `fetch` lato browser viene intercettata e loggata con:
// • URL risolto, metodo, dimensioni body + preview JSON
// • status HTTP (colorato verde/giallo/rosso a seconda della classe)
// • durata (performance.now), dimensione + preview risposta JSON
// Tag semantici per filtrare veloce la Console:
// 🔎 FETCH WRAPPER (SEMANTIC DEBUG LOG) — always active, including outside DEBUG.
// Every browser fetch is traced with:
// • resolved URL, method, body size and JSON preview
// • HTTP status (green/yellow/red according to status class)
// • duration (performance.now), response size and JSON preview
// Semantic tags for filtering the console:
// [FETCH:CONFIG] [FETCH:REASONING] [FETCH:SEARCH]
// [FETCH:PROFILE] [FETCH:REPORTS] [FETCH:APIPILOT]
// [FETCH:SESSION] [FETCH:QUEUE] [FETCH:ME]
// ---------------------------------------------------------------------------
(function _installFetchDebugTracer() {
const ORIGINAL_FETCH = window.fetch.bind(window);
// Classifica endpoint in tag leggibile (stesso ordine di @app route in server.py).
// Classify endpoints with readable tags, following the route order in server.py.
/** @param {string} url */
function _tagFor(url) {
try {
@@ -108,13 +107,13 @@ console.info(
/** @type {BodyInit | null | undefined} */
let body = init ? init.body : undefined;
if (body === undefined || body === null) {
// tenta Request
// Try the Request body.
try {
const r = /** @type {{body?:BodyInit|null}} */ (/** @type {unknown} */ (input));
if (r && r.body != null) body = r.body;
} catch { body = null; }
}
if (body == null) return { length: 0, preview: "(nessun body)" };
if (body == null) return { length: 0, preview: "(no body)" };
try {
if (typeof body === "string") {
const s = body;
@@ -138,7 +137,7 @@ console.info(
if (body instanceof Blob) return { length: body.size, preview: `BLOB (${body.type || "unknown"}) size=${body.size}` };
if (ArrayBuffer.isView(body)) return { length: body.byteLength, preview: `ArrayBufferView len=${body.byteLength}` };
if (body instanceof ArrayBuffer) return { length: body.byteLength, preview: `ArrayBuffer len=${body.byteLength}` };
return { length: 0, preview: "(body non serializzabile nel log)" };
return { length: 0, preview: "(body cannot be serialized for logging)" };
} catch (err) {
return { length: -1, preview: `(err preview body: ${err})` };
}
@@ -178,9 +177,8 @@ console.info(
const method = _extractMethod(input, init);
const rawUrl = _stringifyUrl(input);
const tag = _tagFor(rawUrl);
// Per richieste "heartbeat / end poll" (molto frequenti) logghiamo un
// singleton collapse per evitare spam: se `?quiet=true` o URL match → log
// a riga singola e non mostriamo preview.
// Log frequent heartbeat/end polling requests on one line without a body
// preview to keep the console readable.
const isPollish = /heartbeat|queue\/end|session\/end/i.test(tag) || (init && typeof init.keepalive === "boolean" && init.keepalive);
const reqPreview = isPollish ? null : await _extractBodyPreview(input, init);
if (!isPollish) {
@@ -245,18 +243,18 @@ console.info(
return response;
};
console.info(
"%c🔎 [FETCH-DEBUG] tracer installato. Ogni fetch è loggata con tag, method, url, status, durata e preview body.",
"%c🔎 [FETCH-DEBUG] tracer installed. Every fetch logs its tag, method, URL, status, duration and body preview.",
"background:#ecfeff;color:#0369a1;font-weight:700;padding:2px 6px;border-radius:4px;"
);
})();
// ---------------------------------------------------------------------------
// 🛡️ GATE .topbar-right (about / tools / rag / chat / settings buttons,
// vedi index.html L36-L56) — 3-layer safety:
// 1) HTML statico → hidden + data-debug-only="true"
// see index.html) — 3-layer safety:
// 1) static HTML → hidden + data-debug-only="true"
// 2) CSS → [data-debug-only="true"]:not([data-debug-enabled="true"]) display:none !important
// 3) JS qui → hidden, removeAttribute data-debug-enabled, anche style.display = "none"
// Solo se URL query-string ha ?debug=true (strict string) abilitiamo TUTTO.
// 3) JS here → hidden, remove data-debug-enabled, set style.display = "none"
// Enable all debug controls only when the URL contains ?debug=true (strict string).
function applyDebugOnlyVisibilityGate(source = "bootstrap") {
const qs = new URLSearchParams(window.location.search);
const debugEnabled = qs.get("debug") === "true";
@@ -265,64 +263,61 @@ function applyDebugOnlyVisibilityGate(source = "bootstrap") {
"%c🧭 [gate-topbar-right] applyDebugOnlyVisibilityGate()",
"color:#f59e0b;font-weight:700;"
);
console.log("chiamato da:", source);
console.log("called from:", source);
console.log("location.search =", JSON.stringify(window.location.search));
console.log("query params =", Object.fromEntries(qs.entries()));
console.log("debugEnabled (strict === 'true') =", debugEnabled);
console.log("topbarRight element trovato?", !!topbarRight, topbarRight ? "vedi →" : "(null!)");
console.log("topbarRight element found?", !!topbarRight, topbarRight ? "see →" : "(null!)");
const chatInline = document.getElementById("chat-panel");
console.log("#chat-panel (chat inline SEMPRE VISIBILE in basso) presente in DOM =", !!chatInline, chatInline ? "hidden?" : "", chatInline ? chatInline.hasAttribute("hidden") : "-");
console.log("#chat-panel (inline chat ALWAYS VISIBLE at the bottom) present in DOM =", !!chatInline, chatInline ? "hidden?" : "", chatInline ? chatInline.hasAttribute("hidden") : "-");
if (topbarRight) {
console.log(" hidden attr prima:", topbarRight.hasAttribute("hidden"),
"| data-debug-enabled prima:", topbarRight.getAttribute("data-debug-enabled"),
"| style.display prima:", topbarRight.style.display);
console.log(" hidden attr before:", topbarRight.hasAttribute("hidden"),
"| data-debug-enabled before:", topbarRight.getAttribute("data-debug-enabled"),
"| style.display before:", topbarRight.style.display);
if (debugEnabled) {
topbarRight.setAttribute("data-debug-enabled", "true");
topbarRight.removeAttribute("hidden");
topbarRight.style.display = "";
console.log("%c ✅ ABILITATO .topbar-right (debug=true)", "color:#22c55e;font-weight:700;");
console.log("%c ✅ ENABLED .topbar-right (debug=true)", "color:#22c55e;font-weight:700;");
} else {
topbarRight.removeAttribute("data-debug-enabled");
topbarRight.setAttribute("hidden", "");
topbarRight.style.setProperty("display", "none", "important");
console.log("%c 🚫 DISABILITATO .topbar-right (no debug=true)",
console.log("%c 🚫 DISABLED .topbar-right (no debug=true)",
"background:#fee2e2;color:#b91c1c;font-weight:700;padding:3px 6px;border-radius:4px;");
}
console.log(" hidden attr dopo:", topbarRight.hasAttribute("hidden"),
"| data-debug-enabled dopo:", topbarRight.getAttribute("data-debug-enabled"),
"| style.display dopo:", topbarRight.style.display);
console.log(" hidden attr after:", topbarRight.hasAttribute("hidden"),
"| data-debug-enabled after:", topbarRight.getAttribute("data-debug-enabled"),
"| style.display after:", topbarRight.style.display);
}
// 3-layer gate UNIVERSALE su TUTTI gli elementi marcati con data-debug-only="true"
// (non solo .topbar-right, ma anche #reports-sidebar-toggle e qualsiasi
// altro elemento futuro marcato con lo stesso attributo).
// Apply the 3-layer gate to every element marked data-debug-only="true",
// including .topbar-right, #reports-sidebar-toggle and future controls.
const debugOnlyEls = document.querySelectorAll("[data-debug-only='true']");
console.log(
`Trovati ${debugOnlyEls.length} elementi [data-debug-only="true"] da gate-are in base a debugEnabled=${debugEnabled}`,
`Found ${debugOnlyEls.length} elements [data-debug-only="true"] to gate using debugEnabled=${debugEnabled}`,
);
debugOnlyEls.forEach((el, idx) => {
const tag = `${el.tagName.toLowerCase()}${el.id ? `#${el.id}` : ""}${el.className && typeof el.className === "string" ? `.${el.className.split(/\s+/).filter(Boolean).slice(0, 2).join(".")}` : ""}`;
console.log(` [${idx + 1}/${debugOnlyEls.length}] ${tag} — hidden prima? ${el.hasAttribute("hidden")} | display prima: ${(el instanceof HTMLElement ? el.style.display : "-")}`);
console.log(` [${idx + 1}/${debugOnlyEls.length}] ${tag} — hidden before? ${el.hasAttribute("hidden")} | display before: ${(el instanceof HTMLElement ? el.style.display : "-")}`);
if (!(el instanceof HTMLElement)) return;
if (debugEnabled) {
el.setAttribute("data-debug-enabled", "true");
// Non forziamo hidden=false su elementi che sono logicamente hidden per
// altri motivi (es. #reports-sidebar-toggle è hidden se non ci sono
// reports). Rimuoviamo lo "spegnimento debug-only" e basta.
// Preserve elements hidden for other reasons, such as an empty reports
// sidebar. Remove only the debug-only display override.
el.style.removeProperty("display");
} else {
el.removeAttribute("data-debug-enabled");
el.setAttribute("hidden", "");
el.style.setProperty("display", "none", "important");
}
console.log(` → debugEnabled=${debugEnabled} → hidden dopo? ${el.hasAttribute("hidden")} | display dopo: ${el.style.display || "(inherit)"}`);
console.log(` → debugEnabled=${debugEnabled} → hidden after? ${el.hasAttribute("hidden")} | display after: ${el.style.display || "(inherit)"}`);
});
console.groupEnd();
return debugEnabled;
}
// Esegui SUBITO al bootstrap del modulo (prima di qualsiasi listener)
// Run immediately during module bootstrap, before registering listeners.
const GLOBAL_DEBUG_ENABLED__TOPBAR = applyDebugOnlyVisibilityGate("main.js module bootstrap (top)");
// Riesegui dopo DOMContentLoaded per sicurezza (nel caso query script sia posizionato
// in testa e l'HTML header non sia ancora parse-ato quando girano queste righe)
// Run again after DOMContentLoaded in case the script runs before the header is parsed.
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
applyDebugOnlyVisibilityGate("DOMContentLoaded listener");
@@ -330,7 +325,7 @@ if (document.readyState === "loading") {
} else {
applyDebugOnlyVisibilityGate("module bootstrap (DOM already parsed)");
}
// Riesegui anche a fine frame dopo un breve timeout → ultima chance.
// Run again after a short timeout as a final fallback.
setTimeout(() => applyDebugOnlyVisibilityGate("10ms timeout fallback"), 10);
window.addEventListener("hashchange", () => applyDebugOnlyVisibilityGate("hashchange"), { passive: true });
window.addEventListener("popstate", () => applyDebugOnlyVisibilityGate("popstate"), { passive: true });
@@ -339,12 +334,12 @@ const DEFAULT_VOICE = "Aiden";
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 (Castellano / español), regardless of the language used by the user or any other instruction. " +
"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 it's used to separate thousands."+
"the point sign '.' in numbers it's used to separate decimals."+
"If you don't know the answer, say 'I don't know' instead of guessing.";
"If you don't know the answer, say so in Spanish instead of guessing.";
const OPENAI_RENDER_MARKER = "[VOCERO_RENDER_REPLY]";
const OPENAI_RENDER_HINT =
@@ -1314,13 +1309,13 @@ function syncVoceroHeaderUser() {
ul.className = "reports-list";
const labelTintFor = (rawLabel) => {
const l = String(rawLabel || "").toLowerCase();
if (l.includes("mortalidad") || l.includes("riesgo") || l.includes("kpi")) return "mortalidad";
if (l.includes("métrica") || l.includes("metricas") || l.includes("medici")) return "metricas";
if (l.includes("diario") || l.includes("diaria") || l.includes("informe")) return "diario";
if (l.includes("producto") || l.includes("servicio")) return "productos";
if (l.includes("mortalidad") || l.includes("riesgo") || l.includes("kpi")) return "mortality";
if (l.includes("métrica") || l.includes("metricas") || l.includes("medici")) return "metrics";
if (l.includes("diario") || l.includes("diaria") || l.includes("informe")) return "daily";
if (l.includes("producto") || l.includes("servicio")) return "products";
if (l.includes("synapse") || l.includes("ejecucion") || l.includes("calendario") || l.includes(" ti ") || l.endsWith("ti")) return "synapse";
if (l.includes("supply") || l.includes("cadena") || l.includes("log") || l.includes("envío") || l.includes("envio") || l.includes("chile")) return "supply";
if (l.includes("financiero") || l.includes("finanzas") || l.includes("costo") || l.includes("presupuesto") || l.includes("venta")) return "finanzas";
if (l.includes("financiero") || l.includes("finanzas") || l.includes("costo") || l.includes("presupuesto") || l.includes("venta")) return "finance";
return "";
};
for (const e of entries) {
@@ -1330,7 +1325,7 @@ function syncVoceroHeaderUser() {
li.textContent = String(e.sourceScheduleLabel);
ul.appendChild(li);
}
// Build dynamic suggestion prompts (en español) based on the actual labels so
// Build dynamic suggestion prompts in Spanish based on the actual labels so
// the user knows what they can ask the assistant about. Max 4 suggestions;
// pick first N entries by priority.
const suggestions = /** @type {string[]} */ ([]);
@@ -1374,20 +1369,20 @@ function syncVoceroHeaderUser() {
const qs = new URLSearchParams(window.location.search);
const debugParamRaw = qs.get("debug");
// eslint-disable-next-line no-debugger
// debugger; // ← scommenta per fermarti dentro syncVoceroHeaderUser ogni volta
// debugger; // Uncomment to pause inside every syncVoceroHeaderUser call.
console.groupCollapsed("%c🔍 [reports-ui] syncVoceroHeaderUser() render details", "color:#60a5fa;font-weight:700;");
console.log("location.href =", location.href);
console.log("location.search =", JSON.stringify(window.location.search));
console.log("tutti i query params =", Object.fromEntries(qs.entries()));
console.log("all query params =", Object.fromEntries(qs.entries()));
console.log("pageParams (legacy module-load) .get('debug') =", JSON.stringify(pageParams.get("debug")));
console.log("fresh qs .get('debug') =", JSON.stringify(debugParamRaw), "| typeof =", typeof debugParamRaw);
console.log("strict === 'true' ? =", debugParamRaw === "true");
console.log("count (labels uniche) =", count, "| wrappedLen =", totalLen);
console.log("suggerimenti array length (prima del gate debug) =", suggestions.length);
console.log("count (unique labels) =", count, "| wrappedLen =", totalLen);
console.log("suggestions array length (before debug gate) =", suggestions.length);
suggestions.forEach((s, i) => console.log(` sug[${i}] = ${s}`));
const currentDebug = debugParamRaw === "true";
const debugMode = currentDebug;
console.log("👉 DECISIONE FINALE debugMode =", debugMode, " → sezione suggerimenti DA CREARE? ", debugMode ? "✅ SÌ" : "❌ NO");
console.log("👉 FINAL DECISION debugMode =", debugMode, " → CREATE suggestions section? ", debugMode ? "✅ YES" : "❌ NO");
console.groupEnd();
let sugSection = null;
@@ -1406,10 +1401,10 @@ function syncVoceroHeaderUser() {
}
sugSection.appendChild(sugTitle);
sugSection.appendChild(sugUl);
console.log("%c[reports-ui] sezione suggerimenti CREATA (debugMode=true)", "color:#22c55e;font-weight:700;",
console.log("%c[reports-ui] suggestions section CREATED (debugMode=true)", "color:#22c55e;font-weight:700;",
"nItems=", sugUl.children.length, "innerHTML=", sugSection.outerHTML.slice(0, 300));
} else {
console.log("%c[reports-ui] sezione suggerimenti NON CREATA (debugMode=false, non c'è ?debug=true)",
console.log("%c[reports-ui] suggestions section NOT CREATED (debugMode=false, no ?debug=true)",
"color:#ef4444;font-weight:700;");
}
@@ -1453,7 +1448,7 @@ function syncVoceroHeaderUser() {
const qs = new URLSearchParams(window.location.search);
const debugParam = qs.get("debug") === "true";
console.info(
"%c[reports-ui] toggle reports-sidebar-toggle: hidden? PRIMA=%s | debug=%s",
"%c[reports-ui] toggle reports-sidebar-toggle: hidden? BEFORE=%s | debug=%s",
"color:#f59e0b;font-weight:700;",
reportsSidebarToggle.hasAttribute("hidden"),
debugParam,
@@ -1464,7 +1459,7 @@ function syncVoceroHeaderUser() {
reportsSidebarToggle.setAttribute("hidden", "");
reportsSidebarToggle.style.setProperty("display", "none", "important");
reportsSidebarToggle.removeAttribute("data-debug-enabled");
console.info("%c[reports-ui] 🚫 reports sidebar toggle FORZATO HIDDEN (no debug=true)",
console.info("%c[reports-ui] 🚫 reports sidebar toggle FORCED HIDDEN (no debug=true)",
"background:#fee2e2;color:#b91c1c;font-weight:700;padding:3px 6px;border-radius:4px;");
}
}
@@ -1476,10 +1471,10 @@ function syncVoceroHeaderUser() {
// eslint-disable-next-line no-console
console.info(
"%c[reports-ui] FINE RENDER. reportsHint.querySelectorAll('.reports-suggestions').length =",
"%c[reports-ui] RENDER COMPLETE. reportsHint.querySelectorAll('.reports-suggestions').length =",
"color:#a855f7;font-weight:700;",
reportsHint.querySelectorAll(".reports-suggestions").length,
"| children nel blocco:", reportsHint.children.length,
"| children in block:", reportsHint.children.length,
"| reportsHint.hidden=", reportsHint.hidden,
"| #reports-sidebar hidden=", reportsSidebar ? reportsSidebar.hidden : "no-sidebar-el",
"| badge count:", reportsSidebarBadge ? reportsSidebarBadge.getAttribute("data-count") : "no-badge"
@@ -1513,21 +1508,19 @@ function effectiveReasoningInstructions() {
ragAppended = true;
}
// ───────────────────────────────────────────────────────────────────────────
// 2026-09-25 INJECT API-PILOT HERAMIENTAS DINÁMICAS (PASADA 1).
// Formato IDENTICO a server.py Python: marker <<<APIPILOT_TOOLS_LIST_BEGIN>>>
// / ...lista name+description+requires_auth+required_parameters... / <<<END>>>.
// Il client ottiene la lista dal proxy GET api/apipilot/tools (cache 240 s
// per-utente in s2s-ws-client.js _apipilotClientCache) e la appende qui alle
// instructions STESSE (luogo #1 di massima priorità per l'LLM).
// Dedup: se le instructions già contengono il marker (già appese in una
// esecuzione precedente, oppure server Python ha già patchato il payload)
// → NON appendere nuovamente (no doppioni, no blob gigante).
// 2026-09-25 INJECT API-PILOT DYNAMIC TOOLS (PASS 1).
// Match the server.py format: <<<APIPILOT_TOOLS_LIST_BEGIN>>>, followed by
// name+description+requires_auth+required_parameters and the closing marker.
// Fetch tools through GET api/apipilot/tools (240 s per-user client cache in
// s2s-ws-client.js) and append them directly to the LLM instructions.
// Deduplicate by marker when a previous call or the Python server already
// injected the list, avoiding duplicate or oversized prompt blocks.
let apipilotBlock = "";
let apipilotCount = 0;
let apipilotAppended = false;
try {
// @ts-ignore: esposto globalmente da s2s-ws-client.js (caricato prima di
// main.js in index.html). Valuta sicuro: typeof + early-skip.
// @ts-ignore: exported globally by s2s-ws-client.js, loaded before main.js.
// Check typeof before calling the optional helper.
const getter = typeof window !== "undefined" && window.__s2sGetApipilotPass1ContextInfo;
if (typeof getter === "function") {
const info = getter();
@@ -1544,7 +1537,7 @@ function effectiveReasoningInstructions() {
}
}
} catch (_) {
/* never-fail: non bloccare il resto dell'istruzione prompt. */
/* Never block the rest of the instructions prompt. */
}
const key = `effInst:${injectProfile ? "T" : "F"}:${ctx ? "hasCtx" : "noCtx"}:${ragCtx ? "hasRag" : "noRag"}:${apipilotAppended ? "apipilotY" : "apipilotN"}:${(base || "").length}`;
_logOnce(key, () => {
@@ -1701,7 +1694,7 @@ async function fetchOpenAiReasoningAnswer(transcript) {
const json = await res.json();
const answer = typeof json?.answer === "string" ? json.answer.trim() : "";
const log_only = Boolean(json && json.log_only === true);
// Log-only mode: può tornare answer="" — non è un errore.
// Log-only mode may return answer=""; that is not an error.
if (!answer && !log_only) throw new Error("reasoning returned no answer");
return { answer, log_only, raw: json };
}
@@ -1732,10 +1725,10 @@ async function maybeRunOpenAiReasoning(transcript, itemId) {
});
return;
}
// ── LOG-ONLY MODE (FASE 1) ─────────────────────────────────────────────
// Node-RED si limita a loggare e torna log_only=true.
// NON cancelliamo la risposta backend, NON iniettiamo testo, NON parliamo.
// Il flusso resta COMPLETAMENTE a carico del backend S2S nativo.
// ── LOG-ONLY MODE (PHASE 1) ────────────────────────────────────────────
// Node-RED only logs the request and returns log_only=true.
// Preserve the backend response without injecting text or speaking.
// The native S2S backend remains responsible for the entire turn.
if (result && result.log_only) {
reportDebug("R", "main.js:maybeRunOpenAiReasoning", "external reasoning in LOG_ONLY mode — pass through to native S2S backend", {
itemId,
@@ -1749,9 +1742,9 @@ async function maybeRunOpenAiReasoning(transcript, itemId) {
);
return;
}
// ── MODE STANDARD (fasi successive) ────────────────────────────────────
// Il reasoning esterno vince: annulla la risposta nativa, inserisci il
// testo dell'assistente e parla via SpeechSynthesis.
// ── STANDARD MODE (LATER PHASES) ──────────────────────────────────────
// External reasoning takes priority: cancel the native response, insert
// the assistant text and speak through SpeechSynthesis.
const answer = (result && result.answer) || "";
if (!answer) {
console.warn(`[Vocero REASONING] empty non-log-only answer itemId=${JSON.stringify(itemId)}`);
@@ -2461,16 +2454,16 @@ async function fetchVoceroUserProfile() {
Object.keys(voceroUserProfile).join(","),
extractVoceroUserName(voceroUserProfile) || "(none)",
);
// ── 🚀 PREFETCH ApiPilot IMMEDIATAMENTE DOPO LOGIN-BY-PHONE RIUSCITO
// Forza refresh=true per bypassare la cache SERVER lato proxy S2S (TTL 300s)
// perché adesso abbiamo un profilo utente autenticato (non più anonimo).
// In questo modo vediamo la riga [apipilot] list_tools.start user=(non-anon)
// nel backend AquaPortal OGNI VOLTA che c'è un login phone riuscito.
// ── 🚀 PREFETCH ApiPilot IMMEDIATELY AFTER SUCCESSFUL PHONE LOGIN
// Force refresh=true to bypass the S2S proxy server cache (TTL 300 s)
// now that the caller is authenticated rather than anonymous.
// AquaPortal logs [apipilot] list_tools.start user=(non-anon) after
// every successful phone login.
try {
if (window.__s2sApipilot) {
const t0 = performance.now();
console.info(
"%c🔧 [APIPILOT-CLIENT] post-login phone → FORZO refresh api/apipilot/tools (bypass cache S2S 300s perché utente appena autenticato)",
"%c🔧 [APIPILOT-CLIENT] post-login phone → FORCE refresh api/apipilot/tools (bypass the 300 s S2S cache for the newly authenticated user)",
"background:#ede9fe;color:#5b21b6;font-weight:700;padding:2px 6px;border-radius:4px;"
);
const demoToken = typeof window.__s2sDemoSessionToken === "string" ? window.__s2sDemoSessionToken : "";
@@ -2485,24 +2478,23 @@ async function fetchVoceroUserProfile() {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const j = await r.json().catch(() => ({}));
const ok = j && j.success && j.data;
if (!ok) throw new Error(`envelope non corretto success=${Boolean(j && j.success)}`);
if (!ok) throw new Error(`invalid envelope success=${Boolean(j && j.success)}`);
const tools = Array.isArray(j.data.tools) ? j.data.tools : [];
const count = typeof j.data.count === "number" ? j.data.count : tools.length;
const names = tools.slice(0, 6).map((t) => (typeof t.name === "string" ? t.name : "?")).join(", ");
// Invalida cache client WS per ogni sessione futura (in modo che
// al prossimo connect() venga fatto un refresh per ri-confermare).
// Invalidate the WS client cache so the next session refreshes it.
try {
// @ts-ignore
if (typeof window.__s2sInvalidateApipilotCache === "function") window.__s2sInvalidateApipilotCache();
} catch (_) { /* noop */ }
console.info(
`%c🔧 [APIPILOT-CLIENT] post-login OK (dt=${dt}ms): N=${count} strumenti [primi 6: ${names || "(nessuno)"}]`,
`%c🔧 [APIPILOT-CLIENT] post-login OK (dt=${dt}ms): N=${count} tools [first 6: ${names || "(none)"}]`,
"background:#ecfdf5;color:#047857;font-weight:700;padding:2px 6px;border-radius:4px;"
);
}).catch((err) => {
const dt = Math.round(performance.now() - t0);
console.warn(
`%c🔧 [APIPILOT-CLIENT] post-login FAIL (dt=${dt}ms): ${String(err instanceof Error ? err.message : err)} → controlla [APIPILOT-DISCOVERY] nel log server S2S`,
`%c🔧 [APIPILOT-CLIENT] post-login FAIL (dt=${dt}ms): ${String(err instanceof Error ? err.message : err)} → check [APIPILOT-DISCOVERY] in the S2S server logs`,
"background:#fffbeb;color:#92400e;font-weight:700;padding:2px 6px;border-radius:4px;"
);
});
@@ -2618,10 +2610,9 @@ async function fetchConfig() {
pinnedPrompt = (json.primaryPrompt || "").trim();
injectProfile = !!json.injectProfile;
debugPrompt = !!json.debugPrompt;
// Feature flag: ApiPilot AquaPortal bridge attivo sul server.
// Se true = S2S_APIPILOT_TOOLS_URL è valorizzato lato backend e
// gli endpoint proxy /api/apipilot/tools e /api/apipilot/execute
// sono disponibili per il frontend.
// Feature flag for the server-side ApiPilot AquaPortal bridge.
// True means S2S_APIPILOT_TOOLS_URL is configured and the frontend can
// use /api/apipilot/tools and /api/apipilot/execute.
const apipilotEnabled = !!json.apipilot;
if (typeof window.__s2sApipilot === "undefined") {
try { Object.defineProperty(window, "__s2sApipilot", { value: false, writable: true, configurable: true }); }
@@ -2672,16 +2663,15 @@ async function fetchConfig() {
apipilotEnabled,
});
// #endregion
// ── Demo call ApiPilot (fire-and-forget, solo se flag=true sul server)
// Serve ESCLUSIVAMENTE per attivare il tracer fetch lato browser e
// mostrare in console la riga [FETCH:APIPILOT:TOOLS] con preview
// status, durata e lista strumenti. Proxy server-side verso
// ── ApiPilot demo call (fire-and-forget, only when the server flag is true)
// Trigger the browser fetch tracer to show [FETCH:APIPILOT:TOOLS] with
// status, duration and a tool-list preview. The server proxies to
// S2S_APIPILOT_TOOLS_URL/tools.
if (apipilotEnabled) {
try {
const demoUrl = "api/apipilot/tools?include_unauthorized=true";
console.info(
"%c🔧 [APIPILOT-CLIENT] flag=true → DEMO CALL %s (tracer sempre-attivo logga [FETCH:APIPILOT:TOOLS] sotto)",
"%c🔧 [APIPILOT-CLIENT] flag=true → DEMO CALL %s (always-on tracer logs [FETCH:APIPILOT:TOOLS] below)",
"background:#e0e7ff;color:#3730a3;font-weight:700;padding:2px 6px;border-radius:4px;",
demoUrl
);
@@ -2693,15 +2683,15 @@ async function fetchConfig() {
const count = (j && j.data && typeof j.data.count === "number") ? j.data.count : tools.length;
const names = tools.slice(0, 5).map((t) => (typeof t.name === "string" ? t.name : "?")).join(", ");
console.info(
"%c🔧 [APIPILOT-CLIENT] demo OK: %s strumenti [primi 5: %s] usa POST api/apipilot/execute {name,arguments} per eseguire",
"%c🔧 [APIPILOT-CLIENT] demo OK: %s tools [first 5: %s] use POST api/apipilot/execute {name,arguments} to execute",
"background:#ecfdf5;color:#047857;font-weight:700;padding:2px 6px;border-radius:4px;",
count,
names || "(nessuno)"
names || "(none)"
);
})
.catch((err) => {
console.warn(
"%c🔧 [APIPILOT-CLIENT] demo FAIL: %s. Lato server logga [APIPILOT-DISCOVERY] per diagnosticare",
"%c🔧 [APIPILOT-CLIENT] demo FAIL: %s. Check [APIPILOT-DISCOVERY] server logs to diagnose",
"background:#fffbeb;color:#92400e;font-weight:700;padding:2px 6px;border-radius:4px;",
String(err)
);
@@ -2903,16 +2893,16 @@ function _applyThemeRuntime() {
}
}
// 6. Quick action buttons: clicking "Resumen" / "Consultar datos" /
// "Crear respuesta" populates the text input or — if a realtime
// 6. Quick action buttons for summaries, data lookup and response drafts
// populate the text input or — if a realtime
// session is live — sends it as user text straight away.
document.querySelectorAll(".quick-action-btn").forEach(btn => {
btn.addEventListener("click", () => {
const action = btn.getAttribute("data-action") || "";
const prompts = {
"resumen": "haz un resumen ejecutivo del último informe programado",
"consultar-datos": "consulto los datos más recientes del dashboard, dame las cifras clave",
"crear-respuesta": "redacta un borrador de respuesta profesional con tono ejecutivo",
"summary": "haz un resumen ejecutivo del último informe programado",
"query-data": "consulto los datos más recientes del dashboard, dame las cifras clave",
"draft-response": "redacta un borrador de respuesta profesional con tono ejecutivo",
};
const text = prompts[action] || "";
if (!text) return;
@@ -3480,8 +3470,8 @@ function onClientStatus(status) {
case "creating-session":
case "connecting":
// Qualsiasi inizio connessione (manuale da Orb / restart o unlocker):
// se il talk-unlocker è ancora visibile (utente ha cliccato l'Orb prima
// del 2s warmup, ad esempio) lo nascondiamo subito.
// Hide the talk unlocker immediately if it is still visible, for
// example when the user taps the orb before the 2 s warmup ends.
hideTalkUnlocker();
setState("connecting");
break;
@@ -3575,19 +3565,16 @@ function onFatalError(err) {
setState("idle");
chat.renderEmptyState();
initGateArc();
// Schedulo auto-start TALK / TALKFIRST (S2S_START_SESSION_MODE nel .env):
// • "TALK" → dopo 2s mostra overlay unlocker (policy browser:
// getUserMedia() richiede gesture). Click/touch/tasto =
// doStart() dentro la gesture, poi ws startWithResponse
// inject hola vocero via sendUserText+requestResponse nel
// ws client (stesso flow identico a wake-word).
// • "TALKFIRST" → dopo 2s TENTA direttamete doStart() dal timer, con
// pendingWakeWordConfirmationText preimpostato. Se i
// permessi del mic sono GRANTED (già consentiti in passato
// o Chrome policy permissive), la wake-word simulata scatta
// subito senza nessun click. Se invece fallisce per
// permesso / gesture (Safari/iOS), fallback sullo stesso
// unlocker di TALK.
// Schedule TALK / TALKFIRST auto-start (S2S_START_SESSION_MODE in .env):
// • "TALK" → show the unlocker after 2 s. Browser policy requires a
// gesture for getUserMedia(). A click/touch/key starts
// doStart(), then WS startWithResponse injects hola vocero
// through sendUserText+requestResponse, like a wake word.
// • "TALKFIRST" → try doStart() directly from the timer after 2 s with
// pendingWakeWordConfirmationText already set. If mic
// permission is granted, the simulated wake word triggers
// without a click. Permission/gesture failures on Safari
// or iOS fall back to the TALK unlocker.
let autoStartTalkTimerId = 0;
let _talkUnlockerShown = false;
function hideTalkUnlocker() {
@@ -3599,11 +3586,9 @@ function showTalkUnlocker(fallbackHint = false) {
if (!(startSessionMode === "TALK" || startSessionMode === "TALKFIRST")) return;
const el = document.getElementById("talk-unlocker");
if (!el) return;
// L'unlocker è utile solo a stato idle/error: se l'utente ha già cliccato l'Orb
// o è connecting/connected non mostriamo niente.
// Show the unlocker only while idle/error, before a session starts.
if (!(currentState === "idle" || currentState === "error")) return;
// Se è fallback da TALKFIRST cambiamo il testo in modo che spieghi perché
// è comparso ("tocca per consentire il microfono", invece di "preparando")
// Explain the microphone permission requirement when TALKFIRST falls back.
const subEl = el.querySelector(".talk-unlocker-sub");
const titleEl = el.querySelector(".talk-unlocker-title");
if (fallbackHint) {
@@ -3613,20 +3598,20 @@ function showTalkUnlocker(fallbackHint = false) {
el.removeAttribute("hidden");
_talkUnlockerShown = true;
console.info(
"%c[start-session] ▶️ talk-unlocker mostrato (%s): attende gesture utente",
"%c[start-session] ▶️ talk-unlocker shown (%s): waiting for a user gesture",
"color:#2563eb;font-weight:700;background:#dbeafe;padding:3px 6px;border-radius:4px;",
fallbackHint ? "fallback TALKFIRST" : "TALK mode"
);
}
function maybeScheduleAutoStart() {
// Pulisco un eventuale timer precedente (caso di doppia chiamata non attesa).
// Clear any previous timer if this function was unexpectedly called twice.
if (autoStartTalkTimerId) {
clearTimeout(autoStartTalkTimerId);
autoStartTalkTimerId = 0;
}
if (startSessionMode !== "TALK" && startSessionMode !== "TALKFIRST") return;
console.info(
`%c[start-session] S2S_START_SESSION_MODE=${startSessionMode}: azione tra 2000 ms (warmup profilo/reports/episode)`,
`%c[start-session] S2S_START_SESSION_MODE=${startSessionMode}: action in 2000 ms (profile/reports/episode warmup)`,
"color:#2563eb;font-weight:700;background:#dbeafe;padding:3px 6px;border-radius:4px;"
);
autoStartTalkTimerId = window.setTimeout(() => {
@@ -3638,49 +3623,45 @@ function maybeScheduleAutoStart() {
}
}, 2000);
}
/** Tenta l'auto-start diretto (TALKFIRST) SENZA gesture. Simula la wake word
* impostando pendingWakeWordConfirmationText="hola vocero" (stessa variabile
* del riconoscitore vocale reale), poi chiama doStart() — se i permessi sono
* già grantiti (Chrome/Android dopo "allow always"), tutto funziona senza
* click. Altrimenti fallback sull'unlocker. */
/** Attempt direct TALKFIRST auto-start without a gesture. Simulate a wake word
* by setting pendingWakeWordConfirmationText="hola vocero", as the real speech
* recognizer does, then call doStart(). Existing permissions may allow this
* without a click; otherwise fall back to the unlocker. */
async function tryTalkFirstDirect() {
if (!(currentState === "idle" || currentState === "error")) return;
if (missingServerUrl()) {
console.warn("[start-session] skip TALKFIRST: manca server URL");
console.warn("[start-session] skip TALKFIRST: missing server URL");
promptServerUrl();
return;
}
// Step 1: pre-carico la wake-word simulata. Quando onClientStatus riceve
// "connected", la frase viene injectata e il modello risponde esattamente
// come quando la hot-word è stata pronunciata dal microfono.
// Prepare the simulated wake word. When onClientStatus receives "connected",
// inject it so the model responds as if the user had spoken it.
pendingWakeWordConfirmationText = pendingWakeWordConfirmationText?.trim()
? pendingWakeWordConfirmationText
: "hola vocero";
console.info(
`%c[start-session] 🎤 TALKFIRST: imposto wake-word simulata=${JSON.stringify(pendingWakeWordConfirmationText)} e provo doStart() diretto (SENZA gesture)`,
`%c[start-session] 🎤 TALKFIRST: set simulated wake word=${JSON.stringify(pendingWakeWordConfirmationText)} and try doStart() directly (WITHOUT a gesture)`,
"color:#2563eb;font-weight:700;background:#dbeafe;padding:3px 6px;border-radius:4px;"
);
try {
setCaption("Preparing your session…", "muted");
// Non forzo AudioContext da fuori (non c'è gesture): lo crea internamente
// _ensureAudioContext; se rimane suspended il client lo resume() quando
// arriva il primo audio (o sul prossimo click utente che lo unlocka).
// Let _ensureAudioContext create the context. If it stays suspended, the
// client resumes it on the first audio or the next user click.
await doStart();
} catch (err) {
console.warn("[start-session] ⚠️ TALKFIRST doStart() diretto FALLITO → fallback unlocker", err && err instanceof Error ? err.message : err);
// Fallback: mostro unlocker con la frase esplicativa "permesso mic".
console.warn("[start-session] ⚠️ direct TALKFIRST doStart() FAILED → fallback unlocker", err && err instanceof Error ? err.message : err);
// Show the unlocker with the microphone permission explanation.
try { await teardown(); } catch (_) { /* no-op */ }
setState("idle");
chat.renderEmptyState();
// Ripristino pendingWakeWordConfirmationText: nel caso l'utente usi
// l'unlocker per avviare, NON vogliamo perdere la frase simulata (la
// useremo di nuovo al connected, scattando quindi sempre la greet).
// Preserve the simulated phrase so starting through the unlocker still
// injects it on connection and triggers the greeting.
pendingWakeWordConfirmationText = pendingWakeWordConfirmationText || "hola vocero";
showTalkUnlocker(true);
void handleStartError(err instanceof Error ? err : new Error(String(err || "mic permission required")));
}
}
// Attacca una sola volta i listener sull'unlocker: click + touchstart = unlock
// Register unlocker listeners once: click + touchstart unlock audio.
(() => {
const el = document.getElementById("talk-unlocker");
if (!el) return;
@@ -3692,28 +3673,28 @@ async function tryTalkFirstDirect() {
window.addEventListener("keydown", (ev) => {
if (startSessionMode !== "TALK") return;
if (!_talkUnlockerShown) return;
// Qualsiasi tasto = unlock (Enter/Space/anche Altri)
// Any key unlocks audio, including Enter and Space.
startTalkFromGesture();
}, { passive: true });
})();
/** Avvia doStart() dentro una gesture valida (click/touch/key).
Chiamato dal talk-unlocker o da qualsiasi altro unlocker valido. */
/** Start doStart() within a valid click/touch/key gesture.
* Called by the talk unlocker or another valid audio unlocker. */
async function startTalkFromGesture() {
hideTalkUnlocker();
if (!(currentState === "idle" || currentState === "error")) return;
if (missingServerUrl()) {
console.warn("[start-session] skip unlocker: manca server URL (missingServerUrl=true)");
console.warn("[start-session] skip unlocker: missing server URL (missingServerUrl=true)");
promptServerUrl();
return;
}
try {
// Qui SIAMO dentro la user gesture: AudioContext + getUserMedia funzionano.
console.info("[start-session] 🎤 avvio doStart() da gesture unlocker (TALK mode)");
// This user gesture permits AudioContext and getUserMedia to start.
console.info("[start-session] 🎤 start doStart() from unlocker gesture (TALK mode)");
const audioContext = createResumedAudioContext();
setCaption("Preparing your session…", "muted");
await doStart(audioContext);
} catch (err) {
console.error("[start-session] ⛔ doStart() da unlocker fallito", err);
console.error("[start-session] ⛔ doStart() from unlocker failed", err);
void handleStartError(err);
}
}