@kb-labs/studio-app 0.6.0

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 (168) hide show
  1. package/.env.example +10 -0
  2. package/README.md +74 -0
  3. package/UI-KIT-COMPONENTS.md +783 -0
  4. package/UI-KIT-PLAN.md +326 -0
  5. package/eslint-rules/no-direct-antd-import.js +151 -0
  6. package/eslint.config.ts +27 -0
  7. package/index.html +17 -0
  8. package/package.json +72 -0
  9. package/postcss.config.js +7 -0
  10. package/public/favicon.ico +0 -0
  11. package/public/favicon.svg +7 -0
  12. package/rspack.config.mjs +118 -0
  13. package/src/App.tsx +46 -0
  14. package/src/components/devtools/devtools-log-feed.module.css +165 -0
  15. package/src/components/devtools/devtools-log-feed.tsx +300 -0
  16. package/src/components/devtools/devtools-panel.module.css +121 -0
  17. package/src/components/devtools/devtools-panel.tsx +115 -0
  18. package/src/components/empty-state.tsx +28 -0
  19. package/src/components/error-boundary.tsx +210 -0
  20. package/src/components/health-banner.module.css +103 -0
  21. package/src/components/health-banner.tsx +134 -0
  22. package/src/components/health-indicator.tsx +12 -0
  23. package/src/components/page-header.tsx +49 -0
  24. package/src/components/page-transition.tsx +24 -0
  25. package/src/components/role-switcher.tsx +148 -0
  26. package/src/components/run-badge.tsx +13 -0
  27. package/src/components/ui/icons.tsx +509 -0
  28. package/src/components/ui/index.ts +25 -0
  29. package/src/components/ui/kb-config-provider.tsx +130 -0
  30. package/src/components/ui/kb-header.module.css +51 -0
  31. package/src/components/ui/kb-header.tsx +112 -0
  32. package/src/components/ui/kb-notification-bell.module.css +103 -0
  33. package/src/components/ui/kb-notification-bell.tsx +199 -0
  34. package/src/components/ui/kb-page-layout.tsx +66 -0
  35. package/src/components/ui/kb-quick-search.module.css +86 -0
  36. package/src/components/ui/kb-quick-search.tsx +196 -0
  37. package/src/components/ui/kb-sidebar.module.css +47 -0
  38. package/src/components/ui/kb-sidebar.tsx +196 -0
  39. package/src/components/ui/kb-stat-card.module.css +21 -0
  40. package/src/components/ui/kb-stat-card.tsx +22 -0
  41. package/src/components/ui/kb-stat-grid.tsx +22 -0
  42. package/src/components/ui/kb-status-bar.module.css +49 -0
  43. package/src/components/ui/kb-status-bar.tsx +145 -0
  44. package/src/components/ui/kb-system-health-indicator.tsx +229 -0
  45. package/src/components/ui/kb-theme-toggle.tsx +42 -0
  46. package/src/components/ui/theme-adapter.ts +812 -0
  47. package/src/components/ui/theme-transition-overlay.tsx +70 -0
  48. package/src/components/ui/utils.ts +6 -0
  49. package/src/components/widget-error-boundary.tsx +172 -0
  50. package/src/config/env.ts +5 -0
  51. package/src/config/feature-flags.ts +482 -0
  52. package/src/config/studio.config.schema.ts +88 -0
  53. package/src/config/studio.config.ts +84 -0
  54. package/src/env.d.ts +23 -0
  55. package/src/hooks/use-command-palette.ts +6 -0
  56. package/src/hooks/use-devtools-store.ts +25 -0
  57. package/src/hooks/use-feature-flags.ts +101 -0
  58. package/src/index.css +10 -0
  59. package/src/layouts/dashboard-grid.tsx +122 -0
  60. package/src/layouts/index.ts +12 -0
  61. package/src/layouts/two-pane.tsx +134 -0
  62. package/src/main.tsx +15 -0
  63. package/src/modules/analytics/components/analytics-daily-stats-chart.tsx +257 -0
  64. package/src/modules/analytics/components/analytics-date-range-picker.tsx +121 -0
  65. package/src/modules/analytics/pages/analytics-cache-page.tsx +285 -0
  66. package/src/modules/analytics/pages/analytics-embeddings-page.tsx +274 -0
  67. package/src/modules/analytics/pages/analytics-events-page.tsx +425 -0
  68. package/src/modules/analytics/pages/analytics-llm-page.tsx +434 -0
  69. package/src/modules/analytics/pages/analytics-overview-page.tsx +336 -0
  70. package/src/modules/analytics/pages/analytics-page.tsx +20 -0
  71. package/src/modules/analytics/pages/analytics-storage-page.tsx +344 -0
  72. package/src/modules/analytics/pages/analytics-vectorstore-page.tsx +279 -0
  73. package/src/modules/analytics/routes/index.tsx +133 -0
  74. package/src/modules/analytics/utils/formatters.test.ts +148 -0
  75. package/src/modules/analytics/utils/formatters.ts +100 -0
  76. package/src/modules/auth/pages/login-page.module.css +113 -0
  77. package/src/modules/auth/pages/login-page.tsx +80 -0
  78. package/src/modules/auth/routes/index.tsx +24 -0
  79. package/src/modules/dashboard/components/ActivityTimelineWidget.tsx +199 -0
  80. package/src/modules/dashboard/components/AiInsightsWidget.tsx +358 -0
  81. package/src/modules/dashboard/components/AnimatedMetricCard.css +88 -0
  82. package/src/modules/dashboard/components/AnimatedMetricCard.tsx +125 -0
  83. package/src/modules/dashboard/components/HeroMetricCard.css +114 -0
  84. package/src/modules/dashboard/components/HeroMetricCard.tsx +152 -0
  85. package/src/modules/dashboard/components/HeroMetricsWidget.tsx +146 -0
  86. package/src/modules/dashboard/components/SystemResourcesWidget.tsx +197 -0
  87. package/src/modules/dashboard/components/dashboard-empty.tsx +22 -0
  88. package/src/modules/dashboard/components/dashboard-kpis-widget.tsx +59 -0
  89. package/src/modules/dashboard/components/dashboard-kpis.tsx +13 -0
  90. package/src/modules/dashboard/components/dashboard-latency-percentiles-widget.tsx +85 -0
  91. package/src/modules/dashboard/components/dashboard-metrics-widget.tsx +78 -0
  92. package/src/modules/dashboard/components/dashboard-plugins-widget.tsx +67 -0
  93. package/src/modules/dashboard/components/dashboard-requests-widget.tsx +62 -0
  94. package/src/modules/dashboard/layouts/dashboard-layout.tsx +16 -0
  95. package/src/modules/dashboard/pages/ai-insights-page.tsx +563 -0
  96. package/src/modules/dashboard/pages/dashboard-animations.css +317 -0
  97. package/src/modules/dashboard/pages/dashboard-page.tsx +13 -0
  98. package/src/modules/dashboard/routes/index.tsx +70 -0
  99. package/src/modules/observability/pages/devkit-page.tsx +263 -0
  100. package/src/modules/observability/pages/incident-detail-page.tsx +571 -0
  101. package/src/modules/observability/pages/incidents-page.tsx +370 -0
  102. package/src/modules/observability/pages/log-detail-page.tsx +508 -0
  103. package/src/modules/observability/pages/logs-page.tsx +1129 -0
  104. package/src/modules/observability/pages/prometheus-metrics-page.tsx +486 -0
  105. package/src/modules/observability/pages/state-broker-page.tsx +229 -0
  106. package/src/modules/observability/pages/system-events-page.tsx +255 -0
  107. package/src/modules/observability/routes/index.tsx +133 -0
  108. package/src/modules/plugins/components/marketplace-card.tsx +153 -0
  109. package/src/modules/plugins/components/plugin-ai-assistant-modal.tsx +374 -0
  110. package/src/modules/plugins/components/plugin-card.tsx +136 -0
  111. package/src/modules/plugins/components/plugin-detail/capabilities-summary.tsx +101 -0
  112. package/src/modules/plugins/components/plugin-detail/cli-commands-table.tsx +101 -0
  113. package/src/modules/plugins/components/plugin-detail/index.ts +7 -0
  114. package/src/modules/plugins/components/plugin-detail/jobs-table.tsx +64 -0
  115. package/src/modules/plugins/components/plugin-detail/permissions/env-permissions.tsx +30 -0
  116. package/src/modules/plugins/components/plugin-detail/permissions/filesystem-permissions.tsx +68 -0
  117. package/src/modules/plugins/components/plugin-detail/permissions/index.ts +6 -0
  118. package/src/modules/plugins/components/plugin-detail/permissions/network-permissions.tsx +43 -0
  119. package/src/modules/plugins/components/plugin-detail/permissions/platform-services.tsx +242 -0
  120. package/src/modules/plugins/components/plugin-detail/permissions/resource-quotas.tsx +96 -0
  121. package/src/modules/plugins/components/plugin-detail/permissions/shell-permissions.tsx +55 -0
  122. package/src/modules/plugins/components/plugin-detail/permissions-view.tsx +47 -0
  123. package/src/modules/plugins/components/plugin-detail/plugin-header.tsx +169 -0
  124. package/src/modules/plugins/components/plugin-detail/rest-routes-table.tsx +159 -0
  125. package/src/modules/plugins/components/plugin-detail/workflows-table.tsx +40 -0
  126. package/src/modules/plugins/components/plugin-grid.tsx +20 -0
  127. package/src/modules/plugins/components/plugin-table.tsx +178 -0
  128. package/src/modules/plugins/pages/marketplace-page.tsx +200 -0
  129. package/src/modules/plugins/pages/plugin-detail-page.tsx +1145 -0
  130. package/src/modules/plugins/routes/index.tsx +48 -0
  131. package/src/modules/plugins/utils/index.ts +1 -0
  132. package/src/modules/plugins/utils/schema-helpers.ts +21 -0
  133. package/src/modules/settings/components/api-routes-viewer.tsx +415 -0
  134. package/src/modules/settings/components/appearance-settings.tsx +200 -0
  135. package/src/modules/settings/components/configuration-settings.tsx +321 -0
  136. package/src/modules/settings/components/data-privacy-settings.tsx +111 -0
  137. package/src/modules/settings/components/experimental-settings.tsx +243 -0
  138. package/src/modules/settings/components/navigation-settings.tsx +198 -0
  139. package/src/modules/settings/components/notifications-settings.tsx +40 -0
  140. package/src/modules/settings/pages/settings-page.tsx +298 -0
  141. package/src/modules/settings/routes/index.tsx +47 -0
  142. package/src/pages/README-assistant.md +91 -0
  143. package/src/pages/login-page.tsx +177 -0
  144. package/src/pages/not-found-page.tsx +85 -0
  145. package/src/providers/auth-provider.tsx +40 -0
  146. package/src/providers/command-palette-provider.tsx +193 -0
  147. package/src/providers/data-sources-provider.tsx +27 -0
  148. package/src/providers/devtools-provider.tsx +72 -0
  149. package/src/providers/navigation-items-provider.tsx +38 -0
  150. package/src/providers/registry-v2-provider.tsx +93 -0
  151. package/src/providers/settings-provider.tsx +158 -0
  152. package/src/router.tsx +302 -0
  153. package/src/routes/helpers.tsx +25 -0
  154. package/src/routes/plugin-page-v2.tsx +52 -0
  155. package/src/routes/types.ts +18 -0
  156. package/src/styles/antd-overrides.css +211 -0
  157. package/src/styles/base.css +190 -0
  158. package/src/styles/settings.css +75 -0
  159. package/src/styles/variables.css +94 -0
  160. package/src/utils/analytics.ts +183 -0
  161. package/src/utils/apply-navigation-categories.ts +83 -0
  162. package/src/utils/commands.ts +252 -0
  163. package/src/utils/export.ts +174 -0
  164. package/src/utils/format-date.ts +166 -0
  165. package/src/utils/logger.ts +110 -0
  166. package/tailwind.config.ts +13 -0
  167. package/tsconfig.json +13 -0
  168. package/vitest-setup.ts +1 -0
@@ -0,0 +1,434 @@
1
+ import { useState, useMemo } from 'react';
2
+ import { UIRow, UICol, UIStatistic, UIAlert, UITable, UISpace, UISelect, useUITheme } from '@kb-labs/studio-ui-kit';
3
+ import {
4
+ RobotOutlined,
5
+ DollarOutlined,
6
+ ThunderboltOutlined,
7
+ ClockCircleOutlined,
8
+ WarningOutlined,
9
+ SaveOutlined,
10
+ } from '@ant-design/icons';
11
+ import { UIText, UIStatisticsChart, UICard, UITag, UIPage, UIPageHeader } from '@kb-labs/studio-ui-kit';
12
+ import { useAdaptersLLMUsage, useAdaptersLLMDailyStats } from '@kb-labs/studio-data-client';
13
+ import { useDataSources } from '../../../providers/data-sources-provider';
14
+ import { AnalyticsDateRangePicker, type DateRangeValue } from '../components/analytics-date-range-picker';
15
+ import { formatCost, formatNumber, formatDuration } from '../utils/formatters';
16
+ import dayjs from 'dayjs';
17
+
18
+
19
+ /**
20
+ * Analytics LLM Usage Page
21
+ *
22
+ * Displays LLM usage statistics:
23
+ * - Total requests, tokens, and costs
24
+ * - Breakdown by model
25
+ * - Performance metrics (avg duration)
26
+ * - Error rates
27
+ */
28
+ export function AnalyticsLLMPage() {
29
+ const { token } = useUITheme();
30
+ const sources = useDataSources();
31
+
32
+ // Date range state (default: last 30 days)
33
+ const [dateRange, setDateRange] = useState<DateRangeValue>([
34
+ dayjs().subtract(30, 'days'),
35
+ dayjs(),
36
+ ]);
37
+
38
+ // Selected models state (empty = all models)
39
+ const [selectedModels, setSelectedModels] = useState<string[]>([]);
40
+
41
+ const {
42
+ data: usageData,
43
+ isLoading,
44
+ error,
45
+ isError,
46
+ } = useAdaptersLLMUsage(sources.adapters, {
47
+ from: dateRange[0].toISOString(),
48
+ to: dateRange[1].toISOString(),
49
+ });
50
+
51
+ // Fetch daily stats for time-series chart
52
+ // Memoize query options to prevent unnecessary re-fetches
53
+ const dailyStatsOptions = useMemo(() => ({
54
+ from: dateRange[0].toISOString(),
55
+ to: dateRange[1].toISOString(),
56
+ models: selectedModels.length > 0 ? selectedModels : undefined,
57
+ }), [dateRange, selectedModels]);
58
+
59
+ const {
60
+ data: dailyStats,
61
+ isLoading: dailyStatsLoading,
62
+ } = useAdaptersLLMDailyStats(sources.adapters, dailyStatsOptions);
63
+
64
+ // Check if analytics is not implemented
65
+ const analyticsNotImplemented =
66
+ isError &&
67
+ error instanceof Error &&
68
+ (error.message.includes('not support') || error.message.includes('not implement'));
69
+
70
+ const llmChart = useMemo(() => (
71
+ <UIStatisticsChart
72
+ data={dailyStats}
73
+ loading={dailyStatsLoading}
74
+ title="" // Empty title since we have it in the card
75
+ metrics={[
76
+ { key: 'count', label: 'Requests' },
77
+ {
78
+ key: 'totalTokens',
79
+ label: 'Tokens (K)',
80
+ transform: (v) => Math.floor(v / 1000),
81
+ },
82
+ {
83
+ key: 'totalCost',
84
+ label: 'Cost ($)',
85
+ format: formatCost,
86
+ },
87
+ ]}
88
+ style={{ marginTop: 0 }}
89
+ />
90
+ ), [dailyStats, dailyStatsLoading]);
91
+
92
+ if (analyticsNotImplemented) {
93
+ return (
94
+ <UIPage>
95
+ <UIPageHeader
96
+ title="LLM Usage"
97
+ description="Track LLM API usage, costs, and performance"
98
+ />
99
+ <UIAlert
100
+ message="Analytics Not Available"
101
+ description="Analytics adapter does not support reading events. Configure an analytics adapter in kb.config.json that implements getEvents()."
102
+ variant="info"
103
+ showIcon
104
+ style={{ marginTop: 16 }}
105
+ />
106
+ </UIPage>
107
+ );
108
+ }
109
+
110
+ if (isError) {
111
+ return (
112
+ <UIPage>
113
+ <UIPageHeader title="LLM Usage" description="LLM usage analytics" />
114
+ <UIAlert
115
+ message="Failed to Load LLM Usage"
116
+ description={error?.message || 'Unknown error occurred'}
117
+ variant="error"
118
+ showIcon
119
+ style={{ marginTop: 16 }}
120
+ />
121
+ </UIPage>
122
+ );
123
+ }
124
+
125
+ const stats = usageData;
126
+
127
+ // Prepare table data
128
+ const tableData = stats
129
+ ? Object.entries(stats.byModel).map(([model, modelStats]) => ({
130
+ key: model,
131
+ model,
132
+ ...modelStats,
133
+ }))
134
+ : [];
135
+
136
+ const columns = [
137
+ {
138
+ title: 'Model',
139
+ dataIndex: 'model',
140
+ key: 'model',
141
+ width: 200,
142
+ fixed: 'left' as const,
143
+ render: (model: string) => (
144
+ <UISpace>
145
+ <RobotOutlined />
146
+ <UIText weight="semibold">{model}</UIText>
147
+ </UISpace>
148
+ ),
149
+ },
150
+ {
151
+ title: 'Requests',
152
+ dataIndex: 'requests',
153
+ key: 'requests',
154
+ width: 100,
155
+ render: (val: number) => formatNumber(val),
156
+ sorter: (a: any, b: any) => a.requests - b.requests,
157
+ },
158
+ {
159
+ title: 'Cost',
160
+ dataIndex: 'cost',
161
+ key: 'cost',
162
+ width: 100,
163
+ render: (val: number) => (
164
+ <UIText weight="semibold" style={{ color: 'var(--error)' }}>
165
+ {formatCost(val)}
166
+ </UIText>
167
+ ),
168
+ sorter: (a: any, b: any) => a.cost - b.cost,
169
+ },
170
+ {
171
+ title: 'Tokens/Request',
172
+ dataIndex: 'tokensPerRequest',
173
+ key: 'tokensPerRequest',
174
+ width: 130,
175
+ render: (val: number) => (
176
+ <UIText weight="semibold">{formatNumber(Math.round(val))}</UIText>
177
+ ),
178
+ sorter: (a: any, b: any) => a.tokensPerRequest - b.tokensPerRequest,
179
+ },
180
+ {
181
+ title: 'Cost/1K Tokens',
182
+ dataIndex: 'costPer1KTokens',
183
+ key: 'costPer1KTokens',
184
+ width: 130,
185
+ render: (val: number) => (
186
+ <UIText style={{ color: 'var(--info)' }}>
187
+ {formatCost(val)}
188
+ </UIText>
189
+ ),
190
+ sorter: (a: any, b: any) => a.costPer1KTokens - b.costPer1KTokens,
191
+ },
192
+ {
193
+ title: 'Error Rate',
194
+ dataIndex: 'errorRate',
195
+ key: 'errorRate',
196
+ width: 100,
197
+ render: (val: number) => {
198
+ const color = val === 0 ? token.colorSuccess : val < 1 ? token.colorWarning : token.colorError;
199
+ return (
200
+ <UIText weight="semibold" style={{ color }}>
201
+ {val.toFixed(2)}%
202
+ </UIText>
203
+ );
204
+ },
205
+ sorter: (a: any, b: any) => a.errorRate - b.errorRate,
206
+ },
207
+ {
208
+ title: 'Total Tokens',
209
+ dataIndex: 'totalTokens',
210
+ key: 'totalTokens',
211
+ width: 120,
212
+ render: (val: number) => formatNumber(val),
213
+ sorter: (a: any, b: any) => a.totalTokens - b.totalTokens,
214
+ },
215
+ {
216
+ title: 'Avg Duration',
217
+ dataIndex: 'avgDurationMs',
218
+ key: 'avgDurationMs',
219
+ width: 120,
220
+ render: (val: number) => (
221
+ <UISpace>
222
+ <ClockCircleOutlined />
223
+ {formatDuration(val)}
224
+ </UISpace>
225
+ ),
226
+ sorter: (a: any, b: any) => a.avgDurationMs - b.avgDurationMs,
227
+ },
228
+ {
229
+ title: 'Prompt Tokens',
230
+ dataIndex: 'promptTokens',
231
+ key: 'promptTokens',
232
+ width: 130,
233
+ render: (val: number) => <UIText color="secondary">{formatNumber(val)}</UIText>,
234
+ },
235
+ {
236
+ title: 'Completion Tokens',
237
+ dataIndex: 'completionTokens',
238
+ key: 'completionTokens',
239
+ width: 150,
240
+ render: (val: number) => <UIText color="secondary">{formatNumber(val)}</UIText>,
241
+ },
242
+ {
243
+ title: 'Cache Tokens',
244
+ dataIndex: 'cacheReadTokens',
245
+ key: 'cacheReadTokens',
246
+ width: 130,
247
+ render: (val: number | undefined) => {
248
+ const n = val ?? 0;
249
+ return (
250
+ <UIText style={{ color: n > 0 ? 'var(--info)' : undefined }} color={n > 0 ? undefined : 'secondary'}>
251
+ {formatNumber(n)}
252
+ </UIText>
253
+ );
254
+ },
255
+ sorter: (a: any, b: any) => (a.cacheReadTokens ?? 0) - (b.cacheReadTokens ?? 0),
256
+ },
257
+ {
258
+ title: 'Billable Tokens',
259
+ dataIndex: 'billableTokens',
260
+ key: 'billableTokens',
261
+ width: 140,
262
+ render: (val: number | undefined) => <UIText color="secondary">{formatNumber(val ?? 0)}</UIText>,
263
+ sorter: (a: any, b: any) => (a.billableTokens ?? 0) - (b.billableTokens ?? 0),
264
+ },
265
+ {
266
+ title: 'Cache Savings',
267
+ dataIndex: 'cacheSavingsUsd',
268
+ key: 'cacheSavingsUsd',
269
+ width: 130,
270
+ render: (val: number | undefined) => {
271
+ const n = val ?? 0;
272
+ return (
273
+ <UIText weight="semibold" style={{ color: n > 0 ? 'var(--success)' : undefined }} color={n > 0 ? undefined : 'secondary'}>
274
+ {n > 0 ? `+${formatCost(n)}` : formatCost(n)}
275
+ </UIText>
276
+ );
277
+ },
278
+ sorter: (a: any, b: any) => (a.cacheSavingsUsd ?? 0) - (b.cacheSavingsUsd ?? 0),
279
+ },
280
+ ];
281
+
282
+ return (
283
+ <UIPage width="full">
284
+ <UIPageHeader
285
+ title="LLM Usage Analytics"
286
+ description="Track LLM API usage, costs, and performance across all models"
287
+ actions={
288
+ <AnalyticsDateRangePicker
289
+ value={dateRange}
290
+ onChange={setDateRange}
291
+ />
292
+ }
293
+ />
294
+
295
+ <div style={{ marginTop: 24 }}>
296
+ {/* Overview Stats */}
297
+ <UIRow gutter={[16, 16]}>
298
+ <UICol xs={24} sm={12} lg={6}>
299
+ <UICard>
300
+ <UIStatistic
301
+ title="Total Requests"
302
+ value={stats?.totalRequests ?? 0}
303
+ prefix={<ThunderboltOutlined />}
304
+ loading={isLoading}
305
+ valueStyle={{ color: 'var(--info)' }}
306
+ />
307
+ </UICard>
308
+ </UICol>
309
+ <UICol xs={24} sm={12} lg={6}>
310
+ <UICard>
311
+ <UIStatistic
312
+ title="Total Tokens"
313
+ value={stats?.totalTokens ?? 0}
314
+ prefix={<RobotOutlined />}
315
+ loading={isLoading}
316
+ valueStyle={{ color: 'var(--success)' }}
317
+ />
318
+ </UICard>
319
+ </UICol>
320
+ <UICol xs={24} sm={12} lg={6}>
321
+ <UICard>
322
+ <UIStatistic
323
+ title="Total Cost"
324
+ value={stats?.totalCost ?? 0}
325
+ prefix={<DollarOutlined />}
326
+ precision={4}
327
+ loading={isLoading}
328
+ valueStyle={{ color: 'var(--error)' }}
329
+ />
330
+ </UICard>
331
+ </UICol>
332
+ <UICol xs={24} sm={12} lg={6}>
333
+ <UICard>
334
+ <UIStatistic
335
+ title="Errors"
336
+ value={stats?.errors ?? 0}
337
+ prefix={<WarningOutlined />}
338
+ loading={isLoading}
339
+ valueStyle={{ color: stats && stats.errors > 0 ? 'var(--warning)' : 'var(--success)' }}
340
+ />
341
+ </UICard>
342
+ </UICol>
343
+ </UIRow>
344
+
345
+ {/* Cache Savings Row */}
346
+ <UIRow gutter={[16, 16]} style={{ marginTop: 16 }}>
347
+ <UICol xs={24} sm={12} lg={8}>
348
+ <UICard>
349
+ <UIStatistic
350
+ title="Cache Read Tokens"
351
+ value={stats?.totalCacheReadTokens ?? 0}
352
+ prefix={<SaveOutlined />}
353
+ loading={isLoading}
354
+ valueStyle={{ color: 'var(--info)' }}
355
+ />
356
+ </UICard>
357
+ </UICol>
358
+ <UICol xs={24} sm={12} lg={8}>
359
+ <UICard>
360
+ <UIStatistic
361
+ title="Billable Tokens"
362
+ value={stats?.totalBillableTokens ?? 0}
363
+ prefix={<RobotOutlined />}
364
+ loading={isLoading}
365
+ valueStyle={{ color: 'var(--warning)' }}
366
+ />
367
+ </UICard>
368
+ </UICol>
369
+ <UICol xs={24} sm={12} lg={8}>
370
+ <UICard>
371
+ <UIStatistic
372
+ title="Cache Savings"
373
+ value={stats?.totalCacheSavingsUsd ?? 0}
374
+ prefix={<DollarOutlined />}
375
+ precision={4}
376
+ loading={isLoading}
377
+ valueStyle={{ color: 'var(--success)' }}
378
+ />
379
+ </UICard>
380
+ </UICol>
381
+ </UIRow>
382
+
383
+ {/* Model Breakdown Table */}
384
+ <UICard
385
+ title="Usage by Model"
386
+ style={{ marginTop: 16 }}
387
+ extra={
388
+ stats && (
389
+ <UITag color="blue">
390
+ {Object.keys(stats.byModel).length} model(s)
391
+ </UITag>
392
+ )
393
+ }
394
+ >
395
+ <UITable
396
+ columns={columns}
397
+ dataSource={tableData}
398
+ loading={isLoading}
399
+ pagination={false}
400
+ size="middle"
401
+ scroll={{ x: 'max-content' }}
402
+ />
403
+ </UICard>
404
+
405
+ {/* Daily Usage Chart */}
406
+ <UICard title="Daily Usage Trend" style={{ marginTop: 16 }} key="daily-usage-chart">
407
+ {/* Model Filter */}
408
+ <div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 12 }}>
409
+ <UIText weight="medium">Filter by models:</UIText>
410
+ <UISelect
411
+ mode="multiple"
412
+ placeholder="All models"
413
+ style={{ minWidth: 300, maxWidth: 600, flex: 1 }}
414
+ value={selectedModels}
415
+ onChange={(v) => setSelectedModels(v as string[])}
416
+ allowClear
417
+ showSearch
418
+ maxTagCount="responsive"
419
+ options={stats ? Object.keys(stats.byModel).map((model) => ({
420
+ label: `${model} (${formatNumber(stats.byModel[model]!.requests)} requests)`,
421
+ value: model,
422
+ })) : []}
423
+ />
424
+ </div>
425
+
426
+ {/* Chart - fixed height container to prevent scroll jumps */}
427
+ <div style={{ minHeight: 420 }}>
428
+ {llmChart}
429
+ </div>
430
+ </UICard>
431
+ </div>
432
+ </UIPage>
433
+ );
434
+ }