@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.
- package/bizar-dash/CHANGELOG.md +37 -276
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +2 -0
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
- package/bizar-dash/dist/assets/main-eWZ4NlCL.css +1 -0
- package/bizar-dash/dist/assets/main-usWhlPWa.js +362 -0
- package/bizar-dash/dist/assets/main-usWhlPWa.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile-DQLFCjwJ.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile-DQLFCjwJ.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.js +352 -0
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +1 -0
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
- package/bizar-dash/skills/bizar/SKILL.md +116 -0
- package/bizar-dash/skills/chat/SKILL.md +74 -0
- package/bizar-dash/skills/headroom/SKILL.md +94 -0
- package/bizar-dash/skills/lightrag/SKILL.md +86 -0
- package/bizar-dash/skills/minimax/SKILL.md +80 -0
- package/bizar-dash/skills/obsidian/SKILL.md +68 -0
- package/bizar-dash/skills/providers/SKILL.md +75 -0
- package/bizar-dash/skills/sdk/SKILL.md +138 -0
- package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
- package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
- package/bizar-dash/skills/usage/SKILL.md +62 -0
- package/bizar-dash/src/server/api.mjs +18 -0
- package/bizar-dash/src/server/headroom.mjs +645 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +272 -2
- package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
- package/bizar-dash/src/server/memory-store.mjs +189 -0
- package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
- package/bizar-dash/src/server/minimax.mjs +196 -5
- package/bizar-dash/src/server/providers-store.mjs +956 -0
- package/bizar-dash/src/server/routes/_shared.mjs +17 -0
- package/bizar-dash/src/server/routes/config.mjs +52 -1
- package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
- package/bizar-dash/src/server/routes/headroom.mjs +126 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
- package/bizar-dash/src/server/routes/memory.mjs +668 -1
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
- package/bizar-dash/src/server/routes/providers.mjs +266 -5
- package/bizar-dash/src/server/routes/skills.mjs +32 -43
- package/bizar-dash/src/server/routes/update.mjs +340 -0
- package/bizar-dash/src/server/routes/usage.mjs +136 -0
- package/bizar-dash/src/server/serve-info.mjs +135 -4
- package/bizar-dash/src/server/server.mjs +20 -0
- package/bizar-dash/src/server/skills-store.mjs +152 -262
- package/bizar-dash/src/web/App.tsx +120 -29
- package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
- package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
- package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
- package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
- package/bizar-dash/src/web/components/Topbar.tsx +2 -1
- package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
- package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
- package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
- package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
- package/bizar-dash/src/web/lib/api.ts +43 -0
- package/bizar-dash/src/web/lib/types.ts +16 -0
- package/bizar-dash/src/web/main.tsx +2 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
- package/bizar-dash/src/web/styles/chat.css +135 -1
- package/bizar-dash/src/web/styles/main.css +46 -0
- package/bizar-dash/src/web/styles/memory.css +955 -0
- package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
- package/bizar-dash/src/web/styles/settings.css +418 -0
- package/bizar-dash/src/web/styles/skills.css +302 -0
- package/bizar-dash/src/web/styles/tasks.css +288 -0
- package/bizar-dash/src/web/views/Chat.tsx +276 -48
- package/bizar-dash/src/web/views/Config.tsx +3 -2065
- package/bizar-dash/src/web/views/Memory.tsx +140 -0
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
- package/bizar-dash/src/web/views/Overview.tsx +3 -0
- package/bizar-dash/src/web/views/Settings.tsx +36 -0
- package/bizar-dash/src/web/views/Skills.tsx +208 -260
- package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
- package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
- package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +354 -0
- package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
- package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
- package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
- package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
- package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
- package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
- package/bizar-dash/tests/headroom-install.test.mjs +173 -0
- package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
- package/bizar-dash/tests/headroom-status.test.mjs +117 -0
- package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
- package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
- package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
- package/bizar-dash/tests/memory-tab.test.mjs +322 -0
- package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
- package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
- package/bizar-dash/tests/mod-upgrade.node.test.mjs +1 -1
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
- package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
- package/bizar-dash/tests/skills-list.test.mjs +232 -0
- package/bizar-dash/tests/skills-search.test.mjs +222 -0
- package/bizar-dash/tests/submit-feedback.test.mjs +6 -6
- package/bizar-dash/tests/tasks-create.test.mjs +187 -0
- package/bizar-dash/tests/update-check.test.mjs +127 -0
- package/bizar-dash/tests/update-run.test.mjs +266 -0
- package/cli/bin.mjs +247 -1
- package/cli/provision.mjs +118 -4
- package/config/agents/_shared/SKILLS.md +109 -0
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
- package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
- package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
- 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
|
+
}
|