From f3df47110958d6d7fb569fe1bb439b505c81a682 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 00:58:15 -0400 Subject: [PATCH] feat: show LoRA adapter training status as 3D holographic panel Adds a floating LoRA adapter status panel to the Nexus scene. The panel shows which fine-tuning adapters are active, their base model, and strength percentage via a bar indicator. Data is loaded from lora-status.json (with fallback stub), refreshed every 60 s. Fixes #277 Co-Authored-By: Claude Sonnet 4.6 --- app.js | 188 +++++++++++++++++++++++++++++++++++++++++++++++ lora-status.json | 9 +++ 2 files changed, 197 insertions(+) create mode 100644 lora-status.json diff --git a/app.js b/app.js index 5a405cf..e957d04 100644 --- a/app.js +++ b/app.js @@ -996,6 +996,12 @@ function animate() { sprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.22; } + // Animate LoRA status panel — gentle float + if (loraPanelSprite) { + const ud = loraPanelSprite.userData; + loraPanelSprite.position.y = ud.baseY + Math.sin(elapsed * ud.floatSpeed + ud.floatPhase) * 0.22; + } + // Animate Timmy speech bubble — fade in, hold, fade out if (timmySpeechState) { const age = elapsed - timmySpeechState.startTime; @@ -1914,6 +1920,188 @@ async function refreshAgentBoard() { refreshAgentBoard(); setInterval(refreshAgentBoard, 30000); +// === LORA ADAPTER STATUS PANEL === +// Holographic panel showing which LoRA fine-tuning adapters are currently active. +// Reads from lora-status.json, falls back to stub data when unavailable. + +const LORA_STATUS_STUB = { + adapters: [ + { name: 'timmy-voice-v3', base: 'mistral-7b', active: true, strength: 0.85 }, + { name: 'nexus-style-v2', base: 'llama-3-8b', active: true, strength: 0.70 }, + { name: 'sovereign-tone-v1', base: 'phi-3-mini', active: false, strength: 0.50 }, + { name: 'btc-domain-v1', base: 'mistral-7b', active: true, strength: 0.60 }, + ], + updated: '', +}; + +const LORA_ACTIVE_COLOR = '#00ff88'; // green — adapter is loaded +const LORA_INACTIVE_COLOR = '#334466'; // dim blue — adapter is off + +/** + * Builds a canvas texture for the LoRA status panel. + * @param {typeof LORA_STATUS_STUB} data + * @returns {THREE.CanvasTexture} + */ +function createLoRAPanelTexture(data) { + const W = 420, H = 260; + const canvas = document.createElement('canvas'); + canvas.width = W; + canvas.height = H; + const ctx = canvas.getContext('2d'); + + // Background + ctx.fillStyle = 'rgba(0, 6, 20, 0.90)'; + ctx.fillRect(0, 0, W, H); + + // Outer border — magenta/purple for "training" theme + ctx.strokeStyle = '#cc44ff'; + ctx.lineWidth = 2; + ctx.strokeRect(1, 1, W - 2, H - 2); + + // Inner border + ctx.strokeStyle = '#cc44ff'; + ctx.lineWidth = 1; + ctx.globalAlpha = 0.3; + ctx.strokeRect(4, 4, W - 8, H - 8); + ctx.globalAlpha = 1.0; + + // Header label + ctx.font = 'bold 14px "Courier New", monospace'; + ctx.fillStyle = '#cc44ff'; + ctx.textAlign = 'left'; + ctx.fillText('MODEL TRAINING', 14, 24); + + // "LoRA ADAPTERS" sub-label + ctx.font = '10px "Courier New", monospace'; + ctx.fillStyle = '#664488'; + ctx.fillText('LoRA ADAPTERS', 14, 38); + + // Active count badge (top-right) + const activeCount = data.adapters.filter(a => a.active).length; + ctx.font = 'bold 13px "Courier New", monospace'; + ctx.fillStyle = LORA_ACTIVE_COLOR; + ctx.textAlign = 'right'; + ctx.fillText(`${activeCount}/${data.adapters.length} ACTIVE`, W - 14, 26); + ctx.textAlign = 'left'; + + // Separator + ctx.strokeStyle = '#2a1a44'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(14, 46); + ctx.lineTo(W - 14, 46); + ctx.stroke(); + + // Adapter rows + const ROW_H = 44; + data.adapters.forEach((adapter, i) => { + const rowY = 50 + i * ROW_H; + const col = adapter.active ? LORA_ACTIVE_COLOR : LORA_INACTIVE_COLOR; + + // Status dot + ctx.beginPath(); + ctx.arc(22, rowY + 12, 6, 0, Math.PI * 2); + ctx.fillStyle = col; + ctx.fill(); + + // Adapter name + ctx.font = 'bold 13px "Courier New", monospace'; + ctx.fillStyle = adapter.active ? '#ddeeff' : '#445566'; + ctx.fillText(adapter.name, 36, rowY + 16); + + // Base model (right-aligned) + ctx.font = '10px "Courier New", monospace'; + ctx.fillStyle = '#556688'; + ctx.textAlign = 'right'; + ctx.fillText(adapter.base, W - 14, rowY + 16); + ctx.textAlign = 'left'; + + // Strength bar + if (adapter.active) { + const BAR_X = 36, BAR_W = W - 80, BAR_Y = rowY + 22, BAR_H = 5; + ctx.fillStyle = '#0a1428'; + ctx.fillRect(BAR_X, BAR_Y, BAR_W, BAR_H); + ctx.fillStyle = col; + ctx.globalAlpha = 0.7; + ctx.fillRect(BAR_X, BAR_Y, BAR_W * adapter.strength, BAR_H); + ctx.globalAlpha = 1.0; + ctx.font = '9px "Courier New", monospace'; + ctx.fillStyle = col; + ctx.textAlign = 'right'; + ctx.fillText(`${Math.round(adapter.strength * 100)}%`, W - 14, rowY + 28); + ctx.textAlign = 'left'; + } + + // Row divider (except after last) + if (i < data.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); +} + +const loraGroup = new THREE.Group(); +scene.add(loraGroup); + +const LORA_PANEL_POS = new THREE.Vector3(-10.5, 4.5, 2.5); + +let loraPanelSprite = null; + +/** + * (Re)builds the LoRA panel sprite from fresh data. + * @param {typeof LORA_STATUS_STUB} data + */ +function rebuildLoRAPanel(data) { + if (loraPanelSprite) { + loraGroup.remove(loraPanelSprite); + if (loraPanelSprite.material.map) loraPanelSprite.material.map.dispose(); + loraPanelSprite.material.dispose(); + loraPanelSprite = null; + } + const texture = createLoRAPanelTexture(data); + const material = new THREE.SpriteMaterial({ + map: texture, + transparent: true, + opacity: 0.93, + depthWrite: false, + }); + loraPanelSprite = new THREE.Sprite(material); + loraPanelSprite.scale.set(6.0, 3.6, 1); + loraPanelSprite.position.copy(LORA_PANEL_POS); + loraPanelSprite.userData = { + baseY: LORA_PANEL_POS.y, + floatPhase: 1.1, + floatSpeed: 0.14, + zoomLabel: 'Model Training — LoRA Adapters', + }; + loraGroup.add(loraPanelSprite); +} + +/** + * Fetches live LoRA adapter status, falling back to stub when unavailable. + */ +async function loadLoRAStatus() { + try { + const res = await fetch('./lora-status.json'); + if (!res.ok) throw new Error('not found'); + const data = await res.json(); + if (!Array.isArray(data.adapters)) throw new Error('invalid'); + rebuildLoRAPanel(data); + } catch { + rebuildLoRAPanel(LORA_STATUS_STUB); + } +} + +loadLoRAStatus(); +// Refresh every 60 s so live updates propagate +setInterval(loadLoRAStatus, 60000); + // === TIMMY SPEECH BUBBLE === // When Timmy sends a chat message, a glowing floating text sprite appears near // his avatar position above the platform. Fades in quickly, holds for 5 s total, diff --git a/lora-status.json b/lora-status.json new file mode 100644 index 0000000..9ae41e3 --- /dev/null +++ b/lora-status.json @@ -0,0 +1,9 @@ +{ + "adapters": [ + { "name": "timmy-voice-v3", "base": "mistral-7b", "active": true, "strength": 0.85 }, + { "name": "nexus-style-v2", "base": "llama-3-8b", "active": true, "strength": 0.70 }, + { "name": "sovereign-tone-v1", "base": "phi-3-mini", "active": false, "strength": 0.50 }, + { "name": "btc-domain-v1", "base": "mistral-7b", "active": true, "strength": 0.60 } + ], + "updated": "2026-03-24T00:00:00Z" +} -- 2.43.0