superlocalmemory 3.4.19 → 3.4.22
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/CHANGELOG.md +24 -0
- package/README.md +42 -34
- package/bin/slm +11 -0
- package/bin/slm.bat +12 -0
- package/package.json +4 -3
- package/pyproject.toml +4 -3
- package/scripts/build-slm-hook.ps1 +40 -0
- package/scripts/build-slm-hook.sh +45 -0
- package/scripts/build_entry.py +452 -0
- package/scripts/ci/stage5b_gate.sh +50 -0
- package/scripts/postinstall/validation.js +187 -0
- package/scripts/postinstall-interactive.js +756 -0
- package/scripts/postinstall_binary.js +287 -0
- package/scripts/release_manifest.py +273 -0
- package/scripts/slm-hook.spec +56 -0
- package/skills/slm-build-graph/SKILL.md +423 -0
- package/skills/slm-list-recent/SKILL.md +348 -0
- package/skills/slm-recall/SKILL.md +343 -0
- package/skills/slm-remember/SKILL.md +194 -0
- package/skills/slm-show-patterns/SKILL.md +224 -0
- package/skills/slm-status/SKILL.md +363 -0
- package/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/cli/commands.py +254 -79
- package/src/superlocalmemory/cli/context_commands.py +192 -0
- package/src/superlocalmemory/cli/daemon.py +15 -1
- package/src/superlocalmemory/cli/db_migrate.py +80 -0
- package/src/superlocalmemory/cli/escape_hatch.py +220 -0
- package/src/superlocalmemory/cli/main.py +72 -1
- package/src/superlocalmemory/core/context_cache.py +397 -0
- package/src/superlocalmemory/core/engine.py +38 -2
- package/src/superlocalmemory/core/engine_wiring.py +1 -1
- package/src/superlocalmemory/core/ram_lock.py +111 -0
- package/src/superlocalmemory/core/recall_pipeline.py +433 -3
- package/src/superlocalmemory/core/recall_worker.py +8 -3
- package/src/superlocalmemory/core/security_primitives.py +635 -0
- package/src/superlocalmemory/core/shadow_router.py +319 -0
- package/src/superlocalmemory/core/slm_disabled.py +87 -0
- package/src/superlocalmemory/core/slmignore.py +125 -0
- package/src/superlocalmemory/core/topic_signature.py +143 -0
- package/src/superlocalmemory/core/worker_pool.py +14 -3
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +2 -2
- package/src/superlocalmemory/evolution/budget.py +321 -0
- package/src/superlocalmemory/evolution/llm_dispatch.py +508 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +144 -94
- package/src/superlocalmemory/hooks/_outcome_common.py +506 -0
- package/src/superlocalmemory/hooks/adapter_base.py +317 -0
- package/src/superlocalmemory/hooks/antigravity_adapter.py +192 -0
- package/src/superlocalmemory/hooks/claude_code_hooks.py +33 -1
- package/src/superlocalmemory/hooks/context_payload.py +312 -0
- package/src/superlocalmemory/hooks/copilot_adapter.py +154 -0
- package/src/superlocalmemory/hooks/cross_platform_connector.py +90 -0
- package/src/superlocalmemory/hooks/cursor_adapter.py +195 -0
- package/src/superlocalmemory/hooks/hook_handlers.py +109 -8
- package/src/superlocalmemory/hooks/ide_connector.py +25 -2
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +165 -0
- package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +223 -0
- package/src/superlocalmemory/hooks/prewarm_auth.py +170 -0
- package/src/superlocalmemory/hooks/session_registry.py +186 -0
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +134 -0
- package/src/superlocalmemory/hooks/sync_loop.py +114 -0
- package/src/superlocalmemory/hooks/user_prompt_hook.py +128 -0
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +202 -0
- package/src/superlocalmemory/infra/backup.py +3 -3
- package/src/superlocalmemory/infra/cloud_backup.py +2 -2
- package/src/superlocalmemory/infra/event_bus.py +2 -2
- package/src/superlocalmemory/infra/webhook_dispatcher.py +3 -3
- package/src/superlocalmemory/learning/arm_catalog.py +99 -0
- package/src/superlocalmemory/learning/bandit.py +526 -0
- package/src/superlocalmemory/learning/bandit_cache.py +133 -0
- package/src/superlocalmemory/learning/behavioral.py +53 -1
- package/src/superlocalmemory/learning/consolidation_cycle.py +381 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +188 -520
- package/src/superlocalmemory/learning/database.py +256 -0
- package/src/superlocalmemory/learning/dedup_hnsw.py +413 -0
- package/src/superlocalmemory/learning/ensemble.py +300 -0
- package/src/superlocalmemory/learning/fact_outcome_joins.py +207 -0
- package/src/superlocalmemory/learning/forgetting_scheduler.py +55 -0
- package/src/superlocalmemory/learning/hnsw_dedup.py +69 -0
- package/src/superlocalmemory/learning/labeler.py +87 -0
- package/src/superlocalmemory/learning/legacy_migration.py +277 -0
- package/src/superlocalmemory/learning/memory_merge.py +160 -0
- package/src/superlocalmemory/learning/model_cache.py +269 -0
- package/src/superlocalmemory/learning/model_rollback.py +278 -0
- package/src/superlocalmemory/learning/outcome_queue.py +284 -0
- package/src/superlocalmemory/learning/pattern_miner.py +415 -0
- package/src/superlocalmemory/learning/pattern_miner_constants.py +47 -0
- package/src/superlocalmemory/learning/ranker.py +225 -81
- package/src/superlocalmemory/learning/ranker_common.py +163 -0
- package/src/superlocalmemory/learning/ranker_retrain_legacy.py +202 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +411 -0
- package/src/superlocalmemory/learning/reward.py +777 -0
- package/src/superlocalmemory/learning/reward_archive.py +210 -0
- package/src/superlocalmemory/learning/reward_boost.py +201 -0
- package/src/superlocalmemory/learning/reward_proxy.py +326 -0
- package/src/superlocalmemory/learning/shadow_test.py +524 -0
- package/src/superlocalmemory/learning/signal_worker.py +270 -0
- package/src/superlocalmemory/learning/signals.py +314 -0
- package/src/superlocalmemory/learning/trigram_index.py +547 -0
- package/src/superlocalmemory/mcp/server.py +5 -5
- package/src/superlocalmemory/mcp/tools_context.py +183 -0
- package/src/superlocalmemory/mcp/tools_core.py +92 -27
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +13 -0
- package/src/superlocalmemory/retrieval/engine.py +52 -0
- package/src/superlocalmemory/server/api.py +2 -2
- package/src/superlocalmemory/server/bandit_loops.py +140 -0
- package/src/superlocalmemory/server/middleware/__init__.py +11 -0
- package/src/superlocalmemory/server/middleware/security_headers.py +144 -0
- package/src/superlocalmemory/server/routes/backup.py +36 -13
- package/src/superlocalmemory/server/routes/behavioral.py +50 -19
- package/src/superlocalmemory/server/routes/brain.py +1234 -0
- package/src/superlocalmemory/server/routes/data_io.py +4 -4
- package/src/superlocalmemory/server/routes/events.py +2 -2
- package/src/superlocalmemory/server/routes/helpers.py +1 -1
- package/src/superlocalmemory/server/routes/learning.py +192 -7
- package/src/superlocalmemory/server/routes/memories.py +189 -1
- package/src/superlocalmemory/server/routes/prewarm.py +171 -0
- package/src/superlocalmemory/server/routes/profiles.py +3 -3
- package/src/superlocalmemory/server/routes/token.py +88 -0
- package/src/superlocalmemory/server/routes/ws.py +5 -5
- package/src/superlocalmemory/server/security_middleware.py +13 -7
- package/src/superlocalmemory/server/ui.py +2 -2
- package/src/superlocalmemory/server/unified_daemon.py +335 -3
- package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +423 -0
- package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +348 -0
- package/src/superlocalmemory/skills/slm-recall/SKILL.md +343 -0
- package/src/superlocalmemory/skills/slm-remember/SKILL.md +194 -0
- package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +224 -0
- package/src/superlocalmemory/skills/slm-status/SKILL.md +363 -0
- package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/storage/migration_runner.py +545 -0
- package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +67 -0
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +132 -0
- package/src/superlocalmemory/storage/migrations/M003_migration_log.py +38 -0
- package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +46 -0
- package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +75 -0
- package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +75 -0
- package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +63 -0
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +54 -0
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +75 -0
- package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +87 -0
- package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +72 -0
- package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +55 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +81 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/ui/css/brain.css +409 -0
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +645 -0
- package/src/superlocalmemory/ui/index.html +459 -1345
- package/src/superlocalmemory/ui/js/brain.js +1321 -0
- package/src/superlocalmemory/ui/js/clusters.js +123 -4
- package/src/superlocalmemory/ui/js/init.js +48 -39
- package/src/superlocalmemory/ui/js/memories.js +88 -2
- package/src/superlocalmemory/ui/js/modal.js +71 -1
- package/src/superlocalmemory/ui/js/ng-shell.js +101 -88
- package/src/superlocalmemory/ui/js/trust-dashboard.js +168 -25
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +2018 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +7 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.min.css +6 -0
- package/src/superlocalmemory/ui/vendor/d3.v7.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology-library.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +8 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/sigma.min.js +1 -0
- package/src/superlocalmemory/ui/js/behavioral.js +0 -447
- package/src/superlocalmemory/ui/js/graph-core.js +0 -447
- package/src/superlocalmemory/ui/js/graph-interactions.js +0 -351
- package/src/superlocalmemory/ui/js/learning.js +0 -435
- package/src/superlocalmemory/ui/js/patterns.js +0 -93
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -647
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -335
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -58
- package/src/superlocalmemory.egg-info/top_level.txt +0 -1
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
// Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
// Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
// Part of SuperLocalMemory v3.4.21 — LLD-04 §4.3 (v2)
|
|
4
|
+
//
|
|
5
|
+
// XSS-safe Brain renderer. Hard rule U7: no unsafe DOM-injection
|
|
6
|
+
// sinks (banned by static grep). Every DOM node is built via
|
|
7
|
+
// document.createElement + textContent. Tooltips use
|
|
8
|
+
// setAttribute('title', string) which the browser auto-escapes.
|
|
9
|
+
//
|
|
10
|
+
// Design principle (April 18, 2026): ONE honest view. Every real ML
|
|
11
|
+
// signal, every adapter, every statistical counter, every interactive
|
|
12
|
+
// control is on the page. No hidden developer mode. No toggles. Non-
|
|
13
|
+
// technical users see exactly what the system is doing and why.
|
|
14
|
+
//
|
|
15
|
+
// Primary endpoint: /api/v3/brain (install-token gated, auto-fetched).
|
|
16
|
+
// Secondary endpoint: /api/behavioral/status (open on loopback).
|
|
17
|
+
// Interactive endpoints:
|
|
18
|
+
// - POST /api/behavioral/report-outcome (Report Outcome form)
|
|
19
|
+
// - POST /api/learning/reset (Reset Learning button)
|
|
20
|
+
|
|
21
|
+
(() => {
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const TOKEN_STORAGE_KEY = 'slm_install_token';
|
|
25
|
+
|
|
26
|
+
// --------------------------------------------------------------------
|
|
27
|
+
// Safe DOM helper — the ONLY way this file creates nodes.
|
|
28
|
+
// --------------------------------------------------------------------
|
|
29
|
+
function EL(tag, props, kids) {
|
|
30
|
+
const el = document.createElement(tag);
|
|
31
|
+
const p = props || {};
|
|
32
|
+
for (const k in p) {
|
|
33
|
+
if (!Object.prototype.hasOwnProperty.call(p, k)) continue;
|
|
34
|
+
const v = p[k];
|
|
35
|
+
if (k === 'className') {
|
|
36
|
+
el.className = v;
|
|
37
|
+
} else if (k === 'text') {
|
|
38
|
+
el.textContent = v == null ? '' : String(v);
|
|
39
|
+
} else if (k.length > 2 && k.slice(0, 2) === 'on') {
|
|
40
|
+
el.addEventListener(k.slice(2).toLowerCase(), v);
|
|
41
|
+
} else {
|
|
42
|
+
el.setAttribute(k, String(v));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const children = kids || [];
|
|
46
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
47
|
+
const c = children[i];
|
|
48
|
+
if (c != null) el.appendChild(c);
|
|
49
|
+
}
|
|
50
|
+
return el;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function withTooltip(node, opts) {
|
|
54
|
+
const o = opts || {};
|
|
55
|
+
const src = o.source ? 'Source: ' + o.source + '. ' : '';
|
|
56
|
+
const ml = o.isRealMl === false ? 'Statistical counter, not ML. ' : '';
|
|
57
|
+
const note = o.note || '';
|
|
58
|
+
node.setAttribute('title', src + ml + note);
|
|
59
|
+
return node;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function badge(kind, label) {
|
|
63
|
+
return EL('span', {
|
|
64
|
+
className: 'brain-honesty-badge ' + kind,
|
|
65
|
+
text: label,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --------------------------------------------------------------------
|
|
70
|
+
// Token handling — auto-fetch from local daemon, never prompt user.
|
|
71
|
+
// --------------------------------------------------------------------
|
|
72
|
+
function readToken() {
|
|
73
|
+
try {
|
|
74
|
+
return window.sessionStorage
|
|
75
|
+
? window.sessionStorage.getItem(TOKEN_STORAGE_KEY)
|
|
76
|
+
: null;
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writeToken(value) {
|
|
83
|
+
try {
|
|
84
|
+
if (window.sessionStorage) {
|
|
85
|
+
window.sessionStorage.setItem(TOKEN_STORAGE_KEY, value);
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
// storage disabled — keep in-memory.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function fetchTokenFromServer() {
|
|
93
|
+
try {
|
|
94
|
+
const resp = await fetch('/internal/token', {
|
|
95
|
+
credentials: 'same-origin',
|
|
96
|
+
});
|
|
97
|
+
if (!resp.ok) return null;
|
|
98
|
+
const data = await resp.json();
|
|
99
|
+
const tok = data && typeof data.token === 'string'
|
|
100
|
+
? data.token.trim() : '';
|
|
101
|
+
if (tok) {
|
|
102
|
+
writeToken(tok);
|
|
103
|
+
return tok;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function ensureToken() {
|
|
112
|
+
const cached = readToken();
|
|
113
|
+
if (cached) return cached;
|
|
114
|
+
return fetchTokenFromServer();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --------------------------------------------------------------------
|
|
118
|
+
// Network
|
|
119
|
+
// --------------------------------------------------------------------
|
|
120
|
+
async function fetchBrain() {
|
|
121
|
+
let token = await ensureToken();
|
|
122
|
+
const headers = {};
|
|
123
|
+
if (token) headers['X-Install-Token'] = token;
|
|
124
|
+
let resp = await fetch('/api/v3/brain', {
|
|
125
|
+
headers: headers,
|
|
126
|
+
credentials: 'same-origin',
|
|
127
|
+
});
|
|
128
|
+
if (resp.status === 401) {
|
|
129
|
+
token = await fetchTokenFromServer();
|
|
130
|
+
if (token) {
|
|
131
|
+
resp = await fetch('/api/v3/brain', {
|
|
132
|
+
headers: {'X-Install-Token': token},
|
|
133
|
+
credentials: 'same-origin',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!resp.ok) throw new Error('brain_fetch_failed:' + resp.status);
|
|
138
|
+
return resp.json();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function fetchBehavioralStatus() {
|
|
142
|
+
try {
|
|
143
|
+
const resp = await fetch('/api/behavioral/status', {
|
|
144
|
+
credentials: 'same-origin',
|
|
145
|
+
});
|
|
146
|
+
if (!resp.ok) return null;
|
|
147
|
+
return resp.json();
|
|
148
|
+
} catch (e) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function postReportOutcome(body) {
|
|
154
|
+
const resp = await fetch('/api/behavioral/report-outcome', {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: {'Content-Type': 'application/json'},
|
|
157
|
+
credentials: 'same-origin',
|
|
158
|
+
body: JSON.stringify(body),
|
|
159
|
+
});
|
|
160
|
+
if (!resp.ok) throw new Error('report_outcome_failed:' + resp.status);
|
|
161
|
+
return resp.json();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function postResetLearning() {
|
|
165
|
+
const resp = await fetch('/api/learning/reset', {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
credentials: 'same-origin',
|
|
168
|
+
});
|
|
169
|
+
if (!resp.ok) throw new Error('reset_learning_failed:' + resp.status);
|
|
170
|
+
return resp.json();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// --------------------------------------------------------------------
|
|
174
|
+
// Formatting helpers
|
|
175
|
+
// --------------------------------------------------------------------
|
|
176
|
+
function fmtBytes(n) {
|
|
177
|
+
const v = Number(n) || 0;
|
|
178
|
+
if (v < 1024) return v + ' B';
|
|
179
|
+
if (v < 1024 * 1024) return (v / 1024).toFixed(1) + ' KB';
|
|
180
|
+
if (v < 1024 * 1024 * 1024) return (v / (1024 * 1024)).toFixed(1) + ' MB';
|
|
181
|
+
return (v / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function fmtDate(iso) {
|
|
185
|
+
if (!iso) return '—';
|
|
186
|
+
try {
|
|
187
|
+
const d = new Date(iso);
|
|
188
|
+
if (isNaN(d.getTime())) return String(iso);
|
|
189
|
+
return d.toLocaleString();
|
|
190
|
+
} catch (e) {
|
|
191
|
+
return String(iso);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function statRow(label, value) {
|
|
196
|
+
return EL('div', {className: 'brain-stat-row'}, [
|
|
197
|
+
EL('span', {className: 'brain-stat-label', text: label}),
|
|
198
|
+
EL('span', {className: 'brain-stat-value', text: String(value)}),
|
|
199
|
+
]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// --------------------------------------------------------------------
|
|
203
|
+
// Card: Learning (phase, signals, model status)
|
|
204
|
+
// --------------------------------------------------------------------
|
|
205
|
+
// Threshold at which the LightGBM ranker becomes active (LLD-02 §4.10).
|
|
206
|
+
const ML_MODEL_THRESHOLD = 200;
|
|
207
|
+
|
|
208
|
+
async function postRetrain(includeSynthetic) {
|
|
209
|
+
const resp = await fetch('/api/learning/retrain', {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: {'Content-Type': 'application/json'},
|
|
212
|
+
credentials: 'same-origin',
|
|
213
|
+
body: JSON.stringify({include_synthetic: !!includeSynthetic}),
|
|
214
|
+
});
|
|
215
|
+
if (!resp.ok) throw new Error('retrain_failed:' + resp.status);
|
|
216
|
+
return resp.json();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function postMigrateLegacy() {
|
|
220
|
+
const resp = await fetch('/api/learning/migrate-legacy', {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
credentials: 'same-origin',
|
|
223
|
+
});
|
|
224
|
+
if (!resp.ok) throw new Error('migrate_failed:' + resp.status);
|
|
225
|
+
return resp.json();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function cardLearning(l) {
|
|
229
|
+
const data = l || {};
|
|
230
|
+
const wrap = EL('section', {className: 'brain-section'});
|
|
231
|
+
wrap.appendChild(EL('h4', {text: "How I'm getting smarter"}));
|
|
232
|
+
|
|
233
|
+
const phase = data.phase || 1;
|
|
234
|
+
const label = data.phase_label || 'Cold start';
|
|
235
|
+
wrap.appendChild(EL('span', {
|
|
236
|
+
className: 'brain-phase-pill active',
|
|
237
|
+
text: 'Phase ' + phase + ' — ' + label,
|
|
238
|
+
}));
|
|
239
|
+
|
|
240
|
+
// Progress toward ML activation — the headline thing.
|
|
241
|
+
const signals = data.signals_total || 0;
|
|
242
|
+
const active = !!data.model_active;
|
|
243
|
+
const threshold = ML_MODEL_THRESHOLD;
|
|
244
|
+
const pct = Math.min(100, Math.round((signals / threshold) * 100));
|
|
245
|
+
|
|
246
|
+
const progWrap = EL('div', {className: 'brain-progress-wrap'});
|
|
247
|
+
progWrap.appendChild(EL('div', {
|
|
248
|
+
className: 'brain-progress-label',
|
|
249
|
+
text: active
|
|
250
|
+
? 'LightGBM ranker trained and active'
|
|
251
|
+
: signals + ' / ' + threshold + ' signals collected',
|
|
252
|
+
}));
|
|
253
|
+
const bar = EL('div', {className: 'brain-progress-bar'});
|
|
254
|
+
bar.appendChild(EL('div', {
|
|
255
|
+
className: 'brain-progress-fill' + (active ? ' done' : ''),
|
|
256
|
+
style: 'width:' + (active ? 100 : pct) + '%',
|
|
257
|
+
}));
|
|
258
|
+
progWrap.appendChild(bar);
|
|
259
|
+
wrap.appendChild(progWrap);
|
|
260
|
+
|
|
261
|
+
// Plain-English explanation for non-technical users.
|
|
262
|
+
const helpText = active
|
|
263
|
+
? 'I have learned from your usage and trained a LightGBM model. '
|
|
264
|
+
+ "It's actively re-ranking recall results now."
|
|
265
|
+
: (signals >= threshold
|
|
266
|
+
? 'Enough signals collected. You can train the model now, or '
|
|
267
|
+
+ 'wait — it trains automatically on the next cycle.'
|
|
268
|
+
: 'I need ' + (threshold - signals) + ' more real recalls to '
|
|
269
|
+
+ 'train the LightGBM ranker. Keep using SLM — no action '
|
|
270
|
+
+ 'needed. It activates automatically.');
|
|
271
|
+
wrap.appendChild(EL('p', {
|
|
272
|
+
className: 'brain-help',
|
|
273
|
+
text: helpText,
|
|
274
|
+
}));
|
|
275
|
+
|
|
276
|
+
// Manual training trigger — always present so the user can run a
|
|
277
|
+
// training cycle on demand. Behaviour depends on available data:
|
|
278
|
+
// below threshold with no migrated legacy data → disabled with help;
|
|
279
|
+
// below threshold WITH migrated legacy rows → "Train on all data"
|
|
280
|
+
// including synthetic; at/above threshold → "Train model now".
|
|
281
|
+
const migrated = data.legacy_migrated_count || 0;
|
|
282
|
+
if (!active) {
|
|
283
|
+
const hasLegacy = migrated > 0;
|
|
284
|
+
const canTrain = signals >= threshold || hasLegacy;
|
|
285
|
+
const trainLabel = hasLegacy
|
|
286
|
+
? 'Train model now (includes ' + migrated + ' legacy rows)'
|
|
287
|
+
: 'Train model now';
|
|
288
|
+
const trainBtn = EL('button', {
|
|
289
|
+
type: 'button',
|
|
290
|
+
className: 'btn btn-sm btn-primary',
|
|
291
|
+
text: trainLabel,
|
|
292
|
+
});
|
|
293
|
+
if (!canTrain) trainBtn.setAttribute('disabled', 'disabled');
|
|
294
|
+
const trainStatus = EL('span', {
|
|
295
|
+
className: 'brain-form-status',
|
|
296
|
+
role: 'status',
|
|
297
|
+
'aria-live': 'polite',
|
|
298
|
+
});
|
|
299
|
+
trainBtn.addEventListener('click', async () => {
|
|
300
|
+
if (!canTrain) return;
|
|
301
|
+
trainStatus.textContent = 'Training…';
|
|
302
|
+
try {
|
|
303
|
+
const r = await postRetrain(hasLegacy);
|
|
304
|
+
if (r && r.trained) {
|
|
305
|
+
trainStatus.textContent = 'Training complete. Refreshing…';
|
|
306
|
+
setTimeout(loadBrain, 1200);
|
|
307
|
+
} else {
|
|
308
|
+
trainStatus.textContent = (r && r.message)
|
|
309
|
+
|| 'Not enough training rows yet.';
|
|
310
|
+
}
|
|
311
|
+
} catch (e) {
|
|
312
|
+
trainStatus.textContent = 'Could not start training — try again.';
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
wrap.appendChild(trainBtn);
|
|
316
|
+
wrap.appendChild(trainStatus);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const grid = EL('div', {className: 'brain-stat-grid'});
|
|
320
|
+
grid.appendChild(statRow(
|
|
321
|
+
'Features computed', String(data.features_total || 0)
|
|
322
|
+
+ ' / ' + String(data.feature_count_expected || 0),
|
|
323
|
+
));
|
|
324
|
+
grid.appendChild(statRow(
|
|
325
|
+
'Model version', data.model_version || '—',
|
|
326
|
+
));
|
|
327
|
+
grid.appendChild(statRow(
|
|
328
|
+
'Model trained', fmtDate(data.model_trained_at),
|
|
329
|
+
));
|
|
330
|
+
grid.appendChild(statRow(
|
|
331
|
+
'Historic rows migrated', migrated,
|
|
332
|
+
));
|
|
333
|
+
wrap.appendChild(grid);
|
|
334
|
+
wrap.appendChild(badge('real', 'real ML'));
|
|
335
|
+
return wrap;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// --------------------------------------------------------------------
|
|
339
|
+
// Card: Legacy migration (only shows when there are un-migrated rows)
|
|
340
|
+
// --------------------------------------------------------------------
|
|
341
|
+
// S9-DASH-03: read a persistent dismissal flag so the card stops
|
|
342
|
+
// nagging once the migration endpoint has reported ``already_done``.
|
|
343
|
+
// Previously this card showed forever while ``pending > 0``, even
|
|
344
|
+
// when the 20 remaining rows were permanently un-migratable stubs
|
|
345
|
+
// (malformed / duplicate). The dismissal is keyed to a migration
|
|
346
|
+
// version so a future re-run with a new sentinel still surfaces.
|
|
347
|
+
function _getMigrationDismissed() {
|
|
348
|
+
try {
|
|
349
|
+
return window.localStorage.getItem('slm_migrate_legacy_done') === '1';
|
|
350
|
+
} catch (e) { return false; }
|
|
351
|
+
}
|
|
352
|
+
function _setMigrationDismissed() {
|
|
353
|
+
try { window.localStorage.setItem('slm_migrate_legacy_done', '1'); }
|
|
354
|
+
catch (e) { /* ignore quota / privacy-mode */ }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function cardLegacyMigration(l) {
|
|
358
|
+
const data = l || {};
|
|
359
|
+
const pending = data.legacy_migration_pending || 0;
|
|
360
|
+
if (pending <= 0) return null; // nothing to show when everything migrated
|
|
361
|
+
if (_getMigrationDismissed()) return null; // user completed it already
|
|
362
|
+
|
|
363
|
+
const wrap = EL('section', {className: 'brain-section brain-migration'});
|
|
364
|
+
wrap.appendChild(EL('h4', {text: 'Historic data — ready to migrate'}));
|
|
365
|
+
wrap.appendChild(EL('p', {
|
|
366
|
+
className: 'brain-help',
|
|
367
|
+
text: 'I found ' + pending + ' historic feedback rows from before '
|
|
368
|
+
+ "v3.4.21. Migrate them into the new learning tables so I can "
|
|
369
|
+
+ 'use them to train the LightGBM ranker. Your memories are '
|
|
370
|
+
+ 'untouched — this only copies feedback metadata forward.',
|
|
371
|
+
}));
|
|
372
|
+
|
|
373
|
+
const btn = EL('button', {
|
|
374
|
+
type: 'button',
|
|
375
|
+
className: 'btn btn-sm btn-primary',
|
|
376
|
+
text: 'Migrate ' + pending + ' rows',
|
|
377
|
+
});
|
|
378
|
+
const status = EL('span', {
|
|
379
|
+
className: 'brain-form-status',
|
|
380
|
+
role: 'status',
|
|
381
|
+
'aria-live': 'polite',
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
btn.addEventListener('click', async () => {
|
|
385
|
+
btn.setAttribute('disabled', 'disabled');
|
|
386
|
+
status.textContent = 'Migrating…';
|
|
387
|
+
try {
|
|
388
|
+
const r = await postMigrateLegacy();
|
|
389
|
+
const copied = (r && r.copied != null) ? r.copied : 0;
|
|
390
|
+
if (r && r.already_done) {
|
|
391
|
+
// The migration previously completed; remaining rows are
|
|
392
|
+
// structurally un-migratable. Dismiss the card permanently
|
|
393
|
+
// so we don't nag on every page load.
|
|
394
|
+
status.textContent = 'Already migrated. ' + pending
|
|
395
|
+
+ ' rows could not be copied (malformed) — skipping.';
|
|
396
|
+
_setMigrationDismissed();
|
|
397
|
+
} else if (r && r.success !== false) {
|
|
398
|
+
status.textContent = 'Migrated ' + copied + ' rows. '
|
|
399
|
+
+ 'Reloading…';
|
|
400
|
+
// Also dismiss in case this run left a residual pending
|
|
401
|
+
// count that won't clear (e.g. sentinel written but a
|
|
402
|
+
// handful of rows failed per-row).
|
|
403
|
+
if (copied === 0) _setMigrationDismissed();
|
|
404
|
+
} else {
|
|
405
|
+
status.textContent = (r && r.error) || 'Migration failed.';
|
|
406
|
+
// Only re-enable on explicit failure so the user can retry.
|
|
407
|
+
btn.removeAttribute('disabled');
|
|
408
|
+
}
|
|
409
|
+
// Reload after a short delay so the refreshed brain state
|
|
410
|
+
// re-evaluates the card visibility. When dismissed, the card
|
|
411
|
+
// will not re-render at all.
|
|
412
|
+
setTimeout(loadBrain, 1000);
|
|
413
|
+
} catch (e) {
|
|
414
|
+
btn.removeAttribute('disabled');
|
|
415
|
+
status.textContent = 'Could not migrate — try again.';
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
wrap.appendChild(btn);
|
|
419
|
+
wrap.appendChild(status);
|
|
420
|
+
return wrap;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// --------------------------------------------------------------------
|
|
424
|
+
// Card: Bandit (adaptive ranking arms)
|
|
425
|
+
// --------------------------------------------------------------------
|
|
426
|
+
function cardBandit(b) {
|
|
427
|
+
const data = b || {};
|
|
428
|
+
const wrap = EL('section', {className: 'brain-section'});
|
|
429
|
+
wrap.appendChild(EL('h4', {text: 'Adaptive ranking (bandit)'}));
|
|
430
|
+
|
|
431
|
+
const top = data.top_arm_global;
|
|
432
|
+
const topText = top
|
|
433
|
+
? String(top.arm_id) + ' (plays=' + (top.plays || 0) + ')'
|
|
434
|
+
: '—';
|
|
435
|
+
|
|
436
|
+
const grid = EL('div', {className: 'brain-stat-grid'});
|
|
437
|
+
grid.appendChild(statRow('Top arm', topText));
|
|
438
|
+
grid.appendChild(statRow(
|
|
439
|
+
'Strata active',
|
|
440
|
+
String(data.strata_active || 0)
|
|
441
|
+
+ ' / ' + String(data.strata_total || 0),
|
|
442
|
+
));
|
|
443
|
+
grid.appendChild(statRow(
|
|
444
|
+
'Unsettled plays', data.unsettled_plays || 0,
|
|
445
|
+
));
|
|
446
|
+
grid.appendChild(statRow(
|
|
447
|
+
'Oldest unsettled',
|
|
448
|
+
(data.oldest_unsettled_seconds || 0) + ' s',
|
|
449
|
+
));
|
|
450
|
+
wrap.appendChild(grid);
|
|
451
|
+
wrap.appendChild(badge('real', 'real ML'));
|
|
452
|
+
return wrap;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// --------------------------------------------------------------------
|
|
456
|
+
// Card: Usage (recalls, top query types)
|
|
457
|
+
// --------------------------------------------------------------------
|
|
458
|
+
function cardUsage(u) {
|
|
459
|
+
const data = u || {};
|
|
460
|
+
const wrap = EL('section', {className: 'brain-section'});
|
|
461
|
+
wrap.appendChild(EL('h4', {text: 'How you use me'}));
|
|
462
|
+
const node = EL('p', {
|
|
463
|
+
text: 'Recalls in the last 24 hours: '
|
|
464
|
+
+ String(data.recalls_last_24h || 0),
|
|
465
|
+
});
|
|
466
|
+
withTooltip(node, {
|
|
467
|
+
source: data.source || 'behavioral_patterns_counters',
|
|
468
|
+
isRealMl: false,
|
|
469
|
+
note: data.disclaimer || '',
|
|
470
|
+
});
|
|
471
|
+
wrap.appendChild(node);
|
|
472
|
+
|
|
473
|
+
// Real payload keys: {type, pct}. Not {name, count}.
|
|
474
|
+
const types = data.top_query_types || [];
|
|
475
|
+
if (types.length > 0) {
|
|
476
|
+
wrap.appendChild(EL('h5', {
|
|
477
|
+
className: 'brain-subhead',
|
|
478
|
+
text: 'Top query types',
|
|
479
|
+
}));
|
|
480
|
+
const list = EL('ul', {className: 'brain-list'});
|
|
481
|
+
for (let i = 0; i < Math.min(types.length, 5); i += 1) {
|
|
482
|
+
const t = types[i] || {};
|
|
483
|
+
const name = t.type || t.name || '—';
|
|
484
|
+
const pct = (t.pct != null) ? Number(t.pct).toFixed(1) + '%'
|
|
485
|
+
: (t.count != null ? String(t.count) : '');
|
|
486
|
+
list.appendChild(EL('li', {
|
|
487
|
+
text: String(name) + ' — ' + pct,
|
|
488
|
+
}));
|
|
489
|
+
}
|
|
490
|
+
wrap.appendChild(list);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Time-of-day buckets — show when you actually use SLM.
|
|
494
|
+
const buckets = data.top_time_buckets || [];
|
|
495
|
+
if (buckets.length > 0) {
|
|
496
|
+
wrap.appendChild(EL('h5', {
|
|
497
|
+
className: 'brain-subhead',
|
|
498
|
+
text: 'When you use me most',
|
|
499
|
+
}));
|
|
500
|
+
const list = EL('ul', {className: 'brain-list'});
|
|
501
|
+
for (let i = 0; i < Math.min(buckets.length, 5); i += 1) {
|
|
502
|
+
const b = buckets[i] || {};
|
|
503
|
+
const name = b.bucket || b.name || '—';
|
|
504
|
+
const pct = (b.pct != null) ? Number(b.pct).toFixed(1) + '%'
|
|
505
|
+
: (b.count != null ? String(b.count) : '');
|
|
506
|
+
list.appendChild(EL('li', {
|
|
507
|
+
text: String(name) + ' — ' + pct,
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
510
|
+
wrap.appendChild(list);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
wrap.appendChild(badge('counter', 'statistical counter'));
|
|
514
|
+
return wrap;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// --------------------------------------------------------------------
|
|
518
|
+
// Card: Preferences (topics, entities, tech)
|
|
519
|
+
// --------------------------------------------------------------------
|
|
520
|
+
function cardPreferences(p) {
|
|
521
|
+
const data = p || {};
|
|
522
|
+
const wrap = EL('section', {className: 'brain-section'});
|
|
523
|
+
wrap.appendChild(EL('h4', {text: 'What I know about you'}));
|
|
524
|
+
|
|
525
|
+
const cols = EL('div', {className: 'brain-pref-cols'});
|
|
526
|
+
|
|
527
|
+
cols.appendChild(prefColumn(
|
|
528
|
+
'Topics', data.topics || [],
|
|
529
|
+
(t) => String(t.name) + ' — '
|
|
530
|
+
+ Math.round(Number(t.strength || 0) * 100) + '%',
|
|
531
|
+
'Topic clusters emerge from a larger memory set. Entities and '
|
|
532
|
+
+ 'tech on the right are already learned.',
|
|
533
|
+
));
|
|
534
|
+
cols.appendChild(prefColumn(
|
|
535
|
+
'Entities', (data.entities || []).slice(0, 12),
|
|
536
|
+
(e) => String(e.name) + ' · ' + (e.mention_count || 0),
|
|
537
|
+
));
|
|
538
|
+
cols.appendChild(prefColumn(
|
|
539
|
+
'Tech', (data.tech || []).slice(0, 12),
|
|
540
|
+
(t) => String(t.name) + ' · '
|
|
541
|
+
+ Math.round(Number(t.frequency || 0) * 100) + '%',
|
|
542
|
+
));
|
|
543
|
+
wrap.appendChild(cols);
|
|
544
|
+
|
|
545
|
+
const redacted = data.redacted_count || 0;
|
|
546
|
+
if (redacted > 0) {
|
|
547
|
+
wrap.appendChild(EL('p', {
|
|
548
|
+
className: 'brain-notice',
|
|
549
|
+
text: String(redacted) + ' values redacted as likely secrets',
|
|
550
|
+
}));
|
|
551
|
+
}
|
|
552
|
+
wrap.appendChild(badge('real', 'real data'));
|
|
553
|
+
return wrap;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function prefColumn(title, items, fmt, emptyMsg) {
|
|
557
|
+
const col = EL('div', {className: 'brain-pref-col'});
|
|
558
|
+
col.appendChild(EL('h5', {
|
|
559
|
+
className: 'brain-subhead',
|
|
560
|
+
text: title,
|
|
561
|
+
}));
|
|
562
|
+
const list = EL('ul', {className: 'brain-list'});
|
|
563
|
+
if (items.length === 0) {
|
|
564
|
+
list.appendChild(EL('li', {
|
|
565
|
+
className: 'brain-empty',
|
|
566
|
+
text: emptyMsg || 'None yet.',
|
|
567
|
+
}));
|
|
568
|
+
}
|
|
569
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
570
|
+
list.appendChild(EL('li', {text: fmt(items[i])}));
|
|
571
|
+
}
|
|
572
|
+
col.appendChild(list);
|
|
573
|
+
return col;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// --------------------------------------------------------------------
|
|
577
|
+
// Card: Behavioral Outcomes + learned patterns
|
|
578
|
+
// --------------------------------------------------------------------
|
|
579
|
+
function cardBehavioralOutcomes(bh) {
|
|
580
|
+
const data = bh || {};
|
|
581
|
+
const wrap = EL('section', {className: 'brain-section'});
|
|
582
|
+
wrap.appendChild(EL('h4', {text: 'Behavioral outcomes'}));
|
|
583
|
+
|
|
584
|
+
const breakdown = data.outcome_breakdown || {};
|
|
585
|
+
const total = data.total_outcomes || 0;
|
|
586
|
+
const patterns = data.patterns || [];
|
|
587
|
+
|
|
588
|
+
// Honest framing: the tiles count user-reported outcomes (via the
|
|
589
|
+
// "Teach the system" form below); the pattern list is auto-detected
|
|
590
|
+
// from your memories. Zero outcomes doesn't mean no learning — the
|
|
591
|
+
// patterns below are evidence of learning from memory structure.
|
|
592
|
+
if (total === 0 && patterns.length > 0) {
|
|
593
|
+
wrap.appendChild(EL('p', {
|
|
594
|
+
className: 'brain-help',
|
|
595
|
+
text: "I haven't received any outcome reports yet — those come "
|
|
596
|
+
+ 'from the form below. But I have auto-detected '
|
|
597
|
+
+ patterns.length + ' patterns from the structure of your '
|
|
598
|
+
+ 'memories (shown below). Reporting outcomes will sharpen '
|
|
599
|
+
+ 'the ranker further.',
|
|
600
|
+
}));
|
|
601
|
+
} else if (total === 0) {
|
|
602
|
+
wrap.appendChild(EL('p', {
|
|
603
|
+
className: 'brain-help',
|
|
604
|
+
text: "I haven't received any outcome reports yet. Use the form "
|
|
605
|
+
+ 'below to tell me what worked; each report improves ranking.',
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const tiles = EL('div', {className: 'brain-outcome-tiles'});
|
|
610
|
+
tiles.appendChild(outcomeTile(
|
|
611
|
+
'Reports', total, 'neutral',
|
|
612
|
+
));
|
|
613
|
+
tiles.appendChild(outcomeTile(
|
|
614
|
+
'Success', breakdown.success || 0, 'success',
|
|
615
|
+
));
|
|
616
|
+
tiles.appendChild(outcomeTile(
|
|
617
|
+
'Failure', breakdown.failure || 0, 'failure',
|
|
618
|
+
));
|
|
619
|
+
tiles.appendChild(outcomeTile(
|
|
620
|
+
'Partial', breakdown.partial || 0, 'warn',
|
|
621
|
+
));
|
|
622
|
+
wrap.appendChild(tiles);
|
|
623
|
+
|
|
624
|
+
if (patterns.length > 0) {
|
|
625
|
+
wrap.appendChild(EL('h5', {
|
|
626
|
+
className: 'brain-subhead',
|
|
627
|
+
text: 'Auto-detected patterns (' + patterns.length + ')',
|
|
628
|
+
}));
|
|
629
|
+
const list = EL('ul', {className: 'brain-list'});
|
|
630
|
+
for (let i = 0; i < Math.min(patterns.length, 10); i += 1) {
|
|
631
|
+
const p = patterns[i];
|
|
632
|
+
const rate = Math.round(Number(p.success_rate || 0) * 100);
|
|
633
|
+
const li = EL('li');
|
|
634
|
+
li.appendChild(EL('span', {
|
|
635
|
+
text: String(p.pattern_type || 'pattern') + ': '
|
|
636
|
+
+ String(p.pattern_key || '—') + ' · '
|
|
637
|
+
+ rate + '% · ' + (p.evidence_count || 0) + ' evidence ',
|
|
638
|
+
}));
|
|
639
|
+
// S9-DASH-04: delete button — one click kills a wrong pattern.
|
|
640
|
+
const del = EL('button', {
|
|
641
|
+
type: 'button',
|
|
642
|
+
className: 'brain-pattern-del',
|
|
643
|
+
text: '✕',
|
|
644
|
+
title: 'Delete this pattern',
|
|
645
|
+
style: 'margin-left:6px; font-size:11px; cursor:pointer; '
|
|
646
|
+
+ 'background:transparent; border:1px solid #555; '
|
|
647
|
+
+ 'color:#c88; border-radius:3px; padding:0 5px;',
|
|
648
|
+
});
|
|
649
|
+
del.addEventListener('click', async () => {
|
|
650
|
+
del.classList.add('slm-anim-click');
|
|
651
|
+
setTimeout(() => del.classList.remove('slm-anim-click'), 200);
|
|
652
|
+
del.setAttribute('disabled', 'disabled');
|
|
653
|
+
del.classList.add('slm-anim-spin');
|
|
654
|
+
del.textContent = '';
|
|
655
|
+
try {
|
|
656
|
+
const resp = await fetch('/api/patterns/delete', {
|
|
657
|
+
method: 'DELETE',
|
|
658
|
+
headers: {'Content-Type': 'application/json'},
|
|
659
|
+
credentials: 'same-origin',
|
|
660
|
+
body: JSON.stringify({
|
|
661
|
+
pattern_type: p.pattern_type || '',
|
|
662
|
+
pattern_key: p.pattern_key || '',
|
|
663
|
+
}),
|
|
664
|
+
});
|
|
665
|
+
const r = await resp.json();
|
|
666
|
+
del.classList.remove('slm-anim-spin');
|
|
667
|
+
if (r && r.success) {
|
|
668
|
+
li.classList.add('slm-anim-success');
|
|
669
|
+
setTimeout(() => {
|
|
670
|
+
li.style.opacity = '0.35';
|
|
671
|
+
li.style.textDecoration = 'line-through';
|
|
672
|
+
}, 400);
|
|
673
|
+
del.textContent = '✓';
|
|
674
|
+
} else {
|
|
675
|
+
li.classList.add('slm-anim-fail');
|
|
676
|
+
setTimeout(() => li.classList.remove('slm-anim-fail'), 700);
|
|
677
|
+
del.textContent = '✕';
|
|
678
|
+
del.removeAttribute('disabled');
|
|
679
|
+
}
|
|
680
|
+
} catch (e) {
|
|
681
|
+
del.classList.remove('slm-anim-spin');
|
|
682
|
+
li.classList.add('slm-anim-fail');
|
|
683
|
+
setTimeout(() => li.classList.remove('slm-anim-fail'), 700);
|
|
684
|
+
del.textContent = '✕';
|
|
685
|
+
del.removeAttribute('disabled');
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
li.appendChild(del);
|
|
689
|
+
list.appendChild(li);
|
|
690
|
+
}
|
|
691
|
+
wrap.appendChild(list);
|
|
692
|
+
}
|
|
693
|
+
wrap.appendChild(badge('real', 'real data'));
|
|
694
|
+
return wrap;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function outcomeTile(label, count, kind) {
|
|
698
|
+
const t = EL('div', {className: 'brain-outcome-tile ' + kind});
|
|
699
|
+
t.appendChild(EL('div', {
|
|
700
|
+
className: 'brain-outcome-count', text: String(count),
|
|
701
|
+
}));
|
|
702
|
+
t.appendChild(EL('div', {
|
|
703
|
+
className: 'brain-outcome-label', text: label,
|
|
704
|
+
}));
|
|
705
|
+
return t;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// --------------------------------------------------------------------
|
|
709
|
+
// Card: Report outcome form (interactive — teach the system)
|
|
710
|
+
// --------------------------------------------------------------------
|
|
711
|
+
function cardReportOutcome() {
|
|
712
|
+
const wrap = EL('section', {className: 'brain-section'});
|
|
713
|
+
wrap.appendChild(EL('h4', {text: 'Teach the system — report an outcome'}));
|
|
714
|
+
wrap.appendChild(EL('p', {
|
|
715
|
+
className: 'brain-help',
|
|
716
|
+
text: 'Tell me what worked. Each report improves ranking.',
|
|
717
|
+
}));
|
|
718
|
+
|
|
719
|
+
const form = EL('form', {
|
|
720
|
+
className: 'brain-form',
|
|
721
|
+
noValidate: 'true',
|
|
722
|
+
});
|
|
723
|
+
form.addEventListener('submit', async (e) => {
|
|
724
|
+
e.preventDefault();
|
|
725
|
+
await handleReportSubmit(form);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const ids = EL('input', {
|
|
729
|
+
type: 'text',
|
|
730
|
+
name: 'memory_ids',
|
|
731
|
+
placeholder: 'Memory IDs, comma-separated',
|
|
732
|
+
className: 'brain-input',
|
|
733
|
+
});
|
|
734
|
+
const outcomeSel = EL('select', {
|
|
735
|
+
name: 'outcome',
|
|
736
|
+
className: 'brain-input',
|
|
737
|
+
}, [
|
|
738
|
+
EL('option', {value: 'success', text: 'Success'}),
|
|
739
|
+
EL('option', {value: 'failure', text: 'Failure'}),
|
|
740
|
+
EL('option', {value: 'partial', text: 'Partial'}),
|
|
741
|
+
]);
|
|
742
|
+
const actionSel = EL('select', {
|
|
743
|
+
name: 'action_type',
|
|
744
|
+
className: 'brain-input',
|
|
745
|
+
}, [
|
|
746
|
+
EL('option', {value: 'code_written', text: 'Code written'}),
|
|
747
|
+
EL('option', {value: 'decision_made', text: 'Decision made'}),
|
|
748
|
+
EL('option', {value: 'debug_resolved', text: 'Debug resolved'}),
|
|
749
|
+
EL('option', {value: 'architecture_chosen', text: 'Architecture chosen'}),
|
|
750
|
+
EL('option', {value: 'other', text: 'Other'}),
|
|
751
|
+
]);
|
|
752
|
+
const context = EL('input', {
|
|
753
|
+
type: 'text',
|
|
754
|
+
name: 'context',
|
|
755
|
+
placeholder: 'Context (optional)',
|
|
756
|
+
className: 'brain-input',
|
|
757
|
+
});
|
|
758
|
+
const submit = EL('button', {
|
|
759
|
+
type: 'submit',
|
|
760
|
+
className: 'btn btn-sm btn-primary brain-form-submit',
|
|
761
|
+
text: 'Report',
|
|
762
|
+
});
|
|
763
|
+
const status = EL('span', {
|
|
764
|
+
className: 'brain-form-status',
|
|
765
|
+
role: 'status',
|
|
766
|
+
'aria-live': 'polite',
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
form.appendChild(ids);
|
|
770
|
+
form.appendChild(outcomeSel);
|
|
771
|
+
form.appendChild(actionSel);
|
|
772
|
+
form.appendChild(context);
|
|
773
|
+
form.appendChild(submit);
|
|
774
|
+
form.appendChild(status);
|
|
775
|
+
wrap.appendChild(form);
|
|
776
|
+
return wrap;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function handleReportSubmit(form) {
|
|
780
|
+
const ids = form.elements.namedItem('memory_ids');
|
|
781
|
+
const outcome = form.elements.namedItem('outcome');
|
|
782
|
+
const actionType = form.elements.namedItem('action_type');
|
|
783
|
+
const context = form.elements.namedItem('context');
|
|
784
|
+
const status = form.querySelector('.brain-form-status');
|
|
785
|
+
|
|
786
|
+
const rawIds = (ids && ids.value || '').trim();
|
|
787
|
+
if (!rawIds) {
|
|
788
|
+
if (status) status.textContent = 'Enter at least one memory ID.';
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const memoryIds = rawIds.split(',')
|
|
792
|
+
.map((s) => s.trim()).filter(Boolean);
|
|
793
|
+
|
|
794
|
+
const body = {
|
|
795
|
+
memory_ids: memoryIds,
|
|
796
|
+
outcome: outcome ? outcome.value : 'success',
|
|
797
|
+
action_type: actionType ? actionType.value : 'other',
|
|
798
|
+
context: context ? context.value : '',
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
const submitBtn = form.querySelector('.brain-form-submit');
|
|
802
|
+
if (submitBtn) {
|
|
803
|
+
submitBtn.classList.add('slm-anim-click');
|
|
804
|
+
setTimeout(() => submitBtn.classList.remove('slm-anim-click'), 200);
|
|
805
|
+
submitBtn.setAttribute('disabled', 'disabled');
|
|
806
|
+
submitBtn.classList.add('slm-anim-spin');
|
|
807
|
+
}
|
|
808
|
+
if (status) status.textContent = 'Reporting…';
|
|
809
|
+
try {
|
|
810
|
+
await postReportOutcome(body);
|
|
811
|
+
if (submitBtn) {
|
|
812
|
+
submitBtn.classList.remove('slm-anim-spin');
|
|
813
|
+
submitBtn.removeAttribute('disabled');
|
|
814
|
+
}
|
|
815
|
+
form.classList.add('slm-anim-success');
|
|
816
|
+
setTimeout(() => form.classList.remove('slm-anim-success'), 900);
|
|
817
|
+
if (status) status.textContent = 'Recorded. Thanks — I will learn from this.';
|
|
818
|
+
form.reset();
|
|
819
|
+
// Refresh the Brain so the outcome tiles update immediately.
|
|
820
|
+
setTimeout(loadBrain, 600);
|
|
821
|
+
} catch (e) {
|
|
822
|
+
if (submitBtn) {
|
|
823
|
+
submitBtn.classList.remove('slm-anim-spin');
|
|
824
|
+
submitBtn.removeAttribute('disabled');
|
|
825
|
+
}
|
|
826
|
+
form.classList.add('slm-anim-fail');
|
|
827
|
+
setTimeout(() => form.classList.remove('slm-anim-fail'), 700);
|
|
828
|
+
if (status) {
|
|
829
|
+
status.textContent = 'Could not record right now — try again.';
|
|
830
|
+
}
|
|
831
|
+
if (window.console && window.console.debug) {
|
|
832
|
+
window.console.debug('report outcome error:', e && e.message);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// --------------------------------------------------------------------
|
|
838
|
+
// Card: Cross-platform adapters
|
|
839
|
+
// --------------------------------------------------------------------
|
|
840
|
+
function cardCrossPlatform(cp) {
|
|
841
|
+
const data = cp || {};
|
|
842
|
+
const wrap = EL('section', {className: 'brain-section'});
|
|
843
|
+
wrap.appendChild(EL('h4', {text: 'Connected clients'}));
|
|
844
|
+
|
|
845
|
+
const grid = EL('div', {className: 'brain-adapter-grid'});
|
|
846
|
+
const order = [
|
|
847
|
+
['claude_code', 'Claude Code'],
|
|
848
|
+
['cursor', 'Cursor'],
|
|
849
|
+
['antigravity', 'Antigravity'],
|
|
850
|
+
['copilot', 'Copilot'],
|
|
851
|
+
['mcp', 'MCP clients'],
|
|
852
|
+
['cli', 'CLI'],
|
|
853
|
+
];
|
|
854
|
+
for (let i = 0; i < order.length; i += 1) {
|
|
855
|
+
const key = order[i][0];
|
|
856
|
+
const nice = order[i][1];
|
|
857
|
+
grid.appendChild(adapterTile(nice, data[key] || {}));
|
|
858
|
+
}
|
|
859
|
+
wrap.appendChild(grid);
|
|
860
|
+
wrap.appendChild(badge('real', 'real sync state'));
|
|
861
|
+
return wrap;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function adapterTile(nice, entry) {
|
|
865
|
+
const active = !!entry.active;
|
|
866
|
+
const tile = EL('div', {
|
|
867
|
+
className: 'brain-adapter-tile ' + (active ? 'on' : 'off'),
|
|
868
|
+
});
|
|
869
|
+
tile.appendChild(EL('div', {
|
|
870
|
+
className: 'brain-adapter-name', text: nice,
|
|
871
|
+
}));
|
|
872
|
+
tile.appendChild(EL('div', {
|
|
873
|
+
className: 'brain-adapter-dot '
|
|
874
|
+
+ (active ? 'dot-on' : 'dot-off'),
|
|
875
|
+
}));
|
|
876
|
+
tile.appendChild(EL('div', {
|
|
877
|
+
className: 'brain-adapter-detail',
|
|
878
|
+
text: active
|
|
879
|
+
? (entry.last_sync
|
|
880
|
+
? 'Synced ' + fmtDate(entry.last_sync)
|
|
881
|
+
: 'Connected')
|
|
882
|
+
: (entry.reason || 'Not connected'),
|
|
883
|
+
}));
|
|
884
|
+
return tile;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// --------------------------------------------------------------------
|
|
888
|
+
// Card: Cache (prewarm hit-rate, size)
|
|
889
|
+
// --------------------------------------------------------------------
|
|
890
|
+
function cardCache(c) {
|
|
891
|
+
const data = c || {};
|
|
892
|
+
const wrap = EL('section', {className: 'brain-section'});
|
|
893
|
+
wrap.appendChild(EL('h4', {text: 'Context cache'}));
|
|
894
|
+
const grid = EL('div', {className: 'brain-stat-grid'});
|
|
895
|
+
grid.appendChild(statRow('Cached entries', data.entry_count || 0));
|
|
896
|
+
grid.appendChild(statRow(
|
|
897
|
+
'Database size', fmtBytes(data.db_size_bytes || 0),
|
|
898
|
+
));
|
|
899
|
+
wrap.appendChild(grid);
|
|
900
|
+
wrap.appendChild(badge('counter', 'statistical counter'));
|
|
901
|
+
return wrap;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// --------------------------------------------------------------------
|
|
905
|
+
// SVG chart helper — XSS-safe (createElementNS + setAttribute only).
|
|
906
|
+
// --------------------------------------------------------------------
|
|
907
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
908
|
+
|
|
909
|
+
function svgEL(tag, attrs, kids) {
|
|
910
|
+
const el = document.createElementNS(SVG_NS, tag);
|
|
911
|
+
const a = attrs || {};
|
|
912
|
+
for (const k in a) {
|
|
913
|
+
if (!Object.prototype.hasOwnProperty.call(a, k)) continue;
|
|
914
|
+
el.setAttribute(k, String(a[k]));
|
|
915
|
+
}
|
|
916
|
+
const children = kids || [];
|
|
917
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
918
|
+
const c = children[i];
|
|
919
|
+
if (c != null) el.appendChild(c);
|
|
920
|
+
}
|
|
921
|
+
return el;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function renderEvolutionChart(points) {
|
|
925
|
+
const n = (points || []).length;
|
|
926
|
+
const W = 640, H = 160, PAD_L = 32, PAD_R = 12, PAD_T = 12, PAD_B = 22;
|
|
927
|
+
const plotW = W - PAD_L - PAD_R;
|
|
928
|
+
const plotH = H - PAD_T - PAD_B;
|
|
929
|
+
const svg = svgEL('svg', {
|
|
930
|
+
viewBox: '0 0 ' + W + ' ' + H,
|
|
931
|
+
preserveAspectRatio: 'xMidYMid meet',
|
|
932
|
+
role: 'img',
|
|
933
|
+
'aria-label': 'Daily learning signals over time',
|
|
934
|
+
className: 'brain-evolution-chart',
|
|
935
|
+
});
|
|
936
|
+
// Background.
|
|
937
|
+
svg.appendChild(svgEL('rect', {
|
|
938
|
+
x: 0, y: 0, width: W, height: H,
|
|
939
|
+
fill: 'rgba(255,255,255,0.02)',
|
|
940
|
+
}));
|
|
941
|
+
|
|
942
|
+
if (n === 0) {
|
|
943
|
+
svg.appendChild(svgEL('text', {
|
|
944
|
+
x: W / 2, y: H / 2, 'text-anchor': 'middle',
|
|
945
|
+
'dominant-baseline': 'middle', fill: 'rgba(255,255,255,0.55)',
|
|
946
|
+
'font-size': '12',
|
|
947
|
+
}, [document.createTextNode('No signals recorded yet')]));
|
|
948
|
+
return svg;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
let maxV = 0;
|
|
952
|
+
for (let i = 0; i < n; i += 1) {
|
|
953
|
+
const v = +points[i].signals || 0;
|
|
954
|
+
if (v > maxV) maxV = v;
|
|
955
|
+
}
|
|
956
|
+
const yMax = Math.max(1, maxV);
|
|
957
|
+
|
|
958
|
+
// Gridlines (4 horizontal).
|
|
959
|
+
for (let g = 0; g <= 4; g += 1) {
|
|
960
|
+
const y = PAD_T + (plotH * g) / 4;
|
|
961
|
+
svg.appendChild(svgEL('line', {
|
|
962
|
+
x1: PAD_L, y1: y, x2: W - PAD_R, y2: y,
|
|
963
|
+
stroke: 'rgba(255,255,255,0.06)', 'stroke-width': '1',
|
|
964
|
+
}));
|
|
965
|
+
const label = Math.round(yMax - (yMax * g) / 4);
|
|
966
|
+
svg.appendChild(svgEL('text', {
|
|
967
|
+
x: PAD_L - 4, y: y + 3, 'text-anchor': 'end',
|
|
968
|
+
fill: 'rgba(255,255,255,0.45)', 'font-size': '10',
|
|
969
|
+
}, [document.createTextNode(String(label))]));
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Line path.
|
|
973
|
+
const stepX = n > 1 ? plotW / (n - 1) : 0;
|
|
974
|
+
const coords = [];
|
|
975
|
+
for (let i = 0; i < n; i += 1) {
|
|
976
|
+
const x = PAD_L + stepX * i;
|
|
977
|
+
const v = +points[i].signals || 0;
|
|
978
|
+
const y = PAD_T + plotH - (plotH * v) / yMax;
|
|
979
|
+
coords.push([x, y]);
|
|
980
|
+
}
|
|
981
|
+
let d = '';
|
|
982
|
+
for (let i = 0; i < coords.length; i += 1) {
|
|
983
|
+
d += (i === 0 ? 'M' : 'L') + coords[i][0].toFixed(1)
|
|
984
|
+
+ ' ' + coords[i][1].toFixed(1);
|
|
985
|
+
}
|
|
986
|
+
svg.appendChild(svgEL('path', {
|
|
987
|
+
d: d, fill: 'none', stroke: '#7b9cff', 'stroke-width': '2',
|
|
988
|
+
'stroke-linejoin': 'round', 'stroke-linecap': 'round',
|
|
989
|
+
}));
|
|
990
|
+
|
|
991
|
+
// Area fill below the line.
|
|
992
|
+
if (coords.length > 1) {
|
|
993
|
+
const first = coords[0], last = coords[coords.length - 1];
|
|
994
|
+
const baseY = PAD_T + plotH;
|
|
995
|
+
const areaD = d + 'L' + last[0].toFixed(1) + ' ' + baseY
|
|
996
|
+
+ 'L' + first[0].toFixed(1) + ' ' + baseY + 'Z';
|
|
997
|
+
svg.appendChild(svgEL('path', {
|
|
998
|
+
d: areaD, fill: 'rgba(123,156,255,0.18)', stroke: 'none',
|
|
999
|
+
}));
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Point markers + native tooltips (browser-escaped).
|
|
1003
|
+
for (let i = 0; i < coords.length; i += 1) {
|
|
1004
|
+
const circle = svgEL('circle', {
|
|
1005
|
+
cx: coords[i][0].toFixed(1), cy: coords[i][1].toFixed(1),
|
|
1006
|
+
r: '2.5', fill: '#7b9cff',
|
|
1007
|
+
});
|
|
1008
|
+
const title = svgEL('title', {}, [document.createTextNode(
|
|
1009
|
+
points[i].date + ' — ' + (+points[i].signals || 0) + ' signal'
|
|
1010
|
+
+ (points[i].signals === 1 ? '' : 's'),
|
|
1011
|
+
)]);
|
|
1012
|
+
circle.appendChild(title);
|
|
1013
|
+
svg.appendChild(circle);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// X-axis labels: first, middle, last date.
|
|
1017
|
+
const pickIdx = n === 1 ? [0] : [0, Math.floor((n - 1) / 2), n - 1];
|
|
1018
|
+
for (let i = 0; i < pickIdx.length; i += 1) {
|
|
1019
|
+
const idx = pickIdx[i];
|
|
1020
|
+
const x = PAD_L + stepX * idx;
|
|
1021
|
+
svg.appendChild(svgEL('text', {
|
|
1022
|
+
x: x, y: H - 6, 'text-anchor': i === 0 ? 'start'
|
|
1023
|
+
: (i === pickIdx.length - 1 ? 'end' : 'middle'),
|
|
1024
|
+
fill: 'rgba(255,255,255,0.55)', 'font-size': '10',
|
|
1025
|
+
}, [document.createTextNode(points[idx].date.slice(5))]));
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return svg;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// --------------------------------------------------------------------
|
|
1032
|
+
// Card: Evolution over time (daily signal trend)
|
|
1033
|
+
// --------------------------------------------------------------------
|
|
1034
|
+
function cardEvolution(ev) {
|
|
1035
|
+
const data = ev || {};
|
|
1036
|
+
const points = Array.isArray(data.points) ? data.points : [];
|
|
1037
|
+
const total = +data.total_signals || 0;
|
|
1038
|
+
const days = +data.days || points.length || 0;
|
|
1039
|
+
|
|
1040
|
+
const wrap = EL('section', {className: 'brain-section'});
|
|
1041
|
+
wrap.appendChild(EL('h4', {text: 'Evolution over time'}));
|
|
1042
|
+
wrap.appendChild(EL('p', {
|
|
1043
|
+
className: 'brain-help',
|
|
1044
|
+
text: 'Daily learning-signal volume over the last '
|
|
1045
|
+
+ days + ' days. ' + total + ' total signal'
|
|
1046
|
+
+ (total === 1 ? '' : 's') + ' in window.',
|
|
1047
|
+
}));
|
|
1048
|
+
|
|
1049
|
+
const chartWrap = EL('div', {className: 'brain-chart-wrap'});
|
|
1050
|
+
chartWrap.appendChild(renderEvolutionChart(points));
|
|
1051
|
+
wrap.appendChild(chartWrap);
|
|
1052
|
+
|
|
1053
|
+
wrap.appendChild(withTooltip(
|
|
1054
|
+
badge('counter', 'real — learning_signals'),
|
|
1055
|
+
{source: 'learning_signals.created_at (grouped by day)',
|
|
1056
|
+
isRealMl: false,
|
|
1057
|
+
note: 'Counts are raw signal writes, not ML predictions.'},
|
|
1058
|
+
));
|
|
1059
|
+
return wrap;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// --------------------------------------------------------------------
|
|
1063
|
+
// Card: Danger zone (reset learning data)
|
|
1064
|
+
// --------------------------------------------------------------------
|
|
1065
|
+
function cardDangerZone() {
|
|
1066
|
+
const wrap = EL('section', {className: 'brain-section brain-danger'});
|
|
1067
|
+
wrap.appendChild(EL('h4', {text: 'Danger zone'}));
|
|
1068
|
+
wrap.appendChild(EL('p', {
|
|
1069
|
+
className: 'brain-help',
|
|
1070
|
+
text: 'Deletes learning.db and all learned signals. '
|
|
1071
|
+
+ 'Your memories are preserved.',
|
|
1072
|
+
}));
|
|
1073
|
+
const btn = EL('button', {
|
|
1074
|
+
type: 'button',
|
|
1075
|
+
className: 'btn btn-sm btn-outline-danger brain-danger-btn',
|
|
1076
|
+
text: 'Reset learning data',
|
|
1077
|
+
});
|
|
1078
|
+
const status = EL('span', {
|
|
1079
|
+
className: 'brain-form-status',
|
|
1080
|
+
role: 'status',
|
|
1081
|
+
'aria-live': 'polite',
|
|
1082
|
+
});
|
|
1083
|
+
btn.addEventListener('click', async () => {
|
|
1084
|
+
const ok = window.confirm(
|
|
1085
|
+
'Reset all learning data? Memories will be preserved, '
|
|
1086
|
+
+ 'but learned patterns and ranking signals will be deleted.',
|
|
1087
|
+
);
|
|
1088
|
+
if (!ok) return;
|
|
1089
|
+
status.textContent = 'Resetting…';
|
|
1090
|
+
try {
|
|
1091
|
+
await postResetLearning();
|
|
1092
|
+
status.textContent = 'Done. Starting fresh.';
|
|
1093
|
+
setTimeout(loadBrain, 800);
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
status.textContent = 'Could not reset — try again.';
|
|
1096
|
+
if (window.console && window.console.debug) {
|
|
1097
|
+
window.console.debug('reset learning error:', e && e.message);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
wrap.appendChild(btn);
|
|
1102
|
+
wrap.appendChild(status);
|
|
1103
|
+
return wrap;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// --------------------------------------------------------------------
|
|
1107
|
+
// Root render — one honest view, all sections
|
|
1108
|
+
// --------------------------------------------------------------------
|
|
1109
|
+
function renderAll(brain, behavioral) {
|
|
1110
|
+
const b = brain || {};
|
|
1111
|
+
const nodes = [
|
|
1112
|
+
cardLearning(b.learning),
|
|
1113
|
+
];
|
|
1114
|
+
// Only shows when there are legacy rows pending migration; hidden
|
|
1115
|
+
// once everything is migrated so the user isn't nagged.
|
|
1116
|
+
const migration = cardLegacyMigration(b.learning);
|
|
1117
|
+
if (migration) nodes.push(migration);
|
|
1118
|
+
nodes.push(cardBandit(b.bandit));
|
|
1119
|
+
nodes.push(cardUsage(b.usage));
|
|
1120
|
+
nodes.push(cardPreferences(b.preferences));
|
|
1121
|
+
nodes.push(cardBehavioralOutcomes(behavioral));
|
|
1122
|
+
nodes.push(cardReportOutcome());
|
|
1123
|
+
// S9-DASH-05: live closed-loop tiles — reward, shadow test,
|
|
1124
|
+
// evolution cost. Data already exposed via /api/v3/brain; we only
|
|
1125
|
+
// needed to render it.
|
|
1126
|
+
const rewardCard = cardRewardPreview(b.reward_preview);
|
|
1127
|
+
if (rewardCard) nodes.push(rewardCard);
|
|
1128
|
+
const shadowCard = cardShadowPreview(b.shadow_preview);
|
|
1129
|
+
if (shadowCard) nodes.push(shadowCard);
|
|
1130
|
+
const evoCostCard = cardEvolutionCostPreview(b.evolution_cost_preview);
|
|
1131
|
+
if (evoCostCard) nodes.push(evoCostCard);
|
|
1132
|
+
const oqCard = cardOutcomeQueue(b.outcome_queue);
|
|
1133
|
+
if (oqCard) nodes.push(oqCard);
|
|
1134
|
+
nodes.push(cardCrossPlatform(b.cross_platform));
|
|
1135
|
+
nodes.push(cardCache(b.cache));
|
|
1136
|
+
nodes.push(cardEvolution(b.evolution_preview));
|
|
1137
|
+
nodes.push(cardDangerZone());
|
|
1138
|
+
return nodes;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// --------------------------------------------------------------------
|
|
1142
|
+
// S9-DASH-05: live closed-loop tiles
|
|
1143
|
+
// --------------------------------------------------------------------
|
|
1144
|
+
function cardRewardPreview(rp) {
|
|
1145
|
+
if (!rp) return null;
|
|
1146
|
+
const wrap = EL('section', {className: 'brain-section'});
|
|
1147
|
+
wrap.appendChild(EL('h4', {text: 'Reward signal (last 24h)'}));
|
|
1148
|
+
wrap.appendChild(EL('p', {
|
|
1149
|
+
className: 'brain-help',
|
|
1150
|
+
text: 'Settled outcomes landing in action_outcomes — the labels '
|
|
1151
|
+
+ 'your LightGBM ranker trains on. Neutral (0.5) is the '
|
|
1152
|
+
+ 'reaper default when no hook signals accumulated.',
|
|
1153
|
+
}));
|
|
1154
|
+
const grid = EL('div', {className: 'brain-kv-grid'});
|
|
1155
|
+
grid.appendChild(kv('Rows (24h)', String(rp.rows_24h || 0)));
|
|
1156
|
+
grid.appendChild(kv('Mean reward', Number(rp.mean_reward_24h || 0).toFixed(3)));
|
|
1157
|
+
wrap.appendChild(grid);
|
|
1158
|
+
wrap.appendChild(badge(rp.is_real ? 'real' : 'stub', rp.source || ''));
|
|
1159
|
+
return wrap;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function cardShadowPreview(sp) {
|
|
1163
|
+
if (!sp) return null;
|
|
1164
|
+
const wrap = EL('section', {className: 'brain-section'});
|
|
1165
|
+
wrap.appendChild(EL('h4', {text: 'Shadow A/B test'}));
|
|
1166
|
+
const hasCandidate = sp.active_candidate_id !== null
|
|
1167
|
+
&& sp.active_candidate_id !== undefined;
|
|
1168
|
+
wrap.appendChild(EL('p', {
|
|
1169
|
+
className: 'brain-help',
|
|
1170
|
+
text: hasCandidate
|
|
1171
|
+
? 'A candidate ranker is being evaluated against the active '
|
|
1172
|
+
+ 'model. Paired NDCG@10 observations accumulate until Phase '
|
|
1173
|
+
+ 'A strong-stop (n=100) or Phase B full validation (n=885).'
|
|
1174
|
+
: 'No candidate in flight. Next retrain will fire when drift '
|
|
1175
|
+
+ 'is detected or on the 6-hour cadence.',
|
|
1176
|
+
}));
|
|
1177
|
+
const grid = EL('div', {className: 'brain-kv-grid'});
|
|
1178
|
+
grid.appendChild(kv(
|
|
1179
|
+
'Candidate id',
|
|
1180
|
+
hasCandidate ? String(sp.active_candidate_id) : 'none',
|
|
1181
|
+
));
|
|
1182
|
+
grid.appendChild(kv(
|
|
1183
|
+
'Paired observations', String(sp.paired_observations || 0),
|
|
1184
|
+
));
|
|
1185
|
+
grid.appendChild(kv(
|
|
1186
|
+
'Rollbacks (90d)', String(sp.rollback_count_90d || 0),
|
|
1187
|
+
));
|
|
1188
|
+
wrap.appendChild(grid);
|
|
1189
|
+
wrap.appendChild(badge(sp.is_real ? 'real' : 'stub', sp.source || ''));
|
|
1190
|
+
return wrap;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function cardEvolutionCostPreview(ec) {
|
|
1194
|
+
if (!ec) return null;
|
|
1195
|
+
const wrap = EL('section', {className: 'brain-section'});
|
|
1196
|
+
wrap.appendChild(EL('h4', {text: 'Evolution LLM cost (7d)'}));
|
|
1197
|
+
wrap.appendChild(EL('p', {
|
|
1198
|
+
className: 'brain-help',
|
|
1199
|
+
text: 'Skill-evolver LLM spend. Mode A never calls an LLM here; '
|
|
1200
|
+
+ 'Mode B uses Ollama (no cost); Mode C may spend API tokens.',
|
|
1201
|
+
}));
|
|
1202
|
+
const grid = EL('div', {className: 'brain-kv-grid'});
|
|
1203
|
+
grid.appendChild(kv('Calls (7d)', String(ec.calls_7d || 0)));
|
|
1204
|
+
grid.appendChild(kv('Cost (USD)', '$' + Number(ec.cost_usd_7d || 0).toFixed(4)));
|
|
1205
|
+
grid.appendChild(kv('Tokens in', String(ec.tokens_in_7d || 0)));
|
|
1206
|
+
grid.appendChild(kv('Tokens out', String(ec.tokens_out_7d || 0)));
|
|
1207
|
+
wrap.appendChild(grid);
|
|
1208
|
+
wrap.appendChild(badge(ec.is_real ? 'real' : 'stub', ec.source || ''));
|
|
1209
|
+
return wrap;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function cardOutcomeQueue(oq) {
|
|
1213
|
+
if (!oq) return null;
|
|
1214
|
+
const wrap = EL('section', {className: 'brain-section'});
|
|
1215
|
+
wrap.appendChild(EL('h4', {text: 'Outcome queue (producer health)'}));
|
|
1216
|
+
wrap.appendChild(EL('p', {
|
|
1217
|
+
className: 'brain-help',
|
|
1218
|
+
text: 'Non-blocking background pipeline that turns every recall '
|
|
1219
|
+
+ 'into a pending_outcome for the closed-loop learning. Drops '
|
|
1220
|
+
+ 'and failures are always zero on a healthy install.',
|
|
1221
|
+
}));
|
|
1222
|
+
const counters = oq.counters || {};
|
|
1223
|
+
const grid = EL('div', {className: 'brain-kv-grid'});
|
|
1224
|
+
grid.appendChild(kv('Queue depth', String(oq.queue_depth || 0)));
|
|
1225
|
+
grid.appendChild(kv('Pending now', String(oq.pending_outcomes_now || 0)));
|
|
1226
|
+
grid.appendChild(kv('Enqueued', String(counters.recall_enqueued || 0)));
|
|
1227
|
+
grid.appendChild(kv('Persisted', String(counters.recall_persisted || 0)));
|
|
1228
|
+
grid.appendChild(kv('Reaped (TTL)', String(counters.recall_reaped || 0)));
|
|
1229
|
+
grid.appendChild(kv('Drops', String(counters.recall_dropped_queue_full || 0)));
|
|
1230
|
+
grid.appendChild(kv('Persist fails', String(counters.recall_persist_failed || 0)));
|
|
1231
|
+
wrap.appendChild(grid);
|
|
1232
|
+
wrap.appendChild(badge(oq.is_real ? 'real' : 'stub', oq.source || ''));
|
|
1233
|
+
return wrap;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function kv(label, value) {
|
|
1237
|
+
const cell = EL('div', {className: 'brain-kv'});
|
|
1238
|
+
cell.appendChild(EL('div', {
|
|
1239
|
+
className: 'brain-kv-label', text: label,
|
|
1240
|
+
}));
|
|
1241
|
+
cell.appendChild(EL('div', {
|
|
1242
|
+
className: 'brain-kv-value', text: value,
|
|
1243
|
+
}));
|
|
1244
|
+
return cell;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function renderError(err) {
|
|
1248
|
+
const root = document.getElementById('brain-content');
|
|
1249
|
+
if (!root) return;
|
|
1250
|
+
root.replaceChildren();
|
|
1251
|
+
root.appendChild(EL('div', {
|
|
1252
|
+
className: 'brain-error',
|
|
1253
|
+
text: "Couldn't load Brain right now. The daemon may still be "
|
|
1254
|
+
+ 'warming up — try again in a moment.',
|
|
1255
|
+
}));
|
|
1256
|
+
const btn = EL('button', {
|
|
1257
|
+
className: 'btn btn-sm btn-outline-secondary',
|
|
1258
|
+
type: 'button',
|
|
1259
|
+
text: 'Retry',
|
|
1260
|
+
});
|
|
1261
|
+
btn.addEventListener('click', loadBrain);
|
|
1262
|
+
root.appendChild(btn);
|
|
1263
|
+
if (err && err.message && window.console && window.console.debug) {
|
|
1264
|
+
window.console.debug('brain load error:', err.message);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function renderInto(nodes) {
|
|
1269
|
+
const root = document.getElementById('brain-content');
|
|
1270
|
+
if (!root) return;
|
|
1271
|
+
root.replaceChildren();
|
|
1272
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
1273
|
+
root.appendChild(nodes[i]);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
async function loadBrain() {
|
|
1278
|
+
try {
|
|
1279
|
+
const [brain, behavioral] = await Promise.all([
|
|
1280
|
+
fetchBrain(),
|
|
1281
|
+
fetchBehavioralStatus(),
|
|
1282
|
+
]);
|
|
1283
|
+
renderInto(renderAll(brain, behavioral));
|
|
1284
|
+
} catch (e) {
|
|
1285
|
+
renderError(e);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// --------------------------------------------------------------------
|
|
1290
|
+
// Public surface + boot
|
|
1291
|
+
// --------------------------------------------------------------------
|
|
1292
|
+
window.loadBrain = loadBrain;
|
|
1293
|
+
// Kept for backward compatibility with any scripts that still call
|
|
1294
|
+
// the old toggleBrainView() — it is a no-op now. The Brain view is
|
|
1295
|
+
// always the full view; there is no developer-only mode.
|
|
1296
|
+
window.toggleBrainView = function toggleBrainView() {
|
|
1297
|
+
loadBrain();
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
function initBrainBoot() {
|
|
1301
|
+
document.addEventListener('shown.bs.tab', (event) => {
|
|
1302
|
+
const t = event && event.target;
|
|
1303
|
+
if (t && t.id === 'brain-tab') setTimeout(loadBrain, 0);
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
const btn = document.getElementById('brain-tab');
|
|
1307
|
+
if (btn) {
|
|
1308
|
+
btn.addEventListener('click', () => setTimeout(loadBrain, 0));
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
if (window.location.hash === '#brain-pane') {
|
|
1312
|
+
setTimeout(loadBrain, 0);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (document.readyState === 'loading') {
|
|
1317
|
+
document.addEventListener('DOMContentLoaded', initBrainBoot);
|
|
1318
|
+
} else {
|
|
1319
|
+
initBrainBoot();
|
|
1320
|
+
}
|
|
1321
|
+
})();
|