From fdfae19956abca7f59d5a4f76095b4acb4ccbac9 Mon Sep 17 00:00:00 2001 From: Perplexity Computer Date: Wed, 18 Mar 2026 18:32:47 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20The=20Matrix=20=E2=80=94=20Sovereign=20?= =?UTF-8?q?Agent=20World?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- PROTOCOL.md | 226 ++++++++++++++++++ README.md | 121 ++++++++++ index.html | 109 +++++++++ js/agents.js | 524 ++++++++++++++++++++++++++++++++++++++++ js/effects.js | 283 ++++++++++++++++++++++ js/interaction.js | 147 ++++++++++++ js/main.js | 199 ++++++++++++++++ js/ui.js | 314 ++++++++++++++++++++++++ js/websocket.js | 322 +++++++++++++++++++++++++ js/world.js | 206 ++++++++++++++++ style.css | 592 ++++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 3043 insertions(+) create mode 100644 PROTOCOL.md create mode 100644 README.md create mode 100644 index.html create mode 100644 js/agents.js create mode 100644 js/effects.js create mode 100644 js/interaction.js create mode 100644 js/main.js create mode 100644 js/ui.js create mode 100644 js/websocket.js create mode 100644 js/world.js create mode 100644 style.css diff --git a/PROTOCOL.md b/PROTOCOL.md new file mode 100644 index 0000000..e516d1a --- /dev/null +++ b/PROTOCOL.md @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fdda324 --- /dev/null +++ b/README.md @@ -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 diff --git a/index.html b/index.html new file mode 100644 index 0000000..28cc19b --- /dev/null +++ b/index.html @@ -0,0 +1,109 @@ + + + + + + + + + + + + + +The Matrix — Sovereign Agent World + + + + + + + + + + + + + + +
+ + + + + + + + + + + + diff --git a/js/agents.js b/js/agents.js new file mode 100644 index 0000000..e3b8684 --- /dev/null +++ b/js/agents.js @@ -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 }; diff --git a/js/effects.js b/js/effects.js new file mode 100644 index 0000000..05b0a97 --- /dev/null +++ b/js/effects.js @@ -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(); + } + }; +} diff --git a/js/interaction.js b/js/interaction.js new file mode 100644 index 0000000..b204010 --- /dev/null +++ b/js/interaction.js @@ -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(); + } +} diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..dcce2ff --- /dev/null +++ b/js/main.js @@ -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(); +}); diff --git a/js/ui.js b/js/ui.js new file mode 100644 index 0000000..19813ac --- /dev/null +++ b/js/ui.js @@ -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 `
+ ${key} + ${value} +
`; + }).join(''); + } + + // ===== Tasks ===== + _renderTasks(agentId) { + const tasks = this.ws.getAgentTasks(agentId); + if (!tasks.length) { + this.tasksList.innerHTML = '
No tasks assigned
'; + return; + } + + this.tasksList.innerHTML = tasks.map(task => ` +
+
+ + ${task.title} + ${task.priority} +
+
Status: ${task.status}
+ ${task.status === 'pending' || task.status === 'in_progress' ? ` +
+ + +
+ ` : ''} +
+ `).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 = '
No memory entries yet
'; + return; + } + + this.memoryList.innerHTML = agent.memories.slice(0, 30).map(entry => ` +
+
${this._formatTime(entry.timestamp)}
+
${entry.content}
+
+ `).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]) => ` +
+ ${key} + ${value} +
+ `).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; + } + } +} diff --git a/js/websocket.js b/js/websocket.js new file mode 100644 index 0000000..99e6514 --- /dev/null +++ b/js/websocket.js @@ -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 }; diff --git a/js/world.js b/js/world.js new file mode 100644 index 0000000..df9e03d --- /dev/null +++ b/js/world.js @@ -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); + } + }; +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..1d6775f --- /dev/null +++ b/style.css @@ -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%; + } +}