Estado inicial del mockup

This commit is contained in:
urieljareth
2026-09-27 19:17:33 -06:00
commit dc6e31a9d7
15 changed files with 6590 additions and 0 deletions
+544
View File
@@ -0,0 +1,544 @@
/* WAM · store.js — dueño D (núcleo).
Estado único, persistencia en localStorage, normalización, generación de IDs
y demo por defecto "Café Aurora" (los 13 tipos, respuestas encadenadas).
No toca el DOM al cargar: solo define WAM.*. */
window.WAM = window.WAM || {};
(function () {
"use strict";
WAM.CLAVE = "wam-mockup-v1";
WAM.estado = null;
var TIPOS = [
"text", "image", "video", "audio", "document", "sticker", "location",
"contact", "template", "buttons", "list", "cta_url", "carousel"
];
var HEADER_TIPOS = ["texto", "imagen", "video", "documento"];
/* ————————————————— utilidades ————————————————— */
function dos(n) { return (n < 10 ? "0" : "") + n; }
WAM.horaAhora = function () {
var d = new Date();
return dos(d.getHours()) + ":" + dos(d.getMinutes());
};
function cad(v, porDefecto) {
if (v === undefined || v === null) return porDefecto || "";
return String(v);
}
function escaparXml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
if (c === "&") return "&amp;";
if (c === "<") return "&lt;";
if (c === ">") return "&gt;";
if (c === '"') return "&quot;";
return "&apos;";
});
}
function aDataUri(svg) {
return "data:image/svg+xml," + encodeURIComponent(svg);
}
/* Placeholder genérico (taza) para src/avatar/headerMedia vacíos. */
WAM.svgPlaceholder = function (texto) {
var t = escaparXml(texto || "");
var svg =
'<svg xmlns="http://www.w3.org/2000/svg" width="480" height="320" viewBox="0 0 480 320">' +
'<rect width="480" height="320" fill="#efe6dc"/>' +
'<path d="M204 96 l8 -16 m16 16 l8 -16 m16 16 l8 -16" fill="none" stroke="#c8a27a" stroke-width="5" stroke-linecap="round"/>' +
'<rect x="184" y="118" width="70" height="52" rx="10" fill="none" stroke="#a9825f" stroke-width="7"/>' +
'<path d="M254 130 h13 a13 13 0 0 1 0 26 h-13" fill="none" stroke="#a9825f" stroke-width="7"/>' +
(t ? '<text x="240" y="242" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="24" fill="#7f5539">' + t + '</text>' : '') +
'</svg>';
return aDataUri(svg);
};
/* Imagen SVG en línea y pequeña para las medias de la demo (sin red). */
function svgDemo(texto, emoji, fondo, acento) {
var svg =
'<svg xmlns="http://www.w3.org/2000/svg" width="480" height="320" viewBox="0 0 480 320">' +
'<rect width="480" height="320" fill="' + fondo + '"/>' +
'<circle cx="240" cy="132" r="76" fill="' + acento + '"/>' +
'<text x="240" y="156" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="68">' + escaparXml(emoji || "☕") + '</text>' +
'<text x="240" y="272" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="25" fill="#5b4232">' + escaparXml(texto || "") + '</text>' +
'</svg>';
return aDataUri(svg);
}
function svgAvatar() {
return aDataUri(
'<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">' +
'<circle cx="48" cy="48" r="48" fill="#6f4e37"/>' +
'<text x="48" y="63" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="42">☕</text>' +
'</svg>'
);
}
function svgSticker() {
return aDataUri(
'<svg xmlns="http://www.w3.org/2000/svg" width="320" height="320" viewBox="0 0 320 320">' +
'<circle cx="160" cy="160" r="150" fill="#fff8ef" stroke="#e3cfae" stroke-width="10"/>' +
'<text x="160" y="198" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="130">☕</text>' +
'</svg>'
);
}
/* ————————————————— IDs secuenciales ————————————————— */
/* Recorre el estado (mensajes, respuestas anidadas, botones, filas, cards)
y junta todos los id existentes para no colisionar nunca. */
function recolectarIds(o, conjunto) {
if (!o || typeof o !== "object") return;
if (Array.isArray(o)) {
for (var i = 0; i < o.length; i++) recolectarIds(o[i], conjunto);
return;
}
if (typeof o.id === "string" && o.id) conjunto.add(o.id);
var claves = ["mensajes", "respuestas", "botones", "secciones", "filas", "cards"];
for (var c = 0; c < claves.length; c++) {
var v = o[claves[c]];
if (v && typeof v === "object") recolectarIds(v, conjunto);
}
}
function crearAlocador(usados) {
var conjunto = usados || new Set();
return function tomar() {
var n = 1;
while (conjunto.has("m" + n)) n++;
var id = "m" + n;
conjunto.add(id);
return id;
};
}
WAM.generarId = function () {
var conjunto = new Set();
recolectarIds(WAM.estado, conjunto);
return crearAlocador(conjunto)();
};
/* ————————————————— mensajes ————————————————— */
var CAMPOS = {
text: function () { return { texto: "" }; },
image: function () { return { src: "", caption: "" }; },
video: function () { return { src: "", caption: "" }; },
audio: function () { return { src: "", voz: true, duracion: "0:00" }; },
document: function () { return { src: "", caption: "", filename: "documento.pdf" }; },
sticker: function () { return { src: "" }; },
location: function () { return { latitud: "", longitud: "", nombre: "", direccion: "" }; },
contact: function () { return { nombre: "", telefono: "", email: "", empresa: "" }; },
template: function () { return { headerTipo: "", headerTexto: "", headerMedia: "", body: "", footer: "", botones: [] }; },
buttons: function () { return { headerTipo: "", headerTexto: "", headerMedia: "", body: "", footer: "", botones: [] }; },
list: function () { return { header: "", body: "", footer: "", boton: "", secciones: [] }; },
cta_url: function () { return { headerMedia: "", body: "", footer: "", textoBoton: "", url: "" }; },
carousel: function () { return { body: "", cards: [] }; }
};
WAM.nuevoMensaje = function (tipo) {
var valido = Object.prototype.hasOwnProperty.call(CAMPOS, tipo);
var fabrica = valido ? CAMPOS[tipo] : CAMPOS.text;
var msg = {
id: WAM.generarId(),
dir: "in",
tipo: valido ? tipo : "text",
hora: WAM.horaAhora(),
estado: "sent"
};
var campos = fabrica();
for (var k in campos) {
if (Object.prototype.hasOwnProperty.call(campos, k)) msg[k] = campos[k];
}
return msg;
};
/* ————————————————— normalización ————————————————— */
/* Rellena los campos que faltan por tipo y devuelve un estado válido.
Usado por cargar() y por editor.importarJSON (import / upgrade). */
WAM.normalizar = function (o) {
var src = (o && typeof o === "object" && !Array.isArray(o)) ? o : {};
var conjunto = new Set();
recolectarIds(src, conjunto);
recolectarIds(WAM.estado, conjunto); /* evita colisiones con lo vivo */
var tomar = crearAlocador(conjunto);
function normalizarMensaje(msj, forzarEntrada) {
var m = (msj && typeof msj === "object") ? msj : {};
var tipo = TIPOS.indexOf(m.tipo) !== -1 ? m.tipo : "text";
var msg = {
id: (typeof m.id === "string" && m.id) ? m.id : tomar(),
dir: (!forzarEntrada && m.dir === "out") ? "out" : "in",
tipo: tipo,
hora: (typeof m.hora === "string" && m.hora) ? m.hora : WAM.horaAhora(),
estado: (m.estado === "delivered" || m.estado === "read") ? m.estado : "sent"
};
function respuestas(valor) {
var lista = Array.isArray(valor) ? valor : [];
return lista.map(function (r) { return normalizarMensaje(r, true); });
}
function botones(valor) {
var lista = Array.isArray(valor) ? valor : [];
return lista.map(function (b) {
var boton = (b && typeof b === "object") ? 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 = boton.url;
return out;
});
}
switch (tipo) {
case "text":
msg.texto = cad(m.texto);
break;
case "image":
case "video":
msg.src = cad(m.src);
msg.caption = cad(m.caption);
break;
case "audio":
msg.src = cad(m.src);
msg.voz = m.voz === true;
msg.duracion = cad(m.duracion, "0:00");
break;
case "document":
msg.src = cad(m.src);
msg.caption = cad(m.caption);
msg.filename = cad(m.filename, "documento.pdf");
break;
case "sticker":
msg.src = cad(m.src);
break;
case "location":
msg.latitud = cad(m.latitud);
msg.longitud = cad(m.longitud);
msg.nombre = cad(m.nombre);
msg.direccion = cad(m.direccion);
break;
case "contact":
msg.nombre = cad(m.nombre);
msg.telefono = cad(m.telefono);
msg.email = cad(m.email);
msg.empresa = cad(m.empresa);
break;
case "template":
msg.headerTipo = HEADER_TIPOS.indexOf(m.headerTipo) !== -1 ? m.headerTipo : "";
msg.headerTexto = cad(m.headerTexto);
msg.headerMedia = cad(m.headerMedia);
msg.body = cad(m.body);
msg.footer = cad(m.footer);
msg.botones = botones(m.botones);
break;
case "buttons":
msg.headerTipo = HEADER_TIPOS.indexOf(m.headerTipo) !== -1 ? m.headerTipo : "";
msg.headerTexto = cad(m.headerTexto);
msg.headerMedia = cad(m.headerMedia);
msg.body = cad(m.body);
msg.footer = cad(m.footer);
msg.botones = botones(m.botones);
break;
case "list":
msg.header = cad(m.header);
msg.body = cad(m.body);
msg.footer = cad(m.footer);
msg.boton = cad(m.boton);
msg.secciones = (Array.isArray(m.secciones) ? m.secciones : []).map(function (s) {
var seccion = (s && typeof s === "object") ? s : {};
return {
titulo: cad(seccion.titulo),
filas: (Array.isArray(seccion.filas) ? seccion.filas : []).map(function (f) {
var fila = (f && typeof f === "object") ? f : {};
return {
titulo: cad(fila.titulo),
descripcion: cad(fila.descripcion),
respuestas: respuestas(fila.respuestas)
};
})
};
});
break;
case "cta_url":
msg.headerMedia = cad(m.headerMedia);
msg.body = cad(m.body);
msg.footer = cad(m.footer);
msg.textoBoton = cad(m.textoBoton);
msg.url = cad(m.url);
break;
case "carousel":
msg.body = cad(m.body);
msg.cards = (Array.isArray(m.cards) ? m.cards : []).map(function (c) {
var card = (c && typeof c === "object") ? c : {};
return {
media: cad(card.media),
texto: cad(card.texto),
textoBoton: cad(card.textoBoton),
url: cad(card.url),
respuestas: respuestas(card.respuestas)
};
});
break;
}
return msg;
}
var cuentaSrc = (src.cuenta && typeof src.cuenta === "object") ? src.cuenta : {};
return {
version: 1,
tema: src.tema === "oscuro" ? "oscuro" : "claro",
cuenta: {
nombre: cad(cuentaSrc.nombre, "Café Aurora"),
avatar: cad(cuentaSrc.avatar),
subtitulo: cad(cuentaSrc.subtitulo, "en línea"),
verificado: cuentaSrc.verificado === true
},
mensajes: (Array.isArray(src.mensajes) ? src.mensajes : []).map(function (x) {
return normalizarMensaje(x, false);
})
};
};
/* ————————————————— persistencia ————————————————— */
WAM.cargar = function () {
var bruto = null;
try { bruto = window.localStorage.getItem(WAM.CLAVE); } catch (e) { bruto = null; }
var dato = null;
if (bruto) {
try { dato = JSON.parse(bruto); } catch (e) { dato = null; }
}
if (!dato) dato = WAM.datosDemo();
WAM.estado = WAM.normalizar(dato);
return WAM.estado;
};
WAM.guardar = function () {
try {
window.localStorage.setItem(WAM.CLAVE, JSON.stringify(WAM.estado));
} catch (e) { /* sin localStorage disponible (cuota/file://): se ignora */ }
};
WAM.resetear = function () {
try { window.localStorage.removeItem(WAM.CLAVE); } catch (e) { /* idem */ }
WAM.estado = WAM.normalizar(WAM.datosDemo());
};
/* ————————————————— demo por defecto: "Café Aurora" ————————————————— */
function men(dir, tipo, hora, campos) {
var msg = { id: "", dir: dir, tipo: tipo, hora: hora, estado: "sent" };
var k;
for (k in campos) {
if (Object.prototype.hasOwnProperty.call(campos, k)) msg[k] = campos[k];
}
return msg;
}
WAM.datosDemo = function () {
return {
version: 1,
tema: "claro",
cuenta: {
nombre: "Café Aurora",
avatar: svgAvatar(),
subtitulo: "en línea",
verificado: true
},
mensajes: [
/* 1 · text in (saludo) */
men("in", "text", "10:24", {
texto: "¡Hola! 👋 Bienvenido a Café Aurora. Soy Luna, tu barista de guardia. ¿Te cuento las novedades?"
}),
/* 2 · text out (cliente) */
men("out", "text", "10:24", {
estado: "read",
texto: "¡Hola, Luna! Sí, cuéntame ☕"
}),
/* 3 · image */
men("in", "image", "10:25", {
src: svgDemo("Nuevo horario", "⏰", "#f3e7d8", "#c8a27a"),
caption: "Nuevo horario desde el 1 de octubre ☕"
}),
/* 4 · video (sin src → placeholder SVG) */
men("in", "video", "10:25", {
src: "",
caption: "Así tostamos nuestros granos cada mañana"
}),
/* 5 · location */
men("in", "location", "10:26", {
latitud: "40.4168",
longitud: "-3.7038",
nombre: "Café Aurora",
direccion: "Calle Mayor 12, 28013 Madrid"
}),
/* 6 · contact */
men("in", "contact", "10:26", {
nombre: "Luna Mendoza",
telefono: "+34 611 222 333",
email: "[email protected]",
empresa: "Café Aurora"
}),
/* 7 · document */
men("in", "document", "10:27", {
src: "",
caption: "Nuestra carta con granos y precios",
filename: "carta-cafe-aurora.pdf"
}),
/* 8 · audio (nota de voz) */
men("in", "audio", "10:27", {
src: "",
voz: true,
duracion: "0:12"
}),
/* 9 · sticker */
men("in", "sticker", "10:28", {
src: svgSticker()
}),
/* 10 · template (header imagen + confirmación de reserva) */
men("in", "template", "10:29", {
headerTipo: "imagen",
headerTexto: "",
headerMedia: svgDemo("Tu mesa te espera", "☕", "#e9d8c3", "#a9825f"),
body: "Hola 👋 Tu mesa para 2 personas está reservada este viernes a las 19:30. ¿Confirmas la reserva?",
footer: "Café Aurora · Reservas",
botones: [
{
texto: "Confirmar",
respuestas: [
men("in", "text", "10:29", {
texto: "¡Genial! Reserva confirmada para el viernes a las 19:30 ✨ Te esperamos."
})
]
},
{
texto: "Cambiar fecha",
respuestas: [
men("in", "text", "10:29", {
texto: "Sin problema. ¿Para qué día y hora te la cambio?"
})
]
}
]
}),
/* 11 · buttons (quick replies, máx 3 · títulos ≤ 20) */
men("in", "buttons", "10:30", {
body: "¿Qué te muestro ahora?",
footer: "Elige una opción",
botones: [
{
texto: "Horarios",
respuestas: [
men("in", "text", "10:30", {
texto: "Abrimos de lunes a viernes de 7:30 a 20:00 y sábados de 9:00 a 14:00."
})
]
},
{
texto: "Promociones",
respuestas: [
men("in", "text", "10:30", { texto: "🔥 Martes de cold brew: 2×1 todo el día." }),
men("in", "text", "10:30", { texto: "🥐 Viernes: croissant gratis con tu café." })
]
},
{
texto: "Hablar con el equipo",
respuestas: [
men("in", "text", "10:30", {
texto: "Te paso con Marta del equipo humano, un momento…"
})
]
}
]
}),
/* 12 · list (filas ≤ 10 · títulos fila ≤ 24 · descripción ≤ 72) */
men("in", "list", "10:31", {
header: "Menú de la casa",
body: "¿Qué te apetece hoy?",
footer: "Catálogo 2026",
boton: "Ver opciones",
secciones: [
{
titulo: "Bebidas",
filas: [
{
titulo: "Espresso double",
descripcion: "Intenso, 100 % arábica",
respuestas: [men("in", "text", "10:31", { texto: "Un espresso double rumbo a tu mesa ☕" })]
},
{
titulo: "Latte de avena",
descripcion: "Suave, con leche de avena",
respuestas: [men("in", "text", "10:31", { texto: "¡Buen gusto! Latte de avena anotado 🌾" })]
},
{
titulo: "Cold brew 12 h",
descripcion: "Macerado en frío durante 12 horas",
respuestas: [men("in", "text", "10:31", { texto: "Cold brew bien frío, ¡perfecto para hoy! 🧊" })]
}
]
},
{
titulo: "Postres",
filas: [
{
titulo: "Tarta de queso",
descripcion: "La de siempre, casera",
respuestas: [men("in", "text", "10:31", { texto: "Una porción de tarta de queso, ¡gran elección! 🍰" })]
},
{
titulo: "Croissant almendra",
descripcion: "Horneado cada mañana",
respuestas: [men("in", "text", "10:31", { texto: "Croissant de almendra recién hecho 🥐" })]
}
]
}
]
}),
/* 13 · cta_url (botón con URL → <a target="_blank">, sin flujo) */
men("in", "cta_url", "10:32", {
headerMedia: svgDemo("Tienda online", "🛍️", "#efe0cd", "#b08968"),
body: "Ya puedes pedir nuestros granos tostados a domicilio.",
footer: "Envío en 24-48 h",
textoBoton: "Abrir tienda",
url: "https://cafeaurora.example/tienda"
}),
/* 14 · carousel (3 cards con respuestas encadenadas) */
men("in", "carousel", "10:33", {
body: "Los favoritos de la casa ☕",
cards: [
{
media: svgDemo("Espresso Aurora", "☕", "#f0e3d0", "#a9825f"),
texto: "Espresso Aurora · tueste medio con notas de cacao. Bolsa 250 g.",
textoBoton: "¡Lo quiero!",
url: "",
respuestas: [men("in", "text", "10:33", { texto: "¡Excelente! El Espresso Aurora sale hoy mismo hacia ti 📦" })]
},
{
media: svgDemo("Cold brew botella", "🧋", "#e4ecf0", "#7f9cab"),
texto: "Cold brew en botella de 1 L, listo para servir muy frío.",
textoBoton: "Pedir",
url: "",
respuestas: [men("in", "text", "10:33", { texto: "Cold brew de 1 L añadido a tu pedido 🧊" })]
},
{
media: svgDemo("Tarta de queso", "🍰", "#f5ebe2", "#c9a227"),
texto: "Tarta de queso casera, entera o por porciones.",
textoBoton: "Probar",
url: "",
respuestas: [men("in", "text", "10:33", { texto: "La tarta de queso sale del horno a las 12:00 🍰" })]
}
]
})
]
};
};
})();