@sesamespace/hivemind 0.8.13 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/{chunk-MLY4VFOO.js → chunk-BHCDOHSK.js} +3 -3
- package/dist/{chunk-PFZO67E2.js → chunk-DPLCEMEC.js} +2 -2
- package/dist/{chunk-HTLHMXAL.js → chunk-FBQBBAPZ.js} +2 -2
- package/dist/{chunk-NSTTILSN.js → chunk-FK6WYXRM.js} +79 -2
- package/dist/chunk-FK6WYXRM.js.map +1 -0
- package/dist/{chunk-LJHJGDKY.js → chunk-ICSJNKI6.js} +62 -2
- package/dist/chunk-ICSJNKI6.js.map +1 -0
- package/dist/{chunk-4Y7A25UG.js → chunk-IXBIAX76.js} +2 -2
- package/dist/{chunk-ZM7RK5YV.js → chunk-M3A2WRXM.js} +560 -37
- package/dist/chunk-M3A2WRXM.js.map +1 -0
- package/dist/commands/fleet.js +3 -3
- package/dist/commands/init.js +3 -3
- package/dist/commands/start.js +3 -3
- package/dist/commands/upgrade.js +1 -1
- package/dist/commands/watchdog.js +3 -3
- package/dist/dashboard.html +873 -131
- package/dist/index.js +2 -2
- package/dist/main.js +375 -7
- package/dist/main.js.map +1 -1
- package/dist/start.js +1 -1
- package/install.sh +162 -0
- package/package.json +24 -23
- package/packages/memory/Cargo.lock +6480 -0
- package/packages/memory/Cargo.toml +21 -0
- package/packages/memory/src/src/context.rs +179 -0
- package/packages/memory/src/src/embeddings.rs +51 -0
- package/packages/memory/src/src/main.rs +887 -0
- package/packages/memory/src/src/promotion.rs +808 -0
- package/packages/memory/src/src/scoring.rs +142 -0
- package/packages/memory/src/src/store.rs +460 -0
- package/packages/memory/src/src/tasks.rs +321 -0
- package/.pnpmrc.json +0 -1
- package/AUTO-DEBUG-DESIGN.md +0 -267
- package/DASHBOARD-PLAN.md +0 -206
- package/MEMORY-ENHANCEMENT-PLAN.md +0 -211
- package/TOOL-USE-DESIGN.md +0 -173
- package/dist/chunk-LJHJGDKY.js.map +0 -1
- package/dist/chunk-NSTTILSN.js.map +0 -1
- package/dist/chunk-ZM7RK5YV.js.map +0 -1
- package/docs/TOOL-PARITY-PLAN.md +0 -191
- package/src/memory/dashboard-integration.ts +0 -295
- package/src/memory/index.ts +0 -187
- package/src/memory/performance-test.ts +0 -208
- package/src/memory/processors/agent-sync.ts +0 -312
- package/src/memory/processors/command-learner.ts +0 -298
- package/src/memory/processors/memory-api-client.ts +0 -105
- package/src/memory/processors/message-flow-integration.ts +0 -168
- package/src/memory/processors/research-digester.ts +0 -204
- package/test-caitlin-access.md +0 -11
- /package/dist/{chunk-MLY4VFOO.js.map → chunk-BHCDOHSK.js.map} +0 -0
- /package/dist/{chunk-PFZO67E2.js.map → chunk-DPLCEMEC.js.map} +0 -0
- /package/dist/{chunk-HTLHMXAL.js.map → chunk-FBQBBAPZ.js.map} +0 -0
- /package/dist/{chunk-4Y7A25UG.js.map → chunk-IXBIAX76.js.map} +0 -0
package/dist/dashboard.html
CHANGED
|
@@ -12,23 +12,43 @@
|
|
|
12
12
|
}
|
|
13
13
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
14
14
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); display: flex; height: 100vh; overflow: hidden; }
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
/* Sidebar */
|
|
17
17
|
.sidebar { width: 200px; background: var(--bg2); border-right: 1px solid var(--border); padding: 16px 0; flex-shrink: 0; display: flex; flex-direction: column; }
|
|
18
|
-
.sidebar
|
|
19
|
-
.sidebar
|
|
18
|
+
.sidebar-header { padding: 0 16px 12px; border-bottom: 1px solid var(--border); }
|
|
19
|
+
.sidebar-header h1 { font-size: 16px; color: var(--accent); margin-bottom: 6px; }
|
|
20
|
+
.health-indicator { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text2); }
|
|
21
|
+
.health-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text2); flex-shrink: 0; }
|
|
22
|
+
.health-dot.ok { background: var(--green); }
|
|
23
|
+
.health-dot.degraded { background: var(--yellow); }
|
|
24
|
+
.health-dot.offline { background: var(--red); }
|
|
25
|
+
.sidebar nav { padding: 8px 0; flex: 1; }
|
|
20
26
|
.sidebar a { display: block; padding: 8px 16px; color: var(--text2); text-decoration: none; font-size: 14px; cursor: pointer; }
|
|
21
27
|
.sidebar a:hover, .sidebar a.active { color: var(--text); background: var(--bg3); }
|
|
22
|
-
|
|
28
|
+
|
|
23
29
|
/* Main */
|
|
24
30
|
.main { flex: 1; overflow-y: auto; padding: 24px; }
|
|
25
|
-
|
|
31
|
+
|
|
32
|
+
/* Toast notifications */
|
|
33
|
+
.toast-container { position: fixed; top: 16px; right: 16px; z-index: 9999; display: flex; flex-direction: column; gap: 8px; }
|
|
34
|
+
.toast { padding: 10px 16px; border-radius: 6px; font-size: 13px; color: #fff; animation: toastIn 0.3s ease; max-width: 400px; cursor: pointer; }
|
|
35
|
+
.toast.info { background: var(--accent2); }
|
|
36
|
+
.toast.success { background: #238636; }
|
|
37
|
+
.toast.warning { background: #9e6a03; }
|
|
38
|
+
.toast.error { background: #da3633; }
|
|
39
|
+
@keyframes toastIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: none; } }
|
|
40
|
+
|
|
26
41
|
/* Filters */
|
|
27
|
-
.filters { display: flex; gap: 12px; margin-bottom: 16px; align-items: center; }
|
|
42
|
+
.filters { display: flex; gap: 12px; margin-bottom: 16px; align-items: center; flex-wrap: wrap; }
|
|
28
43
|
.filters input, .filters select { background: var(--bg2); border: 1px solid var(--border); color: var(--text); padding: 6px 10px; border-radius: 6px; font-size: 13px; }
|
|
29
44
|
.filters input:focus, .filters select:focus { outline: none; border-color: var(--accent); }
|
|
30
45
|
.filters button { background: var(--accent2); color: #fff; border: none; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; }
|
|
31
|
-
|
|
46
|
+
.filters button:hover { background: var(--accent); }
|
|
47
|
+
|
|
48
|
+
/* Auto-refresh toggle */
|
|
49
|
+
.auto-refresh-btn { background: var(--bg3) !important; border: 1px solid var(--border) !important; color: var(--text2) !important; }
|
|
50
|
+
.auto-refresh-btn.active { border-color: var(--green) !important; color: var(--green) !important; }
|
|
51
|
+
|
|
32
52
|
/* Request list */
|
|
33
53
|
.req-row { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; cursor: pointer; transition: border-color 0.15s; }
|
|
34
54
|
.req-row:hover { border-color: var(--accent); }
|
|
@@ -39,14 +59,14 @@
|
|
|
39
59
|
.req-summary .model { color: var(--text2); }
|
|
40
60
|
.req-summary .latency { color: var(--yellow); text-align: right; }
|
|
41
61
|
.req-summary .tokens { color: var(--text2); text-align: right; }
|
|
42
|
-
|
|
62
|
+
|
|
43
63
|
/* Detail */
|
|
44
64
|
.req-detail { padding: 0 14px 14px; border-top: 1px solid var(--border); display: none; }
|
|
45
65
|
.req-detail.open { display: block; }
|
|
46
66
|
.section { margin-top: 12px; }
|
|
47
67
|
.section-header { font-size: 12px; font-weight: 600; color: var(--accent); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; cursor: pointer; user-select: none; }
|
|
48
|
-
.section-header::before { content: '
|
|
49
|
-
.section-header.open::before { content: '
|
|
68
|
+
.section-header::before { content: '\25B8 '; }
|
|
69
|
+
.section-header.open::before { content: '\25BE '; }
|
|
50
70
|
.section-body { display: none; }
|
|
51
71
|
.section-body.open { display: block; }
|
|
52
72
|
.section-body pre { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px; font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; max-height: 400px; overflow-y: auto; }
|
|
@@ -54,78 +74,187 @@
|
|
|
54
74
|
.episode .meta { color: var(--text2); font-size: 11px; margin-bottom: 2px; }
|
|
55
75
|
.episode .score { color: var(--yellow); }
|
|
56
76
|
.l3-entry { background: var(--bg); border-left: 3px solid var(--purple); padding: 6px 10px; margin-bottom: 4px; font-size: 12px; }
|
|
57
|
-
|
|
58
|
-
/* Token bar */
|
|
59
|
-
.token-
|
|
60
|
-
.token-
|
|
77
|
+
|
|
78
|
+
/* Token cards + bar */
|
|
79
|
+
.token-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 8px; }
|
|
80
|
+
.token-card { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 6px 8px; text-align: center; }
|
|
81
|
+
.token-card .label { font-size: 10px; color: var(--text2); text-transform: uppercase; }
|
|
82
|
+
.token-card .value { font-size: 16px; font-weight: 600; margin-top: 2px; }
|
|
83
|
+
.token-card .pct { font-size: 10px; color: var(--text2); }
|
|
84
|
+
.token-bar { display: flex; height: 20px; border-radius: 4px; overflow: hidden; font-size: 10px; }
|
|
85
|
+
.token-bar div { display: flex; align-items: center; justify-content: center; color: #fff; min-width: 30px; position: relative; }
|
|
86
|
+
.token-bar div[title] { cursor: help; }
|
|
61
87
|
.tb-sys { background: #58a6ff; }
|
|
62
88
|
.tb-hist { background: #bc8cff; }
|
|
63
89
|
.tb-user { background: #3fb950; }
|
|
64
|
-
|
|
90
|
+
|
|
65
91
|
/* Config */
|
|
66
92
|
.config-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
|
|
67
93
|
.config-item { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 8px; text-align: center; }
|
|
68
94
|
.config-item .label { font-size: 10px; color: var(--text2); text-transform: uppercase; }
|
|
69
95
|
.config-item .value { font-size: 14px; font-weight: 600; margin-top: 2px; }
|
|
70
|
-
|
|
96
|
+
|
|
97
|
+
/* Status cards grid */
|
|
98
|
+
.status-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; margin-bottom: 24px; }
|
|
99
|
+
.status-card { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
|
|
100
|
+
.status-card .label { font-size: 11px; color: var(--text2); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
101
|
+
.status-card .value { font-size: 24px; font-weight: 700; margin-top: 4px; }
|
|
102
|
+
.status-card .sub { font-size: 11px; color: var(--text2); margin-top: 2px; }
|
|
103
|
+
|
|
71
104
|
/* Memory browser */
|
|
72
105
|
.ctx-card { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; padding: 14px; margin-bottom: 8px; }
|
|
73
106
|
.ctx-card h3 { font-size: 14px; color: var(--accent); margin-bottom: 4px; }
|
|
74
107
|
.ctx-card .meta { font-size: 12px; color: var(--text2); }
|
|
108
|
+
.ctx-card .actions { display: flex; gap: 6px; margin-top: 8px; }
|
|
75
109
|
.btn-sm { background: var(--bg3); border: 1px solid var(--border); color: var(--text2); padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px; }
|
|
76
110
|
.btn-sm:hover { border-color: var(--accent); color: var(--text); }
|
|
77
111
|
.btn-danger { border-color: var(--red); color: var(--red); }
|
|
78
112
|
.btn-danger:hover { background: var(--red); color: #fff; }
|
|
79
|
-
|
|
113
|
+
.btn-primary { background: var(--accent2); border-color: var(--accent2); color: #fff; }
|
|
114
|
+
.btn-primary:hover { background: var(--accent); }
|
|
115
|
+
|
|
80
116
|
/* Pagination */
|
|
81
117
|
.pagination { display: flex; gap: 8px; margin-top: 16px; justify-content: center; align-items: center; font-size: 13px; color: var(--text2); }
|
|
82
118
|
.pagination button { background: var(--bg2); border: 1px solid var(--border); color: var(--text); padding: 4px 12px; border-radius: 4px; cursor: pointer; }
|
|
83
119
|
.pagination button:disabled { opacity: 0.4; cursor: default; }
|
|
84
|
-
|
|
120
|
+
|
|
85
121
|
.hidden { display: none; }
|
|
86
122
|
.view { display: none; }
|
|
87
123
|
.view.active { display: block; }
|
|
88
|
-
|
|
124
|
+
|
|
89
125
|
/* Memory browser sub-nav */
|
|
90
126
|
.mem-tabs { display: flex; gap: 8px; margin-bottom: 16px; }
|
|
91
127
|
.mem-tab { padding: 6px 14px; border-radius: 6px; background: var(--bg2); border: 1px solid var(--border); color: var(--text2); cursor: pointer; font-size: 13px; }
|
|
92
128
|
.mem-tab.active { border-color: var(--accent); color: var(--accent); }
|
|
129
|
+
|
|
130
|
+
/* Sub-tabs for promotion view */
|
|
131
|
+
.sub-tabs { display: flex; gap: 8px; margin-bottom: 16px; }
|
|
132
|
+
.sub-tab { padding: 6px 14px; border-radius: 6px; background: var(--bg2); border: 1px solid var(--border); color: var(--text2); cursor: pointer; font-size: 13px; }
|
|
133
|
+
.sub-tab.active { border-color: var(--accent); color: var(--accent); }
|
|
134
|
+
|
|
135
|
+
/* Progress bar */
|
|
136
|
+
.progress-bar { background: var(--bg3); border-radius: 4px; height: 16px; overflow: hidden; position: relative; }
|
|
137
|
+
.progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #fff; }
|
|
138
|
+
.progress-fill.green { background: var(--green); }
|
|
139
|
+
.progress-fill.yellow { background: var(--yellow); }
|
|
140
|
+
.progress-fill.purple { background: var(--purple); }
|
|
141
|
+
|
|
142
|
+
/* Search results */
|
|
143
|
+
.search-result { background: var(--bg2); border: 1px solid var(--border); border-radius: 6px; padding: 10px 14px; margin-bottom: 8px; }
|
|
144
|
+
.search-result .meta { font-size: 11px; color: var(--text2); margin-bottom: 4px; }
|
|
145
|
+
.search-result .content { font-size: 13px; }
|
|
146
|
+
.search-group-header { font-size: 14px; font-weight: 600; color: var(--purple); margin: 16px 0 8px; padding-bottom: 4px; border-bottom: 1px solid var(--border); }
|
|
147
|
+
|
|
148
|
+
/* L3 edit textarea */
|
|
149
|
+
.l3-edit-area { width: 100%; min-height: 80px; background: var(--bg); border: 1px solid var(--accent); color: var(--text); padding: 8px; border-radius: 4px; font-family: inherit; font-size: 12px; resize: vertical; }
|
|
150
|
+
|
|
151
|
+
/* Decay curve SVG */
|
|
152
|
+
.decay-curve { margin-top: 8px; }
|
|
153
|
+
|
|
154
|
+
/* Table */
|
|
155
|
+
.data-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
156
|
+
.data-table th { text-align: left; padding: 8px; border-bottom: 2px solid var(--border); color: var(--text2); font-size: 11px; text-transform: uppercase; }
|
|
157
|
+
.data-table td { padding: 8px; border-bottom: 1px solid var(--border); }
|
|
158
|
+
.data-table tr:hover td { background: var(--bg3); }
|
|
159
|
+
|
|
160
|
+
/* L1 message */
|
|
161
|
+
.l1-msg { padding: 8px 12px; margin-bottom: 4px; border-radius: 6px; font-size: 13px; }
|
|
162
|
+
.l1-msg.user { background: var(--bg3); border-left: 3px solid var(--green); }
|
|
163
|
+
.l1-msg.assistant { background: var(--bg2); border-left: 3px solid var(--accent); }
|
|
164
|
+
.l1-msg .role { font-size: 11px; font-weight: 600; color: var(--text2); margin-bottom: 2px; text-transform: uppercase; }
|
|
93
165
|
</style>
|
|
94
166
|
</head>
|
|
95
167
|
<body>
|
|
96
168
|
<div class="sidebar">
|
|
97
|
-
<
|
|
169
|
+
<div class="sidebar-header">
|
|
170
|
+
<h1>Hivemind</h1>
|
|
171
|
+
<div class="health-indicator">
|
|
172
|
+
<span class="health-dot" id="health-dot"></span>
|
|
173
|
+
<span id="health-text">Checking...</span>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
98
176
|
<nav>
|
|
99
|
-
<a data-view="
|
|
177
|
+
<a data-view="health" class="active">Health</a>
|
|
178
|
+
<a data-view="requests">Requests</a>
|
|
100
179
|
<a data-view="memory">Memory</a>
|
|
180
|
+
<a data-view="search">Search</a>
|
|
181
|
+
<a data-view="promotion">Promotion</a>
|
|
101
182
|
<a data-view="contexts">Contexts</a>
|
|
102
183
|
</nav>
|
|
103
184
|
</div>
|
|
104
185
|
<div class="main">
|
|
186
|
+
|
|
187
|
+
<!-- Toast Container -->
|
|
188
|
+
<div class="toast-container" id="toast-container"></div>
|
|
189
|
+
|
|
190
|
+
<!-- Health View -->
|
|
191
|
+
<div id="v-health" class="view active">
|
|
192
|
+
<h2 style="margin-bottom:16px">System Status</h2>
|
|
193
|
+
<div class="status-grid" id="health-cards"></div>
|
|
194
|
+
<h3 style="margin:24px 0 12px">Context Summary</h3>
|
|
195
|
+
<table class="data-table" id="ctx-summary-table">
|
|
196
|
+
<thead><tr><th>Context</th><th>Episodes</th><th>L3 Entries</th><th>Created</th></tr></thead>
|
|
197
|
+
<tbody id="ctx-summary-body"></tbody>
|
|
198
|
+
</table>
|
|
199
|
+
<h3 style="margin:24px 0 12px">Scoring Configuration</h3>
|
|
200
|
+
<div id="scoring-config-section"></div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
105
203
|
<!-- Requests View -->
|
|
106
|
-
<div id="v-requests" class="view
|
|
204
|
+
<div id="v-requests" class="view">
|
|
107
205
|
<div class="filters">
|
|
108
|
-
<input type="text" id="f-context" placeholder="Filter context
|
|
109
|
-
<input type="text" id="f-sender" placeholder="Filter sender
|
|
206
|
+
<input type="text" id="f-context" placeholder="Filter context..." />
|
|
207
|
+
<input type="text" id="f-sender" placeholder="Filter sender..." />
|
|
110
208
|
<button onclick="loadRequests()">Filter</button>
|
|
209
|
+
<button class="auto-refresh-btn" id="ar-requests" onclick="toggleAutoRefresh('requests')">Auto-refresh</button>
|
|
111
210
|
</div>
|
|
112
211
|
<div id="req-list"></div>
|
|
113
212
|
<div class="pagination" id="req-pagination"></div>
|
|
114
213
|
</div>
|
|
115
|
-
|
|
214
|
+
|
|
116
215
|
<!-- Memory View -->
|
|
117
216
|
<div id="v-memory" class="view">
|
|
118
217
|
<div class="mem-tabs">
|
|
119
218
|
<div class="mem-tab active" data-mtab="l3">L3 Knowledge</div>
|
|
120
219
|
<div class="mem-tab" data-mtab="l2">L2 Episodes</div>
|
|
220
|
+
<div class="mem-tab" data-mtab="l1">L1 History</div>
|
|
121
221
|
</div>
|
|
122
|
-
<div id="mem-ctx-select" style="margin-bottom:12px">
|
|
123
|
-
<select id="mem-context"><option value="">Select context
|
|
222
|
+
<div id="mem-ctx-select" style="margin-bottom:12px;display:flex;gap:12px;align-items:center">
|
|
223
|
+
<select id="mem-context"><option value="">Select context...</option></select>
|
|
224
|
+
<button class="auto-refresh-btn" id="ar-memory" onclick="toggleAutoRefresh('memory')">Auto-refresh</button>
|
|
124
225
|
</div>
|
|
125
226
|
<div id="mem-l3" class="view active"></div>
|
|
126
227
|
<div id="mem-l2" class="view"></div>
|
|
228
|
+
<div id="mem-l1" class="view"></div>
|
|
127
229
|
</div>
|
|
128
|
-
|
|
230
|
+
|
|
231
|
+
<!-- Search View -->
|
|
232
|
+
<div id="v-search" class="view">
|
|
233
|
+
<h2 style="margin-bottom:16px">Cross-Context Search</h2>
|
|
234
|
+
<div class="filters">
|
|
235
|
+
<input type="text" id="search-query" placeholder="Search query..." style="flex:1;min-width:200px" />
|
|
236
|
+
<select id="search-scope">
|
|
237
|
+
<option value="all">All Contexts</option>
|
|
238
|
+
</select>
|
|
239
|
+
<input type="number" id="search-limit" value="20" min="1" max="100" style="width:60px" />
|
|
240
|
+
<button onclick="runSearch()">Search</button>
|
|
241
|
+
</div>
|
|
242
|
+
<div id="search-results"></div>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<!-- Promotion View -->
|
|
246
|
+
<div id="v-promotion" class="view">
|
|
247
|
+
<h2 style="margin-bottom:16px">Promotion & Scoring</h2>
|
|
248
|
+
<div class="sub-tabs">
|
|
249
|
+
<div class="sub-tab active" data-ptab="thresholds">Thresholds</div>
|
|
250
|
+
<div class="sub-tab" data-ptab="candidates">Candidates</div>
|
|
251
|
+
<div class="sub-tab" data-ptab="scoring">Scoring</div>
|
|
252
|
+
</div>
|
|
253
|
+
<div id="promo-thresholds" class="view active"></div>
|
|
254
|
+
<div id="promo-candidates" class="view"></div>
|
|
255
|
+
<div id="promo-scoring" class="view"></div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
129
258
|
<!-- Contexts View -->
|
|
130
259
|
<div id="v-contexts" class="view">
|
|
131
260
|
<div id="ctx-list"></div>
|
|
@@ -136,19 +265,104 @@
|
|
|
136
265
|
const API = '';
|
|
137
266
|
let reqOffset = 0;
|
|
138
267
|
const REQ_LIMIT = 30;
|
|
268
|
+
let l2Page = 0;
|
|
269
|
+
const L2_PAGE_SIZE = 50;
|
|
270
|
+
let l2AllEpisodes = [];
|
|
271
|
+
let l2SearchMode = false;
|
|
272
|
+
let autoRefreshTimers = {};
|
|
273
|
+
let activeMemTab = 'l3';
|
|
274
|
+
|
|
275
|
+
// ===== Toast Notifications =====
|
|
276
|
+
function showNotification(message, type = 'info') {
|
|
277
|
+
const container = document.getElementById('toast-container');
|
|
278
|
+
const toast = document.createElement('div');
|
|
279
|
+
toast.className = 'toast ' + type;
|
|
280
|
+
toast.textContent = message;
|
|
281
|
+
toast.addEventListener('click', () => toast.remove());
|
|
282
|
+
container.appendChild(toast);
|
|
283
|
+
setTimeout(() => { if (toast.parentNode) toast.remove(); }, 5000);
|
|
284
|
+
}
|
|
139
285
|
|
|
140
|
-
//
|
|
286
|
+
// ===== Health Polling =====
|
|
287
|
+
let daemonStatus = 'offline';
|
|
288
|
+
let daemonVersion = '';
|
|
289
|
+
|
|
290
|
+
async function checkHealth() {
|
|
291
|
+
const dot = document.getElementById('health-dot');
|
|
292
|
+
const text = document.getElementById('health-text');
|
|
293
|
+
try {
|
|
294
|
+
const res = await fetch(API + '/api/health');
|
|
295
|
+
if (res.ok) {
|
|
296
|
+
const data = await res.json();
|
|
297
|
+
daemonStatus = data.status || 'ok';
|
|
298
|
+
daemonVersion = data.version || '';
|
|
299
|
+
dot.className = 'health-dot ok';
|
|
300
|
+
text.textContent = 'v' + daemonVersion;
|
|
301
|
+
} else {
|
|
302
|
+
daemonStatus = 'degraded';
|
|
303
|
+
dot.className = 'health-dot degraded';
|
|
304
|
+
text.textContent = 'Degraded';
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
daemonStatus = 'offline';
|
|
308
|
+
dot.className = 'health-dot offline';
|
|
309
|
+
text.textContent = 'Daemon offline';
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
checkHealth();
|
|
313
|
+
setInterval(checkHealth, 10000);
|
|
314
|
+
|
|
315
|
+
// ===== Auto-Refresh =====
|
|
316
|
+
function toggleAutoRefresh(view) {
|
|
317
|
+
const btn = document.getElementById('ar-' + view);
|
|
318
|
+
if (autoRefreshTimers[view]) {
|
|
319
|
+
clearInterval(autoRefreshTimers[view]);
|
|
320
|
+
autoRefreshTimers[view] = null;
|
|
321
|
+
btn.classList.remove('active');
|
|
322
|
+
localStorage.removeItem('ar-' + view);
|
|
323
|
+
} else {
|
|
324
|
+
autoRefreshTimers[view] = setInterval(() => refreshView(view), 5000);
|
|
325
|
+
btn.classList.add('active');
|
|
326
|
+
localStorage.setItem('ar-' + view, '1');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function refreshView(view) {
|
|
331
|
+
if (view === 'requests') loadRequests();
|
|
332
|
+
else if (view === 'memory') {
|
|
333
|
+
const ctx = document.getElementById('mem-context').value;
|
|
334
|
+
if (ctx) {
|
|
335
|
+
if (activeMemTab === 'l3') loadL3(ctx);
|
|
336
|
+
else if (activeMemTab === 'l2') loadL2(ctx);
|
|
337
|
+
else if (activeMemTab === 'l1') loadL1(ctx);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Restore auto-refresh state
|
|
343
|
+
['requests', 'memory'].forEach(v => {
|
|
344
|
+
if (localStorage.getItem('ar-' + v)) {
|
|
345
|
+
const btn = document.getElementById('ar-' + v);
|
|
346
|
+
if (btn) {
|
|
347
|
+
autoRefreshTimers[v] = setInterval(() => refreshView(v), 5000);
|
|
348
|
+
btn.classList.add('active');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// ===== Navigation =====
|
|
141
354
|
document.querySelectorAll('.sidebar a').forEach(a => {
|
|
142
355
|
a.addEventListener('click', () => {
|
|
143
356
|
document.querySelectorAll('.sidebar a').forEach(x => x.classList.remove('active'));
|
|
144
357
|
a.classList.add('active');
|
|
145
|
-
document.querySelectorAll('.view').forEach(v =>
|
|
146
|
-
if (v.closest('.main')) v.classList.remove('active');
|
|
147
|
-
});
|
|
358
|
+
document.querySelectorAll('.main > .view').forEach(v => v.classList.remove('active'));
|
|
148
359
|
const viewId = 'v-' + a.dataset.view;
|
|
149
360
|
document.getElementById(viewId)?.classList.add('active');
|
|
361
|
+
if (a.dataset.view === 'health') loadHealthView();
|
|
150
362
|
if (a.dataset.view === 'contexts') loadContexts();
|
|
151
363
|
if (a.dataset.view === 'memory') loadMemoryContexts();
|
|
364
|
+
if (a.dataset.view === 'search') loadSearchContexts();
|
|
365
|
+
if (a.dataset.view === 'promotion') loadPromotion();
|
|
152
366
|
});
|
|
153
367
|
});
|
|
154
368
|
|
|
@@ -157,85 +371,189 @@ document.querySelectorAll('.mem-tab').forEach(t => {
|
|
|
157
371
|
t.addEventListener('click', () => {
|
|
158
372
|
document.querySelectorAll('.mem-tab').forEach(x => x.classList.remove('active'));
|
|
159
373
|
t.classList.add('active');
|
|
160
|
-
|
|
161
|
-
document.getElementById('mem-
|
|
374
|
+
activeMemTab = t.dataset.mtab;
|
|
375
|
+
document.getElementById('mem-l3').classList.toggle('active', activeMemTab === 'l3');
|
|
376
|
+
document.getElementById('mem-l2').classList.toggle('active', activeMemTab === 'l2');
|
|
377
|
+
document.getElementById('mem-l1').classList.toggle('active', activeMemTab === 'l1');
|
|
378
|
+
const ctx = document.getElementById('mem-context').value;
|
|
379
|
+
if (ctx) {
|
|
380
|
+
if (activeMemTab === 'l1') loadL1(ctx);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Promotion sub-tabs
|
|
386
|
+
document.querySelectorAll('.sub-tab').forEach(t => {
|
|
387
|
+
t.addEventListener('click', () => {
|
|
388
|
+
document.querySelectorAll('.sub-tab').forEach(x => x.classList.remove('active'));
|
|
389
|
+
t.classList.add('active');
|
|
390
|
+
document.getElementById('promo-thresholds').classList.toggle('active', t.dataset.ptab === 'thresholds');
|
|
391
|
+
document.getElementById('promo-candidates').classList.toggle('active', t.dataset.ptab === 'candidates');
|
|
392
|
+
document.getElementById('promo-scoring').classList.toggle('active', t.dataset.ptab === 'scoring');
|
|
393
|
+
if (t.dataset.ptab === 'candidates') loadCandidates();
|
|
394
|
+
if (t.dataset.ptab === 'scoring') loadScoringTab();
|
|
162
395
|
});
|
|
163
396
|
});
|
|
164
397
|
|
|
165
398
|
document.getElementById('mem-context').addEventListener('change', () => {
|
|
166
399
|
const ctx = document.getElementById('mem-context').value;
|
|
167
|
-
if (ctx) { loadL3(ctx); loadL2(ctx); }
|
|
400
|
+
if (ctx) { loadL3(ctx); loadL2(ctx); if (activeMemTab === 'l1') loadL1(ctx); }
|
|
168
401
|
});
|
|
169
402
|
|
|
170
|
-
//
|
|
403
|
+
// ===== Health & System Status View =====
|
|
404
|
+
async function loadHealthView() {
|
|
405
|
+
const cards = document.getElementById('health-cards');
|
|
406
|
+
const tbody = document.getElementById('ctx-summary-body');
|
|
407
|
+
const scoringEl = document.getElementById('scoring-config-section');
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const [statsRes, ctxRes] = await Promise.all([
|
|
411
|
+
fetch(API + '/api/stats'),
|
|
412
|
+
fetch(API + '/api/contexts'),
|
|
413
|
+
]);
|
|
414
|
+
|
|
415
|
+
if (!statsRes.ok || !ctxRes.ok) {
|
|
416
|
+
showNotification('Failed to load system stats', 'error');
|
|
417
|
+
cards.innerHTML = '<p style="color:var(--red)">Failed to load stats (daemon may be offline)</p>';
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const stats = await statsRes.json();
|
|
422
|
+
const ctxData = await ctxRes.json();
|
|
423
|
+
const contexts = ctxData.contexts || [];
|
|
424
|
+
|
|
425
|
+
const totalEpisodes = Object.values(stats.total_episodes || {}).reduce((a, b) => a + b, 0);
|
|
426
|
+
const reqCountRes = await fetch(API + '/api/requests?limit=1');
|
|
427
|
+
const reqCount = reqCountRes.ok ? (await reqCountRes.json()).total || 0 : '?';
|
|
428
|
+
|
|
429
|
+
cards.innerHTML = `
|
|
430
|
+
<div class="status-card"><div class="label">Daemon Status</div><div class="value" style="color:var(${daemonStatus === 'ok' ? '--green' : '--red'})">${daemonStatus === 'ok' ? 'Online' : daemonStatus}</div><div class="sub">v${esc(daemonVersion)}</div></div>
|
|
431
|
+
<div class="status-card"><div class="label">Embedding Model</div><div class="value" style="font-size:14px">${esc(stats.embedding_model)}</div></div>
|
|
432
|
+
<div class="status-card"><div class="label">Total Episodes</div><div class="value">${totalEpisodes}</div><div class="sub">${contexts.length} contexts</div></div>
|
|
433
|
+
<div class="status-card"><div class="label">L3 Entries</div><div class="value">${stats.total_l3_entries}</div></div>
|
|
434
|
+
<div class="status-card"><div class="label">Access Records</div><div class="value">${stats.total_access_records}</div></div>
|
|
435
|
+
<div class="status-card"><div class="label">Request Logs</div><div class="value">${reqCount}</div></div>
|
|
436
|
+
`;
|
|
437
|
+
|
|
438
|
+
// Context summary table
|
|
439
|
+
tbody.innerHTML = '';
|
|
440
|
+
for (const c of contexts) {
|
|
441
|
+
const epCount = stats.total_episodes?.[c.name] ?? c.episode_count ?? 0;
|
|
442
|
+
const tr = document.createElement('tr');
|
|
443
|
+
tr.innerHTML = `<td style="color:var(--accent)">${esc(c.name)}</td><td>${epCount}</td><td>-</td><td>${c.created_at ? new Date(c.created_at).toLocaleDateString() : '-'}</td>`;
|
|
444
|
+
tbody.appendChild(tr);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Scoring section
|
|
448
|
+
const defaultHL = stats.default_half_life_hours || 48;
|
|
449
|
+
scoringEl.innerHTML = `
|
|
450
|
+
<p style="color:var(--text2);font-size:13px;margin-bottom:12px">Default half-life: <strong>${defaultHL}h</strong>. Episodes lose ~50% relevance weight every ${defaultHL} hours.</p>
|
|
451
|
+
${renderDecayCurve(defaultHL)}
|
|
452
|
+
`;
|
|
453
|
+
|
|
454
|
+
} catch (err) {
|
|
455
|
+
cards.innerHTML = '<p style="color:var(--red)">Cannot connect to memory daemon</p>';
|
|
456
|
+
showNotification('Health view failed: ' + err.message, 'error');
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function renderDecayCurve(halfLife) {
|
|
461
|
+
const w = 300, h = 100, pad = 30;
|
|
462
|
+
const points = [];
|
|
463
|
+
const maxH = halfLife * 4;
|
|
464
|
+
for (let t = 0; t <= maxH; t += maxH / 50) {
|
|
465
|
+
const weight = Math.exp(-Math.LN2 / halfLife * t);
|
|
466
|
+
const x = pad + (t / maxH) * (w - pad * 2);
|
|
467
|
+
const y = pad + (1 - weight) * (h - pad * 2);
|
|
468
|
+
points.push(`${x},${y}`);
|
|
469
|
+
}
|
|
470
|
+
return `<svg width="${w}" height="${h}" class="decay-curve" style="background:var(--bg);border:1px solid var(--border);border-radius:4px">
|
|
471
|
+
<polyline points="${points.join(' ')}" fill="none" stroke="var(--accent)" stroke-width="2"/>
|
|
472
|
+
<text x="${pad}" y="${pad - 8}" fill="var(--text2)" font-size="10">1.0</text>
|
|
473
|
+
<text x="${pad}" y="${h - 8}" fill="var(--text2)" font-size="10">0.0</text>
|
|
474
|
+
<text x="${w - pad}" y="${h - 8}" fill="var(--text2)" font-size="10">${maxH}h</text>
|
|
475
|
+
<line x1="${pad + (halfLife / maxH) * (w - pad * 2)}" y1="${pad}" x2="${pad + (halfLife / maxH) * (w - pad * 2)}" y2="${h - pad}" stroke="var(--yellow)" stroke-dasharray="4"/>
|
|
476
|
+
<text x="${pad + (halfLife / maxH) * (w - pad * 2) - 10}" y="${h - 8}" fill="var(--yellow)" font-size="9">${halfLife}h</text>
|
|
477
|
+
</svg>`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ===== Requests =====
|
|
171
481
|
async function loadRequests() {
|
|
172
482
|
const ctx = document.getElementById('f-context').value;
|
|
173
483
|
const sender = document.getElementById('f-sender').value;
|
|
174
484
|
const params = new URLSearchParams({ limit: REQ_LIMIT, offset: reqOffset });
|
|
175
485
|
if (ctx) params.set('context', ctx);
|
|
176
486
|
if (sender) params.set('sender', sender);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
const res = await fetch(API + '/api/requests?' + params);
|
|
490
|
+
if (!res.ok) { showNotification('Failed to load requests', 'error'); return; }
|
|
491
|
+
const data = await res.json();
|
|
492
|
+
|
|
493
|
+
const list = document.getElementById('req-list');
|
|
494
|
+
list.innerHTML = '';
|
|
495
|
+
|
|
496
|
+
for (const r of data.requests) {
|
|
497
|
+
const row = document.createElement('div');
|
|
498
|
+
row.className = 'req-row';
|
|
499
|
+
const ts = new Date(r.timestamp).toLocaleString();
|
|
500
|
+
const latency = r.response_latency_ms + 'ms';
|
|
501
|
+
const tokens = r.token_est_total;
|
|
502
|
+
const model = r.response_model.split('/').pop().slice(0, 15);
|
|
503
|
+
|
|
504
|
+
row.innerHTML = `
|
|
505
|
+
<div class="req-summary">
|
|
506
|
+
<span class="ts">${ts}</span>
|
|
507
|
+
<span class="sender">${esc(r.sender_handle || 'stdin')}</span>
|
|
508
|
+
<span class="ctx">${esc(r.context)}</span>
|
|
509
|
+
<span class="model">${esc(model)}</span>
|
|
510
|
+
<span class="latency">${latency}</span>
|
|
511
|
+
<span class="tokens">~${tokens}t</span>
|
|
512
|
+
</div>
|
|
513
|
+
<div class="req-detail" id="detail-${r.id}"></div>
|
|
514
|
+
`;
|
|
515
|
+
|
|
516
|
+
row.querySelector('.req-summary').addEventListener('click', () => {
|
|
517
|
+
const detail = document.getElementById('detail-' + r.id);
|
|
518
|
+
if (detail.classList.contains('open')) {
|
|
519
|
+
detail.classList.remove('open');
|
|
520
|
+
} else {
|
|
521
|
+
renderDetail(detail, r);
|
|
522
|
+
detail.classList.add('open');
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
list.appendChild(row);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Pagination
|
|
529
|
+
const pag = document.getElementById('req-pagination');
|
|
530
|
+
const page = Math.floor(reqOffset / REQ_LIMIT) + 1;
|
|
531
|
+
const totalPages = Math.ceil(data.total / REQ_LIMIT);
|
|
532
|
+
pag.innerHTML = `
|
|
533
|
+
<button ${reqOffset === 0 ? 'disabled' : ''} onclick="reqOffset -= ${REQ_LIMIT}; loadRequests()">Prev</button>
|
|
534
|
+
<span>Page ${page} of ${totalPages} (${data.total} total)</span>
|
|
535
|
+
<button ${reqOffset + REQ_LIMIT >= data.total ? 'disabled' : ''} onclick="reqOffset += ${REQ_LIMIT}; loadRequests()">Next</button>
|
|
202
536
|
`;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const detail = document.getElementById('detail-' + r.id);
|
|
206
|
-
if (detail.classList.contains('open')) {
|
|
207
|
-
detail.classList.remove('open');
|
|
208
|
-
} else {
|
|
209
|
-
renderDetail(detail, r);
|
|
210
|
-
detail.classList.add('open');
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
list.appendChild(row);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
showNotification('Failed to load requests: ' + err.message, 'error');
|
|
214
539
|
}
|
|
215
|
-
|
|
216
|
-
// Pagination
|
|
217
|
-
const pag = document.getElementById('req-pagination');
|
|
218
|
-
const page = Math.floor(reqOffset / REQ_LIMIT) + 1;
|
|
219
|
-
const totalPages = Math.ceil(data.total / REQ_LIMIT);
|
|
220
|
-
pag.innerHTML = `
|
|
221
|
-
<button ${reqOffset === 0 ? 'disabled' : ''} onclick="reqOffset -= ${REQ_LIMIT}; loadRequests()">← Prev</button>
|
|
222
|
-
<span>Page ${page} of ${totalPages} (${data.total} total)</span>
|
|
223
|
-
<button ${reqOffset + REQ_LIMIT >= data.total ? 'disabled' : ''} onclick="reqOffset += ${REQ_LIMIT}; loadRequests()">Next →</button>
|
|
224
|
-
`;
|
|
225
540
|
}
|
|
226
541
|
|
|
227
542
|
function renderDetail(el, r) {
|
|
228
543
|
const comps = typeof r.system_prompt_components === 'string' ? JSON.parse(r.system_prompt_components) : r.system_prompt_components;
|
|
229
544
|
const history = typeof r.conversation_history === 'string' ? JSON.parse(r.conversation_history) : r.conversation_history;
|
|
230
545
|
const config = typeof r.config_snapshot === 'string' ? JSON.parse(r.config_snapshot) : r.config_snapshot;
|
|
231
|
-
|
|
546
|
+
|
|
232
547
|
const totalTokens = r.token_est_total || 1;
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
|
|
548
|
+
const sysTokens = r.token_est_system || 0;
|
|
549
|
+
const histTokens = r.token_est_history || 0;
|
|
550
|
+
const userTokens = r.token_est_user || 0;
|
|
551
|
+
const sysPct = Math.round((sysTokens / totalTokens) * 100);
|
|
552
|
+
const histPct = Math.round((histTokens / totalTokens) * 100);
|
|
553
|
+
const userPct = Math.max(100 - sysPct - histPct, 0);
|
|
554
|
+
|
|
237
555
|
let html = '';
|
|
238
|
-
|
|
556
|
+
|
|
239
557
|
// Config
|
|
240
558
|
html += `<div class="section"><div class="config-grid">
|
|
241
559
|
<div class="config-item"><div class="label">Model</div><div class="value">${esc(config?.model || r.response_model)}</div></div>
|
|
@@ -243,56 +561,62 @@ function renderDetail(el, r) {
|
|
|
243
561
|
<div class="config-item"><div class="label">Top K</div><div class="value">${config?.topK ?? '?'}</div></div>
|
|
244
562
|
<div class="config-item"><div class="label">Temperature</div><div class="value">${config?.temperature ?? '?'}</div></div>
|
|
245
563
|
</div></div>`;
|
|
246
|
-
|
|
247
|
-
// Token bar
|
|
564
|
+
|
|
565
|
+
// Token cards + bar
|
|
248
566
|
html += `<div class="section">
|
|
249
567
|
<div class="section-header open">Token Breakdown (~${totalTokens} total)</div>
|
|
250
568
|
<div class="section-body open">
|
|
569
|
+
<div class="token-cards">
|
|
570
|
+
<div class="token-card"><div class="label">System</div><div class="value" style="color:var(--accent)">${sysTokens.toLocaleString()}</div><div class="pct">${sysPct}%</div></div>
|
|
571
|
+
<div class="token-card"><div class="label">History</div><div class="value" style="color:var(--purple)">${histTokens.toLocaleString()}</div><div class="pct">${histPct}%</div></div>
|
|
572
|
+
<div class="token-card"><div class="label">User</div><div class="value" style="color:var(--green)">${userTokens.toLocaleString()}</div><div class="pct">${userPct}%</div></div>
|
|
573
|
+
<div class="token-card"><div class="label">Total</div><div class="value">${totalTokens.toLocaleString()}</div><div class="pct">100%</div></div>
|
|
574
|
+
</div>
|
|
251
575
|
<div class="token-bar">
|
|
252
|
-
<div class="tb-sys" style="width:${sysPct}%"
|
|
253
|
-
<div class="tb-hist" style="width:${histPct}%">${
|
|
254
|
-
<div class="tb-user" style="width:${Math.max(userPct, 5)}%"
|
|
576
|
+
<div class="tb-sys" style="width:${sysPct}%" title="System: ${sysTokens} tokens (${sysPct}%)">sys</div>
|
|
577
|
+
<div class="tb-hist" style="width:${histPct}%" title="History: ${histTokens} tokens (${histPct}%)">${histTokens > 0 ? 'hist' : ''}</div>
|
|
578
|
+
<div class="tb-user" style="width:${Math.max(userPct, 5)}%" title="User: ${userTokens} tokens (${userPct}%)">user</div>
|
|
255
579
|
</div>
|
|
256
580
|
</div>
|
|
257
581
|
</div>`;
|
|
258
|
-
|
|
582
|
+
|
|
259
583
|
// User message
|
|
260
584
|
html += section('User Message', `<pre>${esc(r.user_message)}</pre>`, true);
|
|
261
|
-
|
|
585
|
+
|
|
262
586
|
// Response
|
|
263
587
|
const skipped = r.response_skipped ? ' <span style="color:var(--yellow)">[SKIPPED]</span>' : '';
|
|
264
588
|
html += section('Response' + skipped, `<pre>${esc(r.response_content)}</pre>`, true);
|
|
265
|
-
|
|
589
|
+
|
|
266
590
|
// L3 Knowledge
|
|
267
591
|
if (comps?.l3Knowledge?.length > 0) {
|
|
268
592
|
const l3Html = comps.l3Knowledge.map(k => `<div class="l3-entry">${esc(k)}</div>`).join('');
|
|
269
593
|
html += section(`L3 Knowledge (${comps.l3Knowledge.length})`, l3Html, true);
|
|
270
594
|
}
|
|
271
|
-
|
|
595
|
+
|
|
272
596
|
// L2 Episodes
|
|
273
597
|
if (comps?.l2Episodes?.length > 0) {
|
|
274
598
|
const epHtml = comps.l2Episodes.map(ep => `
|
|
275
599
|
<div class="episode">
|
|
276
|
-
<div class="meta">${esc(ep.role)}
|
|
600
|
+
<div class="meta">${esc(ep.role)} · ${esc(ep.context_name)} · <span class="score">score: ${ep.score?.toFixed(3)}</span> · ${new Date(ep.timestamp).toLocaleString()}</div>
|
|
277
601
|
${esc(ep.content)}
|
|
278
602
|
</div>
|
|
279
603
|
`).join('');
|
|
280
604
|
html += section(`L2 Episodes (${comps.l2Episodes.length})`, epHtml, true);
|
|
281
605
|
}
|
|
282
|
-
|
|
606
|
+
|
|
283
607
|
// L1 History
|
|
284
608
|
if (history?.length > 0) {
|
|
285
609
|
const histHtml = history.map(m => `<div class="episode"><div class="meta">${esc(m.role)}</div>${esc(m.content)}</div>`).join('');
|
|
286
610
|
html += section(`L1 History (${history.length} turns)`, histHtml, false);
|
|
287
611
|
}
|
|
288
|
-
|
|
289
|
-
// Identity
|
|
612
|
+
|
|
613
|
+
// Identity
|
|
290
614
|
if (comps?.identity) {
|
|
291
615
|
html += section('Identity Files', `<pre>${esc(comps.identity)}</pre>`, false);
|
|
292
616
|
}
|
|
293
|
-
|
|
617
|
+
|
|
294
618
|
el.innerHTML = html;
|
|
295
|
-
|
|
619
|
+
|
|
296
620
|
// Wire section toggles
|
|
297
621
|
el.querySelectorAll('.section-header').forEach(h => {
|
|
298
622
|
h.addEventListener('click', () => {
|
|
@@ -309,10 +633,11 @@ function section(title, content, startOpen) {
|
|
|
309
633
|
</div>`;
|
|
310
634
|
}
|
|
311
635
|
|
|
312
|
-
// Contexts
|
|
636
|
+
// ===== Contexts View =====
|
|
313
637
|
async function loadContexts() {
|
|
314
638
|
try {
|
|
315
|
-
const res = await fetch(
|
|
639
|
+
const res = await fetch(API + '/api/contexts');
|
|
640
|
+
if (!res.ok) { showNotification('Failed to load contexts', 'error'); return; }
|
|
316
641
|
const data = await res.json();
|
|
317
642
|
const contexts = data.contexts || [];
|
|
318
643
|
const list = document.getElementById('ctx-list');
|
|
@@ -323,8 +648,13 @@ async function loadContexts() {
|
|
|
323
648
|
list.innerHTML = contexts.map(c => `
|
|
324
649
|
<div class="ctx-card">
|
|
325
650
|
<h3>${esc(c.name)}</h3>
|
|
326
|
-
<div class="meta">${c.episode_count ?? '?'} episodes
|
|
651
|
+
<div class="meta">${c.episode_count ?? '?'} episodes · created ${c.created_at ? new Date(c.created_at).toLocaleDateString() : '?'}</div>
|
|
327
652
|
${c.description ? `<div class="meta">${esc(c.description)}</div>` : ''}
|
|
653
|
+
<div class="actions">
|
|
654
|
+
<button class="btn-sm btn-primary" onclick="drillToMemory('${esc(c.name)}')">Browse Episodes</button>
|
|
655
|
+
<button class="btn-sm" onclick="viewScoring('${esc(c.name)}')">Scoring</button>
|
|
656
|
+
${c.name !== 'global' ? `<button class="btn-sm btn-danger" onclick="deleteContext('${esc(c.name)}')">Delete</button>` : ''}
|
|
657
|
+
</div>
|
|
328
658
|
</div>
|
|
329
659
|
`).join('');
|
|
330
660
|
} catch (err) {
|
|
@@ -332,37 +662,100 @@ async function loadContexts() {
|
|
|
332
662
|
}
|
|
333
663
|
}
|
|
334
664
|
|
|
335
|
-
|
|
665
|
+
function drillToMemory(ctx) {
|
|
666
|
+
// Switch to memory view with context pre-selected
|
|
667
|
+
document.querySelectorAll('.sidebar a').forEach(x => x.classList.remove('active'));
|
|
668
|
+
document.querySelector('[data-view="memory"]').classList.add('active');
|
|
669
|
+
document.querySelectorAll('.main > .view').forEach(v => v.classList.remove('active'));
|
|
670
|
+
document.getElementById('v-memory').classList.add('active');
|
|
671
|
+
loadMemoryContexts().then(() => {
|
|
672
|
+
document.getElementById('mem-context').value = ctx;
|
|
673
|
+
loadL3(ctx);
|
|
674
|
+
loadL2(ctx);
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function viewScoring(ctx) {
|
|
679
|
+
// Switch to promotion > scoring
|
|
680
|
+
document.querySelectorAll('.sidebar a').forEach(x => x.classList.remove('active'));
|
|
681
|
+
document.querySelector('[data-view="promotion"]').classList.add('active');
|
|
682
|
+
document.querySelectorAll('.main > .view').forEach(v => v.classList.remove('active'));
|
|
683
|
+
document.getElementById('v-promotion').classList.add('active');
|
|
684
|
+
document.querySelectorAll('.sub-tab').forEach(x => x.classList.remove('active'));
|
|
685
|
+
document.querySelector('[data-ptab="scoring"]').classList.add('active');
|
|
686
|
+
document.getElementById('promo-thresholds').classList.remove('active');
|
|
687
|
+
document.getElementById('promo-candidates').classList.remove('active');
|
|
688
|
+
document.getElementById('promo-scoring').classList.add('active');
|
|
689
|
+
loadScoringTab();
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function deleteContext(name) {
|
|
693
|
+
if (!confirm(`Delete context "${name}" and all its episodes? This cannot be undone.`)) return;
|
|
694
|
+
try {
|
|
695
|
+
const res = await fetch(API + '/api/contexts/' + encodeURIComponent(name), { method: 'DELETE' });
|
|
696
|
+
if (res.ok || res.status === 204) {
|
|
697
|
+
showNotification('Context "' + name + '" deleted', 'success');
|
|
698
|
+
loadContexts();
|
|
699
|
+
} else {
|
|
700
|
+
showNotification('Failed to delete context', 'error');
|
|
701
|
+
}
|
|
702
|
+
} catch (err) {
|
|
703
|
+
showNotification('Delete failed: ' + err.message, 'error');
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ===== Memory Browser =====
|
|
336
708
|
async function loadMemoryContexts() {
|
|
337
709
|
try {
|
|
338
|
-
const res = await fetch(
|
|
710
|
+
const res = await fetch(API + '/api/contexts');
|
|
711
|
+
if (!res.ok) {
|
|
712
|
+
showNotification('Failed to load memory contexts', 'error');
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
339
715
|
const data = await res.json();
|
|
340
716
|
const sel = document.getElementById('mem-context');
|
|
341
717
|
const current = sel.value;
|
|
342
|
-
sel.innerHTML = '<option value="">Select context
|
|
718
|
+
sel.innerHTML = '<option value="">Select context...</option>';
|
|
343
719
|
for (const c of (data.contexts || [])) {
|
|
344
720
|
sel.innerHTML += `<option value="${esc(c.name)}">${esc(c.name)} (${c.episode_count ?? '?'} eps)</option>`;
|
|
345
721
|
}
|
|
346
722
|
if (current) { sel.value = current; loadL3(current); loadL2(current); }
|
|
347
|
-
} catch {
|
|
723
|
+
} catch (err) {
|
|
724
|
+
showNotification('Failed to load contexts: ' + err.message, 'error');
|
|
725
|
+
}
|
|
348
726
|
}
|
|
349
727
|
|
|
728
|
+
// ----- L3 Knowledge -----
|
|
350
729
|
async function loadL3(ctx) {
|
|
351
730
|
const el = document.getElementById('mem-l3');
|
|
352
731
|
try {
|
|
353
|
-
const res = await fetch(
|
|
732
|
+
const res = await fetch(API + '/api/contexts/' + encodeURIComponent(ctx) + '/l3');
|
|
733
|
+
if (!res.ok) { el.innerHTML = '<p style="color:var(--red)">Failed to load L3 data</p>'; showNotification('L3 load failed', 'error'); return; }
|
|
354
734
|
const data = await res.json();
|
|
355
735
|
const entries = data.entries || [];
|
|
736
|
+
|
|
737
|
+
let html = `<div style="margin-bottom:12px"><button class="btn-sm btn-primary" onclick="runPromotion('${esc(ctx)}')">Run Promotion</button></div>`;
|
|
738
|
+
|
|
356
739
|
if (entries.length === 0) {
|
|
357
|
-
|
|
740
|
+
html += '<p style="color:var(--text2)">No L3 knowledge entries</p>';
|
|
741
|
+
el.innerHTML = html;
|
|
358
742
|
return;
|
|
359
743
|
}
|
|
360
|
-
|
|
361
|
-
<div class="l3-entry" style="
|
|
362
|
-
<div style="flex:
|
|
363
|
-
|
|
744
|
+
html += entries.map(e => `
|
|
745
|
+
<div class="l3-entry" style="margin-bottom:8px;padding:10px" id="l3-${esc(e.id)}">
|
|
746
|
+
<div style="display:flex;justify-content:space-between;align-items:start">
|
|
747
|
+
<div style="flex:1" class="l3-content">${esc(e.content)}</div>
|
|
748
|
+
<div style="display:flex;gap:4px;margin-left:8px">
|
|
749
|
+
<button class="btn-sm" onclick="editL3('${esc(e.id)}','${esc(ctx)}')">Edit</button>
|
|
750
|
+
<button class="btn-sm btn-danger" onclick="deleteL3('${esc(e.id)}','${esc(ctx)}')">Delete</button>
|
|
751
|
+
</div>
|
|
752
|
+
</div>
|
|
753
|
+
<div style="font-size:11px;color:var(--text2);margin-top:4px">
|
|
754
|
+
source: ${esc(e.source_episode_id?.slice(0,8))} · access: ${e.access_count} · density: ${e.connection_density} · promoted: ${e.promoted_at ? new Date(e.promoted_at).toLocaleString() : '?'}
|
|
755
|
+
</div>
|
|
364
756
|
</div>
|
|
365
757
|
`).join('');
|
|
758
|
+
el.innerHTML = html;
|
|
366
759
|
} catch (err) {
|
|
367
760
|
el.innerHTML = `<p style="color:var(--red)">Failed: ${esc(err.message)}</p>`;
|
|
368
761
|
}
|
|
@@ -370,41 +763,390 @@ async function loadL3(ctx) {
|
|
|
370
763
|
|
|
371
764
|
async function deleteL3(id, ctx) {
|
|
372
765
|
if (!confirm('Delete this L3 entry?')) return;
|
|
373
|
-
|
|
374
|
-
|
|
766
|
+
try {
|
|
767
|
+
const res = await fetch(API + '/api/l3/' + encodeURIComponent(id), { method: 'DELETE' });
|
|
768
|
+
if (res.ok || res.status === 204) {
|
|
769
|
+
showNotification('L3 entry deleted', 'success');
|
|
770
|
+
loadL3(ctx);
|
|
771
|
+
} else {
|
|
772
|
+
const data = await res.json().catch(() => ({}));
|
|
773
|
+
showNotification('Delete failed: ' + (data.error || res.statusText), 'error');
|
|
774
|
+
}
|
|
775
|
+
} catch (err) {
|
|
776
|
+
showNotification('Delete failed: ' + err.message, 'error');
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function editL3(id, ctx) {
|
|
781
|
+
const el = document.getElementById('l3-' + id);
|
|
782
|
+
const contentEl = el.querySelector('.l3-content');
|
|
783
|
+
const currentContent = contentEl.textContent;
|
|
784
|
+
contentEl.innerHTML = `
|
|
785
|
+
<textarea class="l3-edit-area" id="l3-edit-${id}">${esc(currentContent)}</textarea>
|
|
786
|
+
<div style="margin-top:6px;display:flex;gap:4px">
|
|
787
|
+
<button class="btn-sm btn-primary" onclick="saveL3('${esc(id)}','${esc(ctx)}')">Save</button>
|
|
788
|
+
<button class="btn-sm" onclick="loadL3('${esc(ctx)}')">Cancel</button>
|
|
789
|
+
</div>
|
|
790
|
+
`;
|
|
375
791
|
}
|
|
376
792
|
|
|
793
|
+
async function saveL3(id, ctx) {
|
|
794
|
+
const textarea = document.getElementById('l3-edit-' + id);
|
|
795
|
+
const content = textarea.value;
|
|
796
|
+
try {
|
|
797
|
+
const res = await fetch(API + '/api/l3/' + encodeURIComponent(id), {
|
|
798
|
+
method: 'PATCH',
|
|
799
|
+
headers: { 'Content-Type': 'application/json' },
|
|
800
|
+
body: JSON.stringify({ content }),
|
|
801
|
+
});
|
|
802
|
+
if (res.ok || res.status === 204) {
|
|
803
|
+
showNotification('L3 entry updated', 'success');
|
|
804
|
+
loadL3(ctx);
|
|
805
|
+
} else {
|
|
806
|
+
showNotification('Update failed: ' + res.statusText, 'error');
|
|
807
|
+
}
|
|
808
|
+
} catch (err) {
|
|
809
|
+
showNotification('Update failed: ' + err.message, 'error');
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
async function runPromotion(ctx) {
|
|
814
|
+
try {
|
|
815
|
+
const res = await fetch(API + '/api/promotion/run?context=' + encodeURIComponent(ctx), { method: 'POST' });
|
|
816
|
+
if (res.ok) {
|
|
817
|
+
const data = await res.json();
|
|
818
|
+
showNotification(`Promotion complete: ${data.promoted_count} episodes promoted`, 'success');
|
|
819
|
+
loadL3(ctx);
|
|
820
|
+
} else {
|
|
821
|
+
showNotification('Promotion failed: ' + res.statusText, 'error');
|
|
822
|
+
}
|
|
823
|
+
} catch (err) {
|
|
824
|
+
showNotification('Promotion failed: ' + err.message, 'error');
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// ----- L2 Episodes -----
|
|
377
829
|
async function loadL2(ctx) {
|
|
378
830
|
const el = document.getElementById('mem-l2');
|
|
831
|
+
l2SearchMode = false;
|
|
832
|
+
l2Page = 0;
|
|
379
833
|
try {
|
|
380
|
-
const res = await fetch(
|
|
834
|
+
const res = await fetch(API + '/api/contexts/' + encodeURIComponent(ctx) + '/episodes');
|
|
835
|
+
if (!res.ok) { el.innerHTML = '<p style="color:var(--red)">Failed to load episodes</p>'; return; }
|
|
381
836
|
const data = await res.json();
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
837
|
+
l2AllEpisodes = Array.isArray(data) ? data : (data.episodes || []);
|
|
838
|
+
renderL2(ctx);
|
|
839
|
+
} catch (err) {
|
|
840
|
+
el.innerHTML = `<p style="color:var(--red)">Failed: ${esc(err.message)}</p>`;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function renderL2(ctx) {
|
|
845
|
+
const el = document.getElementById('mem-l2');
|
|
846
|
+
const start = l2Page * L2_PAGE_SIZE;
|
|
847
|
+
const pageEpisodes = l2AllEpisodes.slice(start, start + L2_PAGE_SIZE);
|
|
848
|
+
const totalPages = Math.ceil(l2AllEpisodes.length / L2_PAGE_SIZE);
|
|
849
|
+
|
|
850
|
+
let html = `
|
|
851
|
+
<div class="filters" style="margin-bottom:12px">
|
|
852
|
+
<input type="text" id="l2-search" placeholder="Semantic search..." style="flex:1" />
|
|
853
|
+
<button onclick="searchL2('${esc(ctx)}')">Search</button>
|
|
854
|
+
${l2SearchMode ? `<button onclick="loadL2('${esc(ctx)}')">Clear Search</button>` : ''}
|
|
855
|
+
</div>
|
|
856
|
+
`;
|
|
857
|
+
|
|
858
|
+
if (l2AllEpisodes.length === 0) {
|
|
859
|
+
html += '<p style="color:var(--text2)">No episodes</p>';
|
|
860
|
+
el.innerHTML = html;
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
html += pageEpisodes.map(ep => `
|
|
865
|
+
<div class="episode">
|
|
866
|
+
<div class="meta">
|
|
867
|
+
${esc(ep.role)} · ${new Date(ep.timestamp).toLocaleString()} · ${esc(ep.id?.slice(0,8) || '')}
|
|
868
|
+
${ep.score !== undefined ? ` · <span class="score">score: ${ep.score.toFixed(4)}</span>` : ''}
|
|
869
|
+
</div>
|
|
870
|
+
${esc(ep.content)}
|
|
871
|
+
</div>
|
|
872
|
+
`).join('');
|
|
873
|
+
|
|
874
|
+
if (totalPages > 1) {
|
|
875
|
+
html += `<div class="pagination">
|
|
876
|
+
<button ${l2Page === 0 ? 'disabled' : ''} onclick="l2Page--; renderL2('${esc(ctx)}')">Prev</button>
|
|
877
|
+
<span>Page ${l2Page + 1} of ${totalPages} (${l2AllEpisodes.length} total)</span>
|
|
878
|
+
<button ${l2Page >= totalPages - 1 ? 'disabled' : ''} onclick="l2Page++; renderL2('${esc(ctx)}')">Next</button>
|
|
879
|
+
</div>`;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
el.innerHTML = html;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
async function searchL2(ctx) {
|
|
886
|
+
const query = document.getElementById('l2-search')?.value;
|
|
887
|
+
if (!query?.trim()) return;
|
|
888
|
+
const el = document.getElementById('mem-l2');
|
|
889
|
+
try {
|
|
890
|
+
const res = await fetch(API + '/api/search?q=' + encodeURIComponent(query) + '&context=' + encodeURIComponent(ctx) + '&limit=50');
|
|
891
|
+
if (!res.ok) { showNotification('Search failed', 'error'); return; }
|
|
892
|
+
const data = await res.json();
|
|
893
|
+
l2AllEpisodes = (data.episodes || []).map(e => ({...e.episode || e, score: e.score}));
|
|
894
|
+
l2SearchMode = true;
|
|
895
|
+
l2Page = 0;
|
|
896
|
+
renderL2(ctx);
|
|
897
|
+
} catch (err) {
|
|
898
|
+
showNotification('Search failed: ' + err.message, 'error');
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// ----- L1 History -----
|
|
903
|
+
async function loadL1(ctx) {
|
|
904
|
+
const el = document.getElementById('mem-l1');
|
|
905
|
+
try {
|
|
906
|
+
const res = await fetch(API + '/api/l1/' + encodeURIComponent(ctx));
|
|
907
|
+
if (!res.ok) { el.innerHTML = '<p style="color:var(--text2)">L1 data unavailable</p>'; return; }
|
|
908
|
+
const data = await res.json();
|
|
909
|
+
const messages = data.messages || [];
|
|
910
|
+
if (messages.length === 0) {
|
|
911
|
+
el.innerHTML = '<p style="color:var(--text2)">No L1 conversation history for this context</p>';
|
|
385
912
|
return;
|
|
386
913
|
}
|
|
387
|
-
el.innerHTML =
|
|
388
|
-
|
|
389
|
-
<div class="
|
|
390
|
-
|
|
914
|
+
el.innerHTML = `<p style="color:var(--text2);margin-bottom:12px">${messages.length} messages in working memory</p>` +
|
|
915
|
+
messages.map(m => `
|
|
916
|
+
<div class="l1-msg ${esc(m.role)}">
|
|
917
|
+
<div class="role">${esc(m.role)}</div>
|
|
918
|
+
${esc(m.content)}
|
|
919
|
+
</div>
|
|
920
|
+
`).join('');
|
|
921
|
+
} catch (err) {
|
|
922
|
+
el.innerHTML = `<p style="color:var(--text2)">L1 data unavailable</p>`;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// ===== Search View =====
|
|
927
|
+
async function loadSearchContexts() {
|
|
928
|
+
try {
|
|
929
|
+
const res = await fetch(API + '/api/contexts');
|
|
930
|
+
if (!res.ok) return;
|
|
931
|
+
const data = await res.json();
|
|
932
|
+
const sel = document.getElementById('search-scope');
|
|
933
|
+
sel.innerHTML = '<option value="all">All Contexts</option>';
|
|
934
|
+
for (const c of (data.contexts || [])) {
|
|
935
|
+
sel.innerHTML += `<option value="${esc(c.name)}">${esc(c.name)}</option>`;
|
|
936
|
+
}
|
|
937
|
+
} catch {}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
async function runSearch() {
|
|
941
|
+
const query = document.getElementById('search-query').value.trim();
|
|
942
|
+
if (!query) return;
|
|
943
|
+
const scope = document.getElementById('search-scope').value;
|
|
944
|
+
const limit = document.getElementById('search-limit').value || 20;
|
|
945
|
+
const el = document.getElementById('search-results');
|
|
946
|
+
el.innerHTML = '<p style="color:var(--text2)">Searching...</p>';
|
|
947
|
+
|
|
948
|
+
try {
|
|
949
|
+
let url, res;
|
|
950
|
+
if (scope === 'all') {
|
|
951
|
+
url = API + '/api/search/cross-context?q=' + encodeURIComponent(query) + '&limit=' + limit;
|
|
952
|
+
res = await fetch(url);
|
|
953
|
+
if (!res.ok) { showNotification('Search failed', 'error'); el.innerHTML = ''; return; }
|
|
954
|
+
const data = await res.json();
|
|
955
|
+
const results = data.results || [];
|
|
956
|
+
if (results.length === 0) {
|
|
957
|
+
el.innerHTML = '<p style="color:var(--text2)">No results found</p>';
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
let html = '';
|
|
961
|
+
for (const group of results) {
|
|
962
|
+
html += `<div class="search-group-header">${esc(group.context)} (${group.episodes.length})</div>`;
|
|
963
|
+
for (const ep of group.episodes) {
|
|
964
|
+
html += `<div class="search-result">
|
|
965
|
+
<div class="meta">${esc(ep.episode?.role || ep.role)} · ${esc(group.context)} · <span class="score">score: ${(ep.score || 0).toFixed(4)}</span> · ${ep.episode?.timestamp ? new Date(ep.episode.timestamp).toLocaleString() : ''}</div>
|
|
966
|
+
<div class="content">${esc(ep.episode?.content || ep.content)}</div>
|
|
967
|
+
</div>`;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
el.innerHTML = html;
|
|
971
|
+
} else {
|
|
972
|
+
url = API + '/api/search?q=' + encodeURIComponent(query) + '&context=' + encodeURIComponent(scope) + '&limit=' + limit;
|
|
973
|
+
res = await fetch(url);
|
|
974
|
+
if (!res.ok) { showNotification('Search failed', 'error'); el.innerHTML = ''; return; }
|
|
975
|
+
const data = await res.json();
|
|
976
|
+
const episodes = data.episodes || [];
|
|
977
|
+
if (episodes.length === 0) {
|
|
978
|
+
el.innerHTML = '<p style="color:var(--text2)">No results found</p>';
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
el.innerHTML = episodes.map(ep => `
|
|
982
|
+
<div class="search-result">
|
|
983
|
+
<div class="meta">${esc(ep.episode?.role || ep.role)} · ${esc(scope)} · <span class="score">score: ${(ep.score || 0).toFixed(4)}</span> · ${ep.episode?.timestamp ? new Date(ep.episode.timestamp).toLocaleString() : ''}</div>
|
|
984
|
+
<div class="content">${esc(ep.episode?.content || ep.content)}</div>
|
|
985
|
+
</div>
|
|
986
|
+
`).join('');
|
|
987
|
+
}
|
|
988
|
+
} catch (err) {
|
|
989
|
+
showNotification('Search failed: ' + err.message, 'error');
|
|
990
|
+
el.innerHTML = '';
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// ===== Promotion & Scoring View =====
|
|
995
|
+
async function loadPromotion() {
|
|
996
|
+
loadThresholds();
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
async function loadThresholds() {
|
|
1000
|
+
const el = document.getElementById('promo-thresholds');
|
|
1001
|
+
try {
|
|
1002
|
+
const res = await fetch(API + '/api/stats');
|
|
1003
|
+
if (!res.ok) { el.innerHTML = '<p style="color:var(--red)">Failed to load stats</p>'; return; }
|
|
1004
|
+
const stats = await res.json();
|
|
1005
|
+
const t = stats.promotion_thresholds || { access: 5, cooccurrence: 3 };
|
|
1006
|
+
el.innerHTML = `
|
|
1007
|
+
<div class="status-grid" style="margin-bottom:24px">
|
|
1008
|
+
<div class="status-card">
|
|
1009
|
+
<div class="label">Access Threshold</div>
|
|
1010
|
+
<div class="value">${t.access}</div>
|
|
1011
|
+
<div class="sub">Episode must be accessed at least ${t.access} times</div>
|
|
1012
|
+
</div>
|
|
1013
|
+
<div class="status-card">
|
|
1014
|
+
<div class="label">Co-occurrence Threshold</div>
|
|
1015
|
+
<div class="value">${t.cooccurrence}</div>
|
|
1016
|
+
<div class="sub">Episode must co-occur with others at least ${t.cooccurrence} times</div>
|
|
1017
|
+
</div>
|
|
391
1018
|
</div>
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
1019
|
+
<h3 style="margin-bottom:12px">How Promotion Works</h3>
|
|
1020
|
+
<div style="color:var(--text2);font-size:13px;line-height:1.8">
|
|
1021
|
+
<p><strong>L2 → L3 promotion</strong> happens when an episode meets <em>both</em> thresholds:</p>
|
|
1022
|
+
<ol style="margin:8px 0 8px 20px">
|
|
1023
|
+
<li>The episode has been accessed (retrieved in search results) at least <strong>${t.access} times</strong></li>
|
|
1024
|
+
<li>The episode has co-occurred with other episodes at least <strong>${t.cooccurrence} times total</strong> (connection density)</li>
|
|
1025
|
+
</ol>
|
|
1026
|
+
<p>Once promoted, the episode's content is stored as long-term semantic knowledge (L3) and included in future system prompts for that context.</p>
|
|
1027
|
+
</div>
|
|
1028
|
+
`;
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
el.innerHTML = `<p style="color:var(--red)">Failed: ${esc(err.message)}</p>`;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
async function loadCandidates() {
|
|
1035
|
+
const el = document.getElementById('promo-candidates');
|
|
1036
|
+
el.innerHTML = '<p style="color:var(--text2)">Loading candidates...</p>';
|
|
1037
|
+
try {
|
|
1038
|
+
const res = await fetch(API + '/api/access/top?limit=50');
|
|
1039
|
+
if (!res.ok) { el.innerHTML = '<p style="color:var(--red)">Failed to load candidates</p>'; return; }
|
|
1040
|
+
const data = await res.json();
|
|
1041
|
+
const records = data.records || [];
|
|
1042
|
+
|
|
1043
|
+
if (records.length === 0) {
|
|
1044
|
+
el.innerHTML = '<p style="color:var(--text2)">No access records found. Episodes need to be searched/retrieved to generate access counts.</p>';
|
|
1045
|
+
return;
|
|
395
1046
|
}
|
|
1047
|
+
|
|
1048
|
+
el.innerHTML = records.map(r => {
|
|
1049
|
+
const accessPct = Math.min((r.access_count / 5) * 100, 100);
|
|
1050
|
+
const densityPct = Math.min((r.connection_density / 3) * 100, 100);
|
|
1051
|
+
const promoted = r.is_promoted;
|
|
1052
|
+
return `
|
|
1053
|
+
<div class="ctx-card">
|
|
1054
|
+
<div style="display:flex;justify-content:space-between;align-items:start">
|
|
1055
|
+
<div style="flex:1">
|
|
1056
|
+
<div style="font-size:12px;color:var(--text2)">${esc(r.context_name || '?')} · ${esc(r.episode_id.slice(0, 12))}${promoted ? ' · <span style="color:var(--green)">PROMOTED</span>' : ''}</div>
|
|
1057
|
+
<div style="font-size:13px;margin-top:4px">${esc(r.content_preview || '(no content)')}</div>
|
|
1058
|
+
</div>
|
|
1059
|
+
</div>
|
|
1060
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:10px">
|
|
1061
|
+
<div>
|
|
1062
|
+
<div style="font-size:11px;color:var(--text2);margin-bottom:2px">Access: ${r.access_count}/5</div>
|
|
1063
|
+
<div class="progress-bar"><div class="progress-fill ${accessPct >= 100 ? 'green' : 'yellow'}" style="width:${accessPct}%">${r.access_count}/5</div></div>
|
|
1064
|
+
</div>
|
|
1065
|
+
<div>
|
|
1066
|
+
<div style="font-size:11px;color:var(--text2);margin-bottom:2px">Density: ${r.connection_density}/3</div>
|
|
1067
|
+
<div class="progress-bar"><div class="progress-fill ${densityPct >= 100 ? 'green' : 'purple'}" style="width:${densityPct}%">${r.connection_density}/3</div></div>
|
|
1068
|
+
</div>
|
|
1069
|
+
</div>
|
|
1070
|
+
</div>
|
|
1071
|
+
`;
|
|
1072
|
+
}).join('');
|
|
396
1073
|
} catch (err) {
|
|
397
1074
|
el.innerHTML = `<p style="color:var(--red)">Failed: ${esc(err.message)}</p>`;
|
|
398
1075
|
}
|
|
399
1076
|
}
|
|
400
1077
|
|
|
1078
|
+
async function loadScoringTab() {
|
|
1079
|
+
const el = document.getElementById('promo-scoring');
|
|
1080
|
+
try {
|
|
1081
|
+
const ctxRes = await fetch(API + '/api/contexts');
|
|
1082
|
+
if (!ctxRes.ok) { el.innerHTML = '<p style="color:var(--red)">Failed to load contexts</p>'; return; }
|
|
1083
|
+
const ctxData = await ctxRes.json();
|
|
1084
|
+
const contexts = ctxData.contexts || [];
|
|
1085
|
+
|
|
1086
|
+
// Fetch scoring for each context
|
|
1087
|
+
const scoringData = {};
|
|
1088
|
+
let defaultHL = 48;
|
|
1089
|
+
for (const c of contexts) {
|
|
1090
|
+
try {
|
|
1091
|
+
const sRes = await fetch(API + '/api/contexts/' + encodeURIComponent(c.name) + '/scoring');
|
|
1092
|
+
if (sRes.ok) {
|
|
1093
|
+
const sd = await sRes.json();
|
|
1094
|
+
scoringData[c.name] = sd;
|
|
1095
|
+
defaultHL = sd.default_half_life_hours || 48;
|
|
1096
|
+
}
|
|
1097
|
+
} catch {}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
let html = `<p style="color:var(--text2);font-size:13px;margin-bottom:16px">Default half-life: <strong>${defaultHL}h</strong>. Settings reset when daemon restarts (in-memory only).</p>`;
|
|
1101
|
+
|
|
1102
|
+
html += '<table class="data-table"><thead><tr><th>Context</th><th>Half-Life (hours)</th><th>Custom?</th><th>Decay Curve</th><th>Actions</th></tr></thead><tbody>';
|
|
1103
|
+
for (const c of contexts) {
|
|
1104
|
+
const sd = scoringData[c.name] || {};
|
|
1105
|
+
const hl = sd.half_life_hours || defaultHL;
|
|
1106
|
+
const isCustom = sd.is_default === false;
|
|
1107
|
+
html += `<tr>
|
|
1108
|
+
<td style="color:var(--accent)">${esc(c.name)}</td>
|
|
1109
|
+
<td><input type="number" value="${hl}" min="1" max="8760" step="1" style="width:80px;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:4px;border-radius:4px" id="hl-${esc(c.name)}" /></td>
|
|
1110
|
+
<td>${isCustom ? '<span style="color:var(--yellow)">Yes</span>' : 'Default'}</td>
|
|
1111
|
+
<td>${renderDecayCurve(hl)}</td>
|
|
1112
|
+
<td><button class="btn-sm" onclick="saveScoring('${esc(c.name)}')">Save</button></td>
|
|
1113
|
+
</tr>`;
|
|
1114
|
+
}
|
|
1115
|
+
html += '</tbody></table>';
|
|
1116
|
+
el.innerHTML = html;
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
el.innerHTML = `<p style="color:var(--red)">Failed: ${esc(err.message)}</p>`;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async function saveScoring(ctx) {
|
|
1123
|
+
const input = document.getElementById('hl-' + ctx);
|
|
1124
|
+
const hours = parseFloat(input.value);
|
|
1125
|
+
if (!hours || hours <= 0) { showNotification('Invalid half-life value', 'warning'); return; }
|
|
1126
|
+
try {
|
|
1127
|
+
const res = await fetch(API + '/api/contexts/' + encodeURIComponent(ctx) + '/scoring', {
|
|
1128
|
+
method: 'POST',
|
|
1129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1130
|
+
body: JSON.stringify({ half_life_hours: hours }),
|
|
1131
|
+
});
|
|
1132
|
+
if (res.ok || res.status === 204) {
|
|
1133
|
+
showNotification(`Half-life for "${ctx}" set to ${hours}h`, 'success');
|
|
1134
|
+
} else {
|
|
1135
|
+
showNotification('Failed to save scoring', 'error');
|
|
1136
|
+
}
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
showNotification('Failed: ' + err.message, 'error');
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// ===== Utility =====
|
|
401
1143
|
function esc(s) {
|
|
402
1144
|
if (!s) return '';
|
|
403
1145
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
404
1146
|
}
|
|
405
1147
|
|
|
406
|
-
// Auto-load
|
|
407
|
-
|
|
1148
|
+
// ===== Auto-load =====
|
|
1149
|
+
loadHealthView();
|
|
408
1150
|
</script>
|
|
409
1151
|
</body>
|
|
410
1152
|
</html>
|