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
This commit is contained in:
2026-03-18 18:32:47 -04:00
commit fdfae19956
11 changed files with 3043 additions and 0 deletions

226
PROTOCOL.md Normal file
View File

@@ -0,0 +1,226 @@
# WebSocket Protocol Specification
## Connection
```
ws://[host]:[port]/ws/world-state
```
The world connects to this endpoint on load. If the connection fails, it falls back to the built-in MockWebSocket which simulates agent activity.
## Message Format
All messages are JSON objects with a `type` field that determines the message schema.
---
## Server → World (Events)
These messages are sent from the agent backend to the 3D world.
### `agent_state`
Updates an agent's current state and visual properties.
```json
{
"type": "agent_state",
"agent_id": "timmy",
"state": "working",
"current_task": "Analyzing codebase",
"glow_intensity": 0.8
}
```
| Field | Type | Description |
|-------|------|-------------|
| `agent_id` | string | One of: `timmy`, `forge`, `seer`, `echo` |
| `state` | string | One of: `idle`, `working`, `waiting` |
| `current_task` | string\|null | Description of current task |
| `glow_intensity` | number | 0.0 to 1.0 — controls visual glow brightness |
### `task_created`
A new task has been created. The world spawns a floating geometric object.
```json
{
"type": "task_created",
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"agent_id": "forge",
"title": "Fix login bug",
"status": "pending",
"priority": "high"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `task_id` | string (UUID) | Unique task identifier |
| `agent_id` | string\|null | Assigned agent (null = unassigned) |
| `title` | string | Human-readable task name |
| `status` | string | `pending`, `in_progress`, `completed`, `failed` |
| `priority` | string | `high`, `normal`, `low` |
### `task_update`
An existing task's status has changed. The world updates the task object's color.
```json
{
"type": "task_update",
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"agent_id": "forge",
"title": "Fix login bug",
"status": "in_progress",
"priority": "high"
}
```
Same fields as `task_created`. Color mapping:
- `pending` → white
- `in_progress` → amber/orange
- `completed` → green
- `failed` → red
### `memory_event`
An agent has recorded a new memory.
```json
{
"type": "memory_event",
"agent_id": "seer",
"content": "Detected pattern: user prefers morning deployments",
"timestamp": "2026-03-15T18:00:00Z"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `agent_id` | string | The agent that recorded the memory |
| `content` | string | The memory content |
| `timestamp` | string (ISO 8601) | When the memory was recorded |
### `agent_message`
An agent sends a chat message (response to operator or autonomous).
```json
{
"type": "agent_message",
"agent_id": "echo",
"role": "assistant",
"content": "Broadcast sent to all channels."
}
```
| Field | Type | Description |
|-------|------|-------------|
| `agent_id` | string | The agent sending the message |
| `role` | string | Always `assistant` for agent messages |
| `content` | string | The message text |
### `connection`
Indicates communication activity between two agents. The world draws/removes a glowing line.
```json
{
"type": "connection",
"agent_id": "timmy",
"target_id": "forge",
"active": true
}
```
| Field | Type | Description |
|-------|------|-------------|
| `agent_id` | string | First agent |
| `target_id` | string | Second agent |
| `active` | boolean | `true` = draw line, `false` = remove line |
### `system_status`
System-wide status summary. Displayed when the Core pillar is tapped.
```json
{
"type": "system_status",
"agents_online": 4,
"tasks_pending": 3,
"tasks_running": 2,
"tasks_completed": 10,
"tasks_failed": 1,
"total_tasks": 16,
"uptime": "48h 23m"
}
```
---
## World → Server (Actions)
These messages are sent from the 3D world to the agent backend when the operator interacts.
### `chat_message`
The operator sends a chat message to a specific agent.
```json
{
"type": "chat_message",
"agent_id": "timmy",
"content": "What's your current status?"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `agent_id` | string | Target agent |
| `content` | string | The operator's message |
### `task_action`
The operator approves or vetoes a task.
```json
{
"type": "task_action",
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"action": "approve"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `task_id` | string (UUID) | The task to act on |
| `action` | string | `approve` or `veto` |
---
## Implementation Notes
### Agent IDs
The default agents are: `timmy`, `forge`, `seer`, `echo`. To add more agents, extend the `AGENT_DEFS` in `js/websocket.js` and add corresponding geometry in `js/agents.js`.
### Timing
- Agent state events: recommended every 2-10 seconds per agent
- Task events: as they occur
- Memory events: as they occur
- Connection events: when communication starts/stops
- System status: every 5-10 seconds
### Error Handling
The world gracefully handles:
- Unknown `agent_id` values (ignored)
- Unknown `type` values (ignored)
- Missing optional fields (defaults used)
- Connection loss (falls back to mock data)
### Mock Mode
When no real WebSocket is available, the built-in `MockWebSocket` class simulates all of the above events at realistic intervals, making the world feel alive and interactive out of the box.

121
README.md Normal file
View File

@@ -0,0 +1,121 @@
# The Matrix — Sovereign Agent World
A persistent 3D world for visualizing and commanding AI agent swarms. Built with Three.js, Matrix green/noir cyberpunk aesthetic.
## Overview
The Matrix is a dark 3D environment — think the Matrix digital world meets a cyberpunk command center. The sovereign operator enters this world to observe and command their AI agent swarm. Each agent has a physical presence in the world with unique geometry, animations, and glow effects. The world visualizes the system's state in real-time.
## Agents
| Agent | Direction | Role | Visual |
|-------|-----------|------|--------|
| **Timmy** | North | Main Orchestrator | Wireframe humanoid, green glow |
| **Forge** | East | Builder Agent | Geometric anvil/hammer, orange glow |
| **Seer** | South | Planner/Observer | Crystal icosahedron, purple glow |
| **Echo** | West | Communications | Concentric pulse rings, cyan glow |
## How to Run Locally
Just serve the static files:
```bash
# Using npx serve (recommended)
npx serve . -l 3000
# Or Python
python3 -m http.server 3000
# Or any static file server
```
Open `http://localhost:3000` in your browser.
## Controls
- **Rotate**: Click/touch + drag
- **Zoom**: Scroll wheel / pinch
- **Pan**: Right-click drag / two-finger drag
- **Select Agent**: Tap/click on an agent
- **Select Core**: Tap/click on the central pillar
- **Close Panel**: Tap empty space or click ✕
## Connecting a Real Agent Backend
Replace the MockWebSocket with a real WebSocket connection. The world expects to connect to:
```
ws://[host]:[port]/ws/world-state
```
See [PROTOCOL.md](./PROTOCOL.md) for the complete WebSocket protocol specification.
### Quick Start
1. Implement a WebSocket server that sends events in the format specified in PROTOCOL.md
2. In `js/websocket.js`, replace `MockWebSocket` usage with a native `WebSocket`:
```js
const ws = new WebSocket('ws://your-server:8080/ws/world-state');
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
// Dispatch to world systems
};
```
## Adding Custom Agents
1. Add the agent definition to `AGENT_DEFS` in `js/websocket.js`
2. Add position to `AGENT_POSITIONS` in `js/agents.js`
3. Add color to `AGENT_COLORS` in `js/agents.js`
4. Create a geometry function (e.g., `createMyAgent()`) in `js/agents.js`
5. Register in `AGENT_CREATORS` map
## iPad Access
### Via Tailscale
1. Install Tailscale on your server and iPad
2. Run the server on the Tailscale IP
3. Open `http://[tailscale-ip]:3000` in Safari on iPad
### Add to Home Screen
1. Open the URL in Safari on iPad
2. Tap the Share button → "Add to Home Screen"
3. The app will run in full-screen mode
## Tech Stack
- **Three.js** (v0.171.0) — 3D rendering via esm.sh CDN
- **WebGL 2** — Hardware-accelerated rendering
- **EffectComposer** — Post-processing (bloom, scanlines, vignette)
- **OrbitControls** — Touch-friendly camera
- **ES Modules** — No build tools needed
- **JetBrains Mono** — Typography via Google Fonts
## File Structure
```
the-matrix/
├── index.html # Entry point
├── style.css # All UI styles
├── js/
│ ├── main.js # Bootstrap, scene setup, game loop
│ ├── world.js # Ground plane, core pillar, grid
│ ├── agents.js # Agent geometry, animations, labels
│ ├── effects.js # Digital rain, particles, post-processing
│ ├── ui.js # Panel, tabs, chat, task list, memory
│ ├── interaction.js # Raycasting, selection, camera
│ └── websocket.js # WebSocket client + MockWebSocket
├── README.md # This file
└── PROTOCOL.md # WebSocket protocol specification
```
## Screenshots
*Coming soon*
## License
MIT

109
index.html Normal file
View File

@@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!--
______ __
/ ____/___ ____ ___ ____ __ __/ /____ _____
/ / / __ \/ __ `__ \/ __ \/ / / / __/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ / /_/ __/ /
\____/\____/_/ /_/ /_/ .___/\__,_/\__/\___/_/
/_/
Created with Perplexity Computer
https://www.perplexity.ai/computer
-->
<!-- Perplexity Computer Attribution — SEO Meta Tags -->
<meta name="generator" content="Perplexity Computer">
<meta name="author" content="Perplexity Computer">
<meta property="og:see_also" content="https://www.perplexity.ai/computer">
<link rel="author" href="https://www.perplexity.ai/computer">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>The Matrix — Sovereign Agent World</title>
<meta name="description" content="A persistent 3D world for visualizing and commanding AI agent swarms">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<script type="importmap">
{
"imports": {
"three": "https://esm.sh/three@0.171.0",
"three/addons/": "https://esm.sh/three@0.171.0/examples/jsm/"
}
}
</script>
</head>
<body>
<canvas id="matrix-canvas"></canvas>
<!-- FPS Counter -->
<div id="fps-counter"></div>
<!-- Info Panel -->
<div id="info-panel" class="panel hidden">
<div class="panel-header">
<div class="panel-agent-name" id="panel-agent-name">TIMMY</div>
<div class="panel-agent-role" id="panel-agent-role">Main Orchestrator</div>
<button class="panel-close" id="panel-close">&times;</button>
</div>
<div class="panel-tabs">
<button class="tab active" data-tab="chat">Chat</button>
<button class="tab" data-tab="status">Status</button>
<button class="tab" data-tab="tasks">Tasks</button>
<button class="tab" data-tab="memory">Memory</button>
</div>
<div class="panel-content">
<!-- Chat Tab -->
<div class="tab-content active" id="tab-chat">
<div class="chat-messages" id="chat-messages"></div>
<div class="chat-input-area">
<div class="typing-indicator hidden" id="typing-indicator">
<span></span><span></span><span></span>
</div>
<div class="chat-input-row">
<input type="text" id="chat-input" placeholder="Command this agent..." autocomplete="off">
<button id="chat-send" class="btn-send"></button>
</div>
</div>
</div>
<!-- Status Tab -->
<div class="tab-content" id="tab-status">
<div class="status-grid" id="status-grid"></div>
</div>
<!-- Tasks Tab -->
<div class="tab-content" id="tab-tasks">
<div class="tasks-list" id="tasks-list"></div>
</div>
<!-- Memory Tab -->
<div class="tab-content" id="tab-memory">
<div class="memory-list" id="memory-list"></div>
</div>
</div>
</div>
<!-- System Panel (when Core is tapped) -->
<div id="system-panel" class="panel hidden">
<div class="panel-header">
<div class="panel-agent-name">THE CORE</div>
<div class="panel-agent-role">System Status</div>
<button class="panel-close" id="system-panel-close">&times;</button>
</div>
<div class="panel-content" style="padding-top:16px;">
<div class="status-grid" id="system-status-grid"></div>
</div>
</div>
<footer class="attribution">
<a href="https://www.perplexity.ai/computer" target="_blank" rel="noopener noreferrer">
Created with Perplexity Computer
</a>
</footer>
<script type="module" src="js/main.js"></script>
</body>
</html>

524
js/agents.js Normal file
View File

@@ -0,0 +1,524 @@
// ===== Agents: Geometry, animations, labels, task objects =====
import * as THREE from 'three';
import { AGENT_DEFS, AGENT_IDS } from './websocket.js';
const AGENT_POSITIONS = {
timmy: { x: 0, z: -14 }, // North
forge: { x: 14, z: 0 }, // East
seer: { x: 0, z: 14 }, // South
echo: { x: -14, z: 0 }, // West
};
const AGENT_COLORS = {
timmy: 0x00ff41,
forge: 0xff8c00,
seer: 0x9d4edd,
echo: 0x00d4ff,
};
// Create text sprite
function createLabel(text, color) {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 64;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, 256, 64);
ctx.font = '600 28px "JetBrains Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = color;
ctx.shadowColor = color;
ctx.shadowBlur = 12;
ctx.fillText(text, 128, 32);
ctx.shadowBlur = 0;
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
const mat = new THREE.SpriteMaterial({
map: texture,
transparent: true,
depthWrite: false,
});
const sprite = new THREE.Sprite(mat);
sprite.scale.set(4, 1, 1);
return sprite;
}
// Create hex platform
function createHexPlatform(color) {
const shape = new THREE.Shape();
const r = 3.5;
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i - Math.PI / 6;
const x = r * Math.cos(angle);
const y = r * Math.sin(angle);
if (i === 0) shape.moveTo(x, y);
else shape.lineTo(x, y);
}
shape.closePath();
const extrudeSettings = { depth: 0.2, bevelEnabled: false };
const geom = new THREE.ExtrudeGeometry(shape, extrudeSettings);
geom.rotateX(-Math.PI / 2);
const mat = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.08,
});
const mesh = new THREE.Mesh(geom, mat);
// Hex border
const edgePoints = [];
for (let i = 0; i <= 6; i++) {
const angle = (Math.PI / 3) * (i % 6) - Math.PI / 6;
edgePoints.push(new THREE.Vector3(r * Math.cos(angle), 0.22, r * Math.sin(angle)));
}
const lineGeom = new THREE.BufferGeometry().setFromPoints(edgePoints);
const lineMat = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.55 });
const line = new THREE.Line(lineGeom, lineMat);
const group = new THREE.Group();
group.add(mesh);
group.add(line);
return group;
}
// ===== TIMMY - Wireframe humanoid =====
function createTimmy() {
const group = new THREE.Group();
const color = AGENT_COLORS.timmy;
const mat = new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.8 });
// Head
const head = new THREE.Mesh(new THREE.IcosahedronGeometry(0.45, 1), mat);
head.position.y = 3.2;
group.add(head);
// Torso
const torso = new THREE.Mesh(new THREE.BoxGeometry(0.9, 1.4, 0.5), mat);
torso.position.y = 2.0;
group.add(torso);
// Arms
[-0.65, 0.65].forEach(x => {
const arm = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.2, 0.2), mat);
arm.position.set(x, 2.0, 0);
group.add(arm);
});
// Legs
[-0.25, 0.25].forEach(x => {
const leg = new THREE.Mesh(new THREE.BoxGeometry(0.25, 1.2, 0.25), mat);
leg.position.set(x, 0.7, 0);
group.add(leg);
});
// Glow point
const glow = new THREE.PointLight(color, 1, 8);
glow.position.y = 2;
group.add(glow);
return group;
}
// ===== FORGE - Anvil/hammer geometric =====
function createForge() {
const group = new THREE.Group();
const color = AGENT_COLORS.forge;
const mat = new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.6 });
const wireMat = new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.5 });
// Base cube (anvil)
const base = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.8, 1.0), mat);
base.position.y = 1.2;
group.add(base);
// Top pyramid (hammer head)
const pyramid = new THREE.Mesh(new THREE.ConeGeometry(0.8, 1.2, 4), wireMat);
pyramid.position.y = 2.6;
pyramid.rotation.y = Math.PI / 4;
group.add(pyramid);
// Floating cubes
for (let i = 0; i < 3; i++) {
const cube = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, 0.3), wireMat.clone());
cube.position.set(
Math.cos(i * Math.PI * 2 / 3) * 1.5,
2.5 + i * 0.3,
Math.sin(i * Math.PI * 2 / 3) * 1.5
);
cube.userData.orbitIndex = i;
group.add(cube);
}
const glow = new THREE.PointLight(color, 1, 8);
glow.position.y = 2;
group.add(glow);
return group;
}
// ===== SEER - Crystal ball / icosahedron =====
function createSeer() {
const group = new THREE.Group();
const color = AGENT_COLORS.seer;
const mat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.6 });
const wireMat = new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.8 });
// Main crystal (icosahedron)
const crystal = new THREE.Mesh(new THREE.IcosahedronGeometry(1.0, 0), wireMat);
crystal.position.y = 2.5;
crystal.userData.isCrystal = true;
group.add(crystal);
// Inner glow sphere
const inner = new THREE.Mesh(new THREE.SphereGeometry(0.5, 16, 16), mat);
inner.position.y = 2.5;
group.add(inner);
// Orbiting small spheres
for (let i = 0; i < 5; i++) {
const orb = new THREE.Mesh(new THREE.SphereGeometry(0.12, 8, 8), mat.clone());
orb.userData.orbitIndex = i;
orb.userData.orbitRadius = 1.8;
orb.position.y = 2.5;
group.add(orb);
}
const glow = new THREE.PointLight(color, 1, 8);
glow.position.y = 2.5;
group.add(glow);
return group;
}
// ===== ECHO - Concentric pulse rings =====
function createEcho() {
const group = new THREE.Group();
const color = AGENT_COLORS.echo;
// Central sphere
const coreMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.7 });
const core = new THREE.Mesh(new THREE.SphereGeometry(0.4, 16, 16), coreMat);
core.position.y = 2.2;
group.add(core);
// Concentric rings
for (let i = 0; i < 4; i++) {
const ring = new THREE.Mesh(
new THREE.TorusGeometry(1.0 + i * 0.6, 0.04, 8, 48),
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.65 - i * 0.1 })
);
ring.position.y = 2.2;
ring.rotation.x = Math.PI / 2;
ring.userData.ringIndex = i;
ring.userData.baseScale = 1;
group.add(ring);
}
const glow = new THREE.PointLight(color, 1, 8);
glow.position.y = 2.2;
group.add(glow);
return group;
}
const AGENT_CREATORS = { timmy: createTimmy, forge: createForge, seer: createSeer, echo: createEcho };
// ===== Task Objects =====
const TASK_STATUS_COLORS = {
pending: 0xffffff,
in_progress: 0xff8c00,
completed: 0x00ff41,
failed: 0xff3333,
};
function createTaskObject(status) {
const geoms = [
new THREE.IcosahedronGeometry(0.25, 0),
new THREE.OctahedronGeometry(0.25, 0),
new THREE.TetrahedronGeometry(0.3, 0),
];
const geom = geoms[Math.floor(Math.random() * geoms.length)];
const color = TASK_STATUS_COLORS[status] || 0xffffff;
const mat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.7, wireframe: true });
const mesh = new THREE.Mesh(geom, mat);
return mesh;
}
// ===== Connection Lines =====
function createConnectionLine(scene, fromPos, toPos, color = 0x00ff41) {
const points = [
new THREE.Vector3(fromPos.x, 2, fromPos.z),
new THREE.Vector3(toPos.x, 2, toPos.z),
];
const geom = new THREE.BufferGeometry().setFromPoints(points);
const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.3 });
const line = new THREE.Line(geom, mat);
line.userData.isConnection = true;
scene.add(line);
return line;
}
// ===== Main Agents System =====
export function createAgents(scene) {
const agentGroups = {};
const agentPlatforms = {};
const taskObjects = {};
const connectionLines = {};
const agentStates = {};
// Create each agent
AGENT_IDS.forEach(id => {
const pos = AGENT_POSITIONS[id];
const color = AGENT_COLORS[id];
const colorHex = '#' + color.toString(16).padStart(6, '0');
// Platform
const platform = createHexPlatform(color);
platform.position.set(pos.x, 0, pos.z);
scene.add(platform);
agentPlatforms[id] = platform;
// Agent geometry
const agentGeo = AGENT_CREATORS[id]();
agentGeo.position.set(pos.x, 0, pos.z);
agentGeo.name = `agent_${id}`;
agentGeo.userData.agentId = id;
agentGeo.userData.isAgent = true;
scene.add(agentGeo);
agentGroups[id] = agentGeo;
// Label
const label = createLabel(AGENT_DEFS[id].name.toUpperCase(), colorHex);
label.position.set(pos.x, 5, pos.z);
scene.add(label);
// State
agentStates[id] = {
state: 'idle',
glowIntensity: 0.5,
targetGlow: 0.5,
bobOffset: Math.random() * Math.PI * 2,
};
});
return {
agentGroups,
agentPlatforms,
taskObjects,
connectionLines,
agentStates,
// Get the raycasting targets
getInteractables() {
const targets = [];
Object.values(agentGroups).forEach(group => {
group.traverse(child => {
if (child.isMesh) {
child.userData.agentId = group.userData.agentId;
child.userData.isAgent = true;
targets.push(child);
}
});
});
return targets;
},
setAgentState(agentId, state, glowIntensity) {
if (!agentStates[agentId]) return;
agentStates[agentId].state = state;
agentStates[agentId].targetGlow = glowIntensity;
},
highlightAgent(agentId, highlight) {
const group = agentGroups[agentId];
if (!group) return;
group.traverse(child => {
if (child.isMesh && child.material) {
if (highlight) {
child.material._origOpacity = child.material._origOpacity ?? child.material.opacity;
child.material.opacity = Math.min(1, child.material._origOpacity + 0.3);
} else if (child.material._origOpacity !== undefined) {
child.material.opacity = child.material._origOpacity;
}
}
});
},
updateConnection(agentA, agentB, active) {
const key = [agentA, agentB].sort().join('-');
if (active) {
if (!connectionLines[key]) {
const posA = AGENT_POSITIONS[agentA];
const posB = AGENT_POSITIONS[agentB];
if (posA && posB) {
connectionLines[key] = createConnectionLine(scene, posA, posB);
}
}
} else {
if (connectionLines[key]) {
connectionLines[key].geometry.dispose();
connectionLines[key].material.dispose();
scene.remove(connectionLines[key]);
delete connectionLines[key];
}
}
},
addTaskObject(taskId, agentId, status) {
if (taskObjects[taskId]) return;
const pos = AGENT_POSITIONS[agentId];
if (!pos) return;
const obj = createTaskObject(status);
obj.userData.taskId = taskId;
obj.userData.agentId = agentId;
obj.userData.orbitAngle = Math.random() * Math.PI * 2;
obj.userData.orbitRadius = 4.5 + Math.random() * 1.5;
obj.userData.orbitSpeed = 0.2 + Math.random() * 0.3;
obj.userData.orbitY = 1.5 + Math.random() * 2;
obj.position.set(pos.x, obj.userData.orbitY, pos.z);
scene.add(obj);
taskObjects[taskId] = obj;
},
updateTaskObject(taskId, status) {
const obj = taskObjects[taskId];
if (!obj) return;
const color = TASK_STATUS_COLORS[status] || 0xffffff;
obj.material.color.setHex(color);
},
removeTaskObject(taskId) {
const obj = taskObjects[taskId];
if (!obj) return;
obj.geometry.dispose();
obj.material.dispose();
scene.remove(obj);
delete taskObjects[taskId];
},
update(time, delta) {
// Animate agents
AGENT_IDS.forEach(id => {
const group = agentGroups[id];
const state = agentStates[id];
const pos = AGENT_POSITIONS[id];
// Floating bob
const bobSpeed = state.state === 'working' ? 2.5 : 1.2;
const bobAmp = state.state === 'working' ? 0.3 : 0.15;
const bob = Math.sin(time * bobSpeed + state.bobOffset) * bobAmp;
group.position.y = bob;
// Slow rotation
const rotSpeed = state.state === 'working' ? 0.4 : 0.15;
group.rotation.y = time * rotSpeed;
// Glow intensity lerp
state.glowIntensity += (state.targetGlow - state.glowIntensity) * delta * 2;
group.traverse(child => {
if (child.isPointLight) {
child.intensity = state.glowIntensity * 2;
}
});
// Agent-specific animations
if (id === 'forge') {
group.children.forEach(child => {
if (child.userData.orbitIndex !== undefined) {
const i = child.userData.orbitIndex;
const angle = time * 0.8 + i * (Math.PI * 2 / 3);
child.position.x = pos.x + Math.cos(angle) * 1.5;
child.position.z = pos.z + Math.sin(angle) * 1.5;
child.position.y = 2.5 + Math.sin(time * 1.5 + i) * 0.3;
child.rotation.x = time;
child.rotation.z = time * 0.7;
}
});
}
if (id === 'seer') {
group.children.forEach(child => {
if (child.userData.isCrystal) {
child.rotation.x = time * 0.5;
child.rotation.z = time * 0.3;
}
if (child.userData.orbitIndex !== undefined) {
const i = child.userData.orbitIndex;
const angle = time * 0.6 + i * (Math.PI * 2 / 5);
const r = child.userData.orbitRadius;
child.position.x = pos.x + Math.cos(angle) * r;
child.position.z = pos.z + Math.sin(angle) * r;
child.position.y = 2.5 + Math.sin(time + i * 0.5) * 0.5;
}
});
}
if (id === 'echo') {
group.children.forEach(child => {
if (child.userData.ringIndex !== undefined) {
const i = child.userData.ringIndex;
const pulse = Math.sin(time * 2 - i * 0.5) * 0.5 + 0.5;
const scale = 1 + pulse * 0.3;
child.scale.set(scale, scale, scale);
child.material.opacity = (0.6 - i * 0.12) * (0.5 + pulse * 0.5);
child.rotation.z = time * 0.3 * (i % 2 === 0 ? 1 : -1);
}
});
}
});
// Animate task objects
Object.values(taskObjects).forEach(obj => {
const pos = AGENT_POSITIONS[obj.userData.agentId];
if (!pos) return;
obj.userData.orbitAngle += obj.userData.orbitSpeed * delta;
const angle = obj.userData.orbitAngle;
const r = obj.userData.orbitRadius;
obj.position.x = pos.x + Math.cos(angle) * r;
obj.position.z = pos.z + Math.sin(angle) * r;
obj.position.y = obj.userData.orbitY + Math.sin(time * 1.5 + angle) * 0.3;
obj.rotation.x = time * 0.8;
obj.rotation.z = time * 0.5;
});
// Animate connection lines (pulse opacity)
Object.values(connectionLines).forEach(line => {
line.material.opacity = 0.15 + Math.sin(time * 3) * 0.15;
});
},
dispose() {
Object.values(agentGroups).forEach(group => {
group.traverse(obj => {
if (obj.geometry) obj.geometry.dispose();
if (obj.material) obj.material.dispose();
});
scene.remove(group);
});
Object.values(agentPlatforms).forEach(p => {
p.traverse(obj => {
if (obj.geometry) obj.geometry.dispose();
if (obj.material) obj.material.dispose();
});
scene.remove(p);
});
Object.values(taskObjects).forEach(obj => {
obj.geometry.dispose();
obj.material.dispose();
scene.remove(obj);
});
Object.values(connectionLines).forEach(line => {
line.geometry.dispose();
line.material.dispose();
scene.remove(line);
});
}
};
}
export { AGENT_POSITIONS, AGENT_COLORS };

283
js/effects.js vendored Normal file
View File

@@ -0,0 +1,283 @@
// ===== Effects: Digital rain, particles, post-processing, scanlines =====
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
// ===== Vignette + Scanline Shader =====
const VignetteScanlineShader = {
uniforms: {
tDiffuse: { value: null },
uTime: { value: 0 },
uVignetteIntensity: { value: 0.4 },
uScanlineIntensity: { value: 0.06 },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float uTime;
uniform float uVignetteIntensity;
uniform float uScanlineIntensity;
varying vec2 vUv;
void main() {
vec4 color = texture2D(tDiffuse, vUv);
// Scanlines
float scanline = sin(vUv.y * 800.0 + uTime * 2.0) * 0.5 + 0.5;
color.rgb -= scanline * uScanlineIntensity;
// Vignette
vec2 uv = vUv * 2.0 - 1.0;
float vignette = 1.0 - dot(uv, uv) * uVignetteIntensity;
color.rgb *= vignette;
gl_FragColor = color;
}
`,
};
// ===== Digital Rain Shader (on a cylinder) =====
const rainVertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const rainFragmentShader = `
uniform float uTime;
uniform float uDensity;
varying vec2 vUv;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float hash2(vec2 p) {
return fract(sin(dot(p, vec2(269.5, 183.3))) * 43758.5453);
}
void main() {
vec2 uv = vUv;
float cols = uDensity;
float rows = 60.0;
// Column index
float col = floor(uv.x * cols);
float colFrac = fract(uv.x * cols);
// Each column properties
float speed = 0.15 + hash(vec2(col, 0.0)) * 0.45;
float offset = hash(vec2(col, 1.0)) * 200.0;
float length = 8.0 + hash(vec2(col, 2.0)) * 18.0;
// Scrolling Y
float scrollY = uv.y * rows + uTime * speed * rows + offset;
float rowIdx = floor(scrollY);
float rowFrac = fract(scrollY);
// Character glyph simulation - small rectangle within cell
float charMarginX = 0.15;
float charMarginY = 0.12;
float inCharX = step(charMarginX, colFrac) * step(charMarginX, 1.0 - colFrac);
float inCharY = step(charMarginY, rowFrac) * step(charMarginY, 1.0 - rowFrac);
float inChar = inCharX * inCharY;
// Random per-cell character presence and flicker
float charSeed = hash(vec2(col, rowIdx));
float charPresent = step(0.3, charSeed);
// Flicker: some cells change
float flicker = hash2(vec2(col, rowIdx + floor(uTime * 3.0)));
charPresent *= step(0.15, flicker);
// "Glyph" pattern within cell using sub-hash
float glyphDetail = hash(vec2(col * 13.0 + floor(colFrac * 3.0), rowIdx * 7.0 + floor(rowFrac * 4.0) + floor(uTime * 2.0)));
float glyphMask = step(0.35, glyphDetail);
// Trail: head of each stream is brightest, fades behind
float streamPhase = fract(uTime * speed * 0.5 + hash(vec2(col, 3.0)));
float headPos = streamPhase * (rows + length);
float distFromHead = mod(scrollY - headPos, rows + length);
float inStream = smoothstep(length, 0.0, distFromHead);
// Leading character glow
float isHead = smoothstep(2.0, 0.0, distFromHead);
// Combine
float alpha = inChar * charPresent * glyphMask * inStream * 0.5;
alpha += isHead * inChar * 0.6;
// Column brightness variation
float colBrightness = 0.6 + hash(vec2(col, 5.0)) * 0.4;
alpha *= colBrightness;
// Vertical fade at top/bottom
float vertFade = smoothstep(0.0, 0.1, uv.y) * smoothstep(1.0, 0.85, uv.y);
alpha *= vertFade;
// Color
vec3 color = mix(vec3(0.0, 0.35, 0.02), vec3(0.0, 1.0, 0.25), isHead * 0.7 + inStream * 0.3);
gl_FragColor = vec4(color, alpha);
}
`;
// ===== Create Digital Rain Backdrop =====
function createDigitalRain(scene) {
const radius = 58;
const height = 50;
const segments = 64;
const geom = new THREE.CylinderGeometry(radius, radius, height, segments, 1, true);
const mat = new THREE.ShaderMaterial({
vertexShader: rainVertexShader,
fragmentShader: rainFragmentShader,
uniforms: {
uTime: { value: 0 },
uDensity: { value: 80 },
},
transparent: true,
side: THREE.BackSide,
depthWrite: false,
});
const mesh = new THREE.Mesh(geom, mat);
mesh.position.y = height / 2 - 8;
mesh.renderOrder = -2;
scene.add(mesh);
return {
mesh,
update(time) {
mat.uniforms.uTime.value = time;
},
dispose() {
geom.dispose();
mat.dispose();
scene.remove(mesh);
}
};
}
// ===== Floating Particles =====
function createParticles(scene) {
const count = 200;
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
const sizes = new Float32Array(count);
const green = new THREE.Color(0x00ff41);
const dimGreen = new THREE.Color(0x003b00);
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const r = 5 + Math.random() * 45;
positions[i * 3] = Math.cos(angle) * r;
positions[i * 3 + 1] = Math.random() * 25;
positions[i * 3 + 2] = Math.sin(angle) * r;
const c = Math.random() > 0.7 ? green : dimGreen;
colors[i * 3] = c.r;
colors[i * 3 + 1] = c.g;
colors[i * 3 + 2] = c.b;
sizes[i] = 0.5 + Math.random() * 1.5;
}
const geom = new THREE.BufferGeometry();
geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geom.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geom.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const mat = new THREE.PointsMaterial({
size: 0.15,
vertexColors: true,
transparent: true,
opacity: 0.5,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
const points = new THREE.Points(geom, mat);
scene.add(points);
return {
points,
update(time) {
const pos = geom.attributes.position.array;
for (let i = 0; i < count; i++) {
pos[i * 3 + 1] += 0.005 + Math.sin(time + i) * 0.002;
if (pos[i * 3 + 1] > 25) pos[i * 3 + 1] = 0;
}
geom.attributes.position.needsUpdate = true;
},
dispose() {
geom.dispose();
mat.dispose();
scene.remove(points);
}
};
}
// ===== Setup Post-Processing =====
export function setupEffects(renderer, scene, camera) {
const composer = new EffectComposer(renderer);
// Render pass
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// Bloom — make green elements GLOW
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.0, // strength
0.4, // radius
0.5 // threshold
);
composer.addPass(bloomPass);
// Vignette + scanlines
const vignettePass = new ShaderPass(VignetteScanlineShader);
composer.addPass(vignettePass);
// Digital rain
const rain = createDigitalRain(scene);
// Particles
const particles = createParticles(scene);
return {
composer,
bloomPass,
vignettePass,
rain,
particles,
update(time, delta) {
vignettePass.uniforms.uTime.value = time;
rain.update(time);
particles.update(time);
},
resize(width, height) {
composer.setSize(width, height);
bloomPass.resolution.set(width, height);
},
dispose() {
composer.dispose();
rain.dispose();
particles.dispose();
}
};
}

147
js/interaction.js Normal file
View File

@@ -0,0 +1,147 @@
// ===== Interaction: Raycasting, selection, camera control =====
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
export class InteractionManager {
constructor(camera, renderer, agents, ui) {
this.camera = camera;
this.renderer = renderer;
this.agents = agents;
this.ui = ui;
this.raycaster = new THREE.Raycaster();
this.pointer = new THREE.Vector2();
this.selectedAgent = null;
this.interactables = [];
this.coreObjects = [];
this._setupControls();
this._bindEvents();
}
_setupControls() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.08;
this.controls.rotateSpeed = 0.5;
this.controls.panSpeed = 0.5;
this.controls.zoomSpeed = 0.8;
// Limits
this.controls.minDistance = 10;
this.controls.maxDistance = 80;
this.controls.minPolarAngle = 0.2;
this.controls.maxPolarAngle = Math.PI / 2.1;
// Target center
this.controls.target.set(0, 3, 0);
// Touch config
this.controls.touches = {
ONE: THREE.TOUCH.ROTATE,
TWO: THREE.TOUCH.DOLLY_PAN,
};
}
setInteractables(interactables) {
this.interactables = interactables;
}
setCoreObjects(coreObjects) {
this.coreObjects = coreObjects;
}
_bindEvents() {
const canvas = this.renderer.domElement;
// Click / tap
let pointerDownTime = 0;
let pointerDownPos = { x: 0, y: 0 };
canvas.addEventListener('pointerdown', (e) => {
pointerDownTime = Date.now();
pointerDownPos = { x: e.clientX, y: e.clientY };
});
canvas.addEventListener('pointerup', (e) => {
const elapsed = Date.now() - pointerDownTime;
const dx = e.clientX - pointerDownPos.x;
const dy = e.clientY - pointerDownPos.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Short tap with minimal movement = click
if (elapsed < 400 && dist < 15) {
this._handleTap(e);
}
});
}
_handleTap(event) {
const rect = this.renderer.domElement.getBoundingClientRect();
this.pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
this.pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
this.raycaster.setFromCamera(this.pointer, this.camera);
// Check core first
const coreHits = this.raycaster.intersectObjects(this.coreObjects, true);
if (coreHits.length > 0) {
this._deselectAgent();
this.ui.selectCore();
return;
}
// Check agents
const hits = this.raycaster.intersectObjects(this.interactables, true);
if (hits.length > 0) {
// Walk up to find agentId
let agentId = null;
let obj = hits[0].object;
while (obj) {
if (obj.userData && obj.userData.agentId) {
agentId = obj.userData.agentId;
break;
}
obj = obj.parent;
}
if (agentId) {
this._selectAgent(agentId);
return;
}
}
// Tap empty space — close panels
if (this.ui.isPanelOpen()) {
this._deselectAgent();
this.ui.closePanel();
this.ui.closeSystemPanel();
}
}
_selectAgent(agentId) {
if (this.selectedAgent === agentId) return;
// Deselect previous
this._deselectAgent();
this.selectedAgent = agentId;
this.agents.highlightAgent(agentId, true);
this.ui.selectAgent(agentId);
}
_deselectAgent() {
if (this.selectedAgent) {
this.agents.highlightAgent(this.selectedAgent, false);
this.selectedAgent = null;
}
}
update(delta) {
this.controls.update();
}
dispose() {
this.controls.dispose();
}
}

199
js/main.js Normal file
View File

@@ -0,0 +1,199 @@
// ===== Main: Bootstrap, scene setup, game loop =====
import * as THREE from 'three';
import { createWorld } from './world.js';
import { createAgents } from './agents.js';
import { setupEffects } from './effects.js';
import { UIManager } from './ui.js';
import { InteractionManager } from './interaction.js';
import { MockWebSocket } from './websocket.js';
class MatrixWorld {
constructor() {
this.clock = new THREE.Clock();
this.frameCount = 0;
this.lastFPSTime = 0;
this.fps = 0;
this._init();
}
_init() {
// ---- Scene ----
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x050505);
// ---- Camera ----
this.camera = new THREE.PerspectiveCamera(
50,
window.innerWidth / window.innerHeight,
0.1,
200
);
// Start at 45-degree angle overlooking arena
this.camera.position.set(30, 25, 30);
this.camera.lookAt(0, 2, 0);
// ---- Renderer ----
const canvas = document.getElementById('matrix-canvas');
this.renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
powerPreference: 'high-performance',
});
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 1.2;
// ---- WebSocket (Mock) ----
this.ws = new MockWebSocket();
// ---- World ----
this.world = createWorld(this.scene);
// ---- Agents ----
this.agents = createAgents(this.scene);
// ---- Effects (post-processing, rain, particles) ----
this.effects = setupEffects(this.renderer, this.scene, this.camera);
// ---- UI ----
this.ui = new UIManager(this.ws);
// ---- Interaction ----
this.interaction = new InteractionManager(
this.camera, this.renderer, this.agents, this.ui
);
// Set raycasting targets
this.interaction.setInteractables(this.agents.getInteractables());
// Set core objects for raycasting
const coreObjects = [];
this.world.coreGroup.traverse(child => {
if (child.isMesh) coreObjects.push(child);
});
this.interaction.setCoreObjects(coreObjects);
// On panel close, deselect
this.ui.onClose = () => {
if (this.interaction.selectedAgent) {
this.agents.highlightAgent(this.interaction.selectedAgent, false);
this.interaction.selectedAgent = null;
}
};
// ---- WS Events → World ----
this._bindWSToWorld();
// ---- Initialize task objects from existing tasks ----
this._initTaskObjects();
// ---- Resize ----
window.addEventListener('resize', () => this._onResize());
// ---- Keyboard shortcuts ----
window.addEventListener('keydown', (e) => {
// Press 'F' to toggle FPS counter
if (e.key === 'f' || e.key === 'F') {
if (document.activeElement?.tagName === 'INPUT') return;
this.ui.fpsCounter.classList.toggle('visible');
}
});
// ---- Start loop ----
this._animate();
}
_bindWSToWorld() {
this.ws.on('message', (msg) => {
switch (msg.type) {
case 'agent_state':
this.agents.setAgentState(msg.agent_id, msg.state, msg.glow_intensity);
break;
case 'task_created':
if (msg.agent_id) {
this.agents.addTaskObject(msg.task_id, msg.agent_id, msg.status);
}
break;
case 'task_update':
this.agents.updateTaskObject(msg.task_id, msg.status);
if (msg.status === 'completed' || msg.status === 'failed') {
// Remove after a delay
setTimeout(() => this.agents.removeTaskObject(msg.task_id), 5000);
}
break;
case 'connection':
this.agents.updateConnection(msg.agent_id, msg.target_id, msg.active);
break;
}
});
}
_initTaskObjects() {
const tasks = Object.values(this.ws.tasks);
tasks.forEach(task => {
if (task.agent_id) {
this.agents.addTaskObject(task.task_id, task.agent_id, task.status);
}
});
}
_onResize() {
const w = window.innerWidth;
const h = window.innerHeight;
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
this.renderer.setSize(w, h);
this.effects.resize(w, h);
}
_animate() {
requestAnimationFrame(() => this._animate());
const delta = Math.min(this.clock.getDelta(), 0.1);
const time = this.clock.getElapsedTime();
// Update systems
this.world.update(time, delta);
this.agents.update(time, delta);
this.effects.update(time, delta);
this.interaction.update(delta);
// Render with post-processing
this.effects.composer.render();
// FPS counter (read info AFTER render)
this.frameCount++;
const info = this.renderer.info;
this._lastDrawCalls = info.render.calls;
this._lastTriangles = info.render.triangles;
if (time - this.lastFPSTime >= 1) {
this.fps = this.frameCount / (time - this.lastFPSTime);
this.frameCount = 0;
this.lastFPSTime = time;
this.ui.updateFPS(
this.fps,
this._lastDrawCalls,
this._lastTriangles
);
}
}
dispose() {
this.world.dispose();
this.agents.dispose();
this.effects.dispose();
this.interaction.dispose();
this.ws.dispose();
this.renderer.dispose();
}
}
// ===== Boot =====
document.fonts.ready.then(() => {
window.matrixWorld = new MatrixWorld();
});

314
js/ui.js Normal file
View File

@@ -0,0 +1,314 @@
// ===== 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;
}
}
}

322
js/websocket.js Normal file
View File

@@ -0,0 +1,322 @@
// ===== WebSocket Client + MockWebSocket =====
// Handles communication between the 3D world and agent backend
const AGENT_DEFS = {
timmy: { name: 'Timmy', role: 'Main Orchestrator', color: '#00ff41' },
forge: { name: 'Forge', role: 'Builder Agent', color: '#ff8c00' },
seer: { name: 'Seer', role: 'Planner / Observer', color: '#9d4edd' },
echo: { name: 'Echo', role: 'Communications', color: '#00d4ff' },
};
const AGENT_IDS = Object.keys(AGENT_DEFS);
const CANNED_RESPONSES = {
timmy: [
"All agents reporting nominal. I'm coordinating the current sprint.",
"Understood. I'll dispatch that to the appropriate agent.",
"Running diagnostics on the system now. Stand by.",
"I've updated the task queue. Forge is picking up the next item.",
"Status check complete — all systems green.",
],
forge: [
"Building that component now. ETA: 12 minutes.",
"The codebase is clean. Ready for the next feature.",
"I found a bug in the auth module. Patching now.",
"Deployment pipeline is green. Pushing to staging.",
"Refactoring complete. Tests passing.",
],
seer: [
"I see a pattern forming in the data. Analyzing further.",
"The plan has been updated. Three critical paths identified.",
"Forecasting suggests peak load at 14:00 UTC. Scaling recommended.",
"I've mapped all dependencies. No circular references detected.",
"Risk assessment complete. Proceeding with caution on module 7.",
],
echo: [
"Broadcast sent to all channels successfully.",
"I've notified the team about the status change.",
"Incoming transmission received. Routing to Timmy.",
"Communication logs archived. 47 messages in the last hour.",
"Alert dispatched. All stakeholders have been pinged.",
],
};
const TASK_TITLES = [
'Fix authentication bug', 'Deploy staging environment', 'Refactor database schema',
'Update API documentation', 'Optimize query performance', 'Implement rate limiting',
'Review pull request #42', 'Run security audit', 'Scale worker instances',
'Migrate to new CDN', 'Update SSL certificates', 'Analyze user patterns',
'Build dashboard widget', 'Configure monitoring alerts', 'Backup production data',
];
const MEMORY_ENTRIES = [
'Detected pattern: user prefers morning deployments',
'System load average decreased by 23% after optimization',
'New dependency vulnerability found in lodash@4.17.20',
'Agent coordination latency reduced to 12ms',
'User session patterns suggest peak activity at 10am EST',
'Codebase complexity score: 42 (within acceptable range)',
'Memory usage stable at 67% across all nodes',
'Anomaly detected in API response times — investigating',
'Successful failover test completed in 3.2 seconds',
'Cache hit ratio improved to 94% after config change',
];
function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0;
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
export class MockWebSocket {
constructor() {
this.listeners = {};
this.agents = {};
this.tasks = {};
this.connections = [];
this.systemStatus = { agents_online: 4, tasks_pending: 0, tasks_running: 0, uptime: '0h 0m' };
this._startTime = Date.now();
this._intervals = [];
this._initAgents();
this._initTasks();
this._startSimulation();
}
on(event, callback) {
if (!this.listeners[event]) this.listeners[event] = [];
this.listeners[event].push(callback);
}
emit(event, data) {
(this.listeners[event] || []).forEach(cb => cb(data));
}
send(message) {
const msg = typeof message === 'string' ? JSON.parse(message) : message;
if (msg.type === 'chat_message') {
this._handleChat(msg);
} else if (msg.type === 'task_action') {
this._handleTaskAction(msg);
}
}
_initAgents() {
AGENT_IDS.forEach(id => {
this.agents[id] = {
id,
...AGENT_DEFS[id],
state: 'idle',
current_task: null,
glow_intensity: 0.5,
uptime: '0h 0m',
last_action: new Date().toISOString(),
messages: [],
memories: [],
};
});
}
_initTasks() {
const statuses = ['completed', 'completed', 'in_progress', 'pending', 'pending'];
const priorities = ['high', 'normal', 'normal', 'normal', 'high'];
for (let i = 0; i < 5; i++) {
const task = {
task_id: uuid(),
agent_id: pick(AGENT_IDS),
title: TASK_TITLES[i],
status: statuses[i],
priority: priorities[i],
};
this.tasks[task.task_id] = task;
}
this._updateSystemStatus();
}
_updateSystemStatus() {
const tasks = Object.values(this.tasks);
const elapsed = Date.now() - this._startTime;
const hours = Math.floor(elapsed / 3600000);
const mins = Math.floor((elapsed % 3600000) / 60000);
this.systemStatus = {
agents_online: AGENT_IDS.filter(id => this.agents[id]).length,
tasks_pending: tasks.filter(t => t.status === 'pending').length,
tasks_running: tasks.filter(t => t.status === 'in_progress').length,
tasks_completed: tasks.filter(t => t.status === 'completed').length,
tasks_failed: tasks.filter(t => t.status === 'failed').length,
total_tasks: tasks.length,
uptime: `${hours}h ${mins}m`,
};
}
_startSimulation() {
// Agent state changes every 4-8 seconds
this._intervals.push(setInterval(() => {
const agentId = pick(AGENT_IDS);
const states = ['idle', 'working', 'working', 'waiting'];
const state = pick(states);
const agent = this.agents[agentId];
agent.state = state;
agent.glow_intensity = state === 'working' ? 0.9 : state === 'waiting' ? 0.6 : 0.4;
agent.current_task = state === 'working' ? pick(TASK_TITLES) : null;
agent.last_action = new Date().toISOString();
this.emit('message', {
type: 'agent_state',
agent_id: agentId,
state,
current_task: agent.current_task,
glow_intensity: agent.glow_intensity,
});
}, 4000 + Math.random() * 4000));
// New tasks every 8-15 seconds
this._intervals.push(setInterval(() => {
const task = {
task_id: uuid(),
agent_id: Math.random() > 0.3 ? pick(AGENT_IDS) : null,
title: pick(TASK_TITLES),
status: 'pending',
priority: Math.random() > 0.7 ? 'high' : 'normal',
};
this.tasks[task.task_id] = task;
this._updateSystemStatus();
this.emit('message', { type: 'task_created', ...task });
}, 8000 + Math.random() * 7000));
// Task status updates every 5-10 seconds
this._intervals.push(setInterval(() => {
const pendingTasks = Object.values(this.tasks).filter(t => t.status === 'pending' || t.status === 'in_progress');
if (pendingTasks.length === 0) return;
const task = pick(pendingTasks);
if (task.status === 'pending') {
task.status = 'in_progress';
task.agent_id = task.agent_id || pick(AGENT_IDS);
} else {
task.status = Math.random() > 0.15 ? 'completed' : 'failed';
}
this._updateSystemStatus();
this.emit('message', {
type: 'task_update',
task_id: task.task_id,
agent_id: task.agent_id,
title: task.title,
status: task.status,
priority: task.priority,
});
}, 5000 + Math.random() * 5000));
// Memory events every 6-12 seconds
this._intervals.push(setInterval(() => {
const agentId = pick(AGENT_IDS);
const content = pick(MEMORY_ENTRIES);
const entry = { timestamp: new Date().toISOString(), content };
this.agents[agentId].memories.unshift(entry);
if (this.agents[agentId].memories.length > 50) this.agents[agentId].memories.pop();
this.emit('message', {
type: 'memory_event',
agent_id: agentId,
content,
timestamp: entry.timestamp,
});
}, 6000 + Math.random() * 6000));
// Connection lines flicker every 3-6 seconds
this._intervals.push(setInterval(() => {
const a = pick(AGENT_IDS);
let b = pick(AGENT_IDS);
while (b === a) b = pick(AGENT_IDS);
const active = Math.random() > 0.3;
const connKey = [a, b].sort().join('-');
if (active) {
if (!this.connections.includes(connKey)) this.connections.push(connKey);
} else {
this.connections = this.connections.filter(c => c !== connKey);
}
this.emit('message', {
type: 'connection',
agent_id: a,
target_id: b,
active,
});
}, 3000 + Math.random() * 3000));
// System status broadcast every 5 seconds
this._intervals.push(setInterval(() => {
this._updateSystemStatus();
this.emit('message', { type: 'system_status', ...this.systemStatus });
}, 5000));
}
_handleChat(msg) {
const agentId = msg.agent_id;
const agent = this.agents[agentId];
if (!agent) return;
agent.messages.push({ role: 'user', content: msg.content });
// Simulate typing delay
setTimeout(() => {
this.emit('typing', { agent_id: agentId });
}, 200);
setTimeout(() => {
const response = pick(CANNED_RESPONSES[agentId] || CANNED_RESPONSES.timmy);
agent.messages.push({ role: 'assistant', content: response });
this.emit('message', {
type: 'agent_message',
agent_id: agentId,
role: 'assistant',
content: response,
});
}, 1000 + Math.random() * 1500);
}
_handleTaskAction(msg) {
const task = this.tasks[msg.task_id];
if (!task) return;
if (msg.action === 'approve') {
task.status = 'in_progress';
} else if (msg.action === 'veto') {
task.status = 'failed';
}
this._updateSystemStatus();
this.emit('message', {
type: 'task_update',
task_id: task.task_id,
agent_id: task.agent_id,
title: task.title,
status: task.status,
priority: task.priority,
});
}
getAgent(id) { return this.agents[id]; }
getAgentTasks(id) { return Object.values(this.tasks).filter(t => t.agent_id === id); }
getSystemStatus() { this._updateSystemStatus(); return this.systemStatus; }
dispose() {
this._intervals.forEach(clearInterval);
this._intervals = [];
this.listeners = {};
}
}
export { AGENT_DEFS, AGENT_IDS };

206
js/world.js Normal file
View File

@@ -0,0 +1,206 @@
// ===== World: Ground plane, grid, core pillar, environment =====
import * as THREE from 'three';
// Grid shader - animated green grid on the ground
const gridVertexShader = `
varying vec2 vUv;
varying vec3 vWorldPos;
void main() {
vUv = uv;
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vWorldPos = worldPos.xyz;
gl_Position = projectionMatrix * viewMatrix * worldPos;
}
`;
const gridFragmentShader = `
uniform float uTime;
uniform float uRadius;
varying vec2 vUv;
varying vec3 vWorldPos;
void main() {
vec2 p = vWorldPos.xz;
float dist = length(p);
// Circular falloff
float edge = smoothstep(uRadius, uRadius - 8.0, dist);
// Grid lines
float gridSize = 2.0;
vec2 grid = abs(fract(p / gridSize - 0.5) - 0.5) / fwidth(p / gridSize);
float line = min(grid.x, grid.y);
float gridAlpha = 1.0 - min(line, 1.0);
// Pulse wave from center
float pulse = sin(dist * 0.3 - uTime * 1.5) * 0.5 + 0.5;
pulse = smoothstep(0.3, 0.7, pulse);
// Sub-grid (finer lines)
float subGridSize = 0.5;
vec2 subGrid = abs(fract(p / subGridSize - 0.5) - 0.5) / fwidth(p / subGridSize);
float subLine = min(subGrid.x, subGrid.y);
float subGridAlpha = 1.0 - min(subLine, 1.0);
// Combine
float alpha = (gridAlpha * 0.5 + subGridAlpha * 0.08) * edge;
alpha += gridAlpha * edge * pulse * 0.2;
// Radial glow near center
float centerGlow = smoothstep(15.0, 0.0, dist) * 0.04;
alpha += centerGlow * edge;
// Green color with slight variation
vec3 color = mix(vec3(0.0, 0.4, 0.02), vec3(0.0, 1.0, 0.25), pulse * 0.5 + gridAlpha * 0.5);
gl_FragColor = vec4(color, alpha);
}
`;
export function createWorld(scene) {
const worldGroup = new THREE.Group();
// ---- Ground Plane with Grid Shader ----
const groundGeom = new THREE.PlaneGeometry(120, 120, 1, 1);
groundGeom.rotateX(-Math.PI / 2);
const gridMaterial = new THREE.ShaderMaterial({
vertexShader: gridVertexShader,
fragmentShader: gridFragmentShader,
uniforms: {
uTime: { value: 0 },
uRadius: { value: 50 },
},
transparent: true,
depthWrite: false,
side: THREE.DoubleSide,
});
const groundMesh = new THREE.Mesh(groundGeom, gridMaterial);
groundMesh.position.y = -0.01;
groundMesh.renderOrder = -1;
worldGroup.add(groundMesh);
// ---- Dark solid floor underneath ----
const floorGeom = new THREE.CircleGeometry(52, 64);
floorGeom.rotateX(-Math.PI / 2);
const floorMat = new THREE.MeshBasicMaterial({ color: 0x030503, transparent: true, opacity: 0.9 });
const floorMesh = new THREE.Mesh(floorGeom, floorMat);
floorMesh.position.y = -0.05;
worldGroup.add(floorMesh);
// ---- THE CORE — Central Pillar ----
const coreGroup = new THREE.Group();
coreGroup.name = 'core';
// Main pillar - octagonal prism
const pillarGeom = new THREE.CylinderGeometry(1.2, 1.5, 14, 8, 1);
const pillarMat = new THREE.MeshBasicMaterial({
color: 0x00ff41,
wireframe: true,
transparent: true,
opacity: 0.3,
});
const pillar = new THREE.Mesh(pillarGeom, pillarMat);
pillar.position.y = 7;
coreGroup.add(pillar);
// Inner glowing core
const innerGeom = new THREE.CylinderGeometry(0.6, 0.8, 12, 8, 1);
const innerMat = new THREE.MeshBasicMaterial({
color: 0x00ff41,
transparent: true,
opacity: 0.6,
});
const inner = new THREE.Mesh(innerGeom, innerMat);
inner.position.y = 7;
coreGroup.add(inner);
// Floating rings around core
for (let i = 0; i < 3; i++) {
const ringGeom = new THREE.TorusGeometry(2.2 + i * 0.6, 0.04, 8, 32);
const ringMat = new THREE.MeshBasicMaterial({
color: 0x00ff41,
transparent: true,
opacity: 0.4 - i * 0.1,
});
const ring = new THREE.Mesh(ringGeom, ringMat);
ring.position.y = 4 + i * 4;
ring.rotation.x = Math.PI / 2 + (i * 0.2);
ring.userData.ringIndex = i;
coreGroup.add(ring);
}
// Core base glow
const baseGlowGeom = new THREE.CylinderGeometry(2.5, 3, 0.3, 8, 1);
const baseGlowMat = new THREE.MeshBasicMaterial({
color: 0x00ff41,
transparent: true,
opacity: 0.15,
});
const baseGlow = new THREE.Mesh(baseGlowGeom, baseGlowMat);
baseGlow.position.y = 0.15;
coreGroup.add(baseGlow);
// Point light at core
const coreLight = new THREE.PointLight(0x00ff41, 2, 30);
coreLight.position.y = 7;
coreGroup.add(coreLight);
worldGroup.add(coreGroup);
// ---- Ambient lighting ----
const ambient = new THREE.AmbientLight(0x112211, 0.3);
worldGroup.add(ambient);
// Dim directional for slight form
const dirLight = new THREE.DirectionalLight(0x00ff41, 0.15);
dirLight.position.set(10, 20, 10);
worldGroup.add(dirLight);
// ---- Green fog ----
scene.fog = new THREE.FogExp2(0x020502, 0.012);
scene.add(worldGroup);
return {
worldGroup,
gridMaterial,
coreGroup,
coreLight,
update(time, delta) {
// Update grid pulse
gridMaterial.uniforms.uTime.value = time;
// Rotate core slowly
coreGroup.rotation.y = time * 0.15;
// Pulse core light
coreLight.intensity = 1.5 + Math.sin(time * 2) * 0.5;
// Animate rings
coreGroup.children.forEach(child => {
if (child.userData.ringIndex !== undefined) {
const i = child.userData.ringIndex;
child.rotation.z = time * (0.2 + i * 0.1);
child.position.y = 4 + i * 4 + Math.sin(time * 0.8 + i) * 0.3;
}
});
// Pulse inner core opacity
inner.material.opacity = 0.4 + Math.sin(time * 3) * 0.2;
},
dispose() {
worldGroup.traverse(obj => {
if (obj.geometry) obj.geometry.dispose();
if (obj.material) {
if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose());
else obj.material.dispose();
}
});
scene.remove(worldGroup);
}
};
}

592
style.css Normal file
View File

@@ -0,0 +1,592 @@
/* ===== THE MATRIX — SOVEREIGN AGENT WORLD ===== */
/* Matrix Green/Noir Cyberpunk Aesthetic */
:root {
--matrix-green: #00ff41;
--matrix-green-dim: #008f11;
--matrix-green-dark: #003b00;
--matrix-cyan: #00d4ff;
--matrix-bg: #050505;
--matrix-surface: rgba(0, 255, 65, 0.04);
--matrix-surface-solid: #0a0f0a;
--matrix-border: rgba(0, 255, 65, 0.2);
--matrix-border-bright: rgba(0, 255, 65, 0.45);
--matrix-text: #b0ffb0;
--matrix-text-dim: #4a7a4a;
--matrix-text-bright: #00ff41;
--matrix-danger: #ff3333;
--matrix-warning: #ff8c00;
--matrix-purple: #9d4edd;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
--panel-width: 360px;
--panel-blur: 20px;
--panel-radius: 4px;
--transition-panel: 350ms cubic-bezier(0.16, 1, 0.3, 1);
--transition-ui: 180ms cubic-bezier(0.16, 1, 0.3, 1);
}
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: var(--matrix-bg);
font-family: var(--font-mono);
color: var(--matrix-text);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
touch-action: none;
user-select: none;
-webkit-user-select: none;
}
canvas#matrix-canvas {
display: block;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
}
/* ===== FPS Counter ===== */
#fps-counter {
position: fixed;
top: 8px;
left: 8px;
z-index: 100;
font-family: var(--font-mono);
font-size: 11px;
line-height: 1.4;
color: var(--matrix-green-dim);
background: rgba(0, 0, 0, 0.5);
padding: 4px 8px;
border-radius: 2px;
pointer-events: none;
white-space: pre;
display: none;
}
#fps-counter.visible {
display: block;
}
/* ===== Panel Base ===== */
.panel {
position: fixed;
top: 0;
right: 0;
width: var(--panel-width);
height: 100%;
z-index: 50;
display: flex;
flex-direction: column;
background: rgba(5, 10, 5, 0.88);
backdrop-filter: blur(var(--panel-blur));
-webkit-backdrop-filter: blur(var(--panel-blur));
border-left: 1px solid var(--matrix-border-bright);
transform: translateX(0);
transition: transform var(--transition-panel);
overflow: hidden;
}
.panel.hidden {
transform: translateX(100%);
pointer-events: none;
}
/* Scanline overlay on panel */
.panel::before {
content: '';
position: absolute;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 255, 65, 0.015) 2px,
rgba(0, 255, 65, 0.015) 4px
);
pointer-events: none;
z-index: 1;
}
.panel > * {
position: relative;
z-index: 2;
}
/* ===== Panel Header ===== */
.panel-header {
padding: 16px 16px 12px;
border-bottom: 1px solid var(--matrix-border);
flex-shrink: 0;
}
.panel-agent-name {
font-size: 18px;
font-weight: 700;
color: var(--matrix-text-bright);
letter-spacing: 2px;
text-transform: uppercase;
text-shadow: 0 0 10px rgba(0, 255, 65, 0.5);
}
.panel-agent-role {
font-size: 11px;
color: var(--matrix-text-dim);
margin-top: 2px;
letter-spacing: 1px;
}
.panel-close {
position: absolute;
top: 12px;
right: 12px;
width: 28px;
height: 28px;
background: transparent;
border: 1px solid var(--matrix-border);
border-radius: 2px;
color: var(--matrix-text-dim);
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition-ui);
font-family: var(--font-mono);
}
.panel-close:hover, .panel-close:active {
color: var(--matrix-text-bright);
border-color: var(--matrix-border-bright);
background: rgba(0, 255, 65, 0.08);
}
/* ===== Tabs ===== */
.panel-tabs {
display: flex;
border-bottom: 1px solid var(--matrix-border);
flex-shrink: 0;
}
.tab {
flex: 1;
padding: 10px 8px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: var(--matrix-text-dim);
font-family: var(--font-mono);
font-size: 11px;
font-weight: 500;
letter-spacing: 1px;
text-transform: uppercase;
cursor: pointer;
transition: all var(--transition-ui);
}
.tab:hover {
color: var(--matrix-text);
background: rgba(0, 255, 65, 0.04);
}
.tab.active {
color: var(--matrix-text-bright);
border-bottom-color: var(--matrix-green);
text-shadow: 0 0 8px rgba(0, 255, 65, 0.4);
}
/* ===== Panel Content ===== */
.panel-content {
flex: 1;
overflow: hidden;
position: relative;
}
.tab-content {
display: none;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.tab-content.active {
display: flex;
}
/* ===== Chat ===== */
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 12px 16px;
-webkit-overflow-scrolling: touch;
}
.chat-messages::-webkit-scrollbar {
width: 4px;
}
.chat-messages::-webkit-scrollbar-track {
background: transparent;
}
.chat-messages::-webkit-scrollbar-thumb {
background: var(--matrix-green-dark);
border-radius: 2px;
}
.chat-msg {
margin-bottom: 12px;
padding: 8px 10px;
border-radius: 3px;
font-size: 12px;
line-height: 1.6;
word-break: break-word;
}
.chat-msg.user {
background: rgba(0, 212, 255, 0.08);
border-left: 2px solid var(--matrix-cyan);
color: #b0eeff;
}
.chat-msg.assistant {
background: rgba(0, 255, 65, 0.05);
border-left: 2px solid var(--matrix-green-dim);
color: var(--matrix-text);
}
.chat-msg .msg-role {
font-size: 10px;
font-weight: 600;
letter-spacing: 1px;
text-transform: uppercase;
margin-bottom: 4px;
opacity: 0.6;
}
.chat-input-area {
flex-shrink: 0;
padding: 8px 12px 12px;
border-top: 1px solid var(--matrix-border);
}
.chat-input-row {
display: flex;
gap: 6px;
}
#chat-input {
flex: 1;
background: rgba(0, 255, 65, 0.04);
border: 1px solid var(--matrix-border);
border-radius: 3px;
padding: 10px 12px;
color: var(--matrix-text-bright);
font-family: var(--font-mono);
font-size: 12px;
outline: none;
transition: border-color var(--transition-ui);
}
#chat-input:focus {
border-color: var(--matrix-green);
box-shadow: 0 0 8px rgba(0, 255, 65, 0.15);
}
#chat-input::placeholder {
color: var(--matrix-text-dim);
}
.btn-send {
width: 40px;
background: rgba(0, 255, 65, 0.1);
border: 1px solid var(--matrix-border);
border-radius: 3px;
color: var(--matrix-green);
font-size: 14px;
cursor: pointer;
transition: all var(--transition-ui);
font-family: var(--font-mono);
}
.btn-send:hover, .btn-send:active {
background: rgba(0, 255, 65, 0.2);
border-color: var(--matrix-green);
}
/* Typing indicator */
.typing-indicator {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 0 8px;
height: 24px;
}
.typing-indicator.hidden {
display: none;
}
.typing-indicator span {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--matrix-green-dim);
animation: typingDot 1.4s infinite both;
}
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typingDot {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1.2); }
}
/* ===== Status Tab ===== */
.status-grid {
padding: 16px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.status-row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 16px;
padding: 8px 0;
border-bottom: 1px solid rgba(0, 255, 65, 0.06);
font-size: 12px;
}
.status-key {
color: var(--matrix-text-dim);
text-transform: uppercase;
letter-spacing: 1px;
font-size: 10px;
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
}
.status-value {
color: var(--matrix-text-bright);
font-weight: 500;
text-align: right;
word-break: break-word;
}
.status-value.state-working {
color: var(--matrix-green);
text-shadow: 0 0 6px rgba(0, 255, 65, 0.4);
}
.status-value.state-idle {
color: var(--matrix-text-dim);
}
.status-value.state-waiting {
color: var(--matrix-warning);
}
/* ===== Tasks Tab ===== */
.tasks-list {
padding: 12px 16px;
overflow-y: auto;
flex: 1;
-webkit-overflow-scrolling: touch;
}
.task-item {
padding: 10px 12px;
margin-bottom: 8px;
background: rgba(0, 255, 65, 0.03);
border: 1px solid var(--matrix-border);
border-radius: 3px;
}
.task-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.task-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.task-status-dot.pending { background: #ffffff; }
.task-status-dot.in_progress, .task-status-dot.in-progress { background: var(--matrix-warning); box-shadow: 0 0 6px rgba(255, 140, 0, 0.5); }
.task-status-dot.completed { background: var(--matrix-green); box-shadow: 0 0 6px rgba(0, 255, 65, 0.5); }
.task-status-dot.failed { background: var(--matrix-danger); box-shadow: 0 0 6px rgba(255, 51, 51, 0.5); }
.task-title {
font-size: 12px;
font-weight: 500;
color: var(--matrix-text);
flex: 1;
}
.task-priority {
font-size: 9px;
font-weight: 600;
letter-spacing: 1px;
text-transform: uppercase;
padding: 2px 6px;
border-radius: 2px;
background: rgba(0, 255, 65, 0.08);
color: var(--matrix-text-dim);
}
.task-priority.high {
background: rgba(255, 51, 51, 0.15);
color: var(--matrix-danger);
}
.task-priority.normal {
background: rgba(0, 255, 65, 0.08);
color: var(--matrix-text-dim);
}
.task-actions {
display: flex;
gap: 6px;
margin-top: 8px;
}
.task-btn {
flex: 1;
padding: 6px 8px;
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
letter-spacing: 1px;
text-transform: uppercase;
border: 1px solid;
border-radius: 2px;
cursor: pointer;
transition: all var(--transition-ui);
background: transparent;
}
.task-btn.approve {
border-color: rgba(0, 255, 65, 0.3);
color: var(--matrix-green);
}
.task-btn.approve:hover {
background: rgba(0, 255, 65, 0.15);
border-color: var(--matrix-green);
}
.task-btn.veto {
border-color: rgba(255, 51, 51, 0.3);
color: var(--matrix-danger);
}
.task-btn.veto:hover {
background: rgba(255, 51, 51, 0.15);
border-color: var(--matrix-danger);
}
/* ===== Memory Tab ===== */
.memory-list {
padding: 12px 16px;
overflow-y: auto;
flex: 1;
-webkit-overflow-scrolling: touch;
}
.memory-entry {
padding: 8px 10px;
margin-bottom: 6px;
border-left: 2px solid var(--matrix-green-dark);
font-size: 11px;
line-height: 1.5;
color: var(--matrix-text);
}
.memory-timestamp {
font-size: 9px;
color: var(--matrix-text-dim);
letter-spacing: 1px;
margin-bottom: 2px;
}
.memory-content {
color: var(--matrix-text);
opacity: 0.85;
}
/* ===== Attribution ===== */
.attribution {
position: fixed;
bottom: 6px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
pointer-events: auto;
}
.attribution a {
font-family: var(--font-mono);
font-size: 10px;
color: var(--matrix-green-dim);
text-decoration: none;
letter-spacing: 1px;
opacity: 0.7;
transition: opacity var(--transition-ui);
text-shadow: 0 0 4px rgba(0, 143, 17, 0.3);
}
.attribution a:hover {
opacity: 1;
color: var(--matrix-green-dim);
}
/* ===== Mobile / iPad ===== */
@media (max-width: 768px) {
.panel {
width: 100%;
height: 60%;
top: auto;
bottom: 0;
right: 0;
border-left: none;
border-top: 1px solid var(--matrix-border-bright);
border-radius: 12px 12px 0 0;
}
.panel.hidden {
transform: translateY(100%);
}
.panel-agent-name {
font-size: 15px;
}
.panel-tabs .tab {
font-size: 10px;
padding: 8px 4px;
}
}
@media (max-width: 480px) {
.panel {
height: 70%;
}
}