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
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/hooks/useInstalledModels.js
|
|
3
|
-
* @description Hook for Installed Models — scans tool configs and soft-deletes models.
|
|
4
|
-
* 📖 M4: Fetches /api/installed-models, provides disable(action) for soft-delete.
|
|
5
|
-
*
|
|
6
|
-
* @functions useInstalledModels → { results, loading, refresh, disableModel }
|
|
7
|
-
*/
|
|
8
|
-
import { useState, useEffect, useCallback } from 'react'
|
|
9
|
-
|
|
10
|
-
export function useInstalledModels() {
|
|
11
|
-
const [results, setResults] = useState([])
|
|
12
|
-
const [loading, setLoading] = useState(true)
|
|
13
|
-
|
|
14
|
-
const refresh = useCallback(async () => {
|
|
15
|
-
setLoading(true)
|
|
16
|
-
try {
|
|
17
|
-
const resp = await fetch('/api/installed-models')
|
|
18
|
-
const data = await resp.json()
|
|
19
|
-
setResults(data.results || [])
|
|
20
|
-
} catch {
|
|
21
|
-
setResults([])
|
|
22
|
-
} finally {
|
|
23
|
-
setLoading(false)
|
|
24
|
-
}
|
|
25
|
-
}, [])
|
|
26
|
-
|
|
27
|
-
useEffect(() => { void refresh() }, [refresh])
|
|
28
|
-
|
|
29
|
-
const disableModel = useCallback(async (toolMode, modelId) => {
|
|
30
|
-
const resp = await fetch(`/api/installed-models/${encodeURIComponent(toolMode)}/${encodeURIComponent(modelId)}/disable`, {
|
|
31
|
-
method: 'POST',
|
|
32
|
-
})
|
|
33
|
-
const result = await resp.json()
|
|
34
|
-
if (result.success) {
|
|
35
|
-
// 📖 Refresh after successful delete
|
|
36
|
-
await refresh()
|
|
37
|
-
}
|
|
38
|
-
return result
|
|
39
|
-
}, [refresh])
|
|
40
|
-
|
|
41
|
-
return { results, loading, refresh, disableModel }
|
|
42
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/hooks/useRecommend.js
|
|
3
|
-
* @description Smart Recommend hook for M3 Web parity. Owns the 10 second
|
|
4
|
-
* analysis phase and calls `/api/recommend`, which uses the shared TUI scoring
|
|
5
|
-
* engine in `src/core/utils.js`.
|
|
6
|
-
*
|
|
7
|
-
* @functions useRecommend → run/cancel recommendation analysis
|
|
8
|
-
* @exports useRecommend
|
|
9
|
-
*/
|
|
10
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
11
|
-
|
|
12
|
-
export function useRecommend({ onToast } = {}) {
|
|
13
|
-
const [loading, setLoading] = useState(false)
|
|
14
|
-
const [progress, setProgress] = useState(0)
|
|
15
|
-
const [results, setResults] = useState([])
|
|
16
|
-
const [error, setError] = useState(null)
|
|
17
|
-
const timerRef = useRef(null)
|
|
18
|
-
|
|
19
|
-
useEffect(() => () => { if (timerRef.current) clearInterval(timerRef.current) }, [])
|
|
20
|
-
|
|
21
|
-
const recommend = useCallback(async (answers) => {
|
|
22
|
-
if (timerRef.current) clearInterval(timerRef.current)
|
|
23
|
-
setLoading(true)
|
|
24
|
-
setProgress(0)
|
|
25
|
-
setResults([])
|
|
26
|
-
setError(null)
|
|
27
|
-
|
|
28
|
-
const started = Date.now()
|
|
29
|
-
timerRef.current = setInterval(() => {
|
|
30
|
-
const pct = Math.min(98, Math.round(((Date.now() - started) / 10_000) * 100))
|
|
31
|
-
setProgress(pct)
|
|
32
|
-
}, 250)
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
await new Promise((resolve) => setTimeout(resolve, 10_000))
|
|
36
|
-
const resp = await fetch('/api/recommend', {
|
|
37
|
-
method: 'POST',
|
|
38
|
-
headers: { 'Content-Type': 'application/json' },
|
|
39
|
-
body: JSON.stringify({ answers }),
|
|
40
|
-
})
|
|
41
|
-
const payload = await resp.json().catch(() => ({}))
|
|
42
|
-
if (!resp.ok) throw new Error(payload.error || `HTTP ${resp.status}`)
|
|
43
|
-
setResults(Array.isArray(payload.top3) ? payload.top3 : [])
|
|
44
|
-
setProgress(100)
|
|
45
|
-
return { ok: true, top3: payload.top3 || [] }
|
|
46
|
-
} catch (err) {
|
|
47
|
-
setError(err.message)
|
|
48
|
-
onToast?.(`Recommend failed: ${err.message}`, 'error')
|
|
49
|
-
return { ok: false, error: err.message }
|
|
50
|
-
} finally {
|
|
51
|
-
if (timerRef.current) clearInterval(timerRef.current)
|
|
52
|
-
timerRef.current = null
|
|
53
|
-
setLoading(false)
|
|
54
|
-
}
|
|
55
|
-
}, [onToast])
|
|
56
|
-
|
|
57
|
-
const reset = useCallback(() => {
|
|
58
|
-
if (timerRef.current) clearInterval(timerRef.current)
|
|
59
|
-
timerRef.current = null
|
|
60
|
-
setLoading(false)
|
|
61
|
-
setProgress(0)
|
|
62
|
-
setResults([])
|
|
63
|
-
setError(null)
|
|
64
|
-
}, [])
|
|
65
|
-
|
|
66
|
-
return { recommend, loading, progress, results, error, reset }
|
|
67
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/hooks/useRouterDashboard.js
|
|
3
|
-
* @description Hook for Router Dashboard — polls daemon status, proxies actions.
|
|
4
|
-
* 📖 M4: Provides status, stats, tokens, sets, start/stop, probe-mode control.
|
|
5
|
-
* 📖 Polls /api/router/status every 5s; fetches full stats on demand.
|
|
6
|
-
*
|
|
7
|
-
* @functions useRouterDashboard → { status, stats, tokens, start, stop, setProbeMode, refresh }
|
|
8
|
-
*/
|
|
9
|
-
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
10
|
-
|
|
11
|
-
const POLL_INTERVAL_MS = 5000
|
|
12
|
-
|
|
13
|
-
export function useRouterDashboard() {
|
|
14
|
-
const [status, setStatus] = useState(null)
|
|
15
|
-
const [stats, setStats] = useState(null)
|
|
16
|
-
const [tokens, setTokens] = useState(null)
|
|
17
|
-
const [sets, setSets] = useState(null)
|
|
18
|
-
const [loading, setLoading] = useState(true)
|
|
19
|
-
const [actionLoading, setActionLoading] = useState(false)
|
|
20
|
-
const pollRef = useRef(null)
|
|
21
|
-
|
|
22
|
-
const fetchStatus = useCallback(async () => {
|
|
23
|
-
try {
|
|
24
|
-
const resp = await fetch('/api/router/status')
|
|
25
|
-
const data = await resp.json()
|
|
26
|
-
setStatus(data)
|
|
27
|
-
return data
|
|
28
|
-
} catch {
|
|
29
|
-
setStatus({ ok: false, running: false })
|
|
30
|
-
return null
|
|
31
|
-
}
|
|
32
|
-
}, [])
|
|
33
|
-
|
|
34
|
-
const fetchStats = useCallback(async () => {
|
|
35
|
-
try {
|
|
36
|
-
const resp = await fetch('/api/router/stats')
|
|
37
|
-
const data = await resp.json()
|
|
38
|
-
if (data.ok) setStats(data)
|
|
39
|
-
return data
|
|
40
|
-
} catch { return null }
|
|
41
|
-
}, [])
|
|
42
|
-
|
|
43
|
-
const fetchTokens = useCallback(async () => {
|
|
44
|
-
try {
|
|
45
|
-
const resp = await fetch('/api/router/tokens')
|
|
46
|
-
const data = await resp.json()
|
|
47
|
-
setTokens(data)
|
|
48
|
-
return data
|
|
49
|
-
} catch { return null }
|
|
50
|
-
}, [])
|
|
51
|
-
|
|
52
|
-
const fetchSets = useCallback(async () => {
|
|
53
|
-
try {
|
|
54
|
-
const resp = await fetch('/api/router/sets')
|
|
55
|
-
const data = await resp.json()
|
|
56
|
-
setSets(data)
|
|
57
|
-
return data
|
|
58
|
-
} catch { return null }
|
|
59
|
-
}, [])
|
|
60
|
-
|
|
61
|
-
// 📖 Poll daemon status + refresh stats when running
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
let mounted = true
|
|
64
|
-
|
|
65
|
-
const poll = async () => {
|
|
66
|
-
const s = await fetchStatus()
|
|
67
|
-
if (s?.ok && mounted) {
|
|
68
|
-
await fetchStats()
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
poll().then(() => { if (mounted) setLoading(false) })
|
|
73
|
-
pollRef.current = setInterval(poll, POLL_INTERVAL_MS)
|
|
74
|
-
|
|
75
|
-
return () => {
|
|
76
|
-
mounted = false
|
|
77
|
-
clearInterval(pollRef.current)
|
|
78
|
-
}
|
|
79
|
-
}, [fetchStatus, fetchStats])
|
|
80
|
-
|
|
81
|
-
const start = useCallback(async () => {
|
|
82
|
-
setActionLoading(true)
|
|
83
|
-
try {
|
|
84
|
-
const resp = await fetch('/api/router/start', { method: 'POST' })
|
|
85
|
-
const data = await resp.json()
|
|
86
|
-
await fetchStatus()
|
|
87
|
-
if (data.ok) await fetchStats()
|
|
88
|
-
return data
|
|
89
|
-
} finally { setActionLoading(false) }
|
|
90
|
-
}, [fetchStatus, fetchStats])
|
|
91
|
-
|
|
92
|
-
const stop = useCallback(async () => {
|
|
93
|
-
setActionLoading(true)
|
|
94
|
-
try {
|
|
95
|
-
const resp = await fetch('/api/router/stop', { method: 'POST' })
|
|
96
|
-
const data = await resp.json()
|
|
97
|
-
setStatus({ ok: false, running: false })
|
|
98
|
-
setStats(null)
|
|
99
|
-
return data
|
|
100
|
-
} finally { setActionLoading(false) }
|
|
101
|
-
}, [])
|
|
102
|
-
|
|
103
|
-
const setProbeMode = useCallback(async (mode) => {
|
|
104
|
-
try {
|
|
105
|
-
await fetch('/api/router/probe-mode', {
|
|
106
|
-
method: 'POST',
|
|
107
|
-
headers: { 'Content-Type': 'application/json' },
|
|
108
|
-
body: JSON.stringify({ probeMode: mode }),
|
|
109
|
-
})
|
|
110
|
-
await fetchStats()
|
|
111
|
-
} catch {}
|
|
112
|
-
}, [fetchStats])
|
|
113
|
-
|
|
114
|
-
const refresh = useCallback(async () => {
|
|
115
|
-
setLoading(true)
|
|
116
|
-
await Promise.all([fetchStatus(), fetchTokens(), fetchSets()])
|
|
117
|
-
setLoading(false)
|
|
118
|
-
}, [fetchStatus, fetchTokens, fetchSets])
|
|
119
|
-
|
|
120
|
-
const getQuickSetup = useCallback(async () => {
|
|
121
|
-
try {
|
|
122
|
-
const resp = await fetch('/api/router/quick-setup')
|
|
123
|
-
return await resp.json()
|
|
124
|
-
} catch { return null }
|
|
125
|
-
}, [])
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
status, stats, tokens, sets,
|
|
129
|
-
loading, actionLoading,
|
|
130
|
-
start, stop, setProbeMode, refresh,
|
|
131
|
-
fetchTokens, fetchSets, getQuickSetup,
|
|
132
|
-
}
|
|
133
|
-
}
|
package/web/src/hooks/useSSE.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/hooks/useSSE.js
|
|
3
|
-
* @description React hook for SSE (Server-Sent Events) connection.
|
|
4
|
-
* 📖 Connects to /api/events, auto-reconnects on failure, returns live model data.
|
|
5
|
-
* → useSSE
|
|
6
|
-
*/
|
|
7
|
-
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
8
|
-
|
|
9
|
-
const RECONNECT_DELAY = 2000
|
|
10
|
-
|
|
11
|
-
export function useSSE(url = '/api/events') {
|
|
12
|
-
const [models, setModels] = useState([])
|
|
13
|
-
const [connected, setConnected] = useState(false)
|
|
14
|
-
const [updateCount, setUpdateCount] = useState(0)
|
|
15
|
-
const esRef = useRef(null)
|
|
16
|
-
const reconnectRef = useRef(null)
|
|
17
|
-
const mountedRef = useRef(true)
|
|
18
|
-
|
|
19
|
-
const connect = useCallback(() => {
|
|
20
|
-
if (!mountedRef.current) return
|
|
21
|
-
|
|
22
|
-
// Close existing connection
|
|
23
|
-
if (esRef.current) {
|
|
24
|
-
esRef.current.close()
|
|
25
|
-
esRef.current = null
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
clearTimeout(reconnectRef.current)
|
|
29
|
-
|
|
30
|
-
const es = new EventSource(url)
|
|
31
|
-
esRef.current = es
|
|
32
|
-
|
|
33
|
-
es.onopen = () => {
|
|
34
|
-
if (mountedRef.current) setConnected(true)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
es.onmessage = (event) => {
|
|
38
|
-
if (!mountedRef.current) return
|
|
39
|
-
try {
|
|
40
|
-
const data = JSON.parse(event.data)
|
|
41
|
-
setModels(data)
|
|
42
|
-
setUpdateCount(c => c + 1)
|
|
43
|
-
} catch (e) {
|
|
44
|
-
console.warn('[useSSE] parse error:', e)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
es.onerror = () => {
|
|
49
|
-
setConnected(false)
|
|
50
|
-
es.close()
|
|
51
|
-
esRef.current = null
|
|
52
|
-
|
|
53
|
-
if (mountedRef.current) {
|
|
54
|
-
reconnectRef.current = setTimeout(() => {
|
|
55
|
-
if (mountedRef.current) connect()
|
|
56
|
-
}, RECONNECT_DELAY)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}, [url])
|
|
60
|
-
|
|
61
|
-
useEffect(() => {
|
|
62
|
-
mountedRef.current = true
|
|
63
|
-
connect()
|
|
64
|
-
|
|
65
|
-
return () => {
|
|
66
|
-
mountedRef.current = false
|
|
67
|
-
clearTimeout(reconnectRef.current)
|
|
68
|
-
esRef.current?.close()
|
|
69
|
-
}
|
|
70
|
-
}, [connect])
|
|
71
|
-
|
|
72
|
-
return { models, connected, updateCount }
|
|
73
|
-
}
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/hooks/useSocket.js
|
|
3
|
-
* @description Realtime model-data hook with Socket.IO primary, SSE fallback, and REST polling safety net.
|
|
4
|
-
*
|
|
5
|
-
* 📖 The dashboard runs locally, so freshness matters more than shaving every
|
|
6
|
-
* byte. Socket.IO gives the snappiest path in `pnpm dev:web`; SSE keeps Docker /
|
|
7
|
-
* daemon-style servers working without Socket.IO; REST polling guarantees the UI
|
|
8
|
-
* eventually recovers if both streaming transports are interrupted.
|
|
9
|
-
*
|
|
10
|
-
* @functions
|
|
11
|
-
* → useSocket(serverUrl) — Subscribe to live dashboard state
|
|
12
|
-
* @exports useSocket
|
|
13
|
-
*/
|
|
14
|
-
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
15
|
-
import { io } from 'socket.io-client'
|
|
16
|
-
|
|
17
|
-
const REST_FALLBACK_INTERVAL_MS = 2_000
|
|
18
|
-
const STALE_UPDATE_MS = 4_000
|
|
19
|
-
const ACTIVITY_THROTTLE_MS = 1_000
|
|
20
|
-
|
|
21
|
-
function normalizePayload(data) {
|
|
22
|
-
if (!data) return null
|
|
23
|
-
if (Array.isArray(data)) return { models: data }
|
|
24
|
-
if (Array.isArray(data.models)) return data
|
|
25
|
-
return null
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function sameOriginUrl(path, serverUrl) {
|
|
29
|
-
if (!serverUrl) return path
|
|
30
|
-
return `${serverUrl.replace(/\/$/, '')}${path}`
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function useSocket(serverUrl = '') {
|
|
34
|
-
const [models, setModels] = useState([])
|
|
35
|
-
const [connected, setConnected] = useState(false)
|
|
36
|
-
const [transport, setTransport] = useState('connecting')
|
|
37
|
-
const [updateCount, setUpdateCount] = useState(0)
|
|
38
|
-
const [nextPingAt, setNextPingAt] = useState(null)
|
|
39
|
-
const [serverIsPinging, setServerIsPinging] = useState(false)
|
|
40
|
-
const [pendingPings, setPendingPings] = useState(0)
|
|
41
|
-
const [pingMode, setPingMode] = useState('speed')
|
|
42
|
-
const [globalBenchmarkRunning, setGlobalBenchmarkRunning] = useState(false)
|
|
43
|
-
const [globalBenchmarkTotal, setGlobalBenchmarkTotal] = useState(0)
|
|
44
|
-
const [globalBenchmarkCompleted, setGlobalBenchmarkCompleted] = useState(0)
|
|
45
|
-
const [updateStatus, setUpdateStatus] = useState(null)
|
|
46
|
-
|
|
47
|
-
const socketRef = useRef(null)
|
|
48
|
-
const esRef = useRef(null)
|
|
49
|
-
const pollRef = useRef(null)
|
|
50
|
-
const lastUpdateRef = useRef(0)
|
|
51
|
-
const lastActivityRef = useRef(0)
|
|
52
|
-
const mountedRef = useRef(false)
|
|
53
|
-
|
|
54
|
-
const applyPayload = useCallback((raw, source = 'unknown') => {
|
|
55
|
-
const data = normalizePayload(raw)
|
|
56
|
-
if (!data || !mountedRef.current) return
|
|
57
|
-
|
|
58
|
-
setModels(data.models ?? [])
|
|
59
|
-
setPingMode(data.pingMode ?? 'speed')
|
|
60
|
-
setNextPingAt(data.nextPingAt ?? null)
|
|
61
|
-
setServerIsPinging(Boolean(data.isPinging))
|
|
62
|
-
setPendingPings(Number.isFinite(data.pendingPings) ? data.pendingPings : 0)
|
|
63
|
-
setGlobalBenchmarkRunning(Boolean(data.globalBenchmarkRunning))
|
|
64
|
-
setGlobalBenchmarkTotal(Number.isFinite(data.globalBenchmarkTotal) ? data.globalBenchmarkTotal : 0)
|
|
65
|
-
setGlobalBenchmarkCompleted(Number.isFinite(data.globalBenchmarkCompleted) ? data.globalBenchmarkCompleted : 0)
|
|
66
|
-
setUpdateStatus(data.updateStatus && data.updateStatus.allowedOutdated ? data.updateStatus : null)
|
|
67
|
-
setUpdateCount((count) => count + 1)
|
|
68
|
-
lastUpdateRef.current = Date.now()
|
|
69
|
-
if (source !== 'poll') {
|
|
70
|
-
setConnected(true)
|
|
71
|
-
setTransport(source)
|
|
72
|
-
}
|
|
73
|
-
}, [])
|
|
74
|
-
|
|
75
|
-
const fetchSnapshot = useCallback(async () => {
|
|
76
|
-
try {
|
|
77
|
-
const stateResponse = await fetch(sameOriginUrl('/api/state', serverUrl), { headers: { Accept: 'application/json' } })
|
|
78
|
-
if (stateResponse.ok) {
|
|
79
|
-
applyPayload(await stateResponse.json(), 'poll')
|
|
80
|
-
setConnected(true)
|
|
81
|
-
setTransport((current) => current === 'socket' || current === 'sse' ? current : 'poll')
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
} catch {}
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
const response = await fetch(sameOriginUrl('/api/models', serverUrl), { headers: { Accept: 'application/json' } })
|
|
88
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
89
|
-
applyPayload(await response.json(), 'poll')
|
|
90
|
-
setConnected(true)
|
|
91
|
-
setTransport((current) => current === 'socket' || current === 'sse' ? current : 'poll')
|
|
92
|
-
} catch {
|
|
93
|
-
setConnected(false)
|
|
94
|
-
setTransport('offline')
|
|
95
|
-
}
|
|
96
|
-
}, [applyPayload, serverUrl])
|
|
97
|
-
|
|
98
|
-
const startSse = useCallback(() => {
|
|
99
|
-
if (esRef.current || !mountedRef.current) return
|
|
100
|
-
try {
|
|
101
|
-
const es = new EventSource(sameOriginUrl('/api/events', serverUrl))
|
|
102
|
-
esRef.current = es
|
|
103
|
-
es.onopen = () => {
|
|
104
|
-
if (!mountedRef.current) return
|
|
105
|
-
setConnected(true)
|
|
106
|
-
setTransport('sse')
|
|
107
|
-
}
|
|
108
|
-
const handleSsePayload = (event) => {
|
|
109
|
-
try { applyPayload(JSON.parse(event.data), 'sse') }
|
|
110
|
-
catch (err) { console.warn('[useSocket] SSE parse error:', err) }
|
|
111
|
-
}
|
|
112
|
-
es.onmessage = handleSsePayload
|
|
113
|
-
es.addEventListener('models', handleSsePayload)
|
|
114
|
-
es.onerror = () => {
|
|
115
|
-
es.close()
|
|
116
|
-
if (esRef.current === es) esRef.current = null
|
|
117
|
-
if (!socketRef.current?.connected) setConnected(false)
|
|
118
|
-
}
|
|
119
|
-
} catch {
|
|
120
|
-
esRef.current = null
|
|
121
|
-
}
|
|
122
|
-
}, [applyPayload, serverUrl])
|
|
123
|
-
|
|
124
|
-
const sendActivity = useCallback(() => {
|
|
125
|
-
const now = Date.now()
|
|
126
|
-
if (now - lastActivityRef.current < ACTIVITY_THROTTLE_MS) return
|
|
127
|
-
lastActivityRef.current = now
|
|
128
|
-
|
|
129
|
-
if (socketRef.current?.connected) {
|
|
130
|
-
socketRef.current.emit('client:activity')
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
fetch(sameOriginUrl('/api/activity', serverUrl), { method: 'POST' }).catch(() => {})
|
|
135
|
-
}, [serverUrl])
|
|
136
|
-
|
|
137
|
-
useEffect(() => {
|
|
138
|
-
mountedRef.current = true
|
|
139
|
-
|
|
140
|
-
const socket = io(serverUrl || undefined, {
|
|
141
|
-
transports: ['websocket', 'polling'],
|
|
142
|
-
reconnectionDelay: 500,
|
|
143
|
-
reconnectionDelayMax: 2500,
|
|
144
|
-
timeout: 1200,
|
|
145
|
-
})
|
|
146
|
-
socketRef.current = socket
|
|
147
|
-
|
|
148
|
-
socket.on('connect', () => {
|
|
149
|
-
setConnected(true)
|
|
150
|
-
setTransport('socket')
|
|
151
|
-
esRef.current?.close()
|
|
152
|
-
esRef.current = null
|
|
153
|
-
socket.emit('models:refresh')
|
|
154
|
-
socket.emit('client:activity')
|
|
155
|
-
})
|
|
156
|
-
socket.on('disconnect', () => {
|
|
157
|
-
if (!mountedRef.current) return
|
|
158
|
-
setConnected(false)
|
|
159
|
-
startSse()
|
|
160
|
-
})
|
|
161
|
-
socket.on('connect_error', () => {
|
|
162
|
-
if (!mountedRef.current) return
|
|
163
|
-
setConnected(false)
|
|
164
|
-
startSse()
|
|
165
|
-
})
|
|
166
|
-
socket.on('models:update', (data) => applyPayload(data, 'socket'))
|
|
167
|
-
|
|
168
|
-
pollRef.current = setInterval(() => {
|
|
169
|
-
const stale = Date.now() - lastUpdateRef.current > STALE_UPDATE_MS
|
|
170
|
-
if (!socket.connected || stale) void fetchSnapshot()
|
|
171
|
-
}, REST_FALLBACK_INTERVAL_MS)
|
|
172
|
-
|
|
173
|
-
void fetchSnapshot()
|
|
174
|
-
|
|
175
|
-
const activityEvents = ['keydown', 'pointerdown', 'mousemove', 'focus']
|
|
176
|
-
for (const eventName of activityEvents) window.addEventListener(eventName, sendActivity, { passive: true })
|
|
177
|
-
|
|
178
|
-
return () => {
|
|
179
|
-
mountedRef.current = false
|
|
180
|
-
for (const eventName of activityEvents) window.removeEventListener(eventName, sendActivity)
|
|
181
|
-
if (pollRef.current) clearInterval(pollRef.current)
|
|
182
|
-
socket.disconnect()
|
|
183
|
-
esRef.current?.close()
|
|
184
|
-
socketRef.current = null
|
|
185
|
-
esRef.current = null
|
|
186
|
-
}
|
|
187
|
-
}, [applyPayload, fetchSnapshot, sendActivity, serverUrl, startSse])
|
|
188
|
-
|
|
189
|
-
return {
|
|
190
|
-
models,
|
|
191
|
-
connected,
|
|
192
|
-
transport,
|
|
193
|
-
updateCount,
|
|
194
|
-
nextPingAt,
|
|
195
|
-
isPinging: serverIsPinging,
|
|
196
|
-
pendingPings,
|
|
197
|
-
pingMode,
|
|
198
|
-
globalBenchmarkRunning,
|
|
199
|
-
globalBenchmarkTotal,
|
|
200
|
-
globalBenchmarkCompleted,
|
|
201
|
-
updateStatus,
|
|
202
|
-
}
|
|
203
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/hooks/useTheme.js
|
|
3
|
-
* @description React hook for tri-state theme cycle (auto / dark / light) — M1 parity.
|
|
4
|
-
* 📖 Mirrors the TUI's `G` key behavior:
|
|
5
|
-
* 📖 auto → system preference (resolved once at mount)
|
|
6
|
-
* 📖 dark → explicit dark
|
|
7
|
-
* 📖 light → explicit light
|
|
8
|
-
* 📖 Persists the choice on <html data-theme="auto|dark|light">. When set to
|
|
9
|
-
* 📖 `auto`, the hook resolves the actual rendered theme and writes it back to
|
|
10
|
-
* 📖 <html data-theme="..."> via resolveSystemTheme() (light/dark) so the
|
|
11
|
-
* 📖 rest of the CSS keeps working without any "auto" branch in stylesheets.
|
|
12
|
-
*
|
|
13
|
-
* @functions
|
|
14
|
-
* → useTheme() — { theme, resolvedTheme, setTheme, cycle }
|
|
15
|
-
*/
|
|
16
|
-
import { useState, useCallback, useEffect } from 'react'
|
|
17
|
-
|
|
18
|
-
export const THEME_CYCLE = ['auto', 'dark', 'light']
|
|
19
|
-
|
|
20
|
-
// 📖 Read the OS-level dark/light preference. Falls back to dark on browsers
|
|
21
|
-
// 📖 that don't support matchMedia (very rare; SSR snapshots, etc.).
|
|
22
|
-
function resolveSystemTheme() {
|
|
23
|
-
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return 'dark'
|
|
24
|
-
try {
|
|
25
|
-
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
26
|
-
} catch {
|
|
27
|
-
return 'dark'
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function readInitialTheme() {
|
|
32
|
-
if (typeof document === 'undefined') return 'auto'
|
|
33
|
-
const attr = document.documentElement.getAttribute('data-theme')
|
|
34
|
-
if (attr === 'auto' || attr === 'dark' || attr === 'light') return attr
|
|
35
|
-
return 'auto'
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function useTheme() {
|
|
39
|
-
const [theme, setThemeState] = useState(readInitialTheme)
|
|
40
|
-
const [systemTheme, setSystemTheme] = useState(resolveSystemTheme)
|
|
41
|
-
|
|
42
|
-
// 📖 Listen for OS theme changes while the user is in "auto" mode.
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return
|
|
45
|
-
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
|
46
|
-
const handler = (e) => setSystemTheme(e.matches ? 'dark' : 'light')
|
|
47
|
-
if (typeof mq.addEventListener === 'function') {
|
|
48
|
-
mq.addEventListener('change', handler)
|
|
49
|
-
return () => mq.removeEventListener('change', handler)
|
|
50
|
-
}
|
|
51
|
-
// 📖 Safari < 14 fallback
|
|
52
|
-
if (typeof mq.addListener === 'function') {
|
|
53
|
-
mq.addListener(handler)
|
|
54
|
-
return () => mq.removeListener(handler)
|
|
55
|
-
}
|
|
56
|
-
}, [])
|
|
57
|
-
|
|
58
|
-
// 📖 Apply the resolved theme to <html data-theme="..."> on every change.
|
|
59
|
-
// 📖 We write the *resolved* value (light / dark) so existing CSS keeps working.
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
const resolved = theme === 'auto' ? systemTheme : theme
|
|
62
|
-
document.documentElement.setAttribute('data-theme', resolved)
|
|
63
|
-
// 📖 Also stamp the user-chosen mode so other parts of the app can read it
|
|
64
|
-
// 📖 (e.g. to render the right "auto" badge in the theme button).
|
|
65
|
-
document.documentElement.setAttribute('data-theme-preference', theme)
|
|
66
|
-
}, [theme, systemTheme])
|
|
67
|
-
|
|
68
|
-
// 📖 Cycle through auto → dark → light → auto, matching the TUI's `G` key.
|
|
69
|
-
const cycle = useCallback(() => {
|
|
70
|
-
setThemeState((prev) => {
|
|
71
|
-
const idx = THEME_CYCLE.indexOf(prev)
|
|
72
|
-
return THEME_CYCLE[(idx + 1) % THEME_CYCLE.length]
|
|
73
|
-
})
|
|
74
|
-
}, [])
|
|
75
|
-
|
|
76
|
-
const setTheme = useCallback((next) => {
|
|
77
|
-
if (THEME_CYCLE.includes(next)) setThemeState(next)
|
|
78
|
-
}, [])
|
|
79
|
-
|
|
80
|
-
const resolvedTheme = theme === 'auto' ? systemTheme : theme
|
|
81
|
-
|
|
82
|
-
return { theme, resolvedTheme, setTheme, cycle }
|
|
83
|
-
}
|