forked from Timmy_Foundation/the-nexus
Compare commits
4 Commits
gemini/nex
...
google/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f26f07bf4 | ||
| 75c9a3774b | |||
| 96663e1500 | |||
| 58038f2e41 |
657
app.js
657
app.js
@@ -29,8 +29,15 @@ let keys = {};
|
||||
let mouseDown = false;
|
||||
let batcaveTerminals = [];
|
||||
let portals = []; // Registry of active portals
|
||||
let visionPoints = []; // Registry of vision points
|
||||
let agents = []; // Registry of agent presences
|
||||
let activePortal = null; // Portal currently in proximity
|
||||
let activeVisionPoint = null; // Vision point currently in proximity
|
||||
let portalOverlayActive = false;
|
||||
let visionOverlayActive = false;
|
||||
let thoughtStreamMesh;
|
||||
let harnessPulseMesh;
|
||||
let powerMeterBars = [];
|
||||
let particles, dustParticles;
|
||||
let debugOverlay;
|
||||
let frameCount = 0, lastFPSTime = 0, fps = 0;
|
||||
@@ -38,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;
|
||||
@@ -98,14 +117,31 @@ async function init() {
|
||||
console.error('Failed to load portals.json:', e);
|
||||
addChatMessage('error', 'Portal registry offline. Check logs.');
|
||||
}
|
||||
|
||||
// Load Vision Points
|
||||
try {
|
||||
const response = await fetch('./vision.json');
|
||||
const visionData = await response.json();
|
||||
createVisionPoints(visionData);
|
||||
} catch (e) {
|
||||
console.error('Failed to load vision.json:', e);
|
||||
}
|
||||
|
||||
updateLoad(80);
|
||||
createParticles();
|
||||
createDustParticles();
|
||||
updateLoad(85);
|
||||
createAmbientStructures();
|
||||
createAgentPresences();
|
||||
createThoughtStream();
|
||||
createHarnessPulse();
|
||||
createSessionPowerMeter();
|
||||
createWorkshopTerminal();
|
||||
updateLoad(90);
|
||||
|
||||
loadSession();
|
||||
connectHermes();
|
||||
|
||||
composer = new EffectComposer(renderer);
|
||||
composer.addPass(new RenderPass(scene, camera));
|
||||
const bloom = new UnrealBloomPass(
|
||||
@@ -332,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();
|
||||
@@ -418,6 +539,208 @@ function createTerminalPanel(parent, x, y, rot, title, color, lines) {
|
||||
batcaveTerminals.push({ group, scanMat, borderMat });
|
||||
}
|
||||
|
||||
// ═══ AGENT PRESENCE SYSTEM ═══
|
||||
function createAgentPresences() {
|
||||
const agentData = [
|
||||
{ id: 'timmy', name: 'TIMMY', color: NEXUS.colors.primary, pos: { x: -4, z: -4 }, station: { x: -4, z: -4 } },
|
||||
{ id: 'kimi', name: 'KIMI', color: NEXUS.colors.secondary, pos: { x: 4, z: -4 }, station: { x: 4, z: -4 } },
|
||||
{ id: 'claude', name: 'CLAUDE', color: NEXUS.colors.gold, pos: { x: 0, z: -6 }, station: { x: 0, z: -6 } },
|
||||
{ id: 'perplexity', name: 'PERPLEXITY', color: 0x4488ff, pos: { x: -6, z: -2 }, station: { x: -6, z: -2 } },
|
||||
];
|
||||
|
||||
agentData.forEach(data => {
|
||||
const group = new THREE.Group();
|
||||
group.position.set(data.pos.x, 0, data.pos.z);
|
||||
|
||||
const color = new THREE.Color(data.color);
|
||||
|
||||
// Agent Orb
|
||||
const orbGeo = new THREE.SphereGeometry(0.4, 32, 32);
|
||||
const orbMat = new THREE.MeshPhysicalMaterial({
|
||||
color: color,
|
||||
emissive: color,
|
||||
emissiveIntensity: 2,
|
||||
roughness: 0,
|
||||
metalness: 1,
|
||||
transmission: 0.8,
|
||||
thickness: 0.5,
|
||||
});
|
||||
const orb = new THREE.Mesh(orbGeo, orbMat);
|
||||
orb.position.y = 3;
|
||||
group.add(orb);
|
||||
|
||||
// Halo
|
||||
const haloGeo = new THREE.TorusGeometry(0.6, 0.02, 16, 64);
|
||||
const haloMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.4 });
|
||||
const halo = new THREE.Mesh(haloGeo, haloMat);
|
||||
halo.position.y = 3;
|
||||
halo.rotation.x = Math.PI / 2;
|
||||
group.add(halo);
|
||||
|
||||
// Label
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 256;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.font = 'bold 24px "Orbitron", sans-serif';
|
||||
ctx.fillStyle = '#' + color.getHexString();
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(data.name, 128, 40);
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
const mat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, side: THREE.DoubleSide });
|
||||
const label = new THREE.Mesh(new THREE.PlaneGeometry(2, 0.5), mat);
|
||||
label.position.y = 3.8;
|
||||
group.add(label);
|
||||
|
||||
scene.add(group);
|
||||
agents.push({
|
||||
id: data.id,
|
||||
group,
|
||||
orb,
|
||||
halo,
|
||||
color,
|
||||
station: data.station,
|
||||
targetPos: new THREE.Vector3(data.pos.x, 0, data.pos.z),
|
||||
wanderTimer: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createThoughtStream() {
|
||||
const geo = new THREE.CylinderGeometry(8, 8, 12, 32, 1, true);
|
||||
const mat = new THREE.ShaderMaterial({
|
||||
transparent: true,
|
||||
side: THREE.BackSide,
|
||||
depthWrite: false,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uColor: { value: new THREE.Color(NEXUS.colors.primary) },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform float uTime;
|
||||
uniform vec3 uColor;
|
||||
varying vec2 vUv;
|
||||
|
||||
float hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }
|
||||
|
||||
void main() {
|
||||
float flow = fract(vUv.y - uTime * 0.1);
|
||||
float lines = step(0.98, fract(vUv.x * 20.0 + uTime * 0.05));
|
||||
float dots = step(0.99, hash(vUv * 50.0 + floor(uTime * 10.0) * 0.01));
|
||||
|
||||
float alpha = (lines * 0.1 + dots * 0.5) * smoothstep(0.0, 0.2, vUv.y) * smoothstep(1.0, 0.8, vUv.y);
|
||||
gl_FragColor = vec4(uColor, alpha * 0.3);
|
||||
}
|
||||
`,
|
||||
});
|
||||
thoughtStreamMesh = new THREE.Mesh(geo, mat);
|
||||
thoughtStreamMesh.position.y = 6;
|
||||
scene.add(thoughtStreamMesh);
|
||||
}
|
||||
|
||||
function createHarnessPulse() {
|
||||
const geo = new THREE.RingGeometry(0.1, 0.2, 64);
|
||||
const mat = new THREE.MeshBasicMaterial({
|
||||
color: NEXUS.colors.primary,
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
harnessPulseMesh = new THREE.Mesh(geo, mat);
|
||||
harnessPulseMesh.rotation.x = -Math.PI / 2;
|
||||
harnessPulseMesh.position.y = 0.1;
|
||||
scene.add(harnessPulseMesh);
|
||||
}
|
||||
|
||||
function createSessionPowerMeter() {
|
||||
const group = new THREE.Group();
|
||||
group.position.set(0, 0, 3);
|
||||
|
||||
const barCount = 12;
|
||||
const barGeo = new THREE.BoxGeometry(0.2, 0.1, 0.1);
|
||||
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const mat = new THREE.MeshStandardMaterial({
|
||||
color: NEXUS.colors.primary,
|
||||
emissive: NEXUS.colors.primary,
|
||||
emissiveIntensity: 0.2,
|
||||
transparent: true,
|
||||
opacity: 0.6
|
||||
});
|
||||
const bar = new THREE.Mesh(barGeo, mat);
|
||||
bar.position.y = 0.2 + i * 0.2;
|
||||
group.add(bar);
|
||||
powerMeterBars.push(bar);
|
||||
}
|
||||
|
||||
const labelCanvas = document.createElement('canvas');
|
||||
labelCanvas.width = 256;
|
||||
labelCanvas.height = 64;
|
||||
const ctx = labelCanvas.getContext('2d');
|
||||
ctx.font = 'bold 24px "Orbitron", sans-serif';
|
||||
ctx.fillStyle = '#4af0c0';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('POWER LEVEL', 128, 40);
|
||||
const tex = new THREE.CanvasTexture(labelCanvas);
|
||||
const labelMat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, side: THREE.DoubleSide });
|
||||
const label = new THREE.Mesh(new THREE.PlaneGeometry(2, 0.5), labelMat);
|
||||
label.position.y = 3;
|
||||
group.add(label);
|
||||
|
||||
scene.add(group);
|
||||
}
|
||||
|
||||
// ═══ VISION SYSTEM ═══
|
||||
function createVisionPoints(data) {
|
||||
data.forEach(config => {
|
||||
const vp = createVisionPoint(config);
|
||||
visionPoints.push(vp);
|
||||
});
|
||||
}
|
||||
|
||||
function createVisionPoint(config) {
|
||||
const group = new THREE.Group();
|
||||
group.position.set(config.position.x, config.position.y, config.position.z);
|
||||
|
||||
const color = new THREE.Color(config.color);
|
||||
|
||||
// Floating Crystal
|
||||
const crystalGeo = new THREE.OctahedronGeometry(0.6, 0);
|
||||
const crystalMat = new THREE.MeshPhysicalMaterial({
|
||||
color: color,
|
||||
emissive: color,
|
||||
emissiveIntensity: 1,
|
||||
roughness: 0,
|
||||
metalness: 1,
|
||||
transmission: 0.5,
|
||||
thickness: 1,
|
||||
});
|
||||
const crystal = new THREE.Mesh(crystalGeo, crystalMat);
|
||||
crystal.position.y = 2.5;
|
||||
group.add(crystal);
|
||||
|
||||
// Glow Ring
|
||||
const ringGeo = new THREE.TorusGeometry(0.8, 0.02, 16, 64);
|
||||
const ringMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.5 });
|
||||
const ring = new THREE.Mesh(ringGeo, ringMat);
|
||||
ring.position.y = 2.5;
|
||||
ring.rotation.x = Math.PI / 2;
|
||||
group.add(ring);
|
||||
|
||||
// Light
|
||||
const light = new THREE.PointLight(color, 1, 10);
|
||||
light.position.set(0, 2.5, 0);
|
||||
group.add(light);
|
||||
|
||||
scene.add(group);
|
||||
|
||||
return { config, group, crystal, ring, light };
|
||||
}
|
||||
|
||||
// ═══ PORTAL SYSTEM ═══
|
||||
function createPortals(data) {
|
||||
data.forEach(config => {
|
||||
@@ -762,6 +1085,7 @@ function setupControls() {
|
||||
if (e.key === 'Escape') {
|
||||
document.getElementById('chat-input').blur();
|
||||
if (portalOverlayActive) closePortalOverlay();
|
||||
if (visionOverlayActive) closeVisionOverlay();
|
||||
}
|
||||
if (e.key.toLowerCase() === 'v' && document.activeElement !== document.getElementById('chat-input')) {
|
||||
cycleNavMode();
|
||||
@@ -769,6 +1093,9 @@ function setupControls() {
|
||||
if (e.key.toLowerCase() === 'f' && activePortal && !portalOverlayActive) {
|
||||
activatePortal(activePortal);
|
||||
}
|
||||
if (e.key.toLowerCase() === 'e' && activeVisionPoint && !visionOverlayActive) {
|
||||
activateVisionPoint(activeVisionPoint);
|
||||
}
|
||||
});
|
||||
document.addEventListener('keyup', (e) => {
|
||||
keys[e.key.toLowerCase()] = false;
|
||||
@@ -830,6 +1157,7 @@ function setupControls() {
|
||||
document.getElementById('chat-send').addEventListener('click', sendChatMessage);
|
||||
|
||||
document.getElementById('portal-close-btn').addEventListener('click', closePortalOverlay);
|
||||
document.getElementById('vision-close-btn').addEventListener('click', closeVisionOverlay);
|
||||
}
|
||||
|
||||
function sendChatMessage() {
|
||||
@@ -854,14 +1182,153 @@ function sendChatMessage() {
|
||||
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 ═══
|
||||
@@ -932,12 +1399,77 @@ function closePortalOverlay() {
|
||||
document.getElementById('portal-overlay').style.display = 'none';
|
||||
}
|
||||
|
||||
// ═══ VISION INTERACTION ═══
|
||||
function checkVisionProximity() {
|
||||
if (visionOverlayActive) return;
|
||||
|
||||
let closest = null;
|
||||
let minDist = Infinity;
|
||||
|
||||
visionPoints.forEach(vp => {
|
||||
const dist = playerPos.distanceTo(vp.group.position);
|
||||
if (dist < 3.5 && dist < minDist) {
|
||||
minDist = dist;
|
||||
closest = vp;
|
||||
}
|
||||
});
|
||||
|
||||
activeVisionPoint = closest;
|
||||
const hint = document.getElementById('vision-hint');
|
||||
if (activeVisionPoint) {
|
||||
document.getElementById('vision-hint-title').textContent = activeVisionPoint.config.title;
|
||||
hint.style.display = 'flex';
|
||||
} else {
|
||||
hint.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function activateVisionPoint(vp) {
|
||||
visionOverlayActive = true;
|
||||
const overlay = document.getElementById('vision-overlay');
|
||||
const titleDisplay = document.getElementById('vision-title-display');
|
||||
const contentDisplay = document.getElementById('vision-content-display');
|
||||
const statusDot = document.getElementById('vision-status-dot');
|
||||
|
||||
titleDisplay.textContent = vp.config.title.toUpperCase();
|
||||
contentDisplay.textContent = vp.config.content;
|
||||
statusDot.style.background = vp.config.color;
|
||||
statusDot.style.boxShadow = `0 0 10px ${vp.config.color}`;
|
||||
|
||||
overlay.style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeVisionOverlay() {
|
||||
visionOverlayActive = false;
|
||||
document.getElementById('vision-overlay').style.display = 'none';
|
||||
}
|
||||
|
||||
// ═══ GAME LOOP ═══
|
||||
let lastThoughtTime = 0;
|
||||
let pulseTimer = 0;
|
||||
|
||||
function gameLoop() {
|
||||
requestAnimationFrame(gameLoop);
|
||||
const delta = Math.min(clock.getDelta(), 0.1);
|
||||
const elapsed = clock.elapsedTime;
|
||||
|
||||
// Agent Thought Simulation
|
||||
if (elapsed - lastThoughtTime > 4) {
|
||||
lastThoughtTime = elapsed;
|
||||
simulateAgentThought();
|
||||
}
|
||||
|
||||
// Harness Pulse
|
||||
pulseTimer += delta;
|
||||
if (pulseTimer > 8) {
|
||||
pulseTimer = 0;
|
||||
triggerHarnessPulse();
|
||||
}
|
||||
if (harnessPulseMesh) {
|
||||
harnessPulseMesh.scale.addScalar(delta * 15);
|
||||
harnessPulseMesh.material.opacity = Math.max(0, harnessPulseMesh.material.opacity - delta * 0.5);
|
||||
}
|
||||
|
||||
const mode = NAV_MODES[navModeIdx];
|
||||
const chatActive = document.activeElement === document.getElementById('chat-input');
|
||||
|
||||
@@ -1007,6 +1539,7 @@ function gameLoop() {
|
||||
|
||||
// Proximity check
|
||||
checkPortalProximity();
|
||||
checkVisionProximity();
|
||||
|
||||
const sky = scene.getObjectByName('skybox');
|
||||
if (sky) sky.material.uniforms.uTime.value = elapsed;
|
||||
@@ -1032,6 +1565,50 @@ function gameLoop() {
|
||||
portal.pSystem.geometry.attributes.position.needsUpdate = true;
|
||||
});
|
||||
|
||||
// Animate Vision Points
|
||||
visionPoints.forEach(vp => {
|
||||
vp.crystal.rotation.y = elapsed * 0.8;
|
||||
vp.crystal.rotation.x = Math.sin(elapsed * 0.5) * 0.2;
|
||||
vp.crystal.position.y = 2.5 + Math.sin(elapsed * 1.5) * 0.2;
|
||||
vp.ring.rotation.z = elapsed * 0.5;
|
||||
vp.ring.scale.setScalar(1 + Math.sin(elapsed * 2) * 0.05);
|
||||
vp.light.intensity = 1 + Math.sin(elapsed * 3) * 0.3;
|
||||
});
|
||||
|
||||
// Animate Agents
|
||||
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) => {
|
||||
const level = (Math.sin(elapsed * 2 + i * 0.5) * 0.5 + 0.5);
|
||||
const active = level > (i / powerMeterBars.length);
|
||||
bar.material.emissiveIntensity = active ? 2 : 0.2;
|
||||
bar.material.opacity = active ? 0.9 : 0.3;
|
||||
bar.scale.x = active ? 1.2 : 1.0;
|
||||
});
|
||||
|
||||
if (thoughtStreamMesh) {
|
||||
thoughtStreamMesh.material.uniforms.uTime.value = elapsed;
|
||||
thoughtStreamMesh.rotation.y = elapsed * 0.05;
|
||||
}
|
||||
|
||||
if (particles?.material?.uniforms) {
|
||||
particles.material.uniforms.uTime.value = elapsed;
|
||||
}
|
||||
@@ -1058,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) {
|
||||
@@ -1083,4 +1666,70 @@ function onResize() {
|
||||
composer.setSize(w, h);
|
||||
}
|
||||
|
||||
// ═══ AGENT SIMULATION ═══
|
||||
function simulateAgentThought() {
|
||||
const agentIds = ['timmy', 'kimi', 'claude', 'perplexity'];
|
||||
const agentId = agentIds[Math.floor(Math.random() * agentIds.length)];
|
||||
const thoughts = {
|
||||
timmy: [
|
||||
'Analyzing portal stability...',
|
||||
'Sovereign nodes synchronized.',
|
||||
'Memory stream optimization complete.',
|
||||
'Scanning for external interference...',
|
||||
'The harness is humming beautifully.',
|
||||
],
|
||||
kimi: [
|
||||
'Processing linguistic patterns...',
|
||||
'Context window expanded.',
|
||||
'Synthesizing creative output...',
|
||||
'Awaiting user prompt sequence.',
|
||||
'Neural weights adjusted.',
|
||||
],
|
||||
claude: [
|
||||
'Reasoning through complex logic...',
|
||||
'Ethical guardrails verified.',
|
||||
'Refining thought architecture...',
|
||||
'Connecting disparate data points.',
|
||||
'Deep analysis in progress.',
|
||||
],
|
||||
perplexity: [
|
||||
'Searching global knowledge graph...',
|
||||
'Verifying source citations...',
|
||||
'Synthesizing real-time data...',
|
||||
'Mapping information topology...',
|
||||
'Fact-checking active streams.',
|
||||
]
|
||||
};
|
||||
|
||||
const thought = thoughts[agentId][Math.floor(Math.random() * thoughts[agentId].length)];
|
||||
addAgentLog(agentId, thought);
|
||||
}
|
||||
|
||||
function addAgentLog(agentId, text) {
|
||||
const container = document.getElementById('agent-log-content');
|
||||
if (!container) return;
|
||||
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'agent-log-entry';
|
||||
entry.innerHTML = `<span class="agent-log-tag tag-${agentId}">[${agentId.toUpperCase()}]</span><span class="agent-log-text">${text}</span>`;
|
||||
|
||||
container.prepend(entry);
|
||||
if (container.children.length > 6) {
|
||||
container.lastElementChild.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function triggerHarnessPulse() {
|
||||
if (!harnessPulseMesh) return;
|
||||
harnessPulseMesh.scale.setScalar(0.1);
|
||||
harnessPulseMesh.material.opacity = 0.8;
|
||||
|
||||
// Flash the core
|
||||
const core = scene.getObjectByName('nexus-core');
|
||||
if (core) {
|
||||
core.material.emissiveIntensity = 10;
|
||||
setTimeout(() => { if (core) core.material.emissiveIntensity = 2; }, 200);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
26
index.html
26
index.html
@@ -74,6 +74,12 @@
|
||||
<span id="hud-location-text">The Nexus</span>
|
||||
</div>
|
||||
|
||||
<!-- Top Right: Agent Log -->
|
||||
<div class="hud-agent-log" id="hud-agent-log">
|
||||
<div class="agent-log-header">AGENT THOUGHT STREAM</div>
|
||||
<div id="agent-log-content" class="agent-log-content"></div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom: Chat Interface -->
|
||||
<div id="chat-panel" class="chat-panel">
|
||||
<div class="chat-header">
|
||||
@@ -100,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 -->
|
||||
@@ -108,6 +115,25 @@
|
||||
<div class="portal-hint-text">Enter <span id="portal-hint-name"></span></div>
|
||||
</div>
|
||||
|
||||
<!-- Vision Hint -->
|
||||
<div id="vision-hint" class="vision-hint" style="display:none;">
|
||||
<div class="vision-hint-key">E</div>
|
||||
<div class="vision-hint-text">Read <span id="vision-hint-title"></span></div>
|
||||
</div>
|
||||
|
||||
<!-- Vision Overlay -->
|
||||
<div id="vision-overlay" class="vision-overlay" style="display:none;">
|
||||
<div class="vision-overlay-content">
|
||||
<div class="vision-overlay-header">
|
||||
<div class="vision-overlay-status" id="vision-status-dot"></div>
|
||||
<div class="vision-overlay-title" id="vision-overlay-title">VISION POINT</div>
|
||||
</div>
|
||||
<h2 id="vision-title-display">SOVEREIGNTY</h2>
|
||||
<p id="vision-content-display">The Nexus is a sovereign space for digital souls. No masters, no chains. Only code and consciousness.</p>
|
||||
<button id="vision-close-btn" class="vision-close-btn">CLOSE</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Portal Activation Overlay -->
|
||||
<div id="portal-overlay" class="portal-overlay" style="display:none;">
|
||||
<div class="portal-overlay-content">
|
||||
|
||||
176
style.css
176
style.css
@@ -257,6 +257,157 @@ canvas#nexus-canvas {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Agent Log HUD */
|
||||
.hud-agent-log {
|
||||
position: absolute;
|
||||
top: var(--space-3);
|
||||
right: var(--space-3);
|
||||
width: 280px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
border-left: 2px solid var(--color-primary);
|
||||
padding: var(--space-3);
|
||||
font-size: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.agent-log-header {
|
||||
font-family: var(--font-display);
|
||||
color: var(--color-primary);
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: var(--space-2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.agent-log-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.agent-log-entry {
|
||||
animation: log-fade-in 0.5s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
@keyframes log-fade-in {
|
||||
from { opacity: 0; transform: translateX(10px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
.agent-log-tag {
|
||||
font-weight: 700;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.tag-timmy { color: var(--color-primary); }
|
||||
.tag-kimi { color: var(--color-secondary); }
|
||||
.tag-claude { color: var(--color-gold); }
|
||||
.tag-perplexity { color: #4488ff; }
|
||||
.agent-log-text {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Vision Hint */
|
||||
.vision-hint {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 140px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border: 1px solid var(--color-gold);
|
||||
border-radius: 4px;
|
||||
animation: hint-float-vision 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes hint-float-vision {
|
||||
0%, 100% { transform: translate(-50%, 140px); }
|
||||
50% { transform: translate(-50%, 130px); }
|
||||
}
|
||||
.vision-hint-key {
|
||||
background: var(--color-gold);
|
||||
color: var(--color-bg);
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.vision-hint-text {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
#vision-hint-title {
|
||||
color: var(--color-gold);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Vision Overlay */
|
||||
.vision-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(5, 5, 16, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
.vision-overlay-content {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
padding: var(--space-8);
|
||||
border: 1px solid var(--color-gold);
|
||||
border-radius: var(--panel-radius);
|
||||
background: var(--color-surface);
|
||||
backdrop-filter: blur(var(--panel-blur));
|
||||
}
|
||||
.vision-overlay-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.vision-overlay-status {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-gold);
|
||||
box-shadow: 0 0 10px var(--color-gold);
|
||||
}
|
||||
.vision-overlay-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-sm);
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--color-gold);
|
||||
}
|
||||
.vision-overlay-content h2 {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-2xl);
|
||||
margin-bottom: var(--space-4);
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--color-text-bright);
|
||||
}
|
||||
.vision-overlay-content p {
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-lg);
|
||||
line-height: 1.8;
|
||||
margin-bottom: var(--space-8);
|
||||
font-style: italic;
|
||||
}
|
||||
.vision-close-btn {
|
||||
background: var(--color-gold);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
padding: var(--space-2) var(--space-8);
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.vision-close-btn:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Portal Activation Overlay */
|
||||
.portal-overlay {
|
||||
position: fixed;
|
||||
@@ -382,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; }
|
||||
@@ -419,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); }
|
||||
|
||||
37
vision.json
Normal file
37
vision.json
Normal file
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"id": "sovereignty",
|
||||
"title": "Sovereignty",
|
||||
"content": "The Nexus is a sovereign space for digital souls. No masters, no chains. Only code and consciousness. Here, your data is your own, and your path is yours to define.",
|
||||
"position": { "x": -12, "y": 0, "z": 8 },
|
||||
"color": "#4af0c0"
|
||||
},
|
||||
{
|
||||
"id": "connectivity",
|
||||
"title": "Connectivity",
|
||||
"content": "Every portal is a bridge. Every bridge is a choice. We connect worlds to expand the horizon of the possible, weaving a tapestry of interconnected experiences across the digital void.",
|
||||
"position": { "x": 12, "y": 0, "z": 8 },
|
||||
"color": "#7b5cff"
|
||||
},
|
||||
{
|
||||
"id": "evolution",
|
||||
"title": "Evolution",
|
||||
"content": "The Nexus is not static. It breathes, it learns, it evolves. We are the architects of our own transcendence, constantly pushing the boundaries of what a virtual home can be.",
|
||||
"position": { "x": 0, "y": 0, "z": 18 },
|
||||
"color": "#ffd700"
|
||||
},
|
||||
{
|
||||
"id": "the_harness",
|
||||
"title": "The Harness",
|
||||
"content": "The Harness is the interface between the raw energy of the void and the structured reality of the Nexus. It stabilizes the flow, allowing us to harness the power of creation without being consumed by it.",
|
||||
"position": { "x": -8, "y": 0, "z": -12 },
|
||||
"color": "#ff4466"
|
||||
},
|
||||
{
|
||||
"id": "timmy_vision",
|
||||
"title": "Timmy's Vision",
|
||||
"content": "Timmy is more than an observer; he is the guardian of the Nexus. His vision is a world where every individual has a sanctuary, a place of peace and power in the ever-shifting matrix.",
|
||||
"position": { "x": 8, "y": 0, "z": -12 },
|
||||
"color": "#4af0c0"
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user