Compare commits
3 Commits
claude/iss
...
mimo/resea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
479b9ec132 | ||
| cc4af009c7 | |||
| 089b06b6f8 |
203
FINDINGS-issue-1047.md
Normal file
203
FINDINGS-issue-1047.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# FINDINGS: MemPalace Local AI Memory System Assessment & Leverage Plan
|
||||
|
||||
**Issue:** #1047
|
||||
**Date:** 2026-04-10
|
||||
**Investigator:** mimo-v2-pro (swarm researcher)
|
||||
|
||||
---
|
||||
|
||||
## 1. What Issue #1047 Claims
|
||||
|
||||
The issue (authored by Bezalel, dated 2026-04-07) describes MemPalace as:
|
||||
- An open-source local-first AI memory system with highest published LongMemEval scores (96.6% R@5)
|
||||
- A Python CLI + MCP server using ChromaDB + SQLite with a "palace" hierarchy metaphor
|
||||
- AAAK compression dialect for ~30x context compression
|
||||
- 19 MCP tools for agent memory
|
||||
|
||||
It recommends that every wizard clone/vendor MemPalace, configure rooms, mine workspace, and wire the searcher into heartbeats.
|
||||
|
||||
## 2. What Actually Exists in the Codebase (Current State)
|
||||
|
||||
The Nexus repo already contains **substantial MemPalace integration** that goes well beyond the original research proposal. Here is the full inventory:
|
||||
|
||||
### 2.1 Core Python Layer — `nexus/mempalace/` (3 files, ~290 lines)
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `config.py` | Environment-driven config: palace paths, fleet path, wing name, core rooms, collection name |
|
||||
| `searcher.py` | ChromaDB-backed search/write API with `search_memories()`, `search_fleet()`, `add_memory()` |
|
||||
| `__init__.py` | Package marker |
|
||||
|
||||
**Status:** Functional. Clean API. Lazy ChromaDB import with graceful `MemPalaceUnavailable` exception.
|
||||
|
||||
### 2.2 Fleet Management Tools — `mempalace/` (8 files, ~800 lines)
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `rooms.yaml` | Fleet-wide room taxonomy standard (5 core rooms + optional rooms) |
|
||||
| `validate_rooms.py` | Validates wizard `mempalace.yaml` against fleet standard |
|
||||
| `audit_privacy.py` | Scans fleet palace for policy violations (raw drawers, oversized closets, private paths) |
|
||||
| `retain_closets.py` | 90-day retention enforcement for closet aging |
|
||||
| `export_closets.sh` | Privacy-safe closet export for rsync to Alpha fleet palace |
|
||||
| `fleet_api.py` | HTTP API for shared fleet palace (search, record, wings) |
|
||||
| `tunnel_sync.py` | Pull closets from remote wizard's fleet API into local palace |
|
||||
| `__init__.py` | Package marker |
|
||||
|
||||
**Status:** Well-structured. Each tool has clear CLI interface and proper error handling.
|
||||
|
||||
### 2.3 Evennia MUD Integration — `nexus/evennia_mempalace/` (6 files, ~580 lines)
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `commands/recall.py` | `CmdRecall` (semantic search), `CmdEnterRoom` (teleport), `CmdAsk` (NPC query) |
|
||||
| `commands/write.py` | `CmdRecord`, `CmdNote`, `CmdEvent` (memory writing commands) |
|
||||
| `typeclasses/rooms.py` | `MemPalaceRoom` typeclass |
|
||||
| `typeclasses/npcs.py` | `StewardNPC` with question-answering via palace search |
|
||||
|
||||
**Status:** Complete. Evennia stub fallback for testing outside live environment.
|
||||
|
||||
### 2.4 3D Visualization — `nexus/components/spatial-memory.js` (~665 lines)
|
||||
|
||||
Maps memory categories to spatial regions in the Nexus Three.js world:
|
||||
- Inner ring: Documents, Projects, Code, Conversations, Working Memory, Archive
|
||||
- Outer ring (MemPalace zones, issue #1168): User Preferences, Project Facts, Tool Knowledge, General Facts
|
||||
- Crystal geometry with deterministic positioning, connection lines, localStorage persistence
|
||||
|
||||
**Status:** Functional 3D visualization with region markers, memory crystals, and animation.
|
||||
|
||||
### 2.5 Frontend Integration — `mempalace.js` (~44 lines)
|
||||
|
||||
Basic Electron/browser integration class that:
|
||||
- Initializes a palace wing
|
||||
- Auto-mines chat content every 30 seconds
|
||||
- Exposes `search()` method
|
||||
- Updates stats display
|
||||
|
||||
**Status:** Minimal but functional as a bridge between browser UI and CLI mempalace.
|
||||
|
||||
### 2.6 Scripts & Automation — `scripts/` (5 files)
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `mempalace-incremental-mine.sh` | Re-mines only changed files since last run |
|
||||
| `mempalace_nightly.sh` | Nightly maintenance |
|
||||
| `mempalace_export.py` | Export utility |
|
||||
| `validate_mempalace_taxonomy.py` | Taxonomy validation script |
|
||||
| `audit_mempalace_privacy.py` | Privacy audit script |
|
||||
| `sync_fleet_to_alpha.sh` | Fleet sync to Alpha server |
|
||||
|
||||
### 2.7 Tests — `tests/` (7 test files)
|
||||
|
||||
| File | Tests |
|
||||
|------|-------|
|
||||
| `test_mempalace_searcher.py` | Searcher API, config |
|
||||
| `test_mempalace_validate_rooms.py` | Room taxonomy validation |
|
||||
| `test_mempalace_retain_closets.py` | Closet retention |
|
||||
| `test_mempalace_audit_privacy.py` | Privacy auditor |
|
||||
| `test_mempalace_fleet_api.py` | Fleet HTTP API |
|
||||
| `test_mempalace_tunnel_sync.py` | Remote wizard sync |
|
||||
| `test_evennia_mempalace_commands.py` | Evennia commands + NPC helpers |
|
||||
|
||||
### 2.8 CI/CD
|
||||
|
||||
- **ci.yml**: Validates palace taxonomy on every PR, plus Python/JSON/YAML syntax checks
|
||||
- **weekly-audit.yml**: Monday 05:00 UTC — runs privacy audit + dry-run retention against test fixtures
|
||||
|
||||
### 2.9 Documentation
|
||||
|
||||
- `docs/mempalace_taxonomy.yaml` — Full taxonomy standard (145 lines)
|
||||
- `docs/mempalace/rooms.yaml` — Rooms documentation
|
||||
- `docs/mempalace/bezalel_example.yaml` — Example wizard config
|
||||
- `docs/bezalel/evennia/` — Evennia integration examples (steward NPC, palace commands)
|
||||
- `reports/bezalel/2026-04-07-mempalace-field-report.md` — Original field report
|
||||
|
||||
## 3. Gap Analysis: Issue #1047 vs. Reality
|
||||
|
||||
| Issue #1047 Proposes | Current State | Gap |
|
||||
|---------------------|---------------|-----|
|
||||
| "Each wizard should clone/vendor it" | Vendor infrastructure exists (`scripts/mempalace-incremental-mine.sh`) | **DONE** |
|
||||
| "Write a mempalace.yaml" | Fleet taxonomy standard + validator exist | **DONE** |
|
||||
| "Run mempalace mine" | Incremental mining script exists | **DONE** |
|
||||
| "Wire searcher into heartbeat scripts" | `nexus/mempalace/searcher.py` provides API | **DONE** (needs adoption verification) |
|
||||
| AAAK compression | Not implemented in repo | **OPEN** — no AAAK dialect code |
|
||||
| MCP server (19 tools) | No MCP server integration | **OPEN** — no MCP tool definitions |
|
||||
| Benchmark validation | No LongMemEval test harness in repo | **OPEN** — claims unverified locally |
|
||||
| Fleet-wide adoption | Only Bezalel field report exists | **OPEN** — no evidence of Timmy/Allegro/Ezra adoption |
|
||||
| Hermes harness integration | No direct harness/memory-tool bridge | **OPEN** — searcher exists but no harness wiring |
|
||||
|
||||
## 4. What's Actually Broken
|
||||
|
||||
### 4.1 No AAAK Implementation
|
||||
The issue describes AAAK (~30x compression, ~170 tokens wake-up context) as a key feature, but there is zero AAAK code in the repo. The `nexus/mempalace/` layer has no compression functions. This is a missing feature, not a bug.
|
||||
|
||||
### 4.2 No MCP Server Bridge
|
||||
The upstream MemPalace offers 19 MCP tools, but the Nexus integration only exposes the ChromaDB Python API. There is no MCP server definition, no tool registration for the harness, and no bridge to the `mcp_config.json` at repo root.
|
||||
|
||||
### 4.3 Fleet Adoption Gap
|
||||
Only Bezalel has a documented field report (#1072). There is no evidence that Timmy, Allegro, or Ezra have populated palaces, configured room taxonomies, or run incremental mining. The `export_closets.sh` script hardcodes Bezalel paths.
|
||||
|
||||
### 4.4 Frontend Integration Stale
|
||||
`mempalace.js` references `window.electronAPI.execPython()` which only works in the Electron shell. The main `app.js` (Three.js world) does not import or use `mempalace.js`. The `spatial-memory.js` component defines MemPalace zones but has no data pipeline to populate them from actual palace data.
|
||||
|
||||
### 4.5 Upstream Quality Concern
|
||||
Bezalel's field report notes the upstream repo is "astroturfed hype" — 13.4k LOC in a single commit, 5,769 GitHub stars in 48 hours, ~125 lines of tests. The code is not malicious but is not production-grade. The Nexus has effectively forked/vendored the useful parts and rewritten the critical integration layers.
|
||||
|
||||
## 5. What's Working Well
|
||||
|
||||
1. **Clean architecture separation** — `nexus/mempalace/` is a proper Python package with config/searcher separation. Testable without ChromaDB installed.
|
||||
|
||||
2. **Privacy-first fleet design** — closet-only export policy, privacy auditor, retention enforcement, and private path detection are solid operational safeguards.
|
||||
|
||||
3. **Taxonomy standardization** — `rooms.yaml` + validator ensures consistent memory structure across wizards.
|
||||
|
||||
4. **CI integration** — Taxonomy validation in PR checks + weekly privacy audit cron are good DevOps practices.
|
||||
|
||||
5. **Evennia integration** — The MUD commands (recall, enter room, ask steward) are well-designed and testable outside Evennia via stubs.
|
||||
|
||||
6. **Spatial visualization** — `spatial-memory.js` is a creative 3D representation with deterministic positioning and category zones.
|
||||
|
||||
## 6. Recommended Actions
|
||||
|
||||
### Priority 1: Fleet Adoption Verification (effort: small)
|
||||
- Confirm each wizard (Timmy, Allegro, Ezra) has run `mempalace mine` and has a populated palace
|
||||
- Verify `mempalace.yaml` exists on each wizard's VPS
|
||||
- Update `export_closets.sh` to not hardcode Bezalel paths (use env vars)
|
||||
|
||||
### Priority 2: Hermes Harness Bridge (effort: medium)
|
||||
- Wire `nexus/mempalace/searcher.py` into the Hermes harness as a memory tool
|
||||
- Add memory search/recall to the agent loop so wizards get cross-session context automatically
|
||||
- Map MemPalace search to the existing `memory`/`fact_store` tools or add a dedicated `palace_search` tool
|
||||
|
||||
### Priority 3: MCP Server Registration (effort: medium)
|
||||
- Create an MCP server that exposes search, write, and status tools
|
||||
- Register in `mcp_config.json`
|
||||
- Enable any harness agent to use MemPalace without Python imports
|
||||
|
||||
### Priority 4: AAAK Compression (effort: large, optional)
|
||||
- Implement or port the AAAK compression dialect
|
||||
- Generate wake-up context summaries from palace data
|
||||
- This is a nice-to-have, not critical — the raw ChromaDB search is functional
|
||||
|
||||
### Priority 5: 3D Pipeline Bridge (effort: medium)
|
||||
- Connect `spatial-memory.js` to live palace data via WebSocket or REST
|
||||
- Populate memory crystals from actual search results
|
||||
- Visual feedback when new memories are added
|
||||
|
||||
## 7. Effort Summary
|
||||
|
||||
| Action | Effort | Impact |
|
||||
|--------|--------|--------|
|
||||
| Fleet adoption verification | 2-4 hours | High — ensures all wizards have memory |
|
||||
| Hermes harness bridge | 1-2 days | High — automatic cross-session context |
|
||||
| MCP server registration | 1 day | Medium — enables any agent to use palace |
|
||||
| AAAK compression | 2-3 days | Low — nice-to-have |
|
||||
| 3D pipeline bridge | 1-2 days | Medium — visual representation of memory |
|
||||
| Fix export_closets.sh hardcoded paths | 30 min | Low — operational hygiene |
|
||||
|
||||
## 8. Conclusion
|
||||
|
||||
Issue #1047 was a research request from 2026-04-07. Since then, significant implementation work has been completed — far exceeding the original proposal. The core memory infrastructure (searcher, fleet tools, privacy, taxonomy, Evennia integration, tests, CI) is **built and functional**.
|
||||
|
||||
The primary remaining gap is **fleet-wide adoption** (only Bezalel has documented use) and **harness integration** (the searcher exists but isn't wired into the agent loop). The AAAK and MCP features from the original research are not implemented but are not blocking — the ChromaDB-backed search provides the core value proposition.
|
||||
|
||||
**Verdict:** The MemPalace integration is substantially complete at the infrastructure level. The next bottleneck is operational adoption and harness wiring, not new feature development.
|
||||
112
app.js
112
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();
|
||||
@@ -1915,8 +1917,19 @@ function setupControls() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2734,6 +2747,62 @@ function _dismissMemoryPanelForce() {
|
||||
}, 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);
|
||||
@@ -2765,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');
|
||||
@@ -3288,12 +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();
|
||||
|
||||
15
index.html
15
index.html
@@ -236,6 +236,21 @@
|
||||
</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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
119
style.css
119
style.css
@@ -1461,3 +1461,122 @@ canvas#nexus-canvas {
|
||||
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