diff --git a/artifacts/api-server/src/lib/world-state.ts b/artifacts/api-server/src/lib/world-state.ts index 5e869bc..809f985 100644 --- a/artifacts/api-server/src/lib/world-state.ts +++ b/artifacts/api-server/src/lib/world-state.ts @@ -16,7 +16,7 @@ const DEFAULT_TIMMY: TimmyState = { const _state: WorldState = { timmyState: { ...DEFAULT_TIMMY }, - agentStates: { alpha: "idle", beta: "idle", gamma: "idle", delta: "idle" }, + agentStates: { alpha: "idle", beta: "idle", gamma: "idle", delta: "idle", kimi: "idle", perplexity: "idle" }, updatedAt: new Date().toISOString(), }; diff --git a/the-matrix/js/agent-defs.js b/the-matrix/js/agent-defs.js index d5572a3..b0f1225 100644 --- a/the-matrix/js/agent-defs.js +++ b/the-matrix/js/agent-defs.js @@ -9,14 +9,18 @@ * label — display name shown in the 3D HUD and chat panel * color — hex integer (0xRRGGBB) used for Three.js materials and lights * role — human-readable role string shown under the label sprite - * direction — cardinal facing direction (for future mesh orientation use) - * x, z — world-space position on the horizontal plane (y is always 0) + * direction — cardinal facing direction (for future mesh orientation use) + * x, z — world-space position on the horizontal plane (y is always 0) + * specialization — optional skill label shown in HUD inspect popup + * external — true for external agents (Kimi, Perplexity) that render 3D bodies */ export const AGENT_DEFS = [ { id: 'alpha', label: 'ALPHA', color: 0x00ff88, role: 'orchestrator', direction: 'north', x: 0, z: -6 }, { id: 'beta', label: 'BETA', color: 0x00aaff, role: 'worker', direction: 'east', x: 6, z: 0 }, { id: 'gamma', label: 'GAMMA', color: 0xff6600, role: 'validator', direction: 'south', x: 0, z: 6 }, { id: 'delta', label: 'DELTA', color: 0xaa00ff, role: 'monitor', direction: 'west', x: -6, z: 0 }, + { id: 'kimi', label: 'KIMI', color: 0x00d4ff, role: 'analyst', specialization: 'Long Context Analysis', external: true, direction: 'northwest', x: -9, z: -5 }, + { id: 'perplexity', label: 'PERPLEXITY', color: 0xff4488, role: 'researcher', specialization: 'Real-time Research', external: true, direction: 'northeast', x: 9, z: -5 }, ]; /** diff --git a/the-matrix/js/agents.js b/the-matrix/js/agents.js index d5c1721..bcd223a 100644 --- a/the-matrix/js/agents.js +++ b/the-matrix/js/agents.js @@ -1,6 +1,10 @@ import * as THREE from 'three'; import { AGENT_DEFS } from './agent-defs.js'; +// ── External agent 3D bodies (Kimi, Perplexity) ────────────────────────────── +const _extAgents = []; +const _extAgentStates = {}; // id -> state string + const TIMMY_POS = new THREE.Vector3(0, 0, -2); export const TIMMY_WORLD_POS = TIMMY_POS.clone(); const CRYSTAL_POS = new THREE.Vector3(0.6, 1.15, -4.1); @@ -100,6 +104,15 @@ function _pickMouthGeo(smileAmount) { export function initAgents(sceneRef) { scene = sceneRef; timmy = buildTimmy(scene); + + // Build 3D bodies for external agents (Kimi, Perplexity) + for (const def of AGENT_DEFS.filter(d => d.external)) { + const body = buildExternalAgentBody(def); + body.group.position.set(def.x, 0, def.z); + scene.add(body.group); + _extAgents.push(body); + _extAgentStates[def.id] = 'idle'; + } } function buildTimmy(sc) { @@ -537,6 +550,52 @@ export function updateAgents(time) { if (timmy.mouth.geometry !== nextMouthGeo) { timmy.mouth.geometry = nextMouthGeo; } + + // ── Animate external agents (Kimi, Perplexity) ──────────────────────────── + for (const agent of _extAgents) { + const state = _extAgentStates[agent.id] || 'idle'; + const active = state !== 'idle'; + + // Light intensity and orbit speed scale with activity + const lightBase = active ? 1.6 : 0.4; + const lightPulse = Math.sin(t * (active ? 3.2 : 1.1)) * (active ? 0.5 : 0.15); + agent.light.intensity = lightBase + lightPulse; + + const speedMult = active ? 2.2 : 0.55; + + // Animate orbiters + for (const orb of agent.orbiters) { + const angle = t * orb.speed * speedMult + orb.phase; + orb.mesh.position.set( + Math.cos(angle) * orb.radius, + orb.yOffset, + Math.sin(angle) * orb.radius, + ); + } + + // Gentle whole-group rotation for Kimi's pillar, pulsing Y bob for Perplexity's core + if (agent.id === 'kimi') { + agent.group.rotation.y += 0.003 * speedMult; + // Emissive pulse on pillar + const pillarMesh = agent.meshes[0]; + if (pillarMesh?.material) { + pillarMesh.material.emissiveIntensity = active + ? 0.25 + Math.sin(t * 2.8) * 0.12 + : 0.08 + Math.sin(t * 0.9) * 0.03; + } + } else if (agent.id === 'perplexity') { + // Slowly spin the icosahedron core and shell + const core = agent.meshes[0]; + const shell = agent.meshes[1]; + if (core) { core.rotation.y += 0.008 * speedMult; core.rotation.x += 0.004 * speedMult; } + if (shell) { shell.rotation.y -= 0.005 * speedMult; } + if (core?.material) { + core.material.emissiveIntensity = active + ? 0.30 + Math.sin(t * 3.5) * 0.15 + : 0.10 + Math.sin(t * 0.8) * 0.04; + } + } + } } // ── setFaceEmotion — public API ─────────────────────────────────────────────── @@ -807,6 +866,7 @@ export function getCameraShakeStrength() { export function setAgentState(agentId, state) { if (agentId in agentStates) agentStates[agentId] = state; + if (agentId in _extAgentStates) _extAgentStates[agentId] = state; } export function setSpeechBubble(text) { @@ -868,6 +928,7 @@ export function applyAgentStates(snapshot) { if (!snapshot) return; for (const [k, v] of Object.entries(snapshot)) { if (k in agentStates) agentStates[k] = v; + if (k in _extAgentStates) _extAgentStates[k] = v; } } @@ -875,6 +936,115 @@ export function getAgentDefs() { return [{ id: 'timmy', label: 'TIMMY', role: 'wizard', color: 0x5599ff, state: deriveTimmyState() }]; } +// ── External agent 3D body builder ──────────────────────────────────────────── +function buildExternalAgentBody(def) { + const group = new THREE.Group(); + const col = def.color; + const meshes = []; // for disposal + const orbiters = []; // { mesh, radius, speed, phase, yOffset } + + if (def.id === 'kimi') { + // Kimi: tall tapered obelisk/pillar (CylinderGeometry) + 3 flat ring-discs orbiting it + const pillarMat = new THREE.MeshStandardMaterial({ + color: col, + emissive: col, + emissiveIntensity: 0.15, + roughness: 0.4, + metalness: 0.6, + }); + const pillar = new THREE.Mesh(new THREE.CylinderGeometry(0.10, 0.28, 2.2, 6), pillarMat); + pillar.position.y = 1.1; + pillar.castShadow = true; + group.add(pillar); + meshes.push(pillar); + + // 3 flat torus-ring discs at different heights, orbiting the pillar + const ringMat = new THREE.MeshStandardMaterial({ + color: col, + emissive: col, + emissiveIntensity: 0.35, + roughness: 0.3, + metalness: 0.7, + transparent: true, + opacity: 0.82, + }); + const ringHeights = [0.6, 1.1, 1.7]; + const ringRadii = [0.55, 0.45, 0.38]; + const ringPhases = [0, Math.PI * 0.66, Math.PI * 1.33]; + const ringSpeed = [0.55, 0.72, 0.48]; + for (let i = 0; i < 3; i++) { + const ring = new THREE.Mesh( + new THREE.TorusGeometry(0.18, 0.028, 6, 20), + ringMat.clone() + ); + ring.rotation.x = Math.PI / 2; + group.add(ring); + meshes.push(ring); + orbiters.push({ mesh: ring, radius: ringRadii[i], speed: ringSpeed[i], phase: ringPhases[i], yOffset: ringHeights[i] }); + } + + } else if (def.id === 'perplexity') { + // Perplexity: icosahedron core + 4 small orbiting dot-spheres + const coreGeo = new THREE.IcosahedronGeometry(0.34, 1); + const coreMat = new THREE.MeshStandardMaterial({ + color: col, + emissive: col, + emissiveIntensity: 0.18, + roughness: 0.25, + metalness: 0.55, + wireframe: false, + }); + const core = new THREE.Mesh(coreGeo, coreMat); + core.position.y = 1.1; + core.castShadow = true; + group.add(core); + meshes.push(core); + + // Thin wireframe shell just slightly larger + const shellMat = new THREE.MeshBasicMaterial({ color: col, wireframe: true, transparent: true, opacity: 0.22 }); + const shell = new THREE.Mesh(new THREE.IcosahedronGeometry(0.46, 1), shellMat); + shell.position.y = 1.1; + group.add(shell); + meshes.push(shell); + + // 4 small orbiting spheres + const dotMat = new THREE.MeshStandardMaterial({ + color: col, + emissive: col, + emissiveIntensity: 0.5, + roughness: 0.3, + metalness: 0.4, + }); + const dotCount = 4; + for (let i = 0; i < dotCount; i++) { + const dot = new THREE.Mesh(new THREE.SphereGeometry(0.07, 8, 8), dotMat.clone()); + group.add(dot); + meshes.push(dot); + orbiters.push({ + mesh: dot, + radius: 0.58, + speed: 0.9 + i * 0.15, + phase: (i / dotCount) * Math.PI * 2, + yOffset: 1.1 + Math.sin((i / dotCount) * Math.PI * 2) * 0.22, + }); + } + } + + // Shared point light for glow + const light = new THREE.PointLight(col, 0.6, 4.5); + light.position.y = 1.1; + group.add(light); + + // Small base platform + const baseMat = new THREE.MeshStandardMaterial({ color: col, emissive: col, emissiveIntensity: 0.08, roughness: 0.8, metalness: 0.3 }); + const base = new THREE.Mesh(new THREE.CylinderGeometry(0.32, 0.38, 0.08, 8), baseMat); + base.position.y = 0.04; + group.add(base); + meshes.push(base); + + return { id: def.id, group, meshes, orbiters, light, def }; +} + export function disposeAgents() { if (!timmy) return; [timmy.robe, timmy.head, timmy.hat, timmy.cb, timmy.pip].forEach(m => { @@ -889,5 +1059,17 @@ export function disposeAgents() { timmy.bubbleTex?.dispose(); timmy.bubbleMat?.dispose(); timmy = null; + + // Dispose external agent bodies + for (const agent of _extAgents) { + for (const mesh of agent.meshes) { + mesh.geometry?.dispose(); + if (Array.isArray(mesh.material)) mesh.material.forEach(m => m.dispose()); + else mesh.material?.dispose(); + } + scene?.remove(agent.group); + } + _extAgents.length = 0; + scene = null; } diff --git a/the-matrix/js/hud-labels.js b/the-matrix/js/hud-labels.js index 8e1432a..ad1a7f9 100644 --- a/the-matrix/js/hud-labels.js +++ b/the-matrix/js/hud-labels.js @@ -19,7 +19,8 @@ let _camera = null; let _labels = []; // { el, worldPos: THREE.Vector3, id } // ── State cache (updated from WS) ──────────────────────────────────────────── -const _states = {}; +const _states = {}; +const _lastTasks = {}; // id -> last task summary string // ── Inspect popup ───────────────────────────────────────────────────────────── let _inspectEl = null; @@ -44,7 +45,7 @@ export function initHudLabels(camera, agentDefs, timmyWorldPos) { for (const def of agentDefs) { const col = colorToCss(def.color); const pos = new THREE.Vector3(def.x, 2.8, def.z); - _labels.push(_makeLabel(container, def.id, def.label, def.role, col, pos)); + _labels.push(_makeLabel(container, def.id, def.label, def.role, col, pos, def.specialization)); _states[def.id] = 'idle'; } @@ -65,7 +66,7 @@ export function initHudLabels(camera, agentDefs, timmyWorldPos) { document.body.appendChild(_inspectEl); } -function _makeLabel(container, id, name, role, color, worldPos) { +function _makeLabel(container, id, name, role, color, worldPos, specialization) { const el = document.createElement('div'); el.className = 'ar-label'; el.dataset.id = id; @@ -97,7 +98,7 @@ function _makeLabel(container, id, name, role, color, worldPos) { `; container.appendChild(el); - return { el, worldPos, id, color }; + return { el, worldPos, id, color, specialization: specialization || null }; } export function setLabelState(id, state) { @@ -111,18 +112,31 @@ export function setLabelState(id, state) { if (dot) dot.style.animation = pulse ? 'ar-pulse 1s ease-in-out infinite' : ''; } +export function setAgentLastTask(id, taskSummary) { + _lastTasks[id] = taskSummary; +} + export function showInspectPopup(id, screenX, screenY) { if (!_inspectEl) return; const entry = _labels.find(l => l.id === id); if (!entry) return; - const state = _states[id] || 'idle'; - const uptime = Math.floor(performance.now() / 1000); + const state = _states[id] || 'idle'; + const uptime = Math.floor(performance.now() / 1000); + const specLine = entry.specialization + ? `
spec   : ${entry.specialization}
` + : ''; + const lastTask = _lastTasks[id]; + const taskLine = lastTask + ? `
last   : ${lastTask}
` + : ''; _inspectEl.innerHTML = `
${id.toUpperCase()}
state  : ${state}
+ ${specLine} + ${taskLine}
uptime : ${uptime}s
network: connected
`; diff --git a/the-matrix/js/websocket.js b/the-matrix/js/websocket.js index ad1d412..fb52dd7 100644 --- a/the-matrix/js/websocket.js +++ b/the-matrix/js/websocket.js @@ -1,7 +1,7 @@ import { setAgentState, setSpeechBubble, applyAgentStates, setMood } from './agents.js'; import { appendSystemMessage, appendDebateMessage, showCostTicker, updateCostTicker } from './ui.js'; import { sentiment } from './edge-worker-client.js'; -import { setLabelState } from './hud-labels.js'; +import { setLabelState, setAgentLastTask } from './hud-labels.js'; function resolveWsUrl() { const explicit = import.meta.env.VITE_WS_URL; @@ -103,6 +103,10 @@ function handleMessage(msg) { if (msg.agentId) { setAgentState(msg.agentId, 'idle'); setLabelState(msg.agentId, 'idle'); + if (msg.agentId === 'kimi' || msg.agentId === 'perplexity') { + const summary = msg.summary || msg.result || `job ${(msg.jobId || '').slice(0, 8)}`; + setAgentLastTask(msg.agentId, summary); + } } appendSystemMessage(`job ${(msg.jobId || '').slice(0, 8)} complete`); break;