Compare commits
2 Commits
mimo/code/
...
feat/mnemo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b505d6d65b | ||
|
|
a1e7af36f2 |
25
app.js
25
app.js
@@ -6,6 +6,7 @@ import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
|
|||||||
import { SpatialMemory } from './nexus/components/spatial-memory.js';
|
import { SpatialMemory } from './nexus/components/spatial-memory.js';
|
||||||
import { MemoryBirth } from './nexus/components/memory-birth.js';
|
import { MemoryBirth } from './nexus/components/memory-birth.js';
|
||||||
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
|
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
|
||||||
|
import { MemoryInspect } from './nexus/components/memory-inspect.js';
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
// NEXUS v1.1 — Portal System Update
|
// NEXUS v1.1 — Portal System Update
|
||||||
@@ -712,6 +713,7 @@ async function init() {
|
|||||||
MemoryBirth.init(scene);
|
MemoryBirth.init(scene);
|
||||||
MemoryBirth.wrapSpatialMemory(SpatialMemory);
|
MemoryBirth.wrapSpatialMemory(SpatialMemory);
|
||||||
SpatialMemory.setCamera(camera);
|
SpatialMemory.setCamera(camera);
|
||||||
|
MemoryInspect.init(SpatialMemory);
|
||||||
updateLoad(90);
|
updateLoad(90);
|
||||||
|
|
||||||
loadSession();
|
loadSession();
|
||||||
@@ -1904,7 +1906,7 @@ function setupControls() {
|
|||||||
orbitState.lastX = e.clientX;
|
orbitState.lastX = e.clientX;
|
||||||
orbitState.lastY = e.clientY;
|
orbitState.lastY = e.clientY;
|
||||||
|
|
||||||
// Raycasting for portals
|
// Raycasting for portals and memory crystals
|
||||||
if (!portalOverlayActive) {
|
if (!portalOverlayActive) {
|
||||||
const mouse = new THREE.Vector2(
|
const mouse = new THREE.Vector2(
|
||||||
(e.clientX / window.innerWidth) * 2 - 1,
|
(e.clientX / window.innerWidth) * 2 - 1,
|
||||||
@@ -1912,11 +1914,26 @@ function setupControls() {
|
|||||||
);
|
);
|
||||||
const raycaster = new THREE.Raycaster();
|
const raycaster = new THREE.Raycaster();
|
||||||
raycaster.setFromCamera(mouse, camera);
|
raycaster.setFromCamera(mouse, camera);
|
||||||
const intersects = raycaster.intersectObjects(portals.map(p => p.ring));
|
|
||||||
if (intersects.length > 0) {
|
// Check portals first
|
||||||
const clickedRing = intersects[0].object;
|
const portalHits = raycaster.intersectObjects(portals.map(p => p.ring));
|
||||||
|
if (portalHits.length > 0) {
|
||||||
|
const clickedRing = portalHits[0].object;
|
||||||
const portal = portals.find(p => p.ring === clickedRing);
|
const portal = portals.find(p => p.ring === clickedRing);
|
||||||
if (portal) activatePortal(portal);
|
if (portal) activatePortal(portal);
|
||||||
|
} else {
|
||||||
|
// Check memory crystals
|
||||||
|
const crystalMeshes = SpatialMemory.getCrystalMeshes();
|
||||||
|
const crystalHits = raycaster.intersectObjects(crystalMeshes);
|
||||||
|
if (crystalHits.length > 0) {
|
||||||
|
const hitMesh = crystalHits[0].object;
|
||||||
|
const memInfo = SpatialMemory.getMemoryFromMesh(hitMesh);
|
||||||
|
if (memInfo && memInfo.data) {
|
||||||
|
MemoryInspect.inspect(memInfo.data.id);
|
||||||
|
}
|
||||||
|
} else if (MemoryInspect.isOpen()) {
|
||||||
|
MemoryInspect.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
407
nexus/components/memory-inspect.js
Normal file
407
nexus/components/memory-inspect.js
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
/**
|
||||||
|
* Memory Inspect Panel — click-to-read detail view for Mnemosyne crystals.
|
||||||
|
*
|
||||||
|
* When a memory crystal is clicked in the Nexus, this panel slides in from
|
||||||
|
* the right showing the memory's content, links, region, and metadata.
|
||||||
|
*
|
||||||
|
* Depends on SpatialMemory (for data access) — wired from app.js.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const MemoryInspect = (() => {
|
||||||
|
let _panel = null;
|
||||||
|
let _isOpen = false;
|
||||||
|
let _currentMemId = null;
|
||||||
|
let _spatialMemory = null;
|
||||||
|
|
||||||
|
// ─── PUBLIC API ──────────────────────────────────────
|
||||||
|
|
||||||
|
function init(spatialMemoryRef) {
|
||||||
|
_spatialMemory = spatialMemoryRef;
|
||||||
|
_buildPanel();
|
||||||
|
_injectStyles();
|
||||||
|
console.info('[Mnemosyne] Memory Inspect panel initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
function inspect(memId) {
|
||||||
|
if (!_spatialMemory) return;
|
||||||
|
const memObj = _getMemoryObject(memId);
|
||||||
|
if (!memObj) return;
|
||||||
|
|
||||||
|
_currentMemId = memId;
|
||||||
|
_renderPanel(memObj);
|
||||||
|
_open();
|
||||||
|
_spatialMemory.highlightMemory(memId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
if (!_isOpen) return;
|
||||||
|
if (_currentMemId && _spatialMemory) {
|
||||||
|
_spatialMemory.clearHighlight();
|
||||||
|
}
|
||||||
|
_currentMemId = null;
|
||||||
|
_close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOpen() {
|
||||||
|
return _isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentMemId() {
|
||||||
|
return _currentMemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── INTERNALS ───────────────────────────────────────
|
||||||
|
|
||||||
|
function _getMemoryObject(memId) {
|
||||||
|
// Access SpatialMemory's internal _memoryObjects via getAllMemories
|
||||||
|
const all = _spatialMemory.getAllMemories();
|
||||||
|
if (!all) return null;
|
||||||
|
// getAllMemories returns array of { id, ...data } objects
|
||||||
|
const entry = all.find(m => m.id === memId);
|
||||||
|
if (!entry) return null;
|
||||||
|
|
||||||
|
// Get region info
|
||||||
|
const regions = _spatialMemory.REGIONS;
|
||||||
|
const region = regions[entry.category] || regions.working;
|
||||||
|
|
||||||
|
return { data: entry, region };
|
||||||
|
}
|
||||||
|
|
||||||
|
function _buildPanel() {
|
||||||
|
_panel = document.createElement('div');
|
||||||
|
_panel.id = 'memory-inspect-panel';
|
||||||
|
_panel.className = 'memory-inspect-panel memory-inspect-hidden';
|
||||||
|
_panel.innerHTML = `
|
||||||
|
<div class="memory-inspect-header">
|
||||||
|
<span class="memory-inspect-glyph" id="inspect-glyph"></span>
|
||||||
|
<span class="memory-inspect-title" id="inspect-title"></span>
|
||||||
|
<button class="memory-inspect-close" id="inspect-close" title="Close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="memory-inspect-body">
|
||||||
|
<div class="memory-inspect-meta" id="inspect-meta"></div>
|
||||||
|
<div class="memory-inspect-content" id="inspect-content"></div>
|
||||||
|
<div class="memory-inspect-links" id="inspect-links"></div>
|
||||||
|
</div>
|
||||||
|
<div class="memory-inspect-footer" id="inspect-footer"></div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(_panel);
|
||||||
|
|
||||||
|
document.getElementById('inspect-close').addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _renderPanel(memObj) {
|
||||||
|
const { data, region } = memObj;
|
||||||
|
const strength = data.strength != null ? data.strength : 0.7;
|
||||||
|
const vitalityBand = _getVitalityBand(strength);
|
||||||
|
const bandColors = {
|
||||||
|
vibrant: '#00e5ff',
|
||||||
|
alive: '#4488ff',
|
||||||
|
fading: '#ffaa00',
|
||||||
|
dim: '#ff6644',
|
||||||
|
ghost: '#667788'
|
||||||
|
};
|
||||||
|
const bandColor = bandColors[vitalityBand] || bandColors.alive;
|
||||||
|
|
||||||
|
// Header
|
||||||
|
document.getElementById('inspect-glyph').textContent = region.glyph || '\uD83D\uDCCB';
|
||||||
|
document.getElementById('inspect-title').textContent = data.content
|
||||||
|
? (data.content.length > 60 ? data.content.slice(0, 60) + '\u2026' : data.content)
|
||||||
|
: data.id || 'Memory';
|
||||||
|
|
||||||
|
// Meta bar
|
||||||
|
const metaEl = document.getElementById('inspect-meta');
|
||||||
|
const catLabel = region.label || data.category || 'Unknown';
|
||||||
|
const created = data.timestamp ? _formatTime(data.timestamp) : '';
|
||||||
|
metaEl.innerHTML = `
|
||||||
|
<span class="inspect-badge" style="border-color:${'#' + region.color.toString(16).padStart(6,'0')}">${catLabel}</span>
|
||||||
|
<span class="inspect-vitality" style="color:${bandColor}">
|
||||||
|
\u25CF ${vitalityBand} (${Math.round(strength * 100)}%)
|
||||||
|
</span>
|
||||||
|
${created ? `<span class="inspect-time">${created}</span>` : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Content
|
||||||
|
const contentEl = document.getElementById('inspect-content');
|
||||||
|
contentEl.textContent = data.content || '(no content)';
|
||||||
|
|
||||||
|
// Links
|
||||||
|
const linksEl = document.getElementById('inspect-links');
|
||||||
|
const connections = data.connections || [];
|
||||||
|
if (connections.length > 0) {
|
||||||
|
let linksHtml = '<div class="inspect-links-header">Linked Memories (' + connections.length + ')</div>';
|
||||||
|
linksHtml += '<div class="inspect-links-list">';
|
||||||
|
connections.forEach(cid => {
|
||||||
|
const linked = _getLinkedPreview(cid);
|
||||||
|
linksHtml += `<div class="inspect-link-item" data-mem-id="${cid}">
|
||||||
|
<span class="inspect-link-dot"></span>
|
||||||
|
<span class="inspect-link-text">${linked}</span>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
linksHtml += '</div>';
|
||||||
|
linksEl.innerHTML = linksHtml;
|
||||||
|
|
||||||
|
// Wire click handlers for linked memories
|
||||||
|
linksEl.querySelectorAll('.inspect-link-item').forEach(el => {
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
const targetId = el.dataset.memId;
|
||||||
|
if (targetId) inspect(targetId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
linksEl.innerHTML = '<div class="inspect-no-links">No linked memories</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
const footerEl = document.getElementById('inspect-footer');
|
||||||
|
footerEl.innerHTML = `
|
||||||
|
<button class="inspect-action-btn" id="inspect-copy-btn" title="Copy content">\uD83D\uDCCB Copy</button>
|
||||||
|
<span class="inspect-id" title="Memory ID">${data.id || ''}</span>
|
||||||
|
`;
|
||||||
|
document.getElementById('inspect-copy-btn').addEventListener('click', () => {
|
||||||
|
navigator.clipboard.writeText(data.content || '').then(() => {
|
||||||
|
const btn = document.getElementById('inspect-copy-btn');
|
||||||
|
btn.textContent = '\u2713 Copied';
|
||||||
|
setTimeout(() => { btn.textContent = '\uD83D\uDCCB Copy'; }, 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getLinkedPreview(memId) {
|
||||||
|
if (!_spatialMemory) return memId;
|
||||||
|
const all = _spatialMemory.getAllMemories();
|
||||||
|
if (!all) return memId;
|
||||||
|
const entry = all.find(m => m.id === memId);
|
||||||
|
if (!entry || !entry.content) return memId;
|
||||||
|
return entry.content.length > 40 ? entry.content.slice(0, 40) + '\u2026' : entry.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getVitalityBand(strength) {
|
||||||
|
if (strength >= 0.8) return 'vibrant';
|
||||||
|
if (strength >= 0.5) return 'alive';
|
||||||
|
if (strength >= 0.25) return 'fading';
|
||||||
|
if (strength >= 0.1) return 'dim';
|
||||||
|
return 'ghost';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _formatTime(isoStr) {
|
||||||
|
try {
|
||||||
|
const d = new Date(isoStr);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now - d;
|
||||||
|
const diffMin = Math.floor(diffMs / 60000);
|
||||||
|
if (diffMin < 1) return 'just now';
|
||||||
|
if (diffMin < 60) return diffMin + 'm ago';
|
||||||
|
const diffHr = Math.floor(diffMin / 60);
|
||||||
|
if (diffHr < 24) return diffHr + 'h ago';
|
||||||
|
const diffDay = Math.floor(diffHr / 24);
|
||||||
|
if (diffDay < 30) return diffDay + 'd ago';
|
||||||
|
return d.toLocaleDateString();
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _open() {
|
||||||
|
_isOpen = true;
|
||||||
|
_panel.classList.remove('memory-inspect-hidden');
|
||||||
|
_panel.classList.add('memory-inspect-visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _close() {
|
||||||
|
_isOpen = false;
|
||||||
|
_panel.classList.remove('memory-inspect-visible');
|
||||||
|
_panel.classList.add('memory-inspect-hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _injectStyles() {
|
||||||
|
if (document.getElementById('memory-inspect-styles')) return;
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'memory-inspect-styles';
|
||||||
|
style.textContent = `
|
||||||
|
.memory-inspect-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 60px;
|
||||||
|
right: 0;
|
||||||
|
width: 340px;
|
||||||
|
max-height: calc(100vh - 80px);
|
||||||
|
background: rgba(8, 12, 32, 0.92);
|
||||||
|
border: 1px solid rgba(74, 240, 192, 0.2);
|
||||||
|
border-right: none;
|
||||||
|
border-radius: 8px 0 0 8px;
|
||||||
|
color: #c8d8e8;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
z-index: 900;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.5);
|
||||||
|
transition: transform 0.25s ease, opacity 0.25s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.memory-inspect-hidden {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.memory-inspect-visible {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.memory-inspect-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid rgba(74, 240, 192, 0.15);
|
||||||
|
background: rgba(74, 240, 192, 0.05);
|
||||||
|
}
|
||||||
|
.memory-inspect-glyph {
|
||||||
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.memory-inspect-title {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4af0c0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.memory-inspect-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #667788;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 4px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.memory-inspect-close:hover {
|
||||||
|
color: #ff4466;
|
||||||
|
}
|
||||||
|
.memory-inspect-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.memory-inspect-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.inspect-badge {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.inspect-vitality {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.inspect-time {
|
||||||
|
color: #556677;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.memory-inspect-content {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.inspect-links-header {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #7b5cff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.inspect-links-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.inspect-link-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: rgba(123, 92, 255, 0.08);
|
||||||
|
border: 1px solid rgba(123, 92, 255, 0.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.inspect-link-item:hover {
|
||||||
|
background: rgba(123, 92, 255, 0.18);
|
||||||
|
}
|
||||||
|
.inspect-link-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #7b5cff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.inspect-link-text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.inspect-no-links {
|
||||||
|
color: #445566;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.memory-inspect-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-top: 1px solid rgba(74, 240, 192, 0.1);
|
||||||
|
}
|
||||||
|
.inspect-action-btn {
|
||||||
|
background: rgba(74, 240, 192, 0.1);
|
||||||
|
border: 1px solid rgba(74, 240, 192, 0.25);
|
||||||
|
color: #4af0c0;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.inspect-action-btn:hover {
|
||||||
|
background: rgba(74, 240, 192, 0.2);
|
||||||
|
}
|
||||||
|
.inspect-id {
|
||||||
|
color: #334455;
|
||||||
|
font-size: 9px;
|
||||||
|
max-width: 160px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { init, inspect, close, isOpen, getCurrentMemId };
|
||||||
|
})();
|
||||||
|
|
||||||
|
export { MemoryInspect };
|
||||||
Reference in New Issue
Block a user