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,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
- }