Files
neuron-web/src/js/chat-widget.el
T
will.anderson 2b8915bd60 Fix JS syntax errors and stage supabase-config CORS in CI
chat-widget.el: apostrophe in El native_js double-quoted strings caused
the El compiler to drop the backslash, producing broken JS single-quoted
strings. Switched those four string literals to double-quoted JS strings
using \" escaping so the compiled output is valid.

main.el: /api/supabase-config was returning 403 for all stage Cloud Run
origins. Added marketing-stage-* prefix to the allowed list so the
checkout page can initialise Supabase during CI E2E runs.
2026-05-11 12:15:18 -05:00

604 lines
25 KiB
EmacsLisp

// 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 = '<svg width=\"18\" height=\"18\" viewBox=\"0 0 48 48\"><path fill=\"#EA4335\" d=\"M24 9.5c3.14 0 5.95 1.08 8.17 2.84l6.09-6.09C34.46 3.19 29.53 1 24 1 14.62 1 6.68 6.84 3.32 15.09l7.1 5.52C12.16 14.02 17.6 9.5 24 9.5z\"/><path fill=\"#4285F4\" d=\"M46.5 24.5c0-1.64-.15-3.22-.42-4.75H24v9h12.7c-.55 2.99-2.2 5.53-4.68 7.24l7.19 5.59C43.07 37.23 46.5 31.3 46.5 24.5z\"/><path fill=\"#FBBC05\" d=\"M10.42 28.39A14.6 14.6 0 0 1 9.5 24c0-1.52.26-3 .72-4.39l-7.1-5.52A23.5 23.5 0 0 0 .5 24c0 3.78.88 7.36 2.44 10.56l7.48-6.17z\"/><path fill=\"#34A853\" d=\"M24 47c5.53 0 10.17-1.83 13.56-4.97l-7.19-5.59C28.56 37.88 26.38 38.5 24 38.5c-6.4 0-11.84-4.52-13.58-10.61l-7.48 6.17C6.68 43.16 14.62 47 24 47z\"/><path fill=\"none\" d=\"M0 0h48v48H0z\"/></svg> 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 = '<input type=\"email\" id=\"demo-auth-email\" placeholder=\"Email\" autocomplete=\"email\" />'
+ '<input type=\"password\" id=\"demo-auth-password\" placeholder=\"Password\" autocomplete=\"current-password\" />'
+ '<button class=\"demo-auth-submit-btn\" id=\"demo-auth-submit\">Sign in</button>'
+ '<p class=\"demo-auth-msg\" id=\"demo-auth-msg\" style=\"display:none\"></p>';
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 = '<span></span><span></span><span></span>';
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(); }
});
}
})()")
}