[claude] Mnemosyne holographic fact detail panel (#1172) #1177

Merged
claude merged 1 commits from claude/issue-1172 into main 2026-04-10 22:29:18 +00:00
3 changed files with 259 additions and 18 deletions

136
app.js
View File

@@ -1909,7 +1909,7 @@ function setupControls() {
const memInfo = SpatialMemory.getMemoryFromMesh(hitMesh);
if (memInfo) {
SpatialMemory.highlightMemory(memInfo.data.id);
showMemoryPanel(memInfo);
showMemoryPanel(memInfo, e.clientX, e.clientY);
return;
}
}
@@ -2576,46 +2576,162 @@ let pulseTimer = 0;
// MNEMOSYNE — MEMORY CRYSTAL INSPECTION
// ═══════════════════════════════════════════
// ── pin state for memory panel ──
let _memPanelPinned = false;
/** Convert a packed hex color integer to "r,g,b" string for CSS rgba(). */
function _hexToRgb(hex) {
return ((hex >> 16) & 255) + ',' + ((hex >> 8) & 255) + ',' + (hex & 255);
}
/**
* Show the memory inspection panel with data from a clicked crystal.
* Position the panel near the screen click coordinates, keeping it on-screen.
*/
function showMemoryPanel(memInfo) {
function _positionPanel(panel, clickX, clickY) {
const W = window.innerWidth;
const H = window.innerHeight;
const panelW = 356; // matches CSS width + padding
const panelH = 420; // generous estimate
const margin = 12;
let left = clickX + 24;
if (left + panelW > W - margin) left = clickX - panelW - 24;
left = Math.max(margin, Math.min(W - panelW - margin, left));
let top = clickY - 80;
top = Math.max(margin, Math.min(H - panelH - margin, top));
panel.style.right = 'auto';
panel.style.top = top + 'px';
panel.style.left = left + 'px';
panel.style.transform = 'none';
}
/**
* Navigate to (highlight + show panel for) a memory crystal by id.
*/
function _navigateToMemory(memId) {
SpatialMemory.highlightMemory(memId);
addChatMessage('system', `Focus: ${memId.replace(/_/g, ' ')}`);
const meshes = SpatialMemory.getCrystalMeshes();
for (const mesh of meshes) {
if (mesh.userData && mesh.userData.memId === memId) {
const memInfo = SpatialMemory.getMemoryFromMesh(mesh);
if (memInfo) { showMemoryPanel(memInfo); break; }
}
}
}
/**
* Show the holographic detail panel for a clicked crystal.
* @param {object} memInfo — { data, region } from SpatialMemory.getMemoryFromMesh()
* @param {number} [clickX] — screen X of the click (for panel positioning)
* @param {number} [clickY] — screen Y of the click
*/
function showMemoryPanel(memInfo, clickX, clickY) {
const panel = document.getElementById('memory-panel');
if (!panel) return;
const { data, region } = memInfo;
const regionDef = SpatialMemory.REGIONS[region] || SpatialMemory.REGIONS.working;
const colorHex = regionDef.color.toString(16).padStart(6, '0');
const colorRgb = _hexToRgb(regionDef.color);
// Header — region dot + label
document.getElementById('memory-panel-region').textContent = regionDef.label;
document.getElementById('memory-panel-region-dot').style.background = '#' + regionDef.color.toString(16).padStart(6, '0');
document.getElementById('memory-panel-region-dot').style.background = '#' + colorHex;
// Category badge
const badge = document.getElementById('memory-panel-category-badge');
if (badge) {
badge.textContent = (data.category || region || 'memory').toUpperCase();
badge.style.background = 'rgba(' + colorRgb + ',0.16)';
badge.style.color = '#' + colorHex;
badge.style.borderColor = 'rgba(' + colorRgb + ',0.4)';
}
// Entity name (humanised id)
const entityEl = document.getElementById('memory-panel-entity-name');
if (entityEl) entityEl.textContent = (data.id || '\u2014').replace(/_/g, ' ');
// Fact content
document.getElementById('memory-panel-content').textContent = data.content || '(empty)';
// Trust score bar
const strength = data.strength != null ? data.strength : 0.7;
const trustFill = document.getElementById('memory-panel-trust-fill');
const trustVal = document.getElementById('memory-panel-trust-value');
if (trustFill) {
trustFill.style.width = (strength * 100).toFixed(0) + '%';
trustFill.style.background = '#' + colorHex;
}
if (trustVal) trustVal.textContent = (strength * 100).toFixed(0) + '%';
// Meta rows
document.getElementById('memory-panel-id').textContent = data.id || '\u2014';
document.getElementById('memory-panel-source').textContent = data.source || 'unknown';
document.getElementById('memory-panel-time').textContent = data.timestamp ? new Date(data.timestamp).toLocaleString() : '\u2014';
// Related entities — clickable links
const connEl = document.getElementById('memory-panel-connections');
connEl.innerHTML = '';
if (data.connections && data.connections.length > 0) {
data.connections.forEach(cid => {
const tag = document.createElement('span');
tag.className = 'memory-conn-tag';
tag.textContent = cid;
connEl.appendChild(tag);
const btn = document.createElement('button');
btn.className = 'memory-conn-tag memory-conn-link';
btn.textContent = cid.replace(/_/g, ' ');
btn.title = 'Go to: ' + cid;
btn.addEventListener('click', (ev) => { ev.stopPropagation(); _navigateToMemory(cid); });
connEl.appendChild(btn);
});
} else {
connEl.innerHTML = '<span style="color:var(--color-text-muted)">None</span>';
}
// Pin button — reset on fresh open
_memPanelPinned = false;
const pinBtn = document.getElementById('memory-panel-pin');
if (pinBtn) {
pinBtn.classList.remove('pinned');
pinBtn.title = 'Pin panel';
pinBtn.onclick = () => {
_memPanelPinned = !_memPanelPinned;
pinBtn.classList.toggle('pinned', _memPanelPinned);
pinBtn.title = _memPanelPinned ? 'Unpin panel' : 'Pin panel';
};
}
// Positioning — near click if coords provided
if (clickX != null && clickY != null) {
_positionPanel(panel, clickX, clickY);
}
// Fade in
panel.classList.remove('memory-panel-fade-out');
panel.style.display = 'flex';
}
/**
* Dismiss the memory panel and clear any crystal highlight.
* Dismiss the panel (respects pin). Called on empty-space click.
*/
function dismissMemoryPanel() {
if (_memPanelPinned) return;
_dismissMemoryPanelForce();
}
/**
* Force-dismiss the panel regardless of pin state. Used by the close button.
*/
function _dismissMemoryPanelForce() {
_memPanelPinned = false;
SpatialMemory.clearHighlight();
const panel = document.getElementById('memory-panel');
if (panel) panel.style.display = 'none';
if (!panel || panel.style.display === 'none') return;
panel.classList.add('memory-panel-fade-out');
setTimeout(() => {
panel.style.display = 'none';
panel.classList.remove('memory-panel-fade-out');
}, 200);
}

View File

@@ -212,16 +212,26 @@
<div id="memory-panel" class="memory-panel" style="display:none;">
<div class="memory-panel-content">
<div class="memory-panel-header">
<span class="memory-category-badge" id="memory-panel-category-badge">MEM</span>
<div class="memory-panel-region-dot" id="memory-panel-region-dot"></div>
<div class="memory-panel-region" id="memory-panel-region">MEMORY</div>
<button id="memory-panel-close" class="memory-panel-close" onclick="dismissMemoryPanel()">\u2715</button>
<button id="memory-panel-pin" class="memory-panel-pin" title="Pin panel">&#x1F4CC;</button>
<button id="memory-panel-close" class="memory-panel-close" onclick="_dismissMemoryPanelForce()">\u2715</button>
</div>
<div class="memory-entity-name" id="memory-panel-entity-name">\u2014</div>
<div class="memory-panel-body" id="memory-panel-content">(empty)</div>
<div class="memory-trust-row">
<span class="memory-meta-label">Trust</span>
<div class="memory-trust-bar">
<div class="memory-trust-fill" id="memory-panel-trust-fill"></div>
</div>
<span class="memory-trust-value" id="memory-panel-trust-value"></span>
</div>
<div class="memory-panel-meta">
<div class="memory-meta-row"><span class="memory-meta-label">ID</span><span id="memory-panel-id">\u2014</span></div>
<div class="memory-meta-row"><span class="memory-meta-label">Source</span><span id="memory-panel-source">\u2014</span></div>
<div class="memory-meta-row"><span class="memory-meta-label">Time</span><span id="memory-panel-time">\u2014</span></div>
<div class="memory-meta-row"><span class="memory-meta-label">Links</span><span id="memory-panel-connections">\u2014</span></div>
<div class="memory-meta-row memory-meta-row--related"><span class="memory-meta-label">Related</span><span id="memory-panel-connections">\u2014</span></div>
</div>
</div>
</div>

127
style.css
View File

@@ -1235,16 +1235,25 @@ canvas#nexus-canvas {
right: 24px;
transform: translateY(-50%);
z-index: 120;
animation: memoryPanelIn 0.25s ease-out;
animation: memoryPanelIn 0.22s ease-out forwards;
}
.memory-panel-fade-out {
animation: memoryPanelOut 0.18s ease-in forwards !important;
}
@keyframes memoryPanelIn {
from { opacity: 0; transform: translateY(-50%) translateX(20px); }
to { opacity: 1; transform: translateY(-50%) translateX(0); }
from { opacity: 0; transform: translateY(-50%) translateX(16px); }
to { opacity: 1; transform: translateY(-50%) translateX(0); }
}
@keyframes memoryPanelOut {
from { opacity: 1; }
to { opacity: 0; transform: translateY(-50%) translateX(12px); }
}
.memory-panel-content {
width: 320px;
width: 340px;
background: rgba(8, 8, 24, 0.92);
backdrop-filter: blur(12px);
border: 1px solid rgba(74, 240, 192, 0.25);
@@ -1256,8 +1265,8 @@ canvas#nexus-canvas {
.memory-panel-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
gap: 6px;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
@@ -1346,3 +1355,109 @@ canvas#nexus-canvas {
margin: 1px 2px;
}
.memory-conn-link {
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.memory-conn-link:hover {
background: rgba(74, 240, 192, 0.22);
border-color: rgba(74, 240, 192, 0.5);
color: #fff;
}
/* Entity name — large heading inside panel */
.memory-entity-name {
font-family: var(--font-display, monospace);
font-size: 17px;
font-weight: 700;
color: #fff;
letter-spacing: 0.04em;
margin-bottom: 8px;
text-transform: capitalize;
word-break: break-word;
}
/* Category badge */
.memory-category-badge {
font-family: var(--font-display, monospace);
font-size: 9px;
letter-spacing: 0.12em;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid rgba(74, 240, 192, 0.3);
background: rgba(74, 240, 192, 0.12);
color: var(--color-primary, #4af0c0);
flex-shrink: 0;
}
/* Trust score bar */
.memory-trust-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 11px;
}
.memory-trust-bar {
flex: 1;
height: 5px;
background: rgba(255, 255, 255, 0.08);
border-radius: 3px;
overflow: hidden;
}
.memory-trust-fill {
height: 100%;
border-radius: 3px;
background: var(--color-primary, #4af0c0);
transition: width 0.35s ease;
}
.memory-trust-value {
color: var(--color-text-muted, #888);
min-width: 32px;
text-align: right;
}
/* Pin button */
.memory-panel-pin {
background: none;
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--color-text-muted, #888);
font-size: 11px;
cursor: pointer;
width: 24px;
height: 24px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
flex-shrink: 0;
}
.memory-panel-pin:hover {
background: rgba(255, 255, 255, 0.05);
color: #fff;
}
.memory-panel-pin.pinned {
background: rgba(74, 240, 192, 0.15);
border-color: rgba(74, 240, 192, 0.4);
color: var(--color-primary, #4af0c0);
}
/* Related row — allow wrapping */
.memory-meta-row--related {
align-items: flex-start;
}
.memory-meta-row--related span:last-child {
flex-wrap: wrap;
display: flex;
gap: 2px;
}