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