free-coding-models 0.5.85 → 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.86.md +10 -0
- 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 +47 -16
- package/src/tui/command-palette.js +22 -2
- package/src/tui/key-handler.js +123 -179
- package/src/tui/overlays.js +35 -75
- package/src/tui/render-helpers.js +62 -4
- package/src/tui/render-table.js +46 -5
- 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-DWUely84.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,135 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/components/changelog/ChangelogView.jsx
|
|
3
|
-
* @description Changelog modal — M2 parity with the TUI's Changelog overlay (N key / Settings link).
|
|
4
|
-
* 📖 Two-phase UI: index of versions on the left, details on the right. Same
|
|
5
|
-
* 📖 content as the TUI's renderChangelog (which uses src/core/changelog-loader.js
|
|
6
|
-
* 📖 — we hit the same data through `/api/changelog`).
|
|
7
|
-
*
|
|
8
|
-
* @functions
|
|
9
|
-
* → ChangelogView → main modal component
|
|
10
|
-
*/
|
|
11
|
-
import { useState } from 'react'
|
|
12
|
-
import { IconArrowLeft, IconX, IconCalendar } from '@tabler/icons-react'
|
|
13
|
-
import { useChangelog } from '../../hooks/useChangelog.js'
|
|
14
|
-
import styles from './ChangelogView.module.css'
|
|
15
|
-
|
|
16
|
-
// 📖 Section order matches the changelog files (`### Added` / `### Fixed` /
|
|
17
|
-
// 📖 `### Changed` / `### Updated`). The TUI uses the same order in
|
|
18
|
-
// 📖 formatChangelogForDisplay.
|
|
19
|
-
const SECTION_LABELS = [
|
|
20
|
-
{ key: 'added', label: '✨ Added', icon: '✨' },
|
|
21
|
-
{ key: 'fixed', label: '🐛 Fixed', icon: '🐛' },
|
|
22
|
-
{ key: 'changed', label: '🔄 Changed', icon: '🔄' },
|
|
23
|
-
{ key: 'updated', label: '📝 Updated', icon: '📝' },
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
export default function ChangelogView({ onClose, defaultVersion = null }) {
|
|
27
|
-
const { sortedVersions, getVersion, loading, error } = useChangelog()
|
|
28
|
-
// 📖 Two-phase navigation: 'index' (version list) or 'details' (one version).
|
|
29
|
-
// 📖 `selectedVersion` is null on the index and a version string on details.
|
|
30
|
-
const [phase, setPhase] = useState(defaultVersion ? 'details' : 'index')
|
|
31
|
-
const [selectedVersion, setSelectedVersion] = useState(defaultVersion)
|
|
32
|
-
|
|
33
|
-
const openDetails = (version) => {
|
|
34
|
-
setSelectedVersion(version)
|
|
35
|
-
setPhase('details')
|
|
36
|
-
}
|
|
37
|
-
const backToIndex = () => {
|
|
38
|
-
setSelectedVersion(null)
|
|
39
|
-
setPhase('index')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const details = selectedVersion ? getVersion(selectedVersion) : null
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div className={styles.backdrop} onClick={onClose}>
|
|
46
|
-
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
|
|
47
|
-
<div className={styles.header}>
|
|
48
|
-
<div className={styles.titleRow}>
|
|
49
|
-
<h2 className={styles.title}>
|
|
50
|
-
{phase === 'index' ? '📋 Changelog' : `📋 v${selectedVersion}`}
|
|
51
|
-
</h2>
|
|
52
|
-
{phase === 'details' && (
|
|
53
|
-
<button className={styles.backBtn} onClick={backToIndex} title="Back to index (TUI: B)">
|
|
54
|
-
<IconArrowLeft size={14} stroke={1.5} /> Index
|
|
55
|
-
</button>
|
|
56
|
-
)}
|
|
57
|
-
<button className={styles.closeBtn} onClick={onClose} aria-label="Close changelog">
|
|
58
|
-
<IconX size={18} stroke={1.5} />
|
|
59
|
-
</button>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
|
|
63
|
-
<div className={styles.body}>
|
|
64
|
-
{loading && <div className={styles.empty}>Loading changelog…</div>}
|
|
65
|
-
{error && !loading && <div className={styles.empty}>Failed to load changelog: {error}</div>}
|
|
66
|
-
|
|
67
|
-
{!loading && !error && phase === 'index' && (
|
|
68
|
-
<div className={styles.indexWrap}>
|
|
69
|
-
<p className={styles.indexHint}>
|
|
70
|
-
{sortedVersions.length} versions. Click any to read the release notes.
|
|
71
|
-
</p>
|
|
72
|
-
<ul className={styles.versionList}>
|
|
73
|
-
{sortedVersions.map((version) => {
|
|
74
|
-
const changes = getVersion(version)
|
|
75
|
-
const summary = []
|
|
76
|
-
if (changes?.added?.length) summary.push(`${changes.added.length} added`)
|
|
77
|
-
if (changes?.fixed?.length) summary.push(`${changes.fixed.length} fixed`)
|
|
78
|
-
if (changes?.changed?.length) summary.push(`${changes.changed.length} changed`)
|
|
79
|
-
return (
|
|
80
|
-
<li key={version}>
|
|
81
|
-
<button
|
|
82
|
-
className={styles.versionBtn}
|
|
83
|
-
onClick={() => openDetails(version)}
|
|
84
|
-
>
|
|
85
|
-
<span className={styles.versionLabel}>v{version}</span>
|
|
86
|
-
<span className={styles.versionSummary}>
|
|
87
|
-
{summary.length > 0 ? summary.join(' · ') : '—'}
|
|
88
|
-
</span>
|
|
89
|
-
</button>
|
|
90
|
-
</li>
|
|
91
|
-
)
|
|
92
|
-
})}
|
|
93
|
-
</ul>
|
|
94
|
-
</div>
|
|
95
|
-
)}
|
|
96
|
-
|
|
97
|
-
{!loading && !error && phase === 'details' && details && (
|
|
98
|
-
<div className={styles.details}>
|
|
99
|
-
{SECTION_LABELS.map(({ key, label }) => {
|
|
100
|
-
const items = details[key]
|
|
101
|
-
if (!items || items.length === 0) return null
|
|
102
|
-
return (
|
|
103
|
-
<section key={key} className={styles.section}>
|
|
104
|
-
<h3 className={styles.sectionTitle}>{label}</h3>
|
|
105
|
-
<ul className={styles.itemList}>
|
|
106
|
-
{items.map((item, idx) => (
|
|
107
|
-
<li key={idx} className={styles.item}>{formatItem(item)}</li>
|
|
108
|
-
))}
|
|
109
|
-
</ul>
|
|
110
|
-
</section>
|
|
111
|
-
)
|
|
112
|
-
})}
|
|
113
|
-
{SECTION_LABELS.every(({ key }) => !details[key]?.length) && (
|
|
114
|
-
<div className={styles.empty}>No release notes for v{selectedVersion}.</div>
|
|
115
|
-
)}
|
|
116
|
-
</div>
|
|
117
|
-
)}
|
|
118
|
-
|
|
119
|
-
{!loading && !error && phase === 'details' && !details && (
|
|
120
|
-
<div className={styles.empty}>No notes for v{selectedVersion}.</div>
|
|
121
|
-
)}
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// 📖 Strip **bold** and `code` markdown markers for the Web view. The TUI does
|
|
129
|
-
// 📖 the same in formatChangelogForDisplay, so the two surfaces stay in sync.
|
|
130
|
-
function formatItem(text) {
|
|
131
|
-
return text
|
|
132
|
-
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
133
|
-
.replace(/`([^`]+)`/g, '$1')
|
|
134
|
-
.trim()
|
|
135
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/components/changelog/ChangelogView.module.css
|
|
3
|
-
* @description Two-phase changelog modal styles (TUI parity).
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
.backdrop {
|
|
7
|
-
display: flex;
|
|
8
|
-
flex-direction: column;
|
|
9
|
-
flex: 1;
|
|
10
|
-
min-height: 0;
|
|
11
|
-
width: 100%;
|
|
12
|
-
padding: 24px;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.modal {
|
|
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);
|
|
21
|
-
border-radius: 14px;
|
|
22
|
-
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
23
|
-
display: flex;
|
|
24
|
-
flex-direction: column;
|
|
25
|
-
overflow: hidden;
|
|
26
|
-
flex: 1;
|
|
27
|
-
min-height: 0;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
.header {
|
|
31
|
-
padding: 16px 20px;
|
|
32
|
-
border-bottom: 1px solid var(--color-border);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.titleRow {
|
|
36
|
-
display: flex;
|
|
37
|
-
align-items: center;
|
|
38
|
-
gap: 12px;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.title {
|
|
42
|
-
font-size: 16px;
|
|
43
|
-
font-weight: 700;
|
|
44
|
-
margin: 0;
|
|
45
|
-
color: var(--color-text);
|
|
46
|
-
flex: 1;
|
|
47
|
-
min-width: 0;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.backBtn {
|
|
51
|
-
display: inline-flex;
|
|
52
|
-
align-items: center;
|
|
53
|
-
gap: 4px;
|
|
54
|
-
padding: 4px 10px;
|
|
55
|
-
background: var(--color-surface);
|
|
56
|
-
color: var(--color-text);
|
|
57
|
-
border: 1px solid var(--color-border);
|
|
58
|
-
border-radius: 6px;
|
|
59
|
-
font-size: 11px;
|
|
60
|
-
font-weight: 600;
|
|
61
|
-
cursor: pointer;
|
|
62
|
-
font-family: var(--font-sans);
|
|
63
|
-
white-space: nowrap;
|
|
64
|
-
}
|
|
65
|
-
.backBtn:hover { background: var(--color-bg-hover); border-color: var(--color-text-muted); }
|
|
66
|
-
|
|
67
|
-
.closeBtn {
|
|
68
|
-
background: transparent;
|
|
69
|
-
border: none;
|
|
70
|
-
color: var(--color-text-muted);
|
|
71
|
-
cursor: pointer;
|
|
72
|
-
padding: 4px;
|
|
73
|
-
border-radius: 4px;
|
|
74
|
-
display: inline-flex;
|
|
75
|
-
align-items: center;
|
|
76
|
-
justify-content: center;
|
|
77
|
-
flex-shrink: 0;
|
|
78
|
-
}
|
|
79
|
-
.closeBtn:hover { color: var(--color-text); background: var(--color-bg-hover); }
|
|
80
|
-
|
|
81
|
-
.body {
|
|
82
|
-
flex: 1;
|
|
83
|
-
overflow-y: auto;
|
|
84
|
-
padding: 16px 20px 20px;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
.indexWrap {}
|
|
88
|
-
.indexHint {
|
|
89
|
-
margin: 0 0 12px;
|
|
90
|
-
font-size: 11px;
|
|
91
|
-
color: var(--color-text-muted);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
.versionList {
|
|
95
|
-
list-style: none;
|
|
96
|
-
margin: 0;
|
|
97
|
-
padding: 0;
|
|
98
|
-
display: flex;
|
|
99
|
-
flex-direction: column;
|
|
100
|
-
gap: 4px;
|
|
101
|
-
}
|
|
102
|
-
.versionBtn {
|
|
103
|
-
display: flex;
|
|
104
|
-
align-items: center;
|
|
105
|
-
justify-content: space-between;
|
|
106
|
-
gap: 12px;
|
|
107
|
-
width: 100%;
|
|
108
|
-
padding: 8px 12px;
|
|
109
|
-
background: var(--color-surface);
|
|
110
|
-
border: 1px solid var(--color-border);
|
|
111
|
-
border-radius: 6px;
|
|
112
|
-
cursor: pointer;
|
|
113
|
-
text-align: left;
|
|
114
|
-
transition: all 100ms;
|
|
115
|
-
font-family: var(--font-sans);
|
|
116
|
-
}
|
|
117
|
-
.versionBtn:hover { background: var(--color-bg-hover); border-color: var(--color-text-muted); }
|
|
118
|
-
.versionLabel {
|
|
119
|
-
font-family: var(--font-mono);
|
|
120
|
-
font-size: 13px;
|
|
121
|
-
font-weight: 700;
|
|
122
|
-
color: var(--color-text);
|
|
123
|
-
}
|
|
124
|
-
.versionSummary {
|
|
125
|
-
font-size: 11px;
|
|
126
|
-
color: var(--color-text-muted);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.details { display: flex; flex-direction: column; gap: 14px; }
|
|
130
|
-
.section {}
|
|
131
|
-
.sectionTitle {
|
|
132
|
-
font-size: 12px;
|
|
133
|
-
font-weight: 700;
|
|
134
|
-
text-transform: uppercase;
|
|
135
|
-
letter-spacing: 0.6px;
|
|
136
|
-
color: var(--color-text-muted);
|
|
137
|
-
margin: 0 0 6px;
|
|
138
|
-
}
|
|
139
|
-
.itemList {
|
|
140
|
-
list-style: disc;
|
|
141
|
-
padding-left: 20px;
|
|
142
|
-
margin: 0;
|
|
143
|
-
display: flex;
|
|
144
|
-
flex-direction: column;
|
|
145
|
-
gap: 4px;
|
|
146
|
-
}
|
|
147
|
-
.item {
|
|
148
|
-
font-size: 12px;
|
|
149
|
-
color: var(--color-text);
|
|
150
|
-
line-height: 1.55;
|
|
151
|
-
word-break: break-word;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
.empty {
|
|
155
|
-
text-align: center;
|
|
156
|
-
padding: 40px 20px;
|
|
157
|
-
color: var(--color-text-dim);
|
|
158
|
-
font-size: 13px;
|
|
159
|
-
}
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/components/dashboard/DetailPanel.jsx
|
|
3
|
-
* @description Slide-in detail panel showing full model stats, metrics, and a latency chart.
|
|
4
|
-
* 📖 Rendered as a fixed overlay on the right side. Controlled by `model` prop (null = hidden).
|
|
5
|
-
* Displays model ID, provider, tier, SWE score, context, status, pings, stability, verdict, uptime,
|
|
6
|
-
* ping count, API key status, a larger SVG latency trend chart, and an M1 favorites + benchmark
|
|
7
|
-
* 📖 action section (star + reorder + per-row AI Speed Test button).
|
|
8
|
-
* @functions DetailPanel → main panel component, buildDetailChart → SVG chart builder
|
|
9
|
-
*/
|
|
10
|
-
import { IconStar, IconStarFilled, IconChevronUp, IconChevronDown, IconPlayerPlayFilled, IconAlertTriangle } from '@tabler/icons-react'
|
|
11
|
-
import TierBadge from '../atoms/TierBadge.jsx'
|
|
12
|
-
import VerdictBadge from '../atoms/VerdictBadge.jsx'
|
|
13
|
-
import StatusDot from '../atoms/StatusDot.jsx'
|
|
14
|
-
import StabilityCell from '../atoms/StabilityCell.jsx'
|
|
15
|
-
import { formatPing, formatAvg, pingClass } from '../../utils/format.js'
|
|
16
|
-
import { sweClass } from '../../utils/ranks.js'
|
|
17
|
-
import LaunchButton from '../launch/LaunchButton.jsx'
|
|
18
|
-
import ToolPicker from '../tools/ToolPicker.jsx'
|
|
19
|
-
import { getToolMeta, isModelCompatibleWithTool } from '../../../../src/core/tool-metadata.js'
|
|
20
|
-
import styles from './DetailPanel.module.css'
|
|
21
|
-
|
|
22
|
-
function buildDetailChart(history) {
|
|
23
|
-
if (!history || history.length < 2) {
|
|
24
|
-
return <div className={styles.chartEmpty}>Waiting for ping data...</div>
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const valid = history.filter(p => p.code === '200' || p.code === '401')
|
|
28
|
-
if (valid.length < 2) {
|
|
29
|
-
return <div className={styles.chartEmpty}>Not enough data yet...</div>
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const values = valid.map(p => p.ms)
|
|
33
|
-
const max = Math.max(...values, 1)
|
|
34
|
-
const min = Math.min(...values, 0)
|
|
35
|
-
const range = max - min || 1
|
|
36
|
-
const w = 340, h = 100
|
|
37
|
-
const padding = 4
|
|
38
|
-
|
|
39
|
-
const points = values.map((v, i) => {
|
|
40
|
-
const x = padding + i * ((w - 2 * padding) / (values.length - 1))
|
|
41
|
-
const y = padding + (h - 2 * padding) - ((v - min) / range) * (h - 2 * padding)
|
|
42
|
-
return [x.toFixed(1), y.toFixed(1)]
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
const linePoints = points.map(p => p.join(',')).join(' ')
|
|
46
|
-
const areaPoints = `${points[0][0]},${h - padding} ${linePoints} ${points[points.length - 1][0]},${h - padding}`
|
|
47
|
-
const lastPt = points[points.length - 1]
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<svg width="100%" viewBox={`0 0 ${w} ${h}`} style={{ display: 'block' }}>
|
|
51
|
-
<defs>
|
|
52
|
-
<linearGradient id="chart-grad" x1="0" y1="0" x2="0" y2="1">
|
|
53
|
-
<stop offset="0%" stopColor="var(--color-accent)" stopOpacity="0.3" />
|
|
54
|
-
<stop offset="100%" stopColor="var(--color-accent)" stopOpacity="0.02" />
|
|
55
|
-
</linearGradient>
|
|
56
|
-
</defs>
|
|
57
|
-
<polygon fill="url(#chart-grad)" points={areaPoints} />
|
|
58
|
-
<polyline fill="none" stroke="var(--color-accent)" strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" points={linePoints} />
|
|
59
|
-
<circle cx={lastPt[0]} cy={lastPt[1]} r="3.5" fill="var(--color-accent)" stroke="var(--color-bg)" strokeWidth="1.5" />
|
|
60
|
-
<text x={padding} y={h - 2} fontSize="9" fill="var(--color-text-dim)" fontFamily="var(--font-mono)">{min}ms</text>
|
|
61
|
-
<text x={w - padding} y={padding + 8} fontSize="9" fill="var(--color-text-dim)" fontFamily="var(--font-mono)" textAnchor="end">{max}ms</text>
|
|
62
|
-
</svg>
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function StatRow({ label, children }) {
|
|
67
|
-
return (
|
|
68
|
-
<div className={styles.stat}>
|
|
69
|
-
<span className={styles.statLabel}>{label}</span>
|
|
70
|
-
<span className={styles.statValue}>{children}</span>
|
|
71
|
-
</div>
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export default function DetailPanel({
|
|
76
|
-
model, onClose, favorites, onBenchmark, onLaunch, onToast,
|
|
77
|
-
toolMode = 'opencode', onSetToolMode, onCycleToolMode, onOpenFallback,
|
|
78
|
-
}) {
|
|
79
|
-
if (!model) return null
|
|
80
|
-
|
|
81
|
-
const chartSvg = buildDetailChart(model.pingHistory)
|
|
82
|
-
const isFav = favorites?.isFavorite(model) ?? false
|
|
83
|
-
const favRank = favorites?.favoriteRank(model) ?? Number.MAX_SAFE_INTEGER
|
|
84
|
-
const isFavAtTop = isFav && favRank === 0
|
|
85
|
-
const isFavAtBottom = isFav && favRank === (favorites?.favorites.length ?? 0) - 1
|
|
86
|
-
const toolMeta = getToolMeta(toolMode)
|
|
87
|
-
const compatible = isModelCompatibleWithTool(model.providerKey, toolMode)
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<div className={`${styles.panel} ${styles.panelOpen}`}>
|
|
91
|
-
<div className={styles.header}>
|
|
92
|
-
<h3 className={styles.title}>{model.label}</h3>
|
|
93
|
-
<button className={styles.closeBtn} onClick={onClose}>×</button>
|
|
94
|
-
</div>
|
|
95
|
-
<div className={styles.body}>
|
|
96
|
-
<StatRow label="Model ID">
|
|
97
|
-
<span style={{ fontSize: 11, wordBreak: 'break-all' }}>{model.modelId}</span>
|
|
98
|
-
</StatRow>
|
|
99
|
-
<StatRow label="Provider">{model.origin}</StatRow>
|
|
100
|
-
<StatRow label="Tier"><TierBadge tier={model.tier} /></StatRow>
|
|
101
|
-
<StatRow label="SWE-bench Score">
|
|
102
|
-
<span className={`${styles.swe} ${styles[sweClass(model.sweScore)]}`}>{model.sweScore || '—'}</span>
|
|
103
|
-
</StatRow>
|
|
104
|
-
<StatRow label="Context Window">{model.ctx || '—'}</StatRow>
|
|
105
|
-
<StatRow label="Status">
|
|
106
|
-
<StatusDot status={model.status} /> {model.status}
|
|
107
|
-
</StatRow>
|
|
108
|
-
<StatRow label="Latest Ping">
|
|
109
|
-
<span className={`${styles.ping} ${styles[pingClass(model.latestPing)]}`}>
|
|
110
|
-
{formatPing(model.latestPing, model.latestCode).text}
|
|
111
|
-
</span>
|
|
112
|
-
</StatRow>
|
|
113
|
-
<StatRow label="Average Latency">
|
|
114
|
-
<span className={`${styles.ping} ${styles[pingClass(model.avg)]}`}>
|
|
115
|
-
{formatAvg(model.avg).text}
|
|
116
|
-
</span>
|
|
117
|
-
</StatRow>
|
|
118
|
-
<StatRow label="P95 Latency">
|
|
119
|
-
{model.p95 != null && model.p95 !== Infinity ? `${model.p95}ms` : '—'}
|
|
120
|
-
</StatRow>
|
|
121
|
-
<StatRow label="Jitter (σ)">
|
|
122
|
-
{model.jitter != null && model.jitter !== Infinity ? `${model.jitter}ms` : '—'}
|
|
123
|
-
</StatRow>
|
|
124
|
-
<StatRow label="Stability Score">
|
|
125
|
-
<StabilityCell score={model.stability} />
|
|
126
|
-
</StatRow>
|
|
127
|
-
<StatRow label="Verdict">
|
|
128
|
-
<VerdictBadge verdict={model.verdict} httpCode={model.httpCode} />
|
|
129
|
-
</StatRow>
|
|
130
|
-
<StatRow label="Uptime">
|
|
131
|
-
{model.uptime > 0 ? `${model.uptime}%` : '—'}
|
|
132
|
-
</StatRow>
|
|
133
|
-
<StatRow label="Ping Count">{model.pingCount}</StatRow>
|
|
134
|
-
<StatRow label="API Key">
|
|
135
|
-
{model.hasApiKey ? '✅ Configured' : '❌ Missing'}
|
|
136
|
-
</StatRow>
|
|
137
|
-
<StatRow label="Tool">
|
|
138
|
-
<ToolPicker
|
|
139
|
-
compact
|
|
140
|
-
toolMode={toolMode}
|
|
141
|
-
onSetToolMode={onSetToolMode}
|
|
142
|
-
onCycleToolMode={onCycleToolMode}
|
|
143
|
-
/>
|
|
144
|
-
</StatRow>
|
|
145
|
-
{!compatible && (
|
|
146
|
-
<div className={styles.compatWarning}>
|
|
147
|
-
<IconAlertTriangle size={14} />
|
|
148
|
-
<span>{model.label} is not compatible with {toolMeta.emoji} {toolMeta.label}.</span>
|
|
149
|
-
<button onClick={() => onOpenFallback?.(model)}>Install in compatible tool</button>
|
|
150
|
-
</div>
|
|
151
|
-
)}
|
|
152
|
-
|
|
153
|
-
{/* ── M1/M3: favorites + benchmark + endpoint install action section ── */}
|
|
154
|
-
{(favorites || onBenchmark) && (
|
|
155
|
-
<div className={styles.actions}>
|
|
156
|
-
{favorites && (
|
|
157
|
-
<div className={styles.favBlock}>
|
|
158
|
-
<button
|
|
159
|
-
className={`${styles.favBtn} ${isFav ? styles.favBtnActive : ''}`}
|
|
160
|
-
onClick={() => favorites.toggle(model)}
|
|
161
|
-
title={isFav ? `Unfavorite ${model.label} (TUI: F)` : `Favorite ${model.label} (TUI: F)`}
|
|
162
|
-
>
|
|
163
|
-
{isFav ? <IconStarFilled size={14} stroke={1.5} /> : <IconStar size={14} stroke={1.5} />}
|
|
164
|
-
<span>{isFav ? 'Favorited' : 'Add to favorites'}</span>
|
|
165
|
-
</button>
|
|
166
|
-
{isFav && (
|
|
167
|
-
<div className={styles.reorderRow}>
|
|
168
|
-
<button
|
|
169
|
-
className={styles.reorderBtn}
|
|
170
|
-
disabled={isFavAtTop}
|
|
171
|
-
onClick={() => favorites.reorder(model, 'up')}
|
|
172
|
-
title="Move up in router priority (TUI: Shift+↑)"
|
|
173
|
-
aria-label="Move favorite up"
|
|
174
|
-
>
|
|
175
|
-
<IconChevronUp size={12} stroke={1.5} /> Up
|
|
176
|
-
</button>
|
|
177
|
-
<button
|
|
178
|
-
className={styles.reorderBtn}
|
|
179
|
-
disabled={isFavAtBottom}
|
|
180
|
-
onClick={() => favorites.reorder(model, 'down')}
|
|
181
|
-
title="Move down in router priority (TUI: Shift+↓)"
|
|
182
|
-
aria-label="Move favorite down"
|
|
183
|
-
>
|
|
184
|
-
<IconChevronDown size={12} stroke={1.5} /> Down
|
|
185
|
-
</button>
|
|
186
|
-
<span className={styles.rankBadge}>#{favRank + 1}</span>
|
|
187
|
-
</div>
|
|
188
|
-
)}
|
|
189
|
-
</div>
|
|
190
|
-
)}
|
|
191
|
-
{onLaunch && (
|
|
192
|
-
<LaunchButton model={model} toolMode={toolMode} onLaunch={onLaunch} />
|
|
193
|
-
)}
|
|
194
|
-
{onBenchmark && (
|
|
195
|
-
<button
|
|
196
|
-
className={styles.benchBtn}
|
|
197
|
-
onClick={() => {
|
|
198
|
-
onBenchmark(model)
|
|
199
|
-
onToast?.(`Benchmark started for ${model.label}…`, 'info')
|
|
200
|
-
}}
|
|
201
|
-
title="Run AI Speed Test on this model (TUI: Ctrl+A)"
|
|
202
|
-
>
|
|
203
|
-
<IconPlayerPlayFilled size={12} stroke={1.5} />
|
|
204
|
-
<span>AI Speed Test</span>
|
|
205
|
-
</button>
|
|
206
|
-
)}
|
|
207
|
-
</div>
|
|
208
|
-
)}
|
|
209
|
-
|
|
210
|
-
<div className={styles.chart}>
|
|
211
|
-
<div className={styles.chartTitle}>Latency Trend (last 20 pings)</div>
|
|
212
|
-
{chartSvg}
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
</div>
|
|
216
|
-
)
|
|
217
|
-
}
|