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.
- package/README.md +110 -881
- package/bin/free-coding-models.js +4 -1
- package/changelog/v0.5.86.md +10 -0
- package/changelog/v0.5.87.md +48 -0
- package/package.json +8 -5
- package/sources.js +4 -4
- package/src/core/cache.js +3 -1
- package/src/core/config.js +43 -5
- package/src/core/endpoint-installer.js +23 -12
- package/src/core/installed-models-manager.js +43 -20
- package/src/core/kilo-config.js +51 -5
- package/src/core/kilo.js +5 -3
- package/src/core/legacy-proxy-cleanup.js +22 -5
- package/src/core/model-merger.js +4 -0
- package/src/core/models-drift.js +9 -0
- package/src/core/opencode-config.js +57 -5
- package/src/core/opencode.js +67 -41
- package/src/core/playground.js +7 -0
- package/src/core/probe-cache.js +35 -4
- package/src/core/provider-key-tester.js +10 -2
- package/src/core/provider-metadata.js +10 -0
- package/src/core/provider-quota-fetchers.js +14 -7
- package/src/core/router-daemon.js +208 -26
- package/src/core/runtime-telemetry.js +27 -1
- package/src/core/shared-helpers.js +12 -1
- package/src/core/shell-env.js +3 -1
- package/src/core/telemetry.js +18 -5
- package/src/core/tool-launchers.js +9 -6
- package/src/core/updater.js +28 -2
- package/src/core/utils.js +76 -71
- package/src/tui/app.js +47 -16
- package/src/tui/command-palette.js +22 -2
- package/src/tui/key-handler.js +123 -179
- package/src/tui/overlays.js +35 -75
- package/src/tui/render-helpers.js +62 -4
- package/src/tui/render-table.js +46 -5
- package/src/tui/theme.js +36 -5
- package/src/tui/tui-filters.js +28 -8
- package/src/tui/tui-state.js +22 -6
- package/web/dist/assets/index-BpTpPS0g.js +44 -0
- package/web/dist/assets/index-CAzFIt8P.css +1 -0
- package/web/dist/index.html +5 -9
- package/web/index.html +3 -7
- package/web/server.js +97 -23
- package/web/vite.config.js +8 -0
- package/web/app.legacy.js +0 -900
- package/web/dist/assets/index-C5-Ywvv3.css +0 -1
- package/web/dist/assets/index-DWUely84.js +0 -40
- package/web/public/favicon.ico +0 -0
- package/web/public/favicons/apple-touch-icon.png +0 -0
- package/web/public/favicons/browserconfig.xml +0 -12
- package/web/public/favicons/favicon-16x16.png +0 -0
- package/web/public/favicons/favicon-192x192.png +0 -0
- package/web/public/favicons/favicon-32x32.png +0 -0
- package/web/public/favicons/favicon-48x48.png +0 -0
- package/web/public/favicons/favicon-512x512.png +0 -0
- package/web/public/favicons/favicon-96x96.png +0 -0
- package/web/public/favicons/favicon.ico +0 -0
- package/web/public/favicons/mstile-150x150.png +0 -0
- package/web/public/favicons/mstile-310x150.png +0 -0
- package/web/public/favicons/mstile-310x310.png +0 -0
- package/web/public/favicons/mstile-512x512.png +0 -0
- package/web/public/favicons/mstile-70x70.png +0 -0
- package/web/public/favicons/site.webmanifest +0 -25
- package/web/src/App.jsx +0 -521
- package/web/src/components/analytics/AnalyticsView.jsx +0 -147
- package/web/src/components/analytics/AnalyticsView.module.css +0 -190
- package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
- package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
- package/web/src/components/atoms/AILatencyCell.jsx +0 -38
- package/web/src/components/atoms/AILatencyCell.module.css +0 -51
- package/web/src/components/atoms/HealthCell.jsx +0 -143
- package/web/src/components/atoms/HealthCell.module.css +0 -60
- package/web/src/components/atoms/LastPingCell.jsx +0 -35
- package/web/src/components/atoms/LastPingCell.module.css +0 -45
- package/web/src/components/atoms/MoodCell.jsx +0 -25
- package/web/src/components/atoms/MoodCell.module.css +0 -6
- package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
- package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
- package/web/src/components/atoms/ProviderLogo.jsx +0 -187
- package/web/src/components/atoms/ProviderLogo.module.css +0 -103
- package/web/src/components/atoms/RankCell.jsx +0 -9
- package/web/src/components/atoms/RankCell.module.css +0 -9
- package/web/src/components/atoms/Sparkline.jsx +0 -63
- package/web/src/components/atoms/StabilityCell.jsx +0 -18
- package/web/src/components/atoms/StabilityCell.module.css +0 -13
- package/web/src/components/atoms/StatusDot.jsx +0 -13
- package/web/src/components/atoms/StatusDot.module.css +0 -29
- package/web/src/components/atoms/TPSCell.jsx +0 -36
- package/web/src/components/atoms/TPSCell.module.css +0 -44
- package/web/src/components/atoms/TierBadge.jsx +0 -10
- package/web/src/components/atoms/TierBadge.module.css +0 -29
- package/web/src/components/atoms/Toast.jsx +0 -25
- package/web/src/components/atoms/Toast.module.css +0 -41
- package/web/src/components/atoms/ToastContainer.jsx +0 -16
- package/web/src/components/atoms/ToastContainer.module.css +0 -10
- package/web/src/components/atoms/VerdictBadge.jsx +0 -36
- package/web/src/components/atoms/VerdictBadge.module.css +0 -40
- package/web/src/components/changelog/ChangelogView.jsx +0 -135
- package/web/src/components/changelog/ChangelogView.module.css +0 -159
- package/web/src/components/dashboard/DetailPanel.jsx +0 -217
- package/web/src/components/dashboard/DetailPanel.module.css +0 -260
- package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
- package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
- package/web/src/components/dashboard/ExportModal.jsx +0 -84
- package/web/src/components/dashboard/ExportModal.module.css +0 -99
- package/web/src/components/dashboard/FilterBar.jsx +0 -338
- package/web/src/components/dashboard/FilterBar.module.css +0 -342
- package/web/src/components/dashboard/ModelTable.jsx +0 -672
- package/web/src/components/dashboard/ModelTable.module.css +0 -446
- package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
- package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
- package/web/src/components/help/HelpView.jsx +0 -201
- package/web/src/components/help/HelpView.module.css +0 -156
- package/web/src/components/install/InstallEndpointsView.jsx +0 -309
- package/web/src/components/install/InstallEndpointsView.module.css +0 -427
- package/web/src/components/installed/InstalledModelsView.jsx +0 -89
- package/web/src/components/installed/InstalledModelsView.module.css +0 -200
- package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
- package/web/src/components/launch/LaunchButton.jsx +0 -30
- package/web/src/components/launch/LaunchButton.module.css +0 -35
- package/web/src/components/launch/LaunchModal.module.css +0 -125
- package/web/src/components/layout/Footer.jsx +0 -19
- package/web/src/components/layout/Footer.module.css +0 -10
- package/web/src/components/layout/Header.jsx +0 -265
- package/web/src/components/layout/Header.module.css +0 -357
- package/web/src/components/palette/CommandPalette.jsx +0 -301
- package/web/src/components/palette/CommandPalette.module.css +0 -121
- package/web/src/components/playground/PlaygroundChat.jsx +0 -527
- package/web/src/components/playground/PlaygroundChat.module.css +0 -382
- package/web/src/components/playground/PlaygroundView.jsx +0 -371
- package/web/src/components/playground/PlaygroundView.module.css +0 -504
- package/web/src/components/recommend/RecommendView.jsx +0 -121
- package/web/src/components/recommend/RecommendView.module.css +0 -57
- package/web/src/components/router/RouterView.jsx +0 -1212
- package/web/src/components/router/RouterView.module.css +0 -1289
- package/web/src/components/settings/SettingsView.jsx +0 -555
- package/web/src/components/settings/SettingsView.module.css +0 -550
- package/web/src/components/tools/ToolPicker.jsx +0 -86
- package/web/src/components/tools/ToolPicker.module.css +0 -97
- package/web/src/components/update/UpdateChip.jsx +0 -104
- package/web/src/components/update/UpdateChip.module.css +0 -146
- package/web/src/global.css +0 -298
- package/web/src/hooks/urlState.constants.js +0 -28
- package/web/src/hooks/useChangelog.js +0 -51
- package/web/src/hooks/useColumnSizing.js +0 -211
- package/web/src/hooks/useFavorites.js +0 -150
- package/web/src/hooks/useFilter.js +0 -310
- package/web/src/hooks/useInstalledModels.js +0 -42
- package/web/src/hooks/useRecommend.js +0 -67
- package/web/src/hooks/useRouterDashboard.js +0 -133
- package/web/src/hooks/useSSE.js +0 -73
- package/web/src/hooks/useSocket.js +0 -203
- package/web/src/hooks/useTheme.js +0 -83
- package/web/src/hooks/useTokenUsage.js +0 -103
- package/web/src/hooks/useToolMode.js +0 -77
- package/web/src/hooks/useUpdateChecker.js +0 -91
- package/web/src/hooks/useUrlState.js +0 -170
- package/web/src/main.jsx +0 -15
- package/web/src/utils/download.js +0 -15
- package/web/src/utils/format.js +0 -42
- package/web/src/utils/m3.js +0 -57
- package/web/src/utils/ranks.js +0 -37
- package/web/styles.legacy.css +0 -963
|
@@ -1,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
|
-
}
|