free-coding-models 0.5.86 → 0.5.88

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