/* === 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 (
U
UpFlux Studio
/
{demo?.name || `Demo · ${state.cosmetic.company}`}
{demo?.version && {demo.version} }
Salvo automaticamente
{screens.map((s) => (
setState({ ...state, activeScreen: s.id })}
>
{s.label}
))}
setState({ ...state, editMode: !state.editMode })}
title="Permite clicar e editar valores direto no preview"
>
{state.editMode ? 'Sair da edição' : 'Modo edição'}
setState({ ...state, previewMode: true })}
>
Visualizar
setState({ ...state, showShareModal: true })}
>
Compartilhar
{(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 (
setState({
...state,
levels: { ...state.levels, [L.n]: { ...lvlState, enabled: !on } },
expanded: state.expanded === L.n ? state.expanded : L.n,
})}
title={`Camada ${L.n} · ${L.label}`}
>
{L.n}. {L.label}
);
})}
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 (
);
})()}
{state.editMode && (
Modo edição · clique em qualquer texto ou número
setState({ ...state, editMode: false })}>
Concluir
)}
);
}
/* ---------- 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.
Fechar
);
}
/* ---------- 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()}
/>
{copied ? 'Copiado!' : 'Copiar'}
Abrir em nova aba
Estado da demo encodado no hash · {demo?.version || window.STUDIO_DATA.STUDIO_VERSION}.
Alternativas
{downloaded ? 'Baixado!' : 'Download HTML standalone'}
Arquivo .html que abre a demo · ideal para email
alert('Em breve: gravação automática do tour com a voz do AE.')}>
Gravar tour de vídeo (MP4)
Felipe narra cada tela · até 5 min
alert('Em breve: envio direto por email + tracking de interação.')}>
Convidar por email
Adicionar destinatários · notificar interação
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.
Cancelar
{copied ? 'Link copiado' : 'Copiar e enviar'}
);
}
/* ---------- 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 && (
Biblioteca
)}
{!isProspect && (
setState({ ...state, previewMode: false })}>
Voltar ao editor
)}
{!isProspect && (
setState({ ...state, showShareModal: true })}>
Compartilhar
)}
{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( );