Compare commits
1 Commits
v0-golden
...
google/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f26f07bf4 |
514
app.js
514
app.js
@@ -45,6 +45,18 @@ let chatOpen = true;
|
||||
let loadProgress = 0;
|
||||
let performanceTier = 'high';
|
||||
|
||||
// ═══ HERMES WS STATE ═══
|
||||
let hermesWs = null;
|
||||
let wsReconnectTimer = null;
|
||||
let wsConnected = false;
|
||||
let recentToolOutputs = [];
|
||||
let workshopPanelCtx = null;
|
||||
let workshopPanelTexture = null;
|
||||
let workshopPanelCanvas = null;
|
||||
let workshopScanMat = null;
|
||||
let workshopPanelRefreshTimer = 0;
|
||||
let lastFocusedPortal = null;
|
||||
|
||||
// ═══ NAVIGATION SYSTEM ═══
|
||||
const NAV_MODES = ['walk', 'orbit', 'fly'];
|
||||
let navModeIdx = 0;
|
||||
@@ -124,8 +136,12 @@ async function init() {
|
||||
createThoughtStream();
|
||||
createHarnessPulse();
|
||||
createSessionPowerMeter();
|
||||
createWorkshopTerminal();
|
||||
updateLoad(90);
|
||||
|
||||
loadSession();
|
||||
connectHermes();
|
||||
|
||||
composer = new EffectComposer(renderer);
|
||||
composer.addPass(new RenderPass(scene, camera));
|
||||
const bloom = new UnrealBloomPass(
|
||||
@@ -352,6 +368,91 @@ function createBatcaveTerminal() {
|
||||
scene.add(terminalGroup);
|
||||
}
|
||||
|
||||
// ═══ WORKSHOP TERMINAL ═══
|
||||
function createWorkshopTerminal() {
|
||||
const w = 6, h = 4;
|
||||
const group = new THREE.Group();
|
||||
group.position.set(-14, 3, 0);
|
||||
group.rotation.y = Math.PI / 4;
|
||||
|
||||
workshopPanelCanvas = document.createElement('canvas');
|
||||
workshopPanelCanvas.width = 1024;
|
||||
workshopPanelCanvas.height = 512;
|
||||
workshopPanelCtx = workshopPanelCanvas.getContext('2d');
|
||||
|
||||
workshopPanelTexture = new THREE.CanvasTexture(workshopPanelCanvas);
|
||||
workshopPanelTexture.minFilter = THREE.LinearFilter;
|
||||
|
||||
const panelGeo = new THREE.PlaneGeometry(w, h);
|
||||
const panelMat = new THREE.MeshBasicMaterial({
|
||||
map: workshopPanelTexture,
|
||||
transparent: true,
|
||||
opacity: 0.9,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
const panel = new THREE.Mesh(panelGeo, panelMat);
|
||||
group.add(panel);
|
||||
|
||||
const scanGeo = new THREE.PlaneGeometry(w + 0.1, h + 0.1);
|
||||
workshopScanMat = new THREE.ShaderMaterial({
|
||||
transparent: true,
|
||||
uniforms: { uTime: { value: 0 } },
|
||||
vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
|
||||
fragmentShader: `
|
||||
uniform float uTime;
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
float scan = sin(vUv.y * 200.0 + uTime * 10.0) * 0.05;
|
||||
float noise = fract(sin(dot(vUv, vec2(12.9898, 78.233))) * 43758.5453) * 0.05;
|
||||
gl_FragColor = vec4(0.0, 0.1, 0.2, scan + noise);
|
||||
}
|
||||
`
|
||||
});
|
||||
const scan = new THREE.Mesh(scanGeo, workshopScanMat);
|
||||
scan.position.z = 0.01;
|
||||
group.add(scan);
|
||||
|
||||
scene.add(group);
|
||||
refreshWorkshopPanel();
|
||||
}
|
||||
|
||||
function refreshWorkshopPanel() {
|
||||
if (!workshopPanelCtx) return;
|
||||
const ctx = workshopPanelCtx;
|
||||
const w = 1024, h = 512;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.fillStyle = 'rgba(10, 15, 40, 0.8)';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
ctx.fillStyle = '#4af0c0';
|
||||
ctx.font = 'bold 40px "Orbitron", sans-serif';
|
||||
ctx.fillText('WORKSHOP TERMINAL v1.0', 40, 60);
|
||||
ctx.fillRect(40, 80, 944, 4);
|
||||
|
||||
ctx.font = '24px "JetBrains Mono", monospace';
|
||||
ctx.fillStyle = wsConnected ? '#4af0c0' : '#ff4466';
|
||||
ctx.fillText(`HERMES STATUS: ${wsConnected ? 'ONLINE' : 'OFFLINE'}`, 40, 120);
|
||||
|
||||
ctx.fillStyle = '#7b5cff';
|
||||
const contextName = activePortal ? activePortal.name.toUpperCase() : 'NEXUS CORE';
|
||||
ctx.fillText(`CONTEXT: ${contextName}`, 40, 160);
|
||||
|
||||
ctx.fillStyle = '#a0b8d0';
|
||||
ctx.font = 'bold 20px "Orbitron", sans-serif';
|
||||
ctx.fillText('TOOL OUTPUT STREAM', 40, 220);
|
||||
ctx.fillRect(40, 230, 400, 2);
|
||||
|
||||
ctx.font = '16px "JetBrains Mono", monospace';
|
||||
recentToolOutputs.slice(-10).forEach((out, i) => {
|
||||
ctx.fillStyle = out.type === 'call' ? '#ffd700' : '#4af0c0';
|
||||
const text = `[${out.agent}] ${out.content.substring(0, 80)}${out.content.length > 80 ? '...' : ''}`;
|
||||
ctx.fillText(text, 40, 260 + i * 24);
|
||||
});
|
||||
|
||||
workshopPanelTexture.needsUpdate = true;
|
||||
}
|
||||
|
||||
function createTerminalPanel(parent, x, y, rot, title, color, lines) {
|
||||
const w = 2.8, h = 3.5;
|
||||
const group = new THREE.Group();
|
||||
@@ -438,76 +539,6 @@ function createTerminalPanel(parent, x, y, rot, title, color, lines) {
|
||||
batcaveTerminals.push({ group, scanMat, borderMat });
|
||||
}
|
||||
|
||||
// ═══ AGENT IDLE BEHAVIOR SYSTEM ═══
|
||||
const AGENT_STATES = { IDLE: 'IDLE', PACING: 'PACING', LOOKING: 'LOOKING', READING: 'READING' };
|
||||
const ACTIVITY_STATES = { NONE: 'NONE', WAITING: 'WAITING', THINKING: 'THINKING', PROCESSING: 'PROCESSING' };
|
||||
|
||||
function createActivityIndicator(color) {
|
||||
const group = new THREE.Group();
|
||||
group.position.y = 4.2;
|
||||
group.visible = false;
|
||||
|
||||
// WAITING — pulsing sphere
|
||||
const waitGeo = new THREE.SphereGeometry(0.18, 16, 16);
|
||||
const waitMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.85 });
|
||||
const waitMesh = new THREE.Mesh(waitGeo, waitMat);
|
||||
waitMesh.name = 'indicator_waiting';
|
||||
waitMesh.visible = false;
|
||||
group.add(waitMesh);
|
||||
|
||||
// THINKING — wireframe octahedron
|
||||
const thinkGeo = new THREE.OctahedronGeometry(0.2, 0);
|
||||
const thinkMat = new THREE.MeshBasicMaterial({ color, wireframe: true });
|
||||
const thinkMesh = new THREE.Mesh(thinkGeo, thinkMat);
|
||||
thinkMesh.name = 'indicator_thinking';
|
||||
thinkMesh.visible = false;
|
||||
group.add(thinkMesh);
|
||||
|
||||
// PROCESSING — spinning torus ring
|
||||
const procGeo = new THREE.TorusGeometry(0.18, 0.04, 8, 32);
|
||||
const procMat = new THREE.MeshBasicMaterial({ color });
|
||||
const procMesh = new THREE.Mesh(procGeo, procMat);
|
||||
procMesh.name = 'indicator_processing';
|
||||
procMesh.visible = false;
|
||||
group.add(procMesh);
|
||||
|
||||
return { group, waitMesh, thinkMesh, procMesh };
|
||||
}
|
||||
|
||||
function setAgentActivity(agent, state) {
|
||||
agent.activityState = state;
|
||||
agent.indicator.group.visible = (state !== ACTIVITY_STATES.NONE);
|
||||
agent.indicator.waitMesh.visible = (state === ACTIVITY_STATES.WAITING);
|
||||
agent.indicator.thinkMesh.visible = (state === ACTIVITY_STATES.THINKING);
|
||||
agent.indicator.procMesh.visible = (state === ACTIVITY_STATES.PROCESSING);
|
||||
}
|
||||
|
||||
function buildPacingPath(station) {
|
||||
// Small 3-waypoint circuit around the station
|
||||
const r = 1.8;
|
||||
return [
|
||||
new THREE.Vector3(station.x - r, 0, station.z),
|
||||
new THREE.Vector3(station.x, 0, station.z + r),
|
||||
new THREE.Vector3(station.x + r, 0, station.z - r * 0.5),
|
||||
];
|
||||
}
|
||||
|
||||
function pickNextState(agent) {
|
||||
const weights = {
|
||||
[AGENT_STATES.IDLE]: 40,
|
||||
[AGENT_STATES.PACING]: 25,
|
||||
[AGENT_STATES.LOOKING]: 20,
|
||||
[AGENT_STATES.READING]: 15,
|
||||
};
|
||||
const total = Object.values(weights).reduce((a, b) => a + b, 0);
|
||||
let r = Math.random() * total;
|
||||
for (const [state, w] of Object.entries(weights)) {
|
||||
r -= w;
|
||||
if (r <= 0) return state;
|
||||
}
|
||||
return AGENT_STATES.IDLE;
|
||||
}
|
||||
|
||||
// ═══ AGENT PRESENCE SYSTEM ═══
|
||||
function createAgentPresences() {
|
||||
const agentData = [
|
||||
@@ -561,30 +592,16 @@ function createAgentPresences() {
|
||||
label.position.y = 3.8;
|
||||
group.add(label);
|
||||
|
||||
// Activity Indicator
|
||||
const indicator = createActivityIndicator(color);
|
||||
group.add(indicator.group);
|
||||
|
||||
scene.add(group);
|
||||
agents.push({
|
||||
id: data.id,
|
||||
group,
|
||||
orb,
|
||||
halo,
|
||||
color,
|
||||
station: data.station,
|
||||
agents.push({
|
||||
id: data.id,
|
||||
group,
|
||||
orb,
|
||||
halo,
|
||||
color,
|
||||
station: data.station,
|
||||
targetPos: new THREE.Vector3(data.pos.x, 0, data.pos.z),
|
||||
// Idle state machine
|
||||
state: AGENT_STATES.IDLE,
|
||||
stateTimer: 2 + Math.random() * 4,
|
||||
lookAngle: 0,
|
||||
lookSpeed: 0.4 + Math.random() * 0.3,
|
||||
pacingPath: buildPacingPath(data.station),
|
||||
pacingIdx: 0,
|
||||
// Activity indicators
|
||||
indicator,
|
||||
activityState: ACTIVITY_STATES.NONE,
|
||||
activityLocked: false,
|
||||
wanderTimer: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1149,19 +1166,6 @@ function sendChatMessage() {
|
||||
if (!text) return;
|
||||
addChatMessage('user', text);
|
||||
input.value = '';
|
||||
|
||||
// Drive Timmy activity indicators
|
||||
const timmy = agents.find(a => a.id === 'timmy');
|
||||
if (timmy) {
|
||||
timmy.activityLocked = true;
|
||||
setAgentActivity(timmy, ACTIVITY_STATES.THINKING);
|
||||
}
|
||||
|
||||
const delay = 500 + Math.random() * 1000;
|
||||
if (timmy) {
|
||||
setTimeout(() => setAgentActivity(timmy, ACTIVITY_STATES.PROCESSING), delay * 0.4);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const responses = [
|
||||
'Processing your request through the harness...',
|
||||
@@ -1174,25 +1178,157 @@ function sendChatMessage() {
|
||||
];
|
||||
const resp = responses[Math.floor(Math.random() * responses.length)];
|
||||
addChatMessage('timmy', resp);
|
||||
if (timmy) {
|
||||
setAgentActivity(timmy, ACTIVITY_STATES.WAITING);
|
||||
setTimeout(() => {
|
||||
setAgentActivity(timmy, ACTIVITY_STATES.NONE);
|
||||
timmy.activityLocked = false;
|
||||
}, 2000);
|
||||
}
|
||||
}, delay);
|
||||
}, 500 + Math.random() * 1000);
|
||||
input.blur();
|
||||
}
|
||||
|
||||
function addChatMessage(type, text) {
|
||||
// ═══ HERMES WEBSOCKET ═══
|
||||
function connectHermes() {
|
||||
if (hermesWs) return;
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/world/ws`;
|
||||
|
||||
console.log(`Connecting to Hermes at ${wsUrl}...`);
|
||||
hermesWs = new WebSocket(wsUrl);
|
||||
|
||||
hermesWs.onopen = () => {
|
||||
console.log('Hermes connected.');
|
||||
wsConnected = true;
|
||||
addChatMessage('system', 'Hermes link established.');
|
||||
updateWsHudStatus(true);
|
||||
refreshWorkshopPanel();
|
||||
};
|
||||
|
||||
hermesWs.onmessage = (evt) => {
|
||||
try {
|
||||
const data = JSON.parse(evt.data);
|
||||
handleHermesMessage(data);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse Hermes message:', e);
|
||||
}
|
||||
};
|
||||
|
||||
hermesWs.onclose = () => {
|
||||
console.warn('Hermes disconnected. Retrying in 5s...');
|
||||
wsConnected = false;
|
||||
hermesWs = null;
|
||||
updateWsHudStatus(false);
|
||||
refreshWorkshopPanel();
|
||||
if (wsReconnectTimer) clearTimeout(wsReconnectTimer);
|
||||
wsReconnectTimer = setTimeout(connectHermes, 5000);
|
||||
};
|
||||
|
||||
hermesWs.onerror = (err) => {
|
||||
console.error('Hermes WS error:', err);
|
||||
};
|
||||
}
|
||||
|
||||
function handleHermesMessage(data) {
|
||||
if (data.type === 'chat') {
|
||||
addChatMessage(data.agent || 'timmy', data.text);
|
||||
} else if (data.type === 'tool_call') {
|
||||
const content = `Calling ${data.tool}(${JSON.stringify(data.args)})`;
|
||||
recentToolOutputs.push({ type: 'call', agent: data.agent || 'SYSTEM', content });
|
||||
addToolMessage(data.agent || 'SYSTEM', 'call', content);
|
||||
refreshWorkshopPanel();
|
||||
} else if (data.type === 'tool_result') {
|
||||
const content = `Result: ${JSON.stringify(data.result)}`;
|
||||
recentToolOutputs.push({ type: 'result', agent: data.agent || 'SYSTEM', content });
|
||||
addToolMessage(data.agent || 'SYSTEM', 'result', content);
|
||||
refreshWorkshopPanel();
|
||||
} else if (data.type === 'history') {
|
||||
const container = document.getElementById('chat-messages');
|
||||
container.innerHTML = '';
|
||||
data.messages.forEach(msg => {
|
||||
if (msg.type === 'tool_call') addToolMessage(msg.agent, 'call', msg.content, false);
|
||||
else if (msg.type === 'tool_result') addToolMessage(msg.agent, 'result', msg.content, false);
|
||||
else addChatMessage(msg.agent, msg.text, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateWsHudStatus(connected) {
|
||||
const dot = document.querySelector('.chat-status-dot');
|
||||
if (dot) {
|
||||
dot.style.background = connected ? '#4af0c0' : '#ff4466';
|
||||
dot.style.boxShadow = connected ? '0 0 10px #4af0c0' : '0 0 10px #ff4466';
|
||||
}
|
||||
}
|
||||
|
||||
// ═══ SESSION PERSISTENCE ═══
|
||||
function saveSession() {
|
||||
const msgs = Array.from(document.querySelectorAll('.chat-msg')).slice(-60).map(el => ({
|
||||
html: el.innerHTML,
|
||||
className: el.className
|
||||
}));
|
||||
localStorage.setItem('nexus_chat_history', JSON.stringify(msgs));
|
||||
}
|
||||
|
||||
function loadSession() {
|
||||
const saved = localStorage.getItem('nexus_chat_history');
|
||||
if (saved) {
|
||||
const msgs = JSON.parse(saved);
|
||||
const container = document.getElementById('chat-messages');
|
||||
container.innerHTML = '';
|
||||
msgs.forEach(m => {
|
||||
const div = document.createElement('div');
|
||||
div.className = m.className;
|
||||
div.innerHTML = m.html;
|
||||
container.appendChild(div);
|
||||
});
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function addChatMessage(agent, text, shouldSave = true) {
|
||||
const container = document.getElementById('chat-messages');
|
||||
const div = document.createElement('div');
|
||||
div.className = `chat-msg chat-msg-${type}`;
|
||||
const prefixes = { user: '[ALEXANDER]', timmy: '[TIMMY]', system: '[NEXUS]', error: '[ERROR]' };
|
||||
div.innerHTML = `<span class="chat-msg-prefix">${prefixes[type] || '[???]'}</span> ${text}`;
|
||||
div.className = `chat-msg chat-msg-${agent}`;
|
||||
|
||||
const prefixes = {
|
||||
user: '[ALEXANDER]',
|
||||
timmy: '[TIMMY]',
|
||||
system: '[NEXUS]',
|
||||
error: '[ERROR]',
|
||||
kimi: '[KIMI]',
|
||||
claude: '[CLAUDE]',
|
||||
perplexity: '[PERPLEXITY]'
|
||||
};
|
||||
|
||||
const prefix = document.createElement('span');
|
||||
prefix.className = 'chat-msg-prefix';
|
||||
prefix.textContent = `${prefixes[agent] || '[' + agent.toUpperCase() + ']'} `;
|
||||
|
||||
div.appendChild(prefix);
|
||||
div.appendChild(document.createTextNode(text));
|
||||
|
||||
container.appendChild(div);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
|
||||
if (shouldSave) saveSession();
|
||||
}
|
||||
|
||||
function addToolMessage(agent, type, content, shouldSave = true) {
|
||||
const container = document.getElementById('chat-messages');
|
||||
const div = document.createElement('div');
|
||||
div.className = `chat-msg chat-msg-tool tool-${type}`;
|
||||
|
||||
const prefix = document.createElement('div');
|
||||
prefix.className = 'chat-msg-prefix';
|
||||
prefix.textContent = `[${agent.toUpperCase()} TOOL ${type.toUpperCase()}]`;
|
||||
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'tool-content';
|
||||
pre.textContent = content;
|
||||
|
||||
div.appendChild(prefix);
|
||||
div.appendChild(pre);
|
||||
|
||||
container.appendChild(div);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
|
||||
if (shouldSave) saveSession();
|
||||
}
|
||||
|
||||
// ═══ PORTAL INTERACTION ═══
|
||||
@@ -1440,7 +1576,24 @@ function gameLoop() {
|
||||
});
|
||||
|
||||
// Animate Agents
|
||||
updateAgents(elapsed, delta);
|
||||
agents.forEach((agent, i) => {
|
||||
// Wander logic
|
||||
agent.wanderTimer -= delta;
|
||||
if (agent.wanderTimer <= 0) {
|
||||
agent.wanderTimer = 3 + Math.random() * 5;
|
||||
agent.targetPos.set(
|
||||
agent.station.x + (Math.random() - 0.5) * 4,
|
||||
0,
|
||||
agent.station.z + (Math.random() - 0.5) * 4
|
||||
);
|
||||
}
|
||||
agent.group.position.lerp(agent.targetPos, delta * 0.5);
|
||||
|
||||
agent.orb.position.y = 3 + Math.sin(elapsed * 2 + i) * 0.15;
|
||||
agent.halo.rotation.z = elapsed * 0.5;
|
||||
agent.halo.scale.setScalar(1 + Math.sin(elapsed * 3 + i) * 0.1);
|
||||
agent.orb.material.emissiveIntensity = 2 + Math.sin(elapsed * 4 + i) * 1;
|
||||
});
|
||||
|
||||
// Animate Power Meter
|
||||
powerMeterBars.forEach((bar, i) => {
|
||||
@@ -1482,6 +1635,12 @@ function gameLoop() {
|
||||
|
||||
composer.render();
|
||||
|
||||
if (workshopScanMat) workshopScanMat.uniforms.uTime.value = clock.getElapsedTime();
|
||||
if (activePortal !== lastFocusedPortal) {
|
||||
lastFocusedPortal = activePortal;
|
||||
refreshWorkshopPanel();
|
||||
}
|
||||
|
||||
frameCount++;
|
||||
const now = performance.now();
|
||||
if (now - lastFPSTime >= 1000) {
|
||||
@@ -1507,125 +1666,6 @@ function onResize() {
|
||||
composer.setSize(w, h);
|
||||
}
|
||||
|
||||
// ═══ AGENT IDLE ANIMATION ═══
|
||||
function updateAgents(elapsed, delta) {
|
||||
const ATTENTION_RADIUS = 7;
|
||||
const terminalFacing = new THREE.Vector3(0, 0, -8); // batcave terminal bank Z
|
||||
|
||||
agents.forEach((agent, i) => {
|
||||
const stationWorld = new THREE.Vector3(agent.station.x, 0, agent.station.z);
|
||||
|
||||
// ── Attention system: face player when close ──
|
||||
const toPlayer = new THREE.Vector3(
|
||||
playerPos.x - agent.group.position.x,
|
||||
0,
|
||||
playerPos.z - agent.group.position.z
|
||||
);
|
||||
const playerDist = toPlayer.length();
|
||||
const playerNearby = playerDist < ATTENTION_RADIUS && !agent.activityLocked;
|
||||
|
||||
if (playerNearby) {
|
||||
const targetAngle = Math.atan2(toPlayer.x, toPlayer.z);
|
||||
const currentAngle = agent.group.rotation.y;
|
||||
const diff = ((targetAngle - currentAngle + Math.PI * 3) % (Math.PI * 2)) - Math.PI;
|
||||
agent.group.rotation.y += diff * Math.min(delta * 3, 1);
|
||||
}
|
||||
|
||||
// ── State machine (skip if activity locked or player nearby) ──
|
||||
if (!playerNearby && !agent.activityLocked) {
|
||||
agent.stateTimer -= delta;
|
||||
|
||||
if (agent.stateTimer <= 0) {
|
||||
agent.state = pickNextState(agent);
|
||||
switch (agent.state) {
|
||||
case AGENT_STATES.IDLE:
|
||||
agent.stateTimer = 4 + Math.random() * 6;
|
||||
agent.targetPos.copy(stationWorld);
|
||||
break;
|
||||
case AGENT_STATES.PACING:
|
||||
agent.stateTimer = 8 + Math.random() * 6;
|
||||
agent.pacingIdx = 0;
|
||||
break;
|
||||
case AGENT_STATES.LOOKING:
|
||||
agent.stateTimer = 4 + Math.random() * 4;
|
||||
agent.lookAngle = agent.group.rotation.y;
|
||||
break;
|
||||
case AGENT_STATES.READING:
|
||||
agent.stateTimer = 5 + Math.random() * 5;
|
||||
agent.targetPos.copy(stationWorld);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Movement per state ──
|
||||
if (agent.state === AGENT_STATES.PACING) {
|
||||
const wp = agent.pacingPath[agent.pacingIdx];
|
||||
const toWp = new THREE.Vector3(wp.x - agent.group.position.x, 0, wp.z - agent.group.position.z);
|
||||
if (toWp.length() < 0.3) {
|
||||
agent.pacingIdx = (agent.pacingIdx + 1) % agent.pacingPath.length;
|
||||
} else {
|
||||
agent.group.position.addScaledVector(toWp.normalize(), delta * 1.2);
|
||||
agent.group.rotation.y += (Math.atan2(toWp.x, toWp.z) - agent.group.rotation.y) * Math.min(delta * 4, 1);
|
||||
}
|
||||
} else if (agent.state === AGENT_STATES.READING) {
|
||||
// Face the terminal bank
|
||||
const toTerminal = new THREE.Vector3(
|
||||
terminalFacing.x - agent.group.position.x,
|
||||
0,
|
||||
terminalFacing.z - agent.group.position.z
|
||||
);
|
||||
const targetAngle = Math.atan2(toTerminal.x, toTerminal.z);
|
||||
agent.group.rotation.y += (targetAngle - agent.group.rotation.y) * Math.min(delta * 2, 1);
|
||||
agent.group.position.lerp(agent.targetPos, delta * 0.4);
|
||||
} else if (agent.state === AGENT_STATES.LOOKING) {
|
||||
// Slow environmental scan left/right
|
||||
agent.lookAngle += Math.sin(elapsed * agent.lookSpeed + i) * delta * 0.8;
|
||||
agent.group.rotation.y += (agent.lookAngle - agent.group.rotation.y) * Math.min(delta * 1.5, 1);
|
||||
agent.group.position.lerp(agent.targetPos, delta * 0.3);
|
||||
} else {
|
||||
// IDLE — drift gently back to station
|
||||
agent.group.position.lerp(agent.targetPos, delta * 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Orb & halo animation ──
|
||||
const bobAmt = agent.activityState === ACTIVITY_STATES.THINKING ? 0.25 : 0.15;
|
||||
agent.orb.position.y = 3 + Math.sin(elapsed * 2 + i) * bobAmt;
|
||||
agent.halo.rotation.z = elapsed * 0.5;
|
||||
agent.halo.scale.setScalar(1 + Math.sin(elapsed * 3 + i) * 0.1);
|
||||
const baseEmissive = agent.activityState === ACTIVITY_STATES.NONE ? 2 : 3;
|
||||
agent.orb.material.emissiveIntensity = baseEmissive + Math.sin(elapsed * 4 + i) * 1;
|
||||
|
||||
// ── Activity indicator animation ──
|
||||
if (agent.activityState !== ACTIVITY_STATES.NONE) {
|
||||
// Floating bob
|
||||
agent.indicator.group.position.y = 4.2 + Math.sin(elapsed * 2 + i * 1.3) * 0.1;
|
||||
|
||||
if (agent.activityState === ACTIVITY_STATES.WAITING) {
|
||||
const pulse = 0.7 + Math.sin(elapsed * 4 + i) * 0.3;
|
||||
agent.indicator.waitMesh.scale.setScalar(pulse);
|
||||
agent.indicator.waitMesh.material.opacity = 0.5 + pulse * 0.35;
|
||||
} else if (agent.activityState === ACTIVITY_STATES.THINKING) {
|
||||
agent.indicator.thinkMesh.rotation.y = elapsed * 2.5;
|
||||
agent.indicator.thinkMesh.rotation.x = elapsed * 1.5;
|
||||
} else if (agent.activityState === ACTIVITY_STATES.PROCESSING) {
|
||||
agent.indicator.procMesh.rotation.z = elapsed * 4;
|
||||
agent.indicator.procMesh.rotation.x = Math.sin(elapsed * 1.2) * 0.5;
|
||||
}
|
||||
|
||||
// Billboard — indicator faces camera
|
||||
const toCamera = new THREE.Vector3(
|
||||
camera.position.x - agent.group.position.x,
|
||||
0,
|
||||
camera.position.z - agent.group.position.z
|
||||
);
|
||||
if (toCamera.length() > 0.01) {
|
||||
agent.indicator.group.rotation.y = Math.atan2(toCamera.x, toCamera.z);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ═══ AGENT SIMULATION ═══
|
||||
function simulateAgentThought() {
|
||||
const agentIds = ['timmy', 'kimi', 'claude', 'perplexity'];
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
<span>WASD</span> move <span>Mouse</span> look <span>Enter</span> chat
|
||||
<span>V</span> mode: <span id="nav-mode-label">WALK</span>
|
||||
<span id="nav-mode-hint" class="nav-mode-hint"></span>
|
||||
<span class="ws-hud-status">HERMES: <span id="ws-status-dot" class="chat-status-dot"></span></span>
|
||||
</div>
|
||||
|
||||
<!-- Portal Hint -->
|
||||
|
||||
25
style.css
25
style.css
@@ -533,7 +533,7 @@ canvas#nexus-canvas {
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
box-shadow: 0 0 6px var(--color-primary);
|
||||
animation: dot-pulse 2s ease-in-out infinite;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
@keyframes dot-pulse {
|
||||
0%, 100% { opacity: 0.6; }
|
||||
@@ -570,6 +570,29 @@ canvas#nexus-canvas {
|
||||
.chat-msg-prefix {
|
||||
font-weight: 700;
|
||||
}
|
||||
.chat-msg-kimi .chat-msg-prefix { color: var(--color-secondary); }
|
||||
.chat-msg-claude .chat-msg-prefix { color: var(--color-gold); }
|
||||
.chat-msg-perplexity .chat-msg-prefix { color: #4488ff; }
|
||||
|
||||
/* Tool Output Styling */
|
||||
.chat-msg-tool {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-left: 2px solid #ffd700;
|
||||
font-size: 11px;
|
||||
padding: 8px;
|
||||
margin: 4px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.tool-call { border-left-color: #ffd700; }
|
||||
.tool-result { border-left-color: #4af0c0; }
|
||||
.tool-content {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
opacity: 0.8;
|
||||
margin: 4px 0 0 0;
|
||||
color: #a0b8d0;
|
||||
}
|
||||
.chat-msg-system .chat-msg-prefix { color: var(--color-text-muted); }
|
||||
.chat-msg-timmy .chat-msg-prefix { color: var(--color-primary); }
|
||||
.chat-msg-user .chat-msg-prefix { color: var(--color-gold); }
|
||||
|
||||
Reference in New Issue
Block a user