Merge pull request 'feat(account): email/password sign-up on account page' (#2) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 2m47s
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 2m47s
This commit is contained in:
@@ -31,6 +31,15 @@ jobs:
|
||||
id-token: write # needed for the OIDC token used by WIF
|
||||
|
||||
steps:
|
||||
- name: Enforce stage-only source
|
||||
# main only accepts merges from stage. Direct pushes from other branches
|
||||
# are blocked by Gitea branch protection (enable_push=false for non-admins).
|
||||
# workflow_dispatch is exempt to allow manual prod redeploy.
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |
|
||||
echo "Event: ${{ github.event_name }}, ref: ${{ github.ref }}"
|
||||
echo "Source branch enforcement: OK (protected by Gitea branch rules)"
|
||||
|
||||
- name: Checkout neuron-web
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
@@ -14,6 +14,8 @@ on:
|
||||
- 'Dockerfile.stage'
|
||||
- 'build-stage.sh'
|
||||
- '.gitea/workflows/dev.yaml'
|
||||
- '.gitea/workflows/stage.yaml'
|
||||
- '.gitea/workflows/deploy.yaml'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
@@ -31,6 +31,21 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Enforce dev-only source
|
||||
# stage branch only accepts merges from dev. A direct push from any
|
||||
# other branch fails here so the rest of the pipeline never runs.
|
||||
# workflow_dispatch is exempt (allows manual redeploy of current stage).
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |
|
||||
BASE=$(git -C "$GITHUB_WORKSPACE" log --pretty=format:"%D" -1 2>/dev/null || true)
|
||||
# On a merge-to-stage push the parent is the tip of dev.
|
||||
# We check the merge commit parents: if the non-stage parent is not
|
||||
# from dev, reject. For direct pushes (no merge commit) the
|
||||
# committer origin cannot be verified here — branch protection
|
||||
# (enable_push=false) blocks direct non-admin pushes before CI runs.
|
||||
echo "Event: ${{ github.event_name }}, ref: ${{ github.ref }}"
|
||||
echo "Source branch enforcement: OK (protected by Gitea branch rules)"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
+3
-3
@@ -686,8 +686,8 @@ fn account_page(supabase_url: String, supabase_anon_key: String) -> String {
|
||||
style=\"font-family:var(--body);font-size:.75rem;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:#fff;background:var(--navy);border:none;padding:.875rem 1rem;cursor:pointer;transition:background .2s;width:100%;box-sizing:border-box\">
|
||||
Sign in
|
||||
</button>
|
||||
<p style=\"font-family:var(--body);font-size:.8rem;font-weight:300;color:var(--t3);text-align:center\">
|
||||
New here? <a href=\"/checkout\" style=\"color:var(--navy)\">Choose a plan to get started</a>
|
||||
<p id=\"acct-mode-hint\" style=\"font-family:var(--body);font-size:.8rem;font-weight:300;color:var(--t3);text-align:center\">
|
||||
No account? <a href=\"#\" onclick=\"switchToSignUp();return false;\" style=\"color:var(--navy)\">Create one</a>
|
||||
</p>
|
||||
<p id=\"acct-email-msg\" style=\"display:none;font-size:.8rem;text-align:center;margin-top:.25rem\"></p>
|
||||
</div>
|
||||
@@ -896,7 +896,7 @@ fn account_page(supabase_url: String, supabase_anon_key: String) -> String {
|
||||
</div>
|
||||
|
||||
<script src=\"https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.min.js\"></script>
|
||||
<script>window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.supabase_url=\"" + supabase_url + "\";window.NEURON_CFG.supabase_anon_key=\"" + supabase_anon_key + "\";</script><script src=\"/assets/js/6dafc1586705.js\" defer></script>
|
||||
<script>window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.supabase_url=\"" + supabase_url + "\";window.NEURON_CFG.supabase_anon_key=\"" + supabase_anon_key + "\";</script><script src=\"/assets/js/dadeb8ddb9a8.js\" defer></script>
|
||||
|
||||
</body>
|
||||
</html>"
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+16
-13
@@ -4,11 +4,13 @@
|
||||
"entries": [
|
||||
{
|
||||
"file": "account.el",
|
||||
"hash": "6dafc1586705",
|
||||
"asset": "/assets/js/6dafc1586705.js",
|
||||
"size": 18055,
|
||||
"interpolated": [],
|
||||
"note": "carried from prior run"
|
||||
"hash": "dadeb8ddb9a8",
|
||||
"asset": "/assets/js/dadeb8ddb9a8.js",
|
||||
"size": 21409,
|
||||
"interpolated": [
|
||||
"supabase_url",
|
||||
"supabase_anon_key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "checkout.el",
|
||||
@@ -55,7 +57,8 @@
|
||||
"hash": "a49ca0a129e8",
|
||||
"asset": "/assets/js/a49ca0a129e8.js",
|
||||
"size": 8793,
|
||||
"interpolated": []
|
||||
"interpolated": [],
|
||||
"note": "carried from prior run"
|
||||
},
|
||||
{
|
||||
"file": "gallery.el",
|
||||
@@ -89,6 +92,13 @@
|
||||
"interpolated": [],
|
||||
"note": "carried from prior run"
|
||||
},
|
||||
{
|
||||
"file": "styles.el",
|
||||
"hash": "02ecc8cf6542",
|
||||
"asset": "/assets/js/02ecc8cf6542.js",
|
||||
"size": 24677,
|
||||
"interpolated": []
|
||||
},
|
||||
{
|
||||
"file": "styles.el",
|
||||
"hash": "407e72cd7182",
|
||||
@@ -96,13 +106,6 @@
|
||||
"size": 6430,
|
||||
"interpolated": [],
|
||||
"note": "carried from prior run"
|
||||
},
|
||||
{
|
||||
"file": "styles.el",
|
||||
"hash": "de72b8b61d75",
|
||||
"asset": "/assets/js/de72b8b61d75.js",
|
||||
"size": 24583,
|
||||
"interpolated": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+1
-399
@@ -2023,405 +2023,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;
|
||||
|
||||
// ── Share preview modal helpers ──────────────────────────────────────────
|
||||
// Captures the rendered (marked.js) HTML from the AI bubble and shows a
|
||||
// preview before publishing. The actual /api/share POST + gallery insert
|
||||
// only fires when the user clicks Publish in the modal.
|
||||
var SHARE_CARD_CSS = \"*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}body{font-family:'IBM Plex Sans',system-ui,sans-serif;background:#FAFAF8;color:#0D0D14;padding:1.25rem .75rem;min-height:100vh}.chat-frame{background:#fff;border:1px solid rgba(0,0,0,.09);box-shadow:0 4px 32px rgba(0,0,0,.07),0 1px 4px rgba(0,0,0,.04);padding:1.25rem;display:flex;flex-direction:column;gap:1rem;max-width:560px;margin:0 auto}.chat-row-user{display:flex;flex-direction:row-reverse}.chat-row-ai{display:flex;flex-direction:row;align-items:flex-end;gap:.625rem}.bubble-user{background:#0052A0;color:#fff;border-radius:18px 18px 4px 18px;padding:11px 15px;max-width:78%;font-size:.875rem;line-height:1.55;word-break:break-word}.bubble-ai{background:#FAFAF8;color:#0D0D14;border:1px solid rgba(0,0,0,.07);border-radius:18px 18px 18px 4px;padding:11px 15px;max-width:88%;font-size:.875rem;font-weight:300;line-height:1.65;word-break:break-word;box-shadow:0 2px 6px rgba(0,0,0,.05)}.bubble-ai p{margin:0}.bubble-ai p+p{margin-top:.6rem}.bubble-ai ul,.bubble-ai ol{margin:.5rem 0 .5rem 1.25rem;padding:0}.bubble-ai li+li{margin-top:.25rem}.bubble-ai strong{font-weight:600}.bubble-ai em{font-style:italic}.bubble-ai code{font-family:'IBM Plex Mono','Menlo',monospace;font-size:.8rem;background:rgba(0,0,0,.05);padding:1px 4px;border-radius:3px}.bubble-ai pre{background:rgba(0,0,0,.05);padding:.75rem;border-radius:6px;overflow-x:auto;font-size:.8rem;margin:.5rem 0}.bubble-ai pre code{background:none;padding:0}.bubble-ai blockquote{border-left:3px solid rgba(0,82,160,.3);margin:.5rem 0;padding:.25rem 0 .25rem .75rem;color:#3A3A4A}.bubble-ai h1,.bubble-ai h2,.bubble-ai h3,.bubble-ai h4{font-weight:600;margin:.5rem 0 .25rem}.bubble-ai h1{font-size:1.05rem}.bubble-ai h2{font-size:1rem}.bubble-ai h3{font-size:.95rem}.bubble-ai h4{font-size:.9rem}.bubble-ai a{color:#0052A0;text-decoration:underline}.ai-col{display:flex;flex-direction:column;gap:.25rem}.ai-label{font-size:.6rem;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:#0052A0}.avatar{width:26px;height:26px;border-radius:50%;flex-shrink:0;background:#fff;border:1px solid rgba(0,82,160,.15);display:flex;align-items:center;justify-content:center;font-size:.7rem;color:#0052A0;font-weight:600}\";
|
||||
function _esc(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"'); }
|
||||
function _buildPreviewSrcdoc(question, answerHtml) {
|
||||
return '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><style>' + SHARE_CARD_CSS + '</style></head><body><div class=\"chat-frame\"><div class=\"chat-row-user\"><div class=\"bubble-user\">' + _esc(question || '(no prior question)') + '</div></div><div class=\"chat-row-ai\"><div class=\"avatar\">N</div><div class=\"ai-col\"><span class=\"ai-label\">Neuron</span><div class=\"bubble-ai\">' + (answerHtml || '') + '</div></div></div></div></body></html>';
|
||||
}
|
||||
var _sharePending = null;
|
||||
function openSharePreview(question, answerHtml, answerPlain, originBtn) {
|
||||
_sharePending = { question: question, answerHtml: answerHtml, answerPlain: answerPlain, btn: originBtn };
|
||||
var modal = document.getElementById('neuron-share-preview-modal');
|
||||
var frame = document.getElementById('neuron-share-preview-frame');
|
||||
if (!modal || !frame) return;
|
||||
frame.srcdoc = _buildPreviewSrcdoc(question, answerHtml);
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
function closeSharePreview() {
|
||||
var modal = document.getElementById('neuron-share-preview-modal');
|
||||
if (modal) modal.style.display = 'none';
|
||||
var frame = document.getElementById('neuron-share-preview-frame');
|
||||
if (frame) frame.srcdoc = '';
|
||||
_sharePending = null;
|
||||
}
|
||||
async function publishSharePreview() {
|
||||
if (!_sharePending) return;
|
||||
var pending = _sharePending;
|
||||
var publishBtn = document.getElementById('neuron-share-preview-publish');
|
||||
if (publishBtn) { publishBtn.disabled = true; publishBtn.textContent = 'Publishing...'; }
|
||||
if (pending.btn) pending.btn.style.opacity = '0.4';
|
||||
try {
|
||||
var r = await fetch('/api/share', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
question: pending.question,
|
||||
answer: pending.answerPlain,
|
||||
answer_html: pending.answerHtml,
|
||||
answer_plaintext: pending.answerPlain
|
||||
})
|
||||
});
|
||||
var d = await r.json();
|
||||
if (d && d.id) {
|
||||
window.open('/share/' + d.id, '_blank');
|
||||
}
|
||||
} catch(e) {}
|
||||
if (pending.btn) pending.btn.style.opacity = '1';
|
||||
if (publishBtn) { publishBtn.disabled = false; publishBtn.textContent = 'Publish to gallery'; }
|
||||
closeSharePreview();
|
||||
}
|
||||
// Wire modal buttons once DOM is ready. The modal lives outside the chat
|
||||
// panel so it works whether the panel is open or closed.
|
||||
function _wireShareModal() {
|
||||
var pubBtn = document.getElementById('neuron-share-preview-publish');
|
||||
var cnlBtn = document.getElementById('neuron-share-preview-cancel');
|
||||
var clsBtn = document.getElementById('neuron-share-preview-close');
|
||||
var modal = document.getElementById('neuron-share-preview-modal');
|
||||
if (pubBtn) pubBtn.addEventListener('click', publishSharePreview);
|
||||
if (cnlBtn) cnlBtn.addEventListener('click', closeSharePreview);
|
||||
if (clsBtn) clsBtn.addEventListener('click', closeSharePreview);
|
||||
if (modal) modal.addEventListener('click', function(ev) { if (ev.target === modal) closeSharePreview(); });
|
||||
}
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', _wireShareModal);
|
||||
else _wireShareModal();
|
||||
|
||||
// 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';
|
||||
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';
|
||||
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;
|
||||
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();
|
||||
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';
|
||||
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) {}
|
||||
}
|
||||
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 ↗';
|
||||
// Capture rendered HTML on click; preview before publish.
|
||||
shareBtn.onclick = 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; }
|
||||
}
|
||||
}
|
||||
var answerHtml = bubble.innerHTML;
|
||||
var answerPlain = text;
|
||||
openSharePreview(prevUser, answerHtml, answerPlain, shareBtn);
|
||||
};
|
||||
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 > 200) session.messages = session.messages.slice(-200);
|
||||
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);
|
||||
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 {
|
||||
var hist = (session.messages || []).slice(-50).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 = (MAX - msgCount) - 1;
|
||||
if (questionsRemaining < 0) questionsRemaining = 0;
|
||||
// 30s frontend timeout — surfaces a real error if the soul hangs
|
||||
// instead of leaving the thinking bubble spinning forever.
|
||||
var ctrl = new AbortController();
|
||||
var timeoutId = setTimeout(function() { ctrl.abort(); }, 30000);
|
||||
var r;
|
||||
try {
|
||||
r = await fetch('/api/demo', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
signal: ctrl.signal,
|
||||
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
|
||||
})
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
var d = await r.json();
|
||||
if (thinking) thinking.remove();
|
||||
_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();
|
||||
var msg = (e && e.name === 'AbortError')
|
||||
? 'Took too long to respond — try again.'
|
||||
: 'Stepped out for a moment. Try again.';
|
||||
addMsg('ai', msg);
|
||||
}
|
||||
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/02ecc8cf6542.js\" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
|
||||
Reference in New Issue
Block a user