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.
Files changed (163) hide show
  1. package/README.md +110 -881
  2. package/bin/free-coding-models.js +4 -1
  3. package/changelog/v0.5.87.md +48 -0
  4. package/package.json +8 -5
  5. package/sources.js +4 -4
  6. package/src/core/cache.js +3 -1
  7. package/src/core/config.js +43 -5
  8. package/src/core/endpoint-installer.js +23 -12
  9. package/src/core/installed-models-manager.js +43 -20
  10. package/src/core/kilo-config.js +51 -5
  11. package/src/core/kilo.js +5 -3
  12. package/src/core/legacy-proxy-cleanup.js +22 -5
  13. package/src/core/model-merger.js +4 -0
  14. package/src/core/models-drift.js +9 -0
  15. package/src/core/opencode-config.js +57 -5
  16. package/src/core/opencode.js +67 -41
  17. package/src/core/playground.js +7 -0
  18. package/src/core/probe-cache.js +35 -4
  19. package/src/core/provider-key-tester.js +10 -2
  20. package/src/core/provider-metadata.js +10 -0
  21. package/src/core/provider-quota-fetchers.js +14 -7
  22. package/src/core/router-daemon.js +208 -26
  23. package/src/core/runtime-telemetry.js +27 -1
  24. package/src/core/shared-helpers.js +12 -1
  25. package/src/core/shell-env.js +3 -1
  26. package/src/core/telemetry.js +18 -5
  27. package/src/core/tool-launchers.js +9 -6
  28. package/src/core/updater.js +28 -2
  29. package/src/core/utils.js +76 -71
  30. package/src/tui/app.js +38 -14
  31. package/src/tui/command-palette.js +22 -2
  32. package/src/tui/key-handler.js +123 -179
  33. package/src/tui/overlays.js +21 -71
  34. package/src/tui/render-helpers.js +62 -4
  35. package/src/tui/render-table.js +34 -3
  36. package/src/tui/theme.js +36 -5
  37. package/src/tui/tui-filters.js +28 -8
  38. package/src/tui/tui-state.js +22 -6
  39. package/web/dist/assets/index-BpTpPS0g.js +44 -0
  40. package/web/dist/assets/index-CAzFIt8P.css +1 -0
  41. package/web/dist/index.html +5 -9
  42. package/web/index.html +3 -7
  43. package/web/server.js +97 -23
  44. package/web/vite.config.js +8 -0
  45. package/web/app.legacy.js +0 -900
  46. package/web/dist/assets/index-C5-Ywvv3.css +0 -1
  47. package/web/dist/assets/index-Dm-Xfby9.js +0 -40
  48. package/web/public/favicon.ico +0 -0
  49. package/web/public/favicons/apple-touch-icon.png +0 -0
  50. package/web/public/favicons/browserconfig.xml +0 -12
  51. package/web/public/favicons/favicon-16x16.png +0 -0
  52. package/web/public/favicons/favicon-192x192.png +0 -0
  53. package/web/public/favicons/favicon-32x32.png +0 -0
  54. package/web/public/favicons/favicon-48x48.png +0 -0
  55. package/web/public/favicons/favicon-512x512.png +0 -0
  56. package/web/public/favicons/favicon-96x96.png +0 -0
  57. package/web/public/favicons/favicon.ico +0 -0
  58. package/web/public/favicons/mstile-150x150.png +0 -0
  59. package/web/public/favicons/mstile-310x150.png +0 -0
  60. package/web/public/favicons/mstile-310x310.png +0 -0
  61. package/web/public/favicons/mstile-512x512.png +0 -0
  62. package/web/public/favicons/mstile-70x70.png +0 -0
  63. package/web/public/favicons/site.webmanifest +0 -25
  64. package/web/src/App.jsx +0 -521
  65. package/web/src/components/analytics/AnalyticsView.jsx +0 -147
  66. package/web/src/components/analytics/AnalyticsView.module.css +0 -190
  67. package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
  68. package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
  69. package/web/src/components/atoms/AILatencyCell.jsx +0 -38
  70. package/web/src/components/atoms/AILatencyCell.module.css +0 -51
  71. package/web/src/components/atoms/HealthCell.jsx +0 -143
  72. package/web/src/components/atoms/HealthCell.module.css +0 -60
  73. package/web/src/components/atoms/LastPingCell.jsx +0 -35
  74. package/web/src/components/atoms/LastPingCell.module.css +0 -45
  75. package/web/src/components/atoms/MoodCell.jsx +0 -25
  76. package/web/src/components/atoms/MoodCell.module.css +0 -6
  77. package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
  78. package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
  79. package/web/src/components/atoms/ProviderLogo.jsx +0 -187
  80. package/web/src/components/atoms/ProviderLogo.module.css +0 -103
  81. package/web/src/components/atoms/RankCell.jsx +0 -9
  82. package/web/src/components/atoms/RankCell.module.css +0 -9
  83. package/web/src/components/atoms/Sparkline.jsx +0 -63
  84. package/web/src/components/atoms/StabilityCell.jsx +0 -18
  85. package/web/src/components/atoms/StabilityCell.module.css +0 -13
  86. package/web/src/components/atoms/StatusDot.jsx +0 -13
  87. package/web/src/components/atoms/StatusDot.module.css +0 -29
  88. package/web/src/components/atoms/TPSCell.jsx +0 -36
  89. package/web/src/components/atoms/TPSCell.module.css +0 -44
  90. package/web/src/components/atoms/TierBadge.jsx +0 -10
  91. package/web/src/components/atoms/TierBadge.module.css +0 -29
  92. package/web/src/components/atoms/Toast.jsx +0 -25
  93. package/web/src/components/atoms/Toast.module.css +0 -41
  94. package/web/src/components/atoms/ToastContainer.jsx +0 -16
  95. package/web/src/components/atoms/ToastContainer.module.css +0 -10
  96. package/web/src/components/atoms/VerdictBadge.jsx +0 -36
  97. package/web/src/components/atoms/VerdictBadge.module.css +0 -40
  98. package/web/src/components/changelog/ChangelogView.jsx +0 -135
  99. package/web/src/components/changelog/ChangelogView.module.css +0 -159
  100. package/web/src/components/dashboard/DetailPanel.jsx +0 -217
  101. package/web/src/components/dashboard/DetailPanel.module.css +0 -260
  102. package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
  103. package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
  104. package/web/src/components/dashboard/ExportModal.jsx +0 -84
  105. package/web/src/components/dashboard/ExportModal.module.css +0 -99
  106. package/web/src/components/dashboard/FilterBar.jsx +0 -338
  107. package/web/src/components/dashboard/FilterBar.module.css +0 -342
  108. package/web/src/components/dashboard/ModelTable.jsx +0 -672
  109. package/web/src/components/dashboard/ModelTable.module.css +0 -446
  110. package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
  111. package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
  112. package/web/src/components/help/HelpView.jsx +0 -201
  113. package/web/src/components/help/HelpView.module.css +0 -156
  114. package/web/src/components/install/InstallEndpointsView.jsx +0 -309
  115. package/web/src/components/install/InstallEndpointsView.module.css +0 -427
  116. package/web/src/components/installed/InstalledModelsView.jsx +0 -89
  117. package/web/src/components/installed/InstalledModelsView.module.css +0 -200
  118. package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
  119. package/web/src/components/launch/LaunchButton.jsx +0 -30
  120. package/web/src/components/launch/LaunchButton.module.css +0 -35
  121. package/web/src/components/launch/LaunchModal.module.css +0 -125
  122. package/web/src/components/layout/Footer.jsx +0 -19
  123. package/web/src/components/layout/Footer.module.css +0 -10
  124. package/web/src/components/layout/Header.jsx +0 -265
  125. package/web/src/components/layout/Header.module.css +0 -357
  126. package/web/src/components/palette/CommandPalette.jsx +0 -301
  127. package/web/src/components/palette/CommandPalette.module.css +0 -121
  128. package/web/src/components/playground/PlaygroundChat.jsx +0 -527
  129. package/web/src/components/playground/PlaygroundChat.module.css +0 -382
  130. package/web/src/components/playground/PlaygroundView.jsx +0 -371
  131. package/web/src/components/playground/PlaygroundView.module.css +0 -504
  132. package/web/src/components/recommend/RecommendView.jsx +0 -121
  133. package/web/src/components/recommend/RecommendView.module.css +0 -57
  134. package/web/src/components/router/RouterView.jsx +0 -1212
  135. package/web/src/components/router/RouterView.module.css +0 -1289
  136. package/web/src/components/settings/SettingsView.jsx +0 -555
  137. package/web/src/components/settings/SettingsView.module.css +0 -550
  138. package/web/src/components/tools/ToolPicker.jsx +0 -86
  139. package/web/src/components/tools/ToolPicker.module.css +0 -97
  140. package/web/src/components/update/UpdateChip.jsx +0 -104
  141. package/web/src/components/update/UpdateChip.module.css +0 -146
  142. package/web/src/global.css +0 -298
  143. package/web/src/hooks/urlState.constants.js +0 -28
  144. package/web/src/hooks/useChangelog.js +0 -51
  145. package/web/src/hooks/useColumnSizing.js +0 -211
  146. package/web/src/hooks/useFavorites.js +0 -150
  147. package/web/src/hooks/useFilter.js +0 -310
  148. package/web/src/hooks/useInstalledModels.js +0 -42
  149. package/web/src/hooks/useRecommend.js +0 -67
  150. package/web/src/hooks/useRouterDashboard.js +0 -133
  151. package/web/src/hooks/useSSE.js +0 -73
  152. package/web/src/hooks/useSocket.js +0 -203
  153. package/web/src/hooks/useTheme.js +0 -83
  154. package/web/src/hooks/useTokenUsage.js +0 -103
  155. package/web/src/hooks/useToolMode.js +0 -77
  156. package/web/src/hooks/useUpdateChecker.js +0 -91
  157. package/web/src/hooks/useUrlState.js +0 -170
  158. package/web/src/main.jsx +0 -15
  159. package/web/src/utils/download.js +0 -15
  160. package/web/src/utils/format.js +0 -42
  161. package/web/src/utils/m3.js +0 -57
  162. package/web/src/utils/ranks.js +0 -37
  163. 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
- }