free-coding-models 0.5.15 → 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.
@@ -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
+ }
@@ -13,7 +13,7 @@
13
13
  * 📖 Custom widths persist in localStorage via the useColumnSizing hook and survive reloads.
14
14
  * 📖 A "Reset columns" button appears in the toolbar only when the user has custom widths.
15
15
  */
16
- import { useMemo, useEffect } from 'react'
16
+ import { useMemo, useEffect, useState, useCallback, Fragment } from 'react'
17
17
  import {
18
18
  useReactTable,
19
19
  getCoreRowModel,
@@ -43,6 +43,7 @@ import { sweClass } from '../../utils/ranks.js'
43
43
  // 📖 file the TUI uses; both surfaces share the source of truth for which
44
44
  // 📖 providers are compatible with which tools.
45
45
  import { getCompatibleTools } from '../../../../src/core/tool-metadata.js'
46
+ import ExpandedDetailRow from './ExpandedDetailRow.jsx'
46
47
  import styles from './ModelTable.module.css'
47
48
 
48
49
  const colHelper = createColumnHelper()
@@ -345,7 +346,15 @@ export default function ModelTable({
345
346
  filtered, onSelectModel, onBenchmarkRow, onLaunch, favorites,
346
347
  sortColumn, sortDirection, onSort,
347
348
  toolMode = null,
349
+ onToast, onSetToolMode, onCycleToolMode, onOpenFallback,
348
350
  }) {
351
+ // 📖 Expand row state — only one row expanded at a time (accordion)
352
+ const [expandedRowId, setExpandedRowId] = useState(null)
353
+
354
+ const toggleExpand = useCallback((model) => {
355
+ const key = `${model.providerKey}/${model.modelId}`
356
+ setExpandedRowId((prev) => prev === key ? null : key)
357
+ }, [])
349
358
  // Compute top3 for medal rows
350
359
  const top3Ids = useMemo(() => {
351
360
  const online = filtered.filter(m => m.status === 'up' && m.avg != null && m.avg !== Infinity && m.avg < 99000)
@@ -501,49 +510,60 @@ export default function ModelTable({
501
510
  <tbody>
502
511
  {rows.map((row, i) => {
503
512
  const m = row.original
513
+ const rowKey = `${m.providerKey}/${m.modelId}`
514
+ const isExpanded = expandedRowId === rowKey
504
515
  const rankIdx = [...top3Ids].indexOf(m.modelId)
505
516
  const rowClasses = []
506
517
  if (rankIdx >= 0) rowClasses.push(styles[`rank${rankIdx + 1}`])
507
- // 📖 Per-row benchmark state comes from the backend. The old global
508
- // 📖 heuristic made every unbenchmarked row spin until the whole scan ended.
509
518
  if (m.isBenchmarking) rowClasses.push(styles.benchRow)
510
- // 📖 Tool-compat dark-red row when an active tool mode is set and the
511
- // 📖 model isn't compatible with it. Mirrors the TUI render-table.js
512
- // 📖 behavior. The picker itself ships in M3; for M1 the visual is wired.
519
+ if (isExpanded) rowClasses.push(styles.expandedRow)
513
520
  if (toolMode) {
514
521
  const compat = getCompatibleTools(m.providerKey)
515
522
  if (!compat.includes(toolMode)) {
516
523
  rowClasses.push(styles.incompatible)
517
524
  }
518
525
  }
519
- // 📖 "Unusable" rows (no API key / bad API key) get the same 80% opacity
520
- // 📖 fade the TUI uses, so the user can see at a glance which models
521
- // 📖 they cannot actually use. The fade is applied as opacity on the
522
- // 📖 whole <tr> so it composes cleanly with rank/bench/incompatible
523
- // 📖 styles. Cursor hover is not implemented in the web, so the
524
- // 📖 fade is never overridden by an active-selection highlight.
525
526
  if (m.status === 'noauth' || m.status === 'auth_error') {
526
527
  rowClasses.push(styles.unusable)
527
528
  }
528
529
  return (
529
- <tr
530
- key={row.id}
531
- className={rowClasses.join(' ')}
532
- onClick={() => onSelectModel(row.original)}
533
- >
534
- {row.getVisibleCells().map(cell => {
535
- const sizePx = `${cell.column.getSize()}px`
536
- return (
537
- <td
538
- key={cell.id}
539
- className={styles.td}
540
- style={{ width: sizePx, minWidth: sizePx, maxWidth: sizePx }}
541
- >
542
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
530
+ <Fragment key={row.id}>
531
+ <tr
532
+ className={rowClasses.join(' ')}
533
+ onClick={() => toggleExpand(m)}
534
+ >
535
+ {row.getVisibleCells().map(cell => {
536
+ const sizePx = `${cell.column.getSize()}px`
537
+ return (
538
+ <td
539
+ key={cell.id}
540
+ className={styles.td}
541
+ style={{ width: sizePx, minWidth: sizePx, maxWidth: sizePx }}
542
+ >
543
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
544
+ </td>
545
+ )
546
+ })}
547
+ </tr>
548
+ {/* 📖 Expanded detail row — 3-column panel with info, playground, AI latency */}
549
+ {isExpanded && (
550
+ <tr className={styles.expandRowWrapper}>
551
+ <td colSpan={row.getVisibleCells().length} className={styles.expandRowCell}>
552
+ <ExpandedDetailRow
553
+ model={m}
554
+ favorites={favorites}
555
+ onBenchmark={onBenchmarkRow}
556
+ onLaunch={onLaunch}
557
+ onToast={onToast}
558
+ toolMode={toolMode}
559
+ onSetToolMode={onSetToolMode}
560
+ onCycleToolMode={onCycleToolMode}
561
+ onOpenFallback={onOpenFallback}
562
+ />
543
563
  </td>
544
- )
545
- })}
546
- </tr>
564
+ </tr>
565
+ )}
566
+ </Fragment>
547
567
  )
548
568
  })}
549
569
  </tbody>
@@ -356,4 +356,16 @@
356
356
  .incompatible .launchCell,
357
357
  .benchRow .launchCell {
358
358
  opacity: 1;
359
+ }
360
+
361
+ /* ─── Expandable row styles ─── */
362
+ .expandedRow {
363
+ background: var(--color-bg-active) !important;
364
+ }
365
+ .expandRowWrapper {
366
+ border: none;
367
+ }
368
+ .expandRowCell {
369
+ padding: 0 !important;
370
+ border-bottom: 2px solid var(--color-accent) !important;
359
371
  }