// === 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'; // === AGENT STATUS BOARD === let _agentStatusCache = null; let _agentStatusCacheTime = 0; const AGENT_STATUS_CACHE_MS = 5 * 60 * 1000; const GITEA_BASE = 'http://143.198.27.163:3000/api/v1'; const GITEA_TOKEN='81a88f...ae2d'; const GITEA_REPOS = ['Timmy_Foundation/the-nexus', 'Timmy_Foundation/hermes-agent']; const AGENT_NAMES = ['Claude', 'Kimi', 'Perplexity', 'Groq', 'Grok', 'Ollama']; async function fetchAgentStatusFromGitea() { const now = Date.now(); if (_agentStatusCache && (now - _agentStatusCacheTime < AGENT_STATUS_CACHE_MS)) { return _agentStatusCache; } const DAY_MS = 86400000; const HOUR_MS = 3600000; const agents = []; const allRepoCommits = await Promise.all(GITEA_REPOS.map(async (repo) => { try { const res = await fetch(`${GITEA_BASE}/repos/${repo}/commits?sha=main&limit=30&token=${GITEA_TOKEN}`); if (!res.ok) return []; return await res.json(); } catch { return []; } })); let openPRs = []; try { const prRes = await fetch(`${GITEA_BASE}/repos/Timmy_Foundation/the-nexus/pulls?state=open&limit=50&token=${GITEA_TOKEN}`); if (prRes.ok) openPRs = await prRes.json(); } catch { /* ignore */ } for (const agentName of AGENT_NAMES) { const nameLower = agentName.toLowerCase(); const allCommits = []; for (const repoCommits of allRepoCommits) { if (!Array.isArray(repoCommits)) continue; const matching = repoCommits.filter(c => (c.commit?.author?.name || '').toLowerCase().includes(nameLower) ); allCommits.push(...matching); } let status = 'dormant'; let lastSeen = null; let currentWork = null; if (allCommits.length > 0) { allCommits.sort((a, b) => new Date(b.commit.author.date) - new Date(a.commit.author.date) ); const latest = allCommits[0]; const commitTime = new Date(latest.commit.author.date).getTime(); lastSeen = latest.commit.author.date; currentWork = latest.commit.message.split('\n')[0]; if (now - commitTime < HOUR_MS) status = 'working'; else if (now - commitTime < DAY_MS) status = 'idle'; else status = 'dormant'; } const agentPRs = openPRs.filter(pr => (pr.user?.login || '').toLowerCase().includes(nameLower) || (pr.head?.label || '').toLowerCase().includes(nameLower) ); agents.push({ name: agentName.toLowerCase(), status, issue: currentWork, prs_today: agentPRs.length, local: nameLower === 'ollama', }); } _agentStatusCache = { agents }; _agentStatusCacheTime = now; return _agentStatusCache; } 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); }); } async function fetchAgentStatus() { try { return await fetchAgentStatusFromGitea(); } catch { return { agents: AGENT_NAMES.map(n => ({ name: n.toLowerCase(), status: 'unreachable', issue: null, prs_today: 0, local: false, })) }; } } export async function refreshAgentBoard() { const data = await fetchAgentStatus(); rebuildAgentPanels(data); S._activeAgentCount = data.agents.filter(a => a.status === 'working').length; } 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: [] }); }