free-coding-models 0.5.85 → 0.5.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +110 -881
  2. package/bin/free-coding-models.js +4 -1
  3. package/changelog/v0.5.86.md +10 -0
  4. package/changelog/v0.5.87.md +48 -0
  5. package/package.json +8 -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/shared-helpers.js +12 -1
  26. package/src/core/shell-env.js +3 -1
  27. package/src/core/telemetry.js +18 -5
  28. package/src/core/tool-launchers.js +9 -6
  29. package/src/core/updater.js +28 -2
  30. package/src/core/utils.js +76 -71
  31. package/src/tui/app.js +47 -16
  32. package/src/tui/command-palette.js +22 -2
  33. package/src/tui/key-handler.js +123 -179
  34. package/src/tui/overlays.js +35 -75
  35. package/src/tui/render-helpers.js +62 -4
  36. package/src/tui/render-table.js +46 -5
  37. package/src/tui/theme.js +36 -5
  38. package/src/tui/tui-filters.js +28 -8
  39. package/src/tui/tui-state.js +22 -6
  40. package/web/dist/assets/index-BpTpPS0g.js +44 -0
  41. package/web/dist/assets/index-CAzFIt8P.css +1 -0
  42. package/web/dist/index.html +5 -9
  43. package/web/index.html +3 -7
  44. package/web/server.js +97 -23
  45. package/web/vite.config.js +8 -0
  46. package/web/app.legacy.js +0 -900
  47. package/web/dist/assets/index-C5-Ywvv3.css +0 -1
  48. package/web/dist/assets/index-DWUely84.js +0 -40
  49. package/web/public/favicon.ico +0 -0
  50. package/web/public/favicons/apple-touch-icon.png +0 -0
  51. package/web/public/favicons/browserconfig.xml +0 -12
  52. package/web/public/favicons/favicon-16x16.png +0 -0
  53. package/web/public/favicons/favicon-192x192.png +0 -0
  54. package/web/public/favicons/favicon-32x32.png +0 -0
  55. package/web/public/favicons/favicon-48x48.png +0 -0
  56. package/web/public/favicons/favicon-512x512.png +0 -0
  57. package/web/public/favicons/favicon-96x96.png +0 -0
  58. package/web/public/favicons/favicon.ico +0 -0
  59. package/web/public/favicons/mstile-150x150.png +0 -0
  60. package/web/public/favicons/mstile-310x150.png +0 -0
  61. package/web/public/favicons/mstile-310x310.png +0 -0
  62. package/web/public/favicons/mstile-512x512.png +0 -0
  63. package/web/public/favicons/mstile-70x70.png +0 -0
  64. package/web/public/favicons/site.webmanifest +0 -25
  65. package/web/src/App.jsx +0 -521
  66. package/web/src/components/analytics/AnalyticsView.jsx +0 -147
  67. package/web/src/components/analytics/AnalyticsView.module.css +0 -190
  68. package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
  69. package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
  70. package/web/src/components/atoms/AILatencyCell.jsx +0 -38
  71. package/web/src/components/atoms/AILatencyCell.module.css +0 -51
  72. package/web/src/components/atoms/HealthCell.jsx +0 -143
  73. package/web/src/components/atoms/HealthCell.module.css +0 -60
  74. package/web/src/components/atoms/LastPingCell.jsx +0 -35
  75. package/web/src/components/atoms/LastPingCell.module.css +0 -45
  76. package/web/src/components/atoms/MoodCell.jsx +0 -25
  77. package/web/src/components/atoms/MoodCell.module.css +0 -6
  78. package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
  79. package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
  80. package/web/src/components/atoms/ProviderLogo.jsx +0 -187
  81. package/web/src/components/atoms/ProviderLogo.module.css +0 -103
  82. package/web/src/components/atoms/RankCell.jsx +0 -9
  83. package/web/src/components/atoms/RankCell.module.css +0 -9
  84. package/web/src/components/atoms/Sparkline.jsx +0 -63
  85. package/web/src/components/atoms/StabilityCell.jsx +0 -18
  86. package/web/src/components/atoms/StabilityCell.module.css +0 -13
  87. package/web/src/components/atoms/StatusDot.jsx +0 -13
  88. package/web/src/components/atoms/StatusDot.module.css +0 -29
  89. package/web/src/components/atoms/TPSCell.jsx +0 -36
  90. package/web/src/components/atoms/TPSCell.module.css +0 -44
  91. package/web/src/components/atoms/TierBadge.jsx +0 -10
  92. package/web/src/components/atoms/TierBadge.module.css +0 -29
  93. package/web/src/components/atoms/Toast.jsx +0 -25
  94. package/web/src/components/atoms/Toast.module.css +0 -41
  95. package/web/src/components/atoms/ToastContainer.jsx +0 -16
  96. package/web/src/components/atoms/ToastContainer.module.css +0 -10
  97. package/web/src/components/atoms/VerdictBadge.jsx +0 -36
  98. package/web/src/components/atoms/VerdictBadge.module.css +0 -40
  99. package/web/src/components/changelog/ChangelogView.jsx +0 -135
  100. package/web/src/components/changelog/ChangelogView.module.css +0 -159
  101. package/web/src/components/dashboard/DetailPanel.jsx +0 -217
  102. package/web/src/components/dashboard/DetailPanel.module.css +0 -260
  103. package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
  104. package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
  105. package/web/src/components/dashboard/ExportModal.jsx +0 -84
  106. package/web/src/components/dashboard/ExportModal.module.css +0 -99
  107. package/web/src/components/dashboard/FilterBar.jsx +0 -338
  108. package/web/src/components/dashboard/FilterBar.module.css +0 -342
  109. package/web/src/components/dashboard/ModelTable.jsx +0 -672
  110. package/web/src/components/dashboard/ModelTable.module.css +0 -446
  111. package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
  112. package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
  113. package/web/src/components/help/HelpView.jsx +0 -201
  114. package/web/src/components/help/HelpView.module.css +0 -156
  115. package/web/src/components/install/InstallEndpointsView.jsx +0 -309
  116. package/web/src/components/install/InstallEndpointsView.module.css +0 -427
  117. package/web/src/components/installed/InstalledModelsView.jsx +0 -89
  118. package/web/src/components/installed/InstalledModelsView.module.css +0 -200
  119. package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
  120. package/web/src/components/launch/LaunchButton.jsx +0 -30
  121. package/web/src/components/launch/LaunchButton.module.css +0 -35
  122. package/web/src/components/launch/LaunchModal.module.css +0 -125
  123. package/web/src/components/layout/Footer.jsx +0 -19
  124. package/web/src/components/layout/Footer.module.css +0 -10
  125. package/web/src/components/layout/Header.jsx +0 -265
  126. package/web/src/components/layout/Header.module.css +0 -357
  127. package/web/src/components/palette/CommandPalette.jsx +0 -301
  128. package/web/src/components/palette/CommandPalette.module.css +0 -121
  129. package/web/src/components/playground/PlaygroundChat.jsx +0 -527
  130. package/web/src/components/playground/PlaygroundChat.module.css +0 -382
  131. package/web/src/components/playground/PlaygroundView.jsx +0 -371
  132. package/web/src/components/playground/PlaygroundView.module.css +0 -504
  133. package/web/src/components/recommend/RecommendView.jsx +0 -121
  134. package/web/src/components/recommend/RecommendView.module.css +0 -57
  135. package/web/src/components/router/RouterView.jsx +0 -1212
  136. package/web/src/components/router/RouterView.module.css +0 -1289
  137. package/web/src/components/settings/SettingsView.jsx +0 -555
  138. package/web/src/components/settings/SettingsView.module.css +0 -550
  139. package/web/src/components/tools/ToolPicker.jsx +0 -86
  140. package/web/src/components/tools/ToolPicker.module.css +0 -97
  141. package/web/src/components/update/UpdateChip.jsx +0 -104
  142. package/web/src/components/update/UpdateChip.module.css +0 -146
  143. package/web/src/global.css +0 -298
  144. package/web/src/hooks/urlState.constants.js +0 -28
  145. package/web/src/hooks/useChangelog.js +0 -51
  146. package/web/src/hooks/useColumnSizing.js +0 -211
  147. package/web/src/hooks/useFavorites.js +0 -150
  148. package/web/src/hooks/useFilter.js +0 -310
  149. package/web/src/hooks/useInstalledModels.js +0 -42
  150. package/web/src/hooks/useRecommend.js +0 -67
  151. package/web/src/hooks/useRouterDashboard.js +0 -133
  152. package/web/src/hooks/useSSE.js +0 -73
  153. package/web/src/hooks/useSocket.js +0 -203
  154. package/web/src/hooks/useTheme.js +0 -83
  155. package/web/src/hooks/useTokenUsage.js +0 -103
  156. package/web/src/hooks/useToolMode.js +0 -77
  157. package/web/src/hooks/useUpdateChecker.js +0 -91
  158. package/web/src/hooks/useUrlState.js +0 -170
  159. package/web/src/main.jsx +0 -15
  160. package/web/src/utils/download.js +0 -15
  161. package/web/src/utils/format.js +0 -42
  162. package/web/src/utils/m3.js +0 -57
  163. package/web/src/utils/ranks.js +0 -37
  164. package/web/styles.legacy.css +0 -963
@@ -1,338 +0,0 @@
1
- /**
2
- * @file web/src/components/dashboard/FilterBar.jsx
3
- * @description Filter controls for the model table — TUI parity chips.
4
- * 📖 M1 parity: tier (T) / status / provider (D) / verdict (V) / health (H) / visibility (E)
5
- * 📖 + custom text filter chip with "X" clear + reset view button (N).
6
- * 📖 Each chip is a cycling button matching the TUI single-key behavior.
7
- * 📖 The "Next ping in Xs" countdown still shows the live status.
8
- * 📖 Filter groups (Tier, Status, Verdict, Health) are collapsible by default
9
- * — click the trigger to expand the chip row, click again or outside to close.
10
- */
11
- import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
12
- import { IconRefresh, IconX, IconFilter, IconChevronDown, IconSearch } from '@tabler/icons-react'
13
- import { getToolMeta } from '../../../../src/core/tool-metadata.js'
14
- import ProviderDropdown from './ProviderDropdown.jsx'
15
- import styles from './FilterBar.module.css'
16
-
17
- // 📖 Chip sets match the TUI cycles 1:1 (see useFilter.js). Keep these in sync.
18
- const TIERS = [
19
- { key: 'all', label: 'All' },
20
- { key: 'S+', label: 'S+' },
21
- { key: 'S', label: 'S' },
22
- { key: 'A+', label: 'A+' },
23
- { key: 'A', label: 'A' },
24
- { key: 'A-', label: 'A-' },
25
- { key: 'B+', label: 'B+' },
26
- { key: 'B', label: 'B' },
27
- { key: 'C', label: 'C' },
28
- ]
29
- const STATUSES = [
30
- { key: 'all', label: 'All' },
31
- { key: 'up', label: 'Up' },
32
- { key: 'down', label: 'Down' },
33
- { key: 'pending', label: 'Pending' },
34
- ]
35
- const VERDICTS = [
36
- { key: 'all', label: 'All' },
37
- { key: 'Perfect', label: 'Perfect' },
38
- { key: 'Normal', label: 'Normal' },
39
- { key: 'Spiky', label: 'Spiky' },
40
- { key: 'Slow', label: 'Slow' },
41
- { key: 'Overloaded', label: 'Overloaded' },
42
- { key: 'Down', label: 'Down' },
43
- { key: 'Unstable', label: 'Unstable' },
44
- { key: 'Pending', label: 'Pending' },
45
- ]
46
- const HEALTHS = [
47
- { key: 'all', label: 'All' },
48
- { key: 'up', label: 'Up' },
49
- { key: 'timeout', label: 'Timeout' },
50
- { key: 'down', label: 'Down' },
51
- { key: 'pending', label: 'Pending' },
52
- { key: 'noauth', label: 'No key' },
53
- { key: 'auth_error', label: 'Auth err' },
54
- ]
55
- // 📖 TUI's E key cycle. The Web mirrors the same 3-state machine.
56
- const VISIBILITY_MODES = [
57
- { key: 'normal', label: 'All models', hint: 'Show everything' },
58
- { key: 'configured', label: 'Configured only', hint: 'Hide models with no key or auth errors' },
59
- { key: 'usable', label: 'Usable only', hint: 'Only Health UP + good verdict' },
60
- ]
61
-
62
- const PING_MODES = [
63
- { key: 'speed', label: '⚡ Speed', interval: '2s', color: '#00ff88' },
64
- { key: 'normal', label: '● Normal', interval: '10s', color: '#ffaa00' },
65
- { key: 'slow', label: '🐢 Slow', interval: '30s', color: '#ff6644' },
66
- { key: 'forced', label: '🔥 Forced', interval: '4s', color: '#ff4466' },
67
- ]
68
-
69
- function formatCountdown(ms) {
70
- if (ms == null) return null
71
- const totalSec = Math.max(0, ms / 1000)
72
- if (totalSec < 60) return `${totalSec.toFixed(2)}s`
73
- const m = Math.floor(totalSec / 60)
74
- const s = totalSec % 60
75
- return `${m}m ${s.toFixed(2)}s`
76
- }
77
-
78
- /**
79
- * 📖 FilterGroup — collapsible chip selector. Shows label + active value as a compact
80
- * trigger. Click to expand a dropdown with all options grouped as a segmented control.
81
- * Click outside or pick a value to collapse. Designed to keep the filter bar minimal
82
- * by default while exposing the full chip set on demand.
83
- */
84
- function FilterGroup({ label, items, value, onChange, colorMap }) {
85
- const [open, setOpen] = useState(false)
86
- const ref = useRef(null)
87
-
88
- const close = useCallback(() => setOpen(false), [])
89
-
90
- useEffect(() => {
91
- if (!open) return
92
- const handler = (e) => {
93
- if (ref.current && !ref.current.contains(e.target)) close()
94
- }
95
- const keyHandler = (e) => { if (e.key === 'Escape') close() }
96
- document.addEventListener('mousedown', handler)
97
- document.addEventListener('keydown', keyHandler)
98
- return () => {
99
- document.removeEventListener('mousedown', handler)
100
- document.removeEventListener('keydown', keyHandler)
101
- }
102
- }, [open, close])
103
-
104
- const activeItem = items.find(i => i.key === value)
105
- const activeLabel = activeItem?.label ?? value ?? 'All'
106
- const isFiltered = value !== 'all'
107
-
108
- return (
109
- <div className={styles.filterGroup} ref={ref}>
110
- <button
111
- className={`${styles.filterTrigger} ${isFiltered ? styles.filterTriggerActive : ''} ${open ? styles.filterTriggerExpanded : ''}`}
112
- onClick={() => setOpen(!open)}
113
- title={`${label}: ${activeLabel}`}
114
- aria-expanded={open}
115
- aria-haspopup="listbox"
116
- >
117
- <span className={styles.filterTriggerLabel}>{label}</span>
118
- <span className={styles.filterTriggerSep}>:</span>
119
- <span className={styles.filterTriggerValue}>{activeLabel}</span>
120
- <IconChevronDown
121
- size={12}
122
- stroke={2}
123
- className={`${styles.filterTriggerChevron} ${open ? styles.chevronOpen : ''}`}
124
- />
125
- </button>
126
- {open && (
127
- <div className={styles.filterDropdown} role="listbox">
128
- <div className={styles.filterChipRow}>
129
- {items.map((item, i) => {
130
- const active = value === item.key
131
- const customColor = colorMap?.[item.key]
132
- return (
133
- <button
134
- key={item.key}
135
- role="option"
136
- aria-selected={active}
137
- className={`${styles.filterChip} ${active ? styles.filterChipActive : ''}`}
138
- style={active && customColor ? { '--chip-active-color': customColor } : {}}
139
- onClick={() => { onChange(item.key); close() }}
140
- title={item.hint || item.label}
141
- >
142
- {item.label}
143
- </button>
144
- )
145
- })}
146
- </div>
147
- </div>
148
- )}
149
- </div>
150
- )
151
- }
152
-
153
- export default function FilterBar({
154
- filterTier, setFilterTier,
155
- filterStatus, setFilterStatus,
156
- filterProvider, setFilterProvider,
157
- filterVerdict, setFilterVerdict,
158
- filterHealth, setFilterHealth,
159
- visibilityMode, setVisibilityMode,
160
- customTextFilter, setCustomTextFilter,
161
- searchQuery, onSearchChange,
162
- onResetView,
163
- providers,
164
- pingMode, setPingMode,
165
- nextPingAt,
166
- isPinging,
167
- globalBenchmarkRunning,
168
- globalBenchmarkTotal,
169
- globalBenchmarkCompleted,
170
- toolMode = 'opencode',
171
- }) {
172
- const [countdown, setCountdown] = useState(null)
173
-
174
- useEffect(() => {
175
- if (nextPingAt == null) return
176
- const tick = () => {
177
- const rem = nextPingAt - Date.now()
178
- setCountdown(rem > 0 ? rem : 0)
179
- }
180
- tick()
181
- const id = setInterval(tick, 100)
182
- return () => clearInterval(id)
183
- }, [nextPingAt])
184
-
185
- const countdownDisplay = countdown !== null ? formatCountdown(countdown) : null
186
- const activeTool = getToolMeta(toolMode)
187
-
188
- // 📖 Custom text filter chip — sticks to the right of the search bar, with
189
- // 📖 an "X" to clear (TUI's `X` key behavior).
190
- const customFilterActive = Boolean(customTextFilter && customTextFilter.trim().length > 0)
191
-
192
- const benchmarkPct = globalBenchmarkRunning && globalBenchmarkTotal > 0
193
- ? Math.round((globalBenchmarkCompleted / globalBenchmarkTotal) * 100)
194
- : 0
195
-
196
- // 📖 Count of active non-default filters to show on the Reset button.
197
- const activeFilterCount = useMemo(() => {
198
- let n = 0
199
- if (filterTier !== 'all') n++
200
- if (filterStatus !== 'all') n++
201
- if (filterProvider !== 'all') n++
202
- if (filterVerdict !== 'all') n++
203
- if (filterHealth !== 'all') n++
204
- if (visibilityMode !== 'normal') n++
205
- if (customFilterActive) n++
206
- if (searchQuery && searchQuery.trim().length > 0) n++
207
- return n
208
- }, [filterTier, filterStatus, filterProvider, filterVerdict, filterHealth, visibilityMode, customFilterActive, searchQuery])
209
-
210
- return (
211
- <section className={styles.filters}>
212
- {/* ── Global benchmark progress bar (Ctrl+U) ── */}
213
- {globalBenchmarkRunning && (
214
- <div className={styles.benchmarkBar}>
215
- <div className={styles.benchmarkLabel}>
216
- <span className={styles.benchmarkSpinner} />
217
- <span>AI Speed Test</span>
218
- <span className={styles.benchmarkCount}>{globalBenchmarkCompleted}/{globalBenchmarkTotal}</span>
219
- </div>
220
- <div className={styles.benchmarkTrack}>
221
- <div className={styles.benchmarkFill} style={{ width: `${benchmarkPct}%` }} />
222
- </div>
223
- <span className={styles.benchmarkPct}>{benchmarkPct}%</span>
224
- </div>
225
- )}
226
-
227
- <FilterGroup label="Tier" items={TIERS} value={filterTier} onChange={setFilterTier} />
228
- <FilterGroup label="Status" items={STATUSES} value={filterStatus} onChange={setFilterStatus} />
229
- <FilterGroup label="Verdict" items={VERDICTS} value={filterVerdict} onChange={setFilterVerdict} />
230
- <FilterGroup label="Health" items={HEALTHS} value={filterHealth} onChange={setFilterHealth} />
231
-
232
- <div className={styles.group}>
233
- <label className={styles.filterLabel} htmlFor="visibility-select">Visibility</label>
234
- <select
235
- id="visibility-select"
236
- className={styles.select}
237
- value={visibilityMode}
238
- onChange={(e) => setVisibilityMode(e.target.value)}
239
- title={VISIBILITY_MODES.find((v) => v.key === visibilityMode)?.hint}
240
- aria-label="Visibility mode"
241
- >
242
- {VISIBILITY_MODES.map((v) => (
243
- <option key={v.key} value={v.key}>{v.label}</option>
244
- ))}
245
- </select>
246
- </div>
247
-
248
- <div className={styles.group}>
249
- <label className={styles.filterLabel}>Provider</label>
250
- <ProviderDropdown
251
- providers={providers}
252
- value={filterProvider}
253
- onChange={setFilterProvider}
254
- />
255
- </div>
256
-
257
- <div className={styles.group}>
258
- <div className={styles.searchBar}>
259
- <span className={styles.searchIcon}><IconSearch size={13} stroke={1.5} /></span>
260
- <input
261
- type="text"
262
- className={styles.searchInput}
263
- placeholder="Search models, providers..."
264
- value={searchQuery || ''}
265
- onChange={(e) => onSearchChange?.(e.target.value)}
266
- autoComplete="off"
267
- aria-label="Search models"
268
- />
269
- </div>
270
- </div>
271
-
272
- <div className={styles.group}>
273
- <label className={styles.filterLabel}>Endpoint target</label>
274
- <div className={styles.toolStatus} title="Active endpoint install target from the Header picker">
275
- <span>{activeTool.emoji}</span>
276
- <strong>{activeTool.label}</strong>
277
- </div>
278
- </div>
279
-
280
- <div className={styles.spacer} />
281
-
282
- {/* ── Custom text filter chip (TUI's Ctrl+P "Apply text filter") ── */}
283
- {customFilterActive && (
284
- <div className={styles.group}>
285
- <label className={styles.filterLabel}>Text</label>
286
- <div className={styles.customFilterChip} title="Click X to clear (TUI: X key)">
287
- <span className={styles.customFilterIcon}><IconFilter size={12} stroke={1.5} /></span>
288
- <span className={styles.customFilterLabel}>{customTextFilter}</span>
289
- <button
290
- className={styles.customFilterClear}
291
- onClick={() => setCustomTextFilter(null)}
292
- title="Clear custom text filter (TUI: X)"
293
- aria-label="Clear custom text filter"
294
- >
295
- <IconX size={12} stroke={2} />
296
- </button>
297
- </div>
298
- </div>
299
- )}
300
-
301
- {/* ── Reset view (TUI: N) — only visible when filters are active ── */}
302
- {activeFilterCount > 0 && (
303
- <button
304
- className={styles.resetBtn}
305
- onClick={onResetView}
306
- title={`Reset ${activeFilterCount} active filter${activeFilterCount === 1 ? '' : 's'} (TUI: N)`}
307
- >
308
- <IconRefresh size={12} stroke={1.5} />
309
- <span>Reset</span>
310
- <span className={styles.resetBadge}>{activeFilterCount}</span>
311
- </button>
312
- )}
313
-
314
- {/* ── Ping interval selector (collapsible group) ── */}
315
- <FilterGroup
316
- label="Ping"
317
- items={PING_MODES}
318
- value={pingMode}
319
- onChange={setPingMode}
320
- colorMap={Object.fromEntries(PING_MODES.map(m => [m.key, m.color]))}
321
- />
322
-
323
- {/* ── Next ping countdown (TUI parity: always show the delay) ──
324
- 📖 The TUI footer always renders `next : Xs` regardless of whether
325
- 📖 a ping is in flight — users care about the delay until the next
326
- 📖 cycle, not the "is pinging right now" boolean. We follow the same
327
- 📖 model: a single countdown line, with a small pulsing dot when
328
- 📖 `isPinging` is true so the LIVE state is still visible without
329
- 📖 resorting to the "Pinging…" text. */}
330
- <div className={styles.group}>
331
- <div className={styles.nextPing} title="Next ping countdown">
332
- <span className={styles.nextPingLabel}>next ping in</span>
333
- <span className={styles.nextPingTime}>{countdownDisplay ?? '—'}</span>
334
- </div>
335
- </div>
336
- </section>
337
- )
338
- }
@@ -1,342 +0,0 @@
1
- /**
2
- * @file web/src/components/dashboard/FilterBar.module.css
3
- * @description Filter bar styles — M1 parity chips (tier, status, verdict, health,
4
- * visibility, provider, custom text filter, reset, ping mode).
5
- * 📖 The bar is a STATIC header above the model table (flex-shrink:0). It used
6
- * 📖 to be `position: sticky; top: 60px` for the old whole-page-scroll layout,
7
- * 📖 but the dashboard now scrolls the table INTERNALLY (main.dashboardView is
8
- * 📖 the bounded box, .scrollInner scrolls). With main as the scroll container,
9
- * 📖 `top: 60px` stuck the bar 60px DOWN inside main, dropping it over the
10
- * 📖 table's green <thead>. Since the bar lives OUTSIDE the internal scroller,
11
- * 📖 it is naturally always visible — no sticky needed. Keep z-index modest so
12
- * 📖 it never fights the sticky table header (z-index: 10) if they ever meet.
13
- */
14
-
15
- .filters {
16
- position: relative; z-index: 5;
17
- flex-shrink: 0;
18
- display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
19
- padding: 10px 20px;
20
- background: var(--color-bg-card);
21
- border-bottom: 1px solid var(--color-border);
22
- backdrop-filter: blur(20px) saturate(1.5);
23
- -webkit-backdrop-filter: blur(20px) saturate(1.5);
24
- }
25
-
26
- .group { display: flex; align-items: center; gap: 6px; }
27
-
28
- .filterLabel {
29
- font-size: 12px; font-weight: 700; color: var(--color-text-muted);
30
- text-transform: uppercase; letter-spacing: 0.5px;
31
- white-space: nowrap;
32
- }
33
-
34
- /* ── Collapsible filter groups (Tier, Status, Verdict, Health) ── */
35
- .filterGroup {
36
- position: relative;
37
- }
38
-
39
- .filterTrigger {
40
- display: inline-flex; align-items: center; gap: 4px;
41
- font-size: 13px; font-family: var(--font-mono);
42
- padding: 4px 8px; border-radius: 5px;
43
- border: 1px solid var(--color-border);
44
- background: var(--color-surface);
45
- color: var(--color-text-muted);
46
- cursor: pointer; transition: all 150ms;
47
- white-space: nowrap;
48
- user-select: none;
49
- }
50
- .filterTrigger:hover {
51
- background: var(--color-bg-hover);
52
- color: var(--color-text);
53
- border-color: var(--color-text-muted);
54
- }
55
- .filterTriggerActive {
56
- border-color: var(--color-accent);
57
- color: var(--color-text);
58
- }
59
- .filterTriggerExpanded {
60
- border-color: var(--color-accent);
61
- background: var(--color-bg-hover);
62
- color: var(--color-text);
63
- border-bottom-left-radius: 0;
64
- border-bottom-right-radius: 0;
65
- z-index: 101;
66
- }
67
-
68
- .filterTriggerLabel {
69
- font-size: 11px; font-weight: 700;
70
- text-transform: uppercase; letter-spacing: 0.5px;
71
- color: var(--color-text-muted);
72
- }
73
- .filterTriggerSep {
74
- color: var(--color-border);
75
- margin: 0 -2px;
76
- }
77
- .filterTriggerValue {
78
- font-weight: 700;
79
- color: var(--color-text);
80
- }
81
- .filterTriggerActive .filterTriggerValue {
82
- color: var(--color-accent);
83
- }
84
-
85
- .filterTriggerChevron {
86
- flex-shrink: 0;
87
- transition: transform 200ms ease;
88
- opacity: 0.6;
89
- }
90
- .chevronOpen {
91
- transform: rotate(180deg);
92
- opacity: 1;
93
- }
94
-
95
- .filterDropdown {
96
- position: absolute; top: 100%; left: 0;
97
- z-index: 100;
98
- padding: 6px 8px 7px;
99
- background: var(--color-surface);
100
- border: 1px solid var(--color-accent);
101
- border-top: none;
102
- border-radius: 0 0 6px 6px;
103
- box-shadow: 0 8px 24px rgba(0,0,0,0.3);
104
- min-width: max-content;
105
- }
106
-
107
- .filterChipRow {
108
- display: flex;
109
- gap: 0;
110
- }
111
-
112
- .filterChip {
113
- font-size: 13px; font-weight: 600; font-family: var(--font-mono);
114
- padding: 4px 9px;
115
- border: 1px solid var(--color-border);
116
- background: var(--color-bg-card);
117
- color: var(--color-text-muted);
118
- cursor: pointer; transition: all 120ms;
119
- white-space: nowrap;
120
- margin-left: -1px;
121
- }
122
- .filterChip:first-child {
123
- border-radius: 4px 0 0 4px;
124
- margin-left: 0;
125
- }
126
- .filterChip:last-child {
127
- border-radius: 0 4px 4px 0;
128
- }
129
- .filterChip:hover {
130
- background: var(--color-bg-hover);
131
- color: var(--color-text);
132
- z-index: 1;
133
- }
134
- .filterChipActive {
135
- background: var(--chip-active-color, var(--color-accent)) !important;
136
- color: #fff !important;
137
- border-color: var(--chip-active-color, var(--color-accent)) !important;
138
- z-index: 2;
139
- font-weight: 700;
140
- }
141
-
142
- /* ── Select inputs (provider, visibility) ── */
143
- .select, .providerSelect {
144
- background: var(--color-surface); color: var(--color-text);
145
- border: 1px solid var(--color-border); border-radius: 5px;
146
- padding: 4px 8px; font-size: 13px; font-family: var(--font-sans);
147
- cursor: pointer; outline: none;
148
- min-width: 130px;
149
- }
150
- .select:focus, .providerSelect:focus { border-color: var(--color-accent); }
151
-
152
- .toolStatus {
153
- display: inline-flex;
154
- align-items: center;
155
- gap: 6px;
156
- padding: 4px 8px;
157
- border: 1px solid var(--color-border);
158
- border-radius: 5px;
159
- background: var(--color-surface);
160
- color: var(--color-text);
161
- font-size: 13px;
162
- white-space: nowrap;
163
- }
164
- .toolStatus strong {
165
- font-size: 13px;
166
- font-weight: 800;
167
- }
168
-
169
- /* ── Spacer + reset ── */
170
- .spacer { flex: 1; }
171
-
172
- .resetBtn {
173
- display: inline-flex; align-items: center; gap: 5px;
174
- font-size: 13px; font-weight: 700; font-family: var(--font-mono);
175
- padding: 4px 9px; border-radius: 5px;
176
- border: 1px solid var(--color-border-hover);
177
- background: var(--color-accent-dim);
178
- color: var(--color-text);
179
- cursor: pointer; transition: all 150ms;
180
- }
181
- .resetBtn:hover {
182
- background: var(--color-accent);
183
- color: var(--color-bg);
184
- }
185
- .resetBadge {
186
- display: inline-flex; align-items: center; justify-content: center;
187
- min-width: 16px; height: 16px;
188
- font-size: 11px; font-weight: 800;
189
- background: var(--color-bg);
190
- color: var(--color-accent);
191
- border-radius: 999px;
192
- padding: 0 4px;
193
- }
194
-
195
- /* ── Custom text filter chip (TUI Ctrl+P "Apply text filter") ── */
196
- .customFilterChip {
197
- display: inline-flex; align-items: center; gap: 6px;
198
- font-size: 13px; font-weight: 600; font-family: var(--font-mono);
199
- padding: 3px 4px 3px 8px;
200
- border: 1px solid var(--color-accent);
201
- background: var(--color-accent-dim);
202
- color: var(--color-accent);
203
- border-radius: 5px;
204
- max-width: 240px;
205
- }
206
- .customFilterIcon { display: inline-flex; flex-shrink: 0; }
207
- .customFilterLabel {
208
- overflow: hidden;
209
- text-overflow: ellipsis;
210
- white-space: nowrap;
211
- }
212
- .customFilterClear {
213
- display: inline-flex; align-items: center; justify-content: center;
214
- width: 18px; height: 18px;
215
- background: transparent;
216
- border: none;
217
- color: var(--color-accent);
218
- border-radius: 3px;
219
- cursor: pointer;
220
- flex-shrink: 0;
221
- }
222
- .customFilterClear:hover { background: var(--color-accent); color: var(--color-bg); }
223
-
224
- /* ── Next ping countdown (TUI parity) ── */
225
- .nextPing {
226
- display: flex; align-items: center; gap: 6px;
227
- font-size: 13px; font-family: var(--font-mono);
228
- padding: 4px 8px;
229
- border: 1px solid var(--color-border);
230
- border-radius: 5px;
231
- background: var(--color-surface);
232
- }
233
- .nextPingLabel {
234
- font-size: 12px; font-weight: 500; color: var(--color-text-muted);
235
- text-transform: uppercase; letter-spacing: 0.5px;
236
- }
237
- .nextPingTime {
238
- font-size: 15px; font-weight: 700; color: var(--color-text);
239
- min-width: 32px;
240
- text-align: right;
241
- font-variant-numeric: tabular-nums;
242
- }
243
- @keyframes pulseDot {
244
- 0%, 100% { box-shadow: 0 0 0 0 var(--color-accent-glow); }
245
- 50% { box-shadow: 0 0 0 6px transparent; }
246
- }
247
-
248
- .live {
249
- display: flex; align-items: center; gap: 5px;
250
- font-size: 12px; font-weight: 700; color: var(--color-accent);
251
- text-transform: uppercase; letter-spacing: 1px;
252
- }
253
- .liveDot {
254
- width: 7px; height: 7px; border-radius: 50%;
255
- background: var(--color-accent); animation: pulseDot 2s ease-in-out infinite;
256
- }
257
-
258
- /* ── Global benchmark progress bar (Ctrl+U) ── */
259
- .benchmarkBar {
260
- display: flex; align-items: center; gap: 10px;
261
- background: rgba(180, 0, 255, 0.12);
262
- border: 1px solid rgba(180, 0, 255, 0.4);
263
- border-radius: 6px;
264
- padding: 6px 12px;
265
- flex-shrink: 0;
266
- width: 100%;
267
- margin-bottom: 4px;
268
- }
269
- .benchmarkLabel {
270
- display: flex; align-items: center; gap: 5px;
271
- font-size: 13px; font-weight: 700;
272
- color: #b400ff; font-family: var(--font-mono);
273
- white-space: nowrap;
274
- }
275
- .benchmarkSpinner {
276
- display: inline-block;
277
- animation: spinSp 0.8s linear infinite;
278
- font-size: 14px; line-height: 1;
279
- }
280
- .benchmarkCount { font-size: 12px; opacity: 0.8; }
281
- .benchmarkTrack {
282
- width: 120px; height: 5px;
283
- background: rgba(255,255,255,0.1);
284
- border-radius: 3px; overflow: hidden;
285
- flex: 1;
286
- }
287
- .benchmarkFill {
288
- height: 100%;
289
- background: linear-gradient(90deg, #b400ff, #ff00ff);
290
- border-radius: 3px;
291
- transition: width 0.5s ease;
292
- }
293
- .benchmarkPct {
294
- font-size: 13px; font-weight: 700;
295
- color: #b400ff; font-family: var(--font-mono);
296
- min-width: 32px; text-align: right;
297
- }
298
-
299
- @keyframes spinSp {
300
- from { transform: rotate(0deg); }
301
- to { transform: rotate(360deg); }
302
- }
303
-
304
- /* ─── Search Bar ─── */
305
- .searchBar {
306
- display: flex;
307
- align-items: center;
308
- gap: 5px;
309
- background: var(--color-surface);
310
- border: 1px solid var(--color-border);
311
- border-radius: 4px;
312
- padding: 0 6px;
313
- height: 24px;
314
- width: 125px;
315
- transition: border-color 150ms, box-shadow 150ms, width 200ms var(--ease-out);
316
- }
317
- .searchBar:focus-within {
318
- border-color: var(--color-accent);
319
- box-shadow: 0 0 0 2px var(--color-accent-glow);
320
- width: 180px;
321
- }
322
- .searchIcon {
323
- display: flex;
324
- align-items: center;
325
- color: var(--color-text-muted);
326
- flex-shrink: 0;
327
- }
328
- .searchInput {
329
- flex: 1;
330
- background: none;
331
- border: none;
332
- outline: none;
333
- color: var(--color-text);
334
- font-size: 10.5px;
335
- font-family: var(--font-sans);
336
- min-width: 0;
337
- padding: 0;
338
- }
339
- .searchInput::placeholder {
340
- color: var(--color-text-dim);
341
- font-size: 10px;
342
- }