free-coding-models 0.5.85 → 0.5.87
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 +110 -881
- package/bin/free-coding-models.js +4 -1
- package/changelog/v0.5.86.md +10 -0
- package/changelog/v0.5.87.md +48 -0
- package/package.json +8 -5
- package/sources.js +4 -4
- package/src/core/cache.js +3 -1
- package/src/core/config.js +43 -5
- package/src/core/endpoint-installer.js +23 -12
- package/src/core/installed-models-manager.js +43 -20
- package/src/core/kilo-config.js +51 -5
- package/src/core/kilo.js +5 -3
- package/src/core/legacy-proxy-cleanup.js +22 -5
- package/src/core/model-merger.js +4 -0
- package/src/core/models-drift.js +9 -0
- package/src/core/opencode-config.js +57 -5
- package/src/core/opencode.js +67 -41
- package/src/core/playground.js +7 -0
- package/src/core/probe-cache.js +35 -4
- package/src/core/provider-key-tester.js +10 -2
- package/src/core/provider-metadata.js +10 -0
- package/src/core/provider-quota-fetchers.js +14 -7
- package/src/core/router-daemon.js +208 -26
- package/src/core/runtime-telemetry.js +27 -1
- package/src/core/shared-helpers.js +12 -1
- package/src/core/shell-env.js +3 -1
- package/src/core/telemetry.js +18 -5
- package/src/core/tool-launchers.js +9 -6
- package/src/core/updater.js +28 -2
- package/src/core/utils.js +76 -71
- package/src/tui/app.js +47 -16
- package/src/tui/command-palette.js +22 -2
- package/src/tui/key-handler.js +123 -179
- package/src/tui/overlays.js +35 -75
- package/src/tui/render-helpers.js +62 -4
- package/src/tui/render-table.js +46 -5
- package/src/tui/theme.js +36 -5
- package/src/tui/tui-filters.js +28 -8
- package/src/tui/tui-state.js +22 -6
- package/web/dist/assets/index-BpTpPS0g.js +44 -0
- package/web/dist/assets/index-CAzFIt8P.css +1 -0
- package/web/dist/index.html +5 -9
- package/web/index.html +3 -7
- package/web/server.js +97 -23
- package/web/vite.config.js +8 -0
- package/web/app.legacy.js +0 -900
- package/web/dist/assets/index-C5-Ywvv3.css +0 -1
- package/web/dist/assets/index-DWUely84.js +0 -40
- package/web/public/favicon.ico +0 -0
- package/web/public/favicons/apple-touch-icon.png +0 -0
- package/web/public/favicons/browserconfig.xml +0 -12
- package/web/public/favicons/favicon-16x16.png +0 -0
- package/web/public/favicons/favicon-192x192.png +0 -0
- package/web/public/favicons/favicon-32x32.png +0 -0
- package/web/public/favicons/favicon-48x48.png +0 -0
- package/web/public/favicons/favicon-512x512.png +0 -0
- package/web/public/favicons/favicon-96x96.png +0 -0
- package/web/public/favicons/favicon.ico +0 -0
- package/web/public/favicons/mstile-150x150.png +0 -0
- package/web/public/favicons/mstile-310x150.png +0 -0
- package/web/public/favicons/mstile-310x310.png +0 -0
- package/web/public/favicons/mstile-512x512.png +0 -0
- package/web/public/favicons/mstile-70x70.png +0 -0
- package/web/public/favicons/site.webmanifest +0 -25
- package/web/src/App.jsx +0 -521
- package/web/src/components/analytics/AnalyticsView.jsx +0 -147
- package/web/src/components/analytics/AnalyticsView.module.css +0 -190
- package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
- package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
- package/web/src/components/atoms/AILatencyCell.jsx +0 -38
- package/web/src/components/atoms/AILatencyCell.module.css +0 -51
- package/web/src/components/atoms/HealthCell.jsx +0 -143
- package/web/src/components/atoms/HealthCell.module.css +0 -60
- package/web/src/components/atoms/LastPingCell.jsx +0 -35
- package/web/src/components/atoms/LastPingCell.module.css +0 -45
- package/web/src/components/atoms/MoodCell.jsx +0 -25
- package/web/src/components/atoms/MoodCell.module.css +0 -6
- package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
- package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
- package/web/src/components/atoms/ProviderLogo.jsx +0 -187
- package/web/src/components/atoms/ProviderLogo.module.css +0 -103
- package/web/src/components/atoms/RankCell.jsx +0 -9
- package/web/src/components/atoms/RankCell.module.css +0 -9
- package/web/src/components/atoms/Sparkline.jsx +0 -63
- package/web/src/components/atoms/StabilityCell.jsx +0 -18
- package/web/src/components/atoms/StabilityCell.module.css +0 -13
- package/web/src/components/atoms/StatusDot.jsx +0 -13
- package/web/src/components/atoms/StatusDot.module.css +0 -29
- package/web/src/components/atoms/TPSCell.jsx +0 -36
- package/web/src/components/atoms/TPSCell.module.css +0 -44
- package/web/src/components/atoms/TierBadge.jsx +0 -10
- package/web/src/components/atoms/TierBadge.module.css +0 -29
- package/web/src/components/atoms/Toast.jsx +0 -25
- package/web/src/components/atoms/Toast.module.css +0 -41
- package/web/src/components/atoms/ToastContainer.jsx +0 -16
- package/web/src/components/atoms/ToastContainer.module.css +0 -10
- package/web/src/components/atoms/VerdictBadge.jsx +0 -36
- package/web/src/components/atoms/VerdictBadge.module.css +0 -40
- package/web/src/components/changelog/ChangelogView.jsx +0 -135
- package/web/src/components/changelog/ChangelogView.module.css +0 -159
- package/web/src/components/dashboard/DetailPanel.jsx +0 -217
- package/web/src/components/dashboard/DetailPanel.module.css +0 -260
- package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
- package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
- package/web/src/components/dashboard/ExportModal.jsx +0 -84
- package/web/src/components/dashboard/ExportModal.module.css +0 -99
- package/web/src/components/dashboard/FilterBar.jsx +0 -338
- package/web/src/components/dashboard/FilterBar.module.css +0 -342
- package/web/src/components/dashboard/ModelTable.jsx +0 -672
- package/web/src/components/dashboard/ModelTable.module.css +0 -446
- package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
- package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
- package/web/src/components/help/HelpView.jsx +0 -201
- package/web/src/components/help/HelpView.module.css +0 -156
- package/web/src/components/install/InstallEndpointsView.jsx +0 -309
- package/web/src/components/install/InstallEndpointsView.module.css +0 -427
- package/web/src/components/installed/InstalledModelsView.jsx +0 -89
- package/web/src/components/installed/InstalledModelsView.module.css +0 -200
- package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
- package/web/src/components/launch/LaunchButton.jsx +0 -30
- package/web/src/components/launch/LaunchButton.module.css +0 -35
- package/web/src/components/launch/LaunchModal.module.css +0 -125
- package/web/src/components/layout/Footer.jsx +0 -19
- package/web/src/components/layout/Footer.module.css +0 -10
- package/web/src/components/layout/Header.jsx +0 -265
- package/web/src/components/layout/Header.module.css +0 -357
- package/web/src/components/palette/CommandPalette.jsx +0 -301
- package/web/src/components/palette/CommandPalette.module.css +0 -121
- package/web/src/components/playground/PlaygroundChat.jsx +0 -527
- package/web/src/components/playground/PlaygroundChat.module.css +0 -382
- package/web/src/components/playground/PlaygroundView.jsx +0 -371
- package/web/src/components/playground/PlaygroundView.module.css +0 -504
- package/web/src/components/recommend/RecommendView.jsx +0 -121
- package/web/src/components/recommend/RecommendView.module.css +0 -57
- package/web/src/components/router/RouterView.jsx +0 -1212
- package/web/src/components/router/RouterView.module.css +0 -1289
- package/web/src/components/settings/SettingsView.jsx +0 -555
- package/web/src/components/settings/SettingsView.module.css +0 -550
- package/web/src/components/tools/ToolPicker.jsx +0 -86
- package/web/src/components/tools/ToolPicker.module.css +0 -97
- package/web/src/components/update/UpdateChip.jsx +0 -104
- package/web/src/components/update/UpdateChip.module.css +0 -146
- package/web/src/global.css +0 -298
- package/web/src/hooks/urlState.constants.js +0 -28
- package/web/src/hooks/useChangelog.js +0 -51
- package/web/src/hooks/useColumnSizing.js +0 -211
- package/web/src/hooks/useFavorites.js +0 -150
- package/web/src/hooks/useFilter.js +0 -310
- package/web/src/hooks/useInstalledModels.js +0 -42
- package/web/src/hooks/useRecommend.js +0 -67
- package/web/src/hooks/useRouterDashboard.js +0 -133
- package/web/src/hooks/useSSE.js +0 -73
- package/web/src/hooks/useSocket.js +0 -203
- package/web/src/hooks/useTheme.js +0 -83
- package/web/src/hooks/useTokenUsage.js +0 -103
- package/web/src/hooks/useToolMode.js +0 -77
- package/web/src/hooks/useUpdateChecker.js +0 -91
- package/web/src/hooks/useUrlState.js +0 -170
- package/web/src/main.jsx +0 -15
- package/web/src/utils/download.js +0 -15
- package/web/src/utils/format.js +0 -42
- package/web/src/utils/m3.js +0 -57
- package/web/src/utils/ranks.js +0 -37
- 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
|
-
}
|