Files
the-nexus/modules/panels/lora-panel.js
Alexander Whitestone f0fe9d76b6
All checks were successful
CI / validate (pull_request) Successful in 6s
CI / auto-merge (pull_request) Successful in 2s
feat: Phase 3 — extract panel modules from app.js (Refs #422)
Create 6 panel modules under modules/panels/ plus supporting core
infrastructure (state.js, theme.js, ticker.js). Each panel:
  - Exports init(scene, state, theme) and update(elapsed, delta)
  - Uses NEXUS.theme for all colors/fonts (no inline hex codes)
  - Reads from state.js (no direct API calls)
  - Subscribes to ticker for animation

Panel modules:
  panels/heatmap.js       — Commit heatmap floor overlay (DATA-TETHERED)
  panels/agent-board.js   — Agent status holographic board (REAL)
  panels/dual-brain.js    — Dual-brain panel (HONEST-OFFLINE)
  panels/lora-panel.js    — LoRA adapter panel (HONEST-OFFLINE)
  panels/sovereignty.js   — Sovereignty meter arc gauge (REAL manual)
  panels/earth.js         — Holographic Earth, activity-tethered (DATA-TETHERED)

Core infrastructure (consumed by panels):
  core/state.js   — shared reactive data bus
  core/theme.js   — NEXUS.theme design system
  core/ticker.js  — single RAF loop + subscribe/unsubscribe API

All files pass `node --check`. app.js unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 14:21:33 -04:00

168 lines
4.8 KiB
JavaScript

// modules/panels/lora-panel.js — LoRA Adapter Status holographic panel
// Shows the model training / LoRA fine-tuning adapter status.
// Displayed as HONEST-OFFLINE: no adapters are deployed. Panel shows empty state.
// Will render real adapters when state.loraAdapters is populated in the future.
//
// Data category: HONEST-OFFLINE
// Data source: — (no LoRA adapters deployed; shows "NO ADAPTERS DEPLOYED")
import * as THREE from 'three';
import { NEXUS } from '../core/theme.js';
import { subscribe } from '../core/ticker.js';
const PANEL_POS = new THREE.Vector3(-10.5, 4.5, 2.5);
const LORA_ACCENT = NEXUS.theme.loraAccent;
const LORA_ACTIVE = NEXUS.theme.loraActive;
const LORA_OFFLINE = NEXUS.theme.loraInactive;
const FONT = NEXUS.theme.fontMono;
let _group, _sprite, _scene;
/**
* Builds the LoRA panel canvas texture.
* @param {{ adapters: Array }|null} data
* @returns {THREE.CanvasTexture}
*/
function _makeTexture(data) {
const W = 420, H = 260;
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 = LORA_ACCENT;
ctx.lineWidth = 2;
ctx.strokeRect(1, 1, W - 2, H - 2);
ctx.strokeStyle = LORA_ACCENT;
ctx.lineWidth = 1;
ctx.globalAlpha = 0.3;
ctx.strokeRect(4, 4, W - 8, H - 8);
ctx.globalAlpha = 1.0;
ctx.font = `bold 14px ${FONT}`;
ctx.fillStyle = LORA_ACCENT;
ctx.textAlign = 'left';
ctx.fillText('MODEL TRAINING', 14, 24);
ctx.font = `10px ${FONT}`;
ctx.fillStyle = '#664488';
ctx.fillText('LoRA ADAPTERS', 14, 38);
ctx.strokeStyle = '#2a1a44';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(14, 46); ctx.lineTo(W - 14, 46); ctx.stroke();
const adapters = data && Array.isArray(data.adapters) ? data.adapters : [];
if (adapters.length === 0) {
// Honest empty state
ctx.font = `bold 18px ${FONT}`;
ctx.fillStyle = LORA_OFFLINE;
ctx.textAlign = 'center';
ctx.fillText('NO ADAPTERS DEPLOYED', W / 2, H / 2 + 10);
ctx.font = `11px ${FONT}`;
ctx.fillStyle = '#223344';
ctx.fillText('Adapters will appear here when trained', W / 2, H / 2 + 36);
return new THREE.CanvasTexture(canvas);
}
// Active count header
const activeCount = adapters.filter(a => a.active).length;
ctx.font = `bold 13px ${FONT}`;
ctx.fillStyle = LORA_ACTIVE;
ctx.textAlign = 'right';
ctx.fillText(`${activeCount}/${adapters.length} ACTIVE`, W - 14, 26);
ctx.textAlign = 'left';
// Adapter rows
const ROW_H = 44;
adapters.forEach((adapter, i) => {
const rowY = 50 + i * ROW_H;
const col = adapter.active ? LORA_ACTIVE : LORA_OFFLINE;
ctx.beginPath();
ctx.arc(22, rowY + 12, 6, 0, Math.PI * 2);
ctx.fillStyle = col;
ctx.fill();
ctx.font = `bold 13px ${FONT}`;
ctx.fillStyle = adapter.active ? '#ddeeff' : '#445566';
ctx.fillText(adapter.name, 36, rowY + 16);
ctx.font = `10px ${FONT}`;
ctx.fillStyle = NEXUS.theme.panelDim;
ctx.textAlign = 'right';
ctx.fillText(adapter.base, W - 14, rowY + 16);
ctx.textAlign = 'left';
if (adapter.active) {
const BX = 36, BW = W - 80, BY = rowY + 22, BH = 5;
ctx.fillStyle = '#0a1428';
ctx.fillRect(BX, BY, BW, BH);
ctx.fillStyle = col;
ctx.globalAlpha = 0.7;
ctx.fillRect(BX, BY, BW * (adapter.strength || 0), BH);
ctx.globalAlpha = 1.0;
}
if (i < adapters.length - 1) {
ctx.strokeStyle = '#1a0a2a';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(14, rowY + ROW_H - 2); ctx.lineTo(W - 14, rowY + ROW_H - 2); ctx.stroke();
}
});
return new THREE.CanvasTexture(canvas);
}
function _buildSprite(data) {
if (_sprite) {
_group.remove(_sprite);
if (_sprite.material.map) _sprite.material.map.dispose();
_sprite.material.dispose();
_sprite = null;
}
const texture = _makeTexture(data);
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0.93, depthWrite: false });
_sprite = new THREE.Sprite(material);
_sprite.scale.set(6.0, 3.6, 1);
_sprite.position.copy(PANEL_POS);
_sprite.userData = {
baseY: PANEL_POS.y,
floatPhase: 1.1,
floatSpeed: 0.14,
zoomLabel: 'Model Training — LoRA Adapters',
};
_group.add(_sprite);
}
/** @param {THREE.Scene} scene */
export function init(scene) {
_scene = scene;
_group = new THREE.Group();
scene.add(_group);
// Honest empty state on init — no adapters deployed
_buildSprite({ adapters: [] });
subscribe(update);
}
/**
* @param {number} elapsed
* @param {number} _delta
*/
export function update(elapsed, _delta) {
if (_sprite) {
const ud = _sprite.userData;
_sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.12;
}
}
export function dispose() {
if (_group) _scene.remove(_group);
}