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
+16 -3
View File
@@ -51,6 +51,8 @@ con `dir:"in"`, anidados, NO se renderizan en el chat: los consume la interactiv
Convenciones: `src`/`avatar`/`headerMedia`/`media` = dataURL o URL remota; `""` → placeholder SVG Convenciones: `src`/`avatar`/`headerMedia`/`media` = dataURL o URL remota; `""` → placeholder SVG
de `WAM.svgPlaceholder(texto)`. Botones con `url` no vacía se renderizan como `<a target="_blank">` de `WAM.svgPlaceholder(texto)`. Botones con `url` no vacía se renderizan como `<a target="_blank">`
(cta_url, template URL, card de carousel); sin `url` → flujo interactivo con `respuestas`. (cta_url, template URL, card de carousel); sin `url` → flujo interactivo con `respuestas`.
Botón de plantilla o card con URL no permitida: `normalizar` deja `url: ""` y añade `urlBloqueada: true`
(sigue siendo un botón de enlace, inerte; nunca un quick reply con flujo; `resolverFlujo` lo ignora).
`WAM.normalizar(o)` rellena campos faltantes por tipo y devuelve el estado válido (import/upgrade). `WAM.normalizar(o)` rellena campos faltantes por tipo y devuelve el estado válido (import/upgrade).
Campo opcional `autoPulsar` (entero ≥ 0) en `template`/`buttons`/`list`/`carousel` (también en respuestas Campo opcional `autoPulsar` (entero ≥ 0) en `template`/`buttons`/`list`/`carousel` (también en respuestas
anidadas): índice del botón, de la fila aplanada de la lista o de la card que «Reproducir demo» pulsa sola anidadas): índice del botón, de la fila aplanada de la lista o de la card que «Reproducir demo» pulsa sola
@@ -72,7 +74,12 @@ cancela demo y flujos (interactivity.js envuelve `WAM.render`).
(main.js lo pinta con `WAM.avisar`) hasta el siguiente guardado correcto. (main.js lo pinta con `WAM.avisar`) hasta el siguiente guardado correcto.
- `WAM.resetear()` → void (borra LS, estado = demo con IDs desde `m1`) - `WAM.resetear()` → void (borra LS, estado = demo con IDs desde `m1`)
- `WAM.normalizar(o)` → estado (acepta un array de mensajes como raíz; IDs duplicados o inválidos se - `WAM.normalizar(o)` → estado (acepta un array de mensajes como raíz; IDs duplicados o inválidos se
reasignan; vacía URLs con esquema no permitido; respuestas string → text; conserva campos extra primitivos) reasignan; vacía URLs con esquema no permitido; respuestas string → text; conserva campos extra primitivos).
Lo recortado (más de 500 mensajes, 10 botones/secciones/filas/cards, 20 respuestas o 12 niveles) queda en
`WAM.ultimosRecortes` y en `estado.__recortes` (no enumerable): `{ mensajes, mensajesOriginales, botones,
secciones, filas, cards, respuestas, profundidad }`.
- `WAM.describirRecortes(r?)` → string en español para el toast de importar («Se importaron 500 de 600
mensajes; se recortaron 3 respuestas…») o `""` si no se recortó nada (por defecto `WAM.ultimosRecortes`)
- `WAM.generarId()` → string (nunca repite un ID ya entregado aunque aún no esté en el estado) - `WAM.generarId()` → string (nunca repite un ID ya entregado aunque aún no esté en el estado)
- `WAM.crearAlocador(extra?)` → `function tomar()` que reparte IDs únicos frente al estado, a los ya - `WAM.crearAlocador(extra?)` → `function tomar()` que reparte IDs únicos frente al estado, a los ya
entregados y a los de `extra` (para clonar mensajes con respuestas anidadas) entregados y a los de `extra` (para clonar mensajes con respuestas anidadas)
@@ -154,9 +161,15 @@ cancela demo y flujos (interactivity.js envuelve `WAM.render`).
" + descripcion`. " + descripcion`.
- `WAM.mensajeDeElemento(el)` → Mensaje · `WAM.abrirLista(msg)` → abre la hoja (`abrirSheet(i | msg)`) - `WAM.mensajeDeElemento(el)` → Mensaje · `WAM.abrirLista(msg)` → abre la hoja (`abrirSheet(i | msg)`)
- `WAM.marcarUsado(el)` → clase `.usado` (gris, sin más clics) · `WAM.reiniciarChat()` (= `render()`) - `WAM.marcarUsado(el)` → clase `.usado` (gris, sin más clics) · `WAM.reiniciarChat()` (= `render()`)
- `WAM.sesionAGuion()` → n: añade la sesión al guion (acción explícita) + guardar + re-render - `WAM.sesionAGuion()` → n: añade la sesión al guion (acción explícita) + `puntoDeshacer` + guardar +
`marcarEditado` + re-render
- Hora de la sesión: las salientes de un flujo y sus respuestas usan la hora del último mensaje visible
+ 1 min (línea temporal coherente con el guion); el composer libre usa `WAM.horaAhora()`. Los IDs de sesión
(`s1`, `s2`…) saltan los que ya existen en el guion.
- `WAM.getVelocidad()` / `WAM.setVelocidad(0.5|1|2)` (persistida en `wam-mockup-v1-velocidad`) - `WAM.getVelocidad()` / `WAM.setVelocidad(0.5|1|2)` (persistida en `wam-mockup-v1-velocidad`)
- `WAM.demoActiva()` → bool · hook opcional `WAM.onDemoChange(activa)` - `WAM.demoActiva()` → bool · hook opcional `WAM.onDemoChange(activa)` (también se llama en cada paso)
- `WAM.progresoDemo()` → `{ paso, total }` (mensajes del guion revelados / total; `{0, 0}` sin demo).
main.js lo pinta en `#demo-paso` («Paso 3/12», `aria-live`) dentro de `#barra-movil`
- `WAM.reproducirDemo()` / `WAM.detenerDemo()` → void: cancela flujos en curso, vacía `#chat-cuerpo` y revela - `WAM.reproducirDemo()` / `WAM.detenerDemo()` → void: cancela flujos en curso, vacía `#chat-cuerpo` y revela
`estado.mensajes` uno a uno (pulsando solas las opciones `autoPulsar`); detener vuelve al guion `estado.mensajes` uno a uno (pulsando solas las opciones `autoPulsar`); detener vuelve al guion
(~600 ms; mensaje `in` precedido de typing ~700 ms; cancelable) (~600 ms; mensaje `in` precedido de typing ~700 ms; cancelable)
+56 -8
View File
@@ -27,7 +27,42 @@ window.WAM = window.WAM || {};
/* Mensajes de la sesión en curso (efímeros). */ /* Mensajes de la sesión en curso (efímeros). */
WAM.sesion = []; WAM.sesion = [];
var contSesion = 0; 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 ————————————————— */ /* ————————————————— temporizadores cancelables por grupo ————————————————— */
@@ -215,12 +250,12 @@ window.WAM = window.WAM || {};
return pintar(msg); return pintar(msg);
} }
function mensajeOut(texto, cita) { function mensajeOut(texto, cita, hora) {
var msg = { var msg = {
id: idSesion(), id: idSesion(),
dir: "out", dir: "out",
tipo: "text", tipo: "text",
hora: WAM.horaAhora(), hora: hora || WAM.horaAhora(),
estado: "sent", estado: "sent",
texto: String(texto == null ? "" : texto) texto: String(texto == null ? "" : texto)
}; };
@@ -243,7 +278,7 @@ window.WAM = window.WAM || {};
if (TIPOS_VALIDOS.indexOf(copia.tipo) === -1) copia.tipo = "text"; if (TIPOS_VALIDOS.indexOf(copia.tipo) === -1) copia.tipo = "text";
copia.id = idSesion(); copia.id = idSesion();
copia.dir = "in"; copia.dir = "in";
copia.hora = WAM.horaAhora(); copia.hora = horaTras();
copia.estado = "sent"; copia.estado = "sent";
delete copia.cita; delete copia.cita;
return copia; return copia;
@@ -413,7 +448,7 @@ window.WAM = window.WAM || {};
WAM.flujo = function (texto, respuestas, cita) { WAM.flujo = function (texto, respuestas, cita) {
var lista = Array.isArray(respuestas) ? respuestas : []; var lista = Array.isArray(respuestas) ? respuestas : [];
var ctx = contexto("flujo"); var ctx = contexto("flujo");
var out = mensajeOut(texto, cita || null); var out = mensajeOut(texto, cita || null, horaTras());
anadirSesion(out); anadirSesion(out);
var tics = animarTics(out, ctx); var tics = animarTics(out, ctx);
tics.catch(nada); tics.catch(nada);
@@ -460,7 +495,8 @@ window.WAM = window.WAM || {};
if (ds.btn !== undefined) { if (ds.btn !== undefined) {
var n = parseInt(ds.btn, 10); var n = parseInt(ds.btn, 10);
var boton = (msg.botones || [])[n]; 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 { return {
texto: vacio(boton.texto) ? "Botón " + (n + 1) : String(boton.texto), texto: vacio(boton.texto) ? "Botón " + (n + 1) : String(boton.texto),
respuestas: boton.respuestas || [], respuestas: boton.respuestas || [],
@@ -472,7 +508,7 @@ window.WAM = window.WAM || {};
if (ds.card !== undefined) { if (ds.card !== undefined) {
var c = parseInt(ds.card, 10); var c = parseInt(ds.card, 10);
var card = (msg.cards || [])[c]; var card = (msg.cards || [])[c];
if (!card) return vacioR; if (!card || card.urlBloqueada) return vacioR;
return { return {
texto: vacio(card.textoBoton) ? "Ver más" : String(card.textoBoton), texto: vacio(card.textoBoton) ? "Ver más" : String(card.textoBoton),
respuestas: card.respuestas || [], respuestas: card.respuestas || [],
@@ -509,11 +545,13 @@ window.WAM = window.WAM || {};
return copia; return copia;
}); });
var n = nuevos.length; var n = nuevos.length;
if (typeof WAM.puntoDeshacer === "function") { try { WAM.puntoDeshacer(); } catch (e) { /* opcional */ } }
WAM.estado = WAM.normalizar ? WAM.normalizar({ WAM.estado = WAM.normalizar ? WAM.normalizar({
version: WAM.estado.version, tema: WAM.estado.tema, cuenta: WAM.estado.cuenta, version: WAM.estado.version, tema: WAM.estado.tema, cuenta: WAM.estado.cuenta,
mensajes: WAM.estado.mensajes.concat(nuevos) mensajes: WAM.estado.mensajes.concat(nuevos)
}) : WAM.estado; }) : WAM.estado;
if (typeof WAM.guardar === "function") WAM.guardar(); 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.render === "function") WAM.render();
if (typeof WAM.renderEditor === "function") WAM.renderEditor(); if (typeof WAM.renderEditor === "function") WAM.renderEditor();
return n; return n;
@@ -594,7 +632,7 @@ window.WAM = window.WAM || {};
async function flujoEnDemo(sel, ctx) { async function flujoEnDemo(sel, ctx) {
if (!sel || !sel.msg) return; if (!sel || !sel.msg) return;
var out = mensajeOut(sel.texto, sel.cita); var out = mensajeOut(sel.texto, sel.cita, horaTras());
anadirSesion(out); anadirSesion(out);
await animarTics(out, ctx); await animarTics(out, ctx);
await responder(Array.isArray(sel.respuestas) ? sel.respuestas : [], ctx, true); 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 () { WAM.reproducirDemo = function () {
if (demoAndando) return; if (demoAndando) return;
/* aborta flujos, ticks y typing en curso: no se mezclan con la demo */ /* aborta flujos, ticks y typing en curso: no se mezclan con la demo */
@@ -621,6 +665,8 @@ window.WAM = window.WAM || {};
vaciarChat(); vaciarChat();
var ctx = contexto("demo"); var ctx = contexto("demo");
var secuencia = ((WAM.estado && WAM.estado.mensajes) || []).slice(); var secuencia = ((WAM.estado && WAM.estado.mensajes) || []).slice();
progreso.paso = 0;
progreso.total = secuencia.length;
(async function () { (async function () {
try { try {
for (var i = 0; i < secuencia.length; i++) { 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 (msg.dir === "in") await escribirDurante(ctx, 700);
if (!ctx.vivo()) throw CANCELADO; if (!ctx.vivo()) throw CANCELADO;
pintar(msg); pintar(msg);
progreso.paso = i + 1;
avisarDemo();
await ctx.esperar(600); await ctx.esperar(600);
await autoPulsar(msg, ctx); await autoPulsar(msg, ctx);
} }
+42 -2
View File
@@ -74,15 +74,55 @@ window.WAM = window.WAM || {};
/* ————————————————— botón "Reproducir demo" ————————————————— */ /* ————————————————— 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() { function pintarBotonDemo() {
var btn = porId("btn-reproducir"); var btn = porId("btn-reproducir");
if (!btn) return; if (!btn) return;
var activa = WAM.demoActiva(); var activa = WAM.demoActiva();
btn.setAttribute("aria-pressed", activa ? "true" : "false"); 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"); var span = btn.querySelector("span");
if (!span) return; if (span) {
if (!span.dataset.etiqueta) span.dataset.etiqueta = span.textContent; if (!span.dataset.etiqueta) span.dataset.etiqueta = span.textContent;
span.textContent = activa ? "■ Detener" : span.dataset.etiqueta; 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. */ /* 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; return n;
} }
function lista(v, max) { /* Contadores de lo que recorta la normalización en curso (ver
return (Array.isArray(v) ? v : []).slice(0, max || MAX_ITEMS); 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. /* Rellena los campos que faltan por tipo y devuelve un estado válido.
Usado por cargar(), resetear() y editor.importarJSON (import / upgrade). Usado por cargar(), resetear() y editor.importarJSON (import / upgrade).
- Acepta también un array de mensajes suelto como raíz. - Acepta también un array de mensajes suelto como raíz.
@@ -239,6 +283,11 @@ window.WAM = window.WAM || {};
: ((o && typeof o === "object") ? o : {}); : ((o && typeof o === "object") ? o : {});
entregados = new Set(); 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(); var conjunto = new Set();
recolectarIds(src, conjunto); recolectarIds(src, conjunto);
var tomar = crearAlocador(conjunto); var tomar = crearAlocador(conjunto);
@@ -270,16 +319,19 @@ window.WAM = window.WAM || {};
}; };
function respuestas(valor) { function respuestas(valor) {
if (profundidad >= MAX_PROFUNDIDAD) return []; if (profundidad >= MAX_PROFUNDIDAD) {
return lista(valor, 20).map(function (r) { return normalizarMensaje(r, true, profundidad + 1); }); 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) { 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 boton = (b && typeof b === "object") ? b : { texto: typeof b === "string" ? b : "" };
var out = { texto: cad(boton.texto), respuestas: respuestas(boton.respuestas) }; var out = { texto: cad(boton.texto), respuestas: respuestas(boton.respuestas) };
/* Botón URL de plantilla (research-api §7.2): se conserva si viene */ /* 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; return out;
}); });
} }
@@ -338,11 +390,11 @@ window.WAM = window.WAM || {};
msg.body = cad(m.body); msg.body = cad(m.body);
msg.footer = cad(m.footer); msg.footer = cad(m.footer);
msg.boton = cad(m.boton); 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 : {}; var seccion = (s && typeof s === "object") ? s : {};
return { return {
titulo: cad(seccion.titulo), 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 : "" }; var fila = (f && typeof f === "object") ? f : { titulo: typeof f === "string" ? f : "" };
return { return {
titulo: cad(fila.titulo), titulo: cad(fila.titulo),
@@ -363,15 +415,17 @@ window.WAM = window.WAM || {};
break; break;
case "carousel": case "carousel":
msg.body = cad(m.body); 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 : {}; var card = (c && typeof c === "object") ? c : {};
return { var salida = {
media: urlLimpia(card.media, "media"), media: urlLimpia(card.media, "media"),
texto: cad(card.texto), texto: cad(card.texto),
textoBoton: cad(card.textoBoton), textoBoton: cad(card.textoBoton),
url: urlLimpia(card.url, "enlace"), url: "",
respuestas: respuestas(card.respuestas) respuestas: respuestas(card.respuestas)
}; };
urlEnlace(salida, card);
return salida;
}); });
autoPulsar(); autoPulsar();
break; break;
@@ -389,7 +443,7 @@ window.WAM = window.WAM || {};
} }
var cuentaSrc = (src.cuenta && typeof src.cuenta === "object") ? src.cuenta : {}; var cuentaSrc = (src.cuenta && typeof src.cuenta === "object") ? src.cuenta : {};
return { var resultado = {
version: 1, version: 1,
tema: src.tema === "oscuro" ? "oscuro" : "claro", tema: src.tema === "oscuro" ? "oscuro" : "claro",
cuenta: { cuenta: {
@@ -404,6 +458,14 @@ window.WAM = window.WAM || {};
return normalizarMensaje(x, false, 0); 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 ————————————————— */ /* ————————————————— persistencia ————————————————— */