refactor: standardize developer text and identifiers in English
This commit is contained in:
+144
-163
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user