@geminilight/mindos 0.5.21 → 0.5.23
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/app/app/api/ask/route.ts +31 -9
- package/app/app/api/bootstrap/route.ts +1 -0
- package/app/app/api/monitoring/route.ts +95 -0
- package/app/app/globals.css +14 -0
- package/app/app/setup/page.tsx +3 -2
- package/app/components/ActivityBar.tsx +183 -0
- package/app/components/AskFab.tsx +39 -97
- package/app/components/AskModal.tsx +13 -371
- package/app/components/Breadcrumb.tsx +4 -4
- package/app/components/FileTree.tsx +21 -4
- package/app/components/Logo.tsx +39 -0
- package/app/components/Panel.tsx +152 -0
- package/app/components/RightAskPanel.tsx +72 -0
- package/app/components/SettingsModal.tsx +9 -235
- package/app/components/SidebarLayout.tsx +426 -12
- package/app/components/SyncStatusBar.tsx +74 -53
- package/app/components/TableOfContents.tsx +4 -2
- package/app/components/ask/AskContent.tsx +418 -0
- package/app/components/ask/MessageList.tsx +2 -2
- package/app/components/panels/AgentsPanel.tsx +231 -0
- package/app/components/panels/PanelHeader.tsx +35 -0
- package/app/components/panels/PluginsPanel.tsx +106 -0
- package/app/components/panels/SearchPanel.tsx +178 -0
- package/app/components/panels/SyncPopover.tsx +105 -0
- package/app/components/renderers/csv/TableView.tsx +4 -4
- package/app/components/settings/AgentsTab.tsx +240 -0
- package/app/components/settings/AiTab.tsx +39 -1
- package/app/components/settings/KnowledgeTab.tsx +116 -2
- package/app/components/settings/McpTab.tsx +6 -6
- package/app/components/settings/MonitoringTab.tsx +202 -0
- package/app/components/settings/SettingsContent.tsx +343 -0
- package/app/components/settings/types.ts +1 -1
- package/app/components/setup/index.tsx +2 -23
- package/app/hooks/useResizeDrag.ts +78 -0
- package/app/instrumentation.ts +7 -2
- package/app/lib/agent/log.ts +1 -0
- package/app/lib/agent/model.ts +33 -10
- package/app/lib/api.ts +12 -3
- package/app/lib/core/csv.ts +2 -1
- package/app/lib/core/fs-ops.ts +7 -6
- package/app/lib/core/index.ts +1 -1
- package/app/lib/core/lines.ts +7 -6
- package/app/lib/core/search-index.ts +174 -0
- package/app/lib/core/search.ts +30 -1
- package/app/lib/core/security.ts +6 -3
- package/app/lib/errors.ts +108 -0
- package/app/lib/format.ts +19 -0
- package/app/lib/fs.ts +6 -3
- package/app/lib/i18n-en.ts +49 -6
- package/app/lib/i18n-zh.ts +48 -5
- package/app/lib/metrics.ts +81 -0
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +1 -1
- package/app/package.json +2 -2
- package/bin/cli.js +27 -97
- package/package.json +4 -2
- package/scripts/setup.js +2 -12
- package/skills/mindos/SKILL.md +226 -8
- package/skills/mindos-zh/SKILL.md +226 -8
- package/app/package-lock.json +0 -15736
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { Loader2, RefreshCw, ChevronDown, ChevronRight, ExternalLink } from 'lucide-react';
|
|
3
|
+
import { apiFetch } from '@/lib/api';
|
|
4
|
+
import type { McpStatus, AgentInfo } from './types';
|
|
5
|
+
import type { Messages } from '@/lib/i18n';
|
|
6
|
+
|
|
7
|
+
interface AgentsTabProps {
|
|
8
|
+
t: Messages;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AgentsTab({ t }: AgentsTabProps) {
|
|
12
|
+
const [agents, setAgents] = useState<AgentInfo[]>([]);
|
|
13
|
+
const [mcpStatus, setMcpStatus] = useState<McpStatus | null>(null);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
const [error, setError] = useState(false);
|
|
16
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
17
|
+
const [showNotDetected, setShowNotDetected] = useState(false);
|
|
18
|
+
const intervalRef = useRef<ReturnType<typeof setInterval>>(undefined);
|
|
19
|
+
|
|
20
|
+
const a = t.settings?.agents as Record<string, unknown> | undefined;
|
|
21
|
+
|
|
22
|
+
// i18n helpers with fallbacks
|
|
23
|
+
const txt = (key: string, fallback: string) => (a?.[key] as string) ?? fallback;
|
|
24
|
+
const txtFn = <T,>(key: string, fallback: (v: T) => string) =>
|
|
25
|
+
(a?.[key] as ((v: T) => string) | undefined) ?? fallback;
|
|
26
|
+
|
|
27
|
+
const fetchAll = useCallback(async (silent = false) => {
|
|
28
|
+
if (!silent) setError(false);
|
|
29
|
+
try {
|
|
30
|
+
const [statusData, agentsData] = await Promise.all([
|
|
31
|
+
apiFetch<McpStatus>('/api/mcp/status'),
|
|
32
|
+
apiFetch<{ agents: AgentInfo[] }>('/api/mcp/agents'),
|
|
33
|
+
]);
|
|
34
|
+
setMcpStatus(statusData);
|
|
35
|
+
setAgents(agentsData.agents);
|
|
36
|
+
setError(false);
|
|
37
|
+
} catch {
|
|
38
|
+
if (!silent) setError(true);
|
|
39
|
+
}
|
|
40
|
+
setLoading(false);
|
|
41
|
+
setRefreshing(false);
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
// Initial fetch + 30s auto-refresh
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
fetchAll();
|
|
47
|
+
intervalRef.current = setInterval(() => fetchAll(true), 30_000);
|
|
48
|
+
return () => clearInterval(intervalRef.current);
|
|
49
|
+
}, [fetchAll]);
|
|
50
|
+
|
|
51
|
+
const handleRefresh = () => {
|
|
52
|
+
setRefreshing(true);
|
|
53
|
+
fetchAll();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (loading) {
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex justify-center py-8">
|
|
59
|
+
<Loader2 size={18} className="animate-spin text-muted-foreground" />
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (error && agents.length === 0) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex flex-col items-center gap-3 py-8 text-center">
|
|
67
|
+
<p className="text-sm text-destructive">{txt('fetchError', 'Failed to load agent data')}</p>
|
|
68
|
+
<button
|
|
69
|
+
onClick={handleRefresh}
|
|
70
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
71
|
+
>
|
|
72
|
+
<RefreshCw size={12} />
|
|
73
|
+
{txt('refresh', 'Refresh')}
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Group agents
|
|
80
|
+
const connected = agents.filter(a => a.present && a.installed);
|
|
81
|
+
const detected = agents.filter(a => a.present && !a.installed);
|
|
82
|
+
const notFound = agents.filter(a => !a.present);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="space-y-5">
|
|
86
|
+
{/* MCP Server Status */}
|
|
87
|
+
<div className="rounded-xl border border-border bg-card p-4 flex items-center justify-between">
|
|
88
|
+
<div className="flex items-center gap-3">
|
|
89
|
+
<span className="text-sm font-medium text-foreground">{txt('mcpServer', 'MCP Server')}</span>
|
|
90
|
+
{mcpStatus?.running ? (
|
|
91
|
+
<span className="flex items-center gap-1.5 text-xs">
|
|
92
|
+
<span className="w-2 h-2 rounded-full bg-emerald-500 inline-block" />
|
|
93
|
+
<span className="text-emerald-600 dark:text-emerald-400">
|
|
94
|
+
{txt('running', 'Running')} {txtFn<number>('onPort', (p) => `on :${p}`)(mcpStatus.port)}
|
|
95
|
+
</span>
|
|
96
|
+
</span>
|
|
97
|
+
) : (
|
|
98
|
+
<span className="flex items-center gap-1.5 text-xs">
|
|
99
|
+
<span className="w-2 h-2 rounded-full bg-zinc-400 inline-block" />
|
|
100
|
+
<span className="text-muted-foreground">{txt('stopped', 'Not running')}</span>
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
<button
|
|
105
|
+
onClick={handleRefresh}
|
|
106
|
+
disabled={refreshing}
|
|
107
|
+
className="flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-50 transition-colors"
|
|
108
|
+
>
|
|
109
|
+
<RefreshCw size={12} className={refreshing ? 'animate-spin' : ''} />
|
|
110
|
+
{refreshing ? txt('refreshing', 'Refreshing...') : txt('refresh', 'Refresh')}
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* Connected Agents */}
|
|
115
|
+
{connected.length > 0 && (
|
|
116
|
+
<section>
|
|
117
|
+
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">
|
|
118
|
+
{txtFn<number>('connectedCount', (n) => `Connected (${n})`)(connected.length)}
|
|
119
|
+
</h3>
|
|
120
|
+
<div className="space-y-2">
|
|
121
|
+
{connected.map(agent => (
|
|
122
|
+
<AgentCard key={agent.key} agent={agent} status="connected" />
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
</section>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{/* Detected but not configured */}
|
|
129
|
+
{detected.length > 0 && (
|
|
130
|
+
<section>
|
|
131
|
+
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">
|
|
132
|
+
{txtFn<number>('detectedCount', (n) => `Detected but not configured (${n})`)(detected.length)}
|
|
133
|
+
</h3>
|
|
134
|
+
<div className="space-y-2">
|
|
135
|
+
{detected.map(agent => (
|
|
136
|
+
<AgentCard
|
|
137
|
+
key={agent.key}
|
|
138
|
+
agent={agent}
|
|
139
|
+
status="detected"
|
|
140
|
+
connectLabel={txt('connect', 'Connect')}
|
|
141
|
+
/>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
</section>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{/* Not Detected */}
|
|
148
|
+
{notFound.length > 0 && (
|
|
149
|
+
<section>
|
|
150
|
+
<button
|
|
151
|
+
onClick={() => setShowNotDetected(!showNotDetected)}
|
|
152
|
+
className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3 hover:text-foreground transition-colors"
|
|
153
|
+
>
|
|
154
|
+
{showNotDetected ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
155
|
+
{txtFn<number>('notDetectedCount', (n) => `Not Detected (${n})`)(notFound.length)}
|
|
156
|
+
</button>
|
|
157
|
+
{showNotDetected && (
|
|
158
|
+
<div className="space-y-2">
|
|
159
|
+
{notFound.map(agent => (
|
|
160
|
+
<AgentCard key={agent.key} agent={agent} status="notFound" />
|
|
161
|
+
))}
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
</section>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
{/* Empty state */}
|
|
168
|
+
{agents.length === 0 && (
|
|
169
|
+
<p className="text-sm text-muted-foreground text-center py-4">
|
|
170
|
+
{txt('noAgents', 'No agents detected on this machine.')}
|
|
171
|
+
</p>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* Auto-refresh hint */}
|
|
175
|
+
<p className="text-[10px] text-muted-foreground/60 text-center">
|
|
176
|
+
{txt('autoRefresh', 'Auto-refresh every 30s')}
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* ── Agent Card ──────────────────────────────────────────────── */
|
|
183
|
+
|
|
184
|
+
function AgentCard({
|
|
185
|
+
agent,
|
|
186
|
+
status,
|
|
187
|
+
connectLabel,
|
|
188
|
+
}: {
|
|
189
|
+
agent: AgentInfo;
|
|
190
|
+
status: 'connected' | 'detected' | 'notFound';
|
|
191
|
+
connectLabel?: string;
|
|
192
|
+
}) {
|
|
193
|
+
const dot =
|
|
194
|
+
status === 'connected' ? 'bg-emerald-500' :
|
|
195
|
+
status === 'detected' ? 'bg-amber-500' :
|
|
196
|
+
'bg-zinc-400';
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div className="rounded-lg border border-border bg-card/50 px-4 py-3 flex items-center justify-between gap-3">
|
|
200
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
201
|
+
<span className={`w-2 h-2 rounded-full shrink-0 ${dot}`} />
|
|
202
|
+
<span className="text-sm font-medium text-foreground truncate">{agent.name}</span>
|
|
203
|
+
{status === 'connected' && (
|
|
204
|
+
<div className="flex items-center gap-2 text-[11px] text-muted-foreground">
|
|
205
|
+
<span className="px-1.5 py-0.5 rounded bg-muted">{agent.transport}</span>
|
|
206
|
+
<span className="px-1.5 py-0.5 rounded bg-muted">{agent.scope}</span>
|
|
207
|
+
{agent.configPath && (
|
|
208
|
+
<span className="truncate max-w-[200px]" title={agent.configPath}>
|
|
209
|
+
{agent.configPath.replace(/^.*[/\\]/, '')}
|
|
210
|
+
</span>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
{status === 'detected' && (
|
|
216
|
+
<a
|
|
217
|
+
href="#"
|
|
218
|
+
onClick={(e) => {
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
// Navigate to MCP tab by dispatching a custom event
|
|
221
|
+
const settingsModal = document.querySelector('[role="dialog"][aria-label="Settings"]');
|
|
222
|
+
if (settingsModal) {
|
|
223
|
+
const mcpBtn = settingsModal.querySelectorAll('button');
|
|
224
|
+
for (const btn of mcpBtn) {
|
|
225
|
+
if (btn.textContent?.trim() === 'MCP') {
|
|
226
|
+
btn.click();
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}}
|
|
232
|
+
className="flex items-center gap-1 px-2.5 py-1 text-xs rounded-lg bg-amber-500/10 text-amber-600 dark:text-amber-400 hover:bg-amber-500/20 transition-colors shrink-0"
|
|
233
|
+
>
|
|
234
|
+
{connectLabel ?? 'Connect'}
|
|
235
|
+
<ExternalLink size={11} />
|
|
236
|
+
</a>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
4
|
import { AlertCircle, Loader2 } from 'lucide-react';
|
|
5
5
|
import type { AiSettings, AgentSettings, ProviderConfig, SettingsData, AiTabProps } from './types';
|
|
6
|
-
import { Field, Select, Input, EnvBadge, ApiKeyInput, Toggle } from './Primitives';
|
|
6
|
+
import { Field, Select, Input, EnvBadge, ApiKeyInput, Toggle, SectionLabel } from './Primitives';
|
|
7
7
|
|
|
8
8
|
type TestState = 'idle' | 'testing' | 'ok' | 'error';
|
|
9
9
|
type ErrorCode = 'auth_error' | 'model_not_found' | 'rate_limited' | 'network_error' | 'unknown';
|
|
@@ -277,6 +277,44 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
277
277
|
)}
|
|
278
278
|
</div>
|
|
279
279
|
</div>
|
|
280
|
+
|
|
281
|
+
{/* Ask AI Display Mode */}
|
|
282
|
+
<AskDisplayMode />
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* ── Ask AI Display Mode (localStorage-based, no server roundtrip) ── */
|
|
288
|
+
|
|
289
|
+
function AskDisplayMode() {
|
|
290
|
+
const [mode, setMode] = useState<'panel' | 'popup'>('panel');
|
|
291
|
+
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
try {
|
|
294
|
+
const stored = localStorage.getItem('ask-mode');
|
|
295
|
+
if (stored === 'popup') setMode('popup');
|
|
296
|
+
} catch {}
|
|
297
|
+
}, []);
|
|
298
|
+
|
|
299
|
+
const handleChange = (value: string) => {
|
|
300
|
+
const next = value as 'panel' | 'popup';
|
|
301
|
+
setMode(next);
|
|
302
|
+
try { localStorage.setItem('ask-mode', next); } catch {}
|
|
303
|
+
// Notify SidebarLayout to pick up the change
|
|
304
|
+
window.dispatchEvent(new StorageEvent('storage', { key: 'ask-mode', newValue: next }));
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<div className="pt-3 border-t border-border">
|
|
309
|
+
<SectionLabel>MindOS Agent</SectionLabel>
|
|
310
|
+
<div className="space-y-4">
|
|
311
|
+
<Field label="Display Mode" hint="Side panel stays docked on the right. Popup opens a floating dialog.">
|
|
312
|
+
<Select value={mode} onChange={e => handleChange(e.target.value)}>
|
|
313
|
+
<option value="panel">Side Panel</option>
|
|
314
|
+
<option value="popup">Popup</option>
|
|
315
|
+
</Select>
|
|
316
|
+
</Field>
|
|
317
|
+
</div>
|
|
280
318
|
</div>
|
|
281
319
|
);
|
|
282
320
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback, useSyncExternalStore } from 'react';
|
|
4
|
-
import { Copy, Check, RefreshCw, Trash2, Sparkles } from 'lucide-react';
|
|
3
|
+
import { useState, useEffect, useCallback, useSyncExternalStore, useRef } from 'react';
|
|
4
|
+
import { Copy, Check, RefreshCw, Trash2, Sparkles, ChevronDown, ChevronRight, Loader2, Cpu, Zap, Database as DatabaseIcon, HardDrive } from 'lucide-react';
|
|
5
5
|
import type { KnowledgeTabProps } from './types';
|
|
6
6
|
import { Field, Input, EnvBadge, SectionLabel, Toggle } from './Primitives';
|
|
7
7
|
import { apiFetch } from '@/lib/api';
|
|
8
|
+
import { formatBytes, formatUptime } from '@/lib/format';
|
|
8
9
|
|
|
9
10
|
export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
10
11
|
const env = data.envOverrides ?? {};
|
|
@@ -205,6 +206,119 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
205
206
|
</div>
|
|
206
207
|
</div>
|
|
207
208
|
)}
|
|
209
|
+
|
|
210
|
+
{/* System Monitoring — collapsible */}
|
|
211
|
+
<MonitoringSection />
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* ── Inline Monitoring Section ── */
|
|
217
|
+
|
|
218
|
+
interface MonitoringData {
|
|
219
|
+
system: {
|
|
220
|
+
uptimeMs: number;
|
|
221
|
+
memory: { heapUsed: number; heapTotal: number; rss: number };
|
|
222
|
+
nodeVersion: string;
|
|
223
|
+
};
|
|
224
|
+
application: {
|
|
225
|
+
agentRequests: number;
|
|
226
|
+
toolExecutions: number;
|
|
227
|
+
totalTokens: { input: number; output: number };
|
|
228
|
+
avgResponseTimeMs: number;
|
|
229
|
+
errors: number;
|
|
230
|
+
};
|
|
231
|
+
knowledgeBase: {
|
|
232
|
+
root: string;
|
|
233
|
+
fileCount: number;
|
|
234
|
+
totalSizeBytes: number;
|
|
235
|
+
};
|
|
236
|
+
mcp: { running: boolean; port: number };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function MonitoringSection() {
|
|
240
|
+
const [expanded, setExpanded] = useState(false);
|
|
241
|
+
const [data, setData] = useState<MonitoringData | null>(null);
|
|
242
|
+
const [loading, setLoading] = useState(false);
|
|
243
|
+
|
|
244
|
+
const fetchData = useCallback(async () => {
|
|
245
|
+
setLoading(true);
|
|
246
|
+
try {
|
|
247
|
+
const d = await apiFetch<MonitoringData>('/api/monitoring', { timeout: 5000 });
|
|
248
|
+
setData(d);
|
|
249
|
+
} catch { /* ignore */ }
|
|
250
|
+
setLoading(false);
|
|
251
|
+
}, []);
|
|
252
|
+
|
|
253
|
+
// Fetch on first expand, then refresh every 10s while expanded
|
|
254
|
+
const hasFetched = useRef(false);
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (!expanded) { hasFetched.current = false; return; }
|
|
257
|
+
if (!hasFetched.current) { fetchData(); hasFetched.current = true; }
|
|
258
|
+
const id = setInterval(fetchData, 10_000);
|
|
259
|
+
return () => clearInterval(id);
|
|
260
|
+
}, [expanded, fetchData]);
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<div className="border-t border-border pt-5">
|
|
264
|
+
<button
|
|
265
|
+
onClick={() => setExpanded(v => !v)}
|
|
266
|
+
className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors w-full"
|
|
267
|
+
>
|
|
268
|
+
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
269
|
+
<Cpu size={12} />
|
|
270
|
+
System Monitoring
|
|
271
|
+
{loading && <Loader2 size={10} className="animate-spin ml-1" />}
|
|
272
|
+
</button>
|
|
273
|
+
|
|
274
|
+
{expanded && data && (
|
|
275
|
+
<div className="mt-3 grid grid-cols-2 gap-x-6 gap-y-2 text-xs">
|
|
276
|
+
<div>
|
|
277
|
+
<span className="text-muted-foreground">Heap</span>
|
|
278
|
+
<span className="ml-2 tabular-nums">{formatBytes(data.system.memory.heapUsed)} / {formatBytes(data.system.memory.heapTotal)}</span>
|
|
279
|
+
</div>
|
|
280
|
+
<div>
|
|
281
|
+
<span className="text-muted-foreground">RSS</span>
|
|
282
|
+
<span className="ml-2 tabular-nums">{formatBytes(data.system.memory.rss)}</span>
|
|
283
|
+
</div>
|
|
284
|
+
<div>
|
|
285
|
+
<span className="text-muted-foreground">Uptime</span>
|
|
286
|
+
<span className="ml-2 tabular-nums">{formatUptime(data.system.uptimeMs)}</span>
|
|
287
|
+
</div>
|
|
288
|
+
<div>
|
|
289
|
+
<span className="text-muted-foreground">Node</span>
|
|
290
|
+
<span className="ml-2">{data.system.nodeVersion}</span>
|
|
291
|
+
</div>
|
|
292
|
+
<div>
|
|
293
|
+
<span className="text-muted-foreground">Requests</span>
|
|
294
|
+
<span className="ml-2 tabular-nums">{data.application.agentRequests}</span>
|
|
295
|
+
</div>
|
|
296
|
+
<div>
|
|
297
|
+
<span className="text-muted-foreground">Tool Calls</span>
|
|
298
|
+
<span className="ml-2 tabular-nums">{data.application.toolExecutions}</span>
|
|
299
|
+
</div>
|
|
300
|
+
<div>
|
|
301
|
+
<span className="text-muted-foreground">Tokens</span>
|
|
302
|
+
<span className="ml-2 tabular-nums">{(data.application.totalTokens.input + data.application.totalTokens.output).toLocaleString()}</span>
|
|
303
|
+
</div>
|
|
304
|
+
<div>
|
|
305
|
+
<span className="text-muted-foreground">Files</span>
|
|
306
|
+
<span className="ml-2 tabular-nums">{data.knowledgeBase.fileCount} ({formatBytes(data.knowledgeBase.totalSizeBytes)})</span>
|
|
307
|
+
</div>
|
|
308
|
+
<div>
|
|
309
|
+
<span className="text-muted-foreground">MCP</span>
|
|
310
|
+
<span className="ml-2">{data.mcp.running ? `Running :${data.mcp.port}` : 'Stopped'}</span>
|
|
311
|
+
</div>
|
|
312
|
+
<div>
|
|
313
|
+
<span className="text-muted-foreground">Errors</span>
|
|
314
|
+
<span className="ml-2 tabular-nums">{data.application.errors}</span>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{expanded && !data && !loading && (
|
|
320
|
+
<p className="mt-2 text-xs text-muted-foreground">Failed to load monitoring data</p>
|
|
321
|
+
)}
|
|
208
322
|
</div>
|
|
209
323
|
);
|
|
210
324
|
}
|
|
@@ -47,17 +47,17 @@ export function McpTab({ t }: McpTabProps) {
|
|
|
47
47
|
<ServerStatus status={mcpStatus} agents={agents} t={t} />
|
|
48
48
|
</div>
|
|
49
49
|
|
|
50
|
-
{/* Agent Configuration */}
|
|
51
|
-
<div>
|
|
52
|
-
<h3 className="text-sm font-medium text-foreground mb-3">{m?.agentsTitle ?? 'Agent Configuration'}</h3>
|
|
53
|
-
<AgentInstall agents={agents} t={t} onRefresh={fetchAll} />
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
50
|
{/* Skills */}
|
|
57
51
|
<div>
|
|
58
52
|
<h3 className="text-sm font-medium text-foreground mb-3">{m?.skillsTitle ?? 'Skills'}</h3>
|
|
59
53
|
<SkillsSection t={t} />
|
|
60
54
|
</div>
|
|
55
|
+
|
|
56
|
+
{/* Agent Configuration */}
|
|
57
|
+
<div>
|
|
58
|
+
<h3 className="text-sm font-medium text-foreground mb-3">{m?.agentsTitle ?? 'Agent Configuration'}</h3>
|
|
59
|
+
<AgentInstall agents={agents} t={t} onRefresh={fetchAll} />
|
|
60
|
+
</div>
|
|
61
61
|
</div>
|
|
62
62
|
);
|
|
63
63
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
4
|
+
import { Activity, Cpu, Database, HardDrive, Loader2, RefreshCw, Zap } from 'lucide-react';
|
|
5
|
+
import { apiFetch } from '@/lib/api';
|
|
6
|
+
import type { Messages } from '@/lib/i18n';
|
|
7
|
+
|
|
8
|
+
interface MonitoringData {
|
|
9
|
+
system: {
|
|
10
|
+
uptimeMs: number;
|
|
11
|
+
memory: { heapUsed: number; heapTotal: number; rss: number };
|
|
12
|
+
nodeVersion: string;
|
|
13
|
+
};
|
|
14
|
+
application: {
|
|
15
|
+
agentRequests: number;
|
|
16
|
+
toolExecutions: number;
|
|
17
|
+
totalTokens: { input: number; output: number };
|
|
18
|
+
avgResponseTimeMs: number;
|
|
19
|
+
errors: number;
|
|
20
|
+
};
|
|
21
|
+
knowledgeBase: {
|
|
22
|
+
root: string;
|
|
23
|
+
fileCount: number;
|
|
24
|
+
totalSizeBytes: number;
|
|
25
|
+
};
|
|
26
|
+
mcp: {
|
|
27
|
+
running: boolean;
|
|
28
|
+
port: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatBytes(bytes: number): string {
|
|
33
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
34
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
35
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
36
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatUptime(ms: number): string {
|
|
40
|
+
const s = Math.floor(ms / 1000);
|
|
41
|
+
if (s < 60) return `${s}s`;
|
|
42
|
+
const m = Math.floor(s / 60);
|
|
43
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
44
|
+
const h = Math.floor(m / 60);
|
|
45
|
+
if (h < 24) return `${h}h ${m % 60}m`;
|
|
46
|
+
const d = Math.floor(h / 24);
|
|
47
|
+
return `${d}d ${h % 24}h`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ProgressBar({ value, max, className }: { value: number; max: number; className?: string }) {
|
|
51
|
+
const pct = max > 0 ? Math.min(100, (value / max) * 100) : 0;
|
|
52
|
+
return (
|
|
53
|
+
<div className={`h-2 w-full rounded-full bg-muted ${className ?? ''}`}>
|
|
54
|
+
<div
|
|
55
|
+
className={`h-full rounded-full transition-all duration-300 ${pct > 85 ? 'bg-destructive' : 'bg-amber-500'}`}
|
|
56
|
+
style={{ width: `${pct}%` }}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function StatCard({ label, value, sub }: { label: string; value: string | number; sub?: string }) {
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex flex-col gap-0.5">
|
|
65
|
+
<span className="text-xs text-muted-foreground">{label}</span>
|
|
66
|
+
<span className="text-sm font-medium tabular-nums">{value}</span>
|
|
67
|
+
{sub && <span className="text-[10px] text-muted-foreground">{sub}</span>}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface MonitoringTabProps {
|
|
73
|
+
t: Messages;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function MonitoringTab({ t }: MonitoringTabProps) {
|
|
77
|
+
const [data, setData] = useState<MonitoringData | null>(null);
|
|
78
|
+
const [loading, setLoading] = useState(true);
|
|
79
|
+
const [error, setError] = useState(false);
|
|
80
|
+
|
|
81
|
+
const mon = t.settings.monitoring;
|
|
82
|
+
|
|
83
|
+
const fetchData = useCallback(async () => {
|
|
84
|
+
try {
|
|
85
|
+
const d = await apiFetch<MonitoringData>('/api/monitoring', { timeout: 5000 });
|
|
86
|
+
setData(d);
|
|
87
|
+
setError(false);
|
|
88
|
+
} catch {
|
|
89
|
+
setError(true);
|
|
90
|
+
} finally {
|
|
91
|
+
setLoading(false);
|
|
92
|
+
}
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
fetchData();
|
|
97
|
+
const id = setInterval(fetchData, 5000);
|
|
98
|
+
return () => clearInterval(id);
|
|
99
|
+
}, [fetchData]);
|
|
100
|
+
|
|
101
|
+
if (loading && !data) {
|
|
102
|
+
return (
|
|
103
|
+
<div className="flex justify-center py-8">
|
|
104
|
+
<Loader2 size={18} className="animate-spin text-muted-foreground" />
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (error && !data) {
|
|
110
|
+
return (
|
|
111
|
+
<div className="text-center py-8 text-sm text-muted-foreground">
|
|
112
|
+
{mon.fetchError || 'Failed to load monitoring data'}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!data) return null;
|
|
118
|
+
|
|
119
|
+
const { system, application, knowledgeBase, mcp } = data;
|
|
120
|
+
const heapPct = system.memory.heapTotal > 0
|
|
121
|
+
? Math.round((system.memory.heapUsed / system.memory.heapTotal) * 100)
|
|
122
|
+
: 0;
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div className="space-y-6">
|
|
126
|
+
{/* System */}
|
|
127
|
+
<section>
|
|
128
|
+
<h3 className="flex items-center gap-1.5 text-xs font-semibold text-foreground mb-3">
|
|
129
|
+
<Cpu size={13} className="text-muted-foreground" />
|
|
130
|
+
{mon.system || 'System'}
|
|
131
|
+
</h3>
|
|
132
|
+
<div className="space-y-3">
|
|
133
|
+
<div>
|
|
134
|
+
<div className="flex justify-between text-xs mb-1">
|
|
135
|
+
<span className="text-muted-foreground">{mon.heapMemory || 'Heap Memory'}</span>
|
|
136
|
+
<span className="tabular-nums">{formatBytes(system.memory.heapUsed)} / {formatBytes(system.memory.heapTotal)} ({heapPct}%)</span>
|
|
137
|
+
</div>
|
|
138
|
+
<ProgressBar value={system.memory.heapUsed} max={system.memory.heapTotal} />
|
|
139
|
+
</div>
|
|
140
|
+
<div className="grid grid-cols-3 gap-4">
|
|
141
|
+
<StatCard label={mon.rss || 'RSS'} value={formatBytes(system.memory.rss)} />
|
|
142
|
+
<StatCard label={mon.uptime || 'Uptime'} value={formatUptime(system.uptimeMs)} />
|
|
143
|
+
<StatCard label={mon.nodeVersion || 'Node'} value={system.nodeVersion} />
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</section>
|
|
147
|
+
|
|
148
|
+
{/* Application */}
|
|
149
|
+
<section>
|
|
150
|
+
<h3 className="flex items-center gap-1.5 text-xs font-semibold text-foreground mb-3">
|
|
151
|
+
<Zap size={13} className="text-muted-foreground" />
|
|
152
|
+
{mon.application || 'Application'}
|
|
153
|
+
</h3>
|
|
154
|
+
<div className="grid grid-cols-3 gap-4">
|
|
155
|
+
<StatCard label={mon.requests || 'Requests'} value={application.agentRequests} />
|
|
156
|
+
<StatCard label={mon.toolCalls || 'Tool Calls'} value={application.toolExecutions} />
|
|
157
|
+
<StatCard label={mon.avgResponse || 'Avg Response'} value={application.avgResponseTimeMs > 0 ? `${application.avgResponseTimeMs}ms` : '—'} />
|
|
158
|
+
<StatCard
|
|
159
|
+
label={mon.tokens || 'Tokens'}
|
|
160
|
+
value={`${(application.totalTokens.input + application.totalTokens.output).toLocaleString()}`}
|
|
161
|
+
sub={`↑${application.totalTokens.input.toLocaleString()} ↓${application.totalTokens.output.toLocaleString()}`}
|
|
162
|
+
/>
|
|
163
|
+
<StatCard label={mon.errors || 'Errors'} value={application.errors} />
|
|
164
|
+
</div>
|
|
165
|
+
</section>
|
|
166
|
+
|
|
167
|
+
{/* Knowledge Base */}
|
|
168
|
+
<section>
|
|
169
|
+
<h3 className="flex items-center gap-1.5 text-xs font-semibold text-foreground mb-3">
|
|
170
|
+
<Database size={13} className="text-muted-foreground" />
|
|
171
|
+
{mon.knowledgeBase || 'Knowledge Base'}
|
|
172
|
+
</h3>
|
|
173
|
+
<div className="grid grid-cols-3 gap-4">
|
|
174
|
+
<StatCard label={mon.files || 'Files'} value={knowledgeBase.fileCount} />
|
|
175
|
+
<StatCard label={mon.totalSize || 'Total Size'} value={formatBytes(knowledgeBase.totalSizeBytes)} />
|
|
176
|
+
<StatCard label={mon.rootPath || 'Root'} value={knowledgeBase.root.split('/').pop() ?? knowledgeBase.root} sub={knowledgeBase.root} />
|
|
177
|
+
</div>
|
|
178
|
+
</section>
|
|
179
|
+
|
|
180
|
+
{/* MCP */}
|
|
181
|
+
<section>
|
|
182
|
+
<h3 className="flex items-center gap-1.5 text-xs font-semibold text-foreground mb-3">
|
|
183
|
+
<HardDrive size={13} className="text-muted-foreground" />
|
|
184
|
+
MCP
|
|
185
|
+
</h3>
|
|
186
|
+
<div className="grid grid-cols-3 gap-4">
|
|
187
|
+
<StatCard
|
|
188
|
+
label={mon.mcpStatus || 'Status'}
|
|
189
|
+
value={mcp.running ? (mon.mcpRunning || 'Running') : (mon.mcpStopped || 'Stopped')}
|
|
190
|
+
/>
|
|
191
|
+
<StatCard label={mon.mcpPort || 'Port'} value={mcp.port} />
|
|
192
|
+
</div>
|
|
193
|
+
</section>
|
|
194
|
+
|
|
195
|
+
{/* Refresh indicator */}
|
|
196
|
+
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
|
197
|
+
<RefreshCw size={10} className={loading ? 'animate-spin' : ''} />
|
|
198
|
+
{mon.autoRefresh || 'Auto-refresh every 5s'}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|