/* === 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 (
{applied ? : num}
{name}
{num === 4 && Template }
{num === 5 && Beta }
{blurb}
{status}
);
}
/* ==================================================================
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
Trocar
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 (
toggleArr('suppliers', s.name)}
>
{on && }
{s.name}
);
})}
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 (
toggleArr('skuCategories', s.name)}
>
{on && }
{s.name}
·{s.count}
);
})}
Unidades fabris
{['Joinville (matriz)', 'Manaus (Polo Industrial)'].map((site) => {
const on = d.sites.includes(site);
return (
toggleArr('sites', site)}
>
{on && }
{site}
);
})}
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) => (
setPersona(p.id)}
>
{(p.name.split(' ').map(s => s[0])).slice(0,2).join('')}
{p.name}
{p.role}
{p.firstScreen}
))}
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) => (
))}
Adicionar variá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 })}
/>
reorderQuestion(cat.id, q.id, 'up')}
title="Subir"
disabled={list.findIndex(x => x.id === q.id) === 0}
>
reorderQuestion(cat.id, q.id, 'down')}
title="Descer"
disabled={list.findIndex(x => x.id === q.id) === list.length - 1}
>
toggleExpanded(key)}
title={hasAnswer ? 'Editar resposta' : 'Adicionar resposta'}
>
removeQuestion(cat.id, q.id)}
title="Remover pergunta"
>
{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)}
/>
)}
);
})}
addQuestion(cat.id)}>
Adicionar pergunta
);
})}
{/* ===== 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()}>
reorder(h.id, 'up')} title="Subir"
disabled={i === 0}>
reorder(h.id, 'down')} title="Descer"
disabled={i === screenHotspots.length - 1}>
removeHotspot(h.id)} title="Excluir">
{selected && (
Tipo
{Object.entries(HOTSPOT_KINDS).map(([k, v]) => (
updateHotspot(h.id, { kind: k })}
style={h.kind === k ? { borderColor: v.color, color: v.color } : null}
>
{v.label}
))}
Título
updateHotspot(h.id, { title: e.target.value })}
/>
Texto / descrição
)}
);
})}
Adicionar hotspot na tela {state.activeScreen}
);
}
/* ==================================================================
The full panel
================================================================== */
function BuilderPanel({ state, setState }) {
const setExpanded = (n) => setState({ ...state, expanded: state.expanded === n ? null : n });
const LEVELS = [
{
num: 1,
name: 'Cosmética',
blurb: 'Logo, nome, cores e usuário logado.',
status: 'Aplicado',
applied: true,
Body: Level1Body,
},
{
num: 2,
name: 'Dados realistas do segmento',
blurb: 'Fornecedores, SKUs, KPIs do setor do prospect.',
status: 'Aplicado',
applied: true,
Body: Level2Body,
},
{
num: 3,
name: 'Fluxo personalizado por persona',
blurb: 'Ramificações por audiência: Ops, CFO, CIO, Analista.',
status: 'Aplicado',
applied: true,
Body: Level3Body,
},
{
num: 4,
name: 'Variáveis dinâmicas (template)',
blurb: 'Preencha um form, gere a demo.',
status: 'Aplicado',
applied: true,
Body: Level4Body,
},
{
num: 5,
name: 'Conteúdo guiado contextual',
blurb: 'Tooltips, vídeos do AE e CTAs no fluxo.',
status: '3 de 4 hotspots',
applied: false,
partial: true,
Body: Level5Body,
},
];
const totalApplied = LEVELS.filter(l => l.applied).length;
const progressPct = (totalApplied / LEVELS.length) * 100;
return (
Personalização da demo
5 níveis em camadas. Comece pelo básico (cosmética), pare quando a
demo estiver convincente para o prospect.
{LEVELS.map((L) => {
const open = state.expanded === L.num;
return (
setExpanded(L.num)}>
{L.applied ? : L.num}
{L.name}
{L.num === 4 && Template }
{L.num === 5 && Beta }
{L.blurb}
{L.status}
{open && }
);
})}
Pronto para a call
Última edição há 2 min · backup automático
Compartilhar
);
}
Object.assign(window, { BuilderPanel });
/* ==================================================================
QuestionAnswerEditor — expanded form to edit a question's response
(narrative text + KPI grid or custom HTML), used inside Level 5.
================================================================== */
function QuestionAnswerEditor({ question, onUpdateAnswer, onUpdateArtifact, onUpdateKpi, onAddKpi, onRemoveKpi }) {
const answer = question.answer || {};
const artifact = answer.artifact || { type: 'kpi-grid', kpis: [] };
const switchType = (newType) => {
if (newType === 'kpi-grid') {
onUpdateArtifact({
type: 'kpi-grid',
title: artifact.title || 'Resumo',
meta: artifact.meta || ['Período', 'Escopo'],
kpis: artifact.kpis && artifact.kpis.length ? artifact.kpis : [
{ label: 'KPI PRINCIPAL', value: '0', sub: 'descrição', highlight: true },
],
});
} else {
onUpdateArtifact({
type: 'custom-html',
html: artifact.html || '\n
Resposta para {{empresa}} \n
Substitua este HTML pelo template do prospect. Use {{variaveis}} onde quiser injetar dados.
\n
',
});
}
};
/* Trigger an upload-then-read for custom HTML */
const fileRef = React.useRef(null);
const uploadHTML = (e) => {
const f = e.target.files?.[0];
if (!f) return;
const r = new FileReader();
r.onload = () => onUpdateArtifact({ type: 'custom-html', html: String(r.result || '') });
r.readAsText(f);
e.target.value = '';
};
return (
Resposta narrativa
use **negrito** e {'{{var}}'}
Tipo de artefato
switchType('kpi-grid')}
>
KPI grid
switchType('custom-html')}
>
HTML custom
{artifact.type !== 'custom-html' ? (
<>
KPIs
{(artifact.kpis || []).map((k, i) => (
))}
Adicionar KPI
>
) : (
)}
);
}
Object.assign(window, { QuestionAnswerEditor });