// 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
Evento personalizado
{(customType === 'comment' || customType === 'dm') && ( setCustomKey(e.target.value)} /> )}
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;