Demo con contador de pasos e icono de detener, horas de sesión coherentes, IDs de sesión sin choques, URLs bloqueadas inertes, recortes de importación contados y sesionAGuion con deshacer

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
urieljareth
2026-09-27 21:09:42 -06:00
co-authored by Claude Opus 5.5
parent 9a5f21badf
commit c0b7d0c2ce
4 changed files with 189 additions and 26 deletions
+56 -8
View File
@@ -27,7 +27,42 @@ window.WAM = window.WAM || {};
/* Mensajes de la sesión en curso (efímeros). */
WAM.sesion = [];
var contSesion = 0;
function idSesion() { contSesion++; return "s" + contSesion; }
/* 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 ————————————————— */
@@ -215,12 +250,12 @@ window.WAM = window.WAM || {};
return pintar(msg);
}
function mensajeOut(texto, cita) {
function mensajeOut(texto, cita, hora) {
var msg = {
id: idSesion(),
dir: "out",
tipo: "text",
hora: WAM.horaAhora(),
hora: hora || WAM.horaAhora(),
estado: "sent",
texto: String(texto == null ? "" : texto)
};
@@ -243,7 +278,7 @@ window.WAM = window.WAM || {};
if (TIPOS_VALIDOS.indexOf(copia.tipo) === -1) copia.tipo = "text";
copia.id = idSesion();
copia.dir = "in";
copia.hora = WAM.horaAhora();
copia.hora = horaTras();
copia.estado = "sent";
delete copia.cita;
return copia;
@@ -413,7 +448,7 @@ window.WAM = window.WAM || {};
WAM.flujo = function (texto, respuestas, cita) {
var lista = Array.isArray(respuestas) ? respuestas : [];
var ctx = contexto("flujo");
var out = mensajeOut(texto, cita || null);
var out = mensajeOut(texto, cita || null, horaTras());
anadirSesion(out);
var tics = animarTics(out, ctx);
tics.catch(nada);
@@ -460,7 +495,8 @@ window.WAM = window.WAM || {};
if (ds.btn !== undefined) {
var n = parseInt(ds.btn, 10);
var boton = (msg.botones || [])[n];
if (!boton) return vacioR;
/* 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 || [],
@@ -472,7 +508,7 @@ window.WAM = window.WAM || {};
if (ds.card !== undefined) {
var c = parseInt(ds.card, 10);
var card = (msg.cards || [])[c];
if (!card) return vacioR;
if (!card || card.urlBloqueada) return vacioR;
return {
texto: vacio(card.textoBoton) ? "Ver más" : String(card.textoBoton),
respuestas: card.respuestas || [],
@@ -509,11 +545,13 @@ window.WAM = window.WAM || {};
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;
@@ -594,7 +632,7 @@ window.WAM = window.WAM || {};
async function flujoEnDemo(sel, ctx) {
if (!sel || !sel.msg) return;
var out = mensajeOut(sel.texto, sel.cita);
var out = mensajeOut(sel.texto, sel.cita, horaTras());
anadirSesion(out);
await animarTics(out, ctx);
await responder(Array.isArray(sel.respuestas) ? sel.respuestas : [], ctx, true);
@@ -609,6 +647,12 @@ window.WAM = window.WAM || {};
}
}
/* 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 */
@@ -621,6 +665,8 @@ window.WAM = window.WAM || {};
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++) {
@@ -629,6 +675,8 @@ window.WAM = window.WAM || {};
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);
}
+43 -3
View File
@@ -74,15 +74,55 @@ window.WAM = window.WAM || {};
/* ————————————————— botón "Reproducir demo" ————————————————— */
var SVG_STOP = '<rect x="6.5" y="6.5" width="11" height="11" rx="2"/>';
/* 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) return;
if (!span.dataset.etiqueta) span.dataset.etiqueta = span.textContent;
span.textContent = activa ? "■ Detener" : span.dataset.etiqueta;
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. */
+74 -12
View File
@@ -221,10 +221,54 @@ window.WAM = window.WAM || {};
return n;
}
function lista(v, max) {
return (Array.isArray(v) ? v : []).slice(0, max || MAX_ITEMS);
/* Contadores de lo que recorta la normalización en curso (ver
WAM.ultimosRecortes). null fuera de WAM.normalizar. */
var recortes = null;
function lista(v, max, clave) {
var arr = Array.isArray(v) ? v : [];
var tope = max || MAX_ITEMS;
if (recortes && clave && arr.length > tope) recortes[clave] += arr.length - tope;
return arr.slice(0, tope);
}
/* URL de un botón/card de enlace: si venía escrita pero es insegura se
vacía y se marca urlBloqueada, para que siga siendo un botón de enlace
(inerte) y no se convierta en una respuesta rápida con flujo. */
function urlEnlace(destino, origen) {
var bruta = cad(origen.url).trim();
destino.url = urlLimpia(bruta, "enlace");
if (!destino.url && (bruta || origen.urlBloqueada === true || origen.urlBloqueada === "true")) {
destino.urlBloqueada = true;
}
}
/* Texto en español con lo recortado, o "" si no se recortó nada. */
WAM.describirRecortes = function (r) {
r = r || WAM.ultimosRecortes;
if (!r) return "";
var partes = [];
function n(c, uno, varios) { if (r[c] > 0) partes.push(r[c] + " " + (r[c] === 1 ? uno : varios)); }
n("botones", "botón", "botones");
n("secciones", "sección", "secciones");
n("filas", "fila", "filas");
n("cards", "tarjeta", "tarjetas");
n("respuestas", "respuesta", "respuestas");
n("profundidad", "respuesta anidada (más de " + MAX_PROFUNDIDAD + " niveles)",
"respuestas anidadas (más de " + MAX_PROFUNDIDAD + " niveles)");
var txt = "";
if (r.mensajesOriginales > r.mensajes) {
txt = "Se importaron " + r.mensajes + " de " + r.mensajesOriginales + " mensajes";
}
if (partes.length) {
var total = 0;
["botones", "secciones", "filas", "cards", "respuestas", "profundidad"].forEach(function (c) { total += r[c] || 0; });
var verbo = total === 1 ? "se recortó " : "se recortaron ";
txt += (txt ? "; " + verbo : verbo.charAt(0).toUpperCase() + verbo.slice(1)) + partes.join(", ");
}
return txt;
};
/* Rellena los campos que faltan por tipo y devuelve un estado válido.
Usado por cargar(), resetear() y editor.importarJSON (import / upgrade).
- Acepta también un array de mensajes suelto como raíz.
@@ -239,6 +283,11 @@ window.WAM = window.WAM || {};
: ((o && typeof o === "object") ? o : {});
entregados = new Set();
var mensajesSrc = Array.isArray(src.mensajes) ? src.mensajes.length : 0;
recortes = {
mensajes: Math.min(mensajesSrc, 500), mensajesOriginales: mensajesSrc,
botones: 0, secciones: 0, filas: 0, cards: 0, respuestas: 0, profundidad: 0
};
var conjunto = new Set();
recolectarIds(src, conjunto);
var tomar = crearAlocador(conjunto);
@@ -270,16 +319,19 @@ window.WAM = window.WAM || {};
};
function respuestas(valor) {
if (profundidad >= MAX_PROFUNDIDAD) return [];
return lista(valor, 20).map(function (r) { return normalizarMensaje(r, true, profundidad + 1); });
if (profundidad >= MAX_PROFUNDIDAD) {
if (Array.isArray(valor) && valor.length) recortes.profundidad += valor.length;
return [];
}
return lista(valor, 20, "respuestas").map(function (r) { return normalizarMensaje(r, true, profundidad + 1); });
}
function botones(valor) {
return lista(valor).map(function (b) {
return lista(valor, MAX_ITEMS, "botones").map(function (b) {
var boton = (b && typeof b === "object") ? b : { texto: typeof b === "string" ? b : "" };
var out = { texto: cad(boton.texto), respuestas: respuestas(boton.respuestas) };
/* Botón URL de plantilla (research-api §7.2): se conserva si viene */
if (typeof boton.url === "string") out.url = urlLimpia(boton.url, "enlace");
if (typeof boton.url === "string" || boton.urlBloqueada === true) urlEnlace(out, boton);
return out;
});
}
@@ -338,11 +390,11 @@ window.WAM = window.WAM || {};
msg.body = cad(m.body);
msg.footer = cad(m.footer);
msg.boton = cad(m.boton);
msg.secciones = lista(m.secciones).map(function (s) {
msg.secciones = lista(m.secciones, MAX_ITEMS, "secciones").map(function (s) {
var seccion = (s && typeof s === "object") ? s : {};
return {
titulo: cad(seccion.titulo),
filas: lista(seccion.filas).map(function (f) {
filas: lista(seccion.filas, MAX_ITEMS, "filas").map(function (f) {
var fila = (f && typeof f === "object") ? f : { titulo: typeof f === "string" ? f : "" };
return {
titulo: cad(fila.titulo),
@@ -363,15 +415,17 @@ window.WAM = window.WAM || {};
break;
case "carousel":
msg.body = cad(m.body);
msg.cards = lista(m.cards).map(function (c) {
msg.cards = lista(m.cards, MAX_ITEMS, "cards").map(function (c) {
var card = (c && typeof c === "object") ? c : {};
return {
var salida = {
media: urlLimpia(card.media, "media"),
texto: cad(card.texto),
textoBoton: cad(card.textoBoton),
url: urlLimpia(card.url, "enlace"),
url: "",
respuestas: respuestas(card.respuestas)
};
urlEnlace(salida, card);
return salida;
});
autoPulsar();
break;
@@ -389,7 +443,7 @@ window.WAM = window.WAM || {};
}
var cuentaSrc = (src.cuenta && typeof src.cuenta === "object") ? src.cuenta : {};
return {
var resultado = {
version: 1,
tema: src.tema === "oscuro" ? "oscuro" : "claro",
cuenta: {
@@ -404,6 +458,14 @@ window.WAM = window.WAM || {};
return normalizarMensaje(x, false, 0);
})
};
/* Lo recortado queda en WAM.ultimosRecortes y, no enumerable (no se
guarda ni se exporta), en resultado.__recortes. */
WAM.ultimosRecortes = recortes;
try {
Object.defineProperty(resultado, "__recortes", { value: recortes, enumerable: false, configurable: true });
} catch (e) { /* entorno sin defineProperty */ }
recortes = null;
return resultado;
};
/* ————————————————— persistencia ————————————————— */