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.
- package/README.md +110 -881
- package/bin/free-coding-models.js +4 -1
- package/changelog/v0.5.86.md +10 -0
- 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 +47 -16
- package/src/tui/command-palette.js +22 -2
- package/src/tui/key-handler.js +123 -179
- package/src/tui/overlays.js +35 -75
- package/src/tui/render-helpers.js +62 -4
- package/src/tui/render-table.js +46 -5
- 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-DWUely84.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,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/hooks/useTokenUsage.js
|
|
3
|
-
* @description Hook for Token Usage panel — fetches token data from daemon or file.
|
|
4
|
-
* 📖 M4: Returns today's usage, 7-day breakdown, top models, top providers.
|
|
5
|
-
*
|
|
6
|
-
* @functions useTokenUsage → { data, loading, refresh }
|
|
7
|
-
*/
|
|
8
|
-
import { useState, useEffect, useCallback } from 'react'
|
|
9
|
-
|
|
10
|
-
export function useTokenUsage() {
|
|
11
|
-
const [data, setData] = useState(null)
|
|
12
|
-
const [loading, setLoading] = useState(true)
|
|
13
|
-
|
|
14
|
-
const refresh = useCallback(async () => {
|
|
15
|
-
setLoading(true)
|
|
16
|
-
try {
|
|
17
|
-
const resp = await fetch('/api/router/tokens')
|
|
18
|
-
const raw = await resp.json()
|
|
19
|
-
setData(processTokenData(raw))
|
|
20
|
-
} catch {
|
|
21
|
-
setData(null)
|
|
22
|
-
} finally {
|
|
23
|
-
setLoading(false)
|
|
24
|
-
}
|
|
25
|
-
}, [])
|
|
26
|
-
|
|
27
|
-
useEffect(() => { void refresh() }, [refresh])
|
|
28
|
-
|
|
29
|
-
return { data, loading, refresh }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function processTokenData(raw) {
|
|
33
|
-
if (!raw) return null
|
|
34
|
-
|
|
35
|
-
const daily = raw.daily || {}
|
|
36
|
-
const allTime = raw.all_time || {}
|
|
37
|
-
|
|
38
|
-
// 📖 Build 7-day chart data
|
|
39
|
-
const days = []
|
|
40
|
-
const today = new Date()
|
|
41
|
-
for (let i = 6; i >= 0; i--) {
|
|
42
|
-
const d = new Date(today)
|
|
43
|
-
d.setDate(d.getDate() - i)
|
|
44
|
-
const key = d.toISOString().slice(0, 10)
|
|
45
|
-
const entry = daily[key] || { total_tokens: 0, requests: 0 }
|
|
46
|
-
days.push({
|
|
47
|
-
date: key,
|
|
48
|
-
label: d.toLocaleDateString('en', { weekday: 'short' }),
|
|
49
|
-
totalTokens: entry.total_tokens || 0,
|
|
50
|
-
requests: entry.requests || 0,
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// 📖 Top models across all tracked days
|
|
55
|
-
const modelTotals = {}
|
|
56
|
-
for (const entry of Object.values(daily)) {
|
|
57
|
-
const byModel = entry.by_model || {}
|
|
58
|
-
for (const [key, val] of Object.entries(byModel)) {
|
|
59
|
-
if (!modelTotals[key]) modelTotals[key] = { total: 0, requests: 0 }
|
|
60
|
-
modelTotals[key].total += val.total || 0
|
|
61
|
-
modelTotals[key].requests += val.requests || 0
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
const topModels = Object.entries(modelTotals)
|
|
65
|
-
.sort((a, b) => b[1].total - a[1].total)
|
|
66
|
-
.slice(0, 10)
|
|
67
|
-
.map(([key, val]) => ({ key, ...val }))
|
|
68
|
-
|
|
69
|
-
// 📖 Top providers (aggregated from model keys)
|
|
70
|
-
const providerTotals = {}
|
|
71
|
-
for (const [key, val] of Object.entries(modelTotals)) {
|
|
72
|
-
const provider = key.split('/')[0]
|
|
73
|
-
if (!providerTotals[provider]) providerTotals[provider] = { total: 0, requests: 0 }
|
|
74
|
-
providerTotals[provider].total += val.total
|
|
75
|
-
providerTotals[provider].requests += val.requests
|
|
76
|
-
}
|
|
77
|
-
const topProviders = Object.entries(providerTotals)
|
|
78
|
-
.sort((a, b) => b[1].total - a[1].total)
|
|
79
|
-
.slice(0, 10)
|
|
80
|
-
.map(([key, val]) => ({ key, ...val }))
|
|
81
|
-
|
|
82
|
-
const todayKey = today.toISOString().slice(0, 10)
|
|
83
|
-
const todayData = daily[todayKey] || {}
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
today: {
|
|
87
|
-
totalTokens: todayData.total_tokens || 0,
|
|
88
|
-
promptTokens: todayData.prompt_tokens || 0,
|
|
89
|
-
completionTokens: todayData.completion_tokens || 0,
|
|
90
|
-
requests: todayData.requests || 0,
|
|
91
|
-
},
|
|
92
|
-
allTime: {
|
|
93
|
-
totalTokens: allTime.total_tokens || 0,
|
|
94
|
-
promptTokens: allTime.prompt_tokens || 0,
|
|
95
|
-
completionTokens: allTime.completion_tokens || 0,
|
|
96
|
-
requests: allTime.requests || 0,
|
|
97
|
-
},
|
|
98
|
-
sevenDays: days,
|
|
99
|
-
topModels,
|
|
100
|
-
topProviders,
|
|
101
|
-
hasData: allTime.total_tokens > 0 || Object.keys(daily).length > 0,
|
|
102
|
-
}
|
|
103
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/hooks/useToolMode.js
|
|
3
|
-
* @description M3 tool-mode hook for the Web Dashboard. It cycles endpoint install
|
|
4
|
-
* targets by loading and persisting `settings.preferredToolMode` through
|
|
5
|
-
* `/api/tool-mode`, while keeping URL hydration safe from late API responses.
|
|
6
|
-
*
|
|
7
|
-
* @functions
|
|
8
|
-
* → useToolMode — loads, sets, and cycles the active endpoint target
|
|
9
|
-
* @exports useToolMode
|
|
10
|
-
*/
|
|
11
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
12
|
-
import { INSTALL_ENDPOINT_TOOL_MODES } from '../utils/m3.js'
|
|
13
|
-
|
|
14
|
-
const DEFAULT_TOOL_MODE = 'opencode'
|
|
15
|
-
|
|
16
|
-
function normalizeToolMode(mode) {
|
|
17
|
-
return INSTALL_ENDPOINT_TOOL_MODES.includes(mode) ? mode : DEFAULT_TOOL_MODE
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function useToolMode({ onToast } = {}) {
|
|
21
|
-
const [toolModeState, setToolModeState] = useState(DEFAULT_TOOL_MODE)
|
|
22
|
-
const userSetRef = useRef(false)
|
|
23
|
-
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
let cancelled = false
|
|
26
|
-
async function loadToolMode() {
|
|
27
|
-
try {
|
|
28
|
-
const resp = await fetch('/api/tool-mode')
|
|
29
|
-
const payload = await resp.json().catch(() => ({}))
|
|
30
|
-
if (!cancelled && !userSetRef.current && resp.ok) {
|
|
31
|
-
setToolModeState(normalizeToolMode(payload.mode))
|
|
32
|
-
}
|
|
33
|
-
} catch (err) {
|
|
34
|
-
if (!cancelled) onToast?.(`Tool mode load failed: ${err.message}`, 'error')
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
void loadToolMode()
|
|
38
|
-
return () => { cancelled = true }
|
|
39
|
-
}, [onToast])
|
|
40
|
-
|
|
41
|
-
const persistToolMode = useCallback(async (mode) => {
|
|
42
|
-
const normalized = normalizeToolMode(mode)
|
|
43
|
-
userSetRef.current = true
|
|
44
|
-
setToolModeState(normalized)
|
|
45
|
-
try {
|
|
46
|
-
const resp = await fetch('/api/tool-mode', {
|
|
47
|
-
method: 'POST',
|
|
48
|
-
headers: { 'Content-Type': 'application/json' },
|
|
49
|
-
body: JSON.stringify({ mode: normalized }),
|
|
50
|
-
})
|
|
51
|
-
const payload = await resp.json().catch(() => ({}))
|
|
52
|
-
if (!resp.ok) {
|
|
53
|
-
throw new Error(payload.error || `HTTP ${resp.status}`)
|
|
54
|
-
}
|
|
55
|
-
setToolModeState(normalizeToolMode(payload.mode))
|
|
56
|
-
return { ok: true, mode: normalizeToolMode(payload.mode) }
|
|
57
|
-
} catch (err) {
|
|
58
|
-
onToast?.(`Tool mode save failed: ${err.message}`, 'error')
|
|
59
|
-
return { ok: false, error: err.message, mode: normalized }
|
|
60
|
-
}
|
|
61
|
-
}, [onToast])
|
|
62
|
-
|
|
63
|
-
const cycleToolMode = useCallback(() => {
|
|
64
|
-
const currentIndex = INSTALL_ENDPOINT_TOOL_MODES.indexOf(toolModeState)
|
|
65
|
-
const next = INSTALL_ENDPOINT_TOOL_MODES[(currentIndex + 1) % INSTALL_ENDPOINT_TOOL_MODES.length] || DEFAULT_TOOL_MODE
|
|
66
|
-
return persistToolMode(next)
|
|
67
|
-
}, [persistToolMode, toolModeState])
|
|
68
|
-
|
|
69
|
-
const tools = useMemo(() => [...INSTALL_ENDPOINT_TOOL_MODES], [])
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
toolMode: toolModeState,
|
|
73
|
-
tools,
|
|
74
|
-
setToolMode: persistToolMode,
|
|
75
|
-
cycleToolMode,
|
|
76
|
-
}
|
|
77
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/hooks/useUpdateChecker.js
|
|
3
|
-
* @description React hook for the update chip + popover — M2 parity with TUI `Shift+U`.
|
|
4
|
-
* 📖 Polls `/api/version` every 5 minutes (matches the TUI cadence) and exposes
|
|
5
|
-
* 📖 `updateAvailable` (boolean) + `latestVersion` (string) for the header chip.
|
|
6
|
-
* 📖 The chip's "Update now" button calls `/api/update/run` which spawns the
|
|
7
|
-
* 📖 detected package manager in the background; the Web UI just surfaces a
|
|
8
|
-
* 📖 toast and tells the user to restart the dashboard to apply the update.
|
|
9
|
-
*
|
|
10
|
-
* @functions
|
|
11
|
-
* → useUpdateChecker({ onToast }) — { latestVersion, updateAvailable, runUpdate, checkNow, loading }
|
|
12
|
-
*/
|
|
13
|
-
import { useCallback, useEffect, useState, useRef } from 'react'
|
|
14
|
-
|
|
15
|
-
const POLL_INTERVAL_MS = 5 * 60_000
|
|
16
|
-
|
|
17
|
-
// 📖 Lightweight semver compare: returns 1 if a > b, -1 if a < b, 0 if equal.
|
|
18
|
-
function semverCompare(a, b) {
|
|
19
|
-
if (!a || !b) return 0
|
|
20
|
-
const ap = a.replace(/^v/, '').split('.').map(Number)
|
|
21
|
-
const bp = b.replace(/^v/, '').split('.').map(Number)
|
|
22
|
-
for (let i = 0; i < Math.max(ap.length, bp.length); i++) {
|
|
23
|
-
const av = ap[i] || 0
|
|
24
|
-
const bv = bp[i] || 0
|
|
25
|
-
if (bv !== av) return bv - av
|
|
26
|
-
}
|
|
27
|
-
return 0
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function useUpdateChecker({ onToast } = {}) {
|
|
31
|
-
const [localVersion, setLocalVersion] = useState(null)
|
|
32
|
-
const [latestVersion, setLatestVersion] = useState(null)
|
|
33
|
-
const [lastReleaseDate, setLastReleaseDate] = useState(null)
|
|
34
|
-
const [error, setError] = useState(null)
|
|
35
|
-
const [loading, setLoading] = useState(false)
|
|
36
|
-
const intervalRef = useRef(null)
|
|
37
|
-
|
|
38
|
-
const checkNow = useCallback(async () => {
|
|
39
|
-
setLoading(true)
|
|
40
|
-
try {
|
|
41
|
-
const resp = await fetch('/api/version')
|
|
42
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
|
43
|
-
const data = await resp.json()
|
|
44
|
-
setLocalVersion(data.local ?? null)
|
|
45
|
-
setLatestVersion(data.latest ?? null)
|
|
46
|
-
setLastReleaseDate(data.lastReleaseDate ?? null)
|
|
47
|
-
setError(data.error ?? null)
|
|
48
|
-
} catch (err) {
|
|
49
|
-
setError(err.message || 'update check failed')
|
|
50
|
-
} finally {
|
|
51
|
-
setLoading(false)
|
|
52
|
-
}
|
|
53
|
-
}, [])
|
|
54
|
-
|
|
55
|
-
// 📖 Initial check + 5-minute polling. We stop polling on unmount.
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
checkNow()
|
|
58
|
-
intervalRef.current = setInterval(checkNow, POLL_INTERVAL_MS)
|
|
59
|
-
return () => {
|
|
60
|
-
if (intervalRef.current) clearInterval(intervalRef.current)
|
|
61
|
-
}
|
|
62
|
-
}, [checkNow])
|
|
63
|
-
|
|
64
|
-
// 📖 updateAvailable is true when latest > local. A null latest means the
|
|
65
|
-
// 📖 npm registry is unreachable, so we don't surface a chip.
|
|
66
|
-
const updateAvailable = Boolean(latestVersion && localVersion && semverCompare(latestVersion, localVersion) > 0)
|
|
67
|
-
|
|
68
|
-
const runUpdate = useCallback(async () => {
|
|
69
|
-
if (!updateAvailable) {
|
|
70
|
-
onToast?.('No update available.', 'info')
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
try {
|
|
74
|
-
const resp = await fetch('/api/update/run', {
|
|
75
|
-
method: 'POST',
|
|
76
|
-
headers: { 'Content-Type': 'application/json' },
|
|
77
|
-
body: JSON.stringify({ version: latestVersion }),
|
|
78
|
-
})
|
|
79
|
-
const data = await resp.json().catch(() => ({}))
|
|
80
|
-
if (resp.ok && data?.started) {
|
|
81
|
-
onToast?.(`Update to v${latestVersion} started — restart the dashboard to apply.`, 'success')
|
|
82
|
-
} else {
|
|
83
|
-
onToast?.(data?.error || data?.message || 'Update failed', 'error')
|
|
84
|
-
}
|
|
85
|
-
} catch (err) {
|
|
86
|
-
onToast?.(err.message || 'Update request failed', 'error')
|
|
87
|
-
}
|
|
88
|
-
}, [updateAvailable, latestVersion, onToast])
|
|
89
|
-
|
|
90
|
-
return { localVersion, latestVersion, lastReleaseDate, updateAvailable, checkNow, runUpdate, loading, error }
|
|
91
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/hooks/useUrlState.js
|
|
3
|
-
* @description URL deep-linking hook — the Web's answer to TUI CLI flags (--tier, --sort, etc.).
|
|
4
|
-
* 📖 M1 = read-only hydration on mount.
|
|
5
|
-
* 📖 M2 = adds write-back: every filter / sort / view / tool-mode / palette
|
|
6
|
-
* 📖 change pushes to the URL via `history.replaceState` so the URL stays
|
|
7
|
-
* 📖 in sync with the visible Web state and any URL is shareable.
|
|
8
|
-
*
|
|
9
|
-
* 📖 URL params (all optional, all shareable):
|
|
10
|
-
* 📖 ?view=dashboard|settings|analytics|recommend|router|help|changelog
|
|
11
|
-
* 📖 ?tier=S+|S|A+|A|A-|B+|B|C|all
|
|
12
|
-
* 📖 ?status=up|down|pending|all
|
|
13
|
-
* 📖 ?provider=<providerKey>|all
|
|
14
|
-
* 📖 ?verdict=<verdict>|all
|
|
15
|
-
* 📖 ?health=<health>|all
|
|
16
|
-
* 📖 ?sort=<col>&dir=asc|desc
|
|
17
|
-
* 📖 ?q=<searchText>
|
|
18
|
-
* 📖 ?toolMode=<toolKey>
|
|
19
|
-
* 📖 ?palette=open (when the palette should be open on load)
|
|
20
|
-
*
|
|
21
|
-
* @functions
|
|
22
|
-
* → useUrlState({ currentView, setCurrentView, filterState, paletteOpen, setPaletteOpen })
|
|
23
|
-
* → buildUrlParams(state) — pure helper exposed for tests
|
|
24
|
-
*/
|
|
25
|
-
import { useEffect, useRef } from 'react'
|
|
26
|
-
import { VALID_TIERS, VALID_STATUS, VALID_SORTS, VALID_VIEWS, VALID_DIRS, VALID_TOOL_MODES } from './urlState.constants.js'
|
|
27
|
-
|
|
28
|
-
// 📖 Read the current URL params as a normalized object. Returns null on SSR
|
|
29
|
-
// 📖 or when the URL is invalid.
|
|
30
|
-
function parseUrlParams() {
|
|
31
|
-
if (typeof window === 'undefined') return null
|
|
32
|
-
const params = new URLSearchParams(window.location.search)
|
|
33
|
-
const out = {}
|
|
34
|
-
|
|
35
|
-
// 📖 Read pathname as the view slug
|
|
36
|
-
const pathname = window.location.pathname.replace(/^\/|\/$/g, '')
|
|
37
|
-
if (pathname && VALID_VIEWS.has(pathname)) {
|
|
38
|
-
out.view = pathname
|
|
39
|
-
} else if (params.has('view') && VALID_VIEWS.has(params.get('view'))) {
|
|
40
|
-
out.view = params.get('view')
|
|
41
|
-
} else {
|
|
42
|
-
out.view = 'dashboard'
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (params.has('tier') && VALID_TIERS.has(params.get('tier'))) out.tier = params.get('tier')
|
|
46
|
-
if (params.has('status') && VALID_STATUS.has(params.get('status'))) out.status = params.get('status')
|
|
47
|
-
if (params.has('provider')) out.provider = params.get('provider')
|
|
48
|
-
if (params.has('verdict')) out.verdict = params.get('verdict')
|
|
49
|
-
if (params.has('health')) out.health = params.get('health')
|
|
50
|
-
if (params.has('sort') && VALID_SORTS.has(params.get('sort'))) out.sort = params.get('sort')
|
|
51
|
-
if (params.has('dir') && VALID_DIRS.has(params.get('dir'))) out.dir = params.get('dir')
|
|
52
|
-
if (params.has('q')) out.q = params.get('q')
|
|
53
|
-
if (params.has('toolMode') && VALID_TOOL_MODES.has(params.get('toolMode'))) out.toolMode = params.get('toolMode')
|
|
54
|
-
if (params.has('recommend') && ['1', 'true', 'open'].includes(params.get('recommend'))) out.view = 'recommend'
|
|
55
|
-
if (params.has('palette') && params.get('palette') === 'open') out.palette = 'open'
|
|
56
|
-
return out
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// 📖 Build a URLSearchParams object from the live state. Pure for testing.
|
|
60
|
-
export function buildUrlParams(state) {
|
|
61
|
-
const params = new URLSearchParams()
|
|
62
|
-
if (!state) return params
|
|
63
|
-
if (state.currentView && state.currentView !== 'dashboard') params.set('view', state.currentView)
|
|
64
|
-
if (state.filterTier && state.filterTier !== 'all') params.set('tier', state.filterTier)
|
|
65
|
-
if (state.filterStatus && state.filterStatus !== 'all') params.set('status', state.filterStatus)
|
|
66
|
-
if (state.filterProvider && state.filterProvider !== 'all') params.set('provider', state.filterProvider)
|
|
67
|
-
if (state.filterVerdict && state.filterVerdict !== 'all') params.set('verdict', state.filterVerdict)
|
|
68
|
-
if (state.filterHealth && state.filterHealth !== 'all') params.set('health', state.filterHealth)
|
|
69
|
-
if (state.sortColumn) {
|
|
70
|
-
params.set('sort', state.sortColumn)
|
|
71
|
-
if (state.sortDirection) params.set('dir', state.sortDirection)
|
|
72
|
-
}
|
|
73
|
-
if (state.searchQuery) params.set('q', state.searchQuery)
|
|
74
|
-
if (state.toolMode) params.set('toolMode', state.toolMode)
|
|
75
|
-
if (state.paletteOpen) params.set('palette', 'open')
|
|
76
|
-
return params
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// 📖 Debounced write-back helper. We don't want to push 5 history entries per
|
|
80
|
-
// 📖 second when the user is typing in the search box.
|
|
81
|
-
function writeUrl(state) {
|
|
82
|
-
if (typeof window === 'undefined') return
|
|
83
|
-
const params = buildUrlParams(state)
|
|
84
|
-
|
|
85
|
-
// 📖 Extract view from the search params so it becomes the pathname slug instead of a search param!
|
|
86
|
-
const view = params.get('view') || 'dashboard'
|
|
87
|
-
params.delete('view')
|
|
88
|
-
|
|
89
|
-
const search = params.toString()
|
|
90
|
-
const path = view !== 'dashboard' ? `/${view}` : '/'
|
|
91
|
-
const newUrl = search
|
|
92
|
-
? `${path}?${search}${window.location.hash}`
|
|
93
|
-
: `${path}${window.location.hash}`
|
|
94
|
-
// 📖 replaceState (not pushState) so back/forward don't fill up with
|
|
95
|
-
// 📖 every keystroke. The current URL still updates visibly.
|
|
96
|
-
window.history.replaceState({}, '', newUrl)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function useUrlState({
|
|
100
|
-
currentView, setCurrentView,
|
|
101
|
-
filterState = null,
|
|
102
|
-
paletteOpen = false, setPaletteOpen = () => {},
|
|
103
|
-
toolMode = null, setToolMode = () => {},
|
|
104
|
-
}) {
|
|
105
|
-
// 📖 Track a debounce handle so we can coalesce rapid filter / sort changes.
|
|
106
|
-
const writeTimerRef = useRef(null)
|
|
107
|
-
|
|
108
|
-
// 📖 Hydrate from URL on mount, exactly once. We don't want to re-hydrate
|
|
109
|
-
// 📖 on every render — the user's actions should drive the URL, not vice versa.
|
|
110
|
-
const hydratedRef = useRef(false)
|
|
111
|
-
useEffect(() => {
|
|
112
|
-
if (hydratedRef.current) return
|
|
113
|
-
hydratedRef.current = true
|
|
114
|
-
|
|
115
|
-
const params = parseUrlParams()
|
|
116
|
-
if (!params) return
|
|
117
|
-
|
|
118
|
-
if (params.view && setCurrentView && params.view !== currentView) {
|
|
119
|
-
setCurrentView(params.view)
|
|
120
|
-
}
|
|
121
|
-
if (params.tier && filterState?.setFilterTier) filterState.setFilterTier(params.tier)
|
|
122
|
-
if (params.status && filterState?.setFilterStatus) filterState.setFilterStatus(params.status)
|
|
123
|
-
if (params.provider && filterState?.setFilterProvider) filterState.setFilterProvider(params.provider)
|
|
124
|
-
if (params.verdict && filterState?.setFilterVerdict) filterState.setFilterVerdict(params.verdict)
|
|
125
|
-
if (params.health && filterState?.setFilterHealth) filterState.setFilterHealth(params.health)
|
|
126
|
-
if (params.sort && filterState?.toggleSort) {
|
|
127
|
-
// 📖 Re-trigger sort: pass the column to setSortColumn + setSortDirection
|
|
128
|
-
// 📖 The hook exposes toggleSort which is a 3-state cycle; for hydration
|
|
129
|
-
// 📖 we want a deterministic state, so we use the lower-level setters.
|
|
130
|
-
if (filterState.setSortColumn) filterState.setSortColumn(params.sort)
|
|
131
|
-
if (params.dir && filterState.setSortDirection) filterState.setSortDirection(params.dir)
|
|
132
|
-
}
|
|
133
|
-
if (params.q !== undefined && filterState?.setSearchQuery) filterState.setSearchQuery(params.q)
|
|
134
|
-
if (params.toolMode && setToolMode) setToolMode(params.toolMode)
|
|
135
|
-
if (params.palette === 'open' && setPaletteOpen) setPaletteOpen(true)
|
|
136
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
137
|
-
}, [])
|
|
138
|
-
|
|
139
|
-
// 📖 Write-back: any change to currentView / filter / toolMode / palette
|
|
140
|
-
// 📖 updates the URL. Debounced at 80ms so rapid filter typing doesn't
|
|
141
|
-
// 📖 thrash the history stack.
|
|
142
|
-
useEffect(() => {
|
|
143
|
-
if (!hydratedRef.current) return
|
|
144
|
-
if (writeTimerRef.current) clearTimeout(writeTimerRef.current)
|
|
145
|
-
writeTimerRef.current = setTimeout(() => {
|
|
146
|
-
writeUrl({
|
|
147
|
-
currentView,
|
|
148
|
-
filterTier: filterState?.filterTier,
|
|
149
|
-
filterStatus: filterState?.filterStatus,
|
|
150
|
-
filterProvider: filterState?.filterProvider,
|
|
151
|
-
filterVerdict: filterState?.filterVerdict,
|
|
152
|
-
filterHealth: filterState?.filterHealth,
|
|
153
|
-
sortColumn: filterState?.sortColumn,
|
|
154
|
-
sortDirection: filterState?.sortDirection,
|
|
155
|
-
searchQuery: filterState?.searchQuery,
|
|
156
|
-
toolMode,
|
|
157
|
-
paletteOpen,
|
|
158
|
-
})
|
|
159
|
-
}, 80)
|
|
160
|
-
return () => {
|
|
161
|
-
if (writeTimerRef.current) clearTimeout(writeTimerRef.current)
|
|
162
|
-
}
|
|
163
|
-
}, [
|
|
164
|
-
currentView, paletteOpen, toolMode,
|
|
165
|
-
filterState?.filterTier, filterState?.filterStatus, filterState?.filterProvider,
|
|
166
|
-
filterState?.filterVerdict, filterState?.filterHealth,
|
|
167
|
-
filterState?.sortColumn, filterState?.sortDirection,
|
|
168
|
-
filterState?.searchQuery,
|
|
169
|
-
])
|
|
170
|
-
}
|
package/web/src/main.jsx
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/main.jsx
|
|
3
|
-
* @description React application entry point — mounts the App component into the DOM.
|
|
4
|
-
* 📖 Imports global CSS (design tokens, base styles, background effects), then renders <App />.
|
|
5
|
-
*/
|
|
6
|
-
import { StrictMode } from 'react'
|
|
7
|
-
import { createRoot } from 'react-dom/client'
|
|
8
|
-
import App from './App.jsx'
|
|
9
|
-
import './global.css'
|
|
10
|
-
|
|
11
|
-
createRoot(document.getElementById('root')).render(
|
|
12
|
-
<StrictMode>
|
|
13
|
-
<App />
|
|
14
|
-
</StrictMode>,
|
|
15
|
-
)
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/utils/download.js
|
|
3
|
-
* @description File download helper for export functionality.
|
|
4
|
-
* → downloadFile
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export function downloadFile(content, filename, type) {
|
|
8
|
-
const blob = new Blob([content], { type })
|
|
9
|
-
const url = URL.createObjectURL(blob)
|
|
10
|
-
const a = document.createElement('a')
|
|
11
|
-
a.href = url
|
|
12
|
-
a.download = filename
|
|
13
|
-
a.click()
|
|
14
|
-
URL.revokeObjectURL(url)
|
|
15
|
-
}
|
package/web/src/utils/format.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/utils/format.js
|
|
3
|
-
* @description Shared formatting utilities for the web dashboard.
|
|
4
|
-
* → formatPing, formatAvg, formatCtx, maskKey, escapeHtml
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export function formatPing(ms, code) {
|
|
8
|
-
if (ms == null) return { text: '—', cls: 'pingNone' }
|
|
9
|
-
if (code === '429') return { text: '429', cls: 'pingSlow' }
|
|
10
|
-
if (code === '000') return { text: 'TIMEOUT', cls: 'pingSlow' }
|
|
11
|
-
return { text: `${ms}ms`, cls: pingClass(ms) }
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function formatAvg(avg) {
|
|
15
|
-
if (avg == null || avg === Infinity || avg > 99000) return { text: '—', cls: 'pingNone' }
|
|
16
|
-
return { text: `${avg}ms`, cls: pingClass(avg) }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function pingClass(ms) {
|
|
20
|
-
if (ms == null || ms === Infinity) return 'pingNone'
|
|
21
|
-
if (ms < 500) return 'pingFast'
|
|
22
|
-
if (ms < 1500) return 'pingMedium'
|
|
23
|
-
return 'pingSlow'
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function formatCtx(c) {
|
|
27
|
-
if (!c || c === '—') return 0
|
|
28
|
-
const s = c.toLowerCase()
|
|
29
|
-
if (s.includes('m')) return parseFloat(s) * 1000
|
|
30
|
-
if (s.includes('k')) return parseFloat(s)
|
|
31
|
-
return 0
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function maskKey(key) {
|
|
35
|
-
if (!key || key.length < 8) return '••••••••'
|
|
36
|
-
return '••••••••' + key.slice(-4)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function escapeHtml(str) {
|
|
40
|
-
if (!str) return ''
|
|
41
|
-
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
42
|
-
}
|
package/web/src/utils/m3.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/utils/m3.js
|
|
3
|
-
* @description Small pure helpers for M3 Web parity tests. They keep payload
|
|
4
|
-
* validation/formatting deterministic without importing React components.
|
|
5
|
-
*
|
|
6
|
-
* @functions
|
|
7
|
-
* → recommendScoreShape — validates `/api/recommend` Top 3 payload shape
|
|
8
|
-
* → toolInstallSummary — formats a tool install plan for display
|
|
9
|
-
* @exports INSTALL_ENDPOINT_TOOL_MODES, recommendScoreShape, toolInstallSummary
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export const INSTALL_ENDPOINT_TOOL_MODES = [
|
|
13
|
-
'opencode',
|
|
14
|
-
'pi',
|
|
15
|
-
'jcode',
|
|
16
|
-
'opencode-desktop',
|
|
17
|
-
'opencode-web',
|
|
18
|
-
'openclaw',
|
|
19
|
-
'crush',
|
|
20
|
-
'goose',
|
|
21
|
-
'aider',
|
|
22
|
-
'qwen',
|
|
23
|
-
'openhands',
|
|
24
|
-
'amp',
|
|
25
|
-
'forgecode',
|
|
26
|
-
'fcm_router',
|
|
27
|
-
'zcode',
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
export function recommendScoreShape(payload) {
|
|
31
|
-
const top3 = Array.isArray(payload?.top3) ? payload.top3 : []
|
|
32
|
-
return top3.every((entry) => {
|
|
33
|
-
const result = entry?.result
|
|
34
|
-
return Boolean(result)
|
|
35
|
-
&& typeof result.providerKey === 'string'
|
|
36
|
-
&& typeof result.modelId === 'string'
|
|
37
|
-
&& typeof result.label === 'string'
|
|
38
|
-
&& typeof entry.score === 'number'
|
|
39
|
-
&& entry.score >= 0
|
|
40
|
-
&& entry.score <= 100
|
|
41
|
-
&& typeof entry.reason === 'string'
|
|
42
|
-
&& entry.reason.length > 0
|
|
43
|
-
})
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function toolInstallSummary(plan) {
|
|
47
|
-
if (!plan || typeof plan !== 'object') {
|
|
48
|
-
return { supported: false, title: 'Unknown tool', command: null, docsUrl: null, note: null }
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
supported: plan.supported === true,
|
|
52
|
-
title: plan.summary || plan.reason || `Install ${plan.mode || 'tool'}`,
|
|
53
|
-
command: typeof plan.shellCommand === 'string' ? plan.shellCommand : null,
|
|
54
|
-
docsUrl: typeof plan.docsUrl === 'string' ? plan.docsUrl : null,
|
|
55
|
-
note: typeof plan.note === 'string' ? plan.note : null,
|
|
56
|
-
}
|
|
57
|
-
}
|
package/web/src/utils/ranks.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/utils/ranks.js
|
|
3
|
-
* @description Tier, verdict, and SWE ranking maps and helpers.
|
|
4
|
-
* → tierRank, verdictRank, parseSwe, sweClass
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const TIER_RANKS = { 'S+': 0, S: 1, 'A+': 2, A: 3, 'A-': 4, 'B+': 5, B: 6, C: 7 }
|
|
8
|
-
const VERDICT_RANKS = {
|
|
9
|
-
Perfect: 0, Normal: 1, Slow: 2, Spiky: 3,
|
|
10
|
-
'Very Slow': 4, Overloaded: 5, Unstable: 6,
|
|
11
|
-
'Not Active': 7, Pending: 8,
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function tierRank(tier) { return TIER_RANKS[tier] ?? 99 }
|
|
15
|
-
export function verdictRank(verdict) { return VERDICT_RANKS[verdict] ?? 99 }
|
|
16
|
-
|
|
17
|
-
export function parseSwe(s) {
|
|
18
|
-
if (!s || s === '—') return 0
|
|
19
|
-
return parseFloat(s.replace('%', '')) || 0
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function sweClass(swe) {
|
|
23
|
-
const val = parseSwe(swe)
|
|
24
|
-
if (val >= 65) return 'sweHigh'
|
|
25
|
-
if (val >= 40) return 'sweMid'
|
|
26
|
-
return 'sweLow'
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function verdictCls(verdict) {
|
|
30
|
-
if (!verdict) return 'pending'
|
|
31
|
-
const map = {
|
|
32
|
-
perfect: 'perfect', normal: 'normal', slow: 'slow',
|
|
33
|
-
spiky: 'spiky', veryslow: 'veryslow', overloaded: 'overloaded',
|
|
34
|
-
unstable: 'unstable', notactive: 'notactive', pending: 'pending',
|
|
35
|
-
}
|
|
36
|
-
return map[verdict.toLowerCase().replace(/\s+/g, '')] || 'pending'
|
|
37
|
-
}
|