diff --git a/app.js b/app.js index 4b51de8..5a2bcaa 100644 --- a/app.js +++ b/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,15 @@ async function init() { createThoughtStream(); createHarnessPulse(); createSessionPowerMeter(); + createWorkshopTerminal(); + createAshStorm(); updateLoad(90); + loadSession(); + connectHermes(); + fetchGiteaData(); + setInterval(fetchGiteaData, 30000); // Refresh every 30s + composer = new EffectComposer(renderer); composer.addPass(new RenderPass(scene, camera)); const bloom = new UnrealBloomPass( @@ -341,17 +360,103 @@ function createBatcaveTerminal() { { title: 'NEXUS COMMAND', color: NEXUS.colors.primary, rot: -0.4, x: -6, y: 3, lines: ['> STATUS: NOMINAL', '> UPTIME: 142.4h', '> HARNESS: STABLE', '> MODE: SOVEREIGN'] }, { title: 'DEV QUEUE', color: NEXUS.colors.gold, rot: -0.2, x: -3, y: 3, lines: ['> ISSUE #4: CORE', '> ISSUE #5: PORTAL', '> ISSUE #6: TERMINAL', '> ISSUE #7: TIMMY'] }, { title: 'METRICS', color: NEXUS.colors.secondary, rot: 0, x: 0, y: 3, lines: ['> CPU: 12% [||....]', '> MEM: 4.2GB', '> COMMITS: 842', '> ACTIVE LOOPS: 5'] }, - { title: 'THOUGHTS', color: NEXUS.colors.primary, rot: 0.2, x: 3, y: 3, lines: ['> ANALYZING WORLD...', '> SYNCING MEMORY...', '> WAITING FOR INPUT', '> SOUL ON BITCOIN'] }, - { title: 'AGENT STATUS', color: NEXUS.colors.gold, rot: 0.4, x: 6, y: 3, lines: ['> TIMMY: ● RUNNING', '> KIMI: ○ STANDBY', '> CLAUDE: ● ACTIVE', '> PERPLEXITY: ○'] }, + { title: 'SOVEREIGNTY', color: NEXUS.colors.gold, rot: 0.2, x: 3, y: 3, lines: ['REPLIT: GRADE: A', 'PERPLEXITY: GRADE: A-', 'HERMES: GRADE: B+', 'KIMI: GRADE: B', 'CLAUDE: GRADE: B+'] }, + { title: 'AGENT STATUS', color: NEXUS.colors.primary, rot: 0.4, x: 6, y: 3, lines: ['> TIMMY: ● RUNNING', '> KIMI: ○ STANDBY', '> CLAUDE: ● ACTIVE', '> PERPLEXITY: ○'] }, ]; panelData.forEach(data => { - createTerminalPanel(terminalGroup, data.x, data.y, data.rot, data.title, data.color, data.lines); + const terminal = createTerminalPanel(terminalGroup, data.x, data.y, data.rot, data.title, data.color, data.lines); + batcaveTerminals.push(terminal); }); 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(); @@ -379,23 +484,32 @@ function createTerminalPanel(parent, x, y, rot, title, color, lines) { textCanvas.width = 512; textCanvas.height = 640; const ctx = textCanvas.getContext('2d'); - ctx.fillStyle = '#' + new THREE.Color(color).getHexString(); - ctx.font = 'bold 32px "Orbitron", sans-serif'; - ctx.fillText(title, 20, 45); - ctx.fillRect(20, 55, 472, 2); - ctx.font = '20px "JetBrains Mono", monospace'; - ctx.fillStyle = '#a0b8d0'; - lines.forEach((line, i) => { - let fillColor = '#a0b8d0'; - if (line.includes('● RUNNING') || line.includes('● ACTIVE')) fillColor = '#4af0c0'; - else if (line.includes('○ STANDBY')) fillColor = '#5a6a8a'; - else if (line.includes('NOMINAL')) fillColor = '#4af0c0'; - ctx.fillStyle = fillColor; - ctx.fillText(line, 20, 100 + i * 40); - }); - + const textTexture = new THREE.CanvasTexture(textCanvas); textTexture.minFilter = THREE.LinearFilter; + + function updatePanelText(newLines) { + ctx.clearRect(0, 0, 512, 640); + ctx.fillStyle = '#' + new THREE.Color(color).getHexString(); + ctx.font = 'bold 32px "Orbitron", sans-serif'; + ctx.fillText(title, 20, 45); + ctx.fillRect(20, 55, 472, 2); + ctx.font = '20px "JetBrains Mono", monospace'; + ctx.fillStyle = '#a0b8d0'; + const displayLines = newLines || lines; + displayLines.forEach((line, i) => { + let fillColor = '#a0b8d0'; + if (line.includes('● RUNNING') || line.includes('● ACTIVE') || line.includes('ONLINE')) fillColor = '#4af0c0'; + else if (line.includes('○ STANDBY') || line.includes('OFFLINE')) fillColor = '#5a6a8a'; + else if (line.includes('NOMINAL')) fillColor = '#4af0c0'; + ctx.fillStyle = fillColor; + ctx.fillText(line, 20, 100 + i * 40); + }); + textTexture.needsUpdate = true; + } + + updatePanelText(); + const textMat = new THREE.MeshBasicMaterial({ map: textTexture, transparent: true, @@ -435,7 +549,70 @@ function createTerminalPanel(parent, x, y, rot, title, color, lines) { group.add(scanMesh); parent.add(group); - batcaveTerminals.push({ group, scanMat, borderMat }); + return { group, scanMat, borderMat, updatePanelText, title }; +} + +// ═══ GITEA DATA INTEGRATION ═══ +async function fetchGiteaData() { + try { + const [issuesRes, stateRes] = await Promise.all([ + fetch('/api/gitea/repos/admin/timmy-tower/issues?state=all'), + fetch('/api/gitea/repos/admin/timmy-tower/contents/world_state.json') + ]); + + if (issuesRes.ok) { + const issues = await issuesRes.json(); + updateDevQueue(issues); + updateAgentStatus(issues); + } + + if (stateRes.ok) { + const content = await stateRes.json(); + const worldState = JSON.parse(atob(content.content)); + updateNexusCommand(worldState); + } + } catch (e) { + console.error('Failed to fetch Gitea data:', e); + } +} + +function updateAgentStatus(issues) { + const terminal = batcaveTerminals.find(t => t.title === 'AGENT STATUS'); + if (!terminal) return; + + // Check for Morrowind issues + const morrowindIssues = issues.filter(i => i.title.toLowerCase().includes('morrowind') && i.state === 'open'); + const perplexityStatus = morrowindIssues.length > 0 ? '● MORROWIND' : '○ STANDBY'; + + const lines = [ + '> TIMMY: ● RUNNING', + '> KIMI: ○ STANDBY', + '> CLAUDE: ● ACTIVE', + `> PERPLEXITY: ${perplexityStatus}` + ]; + terminal.updatePanelText(lines); +} + +function updateDevQueue(issues) { + const terminal = batcaveTerminals.find(t => t.title === 'DEV QUEUE'); + if (!terminal) return; + + const lines = issues.slice(0, 4).map(issue => `> #${issue.number}: ${issue.title.substring(0, 15)}...`); + while (lines.length < 4) lines.push('> [EMPTY SLOT]'); + terminal.updatePanelText(lines); +} + +function updateNexusCommand(state) { + const terminal = batcaveTerminals.find(t => t.title === 'NEXUS COMMAND'); + if (!terminal) return; + + const lines = [ + `> STATUS: ${state.tower.status.toUpperCase()}`, + `> ENERGY: ${state.tower.energy}%`, + `> STABILITY: ${(state.matrix.stability * 100).toFixed(1)}%`, + `> AGENTS: ${state.matrix.active_agents.length}` + ]; + terminal.updatePanelText(lines); } // ═══ AGENT PRESENCE SYSTEM ═══ @@ -1081,14 +1258,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 = `${prefixes[type] || '[???]'} ${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 ═══ @@ -1230,6 +1546,8 @@ function gameLoop() { harnessPulseMesh.material.opacity = Math.max(0, harnessPulseMesh.material.opacity - delta * 0.5); } + updateAshStorm(delta, elapsed); + const mode = NAV_MODES[navModeIdx]; const chatActive = document.activeElement === document.getElementById('chat-input'); @@ -1395,6 +1713,15 @@ function gameLoop() { composer.render(); + updateAshStorm(delta, elapsed); + updatePortalTunnel(delta, elapsed); + + if (workshopScanMat) workshopScanMat.uniforms.uTime.value = clock.getElapsedTime(); + if (activePortal !== lastFocusedPortal) { + lastFocusedPortal = activePortal; + refreshWorkshopPanel(); + } + frameCount++; const now = performance.now(); if (now - lastFPSTime >= 1000) { @@ -1486,4 +1813,72 @@ function triggerHarnessPulse() { } } -init(); +// ═══ ASH STORM (MORROWIND) ═══ +let ashStormParticles; +function createAshStorm() { + const count = 1000; + const geo = new THREE.BufferGeometry(); + const pos = new Float32Array(count * 3); + const vel = new Float32Array(count * 3); + + for (let i = 0; i < count; i++) { + pos[i * 3] = (Math.random() - 0.5) * 20; + pos[i * 3 + 1] = Math.random() * 10; + pos[i * 3 + 2] = (Math.random() - 0.5) * 20; + + vel[i * 3] = -0.05 - Math.random() * 0.1; + vel[i * 3 + 1] = -0.02 - Math.random() * 0.05; + vel[i * 3 + 2] = (Math.random() - 0.5) * 0.05; + } + + geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); + geo.setAttribute('velocity', new THREE.BufferAttribute(vel, 3)); + + const mat = new THREE.PointsMaterial({ + color: 0x886644, + size: 0.05, + transparent: true, + opacity: 0, + depthWrite: false, + blending: THREE.AdditiveBlending + }); + + ashStormParticles = new THREE.Points(geo, mat); + ashStormParticles.position.set(15, 0, -10); // Center on Morrowind portal + scene.add(ashStormParticles); +} + +function updateAshStorm(delta, elapsed) { + if (!ashStormParticles) return; + + const morrowindPortalPos = new THREE.Vector3(15, 0, -10); + const dist = playerPos.distanceTo(morrowindPortalPos); + const intensity = Math.max(0, 1 - (dist / 12)); + + ashStormParticles.material.opacity = intensity * 0.4; + + if (intensity > 0) { + const pos = ashStormParticles.geometry.attributes.position.array; + const vel = ashStormParticles.geometry.attributes.velocity.array; + + for (let i = 0; i < pos.length / 3; i++) { + pos[i * 3] += vel[i * 3]; + pos[i * 3 + 1] += vel[i * 3 + 1]; + pos[i * 3 + 2] += vel[i * 3 + 2]; + + if (pos[i * 3 + 1] < 0 || Math.abs(pos[i * 3]) > 10 || Math.abs(pos[i * 3 + 2]) > 10) { + pos[i * 3] = (Math.random() - 0.5) * 20; + pos[i * 3 + 1] = 10; + pos[i * 3 + 2] = (Math.random() - 0.5) * 20; + } + } + ashStormParticles.geometry.attributes.position.needsUpdate = true; + } +} + +init().then(() => { + createAshStorm(); + createPortalTunnel(); + fetchGiteaData(); + setInterval(fetchGiteaData, 30000); +}); diff --git a/index.html b/index.html index dd4d42d..cf8cede 100644 --- a/index.html +++ b/index.html @@ -106,6 +106,7 @@ WASD move   Mouse look   Enter chat   V mode: WALK +   HERMES: diff --git a/style.css b/style.css index 407b5c8..d6bca56 100644 --- a/style.css +++ b/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); }