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,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/components/analytics/AnalyticsView.jsx
|
|
3
|
-
* @description Analytics dashboard page showing provider health, fastest models leaderboard, and tier distribution.
|
|
4
|
-
* 📖 Purely derived from the `models` SSE data. No API calls needed beyond the live model feed.
|
|
5
|
-
* @functions AnalyticsView → renders the three analytics cards
|
|
6
|
-
*/
|
|
7
|
-
import { useMemo } from 'react'
|
|
8
|
-
import { IconActivity, IconTrophy } from '@tabler/icons-react'
|
|
9
|
-
import TierBadge from '../atoms/TierBadge.jsx'
|
|
10
|
-
import TokenUsagePanel from './TokenUsagePanel.jsx'
|
|
11
|
-
import styles from './AnalyticsView.module.css'
|
|
12
|
-
|
|
13
|
-
// 📖 TIER_COLORS is now derived from CSS custom properties so the chart fills
|
|
14
|
-
// 📖 swap to AA-contrast shades in light mode automatically. The keys stay
|
|
15
|
-
// 📖 the same; the values come from --tier-splus / --tier-s / etc.
|
|
16
|
-
function readTierColor(name, fallback) {
|
|
17
|
-
if (typeof window === 'undefined' || !window.document?.documentElement) return fallback
|
|
18
|
-
const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
|
|
19
|
-
return v || fallback
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const TIER_COLOR_KEYS = {
|
|
23
|
-
'S+': '--tier-splus',
|
|
24
|
-
'S': '--tier-s',
|
|
25
|
-
'A+': '--tier-aplus',
|
|
26
|
-
'A': '--tier-a',
|
|
27
|
-
'A-': '--tier-aminus',
|
|
28
|
-
'B+': '--tier-bplus',
|
|
29
|
-
'B': '--tier-b',
|
|
30
|
-
'C': '--tier-c',
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const TIER_COLOR_FALLBACKS = {
|
|
34
|
-
'S+': '#ffd700', S: '#ff8c00', 'A+': '#00c8ff', A: '#3ddc84',
|
|
35
|
-
'A-': '#7ecf7e', 'B+': '#a8a8c8', B: '#808098', C: '#606078',
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// 📖 Resolve the tier color at render time. Memoizing the map per theme switch
|
|
39
|
-
// 📖 would be more efficient, but the cost is one getComputedStyle per tier and
|
|
40
|
-
// 📖 the chart only re-renders when `models` changes, so the extra work is
|
|
41
|
-
// 📖 imperceptible.
|
|
42
|
-
function tierColor(key) {
|
|
43
|
-
return readTierColor(TIER_COLOR_KEYS[key], TIER_COLOR_FALLBACKS[key])
|
|
44
|
-
}
|
|
45
|
-
const TIERS = ['S+', 'S', 'A+', 'A', 'A-', 'B+', 'B', 'C']
|
|
46
|
-
|
|
47
|
-
export default function AnalyticsView({ models }) {
|
|
48
|
-
const providerHealth = useMemo(() => {
|
|
49
|
-
const map = {}
|
|
50
|
-
models.forEach((m) => {
|
|
51
|
-
if (!map[m.origin]) map[m.origin] = { total: 0, online: 0, key: m.providerKey }
|
|
52
|
-
map[m.origin].total++
|
|
53
|
-
if (m.status === 'up') map[m.origin].online++
|
|
54
|
-
})
|
|
55
|
-
return Object.entries(map).sort((a, b) => (b[1].online / b[1].total) - (a[1].online / a[1].total))
|
|
56
|
-
}, [models])
|
|
57
|
-
|
|
58
|
-
const leaderboard = useMemo(() => {
|
|
59
|
-
const online = models.filter((m) => m.status === 'up' && m.avg !== Infinity && m.avg < 99000)
|
|
60
|
-
return [...online].sort((a, b) => a.avg - b.avg).slice(0, 10)
|
|
61
|
-
}, [models])
|
|
62
|
-
|
|
63
|
-
const tierCounts = useMemo(() => {
|
|
64
|
-
const counts = {}
|
|
65
|
-
models.forEach((m) => { counts[m.tier] = (counts[m.tier] || 0) + 1 })
|
|
66
|
-
const maxCount = Math.max(...Object.values(counts), 1)
|
|
67
|
-
return TIERS.map((t) => ({ tier: t, count: counts[t] || 0, pct: ((counts[t] || 0) / maxCount) * 100 }))
|
|
68
|
-
}, [models])
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<div className={styles.page}>
|
|
72
|
-
<div className={styles.pageHeader}>
|
|
73
|
-
<h1 className={styles.pageTitle}>
|
|
74
|
-
<IconActivity size={24} stroke={1.5} style={{ marginRight: 8, verticalAlign: 'middle' }} />
|
|
75
|
-
Analytics
|
|
76
|
-
</h1>
|
|
77
|
-
<p className={styles.pageSubtitle}>Real-time insights across all providers and models</p>
|
|
78
|
-
</div>
|
|
79
|
-
|
|
80
|
-
<div className={styles.grid}>
|
|
81
|
-
<div className={`${styles.card} ${styles.cardWide}`}>
|
|
82
|
-
<h3 className={styles.cardTitle}>Provider Health Overview</h3>
|
|
83
|
-
<div className={styles.cardBody}>
|
|
84
|
-
{providerHealth.length === 0 ? (
|
|
85
|
-
<div className={styles.empty}>Waiting for data...</div>
|
|
86
|
-
) : (
|
|
87
|
-
providerHealth.map(([name, data]) => {
|
|
88
|
-
const pct = data.total > 0 ? Math.round((data.online / data.total) * 100) : 0
|
|
89
|
-
const pctCls = pct > 70 ? styles.pctFast : pct > 30 ? styles.pctMedium : styles.pctSlow
|
|
90
|
-
return (
|
|
91
|
-
<div key={name} className={styles.healthItem}>
|
|
92
|
-
<span className={styles.healthName}>{name}</span>
|
|
93
|
-
<div className={styles.healthBar}>
|
|
94
|
-
<div className={styles.healthFill} style={{ width: `${pct}%` }} />
|
|
95
|
-
</div>
|
|
96
|
-
<span className={`${styles.healthPct} ${pctCls}`}>{pct}%</span>
|
|
97
|
-
</div>
|
|
98
|
-
)
|
|
99
|
-
})
|
|
100
|
-
)}
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
103
|
-
|
|
104
|
-
<div className={styles.card}>
|
|
105
|
-
<h3 className={styles.cardTitle} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
106
|
-
<IconTrophy size={16} stroke={1.5} />
|
|
107
|
-
Fastest Models
|
|
108
|
-
</h3>
|
|
109
|
-
<div className={styles.cardBody}>
|
|
110
|
-
{leaderboard.length === 0 ? (
|
|
111
|
-
<div className={styles.empty}>Waiting for ping data...</div>
|
|
112
|
-
) : (
|
|
113
|
-
leaderboard.map((m, i) => {
|
|
114
|
-
const rankCls = i < 3 ? styles[`rank${i + 1}`] : ''
|
|
115
|
-
return (
|
|
116
|
-
<div key={m.modelId} className={styles.leaderItem}>
|
|
117
|
-
<div className={`${styles.leaderRank} ${rankCls}`}>{i + 1}</div>
|
|
118
|
-
<span className={styles.leaderName}>{m.label}</span>
|
|
119
|
-
<span className={styles.leaderLatency}>{m.avg}ms</span>
|
|
120
|
-
</div>
|
|
121
|
-
)
|
|
122
|
-
})
|
|
123
|
-
)}
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
|
|
127
|
-
<div className={styles.card}>
|
|
128
|
-
<h3 className={styles.cardTitle}>Tier Distribution</h3>
|
|
129
|
-
<div className={styles.cardBody}>
|
|
130
|
-
{tierCounts.map(({ tier, count, pct }) => (
|
|
131
|
-
<div key={tier} className={styles.tierItem}>
|
|
132
|
-
<div className={styles.tierBadge}><TierBadge tier={tier} /></div>
|
|
133
|
-
<div className={styles.tierBar}>
|
|
134
|
-
<div className={styles.tierFill} style={{ width: `${pct}%`, background: tierColor(tier) }} />
|
|
135
|
-
</div>
|
|
136
|
-
<span className={styles.tierCount}>{count}</span>
|
|
137
|
-
</div>
|
|
138
|
-
))}
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
</div>
|
|
142
|
-
|
|
143
|
-
{/* M4: Token Usage — always shown, displays "no data" if router hasn't been used */}
|
|
144
|
-
<TokenUsagePanel />
|
|
145
|
-
</div>
|
|
146
|
-
)
|
|
147
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
.page {
|
|
2
|
-
padding: 24px;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
.pageHeader {
|
|
6
|
-
margin-bottom: 24px;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
.pageTitle {
|
|
10
|
-
font-size: 24px;
|
|
11
|
-
font-weight: 800;
|
|
12
|
-
margin: 0 0 6px;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.pageSubtitle {
|
|
16
|
-
font-size: 13px;
|
|
17
|
-
color: var(--color-text-muted);
|
|
18
|
-
margin: 0;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.grid {
|
|
22
|
-
display: grid;
|
|
23
|
-
grid-template-columns: repeat(2, 1fr);
|
|
24
|
-
gap: 16px;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.card {
|
|
28
|
-
background: var(--color-bg-card);
|
|
29
|
-
backdrop-filter: blur(12px);
|
|
30
|
-
border: 1px solid var(--color-border);
|
|
31
|
-
border-radius: var(--radius-lg);
|
|
32
|
-
padding: 20px;
|
|
33
|
-
overflow: hidden;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.cardWide {
|
|
37
|
-
grid-column: span 2;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.cardTitle {
|
|
41
|
-
font-size: 14px;
|
|
42
|
-
font-weight: 700;
|
|
43
|
-
margin: 0 0 16px;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.cardBody {
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.empty {
|
|
50
|
-
color: var(--color-text-dim);
|
|
51
|
-
text-align: center;
|
|
52
|
-
padding: 20px 0;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.healthItem {
|
|
56
|
-
display: flex;
|
|
57
|
-
align-items: center;
|
|
58
|
-
gap: 12px;
|
|
59
|
-
padding: 8px 0;
|
|
60
|
-
border-bottom: 1px solid var(--color-border);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.healthItem:last-child {
|
|
64
|
-
border-bottom: none;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.healthName {
|
|
68
|
-
width: 120px;
|
|
69
|
-
font-size: 12px;
|
|
70
|
-
font-weight: 600;
|
|
71
|
-
flex-shrink: 0;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.healthBar {
|
|
75
|
-
flex: 1;
|
|
76
|
-
height: 8px;
|
|
77
|
-
border-radius: 4px;
|
|
78
|
-
background: var(--color-surface);
|
|
79
|
-
overflow: hidden;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.healthFill {
|
|
83
|
-
height: 100%;
|
|
84
|
-
border-radius: 4px;
|
|
85
|
-
background: linear-gradient(90deg, var(--color-accent), var(--color-success));
|
|
86
|
-
transition: width var(--transition-medium);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
.healthPct {
|
|
90
|
-
width: 40px;
|
|
91
|
-
text-align: right;
|
|
92
|
-
font-family: var(--font-mono);
|
|
93
|
-
font-size: 12px;
|
|
94
|
-
font-weight: 600;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
.pctFast { color: #00ff88; }
|
|
98
|
-
.pctMedium { color: #ffaa00; }
|
|
99
|
-
.pctSlow { color: #ff4444; }
|
|
100
|
-
|
|
101
|
-
:global([data-theme="light"]) .pctFast { color: #008f4d; }
|
|
102
|
-
:global([data-theme="light"]) .pctMedium { color: #b86b00; }
|
|
103
|
-
:global([data-theme="light"]) .pctSlow { color: #c8143a; }
|
|
104
|
-
|
|
105
|
-
.leaderItem {
|
|
106
|
-
display: flex;
|
|
107
|
-
align-items: center;
|
|
108
|
-
gap: 10px;
|
|
109
|
-
padding: 8px 0;
|
|
110
|
-
border-bottom: 1px solid var(--color-border);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.leaderItem:last-child {
|
|
114
|
-
border-bottom: none;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.leaderRank {
|
|
118
|
-
width: 28px;
|
|
119
|
-
height: 28px;
|
|
120
|
-
display: flex;
|
|
121
|
-
align-items: center;
|
|
122
|
-
justify-content: center;
|
|
123
|
-
border-radius: 50%;
|
|
124
|
-
font-size: 12px;
|
|
125
|
-
font-weight: 700;
|
|
126
|
-
background: var(--color-surface);
|
|
127
|
-
color: var(--color-text-dim);
|
|
128
|
-
flex-shrink: 0;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.rank1 { background: rgba(255, 215, 0, 0.15); color: #ffd700; }
|
|
132
|
-
.rank2 { background: rgba(192, 192, 192, 0.15); color: #c0c0c0; }
|
|
133
|
-
.rank3 { background: rgba(205, 127, 50, 0.15); color: #cd7f32; }
|
|
134
|
-
|
|
135
|
-
.leaderName {
|
|
136
|
-
flex: 1;
|
|
137
|
-
font-size: 12px;
|
|
138
|
-
font-weight: 600;
|
|
139
|
-
min-width: 0;
|
|
140
|
-
overflow: hidden;
|
|
141
|
-
text-overflow: ellipsis;
|
|
142
|
-
white-space: nowrap;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.leaderLatency {
|
|
146
|
-
font-family: var(--font-mono);
|
|
147
|
-
font-size: 12px;
|
|
148
|
-
font-weight: 600;
|
|
149
|
-
color: var(--color-success);
|
|
150
|
-
flex-shrink: 0;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.tierItem {
|
|
154
|
-
display: flex;
|
|
155
|
-
align-items: center;
|
|
156
|
-
gap: 10px;
|
|
157
|
-
padding: 6px 0;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
.tierBadge {
|
|
161
|
-
width: 40px;
|
|
162
|
-
flex-shrink: 0;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
.tierBar {
|
|
166
|
-
flex: 1;
|
|
167
|
-
height: 10px;
|
|
168
|
-
border-radius: 5px;
|
|
169
|
-
background: var(--color-surface);
|
|
170
|
-
overflow: hidden;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.tierFill {
|
|
174
|
-
height: 100%;
|
|
175
|
-
border-radius: 5px;
|
|
176
|
-
transition: width var(--transition-medium);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
.tierCount {
|
|
180
|
-
width: 30px;
|
|
181
|
-
text-align: right;
|
|
182
|
-
font-family: var(--font-mono);
|
|
183
|
-
font-size: 12px;
|
|
184
|
-
color: var(--color-text-muted);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
@media (max-width: 1024px) {
|
|
188
|
-
.grid { grid-template-columns: 1fr; }
|
|
189
|
-
.cardWide { grid-column: span 1; }
|
|
190
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/components/analytics/TokenUsagePanel.jsx
|
|
3
|
-
* @description Token Usage sub-section inside Analytics — 7-day chart, top models/providers.
|
|
4
|
-
* 📖 M4: Uses useTokenUsage hook, pure CSS bars (no charting library).
|
|
5
|
-
*/
|
|
6
|
-
import { useTokenUsage } from '../../hooks/useTokenUsage.js'
|
|
7
|
-
import styles from './TokenUsagePanel.module.css'
|
|
8
|
-
|
|
9
|
-
function formatTokens(n) {
|
|
10
|
-
if (typeof n !== 'number' || !Number.isFinite(n)) return '0'
|
|
11
|
-
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
|
|
12
|
-
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
|
|
13
|
-
return String(Math.round(n))
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export default function TokenUsagePanel() {
|
|
17
|
-
const { data, loading } = useTokenUsage()
|
|
18
|
-
|
|
19
|
-
if (loading) {
|
|
20
|
-
return (
|
|
21
|
-
<div className={styles.card}>
|
|
22
|
-
<h3 className={styles.cardTitle}>Token Usage</h3>
|
|
23
|
-
<div className={styles.empty}>Loading…</div>
|
|
24
|
-
</div>
|
|
25
|
-
)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (!data || !data.hasData) {
|
|
29
|
-
return (
|
|
30
|
-
<div className={styles.card}>
|
|
31
|
-
<h3 className={styles.cardTitle}>Token Usage</h3>
|
|
32
|
-
<div className={styles.empty}>No token data yet. Start the router and send requests to track usage.</div>
|
|
33
|
-
</div>
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const maxDayTokens = Math.max(...data.sevenDays.map(d => d.totalTokens), 1)
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<>
|
|
41
|
-
{/* Summary cards */}
|
|
42
|
-
<div className={styles.summaryGrid}>
|
|
43
|
-
<div className={styles.summaryCard}>
|
|
44
|
-
<span className={styles.summaryLabel}>Today</span>
|
|
45
|
-
<span className={styles.summaryValue}>{formatTokens(data.today.totalTokens)}</span>
|
|
46
|
-
<span className={styles.summarySub}>{data.today.requests} requests</span>
|
|
47
|
-
</div>
|
|
48
|
-
<div className={styles.summaryCard}>
|
|
49
|
-
<span className={styles.summaryLabel}>All Time</span>
|
|
50
|
-
<span className={styles.summaryValue}>{formatTokens(data.allTime.totalTokens)}</span>
|
|
51
|
-
<span className={styles.summarySub}>{data.allTime.requests} requests</span>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
{/* 7-day chart */}
|
|
56
|
-
<div className={styles.card}>
|
|
57
|
-
<h3 className={styles.cardTitle}>7-Day Usage</h3>
|
|
58
|
-
<div className={styles.chart}>
|
|
59
|
-
{data.sevenDays.map((day) => (
|
|
60
|
-
<div key={day.date} className={styles.chartBar}>
|
|
61
|
-
<div
|
|
62
|
-
className={styles.chartFill}
|
|
63
|
-
style={{ height: `${Math.max(2, (day.totalTokens / maxDayTokens) * 100)}%` }}
|
|
64
|
-
title={`${day.date}: ${formatTokens(day.totalTokens)} tokens`}
|
|
65
|
-
/>
|
|
66
|
-
<span className={styles.chartLabel}>{day.label}</span>
|
|
67
|
-
</div>
|
|
68
|
-
))}
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
{/* Top Models */}
|
|
73
|
-
{data.topModels.length > 0 && (
|
|
74
|
-
<div className={styles.card}>
|
|
75
|
-
<h3 className={styles.cardTitle}>Top Models</h3>
|
|
76
|
-
<div className={styles.topList}>
|
|
77
|
-
{data.topModels.map((m) => (
|
|
78
|
-
<div key={m.key} className={styles.topRow}>
|
|
79
|
-
<span className={styles.topKey}>{m.key}</span>
|
|
80
|
-
<span className={styles.topVal}>{formatTokens(m.total)} tokens</span>
|
|
81
|
-
<span className={styles.topReq}>{m.requests} req</span>
|
|
82
|
-
</div>
|
|
83
|
-
))}
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
)}
|
|
87
|
-
|
|
88
|
-
{/* Top Providers */}
|
|
89
|
-
{data.topProviders.length > 0 && (
|
|
90
|
-
<div className={styles.card}>
|
|
91
|
-
<h3 className={styles.cardTitle}>Top Providers</h3>
|
|
92
|
-
<div className={styles.topList}>
|
|
93
|
-
{data.topProviders.map((p) => (
|
|
94
|
-
<div key={p.key} className={styles.topRow}>
|
|
95
|
-
<span className={styles.topKey}>{p.key}</span>
|
|
96
|
-
<span className={styles.topVal}>{formatTokens(p.total)} tokens</span>
|
|
97
|
-
<span className={styles.topReq}>{p.requests} req</span>
|
|
98
|
-
</div>
|
|
99
|
-
))}
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
102
|
-
)}
|
|
103
|
-
</>
|
|
104
|
-
)
|
|
105
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
.card {
|
|
2
|
-
margin-top: 16px;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
.cardTitle {
|
|
6
|
-
font-size: 13px;
|
|
7
|
-
font-weight: 600;
|
|
8
|
-
color: var(--text-secondary, #aaa);
|
|
9
|
-
margin: 0 0 10px 0;
|
|
10
|
-
text-transform: uppercase;
|
|
11
|
-
letter-spacing: 0.5px;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.empty {
|
|
15
|
-
color: var(--text-muted, #666);
|
|
16
|
-
font-size: 13px;
|
|
17
|
-
padding: 20px 0;
|
|
18
|
-
text-align: center;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/* Summary Grid */
|
|
22
|
-
.summaryGrid {
|
|
23
|
-
display: grid;
|
|
24
|
-
grid-template-columns: 1fr 1fr;
|
|
25
|
-
gap: 12px;
|
|
26
|
-
margin-top: 16px;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.summaryCard {
|
|
30
|
-
background: var(--bg-secondary, #14141f);
|
|
31
|
-
border: 1px solid var(--border, #1e1e2e);
|
|
32
|
-
border-radius: 8px;
|
|
33
|
-
padding: 14px 16px;
|
|
34
|
-
display: flex;
|
|
35
|
-
flex-direction: column;
|
|
36
|
-
gap: 2px;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.summaryLabel {
|
|
40
|
-
font-size: 11px;
|
|
41
|
-
font-weight: 600;
|
|
42
|
-
color: var(--text-muted, #888);
|
|
43
|
-
text-transform: uppercase;
|
|
44
|
-
letter-spacing: 0.5px;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.summaryValue {
|
|
48
|
-
font-size: 22px;
|
|
49
|
-
font-weight: 700;
|
|
50
|
-
color: var(--text-primary, #e0e0e0);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.summarySub {
|
|
54
|
-
font-size: 11px;
|
|
55
|
-
color: var(--text-muted, #888);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/* 7-Day Chart */
|
|
59
|
-
.chart {
|
|
60
|
-
display: flex;
|
|
61
|
-
align-items: flex-end;
|
|
62
|
-
gap: 6px;
|
|
63
|
-
height: 100px;
|
|
64
|
-
padding: 0 4px;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.chartBar {
|
|
68
|
-
flex: 1;
|
|
69
|
-
display: flex;
|
|
70
|
-
flex-direction: column;
|
|
71
|
-
align-items: center;
|
|
72
|
-
height: 100%;
|
|
73
|
-
justify-content: flex-end;
|
|
74
|
-
gap: 4px;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.chartFill {
|
|
78
|
-
width: 100%;
|
|
79
|
-
min-height: 2px;
|
|
80
|
-
background: linear-gradient(180deg, #00c864 0%, rgba(0, 200, 100, 0.3) 100%);
|
|
81
|
-
border-radius: 3px 3px 0 0;
|
|
82
|
-
transition: height 0.3s ease;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.chartLabel {
|
|
86
|
-
font-size: 10px;
|
|
87
|
-
color: var(--text-muted, #888);
|
|
88
|
-
text-transform: uppercase;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/* Top Lists */
|
|
92
|
-
.topList {
|
|
93
|
-
display: flex;
|
|
94
|
-
flex-direction: column;
|
|
95
|
-
gap: 4px;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
.topRow {
|
|
99
|
-
display: flex;
|
|
100
|
-
align-items: center;
|
|
101
|
-
gap: 8px;
|
|
102
|
-
padding: 4px 0;
|
|
103
|
-
font-size: 12px;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.topKey {
|
|
107
|
-
flex: 1;
|
|
108
|
-
color: var(--text-primary, #e0e0e0);
|
|
109
|
-
font-family: 'SF Mono', monospace;
|
|
110
|
-
overflow: hidden;
|
|
111
|
-
text-overflow: ellipsis;
|
|
112
|
-
white-space: nowrap;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.topVal {
|
|
116
|
-
color: var(--text-muted, #888);
|
|
117
|
-
min-width: 80px;
|
|
118
|
-
text-align: right;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
.topReq {
|
|
122
|
-
color: var(--text-muted, #666);
|
|
123
|
-
min-width: 50px;
|
|
124
|
-
text-align: right;
|
|
125
|
-
font-size: 11px;
|
|
126
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file web/src/components/atoms/AILatencyCell.jsx
|
|
3
|
-
* @description Benchmark AI latency column — shows animated spinner during benchmarks.
|
|
4
|
-
* 📖 Shows: 4.3s, 12s, ERR, TIMEOUT. Retry badge ↻N in blue.
|
|
5
|
-
* 📖 When isRunning: animated CSS spinner + pulsing row highlight.
|
|
6
|
-
*/
|
|
7
|
-
import styles from './AILatencyCell.module.css'
|
|
8
|
-
|
|
9
|
-
function formatLatency(result, isRunning) {
|
|
10
|
-
if (isRunning) return { text: 'RUN', badge: '' }
|
|
11
|
-
if (!result || !result.ok) return { text: result?.code || '—', badge: '' }
|
|
12
|
-
const totalSec = result.totalMs / 1000
|
|
13
|
-
const badge = result.retries > 0 ? `↻${result.retries}` : ''
|
|
14
|
-
const text = totalSec >= 10 ? `${totalSec.toFixed(0)}s` : `${totalSec.toFixed(1)}s`
|
|
15
|
-
return { text, badge }
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export default function AILatencyCell({ result, isRunning }) {
|
|
19
|
-
const { text, badge } = formatLatency(result, isRunning)
|
|
20
|
-
const ok = result?.ok
|
|
21
|
-
const colorCls = ok ? styles.fast : (result ? styles.slow : styles.dim)
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<span className={`${styles.cell} ${isRunning ? styles.runningCell : ''}`}>
|
|
25
|
-
{isRunning ? (
|
|
26
|
-
<>
|
|
27
|
-
<span className={styles.benchmarkSpinner} />
|
|
28
|
-
<span className={`${styles.value} ${styles.running}`}>{text}</span>
|
|
29
|
-
</>
|
|
30
|
-
) : (
|
|
31
|
-
<>
|
|
32
|
-
<span className={`${styles.value} ${colorCls}`}>{text}</span>
|
|
33
|
-
{badge && <span className={styles.badge}>{badge}</span>}
|
|
34
|
-
</>
|
|
35
|
-
)}
|
|
36
|
-
</span>
|
|
37
|
-
)
|
|
38
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
.cell {
|
|
2
|
-
font-family: var(--font-mono);
|
|
3
|
-
font-size: 12px;
|
|
4
|
-
font-weight: 600;
|
|
5
|
-
display: inline-flex;
|
|
6
|
-
align-items: center;
|
|
7
|
-
gap: 4px;
|
|
8
|
-
min-width: 70px;
|
|
9
|
-
transition: background 200ms;
|
|
10
|
-
border-radius: 4px;
|
|
11
|
-
padding: 1px 3px;
|
|
12
|
-
}
|
|
13
|
-
.value { }
|
|
14
|
-
.fast { color: #00ff88; }
|
|
15
|
-
.medium { color: #ffaa00; }
|
|
16
|
-
.slow { color: #ff4444; }
|
|
17
|
-
.dim { color: var(--color-text-dim); }
|
|
18
|
-
.running { color: #b400ff; font-weight: 700; }
|
|
19
|
-
.badge { color: #06b6d4; font-size: 10px; }
|
|
20
|
-
|
|
21
|
-
/* ── Running/benchmarking row highlight ── */
|
|
22
|
-
.runningCell {
|
|
23
|
-
background: rgba(180, 0, 255, 0.12);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/* ── Benchmark spinner: animated ring ── */
|
|
27
|
-
@keyframes benchSpin {
|
|
28
|
-
to { transform: rotate(360deg); }
|
|
29
|
-
}
|
|
30
|
-
@keyframes benchPulse {
|
|
31
|
-
0%, 100% { box-shadow: 0 0 0 0 rgba(180, 0, 255, 0.4); }
|
|
32
|
-
50% { box-shadow: 0 0 0 4px rgba(180, 0, 255, 0); }
|
|
33
|
-
}
|
|
34
|
-
.benchmarkSpinner {
|
|
35
|
-
display: inline-block;
|
|
36
|
-
width: 12px;
|
|
37
|
-
height: 12px;
|
|
38
|
-
border: 2px solid rgba(180, 0, 255, 0.3);
|
|
39
|
-
border-top-color: #b400ff;
|
|
40
|
-
border-radius: 50%;
|
|
41
|
-
animation: benchSpin 0.6s linear infinite;
|
|
42
|
-
flex-shrink: 0;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/* 📖 Light theme: swap the neon palette for darker, AA-contrast shades. The
|
|
46
|
-
📖 purple running color stays vibrant since it works on both backgrounds. */
|
|
47
|
-
:global([data-theme="light"]) .fast { color: #008f4d; }
|
|
48
|
-
:global([data-theme="light"]) .medium { color: #b86b00; }
|
|
49
|
-
:global([data-theme="light"]) .slow { color: #c8143a; }
|
|
50
|
-
:global([data-theme="light"]) .badge { color: #0e7490; }
|
|
51
|
-
:global([data-theme="light"]) .runningCell { background: rgba(125, 0, 180, 0.08); }
|