rufloui 0.3.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/'1' +0 -0
- package/.env.example +46 -0
- package/CHANGELOG.md +87 -0
- package/CLAUDE.md +287 -0
- package/LICENSE +21 -0
- package/README.md +316 -0
- package/Webhooks) +0 -0
- package/docs/plans/2026-03-11-github-webhooks.md +957 -0
- package/docs/screenshot-swarm-monitor.png +0 -0
- package/frontend +0 -0
- package/index.html +13 -0
- package/package.json +56 -0
- package/public/vite.svg +4 -0
- package/src/backend/__tests__/webhook-github.test.ts +934 -0
- package/src/backend/jsonl-monitor.ts +430 -0
- package/src/backend/server.ts +2972 -0
- package/src/backend/telegram-bot.ts +511 -0
- package/src/backend/webhook-github.ts +350 -0
- package/src/frontend/App.tsx +461 -0
- package/src/frontend/api.ts +281 -0
- package/src/frontend/components/ErrorBoundary.tsx +98 -0
- package/src/frontend/components/Layout.tsx +431 -0
- package/src/frontend/components/ui/Button.tsx +111 -0
- package/src/frontend/components/ui/Card.tsx +51 -0
- package/src/frontend/components/ui/StatusBadge.tsx +60 -0
- package/src/frontend/main.tsx +63 -0
- package/src/frontend/pages/AgentVizPanel.tsx +428 -0
- package/src/frontend/pages/AgentsPanel.tsx +445 -0
- package/src/frontend/pages/ConfigPanel.tsx +661 -0
- package/src/frontend/pages/Dashboard.tsx +482 -0
- package/src/frontend/pages/HiveMindPanel.tsx +355 -0
- package/src/frontend/pages/HooksPanel.tsx +240 -0
- package/src/frontend/pages/LogsPanel.tsx +261 -0
- package/src/frontend/pages/MemoryPanel.tsx +444 -0
- package/src/frontend/pages/NeuralPanel.tsx +301 -0
- package/src/frontend/pages/PerformancePanel.tsx +198 -0
- package/src/frontend/pages/SessionsPanel.tsx +428 -0
- package/src/frontend/pages/SetupWizard.tsx +181 -0
- package/src/frontend/pages/SwarmMonitorPanel.tsx +634 -0
- package/src/frontend/pages/SwarmPanel.tsx +322 -0
- package/src/frontend/pages/TasksPanel.tsx +535 -0
- package/src/frontend/pages/WebhooksPanel.tsx +335 -0
- package/src/frontend/pages/WorkflowsPanel.tsx +448 -0
- package/src/frontend/store.ts +185 -0
- package/src/frontend/styles/global.css +113 -0
- package/src/frontend/test-setup.ts +1 -0
- package/src/frontend/tour/TourContext.tsx +161 -0
- package/src/frontend/tour/tourSteps.ts +181 -0
- package/src/frontend/tour/tourStyles.css +116 -0
- package/src/frontend/types.ts +239 -0
- package/src/frontend/utils/formatTime.test.ts +83 -0
- package/src/frontend/utils/formatTime.ts +23 -0
- package/tsconfig.json +23 -0
- package/vite.config.ts +26 -0
- package/vitest.config.ts +17 -0
- package/{,+ +0 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { useState, useEffect, type CSSProperties } from 'react'
|
|
2
|
+
import { useStore } from '@/store'
|
|
3
|
+
import { api } from '@/api'
|
|
4
|
+
import { Card } from '@/components/ui/Card'
|
|
5
|
+
import { Button } from '@/components/ui/Button'
|
|
6
|
+
import { StatusBadge } from '@/components/ui/StatusBadge'
|
|
7
|
+
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'
|
|
8
|
+
import { Brain, Cpu, Zap } from 'lucide-react'
|
|
9
|
+
|
|
10
|
+
const s = {
|
|
11
|
+
page: { display: 'flex', flexDirection: 'column', gap: 20 } as CSSProperties,
|
|
12
|
+
overview: {
|
|
13
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
14
|
+
padding: '16px 20px', background: 'var(--bg-card)', border: '1px solid var(--border)',
|
|
15
|
+
borderRadius: 'var(--radius-lg)',
|
|
16
|
+
} as CSSProperties,
|
|
17
|
+
overviewLeft: { display: 'flex', alignItems: 'center', gap: 16 } as CSSProperties,
|
|
18
|
+
stat: { fontSize: 13, color: 'var(--text-muted)' } as CSSProperties,
|
|
19
|
+
statValue: { color: 'var(--text-primary)', fontWeight: 600 } as CSSProperties,
|
|
20
|
+
grid: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 } as CSSProperties,
|
|
21
|
+
modelGrid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: 12 } as CSSProperties,
|
|
22
|
+
modelCard: {
|
|
23
|
+
padding: 16, background: 'var(--bg-input)', border: '1px solid var(--border)',
|
|
24
|
+
borderRadius: 'var(--radius)', display: 'flex', flexDirection: 'column', gap: 10,
|
|
25
|
+
} as CSSProperties,
|
|
26
|
+
modelName: { fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' } as CSSProperties,
|
|
27
|
+
modelMeta: { fontSize: 12, color: 'var(--text-muted)' } as CSSProperties,
|
|
28
|
+
progressBar: {
|
|
29
|
+
width: '100%', height: 6, background: 'var(--border)', borderRadius: 3, overflow: 'hidden',
|
|
30
|
+
} as CSSProperties,
|
|
31
|
+
progressFill: {
|
|
32
|
+
height: '100%', borderRadius: 3, transition: 'width 0.3s ease',
|
|
33
|
+
} as CSSProperties,
|
|
34
|
+
label: { fontSize: 13, color: 'var(--text-secondary)', marginBottom: 6, display: 'block' } as CSSProperties,
|
|
35
|
+
select: {
|
|
36
|
+
width: '100%', padding: '8px 12px', fontSize: 13, background: 'var(--bg-input)',
|
|
37
|
+
border: '1px solid var(--border)', borderRadius: 'var(--radius)', color: 'var(--text-primary)',
|
|
38
|
+
outline: 'none',
|
|
39
|
+
} as CSSProperties,
|
|
40
|
+
textarea: {
|
|
41
|
+
width: '100%', minHeight: 100, padding: '8px 12px', fontSize: 13, fontFamily: 'var(--font-mono)',
|
|
42
|
+
background: 'var(--bg-input)', border: '1px solid var(--border)', borderRadius: 'var(--radius)',
|
|
43
|
+
color: 'var(--text-primary)', outline: 'none', resize: 'vertical', boxSizing: 'border-box',
|
|
44
|
+
} as CSSProperties,
|
|
45
|
+
row: { display: 'flex', gap: 8, marginTop: 12 } as CSSProperties,
|
|
46
|
+
resultBox: {
|
|
47
|
+
padding: 16, background: 'var(--bg-input)', borderRadius: 'var(--radius)',
|
|
48
|
+
border: '1px solid var(--border)', marginTop: 12, fontFamily: 'var(--font-mono)',
|
|
49
|
+
fontSize: 13, color: 'var(--text-primary)', whiteSpace: 'pre-wrap', maxHeight: 200,
|
|
50
|
+
overflow: 'auto',
|
|
51
|
+
} as CSSProperties,
|
|
52
|
+
patternGrid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 10 } as CSSProperties,
|
|
53
|
+
patternCard: {
|
|
54
|
+
padding: 12, background: 'var(--bg-input)', border: '1px solid var(--border)',
|
|
55
|
+
borderRadius: 'var(--radius)',
|
|
56
|
+
} as CSSProperties,
|
|
57
|
+
patternName: { fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' } as CSSProperties,
|
|
58
|
+
patternType: { fontSize: 12, color: 'var(--text-muted)', marginTop: 4 } as CSSProperties,
|
|
59
|
+
actionsRow: { display: 'flex', gap: 8, marginTop: 12 } as CSSProperties,
|
|
60
|
+
empty: { fontSize: 13, color: 'var(--text-muted)', fontStyle: 'italic' } as CSSProperties,
|
|
61
|
+
trainingIndicator: {
|
|
62
|
+
display: 'flex', alignItems: 'center', gap: 8, padding: '10px 14px',
|
|
63
|
+
background: 'rgba(59, 130, 246, 0.1)', border: '1px solid var(--accent-blue)',
|
|
64
|
+
borderRadius: 'var(--radius)', marginTop: 12,
|
|
65
|
+
} as CSSProperties,
|
|
66
|
+
spinner: {
|
|
67
|
+
width: 16, height: 16, border: '2px solid var(--border)',
|
|
68
|
+
borderTopColor: 'var(--accent-blue)', borderRadius: '50%',
|
|
69
|
+
animation: 'spin 0.8s linear infinite', flexShrink: 0,
|
|
70
|
+
} as CSSProperties,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function accuracyColor(accuracy: number): string {
|
|
74
|
+
if (accuracy >= 0.9) return 'var(--accent-green)'
|
|
75
|
+
if (accuracy >= 0.7) return 'var(--accent-yellow)'
|
|
76
|
+
return 'var(--accent-red)'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default function NeuralPanel() {
|
|
80
|
+
const { neural, setNeural } = useStore()
|
|
81
|
+
const [loading, setLoading] = useState('')
|
|
82
|
+
const [trainModel, setTrainModel] = useState('')
|
|
83
|
+
const [trainData, setTrainData] = useState('')
|
|
84
|
+
const [predictModel, setPredictModel] = useState('')
|
|
85
|
+
const [predictInput, setPredictInput] = useState('')
|
|
86
|
+
const [predictResult, setPredictResult] = useState<unknown>(null)
|
|
87
|
+
const [patterns, setPatterns] = useState<Array<{ name: string; type: string }>>([])
|
|
88
|
+
const [isTraining, setIsTraining] = useState(false)
|
|
89
|
+
|
|
90
|
+
const models = neural?.models || []
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
api.neural.status().then((data: unknown) => setNeural(data as Parameters<typeof setNeural>[0])).catch(() => {})
|
|
94
|
+
api.neural.patterns().then((data: unknown) => setPatterns(Array.isArray(data) ? data : ((data as { patterns?: Array<{ name: string; type: string }> }).patterns ?? []))).catch(() => {})
|
|
95
|
+
}, [])
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (models.length > 0 && !trainModel) setTrainModel(models[0].name)
|
|
99
|
+
if (models.length > 0 && !predictModel) setPredictModel(models[0].name)
|
|
100
|
+
}, [models])
|
|
101
|
+
|
|
102
|
+
async function handleTrain() {
|
|
103
|
+
if (!trainModel) return
|
|
104
|
+
setLoading('train')
|
|
105
|
+
setIsTraining(true)
|
|
106
|
+
try {
|
|
107
|
+
let data: unknown = undefined
|
|
108
|
+
if (trainData.trim()) {
|
|
109
|
+
data = JSON.parse(trainData)
|
|
110
|
+
}
|
|
111
|
+
await api.neural.train({ model: trainModel, data })
|
|
112
|
+
const status = await api.neural.status()
|
|
113
|
+
setNeural(status as Parameters<typeof setNeural>[0])
|
|
114
|
+
} catch { /* noop */ }
|
|
115
|
+
setIsTraining(false)
|
|
116
|
+
setLoading('')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function handlePredict() {
|
|
120
|
+
if (!predictModel || !predictInput.trim()) return
|
|
121
|
+
setLoading('predict')
|
|
122
|
+
try {
|
|
123
|
+
const input = JSON.parse(predictInput)
|
|
124
|
+
const result = await api.neural.predict({ model: predictModel, input })
|
|
125
|
+
setPredictResult(result)
|
|
126
|
+
} catch { /* noop */ }
|
|
127
|
+
setLoading('')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function handleOptimize() {
|
|
131
|
+
setLoading('optimize')
|
|
132
|
+
try {
|
|
133
|
+
await api.neural.optimize()
|
|
134
|
+
const status = await api.neural.status()
|
|
135
|
+
setNeural(status as Parameters<typeof setNeural>[0])
|
|
136
|
+
} catch { /* noop */ }
|
|
137
|
+
setLoading('')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function handleCompress() {
|
|
141
|
+
setLoading('compress')
|
|
142
|
+
try {
|
|
143
|
+
await api.neural.compress()
|
|
144
|
+
const status = await api.neural.status()
|
|
145
|
+
setNeural(status as Parameters<typeof setNeural>[0])
|
|
146
|
+
} catch { /* noop */ }
|
|
147
|
+
setLoading('')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const chartData = models.map((m) => ({
|
|
151
|
+
name: m.name,
|
|
152
|
+
accuracy: Math.round(m.accuracy * 100),
|
|
153
|
+
}))
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div style={s.page}>
|
|
157
|
+
{/* Status Overview */}
|
|
158
|
+
<div style={s.overview}>
|
|
159
|
+
<div style={s.overviewLeft}>
|
|
160
|
+
<Brain size={20} color="var(--accent-blue)" />
|
|
161
|
+
<StatusBadge status={neural?.enabled ? 'active' : 'inactive'} />
|
|
162
|
+
<span style={s.stat}>Models: <span style={s.statValue}>{models.length}</span></span>
|
|
163
|
+
<span style={s.stat}>Training Queue: <span style={s.statValue}>{neural?.trainingQueue ?? 0}</span></span>
|
|
164
|
+
</div>
|
|
165
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
166
|
+
<Button size="sm" variant="secondary" onClick={handleOptimize} loading={loading === 'optimize'}>
|
|
167
|
+
<Zap size={14} /> Optimize
|
|
168
|
+
</Button>
|
|
169
|
+
<Button size="sm" variant="secondary" onClick={handleCompress} loading={loading === 'compress'}>
|
|
170
|
+
<Cpu size={14} /> Compress
|
|
171
|
+
</Button>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{/* Models Section */}
|
|
176
|
+
<Card title="Models" actions={
|
|
177
|
+
<Button size="sm" variant="secondary" onClick={() => {
|
|
178
|
+
api.neural.status().then((data: unknown) => setNeural(data as Parameters<typeof setNeural>[0])).catch(() => {})
|
|
179
|
+
}}>Refresh</Button>
|
|
180
|
+
}>
|
|
181
|
+
{models.length === 0 && <p style={s.empty}>No models available</p>}
|
|
182
|
+
<div style={s.modelGrid}>
|
|
183
|
+
{models.map((model) => (
|
|
184
|
+
<div key={model.name} style={s.modelCard}>
|
|
185
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
186
|
+
<span style={s.modelName}>{model.name}</span>
|
|
187
|
+
<StatusBadge status={model.status} size="sm" />
|
|
188
|
+
</div>
|
|
189
|
+
<div>
|
|
190
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
|
191
|
+
<span style={s.modelMeta}>Accuracy</span>
|
|
192
|
+
<span style={{ fontSize: 12, fontWeight: 600, color: accuracyColor(model.accuracy) }}>
|
|
193
|
+
{Math.round(model.accuracy * 100)}%
|
|
194
|
+
</span>
|
|
195
|
+
</div>
|
|
196
|
+
<div style={s.progressBar}>
|
|
197
|
+
<div style={{
|
|
198
|
+
...s.progressFill,
|
|
199
|
+
width: `${Math.round(model.accuracy * 100)}%`,
|
|
200
|
+
background: accuracyColor(model.accuracy),
|
|
201
|
+
}} />
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
{model.lastTrained && (
|
|
205
|
+
<span style={s.modelMeta}>Last trained: {new Date(model.lastTrained).toLocaleDateString()}</span>
|
|
206
|
+
)}
|
|
207
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
208
|
+
<Button size="sm" onClick={() => { setTrainModel(model.name) }}>Train</Button>
|
|
209
|
+
<Button size="sm" variant="secondary" onClick={() => { setPredictModel(model.name) }}>Predict</Button>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
))}
|
|
213
|
+
</div>
|
|
214
|
+
{/* Accuracy chart */}
|
|
215
|
+
{chartData.length > 0 && (
|
|
216
|
+
<div style={{ height: 200, marginTop: 16 }}>
|
|
217
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
218
|
+
<BarChart data={chartData}>
|
|
219
|
+
<XAxis dataKey="name" tick={{ fontSize: 12, fill: 'var(--text-muted)' }} />
|
|
220
|
+
<YAxis tick={{ fontSize: 12, fill: 'var(--text-muted)' }} domain={[0, 100]} />
|
|
221
|
+
<Tooltip
|
|
222
|
+
contentStyle={{ background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 6, fontSize: 12 }}
|
|
223
|
+
formatter={(value: number) => [`${value}%`, 'Accuracy']}
|
|
224
|
+
/>
|
|
225
|
+
<Bar dataKey="accuracy" fill="var(--accent-blue)" radius={[4, 4, 0, 0]} />
|
|
226
|
+
</BarChart>
|
|
227
|
+
</ResponsiveContainer>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</Card>
|
|
231
|
+
|
|
232
|
+
<div style={s.grid}>
|
|
233
|
+
{/* Training Section */}
|
|
234
|
+
<Card title="Training">
|
|
235
|
+
<label style={s.label}>Model</label>
|
|
236
|
+
<select style={s.select} value={trainModel} onChange={(e) => setTrainModel(e.target.value)}>
|
|
237
|
+
{models.map((m) => <option key={m.name} value={m.name}>{m.name}</option>)}
|
|
238
|
+
</select>
|
|
239
|
+
<label style={{ ...s.label, marginTop: 12 }}>Training Data (JSON)</label>
|
|
240
|
+
<textarea
|
|
241
|
+
style={s.textarea as CSSProperties}
|
|
242
|
+
value={trainData}
|
|
243
|
+
onChange={(e) => setTrainData(e.target.value)}
|
|
244
|
+
placeholder='{"features": [...], "labels": [...]}'
|
|
245
|
+
/>
|
|
246
|
+
<div style={s.row}>
|
|
247
|
+
<Button onClick={handleTrain} loading={loading === 'train'} disabled={!trainModel}>Start Training</Button>
|
|
248
|
+
</div>
|
|
249
|
+
{isTraining && (
|
|
250
|
+
<div style={s.trainingIndicator}>
|
|
251
|
+
<div style={s.spinner} />
|
|
252
|
+
<span style={{ fontSize: 13, color: 'var(--accent-blue)' }}>Training in progress...</span>
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
</Card>
|
|
256
|
+
|
|
257
|
+
{/* Prediction Section */}
|
|
258
|
+
<Card title="Prediction">
|
|
259
|
+
<label style={s.label}>Model</label>
|
|
260
|
+
<select style={s.select} value={predictModel} onChange={(e) => setPredictModel(e.target.value)}>
|
|
261
|
+
{models.map((m) => <option key={m.name} value={m.name}>{m.name}</option>)}
|
|
262
|
+
</select>
|
|
263
|
+
<label style={{ ...s.label, marginTop: 12 }}>Input (JSON)</label>
|
|
264
|
+
<textarea
|
|
265
|
+
style={s.textarea as CSSProperties}
|
|
266
|
+
value={predictInput}
|
|
267
|
+
onChange={(e) => setPredictInput(e.target.value)}
|
|
268
|
+
placeholder='{"input": [1, 2, 3]}'
|
|
269
|
+
/>
|
|
270
|
+
<div style={s.row}>
|
|
271
|
+
<Button onClick={handlePredict} loading={loading === 'predict'} disabled={!predictModel || !predictInput.trim()}>
|
|
272
|
+
Predict
|
|
273
|
+
</Button>
|
|
274
|
+
</div>
|
|
275
|
+
{predictResult !== null && (
|
|
276
|
+
<div style={s.resultBox}>
|
|
277
|
+
{typeof predictResult === 'string' ? predictResult : JSON.stringify(predictResult, null, 2)}
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
</Card>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
{/* Patterns Section */}
|
|
284
|
+
<Card title="Patterns" actions={
|
|
285
|
+
<Button size="sm" variant="secondary" onClick={() => {
|
|
286
|
+
api.neural.patterns().then((data: unknown) => setPatterns(Array.isArray(data) ? data : ((data as { patterns?: Array<{ name: string; type: string }> }).patterns ?? []))).catch(() => {})
|
|
287
|
+
}}>Refresh</Button>
|
|
288
|
+
}>
|
|
289
|
+
{patterns.length === 0 && <p style={s.empty}>No patterns detected</p>}
|
|
290
|
+
<div style={s.patternGrid}>
|
|
291
|
+
{patterns.map((p, i) => (
|
|
292
|
+
<div key={i} style={s.patternCard}>
|
|
293
|
+
<div style={s.patternName}>{p.name}</div>
|
|
294
|
+
<div style={s.patternType}>{p.type}</div>
|
|
295
|
+
</div>
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
</Card>
|
|
299
|
+
</div>
|
|
300
|
+
)
|
|
301
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback } from 'react'
|
|
2
|
+
import { useStore } from '@/store'
|
|
3
|
+
import { api } from '@/api'
|
|
4
|
+
import { Card } from '@/components/ui/Card'
|
|
5
|
+
import { Button } from '@/components/ui/Button'
|
|
6
|
+
import {
|
|
7
|
+
LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
|
|
8
|
+
} from 'recharts'
|
|
9
|
+
import type { PerformanceMetrics } from '@/types'
|
|
10
|
+
|
|
11
|
+
function StatCard({ label, value, unit }: { label: string; value: string | number; unit?: string }) {
|
|
12
|
+
return (
|
|
13
|
+
<Card>
|
|
14
|
+
<div style={{ padding: '16px 20px', textAlign: 'center' }}>
|
|
15
|
+
<div style={{ fontSize: 24, fontWeight: 700, color: 'var(--text-primary)' }}>
|
|
16
|
+
{value}
|
|
17
|
+
{unit && <span style={{ fontSize: 13, fontWeight: 400, color: 'var(--text-muted)', marginLeft: 4 }}>{unit}</span>}
|
|
18
|
+
</div>
|
|
19
|
+
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 4 }}>{label}</div>
|
|
20
|
+
</div>
|
|
21
|
+
</Card>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function PerformancePanel() {
|
|
26
|
+
const { performance, setPerformance, addLog } = useStore()
|
|
27
|
+
const [actionResult, setActionResult] = useState<unknown>(null)
|
|
28
|
+
const [actionLabel, setActionLabel] = useState('')
|
|
29
|
+
const [loading, setLoading] = useState('')
|
|
30
|
+
|
|
31
|
+
const fetchMetrics = useCallback(async () => {
|
|
32
|
+
try {
|
|
33
|
+
const data = (await api.performance.metrics()) as PerformanceMetrics
|
|
34
|
+
setPerformance(data)
|
|
35
|
+
} catch (err) {
|
|
36
|
+
addLog({ level: 'error', message: `Performance metrics failed: ${(err as Error).message}`, source: 'performance' })
|
|
37
|
+
}
|
|
38
|
+
}, [setPerformance, addLog])
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
fetchMetrics()
|
|
42
|
+
const interval = setInterval(fetchMetrics, 10000)
|
|
43
|
+
return () => clearInterval(interval)
|
|
44
|
+
}, [fetchMetrics])
|
|
45
|
+
|
|
46
|
+
const runAction = async (label: string, action: () => Promise<unknown>) => {
|
|
47
|
+
setLoading(label)
|
|
48
|
+
setActionResult(null)
|
|
49
|
+
setActionLabel(label)
|
|
50
|
+
try {
|
|
51
|
+
const result = await action()
|
|
52
|
+
setActionResult(result)
|
|
53
|
+
addLog({ level: 'info', message: `${label} completed`, source: 'performance' })
|
|
54
|
+
// If result has latency/throughput shape, update performance store
|
|
55
|
+
const r = result as Record<string, unknown>
|
|
56
|
+
if (r?.latency && r?.history) {
|
|
57
|
+
setPerformance(r as unknown as PerformanceMetrics)
|
|
58
|
+
} else {
|
|
59
|
+
// Re-fetch metrics after any action to pick up changes
|
|
60
|
+
await fetchMetrics()
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
setActionResult({ error: (err as Error).message })
|
|
64
|
+
addLog({ level: 'error', message: `${label} failed: ${(err as Error).message}`, source: 'performance' })
|
|
65
|
+
} finally {
|
|
66
|
+
setLoading('')
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const chartData = performance?.history ?? []
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
74
|
+
{/* Metrics Overview */}
|
|
75
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 12 }}>
|
|
76
|
+
<StatCard label="Avg Latency" value={performance?.latency?.avg?.toFixed(1) ?? '--'} unit="ms" />
|
|
77
|
+
<StatCard label="P95 Latency" value={performance?.latency?.p95?.toFixed(1) ?? '--'} unit="ms" />
|
|
78
|
+
<StatCard label="P99 Latency" value={performance?.latency?.p99?.toFixed(1) ?? '--'} unit="ms" />
|
|
79
|
+
<StatCard label="Throughput" value={performance?.throughput?.toFixed(1) ?? '--'} unit="req/s" />
|
|
80
|
+
<StatCard label="Error Rate" value={performance?.errorRate != null ? (performance.errorRate * 100).toFixed(2) : '--'} unit="%" />
|
|
81
|
+
<StatCard label="Active Requests" value={performance?.activeRequests ?? '--'} />
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Performance Chart */}
|
|
85
|
+
<Card>
|
|
86
|
+
<div style={{ padding: '20px 24px' }}>
|
|
87
|
+
<div style={{ fontSize: 15, fontWeight: 600, marginBottom: 16, color: 'var(--text-primary)' }}>
|
|
88
|
+
Performance Over Time
|
|
89
|
+
</div>
|
|
90
|
+
{chartData.length > 0 ? (
|
|
91
|
+
<ResponsiveContainer width="100%" height={280}>
|
|
92
|
+
<LineChart data={chartData} margin={{ top: 4, right: 20, bottom: 0, left: -10 }}>
|
|
93
|
+
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
|
94
|
+
<XAxis
|
|
95
|
+
dataKey="timestamp"
|
|
96
|
+
tick={{ fill: 'var(--text-muted)', fontSize: 11 }}
|
|
97
|
+
tickLine={false}
|
|
98
|
+
axisLine={{ stroke: 'var(--border)' }}
|
|
99
|
+
tickFormatter={(v: string) => new Date(v).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
100
|
+
/>
|
|
101
|
+
<YAxis
|
|
102
|
+
yAxisId="latency"
|
|
103
|
+
tick={{ fill: 'var(--text-muted)', fontSize: 11 }}
|
|
104
|
+
tickLine={false}
|
|
105
|
+
axisLine={{ stroke: 'var(--border)' }}
|
|
106
|
+
label={{ value: 'Latency (ms)', angle: -90, position: 'insideLeft', style: { fill: 'var(--text-muted)', fontSize: 11 } }}
|
|
107
|
+
/>
|
|
108
|
+
<YAxis
|
|
109
|
+
yAxisId="throughput"
|
|
110
|
+
orientation="right"
|
|
111
|
+
tick={{ fill: 'var(--text-muted)', fontSize: 11 }}
|
|
112
|
+
tickLine={false}
|
|
113
|
+
axisLine={{ stroke: 'var(--border)' }}
|
|
114
|
+
label={{ value: 'Throughput', angle: 90, position: 'insideRight', style: { fill: 'var(--text-muted)', fontSize: 11 } }}
|
|
115
|
+
/>
|
|
116
|
+
<Tooltip
|
|
117
|
+
contentStyle={{
|
|
118
|
+
background: 'var(--bg-secondary)',
|
|
119
|
+
border: '1px solid var(--border)',
|
|
120
|
+
borderRadius: 'var(--radius)',
|
|
121
|
+
color: 'var(--text-primary)',
|
|
122
|
+
fontSize: 12,
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
<Line yAxisId="latency" type="monotone" dataKey="latency" stroke="var(--accent-blue)" strokeWidth={2} dot={false} />
|
|
126
|
+
<Line yAxisId="throughput" type="monotone" dataKey="throughput" stroke="var(--accent-cyan)" strokeWidth={2} dot={false} />
|
|
127
|
+
</LineChart>
|
|
128
|
+
</ResponsiveContainer>
|
|
129
|
+
) : (
|
|
130
|
+
<div style={{ height: 280, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-muted)', fontSize: 13 }}>
|
|
131
|
+
No performance data available
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
<div style={{ display: 'flex', justifyContent: 'center', gap: 24, marginTop: 8 }}>
|
|
135
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--text-secondary)' }}>
|
|
136
|
+
<span style={{ width: 12, height: 3, background: 'var(--accent-blue)', borderRadius: 2 }} /> Latency (ms)
|
|
137
|
+
</span>
|
|
138
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--text-secondary)' }}>
|
|
139
|
+
<span style={{ width: 12, height: 3, background: 'var(--accent-cyan)', borderRadius: 2 }} /> Throughput (req/s)
|
|
140
|
+
</span>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</Card>
|
|
144
|
+
|
|
145
|
+
{/* Actions Row */}
|
|
146
|
+
<Card>
|
|
147
|
+
<div style={{ padding: '16px 24px' }}>
|
|
148
|
+
<div style={{ fontSize: 15, fontWeight: 600, marginBottom: 12, color: 'var(--text-primary)' }}>Actions</div>
|
|
149
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
|
150
|
+
<Button loading={loading === 'Benchmark'} onClick={() => runAction('Benchmark', () => api.performance.benchmark())}>
|
|
151
|
+
Run Benchmark
|
|
152
|
+
</Button>
|
|
153
|
+
<Button loading={loading === 'Bottleneck'} onClick={() => runAction('Bottleneck', () => api.performance.bottleneck())}>
|
|
154
|
+
Detect Bottlenecks
|
|
155
|
+
</Button>
|
|
156
|
+
<Button loading={loading === 'Optimize'} onClick={() => runAction('Optimize', () => api.performance.optimize())}>
|
|
157
|
+
Optimize
|
|
158
|
+
</Button>
|
|
159
|
+
<Button loading={loading === 'Profile'} onClick={() => runAction('Profile', () => api.performance.profile())}>
|
|
160
|
+
Generate Profile
|
|
161
|
+
</Button>
|
|
162
|
+
<Button loading={loading === 'Report'} onClick={() => runAction('Report', () => api.performance.report())}>
|
|
163
|
+
Full Report
|
|
164
|
+
</Button>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</Card>
|
|
168
|
+
|
|
169
|
+
{/* Results Area */}
|
|
170
|
+
{actionResult !== null && (
|
|
171
|
+
<Card>
|
|
172
|
+
<div style={{ padding: '20px 24px' }}>
|
|
173
|
+
<div style={{ fontSize: 15, fontWeight: 600, marginBottom: 12, color: 'var(--text-primary)' }}>
|
|
174
|
+
Result: {actionLabel}
|
|
175
|
+
</div>
|
|
176
|
+
<pre
|
|
177
|
+
style={{
|
|
178
|
+
background: 'var(--bg-primary)',
|
|
179
|
+
border: '1px solid var(--border)',
|
|
180
|
+
borderRadius: 'var(--radius)',
|
|
181
|
+
padding: 16,
|
|
182
|
+
fontSize: 12,
|
|
183
|
+
color: 'var(--text-secondary)',
|
|
184
|
+
overflow: 'auto',
|
|
185
|
+
maxHeight: 400,
|
|
186
|
+
margin: 0,
|
|
187
|
+
whiteSpace: 'pre-wrap',
|
|
188
|
+
wordBreak: 'break-word',
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
{JSON.stringify(actionResult, null, 2)}
|
|
192
|
+
</pre>
|
|
193
|
+
</div>
|
|
194
|
+
</Card>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
)
|
|
198
|
+
}
|