/* Hexis AI — Omnipresent Intelligence Layer v4
Speed: instant keyword response + async LLM upgrade
Persistence: session msgs survive page navigation
BLS: behavioral tracking feeds context
*/
const { useState: useStateH, useEffect: useEffectH, useRef: useRefH, useCallback: useCallbackH } = React;
/* ═══════════════════════════════════════════════════════════
SESSION STORE — persists conversation across page nav
Cleared only on explicit close or site reload
═══════════════════════════════════════════════════════════ */
/* Load persisted msgs from localStorage so they survive browser close + close button */
const _STORAGE_KEY = 'hexis_msgs_v2';
const _loadPersisted = () => { try { return JSON.parse(localStorage.getItem(_STORAGE_KEY) || '[]'); } catch { return []; } };
let _sessionMsgs = _loadPersisted();
let _sessionGreeted = _sessionMsgs.length > 0; // skip greeting if msgs already exist
/* ═══════════════════════════════════════════════════════════
HEXIS STORE — alert/open state
═══════════════════════════════════════════════════════════ */
const HexisStore = (() => {
const subs = new Set();
let state = {
open: false,
seenAt: Date.now() - 1,
alerts: [
{ id: 'a1', kind: 'success', title: 'Match Premium identificado', body: 'Miguel Tavares · Veridian VC · 98% — janela ótima 14:32–14:50.', at: Date.now() - 60000 * 6, action: 'Ver Match' },
{ id: 'a2', kind: 'warning', title: 'Decisão pendente', body: 'RFP Banco Invicta expira em 3h. Resposta sugerida pronta.', at: Date.now() - 60000 * 12, action: 'Responder' },
{ id: 'a3', kind: 'info', title: 'Pipeline atualizado', body: '€420K em novas oportunidades atribuídas ao Web Summit.', at: Date.now() - 60000 * 22 },
],
};
const notify = () => subs.forEach(fn => fn({ ...state }));
return {
get() { return { ...state }; },
subscribe(fn) { subs.add(fn); fn({ ...state }); return () => subs.delete(fn); },
open() { state = { ...state, open: true, seenAt: Date.now() }; notify(); },
close() { state = { ...state, open: false }; notify(); },
clearHistory(){ _sessionMsgs = []; _sessionGreeted = false; },
addAlert(a) {
const id = 'a_' + Math.random().toString(36).slice(2, 8);
state = { ...state, alerts: [{ id, at: Date.now(), ...a }, ...state.alerts.slice(0, 19)] };
notify();
},
unreadCount() { return state.alerts.filter(a => a.at > state.seenAt).length; },
highestKind() {
const order = ['danger', 'warning', 'success', 'info'];
const unseen = state.alerts.filter(a => a.at > state.seenAt);
for (const k of order) if (unseen.find(a => a.kind === k)) return k;
return null;
},
};
})();
window.HexisStore = HexisStore;
/* ═══════════════════════════════════════════════════════════
BLS — Behavioral Learning Stream (Edge capture)
Tracks scroll, dwell, CTAs → feeds HexisEngine context
═══════════════════════════════════════════════════════════ */
const HexisBLS = (() => {
const _buffer = [];
const _sessionHash = 'bls_' + Math.random().toString(36).slice(2, 10);
let _lastFlush = Date.now();
const track = (type, target, value) => {
_buffer.push({ type, target, value, ts: new Date().toISOString() });
// Feed HexisEngine immediately for local context updates
if (window.HexisEngine) {
if (type === 'cta_click') HexisEngine.trackClick(target);
if (type === 'page_view') HexisEngine.trackPage(target);
}
// Batch flush every 8 seconds
if (Date.now() - _lastFlush > 8000 && _buffer.length > 0) flush();
};
const flush = () => {
if (_buffer.length === 0) return;
const batch = _buffer.splice(0, _buffer.length);
_lastFlush = Date.now();
const token = window.getAuthToken ? window.getAuthToken() : null;
const userId = window.getAuthUser ? window.getAuthUser()?.id : null;
const headers = { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) };
const base = (typeof API_BASE !== 'undefined') ? API_BASE : 'https://api.marcianusexperience.com/api/v1';
// Fire-and-forget each event, matching analytics schema: { action, category, label, value, user_id }
batch.forEach(ev => {
fetch(`${base}/analytics/track`, {
method: 'POST', headers,
body: JSON.stringify({
user_id: userId,
session_id: _sessionHash,
action: ev.type,
category: ev.type.includes('scroll') ? 'engagement' : ev.type.includes('chat') ? 'assistant' : 'navigation',
label: ev.target,
value: typeof ev.value === 'number' ? ev.value : 1,
metadata: { page: window.location.pathname },
}),
signal: AbortSignal.timeout(3000),
}).catch(() => {}); // silent fail — BLS is non-blocking
});
};
// Scroll depth tracker
const initScroll = () => {
let _maxScroll = 0;
document.addEventListener('scroll', () => {
const pct = Math.round((window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100);
if (pct > _maxScroll + 10) {
_maxScroll = pct;
track('scroll_depth', window.location.pathname, pct);
}
}, { passive: true });
};
setTimeout(initScroll, 1000);
setInterval(flush, 8000);
return { track };
})();
window.HexisBLS = HexisBLS;
/* ═══════════════════════════════════════════════════════════
INSIGHT BAR — inline contextual recommendations
═══════════════════════════════════════════════════════════ */
const HexisInsightBar = ({ page }) => {
const [recs, setRecs] = useStateH([]);
const [idx, setIdx] = useStateH(0);
const [dismissed, setDismissed] = useStateH(false);
useEffectH(() => {
if (!window.HexisEngine) return;
setRecs(HexisEngine.getPageRecs(page));
setIdx(0);
setDismissed(false);
}, [page]);
useEffectH(() => {
if (recs.length <= 1) return;
const t = setInterval(() => setIdx(i => (i + 1) % recs.length), 8000);
return () => clearInterval(t);
}, [recs]);
if (dismissed || recs.length === 0) return null;
const r = recs[idx];
const kindColors = { success: '#00C48C', warning: '#FFD700', danger: '#FF3B5C', info: '#00D1FF', gold: '#FFD700' };
const col = kindColors[r.kind] || '#7B61FF';
return (
Hexis AI{r.title} — {r.body}
{r.action && (
)}
{recs.length > 1 && (
)}
);
};
window.HexisInsightBar = HexisInsightBar;
/* ═══════════════════════════════════════════════════════════
FAB — floating action button
═══════════════════════════════════════════════════════════ */
const HexisFab = () => {
const [s, setS] = useStateH(HexisStore.get());
useEffectH(() => HexisStore.subscribe(setS), []);
const unread = s.alerts.filter(a => a.at > s.seenAt).length;
const kind = HexisStore.highestKind ? HexisStore.highestKind() : null;
const showLight = unread > 0 && !s.open;
return (
);
};
/* ═══════════════════════════════════════════════════════════
MAIN POPUP
═══════════════════════════════════════════════════════════ */
const HexisPopup = () => {
const [s, setS] = useStateH(HexisStore.get());
const [profile, setProfile] = useStateH(window.HexisEngine ? HexisEngine.getProfile() : {});
const [tab, setTab] = useStateH('chat');
const [msgs, setMsgsState] = useStateH(_sessionMsgs);
const [input, setInput] = useStateH('');
const [busy, setBusy] = useStateH(false); // true while instant response shown + LLM upgrading
const scrollRef = useRefH(null);
const inputRef = useRefH(null);
const msgsRef = useRefH(_sessionMsgs);
const popupRef = useRefH(null);
// Persist msgs to module variable + localStorage whenever they change
const setMsgs = (updater) => {
setMsgsState(prev => {
const next = typeof updater === 'function' ? updater(prev) : updater;
_sessionMsgs = next;
msgsRef.current = next;
try { localStorage.setItem(_STORAGE_KEY, JSON.stringify(next.slice(-60))); } catch {}
return next;
});
};
useEffectH(() => HexisStore.subscribe(setS), []);
useEffectH(() => {
if (!window.HexisEngine) return;
return HexisEngine.subscribe(p => setProfile({ ...p }));
}, []);
/* ── Click-outside closes popup ──────────────────────────── */
useEffectH(() => {
if (!s.open) return;
const onDown = (e) => {
if (popupRef.current && !popupRef.current.contains(e.target)) {
// Don't close if clicking the FAB itself
if (e.target.closest('.hexis-fab')) return;
HexisStore.close();
}
};
document.addEventListener('mousedown', onDown);
return () => document.removeEventListener('mousedown', onDown);
}, [s.open]);
/* ── Show greeting only if no session history ─────────────── */
useEffectH(() => {
if (!s.open) return;
if (_sessionGreeted && _sessionMsgs.length > 0) {
// Restore existing conversation
setMsgsState(_sessionMsgs);
return;
}
_sessionGreeted = true;
const authUser = window.getAuthUser ? getAuthUser() : null;
const isGuest = !authUser;
const p = window.HexisEngine ? HexisEngine.getProfile() : {};
const page = p.currentPage || 'home';
const mis = (p.misScore || 0.30).toFixed(2);
const firstName = authUser?.name?.split(' ')[0] || null;
let greeting;
let suggestions;
if (isGuest) {
const guestGreetings = {
home: `Olá! Sou o Hexis AI — o motor de inteligência da Marcianus. Analiso comportamento B2B e identifico conexões de alto valor. Faz login para matching personalizado.`,
entrar: `Bem-vindo! Cria uma conta gratuita ou faz login para aceder ao matching B2B, dashboard e Hexis AI completo.`,
eventos: `Temos 6 eventos confirmados para 2026 — de FinTech Lisboa ao Marcianus Summit (600 pax, 25 Out). Login para recomendações personalizadas.`,
pricing: `Planos desde €0 (Starter) até Enterprise. Para recomendação personalizada ao teu perfil, faz login ou fala connosco.`,
sobre: `A Marcianus nasceu para acabar com eventos sem resultado mensurável. Posso agendar uma call com o fundador Israel Nzambi.`,
conteudos: `10 peças disponíveis — 4 gratuitas. Gratuito em destaque: "A morte do networking espontâneo" (6 min). O que preferes?`,
marketplace: `Kiosk NFC, Hexis AI Live, Pack Leads — produtos para eventos B2B de alto valor. Login para ver preços personalizados.`,
'criar-experiencia':`Para criar uma experiência Marcianus, preciso de conhecer o teu perfil. Login para iniciar o Briefing Inteligente.`,
};
greeting = guestGreetings[page] || `Olá! Sou o Hexis AI da Marcianus. Faz login para matching B2B personalizado e dashboard completo.`;
suggestions = ['Criar Conta Grátis', 'Ver Eventos', 'Como funciona?'];
} else {
const activeMissions = window.HexisEngine ? HexisEngine.getActiveMissions().length : 0;
const authGreetings = {
home: `Olá ${firstName}! MIS atual: ${mis}. ${HexisStore.get().alerts.length} alertas. O que queres optimizar hoje?`,
eventos: `FinTech Lisboa 2026 está LIVE agora. Próximo: Aether Real Estate (20 Mai). Qual te interessa?`,
dashboard: `Hub operacional: ${activeMissions} missões ativas. Por onde queres começar?`,
marketplace: `Marketplace: saldo €${(p.walletBalance || 0).toLocaleString('pt-PT')}. Produto recomendado: Hexis AI Live.`,
'criar-experiencia':`Briefing ligado para o teu perfil (${p.sector || 'Tech'} · ${p.role || 'Executivo'}). Pronto para guiar o setup.`,
pricing: `Posso calcular o ROI esperado para o teu perfil. Ou ver plano recomendado?`,
sobre: `Posso agendar uma call com o founder Israel Nzambi. Tens disponibilidade esta semana?`,
conteudos: `10 peças disponíveis. Gratuitos: "A morte do networking espontâneo" e Podcast #08. O que preferes?`,
entrar: `Olá ${firstName}! Já estás autenticado. Vai ao Dashboard para ver o teu perfil completo.`,
};
greeting = authGreetings[page] || `Olá ${firstName}. MIS ${mis}. O que precisas?`;
const recs = window.HexisEngine ? HexisEngine.getPageRecs(page) : [];
suggestions = recs.length > 0 ? recs.slice(0, 3).map(r => r.action) : ['Ver matches', 'Próximo evento', 'Resumo do dia'];
}
const greetMsg = { id: 1, from: 'hexis', text: greeting, suggestions };
_sessionMsgs = [greetMsg];
msgsRef.current = [greetMsg];
setMsgsState([greetMsg]);
}, [s.open]);
/* ── Auto-scroll to bottom ─────────────────────────────────── */
useEffectH(() => {
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}, [msgs]);
/* ── Scroll to bottom when popup opens ─────────────────────── */
useEffectH(() => {
if (!s.open) return;
const id = setTimeout(() => {
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}, 80);
return () => clearTimeout(id);
}, [s.open]);
/* ── Respond: instant local → async LLM upgrade ────────────── */
/* Strip LLM markdown so it renders as clean text */
const stripMd = (t) => (t || '')
.replace(/\*\*([^*]+)\*\*/g, '$1')
.replace(/\*([^*]+)\*/g, '$1')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/#{1,6}\s+/gm, '')
.replace(/^[\s]*[-*]\s+/gm, '• ')
.trim();
const respond = useCallbackH(async (userText) => {
if (!userText.trim() || busy) return;
SoundFX.tap();
HexisBLS.track('hexis_chat', userText.slice(0, 60), 1);
if (window.HexisEngine) HexisEngine.trackClick('hexis_chat');
const page = window.HexisEngine ? HexisEngine.getProfile().currentPage : 'home';
const msgId = Date.now();
const history = msgsRef.current.filter(m => m.id !== 1);
const local = window.HexisLocal
? HexisLocal.respond(userText, page)
: { text: 'Hexis AI a iniciar…', suggestions: ['Ver Eventos', 'Ver Preços', 'Como funciona?'], fromLocal: true };
setBusy(true);
setMsgs(prev => [...prev, { id: msgId, from: 'hexis', text: local.text, suggestions: local.suggestions, fromLLM: false, fromLocal: true }]);
SoundFX.notify();
const onToken = (partial) => {
if (!partial || typeof partial !== 'string') return;
setMsgs(prev => prev.map(m => m.id === msgId
? { ...m, text: stripMd(partial), thinking: false, fromLLM: true, fromLocal: false }
: m
));
};
try {
const result = window.HexisBackend
? await window.HexisBackend.chat(userText, history, page, onToken)
: window.HexisOllama
? await window.HexisOllama.streamResponse(userText, history, page, onToken)
: null;
if (result?.fromLLM === true && result?.text && result.text.length > 15) {
setMsgs(prev => prev.map(m => m.id === msgId
? { ...m, text: stripMd(result.text), thinking: false, fromLLM: true, fromLocal: false, suggestions: result.suggestions?.length ? result.suggestions : local.suggestions }
: m
));
}
} catch (_) { /* local response stays */ }
finally { setBusy(false); }
}, [busy]);
const send = (text) => {
const t = (text || input).trim();
if (!t || busy) return;
const userMsg = { id: Date.now() - 1, from: 'me', text: t };
setMsgs(prev => [...prev, userMsg]);
setInput('');
respond(t);
};
/* Close — do NOT clear history; conversation persists (localStorage) */
const handleClose = () => { SoundFX.tap(); HexisStore.close(); };
/* Nova Conversa — explicit user action to reset */
const newConversation = () => {
try { localStorage.removeItem(_STORAGE_KEY); } catch {}
_sessionMsgs = [];
_sessionGreeted = false;
msgsRef.current = [];
setMsgsState([]);
HexisStore.close();
setTimeout(() => HexisStore.open(), 80);
};
const lp = window.HexisEngine ? HexisEngine.getLevelProgress() : { level: 1, tier: 'bronze', xp: 0, pct: 0, xpToNext: 200 };
const activeMissions = window.HexisEngine ? HexisEngine.getActiveMissions() : [];
const authUser = window.getAuthUser ? getAuthUser() : null;
const isGuest = !authUser;
// User avatar initials
const userInitials = authUser?.name
? authUser.name.split(' ').slice(0, 2).map(w => w[0]).join('').toUpperCase()
: 'Tu';
if (!s.open) return null;
return (
{/* Header */}
⟡
Hexis AI · {(profile.currentPage || 'home').toUpperCase()}
{isGuest ? (
modo visitante · login para MIS completo
) : (
MIS {(profile.misScore || 0.30).toFixed(2)} · L{lp.level} {lp.tier}
)}
{busy && (
a melhorar…
)}
{/* XP progress — only for authenticated users */}
{!isGuest && (
{(profile.xp || 0).toLocaleString()} XP · {(lp.tier || 'bronze').toUpperCase()}
{lp.xpToNext} XP para L{lp.level + 1}
)}
{/* Tabs */}
{[
{ id: 'chat', label: 'Chat' },
{ id: 'alerts', label: `Alertas${s.alerts.filter(a => a.at > s.seenAt).length > 0 ? ' · ' + s.alerts.filter(a => a.at > s.seenAt).length : ''}` },
{ id: 'missions', label: `Missões${activeMissions.length > 0 ? ' · ' + activeMissions.length : ''}` },
].map(t => (
))}
{/* ── CHAT TAB ──────────────────────────────────────────── */}
{tab === 'chat' && (
<>
{msgs.map(m => (
{m.from === 'hexis' ? '⟡' : userInitials}
{(m.fromLLM ? stripMd(m.text) : m.text)}
{m.fromLLM === true && (
⟡llm
)}
{m.fromLocal === true && !m.fromLLM && (
⟡local
)}
{m.suggestions?.length > 0 && (
{m.suggestions.map((sg, i) => (
))}
)}
))}
setInput(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && !busy) send(); }}
placeholder={busy ? 'Hexis a pensar…' : 'Pergunta à Hexis…'}
autoComplete="off"
/>
>
)}
{/* ── ALERTS TAB ────────────────────────────────────────── */}
{tab === 'alerts' && (
{isGuest && (
⟡ Alertas personalizados
Faz login para receber alertas de match, pipeline e janelas de oportunidade em tempo real.
)}
{s.alerts.filter(a => isGuest ? a.forGuest : true).slice(0, 12).map(a => {
const cols = { success: '#00C48C', warning: '#FFD700', danger: '#FF3B5C', info: '#00D1FF' };
const col = cols[a.kind] || '#7B61FF';
return (
{a.title}
{a.body}
{a.action && (
)}
);
})}
{s.alerts.filter(a => isGuest ? a.forGuest : true).length === 0 && !isGuest && (
Sem alertas de momento.
)}
)}
{/* ── MISSIONS TAB ──────────────────────────────────────── */}
{tab === 'missions' && (
{isGuest && (
<>
Missão Disponível · +50 XP
Cria a tua conta gratuita
Regista-te para desbloquear matching B2B, dashboard e missões com recompensas reais.
Missão Disponível · +10 XP
Explora os Eventos 2026
Vê os 10 eventos B2B confirmados para 2026 e identifica oportunidades.
>
)}
{!isGuest && activeMissions.length === 0 && (
Sem missões ativas agora. Navega para o Dashboard ou Eventos para desbloquear.
)}
{!isGuest && activeMissions.map(m => {
const pct = Math.round((m.elapsed / m.timeLimit) * 100);
return (
{m.game} · +{m.reward.xp} XP
{m.title}
);
})}
)}
);
};
window.HexisFab = HexisFab;
window.HexisPopup = HexisPopup;
window.HexisInsightBar = HexisInsightBar;