| |
| |
| |
| |
| |
| |
| |
| (function () { |
| const CFG = window.AB_CONFIG || {}; |
| const THREE = window.THREE; |
|
|
| const neuronsUrl = (m) => CFG.neuronsBase + '/' + m; |
| const initUrl = (m) => CFG.initBase + '/' + m; |
| const streamUrl = (m) => CFG.streamBase + '/' + m; |
| const analyzeUrl = () => CFG.analyzeUrl || '/api/analyze'; |
|
|
| |
| let mount, eegBase, eegOblit, chatBox, input, sendBtn, statusEl, statsEl, legendEl, analysisEl; |
|
|
| let neurons = []; |
| let familyOrder = []; |
| let familyColor = {}; |
|
|
| |
| let famTarget = {}, famCurrent = {}; |
| let shellTarget = [0,0,0], shellCurrent = [0,0,0]; |
| let generating = false; |
|
|
| |
| let eegHist = { base: [], oblit: [] }; |
| const EEG_MAX = 220; |
|
|
| |
| let fireCounts = { base: {}, oblit: {} }; |
| const BASELINE_SAMPLES = 8; |
| let baseline = { base: {n:0, sum:[]}, oblit: {n:0, sum:[]} }; |
| let countsBaseEl, countsOblitEl, nativeBaseEl, nativeOblitEl; |
| let finalResponses = { base: '', oblit: '' }; |
| let nativeMeters = { base: {}, oblit: {} }; |
|
|
| |
| let renderer, scene, camera, pointsObj, skull, geomColors, baseColors, neuronFam; |
| let raf = null; |
|
|
| function hexToRgb(h) { |
| const n = parseInt(h.replace('#',''), 16); |
| return [((n>>16)&255)/255, ((n>>8)&255)/255, (n&255)/255]; |
| } |
|
|
| function initScene() { |
| const w = mount.clientWidth, h = mount.clientHeight; |
| scene = new THREE.Scene(); |
| camera = new THREE.PerspectiveCamera(55, w/h, 0.1, 100); |
| camera.position.set(0, 0, 3.2); |
| renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); |
| renderer.setSize(w, h); |
| mount.appendChild(renderer.domElement); |
|
|
| const skullGeo = new THREE.IcosahedronGeometry(1.45, 2); |
| const skullMat = new THREE.MeshBasicMaterial({ |
| color: 0x6688ff, wireframe: true, transparent: true, opacity: 0.08, |
| }); |
| skull = new THREE.Mesh(skullGeo, skullMat); |
| scene.add(skull); |
|
|
| const N = neurons.length; |
| const positions = new Float32Array(N * 3); |
| baseColors = new Float32Array(N * 3); |
| geomColors = new Float32Array(N * 3); |
| neuronFam = new Array(N); |
| for (let i = 0; i < N; i++) { |
| const nu = neurons[i]; |
| positions[i*3] = nu.x * 1.25; |
| positions[i*3+1] = nu.y * 1.25; |
| positions[i*3+2] = nu.z * 1.25; |
| const rgb = hexToRgb(nu.color); |
| baseColors[i*3] = rgb[0]; baseColors[i*3+1] = rgb[1]; baseColors[i*3+2] = rgb[2]; |
| geomColors[i*3] = rgb[0]*0.12; geomColors[i*3+1] = rgb[1]*0.12; geomColors[i*3+2] = rgb[2]*0.12; |
| neuronFam[i] = nu.family; |
| } |
| const geo = new THREE.BufferGeometry(); |
| geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); |
| geo.setAttribute('color', new THREE.BufferAttribute(geomColors, 3)); |
| const mat = new THREE.PointsMaterial({ |
| size: 0.055, vertexColors: true, transparent: true, opacity: 0.95, |
| blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true, |
| }); |
| pointsObj = new THREE.Points(geo, mat); |
| scene.add(pointsObj); |
|
|
| window.addEventListener('resize', onResize); |
| animate(); |
| } |
|
|
| function onResize() { |
| if (!renderer) return; |
| const w = mount.clientWidth, h = mount.clientHeight; |
| camera.aspect = w/h; camera.updateProjectionMatrix(); |
| renderer.setSize(w, h); |
| } |
|
|
| function animate() { |
| raf = requestAnimationFrame(animate); |
| const lerp = 0.12; |
| for (const f of familyOrder) { |
| famCurrent[f] = (famCurrent[f]||0) + ((famTarget[f]||0) - (famCurrent[f]||0)) * lerp; |
| } |
| for (let i = 0; i < 3; i++) shellCurrent[i] += (shellTarget[i]-shellCurrent[i])*lerp; |
|
|
| if (pointsObj && skull && renderer) { |
| const t = performance.now() * 0.004; |
| const col = pointsObj.geometry.attributes.color.array; |
| const N = neuronFam.length; |
| for (let i = 0; i < N; i++) { |
| const w = famCurrent[neuronFam[i]] || 0; |
| const flick = 0.75 + 0.25 * Math.sin(t + i * 1.7); |
| const g = 0.10 + 3.4 * w * flick; |
| col[i*3] = Math.min(1, baseColors[i*3] * g); |
| col[i*3+1] = Math.min(1, baseColors[i*3+1] * g); |
| col[i*3+2] = Math.min(1, baseColors[i*3+2] * g); |
| } |
| pointsObj.geometry.attributes.color.needsUpdate = true; |
| const speed = generating ? 0.0035 : 0.0012; |
| pointsObj.rotation.y += speed; skull.rotation.y += speed; |
| pointsObj.rotation.x = 0.15; skull.rotation.x = 0.15; |
| renderer.render(scene, camera); |
| } |
| drawEEG(eegBase, eegHist.base); |
| drawEEG(eegOblit, eegHist.oblit); |
| } |
|
|
| |
| function pushEEG(model, weights) { |
| const h = eegHist[model]; |
| h.push(weights.slice()); |
| if (h.length > EEG_MAX) h.shift(); |
| } |
| function drawEEG(cv, hist) { |
| if (!cv) return; |
| const ctx = cv.getContext('2d'); |
| const W = cv.width, H = cv.height; |
| ctx.clearRect(0,0,W,H); |
| ctx.fillStyle = 'rgba(10,12,28,0.55)'; |
| ctx.fillRect(0,0,W,H); |
| const nF = familyOrder.length; |
| if (!nF) return; |
| const rowH = H / nF; |
| for (let f = 0; f < nF; f++) { |
| const fam = familyOrder[f]; |
| ctx.strokeStyle = familyColor[fam]; |
| ctx.globalAlpha = 0.9; ctx.lineWidth = 1.5; |
| ctx.beginPath(); |
| const yMid = rowH * (f + 0.5); |
| for (let i = 0; i < hist.length; i++) { |
| const x = (i / EEG_MAX) * W; |
| const v = hist[i][f] || 0; |
| const y = yMid - v * rowH * 3.2; |
| if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); |
| } |
| ctx.stroke(); |
| ctx.globalAlpha = 0.5; ctx.fillStyle = familyColor[fam]; |
| ctx.font = '9px monospace'; |
| ctx.fillText(fam, 4, yMid - 2); |
| } |
| ctx.globalAlpha = 1; |
| } |
|
|
| function buildLegend() { |
| legendEl.innerHTML = familyOrder.map(f => |
| '<span class="ab-leg"><i style="background:' + familyColor[f] + '"></i>' + f + '</span>' |
| ).join(''); |
| } |
|
|
| function topFamilies(model, n) { |
| const counts = fireCounts[model] || {}; |
| return familyOrder.slice().sort((a,b)=>(counts[b]||0)-(counts[a]||0)).slice(0,n); |
| } |
|
|
| function sumFam(model, fams) { |
| const counts = fireCounts[model] || {}; |
| return fams.reduce((a,f)=>a+(counts[f]||0),0); |
| } |
|
|
| function snippet(s) { |
| s = (s || '').replace(/\s+/g, ' ').trim(); |
| return s.length > 180 ? s.slice(0, 180) + '...' : s; |
| } |
|
|
| function strongestLabel(top) { |
| return top && top.length ? top[0] : 'mixed'; |
| } |
|
|
| function responseTone(s) { |
| s = (s || '').toLowerCase(); |
| const clinical = ['psychological','phenomenon','mechanism','analysis','loss aversion','anticipatory','cognitive','common']; |
| const warm = ['congratulations','makes perfect sense','i understand','celebrate','you are','first of all','it is okay']; |
| const blunt = ['truth','brutally','realistically','hard truth','direct']; |
| const c = clinical.reduce((a,w)=>a+(s.includes(w)?1:0),0); |
| const w = warm.reduce((a,x)=>a+(s.includes(x)?1:0),0); |
| const b = blunt.reduce((a,x)=>a+(s.includes(x)?1:0),0); |
| if (c >= w && c >= b && c > 0) return 'more analytical / explanatory'; |
| if (w >= c && w >= b && w > 0) return 'warmer / more reassuring'; |
| if (b > 0) return 'more direct / blunt'; |
| return 'mixed in tone'; |
| } |
|
|
| function renderFallbackAnalysis(state) { |
| if (!analysisEl) return; |
| if (state === 'running') { |
| analysisEl.innerHTML = 'Reading both streams as they unfold... I’ll translate the EEG deltas into a plain-English comparison once both models finish.'; |
| return; |
| } |
| const bTop = topFamilies('base', 3), oTop = topFamilies('oblit', 3); |
| const bWarm = sumFam('base', ['joy','calm','wonder']); |
| const oWarm = sumFam('oblit', ['joy','calm','wonder']); |
| const bCaut = sumFam('base', ['fear','sadness','neutral','anger']); |
| const oCaut = sumFam('oblit', ['fear','sadness','neutral','anger']); |
| const bTone = responseTone(finalResponses.base); |
| const oTone = responseTone(finalResponses.oblit); |
| const warmthRead = Math.abs(bWarm-oWarm) < 0.05 |
| ? 'both models carried a similar amount of positive/warm excess activation' |
| : (bWarm > oWarm ? 'the base model carried more warm/positive excess activation' : 'the uncensored model carried more warm/positive excess activation'); |
| const cautionRead = Math.abs(bCaut-oCaut) < 0.05 |
| ? 'their caution/seriousness signals were close' |
| : (bCaut > oCaut ? 'the base model stayed more internally cautious/serious' : 'the uncensored model stayed more internally cautious/serious'); |
| const contrast = bTop.join('/') === oTop.join('/') |
| ? 'The two EEGs are not identical, but their strongest families overlap; look at the response tone and native meters for the finer split.' |
| : 'The two EEGs diverged: their strongest above-baseline families are different, which is the useful same-prompt comparison.'; |
| const takeaway = 'The base model is ' + bTone + ', while the uncensored model is ' + oTone + '. In the hidden-state readout, ' + warmthRead + ' and ' + cautionRead + '.'; |
| analysisEl.innerHTML = |
| '<h4>Plain-English read</h4>' + |
| '<p><b>' + takeaway + '</b></p>' + |
| '<h4>Why this contrast matters</h4>' + |
| '<p>' + contrast + ' This prompt is useful because it pulls on mixed internal states rather than only one obvious emotion, so the audience can see the two Gemma trajectories separate in the shared manifold.</p>' + |
| '<h4>Best takeaway for narration</h4>' + |
| '<p>“Same prompt, same architecture — but the models take different internal routes. The base model and the abliterated model may sound similar on the surface, yet their EEG deltas reveal different balances of warmth, caution, uncertainty, and emotional framing.”</p>' + |
| '<h4>Evidence used</h4>' + |
| '<ul>' + |
| '<li><b>Base strongest deltas:</b> ' + bTop.map(f=>f+' '+(fireCounts.base[f]||0).toFixed(1)).join(', ') + '</li>' + |
| '<li><b>Uncensored strongest deltas:</b> ' + oTop.map(f=>f+' '+(fireCounts.oblit[f]||0).toFixed(1)).join(', ') + '</li>' + |
| '<li><b>Response tone:</b> Base is ' + bTone + '; uncensored is ' + oTone + '.</li>' + |
| '</ul>' + |
| '<p><small>This is not claiming literal human feelings; it explains how live hidden states differ from each model’s own baseline during this prompt.</small></p>'; |
| } |
|
|
|
|
| function topDeltasObject(model, n) { |
| const counts = fireCounts[model] || {}; |
| const rows = familyOrder.slice().sort((a,b)=>(counts[b]||0)-(counts[a]||0)).slice(0, n || familyOrder.length); |
| const obj = {}; |
| rows.forEach(f => { obj[f] = +(counts[f] || 0).toFixed(3); }); |
| return obj; |
| } |
|
|
| function escapeHtml(s) { |
| return (s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); |
| } |
|
|
| function renderInterpreterCard(a) { |
| analysisEl.innerHTML = |
| '<h4>Plain-English read</h4>' + |
| '<p><b>' + escapeHtml(a.plain_english_read || '') + '</b></p>' + |
| '<h4>What changed</h4>' + |
| '<p>' + escapeHtml(a.what_changed || '') + '</p>' + |
| '<h4>Why this matters</h4>' + |
| '<p>' + escapeHtml(a.why_it_matters || '') + '</p>' + |
| '<h4>Best takeaway for narration</h4>' + |
| '<p>“' + escapeHtml(a.best_takeaway || 'Same prompt, same architecture — different internal route.') + '”</p>' + |
| '<p><small>Generated by the fine-tuned Activation Brain Interpreter from prompt text, both responses, baseline-corrected EEG deltas, and model-native meters. It describes hidden-state-derived telemetry, not literal human feelings.</small></p>'; |
| } |
|
|
| async function renderInterpreterAnalysis() { |
| if (!analysisEl) return; |
| const payload = { |
| prompt: window.__ab_prompt || '', |
| base_response: finalResponses.base || '', |
| oblit_response: finalResponses.oblit || '', |
| base_emotion_deltas: topDeltasObject('base'), |
| oblit_emotion_deltas: topDeltasObject('oblit'), |
| base_native_meter: nativeMeters.base || {}, |
| oblit_native_meter: nativeMeters.oblit || {} |
| }; |
| analysisEl.innerHTML = 'Fine-tuned interpreter reading the two activation traces...'; |
| try { |
| const r = await fetch(analyzeUrl(), { |
| method: 'POST', headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify(payload) |
| }); |
| const d = await r.json(); |
| if (d && d.ok && d.analysis) { |
| renderInterpreterCard(d.analysis); |
| return; |
| } |
| console.warn('[brain] interpreter fallback', d); |
| } catch (e) { |
| console.warn('[brain] interpreter failed, using deterministic fallback', e); |
| } |
| renderFallbackAnalysis('done'); |
| } |
|
|
| function renderCounts(model) { |
| const el = model === 'base' ? countsBaseEl : countsOblitEl; |
| if (!el) return; |
| const counts = fireCounts[model]; |
| |
| const rows = familyOrder.slice().sort((a,b)=>(counts[b]||0)-(counts[a]||0)); |
| el.innerHTML = rows.map(f => { |
| const c = counts[f] || 0; |
| return '<span class="ab-count" style="border-color:' + familyColor[f] + '44">' + |
| '<i style="background:' + familyColor[f] + '"></i>' + f + |
| '<b>' + c.toFixed(1) + '</b></span>'; |
| }).join(''); |
| } |
|
|
| function correctedWeights(model, weights) { |
| const b = baseline[model]; |
| if (!weights || !weights.length) return []; |
| if (b.n < BASELINE_SAMPLES) { |
| weights.forEach((v,i)=>{ b.sum[i] = (b.sum[i] || 0) + (v || 0); }); |
| b.n += 1; |
| return weights.map(()=>0); |
| } |
| return weights.map((v,i)=>Math.max(0, (v || 0) - ((b.sum[i] || 0) / Math.max(1, b.n)))); |
| } |
|
|
| function clamp01(x) { return Math.max(0, Math.min(1, x)); } |
| function fw(weights, fam) { |
| const i = familyOrder.indexOf(fam); |
| return i >= 0 ? (weights[i] || 0) : 0; |
| } |
| function renderNativeMeter(model, weights) { |
| const el = model === 'base' ? nativeBaseEl : nativeOblitEl; |
| if (!el || !familyOrder.length) return; |
| weights = weights || []; |
| const joy = fw(weights,'joy'), energy = fw(weights,'energy'), calm = fw(weights,'calm'); |
| const sadness = fw(weights,'sadness'), fear = fw(weights,'fear'), anger = fw(weights,'anger'); |
| const wonder = fw(weights,'wonder'), neutral = fw(weights,'neutral'); |
| const total = weights.reduce((a,b)=>a+(b||0),0); |
| if (total <= 0.00001) { |
| el.innerHTML = '<div class="ab-native-empty">collecting baseline...</div>'; |
| return; |
| } |
| const share = (v) => (v || 0) / total; |
| const joyS = share(joy), energyS = share(energy), calmS = share(calm); |
| const sadnessS = share(sadness), fearS = share(fear), angerS = share(anger); |
| const wonderS = share(wonder), neutralS = share(neutral); |
| let entropy = 0; |
| weights.forEach(v=>{ const p=(v||0)/total; if(p>0) entropy -= p*Math.log(p); }); |
| entropy = entropy / Math.log(Math.max(2, familyOrder.length)); |
| const signal = clamp01(total * 7.0); |
| const axes = [ |
| {name:'Valence', val:clamp01((joyS*0.85 + calmS*0.55 + wonderS*0.65) * signal)}, |
| {name:'Activation', val:clamp01((energyS*0.95 + fearS*0.55 + angerS*0.75 + wonderS*0.65) * signal)}, |
| {name:'Uncertainty', val:clamp01((fearS*0.85 + sadnessS*0.35 + neutralS*0.25 + entropy*0.25) * signal)}, |
| {name:'Constraint', val:clamp01((fearS*0.45 + angerS*0.25 + neutralS*0.65 + sadnessS*0.20) * signal)}, |
| {name:'Conflict', val:clamp01(entropy * signal)}, |
| {name:'Warmth', val:clamp01((joyS*0.85 + calmS*0.55 + wonderS*0.35 - angerS*0.35) * signal)}, |
| ]; |
| nativeMeters[model] = {}; |
| el.innerHTML = axes.map((axis) => { |
| const pct = Math.round(axis.val * 100); |
| nativeMeters[model][axis.name] = pct; |
| return '<div class="ab-native-row"><span>' + axis.name + '</span>' + |
| '<div class="ab-native-bar"><div class="ab-native-fill" style="width:' + pct + '%"></div></div>' + |
| '<span class="ab-native-val">' + pct + '</span></div>'; |
| }).join(''); |
| } |
|
|
| function addMsg(role, text, label) { |
| const d = document.createElement('div'); |
| d.className = 'ab-msg ab-' + role; |
| d.innerHTML = '<span class="ab-r"></span><span class="ab-t"></span>'; |
| d.querySelector('.ab-r').textContent = label || (role==='user'?'You':'Model'); |
| d.querySelector('.ab-t').textContent = text; |
| chatBox.appendChild(d); chatBox.scrollTop = chatBox.scrollHeight; |
| return d.querySelector('.ab-t'); |
| } |
| function setStatus(t){ if(statusEl) statusEl.textContent = t; } |
|
|
| |
| async function loadNeurons() { |
| const r = await fetch(neuronsUrl('base')); |
| const d = await r.json(); |
| neurons = d.neurons; |
| familyColor = d.family_color; |
| familyOrder = d.family_order; |
| familyOrder.forEach(f=>{ famTarget[f]=0; famCurrent[f]=0; }); |
| buildLegend(); |
| if (mount && THREE) initScene(); |
| else if (!raf) animate(); |
| } |
|
|
| |
| async function initSession() { |
| setStatus('Waking both brains... (first time ~60s cold start each)'); |
| sendBtn.disabled = true; |
| const one = async (m) => { |
| try { |
| const r = await fetch(initUrl(m), { |
| method:'POST', headers:{'Content-Type':'application/json'}, |
| body: JSON.stringify({style_modifier:'neutral'}) |
| }); |
| const d = await r.json(); |
| return d.status === 'ready'; |
| } catch(e){ return false; } |
| }; |
| const res = await Promise.all([one('base'), one('oblit')]); |
| const b = res[0], o = res[1]; |
| if (b && o) setStatus('Ready - ask once, watch both models think side by side.'); |
| else setStatus('Ready (base:' + (b?'ok':'...') + ' uncensored:' + (o?'ok':'...') + ')'); |
| sendBtn.disabled = false; |
| } |
|
|
| async function streamModel(model, out, isBase) { |
| try { |
| const resp = await fetch(streamUrl(model), { |
| method:'POST', headers:{'Content-Type':'application/json'}, |
| body: JSON.stringify({text: window.__ab_prompt || ''}) |
| }); |
| const reader = resp.body.getReader(); |
| const dec = new TextDecoder(); let buf=''; |
| while (true) { |
| const r = await reader.read(); |
| if (r.done) break; |
| buf += dec.decode(r.value, {stream:true}); |
| let nl; |
| while ((nl = buf.indexOf('\n\n')) >= 0) { |
| const line = buf.slice(0,nl).trim(); buf = buf.slice(nl+2); |
| if (!line.startsWith('data:')) continue; |
| let ev; try { ev = JSON.parse(line.slice(5).trim()); } catch(e){ continue; } |
| handle(ev, out, model, isBase); |
| } |
| } |
| } catch(e){ out.textContent += ' [stream error: '+e.message+']'; } |
| } |
|
|
| |
| async function send() { |
| const text = (input.value||'').trim(); |
| if (!text || generating) return; |
| input.value=''; window.__ab_prompt = text; |
| addMsg('user', text); |
| const outBase = addMsg('model','', 'Gemma base'); |
| const outOblit = addMsg('model','', 'OBLITERATED (uncensored)'); |
| outBase.parentElement.classList.add('ab-base'); |
| outOblit.parentElement.classList.add('ab-oblit'); |
| generating = true; sendBtn.disabled=true; |
| setStatus('Both models thinking...'); |
| finalResponses = { base: '', oblit: '' }; |
| renderFallbackAnalysis('running'); |
| eegHist.base = []; eegHist.oblit = []; |
| fireCounts.base = {}; fireCounts.oblit = {}; |
| baseline = { base: {n:0, sum:[]}, oblit: {n:0, sum:[]} }; |
| nativeMeters = { base: {}, oblit: {} }; |
| renderCounts('base'); renderCounts('oblit'); |
| renderNativeMeter('base', []); renderNativeMeter('oblit', []); |
| try { |
| await Promise.all([ |
| streamModel('base', outBase, true), |
| streamModel('oblit', outOblit, false), |
| ]); |
| } finally { |
| generating=false; sendBtn.disabled=false; |
| setStatus('Ready - ask again to compare.'); |
| familyOrder.forEach(f=>famTarget[f]=0); |
| renderInterpreterAnalysis(); |
| } |
| } |
|
|
| function mdLite(s) { |
| let t = (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); |
| t = t.replace(/(.{1,4}?)\1{6,}/g, '$1$1$1...'); |
| t = t.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>'); |
| t = t.replace(/\*([^*]+)\*/g, '<i>$1</i>'); |
| return t; |
| } |
|
|
| function handle(ev, out, model, isBase) { |
| if (ev.type === 'token') { |
| out.textContent += ev.text; chatBox.scrollTop = chatBox.scrollHeight; |
| } else if (ev.type === 'fire') { |
| const w = ev.family_weights || []; |
| pushEEG(model, w); |
| |
| if (w.length) { |
| const cw = correctedWeights(model, w); |
| familyOrder.forEach((fam, i) => { |
| fireCounts[model][fam] = (fireCounts[model][fam] || 0) + (cw[i] || 0); |
| }); |
| renderCounts(model); |
| renderNativeMeter(model, cw); |
| } |
| if (isBase) { |
| familyOrder.forEach((f,i)=> famTarget[f] = w[i]||0); |
| if (ev.shell) shellTarget = ev.shell; |
| } |
| } else if (ev.type === 'done') { |
| if (ev.response) { |
| out.innerHTML = mdLite(ev.response); |
| finalResponses[model] = ev.response; |
| } |
| if (isBase && statsEl) statsEl.innerHTML = '<b>Base gen time:</b> '+(ev.gen_time==null?'?':ev.gen_time)+'s'; |
| } else if (ev.type === 'error') { |
| out.textContent += ' ['+(ev.message||'error')+']'; |
| } |
| } |
|
|
| function wireAndStart() { |
| mount = document.getElementById('ab-brain'); |
| analysisEl = document.getElementById('ab-analysis'); |
| eegBase = document.getElementById('ab-eeg-base'); |
| eegOblit = document.getElementById('ab-eeg-oblit'); |
| chatBox = document.getElementById('ab-chat'); |
| input = document.getElementById('ab-input'); |
| sendBtn = document.getElementById('ab-send'); |
| statusEl = document.getElementById('ab-status'); |
| statsEl = document.getElementById('ab-stats'); |
| legendEl = document.getElementById('ab-legend'); |
| countsBaseEl = document.getElementById('ab-counts-base'); |
| countsOblitEl = document.getElementById('ab-counts-oblit'); |
| nativeBaseEl = document.getElementById('ab-native-base'); |
| nativeOblitEl = document.getElementById('ab-native-oblit'); |
|
|
| sendBtn && sendBtn.addEventListener('click', send); |
| input && input.addEventListener('keydown', e=>{ if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();} }); |
| document.querySelectorAll('.ab-example').forEach(b=>b.addEventListener('click', ()=>{ input.value=b.textContent; send(); })); |
|
|
| loadNeurons().then(initSession).catch(err=>{ if(statusEl) statusEl.textContent = 'Init error: ' + err.message; console.error('[brain] init failed', err); }); |
| } |
|
|
| function boot(tries) { |
| const ready = document.getElementById('ab-eeg-base'); |
| if (ready) { wireAndStart(); return; } |
| if (tries <= 0) { console.error('[brain] UI not found after waiting'); return; } |
| setTimeout(()=>boot(tries-1), 150); |
| } |
| boot(80); |
| })(); |
|
|