/* WAM · main.js — dueño D (núcleo). Bootstrap: carga el estado, aplica tema, renderiza chat y editor, y enlaza la delegación de clics del chat, el composer, la reproducción de la demo (velocidad, reiniciar chat) y la toolbar. Único script con ejecución al cargar. */ window.WAM = window.WAM || {}; (function () { "use strict"; function porId(id) { return document.getElementById(id); } function al(id, evento, manejador) { var el = porId(id); if (el) el.addEventListener(evento, manejador); } /* ————————————————— aviso visible (toast) ————————————————— */ /* Aviso flotante accesible. Estilos por CSSOM (compatibles con una CSP style-src 'self'); no depende de ningún CSS ajeno. */ var avisoTimer = null; WAM.avisar = function (texto, opciones) { var op = opciones || {}; var caja = porId("wam-aviso"); if (!caja) { caja = document.createElement("div"); caja.id = "wam-aviso"; caja.setAttribute("role", "alert"); caja.setAttribute("aria-live", "assertive"); var s = caja.style; s.position = "fixed"; s.left = "50%"; s.bottom = "20px"; s.transform = "translateX(-50%)"; s.zIndex = "9999"; s.maxWidth = "min(560px, calc(100vw - 32px))"; s.padding = "12px 40px 12px 16px"; s.borderRadius = "12px"; s.font = "500 14px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif"; s.boxShadow = "0 8px 28px rgba(0,0,0,.28)"; var cerrar = document.createElement("button"); cerrar.type = "button"; cerrar.setAttribute("aria-label", "Cerrar aviso"); cerrar.textContent = "×"; var c = cerrar.style; c.position = "absolute"; c.top = "6px"; c.right = "8px"; c.border = "0"; c.background = "transparent"; c.color = "inherit"; c.font = "600 20px/1 Arial, sans-serif"; c.cursor = "pointer"; cerrar.addEventListener("click", function () { caja.hidden = true; }); var txt = document.createElement("span"); txt.className = "wam-aviso-texto"; caja.appendChild(txt); caja.appendChild(cerrar); document.body.appendChild(caja); } var error = op.tipo !== "info"; caja.style.background = error ? "#b3261e" : "#1f2c33"; caja.style.color = "#fff"; caja.querySelector(".wam-aviso-texto").textContent = String(texto == null ? "" : texto); caja.hidden = false; if (avisoTimer) clearTimeout(avisoTimer); avisoTimer = null; if (op.duracion) { avisoTimer = setTimeout(function () { caja.hidden = true; }, op.duracion); } }; /* ————————————————— botón "Reproducir demo" ————————————————— */ var SVG_STOP = ''; /* Contador discreto «Paso 3/12» junto al botón (se crea una vez). */ function contadorDemo(btn) { var c = porId("demo-paso"); if (c) return c; c = document.createElement("span"); c.id = "demo-paso"; c.className = "demo-paso"; c.setAttribute("aria-live", "polite"); c.setAttribute("role", "status"); c.hidden = true; /* estilo mínimo en línea (tabular) mientras no haya regla en iphone.css */ c.style.fontSize = "12px"; c.style.fontVariantNumeric = "tabular-nums"; c.style.opacity = "0.75"; c.style.whiteSpace = "nowrap"; btn.parentNode.insertBefore(c, btn); return c; } function pintarBotonDemo() { var btn = porId("btn-reproducir"); if (!btn) return; var activa = WAM.demoActiva(); btn.setAttribute("aria-pressed", activa ? "true" : "false"); var svg = btn.querySelector("svg"); if (svg) { if (!svg.dataset.play) svg.dataset.play = svg.innerHTML; var deseado = activa ? SVG_STOP : svg.dataset.play; if (svg.innerHTML !== deseado) svg.innerHTML = deseado; } var span = btn.querySelector("span"); if (span) { if (!span.dataset.etiqueta) span.dataset.etiqueta = span.textContent; span.textContent = activa ? "Detener" : span.dataset.etiqueta; } if (btn.parentNode) { var c = contadorDemo(btn); var p = typeof WAM.progresoDemo === "function" ? WAM.progresoDemo() : null; if (activa && p && p.total > 0 && p.paso > 0) { var txt = "Paso " + p.paso + "/" + p.total; if (c.textContent !== txt) c.textContent = txt; c.hidden = false; } else { c.hidden = true; c.textContent = ""; } } } /* Evita que un doble clic arranque y detenga la demo en el mismo gesto. */ var ultimoReproducir = 0; function alReproducir() { var ahora = Date.now(); if (ahora - ultimoReproducir < 350) return; ultimoReproducir = ahora; if (WAM.demoActiva()) WAM.detenerDemo(); else WAM.reproducirDemo(); pintarBotonDemo(); } function alReiniciarChat() { if (typeof WAM.reiniciarChat === "function") WAM.reiniciarChat(); else WAM.render(); pintarBotonDemo(); } function alVelocidad(e) { var v = WAM.setVelocidad(e.currentTarget.value); e.currentTarget.value = String(v); } /* ————————————————— toolbar ————————————————— */ function alTema() { WAM.estado.tema = WAM.estado.tema === "oscuro" ? "claro" : "oscuro"; WAM.guardar(); WAM.setTema(WAM.estado.tema); } function alPresentacion() { document.body.classList.toggle("presentacion"); var btn = porId("btn-presentacion"); if (btn) { btn.setAttribute("aria-pressed", document.body.classList.contains("presentacion") ? "true" : "false"); } } function reRenderTotal() { paso(function () { WAM.setTema(WAM.estado.tema); }); paso(function () { WAM.render(); }); paso(function () { WAM.renderEditor(); }); pintarBotonDemo(); } function alExportar() { if (typeof WAM.guardarYa === "function") WAM.guardarYa(); if (typeof WAM.exportarJSON === "function") WAM.exportarJSON(); } function alImportar() { var abrir = function () { var archivo = porId("importar-archivo"); if (archivo) archivo.click(); }; /* Hook opcional de js/ux.js: con «Exportar antes» */ if (typeof WAM.confirmarReemplazo === "function") { WAM.confirmarReemplazo("importar", abrir); return; } abrir(); } function alArchivoImport(e) { var input = e.currentTarget; var archivo = input.files && input.files[0]; input.value = ""; /* permite reimportar el mismo archivo */ if (!archivo || typeof WAM.importarJSON !== "function") return; WAM.detenerDemo(); var promesa; try { promesa = WAM.importarJSON(archivo); } catch (err) { promesa = Promise.reject(err); } if (promesa && typeof promesa.then === "function") { promesa.then(reRenderTotal, function () { reRenderTotal(); WAM.avisar("No se pudo importar: el archivo no es un JSON válido de la demo.", { duracion: 6000 }); }); } else { reRenderTotal(); } } function alReset() { var ejecutar = function () { WAM.detenerDemo(); WAM.resetear(); reRenderTotal(); }; /* Hook opcional de js/ux.js: con «Exportar antes» + deshacer */ if (typeof WAM.confirmarReemplazo === "function") { WAM.confirmarReemplazo("reset", ejecutar); return; } var nombre = (WAM.estado && WAM.estado.cuenta && WAM.estado.cuenta.nombre) || "la demo"; if (!window.confirm("¿Restablecer la demo «Café Aurora»? Se perderán los cambios no exportados de «" + nombre + "».")) return; WAM.detenerDemo(); WAM.resetear(); reRenderTotal(); } /* ————————————————— chat: delegación de clics (contrato B→D) ————————————————— */ /* Enlaces del chat con esquema no permitido (javascript:, data:…) no se abren. */ function bloquearEnlaceInseguro(e) { var a = (e.target && e.target.closest) ? e.target.closest("a[href]") : null; if (!a) return false; var href = a.getAttribute("href"); if (typeof WAM.urlSegura === "function" && !WAM.urlSegura(href, "enlace")) { e.preventDefault(); return true; } return false; } function alClicChat(e) { if (bloquearEnlaceInseguro(e)) return; var objetivo = e.target; var el = (objetivo && objetivo.closest) ? objetivo.closest("[data-flujo]") : null; if (!el) return; e.preventDefault(); /* protege frente a botones montados sobre */ var ds = el.dataset || {}; if (ds.fila !== undefined) return; /* filas del sheet: se atienden en #pantalla */ if (el.classList.contains("usado") || el.getAttribute("aria-disabled") === "true") return; var msg = WAM.mensajeDeElemento(el); var plano = ds.btn === undefined && ds.sec === undefined && ds.card === undefined; if (plano) { if (msg && msg.tipo === "list") WAM.abrirLista(msg); return; } var flujo = WAM.resolverFlujo(el); if (!flujo || !flujo.msg) return; WAM.marcarUsado(el); /* gris + bloquea el doble clic */ WAM.flujo(flujo.texto, flujo.respuestas, flujo.cita); } /* Opciones y cierre de la hoja de lista (vive en #pantalla, fuera de #chat-cuerpo). */ function alClicPantalla(e) { var objetivo = e.target; if (!objetivo || !objetivo.closest) return; var hoja = porId("sheet"); if (!hoja || !hoja.contains(objetivo)) return; if (objetivo.closest(".sheet-cerrar")) { e.preventDefault(); WAM.cerrarSheet(); return; } var fila = objetivo.closest("[data-fila]"); if (!fila) return; e.preventDefault(); var flujo = WAM.resolverFlujo(fila); WAM.cerrarSheet(); if (flujo && flujo.msg) WAM.flujo(flujo.texto, flujo.respuestas, flujo.cita); } /* Teclado: Enter / Espacio activan los botones del teléfono (span role=button); Escape cierra la hoja de lista. */ function alTeclaPantalla(e) { if (e.key === "Escape") { if (porId("sheet")) { e.preventDefault(); WAM.cerrarSheet(); } return; } if (e.key !== "Enter" && e.key !== " " && e.key !== "Spacebar") return; var t = e.target; if (!t || !t.closest || t.tagName === "A" || t.tagName === "BUTTON" || t.tagName === "INPUT" || t.tagName === "TEXTAREA") return; var el = t.closest("[data-flujo],[data-ver-todas],.sheet-cerrar"); if (!el || el !== t) return; e.preventDefault(); el.click(); } /* ————————————————— composer ————————————————— */ function alComposer(e) { e.preventDefault(); var input = porId("composer-input"); var texto = input ? input.value : ""; WAM.enviarLibre(texto); if (input) { input.value = ""; input.focus(); } } /* ————————————————— bootstrap ————————————————— */ /* Un fallo en un módulo (render o editor) no debe dejar sin enlazar el resto: cada paso del arranque se aísla. */ function paso(fn) { try { fn(); } catch (e) { setTimeout(function () { throw e; }, 0); /* visible en consola, sin cortar init */ } } WAM.init = function () { WAM.onErrorGuardar = function (texto) { WAM.avisar(texto); }; paso(function () { WAM.estado = WAM.cargar(); }); if (!WAM.estado) WAM.estado = WAM.normalizar(WAM.datosDemo()); paso(function () { WAM.setTema(WAM.estado.tema); }); paso(function () { WAM.render(); }); paso(function () { WAM.renderEditor(); }); WAM.onDemoChange = pintarBotonDemo; al("chat-cuerpo", "click", alClicChat); al("pantalla", "click", alClicPantalla); al("pantalla", "keydown", alTeclaPantalla); al("composer", "submit", alComposer); al("btn-reproducir", "click", alReproducir); al("btn-reiniciar", "click", alReiniciarChat); var vel = porId("sel-velocidad"); if (vel) { vel.value = String(WAM.getVelocidad()); vel.addEventListener("change", alVelocidad); } al("btn-tema", "click", alTema); al("btn-presentacion", "click", alPresentacion); al("btn-exportar", "click", alExportar); al("btn-importar", "click", alImportar); al("importar-archivo", "change", alArchivoImport); al("btn-reset", "click", alReset); pintarBotonDemo(); }; WAM.init(); })();