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>
168 lines
4.8 KiB
JavaScript
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);
|
|
}
|