Compare commits
1 Commits
fix/871
...
fix/1544-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ff0257c0f |
13
app.js
13
app.js
@@ -5,6 +5,7 @@ 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 { SpatialChatAudio } from './nexus/components/spatial-chat-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';
|
||||
@@ -768,6 +769,7 @@ async function init() {
|
||||
SpatialMemory.setCamera(camera);
|
||||
SpatialAudio.init(camera, scene);
|
||||
SpatialAudio.bindSpatialMemory(SpatialMemory);
|
||||
SpatialChatAudio.init(camera);
|
||||
MemoryInspect.init({ onNavigate: _navigateToMemory });
|
||||
MemoryPulse.init(SpatialMemory);
|
||||
ReasoningTrace.init();
|
||||
@@ -2966,6 +2968,17 @@ function loadSession() {
|
||||
function addChatMessage(agent, text, shouldSave = true) {
|
||||
// Mine chat messages for MemPalace
|
||||
mineMemPalaceContent(text);
|
||||
|
||||
// 3D spatial audio notification (issue #1544)
|
||||
if (window.SpatialChatAudio && agent !== 'system') {
|
||||
// Find agent position from tracked agents or fallback to origin
|
||||
let pos = { x: 0, y: 0, z: 0 };
|
||||
const agentEntry = Array.isArray(window._trackedAgents) && window._trackedAgents.find(a => a.name === agent);
|
||||
if (agentEntry && agentEntry.position) {
|
||||
pos = agentEntry.position;
|
||||
}
|
||||
window.SpatialChatAudio.playChatSound(agent, new THREE.Vector3(pos.x, pos.y, pos.z));
|
||||
}
|
||||
// Mine chat messages for MemPalace
|
||||
mineMemPalaceContent(text);
|
||||
const container = document.getElementById('chat-messages');
|
||||
|
||||
68
docs/spatial-chat-audio.md
Normal file
68
docs/spatial-chat-audio.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Spatial Chat Audio — 3D Audio for Chat Messages
|
||||
|
||||
Refs: the-nexus #1544
|
||||
|
||||
## Overview
|
||||
|
||||
Adds spatial awareness to chat notifications so nearby users/agents sound louder.
|
||||
Volume scales with avatar distance from the camera.
|
||||
|
||||
## Features
|
||||
|
||||
### Chat Notification Sounds
|
||||
- Each agent has a distinct tone (frequency + waveform)
|
||||
- Volume decreases with distance (inverse rolloff)
|
||||
- Stereo panning based on relative position to camera
|
||||
- Sounds auto-cleanup after playback
|
||||
|
||||
### 3D Positional Voice (WebRTC-ready)
|
||||
- `createVoiceSource()` returns a PannerNode for real voice streams
|
||||
- HRTF panning model for realistic 3D positioning
|
||||
- Update position in real-time as avatars move
|
||||
|
||||
### Configurable Parameters
|
||||
- `maxHearingDistance` — max distance to hear sounds (default: 40)
|
||||
- `refDistance` — full volume within this range (default: 5)
|
||||
- `rolloffFactor` — volume falloff curve (default: 1.5)
|
||||
- `baseVolume` — master volume cap (default: 0.3)
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import { SpatialChatAudio } from './nexus/components/spatial-chat-audio.js';
|
||||
|
||||
// Initialize with camera
|
||||
SpatialChatAudio.init(camera);
|
||||
|
||||
// Set max hearing distance
|
||||
SpatialChatAudio.setMaxHearingDistance(50);
|
||||
|
||||
// Play a chat sound when a message arrives
|
||||
// position = avatar/agent position in 3D world
|
||||
SpatialChatAudio.playChatSound('timmy', agentPosition);
|
||||
|
||||
// For voice chat: create a persistent 3D source
|
||||
const voice = SpatialChatAudio.createVoiceSource('user', avatarPosition);
|
||||
// Update as avatar moves
|
||||
voice.updatePosition(newPosition);
|
||||
// Cleanup when disconnected
|
||||
voice.destroy();
|
||||
```
|
||||
|
||||
## Agent Sound Profiles
|
||||
|
||||
| Agent | Frequency | Waveform |
|
||||
|--------|-----------|------------|
|
||||
| timmy | 440 Hz | sine |
|
||||
| user | 523 Hz | sine |
|
||||
| system | 330 Hz | triangle |
|
||||
| kimi | 659 Hz | sine |
|
||||
| claude | 392 Hz | sine |
|
||||
| grok | 587 Hz | triangle |
|
||||
| gemini | 494 Hz | sine |
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
node tests/test_spatial_chat_audio.js
|
||||
```
|
||||
@@ -1,71 +0,0 @@
|
||||
# GOFAI Symbolic Engine Debugger Overlay
|
||||
|
||||
Refs: the-nexus #871
|
||||
|
||||
## Overview
|
||||
|
||||
A specialized debug overlay that shows the internal state of the Symbolic Engine in real-time. Press **Ctrl+Shift+G** to toggle.
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Active Symbols Panel
|
||||
Displays all facts currently in the symbolic engine with their truth values:
|
||||
- Green ● = true
|
||||
- Red ○ = false
|
||||
|
||||
### 2. FSM States Panel
|
||||
Shows the current state of all registered finite state machines:
|
||||
- Agent ID on the left
|
||||
- Current state on the right (highlighted)
|
||||
|
||||
### 3. Reasoning Paths Panel
|
||||
Chronological log of all reasoning steps:
|
||||
- Timestamp
|
||||
- Rule that fired
|
||||
- Outcome produced
|
||||
|
||||
### 4. Knowledge Graph Panel
|
||||
- Node count and edge count
|
||||
- Mini visualization of graph topology (up to 15 nodes)
|
||||
- Color-coded by type (Agent, Location, etc.)
|
||||
|
||||
### 5. Performance Metrics
|
||||
- Number of facts
|
||||
- Number of rules
|
||||
- JavaScript heap usage (if available)
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import { SymbolicDebugger } from './nexus/components/symbolic-debugger.js';
|
||||
import { SymbolicEngine } from './nexus/symbolic-engine.js';
|
||||
|
||||
// Create engine
|
||||
const engine = new SymbolicEngine();
|
||||
|
||||
// Initialize debugger
|
||||
SymbolicDebugger.init({
|
||||
engine: engine,
|
||||
fsmRegistry: new Map(), // optional
|
||||
knowledgeGraph: null // optional
|
||||
});
|
||||
|
||||
// Show the overlay
|
||||
SymbolicDebugger.show();
|
||||
|
||||
// Or toggle with Ctrl+Shift+G
|
||||
```
|
||||
|
||||
## Auto-refresh
|
||||
|
||||
The debugger updates automatically every second when visible. Manual refresh via the ↻ button.
|
||||
|
||||
## Dragging
|
||||
|
||||
The overlay can be dragged by its header to reposition on screen.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
node tests/test_symbolic_debugger.js
|
||||
```
|
||||
236
nexus/components/spatial-chat-audio.js
Normal file
236
nexus/components/spatial-chat-audio.js
Normal file
@@ -0,0 +1,236 @@
|
||||
// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// SPATIAL CHAT AUDIO — 3D Audio for Chat Messages (issue #1544)
|
||||
// ════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||
//
|
||||
// Volume scales with avatar distance — closer agents sound louder.
|
||||
// 3D positional audio places chat sounds in world space.
|
||||
//
|
||||
// Usage from app.js:
|
||||
// import { SpatialChatAudio } from './nexus/components/spatial-chat-audio.js';
|
||||
// SpatialChatAudio.init(camera);
|
||||
// SpatialChatAudio.playChatSound('timmy', agentPosition);
|
||||
//
|
||||
// Configuration:
|
||||
// SpatialChatAudio.setMaxHearingDistance(50); // default 40 units
|
||||
// SpatialChatAudio.setEnabled(true/false);
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const SpatialChatAudio = (() => {
|
||||
|
||||
// ─── CONFIG ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
let _config = {
|
||||
maxHearingDistance: 40, // Distance at which volume reaches 0
|
||||
refDistance: 5, // Full volume within this range
|
||||
rolloffFactor: 1.5, // Volume rolloff curve
|
||||
baseVolume: 0.3, // Master volume for chat sounds
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
// ─── STATE ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
let _camera = null;
|
||||
let _listener = null;
|
||||
let _ctx = null;
|
||||
let _masterGain = null;
|
||||
let _initialized = false;
|
||||
|
||||
// Agent sound profiles (frequency + waveform)
|
||||
const AGENT_SOUNDS = {
|
||||
timmy: { freq: 440, type: 'sine' }, // A4 - clear
|
||||
user: { freq: 523, type: 'sine' }, // C5 - higher
|
||||
system: { freq: 330, type: 'triangle' }, // E4 - neutral
|
||||
kimi: { freq: 659, type: 'sine' }, // E5 - bright
|
||||
claude: { freq: 392, type: 'sine' }, // G4 - warm
|
||||
grok: { freq: 587, type: 'triangle' }, // D5 - sharp
|
||||
gemini: { freq: 494, type: 'sine' }, // B4 - balanced
|
||||
default: { freq: 440, type: 'sine' }, // A4 - default
|
||||
};
|
||||
|
||||
// ─── INIT ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
function init(camera) {
|
||||
if (_initialized) return;
|
||||
|
||||
_camera = camera;
|
||||
|
||||
// Create or reuse AudioListener from camera
|
||||
if (!_listener) {
|
||||
_listener = new THREE.AudioListener();
|
||||
camera.add(_listener);
|
||||
} else {
|
||||
_listener = camera.getObjectByProperty('type', 'AudioListener') || _listener;
|
||||
}
|
||||
|
||||
// Get audio context from listener
|
||||
_ctx = _listener.context;
|
||||
_masterGain = _ctx.createGain();
|
||||
_masterGain.gain.value = _config.baseVolume;
|
||||
_masterGain.connect(_ctx.destination);
|
||||
|
||||
_initialized = true;
|
||||
console.info('[SpatialChatAudio] Initialized — max hearing distance:', _config.maxHearingDistance);
|
||||
|
||||
// Resume context if suspended (browser autoplay policy)
|
||||
if (_ctx.state === 'suspended') {
|
||||
const resume = () => {
|
||||
_ctx.resume().then(() => {
|
||||
console.info('[SpatialChatAudio] AudioContext resumed');
|
||||
document.removeEventListener('click', resume);
|
||||
document.removeEventListener('keydown', resume);
|
||||
});
|
||||
};
|
||||
document.addEventListener('click', resume);
|
||||
document.addEventListener('keydown', resume);
|
||||
}
|
||||
|
||||
return _listener;
|
||||
}
|
||||
|
||||
// ─── PLAY CHAT SOUND ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
function playChatSound(agent, position) {
|
||||
if (!_initialized || !_config.enabled) return;
|
||||
|
||||
const sound = AGENT_SOUNDS[agent] || AGENT_SOUNDS.default;
|
||||
const camPos = _camera.position;
|
||||
|
||||
// Calculate distance
|
||||
const dx = position.x - camPos.x;
|
||||
const dy = position.y - camPos.y;
|
||||
const dz = position.z - camPos.z;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
|
||||
// Calculate volume based on distance
|
||||
let volume = 0;
|
||||
if (dist < _config.maxHearingDistance) {
|
||||
volume = 1 / (1 + _config.rolloffFactor * (dist - _config.refDistance));
|
||||
volume = Math.max(0, Math.min(1, volume));
|
||||
}
|
||||
|
||||
// Skip if too quiet
|
||||
if (volume < 0.01) return;
|
||||
|
||||
// Create audio nodes
|
||||
const osc = _ctx.createOscillator();
|
||||
osc.type = sound.type;
|
||||
osc.frequency.value = sound.freq;
|
||||
|
||||
const gain = _ctx.createGain();
|
||||
gain.gain.value = volume;
|
||||
|
||||
const panner = _ctx.createStereoPanner();
|
||||
|
||||
// Calculate stereo panning
|
||||
const camRight = new THREE.Vector3();
|
||||
_camera.getWorldDirection(camRight);
|
||||
camRight.cross(_camera.up).normalize();
|
||||
const toSource = new THREE.Vector3(dx, 0, dz).normalize();
|
||||
const pan = THREE.MathUtils.clamp(toSource.dot(camRight), -1, 1);
|
||||
panner.pan.value = pan;
|
||||
|
||||
// Connect and play
|
||||
osc.connect(gain);
|
||||
gain.connect(panner);
|
||||
panner.connect(_masterGain);
|
||||
|
||||
// Short envelope (attack + decay)
|
||||
const now = _ctx.currentTime;
|
||||
gain.gain.setValueAtTime(0, now);
|
||||
gain.gain.linearRampToValueAtTime(volume, now + 0.01);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.35);
|
||||
|
||||
// Cleanup
|
||||
osc.onended = () => {
|
||||
osc.disconnect();
|
||||
gain.disconnect();
|
||||
panner.disconnect();
|
||||
};
|
||||
|
||||
console.debug(`[SpatialChatAudio] ${agent} at ${dist.toFixed(1)}m, vol=${volume.toFixed(2)}, pan=${pan.toFixed(2)}`);
|
||||
}
|
||||
|
||||
// ─── CONFIGURATION ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
function setMaxHearingDistance(dist) {
|
||||
_config.maxHearingDistance = Math.max(5, dist);
|
||||
console.info('[SpatialChatAudio] Max hearing distance:', _config.maxHearingDistance);
|
||||
}
|
||||
|
||||
function getMaxHearingDistance() {
|
||||
return _config.maxHearingDistance;
|
||||
}
|
||||
|
||||
function setEnabled(enabled) {
|
||||
_config.enabled = enabled;
|
||||
console.info('[SpatialChatAudio]', enabled ? 'Enabled' : 'Disabled');
|
||||
}
|
||||
|
||||
function isEnabled() {
|
||||
return _config.enabled;
|
||||
}
|
||||
|
||||
function setMasterVolume(vol) {
|
||||
if (_masterGain) {
|
||||
_masterGain.gain.setTargetAtTime(
|
||||
THREE.MathUtils.clamp(vol, 0, 1),
|
||||
_ctx.currentTime,
|
||||
0.05
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── VOICE CHAT SUPPORT (WebRTC placeholder) ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
function createVoiceSource(agentId, position) {
|
||||
if (!_initialized) return null;
|
||||
|
||||
// Create a PannerNode for 3D voice positioning
|
||||
const panner = _ctx.createPanner();
|
||||
panner.panningModel = 'HRTF';
|
||||
panner.distanceModel = 'inverse';
|
||||
panner.refDistance = _config.refDistance;
|
||||
panner.maxDistance = _config.maxHearingDistance;
|
||||
panner.rolloffFactor = _config.rolloffFactor;
|
||||
|
||||
// Set initial position
|
||||
panner.positionX.value = position.x;
|
||||
panner.positionY.value = position.y;
|
||||
panner.positionZ.value = position.z;
|
||||
|
||||
// Connect to master
|
||||
panner.connect(_masterGain);
|
||||
|
||||
console.info(`[SpatialChatAudio] Voice source created for ${agentId}`);
|
||||
|
||||
return {
|
||||
panner,
|
||||
agentId,
|
||||
updatePosition(pos) {
|
||||
panner.positionX.setValueAtTime(pos.x, _ctx.currentTime);
|
||||
panner.positionY.setValueAtTime(pos.y, _ctx.currentTime);
|
||||
panner.positionZ.setValueAtTime(pos.z, _ctx.currentTime);
|
||||
},
|
||||
destroy() {
|
||||
panner.disconnect();
|
||||
console.info(`[SpatialChatAudio] Voice source destroyed for ${agentId}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ─── API ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
return {
|
||||
init,
|
||||
playChatSound,
|
||||
createVoiceSource,
|
||||
setMaxHearingDistance,
|
||||
getMaxHearingDistance,
|
||||
setEnabled,
|
||||
isEnabled,
|
||||
setMasterVolume,
|
||||
};
|
||||
})();
|
||||
|
||||
// Export for module or global usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { SpatialChatAudio };
|
||||
} else if (typeof window !== 'undefined') {
|
||||
window.SpatialChatAudio = SpatialChatAudio;
|
||||
}
|
||||
@@ -1,506 +0,0 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// GOFAI Symbolic Engine Debugger Overlay (issue #871)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||
//
|
||||
// Specialized debug overlay showing internal state of the Symbolic Engine:
|
||||
// — Active symbols and their truth values
|
||||
// — Reasoning paths with timestamps
|
||||
// — FSM state visualizations
|
||||
// — Knowledge graph topology
|
||||
// — Performance metrics
|
||||
//
|
||||
// Usage:
|
||||
// import { SymbolicDebugger } from './symbolic-debugger.js';
|
||||
// SymbolicDebugger.init({ engine, blackboard, fsmRegistry });
|
||||
// SymbolicDebugger.show();
|
||||
// SymbolicDebugger.hide();
|
||||
// SymbolicDebugger.update(); // refresh from current state
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const SymbolicDebugger = (() => {
|
||||
let _overlay = null;
|
||||
let _engine = null;
|
||||
let _blackboard = null;
|
||||
let _fsmRegistry = null;
|
||||
let _kg = null;
|
||||
let _visible = false;
|
||||
let _refreshInterval = null;
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function init(opts = {}) {
|
||||
_engine = opts.engine || null;
|
||||
_blackboard = opts.blackboard || null;
|
||||
_fsmRegistry = opts.fsmRegistry || null;
|
||||
_kg = opts.knowledgeGraph || null;
|
||||
|
||||
// Create overlay if not exists
|
||||
if (!document.getElementById('symbolic-debugger-overlay')) {
|
||||
_createOverlay();
|
||||
}
|
||||
_overlay = document.getElementById('symbolic-debugger-overlay');
|
||||
|
||||
// Keyboard shortcut: Ctrl+Shift+G to toggle
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'G') {
|
||||
toggle();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[SymbolicDebugger] Initialized. Press Ctrl+Shift+G to toggle.');
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function _createOverlay() {
|
||||
const div = document.createElement('div');
|
||||
div.id = 'symbolic-debugger-overlay';
|
||||
div.className = 'sym-debug-overlay';
|
||||
div.style.cssText = `
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 20px;
|
||||
width: 480px;
|
||||
max-height: calc(100vh - 80px);
|
||||
background: rgba(10, 15, 30, 0.95);
|
||||
border: 1px solid #4af0c0;
|
||||
border-radius: 4px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: #e0f0ff;
|
||||
overflow-y: auto;
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
box-shadow: 0 0 20px rgba(74, 240, 192, 0.3);
|
||||
`;
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="sym-debug-header" style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: rgba(74, 240, 192, 0.1);
|
||||
border-bottom: 1px solid #4af0c0;
|
||||
cursor: move;
|
||||
">
|
||||
<span style="color: #4af0c0; font-weight: bold;">◈ GOFAI SYMBOLIC DEBUGGER</span>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button id="sym-debug-refresh" style="
|
||||
background: transparent;
|
||||
border: 1px solid #4af0c0;
|
||||
color: #4af0c0;
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
">↻ Refresh</button>
|
||||
<button id="sym-debug-close" style="
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #4af0c0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sym-debug-content" style="padding: 12px;">
|
||||
<!-- Active Symbols Section -->
|
||||
<div class="sym-section" style="margin-bottom: 16px;">
|
||||
<div class="sym-section-title" style="
|
||||
color: #4af0c0;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(74, 240, 192, 0.3);
|
||||
padding-bottom: 4px;
|
||||
">Active Symbols</div>
|
||||
<div id="sym-debug-symbols" style="
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 4px 12px;
|
||||
"></div>
|
||||
</div>
|
||||
|
||||
<!-- FSM States Section -->
|
||||
<div class="sym-section" style="margin-bottom: 16px;">
|
||||
<div class="sym-section-title" style="
|
||||
color: #4af0c0;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(74, 240, 192, 0.3);
|
||||
padding-bottom: 4px;
|
||||
">FSM States</div>
|
||||
<div id="sym-debug-fsm" style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
"></div>
|
||||
</div>
|
||||
|
||||
<!-- Reasoning Log Section -->
|
||||
<div class="sym-section" style="margin-bottom: 16px;">
|
||||
<div class="sym-section-title" style="
|
||||
color: #4af0c0;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(74, 240, 192, 0.3);
|
||||
padding-bottom: 4px;
|
||||
">Reasoning Paths</div>
|
||||
<div id="sym-debug-reasoning" style="
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
font-size: 11px;
|
||||
"></div>
|
||||
</div>
|
||||
|
||||
<!-- Knowledge Graph Section -->
|
||||
<div class="sym-section" style="margin-bottom: 16px;">
|
||||
<div class="sym-section-title" style="
|
||||
color: #4af0c0;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(74, 240, 192, 0.3);
|
||||
padding-bottom: 4px;
|
||||
">Knowledge Graph</div>
|
||||
<div id="sym-debug-kg-stats" style="margin-bottom: 8px; font-size: 11px;"></div>
|
||||
<div id="sym-debug-kg-viz" style="
|
||||
height: 100px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(74, 240, 192, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
"></div>
|
||||
</div>
|
||||
|
||||
<!-- Performance Metrics Section -->
|
||||
<div class="sym-section">
|
||||
<div class="sym-section-title" style="
|
||||
color: #4af0c0;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(74, 240, 192, 0.3);
|
||||
padding-bottom: 4px;
|
||||
">Performance</div>
|
||||
<div id="sym-debug-metrics" style="
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px 12px;
|
||||
font-size: 11px;
|
||||
"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(div);
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('sym-debug-close').onclick = hide;
|
||||
document.getElementById('sym-debug-refresh').onclick = update;
|
||||
|
||||
// Make draggable
|
||||
let isDragging = false;
|
||||
let dragOffsetX = 0;
|
||||
let dragOffsetY = 0;
|
||||
|
||||
const header = div.querySelector('.sym-debug-header');
|
||||
header.addEventListener('mousedown', (e) => {
|
||||
isDragging = true;
|
||||
dragOffsetX = e.clientX - div.offsetLeft;
|
||||
dragOffsetY = e.clientY - div.offsetTop;
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging) return;
|
||||
div.style.left = (e.clientX - dragOffsetX) + 'px';
|
||||
div.style.top = (e.clientY - dragOffsetY) + 'px';
|
||||
div.style.right = 'auto';
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isDragging = false;
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function update() {
|
||||
if (!_visible || !_overlay) return;
|
||||
|
||||
// Update symbols
|
||||
_updateSymbols();
|
||||
|
||||
// Update FSM states
|
||||
_updateFSM();
|
||||
|
||||
// Update reasoning log
|
||||
_updateReasoning();
|
||||
|
||||
// Update knowledge graph
|
||||
_updateKnowledgeGraph();
|
||||
|
||||
// Update metrics
|
||||
_updateMetrics();
|
||||
}
|
||||
|
||||
function _updateSymbols() {
|
||||
const container = document.getElementById('sym-debug-symbols');
|
||||
if (!container || !_engine) {
|
||||
if (container) container.innerHTML = '<span style="color: #888;">No engine connected</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
if (_engine.facts && _engine.facts.size > 0) {
|
||||
for (const [key, value] of _engine.facts) {
|
||||
const truthColor = value ? '#4af0c0' : '#ff4466';
|
||||
const truthIcon = value ? '●' : '○';
|
||||
html += `
|
||||
<span style="color: #e0f0ff;">${_esc(key)}</span>
|
||||
<span style="color: ${truthColor};">${truthIcon} ${_esc(String(value))}</span>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
html = '<span style="color: #888; grid-column: 1 / -1;">No active symbols</span>';
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function _updateFSM() {
|
||||
const container = document.getElementById('sym-debug-fsm');
|
||||
if (!container) return;
|
||||
|
||||
let html = '';
|
||||
if (_fsmRegistry && _fsmRegistry.size > 0) {
|
||||
for (const [agentId, fsm] of _fsmRegistry) {
|
||||
const transitions = fsm.transitions ? Object.keys(fsm.transitions).length : 0;
|
||||
html += `
|
||||
<div style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
background: rgba(74, 240, 192, 0.05);
|
||||
border-left: 2px solid #4af0c0;
|
||||
">
|
||||
<span style="color: #e0f0ff;">${_esc(agentId)}</span>
|
||||
<span style="
|
||||
color: #4af0c0;
|
||||
font-size: 10px;
|
||||
background: rgba(74, 240, 192, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
">${_esc(fsm.state)}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else if (_engine && _engine.fsm) {
|
||||
// Single FSM mode
|
||||
html += `
|
||||
<div style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
background: rgba(74, 240, 192, 0.05);
|
||||
border-left: 2px solid #4af0c0;
|
||||
">
|
||||
<span style="color: #e0f0ff;">Primary FSM</span>
|
||||
<span style="
|
||||
color: #4af0c0;
|
||||
font-size: 10px;
|
||||
background: rgba(74, 240, 192, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
">${_esc(_engine.fsm.state)}</span>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
html = '<span style="color: #888;">No FSM registered</span>';
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function _updateReasoning() {
|
||||
const container = document.getElementById('sym-debug-reasoning');
|
||||
if (!container || !_engine) {
|
||||
if (container) container.innerHTML = '<span style="color: #888;">No reasoning log</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
if (_engine.reasoningLog && _engine.reasoningLog.length > 0) {
|
||||
for (const entry of _engine.reasoningLog) {
|
||||
const time = entry.timestamp
|
||||
? new Date(entry.timestamp).toLocaleTimeString()
|
||||
: '--:--:--';
|
||||
html += `
|
||||
<div style="
|
||||
margin-bottom: 6px;
|
||||
padding: 4px 6px;
|
||||
background: rgba(74, 240, 192, 0.03);
|
||||
border-left: 2px solid rgba(74, 240, 192, 0.3);
|
||||
">
|
||||
<div style="color: #888; font-size: 10px;">${time}</div>
|
||||
<div style="color: #4af0c0;">${_esc(entry.rule || 'Unknown rule')}</div>
|
||||
<div style="color: #e0f0ff;">→ ${_esc(entry.outcome || 'No outcome')}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
html = '<span style="color: #888;">No reasoning paths recorded</span>';
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function _updateKnowledgeGraph() {
|
||||
const statsContainer = document.getElementById('sym-debug-kg-stats');
|
||||
const vizContainer = document.getElementById('sym-debug-kg-viz');
|
||||
if (!statsContainer || !vizContainer) return;
|
||||
|
||||
if (!_kg) {
|
||||
statsContainer.innerHTML = '<span style="color: #888;">No knowledge graph connected</span>';
|
||||
vizContainer.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeCount = _kg.nodes ? _kg.nodes.size : 0;
|
||||
const edgeCount = _kg.edges ? _kg.edges.length : 0;
|
||||
|
||||
statsContainer.innerHTML = `
|
||||
<span style="color: #4af0c0;">${nodeCount}</span> nodes
|
||||
<span style="color: #888;">|</span>
|
||||
<span style="color: #4af0c0;">${edgeCount}</span> edges
|
||||
`;
|
||||
|
||||
// Simple visualization
|
||||
if (nodeCount > 0 && vizContainer) {
|
||||
let vizHtml = '';
|
||||
let idx = 0;
|
||||
for (const [nodeId, node] of _kg.nodes) {
|
||||
const x = 20 + (idx % 5) * 80;
|
||||
const y = 20 + Math.floor(idx / 5) * 30;
|
||||
const color = node.type === 'Agent' ? '#4af0c0' :
|
||||
node.type === 'Location' ? '#ffaa22' : '#4488ff';
|
||||
vizHtml += `
|
||||
<div style="
|
||||
position: absolute;
|
||||
left: ${x}px;
|
||||
top: ${y}px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: ${color};
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 4px ${color};
|
||||
" title="${_esc(nodeId)}"></div>
|
||||
`;
|
||||
idx++;
|
||||
if (idx >= 15) break; // Limit visualization
|
||||
}
|
||||
vizContainer.innerHTML = vizHtml;
|
||||
} else {
|
||||
vizContainer.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
function _updateMetrics() {
|
||||
const container = document.getElementById('sym-debug-metrics');
|
||||
if (!container) return;
|
||||
|
||||
let html = '';
|
||||
|
||||
// Engine metrics
|
||||
if (_engine) {
|
||||
const factCount = _engine.facts ? _engine.facts.size : 0;
|
||||
const ruleCount = _engine.rules ? _engine.rules.length : 0;
|
||||
html += `
|
||||
<span style="color: #888;">Facts:</span> <span style="color: #4af0c0;">${factCount}</span>
|
||||
<span style="color: #888;">Rules:</span> <span style="color: #4af0c0;">${ruleCount}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// Memory usage (rough estimate)
|
||||
if (performance && performance.memory) {
|
||||
const usedMB = Math.round(performance.memory.usedJSHeapSize / 1048576);
|
||||
html += `
|
||||
<span style="color: #888;">Heap:</span> <span style="color: #4af0c0;">${usedMB} MB</span>
|
||||
`;
|
||||
}
|
||||
|
||||
container.innerHTML = html || '<span style="color: #888;">No metrics available</span>';
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function show() {
|
||||
if (!_overlay) return;
|
||||
_overlay.style.display = 'block';
|
||||
_visible = true;
|
||||
update();
|
||||
_startAutoRefresh();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (!_overlay) return;
|
||||
_overlay.style.display = 'none';
|
||||
_visible = false;
|
||||
_stopAutoRefresh();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (_visible) hide();
|
||||
else show();
|
||||
}
|
||||
|
||||
function isVisible() {
|
||||
return _visible;
|
||||
}
|
||||
|
||||
function _startAutoRefresh() {
|
||||
if (_refreshInterval) return;
|
||||
_refreshInterval = setInterval(update, 1000); // Update every second
|
||||
}
|
||||
|
||||
function _stopAutoRefresh() {
|
||||
if (_refreshInterval) {
|
||||
clearInterval(_refreshInterval);
|
||||
_refreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function _esc(str) {
|
||||
if (typeof str !== 'string') return String(str);
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
return {
|
||||
init,
|
||||
show,
|
||||
hide,
|
||||
toggle,
|
||||
isVisible,
|
||||
update,
|
||||
};
|
||||
})();
|
||||
|
||||
// Auto-export for module or global usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { SymbolicDebugger };
|
||||
} else if (typeof window !== 'undefined') {
|
||||
window.SymbolicDebugger = SymbolicDebugger;
|
||||
}
|
||||
125
tests/test_spatial_chat_audio.js
Normal file
125
tests/test_spatial_chat_audio.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Tests for SpatialChatAudio component (issue #1544)
|
||||
*/
|
||||
|
||||
import { SpatialChatAudio } from '../nexus/components/spatial-chat-audio.js';
|
||||
|
||||
// Mock DOM and THREE for Node.js testing
|
||||
if (typeof document === 'undefined') {
|
||||
global.document = {
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof THREE === 'undefined') {
|
||||
global.THREE = {
|
||||
Vector3: class {
|
||||
constructor(x=0, y=0, z=0) { this.x = x; this.y = y; this.z = z; }
|
||||
normalize() { return this; }
|
||||
dot() { return 0; }
|
||||
cross() { return this; }
|
||||
},
|
||||
MathUtils: { clamp: (v, min, max) => Math.max(min, Math.min(max, v)) },
|
||||
AudioListener: class {
|
||||
constructor() {
|
||||
this.context = {
|
||||
state: 'running',
|
||||
currentTime: 0,
|
||||
createOscillator: () => ({
|
||||
type: 'sine',
|
||||
frequency: { value: 440 },
|
||||
connect: () => {},
|
||||
start: () => {},
|
||||
stop: () => {},
|
||||
disconnect: () => {},
|
||||
onended: null,
|
||||
}),
|
||||
createGain: () => ({
|
||||
gain: { value: 1, setValueAtTime: () => {}, linearRampToValueAtTime: () => {}, exponentialRampToValueAtTime: () => {}, setTargetAtTime: () => {} },
|
||||
connect: () => {},
|
||||
disconnect: () => {},
|
||||
}),
|
||||
createStereoPanner: () => ({
|
||||
pan: { value: 0, setValueAtTime: () => {}, setTargetAtTime: () => {} },
|
||||
connect: () => {},
|
||||
disconnect: () => {},
|
||||
}),
|
||||
createPanner: () => ({
|
||||
panningModel: '',
|
||||
distanceModel: '',
|
||||
refDistance: 0,
|
||||
maxDistance: 0,
|
||||
rolloffFactor: 0,
|
||||
positionX: { value: 0, setValueAtTime: () => {} },
|
||||
positionY: { value: 0, setValueAtTime: () => {} },
|
||||
positionZ: { value: 0, setValueAtTime: () => {} },
|
||||
connect: () => {},
|
||||
disconnect: () => {},
|
||||
}),
|
||||
resume: () => Promise.resolve(),
|
||||
destination: {},
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
console.error(`❌ FAILED: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`✔ PASSED: ${message}`);
|
||||
}
|
||||
|
||||
console.log('--- Running SpatialChatAudio Tests ---');
|
||||
|
||||
// Test 1: Module exports
|
||||
assert(typeof SpatialChatAudio === 'object', 'SpatialChatAudio exports an object');
|
||||
assert(typeof SpatialChatAudio.init === 'function', 'SpatialChatAudio has init method');
|
||||
assert(typeof SpatialChatAudio.playChatSound === 'function', 'SpatialChatAudio has playChatSound method');
|
||||
assert(typeof SpatialChatAudio.createVoiceSource === 'function', 'SpatialChatAudio has createVoiceSource method');
|
||||
|
||||
// Test 2: Config defaults
|
||||
assert(SpatialChatAudio.isEnabled() === true, 'Enabled by default');
|
||||
assert(SpatialChatAudio.getMaxHearingDistance() === 40, 'Default max hearing distance is 40');
|
||||
|
||||
// Test 3: Configuration changes
|
||||
SpatialChatAudio.setMaxHearingDistance(60);
|
||||
assert(SpatialChatAudio.getMaxHearingDistance() === 60, 'Max hearing distance updated to 60');
|
||||
|
||||
SpatialChatAudio.setEnabled(false);
|
||||
assert(SpatialChatAudio.isEnabled() === false, 'Can disable audio');
|
||||
|
||||
SpatialChatAudio.setEnabled(true);
|
||||
assert(SpatialChatAudio.isEnabled() === true, 'Can re-enable audio');
|
||||
|
||||
// Test 4: Initialization with mock camera
|
||||
const mockCamera = {
|
||||
position: new THREE.Vector3(0, 0, 0),
|
||||
getWorldDirection: () => new THREE.Vector3(1, 0, 0),
|
||||
up: new THREE.Vector3(0, 1, 0),
|
||||
add: () => {},
|
||||
getObjectByProperty: () => null,
|
||||
};
|
||||
|
||||
SpatialChatAudio.init(mockCamera);
|
||||
assert(true, 'SpatialChatAudio initializes with camera');
|
||||
|
||||
// Test 5: Voice source creation
|
||||
const voiceSource = SpatialChatAudio.createVoiceSource('timmy', new THREE.Vector3(10, 0, 0));
|
||||
assert(voiceSource !== null, 'Voice source created');
|
||||
assert(voiceSource.agentId === 'timmy', 'Voice source has correct agentId');
|
||||
assert(typeof voiceSource.updatePosition === 'function', 'Voice source has updatePosition');
|
||||
assert(typeof voiceSource.destroy === 'function', 'Voice source has destroy');
|
||||
|
||||
// Test 6: Voice source position update
|
||||
voiceSource.updatePosition(new THREE.Vector3(20, 0, 0));
|
||||
assert(true, 'Voice source position updated');
|
||||
|
||||
// Test 7: Voice source cleanup
|
||||
voiceSource.destroy();
|
||||
assert(true, 'Voice source destroyed');
|
||||
|
||||
console.log('--- All SpatialChatAudio Tests Passed ---');
|
||||
@@ -1,95 +0,0 @@
|
||||
/**
|
||||
* Tests for SymbolicDebugger component (issue #871)
|
||||
*/
|
||||
|
||||
import { SymbolicDebugger } from '../nexus/components/symbolic-debugger.js';
|
||||
import { SymbolicEngine, AgentFSM, KnowledgeGraph } from '../nexus/symbolic-engine.js';
|
||||
|
||||
// Mock DOM for Node.js testing
|
||||
if (typeof document === 'undefined') {
|
||||
const mockElements = new Map();
|
||||
|
||||
global.document = {
|
||||
createElement: (tag) => {
|
||||
const el = {
|
||||
tagName: tag,
|
||||
style: {},
|
||||
innerHTML: '',
|
||||
children: [],
|
||||
appendChild: function(child) { this.children.push(child); return child; },
|
||||
prepend: function(child) { this.children.unshift(child); return child; },
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
addEventListener: () => {},
|
||||
setAttribute: () => {},
|
||||
getAttribute: () => null,
|
||||
};
|
||||
return el;
|
||||
},
|
||||
body: {
|
||||
appendChild: (el) => {
|
||||
mockElements.set(el.id, el);
|
||||
return el;
|
||||
}
|
||||
},
|
||||
addEventListener: () => {},
|
||||
getElementById: (id) => mockElements.get(id) || {
|
||||
style: {},
|
||||
innerHTML: '',
|
||||
onclick: null,
|
||||
addEventListener: () => {},
|
||||
},
|
||||
};
|
||||
global.window = { SymbolicDebugger: null };
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
console.error(`❌ FAILED: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`✔ PASSED: ${message}`);
|
||||
}
|
||||
|
||||
console.log('--- Running Symbolic Debugger Tests ---');
|
||||
|
||||
// Test 1: Module exports
|
||||
assert(typeof SymbolicDebugger === 'object', 'SymbolicDebugger exports an object');
|
||||
assert(typeof SymbolicDebugger.init === 'function', 'SymbolicDebugger has init method');
|
||||
assert(typeof SymbolicDebugger.show === 'function', 'SymbolicDebugger has show method');
|
||||
assert(typeof SymbolicDebugger.hide === 'function', 'SymbolicDebugger has hide method');
|
||||
assert(typeof SymbolicDebugger.toggle === 'function', 'SymbolicDebugger has toggle method');
|
||||
assert(typeof SymbolicDebugger.update === 'function', 'SymbolicDebugger has update method');
|
||||
|
||||
// Test 2: Initial state
|
||||
assert(SymbolicDebugger.isVisible() === false, 'Debugger starts hidden');
|
||||
|
||||
// Test 3: Engine integration (mock)
|
||||
const mockEngine = {
|
||||
facts: new Map([['energy', 75], ['stable', true]]),
|
||||
rules: [{ condition: () => true, action: () => 'test' }],
|
||||
reasoningLog: [
|
||||
{ timestamp: Date.now(), rule: 'TestRule', outcome: 'TestOutcome' }
|
||||
]
|
||||
};
|
||||
|
||||
SymbolicDebugger.init({ engine: mockEngine });
|
||||
assert(true, 'Debugger initializes with engine');
|
||||
|
||||
// Test 4: FSM integration
|
||||
const mockFSM = { state: 'IDLE', transitions: { IDLE: [] } };
|
||||
SymbolicDebugger.init({ engine: mockEngine, fsmRegistry: new Map([['Agent1', mockFSM]]) });
|
||||
assert(true, 'Debugger initializes with FSM registry');
|
||||
|
||||
// Test 5: Knowledge Graph integration
|
||||
const mockKG = {
|
||||
nodes: new Map([
|
||||
['A', { id: 'A', type: 'Agent' }],
|
||||
['B', { id: 'B', type: 'Location' }]
|
||||
]),
|
||||
edges: [{ from: 'A', to: 'B', relation: 'AT' }]
|
||||
};
|
||||
SymbolicDebugger.init({ engine: mockEngine, knowledgeGraph: mockKG });
|
||||
assert(true, 'Debugger initializes with Knowledge Graph');
|
||||
|
||||
console.log('--- All Symbolic Debugger Tests Passed ---');
|
||||
Reference in New Issue
Block a user