free-coding-models 0.5.31 → 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/bin/free-coding-models.js +22 -2
- package/changelog/v0.5.32.md +4 -0
- 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 +64 -50
- 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-CsFt3qt5.css +0 -1
- package/web/dist/assets/index-D2n0DqkV.js +0 -40
|
@@ -46,24 +46,24 @@
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/* ─── Brand ─── */
|
|
49
|
-
.logo { display: flex; align-items: center; gap:
|
|
50
|
-
.
|
|
49
|
+
.logo { display: flex; align-items: center; gap: 8px; }
|
|
50
|
+
.logoIconSvg {
|
|
51
51
|
color: var(--color-brand);
|
|
52
|
-
font-family: var(--font-mono);
|
|
53
|
-
font-size: 20px;
|
|
54
|
-
font-weight: 900;
|
|
55
52
|
display: flex;
|
|
56
53
|
align-items: center;
|
|
54
|
+
flex-shrink: 0;
|
|
57
55
|
}
|
|
58
56
|
.logoText {
|
|
59
|
-
font-size:
|
|
57
|
+
font-size: 16px; font-weight: 750;
|
|
58
|
+
font-family: var(--font-mono);
|
|
60
59
|
color: var(--color-text);
|
|
61
60
|
display: flex;
|
|
62
61
|
align-items: center;
|
|
62
|
+
letter-spacing: -0.2px;
|
|
63
63
|
}
|
|
64
64
|
.logoTextHighlight { color: var(--color-brand); }
|
|
65
65
|
.version {
|
|
66
|
-
font-size:
|
|
66
|
+
font-size: 12px; font-weight: 500; font-family: var(--font-mono);
|
|
67
67
|
color: var(--color-text-muted); background: var(--color-surface);
|
|
68
68
|
padding: 2px 6px; border-radius: 999px; border: 1px solid var(--color-border);
|
|
69
69
|
}
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
.nav {
|
|
73
73
|
display: flex;
|
|
74
74
|
align-items: center;
|
|
75
|
-
gap:
|
|
75
|
+
gap: 8px;
|
|
76
76
|
margin-left: 8px;
|
|
77
77
|
padding-left: 12px;
|
|
78
78
|
border-left: 1px solid var(--color-border);
|
|
@@ -82,39 +82,44 @@
|
|
|
82
82
|
.navBtn {
|
|
83
83
|
display: inline-flex;
|
|
84
84
|
align-items: center;
|
|
85
|
-
gap:
|
|
85
|
+
gap: 6px;
|
|
86
86
|
height: 30px;
|
|
87
|
-
padding: 0
|
|
88
|
-
font-size:
|
|
87
|
+
padding: 0 12px;
|
|
88
|
+
font-size: 13px;
|
|
89
89
|
font-weight: 600;
|
|
90
|
-
font-family: var(--font-
|
|
90
|
+
font-family: var(--font-mono); /* Monospace font looks extremely high-tech */
|
|
91
|
+
text-transform: uppercase;
|
|
92
|
+
letter-spacing: 0.5px;
|
|
91
93
|
color: var(--color-text-muted);
|
|
92
|
-
background:
|
|
93
|
-
border: 1px solid
|
|
94
|
+
background: var(--color-surface);
|
|
95
|
+
border: 1px solid var(--color-border);
|
|
94
96
|
border-radius: 6px;
|
|
95
97
|
cursor: pointer;
|
|
96
|
-
transition:
|
|
98
|
+
transition: all 150ms var(--ease-out);
|
|
97
99
|
white-space: nowrap;
|
|
98
100
|
}
|
|
99
101
|
.navBtn:hover {
|
|
100
102
|
background: var(--color-bg-hover);
|
|
101
103
|
color: var(--color-text);
|
|
104
|
+
border-color: var(--color-border-hover);
|
|
102
105
|
}
|
|
103
106
|
.navBtnActive {
|
|
104
|
-
background: var(--color-accent
|
|
105
|
-
color: var(--color-
|
|
106
|
-
border-color: var(--color-
|
|
107
|
-
font-weight:
|
|
107
|
+
background: var(--color-accent);
|
|
108
|
+
color: var(--color-bg);
|
|
109
|
+
border-color: var(--color-accent);
|
|
110
|
+
font-weight: 800;
|
|
111
|
+
box-shadow: 0 0 12px var(--color-accent-glow);
|
|
108
112
|
}
|
|
109
113
|
.navBtnActive:hover {
|
|
110
|
-
background: var(--color-accent-
|
|
111
|
-
color: var(--color-
|
|
114
|
+
background: var(--color-accent-hover);
|
|
115
|
+
color: var(--color-bg);
|
|
116
|
+
border-color: var(--color-accent-hover);
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
/* 📖 "Coming in M2" badge — small inline label that shows the menu structure
|
|
115
120
|
📖 is honest even before those milestones land. */
|
|
116
121
|
.comingBadge {
|
|
117
|
-
font-size:
|
|
122
|
+
font-size: 10px;
|
|
118
123
|
font-weight: 700;
|
|
119
124
|
font-family: var(--font-mono);
|
|
120
125
|
color: var(--color-text-dim);
|
|
@@ -156,7 +161,7 @@
|
|
|
156
161
|
gap: 8px;
|
|
157
162
|
width: 100%;
|
|
158
163
|
padding: 7px 10px;
|
|
159
|
-
font-size:
|
|
164
|
+
font-size: 14px;
|
|
160
165
|
font-weight: 500;
|
|
161
166
|
color: var(--color-text);
|
|
162
167
|
background: transparent;
|
|
@@ -167,6 +172,11 @@
|
|
|
167
172
|
transition: background 100ms;
|
|
168
173
|
}
|
|
169
174
|
.menuItem:hover { background: var(--color-bg-hover); }
|
|
175
|
+
.menuItemActive {
|
|
176
|
+
background: var(--color-accent-dim) !important;
|
|
177
|
+
color: var(--color-accent) !important;
|
|
178
|
+
font-weight: 700;
|
|
179
|
+
}
|
|
170
180
|
.menuItem > span:first-of-type { flex: 1; }
|
|
171
181
|
|
|
172
182
|
/* ─── Search ─── */
|
|
@@ -180,10 +190,10 @@
|
|
|
180
190
|
border-color: var(--color-accent);
|
|
181
191
|
box-shadow: 0 0 0 3px var(--color-accent-glow);
|
|
182
192
|
}
|
|
183
|
-
.searchIcon { font-size:
|
|
193
|
+
.searchIcon { font-size: 16px; flex-shrink: 0; }
|
|
184
194
|
.searchInput {
|
|
185
195
|
flex: 1; background: none; border: none; outline: none;
|
|
186
|
-
color: var(--color-text); font-size:
|
|
196
|
+
color: var(--color-text); font-size: 15px; font-family: var(--font-sans);
|
|
187
197
|
min-width: 0;
|
|
188
198
|
}
|
|
189
199
|
.searchInput::placeholder { color: var(--color-text-dim); }
|
|
@@ -193,7 +203,7 @@
|
|
|
193
203
|
display: inline-flex; align-items: center; justify-content: center;
|
|
194
204
|
padding: 6px 8px; border: 1px solid var(--color-border); border-radius: 6px;
|
|
195
205
|
background: var(--color-surface); color: var(--color-text);
|
|
196
|
-
cursor: pointer; font-size:
|
|
206
|
+
cursor: pointer; font-size: 18px; transition: all 150ms;
|
|
197
207
|
}
|
|
198
208
|
.iconBtn:hover { background: var(--color-bg-hover); border-color: var(--color-border-hover); }
|
|
199
209
|
|
|
@@ -204,7 +214,7 @@
|
|
|
204
214
|
padding: 0 10px; height: 30px;
|
|
205
215
|
border: 1px solid var(--color-border); border-radius: 6px;
|
|
206
216
|
background: var(--color-surface); color: var(--color-text);
|
|
207
|
-
cursor: pointer; font-size:
|
|
217
|
+
cursor: pointer; font-size: 14px; font-weight: 600;
|
|
208
218
|
font-family: var(--font-mono);
|
|
209
219
|
transition: all 150ms;
|
|
210
220
|
}
|
|
@@ -212,12 +222,12 @@
|
|
|
212
222
|
border-color: var(--color-accent);
|
|
213
223
|
background: var(--color-bg-hover);
|
|
214
224
|
}
|
|
215
|
-
.cmdkLabel { font-size:
|
|
225
|
+
.cmdkLabel { font-size: 13px; letter-spacing: 0.5px; }
|
|
216
226
|
|
|
217
227
|
/* ─── AI Latency benchmark button (reused from before) ─── */
|
|
218
228
|
.benchmarkBtn {
|
|
219
229
|
display: inline-flex; align-items: center; gap: 6px;
|
|
220
|
-
padding: 0 12px; height: 30px; font-size:
|
|
230
|
+
padding: 0 12px; height: 30px; font-size: 14px; font-weight: 700;
|
|
221
231
|
border: 1px solid var(--color-border); border-radius: 6px;
|
|
222
232
|
background: var(--color-surface); color: var(--color-text);
|
|
223
233
|
cursor: pointer; font-family: var(--font-mono); transition: all 150ms;
|
|
@@ -307,7 +317,7 @@
|
|
|
307
317
|
align-items: center;
|
|
308
318
|
gap: 10px;
|
|
309
319
|
padding: 10px 12px;
|
|
310
|
-
font-size:
|
|
320
|
+
font-size: 15px;
|
|
311
321
|
font-weight: 600;
|
|
312
322
|
color: var(--color-text-muted);
|
|
313
323
|
background: transparent;
|
|
@@ -329,3 +339,19 @@
|
|
|
329
339
|
background: var(--color-border);
|
|
330
340
|
margin: 6px 0;
|
|
331
341
|
}
|
|
342
|
+
|
|
343
|
+
/* ─── Keyboard Focus & Micro-interaction Active States ─── */
|
|
344
|
+
.navBtn:focus-visible,
|
|
345
|
+
.iconBtn:focus-visible,
|
|
346
|
+
.cmdkBtn:focus-visible,
|
|
347
|
+
.benchmarkBtn:focus-visible {
|
|
348
|
+
outline: 2px solid var(--color-accent);
|
|
349
|
+
outline-offset: 2px;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.navBtn:active,
|
|
353
|
+
.iconBtn:active,
|
|
354
|
+
.cmdkBtn:active,
|
|
355
|
+
.benchmarkBtn:active {
|
|
356
|
+
transform: scale(0.96);
|
|
357
|
+
}
|
|
@@ -37,14 +37,14 @@ const PING_MODE_CYCLE = ['speed', 'normal', 'slow', 'forced']
|
|
|
37
37
|
// 📖 has a few commands the TUI doesn't). They are appended to the TUI registry
|
|
38
38
|
// 📖 after a `Web` separator so the two surfaces stay easy to compare.
|
|
39
39
|
function buildWebEntries(deps) {
|
|
40
|
-
const { onCycleTheme, onResetView, onSetPingMode,
|
|
40
|
+
const { onCycleTheme, onResetView, onSetPingMode, theme, pingMode, modelsCount, onExport, onNavigate } = deps
|
|
41
41
|
const web = [
|
|
42
|
-
// Pages
|
|
43
|
-
{ id: 'page.help', section: 'page', label: 'Open Help', keywords: ['help', 'shortcuts', 'reference'], run: () =>
|
|
44
|
-
{ id: 'page.changelog', section: 'page', label: 'Open Changelog', keywords: ['changelog', 'release', 'history', 'version'], run: () =>
|
|
45
|
-
{ id: 'page.playground', section: 'page', label: 'Open Playground', keywords: ['playground', 'chat', 'try', 'prompt', 'router'], run: () =>
|
|
46
|
-
{ id: 'page.install-endpoints', section: 'page', label: 'Open Install Endpoints
|
|
47
|
-
{ id: 'page.installed-models', section: 'page', label: 'Open Installed Models
|
|
42
|
+
// Pages
|
|
43
|
+
{ id: 'page.help', section: 'page', label: 'Open Help', keywords: ['help', 'shortcuts', 'reference'], run: () => onNavigate?.('help') },
|
|
44
|
+
{ id: 'page.changelog', section: 'page', label: 'Open Changelog', keywords: ['changelog', 'release', 'history', 'version'], run: () => onNavigate?.('changelog') },
|
|
45
|
+
{ id: 'page.playground', section: 'page', label: 'Open Playground', keywords: ['playground', 'chat', 'try', 'prompt', 'router'], run: () => onNavigate?.('playground') },
|
|
46
|
+
{ id: 'page.install-endpoints', section: 'page', label: 'Open Install Endpoints', keywords: ['install', 'endpoint', 'tool', 'configure'], run: () => onNavigate?.('install-endpoints') },
|
|
47
|
+
{ id: 'page.installed-models', section: 'page', label: 'Open Installed Models', keywords: ['installed', 'models', 'tools'], run: () => onNavigate?.('installed-models') },
|
|
48
48
|
|
|
49
49
|
// Theme
|
|
50
50
|
{ id: 'action.theme.cycle', section: 'action', label: `Cycle theme (current: ${theme})`, keywords: ['theme', 'dark', 'light', 'auto'], run: onCycleTheme },
|
|
@@ -92,7 +92,7 @@ export default function CommandPalette({
|
|
|
92
92
|
})
|
|
93
93
|
const webEntries = buildWebEntries({
|
|
94
94
|
onCycleTheme, onResetView, onSetPingMode,
|
|
95
|
-
|
|
95
|
+
theme, pingMode, modelsCount: models?.length ?? 0, onExport, onNavigate,
|
|
96
96
|
})
|
|
97
97
|
// 📖 Update banner entry (only when an update is available) — mirrors
|
|
98
98
|
// 📖 the TUI palette's "auto-prepended when newer version known" rule.
|
|
@@ -177,12 +177,12 @@ export default function CommandPalette({
|
|
|
177
177
|
// 📖 ships in M3), we route the user to the right header menu.
|
|
178
178
|
const id = item.id
|
|
179
179
|
if (id === 'open-settings') { onNavigate?.('settings'); onClose(); return }
|
|
180
|
-
if (id === 'open-help') {
|
|
181
|
-
if (id === 'open-changelog') {
|
|
182
|
-
if (id === 'open-recommend') {
|
|
183
|
-
if (id === 'open-router-dashboard') {
|
|
184
|
-
if (id === 'open-installed-models') {
|
|
185
|
-
if (id === 'open-install-endpoints') {
|
|
180
|
+
if (id === 'open-help') { onNavigate?.('help'); onClose(); return }
|
|
181
|
+
if (id === 'open-changelog') { onNavigate?.('changelog'); onClose(); return }
|
|
182
|
+
if (id === 'open-recommend') { onNavigate?.('recommend'); onClose(); return }
|
|
183
|
+
if (id === 'open-router-dashboard') { onNavigate?.('router'); onClose(); return }
|
|
184
|
+
if (id === 'open-installed-models') { onNavigate?.('installed-models'); onClose(); return }
|
|
185
|
+
if (id === 'open-install-endpoints') { onNavigate?.('install-endpoints'); onClose(); return }
|
|
186
186
|
if (id === 'action-update-now') { onRunUpdate?.(); onClose(); return }
|
|
187
187
|
if (id === 'action-cycle-theme') { onCycleTheme?.(); onClose(); return }
|
|
188
188
|
if (id === 'action-reset-view') { onResetView?.(); onClose(); return }
|
|
@@ -5,27 +5,26 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
.overlay {
|
|
8
|
-
position: fixed;
|
|
9
|
-
inset: 0;
|
|
10
|
-
z-index: 200;
|
|
11
|
-
background: rgba(0, 0, 0, 0.6);
|
|
12
8
|
display: flex;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
flex: 1;
|
|
11
|
+
min-height: 0;
|
|
12
|
+
width: 100%;
|
|
13
|
+
padding: 24px;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
.modal {
|
|
19
|
-
background: var(--bg-
|
|
20
|
-
border: 1px solid var(--border, #
|
|
17
|
+
background: var(--color-bg-card, #080908);
|
|
18
|
+
border: 1px solid var(--color-border, #161a16);
|
|
21
19
|
border-radius: 12px;
|
|
22
|
-
width:
|
|
23
|
-
max-width:
|
|
24
|
-
|
|
20
|
+
width: 100%;
|
|
21
|
+
max-width: 1000px;
|
|
22
|
+
margin: 0 auto;
|
|
23
|
+
flex: 1;
|
|
25
24
|
display: flex;
|
|
26
25
|
flex-direction: column;
|
|
27
|
-
box-shadow: 0 20px
|
|
28
|
-
|
|
26
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
27
|
+
min-height: 0;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
@keyframes fadeIn {
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
/** @file web/src/components/recommend/RecommendView.module.css */
|
|
2
2
|
.backdrop {
|
|
3
|
-
position: fixed;
|
|
4
|
-
inset: 0;
|
|
5
|
-
z-index: 480;
|
|
6
3
|
display: flex;
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
flex: 1;
|
|
6
|
+
min-height: 0;
|
|
7
|
+
width: 100%;
|
|
9
8
|
padding: 24px;
|
|
10
|
-
background: rgba(0, 0, 0, .64);
|
|
11
|
-
backdrop-filter: blur(4px);
|
|
12
9
|
}
|
|
13
10
|
.modal {
|
|
14
|
-
width:
|
|
15
|
-
max-
|
|
11
|
+
width: 100%;
|
|
12
|
+
max-width: 980px;
|
|
13
|
+
margin: 0 auto;
|
|
16
14
|
overflow: auto;
|
|
17
15
|
border: 1px solid var(--color-border);
|
|
18
16
|
border-radius: 16px;
|
|
19
|
-
background: var(--color-bg-
|
|
17
|
+
background: var(--color-bg-card, #080908);
|
|
20
18
|
color: var(--color-text);
|
|
21
|
-
box-shadow: 0 20px
|
|
19
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
20
|
+
flex: 1;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
min-height: 0;
|
|
22
24
|
}
|
|
23
25
|
.header,
|
|
24
26
|
.footer { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 20px 24px; border-bottom: 1px solid var(--color-border); }
|
|
@@ -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: 1000px;
|
|
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 {
|
|
@@ -8,33 +8,46 @@
|
|
|
8
8
|
align-items: center;
|
|
9
9
|
gap: 4px;
|
|
10
10
|
}
|
|
11
|
-
.trigger
|
|
12
|
-
.cycle {
|
|
11
|
+
.trigger {
|
|
13
12
|
height: 30px;
|
|
14
13
|
display: inline-flex;
|
|
15
14
|
align-items: center;
|
|
16
15
|
justify-content: center;
|
|
17
|
-
border: 1px solid var(--color-border);
|
|
18
|
-
background: var(--color-surface);
|
|
19
|
-
color: var(--color-text);
|
|
20
|
-
cursor: pointer;
|
|
21
|
-
transition: background 150ms, border-color 150ms, color 150ms;
|
|
22
|
-
}
|
|
23
|
-
.trigger {
|
|
24
16
|
gap: 6px;
|
|
25
|
-
padding: 0
|
|
17
|
+
padding: 0 12px;
|
|
26
18
|
border-radius: 6px;
|
|
27
|
-
font-size:
|
|
28
|
-
font-weight:
|
|
19
|
+
font-size: 11px;
|
|
20
|
+
font-weight: 800;
|
|
21
|
+
font-family: var(--font-mono);
|
|
22
|
+
text-transform: uppercase;
|
|
23
|
+
letter-spacing: 0.5px;
|
|
24
|
+
background: var(--color-accent);
|
|
25
|
+
color: var(--color-bg);
|
|
26
|
+
border: 1px solid var(--color-accent);
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
transition: all 150ms var(--ease-out);
|
|
29
29
|
}
|
|
30
30
|
.cycle {
|
|
31
|
+
height: 30px;
|
|
31
32
|
width: 30px;
|
|
33
|
+
display: inline-flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
justify-content: center;
|
|
36
|
+
border: 1px solid var(--color-border);
|
|
37
|
+
background: var(--color-surface);
|
|
38
|
+
color: var(--color-text);
|
|
32
39
|
border-radius: 6px;
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
transition: all 150ms var(--ease-out);
|
|
42
|
+
}
|
|
43
|
+
.trigger:hover {
|
|
44
|
+
background: var(--color-accent-hover);
|
|
45
|
+
border-color: var(--color-accent-hover);
|
|
46
|
+
color: var(--color-bg);
|
|
33
47
|
}
|
|
34
|
-
.trigger:hover,
|
|
35
48
|
.cycle:hover {
|
|
36
49
|
background: var(--color-bg-hover);
|
|
37
|
-
border-color: var(--color-
|
|
50
|
+
border-color: var(--color-border-hover);
|
|
38
51
|
color: var(--color-accent);
|
|
39
52
|
}
|
|
40
53
|
.emoji { font-size: 14px; line-height: 1; }
|
package/web/src/global.css
CHANGED
|
@@ -8,24 +8,24 @@
|
|
|
8
8
|
|
|
9
9
|
/* ─── CSS Custom Properties (Design Tokens) ─── */
|
|
10
10
|
:root {
|
|
11
|
-
--color-brand: #
|
|
12
|
-
--color-bg: #
|
|
13
|
-
--color-bg-elevated: #
|
|
14
|
-
--color-bg-card: #
|
|
15
|
-
--color-bg-hover: #
|
|
16
|
-
--color-bg-active: #
|
|
17
|
-
--color-surface: #
|
|
18
|
-
--color-border: #
|
|
19
|
-
--color-border-hover: #
|
|
11
|
+
--color-brand: #c0f20c;
|
|
12
|
+
--color-bg: #050505;
|
|
13
|
+
--color-bg-elevated: #090a09;
|
|
14
|
+
--color-bg-card: #080908;
|
|
15
|
+
--color-bg-hover: #121512;
|
|
16
|
+
--color-bg-active: #181d18;
|
|
17
|
+
--color-surface: #0d100d;
|
|
18
|
+
--color-border: #161a16;
|
|
19
|
+
--color-border-hover: #2e3b2e;
|
|
20
20
|
|
|
21
21
|
--color-text: #ffffff;
|
|
22
|
-
--color-text-muted: #
|
|
23
|
-
--color-text-dim: #
|
|
22
|
+
--color-text-muted: #8fa08f;
|
|
23
|
+
--color-text-dim: #455245;
|
|
24
24
|
|
|
25
|
-
--color-accent: #
|
|
26
|
-
--color-accent-hover: #
|
|
27
|
-
--color-accent-glow: rgba(
|
|
28
|
-
--color-accent-dim: rgba(
|
|
25
|
+
--color-accent: #c0f20c;
|
|
26
|
+
--color-accent-hover: #d2ff2b;
|
|
27
|
+
--color-accent-glow: rgba(192, 242, 12, 0.15);
|
|
28
|
+
--color-accent-dim: rgba(192, 242, 12, 0.08);
|
|
29
29
|
|
|
30
30
|
--color-danger: #ff4444;
|
|
31
31
|
--color-danger-dim: rgba(255, 68, 68, 0.1);
|
|
@@ -160,7 +160,7 @@ textarea:focus-visible {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
html {
|
|
163
|
-
font-size:
|
|
163
|
+
font-size: 16px;
|
|
164
164
|
scroll-behavior: smooth;
|
|
165
165
|
-webkit-font-smoothing: antialiased;
|
|
166
166
|
-moz-osx-font-smoothing: grayscale;
|
|
@@ -179,23 +179,16 @@ body {
|
|
|
179
179
|
|
|
180
180
|
a { color: var(--color-accent); text-decoration: none; }
|
|
181
181
|
a:hover { color: var(--color-accent-hover); }
|
|
182
|
-
code { font-family: var(--font-mono); font-size:
|
|
182
|
+
code { font-family: var(--font-mono); font-size: 14px; background: var(--color-surface); padding: 2px 6px; border-radius: 4px; }
|
|
183
183
|
|
|
184
184
|
/* ─── Background Effects ─── */
|
|
185
|
-
.
|
|
186
|
-
display: none;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
.bg-glow {
|
|
190
|
-
display: none;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
[data-theme="light"] .bg-glow { opacity: 0.12; }
|
|
195
|
-
[data-theme="light"] .bg-grid {
|
|
185
|
+
[data-theme="light"] .app-shell {
|
|
196
186
|
background-image:
|
|
197
|
-
linear-gradient(rgba(0,0,0,0.
|
|
198
|
-
linear-gradient(90deg, rgba(0,0,0,0.
|
|
187
|
+
linear-gradient(rgba(0,0,0,0.03) 1px, transparent 1px),
|
|
188
|
+
linear-gradient(90deg, rgba(0,0,0,0.03) 1px, transparent 1px);
|
|
189
|
+
}
|
|
190
|
+
[data-theme="light"] .app-shell::before {
|
|
191
|
+
background: radial-gradient(circle, rgba(0, 143, 77, 0.03) 0%, transparent 70%);
|
|
199
192
|
}
|
|
200
193
|
|
|
201
194
|
/* ─── App Layout ─── */
|
|
@@ -207,6 +200,25 @@ code { font-family: var(--font-mono); font-size: 12px; background: var(--color-s
|
|
|
207
200
|
width: 100%;
|
|
208
201
|
position: relative;
|
|
209
202
|
z-index: 1;
|
|
203
|
+
background-color: var(--color-bg);
|
|
204
|
+
/* background grid effect */
|
|
205
|
+
background-image:
|
|
206
|
+
linear-gradient(rgba(192, 242, 12, 0.012) 1px, transparent 1px),
|
|
207
|
+
linear-gradient(90deg, rgba(192, 242, 12, 0.012) 1px, transparent 1px);
|
|
208
|
+
background-size: 50px 50px;
|
|
209
|
+
background-position: center top;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.app-shell::before {
|
|
213
|
+
content: "";
|
|
214
|
+
position: absolute;
|
|
215
|
+
top: 0;
|
|
216
|
+
left: 25%;
|
|
217
|
+
width: 600px;
|
|
218
|
+
height: 600px;
|
|
219
|
+
background: radial-gradient(circle, rgba(192, 242, 12, 0.03) 0%, transparent 70%);
|
|
220
|
+
z-index: -1;
|
|
221
|
+
pointer-events: none;
|
|
210
222
|
}
|
|
211
223
|
|
|
212
224
|
.app-content {
|
|
@@ -225,6 +237,21 @@ code { font-family: var(--font-mono); font-size: 12px; background: var(--color-s
|
|
|
225
237
|
min-height: 0;
|
|
226
238
|
}
|
|
227
239
|
|
|
240
|
+
/* 📖 Dashboard-only: bound the main box to the viewport height (minus the
|
|
241
|
+
📖 global header) so the model table scrolls INTERNALLY inside .scrollInner
|
|
242
|
+
📖 instead of growing the whole page. That internal scroll is what lets the
|
|
243
|
+
📖 green <thead> header be position:sticky and stay pinned while you scroll
|
|
244
|
+
📖 190+ model rows. Scoped to the dashboard only (main.dashboardView has
|
|
245
|
+
📖 higher specificity than the shared .view rule, so flex:none + the capped
|
|
246
|
+
📖 height reliably win). Settings / Analytics / Playground keep their layout. */
|
|
247
|
+
main.dashboardView {
|
|
248
|
+
flex: none;
|
|
249
|
+
height: calc(100vh - var(--header-h));
|
|
250
|
+
height: calc(100dvh - var(--header-h));
|
|
251
|
+
min-height: 0;
|
|
252
|
+
overflow: hidden;
|
|
253
|
+
}
|
|
254
|
+
|
|
228
255
|
.update-warning-banner {
|
|
229
256
|
position: sticky;
|
|
230
257
|
top: var(--header-h, 60px);
|
|
@@ -234,7 +261,7 @@ code { font-family: var(--font-mono); font-size: 12px; background: var(--color-s
|
|
|
234
261
|
color: #fff;
|
|
235
262
|
border-bottom: 1px solid rgba(255, 255, 255, 0.22);
|
|
236
263
|
font-weight: 800;
|
|
237
|
-
font-size:
|
|
264
|
+
font-size: 15px;
|
|
238
265
|
letter-spacing: 0.01em;
|
|
239
266
|
text-align: center;
|
|
240
267
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
|
|
@@ -14,7 +14,7 @@ const SORT_VALUES = new Set([
|
|
|
14
14
|
'latestPing', 'avg', 'condition', 'verdict', 'stability', 'uptime',
|
|
15
15
|
'aiLatency', 'tps', 'trend',
|
|
16
16
|
])
|
|
17
|
-
const VIEW_VALUES = new Set(['dashboard', 'settings', 'analytics', 'recommend', 'router', 'help', 'changelog'])
|
|
17
|
+
const VIEW_VALUES = new Set(['dashboard', 'settings', 'analytics', 'recommend', 'router', 'help', 'changelog', 'playground', 'install-endpoints', 'installed-models'])
|
|
18
18
|
const DIR_VALUES = new Set(['asc', 'desc'])
|
|
19
19
|
const TOOL_MODE_VALUES = new Set(TOOL_MODE_ORDER)
|
|
20
20
|
|
|
@@ -31,7 +31,17 @@ function parseUrlParams() {
|
|
|
31
31
|
if (typeof window === 'undefined') return null
|
|
32
32
|
const params = new URLSearchParams(window.location.search)
|
|
33
33
|
const out = {}
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
// 📖 Read pathname as the view slug
|
|
36
|
+
const pathname = window.location.pathname.replace(/^\/|\/$/g, '')
|
|
37
|
+
if (pathname && VALID_VIEWS.has(pathname)) {
|
|
38
|
+
out.view = pathname
|
|
39
|
+
} else if (params.has('view') && VALID_VIEWS.has(params.get('view'))) {
|
|
40
|
+
out.view = params.get('view')
|
|
41
|
+
} else {
|
|
42
|
+
out.view = 'dashboard'
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
if (params.has('tier') && VALID_TIERS.has(params.get('tier'))) out.tier = params.get('tier')
|
|
36
46
|
if (params.has('status') && VALID_STATUS.has(params.get('status'))) out.status = params.get('status')
|
|
37
47
|
if (params.has('provider')) out.provider = params.get('provider')
|
|
@@ -71,10 +81,16 @@ export function buildUrlParams(state) {
|
|
|
71
81
|
function writeUrl(state) {
|
|
72
82
|
if (typeof window === 'undefined') return
|
|
73
83
|
const params = buildUrlParams(state)
|
|
84
|
+
|
|
85
|
+
// 📖 Extract view from the search params so it becomes the pathname slug instead of a search param!
|
|
86
|
+
const view = params.get('view') || 'dashboard'
|
|
87
|
+
params.delete('view')
|
|
88
|
+
|
|
74
89
|
const search = params.toString()
|
|
90
|
+
const path = view !== 'dashboard' ? `/${view}` : '/'
|
|
75
91
|
const newUrl = search
|
|
76
|
-
? `${
|
|
77
|
-
: `${
|
|
92
|
+
? `${path}?${search}${window.location.hash}`
|
|
93
|
+
: `${path}${window.location.hash}`
|
|
78
94
|
// 📖 replaceState (not pushState) so back/forward don't fill up with
|
|
79
95
|
// 📖 every keystroke. The current URL still updates visibly.
|
|
80
96
|
window.history.replaceState({}, '', newUrl)
|