free-coding-models 0.5.14 → 0.5.16
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/changelog/v0.5.12.md +86 -2
- package/changelog/v0.5.13.md +19 -53
- package/changelog/v0.5.15.md +17 -0
- package/changelog/v0.5.16.md +15 -0
- package/package.json +4 -3
- package/src/tui/app.js +16 -4
- package/src/tui/cli-help.js +87 -0
- package/src/tui/key-handler.js +2 -14
- package/src/tui/render-table.js +46 -26
- package/src/tui/theme.js +70 -0
- package/web/README.md +1 -1
- package/web/dist/assets/index-avN2GM3H.css +1 -0
- package/web/dist/assets/index-vsysCImz.js +41 -0
- package/web/dist/index.html +6 -2
- package/web/index.html +4 -0
- package/web/server.js +249 -15
- package/web/src/App.jsx +46 -15
- package/web/src/components/dashboard/DetailPanel.jsx +1 -1
- package/web/src/components/dashboard/DetailPanel.module.css +5 -0
- package/web/src/components/dashboard/ExpandedDetailRow.jsx +464 -0
- package/web/src/components/dashboard/ExpandedDetailRow.module.css +347 -0
- package/web/src/components/dashboard/FilterBar.jsx +98 -58
- package/web/src/components/dashboard/FilterBar.module.css +103 -33
- package/web/src/components/dashboard/ModelTable.jsx +49 -29
- package/web/src/components/dashboard/ModelTable.module.css +12 -0
- package/web/src/components/dashboard/ProviderDropdown.jsx +156 -0
- package/web/src/components/dashboard/ProviderDropdown.module.css +248 -0
- package/web/src/components/help/HelpView.jsx +13 -0
- package/web/src/components/playground/PlaygroundView.jsx +193 -12
- package/web/src/components/playground/PlaygroundView.module.css +82 -0
- package/web/src/components/router/RouterView.jsx +15 -6
- package/web/vite.config.js +1 -1
- package/web/dist/assets/index-Bzqz7KbT.js +0 -39
- package/web/dist/assets/index-H9JWDRIh.css +0 -1
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/dashboard/ExpandedDetailRow.module.css
|
|
3
|
+
* @description CSS module for the 3-column expandable detail row shown beneath a table row.
|
|
4
|
+
* 📖 Columns: Info (left), Mini Playground (center), AI Latency Test (right).
|
|
5
|
+
* 📖 Supports dark/light themes via CSS variables and [data-theme="light"] overrides.
|
|
6
|
+
* 📖 Responsive: collapses to single column on screens < 900px.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/* ─── Root container ─── */
|
|
10
|
+
.row {
|
|
11
|
+
grid-column: 1 / -1;
|
|
12
|
+
padding: 16px 20px;
|
|
13
|
+
background: var(--color-bg-elevated);
|
|
14
|
+
border-bottom: 2px solid var(--color-accent);
|
|
15
|
+
animation: slideDown 180ms ease-out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@keyframes slideDown {
|
|
19
|
+
from {
|
|
20
|
+
opacity: 0;
|
|
21
|
+
max-height: 0;
|
|
22
|
+
}
|
|
23
|
+
to {
|
|
24
|
+
opacity: 1;
|
|
25
|
+
max-height: 600px;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.grid {
|
|
30
|
+
display: grid;
|
|
31
|
+
grid-template-columns: 280px 1fr 300px;
|
|
32
|
+
gap: 20px;
|
|
33
|
+
min-height: 220px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ─── Column shared ─── */
|
|
37
|
+
.col {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
gap: 10px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.colTitle {
|
|
44
|
+
font-size: 10px;
|
|
45
|
+
font-weight: 700;
|
|
46
|
+
text-transform: uppercase;
|
|
47
|
+
letter-spacing: 0.8px;
|
|
48
|
+
color: var(--color-text-muted);
|
|
49
|
+
margin-bottom: 2px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ─── Column 1: Info ─── */
|
|
53
|
+
.statGrid {
|
|
54
|
+
display: grid;
|
|
55
|
+
grid-template-columns: 1fr 1fr;
|
|
56
|
+
gap: 8px 12px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.statItem {
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-direction: column;
|
|
62
|
+
gap: 2px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.statLabel {
|
|
66
|
+
font-size: 9px;
|
|
67
|
+
font-weight: 700;
|
|
68
|
+
text-transform: uppercase;
|
|
69
|
+
letter-spacing: 0.5px;
|
|
70
|
+
color: var(--color-text-dim);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.statValue {
|
|
74
|
+
font-family: var(--font-mono);
|
|
75
|
+
font-size: 12px;
|
|
76
|
+
font-weight: 600;
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 5px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.infoActions {
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
gap: 8px;
|
|
86
|
+
margin-top: auto;
|
|
87
|
+
padding-top: 8px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.favBtn {
|
|
91
|
+
display: inline-flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 4px;
|
|
94
|
+
padding: 6px 10px;
|
|
95
|
+
background: var(--color-bg-card);
|
|
96
|
+
color: var(--color-text-muted);
|
|
97
|
+
border: 1px solid var(--color-border);
|
|
98
|
+
border-radius: 6px;
|
|
99
|
+
font-size: 11px;
|
|
100
|
+
font-weight: 600;
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
transition: all 150ms;
|
|
103
|
+
}
|
|
104
|
+
.favBtn:hover {
|
|
105
|
+
border-color: #ffd700;
|
|
106
|
+
color: #ffd700;
|
|
107
|
+
background: rgba(255, 215, 0, 0.08);
|
|
108
|
+
}
|
|
109
|
+
.favBtnActive {
|
|
110
|
+
border-color: #ffd700;
|
|
111
|
+
color: #ffd700;
|
|
112
|
+
background: rgba(255, 215, 0, 0.12);
|
|
113
|
+
}
|
|
114
|
+
:global([data-theme="light"]) .favBtn { color: #555; }
|
|
115
|
+
:global([data-theme="light"]) .favBtn:hover { color: #b08800; border-color: #b08800; background: rgba(176, 136, 0, 0.06); }
|
|
116
|
+
:global([data-theme="light"]) .favBtnActive { color: #b08800; border-color: #b08800; background: rgba(176, 136, 0, 0.10); }
|
|
117
|
+
|
|
118
|
+
/* ─── Column 2: Mini Playground ─── */
|
|
119
|
+
.playgroundHeader {
|
|
120
|
+
font-size: 12px;
|
|
121
|
+
font-weight: 700;
|
|
122
|
+
color: var(--color-text);
|
|
123
|
+
margin-bottom: 2px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.inputRow {
|
|
127
|
+
display: flex;
|
|
128
|
+
gap: 6px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.inputField {
|
|
132
|
+
flex: 1;
|
|
133
|
+
padding: 8px 10px;
|
|
134
|
+
font-size: 12px;
|
|
135
|
+
font-family: var(--font-mono);
|
|
136
|
+
background: var(--color-bg-card);
|
|
137
|
+
color: var(--color-text);
|
|
138
|
+
border: 1px solid var(--color-border);
|
|
139
|
+
border-radius: 6px;
|
|
140
|
+
outline: none;
|
|
141
|
+
transition: border-color 150ms;
|
|
142
|
+
}
|
|
143
|
+
.inputField:focus {
|
|
144
|
+
border-color: var(--color-accent);
|
|
145
|
+
}
|
|
146
|
+
.inputField::placeholder {
|
|
147
|
+
color: var(--color-text-dim);
|
|
148
|
+
}
|
|
149
|
+
.inputField:disabled {
|
|
150
|
+
opacity: 0.5;
|
|
151
|
+
cursor: not-allowed;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.sendBtn {
|
|
155
|
+
display: inline-flex;
|
|
156
|
+
align-items: center;
|
|
157
|
+
justify-content: center;
|
|
158
|
+
gap: 4px;
|
|
159
|
+
padding: 8px 12px;
|
|
160
|
+
font-size: 12px;
|
|
161
|
+
font-weight: 700;
|
|
162
|
+
font-family: var(--font-mono);
|
|
163
|
+
background: var(--color-accent);
|
|
164
|
+
color: var(--color-bg);
|
|
165
|
+
border: 1px solid transparent;
|
|
166
|
+
border-radius: 6px;
|
|
167
|
+
cursor: pointer;
|
|
168
|
+
transition: all 150ms;
|
|
169
|
+
white-space: nowrap;
|
|
170
|
+
}
|
|
171
|
+
.sendBtn:hover:not(:disabled) {
|
|
172
|
+
opacity: 0.9;
|
|
173
|
+
}
|
|
174
|
+
.sendBtn:disabled {
|
|
175
|
+
opacity: 0.4;
|
|
176
|
+
cursor: not-allowed;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.responseArea {
|
|
180
|
+
flex: 1;
|
|
181
|
+
min-height: 60px;
|
|
182
|
+
max-height: 160px;
|
|
183
|
+
overflow-y: auto;
|
|
184
|
+
padding: 10px 12px;
|
|
185
|
+
font-family: var(--font-mono);
|
|
186
|
+
font-size: 11px;
|
|
187
|
+
line-height: 1.5;
|
|
188
|
+
color: var(--color-text);
|
|
189
|
+
background: var(--color-bg-card);
|
|
190
|
+
border: 1px solid var(--color-border);
|
|
191
|
+
border-radius: 6px;
|
|
192
|
+
white-space: pre-wrap;
|
|
193
|
+
word-break: break-word;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.responsePlaceholder {
|
|
197
|
+
color: var(--color-text-dim);
|
|
198
|
+
font-style: italic;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* ─── Column 3: AI Latency ─── */
|
|
202
|
+
.benchBtn {
|
|
203
|
+
display: inline-flex;
|
|
204
|
+
align-items: center;
|
|
205
|
+
justify-content: center;
|
|
206
|
+
gap: 6px;
|
|
207
|
+
padding: 8px 14px;
|
|
208
|
+
font-size: 12px;
|
|
209
|
+
font-weight: 700;
|
|
210
|
+
font-family: var(--font-mono);
|
|
211
|
+
background: rgba(180, 0, 255, 0.12);
|
|
212
|
+
color: #b400ff;
|
|
213
|
+
border: 1px solid rgba(180, 0, 255, 0.4);
|
|
214
|
+
border-radius: 6px;
|
|
215
|
+
cursor: pointer;
|
|
216
|
+
transition: all 150ms;
|
|
217
|
+
}
|
|
218
|
+
.benchBtn:hover:not(:disabled) {
|
|
219
|
+
background: rgba(180, 0, 255, 0.20);
|
|
220
|
+
border-color: rgba(180, 0, 255, 0.7);
|
|
221
|
+
}
|
|
222
|
+
.benchBtn:disabled {
|
|
223
|
+
opacity: 0.4;
|
|
224
|
+
cursor: not-allowed;
|
|
225
|
+
}
|
|
226
|
+
:global([data-theme="light"]) .benchBtn {
|
|
227
|
+
background: rgba(140, 0, 200, 0.06);
|
|
228
|
+
color: #6a00a0;
|
|
229
|
+
border-color: rgba(140, 0, 200, 0.3);
|
|
230
|
+
}
|
|
231
|
+
:global([data-theme="light"]) .benchBtn:hover:not(:disabled) {
|
|
232
|
+
background: rgba(140, 0, 200, 0.12);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.metricsGrid {
|
|
236
|
+
display: grid;
|
|
237
|
+
grid-template-columns: 1fr 1fr;
|
|
238
|
+
gap: 6px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.metricCard {
|
|
242
|
+
display: flex;
|
|
243
|
+
flex-direction: column;
|
|
244
|
+
gap: 2px;
|
|
245
|
+
padding: 8px 10px;
|
|
246
|
+
background: var(--color-bg-card);
|
|
247
|
+
border: 1px solid var(--color-border);
|
|
248
|
+
border-radius: 6px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.metricLabel {
|
|
252
|
+
font-size: 9px;
|
|
253
|
+
font-weight: 700;
|
|
254
|
+
text-transform: uppercase;
|
|
255
|
+
letter-spacing: 0.5px;
|
|
256
|
+
color: var(--color-text-dim);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.metricValue {
|
|
260
|
+
font-family: var(--font-mono);
|
|
261
|
+
font-size: 14px;
|
|
262
|
+
font-weight: 700;
|
|
263
|
+
color: var(--color-accent);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.streamText {
|
|
267
|
+
min-height: 40px;
|
|
268
|
+
max-height: 80px;
|
|
269
|
+
overflow-y: auto;
|
|
270
|
+
padding: 8px 10px;
|
|
271
|
+
font-family: var(--font-mono);
|
|
272
|
+
font-size: 11px;
|
|
273
|
+
line-height: 1.4;
|
|
274
|
+
color: var(--color-text);
|
|
275
|
+
background: var(--color-bg-card);
|
|
276
|
+
border: 1px solid var(--color-border);
|
|
277
|
+
border-radius: 6px;
|
|
278
|
+
white-space: pre-wrap;
|
|
279
|
+
word-break: break-word;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.progressBar {
|
|
283
|
+
width: 100%;
|
|
284
|
+
height: 4px;
|
|
285
|
+
background: var(--color-bg-card);
|
|
286
|
+
border-radius: 2px;
|
|
287
|
+
overflow: hidden;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.progressFill {
|
|
291
|
+
height: 100%;
|
|
292
|
+
background: var(--color-accent);
|
|
293
|
+
border-radius: 2px;
|
|
294
|
+
transition: width 200ms ease-out;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* ─── SWE score classes ─── */
|
|
298
|
+
.sweHigh { color: #ffd700; font-weight: 700; }
|
|
299
|
+
.sweMid { color: #3ddc84; font-weight: 600; }
|
|
300
|
+
.sweLow { color: var(--color-text-dim); }
|
|
301
|
+
:global([data-theme="light"]) .sweHigh { color: #b08800; }
|
|
302
|
+
:global([data-theme="light"]) .sweMid { color: #1a8f4e; }
|
|
303
|
+
|
|
304
|
+
/* ─── Ping classes ─── */
|
|
305
|
+
.pingFast { color: #00ff88; }
|
|
306
|
+
.pingMedium { color: #ffaa00; }
|
|
307
|
+
.pingSlow { color: #ff4444; }
|
|
308
|
+
.pingNone { color: var(--color-text-dim); }
|
|
309
|
+
:global([data-theme="light"]) .pingFast { color: #008f4d; }
|
|
310
|
+
:global([data-theme="light"]) .pingMedium { color: #b86b00; }
|
|
311
|
+
:global([data-theme="light"]) .pingSlow { color: #c8143a; }
|
|
312
|
+
|
|
313
|
+
/* ─── No API key banner ─── */
|
|
314
|
+
.noKeyBanner {
|
|
315
|
+
grid-column: 1 / -1;
|
|
316
|
+
display: flex;
|
|
317
|
+
align-items: center;
|
|
318
|
+
justify-content: center;
|
|
319
|
+
gap: 10px;
|
|
320
|
+
padding: 32px 20px;
|
|
321
|
+
color: var(--color-text-dim);
|
|
322
|
+
font-size: 13px;
|
|
323
|
+
font-weight: 600;
|
|
324
|
+
text-align: center;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.noKeyIcon {
|
|
328
|
+
font-size: 20px;
|
|
329
|
+
opacity: 0.5;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* ─── Spinning animation ─── */
|
|
333
|
+
.spinning {
|
|
334
|
+
animation: spin 1s linear infinite;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
@keyframes spin {
|
|
338
|
+
from { transform: rotate(0deg); }
|
|
339
|
+
to { transform: rotate(360deg); }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* ─── Responsive: single column below 900px ─── */
|
|
343
|
+
@media (max-width: 900px) {
|
|
344
|
+
.grid {
|
|
345
|
+
grid-template-columns: 1fr;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
* 📖 + custom text filter chip with "X" clear + reset view button (N).
|
|
6
6
|
* 📖 Each chip is a cycling button matching the TUI single-key behavior.
|
|
7
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.
|
|
8
10
|
*/
|
|
9
|
-
import { useState, useEffect, useMemo } from 'react'
|
|
10
|
-
import { IconRefresh, IconX, IconFilter,
|
|
11
|
+
import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
|
|
12
|
+
import { IconRefresh, IconX, IconFilter, IconChevronDown } from '@tabler/icons-react'
|
|
11
13
|
import { getToolMeta } from '../../../../src/core/tool-metadata.js'
|
|
14
|
+
import ProviderDropdown from './ProviderDropdown.jsx'
|
|
12
15
|
import styles from './FilterBar.module.css'
|
|
13
16
|
|
|
14
17
|
// 📖 Chip sets match the TUI cycles 1:1 (see useFilter.js). Keep these in sync.
|
|
@@ -65,30 +68,84 @@ const PING_MODES = [
|
|
|
65
68
|
|
|
66
69
|
function formatCountdown(ms) {
|
|
67
70
|
if (ms == null) return null
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
return `${m}m${rem > 0 ? rem + 's' : ''}`
|
|
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`
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
|
|
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
|
+
|
|
77
108
|
return (
|
|
78
|
-
<div className={styles.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
)}
|
|
92
149
|
</div>
|
|
93
150
|
)
|
|
94
151
|
}
|
|
@@ -121,7 +178,7 @@ export default function FilterBar({
|
|
|
121
178
|
setCountdown(rem > 0 ? rem : 0)
|
|
122
179
|
}
|
|
123
180
|
tick()
|
|
124
|
-
const id = setInterval(tick,
|
|
181
|
+
const id = setInterval(tick, 100)
|
|
125
182
|
return () => clearInterval(id)
|
|
126
183
|
}, [nextPingAt])
|
|
127
184
|
|
|
@@ -167,10 +224,10 @@ export default function FilterBar({
|
|
|
167
224
|
</div>
|
|
168
225
|
)}
|
|
169
226
|
|
|
170
|
-
<
|
|
171
|
-
<
|
|
172
|
-
<
|
|
173
|
-
<
|
|
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} />
|
|
174
231
|
|
|
175
232
|
<div className={styles.group}>
|
|
176
233
|
<label className={styles.filterLabel} htmlFor="visibility-select">Visibility</label>
|
|
@@ -189,19 +246,12 @@ export default function FilterBar({
|
|
|
189
246
|
</div>
|
|
190
247
|
|
|
191
248
|
<div className={styles.group}>
|
|
192
|
-
<label className={styles.filterLabel}
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
className={styles.providerSelect}
|
|
249
|
+
<label className={styles.filterLabel}>Provider</label>
|
|
250
|
+
<ProviderDropdown
|
|
251
|
+
providers={providers}
|
|
196
252
|
value={filterProvider}
|
|
197
|
-
onChange={
|
|
198
|
-
|
|
199
|
-
>
|
|
200
|
-
<option value="all">All Providers</option>
|
|
201
|
-
{providers.map((p) => (
|
|
202
|
-
<option key={p.key} value={p.key}>{p.name} ({p.count})</option>
|
|
203
|
-
))}
|
|
204
|
-
</select>
|
|
253
|
+
onChange={setFilterProvider}
|
|
254
|
+
/>
|
|
205
255
|
</div>
|
|
206
256
|
|
|
207
257
|
<div className={styles.group}>
|
|
@@ -246,23 +296,14 @@ export default function FilterBar({
|
|
|
246
296
|
</button>
|
|
247
297
|
)}
|
|
248
298
|
|
|
249
|
-
{/* ── Ping interval selector ── */}
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
style={pingMode === m.key ? { '--ping-active-color': m.color } : {}}
|
|
258
|
-
onClick={() => setPingMode(m.key)}
|
|
259
|
-
title={`${m.interval} interval`}
|
|
260
|
-
>
|
|
261
|
-
{m.label}
|
|
262
|
-
</button>
|
|
263
|
-
))}
|
|
264
|
-
</div>
|
|
265
|
-
</div>
|
|
299
|
+
{/* ── Ping interval selector (collapsible group) ── */}
|
|
300
|
+
<FilterGroup
|
|
301
|
+
label="Ping"
|
|
302
|
+
items={PING_MODES}
|
|
303
|
+
value={pingMode}
|
|
304
|
+
onChange={setPingMode}
|
|
305
|
+
colorMap={Object.fromEntries(PING_MODES.map(m => [m.key, m.color]))}
|
|
306
|
+
/>
|
|
266
307
|
|
|
267
308
|
{/* ── Next ping countdown (TUI parity: always show the delay) ──
|
|
268
309
|
📖 The TUI footer always renders `next : Xs` regardless of whether
|
|
@@ -275,7 +316,6 @@ export default function FilterBar({
|
|
|
275
316
|
<div className={styles.nextPing} title="Next ping countdown">
|
|
276
317
|
<span className={styles.nextPingLabel}>next ping in</span>
|
|
277
318
|
<span className={styles.nextPingTime}>{countdownDisplay ?? '—'}</span>
|
|
278
|
-
{isPinging && <span className={styles.pingingDot} aria-hidden="true" />}
|
|
279
319
|
</div>
|
|
280
320
|
</div>
|
|
281
321
|
</section>
|