Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
7fde7569e4 fix: [VISITOR] Distinguish visitor mode from operator mode in the Nexus UI (closes #710) 2026-04-10 20:10:33 -04:00
4 changed files with 138 additions and 107 deletions

66
app.js
View File

@@ -48,6 +48,9 @@ let chatOpen = true;
let loadProgress = 0;
let performanceTier = 'high';
// ═══ UI MODE (VISITOR / OPERATOR) ═══
let uiMode = 'visitor'; // 'visitor' | 'operator'
// ═══ HERMES WS STATE ═══
let hermesWs = null;
let wsReconnectTimer = null;
@@ -1867,6 +1870,9 @@ function setupControls() {
if (e.key.toLowerCase() === 'v' && document.activeElement !== document.getElementById('chat-input')) {
cycleNavMode();
}
if (e.key.toLowerCase() === 'o' && document.activeElement !== document.getElementById('chat-input')) {
toggleUIMode();
}
if (e.key.toLowerCase() === 'f' && activePortal && !portalOverlayActive) {
activatePortal(activePortal);
}
@@ -1964,18 +1970,9 @@ function setupControls() {
});
document.getElementById('chat-send').addEventListener('click', () => sendChatMessage());
// Add MemPalace mining button
// Add MemPalace mining button (operator-only)
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>
<button class="quick-action-btn operator-only" onclick="mineMemPalaceContent()">Mine Chat</button>
`;
// Chat quick actions
@@ -2006,6 +2003,53 @@ function setupControls() {
document.getElementById('atlas-toggle-btn').addEventListener('click', openPortalAtlas);
document.getElementById('atlas-close-btn').addEventListener('click', closePortalAtlas);
// ═══ VISITOR / OPERATOR MODE TOGGLE ═══
// Restore saved mode from localStorage
const savedMode = localStorage.getItem('nexus-ui-mode');
if (savedMode === 'operator') {
uiMode = 'operator';
}
applyUIMode();
// Create and append mode toggle button to the HUD controls area
const modeBtn = document.createElement('button');
modeBtn.id = 'mode-toggle-btn';
modeBtn.className = 'mode-toggle-btn';
modeBtn.setAttribute('data-mode', uiMode);
modeBtn.innerHTML = modeToggleLabel();
modeBtn.title = uiMode === 'visitor' ? 'Switch to Operator mode' : 'Switch to Visitor mode';
modeBtn.addEventListener('click', toggleUIMode);
const hudEl = document.getElementById('hud');
if (hudEl) hudEl.appendChild(modeBtn);
}
function toggleUIMode() {
uiMode = uiMode === 'visitor' ? 'operator' : 'visitor';
localStorage.setItem('nexus-ui-mode', uiMode);
applyUIMode();
// Update button
const btn = document.getElementById('mode-toggle-btn');
if (btn) {
btn.setAttribute('data-mode', uiMode);
btn.innerHTML = modeToggleLabel();
btn.title = uiMode === 'visitor' ? 'Switch to Operator mode' : 'Switch to Visitor mode';
}
addChatMessage('system', `Mode: ${uiMode.toUpperCase()}. ${uiMode === 'operator' ? 'Operator panels enabled.' : 'Visitor view. Clean and focused.'}`);
}
function modeToggleLabel() {
if (uiMode === 'visitor') {
return '<span class="mode-icon">👁</span> VISITOR';
}
return '<span class="mode-icon">⚙</span> OPERATOR';
}
function applyUIMode() {
document.body.classList.toggle('visitor-mode', uiMode === 'visitor');
}
function sendChatMessage(overrideText = null) {

View File

@@ -157,7 +157,8 @@
<!-- Controls hint + nav mode -->
<div class="hud-controls">
<span>WASD</span> move &nbsp; <span>Mouse</span> look &nbsp; <span>Enter</span> chat &nbsp;
<span>V</span> mode: <span id="nav-mode-label">WALK</span>
<span>V</span> nav: <span id="nav-mode-label">WALK</span> &nbsp;
<span>O</span> toggle mode &nbsp;
<span id="nav-mode-hint" class="nav-mode-hint"></span>
&nbsp; <span class="ws-hud-status">HERMES: <span id="ws-status-dot" class="chat-status-dot"></span></span>
</div>

View File

@@ -133,9 +133,6 @@ const SpatialMemory = (() => {
let _regionMarkers = {};
let _memoryObjects = {};
let _connectionLines = [];
let _entityLines = []; // entity resolution lines (issue #1167)
let _camera = null; // set by setCamera() for LOD culling
const ENTITY_LOD_DIST = 50; // hide entity lines when camera > this from midpoint
let _initialized = false;
// ─── CRYSTAL GEOMETRY (persistent memories) ───────────
@@ -255,10 +252,6 @@ const SpatialMemory = (() => {
_drawConnections(mem.id, mem.connections);
}
if (mem.entity) {
_drawEntityLines(mem.id, mem);
}
_dirty = true;
saveToStorage();
console.info('[Mnemosyne] Spatial memory placed:', mem.id, 'in', region.label);
@@ -305,77 +298,6 @@ const SpatialMemory = (() => {
});
}
// ─── ENTITY RESOLUTION LINES (#1167) ──────────────────
// Draw lines between crystals that share an entity or are related entities.
// Same entity → thin blue line. Related entities → thin purple dashed line.
function _drawEntityLines(memId, mem) {
if (!mem.entity) return;
const src = _memoryObjects[memId];
if (!src) return;
Object.entries(_memoryObjects).forEach(([otherId, other]) => {
if (otherId === memId) return;
const otherData = other.data;
if (!otherData.entity) return;
let lineType = null;
if (otherData.entity === mem.entity) {
lineType = 'same_entity';
} else if (mem.related_entities && mem.related_entities.includes(otherData.entity)) {
lineType = 'related';
} else if (otherData.related_entities && otherData.related_entities.includes(mem.entity)) {
lineType = 'related';
}
if (!lineType) return;
// Deduplicate — only draw from lower ID to higher
if (memId > otherId) return;
const points = [src.mesh.position.clone(), other.mesh.position.clone()];
const geo = new THREE.BufferGeometry().setFromPoints(points);
let mat;
if (lineType === 'same_entity') {
mat = new THREE.LineBasicMaterial({ color: 0x4488ff, transparent: true, opacity: 0.35 });
} else {
mat = new THREE.LineDashedMaterial({ color: 0x9966ff, dashSize: 0.3, gapSize: 0.2, transparent: true, opacity: 0.25 });
const line = new THREE.Line(geo, mat);
line.computeLineDistances();
line.userData = { type: 'entity_line', from: memId, to: otherId, lineType };
_scene.add(line);
_entityLines.push(line);
return;
}
const line = new THREE.Line(geo, mat);
line.userData = { type: 'entity_line', from: memId, to: otherId, lineType };
_scene.add(line);
_entityLines.push(line);
});
}
function _updateEntityLines() {
if (!_camera) return;
const camPos = _camera.position;
_entityLines.forEach(line => {
// Compute midpoint of line
const posArr = line.geometry.attributes.position.array;
const mx = (posArr[0] + posArr[3]) / 2;
const my = (posArr[1] + posArr[4]) / 2;
const mz = (posArr[2] + posArr[5]) / 2;
const dist = camPos.distanceTo(new THREE.Vector3(mx, my, mz));
if (dist > ENTITY_LOD_DIST) {
line.visible = false;
} else {
line.visible = true;
// Fade based on distance
const fade = Math.max(0, 1 - (dist / ENTITY_LOD_DIST));
const baseOpacity = line.userData.lineType === 'same_entity' ? 0.35 : 0.25;
line.material.opacity = baseOpacity * fade;
}
});
}
// ─── REMOVE A MEMORY ─────────────────────────────────
function removeMemory(memId) {
const obj = _memoryObjects[memId];
@@ -395,16 +317,6 @@ const SpatialMemory = (() => {
}
}
for (let i = _entityLines.length - 1; i >= 0; i--) {
const line = _entityLines[i];
if (line.userData.from === memId || line.userData.to === memId) {
if (line.parent) line.parent.remove(line);
line.geometry.dispose();
line.material.dispose();
_entityLines.splice(i, 1);
}
}
delete _memoryObjects[memId];
_dirty = true;
saveToStorage();
@@ -430,8 +342,6 @@ const SpatialMemory = (() => {
}
});
_updateEntityLines();
Object.values(_regionMarkers).forEach(marker => {
if (marker.ring && marker.ring.material) {
marker.ring.material.opacity = 0.1 + Math.sin(now * 0.001) * 0.05;
@@ -742,11 +652,6 @@ const SpatialMemory = (() => {
return _selectedId;
}
// ─── CAMERA REFERENCE (for entity line LOD) ─────────
function setCamera(camera) {
_camera = camera;
}
return {
init, placeMemory, removeMemory, update,
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,

View File

@@ -1580,3 +1580,84 @@ canvas#nexus-canvas {
text-transform: uppercase;
}
/* === VISITOR / OPERATOR MODE TOGGLE === */
.mode-toggle-btn {
position: absolute;
bottom: var(--space-3);
right: var(--space-3);
pointer-events: auto;
background: rgba(10, 15, 40, 0.7);
border: 1px solid var(--color-primary);
color: var(--color-primary);
padding: 6px 14px;
font-family: var(--font-display);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.12em;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
backdrop-filter: blur(5px);
transition: all var(--transition-ui);
z-index: 20;
}
.mode-toggle-btn:hover {
background: var(--color-primary);
color: var(--color-bg);
box-shadow: 0 0 15px var(--color-primary);
}
.mode-toggle-btn .mode-icon {
font-size: 14px;
}
.mode-toggle-btn[data-mode="operator"] {
border-color: var(--color-gold);
color: var(--color-gold);
box-shadow: 0 0 8px rgba(255, 215, 0, 0.15);
}
.mode-toggle-btn[data-mode="operator"]:hover {
background: var(--color-gold);
color: var(--color-bg);
box-shadow: 0 0 15px var(--color-gold);
}
/* Visitor mode: hide operator-only surfaces */
body.visitor-mode .gofai-hud,
body.visitor-mode .hud-agent-log,
body.visitor-mode .hud-debug,
body.visitor-mode .bannerlord-hud,
body.visitor-mode #mem-palace-status,
body.visitor-mode #mem-palace-controls,
body.visitor-mode #mempalace-results,
body.visitor-mode .mem-palace-ui,
body.visitor-mode .mem-palace-stats,
body.visitor-mode .ws-hud-status,
body.visitor-mode .chat-quick-actions .operator-only,
body.visitor-mode .nexus-footer {
display: none !important;
}
/* Visitor mode: simplify controls hint */
body.visitor-mode .hud-controls {
font-size: var(--text-xs);
opacity: 0.6;
}
/* Visitor mode: cleaner chat */
body.visitor-mode .chat-panel {
width: 320px;
max-height: 300px;
}
/* Operator badge in operator mode */
body:not(.visitor-mode) .hud-controls::after {
content: ' OPERATOR';
color: var(--color-gold);
font-weight: 700;
letter-spacing: 0.1em;
}