free-coding-models 0.5.14 β 0.5.16
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/changelog/v0.5.12.md +86 -2
- package/changelog/v0.5.13.md +19 -53
- package/changelog/v0.5.15.md +17 -0
- package/changelog/v0.5.16.md +15 -0
- package/package.json +4 -3
- package/src/tui/app.js +16 -4
- package/src/tui/cli-help.js +87 -0
- package/src/tui/key-handler.js +2 -14
- package/src/tui/render-table.js +46 -26
- package/src/tui/theme.js +70 -0
- package/web/README.md +1 -1
- package/web/dist/assets/index-avN2GM3H.css +1 -0
- package/web/dist/assets/index-vsysCImz.js +41 -0
- package/web/dist/index.html +6 -2
- package/web/index.html +4 -0
- package/web/server.js +249 -15
- package/web/src/App.jsx +46 -15
- package/web/src/components/dashboard/DetailPanel.jsx +1 -1
- package/web/src/components/dashboard/DetailPanel.module.css +5 -0
- package/web/src/components/dashboard/ExpandedDetailRow.jsx +464 -0
- package/web/src/components/dashboard/ExpandedDetailRow.module.css +347 -0
- package/web/src/components/dashboard/FilterBar.jsx +98 -58
- package/web/src/components/dashboard/FilterBar.module.css +103 -33
- package/web/src/components/dashboard/ModelTable.jsx +49 -29
- package/web/src/components/dashboard/ModelTable.module.css +12 -0
- package/web/src/components/dashboard/ProviderDropdown.jsx +156 -0
- package/web/src/components/dashboard/ProviderDropdown.module.css +248 -0
- package/web/src/components/help/HelpView.jsx +13 -0
- package/web/src/components/playground/PlaygroundView.jsx +193 -12
- package/web/src/components/playground/PlaygroundView.module.css +82 -0
- package/web/src/components/router/RouterView.jsx +15 -6
- package/web/vite.config.js +1 -1
- package/web/dist/assets/index-Bzqz7KbT.js +0 -39
- package/web/dist/assets/index-H9JWDRIh.css +0 -1
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/dashboard/ExpandedDetailRow.jsx
|
|
3
|
+
* @description Expandable 3-column detail row rendered below a model's table row.
|
|
4
|
+
* π Left column: key stats (tier, SWE, context, provider, status, avg ping, stability, verdict, uptime)
|
|
5
|
+
* with favorite toggle + launch button at the bottom.
|
|
6
|
+
* π Center column: mini chat playground β sends a user message to the model via SSE streaming
|
|
7
|
+
* and displays the response in real time.
|
|
8
|
+
* π Right column: AI latency benchmark β POSTs to /api/benchmark-stream, parses SSE events
|
|
9
|
+
* (start/token/done/error), and shows live metrics (latency, tokens, TPS), a progress bar,
|
|
10
|
+
* and the streaming generated text preview.
|
|
11
|
+
* π When the model has no API key configured, a banner is shown instead of the 3 columns.
|
|
12
|
+
* @functions ExpandedDetailRow β main component
|
|
13
|
+
*/
|
|
14
|
+
import { useState, useRef, useCallback } from 'react'
|
|
15
|
+
import { IconSend, IconPlayerPlayFilled, IconStar, IconStarFilled, IconLoader } from '@tabler/icons-react'
|
|
16
|
+
import TierBadge from '../atoms/TierBadge.jsx'
|
|
17
|
+
import VerdictBadge from '../atoms/VerdictBadge.jsx'
|
|
18
|
+
import StatusDot from '../atoms/StatusDot.jsx'
|
|
19
|
+
import StabilityCell from '../atoms/StabilityCell.jsx'
|
|
20
|
+
import { formatAvg, pingClass } from '../../utils/format.js'
|
|
21
|
+
import { sweClass } from '../../utils/ranks.js'
|
|
22
|
+
import LaunchButton from '../launch/LaunchButton.jsx'
|
|
23
|
+
import styles from './ExpandedDetailRow.module.css'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Single stat item with label + value, used in the info column grid.
|
|
27
|
+
* @param {string} label - Uppercase stat label
|
|
28
|
+
* @param {React.ReactNode} children - Stat value content
|
|
29
|
+
*/
|
|
30
|
+
function StatItem({ label, children }) {
|
|
31
|
+
return (
|
|
32
|
+
<div className={styles.statItem}>
|
|
33
|
+
<span className={styles.statLabel}>{label}</span>
|
|
34
|
+
<span className={styles.statValue}>{children}</span>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Expandable detail row with 3 columns: Info, Mini Playground, AI Latency.
|
|
41
|
+
*
|
|
42
|
+
* @param {Object} props
|
|
43
|
+
* @param {Object} props.model - The model data object
|
|
44
|
+
* @param {Object} props.favorites - Favorites controller (isFavorite, toggle)
|
|
45
|
+
* @param {Function} [props.onBenchmark] - Benchmark callback
|
|
46
|
+
* @param {Function} [props.onLaunch] - Launch callback
|
|
47
|
+
* @param {Function} [props.onToast] - Toast notification callback
|
|
48
|
+
* @param {string} [props.toolMode='opencode'] - Current tool mode
|
|
49
|
+
* @param {Function} [props.onSetToolMode] - Set tool mode handler
|
|
50
|
+
* @param {Function} [props.onCycleToolMode] - Cycle tool mode handler
|
|
51
|
+
* @param {Function} [props.onOpenFallback] - Open fallback tool handler
|
|
52
|
+
*/
|
|
53
|
+
export default function ExpandedDetailRow({
|
|
54
|
+
model,
|
|
55
|
+
favorites,
|
|
56
|
+
onBenchmark,
|
|
57
|
+
onLaunch,
|
|
58
|
+
onToast,
|
|
59
|
+
toolMode = 'opencode',
|
|
60
|
+
onSetToolMode,
|
|
61
|
+
onCycleToolMode,
|
|
62
|
+
onOpenFallback,
|
|
63
|
+
}) {
|
|
64
|
+
// βββ Mini Playground state βββ
|
|
65
|
+
const [playgroundInput, setPlaygroundInput] = useState('')
|
|
66
|
+
const [playgroundResponse, setPlaygroundResponse] = useState('')
|
|
67
|
+
const [playgroundBusy, setPlaygroundBusy] = useState(false)
|
|
68
|
+
const playgroundAbort = useRef(null)
|
|
69
|
+
|
|
70
|
+
// βββ AI Latency benchmark state βββ
|
|
71
|
+
const [benchState, setBenchState] = useState('idle') // 'idle' | 'running' | 'done' | 'error'
|
|
72
|
+
const [benchMetrics, setBenchMetrics] = useState({ latency: null, tokens: null, tps: null })
|
|
73
|
+
const [benchText, setBenchText] = useState('')
|
|
74
|
+
const [benchProgress, setBenchProgress] = useState(0)
|
|
75
|
+
const benchAbort = useRef(null)
|
|
76
|
+
|
|
77
|
+
if (!model) return null
|
|
78
|
+
|
|
79
|
+
const isFav = favorites?.isFavorite(model) ?? false
|
|
80
|
+
const avgData = formatAvg(model.avg)
|
|
81
|
+
const avgCls = avgData.cls || pingClass(model.avg)
|
|
82
|
+
|
|
83
|
+
// βββ Mini Playground: send a chat message βββ
|
|
84
|
+
const handlePlaygroundSend = useCallback(async () => {
|
|
85
|
+
const text = playgroundInput.trim()
|
|
86
|
+
if (!text || playgroundBusy) return
|
|
87
|
+
|
|
88
|
+
setPlaygroundBusy(true)
|
|
89
|
+
setPlaygroundResponse('')
|
|
90
|
+
setPlaygroundInput('')
|
|
91
|
+
|
|
92
|
+
// Cancel any previous request
|
|
93
|
+
if (playgroundAbort.current) playgroundAbort.current.abort()
|
|
94
|
+
const controller = new AbortController()
|
|
95
|
+
playgroundAbort.current = controller
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const res = await fetch('/api/playground/chat', {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
model: `${model.providerKey}/${model.modelId}`,
|
|
103
|
+
messages: [{ role: 'user', content: text }],
|
|
104
|
+
stream: true,
|
|
105
|
+
temperature: 0.7,
|
|
106
|
+
}),
|
|
107
|
+
signal: controller.signal,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
const errText = await res.text().catch(() => `HTTP ${res.status}`)
|
|
112
|
+
setPlaygroundResponse(`Error: ${errText}`)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const reader = res.body.getReader()
|
|
117
|
+
const decoder = new TextDecoder()
|
|
118
|
+
let accumulated = ''
|
|
119
|
+
|
|
120
|
+
while (true) {
|
|
121
|
+
const { done, value } = await reader.read()
|
|
122
|
+
if (done) break
|
|
123
|
+
|
|
124
|
+
const chunk = decoder.decode(value, { stream: true })
|
|
125
|
+
// Parse SSE lines
|
|
126
|
+
const lines = chunk.split('\n')
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
if (!line.startsWith('data: ')) continue
|
|
129
|
+
const payload = line.slice(6).trim()
|
|
130
|
+
if (payload === '[DONE]') continue
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const json = JSON.parse(payload)
|
|
134
|
+
const content = json.choices?.[0]?.delta?.content
|
|
135
|
+
if (content) {
|
|
136
|
+
accumulated += content
|
|
137
|
+
setPlaygroundResponse(accumulated)
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// Non-JSON line β ignore
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (err.name !== 'AbortError') {
|
|
146
|
+
setPlaygroundResponse(`Error: ${err.message}`)
|
|
147
|
+
}
|
|
148
|
+
} finally {
|
|
149
|
+
setPlaygroundBusy(false)
|
|
150
|
+
playgroundAbort.current = null
|
|
151
|
+
}
|
|
152
|
+
}, [playgroundInput, playgroundBusy, model.providerKey, model.modelId])
|
|
153
|
+
|
|
154
|
+
// βββ AI Latency: run benchmark βββ
|
|
155
|
+
const handleBenchStart = useCallback(async () => {
|
|
156
|
+
if (benchState === 'running') return
|
|
157
|
+
|
|
158
|
+
setBenchState('running')
|
|
159
|
+
setBenchMetrics({ latency: null, tokens: null, tps: null })
|
|
160
|
+
setBenchText('')
|
|
161
|
+
setBenchProgress(0)
|
|
162
|
+
|
|
163
|
+
// Cancel any previous request
|
|
164
|
+
if (benchAbort.current) benchAbort.current.abort()
|
|
165
|
+
const controller = new AbortController()
|
|
166
|
+
benchAbort.current = controller
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const res = await fetch('/api/benchmark-stream', {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: { 'Content-Type': 'application/json' },
|
|
172
|
+
body: JSON.stringify({ providerKey: model.providerKey, modelId: model.modelId }),
|
|
173
|
+
signal: controller.signal,
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
const errText = await res.text().catch(() => `HTTP ${res.status}`)
|
|
178
|
+
setBenchState('error')
|
|
179
|
+
setBenchText(`Error: ${errText}`)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const reader = res.body.getReader()
|
|
184
|
+
const decoder = new TextDecoder()
|
|
185
|
+
let buffer = ''
|
|
186
|
+
let accumulatedText = ''
|
|
187
|
+
|
|
188
|
+
while (true) {
|
|
189
|
+
const { done, value } = await reader.read()
|
|
190
|
+
if (done) break
|
|
191
|
+
|
|
192
|
+
buffer += decoder.decode(value, { stream: true })
|
|
193
|
+
const lines = buffer.split('\n')
|
|
194
|
+
// Keep the last potentially incomplete line in the buffer
|
|
195
|
+
buffer = lines.pop() || ''
|
|
196
|
+
|
|
197
|
+
for (const line of lines) {
|
|
198
|
+
const trimmed = line.trim()
|
|
199
|
+
if (!trimmed) continue
|
|
200
|
+
|
|
201
|
+
if (trimmed.startsWith('event: ')) {
|
|
202
|
+
// Event type line β we use data lines for actual content
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (trimmed.startsWith('data: ')) {
|
|
207
|
+
const payload = trimmed.slice(6).trim()
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const json = JSON.parse(payload)
|
|
211
|
+
const eventType = json.type || json.event
|
|
212
|
+
|
|
213
|
+
if (eventType === 'token' || json.token !== undefined) {
|
|
214
|
+
// Live token β update streaming text and metrics
|
|
215
|
+
if (json.text) {
|
|
216
|
+
accumulatedText += json.text
|
|
217
|
+
setBenchText(accumulatedText)
|
|
218
|
+
}
|
|
219
|
+
if (json.totalMs != null) {
|
|
220
|
+
setBenchMetrics(prev => ({
|
|
221
|
+
...prev,
|
|
222
|
+
latency: (json.totalMs / 1000).toFixed(2),
|
|
223
|
+
}))
|
|
224
|
+
}
|
|
225
|
+
if (json.tokens != null) {
|
|
226
|
+
setBenchMetrics(prev => ({ ...prev, tokens: json.tokens }))
|
|
227
|
+
setBenchProgress(Math.min(100, Math.round((json.tokens / 140) * 100)))
|
|
228
|
+
}
|
|
229
|
+
if (json.tps != null) {
|
|
230
|
+
setBenchMetrics(prev => ({ ...prev, tps: json.tps.toFixed(1) }))
|
|
231
|
+
}
|
|
232
|
+
} else if (eventType === 'done') {
|
|
233
|
+
// Benchmark complete
|
|
234
|
+
if (json.totalMs != null) {
|
|
235
|
+
setBenchMetrics(prev => ({
|
|
236
|
+
...prev,
|
|
237
|
+
latency: (json.totalMs / 1000).toFixed(2),
|
|
238
|
+
}))
|
|
239
|
+
}
|
|
240
|
+
if (json.outputTokens != null) {
|
|
241
|
+
setBenchMetrics(prev => ({ ...prev, tokens: json.outputTokens }))
|
|
242
|
+
}
|
|
243
|
+
if (json.tokensPerSecond != null) {
|
|
244
|
+
setBenchMetrics(prev => ({
|
|
245
|
+
...prev,
|
|
246
|
+
tps: json.tokensPerSecond.toFixed(1),
|
|
247
|
+
}))
|
|
248
|
+
}
|
|
249
|
+
setBenchProgress(100)
|
|
250
|
+
setBenchState('done')
|
|
251
|
+
} else if (eventType === 'error') {
|
|
252
|
+
setBenchState('error')
|
|
253
|
+
setBenchText(json.message || json.error || 'Unknown error')
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
// Non-JSON payload β ignore
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch (err) {
|
|
262
|
+
if (err.name !== 'AbortError') {
|
|
263
|
+
setBenchState('error')
|
|
264
|
+
setBenchText(`Error: ${err.message}`)
|
|
265
|
+
}
|
|
266
|
+
} finally {
|
|
267
|
+
if (benchState === 'running') {
|
|
268
|
+
setBenchState('done')
|
|
269
|
+
}
|
|
270
|
+
benchAbort.current = null
|
|
271
|
+
}
|
|
272
|
+
}, [benchState, model.providerKey, model.modelId])
|
|
273
|
+
|
|
274
|
+
// βββ No API key β show banner βββ
|
|
275
|
+
if (!model.hasApiKey) {
|
|
276
|
+
return (
|
|
277
|
+
<div className={styles.row}>
|
|
278
|
+
<div className={styles.noKeyBanner}>
|
|
279
|
+
<span className={styles.noKeyIcon}>π</span>
|
|
280
|
+
<span>No API key configured for {model.origin}. Open Settings (P) to add one.</span>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<div className={styles.row}>
|
|
288
|
+
<div className={styles.grid}>
|
|
289
|
+
|
|
290
|
+
{/* βββββββ Column 1: Info βββββββ */}
|
|
291
|
+
<div className={styles.col}>
|
|
292
|
+
<div className={styles.colTitle}>π Model Info</div>
|
|
293
|
+
<div className={styles.statGrid}>
|
|
294
|
+
<StatItem label="Tier">
|
|
295
|
+
<TierBadge tier={model.tier} />
|
|
296
|
+
</StatItem>
|
|
297
|
+
<StatItem label="SWE-bench">
|
|
298
|
+
<span className={styles[sweClass(model.sweScore)]}>
|
|
299
|
+
{model.sweScore || 'β'}
|
|
300
|
+
</span>
|
|
301
|
+
</StatItem>
|
|
302
|
+
<StatItem label="Context">
|
|
303
|
+
{model.ctx || 'β'}
|
|
304
|
+
</StatItem>
|
|
305
|
+
<StatItem label="Provider">
|
|
306
|
+
{model.origin}
|
|
307
|
+
</StatItem>
|
|
308
|
+
<StatItem label="Status">
|
|
309
|
+
<StatusDot status={model.status} />
|
|
310
|
+
<span style={{ fontSize: 11 }}>{model.status}</span>
|
|
311
|
+
</StatItem>
|
|
312
|
+
<StatItem label="Avg Ping">
|
|
313
|
+
<span className={styles[avgCls]}>
|
|
314
|
+
{avgData.text}
|
|
315
|
+
</span>
|
|
316
|
+
</StatItem>
|
|
317
|
+
<StatItem label="Stability">
|
|
318
|
+
<StabilityCell score={model.stability} />
|
|
319
|
+
</StatItem>
|
|
320
|
+
<StatItem label="Verdict">
|
|
321
|
+
<VerdictBadge verdict={model.verdict} httpCode={model.httpCode} />
|
|
322
|
+
</StatItem>
|
|
323
|
+
<StatItem label="Uptime">
|
|
324
|
+
{model.uptime > 0 ? `${model.uptime}%` : 'β'}
|
|
325
|
+
</StatItem>
|
|
326
|
+
</div>
|
|
327
|
+
<div className={styles.infoActions}>
|
|
328
|
+
{favorites && (
|
|
329
|
+
<button
|
|
330
|
+
className={`${styles.favBtn} ${isFav ? styles.favBtnActive : ''}`}
|
|
331
|
+
onClick={() => favorites.toggle(model)}
|
|
332
|
+
title={isFav ? `Unfavorite ${model.label}` : `Favorite ${model.label}`}
|
|
333
|
+
>
|
|
334
|
+
{isFav
|
|
335
|
+
? <IconStarFilled size={13} stroke={1.5} />
|
|
336
|
+
: <IconStar size={13} stroke={1.5} />
|
|
337
|
+
}
|
|
338
|
+
<span>{isFav ? 'Favorited' : 'Favorite'}</span>
|
|
339
|
+
</button>
|
|
340
|
+
)}
|
|
341
|
+
{onLaunch && (
|
|
342
|
+
<LaunchButton
|
|
343
|
+
model={model}
|
|
344
|
+
toolMode={toolMode}
|
|
345
|
+
onLaunch={onLaunch}
|
|
346
|
+
variant="default"
|
|
347
|
+
/>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
{/* βββββββ Column 2: Mini Playground βββββββ */}
|
|
353
|
+
<div className={styles.col}>
|
|
354
|
+
<div className={styles.playgroundHeader}>
|
|
355
|
+
π¬ Mini Playground β {model.label}
|
|
356
|
+
</div>
|
|
357
|
+
<div className={styles.inputRow}>
|
|
358
|
+
<input
|
|
359
|
+
type="text"
|
|
360
|
+
className={styles.inputField}
|
|
361
|
+
placeholder="Type a messageβ¦"
|
|
362
|
+
value={playgroundInput}
|
|
363
|
+
onChange={e => setPlaygroundInput(e.target.value)}
|
|
364
|
+
onKeyDown={e => {
|
|
365
|
+
if (e.key === 'Enter') handlePlaygroundSend()
|
|
366
|
+
}}
|
|
367
|
+
disabled={playgroundBusy}
|
|
368
|
+
/>
|
|
369
|
+
<button
|
|
370
|
+
className={styles.sendBtn}
|
|
371
|
+
onClick={handlePlaygroundSend}
|
|
372
|
+
disabled={playgroundBusy || !playgroundInput.trim()}
|
|
373
|
+
title="Send message"
|
|
374
|
+
>
|
|
375
|
+
{playgroundBusy
|
|
376
|
+
? <IconLoader size={14} stroke={1.8} className={styles.spinning} />
|
|
377
|
+
: <IconSend size={14} stroke={1.8} />
|
|
378
|
+
}
|
|
379
|
+
</button>
|
|
380
|
+
</div>
|
|
381
|
+
<div className={styles.responseArea}>
|
|
382
|
+
{playgroundBusy && !playgroundResponse ? (
|
|
383
|
+
<span className={styles.responsePlaceholder}>
|
|
384
|
+
Waiting for responseβ¦
|
|
385
|
+
</span>
|
|
386
|
+
) : playgroundResponse ? (
|
|
387
|
+
playgroundResponse
|
|
388
|
+
) : (
|
|
389
|
+
<span className={styles.responsePlaceholder}>
|
|
390
|
+
Send a message to test this model. Responses stream in real time.
|
|
391
|
+
</span>
|
|
392
|
+
)}
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
{/* βββββββ Column 3: AI Latency βββββββ */}
|
|
397
|
+
<div className={styles.col}>
|
|
398
|
+
<button
|
|
399
|
+
className={styles.benchBtn}
|
|
400
|
+
onClick={handleBenchStart}
|
|
401
|
+
disabled={benchState === 'running'}
|
|
402
|
+
title={benchState === 'running' ? 'Running benchmarkβ¦' : 'Test AI Latency'}
|
|
403
|
+
>
|
|
404
|
+
{benchState === 'running' ? (
|
|
405
|
+
<IconLoader size={13} stroke={1.8} className={styles.spinning} />
|
|
406
|
+
) : (
|
|
407
|
+
<IconPlayerPlayFilled size={13} stroke={1.8} />
|
|
408
|
+
)}
|
|
409
|
+
<span>{benchState === 'running' ? 'Runningβ¦' : 'Test AI Latency'}</span>
|
|
410
|
+
</button>
|
|
411
|
+
|
|
412
|
+
{/* Live metrics grid */}
|
|
413
|
+
<div className={styles.metricsGrid}>
|
|
414
|
+
<div className={styles.metricCard}>
|
|
415
|
+
<span className={styles.metricLabel}>Latency</span>
|
|
416
|
+
<span className={styles.metricValue}>
|
|
417
|
+
{benchMetrics.latency != null ? `${benchMetrics.latency}s` : 'β'}
|
|
418
|
+
</span>
|
|
419
|
+
</div>
|
|
420
|
+
<div className={styles.metricCard}>
|
|
421
|
+
<span className={styles.metricLabel}>Tokens</span>
|
|
422
|
+
<span className={styles.metricValue}>
|
|
423
|
+
{benchMetrics.tokens != null ? benchMetrics.tokens : 'β'}
|
|
424
|
+
</span>
|
|
425
|
+
</div>
|
|
426
|
+
<div className={styles.metricCard}>
|
|
427
|
+
<span className={styles.metricLabel}>TPS</span>
|
|
428
|
+
<span className={styles.metricValue}>
|
|
429
|
+
{benchMetrics.tps != null ? benchMetrics.tps : 'β'}
|
|
430
|
+
</span>
|
|
431
|
+
</div>
|
|
432
|
+
<div className={styles.metricCard}>
|
|
433
|
+
<span className={styles.metricLabel}>Status</span>
|
|
434
|
+
<span className={styles.metricValue}>
|
|
435
|
+
{benchState === 'idle' && 'β³'}
|
|
436
|
+
{benchState === 'running' && 'β‘'}
|
|
437
|
+
{benchState === 'done' && 'β
'}
|
|
438
|
+
{benchState === 'error' && 'β'}
|
|
439
|
+
</span>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
{/* Progress bar */}
|
|
444
|
+
<div className={styles.progressBar}>
|
|
445
|
+
<div
|
|
446
|
+
className={styles.progressFill}
|
|
447
|
+
style={{ width: `${benchProgress}%` }}
|
|
448
|
+
/>
|
|
449
|
+
</div>
|
|
450
|
+
|
|
451
|
+
{/* Streaming text preview */}
|
|
452
|
+
<div className={styles.streamText}>
|
|
453
|
+
{benchText || (
|
|
454
|
+
<span className={styles.responsePlaceholder}>
|
|
455
|
+
Generated text will appear hereβ¦
|
|
456
|
+
</span>
|
|
457
|
+
)}
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
)
|
|
464
|
+
}
|