/* 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 "&"; if (c === "<") return "<"; if (c === ">") return ">"; if (c === '"') return """; return "'"; }); } 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 = '' + '' + '' + '' + '' + (t ? '' + t + '' : '') + ''; 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 = '' + '' + '' + '' + escaparXml(emoji || "☕") + '' + '' + escaparXml(texto || "") + '' + ''; return aDataUri(svg); } function svgAvatar() { return aDataUri( '' + '' + '☕' + '' ); } function svgSticker() { return aDataUri( '' + '' + '☕' + '' ); } /* ————————————————— 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: "luna@cafeaurora.es", 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 → , 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 🍰" })] } ] }) ] }; }; })();