const { useState, useEffect, useRef, useCallback } = React; const FLOW_CIUDADANO = [ { type: "system", text: "📡 Broadcast diario 17:00h — template €0.05" }, { type: "in", text: "👋 Hola, soy *MadridVOZ*\n\n📍 3 propuestas nuevas en *Chamberí* hoy.\n¿Quieres votar?", btns: ["✅ Ver propuestas", "⏭️ Ahora no"], time: "17:00" }, { type: "out", text: "✅ Ver propuestas", time: "17:01" }, { type: "system", text: "✓ Sesión gratuita abierta — 24h sin coste €0" }, { type: "in", text: "🗳️ *Propuesta #1*\n\nCarril bici en C/ Fuencarral\n(Bilbao → Gran Vía)\n\n👍 *1.247* a favor 👎 213 en contra", btns: ["👍 A favor", "👎 En contra", "➡️ Siguiente"], time: "17:01" }, { type: "out", text: "👍 A favor", time: "17:01" }, { type: "in", text: "✅ *Voto registrado*\n🔐 Anónimo y verificado\n\n¿Envías tu propia propuesta?", btns: ["📝 Proponer", "🏠 Menú"], time: "17:01" }, { type: "out", text: "📝 Proponer", time: "17:02" }, { type: "in", text: "Escríbeme tu propuesta ✍️\n\nLa IA la clasificará y llegará a *1.247 vecinos* esta tarde.", time: "17:02" }, { type: "out", text: "Propongo instalar una fuente pública en la Plaza de Olavide, en verano hace mucho calor.", time: "17:03" }, { type: "in", text: "🤖 IA clasificando…\n\n✅ Categoría: *Urbanismo*\n📍 Plaza Olavide · Chamberí\n\n*#MAD-4021* registrada\nSe envía a 1.247 vecinos a las 19:00h 🚀", time: "17:03" }, ]; const FLOW_EMPRESA = [ { type: "system", text: "🏢 Estudio de mercado — IKEA España · Panel Vallecas" }, { type: "in", text: "📊 *Consulta de IKEA España*\n\nPensamos abrir una sección de productos sostenibles en Vallecas.\n\n¿Te interesaría?", btns: ["💚 Sí, mucho", "😐 Indiferente", "❌ No"], time: "10:30" }, { type: "out", text: "💚 Sí, mucho", time: "10:30" }, { type: "in", text: "¿Cuánto más pagarías por productos sostenibles?", btns: ["+5%", "+10%", "+20%", "Nada más"], time: "10:31" }, { type: "out", text: "+10%", time: "10:31" }, { type: "in", text: "¿Qué categoría te interesa más?", btns: ["🛋️ Muebles", "🍽️ Cocina", "💡 Iluminación", "🌿 Plantas"], time: "10:31" }, { type: "out", text: "🌿 Plantas", time: "10:31" }, { type: "in", text: "✅ *Respuesta registrada*\n\nResultados el *15 de marzo*.\n\n_Estudio encargado por IKEA España_", time: "10:32" }, { type: "system", text: "📈 847 respuestas · 48h · Coste total: €180 · Informe IA generado" }, ]; const SCENES = [ { id: "intro", duration: 3500 }, { id: "problem", duration: 4000 }, { id: "chat1", duration: 0 }, { id: "callout", duration: 3500 }, { id: "chat2", duration: 0 }, { id: "metrics", duration: 5000 }, { id: "outro", duration: 0 }, ]; function TypingBubble() { return (
{[0, 1, 2].map(i => (
))}
); } function ChatBubble({ msg, visible }) { const isIn = msg.type === "in"; const isSystem = msg.type === "system"; if (isSystem) { return (
{msg.text}
); } const parseText = (t) => t.replace(/\*(.*?)\*/g, '$1').replace(/_(.*?)_/g, '$1').replace(/\n/g, '
'); return (
{msg.time && ( {msg.time}{!isIn && ✓✓} )} {msg.btns && (
{msg.btns.map((b, i) => (
0 ? "1px solid #1a2228" : "none" }}>{b}
))}
)}
); } function WAChat({ flow, accentColor, headerName, headerSub, headerEmoji }) { const [visibleMsgs, setVisibleMsgs] = useState([]); const [showTyping, setShowTyping] = useState(false); const [msgItems, setMsgItems] = useState([]); const chatRef = useRef(null); const stepRef = useRef(0); const scrollBottom = () => { setTimeout(() => { if (chatRef.current) chatRef.current.scrollTop = chatRef.current.scrollHeight; }, 50); }; useEffect(() => { const items = flow.map((m, i) => ({ ...m, id: i })); setMsgItems(items); stepRef.current = 0; const delays = [800, 1200, 600, 700, 1300, 600, 1100, 600, 1300, 900, 1200, 800, 1000, 600, 900, 600, 1000]; function processNext() { const idx = stepRef.current; if (idx >= items.length) return; const msg = items[idx]; const gap = delays[idx] || 900; if (msg.type === "in") { setShowTyping(true); scrollBottom(); setTimeout(() => { setShowTyping(false); setVisibleMsgs(v => [...v, { ...msg, show: false }]); setTimeout(() => { setVisibleMsgs(v => v.map((m2, i2) => i2 === v.length - 1 ? { ...m2, show: true } : m2)); scrollBottom(); stepRef.current++; setTimeout(processNext, gap); }, 100); }, 900); } else { setTimeout(() => { setVisibleMsgs(v => [...v, { ...msg, show: false }]); setTimeout(() => { setVisibleMsgs(v => v.map((m2, i2) => i2 === v.length - 1 ? { ...m2, show: true } : m2)); scrollBottom(); }, 80); stepRef.current++; setTimeout(processNext, gap); }, 200); } } const t = setTimeout(processNext, 300); return () => clearTimeout(t); }, [flow]); return (
{/* Header */}
{headerEmoji}
{headerName}
{headerSub}
{/* Chat */}
Hoy
{visibleMsgs.map((m, i) => ( ))} {showTyping && }
{/* Input bar */}
Escribe un mensaje
🎤
); } function App() { const [scene, setScene] = useState(-1); const [playing, setPlaying] = useState(false); const [progress, setProgress] = useState(0); const [chatKey1, setChatKey1] = useState(0); const [chatKey2, setChatKey2] = useState(0); const timerRef = useRef(null); const totalScenes = SCENES.length; const advanceTo = useCallback((idx) => { if (idx >= totalScenes) return; setScene(idx); setProgress(((idx + 1) / totalScenes) * 100); const dur = SCENES[idx].duration; if (dur > 0) { clearTimeout(timerRef.current); timerRef.current = setTimeout(() => advanceTo(idx + 1), dur); } // chat scenes advance automatically via onDone }, [totalScenes]); useEffect(() => { if (playing && scene === -1) advanceTo(0); }, [playing]); const handlePlay = () => { if (!playing) { setPlaying(true); if (scene === -1) advanceTo(0); } }; const handleRestart = () => { clearTimeout(timerRef.current); setScene(-1); setProgress(0); setPlaying(false); setChatKey1(k => k + 1); setChatKey2(k => k + 1); }; const sceneId = scene >= 0 ? SCENES[scene].id : null; return ( <>
{/* Phone */}
{/* Notch */}
{/* Status bar */}
9:41 ▲ 5G 🔋
{/* Scenes */}
{/* IDLE / not started */} {scene === -1 && (
MadridVOZ
Demo interactivo
Hackathon INCIBE 2026
)} {/* INTRO */} {sceneId === "intro" && (
{[1, 2].map(i => (
))}
MadridVOZ
Democracia participativa
en el bolsillo de cada ciudadano
{["WhatsApp API", "IA Municipal", "B2G + B2B"].map(t => ( {t} ))}
)} {/* PROBLEM */} {sceneId === "problem" && (
EL PROBLEMA
1.6%
participan en democracia local
en Madrid
{[ ["Apps con registro", "2–5% uso real", "#444"], ["Email municipal", "5% apertura", "#555"], ["Evento presencial", "€42.000 / vez", "#555"], ["WhatsApp", "98% apertura →", "#25D366"], ].map(([k, v, c]) => (
{k} {v}
))}
)} {/* CHAT 1 - Ciudadano */} {sceneId === "chat1" && (
DEMO — CIUDADANO PROPONE Y VOTA
)} {/* CALLOUT */} {sceneId === "callout" && (
€0.003
Coste por ciudadano alcanzado con MadridVOZ
Evento presencial €85
App móvil €12
Email / SMS €0.08
MadridVOZ €0.003 ←
)} {/* CHAT 2 - Empresa */} {sceneId === "chat2" && (
DEMO — EMPRESA HACE ESTUDIO DE MERCADO
)} {/* METRICS */} {sceneId === "metrics" && (
El negocio
en números
{[ { n: "€2.4M", l: "ARR Año 3 · 40 municipios", c: "#25D366", cls: "pop0", bg: "#0a1f12" }, { n: "90%", l: "Margen estudios B2B", c: "#F59E0B", cls: "pop1", bg: "#1a1200" }, { n: "150×", l: "Más barato que evento presencial", c: "#CC0033", cls: "pop2", bg: "#1a0508" }, { n: "116:1", l: "ROI sobre presupuestos municipales", c: "#FFD100", cls: "pop3", bg: "#14110a" }, ].map(s => (
{s.n}
{s.l}
))}
INVERSIÓN SOLICITADA
€430K
24 meses · Break-even M14
)} {/* OUTRO */} {sceneId === "outro" && (
MadridVOZ
La primera infraestructura de opinión ciudadana sobre WhatsApp
{["WhatsApp API ✓", "INCIBE Emprende", "Equipo Amarillo"].map(t => ( {t} ))}
madridvoz.es
)}
{/* Progress bar */}
{/* Controls */}
{/* Scene nav dots */}
{SCENES.map((s, i) => (
{ clearTimeout(timerRef.current); setScene(i); setProgress(((i + 1) / totalScenes) * 100); }} style={{ width: 7, height: 7, borderRadius: "50%", background: scene === i ? "#FFD100" : "#333", cursor: "pointer", transition: ".2s", transform: scene === i ? "scale(1.4)" : "scale(1)" }} /> ))}
Para grabar: usa OBS · Loom · Grabación de pantalla iOS/Android
); } const rootElement = document.getElementById("root"); if (rootElement) { const root = ReactDOM.createRoot(rootElement); root.render(); }