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.
Files changed (164) hide show
  1. package/README.md +110 -881
  2. package/bin/free-coding-models.js +4 -1
  3. package/changelog/v0.5.86.md +10 -0
  4. package/changelog/v0.5.87.md +48 -0
  5. package/package.json +8 -5
  6. package/sources.js +4 -4
  7. package/src/core/cache.js +3 -1
  8. package/src/core/config.js +43 -5
  9. package/src/core/endpoint-installer.js +23 -12
  10. package/src/core/installed-models-manager.js +43 -20
  11. package/src/core/kilo-config.js +51 -5
  12. package/src/core/kilo.js +5 -3
  13. package/src/core/legacy-proxy-cleanup.js +22 -5
  14. package/src/core/model-merger.js +4 -0
  15. package/src/core/models-drift.js +9 -0
  16. package/src/core/opencode-config.js +57 -5
  17. package/src/core/opencode.js +67 -41
  18. package/src/core/playground.js +7 -0
  19. package/src/core/probe-cache.js +35 -4
  20. package/src/core/provider-key-tester.js +10 -2
  21. package/src/core/provider-metadata.js +10 -0
  22. package/src/core/provider-quota-fetchers.js +14 -7
  23. package/src/core/router-daemon.js +208 -26
  24. package/src/core/runtime-telemetry.js +27 -1
  25. package/src/core/shared-helpers.js +12 -1
  26. package/src/core/shell-env.js +3 -1
  27. package/src/core/telemetry.js +18 -5
  28. package/src/core/tool-launchers.js +9 -6
  29. package/src/core/updater.js +28 -2
  30. package/src/core/utils.js +76 -71
  31. package/src/tui/app.js +47 -16
  32. package/src/tui/command-palette.js +22 -2
  33. package/src/tui/key-handler.js +123 -179
  34. package/src/tui/overlays.js +35 -75
  35. package/src/tui/render-helpers.js +62 -4
  36. package/src/tui/render-table.js +46 -5
  37. package/src/tui/theme.js +36 -5
  38. package/src/tui/tui-filters.js +28 -8
  39. package/src/tui/tui-state.js +22 -6
  40. package/web/dist/assets/index-BpTpPS0g.js +44 -0
  41. package/web/dist/assets/index-CAzFIt8P.css +1 -0
  42. package/web/dist/index.html +5 -9
  43. package/web/index.html +3 -7
  44. package/web/server.js +97 -23
  45. package/web/vite.config.js +8 -0
  46. package/web/app.legacy.js +0 -900
  47. package/web/dist/assets/index-C5-Ywvv3.css +0 -1
  48. package/web/dist/assets/index-DWUely84.js +0 -40
  49. package/web/public/favicon.ico +0 -0
  50. package/web/public/favicons/apple-touch-icon.png +0 -0
  51. package/web/public/favicons/browserconfig.xml +0 -12
  52. package/web/public/favicons/favicon-16x16.png +0 -0
  53. package/web/public/favicons/favicon-192x192.png +0 -0
  54. package/web/public/favicons/favicon-32x32.png +0 -0
  55. package/web/public/favicons/favicon-48x48.png +0 -0
  56. package/web/public/favicons/favicon-512x512.png +0 -0
  57. package/web/public/favicons/favicon-96x96.png +0 -0
  58. package/web/public/favicons/favicon.ico +0 -0
  59. package/web/public/favicons/mstile-150x150.png +0 -0
  60. package/web/public/favicons/mstile-310x150.png +0 -0
  61. package/web/public/favicons/mstile-310x310.png +0 -0
  62. package/web/public/favicons/mstile-512x512.png +0 -0
  63. package/web/public/favicons/mstile-70x70.png +0 -0
  64. package/web/public/favicons/site.webmanifest +0 -25
  65. package/web/src/App.jsx +0 -521
  66. package/web/src/components/analytics/AnalyticsView.jsx +0 -147
  67. package/web/src/components/analytics/AnalyticsView.module.css +0 -190
  68. package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
  69. package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
  70. package/web/src/components/atoms/AILatencyCell.jsx +0 -38
  71. package/web/src/components/atoms/AILatencyCell.module.css +0 -51
  72. package/web/src/components/atoms/HealthCell.jsx +0 -143
  73. package/web/src/components/atoms/HealthCell.module.css +0 -60
  74. package/web/src/components/atoms/LastPingCell.jsx +0 -35
  75. package/web/src/components/atoms/LastPingCell.module.css +0 -45
  76. package/web/src/components/atoms/MoodCell.jsx +0 -25
  77. package/web/src/components/atoms/MoodCell.module.css +0 -6
  78. package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
  79. package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
  80. package/web/src/components/atoms/ProviderLogo.jsx +0 -187
  81. package/web/src/components/atoms/ProviderLogo.module.css +0 -103
  82. package/web/src/components/atoms/RankCell.jsx +0 -9
  83. package/web/src/components/atoms/RankCell.module.css +0 -9
  84. package/web/src/components/atoms/Sparkline.jsx +0 -63
  85. package/web/src/components/atoms/StabilityCell.jsx +0 -18
  86. package/web/src/components/atoms/StabilityCell.module.css +0 -13
  87. package/web/src/components/atoms/StatusDot.jsx +0 -13
  88. package/web/src/components/atoms/StatusDot.module.css +0 -29
  89. package/web/src/components/atoms/TPSCell.jsx +0 -36
  90. package/web/src/components/atoms/TPSCell.module.css +0 -44
  91. package/web/src/components/atoms/TierBadge.jsx +0 -10
  92. package/web/src/components/atoms/TierBadge.module.css +0 -29
  93. package/web/src/components/atoms/Toast.jsx +0 -25
  94. package/web/src/components/atoms/Toast.module.css +0 -41
  95. package/web/src/components/atoms/ToastContainer.jsx +0 -16
  96. package/web/src/components/atoms/ToastContainer.module.css +0 -10
  97. package/web/src/components/atoms/VerdictBadge.jsx +0 -36
  98. package/web/src/components/atoms/VerdictBadge.module.css +0 -40
  99. package/web/src/components/changelog/ChangelogView.jsx +0 -135
  100. package/web/src/components/changelog/ChangelogView.module.css +0 -159
  101. package/web/src/components/dashboard/DetailPanel.jsx +0 -217
  102. package/web/src/components/dashboard/DetailPanel.module.css +0 -260
  103. package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
  104. package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
  105. package/web/src/components/dashboard/ExportModal.jsx +0 -84
  106. package/web/src/components/dashboard/ExportModal.module.css +0 -99
  107. package/web/src/components/dashboard/FilterBar.jsx +0 -338
  108. package/web/src/components/dashboard/FilterBar.module.css +0 -342
  109. package/web/src/components/dashboard/ModelTable.jsx +0 -672
  110. package/web/src/components/dashboard/ModelTable.module.css +0 -446
  111. package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
  112. package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
  113. package/web/src/components/help/HelpView.jsx +0 -201
  114. package/web/src/components/help/HelpView.module.css +0 -156
  115. package/web/src/components/install/InstallEndpointsView.jsx +0 -309
  116. package/web/src/components/install/InstallEndpointsView.module.css +0 -427
  117. package/web/src/components/installed/InstalledModelsView.jsx +0 -89
  118. package/web/src/components/installed/InstalledModelsView.module.css +0 -200
  119. package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
  120. package/web/src/components/launch/LaunchButton.jsx +0 -30
  121. package/web/src/components/launch/LaunchButton.module.css +0 -35
  122. package/web/src/components/launch/LaunchModal.module.css +0 -125
  123. package/web/src/components/layout/Footer.jsx +0 -19
  124. package/web/src/components/layout/Footer.module.css +0 -10
  125. package/web/src/components/layout/Header.jsx +0 -265
  126. package/web/src/components/layout/Header.module.css +0 -357
  127. package/web/src/components/palette/CommandPalette.jsx +0 -301
  128. package/web/src/components/palette/CommandPalette.module.css +0 -121
  129. package/web/src/components/playground/PlaygroundChat.jsx +0 -527
  130. package/web/src/components/playground/PlaygroundChat.module.css +0 -382
  131. package/web/src/components/playground/PlaygroundView.jsx +0 -371
  132. package/web/src/components/playground/PlaygroundView.module.css +0 -504
  133. package/web/src/components/recommend/RecommendView.jsx +0 -121
  134. package/web/src/components/recommend/RecommendView.module.css +0 -57
  135. package/web/src/components/router/RouterView.jsx +0 -1212
  136. package/web/src/components/router/RouterView.module.css +0 -1289
  137. package/web/src/components/settings/SettingsView.jsx +0 -555
  138. package/web/src/components/settings/SettingsView.module.css +0 -550
  139. package/web/src/components/tools/ToolPicker.jsx +0 -86
  140. package/web/src/components/tools/ToolPicker.module.css +0 -97
  141. package/web/src/components/update/UpdateChip.jsx +0 -104
  142. package/web/src/components/update/UpdateChip.module.css +0 -146
  143. package/web/src/global.css +0 -298
  144. package/web/src/hooks/urlState.constants.js +0 -28
  145. package/web/src/hooks/useChangelog.js +0 -51
  146. package/web/src/hooks/useColumnSizing.js +0 -211
  147. package/web/src/hooks/useFavorites.js +0 -150
  148. package/web/src/hooks/useFilter.js +0 -310
  149. package/web/src/hooks/useInstalledModels.js +0 -42
  150. package/web/src/hooks/useRecommend.js +0 -67
  151. package/web/src/hooks/useRouterDashboard.js +0 -133
  152. package/web/src/hooks/useSSE.js +0 -73
  153. package/web/src/hooks/useSocket.js +0 -203
  154. package/web/src/hooks/useTheme.js +0 -83
  155. package/web/src/hooks/useTokenUsage.js +0 -103
  156. package/web/src/hooks/useToolMode.js +0 -77
  157. package/web/src/hooks/useUpdateChecker.js +0 -91
  158. package/web/src/hooks/useUrlState.js +0 -170
  159. package/web/src/main.jsx +0 -15
  160. package/web/src/utils/download.js +0 -15
  161. package/web/src/utils/format.js +0 -42
  162. package/web/src/utils/m3.js +0 -57
  163. package/web/src/utils/ranks.js +0 -37
  164. package/web/styles.legacy.css +0 -963
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
@@ -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
- }