free-coding-models 0.5.32 → 0.5.34
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/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-r8niexgJ.css +1 -0
- package/web/dist/assets/index-zzUvtumw.js +40 -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
|
@@ -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
|
}
|
|
@@ -13,6 +13,24 @@
|
|
|
13
13
|
border-right: 0;
|
|
14
14
|
border-radius: 0;
|
|
15
15
|
margin: 0;
|
|
16
|
+
flex: 1;
|
|
17
|
+
min-height: 0;
|
|
18
|
+
/* 📖 The container itself doesn't scroll — it is the relative clip box
|
|
19
|
+
📖 hosting the scrolling table (.scrollInner). overflow:hidden keeps the
|
|
20
|
+
📖 sticky header + virtualized rows neatly clipped to the table viewport.
|
|
21
|
+
📖 isolation:isolate creates a clean stacking context so the sticky
|
|
22
|
+
📖 <thead> header (z-index 10) reliably layers above body cells. */
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
position: relative;
|
|
25
|
+
isolation: isolate;
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* 📖 The real scroll surface for the table. It is the scroll element for
|
|
31
|
+
📖 both the sticky <thead> header and the @tanstack/react-virtual row
|
|
32
|
+
📖 virtualizer (only ~23 of 189 rows live in the DOM at any time). */
|
|
33
|
+
.scrollInner {
|
|
16
34
|
flex: 1;
|
|
17
35
|
min-height: 0;
|
|
18
36
|
overflow: auto;
|
|
@@ -24,27 +42,29 @@
|
|
|
24
42
|
width: 100%;
|
|
25
43
|
min-width: 1200px;
|
|
26
44
|
border-collapse: collapse;
|
|
27
|
-
font-size:
|
|
45
|
+
font-size: 14px;
|
|
28
46
|
border: 0;
|
|
29
47
|
}
|
|
30
48
|
|
|
31
49
|
/* ─── Header ─── */
|
|
32
50
|
.th {
|
|
33
|
-
padding:
|
|
34
|
-
font-size:
|
|
35
|
-
font-weight:
|
|
51
|
+
padding: 10px 8px;
|
|
52
|
+
font-size: 12px;
|
|
53
|
+
font-weight: 800;
|
|
54
|
+
font-family: var(--font-mono);
|
|
36
55
|
text-transform: uppercase;
|
|
37
|
-
letter-spacing: 0.
|
|
38
|
-
color: var(--color-
|
|
39
|
-
background: var(--color-
|
|
40
|
-
border-bottom:
|
|
41
|
-
border-right: 1px solid
|
|
56
|
+
letter-spacing: 0.8px;
|
|
57
|
+
color: var(--color-bg);
|
|
58
|
+
background: var(--color-accent);
|
|
59
|
+
border-bottom: 2px solid var(--color-border);
|
|
60
|
+
border-right: 1px solid rgba(0, 0, 0, 0.08);
|
|
42
61
|
white-space: nowrap;
|
|
43
62
|
user-select: none;
|
|
44
63
|
position: sticky;
|
|
45
64
|
top: 0;
|
|
46
65
|
z-index: 10;
|
|
47
66
|
text-align: center;
|
|
67
|
+
transition: all 120ms var(--ease-out);
|
|
48
68
|
/* 📖 Disable user-select on the whole header so dragging the resize handle
|
|
49
69
|
📖 never accidentally selects the cell text on rapid drags. */
|
|
50
70
|
-webkit-user-select: none;
|
|
@@ -52,7 +72,7 @@
|
|
|
52
72
|
-ms-user-select: none;
|
|
53
73
|
}
|
|
54
74
|
.th:last-child { border-right: 0; }
|
|
55
|
-
.th:hover { color: var(--color-
|
|
75
|
+
.th:hover { color: var(--color-bg); background: var(--color-accent-hover); }
|
|
56
76
|
|
|
57
77
|
/* 📖 Inner flex container: header label on the left, resize handle on the right.
|
|
58
78
|
📖 Letting the label shrink keeps the column tight when the user makes it narrow. */
|
|
@@ -120,8 +140,10 @@
|
|
|
120
140
|
user-select: none !important;
|
|
121
141
|
}
|
|
122
142
|
|
|
123
|
-
/* 📖 Floating
|
|
124
|
-
📖
|
|
143
|
+
/* 📖 Floating bar shown only when the user has custom column widths.
|
|
144
|
+
📖 Rendered OUTSIDE the scroller (a normal flex child at the top of the
|
|
145
|
+
📖 table container) so it stays put without colliding with the sticky
|
|
146
|
+
📖 <thead>. It shrinks the scroll area; the sticky header sticks below it. */
|
|
125
147
|
.resizeToolbar {
|
|
126
148
|
display: flex;
|
|
127
149
|
align-items: center;
|
|
@@ -130,11 +152,9 @@
|
|
|
130
152
|
padding: 6px 10px;
|
|
131
153
|
background: var(--color-bg-elevated);
|
|
132
154
|
border-bottom: 1px solid var(--color-border);
|
|
133
|
-
font-size:
|
|
155
|
+
font-size: 13px;
|
|
134
156
|
color: var(--color-text-muted);
|
|
135
|
-
|
|
136
|
-
top: 0;
|
|
137
|
-
z-index: 11;
|
|
157
|
+
flex-shrink: 0;
|
|
138
158
|
}
|
|
139
159
|
.resizeToolbarHint {
|
|
140
160
|
display: inline-flex;
|
|
@@ -154,7 +174,7 @@
|
|
|
154
174
|
color: var(--color-text-muted);
|
|
155
175
|
padding: 3px 10px;
|
|
156
176
|
border-radius: 4px;
|
|
157
|
-
font-size:
|
|
177
|
+
font-size: 13px;
|
|
158
178
|
font-weight: 600;
|
|
159
179
|
cursor: pointer;
|
|
160
180
|
transition: background 120ms var(--ease-out, ease), color 120ms var(--ease-out, ease), border-color 120ms var(--ease-out, ease);
|
|
@@ -176,8 +196,8 @@
|
|
|
176
196
|
.th:nth-child(11) { text-align: left; } /* Verdict */
|
|
177
197
|
|
|
178
198
|
/* Sort icons */
|
|
179
|
-
.sortIcon { opacity: 0.
|
|
180
|
-
.sortIconActive { color: var(--color-
|
|
199
|
+
.sortIcon { opacity: 0.4; margin-left: 3px; font-size: 9px; color: var(--color-bg); }
|
|
200
|
+
.sortIconActive { color: var(--color-bg); margin-left: 3px; font-size: 10px; font-weight: 900; }
|
|
181
201
|
|
|
182
202
|
/* ─── Data cells ─── */
|
|
183
203
|
.td {
|
|
@@ -191,6 +211,11 @@
|
|
|
191
211
|
.td:nth-child(7) { text-align: left; } /* Provider */
|
|
192
212
|
.td:nth-child(11) { text-align: left; } /* Verdict */
|
|
193
213
|
|
|
214
|
+
/* 📖 Center the BOOKMARKS column (star button) so it sits nicely in its
|
|
215
|
+
📖 wider header. Other narrow numeric columns are centered by default. */
|
|
216
|
+
.td:nth-child(1),
|
|
217
|
+
.th:nth-child(1) { text-align: center; }
|
|
218
|
+
|
|
194
219
|
/* ─── Row hover ─── */
|
|
195
220
|
.table tbody tr {
|
|
196
221
|
cursor: pointer;
|
|
@@ -206,7 +231,7 @@
|
|
|
206
231
|
/* ─── Shared cell values ─── */
|
|
207
232
|
.rankNum {
|
|
208
233
|
font-family: var(--font-mono);
|
|
209
|
-
font-size:
|
|
234
|
+
font-size: 13px;
|
|
210
235
|
color: var(--color-text-dim);
|
|
211
236
|
text-align: center;
|
|
212
237
|
display: block;
|
|
@@ -217,7 +242,7 @@
|
|
|
217
242
|
.sweMid { color: #3ddc84; }
|
|
218
243
|
.sweLow { color: var(--color-text-dim); }
|
|
219
244
|
|
|
220
|
-
.ctx { font-family: var(--font-mono); font-size:
|
|
245
|
+
.ctx { font-family: var(--font-mono); font-size: 13px; }
|
|
221
246
|
|
|
222
247
|
.ping { font-family: var(--font-mono); font-weight: 600; }
|
|
223
248
|
.pingFast { color: #00ff88; }
|
|
@@ -232,14 +257,14 @@
|
|
|
232
257
|
:global([data-theme="light"]) .pingMedium { color: #b86b00; }
|
|
233
258
|
:global([data-theme="light"]) .pingSlow { color: #c8143a; }
|
|
234
259
|
|
|
235
|
-
.uptime { font-family: var(--font-mono); font-size:
|
|
260
|
+
.uptime { font-family: var(--font-mono); font-size: 13px; }
|
|
236
261
|
|
|
237
262
|
/* ─── Model cell ─── */
|
|
238
263
|
.modelCell { display: flex; align-items: center; gap: 6px; overflow: hidden; }
|
|
239
264
|
.modelMeta { flex: 1; min-width: 0; }
|
|
240
265
|
.modelHeader { display: flex; align-items: center; gap: 5px; }
|
|
241
|
-
.modelName { font-weight: 600; font-size:
|
|
242
|
-
.modelId { font-family: var(--font-mono); font-size:
|
|
266
|
+
.modelName { font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
267
|
+
.modelId { font-family: var(--font-mono); font-size: 11px; color: var(--color-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
243
268
|
|
|
244
269
|
/* 📖 Provider cell now uses <ProviderLogo /> (icon + wordmark) — see
|
|
245
270
|
📖 atoms/ProviderLogo.module.css for the layout. The old bordered pill
|
|
@@ -368,13 +393,43 @@
|
|
|
368
393
|
}
|
|
369
394
|
|
|
370
395
|
/* ─── Expandable row styles ─── */
|
|
371
|
-
.expandedRow {
|
|
372
|
-
background: var(--color-
|
|
396
|
+
.expandedRow td {
|
|
397
|
+
background: var(--color-accent) !important;
|
|
398
|
+
color: var(--color-bg) !important;
|
|
399
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.15) !important;
|
|
400
|
+
}
|
|
401
|
+
.expandedRow td * {
|
|
402
|
+
color: var(--color-bg) !important;
|
|
403
|
+
}
|
|
404
|
+
.expandedRow :global(.status-dot) {
|
|
405
|
+
border-color: var(--color-bg) !important;
|
|
406
|
+
}
|
|
407
|
+
.expandedRow :global(.provider-wordmark) {
|
|
408
|
+
filter: brightness(0) saturate(100%) !important; /* Forces logos to render dark/black for contrast */
|
|
409
|
+
}
|
|
410
|
+
.expandedRow .rankNum,
|
|
411
|
+
.expandedRow .modelId {
|
|
412
|
+
color: rgba(0, 0, 0, 0.6) !important;
|
|
413
|
+
}
|
|
414
|
+
.expandedRow .sweLow,
|
|
415
|
+
.expandedRow .sweMid,
|
|
416
|
+
.expandedRow .sweHigh {
|
|
417
|
+
color: var(--color-bg) !important;
|
|
418
|
+
}
|
|
419
|
+
.expandedRow .pingFast,
|
|
420
|
+
.expandedRow .pingMedium,
|
|
421
|
+
.expandedRow .pingSlow {
|
|
422
|
+
color: var(--color-bg) !important;
|
|
423
|
+
font-weight: 800;
|
|
373
424
|
}
|
|
374
425
|
.expandRowWrapper {
|
|
375
426
|
border: none;
|
|
376
427
|
}
|
|
377
428
|
.expandRowCell {
|
|
378
429
|
padding: 0 !important;
|
|
379
|
-
border-bottom:
|
|
430
|
+
border-bottom: 3px solid var(--color-accent) !important;
|
|
431
|
+
background: var(--color-bg-elevated) !important;
|
|
432
|
+
}
|
|
433
|
+
.expandRowCell * {
|
|
434
|
+
color: inherit;
|
|
380
435
|
}
|
|
@@ -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: 820px;
|
|
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 {
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
.overlay {
|
|
2
|
-
position: fixed;
|
|
3
|
-
inset: 0;
|
|
4
|
-
z-index: 200;
|
|
5
|
-
background: rgba(0, 0, 0, 0.6);
|
|
6
2
|
display: flex;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
flex: 1;
|
|
5
|
+
min-height: 0;
|
|
6
|
+
width: 100%;
|
|
7
|
+
padding: 24px;
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
.modal {
|
|
13
|
-
background: var(--bg-
|
|
14
|
-
border: 1px solid var(--border, #
|
|
11
|
+
background: var(--color-bg-card, #080908);
|
|
12
|
+
border: 1px solid var(--color-border, #161a16);
|
|
15
13
|
border-radius: 12px;
|
|
16
|
-
width:
|
|
17
|
-
max-width:
|
|
18
|
-
|
|
14
|
+
width: 100%;
|
|
15
|
+
max-width: 680px;
|
|
16
|
+
margin: 0 auto;
|
|
17
|
+
flex: 1;
|
|
19
18
|
display: flex;
|
|
20
19
|
flex-direction: column;
|
|
21
|
-
box-shadow: 0
|
|
20
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
21
|
+
min-height: 0;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
.header {
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
.overlay {
|
|
2
|
-
position: fixed;
|
|
3
|
-
inset: 0;
|
|
4
|
-
z-index: 200;
|
|
5
|
-
background: rgba(0, 0, 0, 0.6);
|
|
6
2
|
display: flex;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
flex: 1;
|
|
5
|
+
min-height: 0;
|
|
6
|
+
width: 100%;
|
|
7
|
+
padding: 24px;
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
.modal {
|
|
13
|
-
background: var(--bg-
|
|
14
|
-
border: 1px solid var(--border, #
|
|
11
|
+
background: var(--color-bg-card, #080908);
|
|
12
|
+
border: 1px solid var(--color-border, #161a16);
|
|
15
13
|
border-radius: 12px;
|
|
16
|
-
width:
|
|
17
|
-
max-width:
|
|
18
|
-
|
|
14
|
+
width: 100%;
|
|
15
|
+
max-width: 640px;
|
|
16
|
+
margin: 0 auto;
|
|
17
|
+
flex: 1;
|
|
19
18
|
display: flex;
|
|
20
19
|
flex-direction: column;
|
|
21
|
-
box-shadow: 0
|
|
20
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
21
|
+
min-height: 0;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
.header {
|
|
@@ -22,20 +22,18 @@ import styles from './Header.module.css'
|
|
|
22
22
|
// 📖 order, icon, and "coming soon" milestone are colocated with the
|
|
23
23
|
// 📖 rendering code. When a view ships, remove the `comingIn` field.
|
|
24
24
|
const NAV_ITEMS = [
|
|
25
|
-
{ id: 'dashboard',
|
|
26
|
-
{ id: '
|
|
27
|
-
{ id: '
|
|
28
|
-
{ id: '
|
|
29
|
-
{ id: '
|
|
30
|
-
{ id: 'playground', label: 'Playground', icon: IconMessageChatbot },
|
|
25
|
+
{ id: 'dashboard', label: 'Dashboard', icon: IconLayoutDashboard },
|
|
26
|
+
{ id: 'router', label: 'Router', icon: IconRoute },
|
|
27
|
+
{ id: 'playground', label: 'Playground', icon: IconMessageChatbot },
|
|
28
|
+
{ id: 'help', label: 'Help', icon: IconQuestionMark },
|
|
29
|
+
{ id: 'install-endpoints', label: 'Install Endpoints', icon: IconPlug },
|
|
31
30
|
]
|
|
32
31
|
|
|
33
|
-
// 📖 Overflow menu items
|
|
34
|
-
// 📖 and Installed Models are still M4.
|
|
32
|
+
// 📖 Overflow menu items
|
|
35
33
|
const MENU_ITEMS = [
|
|
36
|
-
{ id: '
|
|
34
|
+
{ id: 'analytics', label: 'Analytics', icon: IconActivity },
|
|
35
|
+
{ id: 'recommend', label: 'Recommend', icon: IconSparkles },
|
|
37
36
|
{ id: 'changelog', label: 'Changelog', icon: IconHistory },
|
|
38
|
-
{ id: 'install-endpoints', label: 'Install Endpoints', icon: IconPlug },
|
|
39
37
|
{ id: 'installed-models', label: 'Installed Models', icon: IconFolders },
|
|
40
38
|
]
|
|
41
39
|
|
|
@@ -90,8 +88,11 @@ export default function Header({
|
|
|
90
88
|
return (
|
|
91
89
|
<header className={styles.header}>
|
|
92
90
|
<div className={styles.left}>
|
|
93
|
-
<div className={styles.logo}>
|
|
94
|
-
<
|
|
91
|
+
<div className={styles.logo} onClick={() => onNavigate('dashboard')} style={{ cursor: 'pointer' }}>
|
|
92
|
+
<svg className={styles.logoIconSvg} width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
93
|
+
<path d="M4 5L11 12L4 19" stroke="var(--color-brand)" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
|
|
94
|
+
<path d="M14 5V19M14 5H21M14 11H18" stroke="var(--color-brand)" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
|
|
95
|
+
</svg>
|
|
95
96
|
<span className={styles.logoText}>
|
|
96
97
|
<span className={styles.logoTextHighlight}>free</span>
|
|
97
98
|
<span>-coding-models</span>
|
|
@@ -168,7 +169,7 @@ export default function Header({
|
|
|
168
169
|
{/* Overflow menu (kebab) — hidden features & occasional flows */}
|
|
169
170
|
<div className={styles.menuWrap} ref={menuRef}>
|
|
170
171
|
<button
|
|
171
|
-
className={`${styles.navBtn} ${styles.menuTrigger}`}
|
|
172
|
+
className={`${styles.navBtn} ${styles.menuTrigger} ${MENU_ITEMS.some((m) => m.id === currentView) ? styles.navBtnActive : ''}`}
|
|
172
173
|
onClick={() => setMenuOpen((o) => !o)}
|
|
173
174
|
title="More features"
|
|
174
175
|
aria-label="More features"
|
|
@@ -181,12 +182,14 @@ export default function Header({
|
|
|
181
182
|
<div className={styles.menuPopover} role="menu">
|
|
182
183
|
{MENU_ITEMS.map((item) => {
|
|
183
184
|
const Icon = item.icon
|
|
185
|
+
const isActive = currentView === item.id
|
|
184
186
|
return (
|
|
185
187
|
<button
|
|
186
188
|
key={item.id}
|
|
187
|
-
className={styles.menuItem}
|
|
189
|
+
className={`${styles.menuItem} ${isActive ? styles.menuItemActive : ''}`}
|
|
188
190
|
onClick={() => handleMenuClick(item)}
|
|
189
191
|
role="menuitem"
|
|
192
|
+
aria-current={isActive ? 'page' : undefined}
|
|
190
193
|
>
|
|
191
194
|
<Icon size={14} stroke={1.5} />
|
|
192
195
|
<span>{item.label}</span>
|
|
@@ -200,21 +203,18 @@ export default function Header({
|
|
|
200
203
|
</nav>
|
|
201
204
|
</div>
|
|
202
205
|
|
|
203
|
-
<div className={styles.center}>
|
|
204
|
-
<div className={styles.searchBar}>
|
|
205
|
-
<span className={styles.searchIcon}><IconSearch size={16} stroke={1.5} /></span>
|
|
206
|
-
<input
|
|
207
|
-
type="text"
|
|
208
|
-
className={styles.searchInput}
|
|
209
|
-
placeholder="Search models, providers, tiers..."
|
|
210
|
-
value={searchQuery}
|
|
211
|
-
onChange={(e) => onSearchChange(e.target.value)}
|
|
212
|
-
autoComplete="off"
|
|
213
|
-
/>
|
|
214
|
-
</div>
|
|
215
|
-
</div>
|
|
216
|
-
|
|
217
206
|
<div className={styles.right}>
|
|
207
|
+
<button
|
|
208
|
+
className={`${styles.navBtn} ${currentView === 'settings' ? styles.navBtnActive : ''}`}
|
|
209
|
+
onClick={() => onNavigate('settings')}
|
|
210
|
+
title="Settings"
|
|
211
|
+
aria-current={currentView === 'settings' ? 'page' : undefined}
|
|
212
|
+
style={{ marginRight: '2px' }}
|
|
213
|
+
>
|
|
214
|
+
<IconSettings size={14} stroke={1.5} />
|
|
215
|
+
<span>Settings</span>
|
|
216
|
+
</button>
|
|
217
|
+
|
|
218
218
|
<ToolPicker
|
|
219
219
|
toolMode={toolMode}
|
|
220
220
|
onSetToolMode={onSetToolMode}
|