// Grape Elevate — Demo mode panel
// Lets the user simulate Instagram events to see automations run in real time.
// Lifts state from App via a `bus` prop ({ runEvent, ... }).
const DEMO_SENDERS = [
{ name: 'Marina Lopes', handle: '@marinalopes', hue: 285 },
{ name: 'Diego Sato', handle: '@diegosato', hue: 200 },
{ name: 'Lia Andrade', handle: '@lia.creates', hue: 340 },
{ name: 'Rafa Tomaz', handle: '@rafatomaz', hue: 30 },
{ name: 'Camila Reis', handle: '@cami.reis', hue: 160 },
{ name: 'João Pires', handle: '@jpires', hue: 240 },
{ name: 'Bia Camargo', handle: '@bia.cam', hue: 95 },
{ name: 'Tiago Faria', handle: '@tfaria', hue: 50 },
{ name: 'Helena Duarte', handle: '@helena.d', hue: 310 },
{ name: 'Pedro Hortelã', handle: '@phortela', hue: 180 },
];
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
// Find an automation matching an event (by trigger type and keyword).
function matchAutomation(automations, event) {
return automations.find((a) => {
if (a.status !== 'live') return false;
if (a.trigger !== event.type) return false;
if (!event.text) return true;
if (!a.triggerKey || a.triggerKey === '—' || a.triggerKey === '*') return true;
return event.text.toLowerCase().includes(a.triggerKey.toLowerCase());
});
}
// Synthesize a bot reply text for a matched automation. For the seeded
// automations we have authored copy; otherwise fall back to a generic line.
function botReplyFor(automation, event) {
const map = {
a1: ['Oi! Aqui está o link da promo 🎁', 'grape.app/promo · cupom GRAPE10'],
a2: ['Oi! Obrigado pela resposta no story 💜', 'Posso te mandar mais detalhes?'],
a3: ['Bem-vindo(a) à comunidade Grape! 🍇', 'Pra começar, dá uma olhada no nosso último carrossel.'],
a4: ['Oi! Bom te ver por aqui 😊', 'Quer saber mais do curso?'],
a6: ['Oba, você comentou IA!', 'Aqui está nosso e-book: grape.app/ia-ebook'],
};
return map[automation.id] || [`Obrigado pela mensagem! ✨`];
}
function DemoModeButton({ bus }) {
const [open, setOpen] = React.useState(false);
const [autoplay, setAutoplay] = React.useState(false);
const [rate, setRate] = React.useState(4); // events per minute
// Custom event builder state
const [customType, setCustomType] = React.useState('comment');
const [customKey, setCustomKey] = React.useState('PROMO');
// Auto-play loop
React.useEffect(() => {
if (!autoplay) return;
const presets = [
{ type: 'comment', text: 'PROMO', hint: 'Comentou "PROMO"' },
{ type: 'comment', text: 'IA', hint: 'Comentou "IA"' },
{ type: 'dm', text: 'oi', hint: 'DM "oi"' },
{ type: 'story', text: '', hint: 'Respondeu story' },
{ type: 'follow', text: '', hint: 'Novo seguidor' },
];
const id = setInterval(() => {
const ev = pick(presets);
bus.runEvent({ ...ev, sender: pick(DEMO_SENDERS) });
}, Math.max(2000, 60000 / rate));
return () => clearInterval(id);
}, [autoplay, rate, bus]);
const fire = (type, text, hint) => bus.runEvent({ type, text, sender: pick(DEMO_SENDERS), hint });
return (
<>
{open && (
Modo demo
Simule eventos do Instagram
Cada evento dispara as automações ativas. A inbox, o feed e os contadores atualizam ao vivo, como se viesse do Instagram de verdade.
Eventos rápidos
Auto-play
Velocidade
setRate(Number(e.target.value))} style={{ flex: 1, accentColor: 'var(--grape)' }} />
{rate} /min
{autoplay &&
🎬 Disparando eventos aleatórios — vá na Inbox ou no Início pra ver o efeito.
}
)}
>
);
}
// Hook factory: returns the bus object given the state setters. Living here
// so the simulation logic is colocated with the panel UI.
function createDemoBus({ automations, setAutomations, setConversations, setActivity, setRecent, showToast, reset }) {
return {
reset,
runEvent({ type, text, sender, hint }) {
const matched = matchAutomation(automations, { type, text });
if (!matched) {
showToast(`📥 ${sender.name} ${hint || 'enviou algo'} — nenhuma automação ativa pegou`);
// Still log to activity / inbox
setConversations((cs) => prependEvent(cs, sender, type, text, null));
setRecent((rs) => [{ kind: type === 'dm' ? 'dm' : type === 'follow' ? 'follow' : 'reply', who: sender.name, what: hint || 'enviou um evento', when: 'agora', auto: '—' }, ...rs].slice(0, 8));
return;
}
// Run automation: bump runs, append a bot reply to the inbox, log activity.
setAutomations((xs) => xs.map((a) => a.id === matched.id ? { ...a, runs: a.runs + 1, updated: 'agora' } : a));
const reply = botReplyFor(matched, { type, text });
setConversations((cs) => prependEvent(cs, sender, type, text, reply));
setActivity((act) => bumpActivity(act, type));
setRecent((rs) => [{ kind: type === 'dm' ? 'dm' : type === 'follow' ? 'follow' : 'reply', who: sender.name, what: hint || 'gerou um evento', when: 'agora', auto: matched.name }, ...rs].slice(0, 8));
showToast(`⚡ ${sender.name} ${hint || 'gerou um evento'} → ${matched.name}`);
},
};
}
// Prepend / merge a conversation when a new event arrives.
function prependEvent(conversations, sender, type, text, replyLines) {
const incoming = text || (type === 'follow' ? 'começou a seguir' : type === 'story' ? '[resposta a story]' : '[evento]');
const existing = conversations.find((c) => c.handle === sender.handle);
if (existing) {
const msgs = [...existing.msgs, { from: 'them', text: incoming, t: 'agora' }];
if (replyLines) replyLines.forEach((l) => msgs.push({ from: 'bot', text: l, t: 'agora' }));
return conversations.map((c) => c.handle === sender.handle ? { ...c, msgs, last: replyLines ? replyLines[replyLines.length - 1] : incoming, time: 'agora', unread: c.unread + 1, bot: !!replyLines } : c);
}
const newC = {
id: 'c' + Date.now(),
name: sender.name,
handle: sender.handle,
avatarHue: sender.hue,
last: replyLines ? replyLines[replyLines.length - 1] : incoming,
unread: 1,
bot: !!replyLines,
time: 'agora',
tags: replyLines ? ['demo-lead'] : ['demo'],
source: type === 'comment' ? 'post' : type === 'story' ? 'story' : type === 'follow' ? 'seguidor' : 'DM direta',
msgs: [
{ from: 'them', text: incoming, t: 'agora' },
...(replyLines || []).map((l) => ({ from: 'bot', text: l, t: 'agora' })),
],
};
return [newC, ...conversations];
}
// Bump the activity sparkline series for today (last point).
function bumpActivity(act, type) {
const next = { ...act };
const inc = (k) => { const arr = [...next[k]]; arr[arr.length - 1] = arr[arr.length - 1] + 1; next[k] = arr; };
if (type === 'dm') inc('dms');
if (type === 'comment') { inc('replies'); inc('dms'); }
if (type === 'story') { inc('replies'); inc('leads'); }
if (type === 'follow') { inc('followers'); inc('dms'); }
return next;
}
const demoStyles = `
.demo-fab {
position: fixed; bottom: 16px; left: 16px;
z-index: 1200;
display: inline-flex; align-items: center; gap: 8px;
padding: 10px 16px 10px 12px;
border-radius: 999px;
border: 1px solid var(--line);
background: var(--surface);
color: var(--text);
font-weight: 700; font-size: 13px;
cursor: pointer;
box-shadow: var(--shadow-pop);
transition: transform 100ms, box-shadow 150ms;
}
.demo-fab:hover { transform: translateY(-1px); }
.demo-fab-emoji { font-size: 16px; }
.demo-fab-pulse {
width: 8px; height: 8px; border-radius: 50%; background: var(--coral);
box-shadow: 0 0 0 4px color-mix(in oklch, var(--coral) 25%, transparent);
animation: demo-pulse 1.2s infinite;
}
@keyframes demo-pulse { 50% { box-shadow: 0 0 0 8px color-mix(in oklch, var(--coral) 0%, transparent); } }
.demo-panel {
position: fixed; bottom: 70px; left: 16px;
z-index: 1201;
width: 360px; max-width: calc(100vw - 32px);
max-height: calc(100vh - 100px);
overflow-y: auto;
padding: 18px;
border-radius: 18px;
background: var(--surface);
border: 1px solid var(--line);
box-shadow: var(--shadow-pop);
display: flex; flex-direction: column; gap: 14px;
animation: demo-in 220ms cubic-bezier(.2,.9,.3,1.1);
}
@keyframes demo-in { from { opacity: 0; transform: translateY(8px); } }
.demo-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.demo-head h3 { margin: 2px 0 0; font-size: 16px; font-weight: 800; letter-spacing: -0.01em; }
.demo-blurb { margin: 0; color: var(--text-soft); font-size: 12.5px; line-height: 1.5; }
.demo-section { display: flex; flex-direction: column; gap: 8px; padding-top: 6px; border-top: 1px dashed var(--line); }
.demo-section:first-of-type { border-top: 0; padding-top: 0; }
.demo-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.demo-btn {
display: flex; align-items: center; gap: 10px;
padding: 10px;
border-radius: 10px;
background: var(--surface-2);
border: 1px solid var(--line);
cursor: pointer;
font: inherit;
font-size: 12.5px;
color: var(--text);
text-align: left;
transition: transform 80ms, border-color 120ms, background 120ms;
}
.demo-btn:hover { transform: translateY(-1px); border-color: var(--grape); background: var(--surface); }
.demo-btn b { font-size: 13px; }
.demo-ic { width: 26px; height: 26px; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.demo-custom { display: grid; grid-template-columns: 110px 1fr auto; gap: 6px; }
.demo-custom select.input, .demo-custom input.input { width: 100%; font-size: 12.5px; padding: 0 10px; height: 36px; }
`;
window.DemoModeButton = DemoModeButton;
window.createDemoBus = createDemoBus;