Some checks failed
Deploy Nexus / deploy (push) Failing after 4s
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local> Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
201 lines
6.4 KiB
JavaScript
201 lines
6.4 KiB
JavaScript
// modules/panels/dual-brain.js — Dual-Brain Status holographic panel
|
|
// Shows the Brain Gap Scorecard with two glowing brain orbs.
|
|
// Displayed as HONEST-OFFLINE: the dual-brain system is not yet deployed.
|
|
// Brain pulse particles are set to ZERO — will flow when system comes online.
|
|
//
|
|
// Data category: HONEST-OFFLINE
|
|
// Data source: — (dual-brain system not deployed; shows "AWAITING DEPLOYMENT")
|
|
|
|
import * as THREE from 'three';
|
|
import { NEXUS } from '../core/theme.js';
|
|
import { subscribe } from '../core/ticker.js';
|
|
|
|
const ORIGIN = new THREE.Vector3(10, 3, -8);
|
|
const OFFLINE_COLOR = NEXUS.theme.agentDormantHex; // dim blue — system offline
|
|
const ACCENT = NEXUS.theme.accentStr;
|
|
const FONT = NEXUS.theme.fontMono;
|
|
|
|
let _group, _sprite, _scanSprite, _scanCanvas, _scanCtx, _scanTexture;
|
|
let _cloudOrb, _localOrb;
|
|
let _scene;
|
|
|
|
function _buildPanelTexture() {
|
|
const W = 512, H = 512;
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = W;
|
|
canvas.height = H;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
ctx.fillStyle = NEXUS.theme.panelBg;
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
ctx.strokeStyle = ACCENT;
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeRect(1, 1, W - 2, H - 2);
|
|
ctx.strokeStyle = '#223366';
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeRect(5, 5, W - 10, H - 10);
|
|
|
|
// Title
|
|
ctx.font = `bold 22px ${FONT}`;
|
|
ctx.fillStyle = '#88ccff';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('\u25C8 DUAL-BRAIN STATUS', W / 2, 40);
|
|
|
|
ctx.strokeStyle = NEXUS.theme.panelBorderFaint;
|
|
ctx.beginPath(); ctx.moveTo(20, 52); ctx.lineTo(W - 20, 52); ctx.stroke();
|
|
|
|
// Section header
|
|
ctx.font = `11px ${FONT}`;
|
|
ctx.fillStyle = NEXUS.theme.panelDim;
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText('BRAIN GAP SCORECARD', 20, 74);
|
|
|
|
const categories = ['Triage', 'Tool Use', 'Code Gen', 'Planning', 'Communication', 'Reasoning'];
|
|
const barX = 20, barW = W - 130, barH = 20;
|
|
let y = 90;
|
|
|
|
for (const cat of categories) {
|
|
ctx.font = `13px ${FONT}`;
|
|
ctx.fillStyle = NEXUS.theme.agentDormant;
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText(cat, barX, y + 14);
|
|
|
|
ctx.font = `bold 13px ${FONT}`;
|
|
ctx.fillStyle = NEXUS.theme.panelVeryDim;
|
|
ctx.textAlign = 'right';
|
|
ctx.fillText('\u2014', W - 20, y + 14); // em dash — no data
|
|
y += 22;
|
|
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.06)';
|
|
ctx.fillRect(barX, y, barW, barH); // empty bar background only
|
|
y += barH + 12;
|
|
}
|
|
|
|
ctx.strokeStyle = NEXUS.theme.panelBorderFaint;
|
|
ctx.beginPath(); ctx.moveTo(20, y + 4); ctx.lineTo(W - 20, y + 4); ctx.stroke();
|
|
y += 22;
|
|
|
|
// Honest offline status
|
|
ctx.font = `bold 18px ${FONT}`;
|
|
ctx.fillStyle = NEXUS.theme.panelVeryDim;
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('AWAITING DEPLOYMENT', W / 2, y + 10);
|
|
|
|
ctx.font = `11px ${FONT}`;
|
|
ctx.fillStyle = '#223344';
|
|
ctx.fillText('Dual-brain system not yet connected', W / 2, y + 32);
|
|
|
|
// Brain indicators — offline dim
|
|
y += 52;
|
|
ctx.beginPath();
|
|
ctx.arc(W / 2 - 60, y + 8, 6, 0, Math.PI * 2);
|
|
ctx.fillStyle = NEXUS.theme.panelVeryDim;
|
|
ctx.fill();
|
|
ctx.font = `11px ${FONT}`;
|
|
ctx.fillStyle = NEXUS.theme.panelVeryDim;
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText('CLOUD', W / 2 - 48, y + 12);
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(W / 2 + 30, y + 8, 6, 0, Math.PI * 2);
|
|
ctx.fillStyle = NEXUS.theme.panelVeryDim;
|
|
ctx.fill();
|
|
ctx.fillText('LOCAL', W / 2 + 42, y + 12);
|
|
|
|
return new THREE.CanvasTexture(canvas);
|
|
}
|
|
|
|
/** @param {THREE.Scene} scene */
|
|
export function init(scene) {
|
|
_scene = scene;
|
|
|
|
_group = new THREE.Group();
|
|
_group.position.copy(ORIGIN);
|
|
_group.lookAt(0, 3, 0);
|
|
scene.add(_group);
|
|
|
|
// Static panel sprite
|
|
const texture = _buildPanelTexture();
|
|
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0.92, depthWrite: false });
|
|
_sprite = new THREE.Sprite(material);
|
|
_sprite.scale.set(5.0, 5.0, 1);
|
|
_sprite.userData = { baseY: 0, floatPhase: 0, floatSpeed: 0.22, zoomLabel: 'Dual-Brain Status' };
|
|
_group.add(_sprite);
|
|
|
|
// Accent light
|
|
const light = new THREE.PointLight(NEXUS.theme.accent, 0.6, 10);
|
|
light.position.set(0, 0.5, 1);
|
|
_group.add(light);
|
|
|
|
// Offline brain orbs — dim
|
|
const orbGeo = new THREE.SphereGeometry(0.35, 32, 32);
|
|
const orbMat = (color) => new THREE.MeshStandardMaterial({
|
|
color, emissive: new THREE.Color(color), emissiveIntensity: 0.1,
|
|
metalness: 0.3, roughness: 0.2, transparent: true, opacity: 0.85,
|
|
});
|
|
|
|
_cloudOrb = new THREE.Mesh(orbGeo, orbMat(OFFLINE_COLOR));
|
|
_cloudOrb.position.set(-2.0, 3.0, 0);
|
|
_cloudOrb.userData.zoomLabel = 'Cloud Brain';
|
|
_group.add(_cloudOrb);
|
|
|
|
_localOrb = new THREE.Mesh(orbGeo.clone(), orbMat(OFFLINE_COLOR));
|
|
_localOrb.position.set(2.0, 3.0, 0);
|
|
_localOrb.userData.zoomLabel = 'Local Brain';
|
|
_group.add(_localOrb);
|
|
|
|
// Brain pulse particles — ZERO count (system offline)
|
|
const particleGeo = new THREE.BufferGeometry();
|
|
particleGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(0), 3));
|
|
const particleMat = new THREE.PointsMaterial({
|
|
color: 0x44ddff, size: 0.08, sizeAttenuation: true,
|
|
transparent: true, opacity: 0.8, depthWrite: false,
|
|
});
|
|
_group.add(new THREE.Points(particleGeo, particleMat));
|
|
|
|
// Scan line overlay
|
|
_scanCanvas = document.createElement('canvas');
|
|
_scanCanvas.width = 512;
|
|
_scanCanvas.height = 512;
|
|
_scanCtx = _scanCanvas.getContext('2d');
|
|
_scanTexture = new THREE.CanvasTexture(_scanCanvas);
|
|
|
|
const scanMat = new THREE.SpriteMaterial({
|
|
map: _scanTexture, transparent: true, opacity: 0.18, depthWrite: false,
|
|
});
|
|
_scanSprite = new THREE.Sprite(scanMat);
|
|
_scanSprite.scale.set(5.0, 5.0, 1);
|
|
_scanSprite.position.set(0, 0, 0.01);
|
|
_group.add(_scanSprite);
|
|
|
|
subscribe(update);
|
|
}
|
|
|
|
/**
|
|
* @param {number} elapsed
|
|
* @param {number} _delta
|
|
*/
|
|
export function update(elapsed, _delta) {
|
|
// Gentle float animation
|
|
const ud = _sprite.userData;
|
|
_sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.08;
|
|
|
|
// Scan line — horizontal sweep
|
|
const W = 512, H = 512;
|
|
_scanCtx.clearRect(0, 0, W, H);
|
|
const scanY = ((elapsed * 60) % H);
|
|
const grad = _scanCtx.createLinearGradient(0, scanY - 20, 0, scanY + 20);
|
|
grad.addColorStop(0, 'rgba(68, 136, 255, 0)');
|
|
grad.addColorStop(0.5, 'rgba(68, 136, 255, 0.4)');
|
|
grad.addColorStop(1, 'rgba(68, 136, 255, 0)');
|
|
_scanCtx.fillStyle = grad;
|
|
_scanCtx.fillRect(0, scanY - 20, W, 40);
|
|
_scanTexture.needsUpdate = true;
|
|
}
|
|
|
|
export function dispose() {
|
|
if (_group) _scene.remove(_group);
|
|
if (_scanTexture) _scanTexture.dispose();
|
|
}
|