/* === preview.jsx ==================================================== Renders the personalized Nous app inside the browser frame. Reactive to: - state.cosmetic (Level 1 → sidebar branding, user name) - state.data (Level 2 → suppliers, KPI values, SKUs) - state.persona (Level 3 → which screen opens + first message) - state.vars (Level 4 → variable substitution in text) - state.hotspots (Level 5 → overlayed callouts) - state.activeScreen (which Nous surface is on screen) ===================================================================== */ const { DOCOL, PERSONAS, FR_MAP, DYNAMIC_VARS } = window.STUDIO_DATA; /* ---------- helpers ---------- */ function firstName(full) { return (full || '').trim().split(' ')[0]; } function initialsOf(full) { return (full || '').trim().split(/\s+/).slice(0,2).map(s => s[0] || '').join('').toUpperCase(); } function findPersona(id) { return PERSONAS.find(p => p.id === id) || PERSONAS[0]; } function timeGreeting() { const h = new Date().getHours(); return h < 12 ? 'manhã' : h < 18 ? 'tarde' : 'noite'; } /* Pull the canonical value for a variable. Vars override; fall back to the original Level 1/3 source so the preview always renders something. */ function V(state, key, fallback) { const raw = state.vars?.[key]; if (raw !== undefined && String(raw).trim() !== '') return String(raw); return fallback ?? ''; } /* Replace {{varname}} with state.vars[varname] (or {{varname}} if missing). */ function subVars(text, vars) { if (typeof text !== 'string' || !text) return text; return text.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, k) => { const v = vars?.[k]; return (v !== undefined && String(v).trim() !== '') ? String(v) : `{{${k}}}`; }); } /* Render text that supports **bold** and \n as paragraphs. Used in question answers so AE can format the narrative simply. */ function RichText({ text, vars }) { const resolved = subVars(text || '', vars); const paragraphs = resolved.split(/\n\n+/); return ( <> {paragraphs.map((p, pi) => (

{p.split(/(\*\*[^*]+\*\*)/g).map((seg, si) => seg.startsWith('**') && seg.endsWith('**') ? {seg.slice(2, -2)} : {seg} )}

))} ); } /* Render a question's answer artifact (kpi-grid or custom-html). */ function AnswerArtifact({ artifact, vars }) { if (!artifact) return null; if (artifact.type === 'custom-html') { const html = subVars(artifact.html || '', vars); return (
); } /* default: kpi-grid */ return (
{subVars(artifact.title || 'Resposta', vars)}
{['bookmark', 'arrow-up', 'file-text', 'share', 'eye'].map((i, k) => ( ))}
{Array.isArray(artifact.meta) && artifact.meta.length > 0 && (
{artifact.meta.map((m, i) => ( {subVars(m, vars)} ))}
)}
{(artifact.kpis || []).map((k, i) => (
{subVars(k.label, vars)}
{subVars(k.value, vars)}
{k.sub &&
{subVars(k.sub, vars)}
}
))}
); } /* ---------- inline variable pill (level 4 preview mode) ---------- */ function Var({ k, vars, level4Open }) { const value = vars[k] ?? ''; if (level4Open) { return {`{{${k}}}`}; } return {value}; } /* ================================================================== PERSONALIZED SIDEBAR ================================================================== */ function PreviewSidebar({ state, onNavigate }) { const { cosmetic, persona, activeScreen } = state; const personaObj = findPersona(persona.activeId); const personaName = V(state, 'persona_nome', personaObj.name); const empresa = V(state, 'empresa', cosmetic.company); const recents = state.recentsByPersona?.[persona.activeId] || state.recentsByPersona?.ops || []; const primary = [ { id: 'chat', label: 'Chats', icon: 'message-circle' }, { id: 'mining', label: 'Process Mining', icon: 'bar-chart-3' }, { id: 'agents', label: 'Agentes', icon: 'briefcase' }, { id: 'roai', label: 'RoAI - Value Tracker', icon: 'trending-up' }, { id: 'rotinas', label: 'Rotinas', icon: 'clock' }, { id: 'achados', label: 'Achados', icon: 'file-text' }, ]; const secondary = [ { id: 'brain', label: 'Nous Brain', icon: 'brain' }, { id: 'playground',label: 'Playground', icon: 'flask' }, { id: 'admin', label: 'Admin Center',icon: 'settings' }, ]; return ( ); } /* ================================================================== CHAT — landing + thread depending on persona ================================================================== */ function PreviewChat({ state, level4Open, onSetState }) { const { persona, vars, discovery, activeCategory } = state; const personaObj = findPersona(persona.activeId); const personaName = V(state, 'persona_nome', personaObj.name); const fn = firstName(personaName); /* threadActive opens an example reply thread when set */ const threadActive = state.threadActive; const isAnalyst = personaObj.id === 'analyst'; if (threadActive) { /* Look up the answer from the discovery library when available */ const ref = state.threadQuestionRef; const question = ref && state.discovery?.[ref.category]?.find(q => q.id === ref.id); const answer = question?.answer; const questionLabel = question?.label || state.threadQuestion || 'Quais são os principais gargalos do nosso processo P2P no último trimestre?'; return (
{questionLabel} {answer ? (
{answer.artifact && }
) : (isAnalyst ? : )}
); } return ; } /* Chat-home landing — greeting, composer, categorias (clickable) + expanded discovery questions when a category is open. Mirrors the real nous.upflux.ai/chat empty state. */ function ChatLanding({ state, firstName, level4Open, onSetState }) { const { discovery, activeCategory } = state; const setCategory = (cat) => onSetState({ ...state, activeCategory: state.activeCategory === cat ? null : cat, }); const CATS = [ { id: 'estrategico', label: 'Estratégico' }, { id: 'gestao', label: 'Gestão' }, { id: 'operacional', label: 'Operacional' }, ]; return (

Como posso ajudar nesta {timeGreeting()},{' '} {level4Open ? : firstName}?

Ou explore por categoria: {CATS.map((c) => ( ))}
{activeCategory && (
{CATS.find(c => c.id === activeCategory).label}
    {(discovery?.[activeCategory] || []).map((q) => (
  • onSetState({ ...state, threadActive: true, threadQuestion: q.label, threadQuestionRef: { category: activeCategory, id: q.id }, })} > {q.id}
  • ))}
)}
); } /* Ops director answer — gargalos P2P, KPIs executivos */ function OpsAnswer({ state, level4Open }) { const { vars, cosmetic, data } = state; const empresa = V(state, 'empresa', cosmetic.company); const meta = V(state, 'meta_lead_time', '25 dias'); return (

Identifiquei gargalos principais no fluxo P2P da{' '} {level4Open ? : {empresa}} {' '}no último trimestre. O maior impacto está na etapa de{' '} aprovação de PO acima de , com lead-time médio de a meta do setor.

} value={} sentiment="danger" delta={{ value: <> vs meta, positive: false }} subtitle="último trimestre" /> } value={} sentiment="warning" delta={{ value: , positive: false }} subtitle={<> do total aprovado} /> } value={} sentiment="info" subtitle={<> fornecedores impactados} />
Etapas com maior lead-time · {empresa}

Próximo passo recomendado: {' '} automatizar o roteamento para aprovadores secundários quando o lead-time exceder 3 dias — estimativa de ganho de{' '} {level4Open ? : {meta}}{' '} no ciclo total.

); } /* Analyst answer — drill em cotações Wieland */ function AnalystAnswer({ state }) { const fornec = V(state, 'fornec_top', 'Wieland do Brasil'); return (

Encontrei 7 cotações abertas com a {fornec}, todas acima de 5 dias sem retorno. Total previsto: R$ 1,82M.

Cotações {fornec} · abertas
{[ ['RFQ-2026-0489', 'Latão CW617N · 40kg/lote', 'R$ 482.300', '11 dias'], ['RFQ-2026-0492', 'Latão CW602N · 25kg/lote', 'R$ 318.900', '9 dias'], ['RFQ-2026-0501', 'Cobre eletrolítico C11000', 'R$ 264.500', '8 dias'], ['RFQ-2026-0507', 'Latão CW724R · 18kg/lote', 'R$ 198.200', '7 dias'], ['RFQ-2026-0513', 'Bronze SAE 660 · usinagem', 'R$ 184.700', '7 dias'], ['RFQ-2026-0519', 'Vergalhão latão · diam. 25mm', 'R$ 218.400', '6 dias'], ['RFQ-2026-0522', 'Tarugos latão · fund. semi', 'R$ 156.000', '5 dias'], ].map((r, i) => ( ))}
RFQ Material Valor previsto Aberta há
{r[0]} {r[1]} {r[2]} {r[3]}

Sugestão: {' '} escalar para o gestor de categoria — todas estas RFQs envolvem o mesmo comprador (Renato F.) e o histórico mostra resposta em 48h após escalação.

); } /* ================================================================== AGENTES — Docol-specific automations ================================================================== */ /* ================================================================== AGENTES — delegates to window.PreviewAgents (in agents-achados.jsx) ================================================================== */ /* Replaced by the version in agents-achados.jsx via window.PreviewAgents */ /* ================================================================== ROTINAS ================================================================== */ function PreviewRotinas({ state }) { const { cosmetic } = state; const empresa = V(state, 'empresa', cosmetic.company); const routines = [ { name: 'Sync SAP MM · POs do dia', when: 'A cada 30 min', last: 'há 12 min', status: 'live', desc: 'Importa POs criadas no SAP MM e atualiza o grafo de eventos do Process Mining.' }, { name: 'Cálculo de KPIs · ciclo P2P', when: 'Diário · 06:00', last: 'hoje 06:01', status: 'live', desc: 'Recalcula lead-time, % aprovado fora do fluxo, glosa de NF e populariza o RoAI Value Tracker.' }, { name: 'Notificação WhatsApp · gestores', when: 'Diário · 08:30', last: 'hoje 08:30', status: 'live', desc: 'Envia para Marcos Silveira + Camila Reis um snapshot do dia anterior com 3 KPIs.' }, { name: 'Backup de eventos · Joinville', when: 'Diário · 23:00', last: 'ontem 23:01',status: 'live', desc: 'Snapshot dos eventos do polo de Joinville · 30 dias de retenção em S3.' }, { name: 'Reindex semântico · base de dados', when: 'Semanal · sábado',last: '17 mai', status: 'paused', desc: 'Reconstrói o índice semântico das descrições de material para melhorar a busca em pt-BR.' }, { name: 'Sync Coupa · cotações abertas', when: 'A cada 1h', last: 'há 7 min', status: 'live', desc: 'Importa cotações abertas no Coupa para o pipeline de Achados.' }, ]; return (

Rotinas

Tarefas agendadas que mantém o Nous em sincronia com SAP MM, Coupa e fontes de dados da {empresa}.

{routines.map((r) => ( ))}
Rotina Recorrência Última execução Estado
{r.name}
{r.desc}
{r.when} {r.last} {r.status === 'live' ? 'Ao vivo' : r.status === 'paused' ? 'Pausada' : 'Rascunho'}
); } /* ================================================================== Top-level preview — picks the screen + overlays hotspots ================================================================== */ function NousPreview({ state, onNavigate, level4Open, onOpenFullMap, onSetState }) { let body; switch (state.activeScreen) { case 'mining': body = ; break; case 'agents': body = React.createElement(window.PreviewAgents, { state, onSetState }); break; case 'achados': body = React.createElement(window.PreviewAchados, { state, onSetState }); break; case 'rotinas': body = ; break; case 'roai': body = React.createElement(window.PreviewRoAI, { state, onSetState }); break; case 'chat': default: body = ; break; } return (
{body}
); } Object.assign(window, { NousPreview });