free-coding-models 0.5.85 → 0.5.87

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