diff --git a/app.js b/app.js index 34a0bafb..19fe69ab 100644 --- a/app.js +++ b/app.js @@ -3520,6 +3520,122 @@ init().then(() => { // Gravity well clustering — attract related crystals, bake positions (issue #1175) 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 = '
No matches
'; + resultsDiv.classList.add('visible'); + return; + } + + SpatialMemory.highlightSearchResults(matches); + + // Build results list + const allMems = SpatialMemory.getAllMemories(); + let html = `
${matches.length} match${matches.length > 1 ? 'es' : ''}
`; + 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 += `
+ [${region}]${label} +
`; + } + }); + 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) // Sessions group facts by conversation/work session with a timestamp. const demoSessions = [ diff --git a/index.html b/index.html index abc7cebd..28c3261d 100644 --- a/index.html +++ b/index.html @@ -66,6 +66,14 @@ chdir: error retrieving current directory: getcwd: cannot access parent director + + + +