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