@sesamespace/hivemind 0.8.13 → 0.11.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/.pnpmrc.json +1 -1
- package/AUTOMATIC-MEMORY-MANAGEMENT.md +109 -0
- package/README.md +2 -1
- package/dist/{chunk-HTLHMXAL.js → chunk-4C6B2AMB.js} +2 -2
- package/dist/{chunk-NSTTILSN.js → chunk-4YXOQGQC.js} +79 -2
- package/dist/chunk-4YXOQGQC.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-K6KL2VD6.js} +2 -2
- package/dist/{chunk-MLY4VFOO.js → chunk-LYL5GG2F.js} +3 -3
- package/dist/{chunk-ZM7RK5YV.js → chunk-OB6OXLPC.js} +989 -38
- package/dist/chunk-OB6OXLPC.js.map +1 -0
- package/dist/{chunk-PFZO67E2.js → chunk-ZA4NWNS6.js} +2 -2
- 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 +913 -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/package.json +1 -1
- 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/dist/{chunk-HTLHMXAL.js.map → chunk-4C6B2AMB.js.map} +0 -0
- /package/dist/{chunk-4Y7A25UG.js.map → chunk-K6KL2VD6.js.map} +0 -0
- /package/dist/{chunk-MLY4VFOO.js.map → chunk-LYL5GG2F.js.map} +0 -0
- /package/dist/{chunk-PFZO67E2.js.map → chunk-ZA4NWNS6.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>
|
|
229
|
+
</div>
|
|
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>
|
|
127
256
|
</div>
|
|
128
|
-
|
|
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
|
+
}
|
|
285
|
+
|
|
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
|
+
}
|
|
139
329
|
|
|
140
|
-
|
|
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,210 @@ 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
|
+
let stats = null;
|
|
410
|
+
let contexts = [];
|
|
411
|
+
|
|
412
|
+
// Fetch stats and contexts independently so partial data still shows
|
|
413
|
+
try {
|
|
414
|
+
const ctxRes = await fetch(API + '/api/contexts');
|
|
415
|
+
if (ctxRes.ok) {
|
|
416
|
+
const ctxData = await ctxRes.json();
|
|
417
|
+
contexts = ctxData.contexts || [];
|
|
418
|
+
}
|
|
419
|
+
} catch {}
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
const statsRes = await fetch(API + '/api/stats');
|
|
423
|
+
if (statsRes.ok) stats = await statsRes.json();
|
|
424
|
+
} catch {}
|
|
425
|
+
|
|
426
|
+
if (!stats && contexts.length === 0) {
|
|
427
|
+
cards.innerHTML = '<p style="color:var(--red)">Cannot connect to memory daemon</p>';
|
|
428
|
+
showNotification('Health view failed: daemon may be offline', 'error');
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
const reqCountRes = await fetch(API + '/api/requests?limit=1');
|
|
434
|
+
var reqCount = reqCountRes.ok ? (await reqCountRes.json()).total || 0 : '?';
|
|
435
|
+
} catch { var reqCount = '?'; }
|
|
436
|
+
|
|
437
|
+
if (stats) {
|
|
438
|
+
const totalEpisodes = Object.values(stats.total_episodes || {}).reduce((a, b) => a + b, 0);
|
|
439
|
+
cards.innerHTML = `
|
|
440
|
+
<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>
|
|
441
|
+
<div class="status-card"><div class="label">Embedding Model</div><div class="value" style="font-size:14px">${esc(stats.embedding_model)}</div></div>
|
|
442
|
+
<div class="status-card"><div class="label">Total Episodes</div><div class="value">${totalEpisodes}</div><div class="sub">${contexts.length} contexts</div></div>
|
|
443
|
+
<div class="status-card"><div class="label">L3 Entries</div><div class="value">${stats.total_l3_entries}</div></div>
|
|
444
|
+
<div class="status-card"><div class="label">Access Records</div><div class="value">${stats.total_access_records}</div></div>
|
|
445
|
+
<div class="status-card"><div class="label">Request Logs</div><div class="value">${reqCount}</div></div>
|
|
446
|
+
`;
|
|
447
|
+
} else {
|
|
448
|
+
const totalEpisodes = contexts.reduce((a, c) => a + (c.episode_count || 0), 0);
|
|
449
|
+
cards.innerHTML = `
|
|
450
|
+
<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>
|
|
451
|
+
<div class="status-card"><div class="label">Stats</div><div class="value" style="font-size:14px;color:var(--yellow)">Unavailable</div><div class="sub">/stats endpoint not found — rebuild daemon</div></div>
|
|
452
|
+
<div class="status-card"><div class="label">Total Episodes</div><div class="value">${totalEpisodes}</div><div class="sub">${contexts.length} contexts</div></div>
|
|
453
|
+
<div class="status-card"><div class="label">Request Logs</div><div class="value">${reqCount}</div></div>
|
|
454
|
+
`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Context summary table — always populate from contexts data
|
|
458
|
+
tbody.innerHTML = '';
|
|
459
|
+
for (const c of contexts) {
|
|
460
|
+
const epCount = stats ? (stats.total_episodes?.[c.name] ?? c.episode_count ?? 0) : (c.episode_count ?? 0);
|
|
461
|
+
const tr = document.createElement('tr');
|
|
462
|
+
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>`;
|
|
463
|
+
tbody.appendChild(tr);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Scoring section
|
|
467
|
+
const defaultHL = (stats && stats.default_half_life_hours) || 48;
|
|
468
|
+
if (stats) {
|
|
469
|
+
scoringEl.innerHTML = `
|
|
470
|
+
<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>
|
|
471
|
+
${renderDecayCurve(defaultHL)}
|
|
472
|
+
`;
|
|
473
|
+
} else {
|
|
474
|
+
scoringEl.innerHTML = `
|
|
475
|
+
<p style="color:var(--yellow);font-size:13px;margin-bottom:12px">Stats unavailable — showing default half-life: <strong>${defaultHL}h</strong>.</p>
|
|
476
|
+
${renderDecayCurve(defaultHL)}
|
|
477
|
+
`;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function renderDecayCurve(halfLife) {
|
|
482
|
+
const w = 300, h = 100, pad = 30;
|
|
483
|
+
const points = [];
|
|
484
|
+
const maxH = halfLife * 4;
|
|
485
|
+
for (let t = 0; t <= maxH; t += maxH / 50) {
|
|
486
|
+
const weight = Math.exp(-Math.LN2 / halfLife * t);
|
|
487
|
+
const x = pad + (t / maxH) * (w - pad * 2);
|
|
488
|
+
const y = pad + (1 - weight) * (h - pad * 2);
|
|
489
|
+
points.push(`${x},${y}`);
|
|
490
|
+
}
|
|
491
|
+
return `<svg width="${w}" height="${h}" class="decay-curve" style="background:var(--bg);border:1px solid var(--border);border-radius:4px">
|
|
492
|
+
<polyline points="${points.join(' ')}" fill="none" stroke="var(--accent)" stroke-width="2"/>
|
|
493
|
+
<text x="${pad}" y="${pad - 8}" fill="var(--text2)" font-size="10">1.0</text>
|
|
494
|
+
<text x="${pad}" y="${h - 8}" fill="var(--text2)" font-size="10">0.0</text>
|
|
495
|
+
<text x="${w - pad}" y="${h - 8}" fill="var(--text2)" font-size="10">${maxH}h</text>
|
|
496
|
+
<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"/>
|
|
497
|
+
<text x="${pad + (halfLife / maxH) * (w - pad * 2) - 10}" y="${h - 8}" fill="var(--yellow)" font-size="9">${halfLife}h</text>
|
|
498
|
+
</svg>`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ===== Requests =====
|
|
171
502
|
async function loadRequests() {
|
|
172
503
|
const ctx = document.getElementById('f-context').value;
|
|
173
504
|
const sender = document.getElementById('f-sender').value;
|
|
174
505
|
const params = new URLSearchParams({ limit: REQ_LIMIT, offset: reqOffset });
|
|
175
506
|
if (ctx) params.set('context', ctx);
|
|
176
507
|
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
|
-
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
const res = await fetch(API + '/api/requests?' + params);
|
|
511
|
+
if (!res.ok) { showNotification('Failed to load requests', 'error'); return; }
|
|
512
|
+
const data = await res.json();
|
|
513
|
+
|
|
514
|
+
const list = document.getElementById('req-list');
|
|
515
|
+
list.innerHTML = '';
|
|
516
|
+
|
|
517
|
+
for (const r of data.requests) {
|
|
518
|
+
const row = document.createElement('div');
|
|
519
|
+
row.className = 'req-row';
|
|
520
|
+
const ts = new Date(r.timestamp).toLocaleString();
|
|
521
|
+
const latency = r.response_latency_ms + 'ms';
|
|
522
|
+
const tokens = r.token_est_total;
|
|
523
|
+
const model = r.response_model.split('/').pop().slice(0, 15);
|
|
524
|
+
|
|
525
|
+
row.innerHTML = `
|
|
526
|
+
<div class="req-summary">
|
|
527
|
+
<span class="ts">${ts}</span>
|
|
528
|
+
<span class="sender">${esc(r.sender_handle || 'stdin')}</span>
|
|
529
|
+
<span class="ctx">${esc(r.context)}</span>
|
|
530
|
+
<span class="model">${esc(model)}</span>
|
|
531
|
+
<span class="latency">${latency}</span>
|
|
532
|
+
<span class="tokens">~${tokens}t</span>
|
|
533
|
+
</div>
|
|
534
|
+
<div class="req-detail" id="detail-${r.id}"></div>
|
|
535
|
+
`;
|
|
536
|
+
|
|
537
|
+
row.querySelector('.req-summary').addEventListener('click', () => {
|
|
538
|
+
const detail = document.getElementById('detail-' + r.id);
|
|
539
|
+
if (detail.classList.contains('open')) {
|
|
540
|
+
detail.classList.remove('open');
|
|
541
|
+
} else {
|
|
542
|
+
renderDetail(detail, r);
|
|
543
|
+
detail.classList.add('open');
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
list.appendChild(row);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Pagination
|
|
550
|
+
const pag = document.getElementById('req-pagination');
|
|
551
|
+
const page = Math.floor(reqOffset / REQ_LIMIT) + 1;
|
|
552
|
+
const totalPages = Math.ceil(data.total / REQ_LIMIT);
|
|
553
|
+
pag.innerHTML = `
|
|
554
|
+
<button ${reqOffset === 0 ? 'disabled' : ''} onclick="reqOffset -= ${REQ_LIMIT}; loadRequests()">Prev</button>
|
|
555
|
+
<span>Page ${page} of ${totalPages} (${data.total} total)</span>
|
|
556
|
+
<button ${reqOffset + REQ_LIMIT >= data.total ? 'disabled' : ''} onclick="reqOffset += ${REQ_LIMIT}; loadRequests()">Next</button>
|
|
202
557
|
`;
|
|
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);
|
|
558
|
+
} catch (err) {
|
|
559
|
+
showNotification('Failed to load requests: ' + err.message, 'error');
|
|
214
560
|
}
|
|
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
561
|
}
|
|
226
562
|
|
|
227
563
|
function renderDetail(el, r) {
|
|
228
564
|
const comps = typeof r.system_prompt_components === 'string' ? JSON.parse(r.system_prompt_components) : r.system_prompt_components;
|
|
229
565
|
const history = typeof r.conversation_history === 'string' ? JSON.parse(r.conversation_history) : r.conversation_history;
|
|
230
566
|
const config = typeof r.config_snapshot === 'string' ? JSON.parse(r.config_snapshot) : r.config_snapshot;
|
|
231
|
-
|
|
567
|
+
|
|
232
568
|
const totalTokens = r.token_est_total || 1;
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
|
|
569
|
+
const sysTokens = r.token_est_system || 0;
|
|
570
|
+
const histTokens = r.token_est_history || 0;
|
|
571
|
+
const userTokens = r.token_est_user || 0;
|
|
572
|
+
const sysPct = Math.round((sysTokens / totalTokens) * 100);
|
|
573
|
+
const histPct = Math.round((histTokens / totalTokens) * 100);
|
|
574
|
+
const userPct = Math.max(100 - sysPct - histPct, 0);
|
|
575
|
+
|
|
237
576
|
let html = '';
|
|
238
|
-
|
|
577
|
+
|
|
239
578
|
// Config
|
|
240
579
|
html += `<div class="section"><div class="config-grid">
|
|
241
580
|
<div class="config-item"><div class="label">Model</div><div class="value">${esc(config?.model || r.response_model)}</div></div>
|
|
@@ -243,56 +582,62 @@ function renderDetail(el, r) {
|
|
|
243
582
|
<div class="config-item"><div class="label">Top K</div><div class="value">${config?.topK ?? '?'}</div></div>
|
|
244
583
|
<div class="config-item"><div class="label">Temperature</div><div class="value">${config?.temperature ?? '?'}</div></div>
|
|
245
584
|
</div></div>`;
|
|
246
|
-
|
|
247
|
-
// Token bar
|
|
585
|
+
|
|
586
|
+
// Token cards + bar
|
|
248
587
|
html += `<div class="section">
|
|
249
588
|
<div class="section-header open">Token Breakdown (~${totalTokens} total)</div>
|
|
250
589
|
<div class="section-body open">
|
|
590
|
+
<div class="token-cards">
|
|
591
|
+
<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>
|
|
592
|
+
<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>
|
|
593
|
+
<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>
|
|
594
|
+
<div class="token-card"><div class="label">Total</div><div class="value">${totalTokens.toLocaleString()}</div><div class="pct">100%</div></div>
|
|
595
|
+
</div>
|
|
251
596
|
<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)}%"
|
|
597
|
+
<div class="tb-sys" style="width:${sysPct}%" title="System: ${sysTokens} tokens (${sysPct}%)">sys</div>
|
|
598
|
+
<div class="tb-hist" style="width:${histPct}%" title="History: ${histTokens} tokens (${histPct}%)">${histTokens > 0 ? 'hist' : ''}</div>
|
|
599
|
+
<div class="tb-user" style="width:${Math.max(userPct, 5)}%" title="User: ${userTokens} tokens (${userPct}%)">user</div>
|
|
255
600
|
</div>
|
|
256
601
|
</div>
|
|
257
602
|
</div>`;
|
|
258
|
-
|
|
603
|
+
|
|
259
604
|
// User message
|
|
260
605
|
html += section('User Message', `<pre>${esc(r.user_message)}</pre>`, true);
|
|
261
|
-
|
|
606
|
+
|
|
262
607
|
// Response
|
|
263
608
|
const skipped = r.response_skipped ? ' <span style="color:var(--yellow)">[SKIPPED]</span>' : '';
|
|
264
609
|
html += section('Response' + skipped, `<pre>${esc(r.response_content)}</pre>`, true);
|
|
265
|
-
|
|
610
|
+
|
|
266
611
|
// L3 Knowledge
|
|
267
612
|
if (comps?.l3Knowledge?.length > 0) {
|
|
268
613
|
const l3Html = comps.l3Knowledge.map(k => `<div class="l3-entry">${esc(k)}</div>`).join('');
|
|
269
614
|
html += section(`L3 Knowledge (${comps.l3Knowledge.length})`, l3Html, true);
|
|
270
615
|
}
|
|
271
|
-
|
|
616
|
+
|
|
272
617
|
// L2 Episodes
|
|
273
618
|
if (comps?.l2Episodes?.length > 0) {
|
|
274
619
|
const epHtml = comps.l2Episodes.map(ep => `
|
|
275
620
|
<div class="episode">
|
|
276
|
-
<div class="meta">${esc(ep.role)}
|
|
621
|
+
<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
622
|
${esc(ep.content)}
|
|
278
623
|
</div>
|
|
279
624
|
`).join('');
|
|
280
625
|
html += section(`L2 Episodes (${comps.l2Episodes.length})`, epHtml, true);
|
|
281
626
|
}
|
|
282
|
-
|
|
627
|
+
|
|
283
628
|
// L1 History
|
|
284
629
|
if (history?.length > 0) {
|
|
285
630
|
const histHtml = history.map(m => `<div class="episode"><div class="meta">${esc(m.role)}</div>${esc(m.content)}</div>`).join('');
|
|
286
631
|
html += section(`L1 History (${history.length} turns)`, histHtml, false);
|
|
287
632
|
}
|
|
288
|
-
|
|
289
|
-
// Identity
|
|
633
|
+
|
|
634
|
+
// Identity
|
|
290
635
|
if (comps?.identity) {
|
|
291
636
|
html += section('Identity Files', `<pre>${esc(comps.identity)}</pre>`, false);
|
|
292
637
|
}
|
|
293
|
-
|
|
638
|
+
|
|
294
639
|
el.innerHTML = html;
|
|
295
|
-
|
|
640
|
+
|
|
296
641
|
// Wire section toggles
|
|
297
642
|
el.querySelectorAll('.section-header').forEach(h => {
|
|
298
643
|
h.addEventListener('click', () => {
|
|
@@ -309,10 +654,11 @@ function section(title, content, startOpen) {
|
|
|
309
654
|
</div>`;
|
|
310
655
|
}
|
|
311
656
|
|
|
312
|
-
// Contexts
|
|
657
|
+
// ===== Contexts View =====
|
|
313
658
|
async function loadContexts() {
|
|
314
659
|
try {
|
|
315
|
-
const res = await fetch(
|
|
660
|
+
const res = await fetch(API + '/api/contexts');
|
|
661
|
+
if (!res.ok) { showNotification('Failed to load contexts', 'error'); return; }
|
|
316
662
|
const data = await res.json();
|
|
317
663
|
const contexts = data.contexts || [];
|
|
318
664
|
const list = document.getElementById('ctx-list');
|
|
@@ -323,8 +669,13 @@ async function loadContexts() {
|
|
|
323
669
|
list.innerHTML = contexts.map(c => `
|
|
324
670
|
<div class="ctx-card">
|
|
325
671
|
<h3>${esc(c.name)}</h3>
|
|
326
|
-
<div class="meta">${c.episode_count ?? '?'} episodes
|
|
672
|
+
<div class="meta">${c.episode_count ?? '?'} episodes · created ${c.created_at ? new Date(c.created_at).toLocaleDateString() : '?'}</div>
|
|
327
673
|
${c.description ? `<div class="meta">${esc(c.description)}</div>` : ''}
|
|
674
|
+
<div class="actions">
|
|
675
|
+
<button class="btn-sm btn-primary" onclick="drillToMemory('${esc(c.name)}')">Browse Episodes</button>
|
|
676
|
+
<button class="btn-sm" onclick="viewScoring('${esc(c.name)}')">Scoring</button>
|
|
677
|
+
${c.name !== 'global' ? `<button class="btn-sm btn-danger" onclick="deleteContext('${esc(c.name)}')">Delete</button>` : ''}
|
|
678
|
+
</div>
|
|
328
679
|
</div>
|
|
329
680
|
`).join('');
|
|
330
681
|
} catch (err) {
|
|
@@ -332,37 +683,100 @@ async function loadContexts() {
|
|
|
332
683
|
}
|
|
333
684
|
}
|
|
334
685
|
|
|
335
|
-
|
|
686
|
+
function drillToMemory(ctx) {
|
|
687
|
+
// Switch to memory view with context pre-selected
|
|
688
|
+
document.querySelectorAll('.sidebar a').forEach(x => x.classList.remove('active'));
|
|
689
|
+
document.querySelector('[data-view="memory"]').classList.add('active');
|
|
690
|
+
document.querySelectorAll('.main > .view').forEach(v => v.classList.remove('active'));
|
|
691
|
+
document.getElementById('v-memory').classList.add('active');
|
|
692
|
+
loadMemoryContexts().then(() => {
|
|
693
|
+
document.getElementById('mem-context').value = ctx;
|
|
694
|
+
loadL3(ctx);
|
|
695
|
+
loadL2(ctx);
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function viewScoring(ctx) {
|
|
700
|
+
// Switch to promotion > scoring
|
|
701
|
+
document.querySelectorAll('.sidebar a').forEach(x => x.classList.remove('active'));
|
|
702
|
+
document.querySelector('[data-view="promotion"]').classList.add('active');
|
|
703
|
+
document.querySelectorAll('.main > .view').forEach(v => v.classList.remove('active'));
|
|
704
|
+
document.getElementById('v-promotion').classList.add('active');
|
|
705
|
+
document.querySelectorAll('.sub-tab').forEach(x => x.classList.remove('active'));
|
|
706
|
+
document.querySelector('[data-ptab="scoring"]').classList.add('active');
|
|
707
|
+
document.getElementById('promo-thresholds').classList.remove('active');
|
|
708
|
+
document.getElementById('promo-candidates').classList.remove('active');
|
|
709
|
+
document.getElementById('promo-scoring').classList.add('active');
|
|
710
|
+
loadScoringTab();
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async function deleteContext(name) {
|
|
714
|
+
if (!confirm(`Delete context "${name}" and all its episodes? This cannot be undone.`)) return;
|
|
715
|
+
try {
|
|
716
|
+
const res = await fetch(API + '/api/contexts/' + encodeURIComponent(name), { method: 'DELETE' });
|
|
717
|
+
if (res.ok || res.status === 204) {
|
|
718
|
+
showNotification('Context "' + name + '" deleted', 'success');
|
|
719
|
+
loadContexts();
|
|
720
|
+
} else {
|
|
721
|
+
showNotification('Failed to delete context', 'error');
|
|
722
|
+
}
|
|
723
|
+
} catch (err) {
|
|
724
|
+
showNotification('Delete failed: ' + err.message, 'error');
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// ===== Memory Browser =====
|
|
336
729
|
async function loadMemoryContexts() {
|
|
337
730
|
try {
|
|
338
|
-
const res = await fetch(
|
|
731
|
+
const res = await fetch(API + '/api/contexts');
|
|
732
|
+
if (!res.ok) {
|
|
733
|
+
showNotification('Failed to load memory contexts', 'error');
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
339
736
|
const data = await res.json();
|
|
340
737
|
const sel = document.getElementById('mem-context');
|
|
341
738
|
const current = sel.value;
|
|
342
|
-
sel.innerHTML = '<option value="">Select context
|
|
739
|
+
sel.innerHTML = '<option value="">Select context...</option>';
|
|
343
740
|
for (const c of (data.contexts || [])) {
|
|
344
741
|
sel.innerHTML += `<option value="${esc(c.name)}">${esc(c.name)} (${c.episode_count ?? '?'} eps)</option>`;
|
|
345
742
|
}
|
|
346
743
|
if (current) { sel.value = current; loadL3(current); loadL2(current); }
|
|
347
|
-
} catch {
|
|
744
|
+
} catch (err) {
|
|
745
|
+
showNotification('Failed to load contexts: ' + err.message, 'error');
|
|
746
|
+
}
|
|
348
747
|
}
|
|
349
748
|
|
|
749
|
+
// ----- L3 Knowledge -----
|
|
350
750
|
async function loadL3(ctx) {
|
|
351
751
|
const el = document.getElementById('mem-l3');
|
|
352
752
|
try {
|
|
353
|
-
const res = await fetch(
|
|
753
|
+
const res = await fetch(API + '/api/contexts/' + encodeURIComponent(ctx) + '/l3');
|
|
754
|
+
if (!res.ok) { el.innerHTML = '<p style="color:var(--red)">Failed to load L3 data</p>'; showNotification('L3 load failed', 'error'); return; }
|
|
354
755
|
const data = await res.json();
|
|
355
756
|
const entries = data.entries || [];
|
|
757
|
+
|
|
758
|
+
let html = `<div style="margin-bottom:12px"><button class="btn-sm btn-primary" onclick="runPromotion('${esc(ctx)}')">Run Promotion</button></div>`;
|
|
759
|
+
|
|
356
760
|
if (entries.length === 0) {
|
|
357
|
-
|
|
761
|
+
html += '<p style="color:var(--text2)">No L3 knowledge entries</p>';
|
|
762
|
+
el.innerHTML = html;
|
|
358
763
|
return;
|
|
359
764
|
}
|
|
360
|
-
|
|
361
|
-
<div class="l3-entry" style="
|
|
362
|
-
<div style="flex:
|
|
363
|
-
|
|
765
|
+
html += entries.map(e => `
|
|
766
|
+
<div class="l3-entry" style="margin-bottom:8px;padding:10px" id="l3-${esc(e.id)}">
|
|
767
|
+
<div style="display:flex;justify-content:space-between;align-items:start">
|
|
768
|
+
<div style="flex:1" class="l3-content">${esc(e.content)}</div>
|
|
769
|
+
<div style="display:flex;gap:4px;margin-left:8px">
|
|
770
|
+
<button class="btn-sm" onclick="editL3('${esc(e.id)}','${esc(ctx)}')">Edit</button>
|
|
771
|
+
<button class="btn-sm btn-danger" onclick="deleteL3('${esc(e.id)}','${esc(ctx)}')">Delete</button>
|
|
772
|
+
</div>
|
|
773
|
+
</div>
|
|
774
|
+
<div style="font-size:11px;color:var(--text2);margin-top:4px">
|
|
775
|
+
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() : '?'}
|
|
776
|
+
</div>
|
|
364
777
|
</div>
|
|
365
778
|
`).join('');
|
|
779
|
+
el.innerHTML = html;
|
|
366
780
|
} catch (err) {
|
|
367
781
|
el.innerHTML = `<p style="color:var(--red)">Failed: ${esc(err.message)}</p>`;
|
|
368
782
|
}
|
|
@@ -370,41 +784,409 @@ async function loadL3(ctx) {
|
|
|
370
784
|
|
|
371
785
|
async function deleteL3(id, ctx) {
|
|
372
786
|
if (!confirm('Delete this L3 entry?')) return;
|
|
373
|
-
|
|
374
|
-
|
|
787
|
+
try {
|
|
788
|
+
const res = await fetch(API + '/api/l3/' + encodeURIComponent(id), { method: 'DELETE' });
|
|
789
|
+
if (res.ok || res.status === 204) {
|
|
790
|
+
showNotification('L3 entry deleted', 'success');
|
|
791
|
+
loadL3(ctx);
|
|
792
|
+
} else {
|
|
793
|
+
const data = await res.json().catch(() => ({}));
|
|
794
|
+
showNotification('Delete failed: ' + (data.error || res.statusText), 'error');
|
|
795
|
+
}
|
|
796
|
+
} catch (err) {
|
|
797
|
+
showNotification('Delete failed: ' + err.message, 'error');
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function editL3(id, ctx) {
|
|
802
|
+
const el = document.getElementById('l3-' + id);
|
|
803
|
+
const contentEl = el.querySelector('.l3-content');
|
|
804
|
+
const currentContent = contentEl.textContent;
|
|
805
|
+
contentEl.innerHTML = `
|
|
806
|
+
<textarea class="l3-edit-area" id="l3-edit-${id}">${esc(currentContent)}</textarea>
|
|
807
|
+
<div style="margin-top:6px;display:flex;gap:4px">
|
|
808
|
+
<button class="btn-sm btn-primary" onclick="saveL3('${esc(id)}','${esc(ctx)}')">Save</button>
|
|
809
|
+
<button class="btn-sm" onclick="loadL3('${esc(ctx)}')">Cancel</button>
|
|
810
|
+
</div>
|
|
811
|
+
`;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async function saveL3(id, ctx) {
|
|
815
|
+
const textarea = document.getElementById('l3-edit-' + id);
|
|
816
|
+
const content = textarea.value;
|
|
817
|
+
try {
|
|
818
|
+
const res = await fetch(API + '/api/l3/' + encodeURIComponent(id), {
|
|
819
|
+
method: 'PATCH',
|
|
820
|
+
headers: { 'Content-Type': 'application/json' },
|
|
821
|
+
body: JSON.stringify({ content }),
|
|
822
|
+
});
|
|
823
|
+
if (res.ok || res.status === 204) {
|
|
824
|
+
showNotification('L3 entry updated', 'success');
|
|
825
|
+
loadL3(ctx);
|
|
826
|
+
} else {
|
|
827
|
+
showNotification('Update failed: ' + res.statusText, 'error');
|
|
828
|
+
}
|
|
829
|
+
} catch (err) {
|
|
830
|
+
showNotification('Update failed: ' + err.message, 'error');
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function runPromotion(ctx) {
|
|
835
|
+
try {
|
|
836
|
+
const res = await fetch(API + '/api/promotion/run?context=' + encodeURIComponent(ctx), { method: 'POST' });
|
|
837
|
+
if (res.ok) {
|
|
838
|
+
const data = await res.json();
|
|
839
|
+
showNotification(`Promotion complete: ${data.promoted_count} episodes promoted`, 'success');
|
|
840
|
+
loadL3(ctx);
|
|
841
|
+
} else {
|
|
842
|
+
showNotification('Promotion failed: ' + res.statusText, 'error');
|
|
843
|
+
}
|
|
844
|
+
} catch (err) {
|
|
845
|
+
showNotification('Promotion failed: ' + err.message, 'error');
|
|
846
|
+
}
|
|
375
847
|
}
|
|
376
848
|
|
|
849
|
+
// ----- L2 Episodes -----
|
|
377
850
|
async function loadL2(ctx) {
|
|
378
851
|
const el = document.getElementById('mem-l2');
|
|
852
|
+
l2SearchMode = false;
|
|
853
|
+
l2Page = 0;
|
|
379
854
|
try {
|
|
380
|
-
const res = await fetch(
|
|
855
|
+
const res = await fetch(API + '/api/contexts/' + encodeURIComponent(ctx) + '/episodes');
|
|
856
|
+
if (!res.ok) { el.innerHTML = '<p style="color:var(--red)">Failed to load episodes</p>'; return; }
|
|
381
857
|
const data = await res.json();
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
858
|
+
l2AllEpisodes = Array.isArray(data) ? data : (data.episodes || []);
|
|
859
|
+
renderL2(ctx);
|
|
860
|
+
} catch (err) {
|
|
861
|
+
el.innerHTML = `<p style="color:var(--red)">Failed: ${esc(err.message)}</p>`;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function renderL2(ctx) {
|
|
866
|
+
const el = document.getElementById('mem-l2');
|
|
867
|
+
const start = l2Page * L2_PAGE_SIZE;
|
|
868
|
+
const pageEpisodes = l2AllEpisodes.slice(start, start + L2_PAGE_SIZE);
|
|
869
|
+
const totalPages = Math.ceil(l2AllEpisodes.length / L2_PAGE_SIZE);
|
|
870
|
+
|
|
871
|
+
let html = `
|
|
872
|
+
<div class="filters" style="margin-bottom:12px">
|
|
873
|
+
<input type="text" id="l2-search" placeholder="Semantic search..." style="flex:1" />
|
|
874
|
+
<button onclick="searchL2('${esc(ctx)}')">Search</button>
|
|
875
|
+
${l2SearchMode ? `<button onclick="loadL2('${esc(ctx)}')">Clear Search</button>` : ''}
|
|
876
|
+
</div>
|
|
877
|
+
`;
|
|
878
|
+
|
|
879
|
+
if (l2AllEpisodes.length === 0) {
|
|
880
|
+
html += '<p style="color:var(--text2)">No episodes</p>';
|
|
881
|
+
el.innerHTML = html;
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
html += pageEpisodes.map(ep => `
|
|
886
|
+
<div class="episode">
|
|
887
|
+
<div class="meta">
|
|
888
|
+
${esc(ep.role)} · ${new Date(ep.timestamp).toLocaleString()} · ${esc(ep.id?.slice(0,8) || '')}
|
|
889
|
+
${ep.score !== undefined ? ` · <span class="score">score: ${ep.score.toFixed(4)}</span>` : ''}
|
|
890
|
+
</div>
|
|
891
|
+
${esc(ep.content)}
|
|
892
|
+
</div>
|
|
893
|
+
`).join('');
|
|
894
|
+
|
|
895
|
+
if (totalPages > 1) {
|
|
896
|
+
html += `<div class="pagination">
|
|
897
|
+
<button ${l2Page === 0 ? 'disabled' : ''} onclick="l2Page--; renderL2('${esc(ctx)}')">Prev</button>
|
|
898
|
+
<span>Page ${l2Page + 1} of ${totalPages} (${l2AllEpisodes.length} total)</span>
|
|
899
|
+
<button ${l2Page >= totalPages - 1 ? 'disabled' : ''} onclick="l2Page++; renderL2('${esc(ctx)}')">Next</button>
|
|
900
|
+
</div>`;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
el.innerHTML = html;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async function searchL2(ctx) {
|
|
907
|
+
const query = document.getElementById('l2-search')?.value;
|
|
908
|
+
if (!query?.trim()) return;
|
|
909
|
+
const el = document.getElementById('mem-l2');
|
|
910
|
+
try {
|
|
911
|
+
const res = await fetch(API + '/api/search?q=' + encodeURIComponent(query) + '&context=' + encodeURIComponent(ctx) + '&limit=50');
|
|
912
|
+
if (!res.ok) { showNotification('Search failed', 'error'); return; }
|
|
913
|
+
const data = await res.json();
|
|
914
|
+
l2AllEpisodes = (data.episodes || []).map(e => ({...e.episode || e, score: e.score}));
|
|
915
|
+
l2SearchMode = true;
|
|
916
|
+
l2Page = 0;
|
|
917
|
+
renderL2(ctx);
|
|
918
|
+
} catch (err) {
|
|
919
|
+
showNotification('Search failed: ' + err.message, 'error');
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// ----- L1 History -----
|
|
924
|
+
async function loadL1(ctx) {
|
|
925
|
+
const el = document.getElementById('mem-l1');
|
|
926
|
+
try {
|
|
927
|
+
const res = await fetch(API + '/api/l1/' + encodeURIComponent(ctx));
|
|
928
|
+
if (!res.ok) { el.innerHTML = '<p style="color:var(--text2)">L1 data unavailable</p>'; return; }
|
|
929
|
+
const data = await res.json();
|
|
930
|
+
const messages = data.messages || [];
|
|
931
|
+
if (messages.length === 0) {
|
|
932
|
+
el.innerHTML = '<p style="color:var(--text2)">No L1 conversation history for this context</p>';
|
|
385
933
|
return;
|
|
386
934
|
}
|
|
387
|
-
el.innerHTML =
|
|
388
|
-
|
|
389
|
-
<div class="
|
|
390
|
-
|
|
935
|
+
el.innerHTML = `<p style="color:var(--text2);margin-bottom:12px">${messages.length} messages in working memory</p>` +
|
|
936
|
+
messages.map(m => `
|
|
937
|
+
<div class="l1-msg ${esc(m.role)}">
|
|
938
|
+
<div class="role">${esc(m.role)}</div>
|
|
939
|
+
${esc(m.content)}
|
|
940
|
+
</div>
|
|
941
|
+
`).join('');
|
|
942
|
+
} catch (err) {
|
|
943
|
+
el.innerHTML = `<p style="color:var(--text2)">L1 data unavailable</p>`;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// ===== Search View =====
|
|
948
|
+
async function loadSearchContexts() {
|
|
949
|
+
try {
|
|
950
|
+
const res = await fetch(API + '/api/contexts');
|
|
951
|
+
if (!res.ok) return;
|
|
952
|
+
const data = await res.json();
|
|
953
|
+
const sel = document.getElementById('search-scope');
|
|
954
|
+
sel.innerHTML = '<option value="all">All Contexts</option>';
|
|
955
|
+
for (const c of (data.contexts || [])) {
|
|
956
|
+
sel.innerHTML += `<option value="${esc(c.name)}">${esc(c.name)}</option>`;
|
|
957
|
+
}
|
|
958
|
+
} catch {}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
async function runSearch() {
|
|
962
|
+
const query = document.getElementById('search-query').value.trim();
|
|
963
|
+
if (!query) return;
|
|
964
|
+
const scope = document.getElementById('search-scope').value;
|
|
965
|
+
const limit = document.getElementById('search-limit').value || 20;
|
|
966
|
+
const el = document.getElementById('search-results');
|
|
967
|
+
el.innerHTML = '<p style="color:var(--text2)">Searching...</p>';
|
|
968
|
+
|
|
969
|
+
try {
|
|
970
|
+
let url, res;
|
|
971
|
+
if (scope === 'all') {
|
|
972
|
+
url = API + '/api/search/cross-context?q=' + encodeURIComponent(query) + '&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 results = data.results || [];
|
|
977
|
+
if (results.length === 0) {
|
|
978
|
+
el.innerHTML = '<p style="color:var(--text2)">No results found</p>';
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
let html = '';
|
|
982
|
+
for (const group of results) {
|
|
983
|
+
html += `<div class="search-group-header">${esc(group.context)} (${group.episodes.length})</div>`;
|
|
984
|
+
for (const ep of group.episodes) {
|
|
985
|
+
html += `<div class="search-result">
|
|
986
|
+
<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>
|
|
987
|
+
<div class="content">${esc(ep.episode?.content || ep.content)}</div>
|
|
988
|
+
</div>`;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
el.innerHTML = html;
|
|
992
|
+
} else {
|
|
993
|
+
url = API + '/api/search?q=' + encodeURIComponent(query) + '&context=' + encodeURIComponent(scope) + '&limit=' + limit;
|
|
994
|
+
res = await fetch(url);
|
|
995
|
+
if (!res.ok) { showNotification('Search failed', 'error'); el.innerHTML = ''; return; }
|
|
996
|
+
const data = await res.json();
|
|
997
|
+
const episodes = data.episodes || [];
|
|
998
|
+
if (episodes.length === 0) {
|
|
999
|
+
el.innerHTML = '<p style="color:var(--text2)">No results found</p>';
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
el.innerHTML = episodes.map(ep => `
|
|
1003
|
+
<div class="search-result">
|
|
1004
|
+
<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>
|
|
1005
|
+
<div class="content">${esc(ep.episode?.content || ep.content)}</div>
|
|
1006
|
+
</div>
|
|
1007
|
+
`).join('');
|
|
1008
|
+
}
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
showNotification('Search failed: ' + err.message, 'error');
|
|
1011
|
+
el.innerHTML = '';
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// ===== Promotion & Scoring View =====
|
|
1016
|
+
async function loadPromotion() {
|
|
1017
|
+
loadThresholds();
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async function loadThresholds() {
|
|
1021
|
+
const el = document.getElementById('promo-thresholds');
|
|
1022
|
+
let stats = null;
|
|
1023
|
+
let statsUnavailable = false;
|
|
1024
|
+
try {
|
|
1025
|
+
const res = await fetch(API + '/api/stats');
|
|
1026
|
+
if (res.ok) stats = await res.json();
|
|
1027
|
+
else statsUnavailable = true;
|
|
1028
|
+
} catch { statsUnavailable = true; }
|
|
1029
|
+
|
|
1030
|
+
const t = (stats && stats.promotion_thresholds) || { access: 5, cooccurrence: 3 };
|
|
1031
|
+
const warning = statsUnavailable ? '<p style="color:var(--yellow);font-size:12px;margin-bottom:12px;padding:8px;border:1px solid var(--yellow);border-radius:4px">/stats endpoint unavailable — showing default thresholds (access: 5, cooccurrence: 3). Rebuild daemon to enable.</p>' : '';
|
|
1032
|
+
|
|
1033
|
+
el.innerHTML = `
|
|
1034
|
+
${warning}
|
|
1035
|
+
<div class="status-grid" style="margin-bottom:24px">
|
|
1036
|
+
<div class="status-card">
|
|
1037
|
+
<div class="label">Access Threshold</div>
|
|
1038
|
+
<div class="value">${t.access}</div>
|
|
1039
|
+
<div class="sub">Episode must be accessed at least ${t.access} times</div>
|
|
391
1040
|
</div>
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
1041
|
+
<div class="status-card">
|
|
1042
|
+
<div class="label">Co-occurrence Threshold</div>
|
|
1043
|
+
<div class="value">${t.cooccurrence}</div>
|
|
1044
|
+
<div class="sub">Episode must co-occur with others at least ${t.cooccurrence} times</div>
|
|
1045
|
+
</div>
|
|
1046
|
+
</div>
|
|
1047
|
+
<h3 style="margin-bottom:12px">How Promotion Works</h3>
|
|
1048
|
+
<div style="color:var(--text2);font-size:13px;line-height:1.8">
|
|
1049
|
+
<p><strong>L2 → L3 promotion</strong> happens when an episode meets <em>both</em> thresholds:</p>
|
|
1050
|
+
<ol style="margin:8px 0 8px 20px">
|
|
1051
|
+
<li>The episode has been accessed (retrieved in search results) at least <strong>${t.access} times</strong></li>
|
|
1052
|
+
<li>The episode has co-occurred with other episodes at least <strong>${t.cooccurrence} times total</strong> (connection density)</li>
|
|
1053
|
+
</ol>
|
|
1054
|
+
<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>
|
|
1055
|
+
</div>
|
|
1056
|
+
`;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
async function loadCandidates() {
|
|
1060
|
+
const el = document.getElementById('promo-candidates');
|
|
1061
|
+
el.innerHTML = '<p style="color:var(--text2)">Loading candidates...</p>';
|
|
1062
|
+
try {
|
|
1063
|
+
const res = await fetch(API + '/api/access/top?limit=50');
|
|
1064
|
+
if (!res.ok) {
|
|
1065
|
+
if (res.status === 404) {
|
|
1066
|
+
el.innerHTML = '<p style="color:var(--yellow);padding:12px;border:1px solid var(--yellow);border-radius:4px">/access/top endpoint not found (404). The running daemon binary may be outdated. Rebuild with: <code style="background:var(--bg);padding:2px 6px;border-radius:3px">cd packages/memory && cargo build --release</code> and restart the daemon.</p>';
|
|
1067
|
+
} else {
|
|
1068
|
+
el.innerHTML = `<p style="color:var(--red)">Failed to load candidates (HTTP ${res.status})</p>`;
|
|
1069
|
+
}
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
const data = await res.json();
|
|
1073
|
+
const records = data.records || [];
|
|
1074
|
+
|
|
1075
|
+
if (records.length === 0) {
|
|
1076
|
+
el.innerHTML = '<p style="color:var(--text2)">No access records found. Episodes need to be searched/retrieved to generate access counts.</p>';
|
|
1077
|
+
return;
|
|
395
1078
|
}
|
|
1079
|
+
|
|
1080
|
+
el.innerHTML = records.map(r => {
|
|
1081
|
+
const accessPct = Math.min((r.access_count / 5) * 100, 100);
|
|
1082
|
+
const densityPct = Math.min((r.connection_density / 3) * 100, 100);
|
|
1083
|
+
const promoted = r.is_promoted;
|
|
1084
|
+
return `
|
|
1085
|
+
<div class="ctx-card">
|
|
1086
|
+
<div style="display:flex;justify-content:space-between;align-items:start">
|
|
1087
|
+
<div style="flex:1">
|
|
1088
|
+
<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>
|
|
1089
|
+
<div style="font-size:13px;margin-top:4px">${esc(r.content_preview || '(no content)')}</div>
|
|
1090
|
+
</div>
|
|
1091
|
+
</div>
|
|
1092
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:10px">
|
|
1093
|
+
<div>
|
|
1094
|
+
<div style="font-size:11px;color:var(--text2);margin-bottom:2px">Access: ${r.access_count}/5</div>
|
|
1095
|
+
<div class="progress-bar"><div class="progress-fill ${accessPct >= 100 ? 'green' : 'yellow'}" style="width:${accessPct}%">${r.access_count}/5</div></div>
|
|
1096
|
+
</div>
|
|
1097
|
+
<div>
|
|
1098
|
+
<div style="font-size:11px;color:var(--text2);margin-bottom:2px">Density: ${r.connection_density}/3</div>
|
|
1099
|
+
<div class="progress-bar"><div class="progress-fill ${densityPct >= 100 ? 'green' : 'purple'}" style="width:${densityPct}%">${r.connection_density}/3</div></div>
|
|
1100
|
+
</div>
|
|
1101
|
+
</div>
|
|
1102
|
+
</div>
|
|
1103
|
+
`;
|
|
1104
|
+
}).join('');
|
|
396
1105
|
} catch (err) {
|
|
397
1106
|
el.innerHTML = `<p style="color:var(--red)">Failed: ${esc(err.message)}</p>`;
|
|
398
1107
|
}
|
|
399
1108
|
}
|
|
400
1109
|
|
|
1110
|
+
async function loadScoringTab() {
|
|
1111
|
+
const el = document.getElementById('promo-scoring');
|
|
1112
|
+
try {
|
|
1113
|
+
const ctxRes = await fetch(API + '/api/contexts');
|
|
1114
|
+
if (!ctxRes.ok) { el.innerHTML = '<p style="color:var(--red)">Failed to load contexts</p>'; return; }
|
|
1115
|
+
const ctxData = await ctxRes.json();
|
|
1116
|
+
const contexts = ctxData.contexts || [];
|
|
1117
|
+
|
|
1118
|
+
// Fetch scoring for each context
|
|
1119
|
+
const scoringData = {};
|
|
1120
|
+
let defaultHL = 48;
|
|
1121
|
+
let scoringAvailable = true;
|
|
1122
|
+
for (const c of contexts) {
|
|
1123
|
+
try {
|
|
1124
|
+
const sRes = await fetch(API + '/api/contexts/' + encodeURIComponent(c.name) + '/scoring');
|
|
1125
|
+
if (sRes.ok) {
|
|
1126
|
+
const sd = await sRes.json();
|
|
1127
|
+
scoringData[c.name] = sd;
|
|
1128
|
+
defaultHL = sd.default_half_life_hours || 48;
|
|
1129
|
+
} else if (sRes.status === 404) {
|
|
1130
|
+
scoringAvailable = false;
|
|
1131
|
+
}
|
|
1132
|
+
} catch {}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
let html = '';
|
|
1136
|
+
if (!scoringAvailable) {
|
|
1137
|
+
html += '<p style="color:var(--yellow);font-size:12px;margin-bottom:12px;padding:8px;border:1px solid var(--yellow);border-radius:4px">/scoring endpoint not found — the running daemon may be outdated. Showing default values. Rebuild daemon to enable per-context scoring configuration.</p>';
|
|
1138
|
+
}
|
|
1139
|
+
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>`;
|
|
1140
|
+
|
|
1141
|
+
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>';
|
|
1142
|
+
for (const c of contexts) {
|
|
1143
|
+
const sd = scoringData[c.name] || {};
|
|
1144
|
+
const hl = sd.half_life_hours || defaultHL;
|
|
1145
|
+
const isCustom = sd.is_default === false;
|
|
1146
|
+
const sourceLabel = !scoringAvailable ? '<span style="color:var(--text2)">Default (endpoint unavailable)</span>' : (isCustom ? '<span style="color:var(--yellow)">Yes</span>' : 'Default');
|
|
1147
|
+
html += `<tr>
|
|
1148
|
+
<td style="color:var(--accent)">${esc(c.name)}</td>
|
|
1149
|
+
<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)}" ${!scoringAvailable ? 'disabled' : ''} /></td>
|
|
1150
|
+
<td>${sourceLabel}</td>
|
|
1151
|
+
<td>${renderDecayCurve(hl)}</td>
|
|
1152
|
+
<td><button class="btn-sm" onclick="saveScoring('${esc(c.name)}')" ${!scoringAvailable ? 'disabled' : ''}>Save</button></td>
|
|
1153
|
+
</tr>`;
|
|
1154
|
+
}
|
|
1155
|
+
html += '</tbody></table>';
|
|
1156
|
+
el.innerHTML = html;
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
el.innerHTML = `<p style="color:var(--red)">Failed: ${esc(err.message)}</p>`;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
async function saveScoring(ctx) {
|
|
1163
|
+
const input = document.getElementById('hl-' + ctx);
|
|
1164
|
+
const hours = parseFloat(input.value);
|
|
1165
|
+
if (!hours || hours <= 0) { showNotification('Invalid half-life value', 'warning'); return; }
|
|
1166
|
+
try {
|
|
1167
|
+
const res = await fetch(API + '/api/contexts/' + encodeURIComponent(ctx) + '/scoring', {
|
|
1168
|
+
method: 'POST',
|
|
1169
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1170
|
+
body: JSON.stringify({ half_life_hours: hours }),
|
|
1171
|
+
});
|
|
1172
|
+
if (res.ok || res.status === 204) {
|
|
1173
|
+
showNotification(`Half-life for "${ctx}" set to ${hours}h`, 'success');
|
|
1174
|
+
} else {
|
|
1175
|
+
showNotification('Failed to save scoring', 'error');
|
|
1176
|
+
}
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
showNotification('Failed: ' + err.message, 'error');
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// ===== Utility =====
|
|
401
1183
|
function esc(s) {
|
|
402
1184
|
if (!s) return '';
|
|
403
1185
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
404
1186
|
}
|
|
405
1187
|
|
|
406
|
-
// Auto-load
|
|
407
|
-
|
|
1188
|
+
// ===== Auto-load =====
|
|
1189
|
+
loadHealthView();
|
|
408
1190
|
</script>
|
|
409
1191
|
</body>
|
|
410
1192
|
</html>
|