Compare commits
4 Commits
mimo/code/
...
feat/spati
| Author | SHA1 | Date | |
|---|---|---|---|
| 72eecf6ee4 | |||
| 0bd3e1f470 | |||
| b73d846334 | |||
| 4c267de5bc |
116
app.js
116
app.js
@@ -3444,6 +3444,122 @@ init().then(() => {
|
|||||||
// Gravity well clustering — attract related crystals, bake positions (issue #1175)
|
// Gravity well clustering — attract related crystals, bake positions (issue #1175)
|
||||||
SpatialMemory.runGravityLayout();
|
SpatialMemory.runGravityLayout();
|
||||||
|
|
||||||
|
|
||||||
|
// ═══ SPATIAL SEARCH (Mnemosyne #1170) ═══
|
||||||
|
(() => {
|
||||||
|
const input = document.getElementById('spatial-search-input');
|
||||||
|
const resultsDiv = document.getElementById('spatial-search-results');
|
||||||
|
if (!input || !resultsDiv) return;
|
||||||
|
|
||||||
|
let searchTimeout = null;
|
||||||
|
let currentMatches = [];
|
||||||
|
|
||||||
|
function runSearch(query) {
|
||||||
|
if (!query.trim()) {
|
||||||
|
SpatialMemory.clearSearch();
|
||||||
|
resultsDiv.classList.remove('visible');
|
||||||
|
resultsDiv.innerHTML = '';
|
||||||
|
currentMatches = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = SpatialMemory.searchContent(query);
|
||||||
|
currentMatches = matches;
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
SpatialMemory.clearSearch();
|
||||||
|
resultsDiv.innerHTML = '<div class="spatial-search-count">No matches</div>';
|
||||||
|
resultsDiv.classList.add('visible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SpatialMemory.highlightSearchResults(matches);
|
||||||
|
|
||||||
|
// Build results list
|
||||||
|
const allMems = SpatialMemory.getAllMemories();
|
||||||
|
let html = `<div class="spatial-search-count">${matches.length} match${matches.length > 1 ? 'es' : ''}</div>`;
|
||||||
|
matches.forEach(id => {
|
||||||
|
const mem = allMems.find(m => m.id === id);
|
||||||
|
if (mem) {
|
||||||
|
const label = (mem.content || id).slice(0, 60);
|
||||||
|
const region = mem.category || '?';
|
||||||
|
html += `<div class="spatial-search-result-item" data-mem-id="${id}">
|
||||||
|
<span class="result-region">[${region}]</span>${label}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resultsDiv.innerHTML = html;
|
||||||
|
resultsDiv.classList.add('visible');
|
||||||
|
|
||||||
|
// Click handler for result items
|
||||||
|
resultsDiv.querySelectorAll('.spatial-search-result-item').forEach(el => {
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
const memId = el.getAttribute('data-mem-id');
|
||||||
|
flyToMemory(memId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fly camera to first match
|
||||||
|
if (matches.length > 0) {
|
||||||
|
flyToMemory(matches[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function flyToMemory(memId) {
|
||||||
|
const pos = SpatialMemory.getSearchMatchPosition(memId);
|
||||||
|
if (!pos) return;
|
||||||
|
|
||||||
|
// Smooth camera fly-to: place camera above and in front of crystal
|
||||||
|
const targetPos = new THREE.Vector3(pos.x, pos.y + 4, pos.z + 6);
|
||||||
|
|
||||||
|
// Use simple lerp animation over ~800ms
|
||||||
|
const startPos = playerPos.clone();
|
||||||
|
const startTime = performance.now();
|
||||||
|
const duration = 800;
|
||||||
|
|
||||||
|
function animateCamera(now) {
|
||||||
|
const elapsed = now - startTime;
|
||||||
|
const t = Math.min(1, elapsed / duration);
|
||||||
|
// Ease out cubic
|
||||||
|
const ease = 1 - Math.pow(1 - t, 3);
|
||||||
|
|
||||||
|
playerPos.lerpVectors(startPos, targetPos, ease);
|
||||||
|
camera.position.copy(playerPos);
|
||||||
|
|
||||||
|
// Look at crystal
|
||||||
|
const lookTarget = pos.clone();
|
||||||
|
lookTarget.y += 1.5;
|
||||||
|
camera.lookAt(lookTarget);
|
||||||
|
|
||||||
|
if (t < 1) {
|
||||||
|
requestAnimationFrame(animateCamera);
|
||||||
|
} else {
|
||||||
|
SpatialMemory.highlightMemory(memId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestAnimationFrame(animateCamera);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounced input handler
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
searchTimeout = setTimeout(() => runSearch(input.value), 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Escape clears search
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
input.value = '';
|
||||||
|
SpatialMemory.clearSearch();
|
||||||
|
resultsDiv.classList.remove('visible');
|
||||||
|
resultsDiv.innerHTML = '';
|
||||||
|
currentMatches = [];
|
||||||
|
input.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
// Project Mnemosyne — seed demo session rooms (#1171)
|
// Project Mnemosyne — seed demo session rooms (#1171)
|
||||||
// Sessions group facts by conversation/work session with a timestamp.
|
// Sessions group facts by conversation/work session with a timestamp.
|
||||||
const demoSessions = [
|
const demoSessions = [
|
||||||
|
|||||||
@@ -66,6 +66,14 @@ chdir: error retrieving current directory: getcwd: cannot access parent director
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Spatial Search Overlay (Mnemosyne #1170) -->
|
||||||
|
<div id="spatial-search" class="spatial-search-overlay">
|
||||||
|
<input type="text" id="spatial-search-input" class="spatial-search-input"
|
||||||
|
placeholder="🔍 Search memories..." autocomplete="off" spellcheck="false">
|
||||||
|
<div id="spatial-search-results" class="spatial-search-results"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- HUD Overlay -->
|
<!-- HUD Overlay -->
|
||||||
<div id="hud" class="game-ui" style="display:none;">
|
<div id="hud" class="game-ui" style="display:none;">
|
||||||
<!-- GOFAI HUD Panels -->
|
<!-- GOFAI HUD Panels -->
|
||||||
|
|||||||
@@ -734,13 +734,86 @@ const SpatialMemory = (() => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ─── SPATIAL SEARCH (issue #1170) ────────────────────
|
||||||
|
let _searchOriginalState = {}; // memId -> { emissiveIntensity, opacity } for restore
|
||||||
|
|
||||||
|
function searchContent(query) {
|
||||||
|
if (!query || !query.trim()) return [];
|
||||||
|
const q = query.toLowerCase().trim();
|
||||||
|
const matches = [];
|
||||||
|
|
||||||
|
Object.values(_memoryObjects).forEach(obj => {
|
||||||
|
const d = obj.data;
|
||||||
|
const searchable = [
|
||||||
|
d.content || '',
|
||||||
|
d.id || '',
|
||||||
|
d.category || '',
|
||||||
|
d.source || '',
|
||||||
|
...(d.connections || [])
|
||||||
|
].join(' ').toLowerCase();
|
||||||
|
|
||||||
|
if (searchable.includes(q)) {
|
||||||
|
matches.push(d.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightSearchResults(matchIds) {
|
||||||
|
// Save original state and apply search highlighting
|
||||||
|
_searchOriginalState = {};
|
||||||
|
const matchSet = new Set(matchIds);
|
||||||
|
|
||||||
|
Object.entries(_memoryObjects).forEach(([id, obj]) => {
|
||||||
|
const mat = obj.mesh.material;
|
||||||
|
_searchOriginalState[id] = {
|
||||||
|
emissiveIntensity: mat.emissiveIntensity,
|
||||||
|
opacity: mat.opacity
|
||||||
|
};
|
||||||
|
|
||||||
|
if (matchSet.has(id)) {
|
||||||
|
// Match: bright white glow
|
||||||
|
mat.emissive.setHex(0xffffff);
|
||||||
|
mat.emissiveIntensity = 5.0;
|
||||||
|
mat.opacity = 1.0;
|
||||||
|
} else {
|
||||||
|
// Non-match: dim to 10% opacity
|
||||||
|
mat.opacity = 0.1;
|
||||||
|
mat.emissiveIntensity = 0.2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSearch() {
|
||||||
|
Object.entries(_memoryObjects).forEach(([id, obj]) => {
|
||||||
|
const mat = obj.mesh.material;
|
||||||
|
const saved = _searchOriginalState[id];
|
||||||
|
if (saved) {
|
||||||
|
// Restore original emissive color from region
|
||||||
|
const region = REGIONS[obj.region] || REGIONS.working;
|
||||||
|
mat.emissive.copy(region.color);
|
||||||
|
mat.emissiveIntensity = saved.emissiveIntensity;
|
||||||
|
mat.opacity = saved.opacity;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_searchOriginalState = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSearchMatchPosition(matchId) {
|
||||||
|
const obj = _memoryObjects[matchId];
|
||||||
|
return obj ? obj.mesh.position.clone() : null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
init, placeMemory, removeMemory, update,
|
init, placeMemory, removeMemory, update,
|
||||||
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
|
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
|
||||||
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
|
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
|
||||||
exportIndex, importIndex, exportToFile, importFromFile, searchNearby, REGIONS,
|
exportIndex, importIndex, exportToFile, importFromFile, searchNearby, REGIONS,
|
||||||
saveToStorage, loadFromStorage, clearStorage,
|
saveToStorage, loadFromStorage, clearStorage,
|
||||||
runGravityLayout
|
runGravityLayout,
|
||||||
|
searchContent, highlightSearchResults, clearSearch, getSearchMatchPosition
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
81
style.css
81
style.css
@@ -1744,3 +1744,84 @@ canvas#nexus-canvas {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ═══ SPATIAL SEARCH OVERLAY (Mnemosyne #1170) ═══ */
|
||||||
|
.spatial-search-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spatial-search-input {
|
||||||
|
width: 260px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
background: rgba(0, 0, 0, 0.65);
|
||||||
|
border: 1px solid rgba(74, 240, 192, 0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #e0f0ff;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spatial-search-input:focus {
|
||||||
|
border-color: rgba(74, 240, 192, 0.7);
|
||||||
|
box-shadow: 0 0 12px rgba(74, 240, 192, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spatial-search-input::placeholder {
|
||||||
|
color: rgba(224, 240, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spatial-search-results {
|
||||||
|
margin-top: 4px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
border: 1px solid rgba(74, 240, 192, 0.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #a0c0d0;
|
||||||
|
width: 260px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spatial-search-results.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spatial-search-result-item {
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid rgba(74, 240, 192, 0.08);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spatial-search-result-item:hover {
|
||||||
|
background: rgba(74, 240, 192, 0.1);
|
||||||
|
color: #e0f0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spatial-search-result-item .result-region {
|
||||||
|
color: #4af0c0;
|
||||||
|
font-size: 9px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spatial-search-count {
|
||||||
|
padding: 4px 10px;
|
||||||
|
color: rgba(74, 240, 192, 0.6);
|
||||||
|
font-size: 10px;
|
||||||
|
border-bottom: 1px solid rgba(74, 240, 192, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user