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