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 */}
);
}
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 => (
))}
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(
);
}