mesh-context-layer 0.4.0__py3-none-any.whl
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.
- mesh/__init__.py +10 -0
- mesh/audit.py +189 -0
- mesh/cli.py +321 -0
- mesh/context_builder.py +96 -0
- mesh/dashboard.py +1869 -0
- mesh/digest.py +194 -0
- mesh/http_server.py +226 -0
- mesh/io.py +215 -0
- mesh/mcp_server.py +387 -0
- mesh/memory.py +343 -0
- mesh/namespaces.py +218 -0
- mesh/passive.py +690 -0
- mesh/store.py +393 -0
- mesh_context_layer-0.4.0.dist-info/METADATA +215 -0
- mesh_context_layer-0.4.0.dist-info/RECORD +18 -0
- mesh_context_layer-0.4.0.dist-info/WHEEL +4 -0
- mesh_context_layer-0.4.0.dist-info/entry_points.txt +14 -0
- mesh_context_layer-0.4.0.dist-info/licenses/LICENSE +21 -0
mesh/dashboard.py
ADDED
|
@@ -0,0 +1,1869 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mesh Dashboard — v0.4
|
|
3
|
+
Redesigned: full memory management (add/edit/delete), dark UI, audit, digest, patterns.
|
|
4
|
+
|
|
5
|
+
Start: mesh-dashboard
|
|
6
|
+
mesh-dashboard --namespace my-project --port 7433
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import webbrowser
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from typing import Optional, List
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
import uvicorn
|
|
17
|
+
from fastapi import FastAPI, HTTPException
|
|
18
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
|
|
21
|
+
from .memory import Mesh
|
|
22
|
+
from .namespaces import list_namespaces, delete_namespace, namespace_stats
|
|
23
|
+
from .io import export_namespace
|
|
24
|
+
|
|
25
|
+
api = FastAPI(title="Mesh Dashboard", version="0.4.0")
|
|
26
|
+
|
|
27
|
+
_mesh: Optional[Mesh] = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_mesh() -> Mesh:
|
|
31
|
+
if _mesh is None:
|
|
32
|
+
raise RuntimeError("Mesh not initialised")
|
|
33
|
+
return _mesh
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── Request models ──────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
class CreateMemoryRequest(BaseModel):
|
|
39
|
+
content: str
|
|
40
|
+
memory_type: str = "fact"
|
|
41
|
+
confidence: float = 1.0
|
|
42
|
+
tags: List[str] = []
|
|
43
|
+
local_only: bool = False
|
|
44
|
+
ttl_days: Optional[float] = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class UpdateMemoryRequest(BaseModel):
|
|
48
|
+
content: str
|
|
49
|
+
memory_type: str = "fact"
|
|
50
|
+
confidence: float = 1.0
|
|
51
|
+
tags: List[str] = []
|
|
52
|
+
local_only: bool = False
|
|
53
|
+
ttl_days: Optional[float] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ── REST API endpoints ──────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
@api.get("/api/stats")
|
|
59
|
+
def get_stats():
|
|
60
|
+
mesh = get_mesh()
|
|
61
|
+
patterns = mesh.patterns()
|
|
62
|
+
contradictions = mesh.contradictions()
|
|
63
|
+
return {
|
|
64
|
+
"namespace": mesh.namespace,
|
|
65
|
+
"total_memories": mesh.count(),
|
|
66
|
+
"open_contradictions": len(contradictions),
|
|
67
|
+
"memory_gaps": len(patterns["memory_gaps"]),
|
|
68
|
+
"stale_memories": len(patterns["stale_memories"]),
|
|
69
|
+
"unused_memories": len(patterns["unused_memories"]),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@api.get("/api/memories")
|
|
74
|
+
def get_memories(limit: int = 200):
|
|
75
|
+
return get_mesh().inspect(limit=limit)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@api.post("/api/memories")
|
|
79
|
+
def create_memory(req: CreateMemoryRequest):
|
|
80
|
+
"""Add a new memory from the dashboard."""
|
|
81
|
+
try:
|
|
82
|
+
mesh = get_mesh()
|
|
83
|
+
result = mesh.learn(
|
|
84
|
+
content=req.content,
|
|
85
|
+
memory_type=req.memory_type,
|
|
86
|
+
confidence=req.confidence,
|
|
87
|
+
tags=req.tags,
|
|
88
|
+
local_only=req.local_only,
|
|
89
|
+
ttl_days=req.ttl_days if req.ttl_days and req.ttl_days > 0 else None,
|
|
90
|
+
)
|
|
91
|
+
return {"status": "ok", "result": result}
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@api.put("/api/memories/{memory_id}")
|
|
97
|
+
def update_memory(memory_id: str, req: UpdateMemoryRequest):
|
|
98
|
+
"""Update a memory: deletes old embedding and re-learns with new content."""
|
|
99
|
+
try:
|
|
100
|
+
mesh = get_mesh()
|
|
101
|
+
mesh.forget(memory_id)
|
|
102
|
+
result = mesh.learn(
|
|
103
|
+
content=req.content,
|
|
104
|
+
memory_type=req.memory_type,
|
|
105
|
+
confidence=req.confidence,
|
|
106
|
+
tags=req.tags,
|
|
107
|
+
local_only=req.local_only,
|
|
108
|
+
ttl_days=req.ttl_days if req.ttl_days and req.ttl_days > 0 else None,
|
|
109
|
+
)
|
|
110
|
+
return {"status": "ok", "result": result}
|
|
111
|
+
except Exception as e:
|
|
112
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@api.delete("/api/memories/{memory_id}")
|
|
116
|
+
def delete_memory(memory_id: str):
|
|
117
|
+
get_mesh().forget(memory_id)
|
|
118
|
+
return {"status": "deleted", "memory_id": memory_id}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@api.get("/api/patterns")
|
|
122
|
+
def get_patterns():
|
|
123
|
+
return get_mesh().patterns()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@api.get("/api/contradictions")
|
|
127
|
+
def get_contradictions():
|
|
128
|
+
return get_mesh().contradictions()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@api.post("/api/contradictions/{contradiction_id}/resolve")
|
|
132
|
+
def resolve_contradiction(contradiction_id: str):
|
|
133
|
+
get_mesh().resolve_contradiction(contradiction_id)
|
|
134
|
+
return {"status": "resolved"}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@api.get("/api/search")
|
|
138
|
+
def search(q: str, n: int = 20):
|
|
139
|
+
if not q.strip():
|
|
140
|
+
raise HTTPException(status_code=400, detail="Query cannot be empty")
|
|
141
|
+
return get_mesh().recall(q, n=n)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@api.get("/api/namespaces")
|
|
145
|
+
def get_namespaces():
|
|
146
|
+
return list_namespaces()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@api.get("/api/namespaces/{namespace}/stats")
|
|
150
|
+
def get_namespace_stats(namespace: str):
|
|
151
|
+
try:
|
|
152
|
+
return namespace_stats(namespace)
|
|
153
|
+
except ValueError as e:
|
|
154
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@api.delete("/api/namespaces/{namespace}")
|
|
158
|
+
def api_delete_namespace(namespace: str):
|
|
159
|
+
count = delete_namespace(namespace)
|
|
160
|
+
return {"deleted": True, "memories_removed": count}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@api.get("/api/export/{namespace}")
|
|
164
|
+
def api_export_namespace(namespace: str, include_embeddings: bool = False):
|
|
165
|
+
return export_namespace(namespace=namespace, include_embeddings=include_embeddings)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@api.get("/api/audit")
|
|
169
|
+
async def get_audit(namespace: str = "shared", limit: int = 100, action: str = "all"):
|
|
170
|
+
from .audit import get_audit_log
|
|
171
|
+
return get_audit_log(namespace=namespace, limit=limit, action_filter=action)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@api.get("/api/digest")
|
|
175
|
+
async def get_digest(namespace: str = "shared", hours: int = 24):
|
|
176
|
+
from .digest import generate_digest
|
|
177
|
+
return generate_digest(namespace=namespace, hours=hours)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ── Dashboard HTML ──────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
DASHBOARD_HTML = """<!DOCTYPE html>
|
|
183
|
+
<html lang="en">
|
|
184
|
+
<head>
|
|
185
|
+
<meta charset="UTF-8">
|
|
186
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
187
|
+
<title>Mesh Dashboard</title>
|
|
188
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
189
|
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
190
|
+
<style>
|
|
191
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
192
|
+
|
|
193
|
+
:root {
|
|
194
|
+
--bg: #070b14;
|
|
195
|
+
--bg-2: #0c1220;
|
|
196
|
+
--bg-3: #111827;
|
|
197
|
+
--bg-4: #1a2236;
|
|
198
|
+
--border: rgba(148,163,184,0.07);
|
|
199
|
+
--border-2: rgba(148,163,184,0.13);
|
|
200
|
+
--border-3: rgba(148,163,184,0.22);
|
|
201
|
+
--text: #e2e8f4;
|
|
202
|
+
--text-2: #94a3b8;
|
|
203
|
+
--text-3: #475569;
|
|
204
|
+
--mint: #10d9a4;
|
|
205
|
+
--mint-dim: rgba(16,217,164,0.12);
|
|
206
|
+
--mint-glow: rgba(16,217,164,0.25);
|
|
207
|
+
--purple: #a78bfa;
|
|
208
|
+
--purple-dim:rgba(167,139,250,0.12);
|
|
209
|
+
--amber: #fbbf24;
|
|
210
|
+
--amber-dim: rgba(251,191,36,0.12);
|
|
211
|
+
--red: #f87171;
|
|
212
|
+
--red-dim: rgba(248,113,113,0.12);
|
|
213
|
+
--blue: #60a5fa;
|
|
214
|
+
--blue-dim: rgba(96,165,250,0.12);
|
|
215
|
+
--green: #34d399;
|
|
216
|
+
--green-dim: rgba(52,211,153,0.12);
|
|
217
|
+
|
|
218
|
+
--type-fact: #60a5fa;
|
|
219
|
+
--type-preference: #a78bfa;
|
|
220
|
+
--type-context: #10d9a4;
|
|
221
|
+
--type-result: #34d399;
|
|
222
|
+
--type-instruction: #fbbf24;
|
|
223
|
+
|
|
224
|
+
--radius: 8px;
|
|
225
|
+
--radius-lg: 12px;
|
|
226
|
+
--radius-xl: 16px;
|
|
227
|
+
--font: 'Outfit', sans-serif;
|
|
228
|
+
--mono: 'JetBrains Mono', monospace;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
html, body { height: 100%; }
|
|
232
|
+
|
|
233
|
+
body {
|
|
234
|
+
font-family: var(--font);
|
|
235
|
+
background: var(--bg);
|
|
236
|
+
color: var(--text);
|
|
237
|
+
font-size: 14px;
|
|
238
|
+
line-height: 1.5;
|
|
239
|
+
overflow-x: hidden;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* ── Scrollbar ── */
|
|
243
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
244
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
245
|
+
::-webkit-scrollbar-thumb { background: var(--bg-4); border-radius: 3px; }
|
|
246
|
+
::-webkit-scrollbar-thumb:hover { background: var(--border-3); }
|
|
247
|
+
|
|
248
|
+
/* ── Layout ── */
|
|
249
|
+
.layout { display: flex; flex-direction: column; height: 100vh; }
|
|
250
|
+
|
|
251
|
+
/* ── Navbar ── */
|
|
252
|
+
nav {
|
|
253
|
+
display: flex;
|
|
254
|
+
align-items: center;
|
|
255
|
+
gap: 16px;
|
|
256
|
+
padding: 0 24px;
|
|
257
|
+
height: 58px;
|
|
258
|
+
background: var(--bg-2);
|
|
259
|
+
border-bottom: 1px solid var(--border);
|
|
260
|
+
flex-shrink: 0;
|
|
261
|
+
position: relative;
|
|
262
|
+
z-index: 100;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.nav-logo {
|
|
266
|
+
display: flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
gap: 10px;
|
|
269
|
+
font-size: 17px;
|
|
270
|
+
font-weight: 700;
|
|
271
|
+
letter-spacing: -0.02em;
|
|
272
|
+
color: var(--text);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.nav-logo-icon {
|
|
276
|
+
width: 28px; height: 28px;
|
|
277
|
+
background: linear-gradient(135deg, var(--mint), #0ea5e9);
|
|
278
|
+
border-radius: 7px;
|
|
279
|
+
display: flex; align-items: center; justify-content: center;
|
|
280
|
+
font-size: 14px;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.nav-ns {
|
|
284
|
+
display: flex; align-items: center; gap: 6px;
|
|
285
|
+
padding: 4px 10px 4px 8px;
|
|
286
|
+
background: var(--bg-3);
|
|
287
|
+
border: 1px solid var(--border-2);
|
|
288
|
+
border-radius: 20px;
|
|
289
|
+
font-size: 12px;
|
|
290
|
+
color: var(--text-2);
|
|
291
|
+
}
|
|
292
|
+
.nav-ns-dot {
|
|
293
|
+
width: 6px; height: 6px;
|
|
294
|
+
border-radius: 50%;
|
|
295
|
+
background: var(--mint);
|
|
296
|
+
box-shadow: 0 0 6px var(--mint-glow);
|
|
297
|
+
animation: pulse 2s ease-in-out infinite;
|
|
298
|
+
}
|
|
299
|
+
@keyframes pulse {
|
|
300
|
+
0%, 100% { opacity: 1; }
|
|
301
|
+
50% { opacity: 0.5; }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.nav-search {
|
|
305
|
+
flex: 1;
|
|
306
|
+
max-width: 380px;
|
|
307
|
+
margin-left: 8px;
|
|
308
|
+
position: relative;
|
|
309
|
+
}
|
|
310
|
+
.nav-search input {
|
|
311
|
+
width: 100%;
|
|
312
|
+
padding: 7px 12px 7px 34px;
|
|
313
|
+
background: var(--bg-3);
|
|
314
|
+
border: 1px solid var(--border-2);
|
|
315
|
+
border-radius: var(--radius);
|
|
316
|
+
color: var(--text);
|
|
317
|
+
font-family: var(--font);
|
|
318
|
+
font-size: 13px;
|
|
319
|
+
outline: none;
|
|
320
|
+
transition: border-color 0.15s;
|
|
321
|
+
}
|
|
322
|
+
.nav-search input:focus { border-color: var(--mint); }
|
|
323
|
+
.nav-search input::placeholder { color: var(--text-3); }
|
|
324
|
+
.nav-search-icon {
|
|
325
|
+
position: absolute;
|
|
326
|
+
left: 10px; top: 50%;
|
|
327
|
+
transform: translateY(-50%);
|
|
328
|
+
color: var(--text-3);
|
|
329
|
+
pointer-events: none;
|
|
330
|
+
font-size: 13px;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.nav-right { margin-left: auto; display: flex; gap: 8px; align-items: center; }
|
|
334
|
+
|
|
335
|
+
.btn-add {
|
|
336
|
+
display: flex; align-items: center; gap: 6px;
|
|
337
|
+
padding: 7px 16px;
|
|
338
|
+
background: var(--mint);
|
|
339
|
+
color: #030a06;
|
|
340
|
+
font-family: var(--font);
|
|
341
|
+
font-size: 13px;
|
|
342
|
+
font-weight: 600;
|
|
343
|
+
border: none;
|
|
344
|
+
border-radius: var(--radius);
|
|
345
|
+
cursor: pointer;
|
|
346
|
+
transition: all 0.15s;
|
|
347
|
+
letter-spacing: -0.01em;
|
|
348
|
+
}
|
|
349
|
+
.btn-add:hover { background: #0ec898; transform: translateY(-1px); box-shadow: 0 4px 12px var(--mint-glow); }
|
|
350
|
+
|
|
351
|
+
.btn-icon {
|
|
352
|
+
padding: 7px;
|
|
353
|
+
background: var(--bg-3);
|
|
354
|
+
border: 1px solid var(--border-2);
|
|
355
|
+
border-radius: var(--radius);
|
|
356
|
+
cursor: pointer;
|
|
357
|
+
color: var(--text-2);
|
|
358
|
+
font-size: 14px;
|
|
359
|
+
transition: all 0.15s;
|
|
360
|
+
display: flex; align-items: center;
|
|
361
|
+
}
|
|
362
|
+
.btn-icon:hover { background: var(--bg-4); color: var(--text); border-color: var(--border-3); }
|
|
363
|
+
|
|
364
|
+
/* ── Stats Bar ── */
|
|
365
|
+
.stats-bar {
|
|
366
|
+
display: flex;
|
|
367
|
+
gap: 1px;
|
|
368
|
+
padding: 0 24px;
|
|
369
|
+
background: var(--bg-2);
|
|
370
|
+
border-bottom: 1px solid var(--border);
|
|
371
|
+
flex-shrink: 0;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.stat-item {
|
|
375
|
+
display: flex;
|
|
376
|
+
align-items: center;
|
|
377
|
+
gap: 10px;
|
|
378
|
+
padding: 10px 20px;
|
|
379
|
+
flex: 1;
|
|
380
|
+
position: relative;
|
|
381
|
+
}
|
|
382
|
+
.stat-item + .stat-item::before {
|
|
383
|
+
content: '';
|
|
384
|
+
position: absolute;
|
|
385
|
+
left: 0; top: 20%; height: 60%;
|
|
386
|
+
width: 1px;
|
|
387
|
+
background: var(--border);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.stat-num {
|
|
391
|
+
font-size: 24px;
|
|
392
|
+
font-weight: 700;
|
|
393
|
+
letter-spacing: -0.03em;
|
|
394
|
+
line-height: 1;
|
|
395
|
+
}
|
|
396
|
+
.stat-num.mint { color: var(--mint); }
|
|
397
|
+
.stat-num.amber { color: var(--amber); }
|
|
398
|
+
.stat-num.red { color: var(--red); }
|
|
399
|
+
.stat-num.purple{ color: var(--purple); }
|
|
400
|
+
.stat-num.blue { color: var(--blue); }
|
|
401
|
+
.stat-num.green { color: var(--green); }
|
|
402
|
+
|
|
403
|
+
.stat-label { font-size: 11px; color: var(--text-3); font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
404
|
+
|
|
405
|
+
/* ── Tabs ── */
|
|
406
|
+
.tabs-bar {
|
|
407
|
+
display: flex;
|
|
408
|
+
gap: 2px;
|
|
409
|
+
padding: 10px 24px 0;
|
|
410
|
+
background: var(--bg-2);
|
|
411
|
+
border-bottom: 1px solid var(--border);
|
|
412
|
+
flex-shrink: 0;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.tab {
|
|
416
|
+
padding: 8px 16px;
|
|
417
|
+
font-size: 13px;
|
|
418
|
+
font-family: var(--font);
|
|
419
|
+
font-weight: 500;
|
|
420
|
+
color: var(--text-3);
|
|
421
|
+
border: none;
|
|
422
|
+
background: transparent;
|
|
423
|
+
cursor: pointer;
|
|
424
|
+
border-bottom: 2px solid transparent;
|
|
425
|
+
margin-bottom: -1px;
|
|
426
|
+
transition: all 0.15s;
|
|
427
|
+
display: flex; align-items: center; gap: 6px;
|
|
428
|
+
border-radius: var(--radius) var(--radius) 0 0;
|
|
429
|
+
}
|
|
430
|
+
.tab:hover { color: var(--text-2); background: var(--bg-3); }
|
|
431
|
+
.tab.active { color: var(--mint); border-bottom-color: var(--mint); }
|
|
432
|
+
.tab-badge {
|
|
433
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
434
|
+
min-width: 18px; height: 18px; padding: 0 5px;
|
|
435
|
+
background: var(--bg-4);
|
|
436
|
+
border-radius: 9px;
|
|
437
|
+
font-size: 10px;
|
|
438
|
+
font-weight: 600;
|
|
439
|
+
color: var(--text-2);
|
|
440
|
+
}
|
|
441
|
+
.tab.active .tab-badge { background: var(--mint-dim); color: var(--mint); }
|
|
442
|
+
|
|
443
|
+
/* ── Content Area ── */
|
|
444
|
+
.content { flex: 1; overflow-y: auto; padding: 20px 24px; }
|
|
445
|
+
|
|
446
|
+
.panel { display: none; }
|
|
447
|
+
.panel.active { display: block; }
|
|
448
|
+
|
|
449
|
+
/* ── Memory Grid / List ── */
|
|
450
|
+
.memories-toolbar {
|
|
451
|
+
display: flex;
|
|
452
|
+
gap: 10px;
|
|
453
|
+
margin-bottom: 16px;
|
|
454
|
+
align-items: center;
|
|
455
|
+
flex-wrap: wrap;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.filter-group { display: flex; gap: 4px; }
|
|
459
|
+
.filter-btn {
|
|
460
|
+
padding: 5px 12px;
|
|
461
|
+
font-size: 12px;
|
|
462
|
+
font-family: var(--font);
|
|
463
|
+
font-weight: 500;
|
|
464
|
+
border: 1px solid var(--border-2);
|
|
465
|
+
border-radius: 20px;
|
|
466
|
+
background: transparent;
|
|
467
|
+
color: var(--text-3);
|
|
468
|
+
cursor: pointer;
|
|
469
|
+
transition: all 0.12s;
|
|
470
|
+
}
|
|
471
|
+
.filter-btn:hover { color: var(--text-2); border-color: var(--border-3); background: var(--bg-3); }
|
|
472
|
+
.filter-btn.active { color: var(--mint); border-color: var(--mint); background: var(--mint-dim); }
|
|
473
|
+
|
|
474
|
+
.sort-select {
|
|
475
|
+
padding: 5px 10px;
|
|
476
|
+
font-size: 12px;
|
|
477
|
+
font-family: var(--font);
|
|
478
|
+
background: var(--bg-3);
|
|
479
|
+
border: 1px solid var(--border-2);
|
|
480
|
+
border-radius: var(--radius);
|
|
481
|
+
color: var(--text-2);
|
|
482
|
+
outline: none;
|
|
483
|
+
cursor: pointer;
|
|
484
|
+
margin-left: auto;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.memory-count {
|
|
488
|
+
font-size: 12px;
|
|
489
|
+
color: var(--text-3);
|
|
490
|
+
margin-left: 4px;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* ── Memory Card ── */
|
|
494
|
+
.memory-list { display: flex; flex-direction: column; gap: 8px; }
|
|
495
|
+
|
|
496
|
+
.memory-card {
|
|
497
|
+
display: flex;
|
|
498
|
+
background: var(--bg-2);
|
|
499
|
+
border: 1px solid var(--border);
|
|
500
|
+
border-radius: var(--radius-lg);
|
|
501
|
+
overflow: hidden;
|
|
502
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
503
|
+
position: relative;
|
|
504
|
+
}
|
|
505
|
+
.memory-card:hover { border-color: var(--border-2); box-shadow: 0 4px 20px rgba(0,0,0,0.3); }
|
|
506
|
+
.memory-card.stale { border-color: rgba(251,191,36,0.25); }
|
|
507
|
+
.memory-card.stale:hover { border-color: var(--amber); }
|
|
508
|
+
|
|
509
|
+
.memory-stripe {
|
|
510
|
+
width: 3px;
|
|
511
|
+
flex-shrink: 0;
|
|
512
|
+
background: var(--type-fact);
|
|
513
|
+
}
|
|
514
|
+
.memory-stripe.fact { background: var(--type-fact); }
|
|
515
|
+
.memory-stripe.preference { background: var(--type-preference); }
|
|
516
|
+
.memory-stripe.context { background: var(--type-context); }
|
|
517
|
+
.memory-stripe.result { background: var(--type-result); }
|
|
518
|
+
.memory-stripe.instruction { background: var(--type-instruction); }
|
|
519
|
+
|
|
520
|
+
.memory-body {
|
|
521
|
+
flex: 1;
|
|
522
|
+
padding: 13px 14px;
|
|
523
|
+
min-width: 0;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.memory-content {
|
|
527
|
+
font-family: var(--mono);
|
|
528
|
+
font-size: 12.5px;
|
|
529
|
+
line-height: 1.6;
|
|
530
|
+
color: var(--text);
|
|
531
|
+
margin-bottom: 10px;
|
|
532
|
+
word-break: break-word;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.memory-meta {
|
|
536
|
+
display: flex;
|
|
537
|
+
align-items: center;
|
|
538
|
+
gap: 6px;
|
|
539
|
+
flex-wrap: wrap;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.badge {
|
|
543
|
+
display: inline-flex; align-items: center; gap: 3px;
|
|
544
|
+
padding: 2px 8px;
|
|
545
|
+
border-radius: 10px;
|
|
546
|
+
font-size: 10.5px;
|
|
547
|
+
font-weight: 600;
|
|
548
|
+
letter-spacing: 0.02em;
|
|
549
|
+
text-transform: uppercase;
|
|
550
|
+
}
|
|
551
|
+
.badge-fact { background: var(--blue-dim); color: var(--blue); }
|
|
552
|
+
.badge-preference { background: var(--purple-dim); color: var(--purple); }
|
|
553
|
+
.badge-context { background: var(--mint-dim); color: var(--mint); }
|
|
554
|
+
.badge-result { background: var(--green-dim); color: var(--green); }
|
|
555
|
+
.badge-instruction { background: var(--amber-dim); color: var(--amber); }
|
|
556
|
+
.badge-stale { background: rgba(251,191,36,0.18); color: var(--amber); border: 1px solid rgba(251,191,36,0.3); }
|
|
557
|
+
.badge-private { background: var(--red-dim); color: var(--red); }
|
|
558
|
+
|
|
559
|
+
.meta-sep { width: 3px; height: 3px; border-radius: 50%; background: var(--text-3); flex-shrink: 0; }
|
|
560
|
+
|
|
561
|
+
.meta-text { font-size: 11px; color: var(--text-3); }
|
|
562
|
+
|
|
563
|
+
.conf-bar-wrap {
|
|
564
|
+
display: flex; align-items: center; gap: 6px;
|
|
565
|
+
font-size: 11px; color: var(--text-3);
|
|
566
|
+
}
|
|
567
|
+
.conf-bar {
|
|
568
|
+
width: 48px; height: 3px;
|
|
569
|
+
background: var(--bg-4);
|
|
570
|
+
border-radius: 2px;
|
|
571
|
+
overflow: hidden;
|
|
572
|
+
}
|
|
573
|
+
.conf-bar-fill {
|
|
574
|
+
height: 100%;
|
|
575
|
+
background: var(--mint);
|
|
576
|
+
border-radius: 2px;
|
|
577
|
+
transition: width 0.3s;
|
|
578
|
+
}
|
|
579
|
+
.conf-bar-fill.amber { background: var(--amber); }
|
|
580
|
+
.conf-bar-fill.red { background: var(--red); }
|
|
581
|
+
|
|
582
|
+
.tag-chip {
|
|
583
|
+
padding: 1px 7px;
|
|
584
|
+
background: var(--bg-4);
|
|
585
|
+
border: 1px solid var(--border-2);
|
|
586
|
+
border-radius: 8px;
|
|
587
|
+
font-size: 10.5px;
|
|
588
|
+
color: var(--text-3);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.memory-actions {
|
|
592
|
+
display: flex;
|
|
593
|
+
flex-direction: column;
|
|
594
|
+
justify-content: center;
|
|
595
|
+
gap: 4px;
|
|
596
|
+
padding: 12px 12px 12px 8px;
|
|
597
|
+
opacity: 0;
|
|
598
|
+
transition: opacity 0.15s;
|
|
599
|
+
}
|
|
600
|
+
.memory-card:hover .memory-actions { opacity: 1; }
|
|
601
|
+
|
|
602
|
+
.action-btn {
|
|
603
|
+
width: 28px; height: 28px;
|
|
604
|
+
display: flex; align-items: center; justify-content: center;
|
|
605
|
+
border: 1px solid var(--border-2);
|
|
606
|
+
border-radius: var(--radius);
|
|
607
|
+
background: var(--bg-3);
|
|
608
|
+
cursor: pointer;
|
|
609
|
+
font-size: 12px;
|
|
610
|
+
color: var(--text-2);
|
|
611
|
+
transition: all 0.12s;
|
|
612
|
+
}
|
|
613
|
+
.action-btn:hover { background: var(--bg-4); color: var(--text); border-color: var(--border-3); }
|
|
614
|
+
.action-btn.edit:hover { border-color: var(--mint); color: var(--mint); background: var(--mint-dim); }
|
|
615
|
+
.action-btn.delete:hover { border-color: var(--red); color: var(--red); background: var(--red-dim); }
|
|
616
|
+
|
|
617
|
+
/* ── Modal ── */
|
|
618
|
+
.modal-overlay {
|
|
619
|
+
position: fixed; inset: 0;
|
|
620
|
+
background: rgba(0,0,0,0.7);
|
|
621
|
+
backdrop-filter: blur(4px);
|
|
622
|
+
display: none;
|
|
623
|
+
align-items: center;
|
|
624
|
+
justify-content: center;
|
|
625
|
+
z-index: 1000;
|
|
626
|
+
padding: 24px;
|
|
627
|
+
}
|
|
628
|
+
.modal-overlay.open { display: flex; }
|
|
629
|
+
|
|
630
|
+
.modal {
|
|
631
|
+
background: var(--bg-2);
|
|
632
|
+
border: 1px solid var(--border-2);
|
|
633
|
+
border-radius: var(--radius-xl);
|
|
634
|
+
width: 100%;
|
|
635
|
+
max-width: 560px;
|
|
636
|
+
max-height: 90vh;
|
|
637
|
+
overflow-y: auto;
|
|
638
|
+
box-shadow: 0 25px 50px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.03);
|
|
639
|
+
animation: modal-in 0.2s ease-out;
|
|
640
|
+
}
|
|
641
|
+
@keyframes modal-in {
|
|
642
|
+
from { opacity: 0; transform: translateY(-12px) scale(0.98); }
|
|
643
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.modal-header {
|
|
647
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
648
|
+
padding: 20px 24px 0;
|
|
649
|
+
}
|
|
650
|
+
.modal-title { font-size: 16px; font-weight: 600; letter-spacing: -0.02em; }
|
|
651
|
+
.modal-close {
|
|
652
|
+
width: 28px; height: 28px;
|
|
653
|
+
display: flex; align-items: center; justify-content: center;
|
|
654
|
+
background: var(--bg-3);
|
|
655
|
+
border: 1px solid var(--border-2);
|
|
656
|
+
border-radius: var(--radius);
|
|
657
|
+
cursor: pointer;
|
|
658
|
+
color: var(--text-2);
|
|
659
|
+
font-size: 16px;
|
|
660
|
+
transition: all 0.12s;
|
|
661
|
+
}
|
|
662
|
+
.modal-close:hover { background: var(--bg-4); color: var(--text); }
|
|
663
|
+
|
|
664
|
+
.modal-body { padding: 20px 24px 24px; display: flex; flex-direction: column; gap: 16px; }
|
|
665
|
+
|
|
666
|
+
.field { display: flex; flex-direction: column; gap: 6px; }
|
|
667
|
+
.field label { font-size: 12px; font-weight: 600; color: var(--text-2); letter-spacing: 0.02em; text-transform: uppercase; }
|
|
668
|
+
|
|
669
|
+
.field textarea, .field input[type="text"], .field input[type="number"], .field select {
|
|
670
|
+
padding: 10px 12px;
|
|
671
|
+
background: var(--bg-3);
|
|
672
|
+
border: 1px solid var(--border-2);
|
|
673
|
+
border-radius: var(--radius);
|
|
674
|
+
color: var(--text);
|
|
675
|
+
font-family: var(--font);
|
|
676
|
+
font-size: 13px;
|
|
677
|
+
outline: none;
|
|
678
|
+
transition: border-color 0.15s;
|
|
679
|
+
width: 100%;
|
|
680
|
+
}
|
|
681
|
+
.field textarea:focus, .field input:focus, .field select:focus { border-color: var(--mint); }
|
|
682
|
+
.field textarea { resize: vertical; min-height: 90px; line-height: 1.6; font-family: var(--mono); font-size: 12.5px; }
|
|
683
|
+
.field select option { background: var(--bg-3); }
|
|
684
|
+
|
|
685
|
+
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
686
|
+
|
|
687
|
+
.field-hint { font-size: 11px; color: var(--text-3); }
|
|
688
|
+
|
|
689
|
+
/* Slider */
|
|
690
|
+
.slider-wrap { display: flex; align-items: center; gap: 10px; }
|
|
691
|
+
.conf-slider {
|
|
692
|
+
flex: 1;
|
|
693
|
+
-webkit-appearance: none;
|
|
694
|
+
height: 4px;
|
|
695
|
+
background: var(--bg-4);
|
|
696
|
+
border-radius: 2px;
|
|
697
|
+
outline: none;
|
|
698
|
+
}
|
|
699
|
+
.conf-slider::-webkit-slider-thumb {
|
|
700
|
+
-webkit-appearance: none;
|
|
701
|
+
width: 16px; height: 16px;
|
|
702
|
+
border-radius: 50%;
|
|
703
|
+
background: var(--mint);
|
|
704
|
+
cursor: pointer;
|
|
705
|
+
box-shadow: 0 0 6px var(--mint-glow);
|
|
706
|
+
}
|
|
707
|
+
.conf-value { font-size: 13px; font-weight: 600; color: var(--mint); min-width: 30px; text-align: right; }
|
|
708
|
+
|
|
709
|
+
/* Toggle */
|
|
710
|
+
.toggle-wrap { display: flex; align-items: center; gap: 10px; }
|
|
711
|
+
.toggle-label { font-size: 13px; color: var(--text-2); }
|
|
712
|
+
.toggle {
|
|
713
|
+
position: relative; display: inline-block;
|
|
714
|
+
width: 36px; height: 20px;
|
|
715
|
+
}
|
|
716
|
+
.toggle input { opacity: 0; width: 0; height: 0; }
|
|
717
|
+
.toggle-slider {
|
|
718
|
+
position: absolute; inset: 0;
|
|
719
|
+
background: var(--bg-4);
|
|
720
|
+
border: 1px solid var(--border-2);
|
|
721
|
+
border-radius: 10px;
|
|
722
|
+
cursor: pointer;
|
|
723
|
+
transition: 0.2s;
|
|
724
|
+
}
|
|
725
|
+
.toggle-slider::before {
|
|
726
|
+
content: '';
|
|
727
|
+
position: absolute;
|
|
728
|
+
height: 14px; width: 14px;
|
|
729
|
+
left: 2px; bottom: 2px;
|
|
730
|
+
background: var(--text-3);
|
|
731
|
+
border-radius: 50%;
|
|
732
|
+
transition: 0.2s;
|
|
733
|
+
}
|
|
734
|
+
.toggle input:checked + .toggle-slider { background: var(--mint); border-color: var(--mint); }
|
|
735
|
+
.toggle input:checked + .toggle-slider::before { transform: translateX(16px); background: #fff; }
|
|
736
|
+
|
|
737
|
+
.modal-footer {
|
|
738
|
+
display: flex; justify-content: flex-end; gap: 8px;
|
|
739
|
+
padding: 0 24px 24px;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.btn {
|
|
743
|
+
padding: 9px 20px;
|
|
744
|
+
font-family: var(--font);
|
|
745
|
+
font-size: 13px;
|
|
746
|
+
font-weight: 600;
|
|
747
|
+
border-radius: var(--radius);
|
|
748
|
+
cursor: pointer;
|
|
749
|
+
transition: all 0.15s;
|
|
750
|
+
border: 1px solid transparent;
|
|
751
|
+
}
|
|
752
|
+
.btn-ghost {
|
|
753
|
+
background: transparent;
|
|
754
|
+
border-color: var(--border-2);
|
|
755
|
+
color: var(--text-2);
|
|
756
|
+
}
|
|
757
|
+
.btn-ghost:hover { background: var(--bg-3); color: var(--text); border-color: var(--border-3); }
|
|
758
|
+
.btn-primary {
|
|
759
|
+
background: var(--mint);
|
|
760
|
+
color: #030a06;
|
|
761
|
+
border-color: var(--mint);
|
|
762
|
+
}
|
|
763
|
+
.btn-primary:hover { background: #0ec898; box-shadow: 0 4px 12px var(--mint-glow); }
|
|
764
|
+
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
765
|
+
|
|
766
|
+
/* ── Conflict cards ── */
|
|
767
|
+
.conflict-card {
|
|
768
|
+
background: var(--bg-2);
|
|
769
|
+
border: 1px solid rgba(251,191,36,0.18);
|
|
770
|
+
border-radius: var(--radius-lg);
|
|
771
|
+
padding: 16px;
|
|
772
|
+
margin-bottom: 10px;
|
|
773
|
+
}
|
|
774
|
+
.conflict-header {
|
|
775
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
776
|
+
margin-bottom: 12px;
|
|
777
|
+
}
|
|
778
|
+
.conflict-pair {
|
|
779
|
+
display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
|
|
780
|
+
margin-bottom: 12px;
|
|
781
|
+
}
|
|
782
|
+
.conflict-side {
|
|
783
|
+
padding: 10px 12px;
|
|
784
|
+
border-radius: var(--radius);
|
|
785
|
+
font-family: var(--mono);
|
|
786
|
+
font-size: 12px;
|
|
787
|
+
line-height: 1.55;
|
|
788
|
+
}
|
|
789
|
+
.conflict-old { background: var(--red-dim); border: 1px solid rgba(248,113,113,0.2); color: var(--text-2); }
|
|
790
|
+
.conflict-new { background: var(--green-dim); border: 1px solid rgba(52,211,153,0.2); color: var(--text); }
|
|
791
|
+
.conflict-meta { font-size: 11px; color: var(--text-3); }
|
|
792
|
+
|
|
793
|
+
/* ── Pattern cards ── */
|
|
794
|
+
.pattern-section { margin-bottom: 28px; }
|
|
795
|
+
.section-header {
|
|
796
|
+
display: flex; align-items: center; gap: 10px;
|
|
797
|
+
margin-bottom: 12px;
|
|
798
|
+
}
|
|
799
|
+
.section-title {
|
|
800
|
+
font-size: 13px;
|
|
801
|
+
font-weight: 600;
|
|
802
|
+
color: var(--text-2);
|
|
803
|
+
text-transform: uppercase;
|
|
804
|
+
letter-spacing: 0.06em;
|
|
805
|
+
}
|
|
806
|
+
.section-badge {
|
|
807
|
+
padding: 2px 8px;
|
|
808
|
+
background: var(--bg-4);
|
|
809
|
+
border-radius: 10px;
|
|
810
|
+
font-size: 11px;
|
|
811
|
+
font-weight: 600;
|
|
812
|
+
color: var(--text-3);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.pattern-card {
|
|
816
|
+
background: var(--bg-2);
|
|
817
|
+
border: 1px solid var(--border);
|
|
818
|
+
border-radius: var(--radius-lg);
|
|
819
|
+
padding: 12px 14px;
|
|
820
|
+
margin-bottom: 8px;
|
|
821
|
+
display: flex; align-items: center; gap: 12px;
|
|
822
|
+
}
|
|
823
|
+
.pattern-query {
|
|
824
|
+
flex: 1;
|
|
825
|
+
font-family: var(--mono);
|
|
826
|
+
font-size: 12.5px;
|
|
827
|
+
color: var(--text);
|
|
828
|
+
}
|
|
829
|
+
.pattern-stats { display: flex; gap: 16px; }
|
|
830
|
+
.pattern-stat { text-align: right; }
|
|
831
|
+
.pattern-stat-num { font-size: 15px; font-weight: 700; color: var(--text); }
|
|
832
|
+
.pattern-stat-label { font-size: 10px; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.04em; }
|
|
833
|
+
|
|
834
|
+
/* ── Gap card ── */
|
|
835
|
+
.gap-card {
|
|
836
|
+
background: var(--bg-2);
|
|
837
|
+
border: 1px solid rgba(251,191,36,0.15);
|
|
838
|
+
border-radius: var(--radius-lg);
|
|
839
|
+
padding: 12px 14px;
|
|
840
|
+
margin-bottom: 8px;
|
|
841
|
+
display: flex; align-items: center; gap: 12px;
|
|
842
|
+
}
|
|
843
|
+
.gap-icon { font-size: 16px; flex-shrink: 0; }
|
|
844
|
+
.gap-body { flex: 1; }
|
|
845
|
+
.gap-query { font-family: var(--mono); font-size: 12.5px; color: var(--text); margin-bottom: 3px; }
|
|
846
|
+
.gap-meta { font-size: 11px; color: var(--text-3); }
|
|
847
|
+
|
|
848
|
+
/* ── Audit entries ── */
|
|
849
|
+
.audit-entry {
|
|
850
|
+
display: flex; align-items: flex-start; gap: 12px;
|
|
851
|
+
padding: 12px;
|
|
852
|
+
background: var(--bg-2);
|
|
853
|
+
border: 1px solid var(--border);
|
|
854
|
+
border-radius: var(--radius-lg);
|
|
855
|
+
margin-bottom: 8px;
|
|
856
|
+
transition: border-color 0.12s;
|
|
857
|
+
}
|
|
858
|
+
.audit-entry:hover { border-color: var(--border-2); }
|
|
859
|
+
.audit-icon {
|
|
860
|
+
width: 32px; height: 32px;
|
|
861
|
+
border-radius: var(--radius);
|
|
862
|
+
display: flex; align-items: center; justify-content: center;
|
|
863
|
+
font-size: 14px;
|
|
864
|
+
flex-shrink: 0;
|
|
865
|
+
}
|
|
866
|
+
.audit-icon.read { background: var(--blue-dim); }
|
|
867
|
+
.audit-icon.write { background: var(--mint-dim); }
|
|
868
|
+
.audit-body { flex: 1; min-width: 0; }
|
|
869
|
+
.audit-top { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
|
870
|
+
.audit-agent { font-size: 12px; font-weight: 600; color: var(--text); }
|
|
871
|
+
.audit-time { font-size: 11px; color: var(--text-3); }
|
|
872
|
+
.audit-text { font-family: var(--mono); font-size: 12px; color: var(--text-2); line-height: 1.5; }
|
|
873
|
+
.audit-sub { font-size: 11px; color: var(--text-3); margin-top: 3px; font-family: var(--mono); }
|
|
874
|
+
|
|
875
|
+
/* ── Namespace table ── */
|
|
876
|
+
.ns-table { width: 100%; border-collapse: collapse; }
|
|
877
|
+
.ns-table th {
|
|
878
|
+
padding: 8px 14px;
|
|
879
|
+
font-size: 11px;
|
|
880
|
+
font-weight: 600;
|
|
881
|
+
text-transform: uppercase;
|
|
882
|
+
letter-spacing: 0.05em;
|
|
883
|
+
color: var(--text-3);
|
|
884
|
+
text-align: left;
|
|
885
|
+
border-bottom: 1px solid var(--border);
|
|
886
|
+
}
|
|
887
|
+
.ns-table td {
|
|
888
|
+
padding: 12px 14px;
|
|
889
|
+
font-size: 13px;
|
|
890
|
+
border-bottom: 1px solid var(--border);
|
|
891
|
+
vertical-align: middle;
|
|
892
|
+
}
|
|
893
|
+
.ns-table tr:last-child td { border-bottom: none; }
|
|
894
|
+
.ns-table tr:hover td { background: var(--bg-3); }
|
|
895
|
+
.ns-name { font-weight: 600; color: var(--text); }
|
|
896
|
+
.ns-actions { display: flex; gap: 6px; }
|
|
897
|
+
|
|
898
|
+
/* ── Digest ── */
|
|
899
|
+
.digest-grid {
|
|
900
|
+
display: grid;
|
|
901
|
+
grid-template-columns: repeat(3, 1fr);
|
|
902
|
+
gap: 10px;
|
|
903
|
+
margin-bottom: 24px;
|
|
904
|
+
}
|
|
905
|
+
.digest-stat {
|
|
906
|
+
background: var(--bg-2);
|
|
907
|
+
border: 1px solid var(--border);
|
|
908
|
+
border-radius: var(--radius-lg);
|
|
909
|
+
padding: 14px 16px;
|
|
910
|
+
}
|
|
911
|
+
.digest-stat-num { font-size: 28px; font-weight: 700; letter-spacing: -0.04em; margin-bottom: 2px; }
|
|
912
|
+
.digest-stat-label { font-size: 11px; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.04em; }
|
|
913
|
+
|
|
914
|
+
/* ── Empty state ── */
|
|
915
|
+
.empty {
|
|
916
|
+
display: flex; flex-direction: column; align-items: center;
|
|
917
|
+
justify-content: center;
|
|
918
|
+
padding: 60px 20px;
|
|
919
|
+
text-align: center;
|
|
920
|
+
}
|
|
921
|
+
.empty-icon { font-size: 36px; margin-bottom: 12px; opacity: 0.5; }
|
|
922
|
+
.empty-title { font-size: 15px; font-weight: 600; color: var(--text-2); margin-bottom: 6px; }
|
|
923
|
+
.empty-sub { font-size: 13px; color: var(--text-3); max-width: 300px; }
|
|
924
|
+
|
|
925
|
+
/* ── Conflict warning in modal ── */
|
|
926
|
+
.conflict-warn {
|
|
927
|
+
padding: 10px 14px;
|
|
928
|
+
background: rgba(251,191,36,0.1);
|
|
929
|
+
border: 1px solid rgba(251,191,36,0.25);
|
|
930
|
+
border-radius: var(--radius);
|
|
931
|
+
font-size: 12px;
|
|
932
|
+
color: var(--amber);
|
|
933
|
+
line-height: 1.5;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/* ── Audit filter tabs ── */
|
|
937
|
+
.sub-tabs { display: flex; gap: 4px; margin-bottom: 14px; }
|
|
938
|
+
.sub-tab {
|
|
939
|
+
padding: 5px 12px;
|
|
940
|
+
font-size: 12px;
|
|
941
|
+
font-family: var(--font);
|
|
942
|
+
background: transparent;
|
|
943
|
+
border: 1px solid var(--border-2);
|
|
944
|
+
border-radius: 20px;
|
|
945
|
+
color: var(--text-3);
|
|
946
|
+
cursor: pointer;
|
|
947
|
+
transition: all 0.12s;
|
|
948
|
+
}
|
|
949
|
+
.sub-tab:hover { color: var(--text-2); }
|
|
950
|
+
.sub-tab.active { color: var(--mint); border-color: var(--mint); background: var(--mint-dim); }
|
|
951
|
+
|
|
952
|
+
/* ── Loading skeleton ── */
|
|
953
|
+
.skeleton {
|
|
954
|
+
background: linear-gradient(90deg, var(--bg-3) 25%, var(--bg-4) 50%, var(--bg-3) 75%);
|
|
955
|
+
background-size: 200% 100%;
|
|
956
|
+
animation: shimmer 1.5s infinite;
|
|
957
|
+
border-radius: var(--radius);
|
|
958
|
+
height: 72px;
|
|
959
|
+
margin-bottom: 8px;
|
|
960
|
+
}
|
|
961
|
+
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
|
962
|
+
|
|
963
|
+
/* ── Toasts ── */
|
|
964
|
+
#toast-container {
|
|
965
|
+
position: fixed;
|
|
966
|
+
bottom: 20px; right: 20px;
|
|
967
|
+
z-index: 9999;
|
|
968
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
969
|
+
}
|
|
970
|
+
.toast {
|
|
971
|
+
padding: 10px 16px;
|
|
972
|
+
background: var(--bg-3);
|
|
973
|
+
border: 1px solid var(--border-2);
|
|
974
|
+
border-radius: var(--radius-lg);
|
|
975
|
+
font-size: 13px;
|
|
976
|
+
color: var(--text);
|
|
977
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
|
978
|
+
animation: toast-in 0.2s ease-out;
|
|
979
|
+
display: flex; align-items: center; gap: 8px;
|
|
980
|
+
}
|
|
981
|
+
.toast.success { border-color: rgba(16,217,164,0.3); }
|
|
982
|
+
.toast.error { border-color: rgba(248,113,113,0.3); }
|
|
983
|
+
@keyframes toast-in {
|
|
984
|
+
from { opacity: 0; transform: translateX(16px); }
|
|
985
|
+
to { opacity: 1; transform: translateX(0); }
|
|
986
|
+
}
|
|
987
|
+
</style>
|
|
988
|
+
</head>
|
|
989
|
+
<body>
|
|
990
|
+
<div class="layout">
|
|
991
|
+
|
|
992
|
+
<!-- Navbar -->
|
|
993
|
+
<nav>
|
|
994
|
+
<div class="nav-logo">
|
|
995
|
+
<div class="nav-logo-icon">◈</div>
|
|
996
|
+
Mesh
|
|
997
|
+
</div>
|
|
998
|
+
<div class="nav-ns">
|
|
999
|
+
<div class="nav-ns-dot"></div>
|
|
1000
|
+
<span id="ns-label">shared</span>
|
|
1001
|
+
</div>
|
|
1002
|
+
<div class="nav-search">
|
|
1003
|
+
<span class="nav-search-icon">⌕</span>
|
|
1004
|
+
<input type="text" id="search-input" placeholder="Semantic search memories…" oninput="onSearchInput(this.value)">
|
|
1005
|
+
</div>
|
|
1006
|
+
<div class="nav-right">
|
|
1007
|
+
<button class="btn-icon" onclick="refreshAll()" title="Refresh">↻</button>
|
|
1008
|
+
<button class="btn-add" onclick="openModal()">+ Add Memory</button>
|
|
1009
|
+
</div>
|
|
1010
|
+
</nav>
|
|
1011
|
+
|
|
1012
|
+
<!-- Stats Bar -->
|
|
1013
|
+
<div class="stats-bar" id="stats-bar">
|
|
1014
|
+
<div class="stat-item">
|
|
1015
|
+
<div>
|
|
1016
|
+
<div class="stat-num mint" id="stat-total">—</div>
|
|
1017
|
+
<div class="stat-label">Memories</div>
|
|
1018
|
+
</div>
|
|
1019
|
+
</div>
|
|
1020
|
+
<div class="stat-item">
|
|
1021
|
+
<div>
|
|
1022
|
+
<div class="stat-num amber" id="stat-stale">—</div>
|
|
1023
|
+
<div class="stat-label">Stale</div>
|
|
1024
|
+
</div>
|
|
1025
|
+
</div>
|
|
1026
|
+
<div class="stat-item">
|
|
1027
|
+
<div>
|
|
1028
|
+
<div class="stat-num red" id="stat-conflicts">—</div>
|
|
1029
|
+
<div class="stat-label">Conflicts</div>
|
|
1030
|
+
</div>
|
|
1031
|
+
</div>
|
|
1032
|
+
<div class="stat-item">
|
|
1033
|
+
<div>
|
|
1034
|
+
<div class="stat-num purple" id="stat-gaps">—</div>
|
|
1035
|
+
<div class="stat-label">Memory Gaps</div>
|
|
1036
|
+
</div>
|
|
1037
|
+
</div>
|
|
1038
|
+
<div class="stat-item">
|
|
1039
|
+
<div>
|
|
1040
|
+
<div class="stat-num blue" id="stat-unused">—</div>
|
|
1041
|
+
<div class="stat-label">Unused</div>
|
|
1042
|
+
</div>
|
|
1043
|
+
</div>
|
|
1044
|
+
</div>
|
|
1045
|
+
|
|
1046
|
+
<!-- Tabs -->
|
|
1047
|
+
<div class="tabs-bar">
|
|
1048
|
+
<button class="tab active" onclick="showTab('memories', this)">
|
|
1049
|
+
Memories <span class="tab-badge" id="tab-badge-memories">0</span>
|
|
1050
|
+
</button>
|
|
1051
|
+
<button class="tab" onclick="showTab('patterns', this)">Patterns</button>
|
|
1052
|
+
<button class="tab" onclick="showTab('conflicts', this)">
|
|
1053
|
+
Conflicts <span class="tab-badge" id="tab-badge-conflicts">0</span>
|
|
1054
|
+
</button>
|
|
1055
|
+
<button class="tab" onclick="showTab('audit', this)">Audit Log</button>
|
|
1056
|
+
<button class="tab" onclick="showTab('namespaces', this)">Namespaces</button>
|
|
1057
|
+
<button class="tab" onclick="showTab('digest', this)">Digest</button>
|
|
1058
|
+
</div>
|
|
1059
|
+
|
|
1060
|
+
<!-- Content -->
|
|
1061
|
+
<div class="content">
|
|
1062
|
+
|
|
1063
|
+
<!-- Memories Panel -->
|
|
1064
|
+
<div class="panel active" id="panel-memories">
|
|
1065
|
+
<div class="memories-toolbar">
|
|
1066
|
+
<div class="filter-group" id="type-filters">
|
|
1067
|
+
<button class="filter-btn active" onclick="setTypeFilter('all', this)">All</button>
|
|
1068
|
+
<button class="filter-btn" onclick="setTypeFilter('fact', this)">Fact</button>
|
|
1069
|
+
<button class="filter-btn" onclick="setTypeFilter('preference', this)">Preference</button>
|
|
1070
|
+
<button class="filter-btn" onclick="setTypeFilter('context', this)">Context</button>
|
|
1071
|
+
<button class="filter-btn" onclick="setTypeFilter('result', this)">Result</button>
|
|
1072
|
+
<button class="filter-btn" onclick="setTypeFilter('instruction', this)">Instruction</button>
|
|
1073
|
+
</div>
|
|
1074
|
+
<select class="sort-select" onchange="setSortOrder(this.value)">
|
|
1075
|
+
<option value="accessed">Last accessed</option>
|
|
1076
|
+
<option value="created">Date created</option>
|
|
1077
|
+
<option value="confidence">Confidence</option>
|
|
1078
|
+
<option value="content">Alphabetical</option>
|
|
1079
|
+
</select>
|
|
1080
|
+
<span class="memory-count" id="memory-count"></span>
|
|
1081
|
+
</div>
|
|
1082
|
+
<div class="memory-list" id="memory-list">
|
|
1083
|
+
<div class="skeleton"></div>
|
|
1084
|
+
<div class="skeleton"></div>
|
|
1085
|
+
<div class="skeleton" style="height:56px"></div>
|
|
1086
|
+
</div>
|
|
1087
|
+
</div>
|
|
1088
|
+
|
|
1089
|
+
<!-- Patterns Panel -->
|
|
1090
|
+
<div class="panel" id="panel-patterns">
|
|
1091
|
+
<div class="pattern-section">
|
|
1092
|
+
<div class="section-header">
|
|
1093
|
+
<div class="section-title">Top Topics</div>
|
|
1094
|
+
<div class="section-badge" id="top-topics-count">0</div>
|
|
1095
|
+
</div>
|
|
1096
|
+
<div id="top-topics-list"></div>
|
|
1097
|
+
</div>
|
|
1098
|
+
<div class="pattern-section">
|
|
1099
|
+
<div class="section-header">
|
|
1100
|
+
<div class="section-title">Memory Gaps</div>
|
|
1101
|
+
<div class="section-badge" id="gaps-count">0</div>
|
|
1102
|
+
<span style="font-size:11px;color:var(--text-3)">— frequently asked, poorly answered</span>
|
|
1103
|
+
</div>
|
|
1104
|
+
<div id="gaps-list"></div>
|
|
1105
|
+
</div>
|
|
1106
|
+
<div class="pattern-section">
|
|
1107
|
+
<div class="section-header">
|
|
1108
|
+
<div class="section-title">Unused Memories</div>
|
|
1109
|
+
<div class="section-badge" id="unused-count">0</div>
|
|
1110
|
+
<span style="font-size:11px;color:var(--text-3)">— created 7+ days ago, never recalled</span>
|
|
1111
|
+
</div>
|
|
1112
|
+
<div id="unused-list"></div>
|
|
1113
|
+
</div>
|
|
1114
|
+
</div>
|
|
1115
|
+
|
|
1116
|
+
<!-- Conflicts Panel -->
|
|
1117
|
+
<div class="panel" id="panel-conflicts">
|
|
1118
|
+
<div id="conflicts-list"></div>
|
|
1119
|
+
</div>
|
|
1120
|
+
|
|
1121
|
+
<!-- Audit Panel -->
|
|
1122
|
+
<div class="panel" id="panel-audit">
|
|
1123
|
+
<div class="sub-tabs">
|
|
1124
|
+
<button class="sub-tab active" onclick="loadAudit('all', this)">All</button>
|
|
1125
|
+
<button class="sub-tab" onclick="loadAudit('read', this)">Reads</button>
|
|
1126
|
+
<button class="sub-tab" onclick="loadAudit('write', this)">Writes</button>
|
|
1127
|
+
</div>
|
|
1128
|
+
<div id="audit-list"></div>
|
|
1129
|
+
</div>
|
|
1130
|
+
|
|
1131
|
+
<!-- Namespaces Panel -->
|
|
1132
|
+
<div class="panel" id="panel-namespaces">
|
|
1133
|
+
<div style="background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden">
|
|
1134
|
+
<table class="ns-table">
|
|
1135
|
+
<thead>
|
|
1136
|
+
<tr>
|
|
1137
|
+
<th>Namespace</th>
|
|
1138
|
+
<th>Memories</th>
|
|
1139
|
+
<th>Private</th>
|
|
1140
|
+
<th>Last Updated</th>
|
|
1141
|
+
<th>Actions</th>
|
|
1142
|
+
</tr>
|
|
1143
|
+
</thead>
|
|
1144
|
+
<tbody id="namespaces-list"></tbody>
|
|
1145
|
+
</table>
|
|
1146
|
+
</div>
|
|
1147
|
+
</div>
|
|
1148
|
+
|
|
1149
|
+
<!-- Digest Panel -->
|
|
1150
|
+
<div class="panel" id="panel-digest">
|
|
1151
|
+
<div class="digest-grid" id="digest-stats"></div>
|
|
1152
|
+
<div id="digest-content"></div>
|
|
1153
|
+
</div>
|
|
1154
|
+
|
|
1155
|
+
</div>
|
|
1156
|
+
</div>
|
|
1157
|
+
|
|
1158
|
+
<!-- Add/Edit Memory Modal -->
|
|
1159
|
+
<div class="modal-overlay" id="modal-overlay" onclick="closeModalOnOverlay(event)">
|
|
1160
|
+
<div class="modal" id="modal">
|
|
1161
|
+
<div class="modal-header">
|
|
1162
|
+
<div class="modal-title" id="modal-title">Add Memory</div>
|
|
1163
|
+
<button class="modal-close" onclick="closeModal()">✕</button>
|
|
1164
|
+
</div>
|
|
1165
|
+
<div class="modal-body">
|
|
1166
|
+
<div class="field">
|
|
1167
|
+
<label>Content</label>
|
|
1168
|
+
<textarea id="modal-content" placeholder="What should agents remember? Be specific — 'The API rate limit is 100 req/min', not 'API stuff'."></textarea>
|
|
1169
|
+
</div>
|
|
1170
|
+
<div class="field-row">
|
|
1171
|
+
<div class="field">
|
|
1172
|
+
<label>Type</label>
|
|
1173
|
+
<select id="modal-type">
|
|
1174
|
+
<option value="fact">Fact — objective info</option>
|
|
1175
|
+
<option value="preference">Preference — user/system prefs</option>
|
|
1176
|
+
<option value="context">Context — situational background</option>
|
|
1177
|
+
<option value="result">Result — outcome of a task</option>
|
|
1178
|
+
<option value="instruction">Instruction — rules to follow</option>
|
|
1179
|
+
</select>
|
|
1180
|
+
</div>
|
|
1181
|
+
<div class="field">
|
|
1182
|
+
<label>TTL (days, optional)</label>
|
|
1183
|
+
<input type="number" id="modal-ttl" placeholder="e.g. 30 — leave blank for permanent" min="1" step="1">
|
|
1184
|
+
<div class="field-hint">Confidence decays to 0 after this many days</div>
|
|
1185
|
+
</div>
|
|
1186
|
+
</div>
|
|
1187
|
+
<div class="field">
|
|
1188
|
+
<label>Confidence: <span id="modal-conf-value">1.0</span></label>
|
|
1189
|
+
<div class="slider-wrap">
|
|
1190
|
+
<input type="range" class="conf-slider" id="modal-conf" min="0" max="1" step="0.05" value="1"
|
|
1191
|
+
oninput="document.getElementById('modal-conf-value').textContent = parseFloat(this.value).toFixed(2)">
|
|
1192
|
+
</div>
|
|
1193
|
+
<div class="field-hint">Use < 0.7 for uncertain or tentative information</div>
|
|
1194
|
+
</div>
|
|
1195
|
+
<div class="field">
|
|
1196
|
+
<label>Tags</label>
|
|
1197
|
+
<input type="text" id="modal-tags" placeholder="api, auth, infra (comma-separated)">
|
|
1198
|
+
</div>
|
|
1199
|
+
<div class="field">
|
|
1200
|
+
<div class="toggle-wrap">
|
|
1201
|
+
<label class="toggle">
|
|
1202
|
+
<input type="checkbox" id="modal-private">
|
|
1203
|
+
<span class="toggle-slider"></span>
|
|
1204
|
+
</label>
|
|
1205
|
+
<span class="toggle-label">Private — never exported or synced</span>
|
|
1206
|
+
</div>
|
|
1207
|
+
<div class="field-hint">Use for API keys, credentials, and sensitive config</div>
|
|
1208
|
+
</div>
|
|
1209
|
+
<div id="modal-conflict-warn" style="display:none"></div>
|
|
1210
|
+
</div>
|
|
1211
|
+
<div class="modal-footer">
|
|
1212
|
+
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
|
|
1213
|
+
<button class="btn btn-primary" id="modal-submit" onclick="submitMemory()">Save Memory</button>
|
|
1214
|
+
</div>
|
|
1215
|
+
</div>
|
|
1216
|
+
</div>
|
|
1217
|
+
|
|
1218
|
+
<!-- Toast Container -->
|
|
1219
|
+
<div id="toast-container"></div>
|
|
1220
|
+
|
|
1221
|
+
<script>
|
|
1222
|
+
// ── State ─────────────────────────────────────────────────────────────────
|
|
1223
|
+
|
|
1224
|
+
let allMemories = [];
|
|
1225
|
+
let displayedMemories = [];
|
|
1226
|
+
let activeTypeFilter = 'all';
|
|
1227
|
+
let sortOrder = 'accessed';
|
|
1228
|
+
let searchQuery = '';
|
|
1229
|
+
let searchTimeout = null;
|
|
1230
|
+
let editingMemoryId = null; // null = adding new, string = editing existing
|
|
1231
|
+
let ns = 'shared';
|
|
1232
|
+
|
|
1233
|
+
// ── Init ──────────────────────────────────────────────────────────────────
|
|
1234
|
+
|
|
1235
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
1236
|
+
// Read namespace from URL or default
|
|
1237
|
+
const urlNs = new URLSearchParams(window.location.search).get('ns');
|
|
1238
|
+
if (urlNs) ns = urlNs;
|
|
1239
|
+
document.getElementById('ns-label').textContent = ns;
|
|
1240
|
+
refreshAll();
|
|
1241
|
+
setInterval(loadStats, 30000);
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
async function refreshAll() {
|
|
1245
|
+
await Promise.all([loadStats(), loadMemories()]);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// ── Stats ─────────────────────────────────────────────────────────────────
|
|
1249
|
+
|
|
1250
|
+
async function loadStats() {
|
|
1251
|
+
const data = await fetchJSON('/api/stats');
|
|
1252
|
+
if (!data) return;
|
|
1253
|
+
document.getElementById('stat-total').textContent = data.total_memories;
|
|
1254
|
+
document.getElementById('stat-stale').textContent = data.stale_memories;
|
|
1255
|
+
document.getElementById('stat-conflicts').textContent = data.open_contradictions;
|
|
1256
|
+
document.getElementById('stat-gaps').textContent = data.memory_gaps;
|
|
1257
|
+
document.getElementById('stat-unused').textContent = data.unused_memories;
|
|
1258
|
+
document.getElementById('tab-badge-memories').textContent = data.total_memories;
|
|
1259
|
+
document.getElementById('tab-badge-conflicts').textContent = data.open_contradictions;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// ── Memories ──────────────────────────────────────────────────────────────
|
|
1263
|
+
|
|
1264
|
+
async function loadMemories() {
|
|
1265
|
+
const data = await fetchJSON('/api/memories?limit=200');
|
|
1266
|
+
if (!data) return;
|
|
1267
|
+
allMemories = data;
|
|
1268
|
+
renderMemories();
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function renderMemories() {
|
|
1272
|
+
let mems = [...allMemories];
|
|
1273
|
+
|
|
1274
|
+
// Filter by type
|
|
1275
|
+
if (activeTypeFilter !== 'all') {
|
|
1276
|
+
mems = mems.filter(m => m.memory_type === activeTypeFilter);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Filter by search query (client-side text filter)
|
|
1280
|
+
if (searchQuery && !isSemanticSearch) {
|
|
1281
|
+
const q = searchQuery.toLowerCase();
|
|
1282
|
+
mems = mems.filter(m =>
|
|
1283
|
+
m.content.toLowerCase().includes(q) ||
|
|
1284
|
+
(m.tags && JSON.parse(m.tags || '[]').some(t => t.toLowerCase().includes(q)))
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Sort
|
|
1289
|
+
if (sortOrder === 'created') {
|
|
1290
|
+
mems.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''));
|
|
1291
|
+
} else if (sortOrder === 'confidence') {
|
|
1292
|
+
mems.sort((a, b) => (b.confidence || 0) - (a.confidence || 0));
|
|
1293
|
+
} else if (sortOrder === 'content') {
|
|
1294
|
+
mems.sort((a, b) => a.content.localeCompare(b.content));
|
|
1295
|
+
}
|
|
1296
|
+
// default: accessed_at order (already from API)
|
|
1297
|
+
|
|
1298
|
+
displayedMemories = mems;
|
|
1299
|
+
document.getElementById('memory-count').textContent = `${mems.length} shown`;
|
|
1300
|
+
|
|
1301
|
+
const el = document.getElementById('memory-list');
|
|
1302
|
+
if (!mems.length) {
|
|
1303
|
+
el.innerHTML = `<div class="empty">
|
|
1304
|
+
<div class="empty-icon">◎</div>
|
|
1305
|
+
<div class="empty-title">No memories found</div>
|
|
1306
|
+
<div class="empty-sub">Try a different filter or add your first memory</div>
|
|
1307
|
+
</div>`;
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
el.innerHTML = mems.map(m => {
|
|
1312
|
+
const tags = parseTags(m.tags);
|
|
1313
|
+
const isStale = m.is_stale || false;
|
|
1314
|
+
const isPrivate = m.local_only || false;
|
|
1315
|
+
const typeClass = m.memory_type || 'fact';
|
|
1316
|
+
const conf = m.confidence || 1.0;
|
|
1317
|
+
const confColor = conf >= 0.7 ? '' : conf >= 0.4 ? 'amber' : 'red';
|
|
1318
|
+
|
|
1319
|
+
return `<div class="memory-card ${isStale ? 'stale' : ''}" id="mc-${esc(m.id)}">
|
|
1320
|
+
<div class="memory-stripe ${typeClass}"></div>
|
|
1321
|
+
<div class="memory-body">
|
|
1322
|
+
<div class="memory-content">${escHtml(m.content)}</div>
|
|
1323
|
+
<div class="memory-meta">
|
|
1324
|
+
<span class="badge badge-${typeClass}">${typeClass}</span>
|
|
1325
|
+
${isStale ? '<span class="badge badge-stale">⚠ Stale</span>' : ''}
|
|
1326
|
+
${isPrivate ? '<span class="badge badge-private">🔒 Private</span>' : ''}
|
|
1327
|
+
<div class="meta-sep"></div>
|
|
1328
|
+
<div class="conf-bar-wrap">
|
|
1329
|
+
<div class="conf-bar"><div class="conf-bar-fill ${confColor}" style="width:${Math.round(conf*100)}%"></div></div>
|
|
1330
|
+
<span>${conf.toFixed(2)}</span>
|
|
1331
|
+
</div>
|
|
1332
|
+
<div class="meta-sep"></div>
|
|
1333
|
+
<span class="meta-text">from ${escHtml(m.source_agent || 'unknown')}</span>
|
|
1334
|
+
${m.created_at ? `<div class="meta-sep"></div><span class="meta-text">${fmtDate(m.created_at)}</span>` : ''}
|
|
1335
|
+
${tags.length ? `<div class="meta-sep"></div>${tags.map(t => `<span class="tag-chip">${escHtml(t)}</span>`).join('')}` : ''}
|
|
1336
|
+
</div>
|
|
1337
|
+
</div>
|
|
1338
|
+
<div class="memory-actions">
|
|
1339
|
+
<button class="action-btn edit" title="Edit" onclick="openEditModal('${esc(m.id)}')">✎</button>
|
|
1340
|
+
<button class="action-btn delete" title="Delete" onclick="deleteMemory('${esc(m.id)}')">⌫</button>
|
|
1341
|
+
</div>
|
|
1342
|
+
</div>`;
|
|
1343
|
+
}).join('');
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function setTypeFilter(type, btn) {
|
|
1347
|
+
activeTypeFilter = type;
|
|
1348
|
+
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
1349
|
+
btn.classList.add('active');
|
|
1350
|
+
renderMemories();
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function setSortOrder(order) {
|
|
1354
|
+
sortOrder = order;
|
|
1355
|
+
renderMemories();
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
let isSemanticSearch = false;
|
|
1359
|
+
|
|
1360
|
+
function onSearchInput(val) {
|
|
1361
|
+
searchQuery = val.trim();
|
|
1362
|
+
clearTimeout(searchTimeout);
|
|
1363
|
+
|
|
1364
|
+
if (!searchQuery) {
|
|
1365
|
+
isSemanticSearch = false;
|
|
1366
|
+
renderMemories();
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Debounce: after 400ms with no typing, do semantic search
|
|
1371
|
+
isSemanticSearch = false;
|
|
1372
|
+
renderMemories(); // client-side filter first
|
|
1373
|
+
|
|
1374
|
+
searchTimeout = setTimeout(async () => {
|
|
1375
|
+
if (searchQuery.length >= 3) {
|
|
1376
|
+
const results = await fetchJSON(`/api/search?q=${encodeURIComponent(searchQuery)}&n=20`);
|
|
1377
|
+
if (results && Array.isArray(results)) {
|
|
1378
|
+
isSemanticSearch = true;
|
|
1379
|
+
displayedMemories = results;
|
|
1380
|
+
document.getElementById('memory-count').textContent = `${results.length} semantic matches`;
|
|
1381
|
+
const el = document.getElementById('memory-list');
|
|
1382
|
+
if (!results.length) {
|
|
1383
|
+
el.innerHTML = `<div class="empty"><div class="empty-icon">◎</div><div class="empty-title">No results</div></div>`;
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
// Re-use render but with results directly
|
|
1387
|
+
const temp = allMemories;
|
|
1388
|
+
allMemories = results;
|
|
1389
|
+
activeTypeFilter = 'all';
|
|
1390
|
+
renderMemories();
|
|
1391
|
+
allMemories = temp;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}, 400);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
async function deleteMemory(id) {
|
|
1398
|
+
if (!confirm('Delete this memory? This cannot be undone.')) return;
|
|
1399
|
+
const res = await fetch(`/api/memories/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
1400
|
+
if (res.ok) {
|
|
1401
|
+
toast('Memory deleted', 'success');
|
|
1402
|
+
allMemories = allMemories.filter(m => m.id !== id);
|
|
1403
|
+
renderMemories();
|
|
1404
|
+
loadStats();
|
|
1405
|
+
} else {
|
|
1406
|
+
toast('Failed to delete memory', 'error');
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// ── Modal ─────────────────────────────────────────────────────────────────
|
|
1411
|
+
|
|
1412
|
+
function openModal() {
|
|
1413
|
+
editingMemoryId = null;
|
|
1414
|
+
document.getElementById('modal-title').textContent = 'Add Memory';
|
|
1415
|
+
document.getElementById('modal-submit').textContent = 'Save Memory';
|
|
1416
|
+
document.getElementById('modal-content').value = '';
|
|
1417
|
+
document.getElementById('modal-type').value = 'fact';
|
|
1418
|
+
document.getElementById('modal-conf').value = 1;
|
|
1419
|
+
document.getElementById('modal-conf-value').textContent = '1.00';
|
|
1420
|
+
document.getElementById('modal-tags').value = '';
|
|
1421
|
+
document.getElementById('modal-ttl').value = '';
|
|
1422
|
+
document.getElementById('modal-private').checked = false;
|
|
1423
|
+
document.getElementById('modal-conflict-warn').style.display = 'none';
|
|
1424
|
+
document.getElementById('modal-overlay').classList.add('open');
|
|
1425
|
+
setTimeout(() => document.getElementById('modal-content').focus(), 100);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function openEditModal(id) {
|
|
1429
|
+
const mem = allMemories.find(m => m.id === id);
|
|
1430
|
+
if (!mem) return;
|
|
1431
|
+
editingMemoryId = id;
|
|
1432
|
+
document.getElementById('modal-title').textContent = 'Edit Memory';
|
|
1433
|
+
document.getElementById('modal-submit').textContent = 'Update Memory';
|
|
1434
|
+
document.getElementById('modal-content').value = mem.content || '';
|
|
1435
|
+
document.getElementById('modal-type').value = mem.memory_type || 'fact';
|
|
1436
|
+
const conf = mem.confidence || 1.0;
|
|
1437
|
+
document.getElementById('modal-conf').value = conf;
|
|
1438
|
+
document.getElementById('modal-conf-value').textContent = conf.toFixed(2);
|
|
1439
|
+
const tags = parseTags(mem.tags);
|
|
1440
|
+
document.getElementById('modal-tags').value = tags.join(', ');
|
|
1441
|
+
document.getElementById('modal-ttl').value = mem.ttl_days || '';
|
|
1442
|
+
document.getElementById('modal-private').checked = mem.local_only || false;
|
|
1443
|
+
document.getElementById('modal-conflict-warn').style.display = 'none';
|
|
1444
|
+
document.getElementById('modal-overlay').classList.add('open');
|
|
1445
|
+
setTimeout(() => document.getElementById('modal-content').focus(), 100);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function closeModal() {
|
|
1449
|
+
document.getElementById('modal-overlay').classList.remove('open');
|
|
1450
|
+
editingMemoryId = null;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function closeModalOnOverlay(e) {
|
|
1454
|
+
if (e.target === document.getElementById('modal-overlay')) closeModal();
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
async function submitMemory() {
|
|
1458
|
+
const content = document.getElementById('modal-content').value.trim();
|
|
1459
|
+
if (!content) { toast('Content is required', 'error'); return; }
|
|
1460
|
+
|
|
1461
|
+
const tagsRaw = document.getElementById('modal-tags').value;
|
|
1462
|
+
const tags = tagsRaw.split(',').map(t => t.trim()).filter(Boolean);
|
|
1463
|
+
const ttlRaw = document.getElementById('modal-ttl').value;
|
|
1464
|
+
const ttl = ttlRaw ? parseFloat(ttlRaw) : null;
|
|
1465
|
+
|
|
1466
|
+
const body = {
|
|
1467
|
+
content,
|
|
1468
|
+
memory_type: document.getElementById('modal-type').value,
|
|
1469
|
+
confidence: parseFloat(document.getElementById('modal-conf').value),
|
|
1470
|
+
tags,
|
|
1471
|
+
local_only: document.getElementById('modal-private').checked,
|
|
1472
|
+
ttl_days: ttl
|
|
1473
|
+
};
|
|
1474
|
+
|
|
1475
|
+
const btn = document.getElementById('modal-submit');
|
|
1476
|
+
btn.disabled = true;
|
|
1477
|
+
btn.textContent = 'Saving…';
|
|
1478
|
+
|
|
1479
|
+
try {
|
|
1480
|
+
let url = '/api/memories';
|
|
1481
|
+
let method = 'POST';
|
|
1482
|
+
if (editingMemoryId) {
|
|
1483
|
+
url = `/api/memories/${encodeURIComponent(editingMemoryId)}`;
|
|
1484
|
+
method = 'PUT';
|
|
1485
|
+
}
|
|
1486
|
+
const res = await fetch(url, {
|
|
1487
|
+
method,
|
|
1488
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1489
|
+
body: JSON.stringify(body)
|
|
1490
|
+
});
|
|
1491
|
+
const data = await res.json();
|
|
1492
|
+
if (!res.ok) {
|
|
1493
|
+
toast(data.detail || 'Failed to save memory', 'error');
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// Show conflict warning if any
|
|
1498
|
+
if (data.result && data.result.conflict) {
|
|
1499
|
+
const c = data.result.conflict;
|
|
1500
|
+
const warnEl = document.getElementById('modal-conflict-warn');
|
|
1501
|
+
warnEl.className = 'conflict-warn';
|
|
1502
|
+
warnEl.style.display = 'block';
|
|
1503
|
+
warnEl.innerHTML = `⚠ Possible conflict (similarity ${c.similarity.toFixed(2)})<br>
|
|
1504
|
+
<span style="opacity:0.8">Existing: "${escHtml(c.existing_content.slice(0, 100))}"</span>`;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
toast(editingMemoryId ? 'Memory updated' : 'Memory stored', 'success');
|
|
1508
|
+
closeModal();
|
|
1509
|
+
await loadMemories();
|
|
1510
|
+
loadStats();
|
|
1511
|
+
} finally {
|
|
1512
|
+
btn.disabled = false;
|
|
1513
|
+
btn.textContent = editingMemoryId ? 'Update Memory' : 'Save Memory';
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// ── Patterns ──────────────────────────────────────────────────────────────
|
|
1518
|
+
|
|
1519
|
+
async function loadPatterns() {
|
|
1520
|
+
const data = await fetchJSON('/api/patterns');
|
|
1521
|
+
if (!data) return;
|
|
1522
|
+
|
|
1523
|
+
const topTopics = data.top_topics || [];
|
|
1524
|
+
document.getElementById('top-topics-count').textContent = topTopics.length;
|
|
1525
|
+
const ttEl = document.getElementById('top-topics-list');
|
|
1526
|
+
ttEl.innerHTML = topTopics.length
|
|
1527
|
+
? topTopics.map(t => `<div class="pattern-card">
|
|
1528
|
+
<div class="pattern-query">${escHtml(t.query_text)}</div>
|
|
1529
|
+
<div class="pattern-stats">
|
|
1530
|
+
<div class="pattern-stat">
|
|
1531
|
+
<div class="pattern-stat-num">${t.query_count}</div>
|
|
1532
|
+
<div class="pattern-stat-label">Queries</div>
|
|
1533
|
+
</div>
|
|
1534
|
+
<div class="pattern-stat">
|
|
1535
|
+
<div class="pattern-stat-num">${(t.avg_similarity || 0).toFixed(2)}</div>
|
|
1536
|
+
<div class="pattern-stat-label">Avg Match</div>
|
|
1537
|
+
</div>
|
|
1538
|
+
</div>
|
|
1539
|
+
</div>`).join('')
|
|
1540
|
+
: `<div class="empty"><div class="empty-icon">◎</div><div class="empty-title">No queries yet</div><div class="empty-sub">Queries will appear here as agents use Mesh recall</div></div>`;
|
|
1541
|
+
|
|
1542
|
+
const gaps = data.memory_gaps || [];
|
|
1543
|
+
document.getElementById('gaps-count').textContent = gaps.length;
|
|
1544
|
+
const gapEl = document.getElementById('gaps-list');
|
|
1545
|
+
gapEl.innerHTML = gaps.length
|
|
1546
|
+
? gaps.map(g => `<div class="gap-card">
|
|
1547
|
+
<div class="gap-icon">⚠</div>
|
|
1548
|
+
<div class="gap-body">
|
|
1549
|
+
<div class="gap-query">${escHtml(g.query_text)}</div>
|
|
1550
|
+
<div class="gap-meta">Asked ${g.query_count}× · avg similarity ${(g.avg_similarity || 0).toFixed(2)} — consider adding a memory for this</div>
|
|
1551
|
+
</div>
|
|
1552
|
+
</div>`).join('')
|
|
1553
|
+
: `<div class="empty"><div class="empty-icon">✓</div><div class="empty-title">No gaps detected</div><div class="empty-sub">All frequent queries have good memory coverage</div></div>`;
|
|
1554
|
+
|
|
1555
|
+
const unused = data.unused_memories || [];
|
|
1556
|
+
document.getElementById('unused-count').textContent = unused.length;
|
|
1557
|
+
const unusedEl = document.getElementById('unused-list');
|
|
1558
|
+
unusedEl.innerHTML = unused.length
|
|
1559
|
+
? unused.map(m => `<div class="pattern-card">
|
|
1560
|
+
<div class="pattern-query">${escHtml(m.content)}</div>
|
|
1561
|
+
<div class="pattern-stats">
|
|
1562
|
+
<div class="pattern-stat">
|
|
1563
|
+
<div class="pattern-stat-num" style="font-size:12px;color:var(--text-3)">${fmtDate(m.created_at)}</div>
|
|
1564
|
+
<div class="pattern-stat-label">Created</div>
|
|
1565
|
+
</div>
|
|
1566
|
+
</div>
|
|
1567
|
+
<button class="action-btn delete" onclick="deleteMemory('${esc(m.id)}')" title="Delete">⌫</button>
|
|
1568
|
+
</div>`).join('')
|
|
1569
|
+
: `<div class="empty"><div class="empty-icon">✓</div><div class="empty-title">No unused memories</div><div class="empty-sub">Every memory has been accessed at least once</div></div>`;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// ── Conflicts ─────────────────────────────────────────────────────────────
|
|
1573
|
+
|
|
1574
|
+
async function loadConflicts() {
|
|
1575
|
+
const data = await fetchJSON('/api/contradictions');
|
|
1576
|
+
if (!data) return;
|
|
1577
|
+
const el = document.getElementById('conflicts-list');
|
|
1578
|
+
if (!data.length) {
|
|
1579
|
+
el.innerHTML = `<div class="empty"><div class="empty-icon">✓</div><div class="empty-title">No open conflicts</div><div class="empty-sub">All contradictions have been resolved</div></div>`;
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
el.innerHTML = data.map(c => `<div class="conflict-card">
|
|
1583
|
+
<div class="conflict-header">
|
|
1584
|
+
<div>
|
|
1585
|
+
<span class="badge badge-stale">⚡ Conflict</span>
|
|
1586
|
+
<span class="meta-text" style="margin-left:8px">similarity ${c.similarity.toFixed(2)}</span>
|
|
1587
|
+
</div>
|
|
1588
|
+
<button class="btn btn-ghost" style="padding:5px 12px;font-size:12px" onclick="resolveConflict('${esc(c.id)}')">Mark Resolved</button>
|
|
1589
|
+
</div>
|
|
1590
|
+
<div class="conflict-pair">
|
|
1591
|
+
<div>
|
|
1592
|
+
<div style="font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--text-3);margin-bottom:6px">Existing memory</div>
|
|
1593
|
+
<div class="conflict-side conflict-old">${escHtml(c.existing_content)}</div>
|
|
1594
|
+
</div>
|
|
1595
|
+
<div>
|
|
1596
|
+
<div style="font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--text-3);margin-bottom:6px">New memory (just stored)</div>
|
|
1597
|
+
<div class="conflict-side conflict-new">${escHtml(c.new_content)}</div>
|
|
1598
|
+
</div>
|
|
1599
|
+
</div>
|
|
1600
|
+
<div class="conflict-meta">Detected ${fmtDate(c.detected_at)}</div>
|
|
1601
|
+
</div>`).join('');
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
async function resolveConflict(id) {
|
|
1605
|
+
await fetch(`/api/contradictions/${encodeURIComponent(id)}/resolve`, { method: 'POST' });
|
|
1606
|
+
toast('Conflict resolved', 'success');
|
|
1607
|
+
loadConflicts();
|
|
1608
|
+
loadStats();
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// ── Audit ─────────────────────────────────────────────────────────────────
|
|
1612
|
+
|
|
1613
|
+
async function loadAudit(filter = 'all', btn = null) {
|
|
1614
|
+
if (btn) {
|
|
1615
|
+
document.querySelectorAll('.sub-tab').forEach(b => b.classList.remove('active'));
|
|
1616
|
+
btn.classList.add('active');
|
|
1617
|
+
}
|
|
1618
|
+
const data = await fetchJSON(`/api/audit?namespace=${encodeURIComponent(ns)}&limit=100&action=${filter}`);
|
|
1619
|
+
if (!data) return;
|
|
1620
|
+
const el = document.getElementById('audit-list');
|
|
1621
|
+
if (!data.length) {
|
|
1622
|
+
el.innerHTML = `<div class="empty"><div class="empty-icon">◎</div><div class="empty-title">No audit entries</div></div>`;
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
el.innerHTML = data.map(e => {
|
|
1626
|
+
const isRead = e.action === 'recall';
|
|
1627
|
+
return `<div class="audit-entry">
|
|
1628
|
+
<div class="audit-icon ${isRead ? 'read' : 'write'}">${isRead ? '⌕' : '✎'}</div>
|
|
1629
|
+
<div class="audit-body">
|
|
1630
|
+
<div class="audit-top">
|
|
1631
|
+
<span class="audit-agent">${escHtml(e.agent_id || 'unknown')}</span>
|
|
1632
|
+
<span class="audit-time">${fmtDate(e.timestamp)}</span>
|
|
1633
|
+
</div>
|
|
1634
|
+
${isRead
|
|
1635
|
+
? `<div class="audit-text">Asked: "${escHtml(e.query || '')}"</div>
|
|
1636
|
+
${e.top_result ? `<div class="audit-sub">→ ${escHtml(e.top_result.slice(0,100))}${e.top_result.length > 100 ? '…' : ''}</div>` : ''}`
|
|
1637
|
+
: `<div class="audit-text">Stored: "${escHtml((e.content || '').slice(0,120))}${(e.content||'').length > 120 ? '…' : ''}"</div>`
|
|
1638
|
+
}
|
|
1639
|
+
</div>
|
|
1640
|
+
</div>`;
|
|
1641
|
+
}).join('');
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// ── Namespaces ────────────────────────────────────────────────────────────
|
|
1645
|
+
|
|
1646
|
+
async function loadNamespaces() {
|
|
1647
|
+
const data = await fetchJSON('/api/namespaces');
|
|
1648
|
+
if (!data) return;
|
|
1649
|
+
const el = document.getElementById('namespaces-list');
|
|
1650
|
+
if (!data.length) {
|
|
1651
|
+
el.innerHTML = `<tr><td colspan="5" style="text-align:center;padding:32px;color:var(--text-3)">No namespaces found</td></tr>`;
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
el.innerHTML = data.map(n => `<tr>
|
|
1655
|
+
<td><span class="ns-name">${escHtml(n.namespace)}</span></td>
|
|
1656
|
+
<td style="color:var(--text-2)">${n.memory_count}</td>
|
|
1657
|
+
<td style="color:var(--text-2)">${n.local_only_count}</td>
|
|
1658
|
+
<td class="meta-text">${fmtDate(n.last_updated)}</td>
|
|
1659
|
+
<td>
|
|
1660
|
+
<div class="ns-actions">
|
|
1661
|
+
<button class="btn btn-ghost" style="padding:4px 10px;font-size:11px" onclick="downloadExport('${escHtml(n.namespace)}')">Export</button>
|
|
1662
|
+
<button class="btn btn-ghost" style="padding:4px 10px;font-size:11px;border-color:rgba(248,113,113,0.3);color:var(--red)" onclick="deleteNs('${escHtml(n.namespace)}')">Delete</button>
|
|
1663
|
+
</div>
|
|
1664
|
+
</td>
|
|
1665
|
+
</tr>`).join('');
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
async function deleteNs(namespace) {
|
|
1669
|
+
if (!confirm(`Delete namespace '${namespace}' and ALL its memories? This cannot be undone.`)) return;
|
|
1670
|
+
await fetch(`/api/namespaces/${encodeURIComponent(namespace)}`, { method: 'DELETE' });
|
|
1671
|
+
toast(`Namespace '${namespace}' deleted`, 'success');
|
|
1672
|
+
loadNamespaces();
|
|
1673
|
+
loadStats();
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
async function downloadExport(namespace) {
|
|
1677
|
+
const data = await fetchJSON(`/api/export/${encodeURIComponent(namespace)}?include_embeddings=false`);
|
|
1678
|
+
if (!data) return;
|
|
1679
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
1680
|
+
const url = URL.createObjectURL(blob);
|
|
1681
|
+
const a = document.createElement('a');
|
|
1682
|
+
a.href = url;
|
|
1683
|
+
a.download = `mesh_${namespace}_export.json`;
|
|
1684
|
+
a.click();
|
|
1685
|
+
URL.revokeObjectURL(url);
|
|
1686
|
+
toast(`Exported ${data.memory_count} memories`, 'success');
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// ── Digest ────────────────────────────────────────────────────────────────
|
|
1690
|
+
|
|
1691
|
+
async function loadDigest() {
|
|
1692
|
+
const data = await fetchJSON(`/api/digest?namespace=${encodeURIComponent(ns)}&hours=24`);
|
|
1693
|
+
if (!data) return;
|
|
1694
|
+
|
|
1695
|
+
document.getElementById('digest-stats').innerHTML = `
|
|
1696
|
+
<div class="digest-stat">
|
|
1697
|
+
<div class="digest-stat-num" style="color:var(--mint)">${data.new_count}</div>
|
|
1698
|
+
<div class="digest-stat-label">New Memories</div>
|
|
1699
|
+
</div>
|
|
1700
|
+
<div class="digest-stat">
|
|
1701
|
+
<div class="digest-stat-num" style="color:var(--red)">${data.contradiction_count}</div>
|
|
1702
|
+
<div class="digest-stat-label">Contradictions</div>
|
|
1703
|
+
</div>
|
|
1704
|
+
<div class="digest-stat">
|
|
1705
|
+
<div class="digest-stat-num" style="color:var(--amber)">${data.stale_count}</div>
|
|
1706
|
+
<div class="digest-stat-label">Going Stale</div>
|
|
1707
|
+
</div>
|
|
1708
|
+
`;
|
|
1709
|
+
|
|
1710
|
+
let html = '';
|
|
1711
|
+
|
|
1712
|
+
if (data.new_count > 0) {
|
|
1713
|
+
html += `<div class="pattern-section">
|
|
1714
|
+
<div class="section-header"><div class="section-title">New Memories</div><div class="section-badge">${data.new_count}</div></div>
|
|
1715
|
+
${data.new_memories.slice(0, 10).map(m => `<div class="gap-card" style="border-color:var(--border)">
|
|
1716
|
+
<div class="gap-icon">${m.local_only ? '🔒' : '+'}</div>
|
|
1717
|
+
<div class="gap-body">
|
|
1718
|
+
<div class="gap-query">${escHtml(m.content)}</div>
|
|
1719
|
+
<div class="gap-meta">[${escHtml(m.memory_type)}] from ${escHtml(m.source_agent)}</div>
|
|
1720
|
+
</div>
|
|
1721
|
+
</div>`).join('')}
|
|
1722
|
+
</div>`;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
if (data.contradiction_count > 0) {
|
|
1726
|
+
html += `<div class="pattern-section">
|
|
1727
|
+
<div class="section-header"><div class="section-title">Contradictions</div><div class="section-badge">${data.contradiction_count}</div></div>
|
|
1728
|
+
${data.contradictions.map(c => `<div class="gap-card" style="border-color:rgba(248,113,113,0.2)">
|
|
1729
|
+
<div class="gap-icon">⚡</div>
|
|
1730
|
+
<div class="gap-body">
|
|
1731
|
+
<div class="gap-query">${escHtml(c.content.slice(0,120))}</div>
|
|
1732
|
+
<div class="gap-meta">flagged by ${escHtml(c.agent_id)}</div>
|
|
1733
|
+
</div>
|
|
1734
|
+
</div>`).join('')}
|
|
1735
|
+
</div>`;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
if (data.stale_count > 0) {
|
|
1739
|
+
html += `<div class="pattern-section">
|
|
1740
|
+
<div class="section-header"><div class="section-title">Going Stale</div><div class="section-badge">${data.stale_count}</div></div>
|
|
1741
|
+
${data.stale_memories.slice(0, 5).map(m => `<div class="gap-card" style="border-color:rgba(251,191,36,0.2)">
|
|
1742
|
+
<div class="gap-icon">⚠</div>
|
|
1743
|
+
<div class="gap-body">
|
|
1744
|
+
<div class="gap-query">${escHtml(m.content.slice(0,120))}</div>
|
|
1745
|
+
<div class="gap-meta">TTL: ${m.ttl_days} days, stored ${fmtDate(m.created_at)}</div>
|
|
1746
|
+
</div>
|
|
1747
|
+
</div>`).join('')}
|
|
1748
|
+
</div>`;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
if (!html) {
|
|
1752
|
+
html = `<div class="empty"><div class="empty-icon">✓</div><div class="empty-title">All clear — no activity in last 24h</div></div>`;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
document.getElementById('digest-content').innerHTML = html;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// ── Tab navigation ────────────────────────────────────────────────────────
|
|
1759
|
+
|
|
1760
|
+
function showTab(name, btn) {
|
|
1761
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
|
1762
|
+
document.querySelectorAll('.tab').forEach(b => b.classList.remove('active'));
|
|
1763
|
+
document.getElementById('panel-' + name).classList.add('active');
|
|
1764
|
+
btn.classList.add('active');
|
|
1765
|
+
|
|
1766
|
+
if (name === 'patterns') loadPatterns();
|
|
1767
|
+
if (name === 'conflicts') loadConflicts();
|
|
1768
|
+
if (name === 'namespaces') loadNamespaces();
|
|
1769
|
+
if (name === 'audit') loadAudit();
|
|
1770
|
+
if (name === 'digest') loadDigest();
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// ── Utilities ─────────────────────────────────────────────────────────────
|
|
1774
|
+
|
|
1775
|
+
async function fetchJSON(url) {
|
|
1776
|
+
try {
|
|
1777
|
+
const res = await fetch(url);
|
|
1778
|
+
return res.ok ? res.json() : null;
|
|
1779
|
+
} catch { return null; }
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function parseTags(raw) {
|
|
1783
|
+
if (!raw) return [];
|
|
1784
|
+
try { return JSON.parse(raw); } catch { return []; }
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
function escHtml(s) {
|
|
1788
|
+
if (!s) return '';
|
|
1789
|
+
return String(s)
|
|
1790
|
+
.replace(/&/g, '&')
|
|
1791
|
+
.replace(/</g, '<')
|
|
1792
|
+
.replace(/>/g, '>')
|
|
1793
|
+
.replace(/"/g, '"');
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function esc(s) {
|
|
1797
|
+
return String(s || '').replace(/'/g, "\\'");
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
function fmtDate(s) {
|
|
1801
|
+
if (!s) return '';
|
|
1802
|
+
try {
|
|
1803
|
+
const d = new Date(s);
|
|
1804
|
+
const now = new Date();
|
|
1805
|
+
const diff = (now - d) / 1000;
|
|
1806
|
+
if (diff < 60) return 'just now';
|
|
1807
|
+
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
1808
|
+
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
|
1809
|
+
if (diff < 604800) return Math.floor(diff / 86400) + 'd ago';
|
|
1810
|
+
return d.toLocaleDateString();
|
|
1811
|
+
} catch { return s.slice(0, 10); }
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function toast(msg, type = 'success') {
|
|
1815
|
+
const el = document.createElement('div');
|
|
1816
|
+
el.className = `toast ${type}`;
|
|
1817
|
+
el.innerHTML = `<span>${type === 'success' ? '✓' : '✕'}</span> ${escHtml(msg)}`;
|
|
1818
|
+
document.getElementById('toast-container').appendChild(el);
|
|
1819
|
+
setTimeout(() => el.remove(), 3200);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// Keyboard shortcut: N = new memory
|
|
1823
|
+
document.addEventListener('keydown', e => {
|
|
1824
|
+
if (e.key === 'n' && !e.ctrlKey && !e.metaKey &&
|
|
1825
|
+
document.activeElement.tagName !== 'INPUT' &&
|
|
1826
|
+
document.activeElement.tagName !== 'TEXTAREA') {
|
|
1827
|
+
openModal();
|
|
1828
|
+
}
|
|
1829
|
+
if (e.key === 'Escape') closeModal();
|
|
1830
|
+
});
|
|
1831
|
+
</script>
|
|
1832
|
+
</body>
|
|
1833
|
+
</html>"""
|
|
1834
|
+
|
|
1835
|
+
|
|
1836
|
+
@api.get("/", response_class=HTMLResponse)
|
|
1837
|
+
def dashboard():
|
|
1838
|
+
return DASHBOARD_HTML
|
|
1839
|
+
|
|
1840
|
+
|
|
1841
|
+
# ── CLI entry point ─────────────────────────────────────────────────────────
|
|
1842
|
+
|
|
1843
|
+
@click.command()
|
|
1844
|
+
@click.option("--namespace", default="shared", envvar="MESH_NAMESPACE",
|
|
1845
|
+
help="Mesh namespace to inspect (default: shared)")
|
|
1846
|
+
@click.option("--port", default=7433, help="Port to run on (default: 7433)")
|
|
1847
|
+
@click.option("--no-browser", is_flag=True, help="Don't auto-open the browser")
|
|
1848
|
+
def run(namespace: str, port: int, no_browser: bool):
|
|
1849
|
+
"""Start the Mesh dashboard at http://localhost:<port>"""
|
|
1850
|
+
global _mesh
|
|
1851
|
+
_mesh = Mesh(namespace=namespace, agent_id="dashboard")
|
|
1852
|
+
|
|
1853
|
+
url = f"http://localhost:{port}"
|
|
1854
|
+
print(f"\n ◈ Mesh Dashboard")
|
|
1855
|
+
print(f" {url}")
|
|
1856
|
+
print(f" namespace: {namespace}")
|
|
1857
|
+
print(f" Press Ctrl+C to stop.\n")
|
|
1858
|
+
|
|
1859
|
+
if not no_browser:
|
|
1860
|
+
def open_browser():
|
|
1861
|
+
time.sleep(1.2)
|
|
1862
|
+
webbrowser.open(url)
|
|
1863
|
+
threading.Thread(target=open_browser, daemon=True).start()
|
|
1864
|
+
|
|
1865
|
+
uvicorn.run(api, host="127.0.0.1", port=port, log_level="error")
|
|
1866
|
+
|
|
1867
|
+
|
|
1868
|
+
if __name__ == "__main__":
|
|
1869
|
+
run()
|