dbrain 0.1.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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +205 -0
  3. package/dist/cli/connect.d.ts +2 -0
  4. package/dist/cli/connect.d.ts.map +1 -0
  5. package/dist/cli/connect.js +108 -0
  6. package/dist/cli/connect.js.map +1 -0
  7. package/dist/cli/index.d.ts +3 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli/index.js +46 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/cli/init.d.ts +4 -0
  12. package/dist/cli/init.d.ts.map +1 -0
  13. package/dist/cli/init.js +177 -0
  14. package/dist/cli/init.js.map +1 -0
  15. package/dist/cli/start.d.ts +2 -0
  16. package/dist/cli/start.d.ts.map +1 -0
  17. package/dist/cli/start.js +33 -0
  18. package/dist/cli/start.js.map +1 -0
  19. package/dist/cli/status.d.ts +2 -0
  20. package/dist/cli/status.d.ts.map +1 -0
  21. package/dist/cli/status.js +30 -0
  22. package/dist/cli/status.js.map +1 -0
  23. package/dist/core/config.d.ts +15 -0
  24. package/dist/core/config.d.ts.map +1 -0
  25. package/dist/core/config.js +24 -0
  26. package/dist/core/config.js.map +1 -0
  27. package/dist/core/db.d.ts +4 -0
  28. package/dist/core/db.d.ts.map +1 -0
  29. package/dist/core/db.js +90 -0
  30. package/dist/core/db.js.map +1 -0
  31. package/dist/core/memory.d.ts +4 -0
  32. package/dist/core/memory.d.ts.map +1 -0
  33. package/dist/core/memory.js +9 -0
  34. package/dist/core/memory.js.map +1 -0
  35. package/dist/core/models.d.ts +67 -0
  36. package/dist/core/models.d.ts.map +1 -0
  37. package/dist/core/models.js +29 -0
  38. package/dist/core/models.js.map +1 -0
  39. package/dist/dashboard/index.html +676 -0
  40. package/dist/dashboard/server.d.ts +2 -0
  41. package/dist/dashboard/server.d.ts.map +1 -0
  42. package/dist/dashboard/server.js +20 -0
  43. package/dist/dashboard/server.js.map +1 -0
  44. package/dist/mcp/server.d.ts +5 -0
  45. package/dist/mcp/server.d.ts.map +1 -0
  46. package/dist/mcp/server.js +386 -0
  47. package/dist/mcp/server.js.map +1 -0
  48. package/dist/server/index.d.ts +7 -0
  49. package/dist/server/index.d.ts.map +1 -0
  50. package/dist/server/index.js +41 -0
  51. package/dist/server/index.js.map +1 -0
  52. package/dist/server/routes/conversations.d.ts +3 -0
  53. package/dist/server/routes/conversations.d.ts.map +1 -0
  54. package/dist/server/routes/conversations.js +86 -0
  55. package/dist/server/routes/conversations.js.map +1 -0
  56. package/dist/server/routes/entities.d.ts +3 -0
  57. package/dist/server/routes/entities.d.ts.map +1 -0
  58. package/dist/server/routes/entities.js +51 -0
  59. package/dist/server/routes/entities.js.map +1 -0
  60. package/dist/server/routes/facts.d.ts +3 -0
  61. package/dist/server/routes/facts.d.ts.map +1 -0
  62. package/dist/server/routes/facts.js +41 -0
  63. package/dist/server/routes/facts.js.map +1 -0
  64. package/dist/server/routes/health.d.ts +3 -0
  65. package/dist/server/routes/health.d.ts.map +1 -0
  66. package/dist/server/routes/health.js +72 -0
  67. package/dist/server/routes/health.js.map +1 -0
  68. package/dist/server/routes/search.d.ts +3 -0
  69. package/dist/server/routes/search.d.ts.map +1 -0
  70. package/dist/server/routes/search.js +60 -0
  71. package/dist/server/routes/search.js.map +1 -0
  72. package/dist/server/routes/workspace.d.ts +3 -0
  73. package/dist/server/routes/workspace.d.ts.map +1 -0
  74. package/dist/server/routes/workspace.js +34 -0
  75. package/dist/server/routes/workspace.js.map +1 -0
  76. package/package.json +82 -0
@@ -0,0 +1,676 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>dbrain — Dashboard</title>
7
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
8
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
9
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
10
+ <style>
11
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
12
+
13
+ * { margin: 0; padding: 0; box-sizing: border-box; }
14
+ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; background: #f0f2f5; color: #1a1a2e; min-height: 100vh; }
15
+
16
+ .header {
17
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
18
+ padding: 28px 32px 24px;
19
+ color: #fff;
20
+ position: sticky;
21
+ top: 0;
22
+ z-index: 100;
23
+ box-shadow: 0 4px 24px rgba(0,0,0,0.15);
24
+ }
25
+ .header-inner {
26
+ max-width: 1280px;
27
+ margin: 0 auto;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: space-between;
31
+ }
32
+ .header-left { display: flex; align-items: center; gap: 14px; }
33
+ .brain-icon {
34
+ width: 42px; height: 42px;
35
+ background: linear-gradient(135deg, #e94560 0%, #ff6b6b 100%);
36
+ border-radius: 12px;
37
+ display: flex; align-items: center; justify-content: center;
38
+ font-size: 22px;
39
+ box-shadow: 0 4px 12px rgba(233,69,96,0.4);
40
+ }
41
+ .header-title { font-size: 22px; font-weight: 700; letter-spacing: -0.5px; }
42
+ .header-version {
43
+ font-size: 12px;
44
+ padding: 3px 10px;
45
+ background: rgba(255,255,255,0.12);
46
+ border-radius: 20px;
47
+ font-weight: 500;
48
+ letter-spacing: 0.3px;
49
+ }
50
+ .header-right { display: flex; align-items: center; gap: 12px; }
51
+ .pulse {
52
+ width: 10px; height: 10px; border-radius: 50%;
53
+ background: #22c55e;
54
+ box-shadow: 0 0 0 0 rgba(34,197,94,0.5);
55
+ animation: pulse 2s ease-in-out infinite;
56
+ }
57
+ @keyframes pulse {
58
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(34,197,94,0.5); }
59
+ 50% { box-shadow: 0 0 0 8px rgba(34,197,94,0); }
60
+ }
61
+ .status-text { font-size: 13px; color: rgba(255,255,255,0.7); font-weight: 500; }
62
+
63
+ .container { max-width: 1280px; margin: 0 auto; padding: 28px 32px; }
64
+
65
+ .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 14px; margin-bottom: 28px; }
66
+ .stat {
67
+ background: #fff;
68
+ border-radius: 16px;
69
+ padding: 22px 20px;
70
+ position: relative;
71
+ overflow: hidden;
72
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 4px 12px rgba(0,0,0,0.04);
73
+ transition: transform 0.2s, box-shadow 0.2s;
74
+ }
75
+ .stat:hover { transform: translateY(-2px); box-shadow: 0 4px 20px rgba(0,0,0,0.1); }
76
+ .stat::before {
77
+ content: '';
78
+ position: absolute;
79
+ top: 0; left: 0; right: 0;
80
+ height: 3px;
81
+ }
82
+ .stat.entities::before { background: linear-gradient(90deg, #6366f1, #8b5cf6); }
83
+ .stat.total::before { background: linear-gradient(90deg, #1a1a2e, #0f3460); }
84
+ .stat.hot::before { background: linear-gradient(90deg, #ef4444, #f97316); }
85
+ .stat.warm::before { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
86
+ .stat.cold::before { background: linear-gradient(90deg, #3b82f6, #60a5fa); }
87
+ .stat.convos::before { background: linear-gradient(90deg, #10b981, #34d399); }
88
+ .stat .value { font-size: 34px; font-weight: 800; letter-spacing: -1px; line-height: 1; }
89
+ .stat.entities .value { color: #6366f1; }
90
+ .stat.total .value { color: #1a1a2e; }
91
+ .stat.hot .value { color: #ef4444; }
92
+ .stat.warm .value { color: #f59e0b; }
93
+ .stat.cold .value { color: #3b82f6; }
94
+ .stat.convos .value { color: #10b981; }
95
+ .stat .label { font-size: 12px; color: #9ca3af; margin-top: 6px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; }
96
+
97
+ .search-box { margin-bottom: 28px; position: relative; }
98
+ .search-box input {
99
+ width: 100%; padding: 14px 20px 14px 48px;
100
+ background: #fff;
101
+ border: 2px solid #e5e7eb;
102
+ border-radius: 14px;
103
+ color: #1a1a2e;
104
+ font-size: 15px;
105
+ font-family: inherit;
106
+ outline: none;
107
+ transition: border-color 0.2s, box-shadow 0.2s;
108
+ box-shadow: 0 1px 3px rgba(0,0,0,0.04);
109
+ }
110
+ .search-box input:focus {
111
+ border-color: #6366f1;
112
+ box-shadow: 0 0 0 4px rgba(99,102,241,0.1), 0 4px 12px rgba(0,0,0,0.06);
113
+ }
114
+ .search-box input::placeholder { color: #b0b4c0; }
115
+ .search-icon {
116
+ position: absolute;
117
+ left: 16px;
118
+ top: 50%;
119
+ transform: translateY(-50%);
120
+ font-size: 18px;
121
+ color: #9ca3af;
122
+ pointer-events: none;
123
+ }
124
+
125
+ .section-title {
126
+ font-size: 13px;
127
+ font-weight: 700;
128
+ color: #9ca3af;
129
+ text-transform: uppercase;
130
+ letter-spacing: 1.2px;
131
+ margin-bottom: 14px;
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 8px;
135
+ }
136
+ .section-title .count {
137
+ background: #e5e7eb;
138
+ color: #6b7280;
139
+ font-size: 11px;
140
+ padding: 2px 8px;
141
+ border-radius: 10px;
142
+ font-weight: 600;
143
+ }
144
+
145
+ .results { margin-bottom: 28px; }
146
+ .result-item {
147
+ background: #fff;
148
+ border-radius: 12px;
149
+ padding: 16px 20px;
150
+ margin-bottom: 8px;
151
+ display: flex;
152
+ justify-content: space-between;
153
+ align-items: flex-start;
154
+ gap: 16px;
155
+ box-shadow: 0 1px 3px rgba(0,0,0,0.04);
156
+ border-left: 3px solid #6366f1;
157
+ transition: transform 0.15s;
158
+ }
159
+ .result-item:hover { transform: translateX(4px); }
160
+ .result-item .fact { flex: 1; font-size: 14px; line-height: 1.6; color: #374151; }
161
+ .result-item .meta { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
162
+ .entity-tag {
163
+ font-size: 11px;
164
+ padding: 3px 10px;
165
+ border-radius: 6px;
166
+ background: linear-gradient(135deg, #eff6ff, #e0e7ff);
167
+ color: #4f46e5;
168
+ font-weight: 600;
169
+ white-space: nowrap;
170
+ }
171
+
172
+ .entities-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 12px; margin-bottom: 32px; }
173
+ .entity-card {
174
+ background: #fff;
175
+ border-radius: 14px;
176
+ padding: 20px;
177
+ cursor: pointer;
178
+ transition: all 0.2s;
179
+ box-shadow: 0 1px 3px rgba(0,0,0,0.04);
180
+ border: 1px solid transparent;
181
+ position: relative;
182
+ overflow: hidden;
183
+ }
184
+ .entity-card::before {
185
+ content: '';
186
+ position: absolute;
187
+ top: 0; left: 0; bottom: 0;
188
+ width: 4px;
189
+ border-radius: 4px 0 0 4px;
190
+ }
191
+ .entity-card.project::before { background: linear-gradient(180deg, #6366f1, #8b5cf6); }
192
+ .entity-card.area::before { background: linear-gradient(180deg, #10b981, #34d399); }
193
+ .entity-card.resource::before { background: linear-gradient(180deg, #f59e0b, #fbbf24); }
194
+ .entity-card.archive::before { background: linear-gradient(180deg, #9ca3af, #d1d5db); }
195
+ .entity-card:hover {
196
+ border-color: #c7d2fe;
197
+ box-shadow: 0 4px 20px rgba(99,102,241,0.12);
198
+ transform: translateY(-2px);
199
+ }
200
+ .entity-card .entity-name { font-size: 15px; font-weight: 700; margin-bottom: 4px; color: #1a1a2e; }
201
+ .entity-card .entity-type {
202
+ font-size: 12px; color: #9ca3af; font-weight: 500;
203
+ display: flex; align-items: center; gap: 4px;
204
+ }
205
+ .entity-card .tiers { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
206
+ .tier {
207
+ font-size: 11px;
208
+ padding: 3px 10px;
209
+ border-radius: 6px;
210
+ font-weight: 600;
211
+ display: flex;
212
+ align-items: center;
213
+ gap: 4px;
214
+ }
215
+ .tier::before { content: ''; width: 6px; height: 6px; border-radius: 50%; }
216
+ .tier.hot { background: #fef2f2; color: #dc2626; }
217
+ .tier.hot::before { background: #ef4444; }
218
+ .tier.warm { background: #fffbeb; color: #d97706; }
219
+ .tier.warm::before { background: #f59e0b; }
220
+ .tier.cold { background: #eff6ff; color: #2563eb; }
221
+ .tier.cold::before { background: #3b82f6; }
222
+ .tier.empty-tier { background: #f9fafb; color: #9ca3af; }
223
+
224
+ .detail-header {
225
+ display: flex;
226
+ align-items: center;
227
+ gap: 16px;
228
+ margin-bottom: 24px;
229
+ }
230
+ .back-btn {
231
+ background: #fff;
232
+ border: 1px solid #e5e7eb;
233
+ border-radius: 10px;
234
+ padding: 8px 12px;
235
+ cursor: pointer;
236
+ font-size: 16px;
237
+ transition: all 0.15s;
238
+ display: flex;
239
+ align-items: center;
240
+ box-shadow: 0 1px 3px rgba(0,0,0,0.04);
241
+ }
242
+ .back-btn:hover { background: #f9fafb; border-color: #d1d5db; }
243
+ .detail-name { font-size: 22px; font-weight: 800; color: #1a1a2e; letter-spacing: -0.5px; }
244
+ .detail-meta {
245
+ font-size: 13px;
246
+ color: #9ca3af;
247
+ font-weight: 500;
248
+ background: #f3f4f6;
249
+ padding: 4px 12px;
250
+ border-radius: 8px;
251
+ }
252
+
253
+ .facts-list { margin-bottom: 32px; }
254
+ .fact-row {
255
+ padding: 14px 20px;
256
+ background: #fff;
257
+ border-radius: 10px;
258
+ margin-bottom: 6px;
259
+ font-size: 14px;
260
+ display: flex;
261
+ justify-content: space-between;
262
+ align-items: center;
263
+ gap: 16px;
264
+ box-shadow: 0 1px 2px rgba(0,0,0,0.03);
265
+ transition: transform 0.15s;
266
+ }
267
+ .fact-row:hover { transform: translateX(4px); }
268
+ .fact-row .fact-text { flex: 1; line-height: 1.5; color: #374151; }
269
+ .fact-row .fact-meta { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
270
+ .fact-row .fact-category {
271
+ font-size: 11px;
272
+ padding: 2px 8px;
273
+ border-radius: 4px;
274
+ background: #f3f4f6;
275
+ color: #6b7280;
276
+ font-weight: 500;
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>
378
+ </head>
379
+ <body>
380
+ <div id="root"></div>
381
+ <script type="text/babel">
382
+ const { useState, useEffect, useCallback } = React;
383
+ const API = `http://${window.location.hostname}:7878`;
384
+ const TOKEN = localStorage.getItem('dbrain-token') || '';
385
+
386
+ function api(path, opts = {}) {
387
+ return fetch(`${API}${path}`, {
388
+ ...opts,
389
+ headers: { 'Authorization': `Bearer ${TOKEN}`, 'Content-Type': 'application/json', ...opts.headers },
390
+ }).then(r => r.json());
391
+ }
392
+
393
+ function categoryClass(cat) {
394
+ if (cat === 'projects') return 'project';
395
+ if (cat === 'areas') return 'area';
396
+ if (cat === 'resources') return 'resource';
397
+ return 'archive';
398
+ }
399
+
400
+ function TokenPrompt({ onSave }) {
401
+ const [token, setToken] = useState('');
402
+ return (
403
+ <div className="token-screen">
404
+ <div className="token-card">
405
+ <div className="logo">&#x1F9E0;</div>
406
+ <h1>dbrain</h1>
407
+ <p>Enter your access token to connect to the brain</p>
408
+ <input
409
+ placeholder="sk-dbr_..."
410
+ value={token}
411
+ onChange={e => setToken(e.target.value)}
412
+ onKeyDown={e => e.key === 'Enter' && token && onSave(token)}
413
+ autoFocus
414
+ />
415
+ <div className="token-hint">Press Enter to connect</div>
416
+ </div>
417
+ </div>
418
+ );
419
+ }
420
+
421
+ function Dashboard() {
422
+ const [health, setHealth] = useState(null);
423
+ const [overview, setOverview] = useState(null);
424
+ const [selectedEntity, setSelectedEntity] = useState(null);
425
+ const [entityDetail, setEntityDetail] = useState(null);
426
+ const [searchQuery, setSearchQuery] = useState('');
427
+ const [searchResults, setSearchResults] = useState(null);
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>
464
+ </div>
465
+ );
466
+
467
+ const totalFacts = overview ? overview.reduce((sum, e) => sum + e.total, 0) : health.facts;
468
+ const hotFacts = overview ? overview.reduce((sum, e) => sum + e.hot, 0) : 0;
469
+ const warmFacts = overview ? overview.reduce((sum, e) => sum + e.warm, 0) : 0;
470
+ const coldFacts = overview ? overview.reduce((sum, e) => sum + e.cold, 0) : 0;
471
+
472
+ if (selectedConvo && convoDetail) {
473
+ return (
474
+ <>
475
+ <div className="header">
476
+ <div className="header-inner">
477
+ <div className="header-left">
478
+ <div className="brain-icon">&#x1F9E0;</div>
479
+ <span className="header-title">{health.name}</span>
480
+ <span className="header-version">v{health.version}</span>
481
+ </div>
482
+ <div className="header-right">
483
+ <span className="pulse"></span>
484
+ <span className="status-text">Online</span>
485
+ </div>
486
+ </div>
487
+ </div>
488
+ <div className="container">
489
+ <div className="detail-header">
490
+ <button className="back-btn" onClick={() => setSelectedConvo(null)}>&larr;</button>
491
+ <span className="detail-name">{convoDetail.started_at?.split('T')[0]}</span>
492
+ <span className="detail-meta">{convoDetail.source}</span>
493
+ </div>
494
+ <div className="section-title">
495
+ Messages
496
+ <span className="count">{convoDetail.messages?.length || 0}</span>
497
+ </div>
498
+ <div className="messages-list">
499
+ {convoDetail.messages?.map(m => (
500
+ <div className={`msg-row ${m.role}`} key={m.id}>
501
+ <div className="msg-role">{m.role}</div>
502
+ <div className="msg-content">{m.content}</div>
503
+ </div>
504
+ ))}
505
+ </div>
506
+ </div>
507
+ </>
508
+ );
509
+ }
510
+
511
+ if (selectedEntity && entityDetail) {
512
+ return (
513
+ <>
514
+ <div className="header">
515
+ <div className="header-inner">
516
+ <div className="header-left">
517
+ <div className="brain-icon">&#x1F9E0;</div>
518
+ <span className="header-title">{health.name}</span>
519
+ <span className="header-version">v{health.version}</span>
520
+ </div>
521
+ <div className="header-right">
522
+ <span className="pulse"></span>
523
+ <span className="status-text">Online</span>
524
+ </div>
525
+ </div>
526
+ </div>
527
+ <div className="container">
528
+ <div className="detail-header">
529
+ <button className="back-btn" onClick={() => setSelectedEntity(null)}>&larr;</button>
530
+ <span className="detail-name">{entityDetail.name}</span>
531
+ <span className="detail-meta">{entityDetail.type} / {entityDetail.category}</span>
532
+ </div>
533
+ <div className="section-title">
534
+ Facts
535
+ <span className="count">{entityDetail.facts?.length || 0}</span>
536
+ </div>
537
+ <div className="facts-list">
538
+ {entityDetail.facts?.map(f => (
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">&#x1F9E0;</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>
569
+ </div>
570
+ </div>
571
+ </div>
572
+
573
+ <div className="container">
574
+ <div className="stats">
575
+ <div className="stat entities">
576
+ <div className="value">{health.entities}</div>
577
+ <div className="label">Entities</div>
578
+ </div>
579
+ <div className="stat total">
580
+ <div className="value">{totalFacts}</div>
581
+ <div className="label">Facts</div>
582
+ </div>
583
+ <div className="stat hot">
584
+ <div className="value">{hotFacts}</div>
585
+ <div className="label">Hot</div>
586
+ </div>
587
+ <div className="stat warm">
588
+ <div className="value">{warmFacts}</div>
589
+ <div className="label">Warm</div>
590
+ </div>
591
+ <div className="stat cold">
592
+ <div className="value">{coldFacts}</div>
593
+ <div className="label">Cold</div>
594
+ </div>
595
+ <div className="stat convos">
596
+ <div className="value">{health.conversations}</div>
597
+ <div className="label">Conversations</div>
598
+ </div>
599
+ </div>
600
+
601
+ <div className="search-box">
602
+ <span className="search-icon">&#x1F50D;</span>
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>
610
+
611
+ {searchResults && (
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
+ )}
629
+
630
+ <div>
631
+ <div className="section-title">
632
+ Entities
633
+ <span className="count">{overview?.length || 0}</span>
634
+ </div>
635
+ <div className="entities-grid">
636
+ {overview?.map(e => (
637
+ <div className={`entity-card ${categoryClass(e.category)}`} key={e.id} onClick={() => setSelectedEntity(e.id)}>
638
+ <div className="entity-name">{e.name}</div>
639
+ <div className="entity-type">{e.type} &middot; {e.category}</div>
640
+ <div className="tiers">
641
+ {e.hot > 0 && <span className="tier hot">{e.hot} hot</span>}
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
+ ))}
648
+ </div>
649
+ </div>
650
+
651
+ {conversations && conversations.length > 0 && (
652
+ <div>
653
+ <div className="section-title">
654
+ Conversations
655
+ <span className="count">{conversations.length}</span>
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
+ ))}
665
+ </div>
666
+ </div>
667
+ )}
668
+ </div>
669
+ </>
670
+ );
671
+ }
672
+
673
+ ReactDOM.createRoot(document.getElementById('root')).render(<Dashboard />);
674
+ </script>
675
+ </body>
676
+ </html>
@@ -0,0 +1,2 @@
1
+ export declare function startDashboard(port: number): void;
2
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"AAQA,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,QAe1C"}