free-coding-models 0.5.86 → 0.5.88

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 (165) hide show
  1. package/README.md +109 -888
  2. package/bin/free-coding-models.js +4 -1
  3. package/changelog/v0.5.87.md +48 -0
  4. package/changelog/v0.5.88.md +16 -0
  5. package/package.json +13 -5
  6. package/sources.js +4 -4
  7. package/src/core/cache.js +3 -1
  8. package/src/core/config.js +43 -5
  9. package/src/core/endpoint-installer.js +23 -12
  10. package/src/core/installed-models-manager.js +43 -20
  11. package/src/core/kilo-config.js +51 -5
  12. package/src/core/kilo.js +5 -3
  13. package/src/core/legacy-proxy-cleanup.js +22 -5
  14. package/src/core/model-merger.js +4 -0
  15. package/src/core/models-drift.js +9 -0
  16. package/src/core/opencode-config.js +57 -5
  17. package/src/core/opencode.js +67 -41
  18. package/src/core/playground.js +7 -0
  19. package/src/core/probe-cache.js +35 -4
  20. package/src/core/provider-key-tester.js +10 -2
  21. package/src/core/provider-metadata.js +10 -0
  22. package/src/core/provider-quota-fetchers.js +14 -7
  23. package/src/core/router-daemon.js +208 -26
  24. package/src/core/runtime-telemetry.js +27 -1
  25. package/src/core/security.js +170 -24
  26. package/src/core/shared-helpers.js +12 -1
  27. package/src/core/shell-env.js +3 -1
  28. package/src/core/telemetry.js +18 -5
  29. package/src/core/tool-launchers.js +9 -6
  30. package/src/core/updater.js +28 -2
  31. package/src/core/utils.js +173 -71
  32. package/src/tui/app.js +38 -14
  33. package/src/tui/command-palette.js +22 -2
  34. package/src/tui/key-handler.js +123 -179
  35. package/src/tui/overlays.js +21 -71
  36. package/src/tui/render-helpers.js +62 -4
  37. package/src/tui/render-table.js +34 -3
  38. package/src/tui/theme.js +36 -5
  39. package/src/tui/tui-filters.js +28 -8
  40. package/src/tui/tui-state.js +22 -6
  41. package/web/dist/assets/index-CAzFIt8P.css +1 -0
  42. package/web/dist/assets/index-DFg1h0Nd.js +44 -0
  43. package/web/dist/index.html +5 -9
  44. package/web/index.html +3 -7
  45. package/web/server.js +97 -23
  46. package/web/vite.config.js +8 -0
  47. package/web/app.legacy.js +0 -900
  48. package/web/dist/assets/index-C5-Ywvv3.css +0 -1
  49. package/web/dist/assets/index-Dm-Xfby9.js +0 -40
  50. package/web/public/favicon.ico +0 -0
  51. package/web/public/favicons/apple-touch-icon.png +0 -0
  52. package/web/public/favicons/browserconfig.xml +0 -12
  53. package/web/public/favicons/favicon-16x16.png +0 -0
  54. package/web/public/favicons/favicon-192x192.png +0 -0
  55. package/web/public/favicons/favicon-32x32.png +0 -0
  56. package/web/public/favicons/favicon-48x48.png +0 -0
  57. package/web/public/favicons/favicon-512x512.png +0 -0
  58. package/web/public/favicons/favicon-96x96.png +0 -0
  59. package/web/public/favicons/favicon.ico +0 -0
  60. package/web/public/favicons/mstile-150x150.png +0 -0
  61. package/web/public/favicons/mstile-310x150.png +0 -0
  62. package/web/public/favicons/mstile-310x310.png +0 -0
  63. package/web/public/favicons/mstile-512x512.png +0 -0
  64. package/web/public/favicons/mstile-70x70.png +0 -0
  65. package/web/public/favicons/site.webmanifest +0 -25
  66. package/web/src/App.jsx +0 -521
  67. package/web/src/components/analytics/AnalyticsView.jsx +0 -147
  68. package/web/src/components/analytics/AnalyticsView.module.css +0 -190
  69. package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
  70. package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
  71. package/web/src/components/atoms/AILatencyCell.jsx +0 -38
  72. package/web/src/components/atoms/AILatencyCell.module.css +0 -51
  73. package/web/src/components/atoms/HealthCell.jsx +0 -143
  74. package/web/src/components/atoms/HealthCell.module.css +0 -60
  75. package/web/src/components/atoms/LastPingCell.jsx +0 -35
  76. package/web/src/components/atoms/LastPingCell.module.css +0 -45
  77. package/web/src/components/atoms/MoodCell.jsx +0 -25
  78. package/web/src/components/atoms/MoodCell.module.css +0 -6
  79. package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
  80. package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
  81. package/web/src/components/atoms/ProviderLogo.jsx +0 -187
  82. package/web/src/components/atoms/ProviderLogo.module.css +0 -103
  83. package/web/src/components/atoms/RankCell.jsx +0 -9
  84. package/web/src/components/atoms/RankCell.module.css +0 -9
  85. package/web/src/components/atoms/Sparkline.jsx +0 -63
  86. package/web/src/components/atoms/StabilityCell.jsx +0 -18
  87. package/web/src/components/atoms/StabilityCell.module.css +0 -13
  88. package/web/src/components/atoms/StatusDot.jsx +0 -13
  89. package/web/src/components/atoms/StatusDot.module.css +0 -29
  90. package/web/src/components/atoms/TPSCell.jsx +0 -36
  91. package/web/src/components/atoms/TPSCell.module.css +0 -44
  92. package/web/src/components/atoms/TierBadge.jsx +0 -10
  93. package/web/src/components/atoms/TierBadge.module.css +0 -29
  94. package/web/src/components/atoms/Toast.jsx +0 -25
  95. package/web/src/components/atoms/Toast.module.css +0 -41
  96. package/web/src/components/atoms/ToastContainer.jsx +0 -16
  97. package/web/src/components/atoms/ToastContainer.module.css +0 -10
  98. package/web/src/components/atoms/VerdictBadge.jsx +0 -36
  99. package/web/src/components/atoms/VerdictBadge.module.css +0 -40
  100. package/web/src/components/changelog/ChangelogView.jsx +0 -135
  101. package/web/src/components/changelog/ChangelogView.module.css +0 -159
  102. package/web/src/components/dashboard/DetailPanel.jsx +0 -217
  103. package/web/src/components/dashboard/DetailPanel.module.css +0 -260
  104. package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
  105. package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
  106. package/web/src/components/dashboard/ExportModal.jsx +0 -84
  107. package/web/src/components/dashboard/ExportModal.module.css +0 -99
  108. package/web/src/components/dashboard/FilterBar.jsx +0 -338
  109. package/web/src/components/dashboard/FilterBar.module.css +0 -342
  110. package/web/src/components/dashboard/ModelTable.jsx +0 -672
  111. package/web/src/components/dashboard/ModelTable.module.css +0 -446
  112. package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
  113. package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
  114. package/web/src/components/help/HelpView.jsx +0 -201
  115. package/web/src/components/help/HelpView.module.css +0 -156
  116. package/web/src/components/install/InstallEndpointsView.jsx +0 -309
  117. package/web/src/components/install/InstallEndpointsView.module.css +0 -427
  118. package/web/src/components/installed/InstalledModelsView.jsx +0 -89
  119. package/web/src/components/installed/InstalledModelsView.module.css +0 -200
  120. package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
  121. package/web/src/components/launch/LaunchButton.jsx +0 -30
  122. package/web/src/components/launch/LaunchButton.module.css +0 -35
  123. package/web/src/components/launch/LaunchModal.module.css +0 -125
  124. package/web/src/components/layout/Footer.jsx +0 -19
  125. package/web/src/components/layout/Footer.module.css +0 -10
  126. package/web/src/components/layout/Header.jsx +0 -265
  127. package/web/src/components/layout/Header.module.css +0 -357
  128. package/web/src/components/palette/CommandPalette.jsx +0 -301
  129. package/web/src/components/palette/CommandPalette.module.css +0 -121
  130. package/web/src/components/playground/PlaygroundChat.jsx +0 -527
  131. package/web/src/components/playground/PlaygroundChat.module.css +0 -382
  132. package/web/src/components/playground/PlaygroundView.jsx +0 -371
  133. package/web/src/components/playground/PlaygroundView.module.css +0 -504
  134. package/web/src/components/recommend/RecommendView.jsx +0 -121
  135. package/web/src/components/recommend/RecommendView.module.css +0 -57
  136. package/web/src/components/router/RouterView.jsx +0 -1212
  137. package/web/src/components/router/RouterView.module.css +0 -1289
  138. package/web/src/components/settings/SettingsView.jsx +0 -555
  139. package/web/src/components/settings/SettingsView.module.css +0 -550
  140. package/web/src/components/tools/ToolPicker.jsx +0 -86
  141. package/web/src/components/tools/ToolPicker.module.css +0 -97
  142. package/web/src/components/update/UpdateChip.jsx +0 -104
  143. package/web/src/components/update/UpdateChip.module.css +0 -146
  144. package/web/src/global.css +0 -298
  145. package/web/src/hooks/urlState.constants.js +0 -28
  146. package/web/src/hooks/useChangelog.js +0 -51
  147. package/web/src/hooks/useColumnSizing.js +0 -211
  148. package/web/src/hooks/useFavorites.js +0 -150
  149. package/web/src/hooks/useFilter.js +0 -310
  150. package/web/src/hooks/useInstalledModels.js +0 -42
  151. package/web/src/hooks/useRecommend.js +0 -67
  152. package/web/src/hooks/useRouterDashboard.js +0 -133
  153. package/web/src/hooks/useSSE.js +0 -73
  154. package/web/src/hooks/useSocket.js +0 -203
  155. package/web/src/hooks/useTheme.js +0 -83
  156. package/web/src/hooks/useTokenUsage.js +0 -103
  157. package/web/src/hooks/useToolMode.js +0 -77
  158. package/web/src/hooks/useUpdateChecker.js +0 -91
  159. package/web/src/hooks/useUrlState.js +0 -170
  160. package/web/src/main.jsx +0 -15
  161. package/web/src/utils/download.js +0 -15
  162. package/web/src/utils/format.js +0 -42
  163. package/web/src/utils/m3.js +0 -57
  164. package/web/src/utils/ranks.js +0 -37
  165. package/web/styles.legacy.css +0 -963
@@ -1,260 +0,0 @@
1
- .panel {
2
- position: fixed;
3
- right: 0;
4
- top: 0;
5
- bottom: 0;
6
- width: min(420px, 90vw);
7
- z-index: 300;
8
- background: var(--color-bg-elevated);
9
- border-left: 1px solid var(--color-border);
10
- box-shadow: var(--shadow-lg);
11
- transform: translateX(100%);
12
- transition: transform var(--transition-medium);
13
- overflow-y: auto;
14
- }
15
-
16
- /* 📖 Slide-in when model is selected (parent passes a non-null model) */
17
- .panelOpen {
18
- transform: translateX(0);
19
- }
20
-
21
- .header {
22
- display: flex;
23
- align-items: center;
24
- justify-content: space-between;
25
- padding: 20px 24px;
26
- border-bottom: 1px solid var(--color-border);
27
- }
28
-
29
- .title {
30
- font-size: 16px;
31
- font-weight: 700;
32
- margin: 0;
33
- }
34
-
35
- .closeBtn {
36
- background: none;
37
- border: none;
38
- color: var(--color-text-muted);
39
- font-size: 22px;
40
- cursor: pointer;
41
- line-height: 1;
42
- }
43
-
44
- .closeBtn:hover {
45
- color: var(--color-text);
46
- }
47
-
48
- .body {
49
- padding: 24px;
50
- }
51
-
52
- .stat {
53
- display: flex;
54
- justify-content: space-between;
55
- align-items: center;
56
- padding: 10px 0;
57
- border-bottom: 1px solid var(--color-border);
58
- }
59
-
60
- .statLabel {
61
- font-size: 12px;
62
- color: var(--color-text-muted);
63
- text-transform: uppercase;
64
- letter-spacing: 0.5px;
65
- font-weight: 600;
66
- }
67
-
68
- .statValue {
69
- font-family: var(--font-mono);
70
- font-weight: 600;
71
- }
72
-
73
- .ping {
74
- font-family: var(--font-mono);
75
- font-weight: 600;
76
- }
77
-
78
- .pingFast { color: #00ff88; }
79
- .pingMedium { color: #ffaa00; }
80
- .pingSlow { color: #ff4444; }
81
- .pingNone { color: var(--color-text-dim); }
82
-
83
- :global([data-theme="light"]) .pingFast { color: #008f4d; }
84
- :global([data-theme="light"]) .pingMedium { color: #b86b00; }
85
- :global([data-theme="light"]) .pingSlow { color: #c8143a; }
86
-
87
- .chart {
88
- margin: 20px 0;
89
- padding: 16px;
90
- background: var(--color-surface);
91
- border-radius: var(--radius-md);
92
- border: 1px solid var(--color-border);
93
- }
94
-
95
- .chartTitle {
96
- font-size: 11px;
97
- font-weight: 700;
98
- color: var(--color-text-muted);
99
- text-transform: uppercase;
100
- letter-spacing: 0.8px;
101
- margin-bottom: 12px;
102
- }
103
-
104
- .chartEmpty {
105
- color: var(--color-text-dim);
106
- text-align: center;
107
- padding: 20px;
108
- }
109
-
110
- /* ─── M1: favorites + per-row benchmark actions ─── */
111
- .actions {
112
- display: flex;
113
- flex-direction: column;
114
- gap: 10px;
115
- margin: 16px 0;
116
- padding: 12px;
117
- background: var(--color-surface);
118
- border-radius: var(--radius-md);
119
- border: 1px solid var(--color-border);
120
- }
121
-
122
- .favBlock {
123
- display: flex;
124
- flex-direction: column;
125
- gap: 8px;
126
- }
127
-
128
- .favBtn {
129
- display: inline-flex;
130
- align-items: center;
131
- gap: 8px;
132
- padding: 8px 12px;
133
- background: var(--color-bg-card);
134
- color: var(--color-text-muted);
135
- border: 1px solid var(--color-border);
136
- border-radius: 6px;
137
- font-size: 12px;
138
- font-weight: 600;
139
- cursor: pointer;
140
- transition: all 150ms;
141
- text-align: left;
142
- }
143
- .favBtn:hover {
144
- border-color: #ffd700;
145
- color: #ffd700;
146
- background: rgba(255, 215, 0, 0.08);
147
- }
148
- .favBtnActive {
149
- border-color: #ffd700;
150
- color: #ffd700;
151
- background: rgba(255, 215, 0, 0.12);
152
- }
153
- .favBtnActive:hover {
154
- background: rgba(255, 215, 0, 0.16);
155
- }
156
- :global([data-theme="light"]) .favBtn { color: #555; }
157
- :global([data-theme="light"]) .favBtn:hover { color: #b08800; border-color: #b08800; background: rgba(176, 136, 0, 0.06); }
158
- :global([data-theme="light"]) .favBtnActive { color: #b08800; border-color: #b08800; background: rgba(176, 136, 0, 0.10); }
159
-
160
- .reorderRow {
161
- display: flex;
162
- align-items: center;
163
- gap: 6px;
164
- padding: 0 4px;
165
- }
166
- .reorderBtn {
167
- display: inline-flex;
168
- align-items: center;
169
- gap: 3px;
170
- padding: 4px 8px;
171
- font-size: 10px;
172
- font-weight: 600;
173
- font-family: var(--font-mono);
174
- background: var(--color-bg-card);
175
- color: var(--color-text-muted);
176
- border: 1px solid var(--color-border);
177
- border-radius: 4px;
178
- cursor: pointer;
179
- transition: all 120ms;
180
- }
181
- .reorderBtn:hover:not(:disabled) {
182
- background: var(--color-bg-hover);
183
- color: var(--color-text);
184
- border-color: var(--color-text-muted);
185
- }
186
- .reorderBtn:disabled {
187
- opacity: 0.4;
188
- cursor: not-allowed;
189
- }
190
- .rankBadge {
191
- margin-left: auto;
192
- font-size: 10px;
193
- font-family: var(--font-mono);
194
- font-weight: 700;
195
- color: #ffd700;
196
- background: rgba(255, 215, 0, 0.12);
197
- border: 1px solid rgba(255, 215, 0, 0.3);
198
- padding: 2px 6px;
199
- border-radius: 999px;
200
- }
201
- :global([data-theme="light"]) .rankBadge {
202
- color: #b08800;
203
- background: rgba(176, 136, 0, 0.12);
204
- border-color: rgba(176, 136, 0, 0.3);
205
- }
206
-
207
- .benchBtn {
208
- display: inline-flex;
209
- align-items: center;
210
- justify-content: center;
211
- gap: 6px;
212
- padding: 8px 12px;
213
- font-size: 12px;
214
- font-weight: 700;
215
- font-family: var(--font-mono);
216
- background: rgba(180, 0, 255, 0.12);
217
- color: #b400ff;
218
- border: 1px solid rgba(180, 0, 255, 0.4);
219
- border-radius: 6px;
220
- cursor: pointer;
221
- transition: all 150ms;
222
- }
223
- .benchBtn:hover {
224
- background: rgba(180, 0, 255, 0.20);
225
- border-color: rgba(180, 0, 255, 0.7);
226
- }
227
-
228
- .compatWarning {
229
- display: flex;
230
- align-items: center;
231
- gap: 8px;
232
- margin: 12px 0;
233
- padding: 10px;
234
- border: 1px solid rgba(255, 90, 122, 0.35);
235
- border-radius: 8px;
236
- background: rgba(255, 90, 122, 0.10);
237
- color: #ff8da3;
238
- font-size: 12px;
239
- line-height: 1.35;
240
- }
241
- .compatWarning span { flex: 1; }
242
- .compatWarning button {
243
- border: 1px solid rgba(255, 90, 122, 0.45);
244
- border-radius: 6px;
245
- background: var(--color-bg-card);
246
- color: #ff8da3;
247
- padding: 5px 8px;
248
- cursor: pointer;
249
- font-size: 11px;
250
- font-weight: 800;
251
- }
252
- .compatWarning button:hover { background: rgba(255, 90, 122, 0.16); }
253
- :global([data-theme="light"]) .benchBtn {
254
- background: rgba(140, 0, 200, 0.06);
255
- color: #6a00a0;
256
- border-color: rgba(140, 0, 200, 0.3);
257
- }
258
- :global([data-theme="light"]) .benchBtn:hover {
259
- background: rgba(140, 0, 200, 0.12);
260
- }
@@ -1,361 +0,0 @@
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 { 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 PlaygroundChat from '../playground/PlaygroundChat.jsx'
24
- import styles from './ExpandedDetailRow.module.css'
25
-
26
- /**
27
- * Single stat item with label + value, used in the info column grid.
28
- * @param {string} label - Uppercase stat label
29
- * @param {React.ReactNode} children - Stat value content
30
- */
31
- function StatItem({ label, children }) {
32
- return (
33
- <div className={styles.statItem}>
34
- <span className={styles.statLabel}>{label}</span>
35
- <span className={styles.statValue}>{children}</span>
36
- </div>
37
- )
38
- }
39
-
40
- /**
41
- * Expandable detail row with 3 columns: Info, Mini Playground, AI Latency.
42
- *
43
- * @param {Object} props
44
- * @param {Object} props.model - The model data object
45
- * @param {Object} props.favorites - Favorites controller (isFavorite, toggle)
46
- * @param {Function} [props.onBenchmark] - Benchmark callback
47
- * @param {Function} [props.onLaunch] - Launch callback
48
- * @param {Function} [props.onToast] - Toast notification callback
49
- * @param {string} [props.toolMode='opencode'] - Current tool mode
50
- * @param {Function} [props.onSetToolMode] - Set tool mode handler
51
- * @param {Function} [props.onCycleToolMode] - Cycle tool mode handler
52
- * @param {Function} [props.onOpenFallback] - Open fallback tool handler
53
- */
54
- export default function ExpandedDetailRow({
55
- model,
56
- favorites,
57
- onBenchmark,
58
- onLaunch,
59
- onToast,
60
- toolMode = 'opencode',
61
- onSetToolMode,
62
- onCycleToolMode,
63
- onOpenFallback,
64
- }) {
65
- // ─── AI Latency benchmark state ───
66
- const [benchState, setBenchState] = useState('idle') // 'idle' | 'running' | 'done' | 'error'
67
- const [benchMetrics, setBenchMetrics] = useState({ latency: null, tokens: null, tps: null })
68
- const [benchText, setBenchText] = useState('')
69
- const [benchProgress, setBenchProgress] = useState(0)
70
- const benchAbort = useRef(null)
71
-
72
- if (!model) return null
73
-
74
- const isFav = favorites?.isFavorite(model) ?? false
75
- const avgData = formatAvg(model.avg)
76
- const avgCls = avgData.cls || pingClass(model.avg)
77
-
78
- // ─── AI Latency: run benchmark ───
79
- const handleBenchStart = useCallback(async () => {
80
- if (benchState === 'running') return
81
-
82
- setBenchState('running')
83
- setBenchMetrics({ latency: null, tokens: null, tps: null })
84
- setBenchText('')
85
- setBenchProgress(0)
86
-
87
- // Cancel any previous request
88
- if (benchAbort.current) benchAbort.current.abort()
89
- const controller = new AbortController()
90
- benchAbort.current = controller
91
-
92
- try {
93
- const res = await fetch('/api/benchmark-stream', {
94
- method: 'POST',
95
- headers: { 'Content-Type': 'application/json' },
96
- body: JSON.stringify({ providerKey: model.providerKey, modelId: model.modelId }),
97
- signal: controller.signal,
98
- })
99
-
100
- if (!res.ok) {
101
- const errText = await res.text().catch(() => `HTTP ${res.status}`)
102
- setBenchState('error')
103
- setBenchText(`Error: ${errText}`)
104
- return
105
- }
106
-
107
- const reader = res.body.getReader()
108
- const decoder = new TextDecoder()
109
- let buffer = ''
110
- let accumulatedText = ''
111
-
112
- while (true) {
113
- const { done, value } = await reader.read()
114
- if (done) break
115
-
116
- buffer += decoder.decode(value, { stream: true })
117
- const lines = buffer.split('\n')
118
- // Keep the last potentially incomplete line in the buffer
119
- buffer = lines.pop() || ''
120
-
121
- for (const line of lines) {
122
- const trimmed = line.trim()
123
- if (!trimmed) continue
124
-
125
- if (trimmed.startsWith('event: ')) {
126
- // Event type line — we use data lines for actual content
127
- continue
128
- }
129
-
130
- if (trimmed.startsWith('data: ')) {
131
- const payload = trimmed.slice(6).trim()
132
-
133
- try {
134
- const json = JSON.parse(payload)
135
- const eventType = json.type || json.event
136
-
137
- if (eventType === 'token' || json.token !== undefined) {
138
- // Live token — update streaming text and metrics
139
- if (json.text) {
140
- accumulatedText += json.text
141
- setBenchText(accumulatedText)
142
- }
143
- if (json.totalMs != null) {
144
- setBenchMetrics(prev => ({
145
- ...prev,
146
- latency: (json.totalMs / 1000).toFixed(2),
147
- }))
148
- }
149
- if (json.tokens != null) {
150
- setBenchMetrics(prev => ({ ...prev, tokens: json.tokens }))
151
- setBenchProgress(Math.min(100, Math.round((json.tokens / 140) * 100)))
152
- }
153
- if (json.tps != null) {
154
- setBenchMetrics(prev => ({ ...prev, tps: json.tps.toFixed(1) }))
155
- }
156
- } else if (eventType === 'done') {
157
- // Benchmark complete
158
- if (json.totalMs != null) {
159
- setBenchMetrics(prev => ({
160
- ...prev,
161
- latency: (json.totalMs / 1000).toFixed(2),
162
- }))
163
- }
164
- if (json.outputTokens != null) {
165
- setBenchMetrics(prev => ({ ...prev, tokens: json.outputTokens }))
166
- }
167
- if (json.tokensPerSecond != null) {
168
- setBenchMetrics(prev => ({
169
- ...prev,
170
- tps: json.tokensPerSecond.toFixed(1),
171
- }))
172
- }
173
- setBenchProgress(100)
174
- setBenchState('done')
175
- } else if (eventType === 'error') {
176
- setBenchState('error')
177
- setBenchText(json.message || json.error || 'Unknown error')
178
- }
179
- } catch {
180
- // Non-JSON payload — ignore
181
- }
182
- }
183
- }
184
- }
185
- } catch (err) {
186
- if (err.name !== 'AbortError') {
187
- setBenchState('error')
188
- setBenchText(`Error: ${err.message}`)
189
- }
190
- } finally {
191
- if (benchState === 'running') {
192
- setBenchState('done')
193
- }
194
- benchAbort.current = null
195
- }
196
- }, [benchState, model.providerKey, model.modelId])
197
-
198
- // ─── No API key — show banner ───
199
- if (!model.hasApiKey) {
200
- return (
201
- <div className={styles.row}>
202
- <div className={styles.noKeyBanner}>
203
- <span className={styles.noKeyIcon}>🔒</span>
204
- <span>No API key configured for {model.origin}. Open Settings (P) to add one.</span>
205
- </div>
206
- </div>
207
- )
208
- }
209
-
210
- return (
211
- <div className={styles.row}>
212
- <div className={styles.grid}>
213
-
214
- {/* ═══════ Column 1: Info ═══════ */}
215
- <div className={styles.col}>
216
- <div className={styles.colTitle}>📊 Model Info</div>
217
- <div className={styles.statGrid}>
218
- <StatItem label="Tier">
219
- <TierBadge tier={model.tier} />
220
- </StatItem>
221
- <StatItem label="SWE-bench">
222
- <span className={styles[sweClass(model.sweScore)]}>
223
- {model.sweScore || '—'}
224
- </span>
225
- </StatItem>
226
- <StatItem label="Context">
227
- {model.ctx || '—'}
228
- </StatItem>
229
- <StatItem label="Provider">
230
- {model.origin}
231
- </StatItem>
232
- <StatItem label="Status">
233
- <StatusDot status={model.status} />
234
- <span style={{ fontSize: 11 }}>{model.status}</span>
235
- </StatItem>
236
- <StatItem label="Avg Ping">
237
- <span className={styles[avgCls]}>
238
- {avgData.text}
239
- </span>
240
- </StatItem>
241
- <StatItem label="Stability">
242
- <StabilityCell score={model.stability} />
243
- </StatItem>
244
- <StatItem label="Verdict">
245
- <VerdictBadge verdict={model.verdict} httpCode={model.httpCode} />
246
- </StatItem>
247
- <StatItem label="Uptime">
248
- {model.uptime > 0 ? `${model.uptime}%` : '—'}
249
- </StatItem>
250
- </div>
251
- <div className={styles.infoActions}>
252
- {favorites && (
253
- <button
254
- className={`${styles.favBtn} ${isFav ? styles.favBtnActive : ''}`}
255
- onClick={() => favorites.toggle(model)}
256
- title={isFav ? `Unfavorite ${model.label}` : `Favorite ${model.label}`}
257
- >
258
- {isFav
259
- ? <IconStarFilled size={13} stroke={1.5} />
260
- : <IconStar size={13} stroke={1.5} />
261
- }
262
- <span>{isFav ? 'Favorited' : 'Favorite'}</span>
263
- </button>
264
- )}
265
- {onLaunch && (
266
- <LaunchButton
267
- model={model}
268
- toolMode={toolMode}
269
- onLaunch={onLaunch}
270
- variant="default"
271
- />
272
- )}
273
- </div>
274
- </div>
275
-
276
- {/* ═══════ Column 2: Mini Playground (shared PlaygroundChat core) ═══════ */}
277
- <div className={styles.col}>
278
- <div className={styles.playgroundHeader}>
279
- 💬 Mini Playground — {model.label}
280
- </div>
281
- <div className={styles.playgroundWrap}>
282
- <PlaygroundChat
283
- model={`${model.providerKey}/${model.modelId}`}
284
- variant="mini"
285
- disabled={!model.hasApiKey}
286
- placeholder="Type a message…"
287
- emptyTitle={`Test ${model.label}`}
288
- emptyHint="Send a message to test this model. Responses stream in real time with latency + TPS."
289
- />
290
- </div>
291
- </div>
292
-
293
- {/* ═══════ Column 3: AI Latency ═══════ */}
294
- <div className={styles.col}>
295
- <button
296
- className={styles.benchBtn}
297
- onClick={handleBenchStart}
298
- disabled={benchState === 'running'}
299
- title={benchState === 'running' ? 'Running benchmark…' : 'Test AI Latency'}
300
- >
301
- {benchState === 'running' ? (
302
- <IconLoader size={13} stroke={1.8} className={styles.spinning} />
303
- ) : (
304
- <IconPlayerPlayFilled size={13} stroke={1.8} />
305
- )}
306
- <span>{benchState === 'running' ? 'Running…' : 'Test AI Latency'}</span>
307
- </button>
308
-
309
- {/* Live metrics grid */}
310
- <div className={styles.metricsGrid}>
311
- <div className={styles.metricCard}>
312
- <span className={styles.metricLabel}>Latency</span>
313
- <span className={styles.metricValue}>
314
- {benchMetrics.latency != null ? `${benchMetrics.latency}s` : '—'}
315
- </span>
316
- </div>
317
- <div className={styles.metricCard}>
318
- <span className={styles.metricLabel}>Tokens</span>
319
- <span className={styles.metricValue}>
320
- {benchMetrics.tokens != null ? benchMetrics.tokens : '—'}
321
- </span>
322
- </div>
323
- <div className={styles.metricCard}>
324
- <span className={styles.metricLabel}>TPS</span>
325
- <span className={styles.metricValue}>
326
- {benchMetrics.tps != null ? benchMetrics.tps : '—'}
327
- </span>
328
- </div>
329
- <div className={styles.metricCard}>
330
- <span className={styles.metricLabel}>Status</span>
331
- <span className={styles.metricValue}>
332
- {benchState === 'idle' && '⏳'}
333
- {benchState === 'running' && '⚡'}
334
- {benchState === 'done' && '✅'}
335
- {benchState === 'error' && '❌'}
336
- </span>
337
- </div>
338
- </div>
339
-
340
- {/* Progress bar */}
341
- <div className={styles.progressBar}>
342
- <div
343
- className={styles.progressFill}
344
- style={{ width: `${benchProgress}%` }}
345
- />
346
- </div>
347
-
348
- {/* Streaming text preview */}
349
- <div className={styles.streamText}>
350
- {benchText || (
351
- <span className={styles.responsePlaceholder}>
352
- Generated text will appear here…
353
- </span>
354
- )}
355
- </div>
356
- </div>
357
-
358
- </div>
359
- </div>
360
- )
361
- }