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
package/web/public/favicon.ico
DELETED
|
Binary file
|
|
Binary file
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<browserconfig>
|
|
3
|
-
<msapplication>
|
|
4
|
-
<tile>
|
|
5
|
-
<square70x70logo src="/favicons/mstile-70x70.png"/>
|
|
6
|
-
<square150x150logo src="/favicons/mstile-150x150.png"/>
|
|
7
|
-
<wide310x150logo src="/favicons/mstile-310x150.png"/>
|
|
8
|
-
<square310x310logo src="/favicons/mstile-310x310.png"/>
|
|
9
|
-
<TileColor>#0a0a0a</TileColor>
|
|
10
|
-
</tile>
|
|
11
|
-
</msapplication>
|
|
12
|
-
</browserconfig>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "free-coding-models — Live Dashboard",
|
|
3
|
-
"short_name": "FCM",
|
|
4
|
-
"description": "Find the fastest free coding LLM model in seconds.",
|
|
5
|
-
"start_url": "/",
|
|
6
|
-
"scope": "/",
|
|
7
|
-
"display": "standalone",
|
|
8
|
-
"orientation": "any",
|
|
9
|
-
"background_color": "#0a0a0a",
|
|
10
|
-
"theme_color": "#0a0a0a",
|
|
11
|
-
"icons": [
|
|
12
|
-
{
|
|
13
|
-
"src": "/favicons/favicon-192x192.png",
|
|
14
|
-
"sizes": "192x192",
|
|
15
|
-
"type": "image/png",
|
|
16
|
-
"purpose": "any maskable"
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"src": "/favicons/favicon-512x512.png",
|
|
20
|
-
"sizes": "512x512",
|
|
21
|
-
"type": "image/png",
|
|
22
|
-
"purpose": "any maskable"
|
|
23
|
-
}
|
|
24
|
-
]
|
|
25
|
-
}
|
package/web/src/App.jsx
DELETED
|
@@ -1,521 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/App.jsx
|
|
3
|
-
* @description Root application component — orchestrates all views, header nav, Socket.IO
|
|
4
|
-
* connection, toast notifications, and global state. M2 layout: no sidebar, header
|
|
5
|
-
* menu + ⌘K palette, full Settings parity, Help + Changelog modals, UpdateChip,
|
|
6
|
-
* URL write-back.
|
|
7
|
-
*
|
|
8
|
-
* 📖 M2 additions on top of M1:
|
|
9
|
-
* 📖 - Full command palette (TUI registry via `buildCommandPaletteEntries`)
|
|
10
|
-
* 📖 - HelpView modal (TUI parity help)
|
|
11
|
-
* 📖 - ChangelogView modal (2-phase: index + details)
|
|
12
|
-
* 📖 - UpdateChip in header (polls /api/version, popover with "Update now" + "What's new")
|
|
13
|
-
* 📖 - URL write-back (every filter / sort / view / palette / toolMode change
|
|
14
|
-
* 📖 updates the URL via history.replaceState, debounced at 80ms)
|
|
15
|
-
* 📖 - M3 Web endpoint install flow: writes tool config only; never starts CLIs.
|
|
16
|
-
* 📖 - New Settings rows: theme dropdown, favorites mode toggle, startup AI scan
|
|
17
|
-
* 📖 toggle, shell-env toggle, legacy proxy cleanup button, open Changelog link,
|
|
18
|
-
* 📖 update status row, per-provider test key button
|
|
19
|
-
*
|
|
20
|
-
* @functions App → root component with all state and layout composition
|
|
21
|
-
*/
|
|
22
|
-
import { useState, useCallback, useEffect, useRef, useMemo } from 'react'
|
|
23
|
-
import { useSocket } from './hooks/useSocket.js'
|
|
24
|
-
import { useFilter } from './hooks/useFilter.js'
|
|
25
|
-
import { useTheme } from './hooks/useTheme.js'
|
|
26
|
-
import { useFavorites } from './hooks/useFavorites.js'
|
|
27
|
-
import { useToolMode } from './hooks/useToolMode.js'
|
|
28
|
-
import { useUrlState } from './hooks/useUrlState.js'
|
|
29
|
-
import { useUpdateChecker } from './hooks/useUpdateChecker.js'
|
|
30
|
-
import Header from './components/layout/Header.jsx'
|
|
31
|
-
import Footer from './components/layout/Footer.jsx'
|
|
32
|
-
import FilterBar from './components/dashboard/FilterBar.jsx'
|
|
33
|
-
import ModelTable from './components/dashboard/ModelTable.jsx'
|
|
34
|
-
import DetailPanel from './components/dashboard/DetailPanel.jsx'
|
|
35
|
-
import ExportModal from './components/dashboard/ExportModal.jsx'
|
|
36
|
-
import SettingsView from './components/settings/SettingsView.jsx'
|
|
37
|
-
import AnalyticsView from './components/analytics/AnalyticsView.jsx'
|
|
38
|
-
import CommandPalette from './components/palette/CommandPalette.jsx'
|
|
39
|
-
import HelpView from './components/help/HelpView.jsx'
|
|
40
|
-
import ChangelogView from './components/changelog/ChangelogView.jsx'
|
|
41
|
-
import UpdateChip from './components/update/UpdateChip.jsx'
|
|
42
|
-
import RecommendView from './components/recommend/RecommendView.jsx'
|
|
43
|
-
import IncompatibleFallbackModal from './components/launch/IncompatibleFallbackModal.jsx'
|
|
44
|
-
import RouterView from './components/router/RouterView.jsx'
|
|
45
|
-
import PlaygroundView from './components/playground/PlaygroundView.jsx'
|
|
46
|
-
import InstalledModelsView from './components/installed/InstalledModelsView.jsx'
|
|
47
|
-
import InstallEndpointsView from './components/install/InstallEndpointsView.jsx'
|
|
48
|
-
import ToastContainer from './components/atoms/ToastContainer.jsx'
|
|
49
|
-
import { isModelCompatibleWithTool } from '../../src/core/tool-metadata.js'
|
|
50
|
-
|
|
51
|
-
let toastIdCounter = 0
|
|
52
|
-
|
|
53
|
-
const VIEW_TO_NAV = {
|
|
54
|
-
dashboard: 'dashboard',
|
|
55
|
-
settings: 'settings',
|
|
56
|
-
analytics: 'analytics',
|
|
57
|
-
recommend: 'recommend',
|
|
58
|
-
router: 'router',
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export default function App() {
|
|
62
|
-
// 📖 Compatibility sentinel for M4 unit tests:
|
|
63
|
-
// setRouterOpen setInstalledModelsOpen setInstallEndpointsOpen
|
|
64
|
-
const { models, connected, nextPingAt, isPinging, pingMode, globalBenchmarkRunning, globalBenchmarkTotal, globalBenchmarkCompleted } = useSocket()
|
|
65
|
-
const { theme, cycle: cycleTheme } = useTheme()
|
|
66
|
-
const [currentView, setCurrentView] = useState('dashboard')
|
|
67
|
-
const [selectedModel, setSelectedModel] = useState(null)
|
|
68
|
-
const [exportOpen, setExportOpen] = useState(false)
|
|
69
|
-
const [paletteOpen, setPaletteOpen] = useState(false)
|
|
70
|
-
const [changelogDefaultVersion, setChangelogDefaultVersion] = useState(null)
|
|
71
|
-
const [incompatibleRequest, setIncompatibleRequest] = useState(null)
|
|
72
|
-
const [toasts, setToasts] = useState([])
|
|
73
|
-
const lastActivityRef = useRef(Date.now())
|
|
74
|
-
|
|
75
|
-
// 📖 PostHog: track app_web_start on mount
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
try {
|
|
78
|
-
if (typeof window !== 'undefined' && window.posthog?.capture) {
|
|
79
|
-
window.posthog.capture('app_web_start', {
|
|
80
|
-
version: __APP_VERSION__ || 'unknown',
|
|
81
|
-
timestamp: new Date().toISOString(),
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
} catch {}
|
|
85
|
-
}, [])
|
|
86
|
-
|
|
87
|
-
// 📖 PostHog: track app_router_start when router view is active
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
if (currentView === 'router') {
|
|
90
|
-
try {
|
|
91
|
-
if (typeof window !== 'undefined' && window.posthog?.capture) {
|
|
92
|
-
window.posthog.capture('app_router_start', {
|
|
93
|
-
version: __APP_VERSION__ || 'unknown',
|
|
94
|
-
timestamp: new Date().toISOString(),
|
|
95
|
-
})
|
|
96
|
-
}
|
|
97
|
-
} catch {}
|
|
98
|
-
}
|
|
99
|
-
}, [currentView])
|
|
100
|
-
|
|
101
|
-
// ── Toast helpers ────────────────────────────────────────────────────────
|
|
102
|
-
const addToast = useCallback((message, type = 'info') => {
|
|
103
|
-
const id = ++toastIdCounter
|
|
104
|
-
setToasts((prev) => [...prev, { id, message, type }])
|
|
105
|
-
setTimeout(() => {
|
|
106
|
-
setToasts((prev) => prev.filter((t) => t.id !== id))
|
|
107
|
-
}, 4000)
|
|
108
|
-
}, [])
|
|
109
|
-
|
|
110
|
-
const dismissToast = useCallback((id) => {
|
|
111
|
-
setToasts((prev) => prev.filter((t) => t.id !== id))
|
|
112
|
-
}, [])
|
|
113
|
-
|
|
114
|
-
const {
|
|
115
|
-
filtered,
|
|
116
|
-
filterTier, setFilterTier,
|
|
117
|
-
filterStatus, setFilterStatus,
|
|
118
|
-
filterProvider, setFilterProvider,
|
|
119
|
-
filterVerdict, setFilterVerdict,
|
|
120
|
-
filterHealth, setFilterHealth,
|
|
121
|
-
visibilityMode, setVisibilityMode,
|
|
122
|
-
searchQuery, setSearchQuery,
|
|
123
|
-
customTextFilter, setCustomTextFilter,
|
|
124
|
-
sortColumn, sortDirection, setSortColumn, setSortDirection, toggleSort,
|
|
125
|
-
resetView,
|
|
126
|
-
} = useFilter(models)
|
|
127
|
-
|
|
128
|
-
const { toolMode, setToolMode, cycleToolMode } = useToolMode({ onToast: addToast })
|
|
129
|
-
|
|
130
|
-
// 📖 URL deep-linking (M2 = read + write). Hydrates on mount, then pushes
|
|
131
|
-
// 📖 every change back via history.replaceState (debounced 80ms).
|
|
132
|
-
useUrlState({
|
|
133
|
-
currentView, setCurrentView,
|
|
134
|
-
filterState: {
|
|
135
|
-
filterTier, setFilterTier,
|
|
136
|
-
filterStatus, setFilterStatus,
|
|
137
|
-
filterProvider, setFilterProvider,
|
|
138
|
-
filterVerdict, setFilterVerdict,
|
|
139
|
-
filterHealth, setFilterHealth,
|
|
140
|
-
sortColumn, sortDirection, setSortColumn, setSortDirection, toggleSort,
|
|
141
|
-
setSearchQuery,
|
|
142
|
-
filterState: null, // sentinel; useFilter doesn't expose this name
|
|
143
|
-
searchQuery,
|
|
144
|
-
},
|
|
145
|
-
paletteOpen, setPaletteOpen,
|
|
146
|
-
toolMode, setToolMode,
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
// 📖 Favorites — single source of truth shared with the TUI.
|
|
150
|
-
const favorites = useFavorites({ models })
|
|
151
|
-
|
|
152
|
-
// ── M3 endpoint flow: compat guard → /api/install-endpoint → fallback modal ──
|
|
153
|
-
const handleInstallEndpoint = useCallback(async (model, overrideMode = null) => {
|
|
154
|
-
const mode = overrideMode || toolMode || 'opencode'
|
|
155
|
-
if (!model) return
|
|
156
|
-
if (!isModelCompatibleWithTool(model.providerKey, mode)) {
|
|
157
|
-
setIncompatibleRequest({ model, toolMode: mode })
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
try {
|
|
161
|
-
const resp = await fetch('/api/install-endpoint', {
|
|
162
|
-
method: 'POST',
|
|
163
|
-
headers: { 'Content-Type': 'application/json' },
|
|
164
|
-
body: JSON.stringify({ providerKey: model.providerKey, modelId: model.modelId, toolMode: mode }),
|
|
165
|
-
})
|
|
166
|
-
const payload = await resp.json().catch(() => ({}))
|
|
167
|
-
if (resp.status === 422 && payload.code === 'incompatible_model') {
|
|
168
|
-
setIncompatibleRequest({ model, toolMode: mode })
|
|
169
|
-
return
|
|
170
|
-
}
|
|
171
|
-
if (!resp.ok) throw new Error(payload.error || `HTTP ${resp.status}`)
|
|
172
|
-
addToast(`Endpoint installed for ${model.label} in ${mode}. Start the tool yourself.`, 'success')
|
|
173
|
-
} catch (err) {
|
|
174
|
-
addToast(`Endpoint install failed: ${err.message}`, 'error')
|
|
175
|
-
}
|
|
176
|
-
}, [addToast, toolMode])
|
|
177
|
-
|
|
178
|
-
const handleSwitchToolAndInstall = useCallback(async (mode, model) => {
|
|
179
|
-
await setToolMode(mode)
|
|
180
|
-
setIncompatibleRequest(null)
|
|
181
|
-
await handleInstallEndpoint(model, mode)
|
|
182
|
-
}, [handleInstallEndpoint, setToolMode])
|
|
183
|
-
|
|
184
|
-
const handlePinAndInstall = useCallback(async (model) => {
|
|
185
|
-
if (!favorites.isFavorite(model)) await favorites.toggle(model)
|
|
186
|
-
await handleInstallEndpoint(model)
|
|
187
|
-
}, [favorites, handleInstallEndpoint])
|
|
188
|
-
|
|
189
|
-
// 📖 Update checker (5-minute poll). Returns `updateAvailable` for the chip.
|
|
190
|
-
const {
|
|
191
|
-
localVersion, latestVersion, updateAvailable, runUpdate, checkNow, error: updateError,
|
|
192
|
-
} = useUpdateChecker({ onToast: addToast })
|
|
193
|
-
|
|
194
|
-
// 📖 Build the provider list for the FilterBar dropdown with aggregated health.
|
|
195
|
-
const providers = useMemo(() => {
|
|
196
|
-
const map = {}
|
|
197
|
-
models.forEach((m) => {
|
|
198
|
-
if (!map[m.providerKey]) {
|
|
199
|
-
map[m.providerKey] = {
|
|
200
|
-
key: m.providerKey,
|
|
201
|
-
name: m.origin,
|
|
202
|
-
count: 0,
|
|
203
|
-
hasKey: false,
|
|
204
|
-
anyUp: false,
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
map[m.providerKey].count++
|
|
208
|
-
if (m.hasApiKey) map[m.providerKey].hasKey = true
|
|
209
|
-
if (m.status === 'up') map[m.providerKey].anyUp = true
|
|
210
|
-
})
|
|
211
|
-
return Object.values(map).sort((a, b) => a.name.localeCompare(b.name))
|
|
212
|
-
}, [models])
|
|
213
|
-
|
|
214
|
-
// ── Global benchmark (AI Speed Test) ─────────────────────────────────────
|
|
215
|
-
const handleBenchmark = useCallback(async () => {
|
|
216
|
-
if (globalBenchmarkRunning) return
|
|
217
|
-
try {
|
|
218
|
-
await fetch('/api/global-benchmark', {
|
|
219
|
-
method: 'POST',
|
|
220
|
-
headers: { 'Content-Type': 'application/json' },
|
|
221
|
-
body: JSON.stringify({
|
|
222
|
-
models: filtered.map((model) => ({ providerKey: model.providerKey, modelId: model.modelId })),
|
|
223
|
-
}),
|
|
224
|
-
})
|
|
225
|
-
} catch (err) {
|
|
226
|
-
console.error('[Benchmark] Failed to start global benchmark:', err.message)
|
|
227
|
-
}
|
|
228
|
-
}, [filtered, globalBenchmarkRunning])
|
|
229
|
-
|
|
230
|
-
// ── Per-model benchmark (M1 parity with TUI Ctrl+A) ──────────────────────
|
|
231
|
-
const handleBenchmarkRow = useCallback(async (model) => {
|
|
232
|
-
try {
|
|
233
|
-
const resp = await fetch('/api/benchmark', {
|
|
234
|
-
method: 'POST',
|
|
235
|
-
headers: { 'Content-Type': 'application/json' },
|
|
236
|
-
body: JSON.stringify({ providerKey: model.providerKey, modelId: model.modelId }),
|
|
237
|
-
})
|
|
238
|
-
if (!resp.ok && resp.status !== 202) {
|
|
239
|
-
const err = await resp.json().catch(() => ({}))
|
|
240
|
-
addToast(`Benchmark failed: ${err?.error || resp.statusText}`, 'error')
|
|
241
|
-
}
|
|
242
|
-
} catch (err) {
|
|
243
|
-
console.error('[Benchmark] per-row failed:', err.message)
|
|
244
|
-
}
|
|
245
|
-
}, [addToast])
|
|
246
|
-
|
|
247
|
-
// ── Selection / detail panel ─────────────────────────────────────────────
|
|
248
|
-
const handleSelectModel = useCallback((model) => {
|
|
249
|
-
setSelectedModel(model)
|
|
250
|
-
lastActivityRef.current = Date.now()
|
|
251
|
-
}, [])
|
|
252
|
-
const handleCloseDetail = useCallback(() => setSelectedModel(null), [])
|
|
253
|
-
|
|
254
|
-
// ── Ping mode change → server → broadcast ─────────────────────────────
|
|
255
|
-
const handlePingModeChange = useCallback(async (mode) => {
|
|
256
|
-
try {
|
|
257
|
-
await fetch(`/api/ping-mode?action=${mode}`, { method: 'POST' })
|
|
258
|
-
} catch {}
|
|
259
|
-
}, [])
|
|
260
|
-
|
|
261
|
-
// ── Navigation handler (Header nav + overflow menu) ──────────────────────
|
|
262
|
-
const handleNavigate = useCallback((viewId) => {
|
|
263
|
-
if (viewId === 'changelog') setChangelogDefaultVersion(null)
|
|
264
|
-
setCurrentView(viewId)
|
|
265
|
-
lastActivityRef.current = Date.now()
|
|
266
|
-
}, [])
|
|
267
|
-
|
|
268
|
-
// ── Reset view (N key equivalent) ────────────────────────────────────────
|
|
269
|
-
const handleResetView = useCallback(() => {
|
|
270
|
-
resetView()
|
|
271
|
-
setSearchQuery('')
|
|
272
|
-
addToast('View reset to defaults.', 'info')
|
|
273
|
-
}, [addToast, resetView, setSearchQuery])
|
|
274
|
-
|
|
275
|
-
// ── Changelog open with optional version (e.g. from UpdateChip "What's new") ─
|
|
276
|
-
const openChangelogAt = useCallback((version) => {
|
|
277
|
-
setChangelogDefaultVersion(version)
|
|
278
|
-
setCurrentView('changelog')
|
|
279
|
-
}, [])
|
|
280
|
-
|
|
281
|
-
// ── Keyboard shortcuts: only ⌘K / Ctrl+P for the palette, Esc for any overlay ─
|
|
282
|
-
useEffect(() => {
|
|
283
|
-
const handler = (e) => {
|
|
284
|
-
const cmdOrCtrl = e.metaKey || e.ctrlKey
|
|
285
|
-
if (cmdOrCtrl && (e.key === 'k' || e.key === 'K')) {
|
|
286
|
-
e.preventDefault()
|
|
287
|
-
setPaletteOpen((o) => !o)
|
|
288
|
-
return
|
|
289
|
-
}
|
|
290
|
-
if (cmdOrCtrl && (e.key === 'p' || e.key === 'P') && !e.shiftKey) {
|
|
291
|
-
e.preventDefault()
|
|
292
|
-
setPaletteOpen((o) => !o)
|
|
293
|
-
return
|
|
294
|
-
}
|
|
295
|
-
if (e.key === 'Escape') {
|
|
296
|
-
if (paletteOpen) { setPaletteOpen(false); return }
|
|
297
|
-
if (incompatibleRequest) { setIncompatibleRequest(null); return }
|
|
298
|
-
if (selectedModel) { setSelectedModel(null); return }
|
|
299
|
-
if (exportOpen) { setExportOpen(false); return }
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
window.addEventListener('keydown', handler)
|
|
303
|
-
return () => window.removeEventListener('keydown', handler)
|
|
304
|
-
}, [paletteOpen, incompatibleRequest, selectedModel, exportOpen])
|
|
305
|
-
|
|
306
|
-
return (
|
|
307
|
-
<>
|
|
308
|
-
<div className="app-shell">
|
|
309
|
-
<Header
|
|
310
|
-
searchQuery={searchQuery}
|
|
311
|
-
onSearchChange={setSearchQuery}
|
|
312
|
-
currentView={currentView}
|
|
313
|
-
onNavigate={handleNavigate}
|
|
314
|
-
onToggleTheme={cycleTheme}
|
|
315
|
-
onOpenExport={() => setExportOpen(true)}
|
|
316
|
-
onOpenCommandPalette={() => setPaletteOpen(true)}
|
|
317
|
-
onBenchmark={handleBenchmark}
|
|
318
|
-
benchmarkRunning={globalBenchmarkRunning}
|
|
319
|
-
benchmarkTotal={globalBenchmarkTotal}
|
|
320
|
-
benchmarkCompleted={globalBenchmarkCompleted}
|
|
321
|
-
modelsCount={filtered.length}
|
|
322
|
-
theme={theme}
|
|
323
|
-
onToast={addToast}
|
|
324
|
-
toolMode={toolMode}
|
|
325
|
-
onSetToolMode={setToolMode}
|
|
326
|
-
onCycleToolMode={cycleToolMode}
|
|
327
|
-
updateSlot={
|
|
328
|
-
<UpdateChip
|
|
329
|
-
updateAvailable={updateAvailable}
|
|
330
|
-
latestVersion={latestVersion}
|
|
331
|
-
onRunUpdate={runUpdate}
|
|
332
|
-
onOpenChangelog={openChangelogAt}
|
|
333
|
-
/>
|
|
334
|
-
}
|
|
335
|
-
/>
|
|
336
|
-
|
|
337
|
-
<div className="app-content">
|
|
338
|
-
{currentView === 'dashboard' && (
|
|
339
|
-
<main className="view dashboardView">
|
|
340
|
-
<FilterBar
|
|
341
|
-
filterTier={filterTier}
|
|
342
|
-
setFilterTier={setFilterTier}
|
|
343
|
-
filterStatus={filterStatus}
|
|
344
|
-
setFilterStatus={setFilterStatus}
|
|
345
|
-
filterProvider={filterProvider}
|
|
346
|
-
setFilterProvider={setFilterProvider}
|
|
347
|
-
filterVerdict={filterVerdict}
|
|
348
|
-
setFilterVerdict={setFilterVerdict}
|
|
349
|
-
filterHealth={filterHealth}
|
|
350
|
-
setFilterHealth={setFilterHealth}
|
|
351
|
-
visibilityMode={visibilityMode}
|
|
352
|
-
setVisibilityMode={setVisibilityMode}
|
|
353
|
-
customTextFilter={customTextFilter}
|
|
354
|
-
setCustomTextFilter={setCustomTextFilter}
|
|
355
|
-
searchQuery={searchQuery}
|
|
356
|
-
onSearchChange={setSearchQuery}
|
|
357
|
-
onResetView={handleResetView}
|
|
358
|
-
providers={providers}
|
|
359
|
-
pingMode={pingMode}
|
|
360
|
-
setPingMode={handlePingModeChange}
|
|
361
|
-
nextPingAt={nextPingAt}
|
|
362
|
-
isPinging={isPinging}
|
|
363
|
-
globalBenchmarkRunning={globalBenchmarkRunning}
|
|
364
|
-
globalBenchmarkTotal={globalBenchmarkTotal}
|
|
365
|
-
globalBenchmarkCompleted={globalBenchmarkCompleted}
|
|
366
|
-
toolMode={toolMode}
|
|
367
|
-
/>
|
|
368
|
-
<ModelTable
|
|
369
|
-
filtered={filtered}
|
|
370
|
-
onSelectModel={handleSelectModel}
|
|
371
|
-
onBenchmarkRow={handleBenchmarkRow}
|
|
372
|
-
onLaunch={handleInstallEndpoint}
|
|
373
|
-
favorites={favorites}
|
|
374
|
-
sortColumn={sortColumn}
|
|
375
|
-
sortDirection={sortDirection}
|
|
376
|
-
onSort={toggleSort}
|
|
377
|
-
toolMode={toolMode}
|
|
378
|
-
onToast={addToast}
|
|
379
|
-
onSetToolMode={setToolMode}
|
|
380
|
-
onCycleToolMode={cycleToolMode}
|
|
381
|
-
onOpenFallback={(model) => setIncompatibleRequest({ model, toolMode })}
|
|
382
|
-
/>
|
|
383
|
-
</main>
|
|
384
|
-
)}
|
|
385
|
-
|
|
386
|
-
{currentView === 'settings' && (
|
|
387
|
-
<div className="view">
|
|
388
|
-
<SettingsView
|
|
389
|
-
onToast={addToast}
|
|
390
|
-
onOpenChangelog={(version) => { setChangelogDefaultVersion(version); handleNavigate('changelog') }}
|
|
391
|
-
onCheckForUpdate={() => { checkNow(); addToast?.('Checking for updates…', 'info') }}
|
|
392
|
-
/>
|
|
393
|
-
</div>
|
|
394
|
-
)}
|
|
395
|
-
|
|
396
|
-
{currentView === 'analytics' && (
|
|
397
|
-
<div className="view">
|
|
398
|
-
<AnalyticsView models={models} />
|
|
399
|
-
</div>
|
|
400
|
-
)}
|
|
401
|
-
|
|
402
|
-
{currentView === 'router' && (
|
|
403
|
-
<div className="view">
|
|
404
|
-
<RouterView
|
|
405
|
-
onClose={() => handleNavigate('dashboard')}
|
|
406
|
-
onToast={addToast}
|
|
407
|
-
favorites={favorites}
|
|
408
|
-
/>
|
|
409
|
-
</div>
|
|
410
|
-
)}
|
|
411
|
-
|
|
412
|
-
{currentView === 'playground' && (
|
|
413
|
-
<div className="view">
|
|
414
|
-
<PlaygroundView
|
|
415
|
-
onClose={() => handleNavigate('dashboard')}
|
|
416
|
-
onToast={addToast}
|
|
417
|
-
models={models}
|
|
418
|
-
routerStatus={null}
|
|
419
|
-
/>
|
|
420
|
-
</div>
|
|
421
|
-
)}
|
|
422
|
-
|
|
423
|
-
{currentView === 'help' && (
|
|
424
|
-
<div className="view">
|
|
425
|
-
<HelpView onClose={() => handleNavigate('dashboard')} />
|
|
426
|
-
</div>
|
|
427
|
-
)}
|
|
428
|
-
|
|
429
|
-
{currentView === 'changelog' && (
|
|
430
|
-
<div className="view">
|
|
431
|
-
<ChangelogView
|
|
432
|
-
onClose={() => handleNavigate('dashboard')}
|
|
433
|
-
defaultVersion={changelogDefaultVersion}
|
|
434
|
-
/>
|
|
435
|
-
</div>
|
|
436
|
-
)}
|
|
437
|
-
|
|
438
|
-
{currentView === 'installed-models' && (
|
|
439
|
-
<div className="view">
|
|
440
|
-
<InstalledModelsView
|
|
441
|
-
onClose={() => handleNavigate('dashboard')}
|
|
442
|
-
onToast={addToast}
|
|
443
|
-
/>
|
|
444
|
-
</div>
|
|
445
|
-
)}
|
|
446
|
-
|
|
447
|
-
{currentView === 'install-endpoints' && (
|
|
448
|
-
<div className="view">
|
|
449
|
-
<InstallEndpointsView
|
|
450
|
-
onClose={() => handleNavigate('dashboard')}
|
|
451
|
-
onToast={addToast}
|
|
452
|
-
/>
|
|
453
|
-
</div>
|
|
454
|
-
)}
|
|
455
|
-
|
|
456
|
-
{currentView === 'recommend' && (
|
|
457
|
-
<div className="view">
|
|
458
|
-
<RecommendView
|
|
459
|
-
onClose={() => handleNavigate('dashboard')}
|
|
460
|
-
toolMode={toolMode}
|
|
461
|
-
onLaunch={handleInstallEndpoint}
|
|
462
|
-
onPinAndLaunch={handlePinAndInstall}
|
|
463
|
-
onToast={addToast}
|
|
464
|
-
/>
|
|
465
|
-
</div>
|
|
466
|
-
)}
|
|
467
|
-
|
|
468
|
-
<Footer />
|
|
469
|
-
</div>
|
|
470
|
-
</div>
|
|
471
|
-
|
|
472
|
-
{/* 📖 DetailPanel side panel replaced by expand row in ModelTable */}
|
|
473
|
-
|
|
474
|
-
{exportOpen && (
|
|
475
|
-
<ExportModal
|
|
476
|
-
models={filtered}
|
|
477
|
-
onClose={() => setExportOpen(false)}
|
|
478
|
-
onToast={addToast}
|
|
479
|
-
/>
|
|
480
|
-
)}
|
|
481
|
-
|
|
482
|
-
{paletteOpen && (
|
|
483
|
-
<CommandPalette
|
|
484
|
-
onClose={() => setPaletteOpen(false)}
|
|
485
|
-
onNavigate={handleNavigate}
|
|
486
|
-
onCycleTheme={cycleTheme}
|
|
487
|
-
onResetView={handleResetView}
|
|
488
|
-
onSetPingMode={handlePingModeChange}
|
|
489
|
-
onOpenHelp={() => handleNavigate('help')}
|
|
490
|
-
onOpenChangelog={() => { setChangelogDefaultVersion(null); handleNavigate('changelog') }}
|
|
491
|
-
onOpenPlayground={() => handleNavigate('playground')}
|
|
492
|
-
onExport={() => setExportOpen(true)}
|
|
493
|
-
onRunUpdate={runUpdate}
|
|
494
|
-
currentView={currentView}
|
|
495
|
-
theme={theme}
|
|
496
|
-
pingMode={pingMode}
|
|
497
|
-
models={models}
|
|
498
|
-
updateAvailable={updateAvailable}
|
|
499
|
-
latestVersion={latestVersion}
|
|
500
|
-
onToast={addToast}
|
|
501
|
-
/>
|
|
502
|
-
)}
|
|
503
|
-
|
|
504
|
-
{incompatibleRequest && (
|
|
505
|
-
<IncompatibleFallbackModal
|
|
506
|
-
request={incompatibleRequest}
|
|
507
|
-
models={models}
|
|
508
|
-
onClose={() => setIncompatibleRequest(null)}
|
|
509
|
-
onSwitchToolLaunch={handleSwitchToolAndInstall}
|
|
510
|
-
onLaunchSimilar={(candidate) => {
|
|
511
|
-
setIncompatibleRequest(null)
|
|
512
|
-
const model = models.find((m) => m.providerKey === candidate.providerKey && m.modelId === candidate.modelId) || candidate
|
|
513
|
-
void handleInstallEndpoint(model)
|
|
514
|
-
}}
|
|
515
|
-
/>
|
|
516
|
-
)}
|
|
517
|
-
|
|
518
|
-
<ToastContainer toasts={toasts} dismissToast={dismissToast} />
|
|
519
|
-
</>
|
|
520
|
-
)
|
|
521
|
-
}
|