Compare commits
2 Commits
mimo/creat
...
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 { MemoryBirth } from './nexus/components/memory-birth.js';
|
||||
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
|
||||
import { MemoryInspect } from './nexus/components/memory-inspect.js';
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// NEXUS v1.1 — Portal System Update
|
||||
@@ -712,6 +713,7 @@ async function init() {
|
||||
MemoryBirth.init(scene);
|
||||
MemoryBirth.wrapSpatialMemory(SpatialMemory);
|
||||
SpatialMemory.setCamera(camera);
|
||||
MemoryInspect.init(SpatialMemory);
|
||||
updateLoad(90);
|
||||
|
||||
loadSession();
|
||||
@@ -1904,7 +1906,7 @@ function setupControls() {
|
||||
orbitState.lastX = e.clientX;
|
||||
orbitState.lastY = e.clientY;
|
||||
|
||||
// Raycasting for portals
|
||||
// Raycasting for portals and memory crystals
|
||||
if (!portalOverlayActive) {
|
||||
const mouse = new THREE.Vector2(
|
||||
(e.clientX / window.innerWidth) * 2 - 1,
|
||||
@@ -1912,11 +1914,26 @@ function setupControls() {
|
||||
);
|
||||
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;
|
||||
|
||||
// Check portals first
|
||||
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);
|
||||
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