// chat-widget.el -- Neuron demo chat widget with Supabase auth, Turnstile, // session persistence, local engram graph, and share-pill. // Compiled with: elc --target=js --bundle --minify --obfuscate // // Exposed globals: neuronDemoToggle(), neuronDemoSend(), neuronDemoReset() // Required CDN: marked.js, Cloudflare Turnstile, Supabase JS fn main() -> Void { native_js("(function() { if (typeof marked !== 'undefined') { marked.setOptions({ breaks: true, gfm: true }); } var TURNSTILE_SITE_KEY = '0x4AAAAAADHAZXyuRb3yD9mr'; var turnstileToken = ''; var turnstileWidgetId = null; var turnstileVerified = false; var isOpen = false; var MAX = 10; var _userName = ''; var _userTimezone = (typeof Intl !== 'undefined' && Intl.DateTimeFormat) ? Intl.DateTimeFormat().resolvedOptions().timeZone : ''; // ── Supabase auth state ─────────────────────────────────────────────────── var supabaseClient = null; var _supabaseSession = null; // current session (null = not authenticated) function initSupabaseWidget(cb) { if (supabaseClient) { cb(); return; } fetch('/api/supabase-config') .then(function(r) { return r.json(); }) .then(function(cfg) { supabaseClient = window.supabase.createClient(cfg.url, cfg.anon_key, { auth: { flowType: 'implicit' } }); supabaseClient.auth.getSession().then(function(res) { if (res.data && res.data.session) { _supabaseSession = res.data.session; } // Listen for sign-in from OAuth redirect supabaseClient.auth.onAuthStateChange(function(event, session) { if (session) { _supabaseSession = session; _onWidgetAuthenticated(); } }); cb(); }); }) .catch(function() { cb(); }); } function _onWidgetAuthenticated() { // Capture user name for personalized greeting if (_supabaseSession && _supabaseSession.user) { var _meta = _supabaseSession.user.user_metadata || {}; _userName = _meta.full_name || _meta.name || _supabaseSession.user.email || ''; } var authPane = document.getElementById('neuron-demo-auth'); var gate = document.getElementById('neuron-demo-gate'); var msgs = document.getElementById('neuron-demo-messages'); var inputRow = document.getElementById('neuron-demo-input-row'); if (authPane) authPane.style.display = 'none'; // Only reveal chat UI if Turnstile has also passed if (turnstileVerified) { if (gate) gate.style.display = 'none'; if (msgs) msgs.style.display = 'flex'; if (inputRow) inputRow.style.display = 'flex'; var msgs2 = document.getElementById('neuron-demo-messages'); if (msgs2 && msgs2.children.length === 0) { if (session && session.messages && session.messages.length > 0) { session.messages.forEach(function(m) { addMsg(m.role, m.text, true); }); } else if (!session.greeted) { _sendIntroGreeting(); } } var inp = document.getElementById('neuron-demo-text'); if (inp) inp.focus(); } } function _renderWidgetAuthPane() { var authPane = document.getElementById('neuron-demo-auth'); if (!authPane) return; authPane.innerHTML = ''; authPane.style.display = 'flex'; var heading = document.createElement('p'); heading.className = 'demo-auth-heading'; heading.textContent = 'Sign in to chat with Neuron'; authPane.appendChild(heading); var googleBtn = document.createElement('button'); googleBtn.className = 'demo-auth-google-btn'; googleBtn.innerHTML = ' Continue with Google'; googleBtn.onclick = function() { if (!supabaseClient) return; supabaseClient.auth.signInWithOAuth({ provider: 'google', options: { redirectTo: window.location.href } }); }; authPane.appendChild(googleBtn); var emailToggle = document.createElement('button'); emailToggle.className = 'demo-auth-email-toggle'; emailToggle.textContent = 'or continue with email'; authPane.appendChild(emailToggle); var emailForm = document.createElement('div'); emailForm.className = 'demo-auth-email-form'; emailForm.style.display = 'none'; emailForm.innerHTML = '' + '' + '' + '
'; authPane.appendChild(emailForm); emailToggle.onclick = function() { emailForm.style.display = emailForm.style.display === 'none' ? 'flex' : 'none'; }; var submitBtn = emailForm.querySelector('#demo-auth-submit'); if (submitBtn) { submitBtn.onclick = function() { var email = (document.getElementById('demo-auth-email') || {}).value || ''; var pass = (document.getElementById('demo-auth-password') || {}).value || ''; var msgEl = document.getElementById('demo-auth-msg'); if (!email || !pass) { if (msgEl) { msgEl.textContent = 'Email and password required.'; msgEl.style.display = ''; msgEl.style.color = '#e53e3e'; } return; } submitBtn.disabled = true; supabaseClient.auth.signInWithPassword({ email: email, password: pass }).then(function(res) { if (res.error) { if (msgEl) { msgEl.textContent = res.error.message || 'Sign in failed.'; msgEl.style.display = ''; msgEl.style.color = '#e53e3e'; } submitBtn.disabled = false; } else { _supabaseSession = res.data.session; _onWidgetAuthenticated(); } }).catch(function() { submitBtn.disabled = false; }); }; } } function loadSession() { try { var s = localStorage.getItem('neuron_demo_session'); return s ? JSON.parse(s) : { messages: [], count: 0, context: '' }; } catch(e) { return { messages: [], count: 0, context: '' }; } } function saveSession(session) { try { localStorage.setItem('neuron_demo_session', JSON.stringify(session)); } catch(e) {} } function clearSession() { try { localStorage.removeItem('neuron_demo_session'); } catch(e) {} } function _mg(s) { return s._m || { nodes: [], edges: [] }; } function _um(s, nn, ne) { if (!nn || !nn.length) return; var g = _mg(s), nm = {}, ek = function(e) { return e.from + '->' + e.to; }, em = {}; g.nodes.forEach(function(n) { nm[n.id] = n; }); (nn || []).forEach(function(n) { if (nm[n.id]) { nm[n.id].w = Math.min(1.0, (nm[n.id].w || 0.5) + 0.08); } else { nm[n.id] = n; } }); g.nodes = Object.values(nm); g.edges.forEach(function(e) { em[ek(e)] = e; }); (ne || []).forEach(function(e) { var k = ek(e); if (em[k]) { em[k].weight = Math.min(1.0, (em[k].weight || 0.5) + 0.05); } else { em[k] = e; } }); g.edges = Object.values(em); s._m = g; saveSession(s); } function _ra(g, q) { if (!g || !g.nodes || !g.nodes.length) return []; var words = q.toLowerCase().split(/\s+/).filter(function(w) { return w.length > 3; }); var sc = {}; g.nodes.forEach(function(n) { var t = (n.content || '').toLowerCase(); sc[n.id] = words.filter(function(w) { return t.indexOf(w) !== -1; }).length * 0.6 + (n.w || 0.5) * 0.4; }); (g.edges || []).forEach(function(e) { if (sc[e.from] > 0.1) sc[e.to] = (sc[e.to] || 0) + sc[e.from] * (e.weight || 0.5) * 0.4; }); return g.nodes.filter(function(n) { return sc[n.id] > 0.2; }) .sort(function(a, b) { return sc[b.id] - sc[a.id]; }).slice(0, 5) .map(function(n) { return { id: n.id, content: n.content, score: sc[n.id] }; }); } if (window.location.search.indexOf('reset=1') !== -1) { clearSession(); window.history.replaceState({}, '', window.location.pathname); } var session = loadSession(); if (!session.uid) { session.uid = 'u' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7); saveSession(session); } var msgCount = session.count || 0; var _headerResetInterval = null; function _nextMidnightUTC() { var now = new Date(); var midnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1)); return Math.floor(midnight.getTime() / 1000); } function _startHeaderResetTimer() { var el = document.getElementById('neuron-demo-countdown'); if (!el) return; if (_headerResetInterval) return; // already ticking var resetAt = _nextMidnightUTC(); function _tick() { var secsLeft = Math.max(0, resetAt - Math.floor(Date.now() / 1000)); var hh = Math.floor(secsLeft / 3600); var mm = Math.floor((secsLeft % 3600) / 60); var ss = secsLeft % 60; var pad = function(n) { return n < 10 ? '0' + n : '' + n; }; var ts = hh + ':' + pad(mm) + ':' + pad(ss); var el2 = document.getElementById('neuron-demo-countdown'); if (!el2) { clearInterval(_headerResetInterval); _headerResetInterval = null; return; } el2.textContent = 'resets in ' + ts; el2.style.color = 'rgba(255,255,255,0.65)'; el2.style.fontWeight = '600'; if (secsLeft <= 0) { clearInterval(_headerResetInterval); _headerResetInterval = null; el2.textContent = '10 questions left'; el2.style.color = '#ffffff'; el2.style.fontWeight = '700'; } } _tick(); _headerResetInterval = setInterval(_tick, 1000); } function updateCountdown() { var el = document.getElementById('neuron-demo-countdown'); if (!el) return; var remaining = MAX - msgCount; if (remaining <= 0) { _startHeaderResetTimer(); } else { if (_headerResetInterval) { clearInterval(_headerResetInterval); _headerResetInterval = null; } el.textContent = remaining + ' question' + (remaining === 1 ? '' : 's') + ' left'; el.style.color = '#ffffff'; el.style.fontWeight = '700'; } } function _timeOfDay() { var h = new Date().getHours(); if (h < 12) return 'morning'; if (h < 17) return 'afternoon'; if (h < 21) return 'evening'; return 'night'; } function _sendIntroGreeting() { if (session.greeted) return; session.greeted = true; saveSession(session); var accessToken = (_supabaseSession && _supabaseSession.access_token) ? _supabaseSession.access_token : ''; fetch('/api/demo', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ message: '__intro_phase1__', history: [], cf_token: '', uid: session.uid || '', access_token: accessToken, user_name: _userName, user_timezone: _userTimezone, time_of_day: _timeOfDay(), is_return: session.count > 0 ? 'true' : 'false', activated_nodes: [], engram_node_count: 0, questions_remaining: MAX, is_last_question: false }) }) .then(function(r) { return r.json(); }) .then(function(d) { var reply = d.response || d.reply || d.message || ''; if (reply) { addMsg('ai', reply, true); session.messages = session.messages || []; session.messages.push({ role: 'ai', text: reply }); saveSession(session); } else { addMsg('ai', \"Hey. What's on your mind?\", true); } }) .catch(function() { addMsg('ai', \"Hey. What's on your mind?\", true); }); } window.neuronDemoReset = function() { if (_headerResetInterval) { clearInterval(_headerResetInterval); _headerResetInterval = null; } clearSession(); session = { messages: [], count: 0, context: '' }; session.uid = 'u' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7); saveSession(session); msgCount = 0; var msgs = document.getElementById('neuron-demo-messages'); if (msgs) msgs.innerHTML = ''; var input = document.getElementById('neuron-demo-text'); if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; } var btn = document.getElementById('neuron-demo-send'); if (btn) btn.disabled = false; _sendIntroGreeting(); }; window.neuronDemoToggle = function() { isOpen = !isOpen; var panel = document.getElementById('neuron-demo-panel'); if (panel) panel.style.display = isOpen ? 'flex' : 'none'; var btn = document.getElementById('neuron-demo-btn'); if (btn) btn.style.display = isOpen ? 'none' : ''; var msgs = document.getElementById('neuron-demo-messages'); if (isOpen && turnstileVerified && _supabaseSession && msgs && msgs.style.display !== 'none' && msgs.children.length === 0) { if (session.messages && session.messages.length > 0) { session.messages.forEach(function(m) { addMsg(m.role, m.text, true); }); var remaining = MAX - msgCount; if (remaining <= 0) { var input = document.getElementById('neuron-demo-text'); if (input) { input.disabled = true; input.placeholder = 'Interaction limit reached'; } } } else if (!session.greeted) { _sendIntroGreeting(); } } var input = document.getElementById('neuron-demo-text'); if (isOpen && input && !input.disabled) input.focus(); updateCountdown(); if (isOpen) { // Initialize Supabase on first open, then decide what to show initSupabaseWidget(function() { if (!_supabaseSession) { // Not authenticated — show auth pane, hide Turnstile gate var gate = document.getElementById('neuron-demo-gate'); if (gate) gate.style.display = 'none'; _renderWidgetAuthPane(); } else { // Authenticated — proceed with Turnstile gate as normal if (!turnstileWidgetId && typeof turnstile !== 'undefined') { var container = document.getElementById('neuron-demo-turnstile'); if (container) { turnstileWidgetId = turnstile.render(container, { sitekey: TURNSTILE_SITE_KEY, size: 'compact', callback: function(token) { turnstileToken = token; turnstileVerified = true; if (typeof turnstile !== 'undefined' && turnstileWidgetId !== null) { try { turnstile.remove(turnstileWidgetId); } catch(e) {} turnstileWidgetId = null; } var gate = document.getElementById('neuron-demo-gate'); var msgs = document.getElementById('neuron-demo-messages'); var inputRow = document.getElementById('neuron-demo-input-row'); if (gate) gate.style.display = 'none'; if (msgs) msgs.style.display = 'flex'; if (inputRow) inputRow.style.display = 'flex'; updateCountdown(); _sendIntroGreeting(); var inp = document.getElementById('neuron-demo-text'); if (inp) inp.focus(); }, 'expired-callback': function() { turnstileToken = ''; turnstileVerified = false; } }); } } } }); } }; function addMsg(role, text, skipSave) { var msgs = document.getElementById('neuron-demo-messages'); if (!msgs) return null; var el = document.createElement('div'); el.className = 'demo-msg demo-msg-' + role; var avatar = document.createElement('div'); avatar.className = 'demo-msg-avatar'; if (role === 'ai') { var img = document.createElement('img'); img.src = '/assets/brand/neuron-brain.png'; img.alt = 'Neuron'; avatar.appendChild(img); } else { var svgNS = 'http://www.w3.org/2000/svg'; var svg = document.createElementNS(svgNS, 'svg'); svg.setAttribute('width', '14'); svg.setAttribute('height', '14'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('fill', 'none'); svg.setAttribute('stroke', 'currentColor'); svg.setAttribute('stroke-width', '2'); var p1 = document.createElementNS(svgNS, 'path'); p1.setAttribute('d', 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'); var c1 = document.createElementNS(svgNS, 'circle'); c1.setAttribute('cx', '12'); c1.setAttribute('cy', '7'); c1.setAttribute('r', '4'); svg.appendChild(p1); svg.appendChild(c1); avatar.appendChild(svg); } var bubble = document.createElement('div'); bubble.className = 'demo-msg-bubble'; if (role === 'ai' && typeof marked !== 'undefined') { try { bubble.innerHTML = marked.parse(text); } catch(e) { bubble.textContent = text; } } else { bubble.textContent = text; } if (role === 'ai') { var bodyWrap = document.createElement('div'); bodyWrap.className = 'demo-msg-ai-body'; bodyWrap.appendChild(bubble); if (!skipSave) { var shareBtn = document.createElement('button'); shareBtn.className = 'demo-share-pill'; shareBtn.title = 'Share this response'; shareBtn.textContent = 'Share ↗'; shareBtn.onclick = async function() { var prevUser = ''; if (session.messages) { for (var i = session.messages.length - 1; i >= 0; i--) { if (session.messages[i].role === 'user') { prevUser = session.messages[i].text; break; } } } shareBtn.style.opacity = '0.4'; try { var r = await fetch('/api/share', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ question: prevUser, answer: text }) }); var d = await r.json(); if (d.id) window.open('/share/' + d.id, '_blank'); } catch(e) {} shareBtn.style.opacity = '1'; }; bodyWrap.appendChild(shareBtn); } el.appendChild(avatar); el.appendChild(bodyWrap); } else { el.appendChild(avatar); el.appendChild(bubble); } msgs.appendChild(el); msgs.scrollTop = msgs.scrollHeight; if (!skipSave && role !== 'thinking') { session.messages = session.messages || []; session.messages.push({ role: role, text: text }); if (session.messages.length > 40) session.messages = session.messages.slice(-40); saveSession(session); } return el; } window.neuronDemoSend = async function() { if (msgCount >= MAX) return; var input = document.getElementById('neuron-demo-text'); var btn = document.getElementById('neuron-demo-send'); if (!input || btn.disabled) return; var msg = input.value.trim(); if (!msg) return; var MAX_CHARS = 8000; if (msg.length > MAX_CHARS) { if (input) { input.style.outline = '2px solid #e53e3e'; setTimeout(function() { input.style.outline = ''; }, 2000); } return; } input.value = ''; btn.disabled = true; addMsg('user', msg); var thinking = document.createElement('div'); thinking.className = 'demo-msg demo-msg-thinking'; var thAvatar = document.createElement('div'); thAvatar.className = 'demo-msg-avatar'; var thImg = document.createElement('img'); thImg.src = '/assets/brand/neuron-brain.png'; thImg.alt = 'Neuron'; thAvatar.appendChild(thImg); thinking.appendChild(thAvatar); var thDots = document.createElement('span'); thDots.className = 'demo-msg-thinking-dots'; thDots.innerHTML = ''; thinking.appendChild(thDots); var thMsgsEl = document.getElementById('neuron-demo-messages'); if (thMsgsEl) { thMsgsEl.appendChild(thinking); thMsgsEl.scrollTop = thMsgsEl.scrollHeight; } try { var hist = (session.messages || []).slice(-20).filter(function(m) { return m.role !== 'thinking'; }).map(function(m) { return { role: m.role === 'ai' ? 'assistant' : 'user', content: m.text }; }); var activated_nodes = _ra(session._m, msg); var questionsRemaining = Math.max(0, (MAX - msgCount) - 1); var accessToken = (_supabaseSession && _supabaseSession.access_token) ? _supabaseSession.access_token : ''; var r = await fetch('/api/demo', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ message: msg, history: hist, cf_token: turnstileVerified && !session._cfSent ? turnstileToken : '', uid: session.uid || '', access_token: accessToken, user_name: _userName, user_timezone: _userTimezone, time_of_day: _timeOfDay(), activated_nodes: activated_nodes, engram_node_count: (session._m && session._m.nodes) ? session._m.nodes.length : 0, questions_remaining: questionsRemaining, is_last_question: questionsRemaining === 0 }) }); var d = await r.json(); if (thinking) thinking.remove(); // Server-side rate limit — show a live countdown to reset if (d.rate_limited && d.reset_at) { var _showRateTimer = function() { var now = Math.floor(Date.now() / 1000); var secsLeft = Math.max(0, d.reset_at - now); var hh = Math.floor(secsLeft / 3600); var mm = Math.floor((secsLeft % 3600) / 60); var ss = secsLeft % 60; var pad = function(n) { return n < 10 ? '0' + n : '' + n; }; var ts = hh > 0 ? (hh + ':' + pad(mm) + ':' + pad(ss)) : (pad(mm) + ':' + pad(ss)); return \"You've had 10 conversations today. Come back in \" + ts + \".\"; }; addMsg('ai', _showRateTimer()); // Update the last ai message with a live ticker var _timerInterval = setInterval(function() { var thMsgsInner = document.getElementById('neuron-demo-msgs'); if (!thMsgsInner) { clearInterval(_timerInterval); return; } var aiMsgs = thMsgsInner.querySelectorAll('.neuron-msg-ai'); var lastAi = aiMsgs[aiMsgs.length - 1]; if (lastAi) { lastAi.textContent = _showRateTimer(); } if (Math.floor(Date.now() / 1000) >= d.reset_at) { clearInterval(_timerInterval); if (lastAi) { lastAi.textContent = \"You're all set — conversations reset. Say hello!\"; } if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; } if (btn) { btn.disabled = false; } } }, 1000); if (input) { input.disabled = true; input.placeholder = 'Come back tomorrow...'; } if (btn) { btn.disabled = true; } if (btn) { btn.disabled = false; } if (input) { input.focus(); } return; } // Auth required — show auth pane again if (d.auth_required) { addMsg('ai', 'Please sign in to continue chatting with Neuron.'); _renderWidgetAuthPane(); var msgs2 = document.getElementById('neuron-demo-messages'); var inputRow2 = document.getElementById('neuron-demo-input-row'); if (msgs2) msgs2.style.display = 'none'; if (inputRow2) inputRow2.style.display = 'none'; return; } // Demo disabled by budget circuit breaker if (d.disabled) { addMsg('ai', d.error || 'The demo is temporarily unavailable. Check back soon.'); if (input) { input.disabled = true; input.placeholder = 'Demo unavailable'; } if (btn) { btn.disabled = true; } return; } _um(session, d.sn, d.se); var reply = d.response || d.reply || d.message || ''; var isError = !reply || reply === 'Stepped out for a moment. Try again.'; if (!isError) { msgCount++; session.count = msgCount; saveSession(session); updateCountdown(); if (msgCount >= MAX && input) { input.disabled = true; input.placeholder = 'Interaction limit reached'; } } addMsg('ai', reply || 'Stepped out for a moment. Try again.'); } catch(e) { if (thinking) thinking.remove(); addMsg('ai', 'Stepped out for a moment. Try again.'); } if (msgCount < MAX && btn) btn.disabled = false; if (input) input.focus(); }; var inp = document.getElementById('neuron-demo-text'); if (inp) { inp.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); window.neuronDemoSend(); } }); } })()") }