free-coding-models 0.5.6 → 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 +14 -9
- package/changelog/v0.5.7.md +20 -0
- package/package.json +1 -1
- package/src/core/endpoint-installer.js +3 -3
- 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 +386 -1
- package/web/src/App.jsx +145 -26
- 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/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 +1 -1
- 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 +70 -11
- package/web/src/components/layout/Header.module.css +62 -2
- 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/tools/ToolPicker.jsx +86 -0
- package/web/src/components/tools/ToolPicker.module.css +84 -0
- package/web/src/global.css +23 -0
- package/web/src/hooks/urlState.constants.js +3 -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/useUrlState.js +4 -3
- package/web/src/utils/m3.js +55 -0
- package/web/dist/assets/index-Blp9QJev.js +0 -39
- package/web/dist/assets/index-Cz_aCLTR.css +0 -1
|
@@ -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
|
|
|
@@ -35,6 +35,7 @@ import AILatencyCell from '../atoms/AILatencyCell.jsx'
|
|
|
35
35
|
import TPSCell from '../atoms/TPSCell.jsx'
|
|
36
36
|
import NoKeyIcon from '../atoms/NoKeyIcon.jsx'
|
|
37
37
|
import ProviderLogo from '../atoms/ProviderLogo.jsx'
|
|
38
|
+
import LaunchButton from '../launch/LaunchButton.jsx'
|
|
38
39
|
|
|
39
40
|
import { pingClass } from '../../utils/format.js'
|
|
40
41
|
import { sweClass } from '../../utils/ranks.js'
|
|
@@ -172,7 +173,7 @@ function TrendCellRenderer({ row }) {
|
|
|
172
173
|
// 📖 Note: `favorites` / `onBenchmarkRow` are read by the cell renderers below
|
|
173
174
|
// 📖 via closure. We rebuild the columns list whenever any of these change so
|
|
174
175
|
// 📖 the cells always see fresh values.
|
|
175
|
-
const buildColumns = ({ favorites, onBenchmarkRow, onSelectModel }) => [
|
|
176
|
+
const buildColumns = ({ favorites, onBenchmarkRow, onSelectModel, onLaunch, toolMode }) => [
|
|
176
177
|
// 📖 Star column — leftmost, mirrors the TUI's `F` key.
|
|
177
178
|
colHelper.display({
|
|
178
179
|
id: 'fav',
|
|
@@ -303,13 +304,24 @@ const buildColumns = ({ favorites, onBenchmarkRow, onSelectModel }) => [
|
|
|
303
304
|
cell: TrendCellRenderer,
|
|
304
305
|
enableSorting: true,
|
|
305
306
|
}),
|
|
307
|
+
colHelper.display({
|
|
308
|
+
id: 'launch',
|
|
309
|
+
header: '🔌',
|
|
310
|
+
size: 42,
|
|
311
|
+
enableSorting: false,
|
|
312
|
+
cell: ({ row }) => (
|
|
313
|
+
<div className={styles.launchCell}>
|
|
314
|
+
<LaunchButton model={row.original} toolMode={toolMode} onLaunch={onLaunch} variant="icon" />
|
|
315
|
+
</div>
|
|
316
|
+
),
|
|
317
|
+
}),
|
|
306
318
|
]
|
|
307
319
|
|
|
308
320
|
// 📖 DEFAULT_COLUMN_SIZING: extracted from the columns above. This is the single
|
|
309
321
|
// 📖 source of truth for both the table layout and the useColumnSizing hook's reset target.
|
|
310
322
|
// 📖 The 'Reset columns' button restores these exact widths.
|
|
311
323
|
// 📖 We build a placeholder columns list (no callbacks needed) to extract sizes.
|
|
312
|
-
const PLACEHOLDER_COLUMNS = buildColumns({ favorites: { isFavorite: () => false, toggle: () => {} }, onBenchmarkRow: null, onSelectModel: null })
|
|
324
|
+
const PLACEHOLDER_COLUMNS = buildColumns({ favorites: { isFavorite: () => false, toggle: () => {} }, onBenchmarkRow: null, onSelectModel: null, onLaunch: null, toolMode: 'opencode' })
|
|
313
325
|
export const DEFAULT_COLUMN_SIZING = PLACEHOLDER_COLUMNS.reduce((acc, c) => {
|
|
314
326
|
if (c.id) acc[c.id] = c.size ?? 80
|
|
315
327
|
return acc
|
|
@@ -330,7 +342,7 @@ function SortIcon({ column }) {
|
|
|
330
342
|
|
|
331
343
|
// ─── Main component ───────────────────────────────────────────────────────────
|
|
332
344
|
export default function ModelTable({
|
|
333
|
-
filtered, onSelectModel, onBenchmarkRow, favorites,
|
|
345
|
+
filtered, onSelectModel, onBenchmarkRow, onLaunch, favorites,
|
|
334
346
|
sortColumn, sortDirection, onSort,
|
|
335
347
|
toolMode = null,
|
|
336
348
|
}) {
|
|
@@ -359,8 +371,8 @@ export default function ModelTable({
|
|
|
359
371
|
// 📖 Build the columns with the current favorites/onBenchmarkRow handlers.
|
|
360
372
|
// 📖 Re-derive when favorites reference changes so the star cells re-render.
|
|
361
373
|
const columns = useMemo(
|
|
362
|
-
() => buildColumns({ favorites, onBenchmarkRow, onSelectModel }),
|
|
363
|
-
[favorites, onBenchmarkRow, onSelectModel]
|
|
374
|
+
() => buildColumns({ favorites, onBenchmarkRow, onSelectModel, onLaunch, toolMode }),
|
|
375
|
+
[favorites, onBenchmarkRow, onSelectModel, onLaunch, toolMode]
|
|
364
376
|
)
|
|
365
377
|
|
|
366
378
|
// 📖 Column sizing state with localStorage persistence (see useColumnSizing.js).
|
|
@@ -239,7 +239,7 @@
|
|
|
239
239
|
.modelMeta { flex: 1; min-width: 0; }
|
|
240
240
|
.modelHeader { display: flex; align-items: center; gap: 5px; }
|
|
241
241
|
.modelName { font-weight: 600; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
242
|
-
.modelId { font-family: var(--font-mono); font-size: 9px; color: var(--color-text-
|
|
242
|
+
.modelId { font-family: var(--font-mono); font-size: 9px; color: var(--color-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
243
243
|
|
|
244
244
|
/* 📖 Provider cell now uses <ProviderLogo /> (icon + wordmark) — see
|
|
245
245
|
📖 atoms/ProviderLogo.module.css for the layout. The old bordered pill
|
|
@@ -330,4 +330,17 @@
|
|
|
330
330
|
:global([data-theme="light"]) .incompatible td {
|
|
331
331
|
background: rgba(200, 20, 58, 0.06) !important;
|
|
332
332
|
color: #888 !important;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* ─── M3: per-row endpoint install plug ─── */
|
|
336
|
+
.launchCell {
|
|
337
|
+
display: flex;
|
|
338
|
+
justify-content: center;
|
|
339
|
+
opacity: 0;
|
|
340
|
+
transition: opacity 120ms;
|
|
341
|
+
}
|
|
342
|
+
.table tbody tr:hover .launchCell,
|
|
343
|
+
.incompatible .launchCell,
|
|
344
|
+
.benchRow .launchCell {
|
|
345
|
+
opacity: 1;
|
|
333
346
|
}
|
|
@@ -74,7 +74,7 @@ const SECTIONS = [
|
|
|
74
74
|
id: 'tools',
|
|
75
75
|
title: '🧰 Tool mode (M3)',
|
|
76
76
|
items: [
|
|
77
|
-
{ key: '
|
|
77
|
+
{ key: 'M3 shipped', desc: 'Endpoint target picker, per-row Install Endpoint button, and incompatible-target fallback modal' },
|
|
78
78
|
],
|
|
79
79
|
},
|
|
80
80
|
{
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file web/src/components/install/InstallEndpointsView.jsx
|
|
3
|
+
* @description Install Endpoints wizard — 4-step modal: provider → tool → models → install.
|
|
4
|
+
* 📖 M4: Full TUI parity for the install endpoints overlay.
|
|
5
|
+
* 📖 Uses existing /api/install-endpoints/providers, /api/install-endpoints/catalog,
|
|
6
|
+
* 📖 and /api/install-endpoints/wizard endpoints.
|
|
7
|
+
*/
|
|
8
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
9
|
+
import {
|
|
10
|
+
IconPlug, IconChevronRight, IconChevronLeft, IconCheck,
|
|
11
|
+
IconLoader,
|
|
12
|
+
} from '@tabler/icons-react'
|
|
13
|
+
import styles from './InstallEndpointsView.module.css'
|
|
14
|
+
|
|
15
|
+
const STEPS = ['Provider', 'Tool', 'Models', 'Install']
|
|
16
|
+
|
|
17
|
+
export default function InstallEndpointsView({ onClose, onToast }) {
|
|
18
|
+
const [step, setStep] = useState(0)
|
|
19
|
+
const [providers, setProviders] = useState([])
|
|
20
|
+
const [selectedProvider, setSelectedProvider] = useState(null)
|
|
21
|
+
const [catalogModels, setCatalogModels] = useState([])
|
|
22
|
+
const [selectedTool, setSelectedTool] = useState(null)
|
|
23
|
+
const [selectedModels, setSelectedModels] = useState([])
|
|
24
|
+
const [scope, setScope] = useState('all')
|
|
25
|
+
const [installing, setInstalling] = useState(false)
|
|
26
|
+
const [installResult, setInstallResult] = useState(null)
|
|
27
|
+
const [loading, setLoading] = useState(true)
|
|
28
|
+
|
|
29
|
+
// 📖 Available install targets from ToolPicker's mode list
|
|
30
|
+
const TOOLS = [
|
|
31
|
+
{ id: 'opencode', label: 'OpenCode CLI', emoji: '💻' },
|
|
32
|
+
{ id: 'opencode-desktop', label: 'OpenCode Desktop', emoji: '🖥️' },
|
|
33
|
+
{ id: 'openclaw', label: 'OpenClaw', emoji: '🦞' },
|
|
34
|
+
{ id: 'crush', label: 'Crush', emoji: '💘' },
|
|
35
|
+
{ id: 'goose', label: 'Goose', emoji: '🪿' },
|
|
36
|
+
{ id: 'pi', label: 'Pi', emoji: 'π' },
|
|
37
|
+
{ id: 'aider', label: 'Aider', emoji: '🛠' },
|
|
38
|
+
{ id: 'qwen', label: 'Qwen', emoji: '🐉' },
|
|
39
|
+
{ id: 'openhands', label: 'OpenHands', emoji: '🤲' },
|
|
40
|
+
{ id: 'amp', label: 'Amp', emoji: '⚡' },
|
|
41
|
+
{ id: 'forgecode', label: 'ForgeCode', emoji: '🔥' },
|
|
42
|
+
{ id: 'fcm_router', label: 'FCM Router', emoji: '🔄' },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
// Load providers on mount
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
fetch('/api/install-endpoints/providers')
|
|
48
|
+
.then(r => r.json())
|
|
49
|
+
.then(data => {
|
|
50
|
+
setProviders(data.providers || [])
|
|
51
|
+
setLoading(false)
|
|
52
|
+
})
|
|
53
|
+
.catch(() => setLoading(false))
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
// Load catalog when provider selected
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!selectedProvider) return
|
|
59
|
+
fetch(`/api/install-endpoints/catalog?provider=${selectedProvider.providerKey}`)
|
|
60
|
+
.then(r => r.json())
|
|
61
|
+
.then(data => setCatalogModels(data.models || []))
|
|
62
|
+
.catch(() => setCatalogModels([]))
|
|
63
|
+
}, [selectedProvider])
|
|
64
|
+
|
|
65
|
+
const toggleModel = useCallback((modelId) => {
|
|
66
|
+
setSelectedModels(prev =>
|
|
67
|
+
prev.includes(modelId)
|
|
68
|
+
? prev.filter(id => id !== modelId)
|
|
69
|
+
: [...prev, modelId]
|
|
70
|
+
)
|
|
71
|
+
}, [])
|
|
72
|
+
|
|
73
|
+
const selectAll = useCallback(() => {
|
|
74
|
+
setSelectedModels(catalogModels.map(m => m.modelId))
|
|
75
|
+
setScope('all')
|
|
76
|
+
}, [catalogModels])
|
|
77
|
+
|
|
78
|
+
const selectNone = useCallback(() => {
|
|
79
|
+
setSelectedModels([])
|
|
80
|
+
setScope('selected')
|
|
81
|
+
}, [])
|
|
82
|
+
|
|
83
|
+
const canNext = () => {
|
|
84
|
+
if (step === 0) return !!selectedProvider
|
|
85
|
+
if (step === 1) return !!selectedTool
|
|
86
|
+
if (step === 2) return scope === 'all' || selectedModels.length > 0
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const handleInstall = async () => {
|
|
91
|
+
setInstalling(true)
|
|
92
|
+
try {
|
|
93
|
+
const resp = await fetch('/api/install-endpoints/wizard', {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: { 'Content-Type': 'application/json' },
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
providerKey: selectedProvider.providerKey,
|
|
98
|
+
toolMode: selectedTool,
|
|
99
|
+
scope,
|
|
100
|
+
modelIds: scope === 'selected' ? selectedModels : [],
|
|
101
|
+
}),
|
|
102
|
+
})
|
|
103
|
+
const data = await resp.json()
|
|
104
|
+
if (data.success) {
|
|
105
|
+
setInstallResult(data)
|
|
106
|
+
onToast?.(`Installed ${data.modelCount} models for ${selectedProvider.label} into ${TOOLS.find(t => t.id === selectedTool)?.label || selectedTool}.`, 'success')
|
|
107
|
+
} else {
|
|
108
|
+
onToast?.(`Install failed: ${data.error || 'unknown'}`, 'error')
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
onToast?.(`Install error: ${err.message}`, 'error')
|
|
112
|
+
} finally {
|
|
113
|
+
setInstalling(false)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const handleNext = () => {
|
|
118
|
+
if (step === 2) {
|
|
119
|
+
handleInstall()
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
setStep(s => Math.min(s + 1, 3))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const handleBack = () => {
|
|
126
|
+
setStep(s => Math.max(s - 1, 0))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className={styles.overlay} onClick={(e) => e.target === e.currentTarget && onClose()}>
|
|
131
|
+
<div className={styles.modal}>
|
|
132
|
+
<div className={styles.header}>
|
|
133
|
+
<h2 className={styles.title}>
|
|
134
|
+
<IconPlug size={20} stroke={1.5} />
|
|
135
|
+
Install Endpoints
|
|
136
|
+
</h2>
|
|
137
|
+
<button className={styles.closeBtn} onClick={onClose} aria-label="Close">✕</button>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{/* Step indicator */}
|
|
141
|
+
<div className={styles.steps}>
|
|
142
|
+
{STEPS.map((label, i) => (
|
|
143
|
+
<div key={i} className={`${styles.step} ${i === step ? styles.stepActive : i < step ? styles.stepDone : ''}`}>
|
|
144
|
+
<span className={styles.stepNum}>{i < step ? <IconCheck size={12} /> : i + 1}</span>
|
|
145
|
+
<span className={styles.stepLabel}>{label}</span>
|
|
146
|
+
</div>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className={styles.body}>
|
|
151
|
+
{/* Step 0: Select Provider */}
|
|
152
|
+
{step === 0 && (
|
|
153
|
+
<div className={styles.stepContent}>
|
|
154
|
+
<p className={styles.stepDesc}>Choose a provider with a configured API key.</p>
|
|
155
|
+
{loading && <div className={styles.loading}>Loading providers…</div>}
|
|
156
|
+
{!loading && providers.length === 0 && (
|
|
157
|
+
<div className={styles.empty}>No configured providers found. Add API keys in Settings first.</div>
|
|
158
|
+
)}
|
|
159
|
+
<div className={styles.grid}>
|
|
160
|
+
{providers.map((p) => (
|
|
161
|
+
<button
|
|
162
|
+
key={p.providerKey}
|
|
163
|
+
className={`${styles.pickCard} ${selectedProvider?.providerKey === p.providerKey ? styles.pickActive : ''}`}
|
|
164
|
+
onClick={() => { setSelectedProvider(p); setStep(1) }}
|
|
165
|
+
>
|
|
166
|
+
<span className={styles.pickLabel}>{p.label}</span>
|
|
167
|
+
<span className={styles.pickMeta}>{p.modelCount} models</span>
|
|
168
|
+
</button>
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* Step 1: Select Tool */}
|
|
175
|
+
{step === 1 && (
|
|
176
|
+
<div className={styles.stepContent}>
|
|
177
|
+
<p className={styles.stepDesc}>
|
|
178
|
+
Install <strong>{selectedProvider?.label}</strong> models into which tool?
|
|
179
|
+
</p>
|
|
180
|
+
<div className={styles.grid}>
|
|
181
|
+
{TOOLS.map((tool) => (
|
|
182
|
+
<button
|
|
183
|
+
key={tool.id}
|
|
184
|
+
className={`${styles.pickCard} ${selectedTool === tool.id ? styles.pickActive : ''}`}
|
|
185
|
+
onClick={() => { setSelectedTool(tool.id); setStep(2) }}
|
|
186
|
+
>
|
|
187
|
+
<span className={styles.pickEmoji}>{tool.emoji}</span>
|
|
188
|
+
<span className={styles.pickLabel}>{tool.label}</span>
|
|
189
|
+
</button>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
|
|
195
|
+
{/* Step 2: Select Models */}
|
|
196
|
+
{step === 2 && (
|
|
197
|
+
<div className={styles.stepContent}>
|
|
198
|
+
<p className={styles.stepDesc}>
|
|
199
|
+
Choose models to install from <strong>{selectedProvider?.label}</strong> into <strong>{TOOLS.find(t => t.id === selectedTool)?.label}</strong>.
|
|
200
|
+
</p>
|
|
201
|
+
<div className={styles.scopeToggle}>
|
|
202
|
+
<button className={`${styles.scopeBtn} ${scope === 'all' ? styles.scopeActive : ''}`} onClick={() => { setScope('all'); selectAll() }}>All Models</button>
|
|
203
|
+
<button className={`${styles.scopeBtn} ${scope === 'selected' ? styles.scopeActive : ''}`} onClick={() => setScope('selected')}>Select Models</button>
|
|
204
|
+
</div>
|
|
205
|
+
{scope === 'selected' && (
|
|
206
|
+
<div className={styles.modelGrid}>
|
|
207
|
+
{catalogModels.map((model) => (
|
|
208
|
+
<button
|
|
209
|
+
key={model.modelId}
|
|
210
|
+
className={`${styles.modelCard} ${selectedModels.includes(model.modelId) ? styles.modelSelected : ''}`}
|
|
211
|
+
onClick={() => toggleModel(model.modelId)}
|
|
212
|
+
>
|
|
213
|
+
<span className={styles.modelName}>{model.label}</span>
|
|
214
|
+
<span className={styles.modelTier}>{model.tier}</span>
|
|
215
|
+
</button>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
{scope === 'all' && (
|
|
220
|
+
<div className={styles.allNotice}>
|
|
221
|
+
All {catalogModels.length} models from {selectedProvider?.label} will be installed.
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{/* Step 3: Installing / Done */}
|
|
228
|
+
{step === 3 && (
|
|
229
|
+
<div className={styles.stepContent}>
|
|
230
|
+
{installing && (
|
|
231
|
+
<div className={styles.installingState}>
|
|
232
|
+
<IconLoader size={24} className={styles.spinner} />
|
|
233
|
+
<span>Installing {scope === 'all' ? 'all' : selectedModels.length} models…</span>
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
{installResult && (
|
|
237
|
+
<div className={styles.doneState}>
|
|
238
|
+
<IconCheck size={32} color="#00c864" />
|
|
239
|
+
<h3>Installation Complete</h3>
|
|
240
|
+
<p>{installResult.modelCount} models installed into {installResult.toolLabel}</p>
|
|
241
|
+
<code className={styles.installPath}>{installResult.path}</code>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{/* Footer nav */}
|
|
249
|
+
<div className={styles.footer}>
|
|
250
|
+
{step > 0 && step < 3 && (
|
|
251
|
+
<button className={styles.backBtn} onClick={handleBack}>
|
|
252
|
+
<IconChevronLeft size={14} />
|
|
253
|
+
Back
|
|
254
|
+
</button>
|
|
255
|
+
)}
|
|
256
|
+
{step < 3 && (
|
|
257
|
+
<button className={styles.nextBtn} onClick={handleNext} disabled={!canNext()}>
|
|
258
|
+
{step === 2 ? (
|
|
259
|
+
<>
|
|
260
|
+
<IconPlug size={14} />
|
|
261
|
+
Install
|
|
262
|
+
</>
|
|
263
|
+
) : (
|
|
264
|
+
<>
|
|
265
|
+
Next
|
|
266
|
+
<IconChevronRight size={14} />
|
|
267
|
+
</>
|
|
268
|
+
)}
|
|
269
|
+
</button>
|
|
270
|
+
)}
|
|
271
|
+
{step === 3 && installResult && (
|
|
272
|
+
<button className={styles.doneBtn} onClick={onClose}>Done</button>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
)
|
|
278
|
+
}
|