superlocalmemory 3.0.16 → 3.0.17
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/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/cli/commands.py +29 -0
- package/src/superlocalmemory/cli/main.py +84 -30
- package/src/superlocalmemory/mcp/tools_v3.py +21 -0
- package/src/superlocalmemory/server/routes/memories.py +51 -9
- package/src/superlocalmemory/server/routes/stats.py +11 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +1 -1
- package/ui/index.html +36 -8
- package/ui/js/clusters.js +127 -101
- package/ui/js/graph-core.js +3 -1
- package/ui/js/recall-lab.js +154 -60
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.17",
|
|
4
4
|
"description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-memory",
|
package/pyproject.toml
CHANGED
|
@@ -23,6 +23,7 @@ def dispatch(args: Namespace) -> None:
|
|
|
23
23
|
"provider": cmd_provider,
|
|
24
24
|
"connect": cmd_connect,
|
|
25
25
|
"migrate": cmd_migrate,
|
|
26
|
+
"list": cmd_list,
|
|
26
27
|
"remember": cmd_remember,
|
|
27
28
|
"recall": cmd_recall,
|
|
28
29
|
"forget": cmd_forget,
|
|
@@ -113,6 +114,34 @@ def cmd_migrate(args: Namespace) -> None:
|
|
|
113
114
|
_migrate(args)
|
|
114
115
|
|
|
115
116
|
|
|
117
|
+
def cmd_list(args: Namespace) -> None:
|
|
118
|
+
"""List recent memories chronologically."""
|
|
119
|
+
from superlocalmemory.core.config import SLMConfig
|
|
120
|
+
from superlocalmemory.core.engine import MemoryEngine
|
|
121
|
+
|
|
122
|
+
config = SLMConfig.load()
|
|
123
|
+
engine = MemoryEngine(config)
|
|
124
|
+
engine.initialize()
|
|
125
|
+
|
|
126
|
+
limit = getattr(args, "limit", 20)
|
|
127
|
+
facts = engine._db.get_all_facts(engine.profile_id)
|
|
128
|
+
# Sort by created_at descending, take limit
|
|
129
|
+
facts.sort(key=lambda f: f.created_at or "", reverse=True)
|
|
130
|
+
facts = facts[:limit]
|
|
131
|
+
|
|
132
|
+
if not facts:
|
|
133
|
+
print("No memories stored yet.")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
print(f"Recent memories ({len(facts)}):\n")
|
|
137
|
+
for i, f in enumerate(facts, 1):
|
|
138
|
+
date = (f.created_at or "")[:19]
|
|
139
|
+
ftype_raw = getattr(f, "fact_type", "")
|
|
140
|
+
ftype = ftype_raw.value if hasattr(ftype_raw, "value") else str(ftype_raw)
|
|
141
|
+
content = f.content[:100] + ("..." if len(f.content) > 100 else "")
|
|
142
|
+
print(f" {i:3d}. [{date}] ({ftype}) {content}")
|
|
143
|
+
|
|
144
|
+
|
|
116
145
|
def cmd_remember(args: Namespace) -> None:
|
|
117
146
|
"""Store a memory via the engine."""
|
|
118
147
|
from superlocalmemory.core.config import SLMConfig
|
|
@@ -14,73 +14,127 @@ from __future__ import annotations
|
|
|
14
14
|
import argparse
|
|
15
15
|
import sys
|
|
16
16
|
|
|
17
|
+
_HELP_EPILOG = """\
|
|
18
|
+
operating modes:
|
|
19
|
+
Mode A Local Guardian — Zero cloud, zero LLM. All processing stays on
|
|
20
|
+
your machine. Full EU AI Act compliance. Best for privacy-first
|
|
21
|
+
use, air-gapped systems, and regulated environments.
|
|
22
|
+
Retrieval score: 74.8% on LoCoMo benchmark.
|
|
23
|
+
|
|
24
|
+
Mode B Smart Local — Uses a local Ollama LLM for summarization and
|
|
25
|
+
enrichment. Data never leaves your network. EU AI Act compliant.
|
|
26
|
+
Requires: ollama running locally with a model pulled.
|
|
27
|
+
|
|
28
|
+
Mode C Full Power — Uses a cloud LLM (OpenAI, Anthropic, etc.) for
|
|
29
|
+
maximum accuracy. Best retrieval quality, agentic multi-hop.
|
|
30
|
+
Retrieval score: 87.7% on LoCoMo benchmark.
|
|
31
|
+
|
|
32
|
+
quick start:
|
|
33
|
+
slm setup Interactive first-time setup
|
|
34
|
+
slm remember "some fact" Store a memory
|
|
35
|
+
slm recall "search query" Semantic search across memories
|
|
36
|
+
slm list -n 20 Show 20 most recent memories
|
|
37
|
+
slm dashboard Open web dashboard at localhost:8765
|
|
38
|
+
|
|
39
|
+
ide integration:
|
|
40
|
+
slm mcp Start MCP server (used by IDEs)
|
|
41
|
+
slm connect Auto-configure all detected IDEs
|
|
42
|
+
slm connect cursor Configure a specific IDE
|
|
43
|
+
|
|
44
|
+
examples:
|
|
45
|
+
slm remember "Project X uses PostgreSQL 16" --tags "project-x,db"
|
|
46
|
+
slm recall "which database does project X use"
|
|
47
|
+
slm list -n 50
|
|
48
|
+
slm mode a Switch to zero-LLM mode
|
|
49
|
+
slm trace "auth flow" Recall with per-channel score breakdown
|
|
50
|
+
slm health Check math layer status
|
|
51
|
+
slm dashboard --port 9000 Dashboard on custom port
|
|
52
|
+
|
|
53
|
+
documentation:
|
|
54
|
+
Website: https://superlocalmemory.com
|
|
55
|
+
GitHub: https://github.com/qualixar/superlocalmemory
|
|
56
|
+
Paper: https://arxiv.org/abs/2603.14588
|
|
57
|
+
"""
|
|
58
|
+
|
|
17
59
|
|
|
18
60
|
def main() -> None:
|
|
19
61
|
"""Parse CLI arguments and dispatch to command handlers."""
|
|
20
|
-
|
|
21
|
-
|
|
62
|
+
try:
|
|
63
|
+
from importlib.metadata import version as _pkg_version
|
|
64
|
+
_ver = _pkg_version("superlocalmemory")
|
|
65
|
+
except Exception:
|
|
66
|
+
_ver = "unknown"
|
|
67
|
+
|
|
68
|
+
parser = argparse.ArgumentParser(
|
|
69
|
+
prog="slm",
|
|
70
|
+
description=f"SuperLocalMemory V3 ({_ver}) — AI agent memory with mathematical foundations",
|
|
71
|
+
epilog=_HELP_EPILOG,
|
|
72
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"-v", "--version", action="version", version=f"superlocalmemory {_ver}",
|
|
76
|
+
)
|
|
77
|
+
sub = parser.add_subparsers(dest="command", title="commands")
|
|
22
78
|
|
|
23
|
-
# Setup
|
|
24
|
-
sub.add_parser("setup", help="
|
|
79
|
+
# -- Setup & Config ------------------------------------------------
|
|
80
|
+
sub.add_parser("setup", help="Interactive first-time setup wizard")
|
|
25
81
|
|
|
26
|
-
|
|
27
|
-
mode_p = sub.add_parser("mode", help="Get or set operating mode")
|
|
82
|
+
mode_p = sub.add_parser("mode", help="Get or set operating mode (a/b/c)")
|
|
28
83
|
mode_p.add_argument(
|
|
29
84
|
"value", nargs="?", choices=["a", "b", "c"], help="Mode to set",
|
|
30
85
|
)
|
|
31
86
|
|
|
32
|
-
|
|
33
|
-
provider_p = sub.add_parser("provider", help="Get or set LLM provider")
|
|
87
|
+
provider_p = sub.add_parser("provider", help="Get or set LLM provider for Mode B/C")
|
|
34
88
|
provider_p.add_argument(
|
|
35
89
|
"action", nargs="?", choices=["set"], help="Action",
|
|
36
90
|
)
|
|
37
91
|
|
|
38
|
-
|
|
39
|
-
connect_p = sub.add_parser("connect", help="Configure IDE integrations")
|
|
92
|
+
connect_p = sub.add_parser("connect", help="Auto-configure IDE integrations (17+ IDEs)")
|
|
40
93
|
connect_p.add_argument("ide", nargs="?", help="Specific IDE to configure")
|
|
41
94
|
connect_p.add_argument(
|
|
42
95
|
"--list", action="store_true", help="List all supported IDEs",
|
|
43
96
|
)
|
|
44
97
|
|
|
45
|
-
|
|
46
|
-
migrate_p = sub.add_parser("migrate", help="Migrate from V2")
|
|
98
|
+
migrate_p = sub.add_parser("migrate", help="Migrate data from V2 to V3 schema")
|
|
47
99
|
migrate_p.add_argument(
|
|
48
100
|
"--rollback", action="store_true", help="Rollback migration",
|
|
49
101
|
)
|
|
50
102
|
|
|
51
|
-
# Memory
|
|
52
|
-
remember_p = sub.add_parser("remember", help="Store a memory")
|
|
103
|
+
# -- Memory Operations ---------------------------------------------
|
|
104
|
+
remember_p = sub.add_parser("remember", help="Store a memory (extracts facts, builds graph)")
|
|
53
105
|
remember_p.add_argument("content", help="Content to remember")
|
|
54
106
|
remember_p.add_argument("--tags", default="", help="Comma-separated tags")
|
|
55
107
|
|
|
56
|
-
recall_p = sub.add_parser("recall", help="
|
|
108
|
+
recall_p = sub.add_parser("recall", help="Semantic search with 4-channel retrieval")
|
|
57
109
|
recall_p.add_argument("query", help="Search query")
|
|
58
|
-
recall_p.add_argument("--limit", type=int, default=10, help="Max results")
|
|
110
|
+
recall_p.add_argument("--limit", type=int, default=10, help="Max results (default 10)")
|
|
59
111
|
|
|
60
|
-
forget_p = sub.add_parser("forget", help="Delete memories matching query")
|
|
61
|
-
forget_p.add_argument("query", help="Query to match")
|
|
112
|
+
forget_p = sub.add_parser("forget", help="Delete memories matching a query")
|
|
113
|
+
forget_p.add_argument("query", help="Query to match for deletion")
|
|
62
114
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
115
|
+
list_p = sub.add_parser("list", help="List recent memories chronologically")
|
|
116
|
+
list_p.add_argument(
|
|
117
|
+
"--limit", "-n", type=int, default=20, help="Number of entries (default 20)",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# -- Diagnostics ---------------------------------------------------
|
|
121
|
+
sub.add_parser("status", help="System status (mode, profile, DB size)")
|
|
122
|
+
sub.add_parser("health", help="Math layer health (Fisher-Rao, Sheaf, Langevin)")
|
|
66
123
|
|
|
67
|
-
trace_p = sub.add_parser("trace", help="Recall with channel breakdown")
|
|
124
|
+
trace_p = sub.add_parser("trace", help="Recall with per-channel score breakdown")
|
|
68
125
|
trace_p.add_argument("query", help="Search query")
|
|
69
126
|
|
|
70
|
-
#
|
|
127
|
+
# -- Services ------------------------------------------------------
|
|
71
128
|
sub.add_parser("mcp", help="Start MCP server (stdio transport for IDE integration)")
|
|
129
|
+
sub.add_parser("warmup", help="Pre-download embedding model (~500MB, one-time)")
|
|
72
130
|
|
|
73
|
-
|
|
74
|
-
sub.add_parser("warmup", help="Pre-download embedding model (~500MB)")
|
|
75
|
-
|
|
76
|
-
# Dashboard
|
|
77
|
-
dashboard_p = sub.add_parser("dashboard", help="Open web dashboard")
|
|
131
|
+
dashboard_p = sub.add_parser("dashboard", help="Open 17-tab web dashboard")
|
|
78
132
|
dashboard_p.add_argument(
|
|
79
133
|
"--port", type=int, default=8765, help="Port (default 8765)",
|
|
80
134
|
)
|
|
81
135
|
|
|
82
|
-
# Profiles
|
|
83
|
-
profile_p = sub.add_parser("profile", help="Profile management")
|
|
136
|
+
# -- Profiles ------------------------------------------------------
|
|
137
|
+
profile_p = sub.add_parser("profile", help="Profile management (list/switch/create)")
|
|
84
138
|
profile_p.add_argument(
|
|
85
139
|
"action", choices=["list", "switch", "create"], help="Action",
|
|
86
140
|
)
|
|
@@ -20,6 +20,27 @@ logger = logging.getLogger(__name__)
|
|
|
20
20
|
def register_v3_tools(server, get_engine: Callable) -> None:
|
|
21
21
|
"""Register 5 V3-exclusive tools on *server*."""
|
|
22
22
|
|
|
23
|
+
# ------------------------------------------------------------------
|
|
24
|
+
# 0. get_version (so IDEs can check compatibility)
|
|
25
|
+
# ------------------------------------------------------------------
|
|
26
|
+
@server.tool()
|
|
27
|
+
async def get_version() -> dict:
|
|
28
|
+
"""Get SuperLocalMemory version, Python version, and platform info."""
|
|
29
|
+
try:
|
|
30
|
+
from importlib.metadata import version as _pkg_version
|
|
31
|
+
slm_ver = _pkg_version("superlocalmemory")
|
|
32
|
+
except Exception:
|
|
33
|
+
slm_ver = "unknown"
|
|
34
|
+
import platform
|
|
35
|
+
import sys as _sys
|
|
36
|
+
return {
|
|
37
|
+
"success": True,
|
|
38
|
+
"version": slm_ver,
|
|
39
|
+
"python": _sys.version.split()[0],
|
|
40
|
+
"platform": platform.system(),
|
|
41
|
+
"arch": platform.machine(),
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
# ------------------------------------------------------------------
|
|
24
45
|
# 1. set_mode
|
|
25
46
|
# ------------------------------------------------------------------
|
|
@@ -46,19 +46,61 @@ def _fetch_graph_data(
|
|
|
46
46
|
) -> tuple[list, list, list]:
|
|
47
47
|
"""Fetch graph nodes, links, clusters from V3 or V2 schema."""
|
|
48
48
|
if use_v3:
|
|
49
|
+
# Graph-first: fetch edges, then get connected nodes, then fill slots
|
|
49
50
|
cursor.execute("""
|
|
50
|
-
SELECT
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
SELECT source_id as source, target_id as target,
|
|
52
|
+
weight, edge_type as relationship_type
|
|
53
|
+
FROM graph_edges WHERE profile_id = ?
|
|
54
|
+
ORDER BY weight DESC
|
|
55
|
+
""", (profile,))
|
|
56
|
+
all_links = cursor.fetchall()
|
|
57
|
+
|
|
58
|
+
connected_ids = set()
|
|
59
|
+
for lk in all_links:
|
|
60
|
+
connected_ids.add(lk['source'])
|
|
61
|
+
connected_ids.add(lk['target'])
|
|
62
|
+
|
|
63
|
+
# Fetch connected nodes first (these have edges to display)
|
|
64
|
+
connected_nodes: list = []
|
|
65
|
+
if connected_ids:
|
|
66
|
+
ph = ','.join('?' * len(connected_ids))
|
|
67
|
+
cursor.execute(f"""
|
|
68
|
+
SELECT fact_id as id, content, fact_type as category,
|
|
69
|
+
confidence as importance, session_id as project_name,
|
|
70
|
+
created_at
|
|
71
|
+
FROM atomic_facts
|
|
72
|
+
WHERE profile_id = ? AND fact_id IN ({ph})
|
|
73
|
+
""", [profile] + list(connected_ids))
|
|
74
|
+
connected_nodes = cursor.fetchall()
|
|
75
|
+
|
|
76
|
+
# Fill remaining slots with top-confidence unconnected nodes
|
|
77
|
+
remaining = max_nodes - len(connected_nodes)
|
|
78
|
+
if remaining > 0:
|
|
79
|
+
existing = {n['id'] for n in connected_nodes}
|
|
80
|
+
cursor.execute("""
|
|
81
|
+
SELECT fact_id as id, content, fact_type as category,
|
|
82
|
+
confidence as importance, session_id as project_name,
|
|
83
|
+
created_at
|
|
84
|
+
FROM atomic_facts
|
|
85
|
+
WHERE profile_id = ? AND confidence >= ?
|
|
86
|
+
ORDER BY confidence DESC, created_at DESC
|
|
87
|
+
LIMIT ?
|
|
88
|
+
""", (profile, min_importance / 10.0, remaining + len(existing)))
|
|
89
|
+
for n in cursor.fetchall():
|
|
90
|
+
if n['id'] not in existing:
|
|
91
|
+
connected_nodes.append(n)
|
|
92
|
+
if len(connected_nodes) >= max_nodes:
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
nodes = connected_nodes[:max_nodes]
|
|
57
96
|
for n in nodes:
|
|
58
97
|
n['entities'] = []
|
|
59
98
|
n['content_preview'] = _preview(n.get('content'))
|
|
60
|
-
|
|
61
|
-
|
|
99
|
+
|
|
100
|
+
# Filter edges to only those between displayed nodes
|
|
101
|
+
node_ids = {n['id'] for n in nodes}
|
|
102
|
+
links = [lk for lk in all_links
|
|
103
|
+
if lk['source'] in node_ids and lk['target'] in node_ids]
|
|
62
104
|
return nodes, links, []
|
|
63
105
|
|
|
64
106
|
# V2 fallback
|
|
@@ -72,6 +72,17 @@ async def get_stats():
|
|
|
72
72
|
total_clusters = cursor.fetchone()['total']
|
|
73
73
|
except Exception:
|
|
74
74
|
pass
|
|
75
|
+
# Fallback: V2-migrated clusters stored as cluster_id on memories
|
|
76
|
+
if total_clusters == 0:
|
|
77
|
+
try:
|
|
78
|
+
cursor.execute(
|
|
79
|
+
"SELECT COUNT(DISTINCT cluster_id) as total FROM memories "
|
|
80
|
+
"WHERE cluster_id IS NOT NULL AND profile = ?",
|
|
81
|
+
(active_profile,),
|
|
82
|
+
)
|
|
83
|
+
total_clusters = cursor.fetchone()['total']
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
75
86
|
|
|
76
87
|
# Fact type breakdown (replaces category in V3)
|
|
77
88
|
cursor.execute("""
|
package/ui/index.html
CHANGED
|
@@ -740,7 +740,7 @@
|
|
|
740
740
|
</li>
|
|
741
741
|
<li class="nav-item">
|
|
742
742
|
<button class="nav-link" id="learning-tab" data-bs-toggle="tab" data-bs-target="#learning-pane">
|
|
743
|
-
<i class="bi bi-mortarboard"></i> Learning
|
|
743
|
+
<i class="bi bi-mortarboard"></i> Learning
|
|
744
744
|
</button>
|
|
745
745
|
</li>
|
|
746
746
|
<li class="nav-item">
|
|
@@ -760,17 +760,17 @@
|
|
|
760
760
|
</li>
|
|
761
761
|
<li class="nav-item" role="presentation">
|
|
762
762
|
<button class="nav-link" id="lifecycle-tab" data-bs-toggle="tab" data-bs-target="#lifecycle-pane" type="button" role="tab">
|
|
763
|
-
<i class="bi bi-hourglass-split"></i> Lifecycle
|
|
763
|
+
<i class="bi bi-hourglass-split"></i> Lifecycle
|
|
764
764
|
</button>
|
|
765
765
|
</li>
|
|
766
766
|
<li class="nav-item" role="presentation">
|
|
767
767
|
<button class="nav-link" id="behavioral-tab" data-bs-toggle="tab" data-bs-target="#behavioral-pane" type="button" role="tab">
|
|
768
|
-
<i class="bi bi-lightbulb"></i> Behavioral
|
|
768
|
+
<i class="bi bi-lightbulb"></i> Behavioral
|
|
769
769
|
</button>
|
|
770
770
|
</li>
|
|
771
771
|
<li class="nav-item" role="presentation">
|
|
772
772
|
<button class="nav-link" id="compliance-tab" data-bs-toggle="tab" data-bs-target="#compliance-pane" type="button" role="tab">
|
|
773
|
-
<i class="bi bi-shield-lock"></i> Compliance
|
|
773
|
+
<i class="bi bi-shield-lock"></i> Compliance
|
|
774
774
|
</button>
|
|
775
775
|
</li>
|
|
776
776
|
<li class="nav-item">
|
|
@@ -1023,8 +1023,13 @@
|
|
|
1023
1023
|
<p class="text-muted">See how each retrieval channel contributes to results.</p>
|
|
1024
1024
|
<div class="input-group mb-3">
|
|
1025
1025
|
<input type="text" id="recall-lab-query" class="form-control form-control-lg" placeholder="Enter your query...">
|
|
1026
|
+
<select id="recall-lab-per-page" class="form-select" style="max-width:100px;">
|
|
1027
|
+
<option value="10" selected>10</option>
|
|
1028
|
+
<option value="25">25</option>
|
|
1029
|
+
<option value="50">50</option>
|
|
1030
|
+
</select>
|
|
1026
1031
|
<button class="btn btn-primary btn-lg" id="recall-lab-search">
|
|
1027
|
-
<i class="bi bi-search"></i> Search
|
|
1032
|
+
<i class="bi bi-search"></i> Search
|
|
1028
1033
|
</button>
|
|
1029
1034
|
</div>
|
|
1030
1035
|
<div id="recall-lab-meta" class="mb-3 text-muted small"></div>
|
|
@@ -1070,8 +1075,19 @@
|
|
|
1070
1075
|
</div>
|
|
1071
1076
|
</div>
|
|
1072
1077
|
|
|
1073
|
-
<!-- Learning System
|
|
1078
|
+
<!-- Learning System -->
|
|
1074
1079
|
<div class="tab-pane fade" id="learning-pane">
|
|
1080
|
+
<!-- Getting Started Guide -->
|
|
1081
|
+
<div class="alert alert-light border mb-3" id="learning-getting-started">
|
|
1082
|
+
<h6 class="mb-2"><i class="bi bi-info-circle text-primary"></i> How Learning Works</h6>
|
|
1083
|
+
<p class="mb-1 small">SuperLocalMemory learns from your usage patterns to improve retrieval ranking over time. This happens automatically as you use <code>slm recall</code> or search via MCP.</p>
|
|
1084
|
+
<ul class="small mb-0">
|
|
1085
|
+
<li><strong>0-20 signals:</strong> Baseline phase (collecting data)</li>
|
|
1086
|
+
<li><strong>20+ signals:</strong> Rule-based ranking adjustments</li>
|
|
1087
|
+
<li><strong>200+ signals:</strong> ML model trained on your patterns</li>
|
|
1088
|
+
</ul>
|
|
1089
|
+
<p class="mb-0 mt-1 small text-muted">Each recall query generates a signal. Keep using SLM and this tab will populate automatically.</p>
|
|
1090
|
+
</div>
|
|
1075
1091
|
<!-- Ranking Phase & Engagement -->
|
|
1076
1092
|
<div class="row g-3 mb-3">
|
|
1077
1093
|
<div class="col-md-4">
|
|
@@ -1424,12 +1440,18 @@
|
|
|
1424
1440
|
</div>
|
|
1425
1441
|
</div>
|
|
1426
1442
|
|
|
1427
|
-
<!-- Behavioral Learning
|
|
1443
|
+
<!-- Behavioral Learning -->
|
|
1428
1444
|
<div class="tab-pane fade" id="behavioral-pane" role="tabpanel">
|
|
1429
1445
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
1430
1446
|
<h5 class="mb-0"><i class="bi bi-lightbulb text-info"></i> Behavioral Learning</h5>
|
|
1431
1447
|
<span class="badge bg-secondary" id="behavioral-profile-badge">default</span>
|
|
1432
1448
|
</div>
|
|
1449
|
+
<!-- Getting Started Guide -->
|
|
1450
|
+
<div class="alert alert-light border mb-3">
|
|
1451
|
+
<h6 class="mb-2"><i class="bi bi-info-circle text-primary"></i> How Behavioral Learning Works</h6>
|
|
1452
|
+
<p class="mb-1 small">This tab tracks how memories are used in practice — which recalls led to successful outcomes (code written, decisions made, bugs fixed) and which didn't.</p>
|
|
1453
|
+
<p class="mb-0 small text-muted">Report outcomes using the form below, or via MCP: <code>report_outcome</code>. Patterns emerge after 10+ outcomes.</p>
|
|
1454
|
+
</div>
|
|
1433
1455
|
<!-- Stats Row -->
|
|
1434
1456
|
<div class="row g-3 mb-4">
|
|
1435
1457
|
<div class="col-md-3"><div class="card p-3 text-center"><div class="fw-bold fs-3 text-success" id="bh-success-count">-</div><small class="text-muted">Successes</small></div></div>
|
|
@@ -1465,12 +1487,18 @@
|
|
|
1465
1487
|
</div>
|
|
1466
1488
|
</div>
|
|
1467
1489
|
|
|
1468
|
-
<!-- Compliance & Audit
|
|
1490
|
+
<!-- Compliance & Audit -->
|
|
1469
1491
|
<div class="tab-pane fade" id="compliance-pane" role="tabpanel">
|
|
1470
1492
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
1471
1493
|
<h5 class="mb-0"><i class="bi bi-shield-lock text-success"></i> Compliance & Audit</h5>
|
|
1472
1494
|
<span class="badge bg-secondary" id="compliance-profile-badge">default</span>
|
|
1473
1495
|
</div>
|
|
1496
|
+
<!-- Getting Started Guide -->
|
|
1497
|
+
<div class="alert alert-light border mb-3">
|
|
1498
|
+
<h6 class="mb-2"><i class="bi bi-info-circle text-primary"></i> Compliance Overview</h6>
|
|
1499
|
+
<p class="mb-1 small">Set retention policies and access controls for your memories. In Mode A, all data stays on your device — EU AI Act compliant by default.</p>
|
|
1500
|
+
<p class="mb-0 small text-muted">Create a retention policy below to start managing memory lifecycle automatically.</p>
|
|
1501
|
+
</div>
|
|
1474
1502
|
<!-- Stats Row -->
|
|
1475
1503
|
<div class="row g-3 mb-4">
|
|
1476
1504
|
<div class="col-md-4"><div class="card p-3 text-center"><div class="fw-bold fs-3" id="cp-audit-count">-</div><small class="text-muted">Audit Events</small></div></div>
|
package/ui/js/clusters.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
// SuperLocalMemory
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// Security: All dynamic values escaped via escapeHtml(). Data from local DB only.
|
|
1
|
+
// SuperLocalMemory V3 - Clusters View
|
|
2
|
+
// Part of Qualixar | https://superlocalmemory.com
|
|
5
3
|
|
|
6
4
|
async function loadClusters() {
|
|
7
5
|
showLoading('clusters-list', 'Loading clusters...');
|
|
@@ -18,152 +16,180 @@ async function loadClusters() {
|
|
|
18
16
|
function renderClusters(clusters) {
|
|
19
17
|
var container = document.getElementById('clusters-list');
|
|
20
18
|
if (!clusters || clusters.length === 0) {
|
|
21
|
-
showEmpty('clusters-list', 'collection', 'No clusters found.
|
|
19
|
+
showEmpty('clusters-list', 'collection', 'No clusters found yet. Clusters form automatically as you store related memories.');
|
|
22
20
|
return;
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
var colors = ['#667eea', '#f093fb', '#4facfe', '#43e97b', '#fa709a'];
|
|
23
|
+
var colors = ['#667eea', '#f093fb', '#4facfe', '#43e97b', '#fa709a', '#30cfd0', '#764ba2', '#f5576c'];
|
|
26
24
|
container.textContent = '';
|
|
27
25
|
|
|
28
26
|
clusters.forEach(function(cluster, idx) {
|
|
29
27
|
var color = colors[idx % colors.length];
|
|
30
28
|
|
|
31
29
|
var card = document.createElement('div');
|
|
32
|
-
card.className = 'card
|
|
33
|
-
card.style.
|
|
34
|
-
card.setAttribute('data-cluster-id', cluster.cluster_id);
|
|
35
|
-
card.title = 'Click to filter graph to this cluster';
|
|
30
|
+
card.className = 'card mb-2';
|
|
31
|
+
card.style.borderLeft = '4px solid ' + color;
|
|
36
32
|
|
|
37
33
|
var body = document.createElement('div');
|
|
38
|
-
body.className = 'card-body';
|
|
34
|
+
body.className = 'card-body py-2 px-3';
|
|
35
|
+
body.style.cursor = 'pointer';
|
|
36
|
+
|
|
37
|
+
// Header row
|
|
38
|
+
var headerRow = document.createElement('div');
|
|
39
|
+
headerRow.className = 'd-flex justify-content-between align-items-center';
|
|
39
40
|
|
|
40
41
|
var title = document.createElement('h6');
|
|
41
|
-
title.className = '
|
|
42
|
-
title.textContent = 'Cluster ' + cluster.cluster_id
|
|
42
|
+
title.className = 'mb-0';
|
|
43
|
+
title.textContent = 'Cluster ' + cluster.cluster_id;
|
|
44
|
+
|
|
45
|
+
var badges = document.createElement('div');
|
|
43
46
|
var countBadge = document.createElement('span');
|
|
44
|
-
countBadge.className = 'badge bg-secondary
|
|
47
|
+
countBadge.className = 'badge bg-secondary me-1';
|
|
45
48
|
countBadge.textContent = cluster.member_count + ' memories';
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
72
|
-
} else {
|
|
73
|
-
var none = document.createElement('span');
|
|
74
|
-
none.className = 'text-muted';
|
|
75
|
-
none.textContent = 'No entities';
|
|
76
|
-
body.appendChild(none);
|
|
49
|
+
badges.appendChild(countBadge);
|
|
50
|
+
|
|
51
|
+
if (cluster.avg_importance) {
|
|
52
|
+
var impBadge = document.createElement('span');
|
|
53
|
+
impBadge.className = 'badge bg-outline-primary';
|
|
54
|
+
impBadge.style.cssText = 'border:1px solid #667eea; color:#667eea;';
|
|
55
|
+
impBadge.textContent = 'imp: ' + parseFloat(cluster.avg_importance).toFixed(1);
|
|
56
|
+
badges.appendChild(impBadge);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
var expandIcon = document.createElement('i');
|
|
60
|
+
expandIcon.className = 'bi bi-chevron-down ms-2';
|
|
61
|
+
expandIcon.style.transition = 'transform 0.2s';
|
|
62
|
+
badges.appendChild(expandIcon);
|
|
63
|
+
|
|
64
|
+
headerRow.appendChild(title);
|
|
65
|
+
headerRow.appendChild(badges);
|
|
66
|
+
body.appendChild(headerRow);
|
|
67
|
+
|
|
68
|
+
// Summary line (categories if available)
|
|
69
|
+
if (cluster.categories) {
|
|
70
|
+
var catLine = document.createElement('small');
|
|
71
|
+
catLine.className = 'text-muted';
|
|
72
|
+
catLine.textContent = cluster.categories;
|
|
73
|
+
body.appendChild(catLine);
|
|
77
74
|
}
|
|
78
75
|
|
|
76
|
+
// Expandable member area (hidden by default)
|
|
77
|
+
var memberArea = document.createElement('div');
|
|
78
|
+
memberArea.className = 'mt-2';
|
|
79
|
+
memberArea.style.display = 'none';
|
|
80
|
+
memberArea.id = 'cluster-members-' + cluster.cluster_id;
|
|
81
|
+
|
|
82
|
+
var loadingText = document.createElement('div');
|
|
83
|
+
loadingText.className = 'text-center text-muted small py-2';
|
|
84
|
+
loadingText.textContent = 'Loading members...';
|
|
85
|
+
memberArea.appendChild(loadingText);
|
|
86
|
+
|
|
87
|
+
body.appendChild(memberArea);
|
|
79
88
|
card.appendChild(body);
|
|
80
89
|
container.appendChild(card);
|
|
81
90
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
// Click to expand/collapse
|
|
92
|
+
var expanded = false;
|
|
93
|
+
body.addEventListener('click', function(e) {
|
|
94
|
+
expanded = !expanded;
|
|
95
|
+
memberArea.style.display = expanded ? 'block' : 'none';
|
|
96
|
+
expandIcon.style.transform = expanded ? 'rotate(180deg)' : 'rotate(0)';
|
|
88
97
|
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
if (expanded && memberArea.children.length === 1 && memberArea.children[0] === loadingText) {
|
|
99
|
+
loadClusterMembers(cluster.cluster_id, memberArea);
|
|
100
|
+
}
|
|
91
101
|
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
92
104
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
async function loadClusterMembers(clusterId, container) {
|
|
106
|
+
try {
|
|
107
|
+
var response = await fetch('/api/clusters/' + clusterId + '?limit=10');
|
|
108
|
+
var data = await response.json();
|
|
109
|
+
container.textContent = '';
|
|
110
|
+
|
|
111
|
+
if (!data.members || data.members.length === 0) {
|
|
112
|
+
var empty = document.createElement('div');
|
|
113
|
+
empty.className = 'text-muted small';
|
|
114
|
+
empty.textContent = 'No members found.';
|
|
115
|
+
container.appendChild(empty);
|
|
116
|
+
return;
|
|
105
117
|
}
|
|
106
118
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
data.members.forEach(function(m, i) {
|
|
120
|
+
var row = document.createElement('div');
|
|
121
|
+
row.className = 'border-bottom py-1';
|
|
122
|
+
if (i === data.members.length - 1) row.className = 'py-1';
|
|
123
|
+
|
|
124
|
+
var content = document.createElement('div');
|
|
125
|
+
content.className = 'small';
|
|
126
|
+
var text = m.content || m.summary || '';
|
|
127
|
+
content.textContent = (i + 1) + '. ' + (text.length > 150 ? text.substring(0, 150) + '...' : text);
|
|
128
|
+
row.appendChild(content);
|
|
129
|
+
|
|
130
|
+
var meta = document.createElement('div');
|
|
131
|
+
meta.className = 'text-muted';
|
|
132
|
+
meta.style.fontSize = '0.7rem';
|
|
133
|
+
var parts = [];
|
|
134
|
+
if (m.category) parts.push(m.category);
|
|
135
|
+
if (m.importance) parts.push('imp: ' + m.importance);
|
|
136
|
+
if (m.created_at) parts.push(m.created_at.substring(0, 10));
|
|
137
|
+
meta.textContent = parts.join(' | ');
|
|
138
|
+
row.appendChild(meta);
|
|
139
|
+
|
|
140
|
+
container.appendChild(row);
|
|
113
141
|
});
|
|
114
|
-
|
|
142
|
+
|
|
143
|
+
// View in graph button
|
|
144
|
+
var graphBtn = document.createElement('button');
|
|
145
|
+
graphBtn.className = 'btn btn-sm btn-outline-primary mt-2';
|
|
146
|
+
graphBtn.textContent = 'View in Knowledge Graph';
|
|
147
|
+
graphBtn.addEventListener('click', function(e) {
|
|
148
|
+
e.stopPropagation();
|
|
149
|
+
filterGraphToCluster(clusterId);
|
|
150
|
+
});
|
|
151
|
+
container.appendChild(graphBtn);
|
|
152
|
+
|
|
153
|
+
} catch (error) {
|
|
154
|
+
container.textContent = '';
|
|
155
|
+
var errDiv = document.createElement('div');
|
|
156
|
+
errDiv.className = 'text-danger small';
|
|
157
|
+
errDiv.textContent = 'Failed to load: ' + error.message;
|
|
158
|
+
container.appendChild(errDiv);
|
|
159
|
+
}
|
|
115
160
|
}
|
|
116
161
|
|
|
117
|
-
// v2.6.5: Filter graph to a specific cluster
|
|
118
162
|
function filterGraphToCluster(clusterId) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (graphTab) {
|
|
122
|
-
graphTab.click();
|
|
123
|
-
}
|
|
163
|
+
var graphTab = document.querySelector('a[href="#graph"]');
|
|
164
|
+
if (graphTab) graphTab.click();
|
|
124
165
|
|
|
125
|
-
// Apply filter after a delay (for tab to load)
|
|
126
166
|
setTimeout(function() {
|
|
127
167
|
if (typeof filterState !== 'undefined' && typeof filterByCluster === 'function' && typeof renderGraph === 'function') {
|
|
128
168
|
filterState.cluster_id = clusterId;
|
|
129
|
-
|
|
169
|
+
var filtered = filterByCluster(originalGraphData, clusterId);
|
|
130
170
|
renderGraph(filtered);
|
|
131
|
-
|
|
132
|
-
// Update URL
|
|
133
|
-
const url = new URL(window.location);
|
|
171
|
+
var url = new URL(window.location);
|
|
134
172
|
url.searchParams.set('cluster_id', clusterId);
|
|
135
173
|
window.history.replaceState({}, '', url);
|
|
136
174
|
}
|
|
137
175
|
}, 300);
|
|
138
176
|
}
|
|
139
177
|
|
|
140
|
-
// v2.6.5: Filter graph by entity
|
|
141
178
|
function filterGraphByEntity(entity) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (graphTab) {
|
|
145
|
-
graphTab.click();
|
|
146
|
-
}
|
|
179
|
+
var graphTab = document.querySelector('a[href="#graph"]');
|
|
180
|
+
if (graphTab) graphTab.click();
|
|
147
181
|
|
|
148
|
-
// Apply filter after a delay
|
|
149
182
|
setTimeout(function() {
|
|
150
183
|
if (typeof filterState !== 'undefined' && typeof filterByEntity === 'function' && typeof renderGraph === 'function') {
|
|
151
184
|
filterState.entity = entity;
|
|
152
|
-
|
|
185
|
+
var filtered = filterByEntity(originalGraphData, entity);
|
|
153
186
|
renderGraph(filtered);
|
|
154
187
|
}
|
|
155
188
|
}, 300);
|
|
156
189
|
}
|
|
157
190
|
|
|
158
|
-
// v2.6.5: Show memories in a cluster (future: sidebar list)
|
|
159
191
|
function showClusterMemories(clusterId) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
memoriesTab.click();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// TODO: Implement sidebar memory list view
|
|
167
|
-
console.log('Show memories for cluster', clusterId);
|
|
168
|
-
showToast('Filtering memories for cluster ' + clusterId);
|
|
192
|
+
var memoriesTab = document.querySelector('a[href="#memories"]');
|
|
193
|
+
if (memoriesTab) memoriesTab.click();
|
|
194
|
+
if (typeof showToast === 'function') showToast('Filtering memories for cluster ' + clusterId);
|
|
169
195
|
}
|
package/ui/js/graph-core.js
CHANGED
|
@@ -98,8 +98,10 @@ function transformDataForCytoscape(data) {
|
|
|
98
98
|
|
|
99
99
|
// Add nodes
|
|
100
100
|
data.nodes.forEach(node => {
|
|
101
|
-
const label = node.category || node.project_name || `Memory #${node.id}`;
|
|
102
101
|
const contentPreview = node.content_preview || node.summary || node.content || '';
|
|
102
|
+
// Label: first 4 words of content (readable on node), fallback to category
|
|
103
|
+
const contentWords = contentPreview.split(/\s+/).slice(0, 4).join(' ');
|
|
104
|
+
const label = contentWords || node.category || `Memory #${node.id}`;
|
|
103
105
|
const preview = contentPreview.substring(0, 50) + (contentPreview.length > 50 ? '...' : '');
|
|
104
106
|
|
|
105
107
|
elements.push({
|
package/ui/js/recall-lab.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
// SuperLocalMemory V3 — Recall Lab
|
|
1
|
+
// SuperLocalMemory V3 — Recall Lab with Pagination
|
|
2
2
|
// Part of Qualixar | https://superlocalmemory.com
|
|
3
3
|
|
|
4
|
+
var recallLabState = {
|
|
5
|
+
allResults: [],
|
|
6
|
+
page: 0,
|
|
7
|
+
perPage: 10,
|
|
8
|
+
query: '',
|
|
9
|
+
};
|
|
10
|
+
|
|
4
11
|
document.getElementById('recall-lab-search')?.addEventListener('click', function() {
|
|
5
12
|
var query = document.getElementById('recall-lab-query').value.trim();
|
|
6
13
|
if (!query) return;
|
|
7
14
|
|
|
15
|
+
recallLabState.query = query;
|
|
16
|
+
recallLabState.page = 0;
|
|
17
|
+
var perPageEl = document.getElementById('recall-lab-per-page');
|
|
18
|
+
recallLabState.perPage = perPageEl ? parseInt(perPageEl.value) : 10;
|
|
19
|
+
var fetchLimit = Math.max(recallLabState.perPage * 5, 50); // Fetch up to 5 pages
|
|
20
|
+
|
|
8
21
|
var resultsDiv = document.getElementById('recall-lab-results');
|
|
9
22
|
var metaDiv = document.getElementById('recall-lab-meta');
|
|
10
23
|
resultsDiv.textContent = '';
|
|
@@ -18,7 +31,7 @@ document.getElementById('recall-lab-search')?.addEventListener('click', function
|
|
|
18
31
|
fetch('/api/v3/recall/trace', {
|
|
19
32
|
method: 'POST',
|
|
20
33
|
headers: {'Content-Type': 'application/json'},
|
|
21
|
-
body: JSON.stringify({query: query, limit:
|
|
34
|
+
body: JSON.stringify({query: query, limit: fetchLimit})
|
|
22
35
|
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
23
36
|
if (data.error) {
|
|
24
37
|
resultsDiv.textContent = '';
|
|
@@ -32,11 +45,13 @@ document.getElementById('recall-lab-search')?.addEventListener('click', function
|
|
|
32
45
|
metaDiv.textContent = '';
|
|
33
46
|
appendMetaField(metaDiv, 'Query type: ', data.query_type || 'unknown');
|
|
34
47
|
metaDiv.appendChild(document.createTextNode(' | '));
|
|
35
|
-
appendMetaField(metaDiv, 'Results: ', String(data.
|
|
48
|
+
appendMetaField(metaDiv, 'Results: ', String((data.results || []).length));
|
|
36
49
|
metaDiv.appendChild(document.createTextNode(' | '));
|
|
37
50
|
appendMetaField(metaDiv, 'Time: ', (data.retrieval_time_ms || 0).toFixed(0) + 'ms');
|
|
38
51
|
|
|
39
|
-
|
|
52
|
+
recallLabState.allResults = data.results || [];
|
|
53
|
+
|
|
54
|
+
if (recallLabState.allResults.length === 0) {
|
|
40
55
|
resultsDiv.textContent = '';
|
|
41
56
|
var infoDiv = document.createElement('div');
|
|
42
57
|
infoDiv.className = 'alert alert-info';
|
|
@@ -45,57 +60,7 @@ document.getElementById('recall-lab-search')?.addEventListener('click', function
|
|
|
45
60
|
return;
|
|
46
61
|
}
|
|
47
62
|
|
|
48
|
-
|
|
49
|
-
var listGroup = document.createElement('div');
|
|
50
|
-
listGroup.className = 'list-group';
|
|
51
|
-
|
|
52
|
-
data.results.forEach(function(r, i) {
|
|
53
|
-
var channels = r.channel_scores || {};
|
|
54
|
-
var maxChannel = Math.max(channels.semantic || 0, channels.bm25 || 0, channels.entity_graph || 0, channels.temporal || 0) || 1;
|
|
55
|
-
|
|
56
|
-
var item = document.createElement('div');
|
|
57
|
-
item.className = 'list-group-item list-group-item-action';
|
|
58
|
-
item.style.cursor = 'pointer';
|
|
59
|
-
item.title = 'Click to view full memory';
|
|
60
|
-
(function(result) {
|
|
61
|
-
item.addEventListener('click', function() {
|
|
62
|
-
if (typeof openMemoryDetail === 'function') {
|
|
63
|
-
openMemoryDetail({
|
|
64
|
-
id: result.fact_id,
|
|
65
|
-
content: result.content,
|
|
66
|
-
score: result.score,
|
|
67
|
-
importance: Math.round((result.confidence || 0.5) * 10),
|
|
68
|
-
category: 'recall',
|
|
69
|
-
tags: Object.keys(result.channel_scores || {}).join(', '),
|
|
70
|
-
created_at: null,
|
|
71
|
-
trust_score: result.trust_score,
|
|
72
|
-
channel_scores: result.channel_scores
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
})(r);
|
|
77
|
-
|
|
78
|
-
var header = document.createElement('h6');
|
|
79
|
-
header.className = 'mb-1';
|
|
80
|
-
header.textContent = (i + 1) + '. ' + (r.content || '').substring(0, 200);
|
|
81
|
-
item.appendChild(header);
|
|
82
|
-
|
|
83
|
-
var meta = document.createElement('small');
|
|
84
|
-
meta.className = 'text-muted';
|
|
85
|
-
meta.textContent = 'Score: ' + r.score + ' | Trust: ' + r.trust_score + ' | Confidence: ' + r.confidence;
|
|
86
|
-
item.appendChild(meta);
|
|
87
|
-
|
|
88
|
-
var barsDiv = document.createElement('div');
|
|
89
|
-
barsDiv.className = 'mt-2';
|
|
90
|
-
barsDiv.appendChild(buildChannelBar('Semantic', channels.semantic || 0, maxChannel, 'primary'));
|
|
91
|
-
barsDiv.appendChild(buildChannelBar('BM25', channels.bm25 || 0, maxChannel, 'success'));
|
|
92
|
-
barsDiv.appendChild(buildChannelBar('Entity', channels.entity_graph || 0, maxChannel, 'info'));
|
|
93
|
-
barsDiv.appendChild(buildChannelBar('Temporal', channels.temporal || 0, maxChannel, 'warning'));
|
|
94
|
-
item.appendChild(barsDiv);
|
|
95
|
-
|
|
96
|
-
listGroup.appendChild(item);
|
|
97
|
-
});
|
|
98
|
-
resultsDiv.appendChild(listGroup);
|
|
63
|
+
renderRecallPage();
|
|
99
64
|
}).catch(function(e) {
|
|
100
65
|
resultsDiv.textContent = '';
|
|
101
66
|
var errDiv = document.createElement('div');
|
|
@@ -105,6 +70,140 @@ document.getElementById('recall-lab-search')?.addEventListener('click', function
|
|
|
105
70
|
});
|
|
106
71
|
});
|
|
107
72
|
|
|
73
|
+
function renderRecallPage() {
|
|
74
|
+
var resultsDiv = document.getElementById('recall-lab-results');
|
|
75
|
+
resultsDiv.textContent = '';
|
|
76
|
+
|
|
77
|
+
var results = recallLabState.allResults;
|
|
78
|
+
var start = recallLabState.page * recallLabState.perPage;
|
|
79
|
+
var end = Math.min(start + recallLabState.perPage, results.length);
|
|
80
|
+
var pageResults = results.slice(start, end);
|
|
81
|
+
var totalPages = Math.ceil(results.length / recallLabState.perPage);
|
|
82
|
+
|
|
83
|
+
var listGroup = document.createElement('div');
|
|
84
|
+
listGroup.className = 'list-group';
|
|
85
|
+
|
|
86
|
+
pageResults.forEach(function(r, i) {
|
|
87
|
+
var globalIndex = start + i;
|
|
88
|
+
var channels = r.channel_scores || {};
|
|
89
|
+
var maxChannel = Math.max(channels.semantic || 0, channels.bm25 || 0, channels.entity_graph || 0, channels.temporal || 0) || 1;
|
|
90
|
+
|
|
91
|
+
var item = document.createElement('div');
|
|
92
|
+
item.className = 'list-group-item list-group-item-action';
|
|
93
|
+
item.style.cursor = 'pointer';
|
|
94
|
+
item.title = 'Click to view full memory';
|
|
95
|
+
(function(result) {
|
|
96
|
+
item.addEventListener('click', function() {
|
|
97
|
+
if (typeof openMemoryDetail === 'function') {
|
|
98
|
+
openMemoryDetail({
|
|
99
|
+
id: result.fact_id,
|
|
100
|
+
content: result.content,
|
|
101
|
+
score: result.score,
|
|
102
|
+
importance: Math.round((result.confidence || 0.5) * 10),
|
|
103
|
+
category: 'recall',
|
|
104
|
+
tags: Object.keys(result.channel_scores || {}).join(', '),
|
|
105
|
+
created_at: null,
|
|
106
|
+
trust_score: result.trust_score,
|
|
107
|
+
channel_scores: result.channel_scores
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
})(r);
|
|
112
|
+
|
|
113
|
+
var header = document.createElement('h6');
|
|
114
|
+
header.className = 'mb-1';
|
|
115
|
+
header.textContent = (globalIndex + 1) + '. ' + (r.content || '').substring(0, 200);
|
|
116
|
+
item.appendChild(header);
|
|
117
|
+
|
|
118
|
+
var meta = document.createElement('small');
|
|
119
|
+
meta.className = 'text-muted';
|
|
120
|
+
meta.textContent = 'Score: ' + r.score + ' | Trust: ' + r.trust_score + ' | Confidence: ' + r.confidence;
|
|
121
|
+
item.appendChild(meta);
|
|
122
|
+
|
|
123
|
+
var barsDiv = document.createElement('div');
|
|
124
|
+
barsDiv.className = 'mt-2';
|
|
125
|
+
barsDiv.appendChild(buildChannelBar('Semantic', channels.semantic || 0, maxChannel, 'primary'));
|
|
126
|
+
barsDiv.appendChild(buildChannelBar('BM25', channels.bm25 || 0, maxChannel, 'success'));
|
|
127
|
+
barsDiv.appendChild(buildChannelBar('Entity', channels.entity_graph || 0, maxChannel, 'info'));
|
|
128
|
+
barsDiv.appendChild(buildChannelBar('Temporal', channels.temporal || 0, maxChannel, 'warning'));
|
|
129
|
+
item.appendChild(barsDiv);
|
|
130
|
+
|
|
131
|
+
listGroup.appendChild(item);
|
|
132
|
+
});
|
|
133
|
+
resultsDiv.appendChild(listGroup);
|
|
134
|
+
|
|
135
|
+
// Pagination controls
|
|
136
|
+
if (totalPages > 1) {
|
|
137
|
+
var nav = document.createElement('nav');
|
|
138
|
+
nav.className = 'mt-3';
|
|
139
|
+
nav.setAttribute('aria-label', 'Recall results pagination');
|
|
140
|
+
var ul = document.createElement('ul');
|
|
141
|
+
ul.className = 'pagination justify-content-center';
|
|
142
|
+
|
|
143
|
+
// Prev
|
|
144
|
+
var prevLi = document.createElement('li');
|
|
145
|
+
prevLi.className = 'page-item' + (recallLabState.page === 0 ? ' disabled' : '');
|
|
146
|
+
var prevA = document.createElement('a');
|
|
147
|
+
prevA.className = 'page-link';
|
|
148
|
+
prevA.href = '#';
|
|
149
|
+
prevA.textContent = 'Previous';
|
|
150
|
+
prevA.addEventListener('click', function(e) {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
if (recallLabState.page > 0) {
|
|
153
|
+
recallLabState.page--;
|
|
154
|
+
renderRecallPage();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
prevLi.appendChild(prevA);
|
|
158
|
+
ul.appendChild(prevLi);
|
|
159
|
+
|
|
160
|
+
// Page numbers
|
|
161
|
+
for (var p = 0; p < totalPages; p++) {
|
|
162
|
+
var li = document.createElement('li');
|
|
163
|
+
li.className = 'page-item' + (p === recallLabState.page ? ' active' : '');
|
|
164
|
+
var a = document.createElement('a');
|
|
165
|
+
a.className = 'page-link';
|
|
166
|
+
a.href = '#';
|
|
167
|
+
a.textContent = String(p + 1);
|
|
168
|
+
(function(pageNum) {
|
|
169
|
+
a.addEventListener('click', function(e) {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
recallLabState.page = pageNum;
|
|
172
|
+
renderRecallPage();
|
|
173
|
+
});
|
|
174
|
+
})(p);
|
|
175
|
+
li.appendChild(a);
|
|
176
|
+
ul.appendChild(li);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Next
|
|
180
|
+
var nextLi = document.createElement('li');
|
|
181
|
+
nextLi.className = 'page-item' + (recallLabState.page >= totalPages - 1 ? ' disabled' : '');
|
|
182
|
+
var nextA = document.createElement('a');
|
|
183
|
+
nextA.className = 'page-link';
|
|
184
|
+
nextA.href = '#';
|
|
185
|
+
nextA.textContent = 'Next';
|
|
186
|
+
nextA.addEventListener('click', function(e) {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
if (recallLabState.page < totalPages - 1) {
|
|
189
|
+
recallLabState.page++;
|
|
190
|
+
renderRecallPage();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
nextLi.appendChild(nextA);
|
|
194
|
+
ul.appendChild(nextLi);
|
|
195
|
+
|
|
196
|
+
nav.appendChild(ul);
|
|
197
|
+
resultsDiv.appendChild(nav);
|
|
198
|
+
|
|
199
|
+
// Page info
|
|
200
|
+
var info = document.createElement('div');
|
|
201
|
+
info.className = 'text-center text-muted small';
|
|
202
|
+
info.textContent = 'Showing ' + (start + 1) + '-' + end + ' of ' + results.length + ' results';
|
|
203
|
+
resultsDiv.appendChild(info);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
108
207
|
function appendMetaField(parent, label, value) {
|
|
109
208
|
var text = document.createTextNode(label);
|
|
110
209
|
parent.appendChild(text);
|
|
@@ -115,27 +214,22 @@ function appendMetaField(parent, label, value) {
|
|
|
115
214
|
|
|
116
215
|
function buildChannelBar(name, score, max, color) {
|
|
117
216
|
var pct = max > 0 ? Math.round((score / max) * 100) : 0;
|
|
118
|
-
|
|
119
217
|
var row = document.createElement('div');
|
|
120
218
|
row.className = 'd-flex align-items-center mb-1';
|
|
121
|
-
|
|
122
219
|
var label = document.createElement('span');
|
|
123
220
|
label.className = 'me-2';
|
|
124
221
|
label.style.width = '70px';
|
|
125
222
|
label.style.fontSize = '0.75rem';
|
|
126
223
|
label.textContent = name;
|
|
127
224
|
row.appendChild(label);
|
|
128
|
-
|
|
129
225
|
var progressWrap = document.createElement('div');
|
|
130
226
|
progressWrap.className = 'progress flex-grow-1';
|
|
131
227
|
progressWrap.style.height = '14px';
|
|
132
|
-
|
|
133
228
|
var bar = document.createElement('div');
|
|
134
229
|
bar.className = 'progress-bar bg-' + color;
|
|
135
230
|
bar.style.width = pct + '%';
|
|
136
231
|
bar.textContent = score.toFixed(3);
|
|
137
232
|
progressWrap.appendChild(bar);
|
|
138
|
-
|
|
139
233
|
row.appendChild(progressWrap);
|
|
140
234
|
return row;
|
|
141
235
|
}
|