/* === builder.jsx ==================================================== Left panel — the 5-level personalization accordion + footer. ===================================================================== */ const { BRAND_SWATCHES, FR_MAP, SUPPLIERS, SKU_CATEGORIES, PERSONAS, DYNAMIC_VARS, HOTSPOT_KINDS } = window.STUDIO_DATA; /* ================================================================== Level header (each accordion row) ================================================================== */ function LevelHead({ num, name, blurb, status, open, applied, partial, onToggle }) { const cls = `lvl${open ? ' open' : ''}${applied ? ' applied' : ''}${partial ? ' partial' : ''}`; return (
); } /* ================================================================== LEVEL 1 — COSMÉTICA ================================================================== */ function Level1Body({ state, setState }) { const c = state.cosmetic; /* When user changes Level 1 cosmetic values, also sync the matching Level 4 variables so the demo stays consistent end-to-end. */ const setC = (patch) => { const next = { ...c, ...patch }; const varsPatch = { ...state.vars }; if ('company' in patch) varsPatch.empresa = patch.company; setState({ ...state, cosmetic: next, vars: varsPatch }); }; return (
Logo do prospect
{c.logoMonogram}
docol-logo-2024.svg
PNG · 280×64 · capturado de docol.com.br
O Nous mantém sua marca — cor, font e botoes. Logo do prospect aparece como{' '} contexto (no header, na assinatura de hotspots), não substitui o Nous.
Nome da empresa
setC({ company: e.target.value })} />
Aparece no sidebar, em saudações e no header. Auto-preenche {'{{empresa}}'}.
Find & replace global (5 mapeamentos)
{FR_MAP.map((m, i) => (
{m.from} {m.to}
))}
); } /* ================================================================== LEVEL 2 — DADOS REALISTAS (Docol manufatura/P2P) ================================================================== */ function Level2Body({ state, setState }) { const d = state.data; const setD = (patch) => setState({ ...state, data: { ...d, ...patch } }); const toggleArr = (key, val) => { const arr = d[key] || []; const next = arr.includes(val) ? arr.filter(x => x !== val) : [...arr, val]; setD({ [key]: next }); }; const fmtBRL = (n) => 'R$ ' + n.toLocaleString('pt-BR'); return (
Fornecedores reais ({d.suppliers.length} ativos)
{SUPPLIERS.map((s) => { const on = d.suppliers.includes(s.name); return ( ); })}
Substitui mock por fornecedores reais do catálogo Docol. Aparecem em cotações, gargalos, listas de POs.
Categorias de SKU
{SKU_CATEGORIES.map((s) => { const on = d.skuCategories.includes(s.name); return ( ); })}
Unidades fabris
{['Joinville (matriz)', 'Manaus (Polo Industrial)'].map((site) => { const on = d.sites.includes(site); return ( ); })}
Faixa de valor de PO (BRL)
setD({ valueRange: [d.valueRange[0], Number(e.target.value)] })} /> {fmtBRL(d.valueRange[0])} – {fmtBRL(d.valueRange[1])}
Calibra os exemplos: POs > R$ 250k entram em "alçada" nos KPIs e agentes.
Aplicar formatação BR (R$ · 1.234,56 · 4d 12h)
); } /* ================================================================== LEVEL 3 — PERSONA / BRANCHING ================================================================== */ function Level3Body({ state, setState }) { const setPersona = (id) => { const p = PERSONAS.find(x => x.id === id); setState({ ...state, persona: { ...state.persona, activeId: id }, activeScreen: p.firstScreen, /* Sync vars.persona_nome so Level 4 reflects the choice. */ vars: { ...state.vars, persona_nome: p.name }, }); }; return (
Persona principal (entra primeiro)
{PERSONAS.map((p) => ( ))}
Cada persona abre em uma tela diferente. O preview reflete a escolha em tempo real.
Ramificação da demo
{PERSONAS.map((p) => (
{p.role.split(' ')[0]} {p.branchSummary}
))}
Permitir prospect alternar entre personas
); } /* ================================================================== LEVEL 4 — VARIÁVEIS DINÂMICAS ================================================================== */ function Level4Body({ state, setState }) { const setVar = (k, v) => setState({ ...state, vars: { ...state.vars, [k]: v } }); const removeVar = (k) => { const next = { ...state.vars }; delete next[k]; setState({ ...state, vars: next }); }; /* Map of where each var is used — displayed inline so the AE knows what changes when they edit a value. */ const USAGE = { empresa: ['Sidebar', 'Chat', 'Agentes', 'RoAI', 'Process Mining'], setor: ['Header da demo'], pain_point: ['Pergunta inicial', 'Hotspot vídeo do AE'], ae_nome: ['Topbar do Studio', 'Hotspots de vídeo'], persona_nome: ['Sidebar (avatar)', 'Greeting do chat', 'Persona switcher'], volume_mensal: ['Process Mining (subtítulo)'], fornec_top: ['Chat (cotações Wieland)', 'Agentes (Cotação)'], meta_lead_time: ['Chat (próximo passo)'], }; /* User-added vars (any key not in the default set) */ const knownKeys = new Set(DYNAMIC_VARS.map(v => v.key)); const customKeys = Object.keys(state.vars).filter(k => !knownKeys.has(k)); const addCustomVar = () => { /* Find a unique name like custom_1, custom_2... */ let i = 1; while (state.vars[`custom_${i}`] !== undefined) i++; setVar(`custom_${i}`, 'novo valor'); }; return (
Estes valores são injetados em textos, KPIs e títulos do preview. Edite uma vez, aplique em todas as superfícies.
{DYNAMIC_VARS.map((v) => { const usage = USAGE[v.key] || []; return (
{`{{${v.key}}}`} setVar(v.key, e.target.value)} />
{usage.length > 0 && (
Usado em: {usage.map((u, i) => ( {u} ))}
)}
); })} {customKeys.map((k) => (
{`{{${k}}}`} setVar(k, e.target.value)} />
Personalizada Referencie como {`{{${k}}}`} em qualquer texto editável
))}
Editando aqui? O preview destaca onde cada variável aparece como{' '} {'{{var}}'} .
); } /* ================================================================== LEVEL 5 — CONTEÚDO GUIADO (HOTSPOTS) ================================================================== */ function Level5Body({ state, setState }) { const screenHotspots = state.hotspots.filter(h => h.screen === state.activeScreen); const setShow = (id) => setState({ ...state, showHotspotPreview: id }); /* Discovery questions editor */ const updateQuestion = (cat, qid, patch) => { const next = (state.discovery?.[cat] || []).map(q => q.id === qid ? { ...q, ...patch } : q); setState({ ...state, discovery: { ...state.discovery, [cat]: next } }); }; const reorderQuestion = (cat, qid, dir) => { const list = state.discovery?.[cat] || []; const idx = list.findIndex(q => q.id === qid); const swap = dir === 'up' ? idx - 1 : idx + 1; if (swap < 0 || swap >= list.length) return; const next = [...list]; [next[idx], next[swap]] = [next[swap], next[idx]]; setState({ ...state, discovery: { ...state.discovery, [cat]: next } }); }; const updateAnswer = (cat, qid, patch) => { const next = (state.discovery?.[cat] || []).map(q => q.id === qid ? { ...q, answer: { ...(q.answer || {}), ...patch } } : q ); setState({ ...state, discovery: { ...state.discovery, [cat]: next } }); }; const updateArtifact = (cat, qid, patch) => { const next = (state.discovery?.[cat] || []).map(q => { if (q.id !== qid) return q; const a = q.answer || {}; const art = a.artifact || { type: 'kpi-grid', kpis: [] }; return { ...q, answer: { ...a, artifact: { ...art, ...patch } } }; }); setState({ ...state, discovery: { ...state.discovery, [cat]: next } }); }; const updateKpi = (cat, qid, kIdx, patch) => { const list = state.discovery?.[cat] || []; const q = list.find(x => x.id === qid); if (!q) return; const a = q.answer || {}; const art = a.artifact || { type: 'kpi-grid', kpis: [] }; const kpis = [...(art.kpis || [])]; kpis[kIdx] = { ...kpis[kIdx], ...patch }; updateArtifact(cat, qid, { kpis }); }; const addKpi = (cat, qid) => { const list = state.discovery?.[cat] || []; const q = list.find(x => x.id === qid); const art = q?.answer?.artifact || { type: 'kpi-grid', kpis: [] }; updateArtifact(cat, qid, { kpis: [...(art.kpis || []), { label: 'NOVO KPI', value: '0', sub: 'descrição' }] }); }; const removeKpi = (cat, qid, kIdx) => { const list = state.discovery?.[cat] || []; const q = list.find(x => x.id === qid); const art = q?.answer?.artifact; if (!art?.kpis) return; updateArtifact(cat, qid, { kpis: art.kpis.filter((_, i) => i !== kIdx) }); }; const removeQuestion = (cat, qid) => { const next = (state.discovery?.[cat] || []).filter(q => q.id !== qid); setState({ ...state, discovery: { ...state.discovery, [cat]: next } }); }; const addQuestion = (cat) => { const list = state.discovery?.[cat] || []; const nextId = String(list.length + 1); const next = [...list, { id: nextId, label: 'Nova pergunta…' }]; setState({ ...state, discovery: { ...state.discovery, [cat]: next } }); }; const toggleExpanded = (key) => { setState({ ...state, expandedDiscovery: state.expandedDiscovery === key ? null : key }); }; const CATS = [ { id: 'estrategico', label: 'Estratégico' }, { id: 'gestao', label: 'Gestão' }, { id: 'operacional', label: 'Operacional' }, ]; return (
{/* ===== Discovery questions ===== */}
Perguntas do discovery — chat home
Quando o prospect clica numa categoria no chat, vê estas perguntas. Edite com base no que capturou no discovery comercial.
{CATS.map((cat) => { const list = state.discovery?.[cat.id] || []; return (
{cat.label} {list.length} perguntas
{list.map((q) => { const key = `${cat.id}.${q.id}`; const isOpen = state.expandedDiscovery === key; const hasAnswer = !!q.answer?.text; return (
updateQuestion(cat.id, q.id, { id: e.target.value })} title="Número da pergunta" /> updateQuestion(cat.id, q.id, { label: e.target.value })} />
{isOpen && ( updateAnswer(cat.id, q.id, patch)} onUpdateArtifact={(patch) => updateArtifact(cat.id, q.id, patch)} onUpdateKpi={(idx, patch) => updateKpi(cat.id, q.id, idx, patch)} onAddKpi={() => addKpi(cat.id, q.id)} onRemoveKpi={(idx) => removeKpi(cat.id, q.id, idx)} /> )}
); })}
); })}
{/* ===== Hotspots (existing) ===== */}
Hotspots e callouts
Hotspots na tela {state.activeScreen} · {screenHotspots.length} ativos. Clique numa linha para editar; arraste o pin no preview para posicionar.
); } /* ================================================================== Hotspot CRUD editor — used inside Level 5 ================================================================== */ function HotspotEditor({ state, setState, screenHotspots }) { const sel = state.showHotspotPreview; const setShow = (id) => setState({ ...state, showHotspotPreview: id }); const updateHotspot = (id, patch) => { const next = state.hotspots.map(h => h.id === id ? { ...h, ...patch } : h); setState({ ...state, hotspots: next }); }; const removeHotspot = (id) => { const next = state.hotspots.filter(h => h.id !== id); setState({ ...state, hotspots: next, showHotspotPreview: next[0]?.id }); }; const addHotspot = () => { const id = 'h' + Date.now().toString(36); const next = [...state.hotspots, { id, screen: state.activeScreen, x: 0.45, y: 0.40, kind: 'tooltip', title: 'Novo hotspot', text: 'Descreva o que esse callout destaca para o prospect.', link: '', cta: '', videoUrl: '', videoDuration: '', }]; setState({ ...state, hotspots: next, showHotspotPreview: id }); }; const reorder = (id, dir) => { const allOnScreen = state.hotspots.filter(h => h.screen === state.activeScreen); const idx = allOnScreen.findIndex(h => h.id === id); const swap = dir === 'up' ? idx - 1 : idx + 1; if (swap < 0 || swap >= allOnScreen.length) return; /* Swap their positions in the master array */ const newArr = [...state.hotspots]; const aIdx = newArr.findIndex(h => h.id === allOnScreen[idx].id); const bIdx = newArr.findIndex(h => h.id === allOnScreen[swap].id); [newArr[aIdx], newArr[bIdx]] = [newArr[bIdx], newArr[aIdx]]; setState({ ...state, hotspots: newArr }); }; return (
{screenHotspots.map((h, i) => { const kind = HOTSPOT_KINDS[h.kind]; const selected = sel === h.id; return (
setShow(selected ? null : h.id)}> {i + 1} {h.title} {kind.label} {h.cta && · "{h.cta}"} {h.videoDuration && · {h.videoDuration}}
e.stopPropagation()}>
{selected && (
Tipo
{Object.entries(HOTSPOT_KINDS).map(([k, v]) => ( ))}
Título
updateHotspot(h.id, { title: e.target.value })} />
Texto / descrição