/* WAM · interactivity.js — dueño D (núcleo). Flujos de clic (quick replies, filas de lista, botones de plantilla y de card), composer libre, indicador "escribiendo…", ticks automáticos y modo "reproducir demo" paso a paso (con autoPulsar y velocidad). No toca el DOM al cargar. Guion frente a sesión: WAM.estado.mensajes es el GUION (persistente, lo que edita el editor y reproduce la demo). Lo que ocurre al pulsar botones o escribir en el composer es la SESIÓN (WAM.sesion, solo en memoria): se pinta con anadirMensajeDOM sin tocar el estado ni localStorage. Cualquier WAM.render() vuelve al guion: vacía la sesión, cierra la hoja de lista y cancela flujos y demo en curso. */ window.WAM = window.WAM || {}; (function () { "use strict"; var CANCELADO = "wam:cancelado"; var TIPOS_VALIDOS = [ "text", "image", "video", "audio", "document", "sticker", "location", "contact", "template", "buttons", "list", "cta_url", "carousel" ]; var CLAVE_VELOCIDAD = "wam-mockup-v1-velocidad"; var VELOCIDADES = [0.5, 1, 2]; function nada() { /* noop */ } /* Mensajes de la sesión en curso (efímeros). */ WAM.sesion = []; var contSesion = 0; /* IDs del guion (mensajes, respuestas, botones, filas, cards) para que un ID de sesión nunca coincida con uno importado (p. ej. un guion con "s1"). */ function idsDelGuion() { var usados = new Set(); (function recorrer(o) { if (!o || typeof o !== "object") return; if (Array.isArray(o)) { for (var i = 0; i < o.length; i++) recorrer(o[i]); return; } if (typeof o.id === "string" && o.id) usados.add(o.id); ["mensajes", "respuestas", "botones", "secciones", "filas", "cards"].forEach(function (k) { if (o[k] && typeof o[k] === "object") recorrer(o[k]); }); })(WAM.estado); return usados; } function idSesion() { var usados = WAM.estado ? idsDelGuion() : null; var id; do { contSesion++; id = "s" + contSesion; } while (usados && usados.has(id)); return id; } /* Hora para los mensajes de un flujo: la del último mensaje visible + 1 min, para que la línea temporal de la demo siga siendo coherente (si no hay ninguna hora legible, el reloj real). */ function horaTras() { var metas = document.querySelectorAll("#chat-cuerpo .wa-row .meta"); var t = null; for (var i = metas.length - 1; i >= 0 && !t; i--) { t = String(metas[i].textContent || "").match(/(\d{1,2}):(\d{2})/); } if (!t) return WAM.horaAhora(); var min = (Number(t[1]) * 60 + Number(t[2]) + 1) % 1440; return ("0" + Math.floor(min / 60)).slice(-2) + ":" + ("0" + (min % 60)).slice(-2); } /* ————————————————— temporizadores cancelables por grupo ————————————————— */ /* Dos grupos: "demo" (reproducción) y "flujo" (clics, composer, ticks). Cancelar un grupo sube su generación: los pasos pendientes de esa generación se rechazan con CANCELADO y ya no pueden programar más. */ var grupos = { demo: { gen: 0, cancelar: [] }, flujo: { gen: 0, cancelar: [] } }; function grupoDe(nombre) { return grupos[nombre] ? nombre : "flujo"; } var velocidad = 1; try { var guardada = parseFloat(window.localStorage.getItem(CLAVE_VELOCIDAD)); if (VELOCIDADES.indexOf(guardada) !== -1) velocidad = guardada; } catch (e) { /* sin localStorage */ } WAM.getVelocidad = function () { return velocidad; }; WAM.setVelocidad = function (v) { var n = parseFloat(v); if (VELOCIDADES.indexOf(n) === -1) n = 1; velocidad = n; try { window.localStorage.setItem(CLAVE_VELOCIDAD, String(n)); } catch (e) { /* idem */ } return velocidad; }; function esperaEn(nombre, ms, gen) { var g = grupos[grupoDe(nombre)]; return new Promise(function (resolver, rechazar) { if (gen !== undefined && gen !== g.gen) { rechazar(CANCELADO); return; } var dur = Math.max(0, (Number(ms) || 0) / velocidad); var id = setTimeout(function () { quitar(); if (gen !== undefined && gen !== g.gen) rechazar(CANCELADO); else resolver(); }, dur); function cancelar() { clearTimeout(id); quitar(); rechazar(CANCELADO); } function quitar() { var i = g.cancelar.indexOf(cancelar); if (i !== -1) g.cancelar.splice(i, 1); } g.cancelar.push(cancelar); }); } /* Promise cancelable (SPEC): todo temporizador pasa por aquí. grupo opcional: "flujo" (por defecto) o "demo". La duración se divide por la velocidad elegida (0,5x / 1x / 2x). */ WAM.espera = function (ms, grupo) { return esperaEn(grupo, ms); }; /* Contexto de una secuencia: fija grupo y generación al empezar. */ function contexto(nombre) { var g = grupoDe(nombre); var gen = grupos[g].gen; return { grupo: g, vivo: function () { return grupos[g].gen === gen; }, esperar: function (ms) { return esperaEn(g, ms, gen); } }; } var colas = { demo: Promise.resolve(), flujo: Promise.resolve() }; function cancelarGrupo(nombre) { var g = grupos[grupoDe(nombre)]; g.gen++; var copia = g.cancelar.slice(); g.cancelar = []; for (var i = 0; i < copia.length; i++) copia[i](); colas[grupoDe(nombre)] = Promise.resolve(); } function cancelarTodo() { cancelarGrupo("demo"); cancelarGrupo("flujo"); escritores.clear(); pintarEscribiendo(); } /* ————————————————— indicador "escribiendo…" (contador de referencias) ————————————————— */ var escritores = new Set(); function pintarEscribiendo() { if (typeof WAM.setEscribiendo === "function") WAM.setEscribiendo(escritores.size > 0); } function empezarEscribir() { var ficha = {}; escritores.add(ficha); pintarEscribiendo(); return ficha; } function dejarEscribir(ficha) { if (escritores.delete(ficha)) pintarEscribiendo(); } async function escribirDurante(ctx, ms) { var ficha = empezarEscribir(); try { await ctx.esperar(ms); } finally { dejarEscribir(ficha); } } /* ————————————————— estado de la demo ————————————————— */ var demoAndando = false; WAM.demoActiva = function () { return demoAndando; }; function avisarDemo() { if (typeof WAM.onDemoChange === "function") { try { WAM.onDemoChange(demoAndando); } catch (e) { /* el hook es opcional */ } } } /* ————————————————— búsqueda de mensajes y filas ————————————————— */ function cuerpoChat() { return document.getElementById("chat-cuerpo"); } function selectorId(id) { return '.wa-row[data-id="' + String(id).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"]'; } function filaDe(msg) { var cuerpo = cuerpoChat(); if (!cuerpo || !msg || typeof msg.id !== "string" || !msg.id) return null; var filas = cuerpo.querySelectorAll(selectorId(msg.id)); return filas.length ? filas[filas.length - 1] : null; } function buscarPorId(id) { if (!id) return null; var guion = (WAM.estado && WAM.estado.mensajes) || []; for (var i = 0; i < guion.length; i++) if (guion[i] && guion[i].id === id) return guion[i]; for (var j = WAM.sesion.length - 1; j >= 0; j--) if (WAM.sesion[j] && WAM.sesion[j].id === id) return WAM.sesion[j]; return null; } /* Mensaje de la hoja de lista abierta (guion o sesión). */ var hojaMsg = null; /* Mensaje al que pertenece un elemento clicable del chat o de la hoja. Se resuelve por el data-id de su fila (inmune a índices obsoletos y válido para mensajes de sesión); el índice data-flujo queda de respaldo. */ WAM.mensajeDeElemento = function (el) { if (!el || !el.closest) return null; var hoja = el.closest("#sheet"); if (hoja) return hojaMsg; var fila = el.closest(".wa-row[data-id]"); var msg = fila ? buscarPorId(fila.getAttribute("data-id")) : null; if (msg) return msg; var indice = parseInt(el.getAttribute("data-flujo"), 10); var guion = (WAM.estado && WAM.estado.mensajes) || []; return isNaN(indice) ? null : (guion[indice] || null); }; /* Abre la hoja de lista de un mensaje (del guion o de la sesión). */ WAM.abrirLista = function (msg) { if (!msg || typeof WAM.abrirSheet !== "function") return; hojaMsg = msg; var guion = (WAM.estado && WAM.estado.mensajes) || []; var i = guion.indexOf(msg); WAM.abrirSheet(i >= 0 ? i : msg); }; /* ————————————————— pintado incremental ————————————————— */ /* grupo / cola (.grupo / .ultimo) los recalcula anadirMensajeDOM según el DOM. */ function pintar(msg) { if (typeof WAM.anadirMensajeDOM !== "function") return null; WAM.anadirMensajeDOM(msg); var fila = filaDe(msg); if (fila && msg.cita) pintarCita(fila, msg.cita); return fila; } function anadirSesion(msg) { WAM.sesion.push(msg); return pintar(msg); } function mensajeOut(texto, cita, hora) { var msg = { id: idSesion(), dir: "out", tipo: "text", hora: hora || WAM.horaAhora(), estado: "sent", texto: String(texto == null ? "" : texto) }; if (cita) msg.cita = cita; return msg; } /* Copia una respuesta anidada (Mensaje completo) para poder reutilizarla cada vez que se vuelve a pulsar el mismo botón. */ function mensajeDesde(origen) { var copia; try { copia = JSON.parse(JSON.stringify( (origen && typeof origen === "object") ? origen : { tipo: "text", texto: String(origen == null ? "" : origen) } )); } catch (e) { copia = { tipo: "text", texto: "" }; } if (!copia || typeof copia !== "object" || Array.isArray(copia)) copia = { tipo: "text", texto: "" }; if (TIPOS_VALIDOS.indexOf(copia.tipo) === -1) copia.tipo = "text"; copia.id = idSesion(); copia.dir = "in"; copia.hora = horaTras(); copia.estado = "sent"; delete copia.cita; return copia; } /* ————————————————— cita de la respuesta (quoted reply) ————————————————— */ function vacio(v) { return v === undefined || v === null || String(v).trim() === ""; } function sinFormato(s) { return String(s == null ? "" : s) .replace(/```([\s\S]*?)```/g, "$1") .replace(/`([^`\n]+)`/g, "$1") .replace(reMarca("\*"), "$1$2") .replace(reMarca("_"), "$1$2") .replace(reMarca("~"), "$1$2") .trim(); } /* Misma regla que render.js: marca pegada a un carácter no blanco por dentro y sin letra, número ni _ por fuera («2 * 3 * 4» queda literal). */ function reMarca(e) { try { return new RegExp("(^|[^\p{L}\p{N}_" + e + "])" + e + "(?=\S)([^" + e + "\n]*?\S)" + e + "(?=[^\p{L}\p{N}_" + e + "]|$)", "gu"); } catch (err) { return new RegExp("(^|[^\w" + e + "])" + e + "(?=\S)([^" + e + "\n]*?\S)" + e + "(?=[^\w" + e + "]|$)", "g"); } } function miniaturaSegura(u) { if (vacio(u)) return ""; var s = String(u); if (/^data:video\//i.test(s)) return ""; if (typeof WAM.urlSegura === "function" && !WAM.urlSegura(s, "media")) return ""; return s; } /* { nombre, titulo, extracto, miniatura, foto } del mensaje original. */ function citaDe(msg, opciones) { if (!msg) return null; var cuenta = (WAM.estado && WAM.estado.cuenta) || {}; var cita = { nombre: vacio(cuenta.nombre) ? "Chat" : String(cuenta.nombre), titulo: "", extracto: "", miniatura: "", foto: false }; var op = opciones || {}; var partes = []; if (msg.tipo === "list") { cita.titulo = sinFormato(msg.header); partes = [msg.body, msg.footer]; } else if (msg.tipo === "carousel") { var card = (msg.cards || [])[op.card] || {}; partes = [vacio(card.texto) ? msg.body : card.texto]; cita.miniatura = miniaturaSegura(card.media); if (!cita.miniatura && vacio(card.media) && typeof WAM.svgPlaceholder === "function") { cita.miniatura = WAM.svgPlaceholder(""); } cita.foto = true; } else { if (msg.headerTipo === "texto") cita.titulo = sinFormato(msg.headerTexto); partes = [msg.body]; if (msg.headerTipo === "imagen") { cita.miniatura = vacio(msg.headerMedia) ? (typeof WAM.svgPlaceholder === "function" ? WAM.svgPlaceholder("") : "") : miniaturaSegura(msg.headerMedia); cita.foto = true; } else if (msg.headerTipo === "video") { cita.foto = true; } else if (msg.headerTipo === "documento") { cita.titulo = vacio(msg.headerTexto) ? "documento.pdf" : String(msg.headerTexto); cita.doc = true; } } cita.extracto = partes.filter(function (p) { return !vacio(p); }).map(sinFormato).join("\n"); if (!cita.extracto && !cita.titulo) cita.extracto = cita.foto ? "Foto" : "Mensaje"; return cita; } var SVG_CAMARA = ''; var SVG_DOC = ''; /* Bloque de cita dentro de la burbuja OUT; todo por textContent (sin XSS). */ function pintarCita(fila, cita) { var burbuja = fila.querySelector(".bubble"); if (!burbuja || burbuja.querySelector(".wa-cita")) return; var caja = document.createElement("div"); caja.className = "wa-cita"; var txt = document.createElement("div"); txt.className = "wa-cita-txt"; var nombre = document.createElement("div"); nombre.className = "wa-cita-nombre"; nombre.textContent = cita.nombre; txt.appendChild(nombre); if (cita.titulo) { var tit = document.createElement("div"); tit.className = "wa-cita-titulo"; if (cita.doc) { var doc = document.createElement("span"); doc.innerHTML = SVG_DOC; /* constante propia, sin datos del usuario */ if (doc.firstChild) tit.appendChild(doc.firstChild); } tit.appendChild(document.createTextNode(cita.titulo)); txt.appendChild(tit); } if (cita.extracto) { var ext = document.createElement("div"); ext.className = "wa-cita-extracto"; if (cita.foto) { var cam = document.createElement("span"); cam.innerHTML = SVG_CAMARA; /* constante propia, sin datos del usuario */ if (cam.firstChild) ext.appendChild(cam.firstChild); } ext.appendChild(document.createTextNode(cita.extracto)); txt.appendChild(ext); } caja.appendChild(txt); if (cita.miniatura) { var img = document.createElement("img"); img.className = "wa-cita-mini"; img.alt = ""; img.draggable = false; img.src = cita.miniatura; caja.appendChild(img); } burbuja.insertBefore(caja, burbuja.firstChild); burbuja.classList.add("wa-con-cita"); } /* ————————————————— ticks automáticos ————————————————— */ /* Ticks sent → delivered → read sobre la burbuja OUT indicada (por data-id, para que dos envíos solapados avancen cada uno su propio tick). */ function repintarTickDe(msg, estado) { if (typeof WAM.tick !== "function") return; var fila = filaDe(msg); if (!fila || !fila.classList.contains("out")) return; var tick = fila.querySelector(".tick"); if (tick && tick.parentNode) { /* WAM.tick devuelve un completo: se REEMPLAZA el nodo. */ var aux = document.createElement("span"); aux.innerHTML = WAM.tick(estado); if (aux.firstChild) tick.parentNode.replaceChild(aux.firstChild, tick); } } /* Los mensajes de sesión no se guardan; si se cancelan los ticks, la burbuja salta a "read" para no quedar congelada en ✓ / ✓✓ gris. */ async function animarTics(msg, ctx) { try { await ctx.esperar(800); msg.estado = "delivered"; repintarTickDe(msg, "delivered"); await ctx.esperar(800); msg.estado = "read"; repintarTickDe(msg, "read"); } catch (e) { if (msg.estado !== "read") { msg.estado = "read"; repintarTickDe(msg, "read"); } throw e; } } /* ————————————————— flujos ————————————————— */ /* Respuestas encadenadas: typing ~900 ms + burbuja IN por cada una. En la demo (enDemo) cada respuesta con autoPulsar se pulsa sola. */ async function responder(lista, ctx, enDemo) { for (var i = 0; i < lista.length; i++) { await escribirDurante(ctx, 900); if (!ctx.vivo()) throw CANCELADO; var r = mensajeDesde(lista[i]); anadirSesion(r); if (enDemo) await autoPulsar(r, ctx); } } /* Burbuja OUT con el texto (y cita opcional {nombre, extracto, miniatura}), ticks auto y respuestas. La burbuja sale al instante; las respuestas de flujos simultáneos se encolan para no intercalarse. Cancelable. */ WAM.flujo = function (texto, respuestas, cita) { var lista = Array.isArray(respuestas) ? respuestas : []; var ctx = contexto("flujo"); var out = mensajeOut(texto, cita || null, horaTras()); anadirSesion(out); var tics = animarTics(out, ctx); tics.catch(nada); var turno = colas.flujo.then(function () { if (!ctx.vivo()) throw CANCELADO; return tics.then(function () { return responder(lista, ctx, false); }); }); colas.flujo = turno.catch(nada); return turno.catch(nada); }; /* Envío libre del composer: burbuja OUT con hora actual y ticks automáticos. */ WAM.enviarLibre = function (texto) { var limpio = String(texto == null ? "" : texto).trim(); if (!limpio) return; var out = mensajeOut(limpio); anadirSesion(out); animarTics(out, contexto("flujo")).catch(nada); }; /* Resuelve { texto, respuestas, cita, msg, tipo } desde los data-* que estampa render.js: data-btn (template/buttons), data-sec + data-fila (lista) o data-card (carousel). Etiquetas vacías = las del render. */ WAM.resolverFlujo = function (el) { var vacioR = { texto: "", respuestas: [], cita: null, msg: null, tipo: "" }; if (!el || typeof el.getAttribute !== "function") return vacioR; var msg = WAM.mensajeDeElemento(el); if (!msg) return vacioR; var ds = el.dataset || {}; if (ds.sec !== undefined || ds.fila !== undefined) { var seccion = (msg.secciones || [])[parseInt(ds.sec, 10)]; var fila = ((seccion && seccion.filas) || [])[parseInt(ds.fila, 10)]; if (!fila) return vacioR; var titulo = vacio(fila.titulo) ? "Opción" : String(fila.titulo); return { texto: titulo + (vacio(fila.descripcion) ? "" : "\n" + fila.descripcion), respuestas: fila.respuestas || [], cita: citaDe(msg), msg: msg, tipo: "fila" }; } if (ds.btn !== undefined) { var n = parseInt(ds.btn, 10); var boton = (msg.botones || [])[n]; /* botón de enlace con URL bloqueada: inerte, nunca un quick reply */ if (!boton || boton.urlBloqueada) return vacioR; return { texto: vacio(boton.texto) ? "Botón " + (n + 1) : String(boton.texto), respuestas: boton.respuestas || [], cita: citaDe(msg), msg: msg, tipo: "boton" }; } if (ds.card !== undefined) { var c = parseInt(ds.card, 10); var card = (msg.cards || [])[c]; if (!card || card.urlBloqueada) return vacioR; return { texto: vacio(card.textoBoton) ? "Ver más" : String(card.textoBoton), respuestas: card.respuestas || [], cita: citaDe(msg, { card: c }), msg: msg, tipo: "card" }; } /* Elemento plano de un mensaje list: es el botón que abre la hoja. */ return vacioR; }; /* Deja el botón pulsado en gris y sin respuesta a más clics (como iOS). */ WAM.marcarUsado = function (el) { if (!el || !el.classList) return; el.classList.add("usado"); el.setAttribute("aria-disabled", "true"); el.setAttribute("tabindex", "-1"); }; /* Guarda la sesión actual como parte del guion (acción explícita). */ WAM.sesionAGuion = function () { if (!WAM.sesion.length || !WAM.estado) return 0; var tomar = typeof WAM.crearAlocador === "function" ? WAM.crearAlocador() : WAM.generarId; var nuevos = WAM.sesion.map(function (m) { var copia = JSON.parse(JSON.stringify(m)); delete copia.cita; (function reasignar(o) { if (!o || typeof o !== "object") return; if (Array.isArray(o)) { o.forEach(reasignar); return; } if (typeof o.id === "string") o.id = tomar(); ["respuestas", "botones", "secciones", "filas", "cards"].forEach(function (k) { reasignar(o[k]); }); })(copia); return copia; }); var n = nuevos.length; if (typeof WAM.puntoDeshacer === "function") { try { WAM.puntoDeshacer(); } catch (e) { /* opcional */ } } WAM.estado = WAM.normalizar ? WAM.normalizar({ version: WAM.estado.version, tema: WAM.estado.tema, cuenta: WAM.estado.cuenta, mensajes: WAM.estado.mensajes.concat(nuevos) }) : WAM.estado; if (typeof WAM.guardar === "function") WAM.guardar(); if (typeof WAM.marcarEditado === "function") { try { WAM.marcarEditado(); } catch (e) { /* opcional */ } } if (typeof WAM.render === "function") WAM.render(); if (typeof WAM.renderEditor === "function") WAM.renderEditor(); return n; }; /* ————————————————— reproducción de la demo ————————————————— */ function pulsacion(el, ctx) { el.classList.add("wam-pulsando"); return ctx.esperar(260).then(function () { el.classList.remove("wam-pulsando"); }, function (e) { el.classList.remove("wam-pulsando"); throw e; }); } function centrarCard(el) { var car = el.closest(".carousel"); var card = el.closest(".card"); if (!car || !card) return; var delta = card.getBoundingClientRect().left - car.getBoundingClientRect().left; try { car.scrollTo({ left: car.scrollLeft + delta - 8, behavior: "smooth" }); } catch (e) { car.scrollLeft += delta - 8; } } /* Pulsa sola la opción msg.autoPulsar (índice de botón, card o fila aplanada de la lista) 1 s después de que aparezca el mensaje. */ async function autoPulsar(msg, ctx) { if (!msg || ["template", "buttons", "list", "carousel"].indexOf(msg.tipo) === -1) return; var idx = msg.autoPulsar; if (idx === null || idx === undefined || idx === "") return; idx = Number(idx); if (!isFinite(idx) || idx < 0) return; var fila = filaDe(msg); if (!fila) return; await ctx.esperar(1000); var el = null; if (msg.tipo === "list") { var s, f, cuenta = 0, destino = null; var secs = msg.secciones || []; for (s = 0; s < secs.length && !destino; s++) { var filas = (secs[s] && secs[s].filas) || []; for (f = 0; f < filas.length; f++) { if (cuenta === idx) { destino = [s, f]; break; } cuenta++; } } if (!destino) return; var abridor = fila.querySelector("[data-flujo]:not([data-btn]):not([data-card])"); if (abridor) await pulsacion(abridor, ctx); WAM.abrirLista(msg); await ctx.esperar(900); var hoja = document.getElementById("sheet"); el = hoja ? hoja.querySelector('[data-sec="' + destino[0] + '"][data-fila="' + destino[1] + '"]') : null; if (!el) { if (WAM.cerrarSheet) WAM.cerrarSheet(); return; } await pulsacion(el, ctx); var selL = WAM.resolverFlujo(el); if (WAM.cerrarSheet) WAM.cerrarSheet(); await flujoEnDemo(selL, ctx); return; } el = fila.querySelector(msg.tipo === "carousel" ? '[data-card="' + idx + '"]' : '[data-btn="' + idx + '"]'); if (!el) return; if (el.hidden) { var plegado = fila.querySelector("[data-ver-todas]"); if (plegado) { plegado.click(); await ctx.esperar(400); } /* B abre la hoja "Todas las opciones" (render.js): se pulsa su fila */ var enHoja = document.querySelector('#sheet [data-opcion="' + idx + '"]'); if (!enHoja) { if (WAM.cerrarSheet) WAM.cerrarSheet(); return; } await pulsacion(enHoja, ctx); if (WAM.cerrarSheet) WAM.cerrarSheet(); } if (msg.tipo === "carousel") { centrarCard(el); await ctx.esperar(500); } await pulsacion(el, ctx); var sel = WAM.resolverFlujo(el); WAM.marcarUsado(el); await flujoEnDemo(sel, ctx); } async function flujoEnDemo(sel, ctx) { if (!sel || !sel.msg) return; var out = mensajeOut(sel.texto, sel.cita, horaTras()); anadirSesion(out); await animarTics(out, ctx); await responder(Array.isArray(sel.respuestas) ? sel.respuestas : [], ctx, true); } function vaciarChat() { var cuerpo = cuerpoChat(); if (!cuerpo) return; var filas = cuerpo.querySelectorAll(".wa-row"); for (var i = 0; i < filas.length; i++) { if (filas[i].id !== "chat-escribiendo") cuerpo.removeChild(filas[i]); } } /* Progreso de la demo: mensajes del guion revelados / total. */ var progreso = { paso: 0, total: 0 }; WAM.progresoDemo = function () { return demoAndando ? { paso: progreso.paso, total: progreso.total } : { paso: 0, total: 0 }; }; WAM.reproducirDemo = function () { if (demoAndando) return; /* aborta flujos, ticks y typing en curso: no se mezclan con la demo */ cancelarTodo(); if (typeof WAM.cerrarSheet === "function") WAM.cerrarSheet(); hojaMsg = null; WAM.sesion.length = 0; demoAndando = true; avisarDemo(); vaciarChat(); var ctx = contexto("demo"); var secuencia = ((WAM.estado && WAM.estado.mensajes) || []).slice(); progreso.paso = 0; progreso.total = secuencia.length; (async function () { try { for (var i = 0; i < secuencia.length; i++) { var msg = secuencia[i]; if (!msg) continue; if (msg.dir === "in") await escribirDurante(ctx, 700); if (!ctx.vivo()) throw CANCELADO; pintar(msg); progreso.paso = i + 1; avisarDemo(); await ctx.esperar(600); await autoPulsar(msg, ctx); } } catch (e) { /* cancelado: detenerDemo() / render() ya dejaron el chat coherente */ } if (ctx.vivo()) { demoAndando = false; avisarDemo(); } })(); }; WAM.detenerDemo = function () { var estaba = demoAndando; demoAndando = false; cancelarGrupo("demo"); /* solo el grupo de la demo */ if (estaba) { /* vuelve al guion completo (y descarta la sesión: ver reiniciarInteraccion) */ if (typeof WAM.render === "function") WAM.render(); avisarDemo(); } }; /* Vuelve al guion: cancela todo, vacía la sesión y cierra la hoja. */ function reiniciarInteraccion() { var estaba = demoAndando; demoAndando = false; cancelarTodo(); WAM.sesion.length = 0; hojaMsg = null; if (typeof WAM.cerrarSheet === "function") WAM.cerrarSheet(); if (estaba) avisarDemo(); } WAM.reiniciarChat = function () { if (typeof WAM.render === "function") WAM.render(); }; /* WAM.render() repinta el guion entero: antes se descarta la sesión y se cancelan demo y flujos para que nada añada filas sobre el repintado (evita data-id duplicados y la hoja con índices obsoletos). */ if (typeof WAM.render === "function" && !WAM.render.__wamEnvuelto) { var renderBase = WAM.render; WAM.render = function () { reiniciarInteraccion(); return renderBase.apply(this, arguments); }; WAM.render.__wamEnvuelto = true; } })();