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.
Files changed (34) hide show
  1. package/changelog/v0.5.12.md +86 -2
  2. package/changelog/v0.5.13.md +19 -53
  3. package/changelog/v0.5.15.md +17 -0
  4. package/changelog/v0.5.16.md +15 -0
  5. package/package.json +4 -3
  6. package/src/tui/app.js +16 -4
  7. package/src/tui/cli-help.js +87 -0
  8. package/src/tui/key-handler.js +2 -14
  9. package/src/tui/render-table.js +46 -26
  10. package/src/tui/theme.js +70 -0
  11. package/web/README.md +1 -1
  12. package/web/dist/assets/index-avN2GM3H.css +1 -0
  13. package/web/dist/assets/index-vsysCImz.js +41 -0
  14. package/web/dist/index.html +6 -2
  15. package/web/index.html +4 -0
  16. package/web/server.js +249 -15
  17. package/web/src/App.jsx +46 -15
  18. package/web/src/components/dashboard/DetailPanel.jsx +1 -1
  19. package/web/src/components/dashboard/DetailPanel.module.css +5 -0
  20. package/web/src/components/dashboard/ExpandedDetailRow.jsx +464 -0
  21. package/web/src/components/dashboard/ExpandedDetailRow.module.css +347 -0
  22. package/web/src/components/dashboard/FilterBar.jsx +98 -58
  23. package/web/src/components/dashboard/FilterBar.module.css +103 -33
  24. package/web/src/components/dashboard/ModelTable.jsx +49 -29
  25. package/web/src/components/dashboard/ModelTable.module.css +12 -0
  26. package/web/src/components/dashboard/ProviderDropdown.jsx +156 -0
  27. package/web/src/components/dashboard/ProviderDropdown.module.css +248 -0
  28. package/web/src/components/help/HelpView.jsx +13 -0
  29. package/web/src/components/playground/PlaygroundView.jsx +193 -12
  30. package/web/src/components/playground/PlaygroundView.module.css +82 -0
  31. package/web/src/components/router/RouterView.jsx +15 -6
  32. package/web/vite.config.js +1 -1
  33. package/web/dist/assets/index-Bzqz7KbT.js +0 -39
  34. 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, IconCircleCheck, IconAlertTriangle, IconActivity, IconEye, IconStar } from '@tabler/icons-react'
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 s = Math.max(0, Math.ceil(ms / 1000))
69
- if (s === 1) return '1s'
70
- if (s < 60) return `${s}s`
71
- const m = Math.floor(s / 60)
72
- const rem = s % 60
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
- function ChipRow({ items, value, onChange, label }) {
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.group}>
79
- {label && <label className={styles.filterLabel}>{label}</label>}
80
- <div className={styles.chipRow}>
81
- {items.map((item) => (
82
- <button
83
- key={item.key}
84
- className={`${styles.chip} ${value === item.key ? styles.chipActive : ''}`}
85
- onClick={() => onChange(item.key)}
86
- title={item.hint || item.label}
87
- >
88
- {item.label}
89
- </button>
90
- ))}
91
- </div>
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, 1000)
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
- <ChipRow label="Tier" items={TIERS} value={filterTier} onChange={setFilterTier} />
171
- <ChipRow label="Status" items={STATUSES} value={filterStatus} onChange={setFilterStatus} />
172
- <ChipRow label="Verdict" items={VERDICTS} value={filterVerdict} onChange={setFilterVerdict} />
173
- <ChipRow label="Health" items={HEALTHS} value={filterHealth} onChange={setFilterHealth} />
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} htmlFor="provider-select">Provider</label>
193
- <select
194
- id="provider-select"
195
- className={styles.providerSelect}
249
+ <label className={styles.filterLabel}>Provider</label>
250
+ <ProviderDropdown
251
+ providers={providers}
196
252
  value={filterProvider}
197
- onChange={(e) => setFilterProvider(e.target.value)}
198
- aria-label="Filter by provider"
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
- <div className={styles.group}>
251
- <label className={styles.filterLabel}>Ping</label>
252
- <div className={styles.pingRow}>
253
- {PING_MODES.map((m) => (
254
- <button
255
- key={m.key}
256
- className={`${styles.pingBtn} ${pingMode === m.key ? styles.pingBtnActive : ''}`}
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>