/* === library.jsx ==================================================== Login screen + Demo library home page for UpFlux Studio. ===================================================================== */ const DEFAULT_STUDIO_PASSWORD = 'upflux2026'; /* Read the current password from localStorage; falls back to the default on first run. Persisted plain (no hashing) — this is a soft access gate for sales team, not a security boundary. */ function getStudioPassword() { try { const stored = localStorage.getItem('studio_password'); return (stored && stored.length > 0) ? stored : DEFAULT_STUDIO_PASSWORD; } catch { return DEFAULT_STUDIO_PASSWORD; } } function setStudioPassword(next) { try { localStorage.setItem('studio_password', next); } catch {} } /* ================================================================== LOGIN SCREEN ================================================================== */ function LoginScreen({ onAuth }) { const [pwd, setPwd] = React.useState(''); const [err, setErr] = React.useState(false); const submit = (e) => { e?.preventDefault(); if (pwd === getStudioPassword()) onAuth(); else { setErr(true); setTimeout(() => setErr(false), 1800); } }; return (
U UpFlux Studio

Acessar o editor de demos

Apenas AEs e Sales Engineers da UpFlux. O acesso público à demo do prospect não exige senha.

setPwd(e.target.value)} placeholder="••••••••" />
{err && (
Senha incorreta — tente novamente
)}
Studio {window.STUDIO_DATA.STUDIO_VERSION} · build {window.STUDIO_DATA.STUDIO_BUILD_DATE}
); } /* ================================================================== LIBRARY SCREEN ================================================================== */ function LibraryScreen({ library, onOpen, onClone, onDelete, onCreate, onLogout }) { const [filter, setFilter] = React.useState('all'); const [query, setQuery] = React.useState(''); const [menuOpenId, setMenuOpenId] = React.useState(null); const [showNewForm, setShowNewForm] = React.useState(false); const [showSettings, setShowSettings] = React.useState(false); const [showAnswers, setShowAnswers] = React.useState(false); const [showAgents, setShowAgents] = React.useState(false); const [showHotspots, setShowHotspots] = React.useState(false); React.useEffect(() => { const close = () => setMenuOpenId(null); window.addEventListener('click', close); return () => window.removeEventListener('click', close); }, []); const filtered = library.filter(d => { if (filter !== 'all' && d.status !== filter) return false; if (query && !`${d.name} ${d.prospect} ${d.industry}`.toLowerCase().includes(query.toLowerCase())) return false; return true; }); const counts = { all: library.length, draft: library.filter(d => d.status === 'draft').length, ready: library.filter(d => d.status === 'ready').length, shared: library.filter(d => d.status === 'shared').length, }; return (
U UpFlux Studio
{window.STUDIO_DATA.STUDIO_VERSION}
Abrir Nous

Biblioteca de demos

Crie uma demo nova, abra uma existente ou clone para começar rápido a partir de um template.

{[ { id: 'all', label: 'Todas' }, { id: 'draft', label: 'Rascunho' }, { id: 'ready', label: 'Prontas' }, { id: 'shared', label: 'Compartilhadas' }, ].map(f => ( ))}
setQuery(e.target.value)} />
{filtered.map((d) => ( { e.stopPropagation(); setMenuOpenId(menuOpenId === d.id ? null : d.id); }} onOpen={() => onOpen(d.id)} onClone={() => { onClone(d.id); setMenuOpenId(null); }} onDelete={() => { if (confirm(`Excluir a demo "${d.name}"? Esta ação não pode ser desfeita.`)) { onDelete(d.id); } setMenuOpenId(null); }} /> ))}
{filtered.length === 0 && (
Nenhuma demo encontrada para esses filtros.
)} {showNewForm && ( setShowNewForm(false)} onCreate={(name) => { setShowNewForm(false); onCreate(name); }} onClone={(id) => { setShowNewForm(false); onClone(id); }} /> )} {showSettings && ( setShowSettings(false)} /> )} {showAnswers && ( setShowAnswers(false)} /> )} {showAgents && ( setShowAgents(false)} /> )} {showHotspots && ( setShowHotspots(false)} /> )}
); } /* ================================================================== SETTINGS MODAL — change password + studio info ================================================================== */ function SettingsModal({ onClose }) { const [current, setCurrent] = React.useState(''); const [next, setNext] = React.useState(''); const [confirm, setConfirm] = React.useState(''); const [feedback, setFeedback] = React.useState(null); /* { kind: 'ok'|'err', msg } */ const [show, setShow] = React.useState(false); const submit = (e) => { e?.preventDefault(); const stored = getStudioPassword(); if (current !== stored) { setFeedback({ kind: 'err', msg: 'Senha atual incorreta.' }); return; } if (!next || next.length < 4) { setFeedback({ kind: 'err', msg: 'A nova senha precisa ter ao menos 4 caracteres.' }); return; } if (next !== confirm) { setFeedback({ kind: 'err', msg: 'A confirmação não bate com a nova senha.' }); return; } setStudioPassword(next); setFeedback({ kind: 'ok', msg: 'Senha atualizada. Use a nova senha na próxima vez que entrar.' }); setCurrent(''); setNext(''); setConfirm(''); }; const reset = () => { if (confirm('Restaurar a senha padrão "upflux2026"?')) { try { localStorage.removeItem('studio_password'); } catch {} setFeedback({ kind: 'ok', msg: 'Senha restaurada para o padrão "upflux2026".' }); } }; return (
e.stopPropagation()}>
Configurações do Studio
Acesso interno · {window.STUDIO_DATA.STUDIO_VERSION} · build {window.STUDIO_DATA.STUDIO_BUILD_DATE}
Senha do editor Quem precisa entrar em {location.host || 'demo.upflux.ai'} usa esta senha. Persiste no navegador — cada AE precisa configurar a sua vez ou compartilhar.
{feedback && (
{feedback.msg}
)}
Resetar senha
Esqueceu a senha? Restaure para a padrão de fábrica (qualquer pessoa no time pode entrar com upflux2026 e configurar a nova).
Sobre o Studio
  • Versão: {window.STUDIO_DATA.STUDIO_VERSION}
  • Build: {window.STUDIO_DATA.STUDIO_BUILD_DATE}
  • Demos no navegador: {JSON.parse(localStorage.getItem('studio_library_v1') || '[]').length}
  • URL pública (após deploy):{' '} demo.upflux.ai
); } /* ================================================================== DEMO CARD ================================================================== */ function DemoCard({ demo, menuOpen, onToggleMenu, onOpen, onClone, onDelete }) { const STATUS_LABEL = { draft: { label: 'Rascunho', cls: 'st-draft' }, ready: { label: 'Pronta', cls: 'st-ready' }, shared: { label: 'Compartilhada', cls: 'st-shared' }, }; const st = STATUS_LABEL[demo.status] || STATUS_LABEL.draft; const versionMismatch = demo.version !== window.STUDIO_DATA.STUDIO_VERSION; return (
{demo.prospect.charAt(0).toUpperCase()}
{/* Subtle decorative shapes */}
{menuOpen && (
e.stopPropagation()}>
)}

{demo.name}

{st.label}
{demo.industry} {demo.persona}
{demo.thumbHint}
{demo.ownerInitials} Editada em {demo.updatedAt} {versionMismatch && ( {demo.version} )} {demo.views > 0 && ( {demo.views} )}
); } /* ================================================================== NEW DEMO MODAL — blank or clone-from-template ================================================================== */ function NewDemoModal({ library, onClose, onCreate, onClone }) { const [name, setName] = React.useState(''); const [mode, setMode] = React.useState('blank'); /* 'blank' | 'clone' */ const [pickedId, setPickedId] = React.useState(library[0]?.id); return (
e.stopPropagation()}>
Nova demo
Comece em branco ou clone uma existente como ponto de partida.
{mode === 'blank' && (
Nome do prospect
setName(e.target.value)} placeholder="Ex: Unimed BH, Magazine Luiza..." />
Aparece no sidebar do Nous, nas saudações e no header da demo.
)} {mode === 'clone' && (
Escolha a base
{library.map(d => ( ))}
)}
); } Object.assign(window, { LoginScreen, LibraryScreen, DemoCard, NewDemoModal, SettingsModal, StandardAnswersAdmin, AgentsMediaAdmin, HotspotsAdmin, getStudioPassword, setStudioPassword }); /* ================================================================== STANDARD ANSWERS ADMIN — manage the mandatory P2P question bank that feeds the chat (by category) for every P2P demo. ================================================================== */ function StandardAnswersAdmin({ onClose }) { const SD = window.STUDIO_DATA; const [bank, setBank] = React.useState(() => SD.getStandardBank()); const [editingKey, setEditingKey] = React.useState(null); const [savedFlash, setSavedFlash] = React.useState(false); const CATS = [ { id: 'estrategico', label: 'Estratégico' }, { id: 'gestao', label: 'Gestão' }, { id: 'operacional', label: 'Operacional' }, ]; const persist = (next) => { setBank(next); SD.saveStandardBank(next); setSavedFlash(true); setTimeout(() => setSavedFlash(false), 1600); }; const update = (key, patch) => persist(bank.map(s => s.key === key ? { ...s, ...patch } : s)); /* Read an attached .html file, store it in IndexedDB and point the entry's html at uploaded:. Works fully client-side. */ const attachHtml = async (key, file) => { if (!file) return; const text = await file.text(); const idbKey = 'uploaded/' + key + '-' + Date.now().toString(36); await SD.idbPutHtml(idbKey, text); update(key, { html: 'uploaded:' + idbKey, htmlName: file.name }); }; const downloadUploaded = async (s) => { const idbKey = s.html.slice('uploaded:'.length); const text = await SD.idbGetHtml(idbKey); if (text == null) { alert('Arquivo não encontrado neste navegador.'); return; } const url = URL.createObjectURL(new Blob([text], { type: 'text/html' })); const a = document.createElement('a'); a.href = url; a.download = s.htmlName || (s.key + '.html'); document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); }; const previewUploaded = async (s) => { const idbKey = s.html.slice('uploaded:'.length); const text = await SD.idbGetHtml(idbKey); if (text == null) { alert('Arquivo não encontrado neste navegador.'); return; } const url = URL.createObjectURL(new Blob([text], { type: 'text/html' })); window.open(url, '_blank'); setTimeout(() => URL.revokeObjectURL(url), 60000); }; const remove = (key) => { if (confirm('Remover esta resposta padrão de todos os ambientes P2P?')) { persist(bank.filter(s => s.key !== key)); setEditingKey(null); } }; const addNew = () => { const key = 'custom-' + Math.random().toString(36).slice(2, 7); const entry = { key, category: 'gestao', label: 'Nova pergunta padrão', summary: 'Resumo da resposta que aparece no chat antes do artefato.', html: '', title: 'Novo artefato', meta: ['Tempo real', 'Fonte: ERP'], height: 760, replaces: [], }; persist([...bank, entry]); setEditingKey(key); }; /* Attach an HTML file directly: creates a new entry, stores the file in IndexedDB, opens it in edit mode so the AE writes the question. */ const uploadRef = React.useRef(null); const onTopUpload = async (e) => { const file = e.target.files && e.target.files[0]; e.target.value = ''; if (!file) return; const key = 'upload-' + Math.random().toString(36).slice(2, 7); const text = await file.text(); const idbKey = 'uploaded/' + key + '-' + Date.now().toString(36); await SD.idbPutHtml(idbKey, text); const niceTitle = file.name.replace(/\.html?$/i, '').replace(/[-_]+/g, ' ').trim(); const entry = { key, category: 'gestao', label: 'Nova pergunta — ' + niceTitle, summary: 'Descreva aqui, em 2-3 frases, o que esta resposta entrega. [Complementar com os números do cliente.]', html: 'uploaded:' + idbKey, htmlName: file.name, title: niceTitle || 'Artefato', meta: ['Tempo real', 'Fonte: ERP'], height: 780, replaces: [], }; persist([...bank, entry]); setEditingKey(key); }; const resetAll = () => { if (confirm('Restaurar a biblioteca de respostas padrão de fábrica? Suas edições serão perdidas.')) { SD.resetStandardBank(); persist(SD.getStandardBank()); } }; return (
e.stopPropagation()}>
Biblioteca de Respostas Padrão · P2P
Perguntas e respostas mínimas, obrigatórias em todo ambiente P2P. Alimentam o chat por categoria. {savedFlash && · salvo}
{CATS.map(cat => { const items = bank.filter(s => s.category === cat.id); return (
{cat.label} {items.length}
{items.map(s => (
setEditingKey(editingKey === s.key ? null : s.key)}>
{s.label}
{s.html && s.html.startsWith('uploaded:') ? '📎 ' + (s.htmlName || 'HTML anexado no navegador') : (s.html && s.html.trim() ? s.html : '◦ Resposta em texto · sem artefato HTML')}
{editingKey === s.key && (
Arquivo HTML da resposta (anexe um .html, cole um caminho de assets/, ou deixe vazio p/ texto)
update(s.key, { html: e.target.value })} placeholder="assets/minha-resposta.html — ou anexe →" /> {s.html.startsWith('uploaded:') && ( )}