Compare commits
13 Commits
mimo/code/
...
mimo/creat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7890bd4886 | ||
| cc4af009c7 | |||
| 089b06b6f8 | |||
| 8beae5ecc1 | |||
| e2edfd3318 | |||
| 8e18fa5311 | |||
| 1bf2af15a0 | |||
| 4095946749 | |||
|
|
845e2f2ced | ||
| c387708892 | |||
| 8694c0f5ad | |||
| c3547196d8 | |||
| 87bfe9b332 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,3 +4,6 @@ nexus/__pycache__/
|
||||
tests/__pycache__/
|
||||
mempalace/__pycache__/
|
||||
.aider*
|
||||
|
||||
# Prevent agents from writing to wrong path (see issue #1145)
|
||||
public/nexus/
|
||||
|
||||
12
CLAUDE.md
12
CLAUDE.md
@@ -42,6 +42,17 @@ Current repo contents are centered on:
|
||||
Do not tell contributors to run Vite or edit a nonexistent root frontend on current `main`.
|
||||
If browser/UI work is being restored, it must happen through the migration backlog and land back here.
|
||||
|
||||
## Canonical File Paths
|
||||
|
||||
**Frontend code lives at repo ROOT, NOT in `public/nexus/`:**
|
||||
- `app.js` — main Three.js app (GOFAI, 3D world, all frontend logic)
|
||||
- `index.html` — main HTML shell
|
||||
- `style.css` — styles
|
||||
- `server.py` — websocket bridge
|
||||
- `gofai_worker.js` — web worker for off-thread reasoning
|
||||
|
||||
**DO NOT write to `public/nexus/`** — this path is gitignored. Agents historically wrote here by mistake, creating corrupt duplicates. See issue #1145 and `INVESTIGATION_ISSUE_1145.md`.
|
||||
|
||||
## Hard Rules
|
||||
|
||||
1. One canonical 3D repo only: `Timmy_Foundation/the-nexus`
|
||||
@@ -50,6 +61,7 @@ If browser/UI work is being restored, it must happen through the migration backl
|
||||
4. Telemetry and durable truth flow through Hermes harness
|
||||
5. OpenClaw remains a sidecar, not the governing authority
|
||||
6. Before claiming visual validation, prove the app being viewed actually comes from current `the-nexus`
|
||||
7. **NEVER write frontend files to `public/nexus/`** — use repo root paths listed above
|
||||
|
||||
## Validation Rule
|
||||
|
||||
|
||||
72
INVESTIGATION_ISSUE_1145.md
Normal file
72
INVESTIGATION_ISSUE_1145.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Investigation Report: Missing Source Code — Classical AI Commits Disappearing
|
||||
|
||||
**Issue:** #1145
|
||||
**Date:** 2026-04-10
|
||||
**Investigator:** mimo-v2-pro swarm worker
|
||||
|
||||
## Summary
|
||||
|
||||
**The classical AI code is NOT missing. It is fully present in root `app.js` (3302 lines).**
|
||||
|
||||
The perception of "disappearing code" was caused by agents writing to the WRONG file path (`public/nexus/app.js` instead of root `app.js`), creating corrupt duplicate files that were repeatedly overwritten and eventually deleted.
|
||||
|
||||
## Root Cause
|
||||
|
||||
**Explanation #1 confirmed: Duplicate agents on different machines overwriting each other's commits.**
|
||||
|
||||
Multiple Google AI Agent instances wrote GOFAI implementations to `public/nexus/app.js` — a path that does not correspond to the canonical app structure. These commits kept overwriting each other:
|
||||
|
||||
| Commit | Date | What happened |
|
||||
|--------|------|---------------|
|
||||
| `8943cf5` | 2026-03-30 | Symbolic reasoning engine written to `public/nexus/app.js` (+2280 lines) |
|
||||
| `e2df240` | 2026-03-30 | Phase 3 Neuro-Symbolic Bridge — overwrote to 284 lines of HTML (wrong path) |
|
||||
| `7f2f23f` | 2026-03-30 | Phase 4 Meta-Reasoning — same destructive overwrite |
|
||||
| `bf3b98b` | 2026-03-30 | A* Search — same destructive overwrite |
|
||||
| `e88bcb4` | 2026-03-30 | Bug fix identified `public/nexus/` files as corrupt duplicates, **deleted them** |
|
||||
|
||||
## Evidence: Code Is Present on Main
|
||||
|
||||
All 13 classical AI classes/functions verified present in root `app.js`:
|
||||
|
||||
| Class/Function | Line | Status |
|
||||
|----------------|------|--------|
|
||||
| `SymbolicEngine` | 82 | ✅ Present |
|
||||
| `AgentFSM` | 135 | ✅ Present |
|
||||
| `KnowledgeGraph` | 160 | ✅ Present |
|
||||
| `Blackboard` | 181 | ✅ Present |
|
||||
| `SymbolicPlanner` | 210 | ✅ Present |
|
||||
| `HTNPlanner` | 295 | ✅ Present |
|
||||
| `CaseBasedReasoner` | 343 | ✅ Present |
|
||||
| `NeuroSymbolicBridge` | 392 | ✅ Present |
|
||||
| `MetaReasoningLayer` | 422 | ✅ Present |
|
||||
| `AdaptiveCalibrator` | 460 | ✅ Present |
|
||||
| `PSELayer` | 566 | ✅ Present |
|
||||
| `setupGOFAI()` | 596 | ✅ Present |
|
||||
| `updateGOFAI()` | 622 | ✅ Present |
|
||||
| Bitmask fact indexing | 86 | ✅ Present |
|
||||
| A* search | 231 | ✅ Present |
|
||||
|
||||
These were injected by commit `af7a4c4` (PR #775, merged via `a855d54`) into the correct path.
|
||||
|
||||
## What Actually Happened
|
||||
|
||||
1. Google AI Agent wrote good GOFAI code to root `app.js` via the correct PR (#775)
|
||||
2. A second wave of Google AI Agent instances also wrote to `public/nexus/app.js` (wrong path)
|
||||
3. Those `public/nexus/` files kept getting overwritten by subsequent agent commits
|
||||
4. Commit `e88bcb4` correctly identified the `public/nexus/` files as corrupt and deleted them
|
||||
5. Alexander interpreted the git log as "classical AI code keeps disappearing"
|
||||
6. The code was never actually gone — it just lived in root `app.js` the whole time
|
||||
|
||||
## Prevention Strategy
|
||||
|
||||
1. **Add `public/nexus/` to `.gitignore`** — prevents agents from accidentally writing to the wrong path again
|
||||
2. **Add canonical path documentation to CLAUDE.md** — any agent reading this repo will know where frontend code lives
|
||||
3. **This report** — serves as the audit trail so this confusion doesn't recur
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Git history audited for classical AI commits
|
||||
- [x] Found the commits — they exist, code was written to wrong path
|
||||
- [x] Root cause identified — duplicate agents writing to `public/nexus/` (wrong path)
|
||||
- [x] Prevention strategy implemented — `.gitignore` + `CLAUDE.md` path guard
|
||||
- [x] Report filed with findings (this document)
|
||||
309
app.js
309
app.js
@@ -4,6 +4,7 @@ import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
||||
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
||||
import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
|
||||
import { SpatialMemory } from './nexus/components/spatial-memory.js';
|
||||
import { SessionRooms } from './nexus/components/session-rooms.js';
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// NEXUS v1.1 — Portal System Update
|
||||
@@ -705,6 +706,7 @@ async function init() {
|
||||
createWorkshopTerminal();
|
||||
createAshStorm();
|
||||
SpatialMemory.init(scene);
|
||||
SessionRooms.init(scene, camera, null);
|
||||
updateLoad(90);
|
||||
|
||||
loadSession();
|
||||
@@ -1883,7 +1885,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,
|
||||
@@ -1891,12 +1893,43 @@ 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;
|
||||
|
||||
// Priority 1: Portals
|
||||
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);
|
||||
if (portal) { activatePortal(portal); return; }
|
||||
}
|
||||
|
||||
// Priority 2: Memory crystals (Mnemosyne)
|
||||
const crystalMeshes = SpatialMemory.getCrystalMeshes();
|
||||
if (crystalMeshes.length > 0) {
|
||||
const crystalHits = raycaster.intersectObjects(crystalMeshes, false);
|
||||
if (crystalHits.length > 0) {
|
||||
const hitMesh = crystalHits[0].object;
|
||||
const memInfo = SpatialMemory.getMemoryFromMesh(hitMesh);
|
||||
if (memInfo) {
|
||||
SpatialMemory.highlightMemory(memInfo.data.id);
|
||||
showMemoryPanel(memInfo, e.clientX, e.clientY);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: Session rooms (Mnemosyne #1171)
|
||||
const roomMeshes = SessionRooms.getClickableMeshes();
|
||||
if (roomMeshes.length > 0) {
|
||||
const roomHits = raycaster.intersectObjects(roomMeshes, false);
|
||||
if (roomHits.length > 0) {
|
||||
const session = SessionRooms.handleRoomClick(roomHits[0].object);
|
||||
if (session) { _showSessionRoomPanel(session); return; }
|
||||
}
|
||||
}
|
||||
|
||||
// Clicked empty space — dismiss panel
|
||||
dismissMemoryPanel();
|
||||
_dismissSessionRoomPanel();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2551,6 +2584,226 @@ function focusPortal(portal) {
|
||||
let lastThoughtTime = 0;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Position the panel near the screen click coordinates, keeping it on-screen.
|
||||
*/
|
||||
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 = '#' + 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 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 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') return;
|
||||
panel.classList.add('memory-panel-fade-out');
|
||||
setTimeout(() => {
|
||||
panel.style.display = 'none';
|
||||
panel.classList.remove('memory-panel-fade-out');
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the session room HUD panel when a chamber is entered.
|
||||
* @param {object} session — { id, timestamp, facts[] }
|
||||
*/
|
||||
function _showSessionRoomPanel(session) {
|
||||
const panel = document.getElementById('session-room-panel');
|
||||
if (!panel) return;
|
||||
|
||||
const dt = session.timestamp ? new Date(session.timestamp) : new Date();
|
||||
const tsEl = document.getElementById('session-room-timestamp');
|
||||
if (tsEl) tsEl.textContent = isNaN(dt.getTime()) ? session.id : dt.toLocaleString();
|
||||
|
||||
const countEl = document.getElementById('session-room-fact-count');
|
||||
const facts = session.facts || [];
|
||||
if (countEl) countEl.textContent = facts.length + (facts.length === 1 ? ' fact' : ' facts') + ' in this chamber';
|
||||
|
||||
const listEl = document.getElementById('session-room-facts');
|
||||
if (listEl) {
|
||||
listEl.innerHTML = '';
|
||||
facts.slice(0, 8).forEach(f => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'session-room-fact-item';
|
||||
item.textContent = f.content || f.id || '(unknown)';
|
||||
item.title = f.content || '';
|
||||
listEl.appendChild(item);
|
||||
});
|
||||
if (facts.length > 8) {
|
||||
const more = document.createElement('div');
|
||||
more.className = 'session-room-fact-item';
|
||||
more.style.color = 'rgba(200,180,255,0.4)';
|
||||
more.textContent = '\u2026 ' + (facts.length - 8) + ' more';
|
||||
listEl.appendChild(more);
|
||||
}
|
||||
}
|
||||
|
||||
// Close button
|
||||
const closeBtn = document.getElementById('session-room-close');
|
||||
if (closeBtn) closeBtn.onclick = () => _dismissSessionRoomPanel();
|
||||
|
||||
panel.classList.remove('session-panel-fade-out');
|
||||
panel.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the session room panel.
|
||||
*/
|
||||
function _dismissSessionRoomPanel() {
|
||||
const panel = document.getElementById('session-room-panel');
|
||||
if (!panel || panel.style.display === 'none') return;
|
||||
panel.classList.add('session-panel-fade-out');
|
||||
setTimeout(() => {
|
||||
panel.style.display = 'none';
|
||||
panel.classList.remove('session-panel-fade-out');
|
||||
}, 200);
|
||||
}
|
||||
|
||||
|
||||
function gameLoop() {
|
||||
requestAnimationFrame(gameLoop);
|
||||
const delta = Math.min(clock.getDelta(), 0.1);
|
||||
@@ -2581,6 +2834,9 @@ function gameLoop() {
|
||||
animateMemoryOrbs(delta);
|
||||
}
|
||||
|
||||
// Project Mnemosyne - Session Rooms (#1171)
|
||||
SessionRooms.update(delta);
|
||||
|
||||
|
||||
const mode = NAV_MODES[navModeIdx];
|
||||
const chatActive = document.activeElement === document.getElementById('chat-input');
|
||||
@@ -3104,9 +3360,52 @@ init().then(() => {
|
||||
{ id: 'mem_hermes_chat', content: 'First conversation through the Hermes gateway', category: 'social', strength: 0.7, connections: [] },
|
||||
{ id: 'mem_mnemosyne_start', content: 'Project Mnemosyne began — the living archive awakens', category: 'projects', strength: 0.9, connections: ['mem_nexus_birth', 'mem_spatial_schema'] },
|
||||
{ id: 'mem_spatial_schema', content: 'Spatial Memory Schema defined — memories gain permanent homes', category: 'engineering', strength: 0.8, connections: ['mem_mnemosyne_start'] },
|
||||
// MemPalace category zone demos — issue #1168
|
||||
{ id: 'mem_pref_dark_mode', content: 'User prefers dark mode and monospace fonts', category: 'user_pref', strength: 0.9, connections: [] },
|
||||
{ id: 'mem_pref_verbose_logs', content: 'User prefers verbose logging during debug sessions', category: 'user_pref', strength: 0.7, connections: [] },
|
||||
{ id: 'mem_proj_nexus_goal', content: 'The Nexus goal: local-first 3D training ground for Timmy', category: 'project', strength: 0.95, connections: ['mem_proj_mnemosyne'] },
|
||||
{ id: 'mem_proj_mnemosyne', content: 'Project Mnemosyne: holographic living archive of facts', category: 'project', strength: 0.85, connections: ['mem_proj_nexus_goal'] },
|
||||
{ id: 'mem_tool_three_js', content: 'Three.js — 3D rendering library used for the Nexus world', category: 'tool', strength: 0.8, connections: [] },
|
||||
{ id: 'mem_tool_gitea', content: 'Gitea API at forge.alexanderwhitestone.com for issue tracking', category: 'tool', strength: 0.75, connections: [] },
|
||||
{ id: 'mem_gen_websocket', content: 'WebSocket bridge (server.py) connects Timmy cognition to the browser', category: 'general', strength: 0.7, connections: [] },
|
||||
{ id: 'mem_gen_hermes', content: 'Hermes harness: telemetry and durable truth pipeline', category: 'general', strength: 0.65, connections: [] },
|
||||
];
|
||||
demoMemories.forEach(m => SpatialMemory.placeMemory(m));
|
||||
|
||||
// Gravity well clustering — attract related crystals, bake positions (issue #1175)
|
||||
SpatialMemory.runGravityLayout();
|
||||
|
||||
// Project Mnemosyne — seed demo session rooms (#1171)
|
||||
// Sessions group facts by conversation/work session with a timestamp.
|
||||
const demoSessions = [
|
||||
{
|
||||
id: 'session_2026_03_01',
|
||||
timestamp: '2026-03-01T10:00:00.000Z',
|
||||
facts: [
|
||||
{ id: 'mem_nexus_birth', content: 'The Nexus came online — first render of the 3D world', category: 'knowledge', strength: 0.95 },
|
||||
{ id: 'mem_mnemosyne_start', content: 'Project Mnemosyne began — the living archive awakens', category: 'projects', strength: 0.9 },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'session_2026_03_15',
|
||||
timestamp: '2026-03-15T14:30:00.000Z',
|
||||
facts: [
|
||||
{ id: 'mem_first_portal', content: 'First portal deployed — connection to external service', category: 'engineering', strength: 0.85 },
|
||||
{ id: 'mem_hermes_chat', content: 'First conversation through the Hermes gateway', category: 'social', strength: 0.7 },
|
||||
{ id: 'mem_spatial_schema', content: 'Spatial Memory Schema defined — memories gain homes', category: 'engineering', strength: 0.8 },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'session_2026_04_10',
|
||||
timestamp: '2026-04-10T09:00:00.000Z',
|
||||
facts: [
|
||||
{ id: 'mem_session_rooms', content: 'Session rooms introduced — holographic chambers per session', category: 'projects', strength: 0.88 },
|
||||
{ id: 'mem_gravity_wells', content: 'Gravity-well clustering bakes crystal positions on load', category: 'engineering', strength: 0.75 },
|
||||
]
|
||||
}
|
||||
];
|
||||
SessionRooms.updateSessions(demoSessions);
|
||||
|
||||
fetchGiteaData();
|
||||
setInterval(fetchGiteaData, 30000);
|
||||
runWeeklyAudit();
|
||||
|
||||
91
docs/media/README.md
Normal file
91
docs/media/README.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Media Production — Veo/Flow Prototypes
|
||||
|
||||
Issue #681: [MEDIA] Veo/Flow flythrough prototypes for The Nexus and Timmy.
|
||||
|
||||
## Contents
|
||||
|
||||
- `veo-storyboard.md` — Full storyboard for 5 clips with shot sequences, prompts, and design focus areas
|
||||
- `clip-metadata.json` — Durable metadata for each clip (prompts, model, outputs, insights)
|
||||
|
||||
## Clips Overview
|
||||
|
||||
| ID | Title | Audience | Purpose |
|
||||
|----|-------|----------|---------|
|
||||
| clip-001 | First Light | PUBLIC | The Nexus reveal teaser |
|
||||
| clip-002 | Between Worlds | INTERNAL | Portal activation UX study |
|
||||
| clip-003 | The Guardian's View | PUBLIC | Timmy's presence promo |
|
||||
| clip-004 | The Void Between | INTERNAL | Ambient environment study |
|
||||
| clip-005 | Command Center | INTERNAL | Terminal UI readability |
|
||||
|
||||
## How to Generate
|
||||
|
||||
### Via Flow (labs.google/flow)
|
||||
1. Open `veo-storyboard.md`, copy the prompt for your clip
|
||||
2. Go to labs.google/flow
|
||||
3. Paste the prompt, select Veo 3.1
|
||||
4. Generate (8-second clips)
|
||||
5. Download output, update `clip-metadata.json` with output path and findings
|
||||
|
||||
### Via Gemini App
|
||||
1. Type "generate a video of [prompt text]" in Gemini
|
||||
2. Uses Veo 3.1 Fast (slightly lower quality, faster)
|
||||
3. Good for quick iteration on prompts
|
||||
|
||||
### Via API (programmatic)
|
||||
```python
|
||||
from google import genai
|
||||
client = genai.Client()
|
||||
|
||||
# See: ai.google.dev/gemini-api/docs/video
|
||||
response = client.models.generate_content(
|
||||
model="veo-3.1",
|
||||
contents="[prompt from storyboard]"
|
||||
)
|
||||
```
|
||||
|
||||
## After Generation
|
||||
|
||||
For each clip:
|
||||
1. Save output file to `outputs/clip-XXX.mp4`
|
||||
2. Update `clip-metadata.json`:
|
||||
- Add output file path to `output_files[]`
|
||||
- Fill in `design_insights.findings` with observations
|
||||
- Add `threejs_changes_suggested` if the clip reveals needed changes
|
||||
3. Share internal clips with the team for design review
|
||||
4. Use public clips in README, social media, project communication
|
||||
|
||||
## Design Insight Workflow
|
||||
|
||||
Each clip has specific questions it's designed to answer:
|
||||
|
||||
**clip-001 (First Light)**
|
||||
- Scale perception: platform vs. portals vs. terminal
|
||||
- Color hierarchy: teal primary, purple secondary, gold accent
|
||||
- Camera movement: cinematic or disorienting?
|
||||
|
||||
**clip-002 (Between Worlds)**
|
||||
- Activation distance: when does interaction become available?
|
||||
- Transition feel: travel or teleportation?
|
||||
- Overlay readability against portal glow
|
||||
|
||||
**clip-003 (The Guardian's View)**
|
||||
- Agent presence: alive or decorative?
|
||||
- Crystal hologram readability
|
||||
- Wide shot: world or tech demo?
|
||||
|
||||
**clip-004 (The Void Between)**
|
||||
- Void atmosphere: alive or empty?
|
||||
- Particle systems: enhance or distract?
|
||||
- Lighting hierarchy clarity
|
||||
|
||||
**clip-005 (Command Center)**
|
||||
- Text readability at 1080p
|
||||
- Color-coded panel hierarchy
|
||||
- Scan-line effect: retro or futuristic?
|
||||
|
||||
## Constraints
|
||||
|
||||
- 8-second clips max (Veo/Flow limitation)
|
||||
- Queued generation (not instant)
|
||||
- Content policies apply
|
||||
- Ultra tier gets highest rate limits
|
||||
239
docs/media/clip-metadata.json
Normal file
239
docs/media/clip-metadata.json
Normal file
@@ -0,0 +1,239 @@
|
||||
{
|
||||
"clips": [
|
||||
{
|
||||
"id": "clip-001",
|
||||
"title": "First Light — The Nexus Reveal",
|
||||
"purpose": "Public-facing teaser. Establishes the Nexus as a place worth visiting.",
|
||||
"audience": "public",
|
||||
"priority": "HIGH",
|
||||
"duration_seconds": 8,
|
||||
"shots": [
|
||||
{
|
||||
"shot": 1,
|
||||
"timeframe": "0-2s",
|
||||
"description": "Void Approach — camera drifts through nebula, hexagonal glow appears",
|
||||
"design_focus": "isolation before connection"
|
||||
},
|
||||
{
|
||||
"shot": 2,
|
||||
"timeframe": "2-4s",
|
||||
"description": "Platform Reveal — camera descends to hexagonal platform, grid pulses",
|
||||
"design_focus": "structure emerges from chaos"
|
||||
},
|
||||
{
|
||||
"shot": 3,
|
||||
"timeframe": "4-6s",
|
||||
"description": "Portal Array — sweep low showing multiple colored portals",
|
||||
"design_focus": "infinite worlds, one home"
|
||||
},
|
||||
{
|
||||
"shot": 4,
|
||||
"timeframe": "6-8s",
|
||||
"description": "Timmy's Terminal — rise to batcave terminal, holographic panels",
|
||||
"design_focus": "someone is home"
|
||||
}
|
||||
],
|
||||
"prompt": "Cinematic flythrough of a futuristic digital nexus hub. Start in deep space with a dark purple nebula, stars twinkling. Camera descends toward a glowing hexagonal platform with pulsing teal grid lines and a luminous ring border. Sweep low across the platform revealing multiple glowing portal archways in orange, teal, gold, and blue — each with flickering holographic labels. Rise toward a central command terminal with holographic data panels showing scrolling status text. Camera pushes into a teal light flare. Cyberpunk aesthetic, volumetric lighting, 8-second sequence, smooth camera movement, concept art quality.",
|
||||
"prompt_variants": [],
|
||||
"model_tool": "veo-3.1",
|
||||
"access_point": "flow",
|
||||
"output_files": [],
|
||||
"design_insights": {
|
||||
"questions": [
|
||||
"Does the scale feel right? (platform vs. portals vs. terminal)",
|
||||
"Does the color hierarchy work? (teal primary, purple secondary, gold accent)",
|
||||
"Is the camera movement cinematic or disorienting?"
|
||||
],
|
||||
"findings": null,
|
||||
"threejs_changes_suggested": []
|
||||
},
|
||||
"status": "pending",
|
||||
"created_at": "2026-04-10T20:15:00Z"
|
||||
},
|
||||
{
|
||||
"id": "clip-002",
|
||||
"title": "Between Worlds — Portal Activation",
|
||||
"purpose": "Internal design reference. Tests portal activation sequence and spatial relationships.",
|
||||
"audience": "internal",
|
||||
"priority": "HIGH",
|
||||
"duration_seconds": 8,
|
||||
"shots": [
|
||||
{
|
||||
"shot": 1,
|
||||
"timeframe": "0-2.5s",
|
||||
"description": "Approach — first-person walk toward Morrowind portal (orange, x:15, z:-10)",
|
||||
"design_focus": "proximity feel, portal scale relative to player"
|
||||
},
|
||||
{
|
||||
"shot": 2,
|
||||
"timeframe": "2.5-5.5s",
|
||||
"description": "Activation — portal brightens, energy vortex, particles accelerate, overlay text",
|
||||
"design_focus": "activation UX, visual feedback timing"
|
||||
},
|
||||
{
|
||||
"shot": 3,
|
||||
"timeframe": "5.5-8s",
|
||||
"description": "Stepping Through — camera pushes in, world dissolves, flash, 'VVARDENFELL' text",
|
||||
"design_focus": "transition smoothness, immersion break points"
|
||||
}
|
||||
],
|
||||
"prompt": "First-person perspective walking toward a glowing orange portal archway in a futuristic digital space. The portal ring has inner energy glow with rising particle effects. A holographic label \"MORROWIND\" flickers above. Camera stops, portal interior brightens into an energy vortex, particles accelerate inward. Camera pushes forward into the portal, world dissolves into an orange energy tunnel, flash to black with text \"VVARDENFELL\". Dark ambient environment with teal grid floor. Cyberpunk aesthetic, volumetric effects, smooth camera movement.",
|
||||
"prompt_variants": [],
|
||||
"model_tool": "veo-3.1",
|
||||
"access_point": "flow",
|
||||
"output_files": [],
|
||||
"design_insights": {
|
||||
"questions": [
|
||||
"Is the activation distance clear? (when does interaction become available?)",
|
||||
"Does the transition feel like travel or teleportation?",
|
||||
"Is the overlay text readable against the portal glow?"
|
||||
],
|
||||
"findings": null,
|
||||
"threejs_changes_suggested": []
|
||||
},
|
||||
"status": "pending",
|
||||
"created_at": "2026-04-10T20:15:00Z"
|
||||
},
|
||||
{
|
||||
"id": "clip-003",
|
||||
"title": "The Guardian's View — Timmy's Perspective",
|
||||
"purpose": "Public-facing. Establishes Timmy as the guardian/presence of the Nexus.",
|
||||
"audience": "public",
|
||||
"priority": "MEDIUM",
|
||||
"duration_seconds": 8,
|
||||
"shots": [
|
||||
{
|
||||
"shot": 1,
|
||||
"timeframe": "0-2s",
|
||||
"description": "Agent Presence — floating glowing orb with trailing particles",
|
||||
"design_focus": "consciousness without body"
|
||||
},
|
||||
{
|
||||
"shot": 2,
|
||||
"timeframe": "2-4s",
|
||||
"description": "Vision Crystal — rotating octahedron with holographic 'SOVEREIGNTY' text",
|
||||
"design_focus": "values inscribed in space"
|
||||
},
|
||||
{
|
||||
"shot": 3,
|
||||
"timeframe": "4-6s",
|
||||
"description": "Harness Pulse — thought stream ribbon, agent orbs drifting",
|
||||
"design_focus": "the system breathes"
|
||||
},
|
||||
{
|
||||
"shot": 4,
|
||||
"timeframe": "6-8s",
|
||||
"description": "Wide View — full Nexus visible, text overlay 'THE NEXUS — Timmy's Sovereign Home'",
|
||||
"design_focus": "this is a world, not a page"
|
||||
}
|
||||
],
|
||||
"prompt": "Cinematic sequence in a futuristic digital nexus. Start with eye-level view of a floating glowing orb (teal-gold light, trailing particles) pulsing gently — an AI agent presence. Shift to a rotating octahedron crystal refracting light, with holographic text \"SOVEREIGNTY — No masters, no chains\" and a ring of light pulsing beneath. Pull back to reveal flowing ribbons of light (thought streams) crossing a hexagonal platform, with agent orbs drifting. Rise to high orbit showing the full nexus: hexagonal platform, multiple colored portal archways, central command terminal, floating crystals, all framed by a dark purple nebula skybox. End with text overlay \"THE NEXUS — Timmy's Sovereign Home\". Cyberpunk aesthetic, volumetric lighting, contemplative pacing.",
|
||||
"prompt_variants": [],
|
||||
"model_tool": "veo-3.1",
|
||||
"access_point": "flow",
|
||||
"output_files": [],
|
||||
"design_insights": {
|
||||
"questions": [
|
||||
"Do agent presences read as 'alive' or decorative?",
|
||||
"Is the crystal-to-text hologram readable?",
|
||||
"Does the wide shot communicate 'world' or 'tech demo'?"
|
||||
],
|
||||
"findings": null,
|
||||
"threejs_changes_suggested": []
|
||||
},
|
||||
"status": "pending",
|
||||
"created_at": "2026-04-10T20:15:00Z"
|
||||
},
|
||||
{
|
||||
"id": "clip-004",
|
||||
"title": "The Void Between — Ambient Environment Study",
|
||||
"purpose": "Internal design reference. Tests ambient environment systems: particles, dust, lighting, skybox.",
|
||||
"audience": "internal",
|
||||
"priority": "MEDIUM",
|
||||
"duration_seconds": 8,
|
||||
"shots": [
|
||||
{
|
||||
"shot": 1,
|
||||
"timeframe": "0-4s",
|
||||
"description": "Particle Systems — static camera, view from platform edge into void, particles visible",
|
||||
"design_focus": "does the void feel alive or empty?"
|
||||
},
|
||||
{
|
||||
"shot": 2,
|
||||
"timeframe": "4-8s",
|
||||
"description": "Lighting Study — slow orbit showing teal/purple point lights on grid floor",
|
||||
"design_focus": "lighting hierarchy, mood consistency"
|
||||
}
|
||||
],
|
||||
"prompt": "Ambient environment study in a futuristic digital void. Static camera with slight drift, viewing from the edge of a hexagonal platform into deep space. Dark purple nebula with twinkling distant stars, subtle color shifts. Floating particles and dust drift slowly. No structures, no portals — pure atmosphere. Then camera slowly orbits showing teal and purple point lights casting volumetric glow on a dark hexagonal grid floor. Ambient lighting fills shadows. Contemplative, moody, atmospheric. Cyberpunk aesthetic, minimal movement, focus on light and particle behavior.",
|
||||
"prompt_variants": [],
|
||||
"model_tool": "veo-3.1",
|
||||
"access_point": "flow",
|
||||
"output_files": [],
|
||||
"design_insights": {
|
||||
"questions": [
|
||||
"Is the void atmospheric or just dark?",
|
||||
"Do the particle systems enhance or distract?",
|
||||
"Is the lighting hierarchy (teal primary, purple secondary) clear?"
|
||||
],
|
||||
"findings": null,
|
||||
"threejs_changes_suggested": []
|
||||
},
|
||||
"status": "pending",
|
||||
"created_at": "2026-04-10T20:15:00Z"
|
||||
},
|
||||
{
|
||||
"id": "clip-005",
|
||||
"title": "Command Center — Batcave Terminal Focus",
|
||||
"purpose": "Internal design reference. Tests readability and hierarchy of holographic terminal panels.",
|
||||
"audience": "internal",
|
||||
"priority": "LOW",
|
||||
"duration_seconds": 8,
|
||||
"shots": [
|
||||
{
|
||||
"shot": 1,
|
||||
"timeframe": "0-2.5s",
|
||||
"description": "Terminal Overview — 5 holographic panels in arc with distinct colors",
|
||||
"design_focus": "panel arrangement, color distinction"
|
||||
},
|
||||
{
|
||||
"shot": 2,
|
||||
"timeframe": "2.5-5.5s",
|
||||
"description": "Panel Detail — zoom into METRICS panel, scrolling text, scan lines",
|
||||
"design_focus": "text readability, information density"
|
||||
},
|
||||
{
|
||||
"shot": 3,
|
||||
"timeframe": "5.5-8s",
|
||||
"description": "Agent Status — shift to panel, pulsing green dots, pull back",
|
||||
"design_focus": "status indication clarity"
|
||||
}
|
||||
],
|
||||
"prompt": "Approach a futuristic holographic command terminal in a dark digital space. Five curved holographic panels float in an arc: \"NEXUS COMMAND\" (teal), \"DEV QUEUE\" (gold), \"METRICS\" (purple), \"SOVEREIGNTY\" (gold), \"AGENT STATUS\" (teal). Camera zooms into the METRICS panel showing scrolling data: \"CPU: 12%\", \"MEM: 4.2GB\", \"COMMITS: 842\" with scan lines and glow effects. Shift to AGENT STATUS panel showing \"TIMMY: ● RUNNING\", \"KIMI: ○ STANDBY\", \"CLAUDE: ● ACTIVE\" with pulsing green dots. Pull back to show full terminal context. Dark ambient environment, cyberpunk aesthetic, holographic UI focus.",
|
||||
"prompt_variants": [],
|
||||
"model_tool": "veo-3.1",
|
||||
"access_point": "flow",
|
||||
"output_files": [],
|
||||
"design_insights": {
|
||||
"questions": [
|
||||
"Can you read the text at 1080p?",
|
||||
"Do the color-coded panels communicate hierarchy?",
|
||||
"Is the scan-line effect too retro or appropriately futuristic?"
|
||||
],
|
||||
"findings": null,
|
||||
"threejs_changes_suggested": []
|
||||
},
|
||||
"status": "pending",
|
||||
"created_at": "2026-04-10T20:15:00Z"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"project": "Timmy_Foundation/the-nexus",
|
||||
"issue": 681,
|
||||
"source_plan": "~/google-ai-ultra-plan.md",
|
||||
"tools_available": ["veo-3.1", "flow", "nano-banana-pro"],
|
||||
"max_clip_duration": 8,
|
||||
"created_by": "mimo-v2-pro swarm",
|
||||
"created_at": "2026-04-10T20:15:00Z"
|
||||
}
|
||||
}
|
||||
0
docs/media/outputs/.gitkeep
Normal file
0
docs/media/outputs/.gitkeep
Normal file
237
docs/media/veo-storyboard.md
Normal file
237
docs/media/veo-storyboard.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Veo/Flow Flythrough Prototypes — Storyboard
|
||||
## The Nexus & Timmy (Issue #681)
|
||||
|
||||
Source: `google-ai-ultra-plan.md` Veo/Flow section.
|
||||
|
||||
Purpose: Turn the current Nexus vision into short promo/concept clips for design leverage and communication.
|
||||
|
||||
---
|
||||
|
||||
## Clip 1: "First Light" — The Nexus Reveal (PUBLIC PROMO)
|
||||
|
||||
**Duration:** 8 seconds
|
||||
**Purpose:** Public-facing teaser. Establishes the Nexus as a place worth visiting.
|
||||
**Tone:** Awe. Discovery. "What is this?"
|
||||
|
||||
### Shot Sequence (4 shots, ~2s each)
|
||||
|
||||
1. **0–2s | Void Approach**
|
||||
- Camera drifts through deep space nebula (dark purples, teals)
|
||||
- Distant stars twinkle
|
||||
- A faint hexagonal glow appears below
|
||||
- *Narrative hook: isolation before connection*
|
||||
|
||||
2. **2–4s | Platform Reveal**
|
||||
- Camera descends toward the hexagonal platform
|
||||
- Grid lines pulse with teal energy
|
||||
- The ring border glows at the edge
|
||||
- *Narrative hook: structure emerges from chaos*
|
||||
|
||||
3. **4–6s | Portal Array**
|
||||
- Camera sweeps low across the platform
|
||||
- 3–4 portals visible: Morrowind (orange), Workshop (teal), Chapel (gold), Archive (blue)
|
||||
- Each portal ring hums with colored light, holographic labels flicker
|
||||
- *Narrative hook: infinite worlds, one home*
|
||||
|
||||
4. **6–8s | Timmy's Terminal**
|
||||
- Camera rises to the batcave terminal
|
||||
- Holographic panels glow: NEXUS COMMAND, METRICS, AGENT STATUS
|
||||
- Text scrolls: "> STATUS: NOMINAL"
|
||||
- Final frame: teal light floods the lens
|
||||
- *Narrative hook: someone is home*
|
||||
|
||||
### Veo Prompt (text-to-video)
|
||||
```
|
||||
Cinematic flythrough of a futuristic digital nexus hub. Start in deep space with a dark purple nebula, stars twinkling. Camera descends toward a glowing hexagonal platform with pulsing teal grid lines and a luminous ring border. Sweep low across the platform revealing multiple glowing portal archways in orange, teal, gold, and blue — each with flickering holographic labels. Rise toward a central command terminal with holographic data panels showing scrolling status text. Camera pushes into a teal light flare. Cyberpunk aesthetic, volumetric lighting, 8-second sequence, smooth camera movement, concept art quality.
|
||||
```
|
||||
|
||||
### Design Insight Target
|
||||
- Does the scale feel right? (platform vs. portals vs. terminal)
|
||||
- Does the color hierarchy work? (teal primary, purple secondary, gold accent)
|
||||
- Is the camera movement cinematic or disorienting?
|
||||
|
||||
---
|
||||
|
||||
## Clip 2: "Between Worlds" — Portal Activation (INTERNAL DESIGN)
|
||||
|
||||
**Duration:** 8 seconds
|
||||
**Purpose:** Internal design reference. Tests the portal activation sequence and spatial relationships.
|
||||
**Tone:** Energy. Connection. "What happens when you step through?"
|
||||
|
||||
### Shot Sequence (3 shots, ~2.5s each)
|
||||
|
||||
1. **0–2.5s | Approach**
|
||||
- First-person perspective walking toward the Morrowind portal (orange, position x:15, z:-10)
|
||||
- Portal ring visible: inner glow, particle effects rising
|
||||
- Holographic label "MORROWIND" flickers above
|
||||
- *Design focus: proximity feel, portal scale relative to player*
|
||||
|
||||
2. **2.5–5.5s | Activation**
|
||||
- Player stops at activation distance
|
||||
- Portal interior brightens — energy vortex forms
|
||||
- Camera tilts up to show the full portal height
|
||||
- Particles accelerate into the portal center
|
||||
- Overlay text appears: "ENTER MORROWIND?"
|
||||
- *Design focus: activation UX, visual feedback timing*
|
||||
|
||||
3. **5.5–8s | Stepping Through**
|
||||
- Camera pushes forward into the portal
|
||||
- World dissolves into orange energy tunnel
|
||||
- Brief flash — then fade to black with "VVARDENFELL" text
|
||||
- *Design focus: transition smoothness, immersion break points*
|
||||
|
||||
### Veo Prompt (text-to-video)
|
||||
```
|
||||
First-person perspective walking toward a glowing orange portal archway in a futuristic digital space. The portal ring has inner energy glow with rising particle effects. A holographic label "MORROWIND" flickers above. Camera stops, portal interior brightens into an energy vortex, particles accelerate inward. Camera pushes forward into the portal, world dissolves into an orange energy tunnel, flash to black with text "VVARDENFELL". Dark ambient environment with teal grid floor. Cyberpunk aesthetic, volumetric effects, smooth camera movement.
|
||||
```
|
||||
|
||||
### Design Insight Target
|
||||
- Is the activation distance clear? (when does interaction become available?)
|
||||
- Does the transition feel like travel or teleportation?
|
||||
- Is the overlay text readable against the portal glow?
|
||||
|
||||
---
|
||||
|
||||
## Clip 3: "The Guardian's View" — Timmy's Perspective (PUBLIC PROMO)
|
||||
|
||||
**Duration:** 8 seconds
|
||||
**Purpose:** Public-facing. Establishes Timmy as the guardian/presence of the Nexus.
|
||||
**Tone:** Contemplative. Sovereign. "Who lives here?"
|
||||
|
||||
### Shot Sequence (4 shots, ~2s each)
|
||||
|
||||
1. **0–2s | Agent Presence**
|
||||
- Camera at eye-level, looking at a floating agent presence (glowing orb with trailing particles)
|
||||
- The orb pulses gently, teal-gold light
|
||||
- Background: the Nexus platform, slightly out of focus
|
||||
- *Narrative hook: consciousness without body*
|
||||
|
||||
2. **2–4s | Vision Crystal**
|
||||
- Camera shifts to a floating octahedron crystal (Sovereignty vision point)
|
||||
- Crystal rotates slowly, refracting light
|
||||
- Text hologram appears: "SOVEREIGNTY — No masters, no chains"
|
||||
- Ring of light pulses beneath
|
||||
- *Narrative hook: values inscribed in space*
|
||||
|
||||
3. **4–6s | The Harness Pulse**
|
||||
- Camera pulls back to show the thought stream — a flowing ribbon of light across the platform
|
||||
- Harness pulse mesh glows at the center
|
||||
- Agent orbs drift along the stream
|
||||
- *Narrative hook: the system breathes*
|
||||
|
||||
4. **6–8s | Wide View**
|
||||
- Camera rises to high orbit view
|
||||
- Entire Nexus visible: platform, portals, terminal, crystals, agents
|
||||
- Nebula skybox frames everything
|
||||
- Final frame: "THE NEXUS — Timmy's Sovereign Home" text overlay
|
||||
- *Narrative hook: this is a world, not a page*
|
||||
|
||||
### Veo Prompt (text-to-video)
|
||||
```
|
||||
Cinematic sequence in a futuristic digital nexus. Start with eye-level view of a floating glowing orb (teal-gold light, trailing particles) pulsing gently — an AI agent presence. Shift to a rotating octahedron crystal refracting light, with holographic text "SOVEREIGNTY — No masters, no chains" and a ring of light pulsing beneath. Pull back to reveal flowing ribbons of light (thought streams) crossing a hexagonal platform, with agent orbs drifting. Rise to high orbit showing the full nexus: hexagonal platform, multiple colored portal archways, central command terminal, floating crystals, all framed by a dark purple nebula skybox. End with text overlay "THE NEXUS — Timmy's Sovereign Home". Cyberpunk aesthetic, volumetric lighting, contemplative pacing.
|
||||
```
|
||||
|
||||
### Design Insight Target
|
||||
- Do agent presences read as "alive" or decorative?
|
||||
- Is the crystal-to-text hologram readable?
|
||||
- Does the wide shot communicate "world" or "tech demo"?
|
||||
|
||||
---
|
||||
|
||||
## Clip 4: "The Void Between" — Ambient Environment Study (INTERNAL DESIGN)
|
||||
|
||||
**Duration:** 8 seconds
|
||||
**Purpose:** Internal design reference. Tests the ambient environment systems: particles, dust, lighting, skybox.
|
||||
**Tone:** Atmosphere. Mood. "What does the Nexus feel like when nothing is happening?"
|
||||
|
||||
### Shot Sequence (2 shots, ~4s each)
|
||||
|
||||
1. **0–4s | Particle Systems**
|
||||
- Static camera, slight drift
|
||||
- View from platform edge, looking out into the void
|
||||
- Particle systems visible: ambient particles, dust particles
|
||||
- Nebula skybox: dark purples, distant stars, subtle color shifts
|
||||
- No portals, no terminals — just the environment
|
||||
- *Design focus: does the void feel alive or empty?*
|
||||
|
||||
2. **4–8s | Lighting Study**
|
||||
- Camera slowly orbits a point on the platform
|
||||
- Teal point light (position 0,1,-5) creates warm glow
|
||||
- Purple point light (position -8,3,-8) adds depth
|
||||
- Ambient light (0x1a1a3a) fills shadows
|
||||
- Grid lines catch the light
|
||||
- *Design focus: lighting hierarchy, mood consistency*
|
||||
|
||||
### Veo Prompt (text-to-video)
|
||||
```
|
||||
Ambient environment study in a futuristic digital void. Static camera with slight drift, viewing from the edge of a hexagonal platform into deep space. Dark purple nebula with twinkling distant stars, subtle color shifts. Floating particles and dust drift slowly. No structures, no portals — pure atmosphere. Then camera slowly orbits showing teal and purple point lights casting volumetric glow on a dark hexagonal grid floor. Ambient lighting fills shadows. Contemplative, moody, atmospheric. Cyberpunk aesthetic, minimal movement, focus on light and particle behavior.
|
||||
```
|
||||
|
||||
### Design Insight Target
|
||||
- Is the void atmospheric or just dark?
|
||||
- Do the particle systems enhance or distract?
|
||||
- Is the lighting hierarchy (teal primary, purple secondary) clear?
|
||||
|
||||
---
|
||||
|
||||
## Clip 5: "Command Center" — Batcave Terminal Focus (INTERNAL DESIGN)
|
||||
|
||||
**Duration:** 8 seconds
|
||||
**Purpose:** Internal design reference. Tests readability and hierarchy of the holographic terminal panels.
|
||||
**Tone:** Information density. Control. "What can you see from here?"
|
||||
|
||||
### Shot Sequence (3 shots, ~2.5s each)
|
||||
|
||||
1. **0–2.5s | Terminal Overview**
|
||||
- Camera approaches the batcave terminal from the front
|
||||
- 5 holographic panels visible in arc: NEXUS COMMAND, DEV QUEUE, METRICS, SOVEREIGNTY, AGENT STATUS
|
||||
- Each panel has distinct color (teal, gold, purple, gold, teal)
|
||||
- *Design focus: panel arrangement, color distinction*
|
||||
|
||||
2. **2.5–5.5s | Panel Detail**
|
||||
- Camera zooms into METRICS panel
|
||||
- Text scrolls: "> CPU: 12% [||....]", "> MEM: 4.2GB", "> COMMITS: 842"
|
||||
- Panel background glows, scan lines visible
|
||||
- *Design focus: text readability, information density*
|
||||
|
||||
3. **5.5–8s | Agent Status**
|
||||
- Camera shifts to AGENT STATUS panel
|
||||
- Text: "> TIMMY: ● RUNNING", "> KIMI: ○ STANDBY", "> CLAUDE: ● ACTIVE"
|
||||
- Green dot pulses next to active agents
|
||||
- Pull back to show panel in context
|
||||
- *Design focus: status indication clarity*
|
||||
|
||||
### Veo Prompt (text-to-video)
|
||||
```
|
||||
Approach a futuristic holographic command terminal in a dark digital space. Five curved holographic panels float in an arc: "NEXUS COMMAND" (teal), "DEV QUEUE" (gold), "METRICS" (purple), "SOVEREIGNTY" (gold), "AGENT STATUS" (teal). Camera zooms into the METRICS panel showing scrolling data: "CPU: 12%", "MEM: 4.2GB", "COMMITS: 842" with scan lines and glow effects. Shift to AGENT STATUS panel showing "TIMMY: ● RUNNING", "KIMI: ○ STANDBY", "CLAUDE: ● ACTIVE" with pulsing green dots. Pull back to show full terminal context. Dark ambient environment, cyberpunk aesthetic, holographic UI focus.
|
||||
```
|
||||
|
||||
### Design Insight Target
|
||||
- Can you read the text at 1080p?
|
||||
- Do the color-coded panels communicate hierarchy?
|
||||
- Is the scan-line effect too retro or appropriately futuristic?
|
||||
|
||||
---
|
||||
|
||||
## Usage Matrix
|
||||
|
||||
| Clip | Title | Purpose | Audience | Priority |
|
||||
|------|-------|---------|----------|----------|
|
||||
| 1 | First Light | Public teaser | External | HIGH |
|
||||
| 2 | Between Worlds | Portal UX design | Internal | HIGH |
|
||||
| 3 | The Guardian's View | Public promo | External | MEDIUM |
|
||||
| 4 | The Void Between | Environment design | Internal | MEDIUM |
|
||||
| 5 | Command Center | Terminal UI design | Internal | LOW |
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Generate each clip using Veo/Flow (text-to-video prompts above)
|
||||
2. Review outputs — update prompts based on what works
|
||||
3. Record metadata in `docs/media/clip-metadata.json`
|
||||
4. Iterate: refine prompts, regenerate, compare
|
||||
5. Use internal design clips to inform Three.js implementation changes
|
||||
6. Use public promo clips for README, social media, project communication
|
||||
|
||||
---
|
||||
|
||||
*Generated for Issue #681 — Timmy_Foundation/the-nexus*
|
||||
44
index.html
44
index.html
@@ -207,6 +207,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Memory Crystal Inspection Panel (Mnemosyne) -->
|
||||
<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-pin" class="memory-panel-pin" title="Pin panel">📌</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 memory-meta-row--related"><span class="memory-meta-label">Related</span><span id="memory-panel-connections">\u2014</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Session Room HUD Panel (Mnemosyne #1171) -->
|
||||
<div id="session-room-panel" class="session-room-panel" style="display:none;">
|
||||
<div class="session-room-panel-content">
|
||||
<div class="session-room-header">
|
||||
<span class="session-room-icon">□</span>
|
||||
<div class="session-room-title">SESSION CHAMBER</div>
|
||||
<button class="session-room-close" id="session-room-close" title="Close">✕</button>
|
||||
</div>
|
||||
<div class="session-room-timestamp" id="session-room-timestamp">—</div>
|
||||
<div class="session-room-fact-count" id="session-room-fact-count">0 facts</div>
|
||||
<div class="session-room-facts" id="session-room-facts"></div>
|
||||
<div class="session-room-hint">Flying into chamber…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Portal Atlas Overlay -->
|
||||
<div id="atlas-overlay" class="atlas-overlay" style="display:none;">
|
||||
<div class="atlas-content">
|
||||
|
||||
413
nexus/components/session-rooms.js
Normal file
413
nexus/components/session-rooms.js
Normal file
@@ -0,0 +1,413 @@
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// PROJECT MNEMOSYNE — SESSION ROOMS (Issue #1171)
|
||||
// ═══════════════════════════════════════════════════════
|
||||
//
|
||||
// Groups memories by session into holographic chambers.
|
||||
// Each session becomes a wireframe cube floating in space.
|
||||
// Rooms are arranged chronologically along a spiral.
|
||||
// Click a room to fly inside; distant rooms LOD to a point.
|
||||
//
|
||||
// Usage from app.js:
|
||||
// SessionRooms.init(scene, camera, controls);
|
||||
// SessionRooms.updateSessions(sessions); // [{id, timestamp, facts[]}]
|
||||
// SessionRooms.update(delta); // call each frame
|
||||
// SessionRooms.getClickableMeshes(); // for raycasting
|
||||
// SessionRooms.handleRoomClick(mesh); // trigger fly-in
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
const SessionRooms = (() => {
|
||||
|
||||
// ─── CONSTANTS ───────────────────────────────────────
|
||||
const MAX_ROOMS = 20;
|
||||
const ROOM_SIZE = 9; // wireframe cube edge length
|
||||
const ROOM_HALF = ROOM_SIZE / 2;
|
||||
const LOD_THRESHOLD = 55; // distance: full → point
|
||||
const LOD_HYSTERESIS = 5; // buffer to avoid flicker
|
||||
const SPIRAL_BASE_R = 20; // spiral inner radius
|
||||
const SPIRAL_R_STEP = 5; // radius growth per room
|
||||
const SPIRAL_ANGLE_INC = 2.399; // golden angle (radians)
|
||||
const SPIRAL_Y_STEP = 1.5; // vertical rise per room
|
||||
const FLY_DURATION = 1.5; // seconds for fly-in tween
|
||||
const FLY_TARGET_DEPTH = ROOM_HALF - 1.5; // how deep inside to stop
|
||||
|
||||
const ROOM_COLOR = 0x7b5cff; // violet — mnemosyne accent
|
||||
const POINT_COLOR = 0x9b7cff;
|
||||
const LABEL_COLOR = '#c8b4ff';
|
||||
const STORAGE_KEY = 'mnemosyne_sessions_v1';
|
||||
|
||||
// ─── STATE ────────────────────────────────────────────
|
||||
let _scene = null;
|
||||
let _camera = null;
|
||||
let _controls = null;
|
||||
|
||||
let _rooms = []; // array of room objects
|
||||
let _sessionIndex = {}; // id → room object
|
||||
|
||||
// Fly-in tween state
|
||||
let _flyActive = false;
|
||||
let _flyElapsed = 0;
|
||||
let _flyFrom = null;
|
||||
let _flyTo = null;
|
||||
let _flyLookFrom = null;
|
||||
let _flyLookTo = null;
|
||||
let _flyActiveRoom = null;
|
||||
|
||||
// ─── SPIRAL POSITION ──────────────────────────────────
|
||||
function _spiralPos(index) {
|
||||
const angle = index * SPIRAL_ANGLE_INC;
|
||||
const r = SPIRAL_BASE_R + index * SPIRAL_R_STEP;
|
||||
const y = index * SPIRAL_Y_STEP;
|
||||
return new THREE.Vector3(
|
||||
Math.cos(angle) * r,
|
||||
y,
|
||||
Math.sin(angle) * r
|
||||
);
|
||||
}
|
||||
|
||||
// ─── CREATE ROOM ──────────────────────────────────────
|
||||
function _createRoom(session, index) {
|
||||
const pos = _spiralPos(index);
|
||||
const group = new THREE.Group();
|
||||
group.position.copy(pos);
|
||||
|
||||
// Wireframe cube
|
||||
const boxGeo = new THREE.BoxGeometry(ROOM_SIZE, ROOM_SIZE, ROOM_SIZE);
|
||||
const edgesGeo = new THREE.EdgesGeometry(boxGeo);
|
||||
const edgesMat = new THREE.LineBasicMaterial({
|
||||
color: ROOM_COLOR,
|
||||
transparent: true,
|
||||
opacity: 0.55
|
||||
});
|
||||
const wireframe = new THREE.LineSegments(edgesGeo, edgesMat);
|
||||
wireframe.userData = { type: 'session_room_wireframe', sessionId: session.id };
|
||||
group.add(wireframe);
|
||||
|
||||
// Collision mesh (invisible, for raycasting)
|
||||
const hitGeo = new THREE.BoxGeometry(ROOM_SIZE, ROOM_SIZE, ROOM_SIZE);
|
||||
const hitMat = new THREE.MeshBasicMaterial({
|
||||
visible: false,
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
side: THREE.FrontSide
|
||||
});
|
||||
const hitMesh = new THREE.Mesh(hitGeo, hitMat);
|
||||
hitMesh.userData = { type: 'session_room', sessionId: session.id, roomIndex: index };
|
||||
group.add(hitMesh);
|
||||
|
||||
// LOD point (small sphere shown at distance)
|
||||
const pointGeo = new THREE.SphereGeometry(0.5, 6, 4);
|
||||
const pointMat = new THREE.MeshBasicMaterial({
|
||||
color: POINT_COLOR,
|
||||
transparent: true,
|
||||
opacity: 0.7
|
||||
});
|
||||
const pointMesh = new THREE.Mesh(pointGeo, pointMat);
|
||||
pointMesh.userData = { type: 'session_room_point', sessionId: session.id };
|
||||
pointMesh.visible = false; // starts hidden; shown only at LOD distance
|
||||
group.add(pointMesh);
|
||||
|
||||
// Timestamp billboard sprite
|
||||
const sprite = _makeTimestampSprite(session.timestamp, session.facts.length);
|
||||
sprite.position.set(0, ROOM_HALF + 1.2, 0);
|
||||
group.add(sprite);
|
||||
|
||||
// Inner ambient glow
|
||||
const glow = new THREE.PointLight(ROOM_COLOR, 0.4, ROOM_SIZE * 1.2);
|
||||
group.add(glow);
|
||||
|
||||
_scene.add(group);
|
||||
|
||||
const room = {
|
||||
session,
|
||||
group,
|
||||
wireframe,
|
||||
hitMesh,
|
||||
pointMesh,
|
||||
sprite,
|
||||
glow,
|
||||
pos: pos.clone(),
|
||||
index,
|
||||
lodActive: false,
|
||||
pulsePhase: Math.random() * Math.PI * 2
|
||||
};
|
||||
|
||||
_rooms.push(room);
|
||||
_sessionIndex[session.id] = room;
|
||||
|
||||
console.info('[SessionRooms] Created room for session', session.id, 'at index', index);
|
||||
return room;
|
||||
}
|
||||
|
||||
// ─── TIMESTAMP SPRITE ────────────────────────────────
|
||||
function _makeTimestampSprite(isoTimestamp, factCount) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 320;
|
||||
canvas.height = 72;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Background pill
|
||||
ctx.clearRect(0, 0, 320, 72);
|
||||
ctx.fillStyle = 'rgba(20, 10, 40, 0.82)';
|
||||
_roundRect(ctx, 4, 4, 312, 64, 14);
|
||||
ctx.fill();
|
||||
|
||||
// Border
|
||||
ctx.strokeStyle = 'rgba(123, 92, 255, 0.6)';
|
||||
ctx.lineWidth = 1.5;
|
||||
_roundRect(ctx, 4, 4, 312, 64, 14);
|
||||
ctx.stroke();
|
||||
|
||||
// Timestamp text
|
||||
const dt = isoTimestamp ? new Date(isoTimestamp) : new Date();
|
||||
const label = _formatDate(dt);
|
||||
ctx.fillStyle = LABEL_COLOR;
|
||||
ctx.font = 'bold 15px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(label, 160, 30);
|
||||
|
||||
// Fact count
|
||||
ctx.fillStyle = 'rgba(200, 180, 255, 0.65)';
|
||||
ctx.font = '12px monospace';
|
||||
ctx.fillText(factCount + (factCount === 1 ? ' fact' : ' facts'), 160, 52);
|
||||
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, opacity: 0.88 });
|
||||
const sprite = new THREE.Sprite(mat);
|
||||
sprite.scale.set(5, 1.1, 1);
|
||||
sprite.userData = { type: 'session_room_label' };
|
||||
return sprite;
|
||||
}
|
||||
|
||||
// ─── HELPERS ──────────────────────────────────────────
|
||||
function _roundRect(ctx, x, y, w, h, r) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.lineTo(x + w - r, y);
|
||||
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
||||
ctx.lineTo(x + w, y + h - r);
|
||||
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
||||
ctx.lineTo(x + r, y + h);
|
||||
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
||||
ctx.lineTo(x, y + r);
|
||||
ctx.quadraticCurveTo(x, y, x + r, y);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function _formatDate(dt) {
|
||||
if (isNaN(dt.getTime())) return 'Unknown session';
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
|
||||
}
|
||||
|
||||
// ─── DISPOSE ROOM ────────────────────────────────────
|
||||
function _disposeRoom(room) {
|
||||
room.wireframe.geometry.dispose();
|
||||
room.wireframe.material.dispose();
|
||||
room.hitMesh.geometry.dispose();
|
||||
room.hitMesh.material.dispose();
|
||||
room.pointMesh.geometry.dispose();
|
||||
room.pointMesh.material.dispose();
|
||||
if (room.sprite.material.map) room.sprite.material.map.dispose();
|
||||
room.sprite.material.dispose();
|
||||
if (room.group.parent) room.group.parent.remove(room.group);
|
||||
delete _sessionIndex[room.session.id];
|
||||
}
|
||||
|
||||
// ─── PUBLIC: UPDATE SESSIONS ─────────────────────────
|
||||
// sessions: [{id, timestamp, facts:[{id,content,category,strength,...}]}]
|
||||
// Sorted chronologically oldest→newest; max MAX_ROOMS shown.
|
||||
function updateSessions(sessions) {
|
||||
if (!_scene) return;
|
||||
|
||||
const sorted = [...sessions]
|
||||
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp))
|
||||
.slice(-MAX_ROOMS); // keep most recent MAX_ROOMS
|
||||
|
||||
// Remove rooms no longer present
|
||||
const incoming = new Set(sorted.map(s => s.id));
|
||||
for (let i = _rooms.length - 1; i >= 0; i--) {
|
||||
const room = _rooms[i];
|
||||
if (!incoming.has(room.session.id)) {
|
||||
_disposeRoom(room);
|
||||
_rooms.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add / update
|
||||
sorted.forEach((session, idx) => {
|
||||
if (_sessionIndex[session.id]) {
|
||||
// Update position if index changed
|
||||
const room = _sessionIndex[session.id];
|
||||
if (room.index !== idx) {
|
||||
room.index = idx;
|
||||
const newPos = _spiralPos(idx);
|
||||
room.group.position.copy(newPos);
|
||||
room.pos.copy(newPos);
|
||||
}
|
||||
} else {
|
||||
_createRoom(session, idx);
|
||||
}
|
||||
});
|
||||
|
||||
saveToStorage(sorted);
|
||||
console.info('[SessionRooms] Updated:', _rooms.length, 'session rooms');
|
||||
}
|
||||
|
||||
// ─── PUBLIC: INIT ─────────────────────────────────────
|
||||
function init(scene, camera, controls) {
|
||||
_scene = scene;
|
||||
_camera = camera;
|
||||
_controls = controls;
|
||||
console.info('[SessionRooms] Initialized');
|
||||
|
||||
// Restore persisted sessions
|
||||
const saved = loadFromStorage();
|
||||
if (saved && saved.length > 0) {
|
||||
updateSessions(saved);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── PUBLIC: UPDATE (per-frame) ───────────────────────
|
||||
function update(delta) {
|
||||
if (!_scene || !_camera) return;
|
||||
|
||||
const camPos = _camera.position;
|
||||
|
||||
_rooms.forEach(room => {
|
||||
const dist = camPos.distanceTo(room.pos);
|
||||
|
||||
// LOD toggle
|
||||
const threshold = room.lodActive
|
||||
? LOD_THRESHOLD + LOD_HYSTERESIS // must come closer to exit LOD
|
||||
: LOD_THRESHOLD;
|
||||
|
||||
if (dist > threshold && !room.lodActive) {
|
||||
room.lodActive = true;
|
||||
room.wireframe.visible = false;
|
||||
room.sprite.visible = false;
|
||||
room.pointMesh.visible = true;
|
||||
} else if (dist <= threshold && room.lodActive) {
|
||||
room.lodActive = false;
|
||||
room.wireframe.visible = true;
|
||||
room.sprite.visible = true;
|
||||
room.pointMesh.visible = false;
|
||||
}
|
||||
|
||||
// Pulse wireframe opacity
|
||||
room.pulsePhase += delta * 0.6;
|
||||
if (!room.lodActive) {
|
||||
room.wireframe.material.opacity = 0.3 + Math.sin(room.pulsePhase) * 0.2;
|
||||
room.glow.intensity = 0.3 + Math.sin(room.pulsePhase * 1.4) * 0.15;
|
||||
}
|
||||
|
||||
// Slowly rotate each room
|
||||
room.group.rotation.y += delta * 0.04;
|
||||
});
|
||||
|
||||
// Fly-in tween
|
||||
if (_flyActive) {
|
||||
_flyElapsed += delta;
|
||||
const t = Math.min(_flyElapsed / FLY_DURATION, 1);
|
||||
const ease = _easeInOut(t);
|
||||
|
||||
_camera.position.lerpVectors(_flyFrom, _flyTo, ease);
|
||||
|
||||
// Interpolate lookAt
|
||||
const lookNow = new THREE.Vector3().lerpVectors(_flyLookFrom, _flyLookTo, ease);
|
||||
_camera.lookAt(lookNow);
|
||||
if (_controls && _controls.target) _controls.target.copy(lookNow);
|
||||
|
||||
if (t >= 1) {
|
||||
_flyActive = false;
|
||||
if (_controls && typeof _controls.update === 'function') _controls.update();
|
||||
console.info('[SessionRooms] Fly-in complete for session', _flyActiveRoom && _flyActiveRoom.session.id);
|
||||
_flyActiveRoom = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── EASING ───────────────────────────────────────────
|
||||
function _easeInOut(t) {
|
||||
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
||||
}
|
||||
|
||||
// ─── PUBLIC: GET CLICKABLE MESHES ─────────────────────
|
||||
function getClickableMeshes() {
|
||||
return _rooms.map(r => r.hitMesh);
|
||||
}
|
||||
|
||||
// ─── PUBLIC: HANDLE ROOM CLICK ────────────────────────
|
||||
function handleRoomClick(mesh) {
|
||||
const { sessionId } = mesh.userData;
|
||||
const room = _sessionIndex[sessionId];
|
||||
if (!room || !_camera) return null;
|
||||
|
||||
// Fly into the room from the front face
|
||||
_flyActive = true;
|
||||
_flyElapsed = 0;
|
||||
_flyActiveRoom = room;
|
||||
|
||||
_flyFrom = _camera.position.clone();
|
||||
|
||||
// Target: step inside the room toward its center
|
||||
const dir = room.pos.clone().sub(_camera.position).normalize();
|
||||
_flyTo = room.pos.clone().add(dir.multiplyScalar(FLY_TARGET_DEPTH));
|
||||
|
||||
_flyLookFrom = _controls && _controls.target
|
||||
? _controls.target.clone()
|
||||
: _camera.position.clone().add(_camera.getWorldDirection(new THREE.Vector3()));
|
||||
_flyLookTo = room.pos.clone();
|
||||
|
||||
console.info('[SessionRooms] Flying into session room:', sessionId);
|
||||
return room.session;
|
||||
}
|
||||
|
||||
// ─── PERSISTENCE ──────────────────────────────────────
|
||||
function saveToStorage(sessions) {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ v: 1, sessions }));
|
||||
} catch (e) {
|
||||
console.warn('[SessionRooms] Failed to save to localStorage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function loadFromStorage() {
|
||||
if (typeof localStorage === 'undefined') return null;
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || parsed.v !== 1 || !Array.isArray(parsed.sessions)) return null;
|
||||
console.info('[SessionRooms] Restored', parsed.sessions.length, 'sessions from localStorage');
|
||||
return parsed.sessions;
|
||||
} catch (e) {
|
||||
console.warn('[SessionRooms] Failed to load from localStorage:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function clearStorage() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
console.info('[SessionRooms] Cleared localStorage');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── PUBLIC API ───────────────────────────────────────
|
||||
return {
|
||||
init,
|
||||
updateSessions,
|
||||
update,
|
||||
getClickableMeshes,
|
||||
handleRoomClick,
|
||||
clearStorage,
|
||||
// For external inspection
|
||||
getRooms: () => _rooms,
|
||||
getSession: (id) => _sessionIndex[id] || null,
|
||||
isFlyActive: () => _flyActive
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
export { SessionRooms };
|
||||
@@ -8,12 +8,20 @@
|
||||
// holographic archive.
|
||||
//
|
||||
// World layout (hex cylinder, radius 25):
|
||||
// North (z-) → Documents & Knowledge
|
||||
// South (z+) → Projects & Tasks
|
||||
// East (x+) → Code & Engineering
|
||||
// West (x-) → Conversations & Social
|
||||
// Center → Active Working Memory
|
||||
// Below (y-) → Archive (cold storage)
|
||||
//
|
||||
// Inner ring — original Mnemosyne taxonomy (radius 15):
|
||||
// North (z-) → Documents & Knowledge
|
||||
// South (z+) → Projects & Tasks
|
||||
// East (x+) → Code & Engineering
|
||||
// West (x-) → Conversations & Social
|
||||
// Center → Active Working Memory
|
||||
// Below (y-) → Archive (cold storage)
|
||||
//
|
||||
// Outer ring — MemPalace category zones (radius 20, issue #1168):
|
||||
// North (z-) → User Preferences [golden]
|
||||
// East (x+) → Project facts [blue]
|
||||
// South (z+) → Tool knowledge [green]
|
||||
// West (x-) → General facts [gray]
|
||||
//
|
||||
// Usage from app.js:
|
||||
// SpatialMemory.init(scene);
|
||||
@@ -73,6 +81,44 @@ const SpatialMemory = (() => {
|
||||
color: 0x334455,
|
||||
glyph: '\uD83D\uDDC4',
|
||||
description: 'Cold storage — rarely accessed, aged-out memories'
|
||||
},
|
||||
|
||||
// ── MemPalace category zones — outer ring, issue #1168 ────────────
|
||||
user_pref: {
|
||||
label: 'User Preferences',
|
||||
center: [0, 0, -20],
|
||||
radius: 10,
|
||||
color: 0xffd700,
|
||||
glyph: '\u2605',
|
||||
description: 'Personal preferences, habits, user-specific settings',
|
||||
labelY: 5
|
||||
},
|
||||
project: {
|
||||
label: 'Project Facts',
|
||||
center: [20, 0, 0],
|
||||
radius: 10,
|
||||
color: 0x4488ff,
|
||||
glyph: '\uD83D\uDCC1',
|
||||
description: 'Project-specific knowledge, goals, context',
|
||||
labelY: 5
|
||||
},
|
||||
tool: {
|
||||
label: 'Tool Knowledge',
|
||||
center: [0, 0, 20],
|
||||
radius: 10,
|
||||
color: 0x44cc66,
|
||||
glyph: '\uD83D\uDD27',
|
||||
description: 'Tools, commands, APIs, and how to use them',
|
||||
labelY: 5
|
||||
},
|
||||
general: {
|
||||
label: 'General Facts',
|
||||
center: [-20, 0, 0],
|
||||
radius: 10,
|
||||
color: 0x8899aa,
|
||||
glyph: '\uD83D\uDCDD',
|
||||
description: 'Miscellaneous facts not fitting other categories',
|
||||
labelY: 5
|
||||
}
|
||||
};
|
||||
|
||||
@@ -99,6 +145,7 @@ const SpatialMemory = (() => {
|
||||
const cx = region.center[0];
|
||||
const cy = region.center[1] + 0.06;
|
||||
const cz = region.center[2];
|
||||
const labelY = region.labelY || 3;
|
||||
|
||||
const ringGeo = new THREE.RingGeometry(region.radius - 0.5, region.radius, 6);
|
||||
const ringMat = new THREE.MeshBasicMaterial({
|
||||
@@ -126,6 +173,22 @@ const SpatialMemory = (() => {
|
||||
_scene.add(ring);
|
||||
_scene.add(disc);
|
||||
|
||||
// Ground glow — brighter disc for MemPalace zones (labelY > 3 signals outer ring)
|
||||
let glowDisc = null;
|
||||
if (labelY > 3) {
|
||||
const glowGeo = new THREE.CircleGeometry(region.radius, 32);
|
||||
const glowMat = new THREE.MeshBasicMaterial({
|
||||
color: region.color,
|
||||
transparent: true,
|
||||
opacity: 0.06,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
glowDisc = new THREE.Mesh(glowGeo, glowMat);
|
||||
glowDisc.rotation.x = -Math.PI / 2;
|
||||
glowDisc.position.set(cx, cy - 0.02, cz);
|
||||
_scene.add(glowDisc);
|
||||
}
|
||||
|
||||
// Floating label
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 256;
|
||||
@@ -139,11 +202,11 @@ const SpatialMemory = (() => {
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
const spriteMat = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0.6 });
|
||||
const sprite = new THREE.Sprite(spriteMat);
|
||||
sprite.position.set(cx, 3, cz);
|
||||
sprite.position.set(cx, labelY, cz);
|
||||
sprite.scale.set(4, 1, 1);
|
||||
_scene.add(sprite);
|
||||
|
||||
return { ring, disc, sprite };
|
||||
return { ring, disc, glowDisc, sprite };
|
||||
}
|
||||
|
||||
// ─── PLACE A MEMORY ──────────────────────────────────
|
||||
@@ -283,6 +346,9 @@ const SpatialMemory = (() => {
|
||||
if (marker.ring && marker.ring.material) {
|
||||
marker.ring.material.opacity = 0.1 + Math.sin(now * 0.001) * 0.05;
|
||||
}
|
||||
if (marker.glowDisc && marker.glowDisc.material) {
|
||||
marker.glowDisc.material.opacity = 0.04 + Math.sin(now * 0.0008) * 0.02;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -456,6 +522,81 @@ const SpatialMemory = (() => {
|
||||
return count;
|
||||
}
|
||||
|
||||
// ─── GRAVITY WELL CLUSTERING ──────────────────────────
|
||||
// Force-directed layout: same-category crystals attract, unrelated repel.
|
||||
// Run on load (bake positions, not per-frame). Spec from issue #1175.
|
||||
const GRAVITY_ITERATIONS = 20;
|
||||
const ATTRACT_FACTOR = 0.10; // 10% closer to same-category centroid per iteration
|
||||
const REPEL_FACTOR = 0.05; // 5% away from nearest unrelated crystal
|
||||
|
||||
function runGravityLayout() {
|
||||
const objs = Object.values(_memoryObjects);
|
||||
if (objs.length < 2) {
|
||||
console.info('[Mnemosyne] Gravity layout: fewer than 2 crystals, skipping');
|
||||
return;
|
||||
}
|
||||
console.info('[Mnemosyne] Gravity layout starting —', objs.length, 'crystals,', GRAVITY_ITERATIONS, 'iterations');
|
||||
|
||||
for (let iter = 0; iter < GRAVITY_ITERATIONS; iter++) {
|
||||
// Accumulate displacements before applying (avoids order-of-iteration bias)
|
||||
const dx = new Float32Array(objs.length);
|
||||
const dy = new Float32Array(objs.length);
|
||||
const dz = new Float32Array(objs.length);
|
||||
|
||||
objs.forEach((obj, i) => {
|
||||
const pos = obj.mesh.position;
|
||||
const cat = obj.region;
|
||||
|
||||
// ── Attraction toward same-category centroid ──────────────
|
||||
let sx = 0, sy = 0, sz = 0, sameCount = 0;
|
||||
objs.forEach(o => {
|
||||
if (o === obj || o.region !== cat) return;
|
||||
sx += o.mesh.position.x;
|
||||
sy += o.mesh.position.y;
|
||||
sz += o.mesh.position.z;
|
||||
sameCount++;
|
||||
});
|
||||
if (sameCount > 0) {
|
||||
dx[i] += ((sx / sameCount) - pos.x) * ATTRACT_FACTOR;
|
||||
dy[i] += ((sy / sameCount) - pos.y) * ATTRACT_FACTOR;
|
||||
dz[i] += ((sz / sameCount) - pos.z) * ATTRACT_FACTOR;
|
||||
}
|
||||
|
||||
// ── Repulsion from nearest unrelated crystal ───────────────
|
||||
let nearestDist = Infinity;
|
||||
let rnx = 0, rny = 0, rnz = 0;
|
||||
objs.forEach(o => {
|
||||
if (o === obj || o.region === cat) return;
|
||||
const ex = pos.x - o.mesh.position.x;
|
||||
const ey = pos.y - o.mesh.position.y;
|
||||
const ez = pos.z - o.mesh.position.z;
|
||||
const d = Math.sqrt(ex * ex + ey * ey + ez * ez);
|
||||
if (d < nearestDist) {
|
||||
nearestDist = d;
|
||||
rnx = ex; rny = ey; rnz = ez;
|
||||
}
|
||||
});
|
||||
if (nearestDist > 0.001 && nearestDist < Infinity) {
|
||||
const len = Math.sqrt(rnx * rnx + rny * rny + rnz * rnz);
|
||||
dx[i] += (rnx / len) * nearestDist * REPEL_FACTOR;
|
||||
dy[i] += (rny / len) * nearestDist * REPEL_FACTOR;
|
||||
dz[i] += (rnz / len) * nearestDist * REPEL_FACTOR;
|
||||
}
|
||||
});
|
||||
|
||||
// Apply displacements
|
||||
objs.forEach((obj, i) => {
|
||||
obj.mesh.position.x += dx[i];
|
||||
obj.mesh.position.y += dy[i];
|
||||
obj.mesh.position.z += dz[i];
|
||||
});
|
||||
}
|
||||
|
||||
// Bake final positions to localStorage
|
||||
saveToStorage();
|
||||
console.info('[Mnemosyne] Gravity layout complete — positions baked to localStorage');
|
||||
}
|
||||
|
||||
// ─── SPATIAL SEARCH ──────────────────────────────────
|
||||
function searchNearby(position, maxResults, maxDist) {
|
||||
maxResults = maxResults || 10;
|
||||
@@ -471,11 +612,53 @@ const SpatialMemory = (() => {
|
||||
return results.slice(0, maxResults);
|
||||
}
|
||||
|
||||
|
||||
// ─── CRYSTAL MESH COLLECTION (for raycasting) ────────
|
||||
function getCrystalMeshes() {
|
||||
return Object.values(_memoryObjects).map(o => o.mesh);
|
||||
}
|
||||
|
||||
// ─── MEMORY DATA FROM MESH ───────────────────────────
|
||||
function getMemoryFromMesh(mesh) {
|
||||
const entry = Object.values(_memoryObjects).find(o => o.mesh === mesh);
|
||||
return entry ? { data: entry.data, region: entry.region } : null;
|
||||
}
|
||||
|
||||
// ─── HIGHLIGHT / SELECT ──────────────────────────────
|
||||
let _selectedId = null;
|
||||
let _selectedOriginalEmissive = null;
|
||||
|
||||
function highlightMemory(memId) {
|
||||
clearHighlight();
|
||||
const obj = _memoryObjects[memId];
|
||||
if (!obj) return;
|
||||
_selectedId = memId;
|
||||
_selectedOriginalEmissive = obj.mesh.material.emissiveIntensity;
|
||||
obj.mesh.material.emissiveIntensity = 4.0;
|
||||
obj.mesh.userData.selected = true;
|
||||
}
|
||||
|
||||
function clearHighlight() {
|
||||
if (_selectedId && _memoryObjects[_selectedId]) {
|
||||
const obj = _memoryObjects[_selectedId];
|
||||
obj.mesh.material.emissiveIntensity = _selectedOriginalEmissive || (obj.data.strength || 0.7) * 2.5;
|
||||
obj.mesh.userData.selected = false;
|
||||
}
|
||||
_selectedId = null;
|
||||
_selectedOriginalEmissive = null;
|
||||
}
|
||||
|
||||
function getSelectedId() {
|
||||
return _selectedId;
|
||||
}
|
||||
|
||||
return {
|
||||
init, placeMemory, removeMemory, update,
|
||||
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
|
||||
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
|
||||
exportIndex, importIndex, searchNearby, REGIONS,
|
||||
saveToStorage, loadFromStorage, clearStorage
|
||||
saveToStorage, loadFromStorage, clearStorage,
|
||||
runGravityLayout
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
357
style.css
357
style.css
@@ -1223,3 +1223,360 @@ canvas#nexus-canvas {
|
||||
.l402-msg { color: #fff; }
|
||||
|
||||
.pse-status { color: #4af0c0; font-weight: 600; }
|
||||
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
MNEMOSYNE — MEMORY CRYSTAL INSPECTION PANEL
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
.memory-panel {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
right: 24px;
|
||||
transform: translateY(-50%);
|
||||
z-index: 120;
|
||||
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(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: 340px;
|
||||
background: rgba(8, 8, 24, 0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(74, 240, 192, 0.25);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 0 30px rgba(74, 240, 192, 0.08), 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.memory-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.memory-panel-region-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.memory-panel-region {
|
||||
font-family: var(--font-display, monospace);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--color-primary, #4af0c0);
|
||||
text-transform: uppercase;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.memory-panel-close {
|
||||
background: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: var(--color-text-muted, #888);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.memory-panel-close:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.memory-panel-body {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text, #ccc);
|
||||
margin-bottom: 14px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.memory-panel-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.memory-meta-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.memory-meta-label {
|
||||
color: var(--color-text-muted, #666);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
min-width: 50px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.memory-meta-row span:last-child {
|
||||
color: var(--color-text, #aaa);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.memory-conn-tag {
|
||||
display: inline-block;
|
||||
background: rgba(74, 240, 192, 0.1);
|
||||
border: 1px solid rgba(74, 240, 192, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
font-size: 10px;
|
||||
font-family: var(--font-mono, monospace);
|
||||
color: var(--color-primary, #4af0c0);
|
||||
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;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
PROJECT MNEMOSYNE — SESSION ROOM HUD PANEL (#1171)
|
||||
═══════════════════════════════════════════════════════ */
|
||||
|
||||
.session-room-panel {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 125;
|
||||
animation: sessionPanelIn 0.25s ease-out forwards;
|
||||
}
|
||||
|
||||
.session-room-panel.session-panel-fade-out {
|
||||
animation: sessionPanelOut 0.2s ease-in forwards !important;
|
||||
}
|
||||
|
||||
@keyframes sessionPanelIn {
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(12px); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes sessionPanelOut {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; transform: translateX(-50%) translateY(10px); }
|
||||
}
|
||||
|
||||
.session-room-panel-content {
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
background: rgba(8, 4, 28, 0.93);
|
||||
backdrop-filter: blur(14px);
|
||||
border: 1px solid rgba(123, 92, 255, 0.35);
|
||||
border-radius: 12px;
|
||||
padding: 14px 18px;
|
||||
box-shadow: 0 0 32px rgba(123, 92, 255, 0.1), 0 8px 32px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.session-room-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
|
||||
}
|
||||
|
||||
.session-room-icon {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.session-room-title {
|
||||
font-family: var(--font-display, monospace);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.18em;
|
||||
color: #9b7cff;
|
||||
text-transform: uppercase;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.session-room-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 0 2px;
|
||||
line-height: 1;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.session-room-close:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.session-room-timestamp {
|
||||
font-family: var(--font-display, monospace);
|
||||
font-size: 13px;
|
||||
color: #c8b4ff;
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.session-room-fact-count {
|
||||
font-size: 11px;
|
||||
color: rgba(200, 180, 255, 0.55);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.session-room-facts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-height: 140px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.session-room-fact-item {
|
||||
font-size: 11px;
|
||||
color: rgba(220, 210, 255, 0.75);
|
||||
padding: 4px 8px;
|
||||
background: rgba(123, 92, 255, 0.07);
|
||||
border-left: 2px solid rgba(123, 92, 255, 0.4);
|
||||
border-radius: 0 4px 4px 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.session-room-hint {
|
||||
margin-top: 10px;
|
||||
font-size: 10px;
|
||||
color: rgba(200, 180, 255, 0.35);
|
||||
text-align: center;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user