5f35ddde39
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 8s
When a user hits the 10-question limit, the header countdown flips from '0 questions left' to a live 'resets in HH:MM:SS' ticker counting to midnight UTC. Clears automatically when the session resets.
391 lines
16 KiB
EmacsLisp
391 lines
16 KiB
EmacsLisp
// chat-widget.el -- Neuron demo chat widget with 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
|
|
|
|
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;
|
|
|
|
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';
|
|
}
|
|
}
|
|
|
|
window.neuronDemoReset = function() {
|
|
if (_headerResetInterval) { clearInterval(_headerResetInterval); _headerResetInterval = null; }
|
|
clearSession();
|
|
session = { messages: [], count: 0, context: '' };
|
|
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;
|
|
addMsg('ai', 'Hey. What is on your mind?', true);
|
|
};
|
|
|
|
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 && 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) {
|
|
addMsg('ai', 'Hey. What is on your mind?', true);
|
|
session.greeted = true;
|
|
saveSession(session);
|
|
}
|
|
}
|
|
var input = document.getElementById('neuron-demo-text');
|
|
if (isOpen && input && !input.disabled) input.focus();
|
|
updateCountdown();
|
|
if (isOpen && !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';
|
|
addMsg('ai', 'Hey. What is on your mind?', true);
|
|
updateCountdown();
|
|
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 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 || '',
|
|
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;
|
|
}
|
|
|
|
_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(); }
|
|
});
|
|
}
|
|
})()")
|
|
}
|