superlocalmemory 3.7.8 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +69 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -1
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +94 -10
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +2 -0
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +35 -11
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +243 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +21 -49
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +8 -1
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +4 -4
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +10 -5
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +182 -57
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +183 -69
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +20 -5
- package/src/superlocalmemory/server/unified_daemon.py +384 -56
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +2 -2
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
// od-memories.js — Memories · Timeline · Knowledge Clusters (OD design port)
|
|
2
|
+
// Render: #memories-pane | Public: window.odRenderMemories(container)
|
|
3
|
+
// CSP-safe: data-od-act event delegation. XSS-safe: _esc() on all API values.
|
|
4
|
+
// nosemgrep: innerHTML — all dynamic values escaped via _esc()
|
|
5
|
+
//
|
|
6
|
+
// Endpoints (live daemon 127.0.0.1:8765):
|
|
7
|
+
// GET /api/memories?limit=N&offset=N[&category=X]
|
|
8
|
+
// → { memories[{id,memory_id,content,category,importance(0-1),
|
|
9
|
+
// access_count,created_at,project_name}], total }
|
|
10
|
+
// GET /api/memories/{id}/facts → { facts[{fact_type,content}] }
|
|
11
|
+
// GET /api/v3/timeline/?range=Xd&group_by=category&limit=N
|
|
12
|
+
// → { events[{id,timestamp,category}] }
|
|
13
|
+
// GET /api/clusters → { clusters[{cluster_id,member_count,categories,summary}] }
|
|
14
|
+
// GET /api/clusters/{id}?limit=N → { members[], summary }
|
|
15
|
+
// TODO: /api/memories/{id}/score-breakdown — Semantic/BM25/Entity/Temporal not yet exposed.
|
|
16
|
+
(function () {
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
function _esc(s) {
|
|
22
|
+
if (typeof escapeHtml === 'function') return escapeHtml(s);
|
|
23
|
+
var d = document.createElement('div');
|
|
24
|
+
d.textContent = String(s == null ? '' : s);
|
|
25
|
+
return d.innerHTML;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function _fmt(iso) {
|
|
29
|
+
if (!iso) return '—';
|
|
30
|
+
var d = new Date(iso);
|
|
31
|
+
return isNaN(d.getTime()) ? String(iso) : d.toLocaleDateString();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _ago(iso) {
|
|
35
|
+
if (!iso) return '';
|
|
36
|
+
var s = (Date.now() - new Date(iso).getTime()) / 1000;
|
|
37
|
+
if (s < 0) s = 0;
|
|
38
|
+
if (s < 60) return Math.floor(s) + 's ago';
|
|
39
|
+
if (s < 3600) return Math.floor(s / 60) + 'm ago';
|
|
40
|
+
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
|
|
41
|
+
return Math.floor(s / 86400) + 'd ago';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// importance arrives as 0–1 float; display as X/10 integer
|
|
45
|
+
function _imp(raw) { return Math.round((parseFloat(raw) || 0) * 10); }
|
|
46
|
+
|
|
47
|
+
// importance (0–1) → badge class for Score column
|
|
48
|
+
function _scoreCls(raw) {
|
|
49
|
+
var v = parseFloat(raw) || 0;
|
|
50
|
+
return v >= 0.7 ? 'ok' : v >= 0.4 ? 'warn' : 'danger';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// API category → badge class
|
|
54
|
+
function _catCls(cat) {
|
|
55
|
+
var MAP = { semantic: 'violet', episodic: 'cyan', opinion: 'warn',
|
|
56
|
+
temporal: 'ok', consolidation: 'danger' };
|
|
57
|
+
return MAP[String(cat).toLowerCase()] || 'neutral';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Normalize /api/search result → memory shape (fact_id→id, score→importance)
|
|
61
|
+
function _normSearch(r) { return {id:r.fact_id||r.memory_id||'',content:r.content||'',category:r.category||'semantic',project_name:r.project_name||'',importance:r.score||r.confidence||0,created_at:r.created_at||'',access_count:r.access_count||0}; }
|
|
62
|
+
// shared_with is stored as a JSON array string; show it comma-separated.
|
|
63
|
+
function _sharedToStr(v) {
|
|
64
|
+
if (!v) return '';
|
|
65
|
+
if (Array.isArray(v)) return v.join(', ');
|
|
66
|
+
try { var a = JSON.parse(v); return Array.isArray(a) ? a.join(', ') : ''; }
|
|
67
|
+
catch (e) { return ''; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Write-auth token: reuses dashboard.js closure when available, else fails gracefully
|
|
71
|
+
function _getMutToken() { return typeof window.dashboardInstallToken==='function'?window.dashboardInstallToken():Promise.resolve(''); }
|
|
72
|
+
|
|
73
|
+
// ── CSS (injected once) ─────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
var _cssInjected = false;
|
|
76
|
+
function _injectCSS() {
|
|
77
|
+
if (_cssInjected) return;
|
|
78
|
+
_cssInjected = true;
|
|
79
|
+
var s = document.createElement('style');
|
|
80
|
+
s.dataset.odModule = 'memories';
|
|
81
|
+
s.textContent =
|
|
82
|
+
'.od-drawer{position:fixed;top:0;right:0;height:100vh;width:420px;max-width:92vw;' +
|
|
83
|
+
'background:var(--card);border-left:1px solid var(--border);box-shadow:var(--sh-lg);' +
|
|
84
|
+
'transform:translateX(100%);transition:transform .28s cubic-bezier(.22,1,.36,1);' +
|
|
85
|
+
'z-index:60;overflow-y:auto;padding:24px;}' +
|
|
86
|
+
'.od-drawer.open{transform:none;}' +
|
|
87
|
+
'.od-drawer-scrim{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:59;' +
|
|
88
|
+
'opacity:0;pointer-events:none;transition:opacity .25s;}' +
|
|
89
|
+
'.od-drawer-scrim.on{opacity:1;pointer-events:auto;}' +
|
|
90
|
+
'.od-prov{padding:14px;border-radius:var(--r-md);background:var(--card-2);' +
|
|
91
|
+
'border:1px solid var(--border);margin-top:12px;}';
|
|
92
|
+
document.head.appendChild(s);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Module state ────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
var _st = {
|
|
98
|
+
rootId: null,
|
|
99
|
+
activeTab: 'all',
|
|
100
|
+
category: null, // null = all; string = filtered category
|
|
101
|
+
sort: 'created',
|
|
102
|
+
page: 0,
|
|
103
|
+
pageSize: 50,
|
|
104
|
+
total: 0,
|
|
105
|
+
memories: [],
|
|
106
|
+
catCounts: {}, // pre-fetched per-category totals
|
|
107
|
+
tlRange: '30d',
|
|
108
|
+
cluLoaded: false, // reset in render() to avoid stale state on re-entry
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Known categories from the daemon (pre-fetched at render time)
|
|
112
|
+
var KNOWN_CATS = ['semantic', 'episodic', 'opinion', 'temporal', 'consolidation'];
|
|
113
|
+
|
|
114
|
+
// ── Main entry ──────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
function render(container) {
|
|
117
|
+
if (!container) return;
|
|
118
|
+
_injectCSS();
|
|
119
|
+
var id = 'od-mem-' + Math.random().toString(36).slice(2, 8);
|
|
120
|
+
_st = Object.assign({}, _st, {
|
|
121
|
+
rootId: id, page: 0, category: null, sort: 'created',
|
|
122
|
+
memories: [], catCounts: {}, cluLoaded: false,
|
|
123
|
+
});
|
|
124
|
+
// Clear per-id timeline cache for this render instance
|
|
125
|
+
_tlLoaded = {};
|
|
126
|
+
container.innerHTML = _scaffold(id);
|
|
127
|
+
_wire(container, id);
|
|
128
|
+
// Fire initial loads in parallel
|
|
129
|
+
_loadMem(id);
|
|
130
|
+
_loadCatCounts(id);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Scaffold HTML ────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
function _scaffold(id) {
|
|
136
|
+
return (
|
|
137
|
+
'<div id="' + id + '">' +
|
|
138
|
+
'<div class="page-head">' +
|
|
139
|
+
'<h2>Everything you\'ve remembered</h2>' +
|
|
140
|
+
'<p id="' + id + '-sub" style="color:var(--fg-2);margin-top:5px">Loading…</p>' +
|
|
141
|
+
'</div>' +
|
|
142
|
+
'<div class="tabs" id="' + id + '-tabs">' +
|
|
143
|
+
'<button class="tab active" data-od-act="tab" data-tab="all">' +
|
|
144
|
+
'All memories <span class="cnt" id="' + id + '-cnt-all">…</span></button>' +
|
|
145
|
+
'<button class="tab" data-od-act="tab" data-tab="timeline">Creation timeline</button>' +
|
|
146
|
+
'<button class="tab" data-od-act="tab" data-tab="clusters">' +
|
|
147
|
+
'Knowledge clusters <span class="cnt" id="' + id + '-cnt-clusters">…</span></button>' +
|
|
148
|
+
'</div>' +
|
|
149
|
+
'<div class="tabpane active" id="' + id + '-pane-all">' + _allScaffold(id) + '</div>' +
|
|
150
|
+
'<div class="tabpane" id="' + id + '-pane-timeline">' + _tlScaffold(id) + '</div>' +
|
|
151
|
+
'<div class="tabpane" id="' + id + '-pane-clusters">' +
|
|
152
|
+
'<div class="launch-grid" id="' + id + '-clu-grid">' +
|
|
153
|
+
_loading('Loading clusters…') +
|
|
154
|
+
'</div>' +
|
|
155
|
+
'</div>' +
|
|
156
|
+
'</div>'
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function _allScaffold(id) {
|
|
161
|
+
return (
|
|
162
|
+
'<div style="margin-bottom:16px;display:flex;gap:8px;align-items:center">' +
|
|
163
|
+
'<input data-od-act="search" placeholder="Search all memories…" autocomplete="off" ' +
|
|
164
|
+
'style="flex:1;padding:8px 12px;border:1px solid var(--border);' +
|
|
165
|
+
'border-radius:var(--r-md);background:var(--card-2);color:var(--fg);' +
|
|
166
|
+
'font-size:13.5px;outline:none">' +
|
|
167
|
+
'<select data-od-act="window" title="Limit search to a time window" ' +
|
|
168
|
+
'style="padding:8px 10px;border:1px solid var(--border);border-radius:var(--r-md);' +
|
|
169
|
+
'background:var(--card-2);color:var(--fg);font-size:13px;outline:none">' +
|
|
170
|
+
'<option value="">Any time</option>' +
|
|
171
|
+
'<option value="24h">Last 24h</option>' +
|
|
172
|
+
'<option value="7d">Last 7 days</option>' +
|
|
173
|
+
'<option value="30d">Last 30 days</option>' +
|
|
174
|
+
'<option value="90d">Last 90 days</option>' +
|
|
175
|
+
'<option value="1y">Last year</option>' +
|
|
176
|
+
'</select>' +
|
|
177
|
+
'</div>' +
|
|
178
|
+
// Filter bar: category chips (populated after cat-count fetch) + sort seg
|
|
179
|
+
'<div id="' + id + '-cats" ' +
|
|
180
|
+
'style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px;align-items:center">' +
|
|
181
|
+
'<div class="chip on" data-od-act="cat" data-cat="">All categories</div>' +
|
|
182
|
+
'<div style="flex:1"></div>' +
|
|
183
|
+
// Scope view: Mine (this profile) / Shared with me / Global / Everything
|
|
184
|
+
'<div class="seg" title="Which memories to show">' +
|
|
185
|
+
'<button class="active" data-od-act="scope-view" data-scope="mine">Mine</button>' +
|
|
186
|
+
'<button data-od-act="scope-view" data-scope="shared">Shared</button>' +
|
|
187
|
+
'<button data-od-act="scope-view" data-scope="global">Global</button>' +
|
|
188
|
+
'<button data-od-act="scope-view" data-scope="all">All</button>' +
|
|
189
|
+
'</div>' +
|
|
190
|
+
'<div class="seg">' +
|
|
191
|
+
'<button class="active" data-od-act="sort" data-sort="created">Newest</button>' +
|
|
192
|
+
'<button data-od-act="sort" data-sort="score">Score</button>' +
|
|
193
|
+
'<button data-od-act="sort" data-sort="importance">Importance</button>' +
|
|
194
|
+
'</div>' +
|
|
195
|
+
'</div>' +
|
|
196
|
+
'<div class="card" id="' + id + '-tbl-wrap">' + _loading('Loading memories…') + '</div>' +
|
|
197
|
+
'<div id="' + id + '-pg" style="margin-top:10px"></div>'
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function _tlScaffold(id) {
|
|
202
|
+
return (
|
|
203
|
+
'<div class="card">' +
|
|
204
|
+
'<div class="card-head">' +
|
|
205
|
+
'<h3>Memory creation timeline</h3>' +
|
|
206
|
+
'<span class="sub" id="' + id + '-tl-sub">facts stored per day</span>' +
|
|
207
|
+
'<div style="flex:1"></div>' +
|
|
208
|
+
'<div class="seg">' +
|
|
209
|
+
'<button data-od-act="tl-range" data-range="7d">7d</button>' +
|
|
210
|
+
'<button class="active" data-od-act="tl-range" data-range="30d">30d</button>' +
|
|
211
|
+
'</div>' +
|
|
212
|
+
'</div>' +
|
|
213
|
+
'<div class="card-pad">' +
|
|
214
|
+
'<div class="bars" id="' + id + '-bars" style="height:120px">' +
|
|
215
|
+
_loading('Loading timeline…') +
|
|
216
|
+
'</div>' +
|
|
217
|
+
'<div style="display:flex;justify-content:space-between;margin-top:10px;' +
|
|
218
|
+
'font-size:11.5px;color:var(--fg-3)">' +
|
|
219
|
+
'<span id="' + id + '-tl-start"></span><span>today</span>' +
|
|
220
|
+
'</div>' +
|
|
221
|
+
'</div>' +
|
|
222
|
+
'</div>' +
|
|
223
|
+
'<div class="card" style="margin-top:16px">' +
|
|
224
|
+
'<div class="card-head"><h3>By fact type</h3></div>' +
|
|
225
|
+
'<div class="card-pad" id="' + id + '-ftypes">' + _loading('Loading breakdown…') + '</div>' +
|
|
226
|
+
'</div>'
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function _loading(msg) {
|
|
231
|
+
return '<div style="padding:40px;text-align:center;color:var(--fg-2);font-size:13px">' +
|
|
232
|
+
_esc(msg) + '</div>';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Tab switching ────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
function _switchTab(id, tab) {
|
|
238
|
+
_st = Object.assign({}, _st, { activeTab: tab });
|
|
239
|
+
var root = document.getElementById(id);
|
|
240
|
+
if (!root) return;
|
|
241
|
+
root.querySelectorAll('#' + id + '-tabs .tab').forEach(function (b) {
|
|
242
|
+
b.classList.toggle('active', b.dataset.tab === tab);
|
|
243
|
+
});
|
|
244
|
+
root.querySelectorAll('.tabpane').forEach(function (p) { p.classList.remove('active'); });
|
|
245
|
+
var pane = document.getElementById(id + '-pane-' + tab);
|
|
246
|
+
if (pane) pane.classList.add('active');
|
|
247
|
+
if (tab === 'timeline') _loadTimeline(id);
|
|
248
|
+
if (tab === 'clusters') _loadClusters(id);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Category counts (pre-fetch real totals) ──────────────────────────────────
|
|
252
|
+
|
|
253
|
+
function _loadCatCounts(id) {
|
|
254
|
+
Promise.all(KNOWN_CATS.map(function (cat) {
|
|
255
|
+
return fetch('/api/memories?limit=1&category=' + encodeURIComponent(cat))
|
|
256
|
+
.then(function (r) { return r.json(); })
|
|
257
|
+
.then(function (d) { return { cat: cat, total: d.total || 0 }; })
|
|
258
|
+
.catch(function () { return { cat: cat, total: 0 }; });
|
|
259
|
+
})).then(function (results) {
|
|
260
|
+
var counts = {};
|
|
261
|
+
results.forEach(function (r) { if (r.total > 0) counts[r.cat] = r.total; });
|
|
262
|
+
_st = Object.assign({}, _st, { catCounts: counts });
|
|
263
|
+
_rebuildCatBar(id);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Rebuild the full filter bar (chips + sort) from current _st
|
|
268
|
+
function _rebuildCatBar(id) {
|
|
269
|
+
var bar = document.getElementById(id + '-cats');
|
|
270
|
+
if (!bar) return;
|
|
271
|
+
var active = _st.category || '';
|
|
272
|
+
var html = '<div class="chip' + (!active ? ' on' : '') +
|
|
273
|
+
'" data-od-act="cat" data-cat="">All categories</div>';
|
|
274
|
+
Object.keys(_st.catCounts).forEach(function (cat) {
|
|
275
|
+
html += '<div class="chip' + (_st.category === cat ? ' on' : '') +
|
|
276
|
+
'" data-od-act="cat" data-cat="' + _esc(cat) + '">' +
|
|
277
|
+
_esc(cat) + ' <span class="cnt">' +
|
|
278
|
+
_st.catCounts[cat].toLocaleString() + '</span></div>';
|
|
279
|
+
});
|
|
280
|
+
var sv = _st.scopeView || 'mine';
|
|
281
|
+
html += '<div style="flex:1"></div>' +
|
|
282
|
+
'<div class="seg" title="Which memories to show">' +
|
|
283
|
+
['mine', 'shared', 'global', 'all'].map(function (s) {
|
|
284
|
+
var label = { mine: 'Mine', shared: 'Shared', global: 'Global', all: 'All' }[s];
|
|
285
|
+
return '<button class="' + (sv === s ? 'active' : '') +
|
|
286
|
+
'" data-od-act="scope-view" data-scope="' + s + '">' + label + '</button>';
|
|
287
|
+
}).join('') +
|
|
288
|
+
'</div>' +
|
|
289
|
+
'<div class="seg">' +
|
|
290
|
+
'<button class="' + (_st.sort === 'created' ? 'active' : '') +
|
|
291
|
+
'" data-od-act="sort" data-sort="created">Newest</button>' +
|
|
292
|
+
'<button class="' + (_st.sort === 'score' ? 'active' : '') +
|
|
293
|
+
'" data-od-act="sort" data-sort="score">Score</button>' +
|
|
294
|
+
'<button class="' + (_st.sort === 'importance' ? 'active' : '') +
|
|
295
|
+
'" data-od-act="sort" data-sort="importance">Importance</button>' +
|
|
296
|
+
'</div>';
|
|
297
|
+
bar.innerHTML = html;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── Memories fetch & render ──────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
function _loadMem(id) {
|
|
303
|
+
var url = '/api/memories?limit=' + _st.pageSize +
|
|
304
|
+
'&offset=' + (_st.page * _st.pageSize);
|
|
305
|
+
if (_st.category) url += '&category=' + encodeURIComponent(_st.category);
|
|
306
|
+
if (_st.scopeView && _st.scopeView !== 'mine') {
|
|
307
|
+
url += '&scope=' + encodeURIComponent(_st.scopeView);
|
|
308
|
+
}
|
|
309
|
+
var wrap = document.getElementById(id + '-tbl-wrap');
|
|
310
|
+
if (wrap) wrap.innerHTML = _loading('Loading memories…');
|
|
311
|
+
|
|
312
|
+
fetch(url)
|
|
313
|
+
.then(function (r) {
|
|
314
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
315
|
+
return r.json();
|
|
316
|
+
})
|
|
317
|
+
.then(function (d) {
|
|
318
|
+
_st = Object.assign({}, _st, { memories: d.memories || [], total: d.total || 0 });
|
|
319
|
+
_renderTable(id, d);
|
|
320
|
+
})
|
|
321
|
+
.catch(function (err) {
|
|
322
|
+
var w = document.getElementById(id + '-tbl-wrap');
|
|
323
|
+
if (w) w.innerHTML = '<div class="card-pad" style="color:var(--danger);text-align:center">' +
|
|
324
|
+
'Failed to load memories: ' + _esc(err.message) + '</div>';
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function _renderTable(id, d) {
|
|
329
|
+
var mems = d.memories || [];
|
|
330
|
+
var total = d.total || 0;
|
|
331
|
+
var wrap = document.getElementById(id + '-tbl-wrap');
|
|
332
|
+
var cntEl = document.getElementById(id + '-cnt-all');
|
|
333
|
+
var subEl = document.getElementById(id + '-sub');
|
|
334
|
+
|
|
335
|
+
if (cntEl) cntEl.textContent = total.toLocaleString();
|
|
336
|
+
if (subEl) {
|
|
337
|
+
var _projs = mems.reduce(function (s, m) {
|
|
338
|
+
if (m.project_name) s.add(m.project_name);
|
|
339
|
+
return s;
|
|
340
|
+
}, new Set()).size;
|
|
341
|
+
var _pStr = _projs > 0
|
|
342
|
+
? ' across ' + _projs + ' project' + (_projs === 1 ? '' : 's')
|
|
343
|
+
: '';
|
|
344
|
+
subEl.textContent = total.toLocaleString() + ' memories' + _pStr +
|
|
345
|
+
'. Every memory shows its provenance — why it was remembered and how strongly it scored.';
|
|
346
|
+
}
|
|
347
|
+
if (!wrap) return;
|
|
348
|
+
|
|
349
|
+
if (mems.length === 0) {
|
|
350
|
+
wrap.innerHTML = '<div class="card-pad" style="text-align:center;padding:40px;' +
|
|
351
|
+
'color:var(--fg-2)">No memories found. Try a different filter.</div>';
|
|
352
|
+
_renderPag(id, d);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
var rows = mems.map(function (m, idx) {
|
|
357
|
+
var imp = _imp(m.importance);
|
|
358
|
+
var impCls = imp >= 8 ? 'ok' : imp >= 5 ? 'warn' : 'neutral';
|
|
359
|
+
var scorePct = Math.round((parseFloat(m.importance) || 0) * 100);
|
|
360
|
+
var scoreCls = _scoreCls(m.importance);
|
|
361
|
+
var cat = m.category || 'semantic';
|
|
362
|
+
var preview = (m.content || '').substring(0, 120);
|
|
363
|
+
if ((m.content || '').length > 120) preview += '…';
|
|
364
|
+
|
|
365
|
+
return '<tr class="row" data-od-act="drawer" data-idx="' + idx + '" style="cursor:pointer">' +
|
|
366
|
+
'<td style="max-width:420px">' +
|
|
367
|
+
'<b class="mono dim" style="font-size:11px">#' +
|
|
368
|
+
_esc((m.id || '').substring(0, 8)) +
|
|
369
|
+
'</b>' +
|
|
370
|
+
'<div style="margin-top:2px">' + _esc(preview) + '</div>' +
|
|
371
|
+
'</td>' +
|
|
372
|
+
'<td><span class="badge ' + _esc(_catCls(cat)) + '">' + _esc(cat) + '</span></td>' +
|
|
373
|
+
'<td class="mono dim" style="font-size:12px">' + _esc(m.project_name || '—') + '</td>' +
|
|
374
|
+
'<td><b class="num">' + imp + '</b>/10</td>' +
|
|
375
|
+
'<td><span class="badge ' + _esc(scoreCls) + '">' + scorePct + '%</span></td>' +
|
|
376
|
+
'<td class="mono dim" style="font-size:12px">' + _esc(_fmt(m.created_at)) + '</td>' +
|
|
377
|
+
'</tr>';
|
|
378
|
+
}).join('');
|
|
379
|
+
|
|
380
|
+
wrap.innerHTML =
|
|
381
|
+
'<table class="tbl">' +
|
|
382
|
+
'<thead><tr>' +
|
|
383
|
+
'<th>Memory</th><th>Category</th><th>Project</th>' +
|
|
384
|
+
'<th>Importance</th><th>Score</th><th>Created</th>' +
|
|
385
|
+
'</tr></thead>' +
|
|
386
|
+
'<tbody>' + rows + '</tbody>' +
|
|
387
|
+
'</table>';
|
|
388
|
+
|
|
389
|
+
_renderPag(id, d);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function _renderPag(id, d) {
|
|
393
|
+
var el = document.getElementById(id + '-pg');
|
|
394
|
+
if (!el) return;
|
|
395
|
+
var tot = d.total || 0;
|
|
396
|
+
var lim = d.limit || _st.pageSize;
|
|
397
|
+
var off = d.offset || (_st.page * _st.pageSize);
|
|
398
|
+
var pg = Math.floor(off / lim);
|
|
399
|
+
var last = Math.max(0, Math.ceil(tot / lim) - 1);
|
|
400
|
+
var show = Math.min(off + lim, tot);
|
|
401
|
+
var from = tot === 0 ? 0 : off + 1;
|
|
402
|
+
|
|
403
|
+
el.innerHTML =
|
|
404
|
+
'<div style="display:flex;justify-content:space-between;align-items:center;' +
|
|
405
|
+
'font-size:12.5px;color:var(--fg-2)">' +
|
|
406
|
+
'<span>Showing ' + from + '–' + show + ' of ' + tot.toLocaleString() + '</span>' +
|
|
407
|
+
'<div class="seg">' +
|
|
408
|
+
'<button ' + (pg <= 0 ? 'disabled style="opacity:.4"' :
|
|
409
|
+
'data-od-act="pg" data-page="' + (pg - 1) + '"') + '>← Prev</button>' +
|
|
410
|
+
'<button disabled style="opacity:.7;cursor:default">Page ' +
|
|
411
|
+
(pg + 1) + ' / ' + (last + 1) + '</button>' +
|
|
412
|
+
'<button ' + (pg >= last ? 'disabled style="opacity:.4"' :
|
|
413
|
+
'data-od-act="pg" data-page="' + (pg + 1) + '"') + '>Next →</button>' +
|
|
414
|
+
'</div>' +
|
|
415
|
+
'</div>';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ── Sort (client-side on current page) ──────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
function _doSort(id, by) {
|
|
421
|
+
_st = Object.assign({}, _st, { sort: by });
|
|
422
|
+
var mems = _st.memories.slice();
|
|
423
|
+
// 'score' and 'importance' both rank by the same underlying importance float
|
|
424
|
+
if (by === 'importance' || by === 'score') {
|
|
425
|
+
mems.sort(function (a, b) {
|
|
426
|
+
return (parseFloat(b.importance) || 0) - (parseFloat(a.importance) || 0);
|
|
427
|
+
});
|
|
428
|
+
} else {
|
|
429
|
+
mems.sort(function (a, b) {
|
|
430
|
+
return String(b.created_at || '').localeCompare(String(a.created_at || ''));
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
_st = Object.assign({}, _st, { memories: mems });
|
|
434
|
+
_renderTable(id, {
|
|
435
|
+
memories: mems, total: _st.total,
|
|
436
|
+
limit: _st.pageSize, offset: _st.page * _st.pageSize,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── Search — POST /api/search (full corpus, 0.5–1.4 s warm) ─────────────────
|
|
441
|
+
|
|
442
|
+
var _searchTimer = null;
|
|
443
|
+
function _doSearch(id, q) {
|
|
444
|
+
clearTimeout(_searchTimer);
|
|
445
|
+
_searchTimer = setTimeout(function () {
|
|
446
|
+
_st = Object.assign({}, _st, { searchQ: q || null });
|
|
447
|
+
if (!q) { _loadMem(id); return; }
|
|
448
|
+
var wrap = document.getElementById(id + '-tbl-wrap');
|
|
449
|
+
if (wrap) wrap.innerHTML = _loading('Searching…');
|
|
450
|
+
fetch('/api/search', { method: 'POST', credentials: 'same-origin',
|
|
451
|
+
headers: { 'Content-Type': 'application/json' },
|
|
452
|
+
body: JSON.stringify({ query: q, limit: 50, window: _st.window || '' })
|
|
453
|
+
}).then(function (r) {
|
|
454
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
455
|
+
return r.json();
|
|
456
|
+
}).then(function (d) {
|
|
457
|
+
var mems = (d.results || []).map(_normSearch);
|
|
458
|
+
_st = Object.assign({}, _st, { memories: mems, total: mems.length });
|
|
459
|
+
_renderTable(id, { memories: mems, total: mems.length, limit: mems.length, offset: 0 });
|
|
460
|
+
}).catch(function (err) {
|
|
461
|
+
var w = document.getElementById(id + '-tbl-wrap');
|
|
462
|
+
if (w) w.innerHTML = '<div class="card-pad" style="color:var(--danger);text-align:center;' +
|
|
463
|
+
'padding:24px">Search failed: ' + _esc(err.message) + '</div>';
|
|
464
|
+
});
|
|
465
|
+
}, 350);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── Timeline fetch & render ──────────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
var _tlLoaded = {};
|
|
471
|
+
function _loadTimeline(id) {
|
|
472
|
+
var range = _st.tlRange;
|
|
473
|
+
if (_tlLoaded[id + '-' + range]) return;
|
|
474
|
+
var barsEl = document.getElementById(id + '-bars');
|
|
475
|
+
var ftEl = document.getElementById(id + '-ftypes');
|
|
476
|
+
var startEl = document.getElementById(id + '-tl-start');
|
|
477
|
+
|
|
478
|
+
fetch('/api/v3/timeline/?range=' + range + '&group_by=category&limit=1000')
|
|
479
|
+
.then(function (r) { return r.json(); })
|
|
480
|
+
.then(function (d) {
|
|
481
|
+
var events = d.events || [];
|
|
482
|
+
_tlLoaded[id + '-' + range] = true;
|
|
483
|
+
if (events.length === 0) {
|
|
484
|
+
if (barsEl) barsEl.innerHTML =
|
|
485
|
+
'<div style="display:grid;place-items:center;width:100%;height:100%;' +
|
|
486
|
+
'color:var(--fg-3);font-size:13px">No memory events in this range.</div>';
|
|
487
|
+
if (ftEl) ftEl.innerHTML =
|
|
488
|
+
'<div style="color:var(--fg-3);font-size:13px">No category data available.</div>';
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
_renderBars(id, events, range, barsEl, startEl);
|
|
492
|
+
_renderFtypes(id, events, ftEl);
|
|
493
|
+
})
|
|
494
|
+
.catch(function (err) {
|
|
495
|
+
if (barsEl) barsEl.innerHTML =
|
|
496
|
+
'<div style="color:var(--fg-3);font-size:13px;text-align:center;padding:16px">' +
|
|
497
|
+
'Timeline unavailable: ' + _esc(err.message) + '</div>';
|
|
498
|
+
// TODO: endpoint returns 404 when timeline feature is disabled
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function _renderBars(id, events, range, barsEl, startEl) {
|
|
503
|
+
if (!barsEl) return;
|
|
504
|
+
var days = range === '30d' ? 30 : 7;
|
|
505
|
+
var today = new Date();
|
|
506
|
+
var counts = new Array(days).fill(0);
|
|
507
|
+
events.forEach(function (ev) {
|
|
508
|
+
var diff = Math.floor((today - new Date(ev.timestamp)) / 86400000);
|
|
509
|
+
if (diff >= 0 && diff < days) counts[days - 1 - diff]++;
|
|
510
|
+
});
|
|
511
|
+
if (typeof slmBars === 'function') {
|
|
512
|
+
slmBars(barsEl, counts);
|
|
513
|
+
} else {
|
|
514
|
+
var maxVal = Math.max.apply(null, counts) || 1;
|
|
515
|
+
barsEl.innerHTML = counts.map(function (v) {
|
|
516
|
+
return '<i style="height:' + Math.max(4, (v / maxVal) * 100) + '%' +
|
|
517
|
+
'" title="' + v + ' events"></i>';
|
|
518
|
+
}).join('');
|
|
519
|
+
}
|
|
520
|
+
if (startEl) startEl.textContent = days + 'd ago';
|
|
521
|
+
var subEl = document.getElementById(id + '-tl-sub');
|
|
522
|
+
if (subEl) subEl.textContent = events.length.toLocaleString() + ' events · last ' + days + ' days';
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function _renderFtypes(id, events, ftEl) {
|
|
526
|
+
if (!ftEl) return;
|
|
527
|
+
var catMap = {};
|
|
528
|
+
events.forEach(function (ev) {
|
|
529
|
+
catMap[ev.category || 'unknown'] = (catMap[ev.category || 'unknown'] || 0) + 1;
|
|
530
|
+
});
|
|
531
|
+
var total = events.length;
|
|
532
|
+
var colors = { temporal:'var(--ok)', semantic:'var(--violet)', episodic:'var(--cyan)', opinion:'var(--warn)' };
|
|
533
|
+
var sorted = Object.keys(catMap).sort(function (a, b) { return catMap[b] - catMap[a]; });
|
|
534
|
+
ftEl.innerHTML = sorted.map(function (cat) {
|
|
535
|
+
var pct = Math.round(catMap[cat] / total * 100);
|
|
536
|
+
var color = colors[cat] || 'var(--fg-3)';
|
|
537
|
+
return '<div style="margin-bottom:13px">' +
|
|
538
|
+
'<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px">' +
|
|
539
|
+
'<span class="mono">' + _esc(cat) + '</span><b>' + pct + '%</b>' +
|
|
540
|
+
'</div>' +
|
|
541
|
+
'<div class="meter"><i style="width:' + pct + '%;background:' + color + '"></i></div>' +
|
|
542
|
+
'</div>';
|
|
543
|
+
}).join('');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ── Clusters fetch & render ──────────────────────────────────────────────────
|
|
547
|
+
|
|
548
|
+
function _loadClusters(id) {
|
|
549
|
+
if (_st.cluLoaded) return;
|
|
550
|
+
var grid = document.getElementById(id + '-clu-grid');
|
|
551
|
+
if (!grid) return;
|
|
552
|
+
fetch('/api/clusters')
|
|
553
|
+
.then(function (r) {
|
|
554
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
555
|
+
return r.json();
|
|
556
|
+
})
|
|
557
|
+
.then(function (d) {
|
|
558
|
+
_st = Object.assign({}, _st, { cluLoaded: true });
|
|
559
|
+
var clus = d.clusters || [];
|
|
560
|
+
var cntEl = document.getElementById(id + '-cnt-clusters');
|
|
561
|
+
if (cntEl) cntEl.textContent = clus.length.toLocaleString();
|
|
562
|
+
if (clus.length === 0) {
|
|
563
|
+
grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:40px;' +
|
|
564
|
+
'color:var(--fg-2)">No clusters yet. Clusters form as memories accumulate.</div>';
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
_renderClusters(id, clus, grid);
|
|
568
|
+
})
|
|
569
|
+
.catch(function (err) {
|
|
570
|
+
if (grid) grid.innerHTML =
|
|
571
|
+
'<div style="grid-column:1/-1;text-align:center;padding:24px;' +
|
|
572
|
+
'color:var(--danger);font-size:13px">Failed to load clusters: ' +
|
|
573
|
+
_esc(err.message) + '</div>';
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
var _PAL = ['var(--violet)', 'var(--cyan)', 'var(--warn)', 'var(--ok)', 'var(--danger)'];
|
|
578
|
+
|
|
579
|
+
function _renderClusters(id, clus, grid) {
|
|
580
|
+
grid.innerHTML = clus.map(function (c, i) {
|
|
581
|
+
var color = _PAL[i % _PAL.length];
|
|
582
|
+
var summary = c.summary || c.categories || '';
|
|
583
|
+
var preview = summary.length > 80 ? summary.substring(0, 80) + '…' : summary;
|
|
584
|
+
var cid = String(c.cluster_id || '');
|
|
585
|
+
return '<div class="launch-card card" style="cursor:pointer"' +
|
|
586
|
+
' data-od-act="expand-cluster" data-cid="' + _esc(cid) + '">' +
|
|
587
|
+
'<div class="ic" style="background:color-mix(in srgb,' + color + ' 15%,transparent);color:' + color + '">' +
|
|
588
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="21" height="21">' +
|
|
589
|
+
'<path d="M12 3l1.9 4.6L18 9l-3.5 3 1 5-3.5-2.5L8.5 17l1-5L6 9l4.1-1.4z"/>' +
|
|
590
|
+
'</svg>' +
|
|
591
|
+
'</div>' +
|
|
592
|
+
'<h3>' + _esc(preview) + '</h3>' +
|
|
593
|
+
'<p><b class="num">' + _esc(String(c.member_count)) + '</b> memories · expand →</p>' +
|
|
594
|
+
'<div style="display:none;margin-top:14px;border-top:1px solid var(--border);' +
|
|
595
|
+
'padding-top:12px;font-size:13px" id="' + id + '-clu-' + _esc(cid) + '"></div>' +
|
|
596
|
+
'</div>';
|
|
597
|
+
}).join('');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function _expandCluster(id, cid) {
|
|
601
|
+
var detEl = document.getElementById(id + '-clu-' + cid);
|
|
602
|
+
if (!detEl) return;
|
|
603
|
+
if (detEl.style.display !== 'none') { detEl.style.display = 'none'; return; }
|
|
604
|
+
detEl.style.display = 'block';
|
|
605
|
+
if (detEl.dataset.loaded) return;
|
|
606
|
+
detEl.dataset.loaded = '1';
|
|
607
|
+
detEl.textContent = 'Loading members…';
|
|
608
|
+
fetch('/api/clusters/' + encodeURIComponent(cid) + '?limit=5')
|
|
609
|
+
.then(function (r) { return r.json(); })
|
|
610
|
+
.then(function (d) {
|
|
611
|
+
var members = d.members || [];
|
|
612
|
+
if (members.length === 0) { detEl.textContent = 'No members found.'; return; }
|
|
613
|
+
detEl.innerHTML = members.map(function (m, i) {
|
|
614
|
+
var txt = m.content || m.summary || '';
|
|
615
|
+
return '<div style="margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid var(--border)">' +
|
|
616
|
+
'<div style="font-size:12.5px">' + (i + 1) + '. ' +
|
|
617
|
+
_esc(txt.substring(0, 120)) + '</div></div>';
|
|
618
|
+
}).join('');
|
|
619
|
+
})
|
|
620
|
+
.catch(function () { detEl.textContent = 'Failed to load members.'; });
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ── Provenance drawer ────────────────────────────────────────────────────────
|
|
624
|
+
|
|
625
|
+
function _ensureDrawer() {
|
|
626
|
+
if (document.getElementById('od-mem-drawer')) return;
|
|
627
|
+
var scrim = document.createElement('div');
|
|
628
|
+
scrim.id = 'od-mem-drawer-scrim';
|
|
629
|
+
scrim.className = 'od-drawer-scrim';
|
|
630
|
+
scrim.dataset.odAct = 'close-drawer';
|
|
631
|
+
document.body.appendChild(scrim);
|
|
632
|
+
var drawer = document.createElement('aside');
|
|
633
|
+
drawer.id = 'od-mem-drawer';
|
|
634
|
+
drawer.className = 'od-drawer';
|
|
635
|
+
document.body.appendChild(drawer);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function _openDrawer(idx) {
|
|
639
|
+
var mem = _st.memories[idx];
|
|
640
|
+
if (!mem) return;
|
|
641
|
+
_ensureDrawer();
|
|
642
|
+
var drawer = document.getElementById('od-mem-drawer');
|
|
643
|
+
var scrim = document.getElementById('od-mem-drawer-scrim');
|
|
644
|
+
if (!drawer || !scrim) return;
|
|
645
|
+
|
|
646
|
+
var imp = _imp(mem.importance);
|
|
647
|
+
var cat = mem.category || 'semantic';
|
|
648
|
+
var scorePct = Math.round((parseFloat(mem.importance) || 0) * 100);
|
|
649
|
+
|
|
650
|
+
drawer.innerHTML =
|
|
651
|
+
'<div style="display:flex;justify-content:space-between;align-items:flex-start">' +
|
|
652
|
+
'<div>' +
|
|
653
|
+
'<div style="font-size:12px;color:var(--fg-2)">Memory</div>' +
|
|
654
|
+
'<h2 style="font-size:19px;margin-top:2px">' +
|
|
655
|
+
_esc('#' + (mem.id || '').substring(0, 12)) + '</h2>' +
|
|
656
|
+
'</div>' +
|
|
657
|
+
'<button class="btn icon ghost" data-od-act="close-drawer">✕</button>' +
|
|
658
|
+
'</div>' +
|
|
659
|
+
'<div class="od-prov" style="font-size:14px;line-height:1.7">' +
|
|
660
|
+
_esc(mem.content || '') +
|
|
661
|
+
'</div>' +
|
|
662
|
+
'<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">' +
|
|
663
|
+
'<span class="badge ' + _esc(_catCls(cat)) + '">' + _esc(cat) + '</span>' +
|
|
664
|
+
'<span class="badge neutral">' + _esc(mem.project_name || 'no project') + '</span>' +
|
|
665
|
+
'<span class="badge ' + (mem.scope === 'global' ? 'success' : mem.scope === 'shared' ? 'warn' : 'neutral') +
|
|
666
|
+
'">' + _esc(mem.scope || 'personal') + '</span>' +
|
|
667
|
+
'</div>' +
|
|
668
|
+
// Scope & sharing control (C2) — set personal/shared/global from the UI
|
|
669
|
+
'<h3 style="font-size:13px;margin:20px 0 8px">Scope & sharing</h3>' +
|
|
670
|
+
'<div class="od-prov" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">' +
|
|
671
|
+
'<select id="od-scope-sel" style="padding:6px 8px;border-radius:6px;background:var(--card-2);color:var(--fg);border:1px solid var(--border);font-size:13px">' +
|
|
672
|
+
['personal', 'shared', 'global'].map(function (s) {
|
|
673
|
+
return '<option value="' + s + '"' + ((mem.scope || 'personal') === s ? ' selected' : '') + '>' + s + '</option>';
|
|
674
|
+
}).join('') +
|
|
675
|
+
'</select>' +
|
|
676
|
+
'<input id="od-scope-shared" placeholder="share with profiles (comma-sep)" ' +
|
|
677
|
+
'value="' + _esc(_sharedToStr(mem.shared_with)) + '" ' +
|
|
678
|
+
'style="flex:1;min-width:140px;padding:6px 8px;border-radius:6px;background:var(--card-2);color:var(--fg);border:1px solid var(--border);font-size:13px">' +
|
|
679
|
+
'<button data-od-act="set-scope" data-mid="' + _esc(mem.id) + '" ' +
|
|
680
|
+
'style="padding:6px 14px;border:1px solid var(--border);border-radius:6px;background:var(--card-2);color:var(--fg);cursor:pointer;font-size:13px">Apply</button>' +
|
|
681
|
+
'</div>' +
|
|
682
|
+
'<h3 style="font-size:13px;margin:20px 0 8px">Why this was remembered</h3>' +
|
|
683
|
+
'<div class="od-prov">' +
|
|
684
|
+
'<div style="font-size:13px;color:var(--fg-2);line-height:1.6">' +
|
|
685
|
+
'Importance: <b>' + imp + '/10</b>. ' +
|
|
686
|
+
'Score: <b>' + scorePct + '%</b>. ' +
|
|
687
|
+
'Recalled <b>' + _esc(String(mem.access_count || 0)) + '</b> time' +
|
|
688
|
+
(mem.access_count === 1 ? '' : 's') + '. ' +
|
|
689
|
+
'Stored ' + _esc(_ago(mem.created_at)) + '.' +
|
|
690
|
+
'</div>' +
|
|
691
|
+
// TODO: /api/memories/{id}/score-breakdown — Semantic/BM25/EntityGraph/Temporal
|
|
692
|
+
// decomposition is not yet exposed by the daemon; show when available.
|
|
693
|
+
'</div>' +
|
|
694
|
+
'<h3 style="font-size:13px;margin:20px 0 8px">Atomic facts</h3>' +
|
|
695
|
+
'<div id="od-drawer-facts" style="color:var(--fg-3);font-size:13px">Loading facts…</div>' +
|
|
696
|
+
'<div style="display:flex;gap:8px;margin-top:20px;padding-top:12px;border-top:1px solid var(--border)">' +
|
|
697
|
+
'<button data-od-act="edit-mem" data-mid="' + _esc(mem.id) + '" style="padding:6px 14px;border:1px solid var(--border);border-radius:6px;background:var(--card-2);color:var(--fg);cursor:pointer;font-size:13px">Edit</button>' +
|
|
698
|
+
'<button data-od-act="del-mem" data-mid="' + _esc(mem.id) + '" style="padding:6px 14px;border:1px solid var(--danger);border-radius:6px;background:transparent;color:var(--danger);cursor:pointer;font-size:13px">Delete</button>' +
|
|
699
|
+
'</div><div id="od-mem-act" style="margin-top:10px"></div>';
|
|
700
|
+
|
|
701
|
+
drawer.classList.add('open');
|
|
702
|
+
scrim.classList.add('on');
|
|
703
|
+
if (mem.id) _loadFacts(mem.id);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function _closeDrawer() {
|
|
707
|
+
var d = document.getElementById('od-mem-drawer');
|
|
708
|
+
var s = document.getElementById('od-mem-drawer-scrim');
|
|
709
|
+
if (d) d.classList.remove('open');
|
|
710
|
+
if (s) s.classList.remove('on');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function _loadFacts(memId) {
|
|
714
|
+
var el = document.getElementById('od-drawer-facts');
|
|
715
|
+
if (!el) return;
|
|
716
|
+
fetch('/api/memories/' + encodeURIComponent(memId) + '/facts')
|
|
717
|
+
.then(function (r) { return r.json(); })
|
|
718
|
+
.then(function (d) {
|
|
719
|
+
var facts = d.facts || [];
|
|
720
|
+
if (facts.length === 0) {
|
|
721
|
+
el.textContent = 'No atomic facts recorded for this memory.'; return;
|
|
722
|
+
}
|
|
723
|
+
el.innerHTML = facts.map(function (f) {
|
|
724
|
+
return '<div class="od-prov" style="margin-bottom:8px">' +
|
|
725
|
+
'<span class="badge neutral" style="font-size:10px">' +
|
|
726
|
+
_esc(f.fact_type || 'fact') +
|
|
727
|
+
'</span>' +
|
|
728
|
+
'<div style="font-size:13px;line-height:1.6;margin-top:4px">' +
|
|
729
|
+
_esc((f.content || '').substring(0, 200)) +
|
|
730
|
+
'</div></div>';
|
|
731
|
+
}).join('');
|
|
732
|
+
})
|
|
733
|
+
.catch(function () { if (el) el.textContent = 'Could not load facts.'; });
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function _startEdit(mid) { // show inline edit form in drawer
|
|
737
|
+
var m = _st.memories.filter(function (x) { return x.id === mid; })[0];
|
|
738
|
+
var el = document.getElementById('od-mem-act'); if (!el || !m) return;
|
|
739
|
+
el.innerHTML = '<textarea id="od-ea" rows="4" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:6px;background:var(--card-2);color:var(--fg);font-size:13px">' + _esc(m.content) + '</textarea><button data-od-act="save-edit" data-mid="' + _esc(mid) + '" style="display:block;margin-top:6px;padding:6px 14px;border:none;border-radius:6px;background:var(--violet);color:#fff;cursor:pointer">Save</button>';
|
|
740
|
+
}
|
|
741
|
+
function _startDel(mid) { // show delete confirmation in drawer
|
|
742
|
+
var el = document.getElementById('od-mem-act'); if (!el) return;
|
|
743
|
+
el.innerHTML = '<p style="font-size:13px;color:var(--fg-2);margin-bottom:10px">Permanently delete this memory? This cannot be undone.</p><button data-od-act="confirm-del" data-mid="' + _esc(mid) + '" style="padding:6px 14px;border-radius:6px;background:var(--danger);color:#fff;border:none;cursor:pointer;margin-right:8px">Confirm delete</button><button data-od-act="cancel-act" style="padding:6px 14px;border-radius:6px;background:var(--card-2);border:1px solid var(--border);color:var(--fg);cursor:pointer">Cancel</button>';
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// ── Event delegation ─────────────────────────────────────────────────────────
|
|
747
|
+
function _wire(container, id) {
|
|
748
|
+
container.addEventListener('click', function (e) {
|
|
749
|
+
var el = e.target.closest('[data-od-act]');
|
|
750
|
+
if (!el) return;
|
|
751
|
+
var act = el.dataset.odAct;
|
|
752
|
+
|
|
753
|
+
if (act === 'tab') {
|
|
754
|
+
_switchTab(id, el.dataset.tab);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (act === 'cat') {
|
|
758
|
+
// Toggle on/off chip states without a full re-render
|
|
759
|
+
var bar = document.getElementById(id + '-cats');
|
|
760
|
+
if (bar) bar.querySelectorAll('[data-od-act="cat"]').forEach(function (c) {
|
|
761
|
+
c.classList.toggle('on', c.dataset.cat === el.dataset.cat);
|
|
762
|
+
});
|
|
763
|
+
_st = Object.assign({}, _st, { category: el.dataset.cat || null, page: 0 });
|
|
764
|
+
_loadMem(id);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (act === 'sort') {
|
|
768
|
+
// Update active state on sort buttons in-place
|
|
769
|
+
var root = document.getElementById(id);
|
|
770
|
+
if (root) root.querySelectorAll('[data-od-act="sort"]').forEach(function (b) {
|
|
771
|
+
b.classList.toggle('active', b.dataset.sort === el.dataset.sort);
|
|
772
|
+
});
|
|
773
|
+
_doSort(id, el.dataset.sort);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (act === 'scope-view') {
|
|
777
|
+
var sroot = document.getElementById(id);
|
|
778
|
+
if (sroot) sroot.querySelectorAll('[data-od-act="scope-view"]').forEach(function (b) {
|
|
779
|
+
b.classList.toggle('active', b.dataset.scope === el.dataset.scope);
|
|
780
|
+
});
|
|
781
|
+
_st = Object.assign({}, _st, { scopeView: el.dataset.scope, page: 0 });
|
|
782
|
+
_loadMem(id);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (act === 'drawer') {
|
|
786
|
+
var row = el.closest('tr[data-od-act]');
|
|
787
|
+
var idx = parseInt((row || el).dataset.idx, 10);
|
|
788
|
+
if (!isNaN(idx)) _openDrawer(idx);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (act === 'close-drawer') { _closeDrawer(); return; }
|
|
792
|
+
if (act === 'pg') {
|
|
793
|
+
var pg = parseInt(el.dataset.page, 10);
|
|
794
|
+
if (!isNaN(pg)) { _st = Object.assign({}, _st, { page: pg }); _loadMem(id); }
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (act === 'expand-cluster') { _expandCluster(id, el.dataset.cid); return; }
|
|
798
|
+
if (act === 'tl-range') {
|
|
799
|
+
var root2 = document.getElementById(id);
|
|
800
|
+
if (root2) root2.querySelectorAll('[data-od-act="tl-range"]').forEach(function (b) {
|
|
801
|
+
b.classList.toggle('active', b.dataset.range === el.dataset.range);
|
|
802
|
+
});
|
|
803
|
+
_tlLoaded[id + '-' + _st.tlRange] = false; // invalidate cache
|
|
804
|
+
_st = Object.assign({}, _st, { tlRange: el.dataset.range });
|
|
805
|
+
_loadTimeline(id);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// Search — input event
|
|
811
|
+
container.addEventListener('change', function (e) {
|
|
812
|
+
if (e.target.dataset.odAct === 'window') {
|
|
813
|
+
_st = Object.assign({}, _st, { window: e.target.value || '' });
|
|
814
|
+
var wq = (_st.searchQ || '').trim();
|
|
815
|
+
if (wq) _doSearch(id, wq); // re-run active search with the new window
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
container.addEventListener('input', function (e) {
|
|
819
|
+
if (e.target.dataset.odAct === 'search') _doSearch(id, e.target.value.trim());
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// Drawer actions — document-level (drawer is outside container), guarded once
|
|
823
|
+
if (!window._slmMemDrawerWired) {
|
|
824
|
+
window._slmMemDrawerWired = true;
|
|
825
|
+
document.addEventListener('click', function (e) {
|
|
826
|
+
var el = e.target.closest('[data-od-act]');
|
|
827
|
+
if (!el) return;
|
|
828
|
+
var act = el.dataset.odAct;
|
|
829
|
+
if (act === 'close-drawer') { _closeDrawer(); return; }
|
|
830
|
+
if (act === 'edit-mem') { _startEdit(el.dataset.mid); return; }
|
|
831
|
+
if (act === 'del-mem') { _startDel(el.dataset.mid); return; }
|
|
832
|
+
if (act === 'cancel-act') { var ac = document.getElementById('od-mem-act'); if (ac) ac.innerHTML = ''; return; }
|
|
833
|
+
if (act === 'set-scope') {
|
|
834
|
+
var smid = el.dataset.mid;
|
|
835
|
+
var scopeVal = (document.getElementById('od-scope-sel') || {}).value || 'personal';
|
|
836
|
+
var sharedVal = ((document.getElementById('od-scope-shared') || {}).value || '').trim();
|
|
837
|
+
if (scopeVal === 'shared' && !sharedVal) {
|
|
838
|
+
showToast('Enter at least one profile to share with'); return;
|
|
839
|
+
}
|
|
840
|
+
_getMutToken().then(function (tok) {
|
|
841
|
+
if (!tok) { showToast('Auth unavailable — reload the page'); return; }
|
|
842
|
+
fetch('/api/memories/' + encodeURIComponent(smid) + '/scope', {
|
|
843
|
+
method: 'PATCH', credentials: 'same-origin',
|
|
844
|
+
headers: { 'Content-Type': 'application/json', 'X-Install-Token': tok },
|
|
845
|
+
body: JSON.stringify({ scope: scopeVal, shared_with: sharedVal }),
|
|
846
|
+
})
|
|
847
|
+
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
|
848
|
+
.then(function () { showToast('Scope updated → ' + scopeVal); _closeDrawer(); _loadMem(_st.rootId); })
|
|
849
|
+
.catch(function (e) { showToast('Scope update failed: ' + e.message); });
|
|
850
|
+
});
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (act === 'save-edit' || act === 'confirm-del') {
|
|
854
|
+
var rid = _st.rootId, isDel = act === 'confirm-del', mid = el.dataset.mid;
|
|
855
|
+
var content = isDel ? '' : ((document.getElementById('od-ea') || {}).value || '').trim();
|
|
856
|
+
if (!isDel && !content) { showToast('Content cannot be empty'); return; }
|
|
857
|
+
_getMutToken().then(function (tok) {
|
|
858
|
+
if (!tok) { showToast('Auth unavailable — reload the page'); return; }
|
|
859
|
+
var opts = isDel
|
|
860
|
+
? {method:'DELETE',credentials:'same-origin',headers:{'X-Install-Token':tok}}
|
|
861
|
+
: {method:'PATCH',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Install-Token':tok},body:JSON.stringify({content:content})};
|
|
862
|
+
fetch('/api/memories/' + encodeURIComponent(mid), opts)
|
|
863
|
+
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
|
864
|
+
.then(function () { showToast(isDel ? 'Memory deleted' : 'Memory updated'); _closeDrawer(); _loadMem(rid); })
|
|
865
|
+
.catch(function (e) { showToast((isDel ? 'Delete' : 'Edit') + ' failed: ' + e.message); });
|
|
866
|
+
});
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// ── Public API + auto-init ───────────────────────────────────────────────────
|
|
874
|
+
|
|
875
|
+
window.odRenderMemories = render;
|
|
876
|
+
|
|
877
|
+
if (document.readyState === 'loading') {
|
|
878
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
879
|
+
var p = document.getElementById('memories-pane');
|
|
880
|
+
if (p) render(p);
|
|
881
|
+
});
|
|
882
|
+
} else {
|
|
883
|
+
var _p = document.getElementById('memories-pane');
|
|
884
|
+
if (_p) render(_p);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
}());
|