@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,25 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { devToolsStore } from '@kb-labs/studio-devtools';
3
+ import type { DevToolsChannel } from '@kb-labs/studio-devtools';
4
+
5
+ export interface UseDevToolsStoreReturn {
6
+ channels: readonly DevToolsChannel[];
7
+ clear(): void;
8
+ }
9
+
10
+ /**
11
+ * Subscribe to the DevTools store and re-render on any change.
12
+ * Uses the same forceUpdate pattern as ReactQueryDevtools.
13
+ */
14
+ export function useDevToolsStore(): UseDevToolsStoreReturn {
15
+ const [, forceUpdate] = useState(0);
16
+
17
+ useEffect(() => {
18
+ return devToolsStore.subscribe(() => { forceUpdate((n) => n + 1); });
19
+ }, []);
20
+
21
+ return {
22
+ channels: devToolsStore.getChannels(),
23
+ clear: () => { devToolsStore.clear(); },
24
+ };
25
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @module @kb-labs/studio-app/hooks/use-feature-flags
3
+ * Hook to access and toggle feature flags
4
+ */
5
+
6
+ import { useCallback, useMemo } from 'react';
7
+ import { useSettings } from '@/providers/settings-provider';
8
+ import type { FeatureId } from '@/config/feature-flags';
9
+ import { isFeatureEnabled as isEnabled, areDependenciesMet } from '@/config/feature-flags';
10
+
11
+ export interface UseFeatureFlagsReturn {
12
+ /** Check if a feature is enabled */
13
+ isEnabled: (featureId: FeatureId) => boolean;
14
+ /** Toggle a feature on/off */
15
+ toggleFeature: (featureId: FeatureId) => void;
16
+ /** Enable a feature */
17
+ enableFeature: (featureId: FeatureId) => void;
18
+ /** Disable a feature */
19
+ disableFeature: (featureId: FeatureId) => void;
20
+ /** Get all enabled feature IDs */
21
+ enabledFeatures: FeatureId[];
22
+ /** Check if feature dependencies are met */
23
+ areDependenciesMet: (featureId: FeatureId) => boolean;
24
+ }
25
+
26
+ export function useFeatureFlags(): UseFeatureFlagsReturn {
27
+ const { settings, updateSettings } = useSettings();
28
+ const enabledFeatures = useMemo(() => settings.experimental?.enabledFeatures ?? [], [settings.experimental?.enabledFeatures]);
29
+
30
+ // Create a preferences map for efficient lookup
31
+ const preferences = useMemo(() => {
32
+ const map: Record<FeatureId, boolean> = {} as Record<FeatureId, boolean>;
33
+ enabledFeatures.forEach((id) => {
34
+ map[id] = true;
35
+ });
36
+ return map;
37
+ }, [enabledFeatures]);
38
+
39
+ const isFeatureEnabled = useCallback(
40
+ (featureId: FeatureId): boolean => {
41
+ return isEnabled(featureId, preferences);
42
+ },
43
+ [preferences]
44
+ );
45
+
46
+ const toggleFeature = useCallback(
47
+ (featureId: FeatureId) => {
48
+ const currentlyEnabled = enabledFeatures.includes(featureId);
49
+ const newFeatures = currentlyEnabled
50
+ ? enabledFeatures.filter((id) => id !== featureId)
51
+ : [...enabledFeatures, featureId];
52
+
53
+ updateSettings({
54
+ experimental: {
55
+ enabledFeatures: newFeatures,
56
+ },
57
+ });
58
+ },
59
+ [enabledFeatures, updateSettings]
60
+ );
61
+
62
+ const enableFeature = useCallback(
63
+ (featureId: FeatureId) => {
64
+ if (!enabledFeatures.includes(featureId)) {
65
+ updateSettings({
66
+ experimental: {
67
+ enabledFeatures: [...enabledFeatures, featureId],
68
+ },
69
+ });
70
+ }
71
+ },
72
+ [enabledFeatures, updateSettings]
73
+ );
74
+
75
+ const disableFeature = useCallback(
76
+ (featureId: FeatureId) => {
77
+ updateSettings({
78
+ experimental: {
79
+ enabledFeatures: enabledFeatures.filter((id) => id !== featureId),
80
+ },
81
+ });
82
+ },
83
+ [enabledFeatures, updateSettings]
84
+ );
85
+
86
+ const checkDependencies = useCallback(
87
+ (featureId: FeatureId): boolean => {
88
+ return areDependenciesMet(featureId, preferences);
89
+ },
90
+ [preferences]
91
+ );
92
+
93
+ return {
94
+ isEnabled: isFeatureEnabled,
95
+ toggleFeature,
96
+ enableFeature,
97
+ disableFeature,
98
+ enabledFeatures,
99
+ areDependenciesMet: checkDependencies,
100
+ };
101
+ }
package/src/index.css ADDED
@@ -0,0 +1,10 @@
1
+ /* Theme variables and base styles - must be first */
2
+ @import './styles/variables.css';
3
+ @import './styles/base.css';
4
+ @import './styles/antd-overrides.css';
5
+ @import './styles/settings.css';
6
+
7
+ @tailwind base;
8
+ @tailwind components;
9
+ @tailwind utilities;
10
+
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @module @kb-labs/studio-app/layouts/DashboardGrid
3
+ * Dashboard Grid layout - responsive CSS Grid using design tokens
4
+ *
5
+ * Refactored to use spacing tokens and remove inline styles where possible.
6
+ */
7
+
8
+ import * as React from 'react';
9
+ import { spacing } from '@kb-labs/studio-ui-core';
10
+ import { useUITheme } from '@kb-labs/studio-ui-kit';
11
+
12
+ export interface DashboardGridProps {
13
+ children: React.ReactNode;
14
+ cols?: {
15
+ sm: number;
16
+ md: number;
17
+ lg: number;
18
+ };
19
+ rowHeight?: number;
20
+ gap?: number;
21
+ }
22
+
23
+ export function DashboardGrid({
24
+ children,
25
+ cols = { sm: 4, md: 8, lg: 12 },
26
+ rowHeight = 8,
27
+ gap = 16
28
+ }: DashboardGridProps) {
29
+ const { token } = useUITheme();
30
+ const childrenArray = React.Children.toArray(children);
31
+
32
+ // Use spacing tokens for gap if available
33
+ const gridGap = gap === 16 ? spacing[4] : gap === 8 ? spacing[2] : `${gap}px`;
34
+
35
+ return (
36
+ <div
37
+ className="widget-dashboard-grid"
38
+ style={
39
+ {
40
+ '--cols-sm': cols.sm,
41
+ '--cols-md': cols.md,
42
+ '--cols-lg': cols.lg,
43
+ '--row-height': `${rowHeight * 8}px`,
44
+ '--gap': gridGap,
45
+ '--border-color': token.colorBorder,
46
+ } as React.CSSProperties
47
+ }
48
+ >
49
+ {childrenArray.map((child, index) => {
50
+ // Extract layoutHint from child props if available
51
+ const layoutHint = (child as React.ReactElement)?.props?.layoutHint || { w: 4, h: 4 };
52
+ const { w, h, minW, minH, height } = layoutHint;
53
+
54
+ // Calculate height based on layoutHint.height
55
+ let itemHeight: string | undefined;
56
+ let itemMaxHeight: string | undefined;
57
+ let itemMinHeight: string | undefined;
58
+
59
+ if (height === 'fit-content') {
60
+ itemHeight = 'fit-content';
61
+ } else if (typeof height === 'number') {
62
+ itemHeight = `${height}px`;
63
+ } else if (height === 'auto' || height === undefined) {
64
+ // Auto height: use grid rows but allow content to expand
65
+ itemMinHeight = minH ? `${minH * rowHeight * 8}px` : `${h * rowHeight * 8}px`;
66
+ } else {
67
+ // Fallback: use grid-based height calculation
68
+ itemMinHeight = minH ? `${minH * rowHeight * 8}px` : `${h * rowHeight * 8}px`;
69
+ }
70
+
71
+ return (
72
+ <div
73
+ key={index}
74
+ className="widget-dashboard-grid__item"
75
+ style={
76
+ {
77
+ gridColumn: `span ${w}`,
78
+ gridRow: height === 'fit-content' ? 'span 1' : `span ${h}`,
79
+ minWidth: minW ? `${minW * (100 / cols.lg)}%` : undefined,
80
+ minHeight: itemMinHeight,
81
+ height: itemHeight,
82
+ maxHeight: itemMaxHeight,
83
+ } as React.CSSProperties
84
+ }
85
+ >
86
+ {child}
87
+ </div>
88
+ );
89
+ })}
90
+ <style>{`
91
+ .widget-dashboard-grid {
92
+ display: grid;
93
+ grid-template-columns: repeat(var(--cols-sm), 1fr);
94
+ gap: var(--gap);
95
+ grid-auto-rows: minmax(var(--row-height), auto);
96
+ align-items: start;
97
+ }
98
+ @media (min-width: 768px) {
99
+ .widget-dashboard-grid {
100
+ grid-template-columns: repeat(var(--cols-md), 1fr);
101
+ }
102
+ }
103
+ @media (min-width: 1024px) {
104
+ .widget-dashboard-grid {
105
+ grid-template-columns: repeat(var(--cols-lg), 1fr);
106
+ }
107
+ }
108
+ .widget-dashboard-grid__item {
109
+ min-height: var(--row-height);
110
+ display: flex;
111
+ flex-direction: column;
112
+ }
113
+ .widget-dashboard-grid__item > * {
114
+ flex: 1;
115
+ min-height: 0;
116
+ }
117
+ `}</style>
118
+ </div>
119
+ );
120
+ }
121
+
122
+
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @module @kb-labs/studio-app/layouts
3
+ * Layout components
4
+ */
5
+
6
+ export { DashboardGrid } from './dashboard-grid';
7
+ export type { DashboardGridProps } from './dashboard-grid';
8
+
9
+ export { TwoPane } from './two-pane';
10
+ export type { TwoPaneProps } from './two-pane';
11
+
12
+
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @module @kb-labs/studio-app/layouts/TwoPane
3
+ * Two Pane layout - flex-based with optional resizer using design tokens
4
+ *
5
+ * Refactored to use Ant Design theme tokens for colors and transitions.
6
+ */
7
+
8
+ import * as React from 'react';
9
+ import { useUITheme } from '@kb-labs/studio-ui-kit';
10
+
11
+ export interface TwoPaneProps {
12
+ left: React.ReactNode;
13
+ right: React.ReactNode;
14
+ leftWidth?: string;
15
+ resizable?: boolean;
16
+ minLeftWidth?: string;
17
+ maxLeftWidth?: string;
18
+ }
19
+
20
+ export function TwoPane({
21
+ left,
22
+ right,
23
+ leftWidth = '50%',
24
+ resizable = false,
25
+ minLeftWidth = '20%',
26
+ maxLeftWidth = '80%',
27
+ }: TwoPaneProps) {
28
+ const { token } = useUITheme();
29
+ const [leftWidthState, setLeftWidthState] = React.useState(leftWidth);
30
+ const [isResizing, setIsResizing] = React.useState(false);
31
+ const containerRef = React.useRef<HTMLDivElement>(null);
32
+
33
+ const handleMouseDown = () => {
34
+ if (!resizable) {return;}
35
+ setIsResizing(true);
36
+ };
37
+
38
+ const handleMouseMove = React.useCallback(
39
+ (e: MouseEvent) => {
40
+ if (!isResizing || !containerRef.current) {return;}
41
+
42
+ const container = containerRef.current;
43
+ const rect = container.getBoundingClientRect();
44
+ const newLeftWidth = ((e.clientX - rect.left) / rect.width) * 100;
45
+
46
+ // Apply min/max constraints
47
+ const min = parseFloat(minLeftWidth);
48
+ const max = parseFloat(maxLeftWidth);
49
+ const clamped = Math.max(min, Math.min(max, newLeftWidth));
50
+
51
+ setLeftWidthState(`${clamped}%`);
52
+ },
53
+ [isResizing, minLeftWidth, maxLeftWidth]
54
+ );
55
+
56
+ const handleMouseUp = () => {
57
+ setIsResizing(false);
58
+ };
59
+
60
+ React.useEffect(() => {
61
+ if (isResizing) {
62
+ document.addEventListener('mousemove', handleMouseMove);
63
+ document.addEventListener('mouseup', handleMouseUp);
64
+ return () => {
65
+ document.removeEventListener('mousemove', handleMouseMove);
66
+ document.removeEventListener('mouseup', handleMouseUp);
67
+ };
68
+ }
69
+ }, [isResizing, handleMouseMove]);
70
+
71
+ return (
72
+ <div
73
+ className="widget-two-pane"
74
+ ref={containerRef}
75
+ style={
76
+ {
77
+ '--border-color': token.colorBorder,
78
+ '--link-color': token.colorLink,
79
+ '--transition-duration': `${token.motionDurationMid}`,
80
+ } as React.CSSProperties
81
+ }
82
+ >
83
+ <div
84
+ className="widget-two-pane__left"
85
+ style={
86
+ {
87
+ width: leftWidthState,
88
+ minWidth: minLeftWidth,
89
+ maxWidth: maxLeftWidth,
90
+ } as React.CSSProperties
91
+ }
92
+ >
93
+ {left}
94
+ </div>
95
+ {resizable && (
96
+ <div
97
+ className="widget-two-pane__resizer"
98
+ onMouseDown={handleMouseDown}
99
+ data-resizing={isResizing}
100
+ />
101
+ )}
102
+ <div className="widget-two-pane__right">{right}</div>
103
+ <style>{`
104
+ .widget-two-pane {
105
+ display: flex;
106
+ height: 100%;
107
+ width: 100%;
108
+ }
109
+ .widget-two-pane__left {
110
+ flex-shrink: 0;
111
+ overflow: auto;
112
+ border-right: 1px solid var(--border-color);
113
+ }
114
+ .widget-two-pane__resizer {
115
+ width: 4px;
116
+ background: var(--border-color);
117
+ cursor: col-resize;
118
+ flex-shrink: 0;
119
+ transition: background var(--transition-duration);
120
+ }
121
+ .widget-two-pane__resizer:hover,
122
+ .widget-two-pane__resizer[data-resizing="true"] {
123
+ background: var(--link-color);
124
+ }
125
+ .widget-two-pane__right {
126
+ flex: 1;
127
+ overflow: auto;
128
+ }
129
+ `}</style>
130
+ </div>
131
+ );
132
+ }
133
+
134
+
package/src/main.tsx ADDED
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import { App } from './App';
4
+ import '@kb-labs/studio-ui-kit/dist/index.css';
5
+ import { registerIconRenderer } from '@kb-labs/studio-ui-kit';
6
+ import { renderIcon } from './components/ui/icons';
7
+ import './index.css';
8
+
9
+ registerIconRenderer(renderIcon);
10
+
11
+ ReactDOM.createRoot(document.getElementById('root')!).render(
12
+ <React.StrictMode>
13
+ <App />
14
+ </React.StrictMode>
15
+ );
@@ -0,0 +1,257 @@
1
+ import { useMemo } from 'react';
2
+ import { UIText, UICard, UIAreaChart, useChartColors } from '@kb-labs/studio-ui-kit';
3
+ import type { DailyStats } from '@kb-labs/studio-data-client';
4
+ import dayjs from 'dayjs';
5
+
6
+ /**
7
+ * Configuration for chart metrics
8
+ */
9
+ export interface ChartMetricConfig {
10
+ /**
11
+ * Metric key from DailyStats.metrics
12
+ */
13
+ key: string;
14
+
15
+ /**
16
+ * Display label for the metric
17
+ */
18
+ label: string;
19
+
20
+ /**
21
+ * Optional transformer function to convert raw value to display value
22
+ * Example: (v) => v / 1000 to convert tokens to K tokens
23
+ */
24
+ transform?: (value: number) => number;
25
+
26
+ /**
27
+ * Optional formatter for tooltip
28
+ */
29
+ format?: (value: number) => string;
30
+ }
31
+
32
+ export interface AnalyticsDailyStatsChartProps {
33
+ /**
34
+ * Daily statistics data
35
+ */
36
+ data: DailyStats[] | undefined;
37
+
38
+ /**
39
+ * Loading state
40
+ */
41
+ loading?: boolean;
42
+
43
+ /**
44
+ * Chart title
45
+ */
46
+ title: string;
47
+
48
+ /**
49
+ * Metrics to display on the chart
50
+ * Each metric will be shown as a separate line
51
+ */
52
+ metrics: ChartMetricConfig[];
53
+
54
+ /**
55
+ * Optional card style
56
+ */
57
+ style?: React.CSSProperties;
58
+
59
+ /**
60
+ * Chart height in pixels
61
+ */
62
+ height?: number;
63
+ }
64
+
65
+ /**
66
+ * Reusable daily statistics chart component
67
+ *
68
+ * Displays time-series chart with multiple metrics for analytics pages.
69
+ * Used across LLM, Embeddings, VectorStore, Cache, and Storage analytics.
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * <AnalyticsDailyStatsChart
74
+ * data={dailyStats}
75
+ * loading={isLoading}
76
+ * title="Daily LLM Usage Trend"
77
+ * metrics={[
78
+ * { key: 'count', label: 'Requests' },
79
+ * { key: 'totalTokens', label: 'Tokens (K)', transform: (v) => v / 1000 },
80
+ * { key: 'totalCost', label: 'Cost ($)', format: (v) => `$${v.toFixed(2)}` },
81
+ * ]}
82
+ * />
83
+ * ```
84
+ */
85
+ export function AnalyticsDailyStatsChart({
86
+ data,
87
+ loading = false,
88
+ title,
89
+ metrics,
90
+ style,
91
+ height = 350,
92
+ }: AnalyticsDailyStatsChartProps) {
93
+ const palette = useChartColors();
94
+
95
+ // Memoize chart colors to ensure stability across renders
96
+ const chartColors = useMemo(() => {
97
+ console.log('[useMemo] Input:', {
98
+ paletteColors: palette.colors,
99
+ isArray: Array.isArray(palette.colors),
100
+ length: palette.colors?.length,
101
+ first3: palette.colors.slice(0, 3),
102
+ metricsLength: metrics.length,
103
+ });
104
+
105
+ const result = palette.colors.slice(0, metrics.length);
106
+
107
+ console.log('[useMemo] Output:', {
108
+ result,
109
+ isArray: Array.isArray(result),
110
+ length: result.length,
111
+ });
112
+
113
+ return result;
114
+ }, [palette.colors, metrics.length]);
115
+
116
+ console.log('[AnalyticsDailyStatsChart] Colors:', {
117
+ metricsCount: metrics.length,
118
+ chartColors,
119
+ paletteColors: palette.colors,
120
+ });
121
+
122
+ // Transform data for chart
123
+ const chartData =
124
+ data?.flatMap((stat) => {
125
+ const rows: Array<{ date: string; category: string; value: number }> = [];
126
+
127
+ for (const metric of metrics) {
128
+ let value: number;
129
+
130
+ if (metric.key === 'count') {
131
+ // Special case: count is top-level field
132
+ value = stat.count;
133
+ } else {
134
+ // Extract from metrics object
135
+ value = stat.metrics?.[metric.key] ?? 0;
136
+ }
137
+
138
+ // Apply transformation if provided
139
+ if (metric.transform) {
140
+ value = metric.transform(value);
141
+ }
142
+
143
+ rows.push({
144
+ date: dayjs(stat.date).format('YYYY-MM-DD'), // Ensure consistent date format
145
+ category: metric.label,
146
+ value,
147
+ });
148
+ }
149
+
150
+ return rows;
151
+ }) ?? [];
152
+
153
+ console.log('[AnalyticsDailyStatsChart] Chart data:', {
154
+ dataLength: chartData.length,
155
+ uniqueCategories: [...new Set(chartData.map(d => d.category))],
156
+ sampleData: chartData.slice(0, 3),
157
+ });
158
+
159
+
160
+ const cardStyle = { marginTop: 16, ...style } as any;
161
+ return (
162
+ <UICard title={title} style={cardStyle}>
163
+ {loading && (
164
+ <div style={{ textAlign: 'center', padding: '40px 0' }}>
165
+ <UIText color="secondary">Loading chart data...</UIText>
166
+ </div>
167
+ )}
168
+
169
+ {!loading && chartData.length > 0 && (
170
+ <div>
171
+ {/* Custom Legend */}
172
+ <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 12, gap: 16 }}>
173
+ {metrics.map((metric, index) => (
174
+ <div key={metric.label} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
175
+ <span
176
+ style={{
177
+ display: 'inline-block',
178
+ width: 10,
179
+ height: 10,
180
+ borderRadius: '50%',
181
+ backgroundColor: chartColors[index % chartColors.length],
182
+ }}
183
+ />
184
+ <span style={{ fontSize: 13, fontWeight: 500 }}>{metric.label}</span>
185
+ </div>
186
+ ))}
187
+ </div>
188
+
189
+ <UIAreaChart
190
+ data={chartData}
191
+ xField="date"
192
+ yField="value"
193
+ colorField="category"
194
+ colors={chartColors}
195
+ height={height}
196
+ axis={{
197
+ x: {
198
+ label: {
199
+ autoRotate: false,
200
+ },
201
+ },
202
+ y: {
203
+ label: {
204
+ formatter: (v: string) => {
205
+ const num = Number(v);
206
+ if (num >= 1000000) {return `${(num / 1000000).toFixed(1)}M`;}
207
+ if (num >= 1000) {return `${(num / 1000).toFixed(1)}K`;}
208
+ return num.toLocaleString();
209
+ },
210
+ },
211
+ },
212
+ }}
213
+ style={{
214
+ fillOpacity: 0.3,
215
+ }}
216
+ legend={false}
217
+ tooltip={{
218
+ title: (d: any) => d.date,
219
+ items: [
220
+ (d: any) => {
221
+ const metric = metrics.find((m) => m.label === d.category);
222
+ const metricIndex = metrics.findIndex((m) => m.label === d.category);
223
+ let formattedValue = d.value?.toLocaleString() || '0';
224
+
225
+ if (metric?.format && d.value != null) {
226
+ formattedValue = metric.format(d.value);
227
+ }
228
+
229
+ const tooltipColor = chartColors[metricIndex >= 0 ? metricIndex : 0];
230
+
231
+ console.log('[Tooltip] Color:', {
232
+ category: d.category,
233
+ metricIndex,
234
+ tooltipColor,
235
+ chartColors,
236
+ });
237
+
238
+ return {
239
+ name: d.category,
240
+ value: formattedValue,
241
+ color: tooltipColor,
242
+ };
243
+ },
244
+ ],
245
+ }}
246
+ />
247
+ </div>
248
+ )}
249
+
250
+ {!loading && chartData.length === 0 && (
251
+ <div style={{ textAlign: 'center', padding: '40px 0' }}>
252
+ <UIText color="secondary">No data available for the selected date range</UIText>
253
+ </div>
254
+ )}
255
+ </UICard>
256
+ );
257
+ }