/* === app.jsx ======================================================== Top-level: topbar + builder panel + canvas, plus preview mode, edit mode and share modal. ===================================================================== */ const { DEFAULT_STATE, AE, DOCOL, PERSONAS } = window.STUDIO_DATA; /* ---------- Top bar ---------- */ function StudioTopbar({ state, setState, demo, onBackToLibrary }) { const screens = [ { id: 'chat', label: 'Chat', icon: 'message-circle' }, { id: 'mining', label: 'Process Mining', icon: 'bar-chart-3' }, { id: 'agents', label: 'Agentes', icon: 'briefcase' }, { id: 'roai', label: 'RoAI', icon: 'trending-up' }, { id: 'rotinas', label: 'Rotinas', icon: 'clock' }, ]; return (
/ Salvo automaticamente
{screens.map((s) => ( ))}
{(state.vars?.ae_nome || AE.name).split(/\s+/).slice(0,2).map(s => s[0] || '').join('').toUpperCase()}
); } /* ---------- Canvas toolbar (above the browser frame) ---------- */ function CanvasToolbar({ state, setState }) { const layers = [ { n: 1, label: 'Cosmética', key: 'L1' }, { n: 2, label: 'Dados', key: 'L2' }, { n: 3, label: 'Persona', key: 'L3' }, { n: 4, label: 'Variáveis', key: 'L4' }, { n: 5, label: 'Guiado', key: 'L5' }, ]; return (
Camadas aplicadas
{layers.map((L) => { const lvlState = state.levels[L.n]; const on = lvlState && lvlState.enabled; return ( ); })}
Preview ao vivo · {state.activeScreen}
Ajustado
); } /* ---------- The browser frame around the Nous preview ---------- */ function BrowserFrame({ state, setState, level4Open, onOpenFullMap, hideChrome }) { const personaObj = PERSONAS.find(p => p.id === state.persona.activeId) || PERSONAS[0]; const urlPath = state.activeScreen === 'chat' ? 'chat' : state.activeScreen === 'mining' ? 'process-mining' : state.activeScreen === 'agents' ? 'agentes' : state.activeScreen === 'rotinas' ? 'rotinas' : 'roai'; const screenHotspots = state.hotspots.filter(h => h.screen === state.activeScreen); /* The callout only appears when the user explicitly clicks a pin — and only the most recently clicked one. Default = no callout open. */ const openCallout = state.openHotspotId ? screenHotspots.find(h => h.id === state.openHotspotId) : null; return (
{!hideChrome && (
nous.upflux.ai/ {urlPath} ?demo={state.cosmetic.company.toLowerCase()}
)}
setState({ ...state, activeScreen: id })} level4Open={level4Open} onOpenFullMap={onOpenFullMap} onSetState={setState} /> {state.persona.branchEnabled && state.levels[3].enabled && (() => { const displayName = (state.vars?.persona_nome && String(state.vars.persona_nome).trim()) || personaObj.name; return (
Visualizando como {displayName.split(' ')[0]} ({personaObj.role.split(' ')[0]})
); })()} {state.levels[5].enabled && screenHotspots.map((h, i) => { const kind = window.STUDIO_DATA.HOTSPOT_KINDS[h.kind] || window.STUDIO_DATA.HOTSPOT_KINDS.tooltip; return (
{ /* Click toggles: open if closed, close if already open */ e.stopPropagation(); setState({ ...state, openHotspotId: state.openHotspotId === h.id ? null : h.id, showHotspotPreview: h.id, }); }} onMouseDown={(e) => { /* Drag the pin to reposition */ e.preventDefault(); const body = e.currentTarget.closest('.browser-body'); if (!body) return; const rect = body.getBoundingClientRect(); const onMove = (ev) => { const nx = Math.max(0.04, Math.min(0.96, (ev.clientX - rect.left) / rect.width)); const ny = Math.max(0.04, Math.min(0.96, (ev.clientY - rect.top) / rect.height)); setState({ ...state, showHotspotPreview: h.id, hotspots: state.hotspots.map(x => x.id === h.id ? { ...x, x: nx, y: ny } : x), }); }; const onUp = () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); }; window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); }} > {i + 1}
); })} {state.levels[5].enabled && openCallout && (() => { const kind = window.STUDIO_DATA.HOTSPOT_KINDS[openCallout.kind] || window.STUDIO_DATA.HOTSPOT_KINDS.tooltip; return (
{kind.label} {' · '} {openCallout.title} {openCallout.videoDuration && ` · ${openCallout.videoDuration}`}
{openCallout.text}
{openCallout.cta && ( { if (!openCallout.link) e.preventDefault(); }} > {openCallout.cta} )}
); })()} {state.editMode && (
Modo edição · clique em qualquer texto ou número
)}
); } /* ---------- Full-map modal (Process Mining) ---------- */ function FullMapModal({ state, onClose }) { return (
e.stopPropagation()}>
Mapa completo · 1 — Compras ao Pagamento
451.756 casos · 38 variantes · janela 90 dias · {state.cosmetic.company}
Cobertura: 85,77%{' '} dos casos representados nesta visão simplificada.
); } /* ---------- State encoding for shareable URL ---------- */ function encodeState(state) { /* Strip transient fields that shouldn't be in a shareable URL */ const payload = { activeScreen: state.activeScreen, cosmetic: state.cosmetic, data: state.data, persona: state.persona, vars: state.vars, hotspots: state.hotspots, levels: state.levels, edits: state.edits, }; try { const json = JSON.stringify(payload); /* URL-safe base64 */ return btoa(unescape(encodeURIComponent(json))) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } catch (e) { return ''; } } function decodeState(hash) { try { const padded = hash + '='.repeat((4 - hash.length % 4) % 4); const json = decodeURIComponent(escape(atob(padded.replace(/-/g, '+').replace(/_/g, '/')))); return JSON.parse(json); } catch (e) { return null; } } function copyToClipboard(text) { /* Modern API first */ if (navigator.clipboard && window.isSecureContext) { return navigator.clipboard.writeText(text).then(() => true).catch(() => fallbackCopy(text)); } return Promise.resolve(fallbackCopy(text)); } function fallbackCopy(text) { /* Works in sandboxed iframes where clipboard API is blocked */ try { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; ta.style.top = '0'; ta.setAttribute('readonly', ''); document.body.appendChild(ta); ta.select(); ta.setSelectionRange(0, text.length); const ok = document.execCommand('copy'); document.body.removeChild(ta); return ok; } catch (e) { return false; } } /* ---------- Share modal ---------- */ const PROD_DOMAIN = 'demo.upflux.ai'; function ShareModal({ state, demo, onClose }) { const encoded = React.useMemo(() => encodeState(state), [state]); /* Live URL — actually works because this HTML serves itself */ const liveUrl = `${location.origin}${location.pathname}#demo=${encoded}`; /* Prod URL — what it'll look like after deploying to Hostinger */ const prodUrl = `https://${PROD_DOMAIN}/#demo=${encoded}`; const shareUrl = liveUrl; const [copied, setCopied] = React.useState(false); const [downloaded, setDownloaded] = React.useState(false); const urlRef = React.useRef(null); const copy = () => { /* Fire copy attempts (may silently fail in sandboxed iframes), then always show optimistic feedback + select the text so user can Ctrl+C */ copyToClipboard(shareUrl); if (urlRef.current) { urlRef.current.focus(); urlRef.current.select(); } setCopied(true); setTimeout(() => setCopied(false), 2200); }; const downloadHtml = () => { /* Build a small standalone HTML file that: 1) sets the hash to the encoded state 2) redirects to the live demo URL (or shows instructions) This is the lightweight version — a true single-file bundle would inline all assets. */ const html = ` Demo Nous · ${state.cosmetic.company}

Demo Nous — ${state.cosmetic.company}

Abrindo a demo personalizada... Se não redirecionar automaticamente:

${shareUrl}

`; const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `demo-nous-${state.cosmetic.company.toLowerCase()}.html`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); setDownloaded(true); setTimeout(() => setDownloaded(false), 2200); }; const openInNewTab = () => window.open(shareUrl, '_blank', 'noopener'); return (
e.stopPropagation()}>
Compartilhar demo · {state.cosmetic.company}
5 níveis aplicados · persona{' '} {(PERSONAS.find(p => p.id === state.persona.activeId) || PERSONAS[0]).name.split(' ')[0]} {' · '}{state.activeScreen}
Link de teste (atual) Funciona já — pode mandar para o prospect
e.target.select()} onClick={(e) => e.target.select()} />
Estado da demo encodado no hash · {demo?.version || window.STUDIO_DATA.STUDIO_VERSION}.
URL de produção (após deploy) Como ficará quando o Hostinger estiver configurado
e.target.select()} />
Alternativas
O que o prospect vai ver
Nous personalizado para {state.cosmetic.company}, abrindo na tela {state.activeScreen} como{' '} {(PERSONAS.find(p => p.id === state.persona.activeId) || PERSONAS[0]).role}. Sem chrome do Studio.
); } /* ---------- Preview-only mode (no studio chrome) ---------- */ function PreviewMode({ state, setState, isProspect, onBackToLibrary }) { return (
U {isProspect ? <>Demo personalizada · UpFlux Nous : 'Pré-visualização · como o prospect verá'} {state.cosmetic.company}
{!isProspect && onBackToLibrary && ( )} {!isProspect && ( )} {!isProspect && ( )} {isProspect && ( Conhecer UpFlux )}
setState({ ...state, showFullMap: true })} hideChrome={false} />
); } /* ---------- App ---------- */ const today = () => new Date().toISOString().slice(0, 10); const uid = () => Math.random().toString(36).slice(2, 9); const STUDIO_PASSWORD = 'upflux2026'; function App() { /* --- Auth ----------------------------------------------------- */ const [authed, setAuthed] = React.useState(() => { try { return sessionStorage.getItem('studio_auth') === 'ok'; } catch { return false; } }); /* --- Library (localStorage with seed fallback) ---------------- */ const [library, setLibrary] = React.useState(() => { try { const raw = localStorage.getItem('studio_library_v1'); if (raw) { const parsed = JSON.parse(raw); if (Array.isArray(parsed) && parsed.length > 0) return parsed; } } catch {} return window.STUDIO_DATA.LIBRARY_SEED; }); React.useEffect(() => { try { localStorage.setItem('studio_library_v1', JSON.stringify(library)); } catch {} }, [library]); /* --- Hash demo (prospect viewer mode, bypasses auth) ----------- */ const hashDemoRef = React.useRef(undefined); if (hashDemoRef.current === undefined) { const m = (location.hash || '').match(/#demo=([\w-]+)/); hashDemoRef.current = m ? decodeState(m[1]) : null; } /* --- Currently edited demo ------------------------------------ */ const [currentDemoId, setCurrentDemoId] = React.useState(null); const currentDemo = library.find(d => d.id === currentDemoId); /* setState that targets the current demo's state field */ const setDemoState = (next) => { setLibrary(lib => lib.map(d => { if (d.id !== currentDemoId) return d; const newState = typeof next === 'function' ? next(d.state) : next; return { ...d, state: newState, updatedAt: today(), version: window.STUDIO_DATA.STUDIO_VERSION, }; })); }; /* For the EditsProvider that mutates demo.state.edits specifically */ const onEditsChange = (newEdits) => { setDemoState(s => ({ ...s, edits: newEdits })); }; /* --- Library actions ----------------------------------------- */ const openDemo = (id) => { /* Clear transient UI flags on every open */ setLibrary(lib => lib.map(d => d.id === id ? { ...d, state: { ...d.state, editMode: false, previewMode: false, showShareModal: false, showFullMap: false, } } : d)); setCurrentDemoId(id); }; const cloneDemo = (id) => { const src = library.find(d => d.id === id); if (!src) return; const newId = `${src.prospect.toLowerCase().replace(/\s/g, '')}-${uid()}`; const copy = { ...JSON.parse(JSON.stringify(src)), id: newId, name: `${src.name} (cópia)`, status: 'draft', version: window.STUDIO_DATA.STUDIO_VERSION, createdAt: today(), updatedAt: today(), views: 0, shareCount: 0, }; setLibrary([copy, ...library]); openDemo(newId); }; const deleteDemo = (id) => { setLibrary(library.filter(d => d.id !== id)); }; const createNewDemo = (prospectName) => { const newId = `${(prospectName || 'novo').toLowerCase().replace(/\s/g, '')}-${uid()}`; const fresh = JSON.parse(JSON.stringify(window.STUDIO_DATA.DEFAULT_STATE)); fresh.cosmetic.company = prospectName || 'Novo prospect'; fresh.cosmetic.logoMonogram = (prospectName || 'N').charAt(0).toUpperCase(); fresh.vars.empresa = prospectName || 'Novo prospect'; const entry = { id: newId, name: prospectName || 'Nova demo', prospect: prospectName || 'Novo prospect', industry: '—', persona: 'Marcos Silveira', personaRole: 'Diretor de Operações', owner: 'Felipe Andrade', ownerInitials: 'FA', status: 'draft', version: window.STUDIO_DATA.STUDIO_VERSION, createdAt: today(), updatedAt: today(), views: 0, shareCount: 0, accent: '#6D4ADD', state: fresh, thumbHint: 'Nova personalização', }; setLibrary([entry, ...library]); openDemo(newId); }; /* --- Routing ------------------------------------------------- */ /* 1) Hash mode — prospect viewer */ if (hashDemoRef.current) { const viewerState = { ...hashDemoRef.current, editMode: false, previewMode: true, showShareModal: false, showFullMap: false, edits: hashDemoRef.current.edits || {}, }; return ( ); } /* 2) Login */ if (!authed) { return { try { sessionStorage.setItem('studio_auth', 'ok'); } catch {} setAuthed(true); }} />; } /* 3) Library home */ if (!currentDemo) { return ( { try { sessionStorage.removeItem('studio_auth'); } catch {} setAuthed(false); }} /> ); } /* 4) Studio editor for currentDemo */ const state = currentDemo.state; const level4Open = state.expanded === 4; const setState = setDemoState; const backToLibrary = () => setCurrentDemoId(null); if (state.previewMode) { return ( {state.showShareModal && ( setState({ ...state, showShareModal: false })} /> )} ); } return (
setState({ ...state, showFullMap: true })} />
{state.showShareModal && ( setState({ ...state, showShareModal: false })} /> )} {state.showFullMap && ( setState({ ...state, showFullMap: false })} /> )}
); } /* ---------- ViewerOnly (prospect view) ---------- */ function ViewerOnly({ initial }) { const [state, setState] = React.useState(initial); const onEditsChange = (e) => setState(s => ({ ...s, edits: e })); return ( ); } ReactDOM.createRoot(document.getElementById('root')).render();