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
+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 ————————————————— */