WIP: mejoras de fidelidad, editor, lógica y suite e2e (checkpoint tras reinicio)
This commit is contained in:
+175
-26
@@ -1,7 +1,8 @@
|
||||
/* 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 y la toolbar. Único script con ejecución al cargar. */
|
||||
de la demo (velocidad, reiniciar chat) y la toolbar. Único script con
|
||||
ejecución al cargar. */
|
||||
window.WAM = window.WAM || {};
|
||||
(function () {
|
||||
"use strict";
|
||||
@@ -15,23 +16,97 @@ window.WAM = window.WAM || {};
|
||||
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" ————————————————— */
|
||||
|
||||
function pintarBotonDemo() {
|
||||
var btn = porId("btn-reproducir");
|
||||
if (!btn) return;
|
||||
var activa = WAM.demoActiva();
|
||||
btn.setAttribute("aria-pressed", activa ? "true" : "false");
|
||||
var span = btn.querySelector("span");
|
||||
if (!span) return;
|
||||
if (!span.dataset.etiqueta) span.dataset.etiqueta = span.textContent;
|
||||
span.textContent = WAM.demoActiva() ? "■ Detener" : span.dataset.etiqueta;
|
||||
span.textContent = activa ? "■ Detener" : span.dataset.etiqueta;
|
||||
}
|
||||
|
||||
/* 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() {
|
||||
@@ -50,19 +125,25 @@ window.WAM = window.WAM || {};
|
||||
}
|
||||
|
||||
function reRenderTotal() {
|
||||
WAM.setTema(WAM.estado.tema);
|
||||
WAM.render();
|
||||
WAM.renderEditor();
|
||||
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 archivo = porId("importar-archivo");
|
||||
if (archivo) archivo.click();
|
||||
var abrir = function () {
|
||||
var archivo = porId("importar-archivo");
|
||||
if (archivo) archivo.click();
|
||||
};
|
||||
/* Hook opcional de js/ux.js: <dialog> con «Exportar antes» */
|
||||
if (typeof WAM.confirmarReemplazo === "function") { WAM.confirmarReemplazo("importar", abrir); return; }
|
||||
abrir();
|
||||
}
|
||||
|
||||
function alArchivoImport(e) {
|
||||
@@ -71,15 +152,29 @@ window.WAM = window.WAM || {};
|
||||
input.value = ""; /* permite reimportar el mismo archivo */
|
||||
if (!archivo || typeof WAM.importarJSON !== "function") return;
|
||||
WAM.detenerDemo();
|
||||
var promesa = WAM.importarJSON(archivo);
|
||||
var promesa;
|
||||
try { promesa = WAM.importarJSON(archivo); } catch (err) { promesa = Promise.reject(err); }
|
||||
if (promesa && typeof promesa.then === "function") {
|
||||
promesa.then(reRenderTotal, reRenderTotal);
|
||||
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: <dialog> 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();
|
||||
@@ -87,39 +182,76 @@ window.WAM = window.WAM || {};
|
||||
|
||||
/* ————————————————— 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 <a href="#"> */
|
||||
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 indice = parseInt(el.getAttribute("data-flujo"), 10);
|
||||
var msg = (!isNaN(indice) && WAM.estado && WAM.estado.mensajes) ? WAM.estado.mensajes[indice] : null;
|
||||
var plano = ds.btn === undefined && ds.sec === undefined &&
|
||||
ds.fila === undefined && ds.card === undefined;
|
||||
var msg = WAM.mensajeDeElemento(el);
|
||||
var plano = ds.btn === undefined && ds.sec === undefined && ds.card === undefined;
|
||||
|
||||
if (plano && msg && msg.tipo === "list") {
|
||||
if (typeof WAM.abrirSheet === "function") WAM.abrirSheet(indice);
|
||||
if (plano) {
|
||||
if (msg && msg.tipo === "list") WAM.abrirLista(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
var flujo = WAM.resolverFlujo(el);
|
||||
if (flujo && flujo.texto) WAM.flujo(flujo.texto, flujo.respuestas);
|
||||
if (!flujo || !flujo.msg) return;
|
||||
WAM.marcarUsado(el); /* gris + bloquea el doble clic */
|
||||
WAM.flujo(flujo.texto, flujo.respuestas, flujo.cita);
|
||||
}
|
||||
|
||||
/* Opciones del sheet de lista (vive en #pantalla, fuera de #chat-cuerpo). */
|
||||
/* Opciones y cierre de la hoja de lista (vive en #pantalla, fuera de #chat-cuerpo). */
|
||||
function alClicPantalla(e) {
|
||||
var objetivo = e.target;
|
||||
var fila = (objetivo && objetivo.closest) ? objetivo.closest("[data-fila]") : null;
|
||||
if (!fila) return;
|
||||
if (!objetivo || !objetivo.closest) return;
|
||||
var hoja = porId("sheet");
|
||||
if (!hoja || !hoja.contains(fila)) return;
|
||||
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);
|
||||
if (typeof WAM.cerrarSheet === "function") WAM.cerrarSheet();
|
||||
if (flujo && flujo.texto) WAM.flujo(flujo.texto, flujo.respuestas);
|
||||
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 ————————————————— */
|
||||
@@ -137,17 +269,34 @@ window.WAM = window.WAM || {};
|
||||
|
||||
/* ————————————————— 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.estado = WAM.cargar();
|
||||
WAM.setTema(WAM.estado.tema);
|
||||
WAM.render();
|
||||
WAM.renderEditor();
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user