@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.
- package/.env.example +10 -0
- package/README.md +74 -0
- package/UI-KIT-COMPONENTS.md +783 -0
- package/UI-KIT-PLAN.md +326 -0
- package/eslint-rules/no-direct-antd-import.js +151 -0
- package/eslint.config.ts +27 -0
- package/index.html +17 -0
- package/package.json +72 -0
- package/postcss.config.js +7 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +7 -0
- package/rspack.config.mjs +118 -0
- package/src/App.tsx +46 -0
- package/src/components/devtools/devtools-log-feed.module.css +165 -0
- package/src/components/devtools/devtools-log-feed.tsx +300 -0
- package/src/components/devtools/devtools-panel.module.css +121 -0
- package/src/components/devtools/devtools-panel.tsx +115 -0
- package/src/components/empty-state.tsx +28 -0
- package/src/components/error-boundary.tsx +210 -0
- package/src/components/health-banner.module.css +103 -0
- package/src/components/health-banner.tsx +134 -0
- package/src/components/health-indicator.tsx +12 -0
- package/src/components/page-header.tsx +49 -0
- package/src/components/page-transition.tsx +24 -0
- package/src/components/role-switcher.tsx +148 -0
- package/src/components/run-badge.tsx +13 -0
- package/src/components/ui/icons.tsx +509 -0
- package/src/components/ui/index.ts +25 -0
- package/src/components/ui/kb-config-provider.tsx +130 -0
- package/src/components/ui/kb-header.module.css +51 -0
- package/src/components/ui/kb-header.tsx +112 -0
- package/src/components/ui/kb-notification-bell.module.css +103 -0
- package/src/components/ui/kb-notification-bell.tsx +199 -0
- package/src/components/ui/kb-page-layout.tsx +66 -0
- package/src/components/ui/kb-quick-search.module.css +86 -0
- package/src/components/ui/kb-quick-search.tsx +196 -0
- package/src/components/ui/kb-sidebar.module.css +47 -0
- package/src/components/ui/kb-sidebar.tsx +196 -0
- package/src/components/ui/kb-stat-card.module.css +21 -0
- package/src/components/ui/kb-stat-card.tsx +22 -0
- package/src/components/ui/kb-stat-grid.tsx +22 -0
- package/src/components/ui/kb-status-bar.module.css +49 -0
- package/src/components/ui/kb-status-bar.tsx +145 -0
- package/src/components/ui/kb-system-health-indicator.tsx +229 -0
- package/src/components/ui/kb-theme-toggle.tsx +42 -0
- package/src/components/ui/theme-adapter.ts +812 -0
- package/src/components/ui/theme-transition-overlay.tsx +70 -0
- package/src/components/ui/utils.ts +6 -0
- package/src/components/widget-error-boundary.tsx +172 -0
- package/src/config/env.ts +5 -0
- package/src/config/feature-flags.ts +482 -0
- package/src/config/studio.config.schema.ts +88 -0
- package/src/config/studio.config.ts +84 -0
- package/src/env.d.ts +23 -0
- package/src/hooks/use-command-palette.ts +6 -0
- package/src/hooks/use-devtools-store.ts +25 -0
- package/src/hooks/use-feature-flags.ts +101 -0
- package/src/index.css +10 -0
- package/src/layouts/dashboard-grid.tsx +122 -0
- package/src/layouts/index.ts +12 -0
- package/src/layouts/two-pane.tsx +134 -0
- package/src/main.tsx +15 -0
- package/src/modules/analytics/components/analytics-daily-stats-chart.tsx +257 -0
- package/src/modules/analytics/components/analytics-date-range-picker.tsx +121 -0
- package/src/modules/analytics/pages/analytics-cache-page.tsx +285 -0
- package/src/modules/analytics/pages/analytics-embeddings-page.tsx +274 -0
- package/src/modules/analytics/pages/analytics-events-page.tsx +425 -0
- package/src/modules/analytics/pages/analytics-llm-page.tsx +434 -0
- package/src/modules/analytics/pages/analytics-overview-page.tsx +336 -0
- package/src/modules/analytics/pages/analytics-page.tsx +20 -0
- package/src/modules/analytics/pages/analytics-storage-page.tsx +344 -0
- package/src/modules/analytics/pages/analytics-vectorstore-page.tsx +279 -0
- package/src/modules/analytics/routes/index.tsx +133 -0
- package/src/modules/analytics/utils/formatters.test.ts +148 -0
- package/src/modules/analytics/utils/formatters.ts +100 -0
- package/src/modules/auth/pages/login-page.module.css +113 -0
- package/src/modules/auth/pages/login-page.tsx +80 -0
- package/src/modules/auth/routes/index.tsx +24 -0
- package/src/modules/dashboard/components/ActivityTimelineWidget.tsx +199 -0
- package/src/modules/dashboard/components/AiInsightsWidget.tsx +358 -0
- package/src/modules/dashboard/components/AnimatedMetricCard.css +88 -0
- package/src/modules/dashboard/components/AnimatedMetricCard.tsx +125 -0
- package/src/modules/dashboard/components/HeroMetricCard.css +114 -0
- package/src/modules/dashboard/components/HeroMetricCard.tsx +152 -0
- package/src/modules/dashboard/components/HeroMetricsWidget.tsx +146 -0
- package/src/modules/dashboard/components/SystemResourcesWidget.tsx +197 -0
- package/src/modules/dashboard/components/dashboard-empty.tsx +22 -0
- package/src/modules/dashboard/components/dashboard-kpis-widget.tsx +59 -0
- package/src/modules/dashboard/components/dashboard-kpis.tsx +13 -0
- package/src/modules/dashboard/components/dashboard-latency-percentiles-widget.tsx +85 -0
- package/src/modules/dashboard/components/dashboard-metrics-widget.tsx +78 -0
- package/src/modules/dashboard/components/dashboard-plugins-widget.tsx +67 -0
- package/src/modules/dashboard/components/dashboard-requests-widget.tsx +62 -0
- package/src/modules/dashboard/layouts/dashboard-layout.tsx +16 -0
- package/src/modules/dashboard/pages/ai-insights-page.tsx +563 -0
- package/src/modules/dashboard/pages/dashboard-animations.css +317 -0
- package/src/modules/dashboard/pages/dashboard-page.tsx +13 -0
- package/src/modules/dashboard/routes/index.tsx +70 -0
- package/src/modules/observability/pages/devkit-page.tsx +263 -0
- package/src/modules/observability/pages/incident-detail-page.tsx +571 -0
- package/src/modules/observability/pages/incidents-page.tsx +370 -0
- package/src/modules/observability/pages/log-detail-page.tsx +508 -0
- package/src/modules/observability/pages/logs-page.tsx +1129 -0
- package/src/modules/observability/pages/prometheus-metrics-page.tsx +486 -0
- package/src/modules/observability/pages/state-broker-page.tsx +229 -0
- package/src/modules/observability/pages/system-events-page.tsx +255 -0
- package/src/modules/observability/routes/index.tsx +133 -0
- package/src/modules/plugins/components/marketplace-card.tsx +153 -0
- package/src/modules/plugins/components/plugin-ai-assistant-modal.tsx +374 -0
- package/src/modules/plugins/components/plugin-card.tsx +136 -0
- package/src/modules/plugins/components/plugin-detail/capabilities-summary.tsx +101 -0
- package/src/modules/plugins/components/plugin-detail/cli-commands-table.tsx +101 -0
- package/src/modules/plugins/components/plugin-detail/index.ts +7 -0
- package/src/modules/plugins/components/plugin-detail/jobs-table.tsx +64 -0
- package/src/modules/plugins/components/plugin-detail/permissions/env-permissions.tsx +30 -0
- package/src/modules/plugins/components/plugin-detail/permissions/filesystem-permissions.tsx +68 -0
- package/src/modules/plugins/components/plugin-detail/permissions/index.ts +6 -0
- package/src/modules/plugins/components/plugin-detail/permissions/network-permissions.tsx +43 -0
- package/src/modules/plugins/components/plugin-detail/permissions/platform-services.tsx +242 -0
- package/src/modules/plugins/components/plugin-detail/permissions/resource-quotas.tsx +96 -0
- package/src/modules/plugins/components/plugin-detail/permissions/shell-permissions.tsx +55 -0
- package/src/modules/plugins/components/plugin-detail/permissions-view.tsx +47 -0
- package/src/modules/plugins/components/plugin-detail/plugin-header.tsx +169 -0
- package/src/modules/plugins/components/plugin-detail/rest-routes-table.tsx +159 -0
- package/src/modules/plugins/components/plugin-detail/workflows-table.tsx +40 -0
- package/src/modules/plugins/components/plugin-grid.tsx +20 -0
- package/src/modules/plugins/components/plugin-table.tsx +178 -0
- package/src/modules/plugins/pages/marketplace-page.tsx +200 -0
- package/src/modules/plugins/pages/plugin-detail-page.tsx +1145 -0
- package/src/modules/plugins/routes/index.tsx +48 -0
- package/src/modules/plugins/utils/index.ts +1 -0
- package/src/modules/plugins/utils/schema-helpers.ts +21 -0
- package/src/modules/settings/components/api-routes-viewer.tsx +415 -0
- package/src/modules/settings/components/appearance-settings.tsx +200 -0
- package/src/modules/settings/components/configuration-settings.tsx +321 -0
- package/src/modules/settings/components/data-privacy-settings.tsx +111 -0
- package/src/modules/settings/components/experimental-settings.tsx +243 -0
- package/src/modules/settings/components/navigation-settings.tsx +198 -0
- package/src/modules/settings/components/notifications-settings.tsx +40 -0
- package/src/modules/settings/pages/settings-page.tsx +298 -0
- package/src/modules/settings/routes/index.tsx +47 -0
- package/src/pages/README-assistant.md +91 -0
- package/src/pages/login-page.tsx +177 -0
- package/src/pages/not-found-page.tsx +85 -0
- package/src/providers/auth-provider.tsx +40 -0
- package/src/providers/command-palette-provider.tsx +193 -0
- package/src/providers/data-sources-provider.tsx +27 -0
- package/src/providers/devtools-provider.tsx +72 -0
- package/src/providers/navigation-items-provider.tsx +38 -0
- package/src/providers/registry-v2-provider.tsx +93 -0
- package/src/providers/settings-provider.tsx +158 -0
- package/src/router.tsx +302 -0
- package/src/routes/helpers.tsx +25 -0
- package/src/routes/plugin-page-v2.tsx +52 -0
- package/src/routes/types.ts +18 -0
- package/src/styles/antd-overrides.css +211 -0
- package/src/styles/base.css +190 -0
- package/src/styles/settings.css +75 -0
- package/src/styles/variables.css +94 -0
- package/src/utils/analytics.ts +183 -0
- package/src/utils/apply-navigation-categories.ts +83 -0
- package/src/utils/commands.ts +252 -0
- package/src/utils/export.ts +174 -0
- package/src/utils/format-date.ts +166 -0
- package/src/utils/logger.ts +110 -0
- package/tailwind.config.ts +13 -0
- package/tsconfig.json +13 -0
- package/vitest-setup.ts +1 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { UIRow, UICol, UIStatistic, UIAlert, UIProgress } from '@kb-labs/studio-ui-kit';
|
|
3
|
+
import {
|
|
4
|
+
SearchOutlined,
|
|
5
|
+
PlusCircleOutlined,
|
|
6
|
+
DeleteOutlined,
|
|
7
|
+
ClockCircleOutlined,
|
|
8
|
+
StarOutlined,
|
|
9
|
+
FileSearchOutlined,
|
|
10
|
+
DatabaseOutlined,
|
|
11
|
+
} from '@ant-design/icons';
|
|
12
|
+
import { UIText, UIStatisticsChart, UICard, UIPage, UIPageHeader } from '@kb-labs/studio-ui-kit';
|
|
13
|
+
import { useAdaptersVectorStoreUsage, useAdaptersVectorStoreDailyStats } from '@kb-labs/studio-data-client';
|
|
14
|
+
import { useDataSources } from '../../../providers/data-sources-provider';
|
|
15
|
+
import { AnalyticsDateRangePicker, type DateRangeValue } from '../components/analytics-date-range-picker';
|
|
16
|
+
import dayjs from 'dayjs';
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Analytics VectorStore Usage Page
|
|
21
|
+
*
|
|
22
|
+
* Displays vector database usage statistics:
|
|
23
|
+
* - Search, upsert, and delete operations
|
|
24
|
+
* - Search quality metrics (avg score)
|
|
25
|
+
* - Performance metrics
|
|
26
|
+
* - Storage growth tracking
|
|
27
|
+
*/
|
|
28
|
+
export function AnalyticsVectorStorePage() {
|
|
29
|
+
const sources = useDataSources();
|
|
30
|
+
|
|
31
|
+
// Date range state (default: last 30 days)
|
|
32
|
+
const [dateRange, setDateRange] = useState<DateRangeValue>([
|
|
33
|
+
dayjs().subtract(30, 'days'),
|
|
34
|
+
dayjs(),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
data: stats,
|
|
39
|
+
isLoading,
|
|
40
|
+
error,
|
|
41
|
+
isError,
|
|
42
|
+
} = useAdaptersVectorStoreUsage(sources.adapters, {
|
|
43
|
+
from: dateRange[0].toISOString(),
|
|
44
|
+
to: dateRange[1].toISOString(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
data: dailyStats,
|
|
49
|
+
isLoading: dailyStatsLoading,
|
|
50
|
+
} = useAdaptersVectorStoreDailyStats(sources.adapters, {
|
|
51
|
+
from: dateRange[0].toISOString(),
|
|
52
|
+
to: dateRange[1].toISOString(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Check if analytics is not implemented
|
|
56
|
+
const analyticsNotImplemented =
|
|
57
|
+
isError &&
|
|
58
|
+
error instanceof Error &&
|
|
59
|
+
(error.message.includes('not support') || error.message.includes('not implement'));
|
|
60
|
+
|
|
61
|
+
if (analyticsNotImplemented) {
|
|
62
|
+
return (
|
|
63
|
+
<UIPage>
|
|
64
|
+
<UIPageHeader
|
|
65
|
+
title="VectorStore Usage"
|
|
66
|
+
description="Track vector database operations and search quality"
|
|
67
|
+
/>
|
|
68
|
+
<UIAlert
|
|
69
|
+
message="Analytics Not Available"
|
|
70
|
+
description="Analytics adapter does not support reading events. Configure an analytics adapter in kb.config.json that implements getEvents()."
|
|
71
|
+
variant="info"
|
|
72
|
+
showIcon
|
|
73
|
+
style={{ marginTop: 16 }}
|
|
74
|
+
/>
|
|
75
|
+
</UIPage>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isError) {
|
|
80
|
+
return (
|
|
81
|
+
<UIPage>
|
|
82
|
+
<UIPageHeader title="VectorStore Usage" description="Vector database usage analytics" />
|
|
83
|
+
<UIAlert
|
|
84
|
+
message="Failed to Load VectorStore Usage"
|
|
85
|
+
description={error?.message || 'Unknown error occurred'}
|
|
86
|
+
variant="error"
|
|
87
|
+
showIcon
|
|
88
|
+
style={{ marginTop: 16 }}
|
|
89
|
+
/>
|
|
90
|
+
</UIPage>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Format helpers
|
|
95
|
+
const _formatNumber = (num: number) => num.toLocaleString();
|
|
96
|
+
const _formatDuration = (ms: number) => (ms < 1000 ? `${ms.toFixed(1)}ms` : `${(ms / 1000).toFixed(2)}s`);
|
|
97
|
+
const _formatScore = (score: number) => (score * 100).toFixed(1) + '%';
|
|
98
|
+
|
|
99
|
+
const totalOperations = stats
|
|
100
|
+
? stats.searchQueries + stats.upsertOperations + stats.deleteOperations
|
|
101
|
+
: 0;
|
|
102
|
+
|
|
103
|
+
const searchPercentage = stats && totalOperations > 0
|
|
104
|
+
? ((stats.searchQueries / totalOperations) * 100).toFixed(1)
|
|
105
|
+
: 0;
|
|
106
|
+
|
|
107
|
+
const scoreStatus = stats && stats.avgSearchScore >= 0.8
|
|
108
|
+
? 'success'
|
|
109
|
+
: stats && stats.avgSearchScore >= 0.6
|
|
110
|
+
? 'normal'
|
|
111
|
+
: 'exception';
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<UIPage width="full">
|
|
115
|
+
<UIPageHeader
|
|
116
|
+
title="VectorStore Usage Analytics"
|
|
117
|
+
description="Track vector database operations, search quality, and storage growth"
|
|
118
|
+
actions={
|
|
119
|
+
<AnalyticsDateRangePicker
|
|
120
|
+
value={dateRange}
|
|
121
|
+
onChange={setDateRange}
|
|
122
|
+
/>
|
|
123
|
+
}
|
|
124
|
+
/>
|
|
125
|
+
|
|
126
|
+
<div style={{ marginTop: 24 }}>
|
|
127
|
+
{/* Overview Stats */}
|
|
128
|
+
<UIRow gutter={[16, 16]}>
|
|
129
|
+
<UICol xs={24} sm={12} lg={6}>
|
|
130
|
+
<UICard>
|
|
131
|
+
<UIStatistic
|
|
132
|
+
title="Search Queries"
|
|
133
|
+
value={stats?.searchQueries ?? 0}
|
|
134
|
+
prefix={<SearchOutlined />}
|
|
135
|
+
loading={isLoading}
|
|
136
|
+
valueStyle={{ color: 'var(--info)' }}
|
|
137
|
+
/>
|
|
138
|
+
</UICard>
|
|
139
|
+
</UICol>
|
|
140
|
+
<UICol xs={24} sm={12} lg={6}>
|
|
141
|
+
<UICard>
|
|
142
|
+
<UIStatistic
|
|
143
|
+
title="Upsert Operations"
|
|
144
|
+
value={stats?.upsertOperations ?? 0}
|
|
145
|
+
prefix={<PlusCircleOutlined />}
|
|
146
|
+
loading={isLoading}
|
|
147
|
+
valueStyle={{ color: 'var(--success)' }}
|
|
148
|
+
/>
|
|
149
|
+
</UICard>
|
|
150
|
+
</UICol>
|
|
151
|
+
<UICol xs={24} sm={12} lg={6}>
|
|
152
|
+
<UICard>
|
|
153
|
+
<UIStatistic
|
|
154
|
+
title="Delete Operations"
|
|
155
|
+
value={stats?.deleteOperations ?? 0}
|
|
156
|
+
prefix={<DeleteOutlined />}
|
|
157
|
+
loading={isLoading}
|
|
158
|
+
valueStyle={{ color: 'var(--warning)' }}
|
|
159
|
+
/>
|
|
160
|
+
</UICard>
|
|
161
|
+
</UICol>
|
|
162
|
+
<UICol xs={24} sm={12} lg={6}>
|
|
163
|
+
<UICard>
|
|
164
|
+
<UIStatistic
|
|
165
|
+
title="Total Operations"
|
|
166
|
+
value={totalOperations}
|
|
167
|
+
prefix={<DatabaseOutlined />}
|
|
168
|
+
loading={isLoading}
|
|
169
|
+
valueStyle={{ color: 'var(--link)' }}
|
|
170
|
+
/>
|
|
171
|
+
</UICard>
|
|
172
|
+
</UICol>
|
|
173
|
+
</UIRow>
|
|
174
|
+
|
|
175
|
+
{/* Search Quality */}
|
|
176
|
+
<UIRow gutter={[16, 16]} style={{ marginTop: 16 }}>
|
|
177
|
+
<UICol xs={24} lg={12}>
|
|
178
|
+
<UICard title="Search Quality">
|
|
179
|
+
<UIStatistic
|
|
180
|
+
title="Average Search Score"
|
|
181
|
+
value={stats?.avgSearchScore ?? 0}
|
|
182
|
+
precision={3}
|
|
183
|
+
prefix={<StarOutlined />}
|
|
184
|
+
loading={isLoading}
|
|
185
|
+
valueStyle={{
|
|
186
|
+
color: scoreStatus === 'success' ? 'var(--success)' : scoreStatus === 'normal' ? 'var(--info)' : 'var(--warning)'
|
|
187
|
+
}}
|
|
188
|
+
/>
|
|
189
|
+
<div style={{ marginTop: 16 }}>
|
|
190
|
+
<UIText color="secondary">Search Quality Rating</UIText>
|
|
191
|
+
<UIProgress
|
|
192
|
+
percent={Number(((stats?.avgSearchScore ?? 0) * 100).toFixed(1))}
|
|
193
|
+
status={scoreStatus}
|
|
194
|
+
format={(percent) => `${percent}%`}
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
<div style={{ marginTop: 16 }}>
|
|
198
|
+
<UIStatistic
|
|
199
|
+
title="Avg Results per Query"
|
|
200
|
+
value={stats?.avgResultsCount ?? 0}
|
|
201
|
+
precision={1}
|
|
202
|
+
prefix={<FileSearchOutlined />}
|
|
203
|
+
loading={isLoading}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
</UICard>
|
|
207
|
+
</UICol>
|
|
208
|
+
<UICol xs={24} lg={12}>
|
|
209
|
+
<UICard title="Performance Metrics">
|
|
210
|
+
<UIStatistic
|
|
211
|
+
title="Avg Search Duration"
|
|
212
|
+
value={stats?.avgSearchDuration ?? 0}
|
|
213
|
+
prefix={<ClockCircleOutlined />}
|
|
214
|
+
suffix="ms"
|
|
215
|
+
precision={1}
|
|
216
|
+
loading={isLoading}
|
|
217
|
+
/>
|
|
218
|
+
<div style={{ marginTop: 24 }}>
|
|
219
|
+
<UIText color="secondary">Operation Distribution</UIText>
|
|
220
|
+
<UIProgress
|
|
221
|
+
percent={Number(searchPercentage)}
|
|
222
|
+
format={(percent) => `${percent}% searches`}
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
</UICard>
|
|
226
|
+
</UICol>
|
|
227
|
+
</UIRow>
|
|
228
|
+
|
|
229
|
+
{/* Storage Growth */}
|
|
230
|
+
<UICard title="Storage Statistics" style={{ marginTop: 16 }}>
|
|
231
|
+
<UIRow gutter={[16, 16]}>
|
|
232
|
+
<UICol xs={24} sm={8}>
|
|
233
|
+
<UIStatistic
|
|
234
|
+
title="Vectors Added"
|
|
235
|
+
value={stats?.totalVectorsUpserted ?? 0}
|
|
236
|
+
prefix={<PlusCircleOutlined />}
|
|
237
|
+
valueStyle={{ color: 'var(--success)' }}
|
|
238
|
+
loading={isLoading}
|
|
239
|
+
/>
|
|
240
|
+
</UICol>
|
|
241
|
+
<UICol xs={24} sm={8}>
|
|
242
|
+
<UIStatistic
|
|
243
|
+
title="Vectors Deleted"
|
|
244
|
+
value={stats?.totalVectorsDeleted ?? 0}
|
|
245
|
+
prefix={<DeleteOutlined />}
|
|
246
|
+
valueStyle={{ color: 'var(--error)' }}
|
|
247
|
+
loading={isLoading}
|
|
248
|
+
/>
|
|
249
|
+
</UICol>
|
|
250
|
+
<UICol xs={24} sm={8}>
|
|
251
|
+
<UIStatistic
|
|
252
|
+
title="Net Growth"
|
|
253
|
+
value={(stats?.totalVectorsUpserted ?? 0) - (stats?.totalVectorsDeleted ?? 0)}
|
|
254
|
+
loading={isLoading}
|
|
255
|
+
valueStyle={{
|
|
256
|
+
color: (stats?.totalVectorsUpserted ?? 0) > (stats?.totalVectorsDeleted ?? 0)
|
|
257
|
+
? 'var(--success)'
|
|
258
|
+
: 'var(--error)'
|
|
259
|
+
}}
|
|
260
|
+
/>
|
|
261
|
+
</UICol>
|
|
262
|
+
</UIRow>
|
|
263
|
+
</UICard>
|
|
264
|
+
|
|
265
|
+
{/* Daily Usage Chart */}
|
|
266
|
+
<UIStatisticsChart
|
|
267
|
+
data={dailyStats}
|
|
268
|
+
loading={dailyStatsLoading}
|
|
269
|
+
title="Daily VectorStore Operations"
|
|
270
|
+
metrics={[
|
|
271
|
+
{ key: 'totalSearches', label: 'Searches' },
|
|
272
|
+
{ key: 'totalUpserts', label: 'Upserts' },
|
|
273
|
+
{ key: 'totalDeletes', label: 'Deletes' },
|
|
274
|
+
]}
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
</UIPage>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @kb-labs/studio-app/modules/analytics/routes
|
|
3
|
+
* Analytics module routing configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RouteObject } from 'react-router-dom';
|
|
7
|
+
import { AnalyticsOverviewPage } from '../pages/analytics-overview-page';
|
|
8
|
+
import { AnalyticsEventsPage } from '../pages/analytics-events-page';
|
|
9
|
+
import { AnalyticsLLMPage } from '../pages/analytics-llm-page';
|
|
10
|
+
import { AnalyticsEmbeddingsPage } from '../pages/analytics-embeddings-page';
|
|
11
|
+
import { AnalyticsVectorStorePage } from '../pages/analytics-vectorstore-page';
|
|
12
|
+
import { AnalyticsCachePage } from '../pages/analytics-cache-page';
|
|
13
|
+
import { AnalyticsStoragePage } from '../pages/analytics-storage-page';
|
|
14
|
+
import { ErrorBoundary } from '../../../components/error-boundary';
|
|
15
|
+
import { renderIcon } from '../../../routes/helpers';
|
|
16
|
+
import type { NavigationItem } from '@/components/ui';
|
|
17
|
+
|
|
18
|
+
// Route paths
|
|
19
|
+
const PATHS = {
|
|
20
|
+
OVERVIEW: '/analytics/overview',
|
|
21
|
+
EVENTS: '/analytics/events',
|
|
22
|
+
LLM: '/analytics/llm',
|
|
23
|
+
EMBEDDINGS: '/analytics/embeddings',
|
|
24
|
+
VECTORSTORE: '/analytics/vectorstore',
|
|
25
|
+
CACHE: '/analytics/cache',
|
|
26
|
+
STORAGE: '/analytics/storage',
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
// Route keys
|
|
30
|
+
const KEYS = {
|
|
31
|
+
MODULE: 'analytics',
|
|
32
|
+
OVERVIEW: 'analytics-overview',
|
|
33
|
+
EVENTS: 'analytics-events',
|
|
34
|
+
LLM: 'analytics-llm',
|
|
35
|
+
EMBEDDINGS: 'analytics-embeddings',
|
|
36
|
+
VECTORSTORE: 'analytics-vectorstore',
|
|
37
|
+
CACHE: 'analytics-cache',
|
|
38
|
+
STORAGE: 'analytics-storage',
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Analytics routes
|
|
43
|
+
*/
|
|
44
|
+
export const analyticsRoutes: RouteObject[] = [
|
|
45
|
+
{
|
|
46
|
+
path: PATHS.OVERVIEW,
|
|
47
|
+
element: <AnalyticsOverviewPage />,
|
|
48
|
+
errorElement: <ErrorBoundary />,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
path: PATHS.EVENTS,
|
|
52
|
+
element: <AnalyticsEventsPage />,
|
|
53
|
+
errorElement: <ErrorBoundary />,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
path: PATHS.LLM,
|
|
57
|
+
element: <AnalyticsLLMPage />,
|
|
58
|
+
errorElement: <ErrorBoundary />,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
path: PATHS.EMBEDDINGS,
|
|
62
|
+
element: <AnalyticsEmbeddingsPage />,
|
|
63
|
+
errorElement: <ErrorBoundary />,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
path: PATHS.VECTORSTORE,
|
|
67
|
+
element: <AnalyticsVectorStorePage />,
|
|
68
|
+
errorElement: <ErrorBoundary />,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
path: PATHS.CACHE,
|
|
72
|
+
element: <AnalyticsCachePage />,
|
|
73
|
+
errorElement: <ErrorBoundary />,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
path: PATHS.STORAGE,
|
|
77
|
+
element: <AnalyticsStoragePage />,
|
|
78
|
+
errorElement: <ErrorBoundary />,
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Analytics navigation item for sidebar
|
|
84
|
+
*/
|
|
85
|
+
export const analyticsNavigation: NavigationItem = {
|
|
86
|
+
key: KEYS.MODULE,
|
|
87
|
+
label: 'Analytics',
|
|
88
|
+
icon: renderIcon('BarChartOutlined'),
|
|
89
|
+
children: [
|
|
90
|
+
{
|
|
91
|
+
key: KEYS.OVERVIEW,
|
|
92
|
+
label: 'Overview',
|
|
93
|
+
path: PATHS.OVERVIEW,
|
|
94
|
+
icon: renderIcon('DashboardOutlined'),
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
key: KEYS.EVENTS,
|
|
98
|
+
label: 'Events',
|
|
99
|
+
path: PATHS.EVENTS,
|
|
100
|
+
icon: renderIcon('ThunderboltOutlined'),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
key: KEYS.LLM,
|
|
104
|
+
label: 'LLM Usage',
|
|
105
|
+
path: PATHS.LLM,
|
|
106
|
+
icon: renderIcon('RobotOutlined'),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
key: KEYS.EMBEDDINGS,
|
|
110
|
+
label: 'Embeddings',
|
|
111
|
+
path: PATHS.EMBEDDINGS,
|
|
112
|
+
icon: renderIcon('NodeIndexOutlined'),
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
key: KEYS.VECTORSTORE,
|
|
116
|
+
label: 'Vector Store',
|
|
117
|
+
path: PATHS.VECTORSTORE,
|
|
118
|
+
icon: renderIcon('DatabaseOutlined'),
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: KEYS.CACHE,
|
|
122
|
+
label: 'Cache',
|
|
123
|
+
path: PATHS.CACHE,
|
|
124
|
+
icon: renderIcon('ThunderboltOutlined'),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
key: KEYS.STORAGE,
|
|
128
|
+
label: 'Storage',
|
|
129
|
+
path: PATHS.STORAGE,
|
|
130
|
+
icon: renderIcon('SaveOutlined'),
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module analytics/utils/formatters.test
|
|
3
|
+
* Tests for analytics formatters
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { formatCost } from './formatters';
|
|
8
|
+
|
|
9
|
+
describe('formatCost', () => {
|
|
10
|
+
describe('real backend data - single LLM calls', () => {
|
|
11
|
+
it('should format very small costs (individual API calls)', () => {
|
|
12
|
+
// Real data from events-20260116.jsonl
|
|
13
|
+
expect(formatCost(0.000043349999999999997)).toBe('$4.33e-5'); // 145+36 tokens
|
|
14
|
+
expect(formatCost(0.00008730000000000001)).toBe('$8.73e-5'); // 234+87 tokens
|
|
15
|
+
expect(formatCost(0.0005536499999999999)).toBe('$0.000554'); // 2847+211 tokens
|
|
16
|
+
expect(formatCost(0.0001407)).toBe('$0.000141'); // 514+106 tokens
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should format slightly larger costs', () => {
|
|
20
|
+
expect(formatCost(0.00028695)).toBe('$0.000287'); // Original bug example
|
|
21
|
+
expect(formatCost(0.000044399999999999995)).toBe('$4.44e-5'); // 160+34 tokens
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('aggregated daily costs', () => {
|
|
26
|
+
it('should format daily totals', () => {
|
|
27
|
+
// Simulate 100 small API calls per day
|
|
28
|
+
const dailyTotal = 0.000043 * 100; // ≈ $0.0043
|
|
29
|
+
expect(formatCost(dailyTotal)).toBe('$0.004300');
|
|
30
|
+
|
|
31
|
+
// Simulate 1000 small API calls per day
|
|
32
|
+
const largeDailyTotal = 0.000043 * 1000; // ≈ $0.043
|
|
33
|
+
expect(formatCost(largeDailyTotal)).toBe('$0.0430');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should format typical production daily costs', () => {
|
|
37
|
+
expect(formatCost(0.001)).toBe('$0.001000'); // 6 decimals for $0.001
|
|
38
|
+
expect(formatCost(0.01)).toBe('$0.0100'); // 4 decimals for $0.01
|
|
39
|
+
expect(formatCost(0.1)).toBe('$0.1000'); // 4 decimals for $0.10
|
|
40
|
+
expect(formatCost(1.5)).toBe('$1.50'); // 2 decimals for $1.50
|
|
41
|
+
expect(formatCost(10.99)).toBe('$10.99'); // 2 decimals for $10.99
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('edge cases', () => {
|
|
46
|
+
it('should handle zero cost', () => {
|
|
47
|
+
expect(formatCost(0)).toBe('$0.00');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should handle very tiny costs (scientific notation)', () => {
|
|
51
|
+
expect(formatCost(0.00000012)).toBe('$1.20e-7');
|
|
52
|
+
expect(formatCost(0.000000001)).toBe('$1.00e-9');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should handle large costs', () => {
|
|
56
|
+
expect(formatCost(100.456)).toBe('$100.46');
|
|
57
|
+
expect(formatCost(1000.123)).toBe('$1000.12');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('precision thresholds', () => {
|
|
62
|
+
it('should use 2 decimals for >= $1', () => {
|
|
63
|
+
expect(formatCost(1)).toBe('$1.00');
|
|
64
|
+
expect(formatCost(1.234)).toBe('$1.23');
|
|
65
|
+
expect(formatCost(1.235)).toBe('$1.24'); // Rounds up
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should use 4 decimals for >= $0.01', () => {
|
|
69
|
+
expect(formatCost(0.01)).toBe('$0.0100');
|
|
70
|
+
expect(formatCost(0.012345)).toBe('$0.0123');
|
|
71
|
+
expect(formatCost(0.012355)).toBe('$0.0124'); // Rounds up
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should use 6 decimals for >= $0.0001', () => {
|
|
75
|
+
expect(formatCost(0.0001)).toBe('$0.000100');
|
|
76
|
+
expect(formatCost(0.00012345)).toBe('$0.000123');
|
|
77
|
+
expect(formatCost(0.00012355)).toBe('$0.000124'); // Rounds up
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should use scientific notation for < $0.0001', () => {
|
|
81
|
+
expect(formatCost(0.00009999)).toBe('$1.00e-4');
|
|
82
|
+
expect(formatCost(0.00001)).toBe('$1.00e-5');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('what we expect to see in the chart tooltip', () => {
|
|
87
|
+
it('should show readable values for typical daily aggregates', () => {
|
|
88
|
+
// If we have 10 requests/day with avg $0.00028695 each
|
|
89
|
+
const cost = 10 * 0.00028695; // = $0.0028695
|
|
90
|
+
expect(formatCost(cost)).toBe('$0.002870'); // 6 decimals, readable
|
|
91
|
+
|
|
92
|
+
// If we have 100 requests/day
|
|
93
|
+
const cost100 = 100 * 0.00028695; // = $0.028695
|
|
94
|
+
expect(formatCost(cost100)).toBe('$0.0287'); // 4 decimals, readable
|
|
95
|
+
|
|
96
|
+
// If we have 1000 requests/day
|
|
97
|
+
const cost1000 = 1000 * 0.00028695; // = $0.28695
|
|
98
|
+
expect(formatCost(cost1000)).toBe('$0.2869'); // 4 decimals, readable (rounds down)
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should NOT show $0.00 for small but non-zero values', () => {
|
|
102
|
+
// These should all be visible (not $0.00)
|
|
103
|
+
expect(formatCost(0.00028695)).not.toBe('$0.00');
|
|
104
|
+
expect(formatCost(0.000043)).not.toBe('$0.00');
|
|
105
|
+
expect(formatCost(0.001)).not.toBe('$0.00');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('real world scenario: what chart shows on Jan 16', () => {
|
|
110
|
+
it('should handle sum of all LLM calls on Jan 16', () => {
|
|
111
|
+
// Sample from real data:
|
|
112
|
+
const costs = [
|
|
113
|
+
0.000043349999999999997,
|
|
114
|
+
0.000043349999999999997,
|
|
115
|
+
0.00008730000000000001,
|
|
116
|
+
0.0005536499999999999,
|
|
117
|
+
0.0001407,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const total = costs.reduce((sum, c) => sum + c, 0);
|
|
121
|
+
// Total ≈ 0.00086265
|
|
122
|
+
|
|
123
|
+
const formatted = formatCost(total);
|
|
124
|
+
|
|
125
|
+
// Should be visible with 6 decimals
|
|
126
|
+
expect(formatted).toBe('$0.000868'); // Actual rounded value
|
|
127
|
+
|
|
128
|
+
// Should NOT be $0.00
|
|
129
|
+
expect(formatted).not.toBe('$0.00');
|
|
130
|
+
|
|
131
|
+
// Should NOT be scientific notation (since > $0.0001)
|
|
132
|
+
expect(formatted).not.toContain('e');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should handle very low daily total (few requests)', () => {
|
|
136
|
+
// If only 2-3 requests on a day
|
|
137
|
+
const lowDailyTotal = 0.000043 + 0.000087; // $0.00013
|
|
138
|
+
|
|
139
|
+
const formatted = formatCost(lowDailyTotal);
|
|
140
|
+
|
|
141
|
+
// Should use 6 decimals (since >= $0.0001)
|
|
142
|
+
expect(formatted).toBe('$0.000130');
|
|
143
|
+
|
|
144
|
+
// Should NOT be $0.00
|
|
145
|
+
expect(formatted).not.toBe('$0.00');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module analytics/utils/formatters
|
|
3
|
+
* Common formatting utilities for analytics pages
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format cost with adaptive precision based on magnitude.
|
|
8
|
+
*
|
|
9
|
+
* Uses different decimal places depending on the value:
|
|
10
|
+
* - >= $1.00: 2 decimal places ($12.34)
|
|
11
|
+
* - >= $0.01: 4 decimal places ($0.0123)
|
|
12
|
+
* - >= $0.0001: 6 decimal places ($0.000123)
|
|
13
|
+
* - < $0.0001: scientific notation ($1.23e-7)
|
|
14
|
+
*
|
|
15
|
+
* This ensures tiny costs (like $0.00028695) are visible in charts
|
|
16
|
+
* while keeping larger values readable.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* formatCost(12.3456) // "$12.35"
|
|
20
|
+
* formatCost(0.0123456) // "$0.0123"
|
|
21
|
+
* formatCost(0.00028695) // "$0.000287"
|
|
22
|
+
* formatCost(0.00000012) // "$1.20e-7"
|
|
23
|
+
*/
|
|
24
|
+
export function formatCost(cost: number | undefined | null): string {
|
|
25
|
+
if (cost == null || isNaN(cost)) {
|
|
26
|
+
return '$0.00';
|
|
27
|
+
}
|
|
28
|
+
// Handle zero explicitly
|
|
29
|
+
if (cost === 0) {
|
|
30
|
+
return '$0.00';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (cost >= 1) {
|
|
34
|
+
return `$${cost.toFixed(2)}`;
|
|
35
|
+
}
|
|
36
|
+
if (cost >= 0.01) {
|
|
37
|
+
return `$${cost.toFixed(4)}`;
|
|
38
|
+
}
|
|
39
|
+
if (cost >= 0.0001) {
|
|
40
|
+
return `$${cost.toFixed(6)}`;
|
|
41
|
+
}
|
|
42
|
+
// For very small values (< $0.0001), use scientific notation
|
|
43
|
+
return `$${cost.toExponential(2)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format number with thousand separators.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* formatNumber(1234567) // "1,234,567"
|
|
51
|
+
*/
|
|
52
|
+
export function formatNumber(num: number): string {
|
|
53
|
+
return num.toLocaleString();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Format duration in milliseconds to human-readable string.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* formatDuration(500) // "500ms"
|
|
61
|
+
* formatDuration(2500) // "2.50s"
|
|
62
|
+
*/
|
|
63
|
+
export function formatDuration(ms: number): string {
|
|
64
|
+
if (ms < 1000) {
|
|
65
|
+
return `${ms.toFixed(0)}ms`;
|
|
66
|
+
}
|
|
67
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Format byte size to human-readable string.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* formatSize(512) // "512 B"
|
|
75
|
+
* formatSize(2048) // "2.00 KB"
|
|
76
|
+
* formatSize(1048576) // "1.00 MB"
|
|
77
|
+
*/
|
|
78
|
+
export function formatSize(bytes: number): string {
|
|
79
|
+
if (bytes < 1024) {
|
|
80
|
+
return `${bytes} B`;
|
|
81
|
+
}
|
|
82
|
+
if (bytes < 1024 * 1024) {
|
|
83
|
+
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
84
|
+
}
|
|
85
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
86
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
87
|
+
}
|
|
88
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Format percentage value.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* formatPercentage(0.856) // "85.6%"
|
|
96
|
+
* formatPercentage(0.1234) // "12.3%"
|
|
97
|
+
*/
|
|
98
|
+
export function formatPercentage(value: number): string {
|
|
99
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
100
|
+
}
|