Files
the-matrix/js/ui.js
Perplexity Computer fdfae19956 feat: The Matrix — Sovereign Agent World
3D visualization for AI agent swarms built with Three.js.
Matrix green/noir cyberpunk aesthetic.

- 4 agents: Timmy (orchestrator), Forge (builder), Seer (planner), Echo (comms)
- Central core pillar, animated green grid, digital rain
- Agent info panels, chat, task list, memory views
- WebSocket protocol for real-time state updates
- iPad-ready: touch controls, add-to-homescreen
- Post-processing: bloom, scanlines, vignette
- No build step — pure ES modules via esm.sh CDN

Created with Perplexity Computer
2026-03-18 18:32:47 -04:00

315 lines
10 KiB
JavaScript

// ===== UI: Panel, tabs, chat, task list, memory =====
import { AGENT_DEFS } from './websocket.js';
export class UIManager {
constructor(ws) {
this.ws = ws;
this.selectedAgent = null;
this.activeTab = 'chat';
this.onClose = null;
// Cache DOM refs
this.panel = document.getElementById('info-panel');
this.systemPanel = document.getElementById('system-panel');
this.panelName = document.getElementById('panel-agent-name');
this.panelRole = document.getElementById('panel-agent-role');
this.chatMessages = document.getElementById('chat-messages');
this.chatInput = document.getElementById('chat-input');
this.chatSend = document.getElementById('chat-send');
this.typingIndicator = document.getElementById('typing-indicator');
this.statusGrid = document.getElementById('status-grid');
this.tasksList = document.getElementById('tasks-list');
this.memoryList = document.getElementById('memory-list');
this.systemStatusGrid = document.getElementById('system-status-grid');
this.fpsCounter = document.getElementById('fps-counter');
this._bindEvents();
this._bindWSEvents();
}
_bindEvents() {
// Tab switching
document.querySelectorAll('.panel-tabs .tab').forEach(tab => {
tab.addEventListener('click', (e) => {
this._switchTab(e.target.dataset.tab);
});
});
// Close buttons
document.getElementById('panel-close').addEventListener('click', () => this.closePanel());
document.getElementById('system-panel-close').addEventListener('click', () => this.closeSystemPanel());
// Chat send
this.chatSend.addEventListener('click', () => this._sendChat());
this.chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') this._sendChat();
});
// Prevent canvas events when interacting with panel
[this.panel, this.systemPanel].forEach(el => {
el.addEventListener('pointerdown', e => e.stopPropagation());
el.addEventListener('touchstart', e => e.stopPropagation(), { passive: true });
});
}
_bindWSEvents() {
this.ws.on('message', (msg) => {
if (!this.selectedAgent) return;
if (msg.type === 'agent_message' && msg.agent_id === this.selectedAgent) {
this._hideTyping();
this._addChatMessage('assistant', msg.content, msg.agent_id);
}
if (msg.type === 'agent_state' && msg.agent_id === this.selectedAgent) {
if (this.activeTab === 'status') this._renderStatus(this.selectedAgent);
}
if (msg.type === 'task_update' || msg.type === 'task_created') {
if (this.activeTab === 'tasks') this._renderTasks(this.selectedAgent);
}
if (msg.type === 'memory_event' && msg.agent_id === this.selectedAgent) {
if (this.activeTab === 'memory') this._renderMemory(this.selectedAgent);
}
if (msg.type === 'system_status') {
this._renderSystemStatus(msg);
}
});
this.ws.on('typing', (data) => {
if (data.agent_id === this.selectedAgent) {
this._showTyping();
}
});
}
selectAgent(agentId) {
this.selectedAgent = agentId;
const def = AGENT_DEFS[agentId];
if (!def) return;
this.panelName.textContent = def.name.toUpperCase();
this.panelRole.textContent = def.role;
this.panelName.style.color = def.color;
this.panelName.style.textShadow = `0 0 10px ${def.color}80`;
this.systemPanel.classList.add('hidden');
this.panel.classList.remove('hidden');
this._switchTab('chat');
this._renderChat(agentId);
this._renderStatus(agentId);
this._renderTasks(agentId);
this._renderMemory(agentId);
}
selectCore() {
this.selectedAgent = null;
this.panel.classList.add('hidden');
this.systemPanel.classList.remove('hidden');
this._renderSystemStatus(this.ws.getSystemStatus());
}
closePanel() {
this.panel.classList.add('hidden');
this.selectedAgent = null;
if (this.onClose) this.onClose();
}
closeSystemPanel() {
this.systemPanel.classList.add('hidden');
if (this.onClose) this.onClose();
}
isPanelOpen() {
return !this.panel.classList.contains('hidden') || !this.systemPanel.classList.contains('hidden');
}
_switchTab(tabName) {
this.activeTab = tabName;
document.querySelectorAll('.panel-tabs .tab').forEach(t => {
t.classList.toggle('active', t.dataset.tab === tabName);
});
document.querySelectorAll('.tab-content').forEach(c => {
c.classList.toggle('active', c.id === `tab-${tabName}`);
});
// Refresh content
if (this.selectedAgent) {
if (tabName === 'status') this._renderStatus(this.selectedAgent);
if (tabName === 'tasks') this._renderTasks(this.selectedAgent);
if (tabName === 'memory') this._renderMemory(this.selectedAgent);
}
}
// ===== Chat =====
_renderChat(agentId) {
const agent = this.ws.getAgent(agentId);
if (!agent) return;
this.chatMessages.innerHTML = '';
agent.messages.forEach(msg => {
this._addChatMessage(msg.role, msg.content, agentId, false);
});
this._scrollChat();
}
_addChatMessage(role, content, agentId, scroll = true) {
const div = document.createElement('div');
div.className = `chat-msg ${role}`;
const roleLabel = document.createElement('div');
roleLabel.className = 'msg-role';
roleLabel.textContent = role === 'user' ? 'OPERATOR' : AGENT_DEFS[agentId]?.name?.toUpperCase() || 'AGENT';
div.appendChild(roleLabel);
const text = document.createElement('div');
text.textContent = content;
div.appendChild(text);
this.chatMessages.appendChild(div);
if (scroll) this._scrollChat();
}
_sendChat() {
const content = this.chatInput.value.trim();
if (!content || !this.selectedAgent) return;
this._addChatMessage('user', content, this.selectedAgent);
this.chatInput.value = '';
this.ws.send({
type: 'chat_message',
agent_id: this.selectedAgent,
content,
});
}
_scrollChat() {
requestAnimationFrame(() => {
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
});
}
_showTyping() {
this.typingIndicator.classList.remove('hidden');
}
_hideTyping() {
this.typingIndicator.classList.add('hidden');
}
// ===== Status =====
_renderStatus(agentId) {
const agent = this.ws.getAgent(agentId);
if (!agent) return;
const rows = [
['Name', agent.name],
['Role', agent.role],
['State', agent.state],
['Current Task', agent.current_task || '—'],
['Glow Intensity', (agent.glow_intensity * 100).toFixed(0) + '%'],
['Last Action', this._formatTime(agent.last_action)],
];
this.statusGrid.innerHTML = rows.map(([key, value]) => {
const stateClass = key === 'State' ? `state-${value}` : '';
return `<div class="status-row">
<span class="status-key">${key}</span>
<span class="status-value ${stateClass}">${value}</span>
</div>`;
}).join('');
}
// ===== Tasks =====
_renderTasks(agentId) {
const tasks = this.ws.getAgentTasks(agentId);
if (!tasks.length) {
this.tasksList.innerHTML = '<div style="padding:20px;text-align:center;color:var(--matrix-text-dim);font-size:12px;">No tasks assigned</div>';
return;
}
this.tasksList.innerHTML = tasks.map(task => `
<div class="task-item" data-task-id="${task.task_id}">
<div class="task-header">
<span class="task-status-dot ${task.status}"></span>
<span class="task-title">${task.title}</span>
<span class="task-priority ${task.priority}">${task.priority}</span>
</div>
<div style="font-size:10px;color:var(--matrix-text-dim);margin-bottom:4px;">Status: ${task.status}</div>
${task.status === 'pending' || task.status === 'in_progress' ? `
<div class="task-actions">
<button class="task-btn approve" data-task-id="${task.task_id}" data-action="approve">Approve</button>
<button class="task-btn veto" data-task-id="${task.task_id}" data-action="veto">Veto</button>
</div>
` : ''}
</div>
`).join('');
// Bind task action buttons
this.tasksList.querySelectorAll('.task-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const taskId = e.target.dataset.taskId;
const action = e.target.dataset.action;
this.ws.send({ type: 'task_action', task_id: taskId, action });
});
});
}
// ===== Memory =====
_renderMemory(agentId) {
const agent = this.ws.getAgent(agentId);
if (!agent || !agent.memories.length) {
this.memoryList.innerHTML = '<div style="padding:20px;text-align:center;color:var(--matrix-text-dim);font-size:12px;">No memory entries yet</div>';
return;
}
this.memoryList.innerHTML = agent.memories.slice(0, 30).map(entry => `
<div class="memory-entry">
<div class="memory-timestamp">${this._formatTime(entry.timestamp)}</div>
<div class="memory-content">${entry.content}</div>
</div>
`).join('');
}
// ===== System Status =====
_renderSystemStatus(status) {
const rows = [
['Agents Online', status.agents_online || 0],
['Tasks Pending', status.tasks_pending || 0],
['Tasks Running', status.tasks_running || 0],
['Tasks Completed', status.tasks_completed || 0],
['Tasks Failed', status.tasks_failed || 0],
['Total Tasks', status.total_tasks || 0],
['System Uptime', status.uptime || '—'],
];
this.systemStatusGrid.innerHTML = rows.map(([key, value]) => `
<div class="status-row">
<span class="status-key">${key}</span>
<span class="status-value">${value}</span>
</div>
`).join('');
}
// ===== FPS =====
updateFPS(fps, drawCalls, triangles) {
this.fpsCounter.textContent = `FPS: ${fps.toFixed(0)} | Draw: ${drawCalls} | Tri: ${triangles}`;
}
// ===== Helpers =====
_formatTime(isoString) {
if (!isoString) return '—';
try {
const d = new Date(isoString);
return d.toLocaleTimeString('en-US', { hour12: false });
} catch {
return isoString;
}
}
}