BURN mode PR replaced the functional connectMemPalace() (which fetches real stats from Fleet API on port 7771) with a mock MCP server that returns hardcoded data and zeroes out stats. The mock definition (line 3907) overwrote the real one (line 2772) because it was defined later in the same scope. Fix: removed the mock connectMemPalace() definition. Now only the Fleet API version exists — fetches /health and /wings from port 7771, counts docs, computes compression ratio, updates UI. JS syntax verified. One connectMemPalace definition remains. Refs #1601
4035 lines
136 KiB
JavaScript
4035 lines
136 KiB
JavaScript
import ResonanceVisualizer from './nexus/components/resonance-visualizer.js';\nimport * as THREE from 'three';
|
||
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
||
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
||
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
||
import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
|
||
import { SpatialMemory } from './nexus/components/spatial-memory.js';
|
||
import { SpatialAudio } from './nexus/components/spatial-audio.js';
|
||
import { MemoryBirth } from './nexus/components/memory-birth.js';
|
||
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
|
||
import { MemoryInspect } from './nexus/components/memory-inspect.js';
|
||
import { MemoryPulse } from './nexus/components/memory-pulse.js';
|
||
import { ReasoningTrace } from './nexus/components/reasoning-trace.js';
|
||
|
||
// ═══════════════════════════════════════════
|
||
// NEXUS v1.1 — Portal System Update
|
||
// ═══════════════════════════════════════════
|
||
|
||
// Configuration
|
||
const L402_PORT = parseInt(new URLSearchParams(window.location.search).get('l402_port') || '8080');
|
||
const L402_URL = `http://localhost:${L402_PORT}/api/cost-estimate`;
|
||
|
||
const NEXUS = {
|
||
colors: {
|
||
primary: 0x4af0c0,
|
||
secondary: 0x7b5cff,
|
||
bg: 0x050510,
|
||
panelBg: 0x0a0f28,
|
||
nebula1: 0x1a0a3e,
|
||
nebula2: 0x0a1a3e,
|
||
gold: 0xffd700,
|
||
danger: 0xff4466,
|
||
gridLine: 0x1a2a4a,
|
||
}
|
||
};
|
||
|
||
// ═══ STATE ═══
|
||
let camera, scene, renderer, composer;
|
||
let clock, playerPos, playerRot;
|
||
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 atlasOverlayActive = false;
|
||
let thoughtStreamMesh;
|
||
let harnessPulseMesh;
|
||
let powerMeterBars = [];
|
||
let particles, dustParticles;
|
||
let debugOverlay;
|
||
let frameCount = 0, lastFPSTime = 0, fps = 0;
|
||
let chatOpen = true;
|
||
let memoryFeedEntries = []; // Mnemosyne: recent memory events for feed panel
|
||
let _memoryFilterOpen = false; // Mnemosyne: filter panel state
|
||
let _clickStartX = 0, _clickStartY = 0; // Mnemosyne: click-vs-drag detection
|
||
let loadProgress = 0;
|
||
let performanceTier = 'high';
|
||
|
||
/** Escape HTML entities for safe innerHTML insertion. */
|
||
function escHtml(s) {
|
||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||
}
|
||
|
||
// ═══ HERMES WS STATE ═══
|
||
let hermesWs = null;
|
||
let wsReconnectTimer = null;
|
||
let wsConnected = false;
|
||
// ═══ EVENNIA ROOM STATE ═══
|
||
let evenniaRoom = null; // {title, desc, exits[], objects[], occupants[], timestamp, roomKey}
|
||
let evenniaConnected = false;
|
||
let evenniaStaleTimer = null;
|
||
const EVENNIA_STALE_MS = 60000; // mark stale after 60s without update
|
||
let recentToolOutputs = [];
|
||
let actionStreamEntries = []; // Evennia command/result flow for action stream panel
|
||
let actionStreamRoom = ''; // Current room from movement events
|
||
let workshopPanelCtx = null;
|
||
let workshopPanelTexture = null;
|
||
let workshopPanelCanvas = null;
|
||
let workshopScanMat = null;
|
||
let workshopPanelRefreshTimer = 0;
|
||
let lastFocusedPortal = null;
|
||
|
||
// ═══ VISITOR / OPERATOR MODE ═══
|
||
let uiMode = 'visitor'; // 'visitor' | 'operator'
|
||
|
||
// ═══ NAVIGATION SYSTEM ═══
|
||
const NAV_MODES = ['walk', 'orbit', 'fly'];
|
||
let navModeIdx = 0;
|
||
|
||
const orbitState = {
|
||
target: new THREE.Vector3(0, 2, 0),
|
||
radius: 14,
|
||
theta: Math.PI,
|
||
phi: Math.PI / 6,
|
||
minR: 3,
|
||
maxR: 40,
|
||
lastX: 0,
|
||
lastY: 0,
|
||
};
|
||
|
||
let flyY = 2;
|
||
|
||
// ═══ INIT ═══
|
||
|
||
import {
|
||
SymbolicEngine, AgentFSM, KnowledgeGraph, Blackboard,
|
||
SymbolicPlanner, HTNPlanner, CaseBasedReasoner,
|
||
NeuroSymbolicBridge, MetaReasoningLayer
|
||
} from './nexus/symbolic-engine.js';
|
||
// ═══ SOVEREIGN SYMBOLIC ENGINE (GOFAI) ═══
|
||
class SymbolicEngine {
|
||
constructor() {
|
||
this.facts = new Map();
|
||
this.factIndices = new Map();
|
||
this.factMask = 0n;
|
||
this.rules = [];
|
||
this.reasoningLog = [];
|
||
}
|
||
|
||
addFact(key, value) {
|
||
this.facts.set(key, value);
|
||
if (!this.factIndices.has(key)) {
|
||
this.factIndices.set(key, BigInt(this.factIndices.size));
|
||
}
|
||
const bitIndex = this.factIndices.get(key);
|
||
if (value) {
|
||
this.factMask |= (1n << bitIndex);
|
||
} else {
|
||
this.factMask &= ~(1n << bitIndex);
|
||
}
|
||
}
|
||
|
||
addRule(condition, action, description, triggerFacts = []) {
|
||
this.rules.push({ condition, action, description, triggerFacts });
|
||
}
|
||
|
||
reason() {
|
||
this.rules.forEach(rule => {
|
||
if (rule.condition(this.facts)) {
|
||
const result = rule.action(this.facts);
|
||
if (result) {
|
||
this.logReasoning(rule.description, result);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
logReasoning(ruleDesc, outcome) {
|
||
const entry = { timestamp: Date.now(), rule: ruleDesc, outcome: outcome };
|
||
this.reasoningLog.unshift(entry);
|
||
if (this.reasoningLog.length > 5) this.reasoningLog.pop();
|
||
|
||
const container = document.getElementById('symbolic-log-content');
|
||
if (container) {
|
||
const logDiv = document.createElement('div');
|
||
logDiv.className = 'symbolic-log-entry';
|
||
logDiv.innerHTML = `<span class="symbolic-rule">[RULE] ${ruleDesc}</span><span class="symbolic-outcome">→ ${outcome}</span>`;
|
||
container.prepend(logDiv);
|
||
if (container.children.length > 5) container.lastElementChild.remove();
|
||
}
|
||
}
|
||
}
|
||
|
||
class AgentFSM {
|
||
constructor(agentId, initialState) {
|
||
this.agentId = agentId;
|
||
this.state = initialState;
|
||
this.transitions = {};
|
||
}
|
||
|
||
addTransition(fromState, toState, condition) {
|
||
if (!this.transitions[fromState]) this.transitions[fromState] = [];
|
||
this.transitions[fromState].push({ toState, condition });
|
||
}
|
||
|
||
update(facts) {
|
||
const possibleTransitions = this.transitions[this.state] || [];
|
||
for (const transition of possibleTransitions) {
|
||
if (transition.condition(facts)) {
|
||
console.log(`[FSM] Agent ${this.agentId} transitioning: ${this.state} -> ${transition.toState}`);
|
||
this.state = transition.toState;
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
|
||
class KnowledgeGraph {
|
||
constructor() {
|
||
this.nodes = new Map();
|
||
this.edges = [];
|
||
}
|
||
|
||
addNode(id, type, metadata = {}) {
|
||
this.nodes.set(id, { id, type, ...metadata });
|
||
}
|
||
|
||
addEdge(from, to, relation) {
|
||
this.edges.push({ from, to, relation });
|
||
}
|
||
|
||
query(from, relation) {
|
||
return this.edges
|
||
.filter(e => e.from === from && e.relation === relation)
|
||
.map(e => this.nodes.get(e.to));
|
||
}
|
||
}
|
||
|
||
class Blackboard {
|
||
constructor() {
|
||
this.data = {};
|
||
this.subscribers = [];
|
||
}
|
||
|
||
write(key, value, source) {
|
||
const oldValue = this.data[key];
|
||
this.data[key] = value;
|
||
this.notify(key, value, oldValue, source);
|
||
}
|
||
|
||
read(key) { return this.data[key]; }
|
||
|
||
subscribe(callback) { this.subscribers.push(callback); }
|
||
|
||
notify(key, value, oldValue, source) {
|
||
this.subscribers.forEach(sub => sub(key, value, oldValue, source));
|
||
const container = document.getElementById('blackboard-log-content');
|
||
if (container) {
|
||
const entry = document.createElement('div');
|
||
entry.className = 'blackboard-entry';
|
||
entry.innerHTML = `<span class="bb-source">[${source}]</span> <span class="bb-key">${key}</span>: <span class="bb-value">${JSON.stringify(value)}</span>`;
|
||
container.prepend(entry);
|
||
if (container.children.length > 8) container.lastElementChild.remove();
|
||
}
|
||
}
|
||
}
|
||
|
||
class SymbolicPlanner {
|
||
constructor() {
|
||
this.actions = [];
|
||
this.currentPlan = [];
|
||
}
|
||
|
||
addAction(name, preconditions, effects) {
|
||
this.actions.push({ name, preconditions, effects });
|
||
}
|
||
|
||
heuristic(state, goal) {
|
||
let h = 0;
|
||
for (let key in goal) {
|
||
if (state[key] !== goal[key]) {
|
||
h += Math.abs((state[key] || 0) - (goal[key] || 0));
|
||
}
|
||
}
|
||
return h;
|
||
}
|
||
|
||
findPlan(initialState, goalState) {
|
||
let openSet = [{ state: initialState, plan: [], g: 0, h: this.heuristic(initialState, goalState) }];
|
||
let visited = new Map();
|
||
visited.set(JSON.stringify(initialState), 0);
|
||
|
||
while (openSet.length > 0) {
|
||
openSet.sort((a, b) => (a.g + a.h) - (b.g + b.h));
|
||
let { state, plan, g } = openSet.shift();
|
||
|
||
if (this.isGoalReached(state, goalState)) return plan;
|
||
|
||
for (let action of this.actions) {
|
||
if (this.arePreconditionsMet(state, action.preconditions)) {
|
||
let nextState = { ...state, ...action.effects };
|
||
let stateStr = JSON.stringify(nextState);
|
||
let nextG = g + 1;
|
||
|
||
if (!visited.has(stateStr) || nextG < visited.get(stateStr)) {
|
||
visited.set(stateStr, nextG);
|
||
openSet.push({
|
||
state: nextState,
|
||
plan: [...plan, action.name],
|
||
g: nextG,
|
||
h: this.heuristic(nextState, goalState)
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
isGoalReached(state, goal) {
|
||
for (let key in goal) {
|
||
if (state[key] !== goal[key]) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
arePreconditionsMet(state, preconditions) {
|
||
for (let key in preconditions) {
|
||
if (state[key] < preconditions[key]) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
logPlan(plan) {
|
||
this.currentPlan = plan;
|
||
const container = document.getElementById('planner-log-content');
|
||
if (container) {
|
||
container.innerHTML = '';
|
||
if (!plan || plan.length === 0) {
|
||
container.innerHTML = '<div class="planner-empty">NO ACTIVE PLAN</div>';
|
||
return;
|
||
}
|
||
plan.forEach((step, i) => {
|
||
const div = document.createElement('div');
|
||
div.className = 'planner-step';
|
||
div.innerHTML = `<span class="step-num">${i+1}.</span> ${step}`;
|
||
container.appendChild(div);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
class HTNPlanner {
|
||
constructor() {
|
||
this.methods = {};
|
||
this.primitiveTasks = {};
|
||
}
|
||
|
||
addMethod(taskName, preconditions, subtasks) {
|
||
if (!this.methods[taskName]) this.methods[taskName] = [];
|
||
this.methods[taskName].push({ preconditions, subtasks });
|
||
}
|
||
|
||
addPrimitiveTask(taskName, preconditions, effects) {
|
||
this.primitiveTasks[taskName] = { preconditions, effects };
|
||
}
|
||
|
||
findPlan(initialState, tasks) {
|
||
return this.decompose(initialState, tasks, []);
|
||
}
|
||
|
||
decompose(state, tasks, plan) {
|
||
if (tasks.length === 0) return plan;
|
||
const [task, ...remainingTasks] = tasks;
|
||
if (this.primitiveTasks[task]) {
|
||
const { preconditions, effects } = this.primitiveTasks[task];
|
||
if (this.arePreconditionsMet(state, preconditions)) {
|
||
const nextState = { ...state, ...effects };
|
||
return this.decompose(nextState, remainingTasks, [...plan, task]);
|
||
}
|
||
return null;
|
||
}
|
||
const methods = this.methods[task] || [];
|
||
for (const method of methods) {
|
||
if (this.arePreconditionsMet(state, method.preconditions)) {
|
||
const result = this.decompose(state, [...method.subtasks, ...remainingTasks], plan);
|
||
if (result) return result;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
arePreconditionsMet(state, preconditions) {
|
||
for (const key in preconditions) {
|
||
if (state[key] < (preconditions[key] || 0)) return false;
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
|
||
class CaseBasedReasoner {
|
||
constructor() {
|
||
this.caseLibrary = [];
|
||
}
|
||
|
||
addCase(situation, action, outcome) {
|
||
this.caseLibrary.push({ situation, action, outcome, timestamp: Date.now() });
|
||
}
|
||
|
||
findSimilarCase(currentSituation) {
|
||
let bestMatch = null;
|
||
let maxSimilarity = -1;
|
||
this.caseLibrary.forEach(c => {
|
||
let similarity = this.calculateSimilarity(currentSituation, c.situation);
|
||
if (similarity > maxSimilarity) {
|
||
maxSimilarity = similarity;
|
||
bestMatch = c;
|
||
}
|
||
});
|
||
return maxSimilarity > 0.7 ? bestMatch : null;
|
||
}
|
||
|
||
calculateSimilarity(s1, s2) {
|
||
let score = 0, total = 0;
|
||
for (let key in s1) {
|
||
if (s2[key] !== undefined) {
|
||
score += 1 - Math.abs(s1[key] - s2[key]);
|
||
total += 1;
|
||
}
|
||
}
|
||
return total > 0 ? score / total : 0;
|
||
}
|
||
|
||
logCase(c) {
|
||
const container = document.getElementById('cbr-log-content');
|
||
if (container) {
|
||
const div = document.createElement('div');
|
||
div.className = 'cbr-entry';
|
||
div.innerHTML = `
|
||
<div class="cbr-match">SIMILAR CASE FOUND (${(this.calculateSimilarity(symbolicEngine.facts, c.situation) * 100).toFixed(0)}%)</div>
|
||
<div class="cbr-action">SUGGESTED: ${c.action}</div>
|
||
<div class="cbr-outcome">PREVIOUS OUTCOME: ${c.outcome}</div>
|
||
`;
|
||
container.prepend(div);
|
||
if (container.children.length > 3) container.lastElementChild.remove();
|
||
}
|
||
}
|
||
}
|
||
|
||
class NeuroSymbolicBridge {
|
||
constructor(symbolicEngine, blackboard) {
|
||
this.engine = symbolicEngine;
|
||
this.blackboard = blackboard;
|
||
this.perceptionLog = [];
|
||
}
|
||
|
||
perceive(rawState) {
|
||
Object.entries(rawState).forEach(([key, value]) => this.engine.addFact(key, value));
|
||
const concepts = [];
|
||
if (rawState.stability < 0.4 && rawState.energy > 60) concepts.push('UNSTABLE_OSCILLATION');
|
||
if (rawState.energy < 30 && rawState.activePortals > 2) concepts.push('CRITICAL_DRAIN_PATTERN');
|
||
concepts.forEach(concept => {
|
||
this.engine.addFact(concept, true);
|
||
this.logPerception(concept);
|
||
});
|
||
return concepts;
|
||
}
|
||
|
||
logPerception(concept) {
|
||
const container = document.getElementById('neuro-bridge-log-content');
|
||
if (container) {
|
||
const div = document.createElement('div');
|
||
div.className = 'neuro-bridge-entry';
|
||
div.innerHTML = `<span class="neuro-icon">🧠</span> <span class="neuro-concept">${concept}</span>`;
|
||
container.prepend(div);
|
||
if (container.children.length > 5) container.lastElementChild.remove();
|
||
}
|
||
}
|
||
}
|
||
|
||
class MetaReasoningLayer {
|
||
constructor(planner, blackboard) {
|
||
this.planner = planner;
|
||
this.blackboard = blackboard;
|
||
this.reasoningCache = new Map();
|
||
this.performanceMetrics = { totalReasoningTime: 0, calls: 0 };
|
||
}
|
||
|
||
getCachedPlan(stateKey) {
|
||
const cached = this.reasoningCache.get(stateKey);
|
||
if (cached && (Date.now() - cached.timestamp < 10000)) return cached.plan;
|
||
return null;
|
||
}
|
||
|
||
cachePlan(stateKey, plan) {
|
||
this.reasoningCache.set(stateKey, { plan, timestamp: Date.now() });
|
||
}
|
||
|
||
reflect() {
|
||
const avgTime = this.performanceMetrics.totalReasoningTime / (this.performanceMetrics.calls || 1);
|
||
const container = document.getElementById('meta-log-content');
|
||
if (container) {
|
||
container.innerHTML = `
|
||
<div class="meta-stat">CACHE SIZE: ${this.reasoningCache.size}</div>
|
||
<div class="meta-stat">AVG LATENCY: ${avgTime.toFixed(2)}ms</div>
|
||
<div class="meta-stat">STATUS: ${avgTime > 50 ? 'OPTIMIZING' : 'NOMINAL'}</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
track(startTime) {
|
||
const duration = performance.now() - startTime;
|
||
this.performanceMetrics.totalReasoningTime += duration;
|
||
this.performanceMetrics.calls++;
|
||
}
|
||
}
|
||
|
||
// ═══ ADAPTIVE CALIBRATOR (LOCAL EFFICIENCY) ═══
|
||
class AdaptiveCalibrator {
|
||
constructor(modelId, initialParams) {
|
||
this.model = modelId;
|
||
this.weights = {
|
||
'input_tokens': 0.0,
|
||
'complexity_score': 0.0,
|
||
'task_type_indicator': 0.0,
|
||
'bias': initialParams.base_rate || 0.0
|
||
};
|
||
this.learningRate = 0.01;
|
||
this.history = [];
|
||
}
|
||
|
||
predict(features) {
|
||
let prediction = this.weights['bias'];
|
||
for (let feature in features) {
|
||
if (this.weights[feature] !== undefined) {
|
||
prediction += this.weights[feature] * features[feature];
|
||
}
|
||
}
|
||
return Math.max(0, prediction);
|
||
}
|
||
|
||
update(features, actualCost) {
|
||
const predicted = this.predict(features);
|
||
const error = actualCost - predicted;
|
||
for (let feature in features) {
|
||
if (this.weights[feature] !== undefined) {
|
||
this.weights[feature] += this.learningRate * error * features[feature];
|
||
}
|
||
}
|
||
this.history.push({ predicted, actual: actualCost, timestamp: Date.now() });
|
||
|
||
const container = document.getElementById('calibrator-log-content');
|
||
if (container) {
|
||
const div = document.createElement('div');
|
||
div.className = 'calibrator-entry';
|
||
div.innerHTML = `<span class="cal-label">CALIBRATED:</span> <span class="cal-val">${predicted.toFixed(4)}</span> <span class="cal-err">ERR: ${error.toFixed(4)}</span>`;
|
||
container.prepend(div);
|
||
if (container.children.length > 5) container.lastElementChild.remove();
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// ═══ NOSTR AGENT REGISTRATION ═══
|
||
class NostrAgent {
|
||
constructor(pubkey) {
|
||
this.pubkey = pubkey;
|
||
this.relays = ['wss://relay.damus.io', 'wss://nos.lol'];
|
||
}
|
||
|
||
async announce(metadata) {
|
||
console.log(`[NOSTR] Announcing agent ${this.pubkey}...`);
|
||
const event = {
|
||
kind: 0,
|
||
pubkey: this.pubkey,
|
||
created_at: Math.floor(Date.now() / 1000),
|
||
tags: [],
|
||
content: JSON.stringify(metadata),
|
||
id: 'mock_id',
|
||
sig: 'mock_sig'
|
||
};
|
||
|
||
this.relays.forEach(url => {
|
||
console.log(`[NOSTR] Publishing to ${url}: `, event);
|
||
});
|
||
|
||
const container = document.getElementById('nostr-log-content');
|
||
if (container) {
|
||
const div = document.createElement('div');
|
||
div.className = 'nostr-entry';
|
||
div.innerHTML = `<span class="nostr-pubkey">[${this.pubkey.substring(0,8)}...]</span> <span class="nostr-status">ANNOUNCED</span>`;
|
||
container.prepend(div);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ═══ L402 CLIENT LOGIC ═══
|
||
class L402Client {
|
||
async fetchWithL402(url) {
|
||
console.log(`[L402] Fetching ${url}...`);
|
||
const response = await fetch(url);
|
||
|
||
if (response.status === 402) {
|
||
const authHeader = response.headers.get('WWW-Authenticate');
|
||
console.log(`[L402] Challenge received: ${authHeader}`);
|
||
|
||
const container = document.getElementById('l402-log-content');
|
||
if (container) {
|
||
const div = document.createElement('div');
|
||
div.className = 'l402-entry';
|
||
div.innerHTML = `<span class="l402-status">CHALLENGE</span> <span class="l402-msg">Payment Required</span>`;
|
||
container.prepend(div);
|
||
}
|
||
return { status: 402, challenge: authHeader };
|
||
}
|
||
|
||
return response.json();
|
||
}
|
||
}
|
||
|
||
let nostrAgent, l402Client;
|
||
|
||
|
||
// ═══ PARALLEL SYMBOLIC EXECUTION (PSE) ═══
|
||
class PSELayer {
|
||
constructor() {
|
||
this.worker = new Worker('gofai_worker.js');
|
||
this.worker.onmessage = (e) => this.handleWorkerMessage(e);
|
||
}
|
||
|
||
handleWorkerMessage(e) {
|
||
const { type, results, plan } = e.data;
|
||
if (type === 'REASON_RESULT') {
|
||
results.forEach(res => symbolicEngine.logReasoning(res.rule, res.outcome));
|
||
} else if (type === 'PLAN_RESULT') {
|
||
symbolicPlanner.logPlan(plan);
|
||
}
|
||
}
|
||
|
||
offloadReasoning(facts, rules) {
|
||
this.worker.postMessage({ type: 'REASON', data: { facts, rules } });
|
||
}
|
||
|
||
offloadPlanning(initialState, goalState, actions) {
|
||
this.worker.postMessage({ type: 'PLAN', data: { initialState, goalState, actions } });
|
||
}
|
||
}
|
||
|
||
let pseLayer;
|
||
|
||
let resonanceViz, metaLayer, neuroBridge, cbr, symbolicPlanner, knowledgeGraph, blackboard, symbolicEngine, calibrator;
|
||
let agentFSMs = {};
|
||
|
||
function setupGOFAI() {
|
||
knowledgeGraph = new KnowledgeGraph();
|
||
blackboard = new Blackboard();
|
||
symbolicEngine = new SymbolicEngine();
|
||
symbolicPlanner = new SymbolicPlanner();
|
||
cbr = new CaseBasedReasoner();
|
||
neuroBridge = new NeuroSymbolicBridge(symbolicEngine, blackboard);
|
||
metaLayer = new MetaReasoningLayer(symbolicPlanner, blackboard);
|
||
nostrAgent = new NostrAgent("npub1...");
|
||
l402Client = new L402Client();
|
||
nostrAgent.announce({ name: "Timmy Nexus Agent", capabilities: ["GOFAI", "L402"] });
|
||
pseLayer = new PSELayer();
|
||
calibrator = new AdaptiveCalibrator('nexus-v1', { base_rate: 0.05 });\n MemoryOptimizer.blackboard = blackboard;
|
||
|
||
// Setup initial facts
|
||
symbolicEngine.addFact('energy', 100);
|
||
symbolicEngine.addFact('stability', 1.0);
|
||
|
||
// Setup FSM
|
||
agentFSMs['timmy'] = new AgentFSM('timmy', 'IDLE');
|
||
agentFSMs['timmy'].addTransition('IDLE', 'ANALYZING', (facts) => facts.get('activePortals') > 0);
|
||
|
||
symbolicEngine.addRule((facts) => facts.get('UNSTABLE_OSCILLATION'), () => 'STABILIZE MATRIX', 'Unstable oscillation demands stabilization', ['UNSTABLE_OSCILLATION']);
|
||
symbolicEngine.addRule((facts) => facts.get('CRITICAL_DRAIN_PATTERN'), () => 'SHED PORTAL LOAD', 'Critical drain demands portal shedding', ['CRITICAL_DRAIN_PATTERN']);
|
||
|
||
// Setup Planner
|
||
symbolicPlanner.addAction('Stabilize Matrix', { energy: 50 }, { stability: 1.0 });
|
||
symbolicPlanner.addAction('Shed Portal Load', { activePortals: 1 }, { activePortals: 0, stability: 0.8 });
|
||
}
|
||
|
||
function deriveGOFAIState(elapsed) {
|
||
const activeBars = powerMeterBars.reduce((n, _, i) => n + ((((Math.sin(elapsed * 2 + i * 0.5) * 0.5) + 0.5) > (i / Math.max(powerMeterBars.length, 1))) ? 1 : 0), 0);
|
||
const energy = Math.round((activeBars / Math.max(powerMeterBars.length, 1)) * 100);
|
||
const stability = Math.max(0.1, Math.min(1, (wsConnected ? 0.55 : 0.2) + (agents.length * 0.05) - (portals.length * 0.03) - (activePortal ? 0.1 : 0) - (portalOverlayActive ? 0.05 : 0)));
|
||
return { stability, energy, activePortals: activePortal ? 1 : 0 };
|
||
}
|
||
|
||
function deriveGOFAIGoal(facts) {
|
||
if (facts.get('CRITICAL_DRAIN_PATTERN')) return { activePortals: 0, stability: 0.8 };
|
||
if (facts.get('UNSTABLE_OSCILLATION')) return { stability: 1.0 };
|
||
return { stability: Math.max(0.7, facts.get('stability') || 0.7) };
|
||
}
|
||
|
||
function updateGOFAI(delta, elapsed) {
|
||
const startTime = performance.now();
|
||
|
||
neuroBridge.perceive(deriveGOFAIState(elapsed));
|
||
agentFSMs['timmy']?.update(symbolicEngine.facts);
|
||
|
||
// Run reasoning
|
||
if (Math.floor(elapsed * 2) > Math.floor((elapsed - delta) * 2)) {
|
||
symbolicEngine.reason();
|
||
pseLayer.offloadReasoning(Array.from(symbolicEngine.facts.entries()), symbolicEngine.rules.map((r) => ({ description: r.description, triggerFacts: r.triggerFacts, workerOutcome: r.action(symbolicEngine.facts), confidence: 0.9 })));
|
||
pseLayer.offloadPlanning(Object.fromEntries(symbolicEngine.facts), deriveGOFAIGoal(symbolicEngine.facts), symbolicPlanner.actions);
|
||
document.getElementById("pse-task-count").innerText = parseInt(document.getElementById("pse-task-count").innerText) + 1;
|
||
metaLayer.reflect();
|
||
|
||
// Simulate calibration update
|
||
calibrator.update({ input_tokens: 100, complexity_score: 0.5 }, 0.06);
|
||
if (Math.random() > 0.95) l402Client.fetchWithL402(L402_URL);
|
||
}
|
||
|
||
metaLayer.track(startTime);
|
||
}
|
||
|
||
async function init() {
|
||
clock = new THREE.Clock();
|
||
playerPos = new THREE.Vector3(0, 2, 12);
|
||
playerRot = new THREE.Euler(0, 0, 0, 'YXZ');
|
||
|
||
const canvas = document.getElementById('nexus-canvas');
|
||
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||
renderer.toneMappingExposure = 1.2;
|
||
renderer.shadowMap.enabled = true;
|
||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||
|
||
performanceTier = detectPerformanceTier();
|
||
updateLoad(10);
|
||
|
||
scene = new THREE.Scene();
|
||
scene.fog = new THREE.FogExp2(0x050510, 0.012);
|
||
|
||
setupGOFAI();\n resonanceViz = new ResonanceVisualizer(scene);
|
||
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||
camera.position.copy(playerPos);
|
||
|
||
// Initialize avatar and LOD systems
|
||
if (window.AvatarCustomization) window.AvatarCustomization.init(scene, camera);
|
||
if (window.LODSystem) window.LODSystem.init(scene, camera);
|
||
|
||
updateLoad(20);
|
||
|
||
createSkybox();
|
||
updateLoad(30);
|
||
createLighting();
|
||
updateLoad(40);
|
||
createFloor();
|
||
updateLoad(50);
|
||
createBatcaveTerminal();
|
||
updateLoad(60);
|
||
|
||
// Load Portals from Registry
|
||
try {
|
||
const response = await fetch('./portals.json');
|
||
const portalData = await response.json();
|
||
createPortals(portalData);
|
||
} catch (e) {
|
||
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);
|
||
if (performanceTier !== "low") createAmbientStructures();
|
||
createAgentPresences();
|
||
if (performanceTier !== "low") createThoughtStream();
|
||
createHarnessPulse();
|
||
createSessionPowerMeter();
|
||
createWorkshopTerminal();
|
||
if (performanceTier !== "low") createAshStorm();
|
||
SpatialMemory.init(scene);
|
||
MemoryBirth.init(scene);
|
||
MemoryBirth.wrapSpatialMemory(SpatialMemory);
|
||
SpatialMemory.setCamera(camera);
|
||
SpatialAudio.init(camera, scene);
|
||
SpatialAudio.bindSpatialMemory(SpatialMemory);
|
||
MemoryInspect.init({ onNavigate: _navigateToMemory });
|
||
MemoryPulse.init(SpatialMemory);
|
||
ReasoningTrace.init();
|
||
updateLoad(90);
|
||
|
||
loadSession();
|
||
connectHermes();
|
||
// Mnemosyne: Periodic GOFAI Optimization
|
||
setInterval(() => {
|
||
console.info('[Mnemosyne] Running periodic optimization...');
|
||
MemoryOptimizer.optimize(SpatialMemory);
|
||
}, 1000 * 60 * 10); // Every 10 minutes
|
||
|
||
fetchGiteaData();
|
||
setInterval(fetchGiteaData, 30000); // Refresh every 30s
|
||
|
||
// Quality-tier feature gating: only enable heavy post-processing on medium/high
|
||
if (performanceTier !== 'low') {
|
||
composer = new EffectComposer(renderer);
|
||
composer.addPass(new RenderPass(scene, camera));
|
||
const bloomStrength = performanceTier === 'high' ? 0.6 : 0.35;
|
||
const bloom = new UnrealBloomPass(
|
||
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
||
bloomStrength, 0.4, 0.85
|
||
);
|
||
composer.addPass(bloom);
|
||
composer.addPass(new SMAAPass(window.innerWidth, window.innerHeight));
|
||
} else {
|
||
composer = null;
|
||
}
|
||
|
||
updateLoad(95);
|
||
|
||
setupControls();
|
||
window.addEventListener('resize', onResize);
|
||
debugOverlay = document.getElementById('debug-overlay');
|
||
|
||
updateLoad(100);
|
||
|
||
setTimeout(() => {
|
||
document.getElementById('loading-screen').classList.add('fade-out');
|
||
const enterPrompt = document.getElementById('enter-prompt');
|
||
enterPrompt.style.display = 'flex';
|
||
|
||
enterPrompt.addEventListener('click', () => {
|
||
enterPrompt.classList.add('fade-out');
|
||
document.body.classList.add('visitor-mode');
|
||
document.getElementById('hud').style.display = 'block';
|
||
const erpPanel = document.getElementById('evennia-room-panel');
|
||
if (erpPanel) erpPanel.style.display = 'block';
|
||
setTimeout(() => { enterPrompt.remove(); }, 600);
|
||
}, { once: true });
|
||
|
||
setTimeout(() => { document.getElementById('loading-screen').remove(); }, 900);
|
||
}, 600);
|
||
|
||
requestAnimationFrame(gameLoop);
|
||
}
|
||
|
||
function updateLoad(pct) {
|
||
loadProgress = pct;
|
||
const fill = document.getElementById('load-progress');
|
||
if (fill) fill.style.width = pct + '%';
|
||
}
|
||
|
||
// ═══ PERFORMANCE BUDGET ═══
|
||
function detectPerformanceTier() {
|
||
const isMobile = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || window.innerWidth < 768;
|
||
const cores = navigator.hardwareConcurrency || 4;
|
||
|
||
if (isMobile) {
|
||
renderer.setPixelRatio(1);
|
||
renderer.shadowMap.enabled = false;
|
||
return 'low';
|
||
} else if (cores < 8) {
|
||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
|
||
renderer.shadowMap.type = THREE.BasicShadowMap;
|
||
return 'medium';
|
||
} else {
|
||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||
return 'high';
|
||
}
|
||
}
|
||
|
||
function particleCount(base) {
|
||
if (performanceTier === 'low') return Math.floor(base * 0.25);
|
||
if (performanceTier === 'medium') return Math.floor(base * 0.6);
|
||
return base;
|
||
}
|
||
|
||
// ═══ SKYBOX ═══
|
||
function createSkybox() {
|
||
const skyGeo = new THREE.SphereGeometry(400, 64, 64);
|
||
const skyMat = new THREE.ShaderMaterial({
|
||
uniforms: {
|
||
uTime: { value: 0 },
|
||
uColor1: { value: new THREE.Color(0x0a0520) },
|
||
uColor2: { value: new THREE.Color(0x1a0a3e) },
|
||
uColor3: { value: new THREE.Color(0x0a1a3e) },
|
||
uStarDensity: { value: 0.97 },
|
||
},
|
||
vertexShader: `
|
||
varying vec3 vPos;
|
||
void main() {
|
||
vPos = position;
|
||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||
}
|
||
`,
|
||
fragmentShader: `
|
||
uniform float uTime;
|
||
uniform vec3 uColor1;
|
||
uniform vec3 uColor2;
|
||
uniform vec3 uColor3;
|
||
uniform float uStarDensity;
|
||
varying vec3 vPos;
|
||
|
||
float hash(vec3 p) {
|
||
p = fract(p * vec3(443.897, 441.423, 437.195));
|
||
p += dot(p, p.yzx + 19.19);
|
||
return fract((p.x + p.y) * p.z);
|
||
}
|
||
|
||
float noise(vec3 p) {
|
||
vec3 i = floor(p);
|
||
vec3 f = fract(p);
|
||
f = f * f * (3.0 - 2.0 * f);
|
||
return mix(
|
||
mix(mix(hash(i), hash(i + vec3(1,0,0)), f.x),
|
||
mix(hash(i + vec3(0,1,0)), hash(i + vec3(1,1,0)), f.x), f.y),
|
||
mix(mix(hash(i + vec3(0,0,1)), hash(i + vec3(1,0,1)), f.x),
|
||
mix(hash(i + vec3(0,1,1)), hash(i + vec3(1,1,1)), f.x), f.y),
|
||
f.z
|
||
);
|
||
}
|
||
|
||
float fbm(vec3 p) {
|
||
float v = 0.0;
|
||
float a = 0.5;
|
||
for (int i = 0; i < 5; i++) {
|
||
v += a * noise(p);
|
||
p *= 2.0;
|
||
a *= 0.5;
|
||
}
|
||
return v;
|
||
}
|
||
|
||
void main() {
|
||
vec3 dir = normalize(vPos);
|
||
float n1 = fbm(dir * 3.0 + uTime * 0.02);
|
||
float n2 = fbm(dir * 5.0 - uTime * 0.015 + 100.0);
|
||
float n3 = fbm(dir * 2.0 + uTime * 0.01 + 200.0);
|
||
|
||
vec3 col = uColor1;
|
||
col = mix(col, uColor2, smoothstep(0.3, 0.7, n1));
|
||
col = mix(col, uColor3, smoothstep(0.4, 0.8, n2) * 0.5);
|
||
|
||
float glow = pow(n1 * n2, 2.0) * 1.5;
|
||
col += vec3(0.15, 0.05, 0.25) * glow;
|
||
col += vec3(0.05, 0.15, 0.25) * pow(n3, 3.0);
|
||
|
||
float starField = hash(dir * 800.0);
|
||
float stars = step(uStarDensity, starField) * (0.5 + 0.5 * hash(dir * 1600.0));
|
||
float twinkle = 0.7 + 0.3 * sin(uTime * 2.0 + hash(dir * 400.0) * 6.28);
|
||
col += vec3(stars * twinkle);
|
||
|
||
float bigStar = step(0.998, starField);
|
||
col += vec3(0.8, 0.9, 1.0) * bigStar * twinkle;
|
||
|
||
gl_FragColor = vec4(col, 1.0);
|
||
}
|
||
`,
|
||
side: THREE.BackSide,
|
||
});
|
||
const sky = new THREE.Mesh(skyGeo, skyMat);
|
||
sky.name = 'skybox';
|
||
scene.add(sky);
|
||
}
|
||
|
||
// ═══ LIGHTING ═══
|
||
function createLighting() {
|
||
const ambient = new THREE.AmbientLight(0x1a1a3a, 0.4);
|
||
scene.add(ambient);
|
||
|
||
const dirLight = new THREE.DirectionalLight(0x4466aa, 0.6);
|
||
dirLight.position.set(10, 20, 10);
|
||
dirLight.castShadow = renderer.shadowMap.enabled;
|
||
const shadowRes = performanceTier === 'high' ? 2048 : performanceTier === 'medium' ? 1024 : 512;
|
||
dirLight.shadow.mapSize.set(shadowRes, shadowRes);
|
||
scene.add(dirLight);
|
||
|
||
const tealLight = new THREE.PointLight(NEXUS.colors.primary, 2, 30, 1.5);
|
||
tealLight.position.set(0, 1, -5);
|
||
scene.add(tealLight);
|
||
|
||
const purpleLight = new THREE.PointLight(NEXUS.colors.secondary, 1.5, 25, 1.5);
|
||
purpleLight.position.set(-8, 3, -8);
|
||
scene.add(purpleLight);
|
||
}
|
||
|
||
// ═══ FLOOR ═══
|
||
function createFloor() {
|
||
const platGeo = new THREE.CylinderGeometry(25, 25, 0.3, 6);
|
||
const platMat = new THREE.MeshStandardMaterial({
|
||
color: 0x0a0f1a,
|
||
roughness: 0.8,
|
||
metalness: 0.3,
|
||
});
|
||
const platform = new THREE.Mesh(platGeo, platMat);
|
||
platform.position.y = -0.15;
|
||
platform.receiveShadow = true;
|
||
scene.add(platform);
|
||
|
||
const gridHelper = new THREE.GridHelper(50, 50, NEXUS.colors.gridLine, NEXUS.colors.gridLine);
|
||
gridHelper.material.opacity = 0.15;
|
||
gridHelper.material.transparent = true;
|
||
gridHelper.position.y = 0.02;
|
||
scene.add(gridHelper);
|
||
|
||
const ringGeo = new THREE.RingGeometry(24.5, 25.2, 6);
|
||
const ringMat = new THREE.MeshBasicMaterial({
|
||
color: NEXUS.colors.primary,
|
||
transparent: true,
|
||
opacity: 0.4,
|
||
side: THREE.DoubleSide,
|
||
});
|
||
const ring = new THREE.Mesh(ringGeo, ringMat);
|
||
ring.rotation.x = Math.PI / 2;
|
||
ring.position.y = 0.05;
|
||
scene.add(ring);
|
||
}
|
||
|
||
// ═══ BATCAVE TERMINAL ═══
|
||
function createBatcaveTerminal() {
|
||
const terminalGroup = new THREE.Group();
|
||
terminalGroup.position.set(0, 0, -8);
|
||
|
||
const panelData = [
|
||
{ 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: '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 => {
|
||
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();
|
||
group.position.set(x, y, 0);
|
||
group.rotation.y = rot;
|
||
|
||
const bgGeo = new THREE.PlaneGeometry(w, h);
|
||
const bgMat = new THREE.MeshPhysicalMaterial({
|
||
color: NEXUS.colors.panelBg,
|
||
transparent: true,
|
||
opacity: 0.6,
|
||
roughness: 0.1,
|
||
metalness: 0.5,
|
||
side: THREE.DoubleSide,
|
||
});
|
||
const bg = new THREE.Mesh(bgGeo, bgMat);
|
||
group.add(bg);
|
||
|
||
const borderMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.3, side: THREE.DoubleSide });
|
||
const border = new THREE.Mesh(new THREE.PlaneGeometry(w + 0.05, h + 0.05), borderMat);
|
||
border.position.z = -0.01;
|
||
group.add(border);
|
||
|
||
const textCanvas = document.createElement('canvas');
|
||
textCanvas.width = 512;
|
||
textCanvas.height = 640;
|
||
const ctx = textCanvas.getContext('2d');
|
||
|
||
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,
|
||
side: THREE.DoubleSide,
|
||
depthWrite: false,
|
||
});
|
||
const textMesh = new THREE.Mesh(new THREE.PlaneGeometry(w * 0.95, h * 0.95), textMat);
|
||
textMesh.position.z = 0.01;
|
||
group.add(textMesh);
|
||
|
||
const scanGeo = new THREE.PlaneGeometry(w, h);
|
||
const scanMat = new THREE.ShaderMaterial({
|
||
transparent: true,
|
||
depthWrite: false,
|
||
uniforms: { uTime: { value: 0 }, uColor: { value: new THREE.Color(color) } },
|
||
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;
|
||
void main() {
|
||
float scanline = sin(vUv.y * 200.0 + uTime * 2.0) * 0.5 + 0.5;
|
||
scanline = pow(scanline, 8.0);
|
||
float sweep = smoothstep(0.0, 0.02, abs(fract(vUv.y - uTime * 0.1) - 0.5));
|
||
sweep = 1.0 - (1.0 - sweep) * 0.3;
|
||
float alpha = scanline * 0.04 + (1.0 - sweep) * 0.08;
|
||
gl_FragColor = vec4(uColor, alpha);
|
||
}
|
||
`,
|
||
side: THREE.DoubleSide,
|
||
});
|
||
const scanMesh = new THREE.Mesh(scanGeo, scanMat);
|
||
scanMesh.position.z = 0.02;
|
||
group.add(scanMesh);
|
||
|
||
parent.add(group);
|
||
return { group, scanMat, borderMat, updatePanelText, title };
|
||
}
|
||
|
||
// ═══ GITEA DATA INTEGRATION ═══
|
||
async function fetchGiteaData() {
|
||
try {
|
||
const [issuesRes, stateRes] = await Promise.all([
|
||
fetch('https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/the-nexus/issues?state=all&limit=20'),
|
||
fetch('https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/the-nexus/contents/vision.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);
|
||
updateSovereignHealth();
|
||
}
|
||
} 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);
|
||
}
|
||
|
||
|
||
async function updateSovereignHealth() {
|
||
const container = document.getElementById('sovereign-health-content');
|
||
if (!container) return;
|
||
|
||
let metrics = { sovereignty_score: 100, local_sessions: 0, total_sessions: 0 };
|
||
let daemonReachable = false;
|
||
try {
|
||
const res = await fetch('http://localhost:8082/metrics');
|
||
if (res.ok) {
|
||
metrics = await res.json();
|
||
daemonReachable = true;
|
||
}
|
||
} catch (e) {
|
||
console.log('Local health daemon not reachable, using static baseline.');
|
||
}
|
||
|
||
const services = [
|
||
{ name: 'LOCAL DAEMON', status: daemonReachable ? 'ONLINE' : 'OFFLINE' },
|
||
{ name: 'FORGE / GITEA', url: 'https://forge.alexanderwhitestone.com', status: 'ONLINE' },
|
||
{ name: 'NEXUS CORE', url: 'https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus', status: 'ONLINE' },
|
||
{ name: 'HERMES WS', url: 'ws://143.198.27.163:8765', status: wsConnected ? 'ONLINE' : 'OFFLINE' },
|
||
{ name: 'SOVEREIGNTY', url: 'http://localhost:8082/metrics', status: metrics.sovereignty_score + '%' }
|
||
];
|
||
|
||
container.innerHTML = '';
|
||
|
||
// Add Sovereignty Bar
|
||
const barDiv = document.createElement('div');
|
||
barDiv.className = 'meta-stat';
|
||
barDiv.style.flexDirection = 'column';
|
||
barDiv.style.alignItems = 'flex-start';
|
||
barDiv.innerHTML = `
|
||
<div style="display:flex; justify-content:space-between; width:100%; margin-bottom:4px;">
|
||
<span>SOVEREIGNTY SCORE</span>
|
||
<span>${metrics.sovereignty_score}%</span>
|
||
</div>
|
||
<div style="width:100%; height:4px; background:rgba(255,255,255,0.1);">
|
||
<div style="width:${metrics.sovereignty_score}%; height:100%; background:var(--accent-color); box-shadow: 0 0 10px var(--accent-color);"></div>
|
||
</div>
|
||
`;
|
||
container.appendChild(barDiv);
|
||
|
||
// Session metrics (if daemon provides them)
|
||
if (daemonReachable && (metrics.local_sessions || metrics.total_sessions)) {
|
||
const sessDiv = document.createElement('div');
|
||
sessDiv.className = 'meta-stat';
|
||
sessDiv.innerHTML = `<span>SESSIONS</span><span>${metrics.local_sessions || 0} local / ${metrics.total_sessions || 0} total</span>`;
|
||
container.appendChild(sessDiv);
|
||
}
|
||
|
||
services.forEach(s => {
|
||
const div = document.createElement('div');
|
||
div.className = 'meta-stat';
|
||
div.innerHTML = `<span>${s.name}</span> <span class="${s.status === 'OFFLINE' ? 'status-offline' : 'status-online'}">${s.status}</span>`;
|
||
container.appendChild(div);
|
||
});
|
||
|
||
// Last updated timestamp
|
||
const tsDiv = document.createElement('div');
|
||
tsDiv.className = 'meta-stat';
|
||
tsDiv.style.opacity = '0.5';
|
||
tsDiv.style.fontSize = '0.7em';
|
||
tsDiv.textContent = `UPDATED ${new Date().toLocaleTimeString()}`;
|
||
container.appendChild(tsDiv);
|
||
}
|
||
|
||
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 ═══
|
||
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 => {
|
||
const portal = createPortal(config);
|
||
portals.push(portal);
|
||
});
|
||
}
|
||
|
||
function createPortal(config) {
|
||
const group = new THREE.Group();
|
||
group.position.set(config.position.x, config.position.y, config.position.z);
|
||
if (config.rotation) {
|
||
group.rotation.y = config.rotation.y;
|
||
}
|
||
|
||
const portalColor = new THREE.Color(config.color);
|
||
|
||
// Torus Ring
|
||
const torusGeo = new THREE.TorusGeometry(3, 0.15, 16, 64);
|
||
const torusMat = new THREE.MeshStandardMaterial({
|
||
color: portalColor,
|
||
emissive: portalColor,
|
||
emissiveIntensity: 1.5,
|
||
roughness: 0.2,
|
||
metalness: 0.8,
|
||
});
|
||
const ring = new THREE.Mesh(torusGeo, torusMat);
|
||
ring.position.y = 3.5;
|
||
ring.name = `portal_ring_${config.id}`;
|
||
group.add(ring);
|
||
|
||
// Swirl Disc
|
||
const swirlGeo = new THREE.CircleGeometry(2.8, 64);
|
||
const swirlMat = new THREE.ShaderMaterial({
|
||
transparent: true,
|
||
side: THREE.DoubleSide,
|
||
uniforms: {
|
||
uTime: { value: 0 },
|
||
uColor: { value: portalColor },
|
||
},
|
||
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;
|
||
void main() {
|
||
vec2 c = vUv - 0.5;
|
||
float r = length(c);
|
||
float a = atan(c.y, c.x);
|
||
float swirl = sin(a * 3.0 + r * 10.0 - uTime * 3.0) * 0.5 + 0.5;
|
||
float swirl2 = sin(a * 5.0 - r * 8.0 + uTime * 2.0) * 0.5 + 0.5;
|
||
float mask = smoothstep(0.5, 0.1, r);
|
||
vec3 col = mix(uColor, vec3(1.0, 1.0, 1.0), swirl * 0.3);
|
||
col = mix(col, vec3(1.0, 1.0, 1.0), swirl2 * 0.2);
|
||
float alpha = mask * (0.5 + 0.3 * swirl);
|
||
gl_FragColor = vec4(col, alpha);
|
||
}
|
||
`,
|
||
});
|
||
const swirl = new THREE.Mesh(swirlGeo, swirlMat);
|
||
swirl.position.y = 3.5;
|
||
group.add(swirl);
|
||
|
||
// Orbital Particles
|
||
const pCount = 120;
|
||
const pGeo = new THREE.BufferGeometry();
|
||
const pPos = new Float32Array(pCount * 3);
|
||
const pSizes = new Float32Array(pCount);
|
||
for (let i = 0; i < pCount; i++) {
|
||
const angle = Math.random() * Math.PI * 2;
|
||
const r = 3.2 + Math.random() * 0.5;
|
||
pPos[i * 3] = Math.cos(angle) * r;
|
||
pPos[i * 3 + 1] = 3.5 + (Math.random() - 0.5) * 6;
|
||
pPos[i * 3 + 2] = (Math.random() - 0.5) * 0.5;
|
||
pSizes[i] = 0.05 + Math.random() * 0.1;
|
||
}
|
||
pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
|
||
pGeo.setAttribute('size', new THREE.BufferAttribute(pSizes, 1));
|
||
const pMat = new THREE.PointsMaterial({
|
||
color: portalColor,
|
||
size: 0.08,
|
||
transparent: true,
|
||
opacity: 0.6,
|
||
blending: THREE.AdditiveBlending,
|
||
depthWrite: false,
|
||
});
|
||
const pSystem = new THREE.Points(pGeo, pMat);
|
||
group.add(pSystem);
|
||
|
||
// Pulsing Point Light
|
||
const light = new THREE.PointLight(portalColor, 2, 15, 1.5);
|
||
light.position.set(0, 3.5, 1);
|
||
group.add(light);
|
||
|
||
// Label
|
||
const labelCanvas = document.createElement('canvas');
|
||
labelCanvas.width = 512;
|
||
labelCanvas.height = 96;
|
||
const lctx = labelCanvas.getContext('2d');
|
||
lctx.font = 'bold 32px "Orbitron", sans-serif';
|
||
lctx.fillStyle = '#' + portalColor.getHexString();
|
||
lctx.textAlign = 'center';
|
||
lctx.fillText(`◈ ${config.name.toUpperCase()}`, 256, 36);
|
||
// Role tag (timmy/reflex/pilot) — defines portal ownership boundary
|
||
if (config.role) {
|
||
const roleColors = { timmy: '#4af0c0', reflex: '#ff4466', pilot: '#ffd700' };
|
||
lctx.font = 'bold 18px "Orbitron", sans-serif';
|
||
lctx.fillStyle = roleColors[config.role] || '#888888';
|
||
lctx.fillText(config.role.toUpperCase(), 256, 68);
|
||
}
|
||
const labelTex = new THREE.CanvasTexture(labelCanvas);
|
||
const labelMat = new THREE.MeshBasicMaterial({ map: labelTex, transparent: true, side: THREE.DoubleSide });
|
||
const labelMesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 0.75), labelMat);
|
||
labelMesh.position.y = 7.5;
|
||
group.add(labelMesh);
|
||
|
||
// Base Pillars
|
||
for (let side of [-1, 1]) {
|
||
const pillarGeo = new THREE.CylinderGeometry(0.2, 0.3, 7, 8);
|
||
const pillarMat = new THREE.MeshStandardMaterial({
|
||
color: 0x1a1a2e,
|
||
roughness: 0.5,
|
||
metalness: 0.7,
|
||
emissive: portalColor,
|
||
emissiveIntensity: 0.1,
|
||
});
|
||
const pillar = new THREE.Mesh(pillarGeo, pillarMat);
|
||
pillar.position.set(side * 3, 3.5, 0);
|
||
pillar.castShadow = true;
|
||
group.add(pillar);
|
||
}
|
||
|
||
scene.add(group);
|
||
|
||
const portalObj = {
|
||
config,
|
||
group,
|
||
ring,
|
||
swirl,
|
||
pSystem,
|
||
light,
|
||
customElements: {}
|
||
};
|
||
|
||
// ═══ DISTINCT VISUAL IDENTITIES ═══
|
||
if (config.id === 'archive') {
|
||
// Floating Data Cubes
|
||
const cubes = [];
|
||
for (let i = 0; i < 6; i++) {
|
||
const cubeGeo = new THREE.BoxGeometry(0.4, 0.4, 0.4);
|
||
const cubeMat = new THREE.MeshStandardMaterial({
|
||
color: portalColor,
|
||
emissive: portalColor,
|
||
emissiveIntensity: 1.5,
|
||
transparent: true,
|
||
opacity: 0.8
|
||
});
|
||
const cube = new THREE.Mesh(cubeGeo, cubeMat);
|
||
group.add(cube);
|
||
cubes.push(cube);
|
||
}
|
||
portalObj.customElements.cubes = cubes;
|
||
} else if (config.id === 'chapel') {
|
||
// Glowing Core + Halo
|
||
const coreGeo = new THREE.SphereGeometry(1.2, 32, 32);
|
||
const coreMat = new THREE.MeshPhysicalMaterial({
|
||
color: 0xffffff,
|
||
emissive: portalColor,
|
||
emissiveIntensity: 2,
|
||
transparent: true,
|
||
opacity: 0.4,
|
||
transmission: 0.9,
|
||
thickness: 2
|
||
});
|
||
const core = new THREE.Mesh(coreGeo, coreMat);
|
||
core.position.y = 3.5;
|
||
group.add(core);
|
||
portalObj.customElements.core = core;
|
||
|
||
const haloGeo = new THREE.TorusGeometry(3.5, 0.05, 16, 100);
|
||
const haloMat = new THREE.MeshBasicMaterial({ color: portalColor, transparent: true, opacity: 0.3 });
|
||
const halo = new THREE.Mesh(haloGeo, haloMat);
|
||
halo.position.y = 3.5;
|
||
group.add(halo);
|
||
portalObj.customElements.halo = halo;
|
||
} else if (config.id === 'courtyard') {
|
||
// Double Rotating Rings
|
||
const outerRingGeo = new THREE.TorusGeometry(4.2, 0.1, 16, 80);
|
||
const outerRingMat = new THREE.MeshStandardMaterial({
|
||
color: portalColor,
|
||
emissive: portalColor,
|
||
emissiveIntensity: 0.8,
|
||
transparent: true,
|
||
opacity: 0.5
|
||
});
|
||
const outerRing = new THREE.Mesh(outerRingGeo, outerRingMat);
|
||
outerRing.position.y = 3.5;
|
||
group.add(outerRing);
|
||
portalObj.customElements.outerRing = outerRing;
|
||
} else if (config.id === 'gate') {
|
||
// Spiky Monoliths
|
||
const spikes = [];
|
||
for (let i = 0; i < 8; i++) {
|
||
const spikeGeo = new THREE.ConeGeometry(0.2, 1.5, 4);
|
||
const spikeMat = new THREE.MeshStandardMaterial({ color: 0x111111, emissive: portalColor, emissiveIntensity: 0.5 });
|
||
const spike = new THREE.Mesh(spikeGeo, spikeMat);
|
||
const angle = (i / 8) * Math.PI * 2;
|
||
spike.position.set(Math.cos(angle) * 3.5, 3.5 + Math.sin(angle) * 3.5, 0);
|
||
spike.rotation.z = angle + Math.PI / 2;
|
||
group.add(spike);
|
||
spikes.push(spike);
|
||
}
|
||
portalObj.customElements.spikes = spikes;
|
||
|
||
// Darker Swirl
|
||
swirl.material.uniforms.uColor.value = new THREE.Color(0x220000);
|
||
}
|
||
|
||
return portalObj;
|
||
}
|
||
|
||
// ═══ PARTICLES ═══
|
||
function createParticles() {
|
||
const count = particleCount(1500);
|
||
const geo = new THREE.BufferGeometry();
|
||
const positions = new Float32Array(count * 3);
|
||
const colors = new Float32Array(count * 3);
|
||
const sizes = new Float32Array(count);
|
||
|
||
const c1 = new THREE.Color(NEXUS.colors.primary);
|
||
const c2 = new THREE.Color(NEXUS.colors.secondary);
|
||
const c3 = new THREE.Color(NEXUS.colors.gold);
|
||
|
||
for (let i = 0; i < count; i++) {
|
||
positions[i * 3] = (Math.random() - 0.5) * 60;
|
||
positions[i * 3 + 1] = Math.random() * 20;
|
||
positions[i * 3 + 2] = (Math.random() - 0.5) * 60;
|
||
|
||
const t = Math.random();
|
||
const col = t < 0.5 ? c1.clone().lerp(c2, t * 2) : c2.clone().lerp(c3, (t - 0.5) * 2);
|
||
colors[i * 3] = col.r;
|
||
colors[i * 3 + 1] = col.g;
|
||
colors[i * 3 + 2] = col.b;
|
||
|
||
sizes[i] = 0.02 + Math.random() * 0.06;
|
||
}
|
||
|
||
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||
geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
||
|
||
const mat = new THREE.ShaderMaterial({
|
||
uniforms: { uTime: { value: 0 } },
|
||
vertexShader: `
|
||
attribute float size;
|
||
attribute vec3 color;
|
||
varying vec3 vColor;
|
||
uniform float uTime;
|
||
void main() {
|
||
vColor = color;
|
||
vec3 pos = position;
|
||
pos.y += sin(uTime * 0.5 + position.x * 0.5) * 0.3;
|
||
pos.x += sin(uTime * 0.3 + position.z * 0.4) * 0.2;
|
||
vec4 mv = modelViewMatrix * vec4(pos, 1.0);
|
||
gl_PointSize = size * 300.0 / -mv.z;
|
||
gl_Position = projectionMatrix * mv;
|
||
}
|
||
`,
|
||
fragmentShader: `
|
||
varying vec3 vColor;
|
||
void main() {
|
||
float d = length(gl_PointCoord - 0.5);
|
||
if (d > 0.5) discard;
|
||
float alpha = smoothstep(0.5, 0.1, d);
|
||
gl_FragColor = vec4(vColor, alpha * 0.7);
|
||
}
|
||
`,
|
||
transparent: true,
|
||
depthWrite: false,
|
||
blending: THREE.AdditiveBlending,
|
||
});
|
||
|
||
particles = new THREE.Points(geo, mat);
|
||
scene.add(particles);
|
||
}
|
||
|
||
function createDustParticles() {
|
||
const count = particleCount(500);
|
||
const geo = new THREE.BufferGeometry();
|
||
const positions = new Float32Array(count * 3);
|
||
|
||
for (let i = 0; i < count; i++) {
|
||
positions[i * 3] = (Math.random() - 0.5) * 40;
|
||
positions[i * 3 + 1] = Math.random() * 15;
|
||
positions[i * 3 + 2] = (Math.random() - 0.5) * 40;
|
||
}
|
||
|
||
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||
|
||
const mat = new THREE.PointsMaterial({
|
||
color: 0x8899bb,
|
||
size: 0.03,
|
||
transparent: true,
|
||
opacity: 0.3,
|
||
depthWrite: false,
|
||
});
|
||
|
||
dustParticles = new THREE.Points(geo, mat);
|
||
scene.add(dustParticles);
|
||
}
|
||
|
||
// ═══ AMBIENT STRUCTURES ═══
|
||
function createAmbientStructures() {
|
||
const crystalMat = new THREE.MeshPhysicalMaterial({
|
||
color: 0x3355aa,
|
||
roughness: 0.1,
|
||
metalness: 0.2,
|
||
transmission: 0.6,
|
||
thickness: 2,
|
||
emissive: 0x1122aa,
|
||
emissiveIntensity: 0.3,
|
||
});
|
||
|
||
const positions = [
|
||
{ x: -18, z: -15, s: 1.5, ry: 0.3 },
|
||
{ x: -20, z: -10, s: 1, ry: 0.8 },
|
||
{ x: -15, z: -18, s: 2, ry: 1.2 },
|
||
{ x: 18, z: -15, s: 1.8, ry: 2.1 },
|
||
{ x: 20, z: -12, s: 1.2, ry: 0.5 },
|
||
{ x: -12, z: 18, s: 1.3, ry: 1.8 },
|
||
{ x: 14, z: 16, s: 1.6, ry: 0.9 },
|
||
];
|
||
|
||
positions.forEach(p => {
|
||
const geo = new THREE.ConeGeometry(0.4 * p.s, 2.5 * p.s, 5);
|
||
const crystal = new THREE.Mesh(geo, crystalMat.clone());
|
||
crystal.position.set(p.x, 1.25 * p.s, p.z);
|
||
crystal.rotation.y = p.ry;
|
||
crystal.rotation.z = (Math.random() - 0.5) * 0.3;
|
||
crystal.castShadow = true;
|
||
scene.add(crystal);
|
||
});
|
||
|
||
for (let i = 0; i < 5; i++) {
|
||
const angle = (i / 5) * Math.PI * 2;
|
||
const r = 10;
|
||
const geo = new THREE.OctahedronGeometry(0.4, 0);
|
||
const mat = new THREE.MeshStandardMaterial({
|
||
color: NEXUS.colors.primary,
|
||
emissive: NEXUS.colors.primary,
|
||
emissiveIntensity: 0.5,
|
||
});
|
||
const stone = new THREE.Mesh(geo, mat);
|
||
stone.position.set(Math.cos(angle) * r, 5 + Math.sin(i * 1.3) * 1.5, Math.sin(angle) * r);
|
||
stone.name = 'runestone_' + i;
|
||
scene.add(stone);
|
||
}
|
||
|
||
const coreGeo = new THREE.IcosahedronGeometry(0.6, 2);
|
||
const coreMat = new THREE.MeshPhysicalMaterial({
|
||
color: 0x4af0c0,
|
||
emissive: 0x4af0c0,
|
||
emissiveIntensity: 2,
|
||
roughness: 0,
|
||
metalness: 1,
|
||
transmission: 0.3,
|
||
thickness: 1,
|
||
});
|
||
const core = new THREE.Mesh(coreGeo, coreMat);
|
||
core.position.set(0, 2.5, 0);
|
||
core.name = 'nexus-core';
|
||
scene.add(core);
|
||
|
||
const pedGeo = new THREE.CylinderGeometry(0.8, 1.2, 1.5, 8);
|
||
const pedMat = new THREE.MeshStandardMaterial({
|
||
color: 0x0a0f1a,
|
||
roughness: 0.4,
|
||
metalness: 0.8,
|
||
emissive: 0x1a2a4a,
|
||
emissiveIntensity: 0.3,
|
||
});
|
||
const pedestal = new THREE.Mesh(pedGeo, pedMat);
|
||
pedestal.position.set(0, 0.75, 0);
|
||
pedestal.castShadow = true;
|
||
scene.add(pedestal);
|
||
}
|
||
|
||
// ═══ NAVIGATION MODE ═══
|
||
// ═══ VISITOR / OPERATOR MODE TOGGLE ═══
|
||
function toggleUIMode() {
|
||
uiMode = uiMode === 'visitor' ? 'operator' : 'visitor';
|
||
document.body.classList.remove('visitor-mode', 'operator-mode');
|
||
document.body.classList.add(uiMode + '-mode');
|
||
const label = document.getElementById('mode-label');
|
||
const icon = document.querySelector('#mode-toggle-btn .hud-icon');
|
||
if (label) label.textContent = uiMode === 'visitor' ? 'VISITOR' : 'OPERATOR';
|
||
if (icon) icon.textContent = uiMode === 'visitor' ? '👁' : '⚙';
|
||
addChatMessage('system', `Switched to ${uiMode.toUpperCase()} mode.`);
|
||
}
|
||
|
||
function cycleNavMode() {
|
||
navModeIdx = (navModeIdx + 1) % NAV_MODES.length;
|
||
const mode = NAV_MODES[navModeIdx];
|
||
if (mode === 'orbit') {
|
||
const dir = new THREE.Vector3(0, 0, -1).applyEuler(playerRot);
|
||
orbitState.target.copy(playerPos).addScaledVector(dir, orbitState.radius);
|
||
orbitState.target.y = Math.max(0, orbitState.target.y);
|
||
const toCamera = new THREE.Vector3().subVectors(playerPos, orbitState.target);
|
||
orbitState.radius = toCamera.length();
|
||
orbitState.theta = Math.atan2(toCamera.x, toCamera.z);
|
||
orbitState.phi = Math.acos(Math.max(-1, Math.min(1, toCamera.y / orbitState.radius)));
|
||
}
|
||
if (mode === 'fly') flyY = playerPos.y;
|
||
updateNavModeUI(mode);
|
||
}
|
||
|
||
function updateNavModeUI(mode) {
|
||
const el = document.getElementById('nav-mode-label');
|
||
if (el) el.textContent = mode.toUpperCase();
|
||
}
|
||
|
||
// ═══ CONTROLS ═══
|
||
function setupControls() {
|
||
document.addEventListener('keydown', (e) => {
|
||
keys[e.key.toLowerCase()] = true;
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
const input = document.getElementById('chat-input');
|
||
if (document.activeElement === input) {
|
||
sendChatMessage();
|
||
} else {
|
||
input.focus();
|
||
}
|
||
}
|
||
if (e.key.toLowerCase() === 'm' && document.activeElement !== document.getElementById('chat-input')) {
|
||
openPortalAtlas();
|
||
}
|
||
if (e.key === 'Escape') {
|
||
document.getElementById('chat-input').blur();
|
||
if (portalOverlayActive) closePortalOverlay();
|
||
if (visionOverlayActive) closeVisionOverlay();
|
||
if (atlasOverlayActive) closePortalAtlas();
|
||
if (_archiveDashboardOpen) toggleArchiveHealthDashboard();
|
||
if (_memoryFilterOpen) closeMemoryFilter();
|
||
}
|
||
if (e.key.toLowerCase() === 'v' && document.activeElement !== document.getElementById('chat-input')) {
|
||
cycleNavMode();
|
||
}
|
||
if (e.key.toLowerCase() === 'f' && activePortal && !portalOverlayActive) {
|
||
activatePortal(activePortal);
|
||
}
|
||
if (e.key.toLowerCase() === 'e' && activeVisionPoint && !visionOverlayActive) {
|
||
activateVisionPoint(activeVisionPoint);
|
||
}
|
||
if (e.key.toLowerCase() === 'h' && document.activeElement !== document.getElementById('chat-input')) {
|
||
toggleArchiveHealthDashboard();
|
||
}
|
||
if (e.key.toLowerCase() === 'g' && document.activeElement !== document.getElementById('chat-input')) {
|
||
toggleMemoryFilter();
|
||
}
|
||
});
|
||
document.addEventListener('keyup', (e) => {
|
||
keys[e.key.toLowerCase()] = false;
|
||
});
|
||
|
||
const canvas = document.getElementById('nexus-canvas');
|
||
canvas.addEventListener('mousedown', (e) => {
|
||
if (e.target === canvas) {
|
||
mouseDown = true;
|
||
orbitState.lastX = e.clientX;
|
||
orbitState.lastY = e.clientY;
|
||
_clickStartX = e.clientX;
|
||
_clickStartY = e.clientY;
|
||
|
||
// Raycasting for portals
|
||
if (!portalOverlayActive) {
|
||
const mouse = new THREE.Vector2(
|
||
(e.clientX / window.innerWidth) * 2 - 1,
|
||
-(e.clientY / window.innerHeight) * 2 + 1
|
||
);
|
||
const raycaster = new THREE.Raycaster();
|
||
raycaster.setFromCamera(mouse, camera);
|
||
const intersects = raycaster.intersectObjects(portals.map(p => p.ring));
|
||
if (intersects.length > 0) {
|
||
const clickedRing = intersects[0].object;
|
||
const portal = portals.find(p => p.ring === clickedRing);
|
||
if (portal) activatePortal(portal);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
document.addEventListener('mouseup', (e) => {
|
||
const wasDrag = Math.abs(e.clientX - _clickStartX) > 5 || Math.abs(e.clientY - _clickStartY) > 5;
|
||
mouseDown = false;
|
||
if (wasDrag || e.target !== canvas) return;
|
||
|
||
// Crystal click detection (Mnemosyne inspect panel, issue #1227)
|
||
if (!portalOverlayActive) {
|
||
const mouse = new THREE.Vector2(
|
||
(e.clientX / window.innerWidth) * 2 - 1,
|
||
-(e.clientY / window.innerHeight) * 2 + 1
|
||
);
|
||
const raycaster = new THREE.Raycaster();
|
||
raycaster.setFromCamera(mouse, camera);
|
||
const crystalMeshes = SpatialMemory.getCrystalMeshes();
|
||
const hits = raycaster.intersectObjects(crystalMeshes);
|
||
if (hits.length > 0) {
|
||
const entry = SpatialMemory.getMemoryFromMesh(hits[0].object);
|
||
if (entry) {
|
||
SpatialMemory.highlightMemory(entry.data.id);
|
||
MemoryPulse.triggerPulse(entry.data.id);
|
||
const regionDef = SpatialMemory.REGIONS[entry.region] || SpatialMemory.REGIONS.working;
|
||
MemoryInspect.show(entry.data, regionDef);
|
||
}
|
||
} else {
|
||
// Clicked empty space — close inspect panel and deselect crystal
|
||
if (MemoryInspect.isOpen()) {
|
||
SpatialMemory.clearHighlight();
|
||
MemoryInspect.hide();
|
||
}
|
||
}
|
||
}
|
||
});
|
||
document.addEventListener('mousemove', (e) => {
|
||
if (!mouseDown) return;
|
||
if (document.activeElement === document.getElementById('chat-input')) return;
|
||
const mode = NAV_MODES[navModeIdx];
|
||
if (mode === 'orbit') {
|
||
const dx = e.clientX - orbitState.lastX;
|
||
const dy = e.clientY - orbitState.lastY;
|
||
orbitState.lastX = e.clientX;
|
||
orbitState.lastY = e.clientY;
|
||
orbitState.theta -= dx * 0.005;
|
||
orbitState.phi = Math.max(0.05, Math.min(Math.PI * 0.85, orbitState.phi + dy * 0.005));
|
||
} else {
|
||
playerRot.y -= e.movementX * 0.003;
|
||
playerRot.x -= e.movementY * 0.003;
|
||
playerRot.x = Math.max(-Math.PI / 3, Math.min(Math.PI / 3, playerRot.x));
|
||
}
|
||
});
|
||
|
||
canvas.addEventListener('wheel', (e) => {
|
||
if (NAV_MODES[navModeIdx] === 'orbit') {
|
||
orbitState.radius = Math.max(orbitState.minR, Math.min(orbitState.maxR, orbitState.radius + e.deltaY * 0.02));
|
||
}
|
||
}, { passive: true });
|
||
|
||
document.getElementById('chat-toggle').addEventListener('click', () => {
|
||
chatOpen = !chatOpen;
|
||
document.getElementById('chat-panel').classList.toggle('collapsed', !chatOpen);
|
||
});
|
||
document.getElementById('chat-send').addEventListener('click', () => sendChatMessage());
|
||
|
||
// Add MemPalace mining button
|
||
document.querySelector('.chat-quick-actions').innerHTML += `
|
||
<button class="quick-action-btn" onclick="mineMemPalaceContent()">Mine Chat</button>
|
||
<div id="mem-palace-stats" class="mem-palace-stats">
|
||
<div>Compression: <span id="compression-ratio">--</span>x</div>
|
||
<div>Docs: <span id="docs-mined">0</span></div>
|
||
<div>AAAK: <span id="aaak-size">0B</span></div>
|
||
<div>Compression: <span id="compression-ratio">--</span>x</div>
|
||
<div>Docs: <span id="docs-mined">0</span></div>
|
||
<div>AAAK: <span id="aaak-size">0B</span></div>
|
||
<div class="mem-palace-logs" style="margin-top:4px; font-size:10px; color:#4af0c0;">Logs: <span id="mem-logs">0</span></div>
|
||
</div>
|
||
`;
|
||
|
||
// Chat quick actions
|
||
document.getElementById('chat-quick-actions').addEventListener('click', (e) => {
|
||
const btn = e.target.closest('.quick-action-btn');
|
||
if (!btn) return;
|
||
|
||
const action = btn.dataset.action;
|
||
|
||
switch(action) {
|
||
case 'status':
|
||
sendChatMessage("Timmy, what is the current system status?");
|
||
break;
|
||
case 'agents':
|
||
sendChatMessage("Timmy, check on all active agents.");
|
||
break;
|
||
case 'portals':
|
||
openPortalAtlas();
|
||
break;
|
||
case 'soul':
|
||
document.getElementById('soul-overlay').style.display = 'flex';
|
||
break;
|
||
case 'help':
|
||
sendChatMessage("Timmy, I need assistance with Nexus navigation.");
|
||
break;
|
||
}
|
||
});
|
||
|
||
document.getElementById('portal-close-btn').addEventListener('click', closePortalOverlay);
|
||
document.getElementById('vision-close-btn').addEventListener('click', closeVisionOverlay);
|
||
|
||
document.getElementById('mode-toggle-btn').addEventListener('click', toggleUIMode);
|
||
document.getElementById('atlas-toggle-btn').addEventListener('click', openPortalAtlas);
|
||
document.getElementById('atlas-close-btn').addEventListener('click', closePortalAtlas);
|
||
initAtlasControls();
|
||
|
||
// SOUL / Oath panel (issue #709)
|
||
document.getElementById('soul-toggle-btn').addEventListener('click', () => {
|
||
document.getElementById('soul-overlay').style.display = 'flex';
|
||
});
|
||
document.getElementById('soul-close-btn').addEventListener('click', () => {
|
||
document.getElementById('soul-overlay').style.display = 'none';
|
||
});
|
||
}
|
||
|
||
function sendChatMessage(overrideText = null) {
|
||
// Mine chat message to MemPalace
|
||
if (overrideText) {
|
||
window.electronAPI.execPython(`mempalace add_drawer "${this.wing}" "chat" "${overrideText}"`);
|
||
}
|
||
const input = document.getElementById('chat-input');
|
||
const text = overrideText || input.value.trim();
|
||
if (!text) return;
|
||
addChatMessage('user', text);
|
||
if (!overrideText) input.value = '';
|
||
setTimeout(() => {
|
||
const responses = [
|
||
'Processing your request through the harness...',
|
||
'I have noted this in my thought stream.',
|
||
'Acknowledged. Routing to appropriate agent loop.',
|
||
'The sovereign space recognizes your command.',
|
||
'Running analysis. Results will appear on the main terminal.',
|
||
'My crystal ball says... yes. Implementing.',
|
||
'Understood, Alexander. Adjusting priorities.',
|
||
];
|
||
const resp = responses[Math.floor(Math.random() * responses.length)];
|
||
addChatMessage('timmy', resp);
|
||
}, 500 + Math.random() * 1000);
|
||
input.blur();
|
||
}
|
||
|
||
// ═══ HERMES WEBSOCKET ═══
|
||
function connectHermes() {
|
||
// Initialize MemPalace before Hermes connection
|
||
initializeMemPalace();
|
||
// Existing Hermes connection code...
|
||
// Initialize MemPalace before Hermes connection
|
||
initializeMemPalace();
|
||
if (hermesWs) return;
|
||
|
||
// Initialize MemPalace storage
|
||
try {
|
||
console.log('Initializing MemPalace memory system...');
|
||
// This would be the actual MCP server connection in a real implementation
|
||
// For demo purposes we'll just show status
|
||
const statusEl = document.getElementById('mem-palace-status');
|
||
if (statusEl) {
|
||
statusEl.textContent = 'MEMPALACE INITIALIZING';
|
||
statusEl.style.color = '#4af0c0';
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to initialize MemPalace:', err);
|
||
const statusEl = document.getElementById('mem-palace-status');
|
||
if (statusEl) {
|
||
statusEl.textContent = 'MEMPALACE ERROR';
|
||
statusEl.style.color = '#ff4466';
|
||
}
|
||
}
|
||
|
||
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();
|
||
|
||
// Mnemosyne: request memory sync from Hermes
|
||
try {
|
||
hermesWs.send(JSON.stringify({ type: 'memory', action: 'sync_request' }));
|
||
console.info('[Mnemosyne] Sent sync_request to Hermes');
|
||
} catch (e) {
|
||
console.warn('[Mnemosyne] Failed to send sync_request:', e);
|
||
}
|
||
};
|
||
|
||
// Initialize MemPalace
|
||
connectMemPalace();
|
||
|
||
hermesWs.onmessage = (evt) => {
|
||
try {
|
||
const data = JSON.parse(evt.data);
|
||
handleHermesMessage(data);
|
||
|
||
// Store in MemPalace
|
||
if (data.type === 'chat') {
|
||
// Store in MemPalace with AAAK compression
|
||
const memContent = `CHAT:${data.agent} ${data.text}`;
|
||
// In a real implementation, we'd use mempalace.add_drawer()
|
||
console.log('Storing in MemPalace:', memContent);
|
||
}
|
||
} 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 === 'memory') {
|
||
handleMemoryMessage(data);
|
||
} 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);
|
||
});
|
||
}
|
||
} else if (data.type && data.type.startsWith('evennia.')) {
|
||
handleEvenniaEvent(data);
|
||
// Evennia event bridge — process command/result/room fields if present
|
||
handleEvenniaEvent(data);
|
||
}
|
||
|
||
|
||
// ═══════════════════════════════════════════
|
||
// TIMMY ACTION STREAM — EVENNIA COMMAND FLOW
|
||
// ═══════════════════════════════════════════
|
||
|
||
const MAX_ACTION_STREAM = 8;
|
||
|
||
/**
|
||
* Add an entry to the action stream panel.
|
||
* @param {'cmd'|'result'|'room'} type
|
||
* @param {string} text
|
||
*/
|
||
function addActionStreamEntry(type, text) {
|
||
const entry = { type, text, ts: Date.now() };
|
||
actionStreamEntries.unshift(entry);
|
||
if (actionStreamEntries.length > MAX_ACTION_STREAM) actionStreamEntries.pop();
|
||
renderActionStream();
|
||
}
|
||
|
||
/**
|
||
* Update the current room display in the action stream.
|
||
* @param {string} room
|
||
*/
|
||
function setActionStreamRoom(room) {
|
||
actionStreamRoom = room;
|
||
const el = document.getElementById('action-stream-room');
|
||
if (el) el.textContent = room ? `◈ ${room}` : '';
|
||
}
|
||
|
||
/**
|
||
* Render the action stream panel entries.
|
||
*/
|
||
function renderActionStream() {
|
||
const el = document.getElementById('action-stream-content');
|
||
if (!el) return;
|
||
el.innerHTML = actionStreamEntries.map(e => {
|
||
const ts = new Date(e.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||
const cls = e.type === 'cmd' ? 'as-cmd' : e.type === 'result' ? 'as-result' : 'as-room';
|
||
const prefix = e.type === 'cmd' ? '>' : e.type === 'result' ? '←' : '◈';
|
||
return `<div class="as-entry ${cls}"><span class="as-prefix">${prefix}</span> <span class="as-text">${escHtml(e.text)}</span> <span class="as-ts">${ts}</span></div>`;
|
||
}).join('');
|
||
}
|
||
|
||
/**
|
||
* Process Evennia-specific fields from Hermes WS messages.
|
||
* Called from handleHermesMessage for any message carrying evennia metadata.
|
||
*/
|
||
function handleEvenniaEvent(data) {
|
||
if (data.evennia_command) {
|
||
addActionStreamEntry('cmd', data.evennia_command);
|
||
}
|
||
if (data.evennia_result) {
|
||
const excerpt = typeof data.evennia_result === 'string'
|
||
? data.evennia_result.substring(0, 120)
|
||
: JSON.stringify(data.evennia_result).substring(0, 120);
|
||
addActionStreamEntry('result', excerpt);
|
||
}
|
||
if (data.evennia_room) {
|
||
setActionStreamRoom(data.evennia_room);
|
||
addActionStreamEntry('room', `Moved to: ${data.evennia_room}`);
|
||
}
|
||
}
|
||
|
||
|
||
// ═══════════════════════════════════════════
|
||
|
||
|
||
// ═══════════════════════════════════════════
|
||
// EVENNIA ROOM SNAPSHOT PANEL (Issue #728)
|
||
// ═══════════════════════════════════════════
|
||
|
||
function handleEvenniaEvent(data) {
|
||
const evtType = data.type;
|
||
|
||
if (evtType === 'evennia.room_snapshot') {
|
||
evenniaRoom = {
|
||
roomKey: data.room_key || data.room_id || '',
|
||
title: data.title || 'Unknown Room',
|
||
desc: data.desc || '',
|
||
exits: data.exits || [],
|
||
objects: data.objects || [],
|
||
occupants: data.occupants || [],
|
||
timestamp: data.timestamp || new Date().toISOString()
|
||
};
|
||
evenniaConnected = true;
|
||
renderEvenniaRoomPanel();
|
||
resetEvenniaStaleTimer();
|
||
} else if (evtType === 'evennia.player_move') {
|
||
// Movement may indicate current room changed; update location text
|
||
if (data.to_room) {
|
||
const locEl = document.getElementById('hud-location-text');
|
||
if (locEl) locEl.textContent = data.to_room;
|
||
}
|
||
} else if (evtType === 'evennia.session_bound') {
|
||
evenniaConnected = true;
|
||
renderEvenniaRoomPanel();
|
||
} else if (evtType === 'evennia.player_join' || evtType === 'evennia.player_leave') {
|
||
// Refresh occupant display if we have room data
|
||
if (evenniaRoom) renderEvenniaRoomPanel();
|
||
}
|
||
}
|
||
|
||
function resetEvenniaStaleTimer() {
|
||
if (evenniaStaleTimer) clearTimeout(evenniaStaleTimer);
|
||
const dot = document.getElementById('erp-live-dot');
|
||
const status = document.getElementById('erp-status');
|
||
if (dot) dot.className = 'erp-live-dot connected';
|
||
if (status) { status.textContent = 'LIVE'; status.className = 'erp-status online'; }
|
||
evenniaStaleTimer = setTimeout(() => {
|
||
if (dot) dot.className = 'erp-live-dot stale';
|
||
if (status) { status.textContent = 'STALE'; status.className = 'erp-status stale'; }
|
||
}, EVENNIA_STALE_MS);
|
||
}
|
||
|
||
function renderEvenniaRoomPanel() {
|
||
const panel = document.getElementById('evennia-room-panel');
|
||
if (!panel) return;
|
||
panel.style.display = 'block';
|
||
|
||
const emptyEl = document.getElementById('erp-empty');
|
||
const roomEl = document.getElementById('erp-room');
|
||
|
||
if (!evenniaRoom) {
|
||
if (emptyEl) emptyEl.style.display = 'flex';
|
||
if (roomEl) roomEl.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
if (emptyEl) emptyEl.style.display = 'none';
|
||
if (roomEl) roomEl.style.display = 'block';
|
||
|
||
const titleEl = document.getElementById('erp-room-title');
|
||
const descEl = document.getElementById('erp-room-desc');
|
||
if (titleEl) titleEl.textContent = evenniaRoom.title;
|
||
if (descEl) descEl.textContent = evenniaRoom.desc;
|
||
|
||
renderEvenniaList('erp-exits', evenniaRoom.exits, (item) => {
|
||
const name = item.key || item.destination_id || item.name || '?';
|
||
const dest = item.destination_key || item.destination_id || '';
|
||
return { icon: '→', label: name, extra: dest && dest !== name ? dest : '' };
|
||
});
|
||
|
||
renderEvenniaList('erp-objects', evenniaRoom.objects, (item) => {
|
||
const name = item.short_desc || item.key || item.id || item.name || '?';
|
||
return { icon: '◇', label: name };
|
||
});
|
||
|
||
renderEvenniaList('erp-occupants', evenniaRoom.occupants, (item) => {
|
||
const name = item.character || item.name || item.account || '?';
|
||
return { icon: '◉', label: name };
|
||
});
|
||
|
||
const tsEl = document.getElementById('erp-footer-ts');
|
||
const roomKeyEl = document.getElementById('erp-footer-room');
|
||
if (tsEl) {
|
||
try {
|
||
const d = new Date(evenniaRoom.timestamp);
|
||
tsEl.textContent = d.toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
|
||
} catch(e) { tsEl.textContent = '—'; }
|
||
}
|
||
if (roomKeyEl) roomKeyEl.textContent = evenniaRoom.roomKey;
|
||
}
|
||
|
||
function renderEvenniaList(containerId, items, mapFn) {
|
||
const container = document.getElementById(containerId);
|
||
if (!container) return;
|
||
container.innerHTML = '';
|
||
|
||
if (!items || items.length === 0) {
|
||
const empty = document.createElement('div');
|
||
empty.className = 'erp-section-empty';
|
||
empty.textContent = 'none';
|
||
container.appendChild(empty);
|
||
return;
|
||
}
|
||
|
||
items.forEach(item => {
|
||
const mapped = mapFn(item);
|
||
const row = document.createElement('div');
|
||
row.className = 'erp-item';
|
||
row.innerHTML = `<span class="erp-item-icon">${mapped.icon}</span><span>${mapped.label}</span>`;
|
||
if (mapped.extra) {
|
||
row.innerHTML += `<span class="erp-item-dest">${mapped.extra}</span>`;
|
||
}
|
||
container.appendChild(row);
|
||
});
|
||
}
|
||
// MNEMOSYNE — LIVE MEMORY BRIDGE
|
||
// ═══════════════════════════════════════════
|
||
|
||
/**
|
||
* Handle incoming memory messages from Hermes WS.
|
||
* Actions: place, remove, update, sync_response
|
||
*/
|
||
|
||
/**
|
||
* Clear all entries from the memory feed.
|
||
*/
|
||
function clearMemoryFeed() {
|
||
memoryFeedEntries = [];
|
||
renderMemoryFeed();
|
||
console.info('[Mnemosyne] Memory feed cleared');
|
||
}
|
||
|
||
/**
|
||
* Navigate to a linked memory from the inspect panel.
|
||
* Highlights the target crystal and re-opens the panel with its data.
|
||
* @param {string} memId
|
||
*/
|
||
function _navigateToMemory(memId) {
|
||
const all = SpatialMemory.getAllMemories();
|
||
const data = all.find(m => m.id === memId);
|
||
if (!data) {
|
||
console.warn('[MemoryInspect] Linked memory not found in scene:', memId);
|
||
return;
|
||
}
|
||
SpatialMemory.highlightMemory(memId);
|
||
const regionDef = SpatialMemory.REGIONS[data.category] || SpatialMemory.REGIONS.working;
|
||
MemoryInspect.show(data, regionDef);
|
||
}
|
||
|
||
function handleMemoryMessage(data) {
|
||
const action = data.action;
|
||
const memory = data.memory;
|
||
const memories = data.memories;
|
||
|
||
if (action === 'place' && memory) {
|
||
const placed = SpatialMemory.placeMemory(memory);
|
||
if (placed) {
|
||
addMemoryFeedEntry('place', memory);
|
||
console.info('[Mnemosyne] Memory placed via WS:', memory.id);
|
||
}
|
||
} else if (action === 'remove' && memory) {
|
||
SpatialMemory.removeMemory(memory.id);
|
||
addMemoryFeedEntry('remove', memory);
|
||
console.info('[Mnemosyne] Memory removed via WS:', memory.id);
|
||
} else if (action === 'update' && memory) {
|
||
SpatialMemory.updateMemory(memory.id, memory);
|
||
addMemoryFeedEntry('update', memory);
|
||
console.info('[Mnemosyne] Memory updated via WS:', memory.id);
|
||
} else if (action === 'sync_response' && Array.isArray(memories)) {
|
||
const count = SpatialMemory.importMemories(memories);
|
||
addMemoryFeedEntry('sync', { content: count + ' memories synced', id: 'sync' });
|
||
console.info('[Mnemosyne] Synced', count, 'memories from Hermes');
|
||
} else {
|
||
console.warn('[Mnemosyne] Unknown memory action:', action);
|
||
}
|
||
if (_archiveDashboardOpen) updateArchiveHealthDashboard();
|
||
}
|
||
|
||
/**
|
||
* Add an entry to the memory activity feed panel.
|
||
*/
|
||
function addMemoryFeedEntry(action, memory) {
|
||
const entry = {
|
||
action,
|
||
content: memory.content || memory.id || '(unknown)',
|
||
category: memory.category || 'working',
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
|
||
memoryFeedEntries.unshift(entry);
|
||
if (memoryFeedEntries.length > 5) memoryFeedEntries.pop();
|
||
|
||
renderMemoryFeed();
|
||
|
||
// Auto-dismiss entries older than 5 minutes
|
||
setTimeout(() => {
|
||
const idx = memoryFeedEntries.indexOf(entry);
|
||
if (idx > -1) {
|
||
memoryFeedEntries.splice(idx, 1);
|
||
renderMemoryFeed();
|
||
}
|
||
}, 300000);
|
||
}
|
||
|
||
/**
|
||
* Render the memory feed panel.
|
||
*/
|
||
function renderMemoryFeed() {
|
||
const container = document.getElementById('memory-feed-list');
|
||
if (!container) return;
|
||
|
||
container.innerHTML = '';
|
||
memoryFeedEntries.forEach(entry => {
|
||
const el = document.createElement('div');
|
||
el.className = 'memory-feed-entry memory-feed-' + entry.action;
|
||
|
||
const regionDef = SpatialMemory.REGIONS[entry.category] || SpatialMemory.REGIONS.working;
|
||
const dotColor = '#' + regionDef.color.toString(16).padStart(6, '0');
|
||
const time = new Date(entry.timestamp).toLocaleTimeString();
|
||
const truncated = entry.content.length > 40 ? entry.content.slice(0, 40) + '\u2026' : entry.content;
|
||
const actionIcon = { place: '\u2795', remove: '\u2796', update: '\u270F', sync: '\u21C4' }[entry.action] || '\u2022';
|
||
|
||
el.innerHTML = '<span class="memory-feed-dot" style="background:' + dotColor + '"></span>' +
|
||
'<span class="memory-feed-action">' + actionIcon + '</span>' +
|
||
'<span class="memory-feed-content">' + truncated + '</span>' +
|
||
'<span class="memory-feed-time">' + time + '</span>';
|
||
|
||
container.appendChild(el);
|
||
});
|
||
|
||
// Show feed if there are entries
|
||
const panel = document.getElementById('memory-feed');
|
||
if (panel) panel.style.display = memoryFeedEntries.length > 0 ? 'block' : 'none';
|
||
}
|
||
|
||
|
||
// ── Archive Health Dashboard (issue #1210) ────────────────────────────
|
||
|
||
let _archiveDashboardOpen = false;
|
||
|
||
/**
|
||
* Toggle the archive health dashboard panel (hotkey H).
|
||
*/
|
||
function toggleArchiveHealthDashboard() {
|
||
_archiveDashboardOpen = !_archiveDashboardOpen;
|
||
const panel = document.getElementById('archive-health-dashboard');
|
||
if (!panel) return;
|
||
if (_archiveDashboardOpen) {
|
||
updateArchiveHealthDashboard();
|
||
panel.style.display = 'block';
|
||
} else {
|
||
panel.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Render current archive statistics into the dashboard panel.
|
||
* Reads live from SpatialMemory.getAllMemories() — no backend needed.
|
||
*/
|
||
function toggleMemoryFilter() {
|
||
_memoryFilterOpen = !_memoryFilterOpen;
|
||
if (_memoryFilterOpen) {
|
||
openMemoryFilter();
|
||
} else {
|
||
closeMemoryFilter();
|
||
}
|
||
}
|
||
|
||
function updateArchiveHealthDashboard() {
|
||
const container = document.getElementById('archive-health-content');
|
||
if (!container) return;
|
||
|
||
const memories = SpatialMemory.getAllMemories();
|
||
const regions = SpatialMemory.REGIONS;
|
||
const total = memories.length;
|
||
|
||
// ── Category breakdown ────────────────────────────────────────────
|
||
const catCounts = {};
|
||
memories.forEach(m => {
|
||
const cat = m.category || 'working';
|
||
catCounts[cat] = (catCounts[cat] || 0) + 1;
|
||
});
|
||
|
||
// ── Trust distribution (using strength field as trust score) ──────
|
||
let trustHigh = 0, trustMid = 0, trustLow = 0;
|
||
memories.forEach(m => {
|
||
const t = m.strength != null ? m.strength : 0.7;
|
||
if (t > 0.8) trustHigh++;
|
||
else if (t >= 0.5) trustMid++;
|
||
else trustLow++;
|
||
});
|
||
|
||
// ── Timestamps ────────────────────────────────────────────────────
|
||
let newestMs = null, oldestMs = null;
|
||
memories.forEach(m => {
|
||
const ts = m.timestamp ? new Date(m.timestamp).getTime() : null;
|
||
if (ts && !isNaN(ts)) {
|
||
if (newestMs === null || ts > newestMs) newestMs = ts;
|
||
if (oldestMs === null || ts < oldestMs) oldestMs = ts;
|
||
}
|
||
});
|
||
const fmtDate = ms => ms ? new Date(ms).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—';
|
||
|
||
// ── Entity connection count ───────────────────────────────────────
|
||
let entityConnCount = 0;
|
||
memories.forEach(m => {
|
||
if (m.connections && Array.isArray(m.connections)) {
|
||
entityConnCount += m.connections.length;
|
||
}
|
||
});
|
||
// Each connection is stored on both ends, divide by 2 for unique links
|
||
const uniqueLinks = Math.floor(entityConnCount / 2);
|
||
|
||
// ── Build HTML ────────────────────────────────────────────────────
|
||
let html = '';
|
||
|
||
// Total count
|
||
html += `<div>
|
||
<div class="ah-section-label">Total Memories</div>
|
||
<div class="ah-total">
|
||
<span class="ah-total-count">${total}</span>
|
||
<span class="ah-total-label">crystals in archive</span>
|
||
</div>
|
||
</div>`;
|
||
|
||
// Category breakdown
|
||
const sortedCats = Object.entries(catCounts).sort((a, b) => b[1] - a[1]);
|
||
if (sortedCats.length > 0) {
|
||
html += `<div><div class="ah-section-label">Categories</div>`;
|
||
sortedCats.forEach(([cat, count]) => {
|
||
const region = regions[cat] || regions.working;
|
||
const color = '#' + region.color.toString(16).padStart(6, '0');
|
||
const pct = total > 0 ? Math.round((count / total) * 100) : 0;
|
||
html += `<div class="ah-category-row">
|
||
<span class="ah-cat-dot" style="background:${color}"></span>
|
||
<span class="ah-cat-label">${region.label || cat}</span>
|
||
<div class="ah-cat-bar-wrap">
|
||
<div class="ah-cat-bar" style="width:${pct}%;background:${color}"></div>
|
||
</div>
|
||
<span class="ah-cat-count">${count}</span>
|
||
</div>`;
|
||
});
|
||
html += `</div>`;
|
||
}
|
||
|
||
// Trust distribution
|
||
html += `<div>
|
||
<div class="ah-section-label">Trust Distribution</div>
|
||
<div class="ah-trust-row">
|
||
<div class="ah-trust-band ah-trust-high">
|
||
<div class="ah-trust-band-count">${trustHigh}</div>
|
||
<div class="ah-trust-band-label">High >0.8</div>
|
||
</div>
|
||
<div class="ah-trust-band ah-trust-mid">
|
||
<div class="ah-trust-band-count">${trustMid}</div>
|
||
<div class="ah-trust-band-label">Mid 0.5–0.8</div>
|
||
</div>
|
||
<div class="ah-trust-band ah-trust-low">
|
||
<div class="ah-trust-band-count">${trustLow}</div>
|
||
<div class="ah-trust-band-label">Low <0.5</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// Timestamps
|
||
html += `<div>
|
||
<div class="ah-section-label">Timeline</div>
|
||
<div class="ah-timestamps">
|
||
<div class="ah-ts-row">
|
||
<span class="ah-ts-label">Newest</span>
|
||
<span class="ah-ts-value">${fmtDate(newestMs)}</span>
|
||
</div>
|
||
<div class="ah-ts-row">
|
||
<span class="ah-ts-label">Oldest</span>
|
||
<span class="ah-ts-value">${fmtDate(oldestMs)}</span>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// Entity connections
|
||
html += `<div>
|
||
<div class="ah-section-label">Entity Connections</div>
|
||
<span class="ah-entity-count">${uniqueLinks}</span>
|
||
<span class="ah-entity-label">unique links</span>
|
||
</div>`;
|
||
|
||
// Hotkey hint
|
||
html += `<div class="ah-hotkey-hint">PRESS H TO CLOSE</div>`;
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function updateWsHudStatus(connected) {
|
||
// Update MemPalace status alongside regular WS status
|
||
updateMemPalaceStatus();
|
||
// Existing WS status code...
|
||
// Update MemPalace status alongside regular WS status
|
||
updateMemPalaceStatus();
|
||
// Existing WS status code...
|
||
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';
|
||
}
|
||
|
||
// Update MemPalace status
|
||
const memStatus = document.getElementById('mem-palace-status');
|
||
if (memStatus) {
|
||
memStatus.textContent = connected ? 'MEMPALACE ACTIVE' : 'MEMPALACE OFFLINE';
|
||
memStatus.style.color = connected ? '#4af0c0' : '#ff4466';
|
||
}
|
||
}
|
||
|
||
function connectMemPalace() {
|
||
const statusEl = document.getElementById('mem-palace-status');
|
||
const ratioEl = document.getElementById('compression-ratio');
|
||
const docsEl = document.getElementById('docs-mined');
|
||
const sizeEl = document.getElementById('aaak-size');
|
||
|
||
// Show connecting state
|
||
if (statusEl) {
|
||
statusEl.textContent = 'MEMPALACE CONNECTING';
|
||
statusEl.style.color = '#ffd700';
|
||
statusEl.style.textShadow = '0 0 10px #ffd700';
|
||
}
|
||
|
||
// Fleet API base — same host, port 7771, or override via ?mempalace=host:port
|
||
const params = new URLSearchParams(window.location.search);
|
||
const override = params.get('mempalace');
|
||
const apiBase = override
|
||
? `http://${override}`
|
||
: `${window.location.protocol}//${window.location.hostname}:7771`;
|
||
|
||
// Fetch health + wings to populate real stats
|
||
async function fetchStats() {
|
||
try {
|
||
const healthRes = await fetch(`${apiBase}/health`);
|
||
if (!healthRes.ok) throw new Error(`Health ${healthRes.status}`);
|
||
const health = await healthRes.json();
|
||
|
||
const wingsRes = await fetch(`${apiBase}/wings`);
|
||
const wings = wingsRes.ok ? await wingsRes.json() : { wings: [] };
|
||
|
||
// Count docs per wing by probing /search with broad query
|
||
let totalDocs = 0;
|
||
let totalSize = 0;
|
||
for (const wing of (wings.wings || [])) {
|
||
try {
|
||
const sr = await fetch(`${apiBase}/search?q=*&wing=${wing}&n=1`);
|
||
if (sr.ok) {
|
||
const sd = await sr.json();
|
||
totalDocs += sd.count || 0;
|
||
}
|
||
} catch (_) { /* skip */ }
|
||
}
|
||
|
||
const compressionRatio = totalDocs > 0 ? Math.max(1, Math.round(totalDocs * 0.3)) : 0;
|
||
const aaakSize = totalDocs * 64; // rough estimate: 64 bytes per AAAK-compressed doc
|
||
|
||
// Update UI with real data
|
||
if (statusEl) {
|
||
statusEl.textContent = 'MEMPALACE ACTIVE';
|
||
statusEl.style.color = '#4af0c0';
|
||
statusEl.style.textShadow = '0 0 10px #4af0c0';
|
||
}
|
||
if (ratioEl) ratioEl.textContent = `${compressionRatio}x`;
|
||
if (docsEl) docsEl.textContent = String(totalDocs);
|
||
if (sizeEl) sizeEl.textContent = formatBytes(aaakSize);
|
||
|
||
console.log(`[MemPalace] Connected to ${apiBase} — ${totalDocs} docs across ${wings.wings?.length || 0} wings`);
|
||
return true;
|
||
} catch (err) {
|
||
console.warn('[MemPalace] Fleet API unavailable:', err.message);
|
||
if (statusEl) {
|
||
statusEl.textContent = 'MEMPALACE OFFLINE';
|
||
statusEl.style.color = '#ff4466';
|
||
statusEl.style.textShadow = '0 0 10px #ff4466';
|
||
}
|
||
if (ratioEl) ratioEl.textContent = '--x';
|
||
if (docsEl) docsEl.textContent = '0';
|
||
if (sizeEl) sizeEl.textContent = '0B';
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Initial fetch + periodic refresh every 60s
|
||
fetchStats().then(ok => {
|
||
if (ok) setInterval(fetchStats, 60000);
|
||
});
|
||
}
|
||
|
||
function formatBytes(bytes) {
|
||
if (bytes === 0) return '0B';
|
||
const k = 1024;
|
||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + sizes[i];
|
||
}
|
||
|
||
function mineMemPalaceContent() {
|
||
const logs = document.getElementById('mem-palace-logs');
|
||
const now = new Date().toLocaleTimeString();
|
||
|
||
// Add mining progress indicator
|
||
logs.innerHTML = `<div>${now} - Mining chat history...</div>` + logs.innerHTML;
|
||
|
||
// Get chat messages to mine
|
||
const messages = Array.from(document.querySelectorAll('.chat-msg')).map(m => m.innerText);
|
||
if (messages.length === 0) {
|
||
logs.innerHTML = `<div style="color:#ff4466;">${now} - No chat content to mine</div>` + logs.innerHTML;
|
||
return;
|
||
}
|
||
|
||
// Update MemPalace stats
|
||
const ratio = parseInt(document.getElementById('compression-ratio').textContent) + 1;
|
||
const docs = parseInt(document.getElementById('docs-mined').textContent) + messages.length;
|
||
const size = parseInt(document.getElementById('aaak-size').textContent.replace('B','')) + (messages.length * 30);
|
||
|
||
document.getElementById('compression-ratio').textContent = `${ratio}x`;
|
||
document.getElementById('docs-mined').textContent = `${docs}`;
|
||
document.getElementById('aaak-size').textContent = `${size}B`;
|
||
|
||
// Add success message
|
||
logs.innerHTML = `<div style="color:#4af0c0;">${now} - Mined ${messages.length} chat entries</div>` + logs.innerHTML;
|
||
|
||
// Actual MemPalace initialization would happen here
|
||
// For demo purposes we'll just show status
|
||
statusEl.textContent = 'Connected to local MemPalace';
|
||
statusEl.style.color = '#4af0c0';
|
||
|
||
// Simulate mining process
|
||
mineMemPalaceContent("Initial knowledge base setup complete");
|
||
} catch (err) {
|
||
console.error('Failed to initialize MemPalace:', err);
|
||
document.getElementById('mem-palace-status').textContent = 'MemPalace ERROR';
|
||
document.getElementById('mem-palace-status').style.color = '#ff4466';
|
||
}
|
||
try {
|
||
// Initialize MemPalace MCP server
|
||
console.log('Initializing MemPalace memory system...');
|
||
// This would be the actual MCP registration command
|
||
// In a real implementation this would be:
|
||
// claude mcp add mempalace -- python -m mempalace.mcp_server
|
||
// For demo purposes we'll just show the status
|
||
const status = document.getElementById('mem-palace-status');
|
||
if (status) {
|
||
status.textContent = 'MEMPALACE INITIALIZING';
|
||
setTimeout(() => {
|
||
status.textContent = 'MEMPALACE ACTIVE';
|
||
status.style.color = '#4af0c0';
|
||
}, 1500);
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to initialize MemPalace:', err);
|
||
const status = document.getElementById('mem-palace-status');
|
||
if (status) {
|
||
status.textContent = 'MEMPALACE ERROR';
|
||
status.style.color = '#ff4466';
|
||
}
|
||
}
|
||
}
|
||
|
||
// ═══ SESSION PERSISTENCE ═══
|
||
function saveSession() {
|
||
const msgs = Array.from(document.querySelectorAll('.chat-msg')).slice(-60).map(el => ({
|
||
html: el.innerHTML,
|
||
className: el.className
|
||
}));
|
||
|
||
// Store in MemPalace
|
||
if (window.mempalace) {
|
||
try {
|
||
mempalace.add_drawer('chat_history', {
|
||
content: JSON.stringify(msgs),
|
||
metadata: {
|
||
type: 'chat',
|
||
timestamp: Date.now()
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('MemPalace save failed:', error);
|
||
}
|
||
}
|
||
|
||
// Fallback to localStorage
|
||
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) {
|
||
// Mine chat messages for MemPalace
|
||
mineMemPalaceContent(text);
|
||
// Mine chat messages for MemPalace
|
||
mineMemPalaceContent(text);
|
||
const container = document.getElementById('chat-messages');
|
||
const div = document.createElement('div');
|
||
div.className = `chat-msg chat-msg-${agent}`;
|
||
|
||
// Store in MemPalace
|
||
if (window.mempalace) {
|
||
mempalace.add_drawer('chat_history', {
|
||
content: text,
|
||
metadata: {
|
||
agent,
|
||
timestamp: Date.now()
|
||
}
|
||
});
|
||
}
|
||
|
||
// Store in MemPalace
|
||
if (agent !== 'system') {
|
||
// In a real implementation, we'd use mempalace.add_drawer()
|
||
console.log(`MemPalace storage: ${agent} - ${text}`);
|
||
}
|
||
|
||
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 ═══
|
||
function checkPortalProximity() {
|
||
if (portalOverlayActive) return;
|
||
|
||
let closest = null;
|
||
let minDist = Infinity;
|
||
|
||
portals.forEach(portal => {
|
||
const dist = playerPos.distanceTo(portal.group.position);
|
||
if (dist < 4.5 && dist < minDist) {
|
||
minDist = dist;
|
||
closest = portal;
|
||
}
|
||
});
|
||
|
||
activePortal = closest;
|
||
const hint = document.getElementById('portal-hint');
|
||
if (activePortal) {
|
||
document.getElementById('portal-hint-name').textContent = activePortal.config.name;
|
||
hint.style.display = 'flex';
|
||
} else {
|
||
hint.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function activatePortal(portal) {
|
||
portalOverlayActive = true;
|
||
const overlay = document.getElementById('portal-overlay');
|
||
const nameDisplay = document.getElementById('portal-name-display');
|
||
const descDisplay = document.getElementById('portal-desc-display');
|
||
const redirectBox = document.getElementById('portal-redirect-box');
|
||
const errorBox = document.getElementById('portal-error-box');
|
||
const timerDisplay = document.getElementById('portal-timer');
|
||
const statusDot = document.getElementById('portal-status-dot');
|
||
|
||
nameDisplay.textContent = portal.config.name.toUpperCase();
|
||
descDisplay.textContent = portal.config.description;
|
||
statusDot.style.background = portal.config.color;
|
||
statusDot.style.boxShadow = `0 0 10px ${portal.config.color}`;
|
||
|
||
overlay.style.display = 'flex';
|
||
|
||
if (portal.config.destination && portal.config.destination.url) {
|
||
redirectBox.style.display = 'block';
|
||
errorBox.style.display = 'none';
|
||
|
||
let count = 5;
|
||
timerDisplay.textContent = count;
|
||
const interval = setInterval(() => {
|
||
count--;
|
||
timerDisplay.textContent = count;
|
||
if (count <= 0) {
|
||
clearInterval(interval);
|
||
if (portalOverlayActive) window.location.href = portal.config.destination.url;
|
||
}
|
||
if (!portalOverlayActive) clearInterval(interval);
|
||
}, 1000);
|
||
} else {
|
||
redirectBox.style.display = 'none';
|
||
errorBox.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
function closePortalOverlay() {
|
||
portalOverlayActive = false;
|
||
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';
|
||
}
|
||
|
||
// ═══ PORTAL ATLAS / WORLD DIRECTORY ═══
|
||
let atlasActiveFilter = 'all';
|
||
let atlasSearchQuery = '';
|
||
|
||
function openPortalAtlas() {
|
||
atlasOverlayActive = true;
|
||
document.getElementById('atlas-overlay').style.display = 'flex';
|
||
populateAtlas();
|
||
// Focus search input
|
||
setTimeout(() => document.getElementById('atlas-search')?.focus(), 100);
|
||
}
|
||
|
||
function closePortalAtlas() {
|
||
atlasOverlayActive = false;
|
||
document.getElementById('atlas-overlay').style.display = 'none';
|
||
atlasSearchQuery = '';
|
||
atlasActiveFilter = 'all';
|
||
}
|
||
|
||
function initAtlasControls() {
|
||
const searchInput = document.getElementById('atlas-search');
|
||
if (searchInput) {
|
||
searchInput.addEventListener('input', (e) => {
|
||
atlasSearchQuery = e.target.value.toLowerCase().trim();
|
||
populateAtlas();
|
||
});
|
||
}
|
||
|
||
const filterBtns = document.querySelectorAll('.atlas-filter-btn');
|
||
filterBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
filterBtns.forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
atlasActiveFilter = btn.dataset.filter;
|
||
populateAtlas();
|
||
});
|
||
});
|
||
}
|
||
|
||
function matchesAtlasFilter(config) {
|
||
if (atlasActiveFilter === 'all') return true;
|
||
if (atlasActiveFilter === 'harness') return (config.portal_type || 'harness') === 'harness' || !config.portal_type;
|
||
if (atlasActiveFilter === 'game-world') return config.portal_type === 'game-world';
|
||
return config.status === atlasActiveFilter;
|
||
}
|
||
|
||
function matchesAtlasSearch(config) {
|
||
if (!atlasSearchQuery) return true;
|
||
const haystack = [config.name, config.description, config.id,
|
||
config.world_category, config.portal_type, config.destination?.type]
|
||
.filter(Boolean).join(' ').toLowerCase();
|
||
return haystack.includes(atlasSearchQuery);
|
||
}
|
||
|
||
function populateAtlas() {
|
||
const grid = document.getElementById('atlas-grid');
|
||
grid.innerHTML = '';
|
||
|
||
let onlineCount = 0;
|
||
let standbyCount = 0;
|
||
let downloadedCount = 0;
|
||
let visibleCount = 0;
|
||
|
||
let readyCount = 0;
|
||
|
||
portals.forEach(portal => {
|
||
const config = portal.config;
|
||
if (config.status === 'online') onlineCount++;
|
||
if (config.status === 'standby') standbyCount++;
|
||
if (config.status === 'downloaded') downloadedCount++;
|
||
|
||
if (!matchesAtlasFilter(config) || !matchesAtlasSearch(config)) return;
|
||
visibleCount++;
|
||
|
||
if (config.interaction_ready && config.status === 'online') readyCount++;
|
||
|
||
const card = document.createElement('div');
|
||
card.className = 'atlas-card';
|
||
card.style.setProperty('--portal-color', config.color);
|
||
|
||
const statusClass = `status-${config.status || 'online'}`;
|
||
const statusLabel = (config.status || 'ONLINE').toUpperCase();
|
||
const portalType = config.portal_type || 'harness';
|
||
const categoryLabel = config.world_category
|
||
? config.world_category.replace(/-/g, ' ').toUpperCase()
|
||
: portalType.replace(/-/g, ' ').toUpperCase();
|
||
|
||
// Readiness bar for game-worlds
|
||
let readinessHTML = '';
|
||
if (config.readiness_steps) {
|
||
const steps = Object.values(config.readiness_steps);
|
||
readinessHTML = `<div class="atlas-card-readiness" title="Readiness: ${steps.filter(s=>s.done).length}/${steps.length}">`;
|
||
steps.forEach(step => {
|
||
readinessHTML += `<div class="readiness-step ${step.done ? 'done' : ''}" title="${step.label}${step.done ? ' ✓' : ''}"></div>`;
|
||
});
|
||
readinessHTML += '</div>';
|
||
}
|
||
|
||
// Action label
|
||
const actionLabel = config.destination?.action_label
|
||
|| (config.status === 'online' ? 'ENTER' : config.status === 'downloaded' ? 'LAUNCH' : 'VIEW');
|
||
const agents = config.agents_present || [];
|
||
const ready = config.interaction_ready && config.status === 'online';
|
||
const presenceLabel = agents.length > 0
|
||
? agents.map(a => a.toUpperCase()).join(', ')
|
||
: 'No agents present';
|
||
const readyLabel = ready ? 'INTERACTION READY' : 'UNAVAILABLE';
|
||
const readyClass = ready ? 'status-online' : 'status-offline';
|
||
|
||
card.innerHTML = `
|
||
<div class="atlas-card-header">
|
||
<div>
|
||
<span class="atlas-card-name">${config.name}</span>
|
||
<span class="atlas-card-category">${categoryLabel}</span>
|
||
</div>
|
||
<div class="atlas-card-status ${statusClass}">${statusLabel}</div>
|
||
</div>
|
||
<div class="atlas-card-desc">${config.description}</div>
|
||
${readinessHTML}
|
||
<div class="atlas-card-presence">
|
||
<div class="atlas-card-agents">${agents.length > 0 ? 'Agents: ' + presenceLabel : presenceLabel}</div>
|
||
<div class="atlas-card-ready ${readyClass}">${readyLabel}</div>
|
||
</div>
|
||
<div class="atlas-card-footer">
|
||
<div class="atlas-card-coord">X:${config.position.x} Z:${config.position.z}</div>
|
||
<div class="atlas-card-action">${actionLabel} →</div>
|
||
${config.role ? `<div class="atlas-card-role role-${config.role}">${config.role.toUpperCase()}</div>` : ''}
|
||
<div class="atlas-card-type">${config.destination?.type?.toUpperCase() || 'UNKNOWN'}</div>
|
||
</div>
|
||
`;
|
||
|
||
card.addEventListener('click', () => {
|
||
focusPortal(portal);
|
||
closePortalAtlas();
|
||
});
|
||
|
||
grid.appendChild(card);
|
||
});
|
||
|
||
// Show empty state
|
||
if (visibleCount === 0) {
|
||
const empty = document.createElement('div');
|
||
empty.className = 'atlas-empty';
|
||
empty.textContent = atlasSearchQuery
|
||
? `No worlds match "${atlasSearchQuery}"`
|
||
: 'No worlds in this category';
|
||
grid.appendChild(empty);
|
||
}
|
||
|
||
document.getElementById('atlas-online-count').textContent = onlineCount;
|
||
document.getElementById('atlas-standby-count').textContent = standbyCount;
|
||
document.getElementById('atlas-downloaded-count').textContent = downloadedCount;
|
||
document.getElementById('atlas-total-count').textContent = portals.length;
|
||
document.getElementById('atlas-ready-count').textContent = readyCount;
|
||
|
||
// Update Bannerlord HUD status
|
||
const bannerlord = portals.find(p => p.config.id === 'bannerlord');
|
||
if (bannerlord) {
|
||
const statusEl = document.getElementById('bannerlord-status');
|
||
statusEl.className = 'hud-status-item ' + (bannerlord.config.status || 'offline');
|
||
}
|
||
}
|
||
|
||
function focusPortal(portal) {
|
||
// Teleport player to a position in front of the portal
|
||
const offset = new THREE.Vector3(0, 0, 6).applyEuler(new THREE.Euler(0, portal.config.rotation?.y || 0, 0));
|
||
playerPos.copy(portal.group.position).add(offset);
|
||
playerPos.y = 2; // Keep at eye level
|
||
|
||
// Rotate player to face the portal
|
||
playerRot.y = (portal.config.rotation?.y || 0) + Math.PI;
|
||
playerRot.x = 0;
|
||
|
||
addChatMessage('system', `Navigation focus: ${portal.config.name}`);
|
||
|
||
// If in orbit mode, reset target
|
||
if (NAV_MODES[navModeIdx] === 'orbit') {
|
||
orbitState.target.copy(portal.group.position);
|
||
orbitState.target.y = 3.5;
|
||
}
|
||
}
|
||
|
||
// ═══ 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);
|
||
}
|
||
|
||
updateAshStorm(delta, elapsed);
|
||
|
||
// Project Mnemosyne - Memory Orb Animation
|
||
if (typeof animateMemoryOrbs === 'function') {
|
||
SpatialMemory.update(delta);
|
||
SpatialAudio.update(delta);
|
||
MemoryBirth.update(delta);
|
||
MemoryPulse.update();
|
||
animateMemoryOrbs(delta);
|
||
}
|
||
|
||
|
||
const mode = NAV_MODES[navModeIdx];
|
||
const chatActive = document.activeElement === document.getElementById('chat-input');
|
||
|
||
if (mode === 'walk') {
|
||
if (!chatActive && !portalOverlayActive) {
|
||
const speed = 6 * delta;
|
||
const dir = new THREE.Vector3();
|
||
if (keys['w']) dir.z -= 1;
|
||
if (keys['s']) dir.z += 1;
|
||
if (keys['a']) dir.x -= 1;
|
||
if (keys['d']) dir.x += 1;
|
||
if (dir.length() > 0) {
|
||
dir.normalize().multiplyScalar(speed);
|
||
dir.applyAxisAngle(new THREE.Vector3(0, 1, 0), playerRot.y);
|
||
playerPos.add(dir);
|
||
const maxR = 24;
|
||
const dist = Math.sqrt(playerPos.x * playerPos.x + playerPos.z * playerPos.z);
|
||
if (dist > maxR) { playerPos.x *= maxR / dist; playerPos.z *= maxR / dist; }
|
||
}
|
||
}
|
||
playerPos.y = 2;
|
||
camera.position.copy(playerPos);
|
||
camera.rotation.set(playerRot.x, playerRot.y, 0, 'YXZ');
|
||
|
||
} else if (mode === 'orbit') {
|
||
if (!chatActive && !portalOverlayActive) {
|
||
const speed = 8 * delta;
|
||
const pan = new THREE.Vector3();
|
||
if (keys['w']) pan.z -= 1;
|
||
if (keys['s']) pan.z += 1;
|
||
if (keys['a']) pan.x -= 1;
|
||
if (keys['d']) pan.x += 1;
|
||
if (pan.length() > 0) {
|
||
pan.normalize().multiplyScalar(speed);
|
||
pan.applyAxisAngle(new THREE.Vector3(0, 1, 0), orbitState.theta);
|
||
orbitState.target.add(pan);
|
||
orbitState.target.y = Math.max(0, Math.min(20, orbitState.target.y));
|
||
}
|
||
}
|
||
const r = orbitState.radius;
|
||
camera.position.set(
|
||
orbitState.target.x + r * Math.sin(orbitState.phi) * Math.sin(orbitState.theta),
|
||
orbitState.target.y + r * Math.cos(orbitState.phi),
|
||
orbitState.target.z + r * Math.sin(orbitState.phi) * Math.cos(orbitState.theta)
|
||
);
|
||
camera.lookAt(orbitState.target);
|
||
playerPos.copy(camera.position);
|
||
playerRot.y = orbitState.theta;
|
||
|
||
} else if (mode === 'fly') {
|
||
if (!chatActive && !portalOverlayActive) {
|
||
const speed = 8 * delta;
|
||
const forward = new THREE.Vector3(-Math.sin(playerRot.y), 0, -Math.cos(playerRot.y));
|
||
const right = new THREE.Vector3( Math.cos(playerRot.y), 0, -Math.sin(playerRot.y));
|
||
if (keys['w']) playerPos.addScaledVector(forward, speed);
|
||
if (keys['s']) playerPos.addScaledVector(forward, -speed);
|
||
if (keys['a']) playerPos.addScaledVector(right, -speed);
|
||
if (keys['d']) playerPos.addScaledVector(right, speed);
|
||
if (keys['q'] || keys[' ']) flyY += speed;
|
||
if (keys['e'] || keys['shift']) flyY -= speed;
|
||
flyY = Math.max(0.5, Math.min(30, flyY));
|
||
playerPos.y = flyY;
|
||
}
|
||
camera.position.copy(playerPos);
|
||
camera.rotation.set(playerRot.x, playerRot.y, 0, 'YXZ');
|
||
}
|
||
|
||
// Proximity check
|
||
checkPortalProximity();
|
||
checkVisionProximity();
|
||
|
||
const sky = scene.getObjectByName('skybox');
|
||
if (sky) sky.material.uniforms.uTime.value = elapsed;
|
||
|
||
batcaveTerminals.forEach(t => {
|
||
if (t.scanMat?.uniforms) t.scanMat.uniforms.uTime.value = elapsed;
|
||
});
|
||
|
||
// Animate Portals
|
||
portals.forEach(portal => {
|
||
portal.ring.rotation.z = elapsed * 0.3;
|
||
portal.ring.rotation.x = Math.sin(elapsed * 0.5) * 0.1;
|
||
if (portal.swirl.material.uniforms) {
|
||
portal.swirl.material.uniforms.uTime.value = elapsed;
|
||
}
|
||
// Pulse light
|
||
portal.light.intensity = 1.5 + Math.sin(elapsed * 2) * 0.5;
|
||
|
||
// Custom animations for distinct identities
|
||
if (portal.config.id === 'archive' && portal.customElements.cubes) {
|
||
portal.customElements.cubes.forEach((cube, i) => {
|
||
cube.rotation.x += delta * (0.5 + i * 0.1);
|
||
cube.rotation.y += delta * (0.3 + i * 0.1);
|
||
const orbitSpeed = 0.5 + i * 0.2;
|
||
const orbitRadius = 4 + Math.sin(elapsed * 0.5 + i) * 0.5;
|
||
cube.position.x = Math.cos(elapsed * orbitSpeed + i) * orbitRadius;
|
||
cube.position.z = Math.sin(elapsed * orbitSpeed + i) * orbitRadius;
|
||
cube.position.y = 3.5 + Math.sin(elapsed * 1.2 + i) * 1.5;
|
||
});
|
||
}
|
||
|
||
if (portal.config.id === 'chapel' && portal.customElements.halo) {
|
||
portal.customElements.halo.rotation.z -= delta * 0.2;
|
||
portal.customElements.halo.scale.setScalar(1 + Math.sin(elapsed * 0.8) * 0.05);
|
||
portal.customElements.core.material.emissiveIntensity = 2 + Math.sin(elapsed * 3) * 1;
|
||
}
|
||
|
||
if (portal.config.id === 'courtyard' && portal.customElements.outerRing) {
|
||
portal.customElements.outerRing.rotation.z -= delta * 0.5;
|
||
portal.customElements.outerRing.rotation.y = Math.cos(elapsed * 0.4) * 0.2;
|
||
}
|
||
|
||
if (portal.config.id === 'gate' && portal.customElements.spikes) {
|
||
portal.customElements.spikes.forEach((spike, i) => {
|
||
const s = 1 + Math.sin(elapsed * 2 + i) * 0.2;
|
||
spike.scale.set(s, s, s);
|
||
});
|
||
}
|
||
|
||
// Animate particles
|
||
const positions = portal.pSystem.geometry.attributes.position.array;
|
||
for (let i = 0; i < positions.length / 3; i++) {
|
||
positions[i * 3 + 1] += Math.sin(elapsed + i) * 0.002;
|
||
}
|
||
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;
|
||
}
|
||
if (dustParticles) {
|
||
dustParticles.rotation.y = elapsed * 0.01;
|
||
}
|
||
|
||
for (let i = 0; i < 5; i++) {
|
||
const stone = scene.getObjectByName('runestone_' + i);
|
||
if (stone) {
|
||
stone.position.y = 5 + Math.sin(elapsed * 0.8 + i * 1.3) * 0.8;
|
||
stone.rotation.y = elapsed * 0.5 + i;
|
||
stone.rotation.x = elapsed * 0.3 + i * 0.7;
|
||
}
|
||
}
|
||
|
||
const core = scene.getObjectByName('nexus-core');
|
||
if (core) {
|
||
core.position.y = 2.5 + Math.sin(elapsed * 1.2) * 0.3;
|
||
core.rotation.y = elapsed * 0.4;
|
||
core.rotation.x = elapsed * 0.2;
|
||
core.material.emissiveIntensity = 1.5 + Math.sin(elapsed * 2) * 0.5;
|
||
}
|
||
|
||
if (composer) { composer.render(); } else { renderer.render(scene, camera); }
|
||
|
||
// Update avatar and LOD systems
|
||
if (window.AvatarCustomization && playerPos) window.AvatarCustomization.update(playerPos);
|
||
if (window.LODSystem && playerPos) window.LODSystem.update(playerPos);
|
||
|
||
updateAshStorm(delta, elapsed);
|
||
|
||
// Project Mnemosyne - Memory Orb Animation
|
||
if (typeof animateMemoryOrbs === 'function') {
|
||
animateMemoryOrbs(delta);
|
||
}
|
||
|
||
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) {
|
||
fps = frameCount;
|
||
frameCount = 0;
|
||
lastFPSTime = now;
|
||
}
|
||
if (debugOverlay) {
|
||
const info = renderer.info;
|
||
debugOverlay.textContent =
|
||
`FPS: ${fps} Draw: ${info.render?.calls} Tri: ${info.render?.triangles} [${performanceTier}]\n` +
|
||
`Pos: ${playerPos.x.toFixed(1)}, ${playerPos.y.toFixed(1)}, ${playerPos.z.toFixed(1)} NAV: ${NAV_MODES[navModeIdx]}`;
|
||
}
|
||
renderer.info.reset();
|
||
}
|
||
|
||
function onResize() {
|
||
const w = window.innerWidth;
|
||
const h = window.innerHeight;
|
||
camera.aspect = w / h;
|
||
camera.updateProjectionMatrix();
|
||
renderer.setSize(w, h);
|
||
if (composer) 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);
|
||
}
|
||
}
|
||
|
||
// ═══ 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;
|
||
}
|
||
}
|
||
|
||
|
||
// ═══════════════════════════════════════════
|
||
// PROJECT MNEMOSYNE — HOLOGRAPHIC MEMORY ORBS
|
||
// ═══════════════════════════════════════════
|
||
|
||
// Memory orbs registry for animation loop
|
||
const memoryOrbs = [];
|
||
|
||
/**
|
||
* Spawn a glowing memory orb at the given position.
|
||
* Used to visualize RAG retrievals and memory recalls in the Nexus.
|
||
*
|
||
* @param {THREE.Vector3} position - World position for the orb
|
||
* @param {number} color - Hex color (default: 0x4af0c0 - cyan)
|
||
* @param {number} size - Radius of the orb (default: 0.5)
|
||
* @param {object} metadata - Optional metadata for the memory (source, timestamp, etc.)
|
||
* @returns {THREE.Mesh} The created orb mesh
|
||
*/
|
||
function spawnMemoryOrb(position, color = 0x4af0c0, size = 0.5, metadata = {}) {
|
||
if (typeof THREE === 'undefined' || typeof scene === 'undefined') {
|
||
console.warn('[Mnemosyne] THREE/scene not available for orb spawn');
|
||
return null;
|
||
}
|
||
|
||
const geometry = new THREE.SphereGeometry(size, 32, 32);
|
||
const material = new THREE.MeshStandardMaterial({
|
||
color: color,
|
||
emissive: color,
|
||
emissiveIntensity: 2.5,
|
||
metalness: 0.3,
|
||
roughness: 0.2,
|
||
transparent: true,
|
||
opacity: 0.85,
|
||
envMapIntensity: 1.5
|
||
});
|
||
|
||
const orb = new THREE.Mesh(geometry, material);
|
||
orb.position.copy(position);
|
||
orb.castShadow = true;
|
||
orb.receiveShadow = true;
|
||
|
||
orb.userData = {
|
||
type: 'memory_orb',
|
||
pulse: Math.random() * Math.PI * 2, // Random phase offset
|
||
pulseSpeed: 0.002 + Math.random() * 0.001,
|
||
originalScale: size,
|
||
metadata: metadata,
|
||
createdAt: Date.now()
|
||
};
|
||
|
||
// Point light for local illumination
|
||
const light = new THREE.PointLight(color, 1.5, 8);
|
||
orb.add(light);
|
||
|
||
scene.add(orb);
|
||
memoryOrbs.push(orb);
|
||
|
||
console.info('[Mnemosyne] Memory orb spawned:', metadata.source || 'unknown');
|
||
return orb;
|
||
}
|
||
|
||
/**
|
||
* Remove a memory orb from the scene and dispose resources.
|
||
* @param {THREE.Mesh} orb - The orb to remove
|
||
*/
|
||
function removeMemoryOrb(orb) {
|
||
if (!orb) return;
|
||
|
||
if (orb.parent) orb.parent.remove(orb);
|
||
if (orb.geometry) orb.geometry.dispose();
|
||
if (orb.material) orb.material.dispose();
|
||
|
||
const idx = memoryOrbs.indexOf(orb);
|
||
if (idx > -1) memoryOrbs.splice(idx, 1);
|
||
}
|
||
|
||
/**
|
||
* Animate all memory orbs — pulse, rotate, and fade.
|
||
* Called from gameLoop() every frame.
|
||
* @param {number} delta - Time since last frame
|
||
*/
|
||
function animateMemoryOrbs(delta) {
|
||
for (let i = memoryOrbs.length - 1; i >= 0; i--) {
|
||
const orb = memoryOrbs[i];
|
||
if (!orb || !orb.userData) continue;
|
||
|
||
// Pulse animation
|
||
orb.userData.pulse += orb.userData.pulseSpeed * delta * 1000;
|
||
const pulseFactor = 1 + Math.sin(orb.userData.pulse) * 0.1;
|
||
orb.scale.setScalar(pulseFactor * orb.userData.originalScale);
|
||
|
||
// Gentle rotation
|
||
orb.rotation.y += delta * 0.5;
|
||
|
||
// Fade after 30 seconds
|
||
const age = (Date.now() - orb.userData.createdAt) / 1000;
|
||
if (age > 30) {
|
||
const fadeDuration = 10;
|
||
const fadeProgress = Math.min(1, (age - 30) / fadeDuration);
|
||
orb.material.opacity = 0.85 * (1 - fadeProgress);
|
||
|
||
if (fadeProgress >= 1) {
|
||
removeMemoryOrb(orb);
|
||
i--; // Adjust index after removal
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Spawn memory orbs arranged in a spiral for RAG retrieval results.
|
||
* @param {Array} results - Array of {content, score, source}
|
||
* @param {THREE.Vector3} center - Center position (default: above avatar)
|
||
*/
|
||
function spawnRetrievalOrbs(results, center) {
|
||
if (!results || !Array.isArray(results) || results.length === 0) return;
|
||
|
||
if (!center) {
|
||
center = new THREE.Vector3(0, 2, 0);
|
||
}
|
||
|
||
const colors = [0x4af0c0, 0x7b5cff, 0xffd700, 0xff4466, 0x00ff88];
|
||
const radius = 3;
|
||
|
||
results.forEach((result, i) => {
|
||
const angle = (i / results.length) * Math.PI * 2;
|
||
const height = (i / results.length) * 2 - 1;
|
||
|
||
const position = new THREE.Vector3(
|
||
center.x + Math.cos(angle) * radius,
|
||
center.y + height,
|
||
center.z + Math.sin(angle) * radius
|
||
);
|
||
|
||
const colorIdx = Math.min(colors.length - 1, Math.floor((result.score || 0.5) * colors.length));
|
||
const size = 0.3 + (result.score || 0.5) * 0.4;
|
||
|
||
spawnMemoryOrb(position, colors[colorIdx], size, {
|
||
source: result.source || 'unknown',
|
||
score: result.score || 0,
|
||
contentPreview: (result.content || '').substring(0, 100)
|
||
});
|
||
});
|
||
}
|
||
|
||
init().then(() => {
|
||
createAshStorm();
|
||
createPortalTunnel();
|
||
|
||
// Project Mnemosyne — seed demo spatial memories
|
||
const demoMemories = [
|
||
{ id: 'mem_nexus_birth', content: 'The Nexus came online — first render of the 3D world', category: 'knowledge', strength: 0.95, connections: ['mem_mnemosyne_start'] },
|
||
{ id: 'mem_first_portal', content: 'First portal deployed — connection to external service', category: 'engineering', strength: 0.85, connections: ['mem_nexus_birth'] },
|
||
{ id: 'mem_hermes_chat', content: 'First conversation through the Hermes gateway', category: 'social', strength: 0.7, connections: [] },
|
||
{ id: 'mem_mnemosyne_start', content: 'Project Mnemosyne began — the living archive awakens', category: 'projects', strength: 0.9, connections: ['mem_nexus_birth', 'mem_spatial_schema'] },
|
||
{ id: 'mem_spatial_schema', content: 'Spatial Memory Schema defined — memories gain permanent homes', category: 'engineering', strength: 0.8, connections: ['mem_mnemosyne_start'] },
|
||
];
|
||
demoMemories.forEach(m => SpatialMemory.placeMemory(m));
|
||
|
||
fetchGiteaData();
|
||
setInterval(fetchGiteaData, 30000);
|
||
runWeeklyAudit();
|
||
setInterval(runWeeklyAudit, 604800000); // 7 days interval
|
||
|
||
// Register service worker for PWA
|
||
if ('serviceWorker' in navigator) {
|
||
navigator.serviceWorker.register('/service-worker.js');
|
||
}
|
||
|
||
// Initialize MemPalace — connects to Fleet API (see connectMemPalace at line 2772)
|
||
|
||
const mempalace = {
|
||
mineChat: () => {
|
||
try {
|
||
const messages = Array.from(document.querySelectorAll('.chat-msg')).map(m => m.innerText);
|
||
if (messages.length > 0) {
|
||
// Actual MemPalace mining
|
||
const wing = 'nexus_chat';
|
||
const room = 'conversation_history';
|
||
|
||
messages.forEach((msg, idx) => {
|
||
// Store in MemPalace
|
||
window.mempalace.add_drawer({
|
||
wing,
|
||
room,
|
||
content: msg,
|
||
metadata: {
|
||
type: 'chat',
|
||
timestamp: Date.now() - (messages.length - idx) * 1000
|
||
}
|
||
});
|
||
});
|
||
|
||
// Update stats
|
||
mempalace.status.docs += messages.length;
|
||
mempalace.status.compression = Math.min(100, mempalace.status.compression + (messages.length / 10));
|
||
mempalace.status.aak = `${Math.floor(parseInt(mempalace.status.aak.replace('B', '')) + messages.length * 30)}B`;
|
||
|
||
updateMemPalaceStatus();
|
||
}
|
||
} catch (error) {
|
||
console.error('MemPalace mine failed:', error);
|
||
document.getElementById('mem-palace-status').textContent = 'Mining Error';
|
||
document.getElementById('mem-palace-status').style.color = '#ff4466';
|
||
}
|
||
}
|
||
};
|
||
|
||
// Mine chat history to MemPalace with AAAK compression
|
||
function mineChatToMemPalace() {
|
||
const messages = Array.from(document.querySelectorAll('.chat-msg')).map(m => m.innerText);
|
||
if (messages.length > 0) {
|
||
try {
|
||
// Convert to AAAK format
|
||
const aaakContent = messages.map(msg => {
|
||
const lines = msg.split('\n');
|
||
return lines.map(line => {
|
||
// Simple AAAK compression pattern
|
||
return line.replace(/(\w+): (.+)/g, '$1: $2')
|
||
.replace(/(\d{4}-\d{2}-\d{2})/, 'DT:$1')
|
||
.replace(/(\d+ years?)/, 'T:$1');
|
||
}).join('\n');
|
||
}).join('\n---\n');
|
||
|
||
mempalace.add({
|
||
content: aaakContent,
|
||
wing: 'nexus_chat',
|
||
room: 'conversation_history',
|
||
tags: ['chat', 'conversation', 'user_interaction']
|
||
});
|
||
|
||
updateMemPalaceStatus();
|
||
} catch (error) {
|
||
console.error('MemPalace mining failed:', error);
|
||
document.getElementById('mem-palace-status').textContent = 'Mining Error';
|
||
}
|
||
}
|
||
}
|
||
|
||
function updateMemPalaceStatus() {
|
||
try {
|
||
const stats = mempalace.status();
|
||
document.getElementById('compression-ratio').textContent =
|
||
stats.compression_ratio.toFixed(1) + 'x';
|
||
document.getElementById('docs-mined').textContent = stats.total_docs;
|
||
document.getElementById('aaak-size').textContent = stats.aaak_size + 'B';
|
||
document.getElementById('mem-palace-status').textContent = 'Mining Active';
|
||
} catch (error) {
|
||
document.getElementById('mem-palace-status').textContent = 'Connection Lost';
|
||
}
|
||
}
|
||
|
||
// Mine chat on send
|
||
document.getElementById('chat-send-btn').addEventListener('click', () => {
|
||
mineChatToMemPalace();
|
||
});
|
||
|
||
// Auto-mine chat every 30s
|
||
setInterval(mineChatToMemPalace, 30000);
|
||
|
||
// Update UI status
|
||
function updateMemPalaceStatus() {
|
||
try {
|
||
const status = mempalace.status();
|
||
document.getElementById('compression-ratio').textContent = status.compression_ratio.toFixed(1) + 'x';
|
||
document.getElementById('docs-mined').textContent = status.total_docs;
|
||
document.getElementById('aaak-size').textContent = status.aaak_size + 'b';
|
||
} catch (error) {
|
||
document.getElementById('mem-palace-status').textContent = 'Connection Lost';
|
||
}
|
||
}
|
||
|
||
// Add mining event listener
|
||
document.getElementById('mem-palace-btn').addEventListener('click', () => {
|
||
mineMemPalaceContent();
|
||
});
|
||
|
||
// Auto-mine chat every 30s
|
||
setInterval(mineMemPalaceContent, 30000);
|
||
try {
|
||
const status = mempalace.status();
|
||
document.getElementById('compression-ratio').textContent = status.compression_ratio.toFixed(1) + 'x';
|
||
document.getElementById('docs-mined').textContent = status.total_docs;
|
||
document.getElementById('aaak-size').textContent = status.aaak_size + 'B';
|
||
} catch (error) {
|
||
console.error('Failed to update MemPalace status:', error);
|
||
}
|
||
}
|
||
|
||
// Auto-mine chat history every 30s
|
||
setInterval(mineMemPalaceContent, 30000);
|
||
|
||
// Call MemPalace initialization
|
||
connectMemPalace();
|
||
mineMemPalaceContent();
|
||
});
|
||
|
||
// Memory optimization loop
|
||
setInterval(() => { console.log('Running optimization...'); }, 60000); |