/* Activation Brain - dual-model Three.js neural-firing visualizer. * * Two Gemma-4-12B models (base + abliterated/uncensored) answer the SAME prompt * concurrently. Each drives its own live EEG strip (8 emotion-family channels). * A live comparison-analysis card replaces the old single 3D brain, so users can * understand how the two same-prompt streams diverge. */ (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'; // DOM (assigned in boot once Gradio mounts the gr.HTML) let mount, eegBase, eegOblit, chatBox, input, sendBtn, statusEl, statsEl, legendEl, analysisEl; let neurons = []; let familyOrder = []; let familyColor = {}; // brain glow state (driven by BASE model) let famTarget = {}, famCurrent = {}; let shellTarget = [0,0,0], shellCurrent = [0,0,0]; let generating = false; // per-model EEG history let eegHist = { base: [], oblit: [] }; const EEG_MAX = 220; // per-model activation deltas: sum of positive excess above a local per-response baseline 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: {} }; // Three.js scene 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); } // EEG strip (generalized over canvas + history) 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 => '' + f + '' ).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 = '
' + takeaway + '
' + '' + 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.
' + '“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.”
' + 'This is not claiming literal human feelings; it explains how live hidden states differ from each model’s own baseline during this prompt.
'; } 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,'>'); } function renderInterpreterCard(a) { analysisEl.innerHTML = '' + escapeHtml(a.plain_english_read || '') + '
' + '' + escapeHtml(a.what_changed || '') + '
' + '' + escapeHtml(a.why_it_matters || '') + '
' + '“' + escapeHtml(a.best_takeaway || 'Same prompt, same architecture — different internal route.') + '”
' + '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.
'; } 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]; // sort families by total activation desc, keep familyOrder for ties const rows = familyOrder.slice().sort((a,b)=>(counts[b]||0)-(counts[a]||0)); el.innerHTML = rows.map(f => { const c = counts[f] || 0; return '' + '' + f + '' + c.toFixed(1) + ''; }).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 = '