free-coding-models 0.5.5 → 0.5.8
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 +19 -11
- package/bin/free-coding-models.js +6 -0
- package/changelog/v0.5.5.md +1 -0
- package/changelog/v0.5.6.md +24 -0
- package/changelog/v0.5.7.md +20 -0
- package/package.json +4 -4
- package/src/core/changelog-loader.js +5 -1
- package/src/core/endpoint-installer.js +3 -3
- package/src/core/router-daemon.js +11 -0
- package/src/core/tool-launchers.js +2 -2
- package/web/dist/assets/index-DKmmiDip.css +1 -0
- package/web/dist/assets/index-DzVt42Nf.js +39 -0
- package/web/dist/index.html +2 -2
- package/web/server.js +532 -2
- package/web/src/App.jsx +235 -73
- package/web/src/components/analytics/AnalyticsView.jsx +4 -0
- package/web/src/components/analytics/TokenUsagePanel.jsx +105 -0
- package/web/src/components/analytics/TokenUsagePanel.module.css +126 -0
- package/web/src/components/atoms/TierBadge.module.css +3 -3
- package/web/src/components/changelog/ChangelogView.jsx +135 -0
- package/web/src/components/changelog/ChangelogView.module.css +160 -0
- package/web/src/components/dashboard/DetailPanel.jsx +29 -4
- package/web/src/components/dashboard/DetailPanel.module.css +26 -0
- package/web/src/components/dashboard/FilterBar.jsx +17 -2
- package/web/src/components/dashboard/FilterBar.module.css +17 -0
- package/web/src/components/dashboard/ModelTable.jsx +17 -5
- package/web/src/components/dashboard/ModelTable.module.css +14 -1
- package/web/src/components/help/HelpView.jsx +188 -0
- package/web/src/components/help/HelpView.module.css +157 -0
- package/web/src/components/install/InstallEndpointsView.jsx +278 -0
- package/web/src/components/install/InstallEndpointsView.module.css +351 -0
- package/web/src/components/installed/InstalledModelsView.jsx +89 -0
- package/web/src/components/installed/InstalledModelsView.module.css +200 -0
- package/web/src/components/launch/IncompatibleFallbackModal.jsx +69 -0
- package/web/src/components/launch/LaunchButton.jsx +30 -0
- package/web/src/components/launch/LaunchButton.module.css +35 -0
- package/web/src/components/launch/LaunchModal.module.css +125 -0
- package/web/src/components/layout/Header.jsx +78 -14
- package/web/src/components/layout/Header.module.css +62 -2
- package/web/src/components/palette/CommandPalette.jsx +228 -74
- package/web/src/components/recommend/RecommendView.jsx +121 -0
- package/web/src/components/recommend/RecommendView.module.css +55 -0
- package/web/src/components/router/RouterView.jsx +286 -0
- package/web/src/components/router/RouterView.module.css +357 -0
- package/web/src/components/settings/SettingsView.jsx +281 -8
- package/web/src/components/settings/SettingsView.module.css +174 -0
- package/web/src/components/tools/ToolPicker.jsx +86 -0
- package/web/src/components/tools/ToolPicker.module.css +84 -0
- package/web/src/components/update/UpdateChip.jsx +104 -0
- package/web/src/components/update/UpdateChip.module.css +146 -0
- package/web/src/global.css +23 -0
- package/web/src/hooks/urlState.constants.js +28 -0
- package/web/src/hooks/useChangelog.js +51 -0
- package/web/src/hooks/useInstalledModels.js +42 -0
- package/web/src/hooks/useRecommend.js +67 -0
- package/web/src/hooks/useRouterDashboard.js +133 -0
- package/web/src/hooks/useTokenUsage.js +103 -0
- package/web/src/hooks/useToolMode.js +77 -0
- package/web/src/hooks/useUpdateChecker.js +91 -0
- package/web/src/hooks/useUrlState.js +123 -62
- package/web/src/utils/m3.js +55 -0
- package/web/dist/assets/index-uD3faN3G.js +0 -39
- package/web/dist/assets/index-uTifVKX1.css +0 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
.card {
|
|
2
|
+
margin-top: 16px;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.cardTitle {
|
|
6
|
+
font-size: 13px;
|
|
7
|
+
font-weight: 600;
|
|
8
|
+
color: var(--text-secondary, #aaa);
|
|
9
|
+
margin: 0 0 10px 0;
|
|
10
|
+
text-transform: uppercase;
|
|
11
|
+
letter-spacing: 0.5px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.empty {
|
|
15
|
+
color: var(--text-muted, #666);
|
|
16
|
+
font-size: 13px;
|
|
17
|
+
padding: 20px 0;
|
|
18
|
+
text-align: center;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Summary Grid */
|
|
22
|
+
.summaryGrid {
|
|
23
|
+
display: grid;
|
|
24
|
+
grid-template-columns: 1fr 1fr;
|
|
25
|
+
gap: 12px;
|
|
26
|
+
margin-top: 16px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.summaryCard {
|
|
30
|
+
background: var(--bg-secondary, #14141f);
|
|
31
|
+
border: 1px solid var(--border, #1e1e2e);
|
|
32
|
+
border-radius: 8px;
|
|
33
|
+
padding: 14px 16px;
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
gap: 2px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.summaryLabel {
|
|
40
|
+
font-size: 11px;
|
|
41
|
+
font-weight: 600;
|
|
42
|
+
color: var(--text-muted, #888);
|
|
43
|
+
text-transform: uppercase;
|
|
44
|
+
letter-spacing: 0.5px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.summaryValue {
|
|
48
|
+
font-size: 22px;
|
|
49
|
+
font-weight: 700;
|
|
50
|
+
color: var(--text-primary, #e0e0e0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.summarySub {
|
|
54
|
+
font-size: 11px;
|
|
55
|
+
color: var(--text-muted, #888);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* 7-Day Chart */
|
|
59
|
+
.chart {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: flex-end;
|
|
62
|
+
gap: 6px;
|
|
63
|
+
height: 100px;
|
|
64
|
+
padding: 0 4px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.chartBar {
|
|
68
|
+
flex: 1;
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
align-items: center;
|
|
72
|
+
height: 100%;
|
|
73
|
+
justify-content: flex-end;
|
|
74
|
+
gap: 4px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.chartFill {
|
|
78
|
+
width: 100%;
|
|
79
|
+
min-height: 2px;
|
|
80
|
+
background: linear-gradient(180deg, #00c864 0%, rgba(0, 200, 100, 0.3) 100%);
|
|
81
|
+
border-radius: 3px 3px 0 0;
|
|
82
|
+
transition: height 0.3s ease;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.chartLabel {
|
|
86
|
+
font-size: 10px;
|
|
87
|
+
color: var(--text-muted, #888);
|
|
88
|
+
text-transform: uppercase;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Top Lists */
|
|
92
|
+
.topList {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: 4px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.topRow {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: 8px;
|
|
102
|
+
padding: 4px 0;
|
|
103
|
+
font-size: 12px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.topKey {
|
|
107
|
+
flex: 1;
|
|
108
|
+
color: var(--text-primary, #e0e0e0);
|
|
109
|
+
font-family: 'SF Mono', monospace;
|
|
110
|
+
overflow: hidden;
|
|
111
|
+
text-overflow: ellipsis;
|
|
112
|
+
white-space: nowrap;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.topVal {
|
|
116
|
+
color: var(--text-muted, #888);
|
|
117
|
+
min-width: 80px;
|
|
118
|
+
text-align: right;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.topReq {
|
|
122
|
+
color: var(--text-muted, #666);
|
|
123
|
+
min-width: 50px;
|
|
124
|
+
text-align: right;
|
|
125
|
+
font-size: 11px;
|
|
126
|
+
}
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
.tier_aplus { background: rgba(0,200,255,0.1); color: #00c8ff; border: 1px solid rgba(0,200,255,0.2); }
|
|
14
14
|
.tier_a { background: rgba(61,220,132,0.1); color: #3ddc84; border: 1px solid rgba(61,220,132,0.2); }
|
|
15
15
|
.tier_aminus{ background: rgba(126,207,126,0.1); color: #7ecf7e; border: 1px solid rgba(126,207,126,0.2); }
|
|
16
|
-
.tier_bplus { background: rgba(168,168,200,0.1); color: #
|
|
17
|
-
.tier_b { background: rgba(128,128,152,0.1); color: #
|
|
18
|
-
.tier_c { background: rgba(96,96,120,0.1); color: #
|
|
16
|
+
.tier_bplus { background: rgba(168,168,200,0.1); color: #b8b8d8; border: 1px solid rgba(168,168,200,0.15); }
|
|
17
|
+
.tier_b { background: rgba(128,128,152,0.1); color: #a0a0b8; border: 1px solid rgba(128,128,152,0.15); }
|
|
18
|
+
.tier_c { background: rgba(96,96,120,0.1); color: #909098; border: 1px solid rgba(96,96,120,0.15); }
|
|
19
19
|
|
|
20
20
|
/* 📖 Light theme: darker, AA-contrast tier text colors. Borders stay the same
|
|
21
21
|
📖 hue so the badge still feels "tier-colored", just more legible. */
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/changelog/ChangelogView.module.css
|
|
3
|
+
* @description Two-phase changelog modal styles (TUI parity).
|
|
4
|
+
*/
|
|
5
|
+
|
|
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
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
padding: 5vh 4vw;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.modal {
|
|
20
|
+
width: min(720px, 95vw);
|
|
21
|
+
max-height: 88vh;
|
|
22
|
+
background: var(--color-bg-elevated);
|
|
23
|
+
border: 1px solid var(--color-border-hover);
|
|
24
|
+
border-radius: 14px;
|
|
25
|
+
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.header {
|
|
32
|
+
padding: 16px 20px;
|
|
33
|
+
border-bottom: 1px solid var(--color-border);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.titleRow {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: 12px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.title {
|
|
43
|
+
font-size: 16px;
|
|
44
|
+
font-weight: 700;
|
|
45
|
+
margin: 0;
|
|
46
|
+
color: var(--color-text);
|
|
47
|
+
flex: 1;
|
|
48
|
+
min-width: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.backBtn {
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: 4px;
|
|
55
|
+
padding: 4px 10px;
|
|
56
|
+
background: var(--color-surface);
|
|
57
|
+
color: var(--color-text);
|
|
58
|
+
border: 1px solid var(--color-border);
|
|
59
|
+
border-radius: 6px;
|
|
60
|
+
font-size: 11px;
|
|
61
|
+
font-weight: 600;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
font-family: var(--font-sans);
|
|
64
|
+
white-space: nowrap;
|
|
65
|
+
}
|
|
66
|
+
.backBtn:hover { background: var(--color-bg-hover); border-color: var(--color-text-muted); }
|
|
67
|
+
|
|
68
|
+
.closeBtn {
|
|
69
|
+
background: transparent;
|
|
70
|
+
border: none;
|
|
71
|
+
color: var(--color-text-muted);
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
padding: 4px;
|
|
74
|
+
border-radius: 4px;
|
|
75
|
+
display: inline-flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
flex-shrink: 0;
|
|
79
|
+
}
|
|
80
|
+
.closeBtn:hover { color: var(--color-text); background: var(--color-bg-hover); }
|
|
81
|
+
|
|
82
|
+
.body {
|
|
83
|
+
flex: 1;
|
|
84
|
+
overflow-y: auto;
|
|
85
|
+
padding: 16px 20px 20px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.indexWrap {}
|
|
89
|
+
.indexHint {
|
|
90
|
+
margin: 0 0 12px;
|
|
91
|
+
font-size: 11px;
|
|
92
|
+
color: var(--color-text-muted);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.versionList {
|
|
96
|
+
list-style: none;
|
|
97
|
+
margin: 0;
|
|
98
|
+
padding: 0;
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: 4px;
|
|
102
|
+
}
|
|
103
|
+
.versionBtn {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: space-between;
|
|
107
|
+
gap: 12px;
|
|
108
|
+
width: 100%;
|
|
109
|
+
padding: 8px 12px;
|
|
110
|
+
background: var(--color-surface);
|
|
111
|
+
border: 1px solid var(--color-border);
|
|
112
|
+
border-radius: 6px;
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
text-align: left;
|
|
115
|
+
transition: all 100ms;
|
|
116
|
+
font-family: var(--font-sans);
|
|
117
|
+
}
|
|
118
|
+
.versionBtn:hover { background: var(--color-bg-hover); border-color: var(--color-text-muted); }
|
|
119
|
+
.versionLabel {
|
|
120
|
+
font-family: var(--font-mono);
|
|
121
|
+
font-size: 13px;
|
|
122
|
+
font-weight: 700;
|
|
123
|
+
color: var(--color-text);
|
|
124
|
+
}
|
|
125
|
+
.versionSummary {
|
|
126
|
+
font-size: 11px;
|
|
127
|
+
color: var(--color-text-muted);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.details { display: flex; flex-direction: column; gap: 14px; }
|
|
131
|
+
.section {}
|
|
132
|
+
.sectionTitle {
|
|
133
|
+
font-size: 12px;
|
|
134
|
+
font-weight: 700;
|
|
135
|
+
text-transform: uppercase;
|
|
136
|
+
letter-spacing: 0.6px;
|
|
137
|
+
color: var(--color-text-muted);
|
|
138
|
+
margin: 0 0 6px;
|
|
139
|
+
}
|
|
140
|
+
.itemList {
|
|
141
|
+
list-style: disc;
|
|
142
|
+
padding-left: 20px;
|
|
143
|
+
margin: 0;
|
|
144
|
+
display: flex;
|
|
145
|
+
flex-direction: column;
|
|
146
|
+
gap: 4px;
|
|
147
|
+
}
|
|
148
|
+
.item {
|
|
149
|
+
font-size: 12px;
|
|
150
|
+
color: var(--color-text);
|
|
151
|
+
line-height: 1.55;
|
|
152
|
+
word-break: break-word;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.empty {
|
|
156
|
+
text-align: center;
|
|
157
|
+
padding: 40px 20px;
|
|
158
|
+
color: var(--color-text-dim);
|
|
159
|
+
font-size: 13px;
|
|
160
|
+
}
|
|
@@ -7,14 +7,16 @@
|
|
|
7
7
|
* 📖 action section (star + reorder + per-row AI Speed Test button).
|
|
8
8
|
* @functions DetailPanel → main panel component, buildDetailChart → SVG chart builder
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import { IconStar, IconStarFilled, IconChevronUp, IconChevronDown, IconPlayerPlayFilled } from '@tabler/icons-react'
|
|
10
|
+
import { IconStar, IconStarFilled, IconChevronUp, IconChevronDown, IconPlayerPlayFilled, IconAlertTriangle } from '@tabler/icons-react'
|
|
12
11
|
import TierBadge from '../atoms/TierBadge.jsx'
|
|
13
12
|
import VerdictBadge from '../atoms/VerdictBadge.jsx'
|
|
14
13
|
import StatusDot from '../atoms/StatusDot.jsx'
|
|
15
14
|
import StabilityCell from '../atoms/StabilityCell.jsx'
|
|
16
15
|
import { formatPing, formatAvg, pingClass } from '../../utils/format.js'
|
|
17
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'
|
|
18
20
|
import styles from './DetailPanel.module.css'
|
|
19
21
|
|
|
20
22
|
function buildDetailChart(history) {
|
|
@@ -70,7 +72,10 @@ function StatRow({ label, children }) {
|
|
|
70
72
|
)
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
export default function DetailPanel({
|
|
75
|
+
export default function DetailPanel({
|
|
76
|
+
model, onClose, favorites, onBenchmark, onLaunch, onToast,
|
|
77
|
+
toolMode = 'opencode', onSetToolMode, onCycleToolMode, onOpenFallback,
|
|
78
|
+
}) {
|
|
74
79
|
if (!model) return null
|
|
75
80
|
|
|
76
81
|
const chartSvg = buildDetailChart(model.pingHistory)
|
|
@@ -78,6 +83,8 @@ export default function DetailPanel({ model, onClose, favorites, onBenchmark, on
|
|
|
78
83
|
const favRank = favorites?.favoriteRank(model) ?? Number.MAX_SAFE_INTEGER
|
|
79
84
|
const isFavAtTop = isFav && favRank === 0
|
|
80
85
|
const isFavAtBottom = isFav && favRank === (favorites?.favorites.length ?? 0) - 1
|
|
86
|
+
const toolMeta = getToolMeta(toolMode)
|
|
87
|
+
const compatible = isModelCompatibleWithTool(model.providerKey, toolMode)
|
|
81
88
|
|
|
82
89
|
return (
|
|
83
90
|
<div className={styles.panel}>
|
|
@@ -127,8 +134,23 @@ export default function DetailPanel({ model, onClose, favorites, onBenchmark, on
|
|
|
127
134
|
<StatRow label="API Key">
|
|
128
135
|
{model.hasApiKey ? '✅ Configured' : '❌ Missing'}
|
|
129
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
|
+
)}
|
|
130
152
|
|
|
131
|
-
{/* ── M1: favorites +
|
|
153
|
+
{/* ── M1/M3: favorites + benchmark + endpoint install action section ── */}
|
|
132
154
|
{(favorites || onBenchmark) && (
|
|
133
155
|
<div className={styles.actions}>
|
|
134
156
|
{favorites && (
|
|
@@ -166,6 +188,9 @@ export default function DetailPanel({ model, onClose, favorites, onBenchmark, on
|
|
|
166
188
|
)}
|
|
167
189
|
</div>
|
|
168
190
|
)}
|
|
191
|
+
{onLaunch && (
|
|
192
|
+
<LaunchButton model={model} toolMode={toolMode} onLaunch={onLaunch} />
|
|
193
|
+
)}
|
|
169
194
|
{onBenchmark && (
|
|
170
195
|
<button
|
|
171
196
|
className={styles.benchBtn}
|
|
@@ -219,6 +219,32 @@
|
|
|
219
219
|
background: rgba(180, 0, 255, 0.20);
|
|
220
220
|
border-color: rgba(180, 0, 255, 0.7);
|
|
221
221
|
}
|
|
222
|
+
|
|
223
|
+
.compatWarning {
|
|
224
|
+
display: flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
gap: 8px;
|
|
227
|
+
margin: 12px 0;
|
|
228
|
+
padding: 10px;
|
|
229
|
+
border: 1px solid rgba(255, 90, 122, 0.35);
|
|
230
|
+
border-radius: 8px;
|
|
231
|
+
background: rgba(255, 90, 122, 0.10);
|
|
232
|
+
color: #ff8da3;
|
|
233
|
+
font-size: 12px;
|
|
234
|
+
line-height: 1.35;
|
|
235
|
+
}
|
|
236
|
+
.compatWarning span { flex: 1; }
|
|
237
|
+
.compatWarning button {
|
|
238
|
+
border: 1px solid rgba(255, 90, 122, 0.45);
|
|
239
|
+
border-radius: 6px;
|
|
240
|
+
background: var(--color-bg-card);
|
|
241
|
+
color: #ff8da3;
|
|
242
|
+
padding: 5px 8px;
|
|
243
|
+
cursor: pointer;
|
|
244
|
+
font-size: 11px;
|
|
245
|
+
font-weight: 800;
|
|
246
|
+
}
|
|
247
|
+
.compatWarning button:hover { background: rgba(255, 90, 122, 0.16); }
|
|
222
248
|
:global([data-theme="light"]) .benchBtn {
|
|
223
249
|
background: rgba(140, 0, 200, 0.06);
|
|
224
250
|
color: #6a00a0;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { useState, useEffect, useMemo } from 'react'
|
|
10
10
|
import { IconRefresh, IconX, IconFilter, IconCircleCheck, IconAlertTriangle, IconActivity, IconEye, IconStar } from '@tabler/icons-react'
|
|
11
|
+
import { getToolMeta } from '../../../../src/core/tool-metadata.js'
|
|
11
12
|
import styles from './FilterBar.module.css'
|
|
12
13
|
|
|
13
14
|
// 📖 Chip sets match the TUI cycles 1:1 (see useFilter.js). Keep these in sync.
|
|
@@ -109,6 +110,7 @@ export default function FilterBar({
|
|
|
109
110
|
globalBenchmarkRunning,
|
|
110
111
|
globalBenchmarkTotal,
|
|
111
112
|
globalBenchmarkCompleted,
|
|
113
|
+
toolMode = 'opencode',
|
|
112
114
|
}) {
|
|
113
115
|
const [countdown, setCountdown] = useState(null)
|
|
114
116
|
|
|
@@ -124,6 +126,7 @@ export default function FilterBar({
|
|
|
124
126
|
}, [nextPingAt])
|
|
125
127
|
|
|
126
128
|
const countdownDisplay = countdown !== null ? formatCountdown(countdown) : null
|
|
129
|
+
const activeTool = getToolMeta(toolMode)
|
|
127
130
|
|
|
128
131
|
// 📖 Custom text filter chip — sticks to the right of the search bar, with
|
|
129
132
|
// 📖 an "X" to clear (TUI's `X` key behavior).
|
|
@@ -170,12 +173,14 @@ export default function FilterBar({
|
|
|
170
173
|
<ChipRow label="Health" items={HEALTHS} value={filterHealth} onChange={setFilterHealth} />
|
|
171
174
|
|
|
172
175
|
<div className={styles.group}>
|
|
173
|
-
<label className={styles.filterLabel}>Visibility</label>
|
|
176
|
+
<label className={styles.filterLabel} htmlFor="visibility-select">Visibility</label>
|
|
174
177
|
<select
|
|
178
|
+
id="visibility-select"
|
|
175
179
|
className={styles.select}
|
|
176
180
|
value={visibilityMode}
|
|
177
181
|
onChange={(e) => setVisibilityMode(e.target.value)}
|
|
178
182
|
title={VISIBILITY_MODES.find((v) => v.key === visibilityMode)?.hint}
|
|
183
|
+
aria-label="Visibility mode"
|
|
179
184
|
>
|
|
180
185
|
{VISIBILITY_MODES.map((v) => (
|
|
181
186
|
<option key={v.key} value={v.key}>{v.label}</option>
|
|
@@ -184,11 +189,13 @@ export default function FilterBar({
|
|
|
184
189
|
</div>
|
|
185
190
|
|
|
186
191
|
<div className={styles.group}>
|
|
187
|
-
<label className={styles.filterLabel}>Provider</label>
|
|
192
|
+
<label className={styles.filterLabel} htmlFor="provider-select">Provider</label>
|
|
188
193
|
<select
|
|
194
|
+
id="provider-select"
|
|
189
195
|
className={styles.providerSelect}
|
|
190
196
|
value={filterProvider}
|
|
191
197
|
onChange={(e) => setFilterProvider(e.target.value)}
|
|
198
|
+
aria-label="Filter by provider"
|
|
192
199
|
>
|
|
193
200
|
<option value="all">All Providers</option>
|
|
194
201
|
{providers.map((p) => (
|
|
@@ -197,6 +204,14 @@ export default function FilterBar({
|
|
|
197
204
|
</select>
|
|
198
205
|
</div>
|
|
199
206
|
|
|
207
|
+
<div className={styles.group}>
|
|
208
|
+
<label className={styles.filterLabel}>Endpoint target</label>
|
|
209
|
+
<div className={styles.toolStatus} title="Active endpoint install target from the Header picker">
|
|
210
|
+
<span>{activeTool.emoji}</span>
|
|
211
|
+
<strong>{activeTool.label}</strong>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
200
215
|
<div className={styles.spacer} />
|
|
201
216
|
|
|
202
217
|
{/* ── Custom text filter chip (TUI's Ctrl+P "Apply text filter") ── */}
|
|
@@ -49,6 +49,23 @@
|
|
|
49
49
|
}
|
|
50
50
|
.select:focus, .providerSelect:focus { border-color: var(--color-accent); }
|
|
51
51
|
|
|
52
|
+
.toolStatus {
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 6px;
|
|
56
|
+
padding: 4px 8px;
|
|
57
|
+
border: 1px solid var(--color-border);
|
|
58
|
+
border-radius: 5px;
|
|
59
|
+
background: var(--color-surface);
|
|
60
|
+
color: var(--color-text);
|
|
61
|
+
font-size: 11px;
|
|
62
|
+
white-space: nowrap;
|
|
63
|
+
}
|
|
64
|
+
.toolStatus strong {
|
|
65
|
+
font-size: 11px;
|
|
66
|
+
font-weight: 800;
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
/* ── Spacer + reset ── */
|
|
53
70
|
.spacer { flex: 1; }
|
|
54
71
|
|