/* === 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} )}

))} ); } /* Resolve an `uploaded:` HTML artifact from IndexedDB into a Blob URL and render it like a normal iframe report. */ function StoredReport({ idbKey, title, meta, height }) { const [url, setUrl] = React.useState(null); const [missing, setMissing] = React.useState(false); React.useEffect(() => { let objUrl; let alive = true; window.STUDIO_DATA.idbGetHtml(idbKey).then(text => { if (!alive) return; if (text == null) { setMissing(true); return; } objUrl = URL.createObjectURL(new Blob([text], { type: 'text/html' })); setUrl(objUrl); }).catch(() => setMissing(true)); return () => { alive = false; if (objUrl) URL.revokeObjectURL(objUrl); }; }, [idbKey]); return (
{title}
{url && ( Abrir em nova aba )}
{meta.length > 0 && (
{meta.map((m, i) => ( {m} ))}
)} {missing ? (
Arquivo HTML não encontrado neste navegador. Reenvie o arquivo pelo gerenciador de Respostas P2P.
) : url ? (