[claude] Model training status — show LoRA adapters (#277) #324
188
app.js
188
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,
|
||||
|
||||
9
lora-status.json
Normal file
9
lora-status.json
Normal file
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user