Files
the-nexus/modules/panels/lora-panel.js
Claude (Opus 4.6) 481a0790d2
Some checks failed
Deploy Nexus / deploy (push) Failing after 4s
[claude] Phase 3: Panel modules — Heatmap, Agent Board, Dual-Brain, LoRA, Sovereignty, Earth (#422) (#446)
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
2026-03-24 18:22:34 +00: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);
}