277 lines
8.1 KiB
JavaScript
277 lines
8.1 KiB
JavaScript
// === AGENT STATUS BOARD + LORA PANEL ===
|
|
import * as THREE from 'three';
|
|
import { NEXUS } from './constants.js';
|
|
import { scene } from './scene-setup.js';
|
|
import { S } from './state.js';
|
|
import { agentPanelSprites } from './bookshelves.js';
|
|
import { refreshAgentData, AGENT_STATUS_CACHE_MS, AGENT_NAMES } from './data/gitea.js';
|
|
|
|
// === AGENT STATUS BOARD ===
|
|
const AGENT_STATUS_COLORS = { working: '#00ff88', idle: '#4488ff', dormant: '#334466', dead: '#ff4444', unreachable: '#ff4444' };
|
|
|
|
function createAgentPanelTexture(agent) {
|
|
const W = 400, H = 200;
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = W;
|
|
canvas.height = H;
|
|
const ctx = canvas.getContext('2d');
|
|
const sc = AGENT_STATUS_COLORS[agent.status] || '#4488ff';
|
|
|
|
ctx.fillStyle = 'rgba(0, 8, 24, 0.88)';
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
ctx.strokeStyle = sc;
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeRect(1, 1, W - 2, H - 2);
|
|
|
|
ctx.strokeStyle = sc;
|
|
ctx.lineWidth = 1;
|
|
ctx.globalAlpha = 0.3;
|
|
ctx.strokeRect(4, 4, W - 8, H - 8);
|
|
ctx.globalAlpha = 1.0;
|
|
|
|
ctx.font = 'bold 28px "Courier New", monospace';
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillText(agent.name.toUpperCase(), 16, 44);
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(W - 30, 26, 10, 0, Math.PI * 2);
|
|
ctx.fillStyle = sc;
|
|
ctx.fill();
|
|
|
|
ctx.font = '13px "Courier New", monospace';
|
|
ctx.fillStyle = sc;
|
|
ctx.textAlign = 'right';
|
|
ctx.fillText(agent.status.toUpperCase(), W - 16, 60);
|
|
ctx.textAlign = 'left';
|
|
|
|
ctx.strokeStyle = '#1a3a6a';
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.moveTo(16, 70);
|
|
ctx.lineTo(W - 16, 70);
|
|
ctx.stroke();
|
|
|
|
ctx.font = '10px "Courier New", monospace';
|
|
ctx.fillStyle = '#556688';
|
|
ctx.fillText('CURRENT ISSUE', 16, 90);
|
|
|
|
ctx.font = '13px "Courier New", monospace';
|
|
ctx.fillStyle = '#ccd6f6';
|
|
const issueText = agent.issue || '\u2014 none \u2014';
|
|
const displayIssue = issueText.length > 40 ? issueText.slice(0, 40) + '\u2026' : issueText;
|
|
ctx.fillText(displayIssue, 16, 110);
|
|
|
|
ctx.strokeStyle = '#1a3a6a';
|
|
ctx.beginPath();
|
|
ctx.moveTo(16, 128);
|
|
ctx.lineTo(W - 16, 128);
|
|
ctx.stroke();
|
|
|
|
ctx.font = '10px "Courier New", monospace';
|
|
ctx.fillStyle = '#556688';
|
|
ctx.fillText('PRs MERGED TODAY', 16, 148);
|
|
|
|
ctx.font = 'bold 28px "Courier New", monospace';
|
|
ctx.fillStyle = '#4488ff';
|
|
ctx.fillText(String(agent.prs_today), 16, 182);
|
|
|
|
const isLocal = agent.local === true;
|
|
const indicatorColor = isLocal ? '#00ff88' : '#ff4444';
|
|
const indicatorLabel = isLocal ? 'LOCAL' : 'CLOUD';
|
|
|
|
ctx.font = '10px "Courier New", monospace';
|
|
ctx.fillStyle = '#556688';
|
|
ctx.textAlign = 'right';
|
|
ctx.fillText('RUNTIME', W - 16, 148);
|
|
|
|
ctx.font = 'bold 13px "Courier New", monospace';
|
|
ctx.fillStyle = indicatorColor;
|
|
ctx.fillText(indicatorLabel, W - 28, 172);
|
|
ctx.textAlign = 'left';
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(W - 16, 167, 6, 0, Math.PI * 2);
|
|
ctx.fillStyle = indicatorColor;
|
|
ctx.fill();
|
|
|
|
return new THREE.CanvasTexture(canvas);
|
|
}
|
|
|
|
const agentBoardGroup = new THREE.Group();
|
|
scene.add(agentBoardGroup);
|
|
|
|
const BOARD_RADIUS = 9.5;
|
|
const BOARD_Y = 4.2;
|
|
const BOARD_SPREAD = Math.PI * 0.75;
|
|
|
|
function rebuildAgentPanels(statusData) {
|
|
while (agentBoardGroup.children.length) agentBoardGroup.remove(agentBoardGroup.children[0]);
|
|
agentPanelSprites.length = 0;
|
|
|
|
const n = statusData.agents.length;
|
|
statusData.agents.forEach((agent, i) => {
|
|
const t = n === 1 ? 0.5 : i / (n - 1);
|
|
const angle = Math.PI + (t - 0.5) * BOARD_SPREAD;
|
|
const x = Math.cos(angle) * BOARD_RADIUS;
|
|
const z = Math.sin(angle) * BOARD_RADIUS;
|
|
|
|
const texture = createAgentPanelTexture(agent);
|
|
const material = new THREE.SpriteMaterial({
|
|
map: texture, transparent: true, opacity: 0.93, depthWrite: false,
|
|
});
|
|
const sprite = new THREE.Sprite(material);
|
|
sprite.scale.set(6.4, 3.2, 1);
|
|
sprite.position.set(x, BOARD_Y, z);
|
|
sprite.userData = {
|
|
baseY: BOARD_Y,
|
|
floatPhase: (i / n) * Math.PI * 2,
|
|
floatSpeed: 0.18 + i * 0.04,
|
|
zoomLabel: `Agent: ${agent.name}`,
|
|
};
|
|
agentBoardGroup.add(sprite);
|
|
agentPanelSprites.push(sprite);
|
|
});
|
|
}
|
|
|
|
export async function refreshAgentBoard() {
|
|
const data = await refreshAgentData();
|
|
rebuildAgentPanels(data);
|
|
}
|
|
|
|
export function initAgentBoard() {
|
|
refreshAgentBoard();
|
|
setInterval(refreshAgentBoard, AGENT_STATUS_CACHE_MS);
|
|
}
|
|
|
|
// === LORA ADAPTER STATUS PANEL ===
|
|
const LORA_ACTIVE_COLOR = '#00ff88';
|
|
const LORA_INACTIVE_COLOR = '#334466';
|
|
|
|
function createLoRAPanelTexture(data) {
|
|
const W = 420, H = 260;
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = W;
|
|
canvas.height = H;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
ctx.fillStyle = 'rgba(0, 6, 20, 0.90)';
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
ctx.strokeStyle = '#cc44ff';
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeRect(1, 1, W - 2, H - 2);
|
|
|
|
ctx.strokeStyle = '#cc44ff';
|
|
ctx.lineWidth = 1;
|
|
ctx.globalAlpha = 0.3;
|
|
ctx.strokeRect(4, 4, W - 8, H - 8);
|
|
ctx.globalAlpha = 1.0;
|
|
|
|
ctx.font = 'bold 14px "Courier New", monospace';
|
|
ctx.fillStyle = '#cc44ff';
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText('MODEL TRAINING', 14, 24);
|
|
|
|
ctx.font = '10px "Courier New", monospace';
|
|
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();
|
|
|
|
if (!data || !data.adapters || data.adapters.length === 0) {
|
|
ctx.font = 'bold 18px "Courier New", monospace';
|
|
ctx.fillStyle = '#334466';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('NO ADAPTERS DEPLOYED', W / 2, H / 2 + 10);
|
|
ctx.font = '11px "Courier New", monospace';
|
|
ctx.fillStyle = '#223344';
|
|
ctx.fillText('Adapters will appear here when trained', W / 2, H / 2 + 36);
|
|
ctx.textAlign = 'left';
|
|
return new THREE.CanvasTexture(canvas);
|
|
}
|
|
|
|
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';
|
|
|
|
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;
|
|
ctx.beginPath();
|
|
ctx.arc(22, rowY + 12, 6, 0, Math.PI * 2);
|
|
ctx.fillStyle = col;
|
|
ctx.fill();
|
|
ctx.font = 'bold 13px "Courier New", monospace';
|
|
ctx.fillStyle = adapter.active ? '#ddeeff' : '#445566';
|
|
ctx.fillText(adapter.name, 36, rowY + 16);
|
|
ctx.font = '10px "Courier New", monospace';
|
|
ctx.fillStyle = '#556688';
|
|
ctx.textAlign = 'right';
|
|
ctx.fillText(adapter.base, W - 14, rowY + 16);
|
|
ctx.textAlign = 'left';
|
|
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;
|
|
}
|
|
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);
|
|
|
|
export let loraPanelSprite = null;
|
|
|
|
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);
|
|
}
|
|
|
|
export function loadLoRAStatus() {
|
|
rebuildLoRAPanel({ adapters: [] });
|
|
}
|