migrate stage build to native elc; chat restores from localStorage on return
Build pipeline - build-stage.sh replaces the old in-Dockerfile bootstrap.py path. Host pre-compiles src/*.el into dist/main.c via the canonical native elc at foundation/el/dist/platform/elc and applies the stub-decl sed before docker buildx runs. - Dockerfile.stage drops bootstrap.py + python3 from the builder stage and just runs cc on the host-supplied dist/main.c. - Pre-rendered HTML shells under /srv/landing/ are now chowned to the landing user so the El page-builder's fs_write at startup can rewrite them — without that, post-COPY edits never reach the served HTML and the served page stays as the stale build-time fallback. Chat restore - session.verified + session.verifiedAt persist through localStorage so a return visit within 24h skips the Turnstile gate and lands directly in the restored conversation. - restoreOrGreet() is the single source of truth for what shows up in the message pane after the gate clears: replays prior messages with skipSave, else drops the canned hello once and remembers it. - applyVerifiedDom() hides the gate / reveals the chat row, called both from the verified-on-load path (DOMContentLoaded if loading, else immediate) and from the Turnstile callback. - neuronDemoReset clears verified + verifiedAt so the gate returns next open. Extracted JS assets (src/assets/js/*.js + manifest.json) and the extract-js.py helper land here too — they match what the new build-stage flow produces and removes the inline <script> blobs from the served HTML.
This commit is contained in:
+3
-442
@@ -1708,7 +1708,7 @@ fn page_open() -> String {
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-left: 2.5rem;
|
||||
margin-left: 0;
|
||||
align-self: flex-start;
|
||||
transition: background 0.15s, border-color 0.15s, transform 0.1s;
|
||||
line-height: 1;
|
||||
@@ -1920,106 +1920,7 @@ fn page_open() -> String {
|
||||
|
||||
fn page_close() -> String {
|
||||
return "
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Nav scroll effect
|
||||
var nav = document.getElementById('nav');
|
||||
if (nav) {
|
||||
window.addEventListener('scroll', function() {
|
||||
if (window.scrollY > 40) {
|
||||
nav.classList.add('scrolled');
|
||||
} else {
|
||||
nav.classList.remove('scrolled');
|
||||
}
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
// Auto-open chat if ?open=chat in URL
|
||||
if (typeof URLSearchParams !== 'undefined' && new URLSearchParams(window.location.search).get('open') === 'chat') {
|
||||
// Wait for widget to initialize, then open
|
||||
setTimeout(function() { if (typeof neuronDemoToggle === 'function') neuronDemoToggle(); }, 600);
|
||||
}
|
||||
|
||||
// Scroll reveal via IntersectionObserver
|
||||
var revealEls = document.querySelectorAll('.reveal');
|
||||
if ('IntersectionObserver' in window) {
|
||||
var observer = new IntersectionObserver(function(entries) {
|
||||
entries.forEach(function(entry) {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
|
||||
revealEls.forEach(function(el) { observer.observe(el); });
|
||||
} else {
|
||||
revealEls.forEach(function(el) { el.classList.add('visible'); });
|
||||
}
|
||||
|
||||
// ── Founding counter - live polling ──────────────────────────────────────
|
||||
var prevSold = null;
|
||||
function updateFoundingUI(data) {
|
||||
var remaining = data.remaining;
|
||||
var sold = data.sold;
|
||||
var total = data.total;
|
||||
var pct = Math.round((sold / total) * 100);
|
||||
var flash = prevSold !== null && sold > prevSold;
|
||||
prevSold = sold;
|
||||
|
||||
// spots card (inside pricing card)
|
||||
var spotLabel = document.querySelector('.founding-spots-label');
|
||||
if (spotLabel) spotLabel.textContent = 'Only ' + remaining + ' left';
|
||||
var spotFill = document.querySelector('.founding-spots-fill');
|
||||
if (spotFill) spotFill.style.width = pct + '%';
|
||||
var spotSub = document.querySelector('.founding-spots-sub');
|
||||
if (spotSub) spotSub.textContent = sold + ' of ' + total + ' claimed';
|
||||
|
||||
// banner
|
||||
var bannerCount = document.querySelector('.founding-banner-count');
|
||||
if (bannerCount) {
|
||||
bannerCount.textContent = remaining;
|
||||
if (flash) {
|
||||
bannerCount.style.color = '#0078D4';
|
||||
setTimeout(function() { bannerCount.style.color = ''; }, 1200);
|
||||
}
|
||||
}
|
||||
var bannerFill = document.querySelector('.founding-banner-fill');
|
||||
if (bannerFill) bannerFill.style.width = pct + '%';
|
||||
}
|
||||
|
||||
function pollFoundingCount() {
|
||||
fetch('/api/founding-count')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) { updateFoundingUI(data); })
|
||||
.catch(function() {});
|
||||
}
|
||||
pollFoundingCount();
|
||||
setInterval(pollFoundingCount, 90000);
|
||||
|
||||
// Hide chat on checkout, account, legal pages — not relevant there
|
||||
if (window.location.pathname.indexOf('/checkout') === 0 ||
|
||||
window.location.pathname.indexOf('/account') === 0 ||
|
||||
window.location.pathname.indexOf('/legal') === 0 ||
|
||||
window.location.pathname.indexOf('/marketplace/success') === 0) {
|
||||
var demoBtn = document.getElementById('neuron-demo-btn');
|
||||
var demoPanel = document.getElementById('neuron-demo-panel');
|
||||
if (demoBtn) demoBtn.style.display = 'none';
|
||||
if (demoPanel) demoPanel.style.display = 'none';
|
||||
}
|
||||
|
||||
// Checkout buttons - navigate to integrated payment page
|
||||
var checkoutBtns = document.querySelectorAll('[data-checkout]');
|
||||
checkoutBtns.forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var plan = btn.getAttribute('data-checkout');
|
||||
window.location.href = '/checkout?plan=' + plan;
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
||||
</script>
|
||||
<script src=\"/assets/js/407e72cd7182.js\" defer></script>
|
||||
|
||||
<!-- ── Neuron Demo Chat Widget ──────────────────────────────────────────────── -->
|
||||
<div id=\"neuron-demo-btn\">
|
||||
@@ -2055,347 +1956,7 @@ fn page_close() -> String {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(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;
|
||||
|
||||
// Persistent session storage - survives page refreshes
|
||||
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] }; });
|
||||
}
|
||||
|
||||
// ?reset=1 clears the session and reloads clean
|
||||
if (window.location.search.indexOf('reset=1') !== -1) {
|
||||
clearSession();
|
||||
var clean = window.location.pathname;
|
||||
window.history.replaceState({}, '', clean);
|
||||
}
|
||||
|
||||
var session = loadSession();
|
||||
// Ensure every user has a stable unique session ID.
|
||||
if (!session.uid) {
|
||||
session.uid = 'u' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
||||
saveSession(session);
|
||||
}
|
||||
var msgCount = session.count || 0;
|
||||
|
||||
function updateCountdown() {
|
||||
var el = document.getElementById('neuron-demo-countdown');
|
||||
if (!el) return;
|
||||
var remaining = MAX - msgCount;
|
||||
el.textContent = remaining + ' question' + (remaining === 1 ? '' : 's') + ' left';
|
||||
// Always bold white. The number itself counts down naturally; the
|
||||
// colour-shift was loud and made the chat feel rationed even when
|
||||
// there were plenty of turns left.
|
||||
el.style.color = '#ffffff';
|
||||
el.style.fontWeight = '700';
|
||||
}
|
||||
|
||||
window.neuronDemoReset = function() {
|
||||
try { localStorage.removeItem('neuron_demo_session'); } catch(e) {}
|
||||
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';
|
||||
// Hide launch button on mobile when panel is open (panel covers it)
|
||||
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) {
|
||||
// Restore previous conversation from localStorage so the visitor
|
||||
// picks up where they left off. Only greet once - the `greeted` flag
|
||||
// sticks in the session so reopening the panel doesn't replay the
|
||||
// canned hello.
|
||||
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;
|
||||
try { localStorage.setItem('neuron_demo_session', JSON.stringify(session)); } catch(e) {}
|
||||
}
|
||||
}
|
||||
var input = document.getElementById('neuron-demo-text');
|
||||
if (isOpen && input && !input.disabled) input.focus();
|
||||
// Init Turnstile on first open
|
||||
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;
|
||||
// Destroy the widget completely
|
||||
if (typeof turnstile !== 'undefined' && turnstileWidgetId !== null) {
|
||||
try { turnstile.remove(turnstileWidgetId); } catch(e) {}
|
||||
turnstileWidgetId = null;
|
||||
}
|
||||
// Swap gate for chat
|
||||
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';
|
||||
// Show opening message
|
||||
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;
|
||||
// Avatar - use DOM API to avoid quote escaping issues
|
||||
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);
|
||||
}
|
||||
// Bubble
|
||||
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;
|
||||
// Persist to localStorage unless restoring
|
||||
if (!skipSave && role !== 'thinking') {
|
||||
session.messages = session.messages || [];
|
||||
session.messages.push({ role: role, text: text });
|
||||
// Keep last 40 messages to avoid storage bloat
|
||||
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;
|
||||
input.value = '';
|
||||
btn.disabled = true;
|
||||
addMsg('user', msg);
|
||||
// Thinking indicator with brain avatar + animated dots
|
||||
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;
|
||||
}
|
||||
if (turnstileVerified && !session._cfSent) { session._cfSent = true; }
|
||||
try {
|
||||
// Build history from session for soul context
|
||||
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};
|
||||
});
|
||||
// Activate local engram nodes relevant to this message
|
||||
var activated_nodes = _ra(session._m, msg);
|
||||
// questions_remaining tells the soul how many turns the visitor has
|
||||
// LEFT after this one. is_last_question is true when this turn is
|
||||
// the visitor final turn under the rate limit, so the soul can
|
||||
// close the conversation in voice instead of leaving them on a hard cap.
|
||||
var questionsRemaining = (MAX - msgCount) - 1;
|
||||
if (questionsRemaining < 0) questionsRemaining = 0;
|
||||
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();
|
||||
// Merge session nodes returned by soul into local engram
|
||||
_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) {
|
||||
// Only count as an interaction on a real response
|
||||
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(); }
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script src=\"/assets/js/fc247ef45b1d.js\" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
|
||||
Reference in New Issue
Block a user