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.
Files changed (56) hide show
  1. package/'1' +0 -0
  2. package/.env.example +46 -0
  3. package/CHANGELOG.md +87 -0
  4. package/CLAUDE.md +287 -0
  5. package/LICENSE +21 -0
  6. package/README.md +316 -0
  7. package/Webhooks) +0 -0
  8. package/docs/plans/2026-03-11-github-webhooks.md +957 -0
  9. package/docs/screenshot-swarm-monitor.png +0 -0
  10. package/frontend +0 -0
  11. package/index.html +13 -0
  12. package/package.json +56 -0
  13. package/public/vite.svg +4 -0
  14. package/src/backend/__tests__/webhook-github.test.ts +934 -0
  15. package/src/backend/jsonl-monitor.ts +430 -0
  16. package/src/backend/server.ts +2972 -0
  17. package/src/backend/telegram-bot.ts +511 -0
  18. package/src/backend/webhook-github.ts +350 -0
  19. package/src/frontend/App.tsx +461 -0
  20. package/src/frontend/api.ts +281 -0
  21. package/src/frontend/components/ErrorBoundary.tsx +98 -0
  22. package/src/frontend/components/Layout.tsx +431 -0
  23. package/src/frontend/components/ui/Button.tsx +111 -0
  24. package/src/frontend/components/ui/Card.tsx +51 -0
  25. package/src/frontend/components/ui/StatusBadge.tsx +60 -0
  26. package/src/frontend/main.tsx +63 -0
  27. package/src/frontend/pages/AgentVizPanel.tsx +428 -0
  28. package/src/frontend/pages/AgentsPanel.tsx +445 -0
  29. package/src/frontend/pages/ConfigPanel.tsx +661 -0
  30. package/src/frontend/pages/Dashboard.tsx +482 -0
  31. package/src/frontend/pages/HiveMindPanel.tsx +355 -0
  32. package/src/frontend/pages/HooksPanel.tsx +240 -0
  33. package/src/frontend/pages/LogsPanel.tsx +261 -0
  34. package/src/frontend/pages/MemoryPanel.tsx +444 -0
  35. package/src/frontend/pages/NeuralPanel.tsx +301 -0
  36. package/src/frontend/pages/PerformancePanel.tsx +198 -0
  37. package/src/frontend/pages/SessionsPanel.tsx +428 -0
  38. package/src/frontend/pages/SetupWizard.tsx +181 -0
  39. package/src/frontend/pages/SwarmMonitorPanel.tsx +634 -0
  40. package/src/frontend/pages/SwarmPanel.tsx +322 -0
  41. package/src/frontend/pages/TasksPanel.tsx +535 -0
  42. package/src/frontend/pages/WebhooksPanel.tsx +335 -0
  43. package/src/frontend/pages/WorkflowsPanel.tsx +448 -0
  44. package/src/frontend/store.ts +185 -0
  45. package/src/frontend/styles/global.css +113 -0
  46. package/src/frontend/test-setup.ts +1 -0
  47. package/src/frontend/tour/TourContext.tsx +161 -0
  48. package/src/frontend/tour/tourSteps.ts +181 -0
  49. package/src/frontend/tour/tourStyles.css +116 -0
  50. package/src/frontend/types.ts +239 -0
  51. package/src/frontend/utils/formatTime.test.ts +83 -0
  52. package/src/frontend/utils/formatTime.ts +23 -0
  53. package/tsconfig.json +23 -0
  54. package/vite.config.ts +26 -0
  55. package/vitest.config.ts +17 -0
  56. 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
+ }