free-coding-models 0.5.32 → 0.5.35
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/LICENSE +43 -21
- package/README.md +2 -1
- package/changelog/v0.5.33.md +4 -0
- package/changelog/v0.5.34.md +4 -0
- package/changelog/v0.5.35.md +4 -0
- package/package.json +3 -2
- package/sources.js +12 -52
- package/src/core/router-daemon.js +56 -49
- package/src/core/schema-normalizer.js +172 -0
- package/src/core/tool-bootstrap.js +11 -0
- package/src/core/tool-launchers.js +37 -2
- package/src/core/tool-metadata.js +3 -0
- package/web/dist/assets/index-B5tTa7_O.js +40 -0
- package/web/dist/assets/index-r8niexgJ.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/src/App.jsx +93 -105
- package/web/src/components/changelog/ChangelogView.module.css +13 -14
- package/web/src/components/dashboard/FilterBar.jsx +17 -2
- package/web/src/components/dashboard/FilterBar.module.css +68 -21
- package/web/src/components/dashboard/ModelTable.jsx +118 -42
- package/web/src/components/dashboard/ModelTable.module.css +82 -27
- package/web/src/components/help/HelpView.module.css +13 -14
- package/web/src/components/install/InstallEndpointsView.module.css +13 -13
- package/web/src/components/installed/InstalledModelsView.module.css +13 -13
- package/web/src/components/layout/Header.jsx +28 -28
- package/web/src/components/layout/Header.module.css +56 -30
- package/web/src/components/palette/CommandPalette.jsx +14 -14
- package/web/src/components/playground/PlaygroundView.module.css +13 -14
- package/web/src/components/recommend/RecommendView.module.css +13 -11
- package/web/src/components/router/RouterView.module.css +13 -13
- package/web/src/components/tools/ToolPicker.module.css +27 -14
- package/web/src/global.css +58 -31
- package/web/src/hooks/urlState.constants.js +1 -1
- package/web/src/hooks/useUrlState.js +19 -3
- package/web/dist/assets/index-C_tCF0A5.js +0 -40
- package/web/dist/assets/index-CsFt3qt5.css +0 -1
|
@@ -4,28 +4,27 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
.backdrop {
|
|
7
|
-
position: fixed;
|
|
8
|
-
inset: 0;
|
|
9
|
-
background: rgba(0, 0, 0, 0.55);
|
|
10
|
-
backdrop-filter: blur(4px);
|
|
11
|
-
-webkit-backdrop-filter: blur(4px);
|
|
12
|
-
z-index: 500;
|
|
13
7
|
display: flex;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
flex: 1;
|
|
10
|
+
min-height: 0;
|
|
11
|
+
width: 100%;
|
|
12
|
+
padding: 24px;
|
|
17
13
|
}
|
|
18
14
|
|
|
19
15
|
.modal {
|
|
20
|
-
width:
|
|
21
|
-
max-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
width: 100%;
|
|
17
|
+
max-width: 720px;
|
|
18
|
+
margin: 0 auto;
|
|
19
|
+
background: var(--color-bg-card, #080908);
|
|
20
|
+
border: 1px solid var(--color-border, #161a16);
|
|
24
21
|
border-radius: 14px;
|
|
25
|
-
box-shadow: 0
|
|
22
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
26
23
|
display: flex;
|
|
27
24
|
flex-direction: column;
|
|
28
25
|
overflow: hidden;
|
|
26
|
+
flex: 1;
|
|
27
|
+
min-height: 0;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
.header {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* — click the trigger to expand the chip row, click again or outside to close.
|
|
10
10
|
*/
|
|
11
11
|
import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
|
|
12
|
-
import { IconRefresh, IconX, IconFilter, IconChevronDown } from '@tabler/icons-react'
|
|
12
|
+
import { IconRefresh, IconX, IconFilter, IconChevronDown, IconSearch } from '@tabler/icons-react'
|
|
13
13
|
import { getToolMeta } from '../../../../src/core/tool-metadata.js'
|
|
14
14
|
import ProviderDropdown from './ProviderDropdown.jsx'
|
|
15
15
|
import styles from './FilterBar.module.css'
|
|
@@ -158,7 +158,7 @@ export default function FilterBar({
|
|
|
158
158
|
filterHealth, setFilterHealth,
|
|
159
159
|
visibilityMode, setVisibilityMode,
|
|
160
160
|
customTextFilter, setCustomTextFilter,
|
|
161
|
-
searchQuery,
|
|
161
|
+
searchQuery, onSearchChange,
|
|
162
162
|
onResetView,
|
|
163
163
|
providers,
|
|
164
164
|
pingMode, setPingMode,
|
|
@@ -254,6 +254,21 @@ export default function FilterBar({
|
|
|
254
254
|
/>
|
|
255
255
|
</div>
|
|
256
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
|
+
|
|
257
272
|
<div className={styles.group}>
|
|
258
273
|
<label className={styles.filterLabel}>Endpoint target</label>
|
|
259
274
|
<div className={styles.toolStatus} title="Active endpoint install target from the Header picker">
|
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
* @file web/src/components/dashboard/FilterBar.module.css
|
|
3
3
|
* @description Filter bar styles — M1 parity chips (tier, status, verdict, health,
|
|
4
4
|
* visibility, provider, custom text filter, reset, ping mode).
|
|
5
|
-
* 📖
|
|
6
|
-
* 📖
|
|
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.
|
|
7
13
|
*/
|
|
8
14
|
|
|
9
15
|
.filters {
|
|
10
|
-
position:
|
|
16
|
+
position: relative; z-index: 5;
|
|
17
|
+
flex-shrink: 0;
|
|
11
18
|
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
|
|
12
19
|
padding: 10px 20px;
|
|
13
20
|
background: var(--color-bg-card);
|
|
@@ -19,7 +26,7 @@
|
|
|
19
26
|
.group { display: flex; align-items: center; gap: 6px; }
|
|
20
27
|
|
|
21
28
|
.filterLabel {
|
|
22
|
-
font-size:
|
|
29
|
+
font-size: 12px; font-weight: 700; color: var(--color-text-muted);
|
|
23
30
|
text-transform: uppercase; letter-spacing: 0.5px;
|
|
24
31
|
white-space: nowrap;
|
|
25
32
|
}
|
|
@@ -31,7 +38,7 @@
|
|
|
31
38
|
|
|
32
39
|
.filterTrigger {
|
|
33
40
|
display: inline-flex; align-items: center; gap: 4px;
|
|
34
|
-
font-size:
|
|
41
|
+
font-size: 13px; font-family: var(--font-mono);
|
|
35
42
|
padding: 4px 8px; border-radius: 5px;
|
|
36
43
|
border: 1px solid var(--color-border);
|
|
37
44
|
background: var(--color-surface);
|
|
@@ -59,7 +66,7 @@
|
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
.filterTriggerLabel {
|
|
62
|
-
font-size:
|
|
69
|
+
font-size: 11px; font-weight: 700;
|
|
63
70
|
text-transform: uppercase; letter-spacing: 0.5px;
|
|
64
71
|
color: var(--color-text-muted);
|
|
65
72
|
}
|
|
@@ -103,7 +110,7 @@
|
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
.filterChip {
|
|
106
|
-
font-size:
|
|
113
|
+
font-size: 13px; font-weight: 600; font-family: var(--font-mono);
|
|
107
114
|
padding: 4px 9px;
|
|
108
115
|
border: 1px solid var(--color-border);
|
|
109
116
|
background: var(--color-bg-card);
|
|
@@ -136,7 +143,7 @@
|
|
|
136
143
|
.select, .providerSelect {
|
|
137
144
|
background: var(--color-surface); color: var(--color-text);
|
|
138
145
|
border: 1px solid var(--color-border); border-radius: 5px;
|
|
139
|
-
padding: 4px 8px; font-size:
|
|
146
|
+
padding: 4px 8px; font-size: 13px; font-family: var(--font-sans);
|
|
140
147
|
cursor: pointer; outline: none;
|
|
141
148
|
min-width: 130px;
|
|
142
149
|
}
|
|
@@ -151,11 +158,11 @@
|
|
|
151
158
|
border-radius: 5px;
|
|
152
159
|
background: var(--color-surface);
|
|
153
160
|
color: var(--color-text);
|
|
154
|
-
font-size:
|
|
161
|
+
font-size: 13px;
|
|
155
162
|
white-space: nowrap;
|
|
156
163
|
}
|
|
157
164
|
.toolStatus strong {
|
|
158
|
-
font-size:
|
|
165
|
+
font-size: 13px;
|
|
159
166
|
font-weight: 800;
|
|
160
167
|
}
|
|
161
168
|
|
|
@@ -164,7 +171,7 @@
|
|
|
164
171
|
|
|
165
172
|
.resetBtn {
|
|
166
173
|
display: inline-flex; align-items: center; gap: 5px;
|
|
167
|
-
font-size:
|
|
174
|
+
font-size: 13px; font-weight: 700; font-family: var(--font-mono);
|
|
168
175
|
padding: 4px 9px; border-radius: 5px;
|
|
169
176
|
border: 1px solid var(--color-border-hover);
|
|
170
177
|
background: var(--color-accent-dim);
|
|
@@ -178,7 +185,7 @@
|
|
|
178
185
|
.resetBadge {
|
|
179
186
|
display: inline-flex; align-items: center; justify-content: center;
|
|
180
187
|
min-width: 16px; height: 16px;
|
|
181
|
-
font-size:
|
|
188
|
+
font-size: 11px; font-weight: 800;
|
|
182
189
|
background: var(--color-bg);
|
|
183
190
|
color: var(--color-accent);
|
|
184
191
|
border-radius: 999px;
|
|
@@ -188,7 +195,7 @@
|
|
|
188
195
|
/* ── Custom text filter chip (TUI Ctrl+P "Apply text filter") ── */
|
|
189
196
|
.customFilterChip {
|
|
190
197
|
display: inline-flex; align-items: center; gap: 6px;
|
|
191
|
-
font-size:
|
|
198
|
+
font-size: 13px; font-weight: 600; font-family: var(--font-mono);
|
|
192
199
|
padding: 3px 4px 3px 8px;
|
|
193
200
|
border: 1px solid var(--color-accent);
|
|
194
201
|
background: var(--color-accent-dim);
|
|
@@ -217,18 +224,18 @@
|
|
|
217
224
|
/* ── Next ping countdown (TUI parity) ── */
|
|
218
225
|
.nextPing {
|
|
219
226
|
display: flex; align-items: center; gap: 6px;
|
|
220
|
-
font-size:
|
|
227
|
+
font-size: 13px; font-family: var(--font-mono);
|
|
221
228
|
padding: 4px 8px;
|
|
222
229
|
border: 1px solid var(--color-border);
|
|
223
230
|
border-radius: 5px;
|
|
224
231
|
background: var(--color-surface);
|
|
225
232
|
}
|
|
226
233
|
.nextPingLabel {
|
|
227
|
-
font-size:
|
|
234
|
+
font-size: 12px; font-weight: 500; color: var(--color-text-muted);
|
|
228
235
|
text-transform: uppercase; letter-spacing: 0.5px;
|
|
229
236
|
}
|
|
230
237
|
.nextPingTime {
|
|
231
|
-
font-size:
|
|
238
|
+
font-size: 15px; font-weight: 700; color: var(--color-text);
|
|
232
239
|
min-width: 32px;
|
|
233
240
|
text-align: right;
|
|
234
241
|
font-variant-numeric: tabular-nums;
|
|
@@ -240,7 +247,7 @@
|
|
|
240
247
|
|
|
241
248
|
.live {
|
|
242
249
|
display: flex; align-items: center; gap: 5px;
|
|
243
|
-
font-size:
|
|
250
|
+
font-size: 12px; font-weight: 700; color: var(--color-accent);
|
|
244
251
|
text-transform: uppercase; letter-spacing: 1px;
|
|
245
252
|
}
|
|
246
253
|
.liveDot {
|
|
@@ -261,16 +268,16 @@
|
|
|
261
268
|
}
|
|
262
269
|
.benchmarkLabel {
|
|
263
270
|
display: flex; align-items: center; gap: 5px;
|
|
264
|
-
font-size:
|
|
271
|
+
font-size: 13px; font-weight: 700;
|
|
265
272
|
color: #b400ff; font-family: var(--font-mono);
|
|
266
273
|
white-space: nowrap;
|
|
267
274
|
}
|
|
268
275
|
.benchmarkSpinner {
|
|
269
276
|
display: inline-block;
|
|
270
277
|
animation: spinSp 0.8s linear infinite;
|
|
271
|
-
font-size:
|
|
278
|
+
font-size: 14px; line-height: 1;
|
|
272
279
|
}
|
|
273
|
-
.benchmarkCount { font-size:
|
|
280
|
+
.benchmarkCount { font-size: 12px; opacity: 0.8; }
|
|
274
281
|
.benchmarkTrack {
|
|
275
282
|
width: 120px; height: 5px;
|
|
276
283
|
background: rgba(255,255,255,0.1);
|
|
@@ -284,7 +291,7 @@
|
|
|
284
291
|
transition: width 0.5s ease;
|
|
285
292
|
}
|
|
286
293
|
.benchmarkPct {
|
|
287
|
-
font-size:
|
|
294
|
+
font-size: 13px; font-weight: 700;
|
|
288
295
|
color: #b400ff; font-family: var(--font-mono);
|
|
289
296
|
min-width: 32px; text-align: right;
|
|
290
297
|
}
|
|
@@ -293,3 +300,43 @@
|
|
|
293
300
|
from { transform: rotate(0deg); }
|
|
294
301
|
to { transform: rotate(360deg); }
|
|
295
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
|
+
}
|
|
@@ -13,13 +13,14 @@
|
|
|
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, useState, useCallback,
|
|
16
|
+
import { useMemo, useEffect, useState, useCallback, useRef } from 'react'
|
|
17
17
|
import {
|
|
18
18
|
useReactTable,
|
|
19
19
|
getCoreRowModel,
|
|
20
20
|
flexRender,
|
|
21
21
|
createColumnHelper,
|
|
22
22
|
} from '@tanstack/react-table'
|
|
23
|
+
import { useVirtualizer } from '@tanstack/react-virtual'
|
|
23
24
|
import { IconStar, IconStarFilled, IconPlayerPlayFilled } from '@tabler/icons-react'
|
|
24
25
|
import { useColumnSizing } from '../../hooks/useColumnSizing.js'
|
|
25
26
|
|
|
@@ -176,10 +177,12 @@ function TrendCellRenderer({ row }) {
|
|
|
176
177
|
// 📖 the cells always see fresh values.
|
|
177
178
|
const buildColumns = ({ favorites, onBenchmarkRow, onSelectModel, onLaunch, toolMode }) => [
|
|
178
179
|
// 📖 Star column — leftmost, mirrors the TUI's `F` key.
|
|
180
|
+
// 📖 First column = bookmarks (TUI `F` key). Widened from 32→104 so the
|
|
181
|
+
// 📖 "BOOKMARKS" header label fits horizontally without clipping.
|
|
179
182
|
colHelper.display({
|
|
180
183
|
id: 'fav',
|
|
181
|
-
header: '
|
|
182
|
-
size:
|
|
184
|
+
header: 'BOOKMARKS',
|
|
185
|
+
size: 104,
|
|
183
186
|
enableSorting: false,
|
|
184
187
|
cell: ({ row }) => {
|
|
185
188
|
const m = row.original
|
|
@@ -351,6 +354,12 @@ export default function ModelTable({
|
|
|
351
354
|
// 📖 Expand row state — only one row expanded at a time (accordion)
|
|
352
355
|
const [expandedRowId, setExpandedRowId] = useState(null)
|
|
353
356
|
|
|
357
|
+
// 📖 scrollRef is the internal scroll container — shared by the sticky
|
|
358
|
+
// 📖 <thead> header and the row virtualizer (only visible rows are in the
|
|
359
|
+
// 📖 DOM; off-screen rows stay in React state so live pings/probes keep
|
|
360
|
+
// 📖 updating them and re-render correctly when scrolled into view).
|
|
361
|
+
const scrollRef = useRef(null)
|
|
362
|
+
|
|
354
363
|
const toggleExpand = useCallback((model) => {
|
|
355
364
|
const key = `${model.providerKey}/${model.modelId}`
|
|
356
365
|
setExpandedRowId((prev) => prev === key ? null : key)
|
|
@@ -402,6 +411,42 @@ export default function ModelTable({
|
|
|
402
411
|
|
|
403
412
|
const rows = table.getRowModel().rows
|
|
404
413
|
|
|
414
|
+
// 📖 Flatten rows + any expanded detail panel into a single virtualized
|
|
415
|
+
// 📖 list. One <tr> per item keeps TanStack Virtual's dynamic measurement
|
|
416
|
+
// 📖 clean (one measured element per virtual item). The expand item only
|
|
417
|
+
// 📖 exists for the single accordion-open row, so count changes by at most 1.
|
|
418
|
+
const flatRows = useMemo(() => {
|
|
419
|
+
const out = []
|
|
420
|
+
for (const row of rows) {
|
|
421
|
+
const m = row.original
|
|
422
|
+
const key = `${m.providerKey}/${m.modelId}`
|
|
423
|
+
out.push({ type: 'row', row, key })
|
|
424
|
+
if (expandedRowId === key) {
|
|
425
|
+
out.push({ type: 'expand', row, key: `${key}__expand` })
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return out
|
|
429
|
+
}, [rows, expandedRowId])
|
|
430
|
+
|
|
431
|
+
// 📖 Row virtualizer: renders only the ~15-25 rows in the viewport (+overscan)
|
|
432
|
+
// 📖 instead of all 190+. Spacer <tr>s above/below preserve the full scroll
|
|
433
|
+
// 📖 height. This is a RENDER-only optimization — ALL model data (including
|
|
434
|
+
// 📖 live pings/probes for off-screen rows) still lives in React state via
|
|
435
|
+
// 📖 useSocket, so updates continue uninterrupted and show fresh values the
|
|
436
|
+
// 📖 moment a row scrolls back into view. Nothing about probing changes.
|
|
437
|
+
const rowVirtualizer = useVirtualizer({
|
|
438
|
+
count: flatRows.length,
|
|
439
|
+
getScrollElement: () => scrollRef.current,
|
|
440
|
+
estimateSize: (i) => (flatRows[i]?.type === 'expand' ? 260 : 47),
|
|
441
|
+
overscan: 10,
|
|
442
|
+
getItemKey: (i) => flatRows[i].key,
|
|
443
|
+
})
|
|
444
|
+
const virtualItems = rowVirtualizer.getVirtualItems()
|
|
445
|
+
const totalSize = rowVirtualizer.getTotalSize()
|
|
446
|
+
const padTop = virtualItems.length ? Math.max(0, virtualItems[0].start) : 0
|
|
447
|
+
const lastVi = virtualItems.length ? virtualItems[virtualItems.length - 1] : null
|
|
448
|
+
const padBottom = lastVi ? Math.max(0, totalSize - lastVi.end) : 0
|
|
449
|
+
|
|
405
450
|
// 📖 Total table width = sum of all column sizes. Used to drop the min-width
|
|
406
451
|
// 📖 override once the user has manually tuned the columns, so horizontal
|
|
407
452
|
// 📖 scrolling reflects the user's layout rather than the 1200px default.
|
|
@@ -433,6 +478,8 @@ export default function ModelTable({
|
|
|
433
478
|
|
|
434
479
|
return (
|
|
435
480
|
<div className={styles.container}>
|
|
481
|
+
{/* 📖 resizeToolbar lives OUTSIDE the scroller so it never overlaps the
|
|
482
|
+
📖 sticky table header (thead th sticks to the top of scrollInner). */}
|
|
436
483
|
{hasCustomSizing && (
|
|
437
484
|
<div className={styles.resizeToolbar}>
|
|
438
485
|
<span className={styles.resizeToolbarHint}>
|
|
@@ -448,6 +495,10 @@ export default function ModelTable({
|
|
|
448
495
|
</button>
|
|
449
496
|
</div>
|
|
450
497
|
)}
|
|
498
|
+
{/* 📖 scrollInner is the actual scroller. It is the scroll element for
|
|
499
|
+
📖 both the sticky <thead> header and the row virtualizer. Bounded by
|
|
500
|
+
📖 .dashboardView (viewport height) so the table scrolls internally. */}
|
|
501
|
+
<div className={styles.scrollInner} ref={scrollRef}>
|
|
451
502
|
<table
|
|
452
503
|
className={styles.table}
|
|
453
504
|
style={hasCustomSizing ? { minWidth: `${totalTableWidth}px` } : undefined}
|
|
@@ -508,10 +559,45 @@ export default function ModelTable({
|
|
|
508
559
|
))}
|
|
509
560
|
</thead>
|
|
510
561
|
<tbody>
|
|
511
|
-
{
|
|
562
|
+
{/* 📖 Top spacer: represents the scroll height of all virtualized-out
|
|
563
|
+
📖 rows ABOVE the viewport. aria-hidden + empty cell keeps it
|
|
564
|
+
📖 invisible while preserving table column layout. */}
|
|
565
|
+
{padTop > 0 && (
|
|
566
|
+
<tr aria-hidden="true" style={{ height: padTop }}>
|
|
567
|
+
<td style={{ height: padTop, padding: 0, border: 0 }} />
|
|
568
|
+
</tr>
|
|
569
|
+
)}
|
|
570
|
+
{virtualItems.map((vi) => {
|
|
571
|
+
const item = flatRows[vi.index]
|
|
572
|
+
// 📖 Expanded detail panel row — its own virtual item so dynamic
|
|
573
|
+
// 📖 measurement stays one-element-per-item (clean heights).
|
|
574
|
+
if (item.type === 'expand') {
|
|
575
|
+
return (
|
|
576
|
+
<tr
|
|
577
|
+
key={item.key}
|
|
578
|
+
className={styles.expandRowWrapper}
|
|
579
|
+
data-index={vi.index}
|
|
580
|
+
ref={rowVirtualizer.measureElement}
|
|
581
|
+
>
|
|
582
|
+
<td colSpan={item.row.getVisibleCells().length} className={styles.expandRowCell}>
|
|
583
|
+
<ExpandedDetailRow
|
|
584
|
+
model={item.row.original}
|
|
585
|
+
favorites={favorites}
|
|
586
|
+
onBenchmark={onBenchmarkRow}
|
|
587
|
+
onLaunch={onLaunch}
|
|
588
|
+
onToast={onToast}
|
|
589
|
+
toolMode={toolMode}
|
|
590
|
+
onSetToolMode={onSetToolMode}
|
|
591
|
+
onCycleToolMode={onCycleToolMode}
|
|
592
|
+
onOpenFallback={onOpenFallback}
|
|
593
|
+
/>
|
|
594
|
+
</td>
|
|
595
|
+
</tr>
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
const row = item.row
|
|
512
599
|
const m = row.original
|
|
513
|
-
const
|
|
514
|
-
const isExpanded = expandedRowId === rowKey
|
|
600
|
+
const isExpanded = expandedRowId === item.key
|
|
515
601
|
const rankIdx = [...top3Ids].indexOf(m.modelId)
|
|
516
602
|
const rowClasses = []
|
|
517
603
|
if (rankIdx >= 0) rowClasses.push(styles[`rank${rankIdx + 1}`])
|
|
@@ -530,47 +616,37 @@ export default function ModelTable({
|
|
|
530
616
|
rowClasses.push(styles.notInSetRow)
|
|
531
617
|
}
|
|
532
618
|
return (
|
|
533
|
-
<
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
})}
|
|
550
|
-
</tr>
|
|
551
|
-
{/* 📖 Expanded detail row — 3-column panel with info, playground, AI latency */}
|
|
552
|
-
{isExpanded && (
|
|
553
|
-
<tr className={styles.expandRowWrapper}>
|
|
554
|
-
<td colSpan={row.getVisibleCells().length} className={styles.expandRowCell}>
|
|
555
|
-
<ExpandedDetailRow
|
|
556
|
-
model={m}
|
|
557
|
-
favorites={favorites}
|
|
558
|
-
onBenchmark={onBenchmarkRow}
|
|
559
|
-
onLaunch={onLaunch}
|
|
560
|
-
onToast={onToast}
|
|
561
|
-
toolMode={toolMode}
|
|
562
|
-
onSetToolMode={onSetToolMode}
|
|
563
|
-
onCycleToolMode={onCycleToolMode}
|
|
564
|
-
onOpenFallback={onOpenFallback}
|
|
565
|
-
/>
|
|
619
|
+
<tr
|
|
620
|
+
key={item.key}
|
|
621
|
+
className={rowClasses.join(' ')}
|
|
622
|
+
onClick={() => toggleExpand(m)}
|
|
623
|
+
data-index={vi.index}
|
|
624
|
+
ref={rowVirtualizer.measureElement}
|
|
625
|
+
>
|
|
626
|
+
{row.getVisibleCells().map(cell => {
|
|
627
|
+
const sizePx = `${cell.column.getSize()}px`
|
|
628
|
+
return (
|
|
629
|
+
<td
|
|
630
|
+
key={cell.id}
|
|
631
|
+
className={styles.td}
|
|
632
|
+
style={{ width: sizePx, minWidth: sizePx, maxWidth: sizePx }}
|
|
633
|
+
>
|
|
634
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
566
635
|
</td>
|
|
567
|
-
|
|
568
|
-
)}
|
|
569
|
-
</
|
|
636
|
+
)
|
|
637
|
+
})}
|
|
638
|
+
</tr>
|
|
570
639
|
)
|
|
571
640
|
})}
|
|
641
|
+
{/* 📖 Bottom spacer: scroll height of all virtualized-out rows BELOW. */}
|
|
642
|
+
{padBottom > 0 && (
|
|
643
|
+
<tr aria-hidden="true" style={{ height: padBottom }}>
|
|
644
|
+
<td style={{ height: padBottom, padding: 0, border: 0 }} />
|
|
645
|
+
</tr>
|
|
646
|
+
)}
|
|
572
647
|
</tbody>
|
|
573
648
|
</table>
|
|
649
|
+
</div>
|
|
574
650
|
</div>
|
|
575
651
|
)
|
|
576
652
|
}
|