@polderlabs/bizar 4.4.13 → 4.5.1

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 (118) hide show
  1. package/bizar-dash/CHANGELOG.md +37 -276
  2. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +2 -0
  3. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
  4. package/bizar-dash/dist/assets/main-eWZ4NlCL.css +1 -0
  5. package/bizar-dash/dist/assets/main-usWhlPWa.js +362 -0
  6. package/bizar-dash/dist/assets/main-usWhlPWa.js.map +1 -0
  7. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile-DQLFCjwJ.js} +2 -2
  8. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile-DQLFCjwJ.js.map} +1 -1
  9. package/bizar-dash/dist/assets/mobile-O6ANdD4W.js +352 -0
  10. package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +1 -0
  11. package/bizar-dash/dist/index.html +3 -3
  12. package/bizar-dash/dist/mobile.html +2 -2
  13. package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
  14. package/bizar-dash/skills/bizar/SKILL.md +116 -0
  15. package/bizar-dash/skills/chat/SKILL.md +74 -0
  16. package/bizar-dash/skills/headroom/SKILL.md +94 -0
  17. package/bizar-dash/skills/lightrag/SKILL.md +86 -0
  18. package/bizar-dash/skills/minimax/SKILL.md +80 -0
  19. package/bizar-dash/skills/obsidian/SKILL.md +68 -0
  20. package/bizar-dash/skills/providers/SKILL.md +75 -0
  21. package/bizar-dash/skills/sdk/SKILL.md +138 -0
  22. package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
  23. package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
  24. package/bizar-dash/skills/usage/SKILL.md +62 -0
  25. package/bizar-dash/src/server/api.mjs +18 -0
  26. package/bizar-dash/src/server/headroom.mjs +645 -0
  27. package/bizar-dash/src/server/memory-lightrag.mjs +272 -2
  28. package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
  29. package/bizar-dash/src/server/memory-store.mjs +189 -0
  30. package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
  31. package/bizar-dash/src/server/minimax.mjs +196 -5
  32. package/bizar-dash/src/server/providers-store.mjs +956 -0
  33. package/bizar-dash/src/server/routes/_shared.mjs +17 -0
  34. package/bizar-dash/src/server/routes/config.mjs +52 -1
  35. package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
  36. package/bizar-dash/src/server/routes/headroom.mjs +126 -0
  37. package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
  38. package/bizar-dash/src/server/routes/memory.mjs +668 -1
  39. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
  40. package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
  41. package/bizar-dash/src/server/routes/providers.mjs +266 -5
  42. package/bizar-dash/src/server/routes/skills.mjs +32 -43
  43. package/bizar-dash/src/server/routes/update.mjs +340 -0
  44. package/bizar-dash/src/server/routes/usage.mjs +136 -0
  45. package/bizar-dash/src/server/serve-info.mjs +135 -4
  46. package/bizar-dash/src/server/server.mjs +20 -0
  47. package/bizar-dash/src/server/skills-store.mjs +152 -262
  48. package/bizar-dash/src/web/App.tsx +120 -29
  49. package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
  50. package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
  51. package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
  52. package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
  53. package/bizar-dash/src/web/components/Topbar.tsx +2 -1
  54. package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
  55. package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
  56. package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
  57. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
  58. package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
  59. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
  60. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
  61. package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
  62. package/bizar-dash/src/web/lib/api.ts +43 -0
  63. package/bizar-dash/src/web/lib/types.ts +16 -0
  64. package/bizar-dash/src/web/main.tsx +2 -0
  65. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
  66. package/bizar-dash/src/web/styles/chat.css +135 -1
  67. package/bizar-dash/src/web/styles/main.css +46 -0
  68. package/bizar-dash/src/web/styles/memory.css +955 -0
  69. package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
  70. package/bizar-dash/src/web/styles/settings.css +418 -0
  71. package/bizar-dash/src/web/styles/skills.css +302 -0
  72. package/bizar-dash/src/web/styles/tasks.css +288 -0
  73. package/bizar-dash/src/web/views/Chat.tsx +276 -48
  74. package/bizar-dash/src/web/views/Config.tsx +3 -2065
  75. package/bizar-dash/src/web/views/Memory.tsx +140 -0
  76. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
  77. package/bizar-dash/src/web/views/Overview.tsx +3 -0
  78. package/bizar-dash/src/web/views/Settings.tsx +36 -0
  79. package/bizar-dash/src/web/views/Skills.tsx +208 -260
  80. package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
  81. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
  82. package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
  83. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
  84. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +354 -0
  85. package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
  86. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
  87. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
  88. package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
  89. package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
  90. package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
  91. package/bizar-dash/tests/headroom-install.test.mjs +173 -0
  92. package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
  93. package/bizar-dash/tests/headroom-status.test.mjs +117 -0
  94. package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
  95. package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
  96. package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
  97. package/bizar-dash/tests/memory-tab.test.mjs +322 -0
  98. package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
  99. package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
  100. package/bizar-dash/tests/mod-upgrade.node.test.mjs +1 -1
  101. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
  102. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
  103. package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
  104. package/bizar-dash/tests/skills-list.test.mjs +232 -0
  105. package/bizar-dash/tests/skills-search.test.mjs +222 -0
  106. package/bizar-dash/tests/submit-feedback.test.mjs +6 -6
  107. package/bizar-dash/tests/tasks-create.test.mjs +187 -0
  108. package/bizar-dash/tests/update-check.test.mjs +127 -0
  109. package/bizar-dash/tests/update-run.test.mjs +266 -0
  110. package/cli/bin.mjs +247 -1
  111. package/cli/provision.mjs +118 -4
  112. package/config/agents/_shared/SKILLS.md +109 -0
  113. package/package.json +1 -1
  114. package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
  115. package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
  116. package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
  117. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
  118. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
@@ -0,0 +1,307 @@
1
+ // src/web/views/memory/LightragPanel.tsx — LightRAG controls, stats, query.
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import {
4
+ Brain,
5
+ CheckCircle2,
6
+ Database,
7
+ Loader2,
8
+ Play,
9
+ Power,
10
+ PowerOff,
11
+ RefreshCw,
12
+ RotateCw,
13
+ Search as SearchIcon,
14
+ Sparkles,
15
+ XCircle,
16
+ Zap,
17
+ } from 'lucide-react';
18
+ import { Button } from '../../components/Button';
19
+ import { Card, CardMeta, CardTitle } from '../../components/Card';
20
+ import { EmptyState } from '../../components/EmptyState';
21
+ import { Spinner } from '../../components/Spinner';
22
+ import { useToast } from '../../components/Toast';
23
+ import { api } from '../../lib/api';
24
+ import { formatTime, cn } from '../../lib/utils';
25
+
26
+ type Stats = {
27
+ running: boolean;
28
+ pid: number | null;
29
+ host: string;
30
+ port: number;
31
+ workingDir: string;
32
+ lastReindexAt: string | null;
33
+ lastReindexOk: boolean | null;
34
+ lastReindexInserted: number | null;
35
+ lastReindexFailed: number | null;
36
+ noteCount: number;
37
+ indexedApprox: number;
38
+ queryCountLast24h: number;
39
+ avgResponseMs: number | null;
40
+ };
41
+
42
+ type Config = {
43
+ enabled: boolean;
44
+ host: string;
45
+ port: number;
46
+ workingDir: string;
47
+ llmBinding: string;
48
+ embeddingBinding: string;
49
+ llmModel: string;
50
+ embeddingModel: string;
51
+ };
52
+
53
+ type Props = { refreshKey: number };
54
+
55
+ export function LightragPanel({ refreshKey }: Props) {
56
+ const toast = useToast();
57
+ const [stats, setStats] = useState<Stats | null>(null);
58
+ const [config, setConfig] = useState<Config | null>(null);
59
+ const [logTail, setLogTail] = useState<string[]>([]);
60
+ const [loading, setLoading] = useState(true);
61
+ const [busy, setBusy] = useState<string | null>(null);
62
+ const [query, setQuery] = useState('');
63
+ const [queryResult, setQueryResult] = useState<{ ok: boolean; text: string } | null>(null);
64
+
65
+ const reload = async () => {
66
+ setLoading(true);
67
+ try {
68
+ const [s, c, log] = await Promise.all([
69
+ api.get<Stats>('/memory/lightrag/stats').catch(() => null),
70
+ api.get<{ lightrag?: Config }>('/memory/config').then((r) => r?.lightrag || null).catch(() => null),
71
+ api.get<{ lines: string[] }>('/memory/lightrag/log').catch(() => ({ lines: [] })),
72
+ ]);
73
+ setStats(s);
74
+ setConfig(c);
75
+ setLogTail(log?.lines || []);
76
+ } catch (err) {
77
+ toast.error(`LightRAG status failed: ${(err as Error).message}`);
78
+ } finally {
79
+ setLoading(false);
80
+ }
81
+ };
82
+
83
+ useEffect(() => {
84
+ reload();
85
+ // eslint-disable-next-line react-hooks/exhaustive-deps
86
+ }, [refreshKey]);
87
+
88
+ const runCommand = async (key: string, fn: () => Promise<unknown>, successMsg: string) => {
89
+ setBusy(key);
90
+ try {
91
+ await fn();
92
+ toast.success(successMsg);
93
+ await reload();
94
+ } catch (err) {
95
+ toast.error(`${key} failed: ${(err as Error).message}`);
96
+ } finally {
97
+ setBusy(null);
98
+ }
99
+ };
100
+
101
+ const onStart = () => runCommand('start', () => api.post('/memory/lightrag/start', {}), 'LightRAG starting…');
102
+ const onStop = () => runCommand('stop', () => api.post('/memory/lightrag/stop', {}), 'LightRAG stopping…');
103
+ const onRestart = async () => {
104
+ setBusy('restart');
105
+ try {
106
+ await api.post('/memory/lightrag/stop', {}).catch(() => {});
107
+ await new Promise((r) => setTimeout(r, 600));
108
+ await api.post('/memory/lightrag/start', {});
109
+ toast.success('LightRAG restarted.');
110
+ await reload();
111
+ } catch (err) {
112
+ toast.error(`Restart failed: ${(err as Error).message}`);
113
+ } finally {
114
+ setBusy(null);
115
+ }
116
+ };
117
+ const onReindex = () => runCommand('reindex', () => api.post('/memory/lightrag/reindex', {}), 'Reindex complete.');
118
+ const onRebuild = () => runCommand('rebuild', () => api.post('/memory/lightrag/rebuild-graph', {}), 'Graph rebuilt.');
119
+
120
+ const onQuery = async () => {
121
+ if (!query.trim()) return;
122
+ setBusy('query');
123
+ try {
124
+ const r = await api.get<{ ok: boolean; q: string; semantic?: { ok: boolean; response?: unknown; error?: string } }>(
125
+ `/memory/query?q=${encodeURIComponent(query)}&topK=8`,
126
+ );
127
+ const text = r?.semantic?.response
128
+ ? (typeof r.semantic.response === 'string'
129
+ ? r.semantic.response
130
+ : JSON.stringify(r.semantic.response))
131
+ : (r?.semantic?.error || 'No response');
132
+ setQueryResult({ ok: !!r?.semantic?.ok, text });
133
+ } catch (err) {
134
+ setQueryResult({ ok: false, text: (err as Error).message });
135
+ } finally {
136
+ setBusy(null);
137
+ }
138
+ };
139
+
140
+ if (loading && !stats) {
141
+ return (
142
+ <div className="view-loading">
143
+ <Spinner size="lg" />
144
+ <p>Loading LightRAG status…</p>
145
+ </div>
146
+ );
147
+ }
148
+ if (!stats) {
149
+ return <div className="muted">No LightRAG data available.</div>;
150
+ }
151
+
152
+ return (
153
+ <div className="memory-panel-content">
154
+ {/* ── Status header ───────────────────────────────────────────── */}
155
+ <Card variant="elevated">
156
+ <CardTitle>
157
+ <Brain size={14} />
158
+ LightRAG
159
+ <span className={cn('memory-source-pill-status', stats.running ? 'on' : 'off')}>
160
+ {stats.running ? 'running' : 'stopped'}
161
+ </span>
162
+ </CardTitle>
163
+ <CardMeta>
164
+ {stats.running
165
+ ? <>PID <code>{stats.pid}</code> · {stats.host}:{stats.port}</>
166
+ : <>Server is not running. Start it from the controls below.</>}
167
+ </CardMeta>
168
+ <div className="memory-action-row">
169
+ <Button variant="primary" size="sm" onClick={onStart} disabled={!!busy || stats.running}>
170
+ {busy === 'start' ? <Loader2 size={12} className="memory-spin" /> : <Play size={12} />}
171
+ Start
172
+ </Button>
173
+ <Button variant="secondary" size="sm" onClick={onStop} disabled={!!busy || !stats.running}>
174
+ {busy === 'stop' ? <Loader2 size={12} className="memory-spin" /> : <PowerOff size={12} />}
175
+ Stop
176
+ </Button>
177
+ <Button variant="secondary" size="sm" onClick={onRestart} disabled={!!busy}>
178
+ {busy === 'restart' ? <Loader2 size={12} className="memory-spin" /> : <Power size={12} />}
179
+ Restart
180
+ </Button>
181
+ <Button variant="ghost" size="sm" onClick={onReindex} disabled={!!busy}>
182
+ {busy === 'reindex' ? <Loader2 size={12} className="memory-spin" /> : <RefreshCw size={12} />}
183
+ Reindex all
184
+ </Button>
185
+ <Button variant="ghost" size="sm" onClick={onRebuild} disabled={!!busy} title="Wipe working dir + reindex from scratch">
186
+ {busy === 'rebuild' ? <Loader2 size={12} className="memory-spin" /> : <RotateCw size={12} />}
187
+ Rebuild graph
188
+ </Button>
189
+ </div>
190
+ </Card>
191
+
192
+ {/* ── Stat cards ─────────────────────────────────────────────── */}
193
+ <div className="memory-stat-grid">
194
+ <StatBox icon={<Database size={14} />} label="Indexed chunks (approx)" value={String(stats.indexedApprox)} />
195
+ <StatBox icon={<FileTextCount n={stats.noteCount} />} label="Notes in vault" value={String(stats.noteCount)} />
196
+ <StatBox icon={<Zap size={14} />} label="Queries last 24h" value={String(stats.queryCountLast24h)} />
197
+ <StatBox
198
+ icon={<Sparkles size={14} />}
199
+ label="Avg response"
200
+ value={stats.avgResponseMs !== null ? `${stats.avgResponseMs} ms` : '—'}
201
+ />
202
+ <StatBox
203
+ icon={stats.lastReindexOk ? <CheckCircle2 size={14} /> : stats.lastReindexOk === false ? <XCircle size={14} /> : <RefreshCw size={14} />}
204
+ label="Last reindex"
205
+ value={stats.lastReindexAt ? formatTime(stats.lastReindexAt) : 'never'}
206
+ sub={stats.lastReindexInserted !== null
207
+ ? `${stats.lastReindexInserted} inserted${stats.lastReindexFailed ? `, ${stats.lastReindexFailed} failed` : ''}`
208
+ : undefined}
209
+ />
210
+ </div>
211
+
212
+ {/* ── Quick search ───────────────────────────────────────────── */}
213
+ <Card>
214
+ <CardTitle>
215
+ <SearchIcon size={14} /> Quick search
216
+ </CardTitle>
217
+ <CardMeta>Send a natural-language query to the LightRAG index.</CardMeta>
218
+ <div className="memory-search-row">
219
+ <input
220
+ type="text"
221
+ className="input"
222
+ placeholder="e.g. how does the memory service write notes?"
223
+ value={query}
224
+ onChange={(e) => setQuery(e.target.value)}
225
+ onKeyDown={(e) => {
226
+ if (e.key === 'Enter') onQuery();
227
+ }}
228
+ disabled={busy === 'query'}
229
+ />
230
+ <Button variant="primary" onClick={onQuery} disabled={busy === 'query' || !query.trim()}>
231
+ {busy === 'query' ? <Loader2 size={12} className="memory-spin" /> : <SearchIcon size={12} />}
232
+ Query
233
+ </Button>
234
+ </div>
235
+ {queryResult && (
236
+ <div className={cn('memory-query-result', queryResult.ok ? 'ok' : 'err')}>
237
+ <pre className="mono text-sm">{queryResult.text}</pre>
238
+ </div>
239
+ )}
240
+ </Card>
241
+
242
+ {/* ── Config snapshot ────────────────────────────────────────── */}
243
+ {config && (
244
+ <Card>
245
+ <CardTitle>Configuration</CardTitle>
246
+ <CardMeta>Effective values from <code>.bizar/memory.json</code> + env defaults.</CardMeta>
247
+ <dl className="memory-config-row">
248
+ <dt>LLM binding</dt>
249
+ <dd><code>{config.llmBinding}</code> · <code>{config.llmModel}</code></dd>
250
+ <dt>Embedding</dt>
251
+ <dd><code>{config.embeddingBinding}</code> · <code>{config.embeddingModel}</code></dd>
252
+ <dt>Host</dt>
253
+ <dd><code>{config.host}:{config.port}</code></dd>
254
+ <dt>Working dir</dt>
255
+ <dd className="mono ellipsis" title={config.workingDir}>{config.workingDir}</dd>
256
+ </dl>
257
+ </Card>
258
+ )}
259
+
260
+ {/* ── Log tail ───────────────────────────────────────────────── */}
261
+ {logTail.length > 0 && (
262
+ <Card>
263
+ <CardTitle>
264
+ <RefreshCw size={14} /> Server log (tail)
265
+ </CardTitle>
266
+ <CardMeta>Last {logTail.length} lines from <code>lightrag.log</code></CardMeta>
267
+ <pre className="memory-log-tail mono text-xs">
268
+ {logTail.join('\n')}
269
+ </pre>
270
+ </Card>
271
+ )}
272
+ </div>
273
+ );
274
+ }
275
+
276
+ function StatBox({
277
+ icon,
278
+ label,
279
+ value,
280
+ sub,
281
+ }: {
282
+ icon: React.ReactNode;
283
+ label: string;
284
+ value: string;
285
+ sub?: string;
286
+ }) {
287
+ return (
288
+ <div className="memory-stat-card">
289
+ <div className="memory-stat-icon">{icon}</div>
290
+ <div className="memory-stat-body">
291
+ <div className="memory-stat-label">{label}</div>
292
+ <div className="memory-stat-value">{value}</div>
293
+ {sub && <div className="memory-stat-sub muted">{sub}</div>}
294
+ </div>
295
+ </div>
296
+ );
297
+ }
298
+
299
+ function FileTextCount({ n }: { n: number }) {
300
+ // Stable icon — use FileText with a small badge
301
+ return (
302
+ <span style={{ position: 'relative', display: 'inline-block' }}>
303
+ <SearchIcon size={14} />
304
+ <span className="memory-mini-badge">{n}</span>
305
+ </span>
306
+ );
307
+ }
@@ -0,0 +1,354 @@
1
+ // src/web/views/memory/MemoryOverview.tsx — Memory tab overview panel (KPIs + sub-system pills).
2
+ import { useEffect, useState } from 'react';
3
+ import {
4
+ Activity,
5
+ Brain,
6
+ CheckCircle2,
7
+ Database,
8
+ FileText,
9
+ GitBranch,
10
+ RefreshCw,
11
+ Search,
12
+ ShieldCheck,
13
+ Sparkles,
14
+ XCircle,
15
+ AlertTriangle,
16
+ } from 'lucide-react';
17
+ import { Button } from '../../components/Button';
18
+ import { Card, CardMeta, CardTitle } from '../../components/Card';
19
+ import { Spinner } from '../../components/Spinner';
20
+ import { useToast } from '../../components/Toast';
21
+ import { api } from '../../lib/api';
22
+ import { formatTime, cn } from '../../lib/utils';
23
+
24
+ export type HealthResponse = {
25
+ score: number;
26
+ status: 'healthy' | 'degraded' | 'unhealthy' | 'unconfigured';
27
+ checks: Array<{ name: string; pass: boolean; detail: string }>;
28
+ message: string;
29
+ };
30
+
31
+ export type VaultStats = {
32
+ exists: boolean;
33
+ vaultRoot: string;
34
+ mode: string;
35
+ noteCount: number;
36
+ totalSize: number;
37
+ folderCount: number;
38
+ folders: string[];
39
+ lastModified: number | null;
40
+ gitClean: boolean | null;
41
+ gitBranch: string | null;
42
+ };
43
+
44
+ type MemoryStatus = {
45
+ initialized: boolean;
46
+ mode?: string;
47
+ projectId?: string;
48
+ vaultRoot?: string;
49
+ branch?: string;
50
+ gitClean?: boolean | null;
51
+ noteCount?: number;
52
+ lastSecretScan?: string | null;
53
+ };
54
+
55
+ type LightragStats = {
56
+ running: boolean;
57
+ pid: number | null;
58
+ host: string;
59
+ port: number;
60
+ indexedApprox: number;
61
+ queryCountLast24h: number;
62
+ lastReindexAt: string | null;
63
+ avgResponseMs?: number | null;
64
+ noteCount?: number;
65
+ };
66
+
67
+ type StorageStats = {
68
+ total: number;
69
+ breakdown: Array<{ name: string; path: string; size: number }>;
70
+ };
71
+
72
+ export type MemoryOverviewData = {
73
+ health: HealthResponse;
74
+ vault: VaultStats;
75
+ lightrag: {
76
+ running: boolean;
77
+ pid: number | null;
78
+ host: string;
79
+ port: number;
80
+ indexedApprox: number;
81
+ queryCountLast24h: number;
82
+ lastReindexAt: string | null;
83
+ };
84
+ storage: {
85
+ total: number;
86
+ breakdown: Array<{ name: string; path: string; size: number }>;
87
+ };
88
+ };
89
+
90
+ type Props = {
91
+ refreshKey: number;
92
+ onRefresh: () => void;
93
+ setActiveSubPanel: (panel: 'overview' | 'lightrag' | 'obsidian' | 'git' | 'semantic' | 'config') => void;
94
+ };
95
+
96
+ export function MemoryOverview({ refreshKey, onRefresh, setActiveSubPanel }: Props) {
97
+ const toast = useToast();
98
+ const [data, setData] = useState<MemoryOverviewData | null>(null);
99
+ const [loading, setLoading] = useState(true);
100
+
101
+ const reload = async () => {
102
+ setLoading(true);
103
+ try {
104
+ const [health, status, lightrag, storage] = await Promise.all([
105
+ api.get<HealthResponse>('/memory/health').catch(() => null),
106
+ api.get<MemoryStatus>('/memory/status').catch(() => null),
107
+ api.get<LightragStats>('/memory/lightrag/stats').catch(() => null),
108
+ api.get<StorageStats>('/memory/storage').catch(() => null),
109
+ ]);
110
+ setData({
111
+ health: health || {
112
+ score: 0,
113
+ status: 'unconfigured',
114
+ checks: [],
115
+ message: 'memory not initialised',
116
+ },
117
+ vault: {
118
+ exists: !!status?.initialized,
119
+ vaultRoot: status?.vaultRoot || '',
120
+ mode: status?.mode || 'local-only',
121
+ noteCount: status?.noteCount || 0,
122
+ totalSize: storage?.total || 0,
123
+ folderCount: 0,
124
+ folders: [],
125
+ lastModified: null,
126
+ gitClean: status?.gitClean ?? null,
127
+ gitBranch: status?.branch || null,
128
+ },
129
+ lightrag: lightrag || {
130
+ running: false,
131
+ pid: null,
132
+ host: '127.0.0.1',
133
+ port: 9621,
134
+ indexedApprox: 0,
135
+ queryCountLast24h: 0,
136
+ lastReindexAt: null,
137
+ },
138
+ storage: storage || { total: 0, breakdown: [] },
139
+ });
140
+ } catch (err) {
141
+ toast.error(`Memory overview failed: ${(err as Error).message}`);
142
+ } finally {
143
+ setLoading(false);
144
+ }
145
+ };
146
+
147
+ useEffect(() => {
148
+ reload();
149
+ // eslint-disable-next-line react-hooks/exhaustive-deps
150
+ }, [refreshKey]);
151
+
152
+ if (loading && !data) {
153
+ return (
154
+ <div className="view-loading">
155
+ <Spinner size="lg" />
156
+ <p>Loading memory overview…</p>
157
+ </div>
158
+ );
159
+ }
160
+ if (!data) {
161
+ return <div className="muted">No memory data available.</div>;
162
+ }
163
+
164
+ const { health, vault, lightrag, storage } = data;
165
+ const scoreColor =
166
+ health.score >= 80 ? 'var(--success)' :
167
+ health.score >= 50 ? 'var(--warning)' :
168
+ 'var(--error)';
169
+
170
+ return (
171
+ <div className="memory-overview">
172
+ {/* ── Health score hero ─────────────────────────────────────────── */}
173
+ <Card variant="elevated" className="memory-health-hero">
174
+ <div className="memory-health-score" style={{ borderColor: scoreColor }}>
175
+ <span className="memory-health-score-value" style={{ color: scoreColor }}>
176
+ {health.score}
177
+ </span>
178
+ <span className="memory-health-score-label">/ 100</span>
179
+ </div>
180
+ <div className="memory-health-meta">
181
+ <h3 className="memory-health-title">
182
+ <Activity size={16} />
183
+ Memory health ·{' '}
184
+ <span style={{ color: scoreColor }}>{health.status}</span>
185
+ </h3>
186
+ <p className="memory-health-message">{health.message}</p>
187
+ <Button variant="ghost" size="sm" onClick={onRefresh}>
188
+ <RefreshCw size={12} /> Refresh
189
+ </Button>
190
+ </div>
191
+ </Card>
192
+
193
+ {/* ── Source pills ─────────────────────────────────────────────── */}
194
+ <div className="memory-source-pills">
195
+ <button
196
+ type="button"
197
+ className="memory-source-pill"
198
+ onClick={() => setActiveSubPanel('lightrag')}
199
+ >
200
+ <Brain size={14} />
201
+ <span>LightRAG</span>
202
+ <span className={cn('memory-source-pill-status', lightrag.running ? 'on' : 'off')}>
203
+ {lightrag.running ? 'running' : 'stopped'}
204
+ </span>
205
+ </button>
206
+ <button
207
+ type="button"
208
+ className="memory-source-pill"
209
+ onClick={() => setActiveSubPanel('obsidian')}
210
+ >
211
+ <FileText size={14} />
212
+ <span>Obsidian Vault</span>
213
+ <span className={cn('memory-source-pill-status', vault.exists ? 'on' : 'off')}>
214
+ {vault.noteCount} notes
215
+ </span>
216
+ </button>
217
+ <button
218
+ type="button"
219
+ className="memory-source-pill"
220
+ onClick={() => setActiveSubPanel('git')}
221
+ >
222
+ <GitBranch size={14} />
223
+ <span>Git Sync</span>
224
+ <span className={cn(
225
+ 'memory-source-pill-status',
226
+ vault.gitClean === null ? 'na' : vault.gitClean ? 'on' : 'warn',
227
+ )}>
228
+ {vault.gitClean === null ? '—' : vault.gitClean ? 'clean' : 'dirty'}
229
+ </span>
230
+ </button>
231
+ <button
232
+ type="button"
233
+ className="memory-source-pill"
234
+ onClick={() => setActiveSubPanel('semantic')}
235
+ >
236
+ <Search size={14} />
237
+ <span>Semantic Search</span>
238
+ <span className="memory-source-pill-status na">cross-source</span>
239
+ </button>
240
+ </div>
241
+
242
+ {/* ── KPI grid ─────────────────────────────────────────────────── */}
243
+ <div className="memory-stat-grid">
244
+ <StatCard
245
+ icon={<Brain size={16} />}
246
+ label="LightRAG"
247
+ value={lightrag.running ? `running · ${lightrag.queryCountLast24h} q/24h` : 'stopped'}
248
+ sub={lightrag.running ? `${lightrag.indexedApprox} indexed chunks` : 'start from the LightRAG panel'}
249
+ onClick={() => setActiveSubPanel('lightrag')}
250
+ />
251
+ <StatCard
252
+ icon={<FileText size={16} />}
253
+ label="Obsidian"
254
+ value={`${vault.noteCount} notes`}
255
+ sub={vault.folderCount > 0 ? `${vault.folderCount} folders` : 'no folders yet'}
256
+ onClick={() => setActiveSubPanel('obsidian')}
257
+ />
258
+ <StatCard
259
+ icon={<GitBranch size={16} />}
260
+ label="Git"
261
+ value={vault.gitBranch ? `branch ${vault.gitBranch}` : 'local-only'}
262
+ sub={
263
+ vault.gitClean === null ? 'not configured' :
264
+ vault.gitClean ? 'clean working tree' :
265
+ 'dirty — commit or pull'
266
+ }
267
+ onClick={() => setActiveSubPanel('git')}
268
+ />
269
+ <StatCard
270
+ icon={<Database size={16} />}
271
+ label="Storage"
272
+ value={formatBytes(storage.total)}
273
+ sub={storage.breakdown.length > 0
274
+ ? `${storage.breakdown.length} dirs tracked`
275
+ : 'no memory data on disk'}
276
+ />
277
+ <StatCard
278
+ icon={<Sparkles size={16} />}
279
+ label="Last reindex"
280
+ value={lightrag.lastReindexAt ? formatTime(lightrag.lastReindexAt) : 'never'}
281
+ sub="lightrag ingest"
282
+ onClick={() => setActiveSubPanel('lightrag')}
283
+ />
284
+ <StatCard
285
+ icon={<ShieldCheck size={16} />}
286
+ label="Health"
287
+ value={`${health.score}/100`}
288
+ sub={health.status}
289
+ onClick={() => setActiveSubPanel('config')}
290
+ />
291
+ </div>
292
+
293
+ {/* ── Health checks detail ─────────────────────────────────────── */}
294
+ {health.checks.length > 0 && (
295
+ <Card>
296
+ <CardTitle>Health checks</CardTitle>
297
+ <CardMeta>Composite score across all memory subsystems</CardMeta>
298
+ <ul className="memory-check-list">
299
+ {health.checks.map((c) => (
300
+ <li key={c.name} className="memory-check-row">
301
+ <span className="memory-check-icon">
302
+ {c.pass
303
+ ? <CheckCircle2 size={14} style={{ color: 'var(--success)' }} />
304
+ : c.name.includes('secrets') || c.name.includes('schema')
305
+ ? <AlertTriangle size={14} style={{ color: 'var(--warning)' }} />
306
+ : <XCircle size={14} style={{ color: 'var(--error)' }} />}
307
+ </span>
308
+ <span className="memory-check-name">{c.name}</span>
309
+ <span className="memory-check-detail muted">{c.detail}</span>
310
+ </li>
311
+ ))}
312
+ </ul>
313
+ </Card>
314
+ )}
315
+ </div>
316
+ );
317
+ }
318
+
319
+ function StatCard({
320
+ icon,
321
+ label,
322
+ value,
323
+ sub,
324
+ onClick,
325
+ }: {
326
+ icon: React.ReactNode;
327
+ label: string;
328
+ value: string;
329
+ sub: string;
330
+ onClick?: () => void;
331
+ }) {
332
+ const Comp = onClick ? 'button' : 'div';
333
+ return (
334
+ <Comp
335
+ type={onClick ? 'button' : undefined}
336
+ onClick={onClick}
337
+ className={cn('memory-stat-card', onClick && 'memory-stat-card-clickable')}
338
+ >
339
+ <div className="memory-stat-icon">{icon}</div>
340
+ <div className="memory-stat-body">
341
+ <div className="memory-stat-label">{label}</div>
342
+ <div className="memory-stat-value">{value}</div>
343
+ <div className="memory-stat-sub muted">{sub}</div>
344
+ </div>
345
+ </Comp>
346
+ );
347
+ }
348
+
349
+ function formatBytes(n: number): string {
350
+ if (n < 1024) return `${n} B`;
351
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
352
+ if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
353
+ return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
354
+ }