free-coding-models 0.5.86 → 0.5.87
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/README.md +110 -881
- package/bin/free-coding-models.js +4 -1
- package/changelog/v0.5.87.md +48 -0
- package/package.json +8 -5
- package/sources.js +4 -4
- package/src/core/cache.js +3 -1
- package/src/core/config.js +43 -5
- package/src/core/endpoint-installer.js +23 -12
- package/src/core/installed-models-manager.js +43 -20
- package/src/core/kilo-config.js +51 -5
- package/src/core/kilo.js +5 -3
- package/src/core/legacy-proxy-cleanup.js +22 -5
- package/src/core/model-merger.js +4 -0
- package/src/core/models-drift.js +9 -0
- package/src/core/opencode-config.js +57 -5
- package/src/core/opencode.js +67 -41
- package/src/core/playground.js +7 -0
- package/src/core/probe-cache.js +35 -4
- package/src/core/provider-key-tester.js +10 -2
- package/src/core/provider-metadata.js +10 -0
- package/src/core/provider-quota-fetchers.js +14 -7
- package/src/core/router-daemon.js +208 -26
- package/src/core/runtime-telemetry.js +27 -1
- package/src/core/shared-helpers.js +12 -1
- package/src/core/shell-env.js +3 -1
- package/src/core/telemetry.js +18 -5
- package/src/core/tool-launchers.js +9 -6
- package/src/core/updater.js +28 -2
- package/src/core/utils.js +76 -71
- package/src/tui/app.js +38 -14
- package/src/tui/command-palette.js +22 -2
- package/src/tui/key-handler.js +123 -179
- package/src/tui/overlays.js +21 -71
- package/src/tui/render-helpers.js +62 -4
- package/src/tui/render-table.js +34 -3
- package/src/tui/theme.js +36 -5
- package/src/tui/tui-filters.js +28 -8
- package/src/tui/tui-state.js +22 -6
- package/web/dist/assets/index-BpTpPS0g.js +44 -0
- package/web/dist/assets/index-CAzFIt8P.css +1 -0
- package/web/dist/index.html +5 -9
- package/web/index.html +3 -7
- package/web/server.js +97 -23
- package/web/vite.config.js +8 -0
- package/web/app.legacy.js +0 -900
- package/web/dist/assets/index-C5-Ywvv3.css +0 -1
- package/web/dist/assets/index-Dm-Xfby9.js +0 -40
- package/web/public/favicon.ico +0 -0
- package/web/public/favicons/apple-touch-icon.png +0 -0
- package/web/public/favicons/browserconfig.xml +0 -12
- package/web/public/favicons/favicon-16x16.png +0 -0
- package/web/public/favicons/favicon-192x192.png +0 -0
- package/web/public/favicons/favicon-32x32.png +0 -0
- package/web/public/favicons/favicon-48x48.png +0 -0
- package/web/public/favicons/favicon-512x512.png +0 -0
- package/web/public/favicons/favicon-96x96.png +0 -0
- package/web/public/favicons/favicon.ico +0 -0
- package/web/public/favicons/mstile-150x150.png +0 -0
- package/web/public/favicons/mstile-310x150.png +0 -0
- package/web/public/favicons/mstile-310x310.png +0 -0
- package/web/public/favicons/mstile-512x512.png +0 -0
- package/web/public/favicons/mstile-70x70.png +0 -0
- package/web/public/favicons/site.webmanifest +0 -25
- package/web/src/App.jsx +0 -521
- package/web/src/components/analytics/AnalyticsView.jsx +0 -147
- package/web/src/components/analytics/AnalyticsView.module.css +0 -190
- package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
- package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
- package/web/src/components/atoms/AILatencyCell.jsx +0 -38
- package/web/src/components/atoms/AILatencyCell.module.css +0 -51
- package/web/src/components/atoms/HealthCell.jsx +0 -143
- package/web/src/components/atoms/HealthCell.module.css +0 -60
- package/web/src/components/atoms/LastPingCell.jsx +0 -35
- package/web/src/components/atoms/LastPingCell.module.css +0 -45
- package/web/src/components/atoms/MoodCell.jsx +0 -25
- package/web/src/components/atoms/MoodCell.module.css +0 -6
- package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
- package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
- package/web/src/components/atoms/ProviderLogo.jsx +0 -187
- package/web/src/components/atoms/ProviderLogo.module.css +0 -103
- package/web/src/components/atoms/RankCell.jsx +0 -9
- package/web/src/components/atoms/RankCell.module.css +0 -9
- package/web/src/components/atoms/Sparkline.jsx +0 -63
- package/web/src/components/atoms/StabilityCell.jsx +0 -18
- package/web/src/components/atoms/StabilityCell.module.css +0 -13
- package/web/src/components/atoms/StatusDot.jsx +0 -13
- package/web/src/components/atoms/StatusDot.module.css +0 -29
- package/web/src/components/atoms/TPSCell.jsx +0 -36
- package/web/src/components/atoms/TPSCell.module.css +0 -44
- package/web/src/components/atoms/TierBadge.jsx +0 -10
- package/web/src/components/atoms/TierBadge.module.css +0 -29
- package/web/src/components/atoms/Toast.jsx +0 -25
- package/web/src/components/atoms/Toast.module.css +0 -41
- package/web/src/components/atoms/ToastContainer.jsx +0 -16
- package/web/src/components/atoms/ToastContainer.module.css +0 -10
- package/web/src/components/atoms/VerdictBadge.jsx +0 -36
- package/web/src/components/atoms/VerdictBadge.module.css +0 -40
- package/web/src/components/changelog/ChangelogView.jsx +0 -135
- package/web/src/components/changelog/ChangelogView.module.css +0 -159
- package/web/src/components/dashboard/DetailPanel.jsx +0 -217
- package/web/src/components/dashboard/DetailPanel.module.css +0 -260
- package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
- package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
- package/web/src/components/dashboard/ExportModal.jsx +0 -84
- package/web/src/components/dashboard/ExportModal.module.css +0 -99
- package/web/src/components/dashboard/FilterBar.jsx +0 -338
- package/web/src/components/dashboard/FilterBar.module.css +0 -342
- package/web/src/components/dashboard/ModelTable.jsx +0 -672
- package/web/src/components/dashboard/ModelTable.module.css +0 -446
- package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
- package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
- package/web/src/components/help/HelpView.jsx +0 -201
- package/web/src/components/help/HelpView.module.css +0 -156
- package/web/src/components/install/InstallEndpointsView.jsx +0 -309
- package/web/src/components/install/InstallEndpointsView.module.css +0 -427
- package/web/src/components/installed/InstalledModelsView.jsx +0 -89
- package/web/src/components/installed/InstalledModelsView.module.css +0 -200
- package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
- package/web/src/components/launch/LaunchButton.jsx +0 -30
- package/web/src/components/launch/LaunchButton.module.css +0 -35
- package/web/src/components/launch/LaunchModal.module.css +0 -125
- package/web/src/components/layout/Footer.jsx +0 -19
- package/web/src/components/layout/Footer.module.css +0 -10
- package/web/src/components/layout/Header.jsx +0 -265
- package/web/src/components/layout/Header.module.css +0 -357
- package/web/src/components/palette/CommandPalette.jsx +0 -301
- package/web/src/components/palette/CommandPalette.module.css +0 -121
- package/web/src/components/playground/PlaygroundChat.jsx +0 -527
- package/web/src/components/playground/PlaygroundChat.module.css +0 -382
- package/web/src/components/playground/PlaygroundView.jsx +0 -371
- package/web/src/components/playground/PlaygroundView.module.css +0 -504
- package/web/src/components/recommend/RecommendView.jsx +0 -121
- package/web/src/components/recommend/RecommendView.module.css +0 -57
- package/web/src/components/router/RouterView.jsx +0 -1212
- package/web/src/components/router/RouterView.module.css +0 -1289
- package/web/src/components/settings/SettingsView.jsx +0 -555
- package/web/src/components/settings/SettingsView.module.css +0 -550
- package/web/src/components/tools/ToolPicker.jsx +0 -86
- package/web/src/components/tools/ToolPicker.module.css +0 -97
- package/web/src/components/update/UpdateChip.jsx +0 -104
- package/web/src/components/update/UpdateChip.module.css +0 -146
- package/web/src/global.css +0 -298
- package/web/src/hooks/urlState.constants.js +0 -28
- package/web/src/hooks/useChangelog.js +0 -51
- package/web/src/hooks/useColumnSizing.js +0 -211
- package/web/src/hooks/useFavorites.js +0 -150
- package/web/src/hooks/useFilter.js +0 -310
- package/web/src/hooks/useInstalledModels.js +0 -42
- package/web/src/hooks/useRecommend.js +0 -67
- package/web/src/hooks/useRouterDashboard.js +0 -133
- package/web/src/hooks/useSSE.js +0 -73
- package/web/src/hooks/useSocket.js +0 -203
- package/web/src/hooks/useTheme.js +0 -83
- package/web/src/hooks/useTokenUsage.js +0 -103
- package/web/src/hooks/useToolMode.js +0 -77
- package/web/src/hooks/useUpdateChecker.js +0 -91
- package/web/src/hooks/useUrlState.js +0 -170
- package/web/src/main.jsx +0 -15
- package/web/src/utils/download.js +0 -15
- package/web/src/utils/format.js +0 -42
- package/web/src/utils/m3.js +0 -57
- package/web/src/utils/ranks.js +0 -37
- package/web/styles.legacy.css +0 -963
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/components/update/UpdateChip.jsx
|
|
3
|
-
* @description Header update chip + popover — M2 parity with TUI's auto-update banner.
|
|
4
|
-
* 📖 Sits in the right side of the header (next to AI Latency, theme, export).
|
|
5
|
-
* 📖 Hidden when no update is available; shows "⬆ vX.Y.Z" when one is.
|
|
6
|
-
* 📖 Click → popover with "Update now" + "What's new" (which opens the
|
|
7
|
-
* 📖 Changelog modal pre-focused on the new version).
|
|
8
|
-
*
|
|
9
|
-
* @functions
|
|
10
|
-
* → UpdateChip — small badge with popover
|
|
11
|
-
*/
|
|
12
|
-
import { useState, useRef, useEffect } from 'react'
|
|
13
|
-
import { IconDownload, IconHistory, IconX, IconExternalLink } from '@tabler/icons-react'
|
|
14
|
-
import styles from './UpdateChip.module.css'
|
|
15
|
-
|
|
16
|
-
export default function UpdateChip({ updateAvailable, latestVersion, onRunUpdate, onOpenChangelog, checking }) {
|
|
17
|
-
const [open, setOpen] = useState(false)
|
|
18
|
-
const ref = useRef(null)
|
|
19
|
-
|
|
20
|
-
// 📖 Close on outside click / Esc — same pattern as the header kebab menu.
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (!open) return
|
|
23
|
-
const onClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false) }
|
|
24
|
-
const onKey = (e) => { if (e.key === 'Escape') setOpen(false) }
|
|
25
|
-
document.addEventListener('mousedown', onClick)
|
|
26
|
-
document.addEventListener('keydown', onKey)
|
|
27
|
-
return () => {
|
|
28
|
-
document.removeEventListener('mousedown', onClick)
|
|
29
|
-
document.removeEventListener('keydown', onKey)
|
|
30
|
-
}
|
|
31
|
-
}, [open])
|
|
32
|
-
|
|
33
|
-
// 📖 No chip when no update. The M1 plan keeps the chip honest — a fresh
|
|
34
|
-
// 📖 install should look clean, not noisy.
|
|
35
|
-
if (!updateAvailable) {
|
|
36
|
-
if (checking) {
|
|
37
|
-
return (
|
|
38
|
-
<span className={styles.checking} title="Checking for updates…">
|
|
39
|
-
<span className={styles.dot} />
|
|
40
|
-
</span>
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
return null
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<div className={styles.wrap} ref={ref}>
|
|
48
|
-
<button
|
|
49
|
-
className={styles.chip}
|
|
50
|
-
onClick={() => setOpen((o) => !o)}
|
|
51
|
-
title={`Update available: v${latestVersion}. Click to install.`}
|
|
52
|
-
aria-expanded={open}
|
|
53
|
-
aria-haspopup="dialog"
|
|
54
|
-
>
|
|
55
|
-
<IconDownload size={13} stroke={1.5} />
|
|
56
|
-
<span>v{latestVersion}</span>
|
|
57
|
-
</button>
|
|
58
|
-
|
|
59
|
-
{open && (
|
|
60
|
-
<div className={styles.popover} role="dialog" aria-label="Update available">
|
|
61
|
-
<div className={styles.popoverHeader}>
|
|
62
|
-
<div className={styles.popoverTitle}>
|
|
63
|
-
<IconDownload size={14} stroke={1.5} />
|
|
64
|
-
<span>Update available</span>
|
|
65
|
-
</div>
|
|
66
|
-
<button
|
|
67
|
-
className={styles.popoverClose}
|
|
68
|
-
onClick={() => setOpen(false)}
|
|
69
|
-
aria-label="Close"
|
|
70
|
-
>
|
|
71
|
-
<IconX size={14} stroke={1.5} />
|
|
72
|
-
</button>
|
|
73
|
-
</div>
|
|
74
|
-
<p className={styles.popoverBody}>
|
|
75
|
-
A newer version (<strong>v{latestVersion}</strong>) is available on npm.
|
|
76
|
-
After updating, restart the dashboard to pick up the changes.
|
|
77
|
-
</p>
|
|
78
|
-
<div className={styles.popoverActions}>
|
|
79
|
-
<button
|
|
80
|
-
className={styles.primaryAction}
|
|
81
|
-
onClick={() => {
|
|
82
|
-
setOpen(false)
|
|
83
|
-
onRunUpdate?.()
|
|
84
|
-
}}
|
|
85
|
-
>
|
|
86
|
-
<IconDownload size={13} stroke={1.5} />
|
|
87
|
-
<span>Update now</span>
|
|
88
|
-
</button>
|
|
89
|
-
<button
|
|
90
|
-
className={styles.secondaryAction}
|
|
91
|
-
onClick={() => {
|
|
92
|
-
setOpen(false)
|
|
93
|
-
onOpenChangelog?.(latestVersion)
|
|
94
|
-
}}
|
|
95
|
-
>
|
|
96
|
-
<IconHistory size={13} stroke={1.5} />
|
|
97
|
-
<span>What's new</span>
|
|
98
|
-
</button>
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
)}
|
|
102
|
-
</div>
|
|
103
|
-
)
|
|
104
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/components/update/UpdateChip.module.css
|
|
3
|
-
* @description Styles for the header update chip + popover.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
.wrap {
|
|
7
|
-
position: relative;
|
|
8
|
-
display: inline-flex;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
.chip {
|
|
12
|
-
display: inline-flex;
|
|
13
|
-
align-items: center;
|
|
14
|
-
gap: 5px;
|
|
15
|
-
height: 28px;
|
|
16
|
-
padding: 0 10px;
|
|
17
|
-
font-size: 11px;
|
|
18
|
-
font-weight: 700;
|
|
19
|
-
font-family: var(--font-mono);
|
|
20
|
-
background: var(--color-success-dim);
|
|
21
|
-
color: var(--color-success);
|
|
22
|
-
border: 1px solid var(--color-success);
|
|
23
|
-
border-radius: 6px;
|
|
24
|
-
cursor: pointer;
|
|
25
|
-
font-family: var(--font-mono);
|
|
26
|
-
transition: all 150ms;
|
|
27
|
-
white-space: nowrap;
|
|
28
|
-
}
|
|
29
|
-
.chip:hover {
|
|
30
|
-
background: var(--color-success);
|
|
31
|
-
color: var(--color-bg);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.checking {
|
|
35
|
-
display: inline-flex;
|
|
36
|
-
align-items: center;
|
|
37
|
-
justify-content: center;
|
|
38
|
-
width: 22px;
|
|
39
|
-
height: 22px;
|
|
40
|
-
opacity: 0.5;
|
|
41
|
-
}
|
|
42
|
-
.dot {
|
|
43
|
-
width: 6px;
|
|
44
|
-
height: 6px;
|
|
45
|
-
border-radius: 50%;
|
|
46
|
-
background: var(--color-text-muted);
|
|
47
|
-
animation: pulse 1.5s ease-in-out infinite;
|
|
48
|
-
}
|
|
49
|
-
@keyframes pulse {
|
|
50
|
-
0%, 100% { opacity: 0.3; }
|
|
51
|
-
50% { opacity: 1; }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.popover {
|
|
55
|
-
position: absolute;
|
|
56
|
-
top: calc(100% + 6px);
|
|
57
|
-
right: 0;
|
|
58
|
-
width: 320px;
|
|
59
|
-
background: var(--color-bg-elevated);
|
|
60
|
-
border: 1px solid var(--color-border-hover);
|
|
61
|
-
border-radius: 10px;
|
|
62
|
-
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
|
|
63
|
-
z-index: 200;
|
|
64
|
-
overflow: hidden;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.popoverHeader {
|
|
68
|
-
display: flex;
|
|
69
|
-
align-items: center;
|
|
70
|
-
justify-content: space-between;
|
|
71
|
-
padding: 10px 12px;
|
|
72
|
-
border-bottom: 1px solid var(--color-border);
|
|
73
|
-
}
|
|
74
|
-
.popoverTitle {
|
|
75
|
-
display: inline-flex;
|
|
76
|
-
align-items: center;
|
|
77
|
-
gap: 6px;
|
|
78
|
-
font-size: 12px;
|
|
79
|
-
font-weight: 700;
|
|
80
|
-
color: var(--color-success);
|
|
81
|
-
}
|
|
82
|
-
.popoverClose {
|
|
83
|
-
background: transparent;
|
|
84
|
-
border: none;
|
|
85
|
-
color: var(--color-text-muted);
|
|
86
|
-
cursor: pointer;
|
|
87
|
-
display: inline-flex;
|
|
88
|
-
align-items: center;
|
|
89
|
-
justify-content: center;
|
|
90
|
-
padding: 2px;
|
|
91
|
-
border-radius: 4px;
|
|
92
|
-
}
|
|
93
|
-
.popoverClose:hover { color: var(--color-text); background: var(--color-bg-hover); }
|
|
94
|
-
|
|
95
|
-
.popoverBody {
|
|
96
|
-
padding: 10px 12px;
|
|
97
|
-
margin: 0;
|
|
98
|
-
font-size: 12px;
|
|
99
|
-
line-height: 1.5;
|
|
100
|
-
color: var(--color-text);
|
|
101
|
-
}
|
|
102
|
-
.popoverBody strong {
|
|
103
|
-
color: var(--color-success);
|
|
104
|
-
font-family: var(--font-mono);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.popoverActions {
|
|
108
|
-
display: flex;
|
|
109
|
-
gap: 6px;
|
|
110
|
-
padding: 8px 12px 12px;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.primaryAction,
|
|
114
|
-
.secondaryAction {
|
|
115
|
-
display: inline-flex;
|
|
116
|
-
align-items: center;
|
|
117
|
-
gap: 5px;
|
|
118
|
-
flex: 1;
|
|
119
|
-
height: 32px;
|
|
120
|
-
padding: 0 10px;
|
|
121
|
-
font-size: 11px;
|
|
122
|
-
font-weight: 600;
|
|
123
|
-
border-radius: 6px;
|
|
124
|
-
cursor: pointer;
|
|
125
|
-
justify-content: center;
|
|
126
|
-
font-family: var(--font-sans);
|
|
127
|
-
transition: all 150ms;
|
|
128
|
-
}
|
|
129
|
-
.primaryAction {
|
|
130
|
-
background: var(--color-success);
|
|
131
|
-
color: var(--color-bg);
|
|
132
|
-
border: 1px solid var(--color-success);
|
|
133
|
-
}
|
|
134
|
-
.primaryAction:hover {
|
|
135
|
-
background: var(--color-success);
|
|
136
|
-
filter: brightness(0.9);
|
|
137
|
-
}
|
|
138
|
-
.secondaryAction {
|
|
139
|
-
background: var(--color-surface);
|
|
140
|
-
color: var(--color-text);
|
|
141
|
-
border: 1px solid var(--color-border);
|
|
142
|
-
}
|
|
143
|
-
.secondaryAction:hover {
|
|
144
|
-
background: var(--color-bg-hover);
|
|
145
|
-
border-color: var(--color-text-muted);
|
|
146
|
-
}
|
package/web/src/global.css
DELETED
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/global.css
|
|
3
|
-
* @description Global CSS with design tokens, base reset, background effects, and shared styles.
|
|
4
|
-
* 📖 Contains all CSS custom properties (--color-*, --font-*, --radius-*, etc.),
|
|
5
|
-
* light theme overrides, reset rules, background grid/glow effects,
|
|
6
|
-
* app-content layout, view visibility, scrollbar, and responsive breakpoints.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/* ─── CSS Custom Properties (Design Tokens) ─── */
|
|
10
|
-
:root {
|
|
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
|
-
|
|
21
|
-
--color-text: #ffffff;
|
|
22
|
-
--color-text-muted: #8fa08f;
|
|
23
|
-
--color-text-dim: #455245;
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
--color-danger: #ff4444;
|
|
31
|
-
--color-danger-dim: rgba(255, 68, 68, 0.1);
|
|
32
|
-
--color-warning: #ffaa00;
|
|
33
|
-
--color-warning-dim: rgba(255, 170, 0, 0.1);
|
|
34
|
-
--color-success: #00ff88;
|
|
35
|
-
--color-success-dim: rgba(0, 255, 136, 0.1);
|
|
36
|
-
--color-info: #06b6d4;
|
|
37
|
-
--color-info-dim: rgba(6, 182, 212, 0.1);
|
|
38
|
-
|
|
39
|
-
--tier-splus: #ffd700;
|
|
40
|
-
--tier-s: #ff8c00;
|
|
41
|
-
--tier-aplus: #00c8ff;
|
|
42
|
-
--tier-a: #3ddc84;
|
|
43
|
-
--tier-aminus: #7ecf7e;
|
|
44
|
-
--tier-bplus: #a8a8c8;
|
|
45
|
-
--tier-b: #808098;
|
|
46
|
-
--tier-c: #606078;
|
|
47
|
-
|
|
48
|
-
--verdict-perfect: #00ff88;
|
|
49
|
-
--verdict-normal: #76b900;
|
|
50
|
-
--verdict-slow: #ffaa00;
|
|
51
|
-
--verdict-spiky: #ff6600;
|
|
52
|
-
--verdict-veryslow: #ff4444;
|
|
53
|
-
--verdict-overloaded: #ff2222;
|
|
54
|
-
--verdict-unstable: #cc0000;
|
|
55
|
-
--verdict-notactive: #555570;
|
|
56
|
-
--verdict-pending: #444460;
|
|
57
|
-
|
|
58
|
-
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
59
|
-
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
60
|
-
|
|
61
|
-
--header-h: 60px;
|
|
62
|
-
--radius-sm: 4px;
|
|
63
|
-
--radius-md: 6px;
|
|
64
|
-
--radius-lg: 8px;
|
|
65
|
-
--radius-xl: 8px;
|
|
66
|
-
|
|
67
|
-
--shadow-sm: none;
|
|
68
|
-
--shadow-md: none;
|
|
69
|
-
--shadow-lg: none;
|
|
70
|
-
--shadow-glow: none;
|
|
71
|
-
|
|
72
|
-
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
73
|
-
--transition-fast: 150ms var(--ease-out);
|
|
74
|
-
--transition-medium: 250ms var(--ease-out);
|
|
75
|
-
--transition-slow: 400ms var(--ease-out);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/* ─── Light Theme ─── */
|
|
79
|
-
[data-theme="light"] {
|
|
80
|
-
--color-bg: #ffffff;
|
|
81
|
-
--color-bg-elevated: #ffffff;
|
|
82
|
-
--color-bg-card: #ffffff;
|
|
83
|
-
--color-bg-hover: #fafafa;
|
|
84
|
-
--color-bg-active: #f0f0f0;
|
|
85
|
-
--color-surface: #fafafa;
|
|
86
|
-
--color-border: #eaeaea;
|
|
87
|
-
--color-border-hover: #d5d5d5;
|
|
88
|
-
--color-text: #000000;
|
|
89
|
-
--color-text-muted: #666666;
|
|
90
|
-
--color-text-dim: #999999;
|
|
91
|
-
--color-accent: #000000;
|
|
92
|
-
--color-accent-hover: #333333;
|
|
93
|
-
--color-accent-glow: rgba(0, 0, 0, 0.05);
|
|
94
|
-
--color-accent-dim: rgba(0, 0, 0, 0.02);
|
|
95
|
-
|
|
96
|
-
/* 📖 Bright neon colors that look great on black become unreadable on white.
|
|
97
|
-
📖 We swap them for darker, AA-contrast equivalents in light mode. The
|
|
98
|
-
📖 hex values are picked to stay visually similar to the dark variant
|
|
99
|
-
📖 (still "green" / "cyan" / "gold") while passing WCAG AA on #fff. */
|
|
100
|
-
--color-success: #008f4d; /* was #00ff88 (neon) */
|
|
101
|
-
--color-warning: #b86b00; /* was #ffaa00 (neon amber) */
|
|
102
|
-
--color-danger: #c8143a; /* was #ff4444 (neon red) */
|
|
103
|
-
--color-info: #0e7490; /* was #06b6d4 (neon cyan) */
|
|
104
|
-
--color-danger-dim: rgba(200, 20, 58, 0.08);
|
|
105
|
-
--color-warning-dim: rgba(184, 107, 0, 0.08);
|
|
106
|
-
--color-success-dim: rgba(0, 143, 77, 0.08);
|
|
107
|
-
--color-info-dim: rgba(14, 116, 144, 0.08);
|
|
108
|
-
|
|
109
|
-
/* 📖 Tier + verdict palette: shift toward darker, more saturated variants
|
|
110
|
-
📖 so badges stay legible on white. The original "neon" feel is dropped
|
|
111
|
-
📖 in favor of "saturated-but-deep" colors that pass contrast. */
|
|
112
|
-
--tier-splus: #b08800; /* was #ffd700 (gold) */
|
|
113
|
-
--tier-s: #c45a00; /* was #ff8c00 (dark orange) */
|
|
114
|
-
--tier-aplus: #0073a8; /* was #00c8ff (neon cyan) */
|
|
115
|
-
--tier-a: #1f7a3e; /* was #3ddc84 (mid green) */
|
|
116
|
-
--tier-aminus: #2f7a2f; /* was #7ecf7e (light green) */
|
|
117
|
-
--tier-bplus: #5a5a8a; /* was #a8a8c8 (lavender) */
|
|
118
|
-
--tier-b: #555566; /* was #808098 (gray-blue) */
|
|
119
|
-
--tier-c: #6a6a7a; /* was #606078 (slate) */
|
|
120
|
-
|
|
121
|
-
--verdict-perfect: #008f4d; /* was #00ff88 */
|
|
122
|
-
--verdict-normal: #4a7a00; /* was #76b900 */
|
|
123
|
-
--verdict-slow: #b86b00; /* was #ffaa00 */
|
|
124
|
-
--verdict-spiky: #c45000; /* was #ff6600 */
|
|
125
|
-
--verdict-veryslow: #c8143a; /* was #ff4444 */
|
|
126
|
-
--verdict-overloaded: #b8101f; /* was #ff2222 */
|
|
127
|
-
--verdict-unstable: #990000; /* was #cc0000 */
|
|
128
|
-
--verdict-notactive: #6a6a7a; /* was #555570 */
|
|
129
|
-
--verdict-pending: #6a6a8a; /* was #444460 */
|
|
130
|
-
|
|
131
|
-
--shadow-sm: none;
|
|
132
|
-
--shadow-md: none;
|
|
133
|
-
--shadow-lg: none;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/* ─── Reset & Base ─── */
|
|
137
|
-
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
138
|
-
|
|
139
|
-
/* 📖 M5: Global :focus-visible outline for keyboard navigation (a11y).
|
|
140
|
-
📖 Only shows for keyboard users, not mouse clicks. Uses the accent color
|
|
141
|
-
📖 with a subtle glow ring so focus is always visible but not jarring. */
|
|
142
|
-
:focus-visible {
|
|
143
|
-
outline: 2px solid var(--color-accent);
|
|
144
|
-
outline-offset: 2px;
|
|
145
|
-
border-radius: var(--radius-sm);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/* 📖 Remove default browser :focus ring — we only want :focus-visible */
|
|
149
|
-
:focus:not(:focus-visible) {
|
|
150
|
-
outline: none;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/* 📖 Inputs keep their own focus styling via component CSS, but keyboard
|
|
154
|
-
📖 users still see the global ring as a fallback safety net. */
|
|
155
|
-
input:focus-visible,
|
|
156
|
-
select:focus-visible,
|
|
157
|
-
textarea:focus-visible {
|
|
158
|
-
outline: 2px solid var(--color-accent);
|
|
159
|
-
outline-offset: 1px;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
html {
|
|
163
|
-
font-size: 16px;
|
|
164
|
-
scroll-behavior: smooth;
|
|
165
|
-
-webkit-font-smoothing: antialiased;
|
|
166
|
-
-moz-osx-font-smoothing: grayscale;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
body {
|
|
170
|
-
font-family: var(--font-sans);
|
|
171
|
-
background: var(--color-bg);
|
|
172
|
-
color: var(--color-text);
|
|
173
|
-
min-height: 100vh;
|
|
174
|
-
overflow-x: hidden;
|
|
175
|
-
position: relative;
|
|
176
|
-
display: flex;
|
|
177
|
-
flex-direction: column;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
a { color: var(--color-accent); text-decoration: none; }
|
|
181
|
-
a:hover { color: var(--color-accent-hover); }
|
|
182
|
-
code { font-family: var(--font-mono); font-size: 14px; background: var(--color-surface); padding: 2px 6px; border-radius: 4px; }
|
|
183
|
-
|
|
184
|
-
/* ─── Background Effects ─── */
|
|
185
|
-
[data-theme="light"] .app-shell {
|
|
186
|
-
background-image:
|
|
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%);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/* ─── App Layout ─── */
|
|
195
|
-
.app-shell {
|
|
196
|
-
flex: 1;
|
|
197
|
-
display: flex;
|
|
198
|
-
flex-direction: column;
|
|
199
|
-
min-height: 100vh;
|
|
200
|
-
width: 100%;
|
|
201
|
-
position: relative;
|
|
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;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
.app-content {
|
|
225
|
-
flex: 1;
|
|
226
|
-
position: relative;
|
|
227
|
-
z-index: 1;
|
|
228
|
-
min-height: calc(100vh - var(--header-h));
|
|
229
|
-
display: flex;
|
|
230
|
-
flex-direction: column;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
.view {
|
|
234
|
-
display: flex;
|
|
235
|
-
flex-direction: column;
|
|
236
|
-
flex: 1;
|
|
237
|
-
min-height: 0;
|
|
238
|
-
}
|
|
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
|
-
|
|
255
|
-
.update-warning-banner {
|
|
256
|
-
position: sticky;
|
|
257
|
-
top: var(--header-h, 60px);
|
|
258
|
-
z-index: 90;
|
|
259
|
-
padding: 10px 18px;
|
|
260
|
-
background: var(--color-danger);
|
|
261
|
-
color: #fff;
|
|
262
|
-
border-bottom: 1px solid rgba(255, 255, 255, 0.22);
|
|
263
|
-
font-weight: 800;
|
|
264
|
-
font-size: 15px;
|
|
265
|
-
letter-spacing: 0.01em;
|
|
266
|
-
text-align: center;
|
|
267
|
-
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/* ─── Scrollbar ─── */
|
|
271
|
-
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
272
|
-
::-webkit-scrollbar-track { background: var(--color-bg); }
|
|
273
|
-
::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: 4px; }
|
|
274
|
-
::-webkit-scrollbar-thumb:hover { background: var(--color-text-dim); }
|
|
275
|
-
::-webkit-scrollbar-corner { background: var(--color-bg); }
|
|
276
|
-
|
|
277
|
-
/* ─── Responsive ─── */
|
|
278
|
-
@media (max-width: 768px) {
|
|
279
|
-
/* 📖 No sidebar to collapse on small viewports — the header itself collapses
|
|
280
|
-
📖 (see Header.module.css). The model table keeps its 100% width. */
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/* ─── Full-page horizontal/vertical scroll layout ─── */
|
|
284
|
-
.app-content {
|
|
285
|
-
flex: 1;
|
|
286
|
-
position: relative;
|
|
287
|
-
z-index: 1;
|
|
288
|
-
min-height: calc(100vh - var(--header-h));
|
|
289
|
-
display: flex;
|
|
290
|
-
flex-direction: column;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
.view {
|
|
294
|
-
display: flex;
|
|
295
|
-
flex-direction: column;
|
|
296
|
-
flex: 1;
|
|
297
|
-
min-height: 0;
|
|
298
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/hooks/urlState.constants.js
|
|
3
|
-
* @description URL param validation tables — extracted from useUrlState for testability.
|
|
4
|
-
* 📖 Keep these in sync with src/tui/tui-state.js TIER_CYCLE / VERDICT_CYCLE /
|
|
5
|
-
* 📖 HEALTH_CYCLE constants so the URL reflects the same filter universe
|
|
6
|
-
* 📖 the TUI uses.
|
|
7
|
-
*/
|
|
8
|
-
import { TOOL_MODE_ORDER } from '../../../src/core/tool-metadata.js'
|
|
9
|
-
|
|
10
|
-
const TIER_VALUES = new Set(['S+', 'S', 'A+', 'A', 'A-', 'B+', 'B', 'C', 'all'])
|
|
11
|
-
const STATUS_VALUES = new Set(['up', 'down', 'pending', 'all'])
|
|
12
|
-
const SORT_VALUES = new Set([
|
|
13
|
-
'mood', 'idx', 'tier', 'sweScore', 'ctx', 'label', 'origin',
|
|
14
|
-
'latestPing', 'avg', 'condition', 'verdict', 'stability', 'uptime',
|
|
15
|
-
'aiLatency', 'tps', 'trend',
|
|
16
|
-
])
|
|
17
|
-
const VIEW_VALUES = new Set(['dashboard', 'settings', 'analytics', 'recommend', 'router', 'help', 'changelog', 'playground', 'install-endpoints', 'installed-models'])
|
|
18
|
-
const DIR_VALUES = new Set(['asc', 'desc'])
|
|
19
|
-
const TOOL_MODE_VALUES = new Set(TOOL_MODE_ORDER)
|
|
20
|
-
|
|
21
|
-
export {
|
|
22
|
-
TIER_VALUES as VALID_TIERS,
|
|
23
|
-
STATUS_VALUES as VALID_STATUS,
|
|
24
|
-
SORT_VALUES as VALID_SORTS,
|
|
25
|
-
VIEW_VALUES as VALID_VIEWS,
|
|
26
|
-
DIR_VALUES as VALID_DIRS,
|
|
27
|
-
TOOL_MODE_VALUES as VALID_TOOL_MODES,
|
|
28
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/hooks/useChangelog.js
|
|
3
|
-
* @description React hook for the changelog data — M2 parity with the TUI's `N` key overlay.
|
|
4
|
-
* 📖 Loads `/api/changelog` once on mount, exposes the parsed { versions } map,
|
|
5
|
-
* 📖 and provides helpers for the index/details two-phase modal.
|
|
6
|
-
*
|
|
7
|
-
* @functions
|
|
8
|
-
* → useChangelog() — { versions, sortedVersions, getVersion, loading, error, refresh }
|
|
9
|
-
*/
|
|
10
|
-
import { useEffect, useMemo, useState, useCallback } from 'react'
|
|
11
|
-
|
|
12
|
-
export function useChangelog() {
|
|
13
|
-
const [versions, setVersions] = useState({})
|
|
14
|
-
const [loading, setLoading] = useState(true)
|
|
15
|
-
const [error, setError] = useState(null)
|
|
16
|
-
|
|
17
|
-
const refresh = useCallback(async () => {
|
|
18
|
-
try {
|
|
19
|
-
const resp = await fetch('/api/changelog')
|
|
20
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
|
21
|
-
const data = await resp.json()
|
|
22
|
-
setVersions(data?.versions ?? {})
|
|
23
|
-
setError(null)
|
|
24
|
-
} catch (err) {
|
|
25
|
-
setError(err.message || 'Failed to load changelog')
|
|
26
|
-
} finally {
|
|
27
|
-
setLoading(false)
|
|
28
|
-
}
|
|
29
|
-
}, [])
|
|
30
|
-
|
|
31
|
-
useEffect(() => { refresh() }, [refresh])
|
|
32
|
-
|
|
33
|
-
// 📖 Sort versions in descending semver order. We do a string-based compare
|
|
34
|
-
// 📖 on the dotted tuples so '0.10.0' > '0.9.0' works correctly.
|
|
35
|
-
const sortedVersions = useMemo(() => {
|
|
36
|
-
return Object.keys(versions).sort((a, b) => {
|
|
37
|
-
const ap = a.split('.').map(Number)
|
|
38
|
-
const bp = b.split('.').map(Number)
|
|
39
|
-
for (let i = 0; i < Math.max(ap.length, bp.length); i++) {
|
|
40
|
-
const av = ap[i] || 0
|
|
41
|
-
const bv = bp[i] || 0
|
|
42
|
-
if (bv !== av) return bv - av
|
|
43
|
-
}
|
|
44
|
-
return 0
|
|
45
|
-
})
|
|
46
|
-
}, [versions])
|
|
47
|
-
|
|
48
|
-
const getVersion = useCallback((v) => versions[v] ?? null, [versions])
|
|
49
|
-
|
|
50
|
-
return { versions, sortedVersions, getVersion, loading, error, refresh }
|
|
51
|
-
}
|