WIP: mejoras de fidelidad, editor, lógica y suite e2e (checkpoint tras reinicio)

This commit is contained in:
urieljareth
2026-09-27 20:46:51 -06:00
parent dc6e31a9d7
commit 9a5f21badf
33 changed files with 7719 additions and 1129 deletions
+175 -26
View File
@@ -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);