dbrain 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/dashboard/icons/apple-touch-icon.png +0 -0
- package/dist/dashboard/icons/favicon-96x96.png +0 -0
- package/dist/dashboard/icons/favicon.ico +0 -0
- package/dist/dashboard/icons/favicon.svg +1 -0
- package/dist/dashboard/icons/icons/apple-touch-icon.png +0 -0
- package/dist/dashboard/icons/icons/favicon-96x96.png +0 -0
- package/dist/dashboard/icons/icons/favicon.ico +0 -0
- package/dist/dashboard/icons/icons/favicon.svg +1 -0
- package/dist/dashboard/icons/icons/site.webmanifest +21 -0
- package/dist/dashboard/icons/icons/web-app-manifest-192x192.png +0 -0
- package/dist/dashboard/icons/icons/web-app-manifest-512x512.png +0 -0
- package/dist/dashboard/icons/site.webmanifest +21 -0
- package/dist/dashboard/icons/web-app-manifest-192x192.png +0 -0
- package/dist/dashboard/icons/web-app-manifest-512x512.png +0 -0
- package/dist/dashboard/index.html +868 -649
- package/dist/dashboard/logo-complete.png +0 -0
- package/dist/dashboard/logo-image.png +0 -0
- package/dist/dashboard/logo.png +0 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +28 -2
- package/dist/dashboard/server.js.map +1 -1
- package/dist/server/routes/conversations.d.ts.map +1 -1
- package/dist/server/routes/conversations.js +5 -3
- package/dist/server/routes/conversations.js.map +1 -1
- package/dist/server/routes/health.d.ts.map +1 -1
- package/dist/server/routes/health.js +7 -1
- package/dist/server/routes/health.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,676 +1,895 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
.fact-row .access-count { font-size: 11px; color: #b0b4c0; font-weight: 500; }
|
|
279
|
-
|
|
280
|
-
.convos-list { margin-bottom: 32px; }
|
|
281
|
-
.conv-row {
|
|
282
|
-
background: #fff;
|
|
283
|
-
border-radius: 10px;
|
|
284
|
-
padding: 14px 20px;
|
|
285
|
-
margin-bottom: 6px;
|
|
286
|
-
display: flex;
|
|
287
|
-
justify-content: space-between;
|
|
288
|
-
align-items: center;
|
|
289
|
-
gap: 16px;
|
|
290
|
-
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
|
|
291
|
-
cursor: pointer;
|
|
292
|
-
transition: transform 0.15s;
|
|
293
|
-
border-left: 3px solid #10b981;
|
|
294
|
-
}
|
|
295
|
-
.conv-row:hover { transform: translateX(4px); }
|
|
296
|
-
.conv-row .conv-date { font-size: 14px; font-weight: 600; color: #1a1a2e; }
|
|
297
|
-
.conv-row .conv-source {
|
|
298
|
-
font-size: 11px; padding: 3px 10px; border-radius: 6px;
|
|
299
|
-
background: #ecfdf5; color: #059669; font-weight: 600;
|
|
300
|
-
}
|
|
301
|
-
.conv-row .conv-summary { font-size: 13px; color: #6b7280; flex: 1; margin: 0 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
302
|
-
|
|
303
|
-
.messages-list { margin-bottom: 32px; }
|
|
304
|
-
.msg-row { padding: 12px 16px; margin-bottom: 4px; border-radius: 10px; font-size: 14px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
|
|
305
|
-
.msg-row.user { background: #eff6ff; border-left: 3px solid #3b82f6; color: #1e3a5f; }
|
|
306
|
-
.msg-row.assistant { background: #fff; border-left: 3px solid #10b981; color: #374151; box-shadow: 0 1px 2px rgba(0,0,0,0.03); }
|
|
307
|
-
.msg-row.system { background: #fefce8; border-left: 3px solid #eab308; color: #713f12; font-size: 13px; }
|
|
308
|
-
.msg-role { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
|
|
309
|
-
.msg-row.user .msg-role { color: #3b82f6; }
|
|
310
|
-
.msg-row.assistant .msg-role { color: #10b981; }
|
|
311
|
-
.msg-row.system .msg-role { color: #eab308; }
|
|
312
|
-
.msg-content { max-height: 200px; overflow-y: auto; }
|
|
313
|
-
.msg-row.expanded .msg-content { max-height: none; }
|
|
314
|
-
|
|
315
|
-
.empty { color: #b0b4c0; font-style: italic; padding: 32px; text-align: center; background: #fff; border-radius: 12px; }
|
|
316
|
-
.loading-screen {
|
|
317
|
-
height: 100vh;
|
|
318
|
-
display: flex;
|
|
319
|
-
flex-direction: column;
|
|
320
|
-
align-items: center;
|
|
321
|
-
justify-content: center;
|
|
322
|
-
gap: 16px;
|
|
323
|
-
}
|
|
324
|
-
.loading-spinner {
|
|
325
|
-
width: 40px; height: 40px;
|
|
326
|
-
border: 3px solid #e5e7eb;
|
|
327
|
-
border-top-color: #6366f1;
|
|
328
|
-
border-radius: 50%;
|
|
329
|
-
animation: spin 0.8s linear infinite;
|
|
330
|
-
}
|
|
331
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
332
|
-
.loading-text { color: #9ca3af; font-size: 14px; font-weight: 500; }
|
|
333
|
-
|
|
334
|
-
.token-screen {
|
|
335
|
-
height: 100vh;
|
|
336
|
-
display: flex;
|
|
337
|
-
align-items: center;
|
|
338
|
-
justify-content: center;
|
|
339
|
-
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
|
340
|
-
}
|
|
341
|
-
.token-card {
|
|
342
|
-
background: #fff;
|
|
343
|
-
border-radius: 20px;
|
|
344
|
-
padding: 48px 40px;
|
|
345
|
-
text-align: center;
|
|
346
|
-
max-width: 440px;
|
|
347
|
-
width: 100%;
|
|
348
|
-
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
349
|
-
}
|
|
350
|
-
.token-card .logo {
|
|
351
|
-
width: 64px; height: 64px;
|
|
352
|
-
background: linear-gradient(135deg, #e94560, #ff6b6b);
|
|
353
|
-
border-radius: 16px;
|
|
354
|
-
margin: 0 auto 20px;
|
|
355
|
-
display: flex; align-items: center; justify-content: center;
|
|
356
|
-
font-size: 32px;
|
|
357
|
-
box-shadow: 0 8px 24px rgba(233,69,96,0.3);
|
|
358
|
-
}
|
|
359
|
-
.token-card h1 { font-size: 24px; font-weight: 800; color: #1a1a2e; margin-bottom: 6px; letter-spacing: -0.5px; }
|
|
360
|
-
.token-card p { color: #9ca3af; margin-bottom: 24px; font-size: 14px; }
|
|
361
|
-
.token-card input {
|
|
362
|
-
width: 100%;
|
|
363
|
-
padding: 14px 18px;
|
|
364
|
-
border: 2px solid #e5e7eb;
|
|
365
|
-
border-radius: 12px;
|
|
366
|
-
font-size: 15px;
|
|
367
|
-
font-family: 'Inter', monospace;
|
|
368
|
-
outline: none;
|
|
369
|
-
transition: border-color 0.2s, box-shadow 0.2s;
|
|
370
|
-
}
|
|
371
|
-
.token-card input:focus {
|
|
372
|
-
border-color: #6366f1;
|
|
373
|
-
box-shadow: 0 0 0 4px rgba(99,102,241,0.1);
|
|
374
|
-
}
|
|
375
|
-
.token-card input::placeholder { color: #d1d5db; }
|
|
376
|
-
.token-hint { font-size: 12px; color: #b0b4c0; margin-top: 12px; }
|
|
377
|
-
</style>
|
|
4
|
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
5
|
+
<title>dbrain - Your distributed brain</title>
|
|
6
|
+
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96">
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
8
|
+
<link rel="shortcut icon" href="/favicon.ico">
|
|
9
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
|
10
|
+
<link rel="manifest" href="/site.webmanifest">
|
|
11
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
12
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
13
|
+
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
14
|
+
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
15
|
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
16
|
+
<style>
|
|
17
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
18
|
+
:root{
|
|
19
|
+
--bg:oklch(0.965 0.008 248);
|
|
20
|
+
--sidebar-bg:oklch(0.99 0.003 250);
|
|
21
|
+
--surface:oklch(1 0 0);
|
|
22
|
+
--surface-2:oklch(0.95 0.01 248);
|
|
23
|
+
--border:oklch(0.87 0.018 248);
|
|
24
|
+
--border-subtle:oklch(0.92 0.01 248);
|
|
25
|
+
--text:oklch(0.20 0.06 252);
|
|
26
|
+
--text-2:oklch(0.42 0.05 250);
|
|
27
|
+
--text-3:oklch(0.60 0.04 248);
|
|
28
|
+
--accent:oklch(0.50 0.22 252);
|
|
29
|
+
--accent-light:oklch(0.94 0.06 252);
|
|
30
|
+
--online:oklch(0.50 0.18 148);
|
|
31
|
+
--font:'DM Sans',system-ui,sans-serif;
|
|
32
|
+
--mono:'JetBrains Mono',monospace;
|
|
33
|
+
--sb:220px;
|
|
34
|
+
--r:8px;
|
|
35
|
+
}
|
|
36
|
+
html,body{height:100%;font-family:var(--font);background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased}
|
|
37
|
+
|
|
38
|
+
/* ── LAYOUT ─────────────────────────────── */
|
|
39
|
+
.shell{display:flex;min-height:100vh}
|
|
40
|
+
.sidebar{
|
|
41
|
+
width:var(--sb);flex-shrink:0;position:fixed;top:0;left:0;bottom:0;
|
|
42
|
+
background:var(--sidebar-bg);border-right:1px solid var(--border-subtle);
|
|
43
|
+
display:flex;flex-direction:column;overflow:hidden;z-index:50;
|
|
44
|
+
}
|
|
45
|
+
.sb-logo{padding:24px 20px 8px;display:flex;flex-direction:column;align-items:center;gap:8px}
|
|
46
|
+
.sb-logo img{width:100px;height:auto;display:block}
|
|
47
|
+
.sb-logo-info{display:flex;flex-direction:column;align-items:center;gap:4px}
|
|
48
|
+
.sb-logo-text{font-size:18px;font-weight:700;letter-spacing:-.03em;color:var(--text)}
|
|
49
|
+
.sb-version{
|
|
50
|
+
font-size:10px;font-family:var(--mono);font-weight:500;
|
|
51
|
+
color:var(--text-3);background:var(--surface-2);
|
|
52
|
+
border:1px solid var(--border-subtle);
|
|
53
|
+
border-radius:4px;padding:2px 7px;display:inline-block;margin-left:2px;
|
|
54
|
+
}
|
|
55
|
+
.sb-nav{flex:1;padding:16px 10px;display:flex;flex-direction:column;gap:2px}
|
|
56
|
+
.sb-link{
|
|
57
|
+
display:flex;align-items:center;gap:10px;padding:9px 12px;
|
|
58
|
+
border-radius:var(--r);cursor:pointer;border:none;background:none;
|
|
59
|
+
font-family:var(--font);font-size:13.5px;font-weight:500;
|
|
60
|
+
color:var(--text-2);width:100%;text-align:left;
|
|
61
|
+
transition:background .12s,color .12s;
|
|
62
|
+
}
|
|
63
|
+
.sb-link:hover{background:var(--surface-2);color:var(--text)}
|
|
64
|
+
.sb-link.active{background:var(--accent-light);color:var(--accent);font-weight:600}
|
|
65
|
+
.sb-link svg{flex-shrink:0;opacity:.7}
|
|
66
|
+
.sb-link.active svg{opacity:1}
|
|
67
|
+
.sb-divider{height:1px;background:var(--border-subtle);margin:10px 12px}
|
|
68
|
+
.sb-footer{padding:16px;display:flex;flex-direction:column;gap:10px}
|
|
69
|
+
.palette-toggle{
|
|
70
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
71
|
+
background:var(--surface-2);border:1px solid var(--border-subtle);
|
|
72
|
+
border-radius:var(--r);padding:8px 12px;
|
|
73
|
+
}
|
|
74
|
+
.palette-toggle-label{font-size:12px;font-weight:500;color:var(--text-2)}
|
|
75
|
+
.palette-toggle-switch{
|
|
76
|
+
display:flex;gap:2px;background:var(--bg);
|
|
77
|
+
border:1px solid var(--border-subtle);border-radius:6px;padding:2px;
|
|
78
|
+
}
|
|
79
|
+
.palette-toggle-opt{
|
|
80
|
+
padding:3px 9px;border-radius:4px;font-size:11px;font-weight:600;
|
|
81
|
+
border:none;background:none;font-family:var(--font);cursor:pointer;
|
|
82
|
+
color:var(--text-3);transition:all .12s;
|
|
83
|
+
}
|
|
84
|
+
.palette-toggle-opt.active{background:var(--surface);color:var(--text);box-shadow:0 1px 3px oklch(0 0 0/.1)}
|
|
85
|
+
.brain-card{
|
|
86
|
+
background:var(--surface-2);border:1px solid var(--border-subtle);
|
|
87
|
+
border-radius:var(--r);padding:12px 14px;
|
|
88
|
+
}
|
|
89
|
+
.brain-card-head{display:flex;align-items:center;gap:8px;margin-bottom:8px}
|
|
90
|
+
.online-dot{
|
|
91
|
+
width:7px;height:7px;border-radius:50%;background:var(--online);
|
|
92
|
+
box-shadow:0 0 6px var(--online);flex-shrink:0;
|
|
93
|
+
animation:blink 2.5s ease-in-out infinite;
|
|
94
|
+
}
|
|
95
|
+
@keyframes blink{0%,100%{opacity:1}50%{opacity:.4}}
|
|
96
|
+
.brain-name{font-size:13px;font-weight:600;color:var(--text)}
|
|
97
|
+
.brain-meta{font-size:11px;color:var(--text-3);line-height:1.7;font-family:var(--mono)}
|
|
98
|
+
.main{margin-left:var(--sb);flex:1;min-width:0}
|
|
99
|
+
.topbar{
|
|
100
|
+
position:sticky;top:0;z-index:40;
|
|
101
|
+
background:oklch(from var(--bg) l c h / .92);backdrop-filter:blur(12px);
|
|
102
|
+
border-bottom:1px solid var(--border-subtle);
|
|
103
|
+
display:flex;align-items:center;gap:12px;padding:0 32px;height:52px;
|
|
104
|
+
}
|
|
105
|
+
.topbar-title{font-size:14px;font-weight:600;color:var(--text);flex:none}
|
|
106
|
+
.topbar-sep{color:var(--border);font-size:16px}
|
|
107
|
+
.search-bar{
|
|
108
|
+
flex:1;max-width:400px;display:flex;align-items:center;gap:8px;
|
|
109
|
+
background:var(--surface);border:1px solid var(--border-subtle);
|
|
110
|
+
border-radius:20px;padding:6px 14px;transition:border-color .15s;
|
|
111
|
+
}
|
|
112
|
+
.search-bar:focus-within{border-color:var(--accent)}
|
|
113
|
+
.search-bar input{flex:1;background:none;border:none;outline:none;font-family:var(--font);font-size:13px;color:var(--text)}
|
|
114
|
+
.search-bar input::placeholder{color:var(--text-3)}
|
|
115
|
+
.page{padding:28px 32px 80px;}
|
|
116
|
+
|
|
117
|
+
/* ── STATS ──────────────────────────────── */
|
|
118
|
+
.stats-row{display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin-bottom:28px}
|
|
119
|
+
.stat-card{
|
|
120
|
+
background:var(--surface);border:1px solid var(--border-subtle);
|
|
121
|
+
border-radius:var(--r);padding:18px 16px 14px;
|
|
122
|
+
transition:border-color .15s,box-shadow .15s;
|
|
123
|
+
}
|
|
124
|
+
.stat-card:hover{border-color:var(--border);box-shadow:0 2px 12px oklch(0 0 0/.06)}
|
|
125
|
+
.stat-num{font-size:28px;font-weight:700;letter-spacing:-.04em;line-height:1;margin-bottom:5px}
|
|
126
|
+
.stat-lbl{font-size:10px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--text-3)}
|
|
127
|
+
|
|
128
|
+
/* ── SECTION HEADERS ────────────────────── */
|
|
129
|
+
.sec-head{display:flex;align-items:center;gap:8px;margin-bottom:12px}
|
|
130
|
+
.sec-title{font-size:11px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--text-3)}
|
|
131
|
+
.sec-badge{
|
|
132
|
+
font-size:11px;font-family:var(--mono);color:var(--text-3);
|
|
133
|
+
background:var(--surface-2);border:1px solid var(--border-subtle);
|
|
134
|
+
padding:1px 8px;border-radius:20px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ── ENTITY GRID ────────────────────────── */
|
|
138
|
+
.entity-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
|
|
139
|
+
.entity-card{
|
|
140
|
+
background:var(--surface);border:1px solid var(--border-subtle);
|
|
141
|
+
border-radius:var(--r);padding:16px;cursor:pointer;
|
|
142
|
+
transition:transform .12s,box-shadow .15s,border-color .15s;
|
|
143
|
+
position:relative;overflow:hidden;
|
|
144
|
+
}
|
|
145
|
+
.entity-card::before{
|
|
146
|
+
content:'';position:absolute;top:0;left:0;right:0;height:3px;
|
|
147
|
+
background:var(--card-accent,var(--accent));
|
|
148
|
+
}
|
|
149
|
+
.entity-card:hover{transform:translateY(-2px);box-shadow:0 8px 24px oklch(0 0 0/.08);border-color:var(--border)}
|
|
150
|
+
.entity-name{font-size:14px;font-weight:600;letter-spacing:-.02em;color:var(--text);margin-bottom:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
151
|
+
.entity-tags{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:10px}
|
|
152
|
+
.entity-footer{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
153
|
+
.tier-pill{
|
|
154
|
+
display:inline-flex;align-items:center;gap:4px;
|
|
155
|
+
font-size:11px;font-family:var(--mono);font-weight:500;
|
|
156
|
+
padding:2px 8px;border-radius:20px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ── BADGE ──────────────────────────────── */
|
|
160
|
+
.badge{
|
|
161
|
+
display:inline-flex;align-items:center;
|
|
162
|
+
padding:2px 7px;border-radius:4px;font-size:10.5px;font-weight:700;
|
|
163
|
+
letter-spacing:.04em;text-transform:uppercase;font-family:var(--mono);
|
|
164
|
+
border:1px solid;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ── CONVERSATIONS ──────────────────────── */
|
|
168
|
+
.conv-filters{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}
|
|
169
|
+
.conv-filter-btn{
|
|
170
|
+
padding:5px 13px;border-radius:20px;border:1px solid var(--border);
|
|
171
|
+
background:none;font-family:var(--font);font-size:12px;font-weight:500;
|
|
172
|
+
color:var(--text-3);cursor:pointer;transition:all .12s;
|
|
173
|
+
}
|
|
174
|
+
.conv-filter-btn:hover{border-color:var(--accent);color:var(--accent)}
|
|
175
|
+
.conv-filter-btn.active{background:var(--accent-light);border-color:var(--accent);color:var(--accent);font-weight:600}
|
|
176
|
+
.conv-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(148px,1fr));gap:8px}
|
|
177
|
+
.conv-card{
|
|
178
|
+
background:var(--surface);border:1px solid var(--border-subtle);
|
|
179
|
+
border-radius:var(--r);padding:12px 14px;cursor:pointer;
|
|
180
|
+
transition:border-color .12s,transform .12s,box-shadow .12s;
|
|
181
|
+
}
|
|
182
|
+
.conv-card:hover{border-color:var(--accent);transform:translateY(-1px);box-shadow:0 4px 14px oklch(0 0 0/.07)}
|
|
183
|
+
.conv-card-date{font-family:var(--mono);font-size:12px;font-weight:700;color:var(--accent);margin-bottom:6px;letter-spacing:-.01em}
|
|
184
|
+
.conv-card-source{margin-bottom:8px}
|
|
185
|
+
.conv-card-meta{font-family:var(--mono);font-size:11px;color:var(--text-3)}
|
|
186
|
+
|
|
187
|
+
/* ── DETAIL ─────────────────────────────── */
|
|
188
|
+
.back-btn{
|
|
189
|
+
display:inline-flex;align-items:center;gap:6px;
|
|
190
|
+
background:none;border:none;color:var(--text-2);
|
|
191
|
+
font-family:var(--font);font-size:13px;font-weight:500;padding:4px 0;
|
|
192
|
+
cursor:pointer;transition:color .12s;
|
|
193
|
+
}
|
|
194
|
+
.back-btn:hover{color:var(--text)}
|
|
195
|
+
.detail-title{font-size:28px;font-weight:700;letter-spacing:-.04em;line-height:1.1;margin-bottom:8px}
|
|
196
|
+
.detail-stat-row{
|
|
197
|
+
display:flex;gap:0;background:var(--surface);border:1px solid var(--border-subtle);
|
|
198
|
+
border-radius:var(--r);overflow:hidden;margin-bottom:28px;
|
|
199
|
+
}
|
|
200
|
+
.detail-stat{padding:16px 24px;flex:1;border-right:1px solid var(--border-subtle)}
|
|
201
|
+
.detail-stat:last-child{border-right:none}
|
|
202
|
+
.detail-stat-val{font-size:22px;font-weight:700;letter-spacing:-.04em;margin-bottom:2px}
|
|
203
|
+
.detail-stat-lbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--text-3)}
|
|
204
|
+
.fact-card{background:var(--surface);border:1px solid var(--border-subtle);border-radius:var(--r);padding:14px 16px;margin-bottom:8px}
|
|
205
|
+
.fact-text{font-size:14px;color:var(--text);line-height:1.6;margin-bottom:8px}
|
|
206
|
+
.fact-meta{display:flex;align-items:center;gap:10px;font-size:11px;color:var(--text-3);font-family:var(--mono)}
|
|
207
|
+
|
|
208
|
+
/* ── MESSAGES ───────────────────────────── */
|
|
209
|
+
.msg{border-radius:var(--r);padding:14px 16px;margin-bottom:10px}
|
|
210
|
+
.msg-user{background:oklch(from var(--accent) l c h /.08);border:1px solid oklch(from var(--accent) l c h /.2)}
|
|
211
|
+
.msg-assistant{background:var(--surface);border:1px solid var(--border-subtle)}
|
|
212
|
+
.msg-role{font-size:10px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;margin-bottom:8px;font-family:var(--mono);color:var(--text-3)}
|
|
213
|
+
.msg-user .msg-role{color:var(--accent)}
|
|
214
|
+
.msg-text{font-size:13px;line-height:1.65;color:var(--text);white-space:pre-wrap;font-family:var(--mono)}
|
|
215
|
+
|
|
216
|
+
/* ── DOCS ───────────────────────────────── */
|
|
217
|
+
.docs-header{margin-bottom:32px}
|
|
218
|
+
.docs-title{font-size:30px;font-weight:700;letter-spacing:-.04em;margin-bottom:8px}
|
|
219
|
+
.docs-sub{font-size:14px;color:var(--text-2);line-height:1.6}
|
|
220
|
+
.docs-sub code{font-family:var(--mono);font-size:12px;background:var(--surface-2);padding:1px 6px;border-radius:4px;color:var(--accent)}
|
|
221
|
+
.meta-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:32px}
|
|
222
|
+
.meta-card{background:var(--surface);border:1px solid var(--border-subtle);border-radius:var(--r);padding:14px 16px}
|
|
223
|
+
.meta-lbl{font-size:10px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--text-3);margin-bottom:5px}
|
|
224
|
+
.meta-val{font-family:var(--mono);font-size:12.5px;color:var(--accent)}
|
|
225
|
+
.doc-group{margin-bottom:36px}
|
|
226
|
+
.doc-group-title{font-size:11px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--text-3);padding-bottom:10px;border-bottom:1px solid var(--border-subtle);margin-bottom:12px}
|
|
227
|
+
.endpoint{background:var(--surface);border:1px solid var(--border-subtle);border-radius:var(--r);padding:14px 16px;margin-bottom:8px;transition:border-color .12s}
|
|
228
|
+
.endpoint:hover{border-color:var(--border)}
|
|
229
|
+
.endpoint-head{display:flex;align-items:center;gap:10px;margin-bottom:6px}
|
|
230
|
+
.endpoint-path{font-family:var(--mono);font-size:13px;color:var(--text)}
|
|
231
|
+
.endpoint-desc{font-size:13px;color:var(--text-2);line-height:1.5}
|
|
232
|
+
.code-block{background:var(--surface);border:1px solid var(--border-subtle);border-radius:var(--r);padding:18px 20px;overflow-x:auto}
|
|
233
|
+
.code-block code{font-family:var(--mono);font-size:12.5px;color:var(--text-2);line-height:1.75;white-space:pre}
|
|
234
|
+
.mcp-card{background:var(--surface);border:1px solid var(--border-subtle);border-radius:var(--r);padding:16px 18px;margin-bottom:10px}
|
|
235
|
+
.mcp-name{font-family:var(--mono);font-size:14px;font-weight:600;color:var(--accent);margin-right:8px}
|
|
236
|
+
.mcp-desc{font-size:13px;color:var(--text-2);line-height:1.5;margin:8px 0 12px}
|
|
237
|
+
.param-row{display:flex;align-items:center;gap:8px;padding:7px 10px;background:var(--surface-2);border-radius:5px;margin-bottom:4px}
|
|
238
|
+
.param-name{font-family:var(--mono);font-size:12px;font-weight:600;color:var(--text);min-width:100px}
|
|
239
|
+
.param-type{font-family:var(--mono);font-size:11px;color:#f59e0b;min-width:55px}
|
|
240
|
+
.param-desc{font-size:12px;color:var(--text-3);flex:1}
|
|
241
|
+
.tier-table{display:flex;flex-direction:column;gap:4px}
|
|
242
|
+
.tier-row{display:flex;align-items:center;gap:16px;padding:12px 16px;background:var(--surface);border:1px solid var(--border-subtle);border-radius:var(--r)}
|
|
243
|
+
|
|
244
|
+
/* ── LOGIN ──────────────────────────────── */
|
|
245
|
+
.login-bg{
|
|
246
|
+
min-height:100vh;display:flex;align-items:center;justify-content:center;
|
|
247
|
+
background:radial-gradient(ellipse 90% 70% at 50% -10%,oklch(.42 .15 252/.18) 0%,var(--bg) 60%);
|
|
248
|
+
position:relative;
|
|
249
|
+
}
|
|
250
|
+
.login-bg::before{
|
|
251
|
+
content:'';position:absolute;inset:0;
|
|
252
|
+
background-image:linear-gradient(var(--border-subtle) 1px,transparent 1px),linear-gradient(90deg,var(--border-subtle) 1px,transparent 1px);
|
|
253
|
+
background-size:36px 36px;opacity:.5;
|
|
254
|
+
}
|
|
255
|
+
.login-card{
|
|
256
|
+
position:relative;z-index:1;background:var(--surface);
|
|
257
|
+
border:1px solid var(--border);border-radius:16px;
|
|
258
|
+
padding:40px 44px;width:400px;text-align:center;
|
|
259
|
+
box-shadow:0 20px 60px oklch(0 0 0/.12),0 0 0 1px oklch(from var(--accent) l c h /.08);
|
|
260
|
+
}
|
|
261
|
+
.login-logo{width:200px;height:auto;margin:0 auto 24px;display:block}
|
|
262
|
+
.login-token-label{font-size:13px;color:var(--text-2);margin-bottom:10px;text-align:left}
|
|
263
|
+
.login-input{
|
|
264
|
+
width:100%;padding:11px 14px;background:var(--surface-2);
|
|
265
|
+
border:1.5px solid var(--border);border-radius:var(--r);
|
|
266
|
+
font-family:var(--mono);font-size:13.5px;color:var(--text);outline:none;
|
|
267
|
+
transition:border-color .15s;margin-bottom:10px;
|
|
268
|
+
}
|
|
269
|
+
.login-input:focus{border-color:var(--accent)}
|
|
270
|
+
.login-input.err{border-color:#e53e3e}
|
|
271
|
+
.login-hint{font-size:12px;color:var(--text-3);margin-bottom:14px}
|
|
272
|
+
.login-err{font-size:12px;color:#e53e3e;margin-bottom:14px}
|
|
273
|
+
|
|
274
|
+
/* ── LOADING ────────────────────────────── */
|
|
275
|
+
.loading{display:flex;align-items:center;justify-content:center;height:60vh;color:var(--text-3);font-size:13px}
|
|
276
|
+
.empty{color:var(--text-3);font-size:13px;padding:32px;text-align:center;background:var(--surface);border-radius:var(--r);border:1px solid var(--border-subtle)}
|
|
277
|
+
</style>
|
|
378
278
|
</head>
|
|
379
279
|
<body>
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
280
|
+
<div id="root"></div>
|
|
281
|
+
<script type="text/babel">
|
|
282
|
+
const { useState, useEffect, useMemo, useCallback } = React;
|
|
283
|
+
|
|
284
|
+
const TOKEN_KEY = 'dbrain_token';
|
|
285
|
+
const THEME_KEY = 'dbrain_theme';
|
|
286
|
+
const API_BASE = `http://${window.location.hostname}:7878`;
|
|
287
|
+
|
|
288
|
+
/* ── PALETTES ─────────────────────────────────────────── */
|
|
289
|
+
const PALETTES = {
|
|
290
|
+
cloud: {'--bg':'oklch(0.965 0.008 248)','--sidebar-bg':'oklch(0.99 0.003 250)','--surface':'oklch(1 0 0)','--surface-2':'oklch(0.95 0.010 248)','--border':'oklch(0.87 0.018 248)','--border-subtle':'oklch(0.92 0.010 248)','--text':'oklch(0.20 0.06 252)','--text-2':'oklch(0.42 0.05 250)','--text-3':'oklch(0.60 0.04 248)','--accent':'oklch(0.50 0.22 252)','--accent-light':'oklch(0.94 0.06 252)','--online':'oklch(0.50 0.18 148)'},
|
|
291
|
+
ocean: {'--bg':'oklch(0.11 0.04 254)','--sidebar-bg':'oklch(0.13 0.046 254)','--surface':'oklch(0.16 0.04 252)','--surface-2':'oklch(0.20 0.04 250)','--border':'oklch(0.26 0.05 250)','--border-subtle':'oklch(0.19 0.04 252)','--text':'oklch(0.93 0.015 245)','--text-2':'oklch(0.65 0.06 245)','--text-3':'oklch(0.47 0.05 248)','--accent':'oklch(0.68 0.20 245)','--accent-light':'oklch(0.68 0.20 245 /.15)','--online':'oklch(0.70 0.18 148)'},
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
function applyPalette(name) {
|
|
295
|
+
const p = PALETTES[name] || PALETTES.cloud;
|
|
296
|
+
Object.entries(p).forEach(([k, v]) => document.documentElement.style.setProperty(k, v));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* ── API HELPER ───────────────────────────────────────── */
|
|
300
|
+
function api(path, opts = {}) {
|
|
301
|
+
const token = localStorage.getItem(TOKEN_KEY) || '';
|
|
302
|
+
return fetch(`${API_BASE}${path}`, {
|
|
303
|
+
...opts,
|
|
304
|
+
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', ...opts.headers },
|
|
305
|
+
}).then(r => { if (!r.ok) throw new Error(r.status); return r.json(); });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* ── ROUTER ───────────────────────────────────────────── */
|
|
309
|
+
function useRouter() {
|
|
310
|
+
const [hash, setHash] = useState(window.location.hash || '#/');
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
const h = () => setHash(window.location.hash || '#/');
|
|
313
|
+
window.addEventListener('hashchange', h);
|
|
314
|
+
return () => window.removeEventListener('hashchange', h);
|
|
315
|
+
}, []);
|
|
316
|
+
const nav = (p) => { window.location.hash = p; };
|
|
317
|
+
const route = hash.replace('#', '') || '/';
|
|
318
|
+
const segs = route.split('/').filter(Boolean);
|
|
319
|
+
return { route, segs, nav };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* ── SHARED COMPONENTS ────────────────────────────────── */
|
|
323
|
+
const TYPE_COLOR = { project: '#3b82f6', system: '#8b5cf6', person: '#10b981', event: '#f59e0b' };
|
|
324
|
+
const SRC_COLOR = { 'openclaw': '#10b981', 'claude-code': '#f59e0b', 'claude-code-mac': '#f59e0b', 'gemini': '#3b82f6', 'cursor': '#8b5cf6' };
|
|
325
|
+
|
|
326
|
+
function Badge({ children, variant = 'default' }) {
|
|
327
|
+
const map = {
|
|
328
|
+
default: { bg: 'oklch(from var(--accent) l c h /.12)', c: 'var(--accent)', b: 'oklch(from var(--accent) l c h /.3)' },
|
|
329
|
+
project: { bg: '#3b82f615', c: '#3b82f6', b: '#3b82f630' },
|
|
330
|
+
system: { bg: '#8b5cf615', c: '#8b5cf6', b: '#8b5cf630' },
|
|
331
|
+
person: { bg: '#10b98115', c: '#10b981', b: '#10b98130' },
|
|
332
|
+
event: { bg: '#f59e0b18', c: '#f59e0b', b: '#f59e0b35' },
|
|
333
|
+
area: { bg: '#06b6d415', c: '#06b6d4', b: '#06b6d430' },
|
|
334
|
+
GET: { bg: '#10b98112', c: '#10b981', b: '#10b98128' },
|
|
335
|
+
POST: { bg: '#3b82f612', c: '#3b82f6', b: '#3b82f628' },
|
|
336
|
+
PUT: { bg: '#f59e0b12', c: '#f59e0b', b: '#f59e0b28' },
|
|
337
|
+
DELETE: { bg: '#ef444412', c: '#ef4444', b: '#ef444428' },
|
|
338
|
+
PATCH: { bg: '#6b728012', c: '#6b7280', b: '#6b728028' },
|
|
339
|
+
primary: { bg: '#3b82f615', c: '#3b82f6', b: '#3b82f630' },
|
|
340
|
+
};
|
|
341
|
+
const s = map[variant] || map.default;
|
|
342
|
+
return <span className="badge" style={{ background: s.bg, color: s.c, borderColor: s.b }}>{children}</span>;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function TierPill({ tier, count }) {
|
|
346
|
+
const cs = { hot: { bg: '#f59e0b18', c: '#f59e0b', dot: '#f59e0b' }, warm: { bg: '#eab30818', c: '#ca8a04', dot: '#eab308' }, cold: { bg: '#3b82f615', c: '#3b82f6', dot: '#3b82f6' } };
|
|
347
|
+
const s = cs[tier] || cs.cold;
|
|
348
|
+
return (
|
|
349
|
+
<span className="tier-pill" style={{ background: s.bg, color: s.c }}>
|
|
350
|
+
<span style={{ width: 5, height: 5, borderRadius: '50%', background: s.dot, display: 'inline-block' }}></span>
|
|
351
|
+
{count != null && count}{count != null && ' '}{tier}
|
|
352
|
+
</span>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function SearchIcon() {
|
|
357
|
+
return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" style={{ color: 'var(--text-3)', flexShrink: 0 }}><circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" /></svg>;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function BackIcon() {
|
|
361
|
+
return <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><polyline points="15 18 9 12 15 6" /></svg>;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* ── SIDEBAR ──────────────────────────────────────────── */
|
|
365
|
+
function Sidebar({ route, nav, palette, onTogglePalette, brain }) {
|
|
366
|
+
const links = [
|
|
367
|
+
{ href: '#/dashboard', label: 'Brain', icon: (
|
|
368
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
|
369
|
+
<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2z" />
|
|
370
|
+
<path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2z" />
|
|
371
|
+
</svg>) },
|
|
372
|
+
{ href: '#/api', label: 'REST API', icon: (
|
|
373
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
|
|
374
|
+
<polyline points="16 18 22 12 16 6" /><polyline points="8 6 2 12 8 18" />
|
|
375
|
+
</svg>) },
|
|
376
|
+
{ href: '#/mcp', label: 'MCP Server', icon: (
|
|
377
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
|
|
378
|
+
<circle cx="12" cy="12" r="3" /><circle cx="4" cy="6" r="2" /><circle cx="20" cy="6" r="2" />
|
|
379
|
+
<circle cx="4" cy="18" r="2" /><circle cx="20" cy="18" r="2" />
|
|
380
|
+
<line x1="6" y1="7" x2="9.5" y2="10.5" /><line x1="18" y1="7" x2="14.5" y2="10.5" />
|
|
381
|
+
<line x1="6" y1="17" x2="9.5" y2="13.5" /><line x1="18" y1="17" x2="14.5" y2="13.5" />
|
|
382
|
+
</svg>) },
|
|
383
|
+
];
|
|
384
|
+
const active = (href) => {
|
|
385
|
+
const p = href.replace('#', '');
|
|
386
|
+
if (p === '/dashboard') return route === '/' || route === '/dashboard' || route.startsWith('/entity') || route.startsWith('/conversation');
|
|
387
|
+
return route.startsWith(p);
|
|
388
|
+
};
|
|
389
|
+
return (
|
|
390
|
+
<aside className="sidebar">
|
|
391
|
+
<div className="sb-logo">
|
|
392
|
+
<img src="/logo-image.png" alt="dbrain" />
|
|
393
|
+
<div className="sb-logo-info">
|
|
394
|
+
<span className="sb-logo-text">dbrain</span>
|
|
395
|
+
<span className="sb-version">v{brain.version}</span>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
<nav className="sb-nav">
|
|
399
|
+
{links.map(l => (
|
|
400
|
+
<button key={l.href} className={`sb-link ${active(l.href) ? 'active' : ''}`} onClick={() => nav(l.href)}>
|
|
401
|
+
{l.icon}{l.label}
|
|
402
|
+
</button>
|
|
403
|
+
))}
|
|
404
|
+
</nav>
|
|
405
|
+
<div className="sb-divider"></div>
|
|
406
|
+
<div className="sb-footer">
|
|
407
|
+
<div className="palette-toggle">
|
|
408
|
+
<span className="palette-toggle-label">Theme</span>
|
|
409
|
+
<div className="palette-toggle-switch">
|
|
410
|
+
<button className={`palette-toggle-opt ${palette === 'cloud' ? 'active' : ''}`} onClick={() => onTogglePalette('cloud')}>☼ Light</button>
|
|
411
|
+
<button className={`palette-toggle-opt ${palette === 'ocean' ? 'active' : ''}`} onClick={() => onTogglePalette('ocean')}>● Dark</button>
|
|
416
412
|
</div>
|
|
417
413
|
</div>
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const [conversations, setConversations] = useState(null);
|
|
429
|
-
const [selectedConvo, setSelectedConvo] = useState(null);
|
|
430
|
-
const [convoDetail, setConvoDetail] = useState(null);
|
|
431
|
-
const [authed, setAuthed] = useState(!!TOKEN);
|
|
432
|
-
|
|
433
|
-
useEffect(() => {
|
|
434
|
-
if (!authed) return;
|
|
435
|
-
api('/health').then(setHealth).catch(() => {});
|
|
436
|
-
api('/memory/summary').then(setOverview).catch(() => {});
|
|
437
|
-
api('/conversations?limit=100').then(setConversations).catch(() => {});
|
|
438
|
-
}, [authed]);
|
|
439
|
-
|
|
440
|
-
useEffect(() => {
|
|
441
|
-
if (!selectedEntity) { setEntityDetail(null); return; }
|
|
442
|
-
api(`/entities/${selectedEntity}`).then(setEntityDetail).catch(() => {});
|
|
443
|
-
}, [selectedEntity]);
|
|
444
|
-
|
|
445
|
-
useEffect(() => {
|
|
446
|
-
if (!selectedConvo) { setConvoDetail(null); return; }
|
|
447
|
-
api(`/conversations/${selectedConvo}`).then(setConvoDetail).catch(() => {});
|
|
448
|
-
}, [selectedConvo]);
|
|
449
|
-
|
|
450
|
-
const doSearch = useCallback(() => {
|
|
451
|
-
if (!searchQuery.trim()) { setSearchResults(null); return; }
|
|
452
|
-
api('/search', { method: 'POST', body: JSON.stringify({ query: searchQuery, limit: 20 }) })
|
|
453
|
-
.then(setSearchResults).catch(() => {});
|
|
454
|
-
}, [searchQuery]);
|
|
455
|
-
|
|
456
|
-
if (!authed) {
|
|
457
|
-
return <TokenPrompt onSave={(t) => { localStorage.setItem('dbrain-token', t); window.location.reload(); }} />;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (!health) return (
|
|
461
|
-
<div className="loading-screen">
|
|
462
|
-
<div className="loading-spinner"></div>
|
|
463
|
-
<div className="loading-text">Connecting to brain...</div>
|
|
414
|
+
<div className="brain-card">
|
|
415
|
+
<div className="brain-card-head">
|
|
416
|
+
<span className="online-dot"></span>
|
|
417
|
+
<span className="brain-name">{brain.name || 'dBrain'}</span>
|
|
418
|
+
</div>
|
|
419
|
+
<div className="brain-meta">
|
|
420
|
+
{brain.entities || 0} entities<br />
|
|
421
|
+
{(brain.facts || 0).toLocaleString()} facts<br />
|
|
422
|
+
{brain.conversations || 0} conversations
|
|
423
|
+
</div>
|
|
464
424
|
</div>
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
425
|
+
</div>
|
|
426
|
+
</aside>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/* ── LOGIN PAGE ───────────────────────────────────────── */
|
|
431
|
+
function LoginPage({ onLogin }) {
|
|
432
|
+
const [token, setToken] = useState('');
|
|
433
|
+
const [err, setErr] = useState('');
|
|
434
|
+
const [loading, setLoading] = useState(false);
|
|
435
|
+
const submit = () => {
|
|
436
|
+
if (!token.trim()) return;
|
|
437
|
+
setLoading(true); setErr('');
|
|
438
|
+
fetch(`${API_BASE}/health`, { headers: { 'Authorization': `Bearer ${token}` } })
|
|
439
|
+
.then(r => { if (!r.ok) throw new Error('Invalid token'); return r.json(); })
|
|
440
|
+
.then(() => onLogin(token))
|
|
441
|
+
.catch(() => { setErr('Invalid token or brain unreachable.'); setLoading(false); });
|
|
442
|
+
};
|
|
443
|
+
return (
|
|
444
|
+
<div className="login-bg">
|
|
445
|
+
<div className="login-card">
|
|
446
|
+
<img src="logo-complete.png" alt="dbrain" className="login-logo" />
|
|
447
|
+
<p className="login-token-label">Access token</p>
|
|
448
|
+
<input className={`login-input${err ? ' err' : ''}`} type="password"
|
|
449
|
+
placeholder="sk-dbr_..." value={token}
|
|
450
|
+
onChange={e => { setToken(e.target.value); setErr(''); }}
|
|
451
|
+
onKeyDown={e => e.key === 'Enter' && submit()} autoFocus spellCheck="false" />
|
|
452
|
+
{err
|
|
453
|
+
? <p className="login-err">{err}</p>
|
|
454
|
+
: <p className="login-hint">{loading ? 'Connecting...' : 'Press Enter to connect'}</p>
|
|
455
|
+
}
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* ── DASHBOARD PAGE ───────────────────────────────────── */
|
|
462
|
+
function DashboardPage({ nav, overview, conversations }) {
|
|
463
|
+
const [q, setQ] = useState('');
|
|
464
|
+
const [srcFilter, setSrcFilter] = useState('all');
|
|
465
|
+
|
|
466
|
+
const entities = overview || [];
|
|
467
|
+
const totalFacts = entities.reduce((s, e) => s + e.total, 0);
|
|
468
|
+
const hotFacts = entities.reduce((s, e) => s + e.hot, 0);
|
|
469
|
+
const warmFacts = entities.reduce((s, e) => s + e.warm, 0);
|
|
470
|
+
const coldFacts = entities.reduce((s, e) => s + e.cold, 0);
|
|
471
|
+
|
|
472
|
+
const filtered = useMemo(() => entities.filter(e =>
|
|
473
|
+
e.name.toLowerCase().includes(q.toLowerCase()) || e.type.toLowerCase().includes(q.toLowerCase())
|
|
474
|
+
), [q, entities]);
|
|
475
|
+
|
|
476
|
+
const sources = useMemo(() => {
|
|
477
|
+
const s = new Set();
|
|
478
|
+
(conversations || []).forEach(c => c.source && s.add(c.source));
|
|
479
|
+
return [...s].sort();
|
|
480
|
+
}, [conversations]);
|
|
481
|
+
|
|
482
|
+
const filteredConvs = useMemo(() =>
|
|
483
|
+
srcFilter === 'all' ? (conversations || []) : (conversations || []).filter(c => c.source === srcFilter)
|
|
484
|
+
, [srcFilter, conversations]);
|
|
485
|
+
|
|
486
|
+
return (
|
|
487
|
+
<>
|
|
488
|
+
<div className="topbar">
|
|
489
|
+
<span className="topbar-title">Overview</span>
|
|
490
|
+
<span className="topbar-sep">·</span>
|
|
491
|
+
<div className="search-bar">
|
|
492
|
+
<SearchIcon />
|
|
493
|
+
<input placeholder="Search entities..." value={q} onChange={e => setQ(e.target.value)} />
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
<div className="page">
|
|
497
|
+
<div className="stats-row">
|
|
498
|
+
{[
|
|
499
|
+
{ l: 'Entities', v: entities.length, c: 'var(--accent)' },
|
|
500
|
+
{ l: 'Total Facts', v: totalFacts.toLocaleString(), c: 'var(--text)' },
|
|
501
|
+
{ l: 'Hot', v: hotFacts, c: '#f59e0b' },
|
|
502
|
+
{ l: 'Warm', v: warmFacts, c: '#eab308' },
|
|
503
|
+
{ l: 'Cold', v: coldFacts.toLocaleString(), c: '#3b82f6' },
|
|
504
|
+
{ l: 'Conversations', v: (conversations || []).length, c: '#10b981' },
|
|
505
|
+
].map(s => (
|
|
506
|
+
<div key={s.l} className="stat-card">
|
|
507
|
+
<div className="stat-num" style={{ color: s.c }}>{s.v}</div>
|
|
508
|
+
<div className="stat-lbl">{s.l}</div>
|
|
487
509
|
</div>
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
510
|
+
))}
|
|
511
|
+
</div>
|
|
512
|
+
|
|
513
|
+
<div className="sec-head"><span className="sec-title">Entities</span><span className="sec-badge">{filtered.length}</span></div>
|
|
514
|
+
<div className="entity-grid" style={{ marginBottom: 36 }}>
|
|
515
|
+
{filtered.map(e => (
|
|
516
|
+
<div key={e.id} className="entity-card" style={{ '--card-accent': TYPE_COLOR[e.type] || 'var(--accent)' }} onClick={() => nav(`#/entity/${e.id}`)}>
|
|
517
|
+
<div className="entity-name">{e.name}</div>
|
|
518
|
+
<div className="entity-tags">
|
|
519
|
+
<Badge variant={e.type}>{e.type}</Badge>
|
|
520
|
+
<Badge variant="area">{e.category}</Badge>
|
|
497
521
|
</div>
|
|
498
|
-
<div className="
|
|
499
|
-
{
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
</div>
|
|
504
|
-
))}
|
|
522
|
+
<div className="entity-footer">
|
|
523
|
+
{e.hot > 0 && <TierPill tier="hot" count={e.hot} />}
|
|
524
|
+
{e.warm > 0 && <TierPill tier="warm" count={e.warm} />}
|
|
525
|
+
{e.cold > 0 && <TierPill tier="cold" count={e.cold} />}
|
|
526
|
+
{e.total === 0 && <TierPill tier="cold" count={0} />}
|
|
505
527
|
</div>
|
|
506
528
|
</div>
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
529
|
+
))}
|
|
530
|
+
{filtered.length === 0 && <div className="empty">No entities found</div>}
|
|
531
|
+
</div>
|
|
532
|
+
|
|
533
|
+
<div className="sec-head" style={{ marginBottom: 10 }}>
|
|
534
|
+
<span className="sec-title">Conversations</span>
|
|
535
|
+
<span className="sec-badge">{filteredConvs.length}</span>
|
|
536
|
+
</div>
|
|
537
|
+
<div className="conv-filters">
|
|
538
|
+
{['all', ...sources].map(src => (
|
|
539
|
+
<button key={src} className={`conv-filter-btn ${srcFilter === src ? 'active' : ''}`}
|
|
540
|
+
onClick={() => setSrcFilter(src)}
|
|
541
|
+
style={srcFilter === src && src !== 'all' ? { background: `${SRC_COLOR[src] || 'var(--accent)'}18`, borderColor: SRC_COLOR[src] || 'var(--accent)', color: SRC_COLOR[src] || 'var(--accent)' } : {}}>
|
|
542
|
+
{src === 'all' ? 'All sources' : src}
|
|
543
|
+
</button>
|
|
544
|
+
))}
|
|
545
|
+
</div>
|
|
546
|
+
<div className="conv-grid">
|
|
547
|
+
{filteredConvs.map(c => {
|
|
548
|
+
const sc = SRC_COLOR[c.source] || 'var(--online)';
|
|
549
|
+
const date = c.started_at ? c.started_at.split('T')[0] : c.id;
|
|
550
|
+
return (
|
|
551
|
+
<div key={c.id} className="conv-card" onClick={() => nav(`#/conversation/${c.id}`)}>
|
|
552
|
+
<div className="conv-card-date">{date}</div>
|
|
553
|
+
<div className="conv-card-source">
|
|
554
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 11, fontWeight: 600, fontFamily: 'var(--mono)', color: sc }}>
|
|
555
|
+
<span style={{ width: 5, height: 5, borderRadius: '50%', background: sc, display: 'inline-block' }}></span>
|
|
556
|
+
{c.source || 'unknown'}
|
|
557
|
+
</span>
|
|
524
558
|
</div>
|
|
559
|
+
<div className="conv-card-meta">{c.message_count || 0} msgs</div>
|
|
525
560
|
</div>
|
|
561
|
+
);
|
|
562
|
+
})}
|
|
563
|
+
{filteredConvs.length === 0 && <div className="empty">No conversations</div>}
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
</>
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* ── ENTITY DETAIL ────────────────────────────────────── */
|
|
571
|
+
function EntityDetailPage({ id, nav }) {
|
|
572
|
+
const [entity, setEntity] = useState(null);
|
|
573
|
+
const [loading, setLoading] = useState(true);
|
|
574
|
+
|
|
575
|
+
useEffect(() => {
|
|
576
|
+
setLoading(true);
|
|
577
|
+
api(`/entities/${id}`).then(setEntity).catch(() => setEntity(null)).finally(() => setLoading(false));
|
|
578
|
+
}, [id]);
|
|
579
|
+
|
|
580
|
+
if (loading) return <><div className="topbar"><span className="topbar-title">Loading...</span></div><div className="loading">Loading entity...</div></>;
|
|
581
|
+
if (!entity) return <><div className="topbar"><button className="back-btn" onClick={() => nav('#/dashboard')}><BackIcon /> Overview</button></div><div className="page"><div className="empty">Entity not found</div></div></>;
|
|
582
|
+
|
|
583
|
+
const facts = entity.facts || [];
|
|
584
|
+
const hot = facts.filter(f => f.tier === 'hot');
|
|
585
|
+
const warm = facts.filter(f => f.tier === 'warm');
|
|
586
|
+
const cold = facts.filter(f => f.tier === 'cold');
|
|
587
|
+
const tiers = [{ t: 'hot', items: hot }, { t: 'warm', items: warm }, { t: 'cold', items: cold }].filter(g => g.items.length);
|
|
588
|
+
|
|
589
|
+
return (
|
|
590
|
+
<>
|
|
591
|
+
<div className="topbar">
|
|
592
|
+
<button className="back-btn" onClick={() => nav('#/dashboard')}><BackIcon /> Overview</button>
|
|
593
|
+
<span className="topbar-sep">·</span>
|
|
594
|
+
<span className="topbar-title">{entity.name}</span>
|
|
595
|
+
</div>
|
|
596
|
+
<div className="page">
|
|
597
|
+
<div style={{ marginBottom: 20, display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
|
598
|
+
<h1 className="detail-title">{entity.name}</h1>
|
|
599
|
+
<Badge variant={entity.type}>{entity.type}</Badge>
|
|
600
|
+
<Badge variant="area">{entity.category}</Badge>
|
|
601
|
+
</div>
|
|
602
|
+
<div className="detail-stat-row">
|
|
603
|
+
{[{ l: 'Total', v: facts.length, c: 'var(--text)' }, { l: 'Hot', v: hot.length, c: '#f59e0b' }, { l: 'Warm', v: warm.length, c: '#eab308' }, { l: 'Cold', v: cold.length, c: '#3b82f6' }].map(s => (
|
|
604
|
+
<div key={s.l} className="detail-stat">
|
|
605
|
+
<div className="detail-stat-val" style={{ color: s.c }}>{s.v}</div>
|
|
606
|
+
<div className="detail-stat-lbl">{s.l}</div>
|
|
526
607
|
</div>
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
<div className="
|
|
534
|
-
|
|
535
|
-
<
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
<div className="fact-row" key={f.id}>
|
|
540
|
-
<span className="fact-text">{f.fact}</span>
|
|
541
|
-
<div className="fact-meta">
|
|
542
|
-
<span className="fact-category">{f.category}</span>
|
|
543
|
-
<span className={`tier ${f.tier}`}>{f.tier}</span>
|
|
544
|
-
<span className="access-count">{f.access_count}x</span>
|
|
545
|
-
</div>
|
|
546
|
-
</div>
|
|
547
|
-
))}
|
|
548
|
-
{(!entityDetail.facts || entityDetail.facts.length === 0) && (
|
|
549
|
-
<div className="empty">No facts recorded for this entity</div>
|
|
550
|
-
)}
|
|
551
|
-
</div>
|
|
552
|
-
</div>
|
|
553
|
-
</>
|
|
554
|
-
);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
return (
|
|
558
|
-
<>
|
|
559
|
-
<div className="header">
|
|
560
|
-
<div className="header-inner">
|
|
561
|
-
<div className="header-left">
|
|
562
|
-
<div className="brain-icon">🧠</div>
|
|
563
|
-
<span className="header-title">{health.name}</span>
|
|
564
|
-
<span className="header-version">v{health.version}</span>
|
|
565
|
-
</div>
|
|
566
|
-
<div className="header-right">
|
|
567
|
-
<span className="pulse"></span>
|
|
568
|
-
<span className="status-text">Online</span>
|
|
608
|
+
))}
|
|
609
|
+
</div>
|
|
610
|
+
{tiers.map(({ t, items }) => (
|
|
611
|
+
<div key={t} style={{ marginBottom: 24 }}>
|
|
612
|
+
<div className="sec-head"><TierPill tier={t} count={items.length} /></div>
|
|
613
|
+
{items.map(f => (
|
|
614
|
+
<div key={f.id} className="fact-card">
|
|
615
|
+
<p className="fact-text">{f.fact}</p>
|
|
616
|
+
<div className="fact-meta">
|
|
617
|
+
<TierPill tier={f.tier} count={null} />
|
|
618
|
+
<span>accessed {f.access_count || 0}x · last {f.last_accessed ? f.last_accessed.split('T')[0] : 'n/a'}</span>
|
|
619
|
+
</div>
|
|
569
620
|
</div>
|
|
570
|
-
|
|
621
|
+
))}
|
|
571
622
|
</div>
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
623
|
+
))}
|
|
624
|
+
{facts.length === 0 && <div className="empty">No facts recorded for this entity</div>}
|
|
625
|
+
</div>
|
|
626
|
+
</>
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/* ── CONVERSATION DETAIL ──────────────────────────────── */
|
|
631
|
+
function ConversationDetailPage({ id, nav }) {
|
|
632
|
+
const [convo, setConvo] = useState(null);
|
|
633
|
+
const [loading, setLoading] = useState(true);
|
|
634
|
+
|
|
635
|
+
useEffect(() => {
|
|
636
|
+
setLoading(true);
|
|
637
|
+
api(`/conversations/${id}`).then(setConvo).catch(() => setConvo(null)).finally(() => setLoading(false));
|
|
638
|
+
}, [id]);
|
|
639
|
+
|
|
640
|
+
if (loading) return <><div className="topbar"><span className="topbar-title">Loading...</span></div><div className="loading">Loading conversation...</div></>;
|
|
641
|
+
if (!convo) return <><div className="topbar"><button className="back-btn" onClick={() => nav('#/dashboard')}><BackIcon /> Overview</button></div><div className="page"><div className="empty">Conversation not found</div></div></>;
|
|
642
|
+
|
|
643
|
+
const date = convo.started_at ? convo.started_at.split('T')[0] : convo.id;
|
|
644
|
+
const msgs = convo.messages || [];
|
|
645
|
+
|
|
646
|
+
return (
|
|
647
|
+
<>
|
|
648
|
+
<div className="topbar">
|
|
649
|
+
<button className="back-btn" onClick={() => nav('#/dashboard')}><BackIcon /> Overview</button>
|
|
650
|
+
<span className="topbar-sep">·</span>
|
|
651
|
+
<span className="topbar-title">{date}</span>
|
|
652
|
+
<Badge>{convo.source || 'unknown'}</Badge>
|
|
653
|
+
</div>
|
|
654
|
+
<div className="page">
|
|
655
|
+
<div style={{ marginBottom: 24, display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
656
|
+
<h1 className="detail-title">{date}</h1>
|
|
657
|
+
<Badge>{convo.source || 'unknown'}</Badge>
|
|
658
|
+
</div>
|
|
659
|
+
<p style={{ fontSize: 12, color: 'var(--text-3)', marginBottom: 24, fontFamily: 'var(--mono)' }}>{msgs.length} messages</p>
|
|
660
|
+
{msgs.map((m, i) => (
|
|
661
|
+
<div key={m.id || i} className={`msg msg-${m.role}`}>
|
|
662
|
+
<div className="msg-role">{m.role}</div>
|
|
663
|
+
<div className="msg-text">{m.content}</div>
|
|
664
|
+
</div>
|
|
665
|
+
))}
|
|
666
|
+
{msgs.length === 0 && <div className="empty">No messages</div>}
|
|
667
|
+
</div>
|
|
668
|
+
</>
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/* ── API DOCS PAGE ────────────────────────────────────── */
|
|
673
|
+
const API_GROUPS = [
|
|
674
|
+
{ g: 'Health & Config', eps: [
|
|
675
|
+
{ m: 'GET', p: '/health', d: 'Brain pulse check. No auth required. Returns status and entity count.' },
|
|
676
|
+
{ m: 'GET', p: '/connect', d: 'Client config: MCP URL, permissions, and CLAUDE.md instructions.' },
|
|
677
|
+
]},
|
|
678
|
+
{ g: 'Workspace (Identity)', eps: [
|
|
679
|
+
{ m: 'GET', p: '/workspace', d: 'List all identity documents.' },
|
|
680
|
+
{ m: 'GET', p: '/workspace/:key', d: 'Read an identity document (MEMORY.md, PARA structure, etc).' },
|
|
681
|
+
{ m: 'PUT', p: '/workspace/:key', d: 'Create or update an identity document.' },
|
|
682
|
+
{ m: 'DELETE', p: '/workspace/:key', d: 'Remove an identity document.' },
|
|
683
|
+
]},
|
|
684
|
+
{ g: 'Entities & Facts', eps: [
|
|
685
|
+
{ m: 'GET', p: '/entities', d: 'List all entities.' },
|
|
686
|
+
{ m: 'GET', p: '/entities/:id', d: 'Entity with all facts, tiers, and metadata.' },
|
|
687
|
+
{ m: 'POST', p: '/entities', d: 'Create a new entity.' },
|
|
688
|
+
{ m: 'DELETE', p: '/entities/:id', d: 'Archive entity.' },
|
|
689
|
+
{ m: 'GET', p: '/entities/:id/facts', d: 'Facts for an entity.' },
|
|
690
|
+
{ m: 'POST', p: '/entities/:id/facts', d: 'Add facts to an entity. Auto-tiered by access pattern.' },
|
|
691
|
+
{ m: 'PATCH', p: '/facts/:id/access', d: 'Bump a memory — resets decay, increments access count.' },
|
|
692
|
+
]},
|
|
693
|
+
{ g: 'Conversations', eps: [
|
|
694
|
+
{ m: 'GET', p: '/conversations', d: 'List all stored conversations.' },
|
|
695
|
+
{ m: 'GET', p: '/conversations/:id', d: 'Full conversation with messages.' },
|
|
696
|
+
{ m: 'POST', p: '/conversations', d: 'Start a new conversation.' },
|
|
697
|
+
{ m: 'POST', p: '/conversations/:id/messages', d: 'Send messages (single or batch).' },
|
|
698
|
+
{ m: 'GET', p: '/conversations/:id/messages', d: 'List messages (filter by ?processed=).' },
|
|
699
|
+
{ m: 'GET', p: '/conversations/pending', d: 'Unprocessed messages overview.' },
|
|
700
|
+
]},
|
|
701
|
+
{ g: 'Search & Memory', eps: [
|
|
702
|
+
{ m: 'POST', p: '/search', d: 'Full-text search (FTS5) over all facts. Bumps matched facts to hot.' },
|
|
703
|
+
{ m: 'GET', p: '/memory/summary', d: 'Overview: entity and fact counts per tier.' },
|
|
704
|
+
]},
|
|
705
|
+
];
|
|
706
|
+
|
|
707
|
+
function APIDocsPage() {
|
|
708
|
+
return (
|
|
709
|
+
<>
|
|
710
|
+
<div className="topbar"><span className="topbar-title">REST API</span></div>
|
|
711
|
+
<div className="page" style={{ maxWidth: 'none' }}>
|
|
712
|
+
<div className="docs-header">
|
|
713
|
+
<h1 className="docs-title">REST API</h1>
|
|
714
|
+
<p className="docs-sub">All endpoints require <code>Authorization: Bearer <token></code> except <code>/health</code>.</p>
|
|
715
|
+
</div>
|
|
716
|
+
<div className="meta-grid">
|
|
717
|
+
{[{ l: 'Base URL', v: API_BASE }, { l: 'Auth', v: 'Authorization: Bearer sk-dbr_...' }, { l: 'Format', v: 'application/json' }].map(m => (
|
|
718
|
+
<div key={m.l} className="meta-card"><div className="meta-lbl">{m.l}</div><code className="meta-val">{m.v}</code></div>
|
|
719
|
+
))}
|
|
720
|
+
</div>
|
|
721
|
+
{API_GROUPS.map(({ g, eps }) => (
|
|
722
|
+
<div key={g} className="doc-group">
|
|
723
|
+
<div className="doc-group-title">{g}</div>
|
|
724
|
+
{eps.map((ep, i) => (
|
|
725
|
+
<div key={i} className="endpoint">
|
|
726
|
+
<div className="endpoint-head"><Badge variant={ep.m}>{ep.m}</Badge><code className="endpoint-path">{ep.p}</code></div>
|
|
727
|
+
<p className="endpoint-desc">{ep.d}</p>
|
|
598
728
|
</div>
|
|
599
|
-
|
|
729
|
+
))}
|
|
730
|
+
</div>
|
|
731
|
+
))}
|
|
732
|
+
<div className="doc-group">
|
|
733
|
+
<div className="doc-group-title">Example — search</div>
|
|
734
|
+
<div className="code-block"><code>{`curl -X POST ${API_BASE}/search \\
|
|
735
|
+
-H "Authorization: Bearer sk-dbr_..." \\
|
|
736
|
+
-H "Content-Type: application/json" \\
|
|
737
|
+
-d '{"query": "typescript react stack"}'
|
|
738
|
+
|
|
739
|
+
# Response
|
|
740
|
+
{
|
|
741
|
+
"results": [{
|
|
742
|
+
"entity": "ivan-campillo",
|
|
743
|
+
"fact": "Stack habitual: React, Next.js, Fastify, SQLite",
|
|
744
|
+
"tier": "warm",
|
|
745
|
+
"score": 0.92,
|
|
746
|
+
"accessCount": 5
|
|
747
|
+
}]
|
|
748
|
+
}`}</code></div>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
</>
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/* ── MCP DOCS PAGE ────────────────────────────────────── */
|
|
756
|
+
const MCP_TOOLS = [
|
|
757
|
+
{ name: 'recall', primary: true, desc: 'Primary tool. Searches memory and loads identity context. Called at session start.', params: [{ n: 'query', t: 'string', req: true, d: 'What to look for' }, { n: 'limit', t: 'number', req: false, d: 'Max results (default 10)' }] },
|
|
758
|
+
{ name: 'remember', primary: false, desc: 'Save a fact to an entity. Auto-tiered, FTS-indexed.', params: [{ n: 'entityId', t: 'string', req: true, d: 'Entity ID' }, { n: 'fact', t: 'string', req: true, d: 'Fact to store' }, { n: 'category', t: 'string', req: false, d: 'context / milestone / status / preference / relationship' }] },
|
|
759
|
+
{ name: 'get_entity', primary: false, desc: 'Read full entity with facts, tiers, and access history.', params: [{ n: 'id', t: 'string', req: true, d: 'Entity ID' }] },
|
|
760
|
+
{ name: 'list_entities', primary: false, desc: 'List entities by type or PARA category.', params: [{ n: 'type', t: 'string', req: false, d: 'project / person / system / event' }, { n: 'category', t: 'string', req: false, d: 'PARA category' }] },
|
|
761
|
+
{ name: 'create_entity', primary: false, desc: 'Create a new entity in the knowledge graph.', params: [{ n: 'id', t: 'string', req: true, d: 'Unique identifier' }, { n: 'name', t: 'string', req: true, d: 'Display name' }, { n: 'type', t: 'string', req: true, d: 'Entity type' }, { n: 'category', t: 'string', req: true, d: 'PARA category' }] },
|
|
762
|
+
{ name: 'bump', primary: false, desc: 'Touch a fact to keep it hot and reset its decay timer.', params: [{ n: 'factId', t: 'string', req: true, d: 'Fact ID to bump' }] },
|
|
763
|
+
{ name: 'log', primary: false, desc: 'Persist conversation messages.', params: [{ n: 'source', t: 'string', req: true, d: 'Client name' }, { n: 'messages', t: 'array', req: true, d: '[{role,content}] array' }, { n: 'conversationId', t: 'string', req: false, d: 'Existing conversation ID' }] },
|
|
764
|
+
{ name: 'wake_up', primary: false, desc: 'Full identity load — all workspace docs in one call.', params: [] },
|
|
765
|
+
{ name: 'overview', primary: false, desc: 'Brain stats: entities, facts, tier counts.', params: [] },
|
|
766
|
+
];
|
|
767
|
+
|
|
768
|
+
function MCPDocsPage() {
|
|
769
|
+
return (
|
|
770
|
+
<>
|
|
771
|
+
<div className="topbar"><span className="topbar-title">MCP Server</span></div>
|
|
772
|
+
<div className="page" style={{ maxWidth: 'none' }}>
|
|
773
|
+
<div className="docs-header">
|
|
774
|
+
<h1 className="docs-title">MCP Server</h1>
|
|
775
|
+
<p className="docs-sub">Connect any MCP-compatible client (Claude Code, Gemini, custom) to your brain.</p>
|
|
776
|
+
</div>
|
|
777
|
+
<div className="meta-grid">
|
|
778
|
+
{[{ l: 'Transport', v: 'Streamable HTTP' }, { l: 'Endpoint', v: `${API_BASE}/mcp` }, { l: 'SDK', v: '@modelcontextprotocol/sdk' }].map(m => (
|
|
779
|
+
<div key={m.l} className="meta-card"><div className="meta-lbl">{m.l}</div><code className="meta-val">{m.v}</code></div>
|
|
780
|
+
))}
|
|
781
|
+
</div>
|
|
782
|
+
<div className="doc-group">
|
|
783
|
+
<div className="doc-group-title">Quick Connect</div>
|
|
784
|
+
<div className="code-block"><code>{`# Interactive wizard
|
|
785
|
+
dbrain connect claude
|
|
600
786
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
<input
|
|
604
|
-
placeholder="Search memories..."
|
|
605
|
-
value={searchQuery}
|
|
606
|
-
onChange={e => setSearchQuery(e.target.value)}
|
|
607
|
-
onKeyDown={e => e.key === 'Enter' && doSearch()}
|
|
608
|
-
/>
|
|
609
|
-
</div>
|
|
787
|
+
# Or with explicit params
|
|
788
|
+
dbrain connect claude http://server:7878 --token=sk-dbr_...
|
|
610
789
|
|
|
611
|
-
|
|
612
|
-
<div className="results">
|
|
613
|
-
<div className="section-title">
|
|
614
|
-
Results
|
|
615
|
-
<span className="count">{searchResults.length}</span>
|
|
616
|
-
</div>
|
|
617
|
-
{searchResults.length === 0 && <div className="empty">No memories found</div>}
|
|
618
|
-
{searchResults.map((r, i) => (
|
|
619
|
-
<div className="result-item" key={i}>
|
|
620
|
-
<div className="fact">{r.fact.fact}</div>
|
|
621
|
-
<div className="meta">
|
|
622
|
-
<span className="entity-tag">{r.entity.name}</span>
|
|
623
|
-
<span className={`tier ${r.fact.tier}`}>{r.fact.tier}</span>
|
|
624
|
-
</div>
|
|
625
|
-
</div>
|
|
626
|
-
))}
|
|
627
|
-
</div>
|
|
628
|
-
)}
|
|
790
|
+
# Supported clients: claude, opencode (coming soon)
|
|
629
791
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
{e.warm > 0 && <span className="tier warm">{e.warm} warm</span>}
|
|
643
|
-
{e.cold > 0 && <span className="tier cold">{e.cold} cold</span>}
|
|
644
|
-
{e.total === 0 && <span className="tier empty-tier">empty</span>}
|
|
645
|
-
</div>
|
|
646
|
-
</div>
|
|
647
|
-
))}
|
|
792
|
+
# Automatically updates:
|
|
793
|
+
# ~/.claude.json MCP registered
|
|
794
|
+
# ~/.claude/settings.json Permissions
|
|
795
|
+
# ~/.claude/CLAUDE.md Behavioral instructions`}</code></div>
|
|
796
|
+
</div>
|
|
797
|
+
<div className="doc-group">
|
|
798
|
+
<div className="doc-group-title">Available Tools</div>
|
|
799
|
+
{MCP_TOOLS.map(tool => (
|
|
800
|
+
<div key={tool.name} className="mcp-card">
|
|
801
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
|
802
|
+
<span className="mcp-name">{tool.name}</span>
|
|
803
|
+
{tool.primary && <Badge variant="primary">primary</Badge>}
|
|
648
804
|
</div>
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
<span className="
|
|
656
|
-
</div>
|
|
657
|
-
<div className="convos-list">
|
|
658
|
-
{conversations.map(c => (
|
|
659
|
-
<div className="conv-row" key={c.id} onClick={() => setSelectedConvo(c.id)}>
|
|
660
|
-
<span className="conv-date">{c.started_at?.split('T')[0]}</span>
|
|
661
|
-
<span className="conv-summary">{c.summary || c.id}</span>
|
|
662
|
-
<span className="conv-source">{c.source}</span>
|
|
663
|
-
</div>
|
|
664
|
-
))}
|
|
805
|
+
<p className="mcp-desc">{tool.desc}</p>
|
|
806
|
+
{tool.params.length > 0 && tool.params.map(p => (
|
|
807
|
+
<div key={p.n} className="param-row">
|
|
808
|
+
<span className="param-name">{p.n}</span>
|
|
809
|
+
<Badge variant={p.req ? 'POST' : 'PATCH'}>{p.req ? 'required' : 'optional'}</Badge>
|
|
810
|
+
<span className="param-type">{p.t}</span>
|
|
811
|
+
<span className="param-desc">{p.d}</span>
|
|
665
812
|
</div>
|
|
813
|
+
))}
|
|
814
|
+
</div>
|
|
815
|
+
))}
|
|
816
|
+
</div>
|
|
817
|
+
<div className="doc-group">
|
|
818
|
+
<div className="doc-group-title">Memory Tiers</div>
|
|
819
|
+
<div className="tier-table">
|
|
820
|
+
{[{ t: 'hot', r: 'Accessed in last 7 days, or accessCount ≥ 10', c: '#f59e0b' }, { t: 'warm', r: '8–30 days since last access', c: '#eab308' }, { t: 'cold', r: '> 30 days — fading, candidate for archival', c: '#3b82f6' }].map(({ t, r, c }) => (
|
|
821
|
+
<div key={t} className="tier-row">
|
|
822
|
+
<span style={{ fontFamily: 'var(--mono)', fontWeight: 700, color: c, minWidth: 46 }}>{t}</span>
|
|
823
|
+
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>{r}</span>
|
|
666
824
|
</div>
|
|
667
|
-
)}
|
|
825
|
+
))}
|
|
668
826
|
</div>
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
827
|
+
</div>
|
|
828
|
+
</div>
|
|
829
|
+
</>
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/* ── APP ──────────────────────────────────────────────── */
|
|
834
|
+
function App() {
|
|
835
|
+
const { route, segs, nav } = useRouter();
|
|
836
|
+
const [token, setToken] = useState(() => localStorage.getItem(TOKEN_KEY) || '');
|
|
837
|
+
const [palette, setPalette] = useState(() => localStorage.getItem(THEME_KEY) || 'cloud');
|
|
838
|
+
const [health, setHealth] = useState(null);
|
|
839
|
+
const [overview, setOverview] = useState(null);
|
|
840
|
+
const [conversations, setConversations] = useState(null);
|
|
841
|
+
const authed = Boolean(token);
|
|
842
|
+
|
|
843
|
+
const togglePalette = (p) => {
|
|
844
|
+
setPalette(p);
|
|
845
|
+
localStorage.setItem(THEME_KEY, p);
|
|
846
|
+
applyPalette(p);
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
useEffect(() => { applyPalette(palette); }, []);
|
|
850
|
+
|
|
851
|
+
useEffect(() => {
|
|
852
|
+
if (!authed) return;
|
|
853
|
+
api('/health').then(setHealth).catch(() => { localStorage.removeItem(TOKEN_KEY); setToken(''); });
|
|
854
|
+
api('/memory/summary').then(setOverview).catch(() => {});
|
|
855
|
+
api('/conversations?limit=200').then(setConversations).catch(() => {});
|
|
856
|
+
}, [authed]);
|
|
857
|
+
|
|
858
|
+
useEffect(() => {
|
|
859
|
+
if (!authed && route !== '/login') nav('#/login');
|
|
860
|
+
if (authed && (route === '/' || route === '/login')) nav('#/dashboard');
|
|
861
|
+
}, [authed]);
|
|
862
|
+
|
|
863
|
+
if (!authed) return <LoginPage onLogin={tk => { localStorage.setItem(TOKEN_KEY, tk); setToken(tk); nav('#/dashboard'); }} />;
|
|
864
|
+
|
|
865
|
+
if (!health) return <div className="loading">Connecting to brain...</div>;
|
|
866
|
+
|
|
867
|
+
const brain = {
|
|
868
|
+
name: health.name || 'dBrain',
|
|
869
|
+
version: health.version || '0.1.0',
|
|
870
|
+
entities: health.entities || 0,
|
|
871
|
+
facts: overview ? overview.reduce((s, e) => s + e.total, 0) : (health.facts || 0),
|
|
872
|
+
conversations: health.conversations || 0,
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
const renderPage = () => {
|
|
876
|
+
const [sec, ...rest] = segs;
|
|
877
|
+
if (sec === 'entity') return <EntityDetailPage id={rest[0]} nav={nav} />;
|
|
878
|
+
if (sec === 'conversation') return <ConversationDetailPage id={rest[0]} nav={nav} />;
|
|
879
|
+
if (sec === 'api') return <APIDocsPage />;
|
|
880
|
+
if (sec === 'mcp') return <MCPDocsPage />;
|
|
881
|
+
return <DashboardPage nav={nav} overview={overview} conversations={conversations} />;
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
return (
|
|
885
|
+
<div className="shell">
|
|
886
|
+
<Sidebar route={route} nav={nav} palette={palette} onTogglePalette={togglePalette} brain={brain} />
|
|
887
|
+
<div className="main">{renderPage()}</div>
|
|
888
|
+
</div>
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
|
893
|
+
</script>
|
|
675
894
|
</body>
|
|
676
895
|
</html>
|