free-coding-models 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/bin/free-coding-models.js +19 -9
- package/changelog/v0.5.0.md +15 -0
- package/changelog/v0.5.1.md +24 -0
- package/package.json +7 -2
- package/src/{analysis.js → core/analysis.js} +5 -5
- package/src/{constants.js → core/constants.js} +1 -1
- package/src/{endpoint-installer.js → core/endpoint-installer.js} +1 -1
- package/src/{installed-models-manager.js → core/installed-models-manager.js} +1 -1
- package/src/{kilo.js → core/kilo.js} +1 -2
- package/src/{openclaw.js → core/openclaw.js} +1 -1
- package/src/{opencode.js → core/opencode.js} +2 -1
- package/src/{ping-loop.js → core/ping-loop.js} +1 -1
- package/src/{router-daemon.js → core/router-daemon.js} +169 -4
- package/src/{router-dashboard.js → core/router-dashboard.js} +2 -2
- package/src/{setup.js → core/setup.js} +1 -1
- package/src/{sync-set.js → core/sync-set.js} +1 -1
- package/src/{telemetry.js → core/telemetry.js} +1 -1
- package/src/{tool-launchers.js → core/tool-launchers.js} +2 -2
- package/src/{updater.js → core/updater.js} +1 -1
- package/src/{utils.js → core/utils.js} +2 -0
- package/src/{app.js → tui/app.js} +38 -38
- package/src/{cli-help.js → tui/cli-help.js} +3 -1
- package/src/{command-palette.js → tui/command-palette.js} +2 -2
- package/src/{key-handler.js → tui/key-handler.js} +11 -11
- package/src/{overlays.js → tui/overlays.js} +2 -2
- package/src/{render-helpers.js → tui/render-helpers.js} +2 -2
- package/src/{render-table.js → tui/render-table.js} +9 -9
- package/src/{tui-filters.js → tui/tui-filters.js} +3 -3
- package/src/{tui-state.js → tui/tui-state.js} +1 -1
- package/web/README.md +46 -0
- package/web/dist/assets/index-ByGf4Kq-.js +14 -0
- package/web/dist/assets/index-Ds7wmHBv.css +1 -0
- package/web/dist/index.html +3 -6
- package/web/index.html +1 -4
- package/web/package.json +11 -0
- package/web/server.js +609 -214
- package/web/src/App.jsx +54 -12
- package/web/src/components/analytics/AnalyticsView.jsx +10 -4
- package/web/src/components/atoms/AILatencyCell.jsx +38 -0
- package/web/src/components/atoms/AILatencyCell.module.css +43 -0
- package/web/src/components/atoms/HealthCell.jsx +53 -0
- package/web/src/components/atoms/HealthCell.module.css +15 -0
- package/web/src/components/atoms/LastPingCell.jsx +35 -0
- package/web/src/components/atoms/LastPingCell.module.css +35 -0
- package/web/src/components/atoms/MoodCell.jsx +25 -0
- package/web/src/components/atoms/MoodCell.module.css +6 -0
- package/web/src/components/atoms/RankCell.jsx +9 -0
- package/web/src/components/atoms/RankCell.module.css +9 -0
- package/web/src/components/atoms/TPSCell.jsx +36 -0
- package/web/src/components/atoms/TPSCell.module.css +38 -0
- package/web/src/components/atoms/VerdictBadge.jsx +30 -7
- package/web/src/components/atoms/VerdictBadge.module.css +24 -15
- package/web/src/components/dashboard/ExportModal.jsx +9 -4
- package/web/src/components/dashboard/FilterBar.jsx +112 -10
- package/web/src/components/dashboard/FilterBar.module.css +86 -1
- package/web/src/components/dashboard/ModelTable.jsx +293 -52
- package/web/src/components/dashboard/ModelTable.module.css +131 -33
- package/web/src/components/dashboard/StatsBar.jsx +7 -5
- package/web/src/components/layout/Footer.jsx +1 -1
- package/web/src/components/layout/Header.jsx +43 -9
- package/web/src/components/layout/Header.module.css +38 -4
- package/web/src/components/layout/Sidebar.jsx +19 -11
- package/web/src/components/layout/Sidebar.module.css +15 -5
- package/web/src/components/settings/SettingsView.jsx +24 -6
- package/web/src/components/settings/SettingsView.module.css +0 -1
- package/web/src/global.css +70 -73
- package/web/src/hooks/useFilter.js +117 -25
- package/web/src/hooks/useSSE.js +33 -9
- package/web/src/hooks/useSocket.js +200 -0
- package/web/vite.config.js +41 -0
- package/src/graphify-out/cache/089db1c1def873cf6d112f1590da4490e61e691aff0db41e006aa2fb15ba0656.json +0 -1
- package/src/graphify-out/cache/0b510b53cf1a1393fb52b1fc3bbbf88b63938e961ec5b82119a2e9715fee8bd7.json +0 -1
- package/src/graphify-out/cache/0ec9a95a326bde58e0316889018b278062d06d494d0f31ba177c9de71e5fed2d.json +0 -1
- package/src/graphify-out/cache/1548663a24a68dce740ebab1bd1d3091048c9604e9d067a1650a42a6d82541d4.json +0 -1
- package/src/graphify-out/cache/1783af63cb6d0dfb4d469009f71ac83a74ba0b33d48186ff2c6e63f9429e900a.json +0 -1
- package/src/graphify-out/cache/1e109f5eb5dc4fd285871c3613e32b6b14a8c225f4080ee34b51c7e1a1764571.json +0 -1
- package/src/graphify-out/cache/1eb24dbeb69b46c8bc1caf925df2f2a964af0f33aea143adf8ddf88e017db6ca.json +0 -1
- package/src/graphify-out/cache/21e1bcfed11685e8347243f9d8516072dda183266a4bfe22c52fb31753a446c8.json +0 -1
- package/src/graphify-out/cache/2327473478b9c4b1940bf7ef66c9ee960b3cba8d5302e56b625df8274246e0b4.json +0 -1
- package/src/graphify-out/cache/25955b81fd25454c8fa90fb71a47db8d1215cf621beb8ff3cbd580aaf011b4f3.json +0 -1
- package/src/graphify-out/cache/2739677f19c702f88f3de0a0bac475066adbda98709907ad3de967aef689f86d.json +0 -1
- package/src/graphify-out/cache/2bba03422f6b3ee7f5b5d29cc90314a064d259e5822a176657bda3e04505cf00.json +0 -1
- package/src/graphify-out/cache/2ddf1d2c6d10147b0402446bc71a7988187b79b6210dd7e7250be8c555b9ff35.json +0 -1
- package/src/graphify-out/cache/2ee07457a5767c95a57f8e9eb95b28f800044f35666e0715e9d88ad1103a092e.json +0 -1
- package/src/graphify-out/cache/2fe9f75dc2951c417f2c8dd22749092cf550dc67599f1c8d1866900dc6e9154e.json +0 -1
- package/src/graphify-out/cache/41c4b7c27e7fc3e2948d3a4bf95a72de2ed9a6f0463994babdce8ed2cc84598c.json +0 -1
- package/src/graphify-out/cache/5028defd54b7fbd3c7e444973e493de036e097e9b1d2a7cae7f19b88d68aacde.json +0 -1
- package/src/graphify-out/cache/5b133aba3fb16410c5b1fdbd1730039fc7fa1ac93abd99d7be08f60da70fc8d4.json +0 -1
- package/src/graphify-out/cache/74252e5b0978d85ab3421a3de1a9384aa282ffd2be2cfe7db2530139089f4275.json +0 -1
- package/src/graphify-out/cache/7695ebeea056095edd14332963cc43354ef3a097caf46f1e28d0f01369642901.json +0 -1
- package/src/graphify-out/cache/777aa7085c395a935c6556bbde182cd871edb61f3a685ed8068ec0c8f6fb0075.json +0 -1
- package/src/graphify-out/cache/82a723881980e82273c113def8315533d7da28827e300413d9ad30f27b7407df.json +0 -1
- package/src/graphify-out/cache/86b87c9603e6cd188f42c7eed3b86c291d48a781c223a707e74f3e7ed0c02a21.json +0 -1
- package/src/graphify-out/cache/890fead9a78cadaed560a2d2453916121fa605c3e43a334910ac4bc951a9ef6d.json +0 -1
- package/src/graphify-out/cache/89d3ea66f52783caa775ef9a30923d7d6225e1d8ae9e962f4741b8c7785dab1e.json +0 -1
- package/src/graphify-out/cache/8cc82cd9edce41f0e1c092f14a94fd52bf847addf3237b616dc5a9e505bd05bd.json +0 -1
- package/src/graphify-out/cache/93ba2e25e3ff7ad525f397902345fbd375df7315de7b402e20cc803c14eccde8.json +0 -1
- package/src/graphify-out/cache/99beed29580b9c7bfecfee794cb3d8e535fcf0eb3b92113108f88bdd0a8e79b3.json +0 -1
- package/src/graphify-out/cache/aeeb931fa477c65ce2e51d8149957350fa54225c613222bbbe8448998d1afd3d.json +0 -1
- package/src/graphify-out/cache/baf91bef5b5ecb2a476433b6cc0c48c563c54ee2d07fc3c192e543685e3e7222.json +0 -1
- package/src/graphify-out/cache/bd98b94ac4e9b92b6336d47b26e0366b51a4eaf0711d722f05f98dfae23ab42b.json +0 -1
- package/src/graphify-out/cache/bfcb51e9328e9cbfbee4f6fee0f56635d7b03488addc9f6c4e4b190b70a73362.json +0 -1
- package/src/graphify-out/cache/c0d3dabeb093aa758c49eadf41b87ecc96a16c1449c2670aaf48cbfc891d8da6.json +0 -1
- package/src/graphify-out/cache/c20d6630236f473c1406068c3ae205853e649b216495c93dfec055dd222c55cf.json +0 -1
- package/src/graphify-out/cache/c22b9122816bebce0a2f79af41a986559d01e00163dbcd579c5755621b4cb483.json +0 -1
- package/src/graphify-out/cache/ca556ec14453ddb8f9e0c5a832dac90d77111b9bad5f8c2d80d272e2e7a06371.json +0 -1
- package/src/graphify-out/cache/d6dbc9135dfa35a756b3b09b06700e4bc229fdccba11bb963f2ba44028e0bbae.json +0 -1
- package/src/graphify-out/cache/e1cf71276f1779d0fa075f79bd7c8a9fd0b8eef6932ac043137451b7c7fa7cbe.json +0 -1
- package/src/graphify-out/cache/e4b3be14494467df2d2ed389bc4f18f099021cb5bc355b901fa88387b2d8b8a2.json +0 -1
- package/src/graphify-out/cache/eaea0dded097f6f9553b654220046c6ec0c9be592a5973d906564ee60af34e0d.json +0 -1
- package/src/graphify-out/cache/ef07d0cd2675d1f79d2a2fdbf3bc3319687638751e9ce89b0d0d97ed1cd9f7e1.json +0 -1
- package/src/graphify-out/cache/f81272d6eb8aaff9e96d5a1d9f06777db70ac3652a646b951ded51f79871d733.json +0 -1
- package/src/graphify-out/cache/f9619dd92186f75a6dbda937e0c606647153918524cdb5763f956e6ec2a9e386.json +0 -1
- package/src/graphify-out/cache/fd88b1b2ff4bfcae08559d9c2aaeeb9a3f1e2f5cd8928762c311196956c170a5.json +0 -1
- package/web/dist/assets/index-CGN-0_A0.css +0 -1
- package/web/dist/assets/index-Czwis3ab.js +0 -11
- /package/src/{benchmark.js → core/benchmark.js} +0 -0
- /package/src/{cache.js → core/cache.js} +0 -0
- /package/src/{changelog-loader.js → core/changelog-loader.js} +0 -0
- /package/src/{config.js → core/config.js} +0 -0
- /package/src/{favorites.js → core/favorites.js} +0 -0
- /package/src/{kilo-config.js → core/kilo-config.js} +0 -0
- /package/src/{legacy-proxy-cleanup.js → core/legacy-proxy-cleanup.js} +0 -0
- /package/src/{model-merger.js → core/model-merger.js} +0 -0
- /package/src/{opencode-config.js → core/opencode-config.js} +0 -0
- /package/src/{ping.js → core/ping.js} +0 -0
- /package/src/{product-flags.js → core/product-flags.js} +0 -0
- /package/src/{provider-metadata.js → core/provider-metadata.js} +0 -0
- /package/src/{provider-quota-fetchers.js → core/provider-quota-fetchers.js} +0 -0
- /package/src/{quota-capabilities.js → core/quota-capabilities.js} +0 -0
- /package/src/{security.js → core/security.js} +0 -0
- /package/src/{shell-env.js → core/shell-env.js} +0 -0
- /package/src/{testfcm.js → core/testfcm.js} +0 -0
- /package/src/{token-usage-reader.js → core/token-usage-reader.js} +0 -0
- /package/src/{tool-bootstrap.js → core/tool-bootstrap.js} +0 -0
- /package/src/{tool-metadata.js → core/tool-metadata.js} +0 -0
- /package/src/{usage-reader.js → core/usage-reader.js} +0 -0
- /package/src/{mouse.js → tui/mouse.js} +0 -0
- /package/src/{theme.js → tui/theme.js} +0 -0
- /package/src/{tier-colors.js → tui/tier-colors.js} +0 -0
- /package/src/{ui-config.js → tui/ui-config.js} +0 -0
package/web/src/App.jsx
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file web/src/App.jsx
|
|
3
|
-
* @description Root application component — orchestrates all views, layout,
|
|
3
|
+
* @description Root application component — orchestrates all views, layout, Socket.IO connection, and global state.
|
|
4
4
|
* 📖 Manages current view (dashboard/settings/analytics), theme toggle, search, filters,
|
|
5
|
-
* selected model for detail panel, export modal,
|
|
6
|
-
* Uses
|
|
5
|
+
* selected model for detail panel, export modal, toast notifications, ping mode, and benchmark.
|
|
6
|
+
* Uses useSocket for live data, useFilter for model filtering/sorting, useTheme for dark/light.
|
|
7
7
|
* @functions App → root component with all state and layout composition
|
|
8
8
|
*/
|
|
9
9
|
import { useState, useCallback, useEffect } from 'react'
|
|
10
|
-
import {
|
|
10
|
+
import { useSocket } from './hooks/useSocket.js'
|
|
11
11
|
import { useFilter } from './hooks/useFilter.js'
|
|
12
12
|
import { useTheme } from './hooks/useTheme.js'
|
|
13
13
|
import Header from './components/layout/Header.jsx'
|
|
14
14
|
import Sidebar from './components/layout/Sidebar.jsx'
|
|
15
15
|
import Footer from './components/layout/Footer.jsx'
|
|
16
|
-
import StatsBar from './components/dashboard/StatsBar.jsx'
|
|
17
16
|
import FilterBar from './components/dashboard/FilterBar.jsx'
|
|
18
17
|
import ModelTable from './components/dashboard/ModelTable.jsx'
|
|
19
18
|
import DetailPanel from './components/dashboard/DetailPanel.jsx'
|
|
@@ -26,7 +25,7 @@ import ToastContainer from './components/atoms/ToastContainer.jsx'
|
|
|
26
25
|
let toastIdCounter = 0
|
|
27
26
|
|
|
28
27
|
export default function App() {
|
|
29
|
-
const { models, connected } =
|
|
28
|
+
const { models, connected, nextPingAt, isPinging, pingMode, globalBenchmarkRunning, globalBenchmarkTotal, globalBenchmarkCompleted } = useSocket()
|
|
30
29
|
const { theme, toggle: toggleTheme } = useTheme()
|
|
31
30
|
const [currentView, setCurrentView] = useState('dashboard')
|
|
32
31
|
const [selectedModel, setSelectedModel] = useState(null)
|
|
@@ -51,6 +50,27 @@ export default function App() {
|
|
|
51
50
|
return Object.values(map).sort((a, b) => a.name.localeCompare(b.name))
|
|
52
51
|
})()
|
|
53
52
|
|
|
53
|
+
// ── Global AI Speed Benchmark (Ctrl+U equivalent) ──
|
|
54
|
+
const handleBenchmark = useCallback(async () => {
|
|
55
|
+
if (globalBenchmarkRunning) {
|
|
56
|
+
console.warn('[Benchmark] Global benchmark already in progress.')
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
await fetch('/api/global-benchmark', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
// 📖 Match the user's current table, not the hidden catalog. If filters/search
|
|
64
|
+
// 📖 hide a model, the benchmark intentionally skips it.
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
models: filtered.map((model) => ({ providerKey: model.providerKey, modelId: model.modelId })),
|
|
67
|
+
}),
|
|
68
|
+
})
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error('[Benchmark] Failed to start global benchmark:', err.message)
|
|
71
|
+
}
|
|
72
|
+
}, [filtered, globalBenchmarkRunning])
|
|
73
|
+
|
|
54
74
|
const addToast = useCallback((message, type = 'info') => {
|
|
55
75
|
const id = ++toastIdCounter
|
|
56
76
|
setToasts((prev) => [...prev, { id, message, type }])
|
|
@@ -60,13 +80,19 @@ export default function App() {
|
|
|
60
80
|
setToasts((prev) => prev.filter((t) => t.id !== id))
|
|
61
81
|
}, [])
|
|
62
82
|
|
|
63
|
-
const handleSelectModel = useCallback((
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}, [models])
|
|
83
|
+
const handleSelectModel = useCallback((model) => {
|
|
84
|
+
setSelectedModel(model)
|
|
85
|
+
}, [])
|
|
67
86
|
|
|
68
87
|
const handleCloseDetail = useCallback(() => setSelectedModel(null), [])
|
|
69
88
|
|
|
89
|
+
// ── Ping mode: sync with backend ──
|
|
90
|
+
const handlePingModeChange = useCallback(async (mode) => {
|
|
91
|
+
try {
|
|
92
|
+
await fetch(`/api/ping-mode?action=${mode}`, { method: 'POST' })
|
|
93
|
+
} catch {}
|
|
94
|
+
}, [])
|
|
95
|
+
|
|
70
96
|
useEffect(() => {
|
|
71
97
|
const handler = (e) => {
|
|
72
98
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
@@ -88,6 +114,7 @@ export default function App() {
|
|
|
88
114
|
currentView={currentView}
|
|
89
115
|
onNavigate={setCurrentView}
|
|
90
116
|
onToggleTheme={toggleTheme}
|
|
117
|
+
theme={theme}
|
|
91
118
|
/>
|
|
92
119
|
|
|
93
120
|
<div className="app-content">
|
|
@@ -99,8 +126,13 @@ export default function App() {
|
|
|
99
126
|
onToggleTheme={toggleTheme}
|
|
100
127
|
onOpenSettings={() => setCurrentView('settings')}
|
|
101
128
|
onOpenExport={() => setExportOpen(true)}
|
|
129
|
+
onBenchmark={handleBenchmark}
|
|
130
|
+
benchmarkRunning={globalBenchmarkRunning}
|
|
131
|
+
benchmarkTotal={globalBenchmarkTotal}
|
|
132
|
+
benchmarkCompleted={globalBenchmarkCompleted}
|
|
133
|
+
modelsCount={filtered.length}
|
|
134
|
+
theme={theme}
|
|
102
135
|
/>
|
|
103
|
-
<StatsBar models={models} />
|
|
104
136
|
<FilterBar
|
|
105
137
|
filterTier={filterTier}
|
|
106
138
|
setFilterTier={setFilterTier}
|
|
@@ -109,10 +141,20 @@ export default function App() {
|
|
|
109
141
|
filterProvider={filterProvider}
|
|
110
142
|
setFilterProvider={setFilterProvider}
|
|
111
143
|
providers={providers}
|
|
144
|
+
pingMode={pingMode}
|
|
145
|
+
setPingMode={handlePingModeChange}
|
|
146
|
+
nextPingAt={nextPingAt}
|
|
147
|
+
isPinging={isPinging}
|
|
148
|
+
globalBenchmarkRunning={globalBenchmarkRunning}
|
|
149
|
+
globalBenchmarkTotal={globalBenchmarkTotal}
|
|
150
|
+
globalBenchmarkCompleted={globalBenchmarkCompleted}
|
|
112
151
|
/>
|
|
113
152
|
<ModelTable
|
|
114
153
|
filtered={filtered}
|
|
115
154
|
onSelectModel={handleSelectModel}
|
|
155
|
+
sortColumn={sortColumn}
|
|
156
|
+
sortDirection={sortDirection}
|
|
157
|
+
onSort={toggleSort}
|
|
116
158
|
/>
|
|
117
159
|
</div>
|
|
118
160
|
)}
|
|
@@ -154,4 +196,4 @@ export default function App() {
|
|
|
154
196
|
<ToastContainer toasts={toasts} dismissToast={dismissToast} />
|
|
155
197
|
</>
|
|
156
198
|
)
|
|
157
|
-
}
|
|
199
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @functions AnalyticsView → renders the three analytics cards
|
|
6
6
|
*/
|
|
7
7
|
import { useMemo } from 'react'
|
|
8
|
+
import { IconActivity, IconTrophy } from '@tabler/icons-react'
|
|
8
9
|
import TierBadge from '../atoms/TierBadge.jsx'
|
|
9
10
|
import styles from './AnalyticsView.module.css'
|
|
10
11
|
|
|
@@ -40,7 +41,10 @@ export default function AnalyticsView({ models }) {
|
|
|
40
41
|
return (
|
|
41
42
|
<div className={styles.page}>
|
|
42
43
|
<div className={styles.pageHeader}>
|
|
43
|
-
<h1 className={styles.pageTitle}
|
|
44
|
+
<h1 className={styles.pageTitle}>
|
|
45
|
+
<IconActivity size={24} stroke={1.5} style={{ marginRight: 8, verticalAlign: 'middle' }} />
|
|
46
|
+
Analytics
|
|
47
|
+
</h1>
|
|
44
48
|
<p className={styles.pageSubtitle}>Real-time insights across all providers and models</p>
|
|
45
49
|
</div>
|
|
46
50
|
|
|
@@ -69,17 +73,19 @@ export default function AnalyticsView({ models }) {
|
|
|
69
73
|
</div>
|
|
70
74
|
|
|
71
75
|
<div className={styles.card}>
|
|
72
|
-
<h3 className={styles.cardTitle}
|
|
76
|
+
<h3 className={styles.cardTitle} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
77
|
+
<IconTrophy size={16} stroke={1.5} />
|
|
78
|
+
Fastest Models
|
|
79
|
+
</h3>
|
|
73
80
|
<div className={styles.cardBody}>
|
|
74
81
|
{leaderboard.length === 0 ? (
|
|
75
82
|
<div className={styles.empty}>Waiting for ping data...</div>
|
|
76
83
|
) : (
|
|
77
84
|
leaderboard.map((m, i) => {
|
|
78
85
|
const rankCls = i < 3 ? styles[`rank${i + 1}`] : ''
|
|
79
|
-
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : (i + 1)
|
|
80
86
|
return (
|
|
81
87
|
<div key={m.modelId} className={styles.leaderItem}>
|
|
82
|
-
<div className={`${styles.leaderRank} ${rankCls}`}>{
|
|
88
|
+
<div className={`${styles.leaderRank} ${rankCls}`}>{i + 1}</div>
|
|
83
89
|
<span className={styles.leaderName}>{m.label}</span>
|
|
84
90
|
<span className={styles.leaderLatency}>{m.avg}ms</span>
|
|
85
91
|
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/atoms/AILatencyCell.jsx
|
|
3
|
+
* @description Benchmark AI latency column — shows animated spinner during benchmarks.
|
|
4
|
+
* 📖 Shows: 4.3s, 12s, ERR, TIMEOUT. Retry badge ↻N in blue.
|
|
5
|
+
* 📖 When isRunning: animated CSS spinner + pulsing row highlight.
|
|
6
|
+
*/
|
|
7
|
+
import styles from './AILatencyCell.module.css'
|
|
8
|
+
|
|
9
|
+
function formatLatency(result, isRunning) {
|
|
10
|
+
if (isRunning) return { text: 'RUN', badge: '' }
|
|
11
|
+
if (!result || !result.ok) return { text: result?.code || '—', badge: '' }
|
|
12
|
+
const totalSec = result.totalMs / 1000
|
|
13
|
+
const badge = result.retries > 0 ? `↻${result.retries}` : ''
|
|
14
|
+
const text = totalSec >= 10 ? `${totalSec.toFixed(0)}s` : `${totalSec.toFixed(1)}s`
|
|
15
|
+
return { text, badge }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function AILatencyCell({ result, isRunning }) {
|
|
19
|
+
const { text, badge } = formatLatency(result, isRunning)
|
|
20
|
+
const ok = result?.ok
|
|
21
|
+
const colorCls = ok ? styles.fast : (result ? styles.slow : styles.dim)
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<span className={`${styles.cell} ${isRunning ? styles.runningCell : ''}`}>
|
|
25
|
+
{isRunning ? (
|
|
26
|
+
<>
|
|
27
|
+
<span className={styles.benchmarkSpinner} />
|
|
28
|
+
<span className={`${styles.value} ${styles.running}`}>{text}</span>
|
|
29
|
+
</>
|
|
30
|
+
) : (
|
|
31
|
+
<>
|
|
32
|
+
<span className={`${styles.value} ${colorCls}`}>{text}</span>
|
|
33
|
+
{badge && <span className={styles.badge}>{badge}</span>}
|
|
34
|
+
</>
|
|
35
|
+
)}
|
|
36
|
+
</span>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
.cell {
|
|
2
|
+
font-family: var(--font-mono);
|
|
3
|
+
font-size: 12px;
|
|
4
|
+
font-weight: 600;
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
gap: 4px;
|
|
8
|
+
min-width: 70px;
|
|
9
|
+
transition: background 200ms;
|
|
10
|
+
border-radius: 4px;
|
|
11
|
+
padding: 1px 3px;
|
|
12
|
+
}
|
|
13
|
+
.value { }
|
|
14
|
+
.fast { color: #00ff88; }
|
|
15
|
+
.medium { color: #ffaa00; }
|
|
16
|
+
.slow { color: #ff4444; }
|
|
17
|
+
.dim { color: var(--color-text-dim); }
|
|
18
|
+
.running { color: #b400ff; font-weight: 700; }
|
|
19
|
+
.badge { color: #06b6d4; font-size: 10px; }
|
|
20
|
+
|
|
21
|
+
/* ── Running/benchmarking row highlight ── */
|
|
22
|
+
.runningCell {
|
|
23
|
+
background: rgba(180, 0, 255, 0.12);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* ── Benchmark spinner: animated ring ── */
|
|
27
|
+
@keyframes benchSpin {
|
|
28
|
+
to { transform: rotate(360deg); }
|
|
29
|
+
}
|
|
30
|
+
@keyframes benchPulse {
|
|
31
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(180, 0, 255, 0.4); }
|
|
32
|
+
50% { box-shadow: 0 0 0 4px rgba(180, 0, 255, 0); }
|
|
33
|
+
}
|
|
34
|
+
.benchmarkSpinner {
|
|
35
|
+
display: inline-block;
|
|
36
|
+
width: 12px;
|
|
37
|
+
height: 12px;
|
|
38
|
+
border: 2px solid rgba(180, 0, 255, 0.3);
|
|
39
|
+
border-top-color: #b400ff;
|
|
40
|
+
border-radius: 50%;
|
|
41
|
+
animation: benchSpin 0.6s linear infinite;
|
|
42
|
+
flex-shrink: 0;
|
|
43
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/atoms/HealthCell.jsx
|
|
3
|
+
* @description Detailed health/status column — matches CLI Health column.
|
|
4
|
+
* 📖 Shows: ✅ UP, 🔑 NO KEY, 🔐 AUTH FAIL, ⏳ wait, ⏳ TIMEOUT,
|
|
5
|
+
* 🔥 429 TRY LATER, 🚫 404, 💥 500, 🔌 502, 🔒 503, ⏰ 504, ❌ ERROR.
|
|
6
|
+
*/
|
|
7
|
+
import styles from './HealthCell.module.css'
|
|
8
|
+
|
|
9
|
+
const ERROR_LABELS = {
|
|
10
|
+
'404': '404 NOT FOUND',
|
|
11
|
+
'410': '410 GONE',
|
|
12
|
+
'429': '429 TRY LATER',
|
|
13
|
+
'500': '500 ERROR',
|
|
14
|
+
'502': '502 ERROR',
|
|
15
|
+
'503': '503 ERROR',
|
|
16
|
+
'504': '504 TIMEOUT',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const STATUS_EMOJI = {
|
|
20
|
+
up: '✅',
|
|
21
|
+
timeout: '⏳',
|
|
22
|
+
down: '❌',
|
|
23
|
+
pending: '⏳',
|
|
24
|
+
noauth: '🔑',
|
|
25
|
+
auth_error: '🔐',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function statusLabel(status, httpCode) {
|
|
29
|
+
const emoji = STATUS_EMOJI[status] || '?'
|
|
30
|
+
if (status === 'noauth') return `${emoji} NO KEY`
|
|
31
|
+
if (status === 'auth_error') return `${emoji} AUTH FAIL`
|
|
32
|
+
if (status === 'pending') return `${emoji} wait`
|
|
33
|
+
if (status === 'timeout') return `${emoji} TIMEOUT`
|
|
34
|
+
if (status === 'down') {
|
|
35
|
+
const label = ERROR_LABELS[httpCode] || (httpCode || 'ERR')
|
|
36
|
+
const errEmoji = { '429': '🔥', '404': '🚫', '500': '💥', '502': '🔌', '503': '🔒', '504': '⏰' }[httpCode] || '❌'
|
|
37
|
+
return `${errEmoji} ${label}`
|
|
38
|
+
}
|
|
39
|
+
if (status === 'up') return `${emoji} UP`
|
|
40
|
+
return `${emoji} ?`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default function HealthCell({ status, httpCode }) {
|
|
44
|
+
const text = statusLabel(status, httpCode)
|
|
45
|
+
const cls = status === 'up' ? styles.up
|
|
46
|
+
: status === 'down' ? styles.error
|
|
47
|
+
: status === 'timeout' ? styles.warning
|
|
48
|
+
: status === 'noauth' ? styles.dim
|
|
49
|
+
: status === 'auth_error' ? styles.errorBold
|
|
50
|
+
: status === 'pending' ? styles.warning
|
|
51
|
+
: styles.dim
|
|
52
|
+
return <span className={`${styles.cell} ${cls}`}>{text}</span>
|
|
53
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.cell {
|
|
2
|
+
font-family: var(--font-mono);
|
|
3
|
+
font-size: 11px;
|
|
4
|
+
font-weight: 600;
|
|
5
|
+
display: inline-block;
|
|
6
|
+
letter-spacing: 0.2px;
|
|
7
|
+
white-space: nowrap;
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
text-overflow: ellipsis;
|
|
10
|
+
}
|
|
11
|
+
.up { color: #00ff88; }
|
|
12
|
+
.warning { color: #ffaa00; }
|
|
13
|
+
.error { color: #ff4444; }
|
|
14
|
+
.errorBold { color: #ff4444; font-weight: 800; }
|
|
15
|
+
.dim { color: var(--color-text-dim); }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/atoms/LastPingCell.jsx
|
|
3
|
+
* @description Last ping latency number with color coding + animated spinner during pings.
|
|
4
|
+
* 📖 Values are milliseconds, displayed without the `ms` suffix to keep table cells compact.
|
|
5
|
+
* 📖 During ping rounds: animated CSS spinner shows which models are being tested.
|
|
6
|
+
*/
|
|
7
|
+
import styles from './LastPingCell.module.css'
|
|
8
|
+
|
|
9
|
+
function pingClass(ms) {
|
|
10
|
+
if (ms == null || ms === Infinity) return styles.none
|
|
11
|
+
if (ms < 500) return styles.fast
|
|
12
|
+
if (ms < 1500) return styles.medium
|
|
13
|
+
return styles.slow
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function LastPingCell({ ms, isPinging }) {
|
|
17
|
+
if (ms == null) {
|
|
18
|
+
return (
|
|
19
|
+
<span className={`${styles.cell} ${styles.none}`}>
|
|
20
|
+
{isPinging ? (
|
|
21
|
+
<span className={styles.spinner} title="Testing…" />
|
|
22
|
+
) : (
|
|
23
|
+
'—'
|
|
24
|
+
)}
|
|
25
|
+
</span>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<span className={`${styles.cell} ${pingClass(ms)}`}>
|
|
31
|
+
<span className={styles.value}>{ms}</span>
|
|
32
|
+
{isPinging && <span className={styles.spinner} />}
|
|
33
|
+
</span>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
.cell {
|
|
2
|
+
font-family: var(--font-mono);
|
|
3
|
+
font-size: 12px;
|
|
4
|
+
font-weight: 600;
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
gap: 4px;
|
|
8
|
+
min-width: 54px;
|
|
9
|
+
}
|
|
10
|
+
.value { }
|
|
11
|
+
.fast { color: #00ff88; }
|
|
12
|
+
.medium { color: #ffaa00; }
|
|
13
|
+
.slow { color: #ff4444; }
|
|
14
|
+
.none { color: var(--color-text-dim); }
|
|
15
|
+
|
|
16
|
+
/* ── Animated spinner for active pings ── */
|
|
17
|
+
@keyframes pingPulse {
|
|
18
|
+
0%, 100% { opacity: 1; }
|
|
19
|
+
50% { opacity: 0.3; }
|
|
20
|
+
}
|
|
21
|
+
@keyframes pingRing {
|
|
22
|
+
0% { transform: rotate(0deg) scale(1); opacity: 1; }
|
|
23
|
+
70% { transform: rotate(180deg) scale(0.6); opacity: 0.6; }
|
|
24
|
+
100% { transform: rotate(360deg) scale(1); opacity: 1; }
|
|
25
|
+
}
|
|
26
|
+
.spinner {
|
|
27
|
+
display: inline-block;
|
|
28
|
+
width: 10px;
|
|
29
|
+
height: 10px;
|
|
30
|
+
border: 1.5px solid rgba(0, 255, 136, 0.25);
|
|
31
|
+
border-top-color: #00ff88;
|
|
32
|
+
border-radius: 50%;
|
|
33
|
+
animation: pingRing 0.8s linear infinite;
|
|
34
|
+
flex-shrink: 0;
|
|
35
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/atoms/MoodCell.jsx
|
|
3
|
+
* @description Tiny verdict indicator emoji (1st column) matching CLI ❔ column.
|
|
4
|
+
* 📖 Mirrors the full Verdict as a compact emoji: 🟩 Perfect, 🟢 Normal, 🟡 Spiky,
|
|
5
|
+
* 🟠 Slow, 🔴 Very Slow, 🔥 Overloaded, 🟥 Unstable, ⚫ Not Active, ⏳ Pending.
|
|
6
|
+
*/
|
|
7
|
+
import styles from './MoodCell.module.css'
|
|
8
|
+
|
|
9
|
+
const VERDICT_EMOJI = {
|
|
10
|
+
Perfect: '🟩',
|
|
11
|
+
Normal: '🟢',
|
|
12
|
+
Spiky: '🟡',
|
|
13
|
+
Slow: '🟠',
|
|
14
|
+
'Very Slow': '🔴',
|
|
15
|
+
Overloaded: '🔥',
|
|
16
|
+
Unstable: '🟥',
|
|
17
|
+
'Not Active':'⚫',
|
|
18
|
+
Pending: '⏳',
|
|
19
|
+
Usable: '🟠', // fallback
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function MoodCell({ verdict }) {
|
|
23
|
+
const emoji = VERDICT_EMOJI[verdict] || '❔'
|
|
24
|
+
return <span className={styles.mood}>{emoji}</span>
|
|
25
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/atoms/RankCell.jsx
|
|
3
|
+
* @description Row index number — triable column, matches CLI Rank (idx).
|
|
4
|
+
*/
|
|
5
|
+
import styles from './RankCell.module.css'
|
|
6
|
+
|
|
7
|
+
export default function RankCell({ index }) {
|
|
8
|
+
return <span className={styles.rank}>{index}</span>
|
|
9
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/atoms/TPSCell.jsx
|
|
3
|
+
* @description Benchmark tokens-per-second column — animated spinner during benchmarks.
|
|
4
|
+
* 📖 Shows: 13, 45, —. Retry badge ↻N in blue.
|
|
5
|
+
* 📖 When isRunning: animated CSS spinner matches AILatencyCell.
|
|
6
|
+
*/
|
|
7
|
+
import styles from './TPSCell.module.css'
|
|
8
|
+
|
|
9
|
+
function formatTps(result, isRunning) {
|
|
10
|
+
if (isRunning) return { text: '…', badge: '' }
|
|
11
|
+
if (!result || !result.ok) return { text: '—', badge: '' }
|
|
12
|
+
const badge = result.retries > 0 ? `↻${result.retries}` : ''
|
|
13
|
+
return { text: String(Math.round(result.tokensPerSecond ?? 0)), badge }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function TPSCell({ result, isRunning }) {
|
|
17
|
+
const { text, badge } = formatTps(result, isRunning)
|
|
18
|
+
const ok = result?.ok
|
|
19
|
+
const colorCls = ok ? styles.fast : (result ? styles.slow : styles.dim)
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<span className={`${styles.cell} ${isRunning ? styles.runningCell : ''}`}>
|
|
23
|
+
{isRunning ? (
|
|
24
|
+
<>
|
|
25
|
+
<span className={styles.miniSpinner} />
|
|
26
|
+
<span className={`${styles.value} ${styles.running}`}>{text}</span>
|
|
27
|
+
</>
|
|
28
|
+
) : (
|
|
29
|
+
<>
|
|
30
|
+
<span className={`${styles.value} ${colorCls}`}>{text}</span>
|
|
31
|
+
{badge && <span className={styles.badge}>{badge}</span>}
|
|
32
|
+
</>
|
|
33
|
+
)}
|
|
34
|
+
</span>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
.cell {
|
|
2
|
+
font-family: var(--font-mono);
|
|
3
|
+
font-size: 12px;
|
|
4
|
+
font-weight: 600;
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
gap: 4px;
|
|
8
|
+
min-width: 40px;
|
|
9
|
+
transition: background 200ms;
|
|
10
|
+
border-radius: 4px;
|
|
11
|
+
padding: 1px 3px;
|
|
12
|
+
}
|
|
13
|
+
.value { }
|
|
14
|
+
.fast { color: #00ff88; }
|
|
15
|
+
.slow { color: #ff4444; }
|
|
16
|
+
.dim { color: var(--color-text-dim); }
|
|
17
|
+
.running { color: #b400ff; font-weight: 700; }
|
|
18
|
+
.badge { color: #06b6d4; font-size: 10px; }
|
|
19
|
+
|
|
20
|
+
/* ── Running row highlight ── */
|
|
21
|
+
.runningCell {
|
|
22
|
+
background: rgba(180, 0, 255, 0.12);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* ── Mini spinner matching AILatencyCell ── */
|
|
26
|
+
@keyframes miniSpin {
|
|
27
|
+
to { transform: rotate(360deg); }
|
|
28
|
+
}
|
|
29
|
+
.miniSpinner {
|
|
30
|
+
display: inline-block;
|
|
31
|
+
width: 10px;
|
|
32
|
+
height: 10px;
|
|
33
|
+
border: 1.5px solid rgba(180, 0, 255, 0.3);
|
|
34
|
+
border-top-color: #b400ff;
|
|
35
|
+
border-radius: 50%;
|
|
36
|
+
animation: miniSpin 0.6s linear infinite;
|
|
37
|
+
flex-shrink: 0;
|
|
38
|
+
}
|
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file web/src/components/atoms/VerdictBadge.jsx
|
|
3
|
-
* @description Renders a verdict badge
|
|
3
|
+
* @description Renders a verdict badge matching exactly the TUI format.
|
|
4
|
+
* 📖 Shows emoji + text: 🟩 Perfect, 🟢 Normal, 🟡 Spiky, 🟠 Slow, 🔴 Very Slow, 🔥 Overloaded, 🟥 Unstable, ⚫ Not Active, ⏳ Pending.
|
|
4
5
|
*/
|
|
5
|
-
import { verdictCls } from '../../utils/ranks.js'
|
|
6
6
|
import styles from './VerdictBadge.module.css'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
// ─── Emoji map matching TUI render-table.js verdictIcon ──────────────────────────
|
|
9
|
+
const VERDICT_WITH_EMOJI = {
|
|
10
|
+
Perfect: { emoji: '🟩', text: 'Perfect', cls: 'perfect' },
|
|
11
|
+
Normal: { emoji: '🟢', text: 'Normal', cls: 'normal' },
|
|
12
|
+
Spiky: { emoji: '🟡', text: 'Spiky', cls: 'spiky' },
|
|
13
|
+
Slow: { emoji: '🟠', text: 'Slow', cls: 'slow' },
|
|
14
|
+
'Very Slow': { emoji: '🔴', text: 'Very Slow', cls: 'veryslow' },
|
|
15
|
+
Overloaded: { emoji: '🔥', text: 'Overloaded', cls: 'overloaded' },
|
|
16
|
+
Unstable: { emoji: '🟥', text: 'Unstable', cls: 'unstable' },
|
|
17
|
+
'Not Active':{ emoji: '⚫', text: 'Not Active', cls: 'notactive' },
|
|
18
|
+
Pending: { emoji: '⏳', text: 'Pending', cls: 'pending' },
|
|
13
19
|
}
|
|
20
|
+
|
|
21
|
+
const DEFAULT_ENTRY = { emoji: '❔', text: 'Pending', cls: 'pending' }
|
|
22
|
+
|
|
23
|
+
export default function VerdictBadge({ verdict, httpCode }) {
|
|
24
|
+
// Handle 429 rate limit from HTTP code (TUI shows 🔥 429 TRY LATER in Health column)
|
|
25
|
+
// In Verdict column, TUI shows 'Overloaded' for 429 — keep same behavior
|
|
26
|
+
const entry = verdict
|
|
27
|
+
? (VERDICT_WITH_EMOJI[verdict] || { emoji: '❔', text: verdict, cls: 'pending' })
|
|
28
|
+
: DEFAULT_ENTRY
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<span className={`${styles.badge} ${styles[entry.cls]}`}>
|
|
32
|
+
<span className={styles.emoji}>{entry.emoji}</span>
|
|
33
|
+
<span className={styles.text}>{entry.text}</span>
|
|
34
|
+
</span>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -1,19 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/atoms/VerdictBadge.module.css
|
|
3
|
+
* @description Styles matching TUI verdict colors (emoji + text).
|
|
4
|
+
*/
|
|
1
5
|
.badge {
|
|
2
|
-
display: inline-
|
|
3
|
-
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
gap: 3px;
|
|
9
|
+
font-size: 11px;
|
|
4
10
|
font-weight: 700;
|
|
5
|
-
|
|
6
|
-
padding: 2px 8px;
|
|
11
|
+
padding: 2px 7px;
|
|
7
12
|
border-radius: 999px;
|
|
8
|
-
|
|
13
|
+
white-space: nowrap;
|
|
9
14
|
}
|
|
10
|
-
|
|
11
|
-
.
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
.
|
|
18
|
-
.
|
|
19
|
-
.
|
|
15
|
+
|
|
16
|
+
.emoji { font-size: 12px; line-height: 1; }
|
|
17
|
+
.text { line-height: 1; }
|
|
18
|
+
|
|
19
|
+
/* ─── Color variants matching TUI theme ─── */
|
|
20
|
+
.perfect { background: rgba(0,255,136,0.12); color: #00ff88; }
|
|
21
|
+
.normal { background: rgba(118,185,0,0.12); color: #76b900; }
|
|
22
|
+
.slow { background: rgba(255,170,0,0.12); color: #ffaa00; }
|
|
23
|
+
.spiky { background: rgba(255,102,0,0.12); color: #ff6600; }
|
|
24
|
+
.veryslow { background: rgba(255,68,68,0.12); color: #ff4444; }
|
|
25
|
+
.overloaded { background: rgba(255,34,34,0.12); color: #ff2222; }
|
|
26
|
+
.unstable { background: rgba(204,0,0,0.12); color: #cc0000; }
|
|
27
|
+
.notactive { background: rgba(85,85,112,0.12); color: #555570; }
|
|
28
|
+
.pending { background: rgba(68,68,96,0.1); color: #444460; }
|