free-coding-models 0.5.86 → 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.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 +38 -14
- package/src/tui/command-palette.js +22 -2
- package/src/tui/key-handler.js +123 -179
- package/src/tui/overlays.js +21 -71
- package/src/tui/render-helpers.js +62 -4
- package/src/tui/render-table.js +34 -3
- 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-Dm-Xfby9.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,446 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/components/dashboard/ModelTable.module.css
|
|
3
|
-
* @description Styles for ModelTable with TanStack Table — full horizontal scroll.
|
|
4
|
-
* 📖 Seamless layout: the container is flush with the page edges (no margin,
|
|
5
|
-
* 📖 no left/right border) so the table runs full-bleed below the sticky
|
|
6
|
-
* 📖 header + sticky FilterBar. Only the bottom border remains.
|
|
7
|
-
*/
|
|
8
|
-
.container {
|
|
9
|
-
background: var(--color-bg-card);
|
|
10
|
-
border-top: 0;
|
|
11
|
-
border-bottom: 1px solid var(--color-border);
|
|
12
|
-
border-left: 0;
|
|
13
|
-
border-right: 0;
|
|
14
|
-
border-radius: 0;
|
|
15
|
-
margin: 0;
|
|
16
|
-
flex: 1;
|
|
17
|
-
min-height: 0;
|
|
18
|
-
/* 📖 The container itself doesn't scroll — it is the relative clip box
|
|
19
|
-
📖 hosting the scrolling table (.scrollInner). overflow:hidden keeps the
|
|
20
|
-
📖 sticky header + virtualized rows neatly clipped to the table viewport.
|
|
21
|
-
📖 isolation:isolate creates a clean stacking context so the sticky
|
|
22
|
-
📖 <thead> header (z-index 10) reliably layers above body cells. */
|
|
23
|
-
overflow: hidden;
|
|
24
|
-
position: relative;
|
|
25
|
-
isolation: isolate;
|
|
26
|
-
display: flex;
|
|
27
|
-
flex-direction: column;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/* 📖 The real scroll surface for the table. It is the scroll element for
|
|
31
|
-
📖 both the sticky <thead> header and the @tanstack/react-virtual row
|
|
32
|
-
📖 virtualizer (only ~23 of 189 rows live in the DOM at any time). */
|
|
33
|
-
.scrollInner {
|
|
34
|
-
flex: 1;
|
|
35
|
-
min-height: 0;
|
|
36
|
-
overflow: auto;
|
|
37
|
-
display: flex;
|
|
38
|
-
flex-direction: column;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.table {
|
|
42
|
-
width: 100%;
|
|
43
|
-
min-width: 1200px;
|
|
44
|
-
border-collapse: collapse;
|
|
45
|
-
font-size: 14px;
|
|
46
|
-
border: 0;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/* ─── Header ─── */
|
|
50
|
-
.th {
|
|
51
|
-
padding: 10px 8px;
|
|
52
|
-
font-size: 12px;
|
|
53
|
-
font-weight: 800;
|
|
54
|
-
font-family: var(--font-mono);
|
|
55
|
-
text-transform: uppercase;
|
|
56
|
-
letter-spacing: 0.8px;
|
|
57
|
-
color: var(--color-bg);
|
|
58
|
-
background: var(--color-accent);
|
|
59
|
-
border-bottom: 2px solid var(--color-border);
|
|
60
|
-
border-right: 1px solid rgba(0, 0, 0, 0.08);
|
|
61
|
-
white-space: nowrap;
|
|
62
|
-
user-select: none;
|
|
63
|
-
position: sticky;
|
|
64
|
-
top: 0;
|
|
65
|
-
z-index: 10;
|
|
66
|
-
text-align: center;
|
|
67
|
-
transition: all 120ms var(--ease-out);
|
|
68
|
-
/* 📖 Disable user-select on the whole header so dragging the resize handle
|
|
69
|
-
📖 never accidentally selects the cell text on rapid drags. */
|
|
70
|
-
-webkit-user-select: none;
|
|
71
|
-
-moz-user-select: none;
|
|
72
|
-
-ms-user-select: none;
|
|
73
|
-
}
|
|
74
|
-
.th:last-child { border-right: 0; }
|
|
75
|
-
.th:hover { color: var(--color-bg); background: var(--color-accent-hover); }
|
|
76
|
-
|
|
77
|
-
/* 📖 Inner flex container: header label on the left, resize handle on the right.
|
|
78
|
-
📖 Letting the label shrink keeps the column tight when the user makes it narrow. */
|
|
79
|
-
.thInner {
|
|
80
|
-
display: flex;
|
|
81
|
-
align-items: center;
|
|
82
|
-
justify-content: space-between;
|
|
83
|
-
gap: 4px;
|
|
84
|
-
width: 100%;
|
|
85
|
-
min-width: 0;
|
|
86
|
-
}
|
|
87
|
-
.thLabel {
|
|
88
|
-
display: inline-flex;
|
|
89
|
-
align-items: center;
|
|
90
|
-
gap: 4px;
|
|
91
|
-
min-width: 0;
|
|
92
|
-
overflow: hidden;
|
|
93
|
-
text-overflow: ellipsis;
|
|
94
|
-
white-space: nowrap;
|
|
95
|
-
flex: 1;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/* 📖 Resize handle: a 6px-wide grab zone on the right edge of every header.
|
|
99
|
-
📖 The visible bar is 2px wide and centered in the grab zone so users get
|
|
100
|
-
📖 generous hit-target without it looking like a hard line. */
|
|
101
|
-
.resizeHandle {
|
|
102
|
-
flex-shrink: 0;
|
|
103
|
-
width: 6px;
|
|
104
|
-
height: 22px;
|
|
105
|
-
margin-left: 2px;
|
|
106
|
-
display: inline-flex;
|
|
107
|
-
align-items: center;
|
|
108
|
-
justify-content: center;
|
|
109
|
-
cursor: col-resize;
|
|
110
|
-
position: relative;
|
|
111
|
-
touch-action: none;
|
|
112
|
-
/* 📖 Stop the click from also triggering the header's sort handler. */
|
|
113
|
-
user-select: none;
|
|
114
|
-
-webkit-user-select: none;
|
|
115
|
-
}
|
|
116
|
-
.resizeHandle::after {
|
|
117
|
-
content: '';
|
|
118
|
-
display: block;
|
|
119
|
-
width: 2px;
|
|
120
|
-
height: 14px;
|
|
121
|
-
background: transparent;
|
|
122
|
-
border-radius: 1px;
|
|
123
|
-
transition: background 120ms var(--ease-out, ease);
|
|
124
|
-
}
|
|
125
|
-
.resizeHandle:hover::after,
|
|
126
|
-
.resizeHandleActive::after {
|
|
127
|
-
background: var(--color-accent);
|
|
128
|
-
}
|
|
129
|
-
.resizeHandleActive {
|
|
130
|
-
cursor: col-resize;
|
|
131
|
-
}
|
|
132
|
-
/* 📖 While a column is actively resizing, highlight its entire header and freeze
|
|
133
|
-
📖 the cursor on the whole document so dragging off the handle still works. */
|
|
134
|
-
.th:has(.resizeHandleActive) {
|
|
135
|
-
background: var(--color-bg-active);
|
|
136
|
-
color: var(--color-accent);
|
|
137
|
-
}
|
|
138
|
-
:global(.col-resizing) {
|
|
139
|
-
cursor: col-resize !important;
|
|
140
|
-
user-select: none !important;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/* 📖 Floating bar shown only when the user has custom column widths.
|
|
144
|
-
📖 Rendered OUTSIDE the scroller (a normal flex child at the top of the
|
|
145
|
-
📖 table container) so it stays put without colliding with the sticky
|
|
146
|
-
📖 <thead>. It shrinks the scroll area; the sticky header sticks below it. */
|
|
147
|
-
.resizeToolbar {
|
|
148
|
-
display: flex;
|
|
149
|
-
align-items: center;
|
|
150
|
-
justify-content: space-between;
|
|
151
|
-
gap: 8px;
|
|
152
|
-
padding: 6px 10px;
|
|
153
|
-
background: var(--color-bg-elevated);
|
|
154
|
-
border-bottom: 1px solid var(--color-border);
|
|
155
|
-
font-size: 13px;
|
|
156
|
-
color: var(--color-text-muted);
|
|
157
|
-
flex-shrink: 0;
|
|
158
|
-
}
|
|
159
|
-
.resizeToolbarHint {
|
|
160
|
-
display: inline-flex;
|
|
161
|
-
align-items: center;
|
|
162
|
-
gap: 4px;
|
|
163
|
-
letter-spacing: 0.2px;
|
|
164
|
-
}
|
|
165
|
-
.resizeToolbarHint strong {
|
|
166
|
-
color: var(--color-text);
|
|
167
|
-
font-weight: 700;
|
|
168
|
-
font-family: var(--font-mono);
|
|
169
|
-
}
|
|
170
|
-
.resizeResetButton {
|
|
171
|
-
appearance: none;
|
|
172
|
-
background: transparent;
|
|
173
|
-
border: 1px solid var(--color-border-hover);
|
|
174
|
-
color: var(--color-text-muted);
|
|
175
|
-
padding: 3px 10px;
|
|
176
|
-
border-radius: 4px;
|
|
177
|
-
font-size: 13px;
|
|
178
|
-
font-weight: 600;
|
|
179
|
-
cursor: pointer;
|
|
180
|
-
transition: background 120ms var(--ease-out, ease), color 120ms var(--ease-out, ease), border-color 120ms var(--ease-out, ease);
|
|
181
|
-
white-space: nowrap;
|
|
182
|
-
}
|
|
183
|
-
.resizeResetButton:hover {
|
|
184
|
-
background: var(--color-bg-active);
|
|
185
|
-
color: var(--color-accent);
|
|
186
|
-
border-color: var(--color-accent);
|
|
187
|
-
}
|
|
188
|
-
.resizeResetButton:focus-visible {
|
|
189
|
-
outline: 2px solid var(--color-accent);
|
|
190
|
-
outline-offset: 1px;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/* Center certain columns */
|
|
194
|
-
.th:nth-child(6) { text-align: left; } /* Model */
|
|
195
|
-
.th:nth-child(7) { text-align: left; } /* Provider */
|
|
196
|
-
.th:nth-child(11) { text-align: left; } /* Verdict */
|
|
197
|
-
|
|
198
|
-
/* Sort icons */
|
|
199
|
-
.sortIcon { opacity: 0.4; margin-left: 3px; font-size: 9px; color: var(--color-bg); }
|
|
200
|
-
.sortIconActive { color: var(--color-bg); margin-left: 3px; font-size: 10px; font-weight: 900; }
|
|
201
|
-
|
|
202
|
-
/* ─── Data cells ─── */
|
|
203
|
-
.td {
|
|
204
|
-
padding: 7px 8px;
|
|
205
|
-
border-bottom: 1px solid var(--color-border);
|
|
206
|
-
border-right: 1px solid var(--color-border);
|
|
207
|
-
white-space: nowrap;
|
|
208
|
-
}
|
|
209
|
-
.td:last-child { border-right: 0; }
|
|
210
|
-
.td:nth-child(6) { text-align: left; } /* Model */
|
|
211
|
-
.td:nth-child(7) { text-align: left; } /* Provider */
|
|
212
|
-
.td:nth-child(11) { text-align: left; } /* Verdict */
|
|
213
|
-
|
|
214
|
-
/* 📖 Center the BOOKMARKS column (star button) so it sits nicely in its
|
|
215
|
-
📖 wider header. Other narrow numeric columns are centered by default. */
|
|
216
|
-
.td:nth-child(1),
|
|
217
|
-
.th:nth-child(1) { text-align: center; }
|
|
218
|
-
|
|
219
|
-
/* ─── Row hover ─── */
|
|
220
|
-
.table tbody tr {
|
|
221
|
-
cursor: pointer;
|
|
222
|
-
transition: background 100ms;
|
|
223
|
-
}
|
|
224
|
-
.table tbody tr:hover { background: var(--color-bg-hover); }
|
|
225
|
-
|
|
226
|
-
/* ─── Medal rows ─── */
|
|
227
|
-
.rank1 td:first-child { border-left: 3px solid #ffd700; }
|
|
228
|
-
.rank2 td:first-child { border-left: 3px solid #c0c0c0; }
|
|
229
|
-
.rank3 td:first-child { border-left: 3px solid #cd7f32; }
|
|
230
|
-
|
|
231
|
-
/* ─── Shared cell values ─── */
|
|
232
|
-
.rankNum {
|
|
233
|
-
font-family: var(--font-mono);
|
|
234
|
-
font-size: 13px;
|
|
235
|
-
color: var(--color-text-dim);
|
|
236
|
-
text-align: center;
|
|
237
|
-
display: block;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
.swe { font-family: var(--font-mono); font-weight: 600; }
|
|
241
|
-
.sweHigh { color: #ffd700; }
|
|
242
|
-
.sweMid { color: #3ddc84; }
|
|
243
|
-
.sweLow { color: var(--color-text-dim); }
|
|
244
|
-
|
|
245
|
-
.ctx { font-family: var(--font-mono); font-size: 13px; }
|
|
246
|
-
|
|
247
|
-
.ping { font-family: var(--font-mono); font-weight: 600; }
|
|
248
|
-
.pingFast { color: #00ff88; }
|
|
249
|
-
.pingMedium { color: #ffaa00; }
|
|
250
|
-
.pingSlow { color: #ff4444; }
|
|
251
|
-
.pingNone { color: var(--color-text-dim); }
|
|
252
|
-
|
|
253
|
-
/* 📖 Light theme: darker SWE + ping colors. */
|
|
254
|
-
:global([data-theme="light"]) .sweHigh { color: #7a5e00; }
|
|
255
|
-
:global([data-theme="light"]) .sweMid { color: #1f7a3e; }
|
|
256
|
-
:global([data-theme="light"]) .pingFast { color: #008f4d; }
|
|
257
|
-
:global([data-theme="light"]) .pingMedium { color: #b86b00; }
|
|
258
|
-
:global([data-theme="light"]) .pingSlow { color: #c8143a; }
|
|
259
|
-
|
|
260
|
-
.uptime { font-family: var(--font-mono); font-size: 13px; }
|
|
261
|
-
|
|
262
|
-
.quota {
|
|
263
|
-
font-family: var(--font-mono);
|
|
264
|
-
font-size: 13px;
|
|
265
|
-
font-weight: 800;
|
|
266
|
-
}
|
|
267
|
-
.quotaGood { color: #00ff88; }
|
|
268
|
-
.quotaWarn { color: #ffaa00; }
|
|
269
|
-
.quotaBad { color: #ff4444; }
|
|
270
|
-
.quotaNA,
|
|
271
|
-
.quotaNone { color: var(--color-text-dim); }
|
|
272
|
-
|
|
273
|
-
/* ─── Model cell ─── */
|
|
274
|
-
.modelCell { display: flex; align-items: center; gap: 6px; overflow: hidden; }
|
|
275
|
-
.modelMeta { flex: 1; min-width: 0; }
|
|
276
|
-
.modelHeader { display: flex; align-items: center; gap: 5px; }
|
|
277
|
-
.modelName { font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
278
|
-
.modelId { font-family: var(--font-mono); font-size: 11px; color: var(--color-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
279
|
-
|
|
280
|
-
/* 📖 Provider cell now uses <ProviderLogo /> (icon + wordmark) — see
|
|
281
|
-
📖 atoms/ProviderLogo.module.css for the layout. The old bordered pill
|
|
282
|
-
📖 was replaced; no extra styling is needed here. */
|
|
283
|
-
|
|
284
|
-
/* ─── Benchmarking row highlight ── */
|
|
285
|
-
@keyframes benchRowPulse {
|
|
286
|
-
0%, 100% { background: rgba(180, 0, 255, 0.04); }
|
|
287
|
-
50% { background: rgba(180, 0, 255, 0.10); }
|
|
288
|
-
}
|
|
289
|
-
.benchRow {
|
|
290
|
-
animation: benchRowPulse 1.2s ease-in-out infinite;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/* ─── Empty state ─── */
|
|
294
|
-
:global([data-theme="light"]) .container {
|
|
295
|
-
border-color: #cfcfcf;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
:global([data-theme="light"]) .table {
|
|
299
|
-
border-color: #cfcfcf;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
:global([data-theme="light"]) .th,
|
|
303
|
-
:global([data-theme="light"]) .td {
|
|
304
|
-
border-color: #d8d8d8;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
.empty { text-align: center; padding: 60px 0; color: var(--color-text-muted); }
|
|
308
|
-
|
|
309
|
-
/* ─── M1: favorites star button (TUI `F` key) ─── */
|
|
310
|
-
.favBtn {
|
|
311
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
312
|
-
width: 24px; height: 24px;
|
|
313
|
-
background: transparent;
|
|
314
|
-
border: none;
|
|
315
|
-
color: var(--color-text-dim);
|
|
316
|
-
cursor: pointer;
|
|
317
|
-
border-radius: 4px;
|
|
318
|
-
transition: color 120ms, background 120ms, transform 120ms;
|
|
319
|
-
}
|
|
320
|
-
.favBtn:hover {
|
|
321
|
-
color: #ffd700;
|
|
322
|
-
background: rgba(255, 215, 0, 0.10);
|
|
323
|
-
transform: scale(1.1);
|
|
324
|
-
}
|
|
325
|
-
.favBtnActive {
|
|
326
|
-
color: #ffd700;
|
|
327
|
-
}
|
|
328
|
-
.favBtnActive:hover {
|
|
329
|
-
color: #ffae00;
|
|
330
|
-
background: rgba(255, 174, 0, 0.14);
|
|
331
|
-
}
|
|
332
|
-
:global([data-theme="light"]) .favBtn { color: #999; }
|
|
333
|
-
:global([data-theme="light"]) .favBtn:hover { color: #b08800; background: rgba(176, 136, 0, 0.10); }
|
|
334
|
-
:global([data-theme="light"]) .favBtnActive { color: #b08800; }
|
|
335
|
-
|
|
336
|
-
/* ─── M1: clickable AI Lat. cell (TUI `Ctrl+A`) ─── */
|
|
337
|
-
.aiLatencyWrap {
|
|
338
|
-
display: inline-flex; align-items: center; justify-content: center; gap: 4px;
|
|
339
|
-
cursor: pointer;
|
|
340
|
-
padding: 2px 4px;
|
|
341
|
-
border-radius: 4px;
|
|
342
|
-
position: relative;
|
|
343
|
-
transition: background 100ms;
|
|
344
|
-
}
|
|
345
|
-
.aiLatencyWrap:hover {
|
|
346
|
-
background: rgba(180, 0, 255, 0.08);
|
|
347
|
-
}
|
|
348
|
-
.aiLatencyWrap.aiLatencyRunning { cursor: progress; }
|
|
349
|
-
.aiLatencyWrap.aiLatencyRunning:hover { background: transparent; }
|
|
350
|
-
.aiLatencyPlay {
|
|
351
|
-
opacity: 0;
|
|
352
|
-
color: #b400ff;
|
|
353
|
-
transition: opacity 100ms;
|
|
354
|
-
}
|
|
355
|
-
.aiLatencyWrap:hover .aiLatencyPlay { opacity: 1; }
|
|
356
|
-
.aiLatencyRunning .aiLatencyPlay { display: none; }
|
|
357
|
-
|
|
358
|
-
/* ─── M1: dark-red row for tool-incompatible models ─── */
|
|
359
|
-
.incompatible td {
|
|
360
|
-
background: rgba(255, 0, 0, 0.06) !important;
|
|
361
|
-
color: var(--color-text-muted) !important;
|
|
362
|
-
}
|
|
363
|
-
.incompatible:hover td {
|
|
364
|
-
background: rgba(255, 0, 0, 0.10) !important;
|
|
365
|
-
}
|
|
366
|
-
:global([data-theme="light"]) .incompatible td {
|
|
367
|
-
background: rgba(200, 20, 58, 0.06) !important;
|
|
368
|
-
color: #888 !important;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/* ─── M4: 80% opacity for "unusable" rows (NO KEY / AUTH FAIL) ───
|
|
372
|
-
📖 Mirrors the TUI's fadedRow() behavior so the same scan-and-spot
|
|
373
|
-
📖 UX works on every surface. Applied to the whole <tr> via opacity
|
|
374
|
-
📖 so it composes cleanly with rank/bench/incompatible styles. The
|
|
375
|
-
📖 fade is held steady on hover so the "you cannot use this" signal
|
|
376
|
-
📖 never disappears while the user is checking what is on the row. */
|
|
377
|
-
.unusable {
|
|
378
|
-
opacity: 0.8;
|
|
379
|
-
}
|
|
380
|
-
.unusable:hover {
|
|
381
|
-
opacity: 0.8;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/* 📖 Models not in the active router set — slightly faded so users can
|
|
385
|
-
📖 instantly see which models are being tested vs just cataloged. */
|
|
386
|
-
.notInSetRow {
|
|
387
|
-
opacity: 0.6;
|
|
388
|
-
}
|
|
389
|
-
.notInSetRow:hover {
|
|
390
|
-
opacity: 0.85;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/* ─── M3: per-row endpoint install plug ─── */
|
|
394
|
-
.launchCell {
|
|
395
|
-
display: flex;
|
|
396
|
-
justify-content: center;
|
|
397
|
-
opacity: 0;
|
|
398
|
-
transition: opacity 120ms;
|
|
399
|
-
}
|
|
400
|
-
.table tbody tr:hover .launchCell,
|
|
401
|
-
.incompatible .launchCell,
|
|
402
|
-
.benchRow .launchCell {
|
|
403
|
-
opacity: 1;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/* ─── Expandable row styles ─── */
|
|
407
|
-
.expandedRow td {
|
|
408
|
-
background: var(--color-accent) !important;
|
|
409
|
-
color: var(--color-bg) !important;
|
|
410
|
-
border-bottom: 1px solid rgba(0, 0, 0, 0.15) !important;
|
|
411
|
-
}
|
|
412
|
-
.expandedRow td * {
|
|
413
|
-
color: var(--color-bg) !important;
|
|
414
|
-
}
|
|
415
|
-
.expandedRow :global(.status-dot) {
|
|
416
|
-
border-color: var(--color-bg) !important;
|
|
417
|
-
}
|
|
418
|
-
.expandedRow :global(.provider-wordmark) {
|
|
419
|
-
filter: brightness(0) saturate(100%) !important; /* Forces logos to render dark/black for contrast */
|
|
420
|
-
}
|
|
421
|
-
.expandedRow .rankNum,
|
|
422
|
-
.expandedRow .modelId {
|
|
423
|
-
color: rgba(0, 0, 0, 0.6) !important;
|
|
424
|
-
}
|
|
425
|
-
.expandedRow .sweLow,
|
|
426
|
-
.expandedRow .sweMid,
|
|
427
|
-
.expandedRow .sweHigh {
|
|
428
|
-
color: var(--color-bg) !important;
|
|
429
|
-
}
|
|
430
|
-
.expandedRow .pingFast,
|
|
431
|
-
.expandedRow .pingMedium,
|
|
432
|
-
.expandedRow .pingSlow {
|
|
433
|
-
color: var(--color-bg) !important;
|
|
434
|
-
font-weight: 800;
|
|
435
|
-
}
|
|
436
|
-
.expandRowWrapper {
|
|
437
|
-
border: none;
|
|
438
|
-
}
|
|
439
|
-
.expandRowCell {
|
|
440
|
-
padding: 0 !important;
|
|
441
|
-
border-bottom: 3px solid var(--color-accent) !important;
|
|
442
|
-
background: var(--color-bg-elevated) !important;
|
|
443
|
-
}
|
|
444
|
-
.expandRowCell * {
|
|
445
|
-
color: inherit;
|
|
446
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/components/dashboard/ProviderDropdown.jsx
|
|
3
|
-
* @description Custom dropdown for provider filtering with inline SVG logos + health indicator.
|
|
4
|
-
*
|
|
5
|
-
* @details
|
|
6
|
-
* - Replaces the native `<select>` for providers with a custom dropdown that
|
|
7
|
-
* renders each provider as [SVG icon + wordmark + health dot + model count].
|
|
8
|
-
* - Uses the same `ProviderLogo` component as the model table so branding is
|
|
9
|
-
* visually consistent across the dashboard.
|
|
10
|
-
* - The health indicator dot shows:
|
|
11
|
-
* 🟢 Green (glow) — provider has at least one model with status 'up' (key works).
|
|
12
|
-
* 🟡 Yellow — provider has keys configured but no models are 'up' yet (pending).
|
|
13
|
-
* 🔴 Red — provider has keys but models are down / auth errors.
|
|
14
|
-
* ⚪ Gray — provider has no API key configured at all.
|
|
15
|
-
* - Theme-aware: colors adapt for both dark and light modes via CSS variables.
|
|
16
|
-
* - Click-outside + Escape to close, keyboard-friendly, scrollable for long lists.
|
|
17
|
-
*
|
|
18
|
-
* @param {object} props
|
|
19
|
-
* @param {Array} props.providers — Array of { key, name, count, hasKey, anyUp }
|
|
20
|
-
* @param {string} props.value — Currently selected provider key ('all' for no filter).
|
|
21
|
-
* @param {function} props.onChange — Callback with new provider key.
|
|
22
|
-
*
|
|
23
|
-
* @functions
|
|
24
|
-
* → ProviderDropdown (default export)
|
|
25
|
-
*
|
|
26
|
-
* @see web/src/components/dashboard/FilterBar.jsx (consumer)
|
|
27
|
-
* @see web/src/components/atoms/ProviderLogo.jsx (logo renderer)
|
|
28
|
-
*/
|
|
29
|
-
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
30
|
-
import { IconChevronDown } from '@tabler/icons-react'
|
|
31
|
-
import ProviderLogo from '../atoms/ProviderLogo.jsx'
|
|
32
|
-
import styles from './ProviderDropdown.module.css'
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 📖 Derives the health indicator state from aggregated provider data.
|
|
36
|
-
* Returns one of: 'active' | 'pending' | 'down' | 'nokey'.
|
|
37
|
-
*/
|
|
38
|
-
function providerHealthState(provider) {
|
|
39
|
-
if (!provider.hasKey) return 'nokey'
|
|
40
|
-
if (provider.anyUp) return 'active'
|
|
41
|
-
return 'down'
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function HealthIndicator({ state }) {
|
|
45
|
-
const cls = styles[`dot_${state}`] || styles.dot_nokey
|
|
46
|
-
const titles = {
|
|
47
|
-
active: 'API key works — at least one model is UP',
|
|
48
|
-
pending: 'API key set — waiting for health data',
|
|
49
|
-
down: 'API key set — models are DOWN or have auth errors',
|
|
50
|
-
nokey: 'No API key configured',
|
|
51
|
-
}
|
|
52
|
-
return <span className={`${styles.healthDot} ${cls}`} title={titles[state] || ''} />
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export default function ProviderDropdown({ providers, value, onChange }) {
|
|
56
|
-
const [open, setOpen] = useState(false)
|
|
57
|
-
const ref = useRef(null)
|
|
58
|
-
const listRef = useRef(null)
|
|
59
|
-
|
|
60
|
-
const close = useCallback(() => setOpen(false), [])
|
|
61
|
-
|
|
62
|
-
// 📖 Close on outside click or Escape.
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
if (!open) return
|
|
65
|
-
const handleClickOutside = (e) => {
|
|
66
|
-
if (ref.current && !ref.current.contains(e.target)) close()
|
|
67
|
-
}
|
|
68
|
-
const handleKey = (e) => { if (e.key === 'Escape') close() }
|
|
69
|
-
document.addEventListener('mousedown', handleClickOutside)
|
|
70
|
-
document.addEventListener('keydown', handleKey)
|
|
71
|
-
return () => {
|
|
72
|
-
document.removeEventListener('mousedown', handleClickOutside)
|
|
73
|
-
document.removeEventListener('keydown', handleKey)
|
|
74
|
-
}
|
|
75
|
-
}, [open, close])
|
|
76
|
-
|
|
77
|
-
// 📖 Scroll selected item into view when dropdown opens.
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
if (!open || !listRef.current) return
|
|
80
|
-
const selected = listRef.current.querySelector(`[data-active="true"]`)
|
|
81
|
-
if (selected) selected.scrollIntoView({ block: 'nearest' })
|
|
82
|
-
}, [open])
|
|
83
|
-
|
|
84
|
-
const selectedProvider = providers.find(p => p.key === value)
|
|
85
|
-
const isFiltered = value !== 'all'
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<div className={styles.dropdown} ref={ref}>
|
|
89
|
-
<button
|
|
90
|
-
className={`${styles.trigger} ${isFiltered ? styles.triggerActive : ''} ${open ? styles.triggerOpen : ''}`}
|
|
91
|
-
onClick={() => setOpen(!open)}
|
|
92
|
-
aria-expanded={open}
|
|
93
|
-
aria-haspopup="listbox"
|
|
94
|
-
title={isFiltered ? `Provider: ${selectedProvider?.name || value}` : 'All providers'}
|
|
95
|
-
>
|
|
96
|
-
<span className={styles.triggerContent}>
|
|
97
|
-
{isFiltered && selectedProvider ? (
|
|
98
|
-
<>
|
|
99
|
-
<span className={styles.triggerLogo}>
|
|
100
|
-
<ProviderLogo providerKey={selectedProvider.key} origin={selectedProvider.name} />
|
|
101
|
-
</span>
|
|
102
|
-
<span className={styles.triggerCount}>{selectedProvider.count}</span>
|
|
103
|
-
</>
|
|
104
|
-
) : (
|
|
105
|
-
<span className={styles.triggerLabel}>All Providers</span>
|
|
106
|
-
)}
|
|
107
|
-
</span>
|
|
108
|
-
<IconChevronDown
|
|
109
|
-
size={12}
|
|
110
|
-
stroke={2}
|
|
111
|
-
className={`${styles.chevron} ${open ? styles.chevronOpen : ''}`}
|
|
112
|
-
/>
|
|
113
|
-
</button>
|
|
114
|
-
|
|
115
|
-
{open && (
|
|
116
|
-
<div className={styles.menu} role="listbox" ref={listRef}>
|
|
117
|
-
{/* ── "All Providers" option ── */}
|
|
118
|
-
<button
|
|
119
|
-
role="option"
|
|
120
|
-
aria-selected={value === 'all'}
|
|
121
|
-
data-active={value === 'all'}
|
|
122
|
-
className={`${styles.option} ${value === 'all' ? styles.optionActive : ''}`}
|
|
123
|
-
onClick={() => { onChange('all'); close() }}
|
|
124
|
-
>
|
|
125
|
-
<span className={styles.optionLabel}>All Providers</span>
|
|
126
|
-
<span className={styles.optionCount}>{providers.reduce((s, p) => s + p.count, 0)}</span>
|
|
127
|
-
</button>
|
|
128
|
-
|
|
129
|
-
<div className={styles.separator} />
|
|
130
|
-
|
|
131
|
-
{/* ── Per-provider options ── */}
|
|
132
|
-
{providers.map((p) => {
|
|
133
|
-
const healthState = providerHealthState(p)
|
|
134
|
-
return (
|
|
135
|
-
<button
|
|
136
|
-
key={p.key}
|
|
137
|
-
role="option"
|
|
138
|
-
aria-selected={value === p.key}
|
|
139
|
-
data-active={value === p.key}
|
|
140
|
-
className={`${styles.option} ${value === p.key ? styles.optionActive : ''}`}
|
|
141
|
-
onClick={() => { onChange(p.key); close() }}
|
|
142
|
-
title={`${p.name} — ${p.count} model${p.count !== 1 ? 's' : ''}`}
|
|
143
|
-
>
|
|
144
|
-
<span className={styles.optionLogo}>
|
|
145
|
-
<ProviderLogo providerKey={p.key} origin={p.name} />
|
|
146
|
-
</span>
|
|
147
|
-
<HealthIndicator state={healthState} />
|
|
148
|
-
<span className={styles.optionCount}>{p.count}</span>
|
|
149
|
-
</button>
|
|
150
|
-
)
|
|
151
|
-
})}
|
|
152
|
-
</div>
|
|
153
|
-
)}
|
|
154
|
-
</div>
|
|
155
|
-
)
|
|
156
|
-
}
|