@kb-labs/studio-app 0.7.0 → 2.8.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 (175) hide show
  1. package/dist/index.html +1 -0
  2. package/dist/main.1d68e922.js +522 -0
  3. package/dist/main.1d68e922.js.map +1 -0
  4. package/dist/main.80b70197.css +2 -0
  5. package/dist/main.80b70197.css.map +1 -0
  6. package/dist/mf-manifest.json +241 -0
  7. package/dist/mf-stats.json +271 -0
  8. package/package.json +56 -39
  9. package/server.js +68 -0
  10. package/.env.example +0 -10
  11. package/UI-KIT-COMPONENTS.md +0 -783
  12. package/UI-KIT-PLAN.md +0 -326
  13. package/eslint-rules/no-direct-antd-import.js +0 -151
  14. package/eslint.config.ts +0 -27
  15. package/index.html +0 -17
  16. package/postcss.config.js +0 -7
  17. package/public/favicon.ico +0 -0
  18. package/rspack.config.mjs +0 -118
  19. package/src/App.tsx +0 -46
  20. package/src/components/devtools/devtools-log-feed.module.css +0 -165
  21. package/src/components/devtools/devtools-log-feed.tsx +0 -300
  22. package/src/components/devtools/devtools-panel.module.css +0 -121
  23. package/src/components/devtools/devtools-panel.tsx +0 -115
  24. package/src/components/empty-state.tsx +0 -28
  25. package/src/components/error-boundary.tsx +0 -210
  26. package/src/components/health-banner.module.css +0 -103
  27. package/src/components/health-banner.tsx +0 -134
  28. package/src/components/health-indicator.tsx +0 -12
  29. package/src/components/page-header.tsx +0 -49
  30. package/src/components/page-transition.tsx +0 -24
  31. package/src/components/role-switcher.tsx +0 -148
  32. package/src/components/run-badge.tsx +0 -13
  33. package/src/components/ui/icons.tsx +0 -509
  34. package/src/components/ui/index.ts +0 -25
  35. package/src/components/ui/kb-config-provider.tsx +0 -130
  36. package/src/components/ui/kb-header.module.css +0 -51
  37. package/src/components/ui/kb-header.tsx +0 -112
  38. package/src/components/ui/kb-notification-bell.module.css +0 -103
  39. package/src/components/ui/kb-notification-bell.tsx +0 -199
  40. package/src/components/ui/kb-page-layout.tsx +0 -66
  41. package/src/components/ui/kb-quick-search.module.css +0 -86
  42. package/src/components/ui/kb-quick-search.tsx +0 -196
  43. package/src/components/ui/kb-sidebar.module.css +0 -47
  44. package/src/components/ui/kb-sidebar.tsx +0 -196
  45. package/src/components/ui/kb-stat-card.module.css +0 -21
  46. package/src/components/ui/kb-stat-card.tsx +0 -22
  47. package/src/components/ui/kb-stat-grid.tsx +0 -22
  48. package/src/components/ui/kb-status-bar.module.css +0 -49
  49. package/src/components/ui/kb-status-bar.tsx +0 -145
  50. package/src/components/ui/kb-system-health-indicator.tsx +0 -229
  51. package/src/components/ui/kb-theme-toggle.tsx +0 -42
  52. package/src/components/ui/theme-adapter.ts +0 -812
  53. package/src/components/ui/theme-transition-overlay.tsx +0 -70
  54. package/src/components/ui/utils.ts +0 -6
  55. package/src/components/widget-error-boundary.tsx +0 -172
  56. package/src/config/env.ts +0 -5
  57. package/src/config/feature-flags.ts +0 -482
  58. package/src/config/studio.config.schema.ts +0 -88
  59. package/src/config/studio.config.ts +0 -84
  60. package/src/env.d.ts +0 -23
  61. package/src/hooks/use-command-palette.ts +0 -6
  62. package/src/hooks/use-devtools-store.ts +0 -25
  63. package/src/hooks/use-feature-flags.ts +0 -101
  64. package/src/index.css +0 -10
  65. package/src/layouts/dashboard-grid.tsx +0 -122
  66. package/src/layouts/index.ts +0 -12
  67. package/src/layouts/two-pane.tsx +0 -134
  68. package/src/main.tsx +0 -15
  69. package/src/modules/analytics/components/analytics-daily-stats-chart.tsx +0 -257
  70. package/src/modules/analytics/components/analytics-date-range-picker.tsx +0 -121
  71. package/src/modules/analytics/pages/analytics-cache-page.tsx +0 -285
  72. package/src/modules/analytics/pages/analytics-embeddings-page.tsx +0 -274
  73. package/src/modules/analytics/pages/analytics-events-page.tsx +0 -425
  74. package/src/modules/analytics/pages/analytics-llm-page.tsx +0 -434
  75. package/src/modules/analytics/pages/analytics-overview-page.tsx +0 -336
  76. package/src/modules/analytics/pages/analytics-page.tsx +0 -20
  77. package/src/modules/analytics/pages/analytics-storage-page.tsx +0 -344
  78. package/src/modules/analytics/pages/analytics-vectorstore-page.tsx +0 -279
  79. package/src/modules/analytics/routes/index.tsx +0 -133
  80. package/src/modules/analytics/utils/formatters.test.ts +0 -148
  81. package/src/modules/analytics/utils/formatters.ts +0 -100
  82. package/src/modules/auth/pages/login-page.module.css +0 -113
  83. package/src/modules/auth/pages/login-page.tsx +0 -80
  84. package/src/modules/auth/routes/index.tsx +0 -24
  85. package/src/modules/dashboard/components/ActivityTimelineWidget.tsx +0 -199
  86. package/src/modules/dashboard/components/AiInsightsWidget.tsx +0 -358
  87. package/src/modules/dashboard/components/AnimatedMetricCard.css +0 -88
  88. package/src/modules/dashboard/components/AnimatedMetricCard.tsx +0 -125
  89. package/src/modules/dashboard/components/HeroMetricCard.css +0 -114
  90. package/src/modules/dashboard/components/HeroMetricCard.tsx +0 -152
  91. package/src/modules/dashboard/components/HeroMetricsWidget.tsx +0 -146
  92. package/src/modules/dashboard/components/SystemResourcesWidget.tsx +0 -197
  93. package/src/modules/dashboard/components/dashboard-empty.tsx +0 -22
  94. package/src/modules/dashboard/components/dashboard-kpis-widget.tsx +0 -59
  95. package/src/modules/dashboard/components/dashboard-kpis.tsx +0 -13
  96. package/src/modules/dashboard/components/dashboard-latency-percentiles-widget.tsx +0 -85
  97. package/src/modules/dashboard/components/dashboard-metrics-widget.tsx +0 -78
  98. package/src/modules/dashboard/components/dashboard-plugins-widget.tsx +0 -67
  99. package/src/modules/dashboard/components/dashboard-requests-widget.tsx +0 -62
  100. package/src/modules/dashboard/layouts/dashboard-layout.tsx +0 -16
  101. package/src/modules/dashboard/pages/ai-insights-page.tsx +0 -563
  102. package/src/modules/dashboard/pages/dashboard-animations.css +0 -317
  103. package/src/modules/dashboard/pages/dashboard-page.tsx +0 -13
  104. package/src/modules/dashboard/routes/index.tsx +0 -70
  105. package/src/modules/observability/pages/devkit-page.tsx +0 -263
  106. package/src/modules/observability/pages/incident-detail-page.tsx +0 -571
  107. package/src/modules/observability/pages/incidents-page.tsx +0 -370
  108. package/src/modules/observability/pages/log-detail-page.tsx +0 -508
  109. package/src/modules/observability/pages/logs-page.tsx +0 -1129
  110. package/src/modules/observability/pages/prometheus-metrics-page.tsx +0 -486
  111. package/src/modules/observability/pages/state-broker-page.tsx +0 -229
  112. package/src/modules/observability/pages/system-events-page.tsx +0 -255
  113. package/src/modules/observability/routes/index.tsx +0 -133
  114. package/src/modules/plugins/components/marketplace-card.tsx +0 -153
  115. package/src/modules/plugins/components/plugin-ai-assistant-modal.tsx +0 -374
  116. package/src/modules/plugins/components/plugin-card.tsx +0 -136
  117. package/src/modules/plugins/components/plugin-detail/capabilities-summary.tsx +0 -101
  118. package/src/modules/plugins/components/plugin-detail/cli-commands-table.tsx +0 -101
  119. package/src/modules/plugins/components/plugin-detail/index.ts +0 -7
  120. package/src/modules/plugins/components/plugin-detail/jobs-table.tsx +0 -64
  121. package/src/modules/plugins/components/plugin-detail/permissions/env-permissions.tsx +0 -30
  122. package/src/modules/plugins/components/plugin-detail/permissions/filesystem-permissions.tsx +0 -68
  123. package/src/modules/plugins/components/plugin-detail/permissions/index.ts +0 -6
  124. package/src/modules/plugins/components/plugin-detail/permissions/network-permissions.tsx +0 -43
  125. package/src/modules/plugins/components/plugin-detail/permissions/platform-services.tsx +0 -242
  126. package/src/modules/plugins/components/plugin-detail/permissions/resource-quotas.tsx +0 -96
  127. package/src/modules/plugins/components/plugin-detail/permissions/shell-permissions.tsx +0 -55
  128. package/src/modules/plugins/components/plugin-detail/permissions-view.tsx +0 -47
  129. package/src/modules/plugins/components/plugin-detail/plugin-header.tsx +0 -169
  130. package/src/modules/plugins/components/plugin-detail/rest-routes-table.tsx +0 -159
  131. package/src/modules/plugins/components/plugin-detail/workflows-table.tsx +0 -40
  132. package/src/modules/plugins/components/plugin-grid.tsx +0 -20
  133. package/src/modules/plugins/components/plugin-table.tsx +0 -178
  134. package/src/modules/plugins/pages/marketplace-page.tsx +0 -200
  135. package/src/modules/plugins/pages/plugin-detail-page.tsx +0 -1145
  136. package/src/modules/plugins/routes/index.tsx +0 -48
  137. package/src/modules/plugins/utils/index.ts +0 -1
  138. package/src/modules/plugins/utils/schema-helpers.ts +0 -21
  139. package/src/modules/settings/components/api-routes-viewer.tsx +0 -415
  140. package/src/modules/settings/components/appearance-settings.tsx +0 -200
  141. package/src/modules/settings/components/configuration-settings.tsx +0 -321
  142. package/src/modules/settings/components/data-privacy-settings.tsx +0 -111
  143. package/src/modules/settings/components/experimental-settings.tsx +0 -243
  144. package/src/modules/settings/components/navigation-settings.tsx +0 -198
  145. package/src/modules/settings/components/notifications-settings.tsx +0 -40
  146. package/src/modules/settings/pages/settings-page.tsx +0 -298
  147. package/src/modules/settings/routes/index.tsx +0 -47
  148. package/src/pages/README-assistant.md +0 -91
  149. package/src/pages/login-page.tsx +0 -177
  150. package/src/pages/not-found-page.tsx +0 -85
  151. package/src/providers/auth-provider.tsx +0 -40
  152. package/src/providers/command-palette-provider.tsx +0 -193
  153. package/src/providers/data-sources-provider.tsx +0 -27
  154. package/src/providers/devtools-provider.tsx +0 -72
  155. package/src/providers/navigation-items-provider.tsx +0 -38
  156. package/src/providers/registry-v2-provider.tsx +0 -93
  157. package/src/providers/settings-provider.tsx +0 -158
  158. package/src/router.tsx +0 -302
  159. package/src/routes/helpers.tsx +0 -25
  160. package/src/routes/plugin-page-v2.tsx +0 -52
  161. package/src/routes/types.ts +0 -18
  162. package/src/styles/antd-overrides.css +0 -211
  163. package/src/styles/base.css +0 -190
  164. package/src/styles/settings.css +0 -75
  165. package/src/styles/variables.css +0 -94
  166. package/src/utils/analytics.ts +0 -183
  167. package/src/utils/apply-navigation-categories.ts +0 -83
  168. package/src/utils/commands.ts +0 -252
  169. package/src/utils/export.ts +0 -174
  170. package/src/utils/format-date.ts +0 -166
  171. package/src/utils/logger.ts +0 -110
  172. package/tailwind.config.ts +0 -13
  173. package/tsconfig.json +0 -13
  174. package/vitest-setup.ts +0 -1
  175. /package/{public → dist}/favicon.svg +0 -0
@@ -1,1129 +0,0 @@
1
- import * as React from 'react';
2
- import {
3
- UICard,
4
- UIAlert,
5
- UIBadge,
6
- UITag,
7
- UIList,
8
- UIListItem,
9
- UITypographyText,
10
- UITypographyParagraph,
11
- UISpace,
12
- UIRow,
13
- UICol,
14
- UISelect,
15
- UIButton,
16
- UIAccordion,
17
- UIDescriptions,
18
- UIDescriptionsItem,
19
- UIMessage,
20
- UIModal,
21
- UIInput,
22
- UISegmented,
23
- UIInputTextArea,
24
- UIDivider,
25
- UISpin,
26
- UIIcon,
27
- } from '@kb-labs/studio-ui-kit';
28
- import { UIRangePicker, UICheckbox } from '@kb-labs/studio-ui-kit';
29
- import { useState, useMemo, useEffect } from 'react';
30
- import { useLogStream } from '@kb-labs/studio-data-client';
31
- import { useDataSources } from '../../../providers/data-sources-provider';
32
- import type { LogRecord, LogSummarizeResponse } from '@kb-labs/studio-data-client';
33
- import type { Dayjs } from 'dayjs';
34
- import dayjs from 'dayjs';
35
- import { UIPage, UIPageHeader } from '@kb-labs/studio-ui-kit';
36
-
37
-
38
- /**
39
- * Time range presets for quick filtering
40
- */
41
- const TIME_RANGES = {
42
- '5m': { label: 'Last 5 minutes', minutes: 5 },
43
- '15m': { label: 'Last 15 minutes', minutes: 15 },
44
- '1h': { label: 'Last hour', minutes: 60 },
45
- '6h': { label: 'Last 6 hours', minutes: 360 },
46
- '24h': { label: 'Last 24 hours', minutes: 1440 },
47
- custom: { label: 'Custom range', minutes: 0 },
48
- };
49
-
50
- /**
51
- * Quick question templates for AI summarization
52
- */
53
- const QUESTION_TEMPLATES = [
54
- { label: 'What happened?', value: 'What happened in these logs?' },
55
- { label: 'Show errors', value: 'What errors occurred and what caused them?' },
56
- { label: 'Find patterns', value: 'Are there any patterns or repeated issues?' },
57
- { label: 'Performance issues', value: 'Are there any performance or latency issues?' },
58
- { label: 'Timeline', value: 'Give me a timeline of key events' },
59
- { label: 'Root cause', value: 'What is the root cause of the errors?' },
60
- ];
61
-
62
- /**
63
- * Format timestamp to time string
64
- */
65
- function formatTime(timestamp: string): string {
66
- return new Date(timestamp).toLocaleTimeString();
67
- }
68
-
69
- /**
70
- * Group logs by a field
71
- */
72
- function groupLogsByField(logs: LogRecord[], field: keyof LogRecord): Map<string, LogRecord[]> {
73
- const groups = new Map<string, LogRecord[]>();
74
- for (const log of logs) {
75
- const key = String(log[field] || 'unknown');
76
- if (!groups.has(key)) {
77
- groups.set(key, []);
78
- }
79
- groups.get(key)!.push(log);
80
- }
81
- return groups;
82
- }
83
-
84
- /**
85
- * Calculate log statistics for AI summarization
86
- */
87
- function calculateLogStats(logs: LogRecord[]) {
88
- const stats = {
89
- total: logs.length,
90
- byLevel: {} as Record<string, number>,
91
- byPlugin: {} as Record<string, number>,
92
- errorCount: 0,
93
- warningCount: 0,
94
- uniqueTraces: new Set<string>(),
95
- uniqueExecutions: new Set<string>(),
96
- timeRange: {
97
- oldest: logs.length > 0 ? logs[0]!.time : null,
98
- newest: logs.length > 0 ? logs[logs.length - 1]!.time : null,
99
- },
100
- };
101
-
102
- for (const log of logs) {
103
- // Count by level
104
- stats.byLevel[log.level] = (stats.byLevel[log.level] || 0) + 1;
105
-
106
- // Count by plugin
107
- if (log.plugin) {
108
- stats.byPlugin[log.plugin] = (stats.byPlugin[log.plugin] || 0) + 1;
109
- }
110
-
111
- // Track errors and warnings
112
- if (log.level === 'error') {stats.errorCount++;}
113
- if (log.level === 'warn') {stats.warningCount++;}
114
-
115
- // Track unique traces and executions
116
- if (log.trace) {stats.uniqueTraces.add(log.trace);}
117
- if (log.executionId) {stats.uniqueExecutions.add(log.executionId);}
118
- }
119
-
120
- return stats;
121
- }
122
-
123
- /**
124
- * Live Logs Page with AI Summarization support
125
- *
126
- * Features:
127
- * - Real-time log streaming via SSE
128
- * - Advanced filtering (time, level, plugin, trace)
129
- * - Log grouping (by trace, execution, plugin)
130
- * - Expandable log details
131
- * - Copy traceId/executionId
132
- * - Statistics for AI summarization
133
- */
134
- export function LogsPage() {
135
- const sources = useDataSources();
136
- const { logs: streamLogs, isConnected, error, clearLogs } = useLogStream(sources.observability);
137
- const [viewMode, setViewMode] = useState<'live' | 'history'>('live');
138
- const [searchQuery, setSearchQuery] = useState('');
139
- const [historyRefreshToken, setHistoryRefreshToken] = useState(0);
140
- const [historyPage, setHistoryPage] = useState(1);
141
- const [historyPageSize, setHistoryPageSize] = useState(50);
142
- const [historyLogs, setHistoryLogs] = useState<LogRecord[]>([]);
143
- const [historyTotal, setHistoryTotal] = useState(0);
144
- const [historyLoading, setHistoryLoading] = useState(false);
145
- const [historyError, setHistoryError] = useState<Error | null>(null);
146
- const [historySource, setHistorySource] = useState<string | undefined>(undefined);
147
- const [historyStats, setHistoryStats] = useState<any>(null);
148
-
149
- // Pause/Resume state
150
- const [isPaused, setIsPaused] = useState(false);
151
- const [frozenLogs, setFrozenLogs] = useState<LogRecord[]>([]);
152
-
153
- // Update frozen logs when not paused
154
- useEffect(() => {
155
- if (!isPaused) {
156
- setFrozenLogs(streamLogs);
157
- }
158
- }, [streamLogs, isPaused]);
159
-
160
- // Filters
161
- const [levelFilter, setLevelFilter] = useState<string | null>(null);
162
- const [pluginFilter, setPluginFilter] = useState<string | null>(null);
163
- const [timeRange, setTimeRange] = useState<string>('15m');
164
- const [customTimeRange, setCustomTimeRange] = useState<[Dayjs, Dayjs] | null>(null);
165
- const [groupBy, setGroupBy] = useState<'none' | 'trace' | 'execution' | 'plugin'>('none');
166
-
167
- // AI Summarize Modal
168
- const [summarizeModalOpen, setSummarizeModalOpen] = useState(false);
169
- const [customQuestion, setCustomQuestion] = useState('');
170
- const [includeContext, setIncludeContext] = useState({
171
- errors: true,
172
- warnings: true,
173
- info: false,
174
- metadata: true,
175
- stackTraces: true,
176
- });
177
- const [summarizing, setSummarizing] = useState(false);
178
- const [summaryResult, setSummaryResult] = useState<LogSummarizeResponse['data'] | null>(null);
179
-
180
- useEffect(() => {
181
- if (viewMode !== 'history') {return;}
182
- setHistoryPage(1);
183
- }, [viewMode, levelFilter, pluginFilter, timeRange, customTimeRange, searchQuery]);
184
-
185
- // Query logs history from backend with server-side filtering/pagination/search
186
- useEffect(() => {
187
- if (viewMode !== 'history') {return;}
188
-
189
- let cancelled = false;
190
-
191
- const fetchHistory = async () => {
192
- setHistoryLoading(true);
193
- setHistoryError(null);
194
-
195
- try {
196
- const query: any = {
197
- limit: historyPageSize,
198
- offset: (historyPage - 1) * historyPageSize,
199
- };
200
-
201
- if (levelFilter) {
202
- query.level = levelFilter;
203
- }
204
- if (pluginFilter) {
205
- query.plugin = pluginFilter;
206
- }
207
- if (searchQuery.trim()) {
208
- query.search = searchQuery.trim();
209
- }
210
-
211
- if (timeRange !== 'custom') {
212
- const rangeMinutes = TIME_RANGES[timeRange as keyof typeof TIME_RANGES]?.minutes || 15;
213
- query.from = dayjs().subtract(rangeMinutes, 'minutes').toISOString();
214
- query.to = dayjs().toISOString();
215
- } else if (customTimeRange) {
216
- query.from = customTimeRange[0].toISOString();
217
- query.to = customTimeRange[1].toISOString();
218
- }
219
-
220
- const result: any = await sources.observability.queryLogs(query);
221
- const data = result?.logs ? result : result?.data ?? {};
222
-
223
- if (cancelled) {return;}
224
-
225
- const logs = Array.isArray(data.logs) ? data.logs : [];
226
- setHistoryLogs(logs);
227
- setHistoryTotal(typeof data.total === 'number' ? data.total : logs.length);
228
- setHistorySource(typeof data.source === 'string' ? data.source : undefined);
229
- setHistoryStats(data.stats ?? null);
230
- } catch (err: any) {
231
- if (cancelled) {return;}
232
- setHistoryError(err instanceof Error ? err : new Error(String(err)));
233
- setHistoryLogs([]);
234
- setHistoryTotal(0);
235
- } finally {
236
- if (!cancelled) {
237
- setHistoryLoading(false);
238
- }
239
- }
240
- };
241
-
242
- void fetchHistory();
243
-
244
- return () => {
245
- cancelled = true;
246
- };
247
- }, [
248
- viewMode,
249
- historyPage,
250
- historyPageSize,
251
- levelFilter,
252
- pluginFilter,
253
- timeRange,
254
- customTimeRange,
255
- searchQuery,
256
- historyRefreshToken,
257
- sources.observability,
258
- ]);
259
-
260
- // Calculate filtered logs
261
- const logs = viewMode === 'history' ? historyLogs : (isPaused ? frozenLogs : streamLogs);
262
-
263
- const filteredLogs = useMemo(() => {
264
- let filtered = [...logs];
265
-
266
- // In history mode level/plugin/time/search are applied on backend.
267
- // In live mode apply all filters client-side.
268
- if (viewMode !== 'history') {
269
- if (levelFilter) {
270
- filtered = filtered.filter((log) => log.level === levelFilter);
271
- }
272
-
273
- if (pluginFilter) {
274
- filtered = filtered.filter((log) => log.plugin === pluginFilter);
275
- }
276
-
277
- if (timeRange !== 'custom') {
278
- const rangeMinutes = TIME_RANGES[timeRange as keyof typeof TIME_RANGES]?.minutes || 15;
279
- const cutoff = Date.now() - rangeMinutes * 60 * 1000;
280
- filtered = filtered.filter((log) => new Date(log.time).getTime() >= cutoff);
281
- } else if (customTimeRange) {
282
- const [start, end] = customTimeRange;
283
- filtered = filtered.filter((log) => {
284
- const logTime = dayjs(log.time);
285
- return logTime.isAfter(start) && logTime.isBefore(end);
286
- });
287
- }
288
-
289
- if (searchQuery.trim()) {
290
- const query = searchQuery.trim().toLowerCase();
291
- filtered = filtered.filter((log) => {
292
- const haystack = [
293
- String(log.msg ?? ''),
294
- String(log.plugin ?? ''),
295
- String(log.trace ?? (log as any).traceId ?? ''),
296
- String(log.executionId ?? ''),
297
- JSON.stringify(log.meta ?? {}),
298
- ].join(' ').toLowerCase();
299
- return haystack.includes(query);
300
- });
301
- }
302
- }
303
-
304
- return filtered;
305
- }, [logs, viewMode, levelFilter, pluginFilter, timeRange, customTimeRange, searchQuery]);
306
-
307
- // Calculate statistics (for AI summarization)
308
- const stats = useMemo(() => calculateLogStats(filteredLogs), [filteredLogs]);
309
-
310
- // Get unique plugins for filter dropdown
311
- const uniquePlugins = useMemo(() => {
312
- return Array.from(new Set([...streamLogs, ...historyLogs].map((log) => log.plugin).filter(Boolean)));
313
- }, [streamLogs, historyLogs]);
314
-
315
- // Group logs if needed
316
- const groupedLogs = useMemo(() => {
317
- if (groupBy === 'none') {return null;}
318
-
319
- switch (groupBy) {
320
- case 'trace':
321
- return groupLogsByField(filteredLogs, 'traceId');
322
- case 'execution':
323
- return groupLogsByField(filteredLogs, 'executionId');
324
- case 'plugin':
325
- return groupLogsByField(filteredLogs, 'plugin');
326
- default:
327
- return null;
328
- }
329
- }, [filteredLogs, groupBy]);
330
-
331
- /**
332
- * Render single log item with expandable details
333
- */
334
- const renderLogItem = (log: LogRecord, index: number) => {
335
- // Build clean JSON representation for expansion
336
- const logData = { ...log };
337
-
338
- // Safe JSON stringify (handles circular refs and errors)
339
- let jsonString: string;
340
- try {
341
- jsonString = JSON.stringify(logData, null, 2);
342
- } catch {
343
- // Fallback if JSON.stringify fails (circular refs, etc.)
344
- jsonString = JSON.stringify(
345
- {
346
- ...logData,
347
- err: logData.err ? {
348
- name: String(logData.err.name || 'Error'),
349
- message: String(logData.err.message || ''),
350
- stack: String(logData.err.stack || '').split('\n').slice(0, 10).join('\n'),
351
- } : undefined,
352
- },
353
- null,
354
- 2
355
- );
356
- }
357
-
358
- const labelText = [
359
- formatTime(log.time),
360
- `[${log.level.toUpperCase()}]`,
361
- log.plugin ? `[${log.plugin}]` : '',
362
- String(log.msg || '(no message)'),
363
- log.err ? `(${String(log.err.name || 'Error')})` : '',
364
- ].filter(Boolean).join(' ');
365
-
366
- return (
367
- <UIAccordion
368
- ghost
369
- key={`${log.time}-${index}`}
370
- items={[
371
- {
372
- key: String(index),
373
- label: labelText,
374
- children: (
375
- <div style={{ backgroundColor: '#fafafa', padding: 16, borderRadius: 4, marginLeft: -24, marginRight: -24 }}>
376
- <UITypographyParagraph
377
- copyable={{ text: jsonString }}
378
- style={{
379
- marginBottom: 0,
380
- fontSize: 12,
381
- fontFamily: 'monospace',
382
- whiteSpace: 'pre-wrap',
383
- maxHeight: 400,
384
- overflow: 'auto',
385
- }}
386
- >
387
- {jsonString}
388
- </UITypographyParagraph>
389
- </div>
390
- ),
391
- },
392
- ]}
393
- />
394
- );
395
- };
396
-
397
- /**
398
- * Render grouped logs
399
- */
400
- const renderGroupedLogs = () => {
401
- if (!groupedLogs) {return null;}
402
-
403
- const groups = Array.from(groupedLogs.entries());
404
-
405
- return (
406
- <UIAccordion
407
- items={groups.map(([key, groupLogs]) => ({
408
- key,
409
- label: `${key} (${groupLogs.length} logs)`,
410
- children: (
411
- <UIList
412
- dataSource={groupLogs}
413
- renderItem={(log, index) => (
414
- <UIListItem style={{ padding: '8px 0' }}>{renderLogItem(log, index)}</UIListItem>
415
- )}
416
- />
417
- ),
418
- }))}
419
- />
420
- );
421
- };
422
-
423
- /**
424
- * Handle AI Summarization
425
- */
426
- const handleSummarize = async () => {
427
- setSummarizing(true);
428
- setSummaryResult(null);
429
-
430
- try {
431
- // Build request
432
- const request = {
433
- question: customQuestion,
434
- includeContext,
435
- timeRange:
436
- timeRange === 'custom' && customTimeRange
437
- ? {
438
- from: customTimeRange[0].toISOString(),
439
- to: customTimeRange[1].toISOString(),
440
- }
441
- : timeRange !== 'custom'
442
- ? {
443
- from: dayjs()
444
- .subtract(TIME_RANGES[timeRange as keyof typeof TIME_RANGES].minutes, 'minutes')
445
- .toISOString(),
446
- to: dayjs().toISOString(),
447
- }
448
- : undefined,
449
- filters: {
450
- level: levelFilter || undefined,
451
- plugin: pluginFilter || undefined,
452
- },
453
- groupBy: groupBy !== 'none' ? groupBy : undefined,
454
- };
455
-
456
- // Call API through data source
457
- // Note: HttpClient auto-unwraps { ok: true, data: {...} } envelope
458
- const result = await sources.observability.summarizeLogs(request);
459
-
460
- // Store the full result data for structured rendering
461
- setSummaryResult(result.data);
462
- } catch (err: any) {
463
- UIMessage.error('Failed to generate summary: ' + err.message);
464
- } finally {
465
- setSummarizing(false);
466
- }
467
- };
468
-
469
- /**
470
- * Open AI Summarize modal
471
- */
472
- const openSummarizeModal = () => {
473
- setSummarizeModalOpen(true);
474
- setSummaryResult(null);
475
- setCustomQuestion('');
476
- };
477
-
478
- return (
479
- <UIPage width="full">
480
- <UIPageHeader
481
- title="Live Logs"
482
- description="Real-time application logs with AI-ready filtering and grouping"
483
- />
484
-
485
- {/* Connection Status */}
486
- {!isConnected && !error && viewMode === 'live' && (
487
- <UIAlert
488
- message="Connecting to log stream..."
489
- variant="info"
490
- showIcon
491
- icon={<UIIcon name="SyncOutlined" spin />}
492
- style={{ marginBottom: 24 }}
493
- />
494
- )}
495
-
496
- {error && viewMode === 'live' && (
497
- <UIAlert
498
- message="Connection failed"
499
- description={error.message + ' - Make sure REST API is running on localhost:5050'}
500
- variant="error"
501
- showIcon
502
- style={{ marginBottom: 24 }}
503
- />
504
- )}
505
-
506
- {isConnected && viewMode === 'live' && (
507
- <UIAlert
508
- message="Connected to log stream"
509
- variant="success"
510
- showIcon
511
- icon={<UIIcon name="CheckCircleOutlined" />}
512
- style={{ marginBottom: 24 }}
513
- />
514
- )}
515
-
516
- {viewMode === 'history' && historyError && (
517
- <UIAlert
518
- message="Failed to load historical logs"
519
- description={historyError.message}
520
- variant="error"
521
- showIcon
522
- style={{ marginBottom: 16 }}
523
- />
524
- )}
525
-
526
- {viewMode === 'history' && (
527
- <UIAlert
528
- message={`Historical logs: ${historyTotal} total`}
529
- description={
530
- historySource
531
- ? `Source: ${historySource}${historyStats?.persistence ? `, persisted logs: ${historyStats.persistence.totalLogs}` : ''}`
532
- : 'Showing stored logs with server-side filtering and search'
533
- }
534
- variant="info"
535
- showIcon
536
- style={{ marginBottom: 16 }}
537
- />
538
- )}
539
-
540
- {/* Filters & Stats (Collapsible) */}
541
- <UICard style={{ marginBottom: 16 }}>
542
- <UIAccordion
543
- defaultActiveKey={['filters']}
544
- ghost
545
- items={[
546
- {
547
- key: 'filters',
548
- label: `Filters & Statistics (${
549
- viewMode === 'history'
550
- ? `${filteredLogs.length}/${historyTotal}`
551
- : filteredLogs.length
552
- } logs)`,
553
- children: (
554
- <>
555
- <UIRow gutter={[16, 16]} style={{ marginBottom: 12 }}>
556
- <UICol span={6}>
557
- <UISpace direction="vertical" style={{ width: '100%' }}>
558
- <UITypographyText type="secondary" style={{ fontSize: 12 }}>
559
- Mode
560
- </UITypographyText>
561
- <UISegmented
562
- value={viewMode}
563
- onChange={(value) => setViewMode(value as 'live' | 'history')}
564
- options={[
565
- { label: 'Live', value: 'live' },
566
- { label: 'History', value: 'history' },
567
- ]}
568
- />
569
- </UISpace>
570
- </UICol>
571
-
572
- <UICol span={18}>
573
- <UISpace direction="vertical" style={{ width: '100%' }}>
574
- <UITypographyText type="secondary" style={{ fontSize: 12 }}>
575
- <UIIcon name="SearchOutlined" /> Search
576
- </UITypographyText>
577
- <UIInput
578
- value={searchQuery}
579
- onChange={(value: string) => setSearchQuery(value)}
580
- placeholder={viewMode === 'history'
581
- ? 'Search in history by message'
582
- : 'Search in live list'}
583
- allowClear
584
- />
585
- </UISpace>
586
- </UICol>
587
- </UIRow>
588
-
589
- <UIRow gutter={[16, 16]}>
590
- <UICol span={6}>
591
- <UISpace direction="vertical" style={{ width: '100%' }}>
592
- <UITypographyText type="secondary" style={{ fontSize: 12 }}>
593
- <UIIcon name="FilterOutlined" /> Level
594
- </UITypographyText>
595
- <UISelect
596
- style={{ width: '100%' }}
597
- placeholder="All levels"
598
- allowClear
599
- value={levelFilter ?? undefined}
600
- onChange={(v) => setLevelFilter(v as string | null)}
601
- options={[
602
- { label: 'Trace', value: 'trace' },
603
- { label: 'Debug', value: 'debug' },
604
- { label: 'Info', value: 'info' },
605
- { label: 'Warning', value: 'warn' },
606
- { label: 'Error', value: 'error' },
607
- ]}
608
- />
609
- </UISpace>
610
- </UICol>
611
-
612
- <UICol span={6}>
613
- <UISpace direction="vertical" style={{ width: '100%' }}>
614
- <UITypographyText type="secondary" style={{ fontSize: 12 }}>
615
- <UIIcon name="FilterOutlined" /> Plugin
616
- </UITypographyText>
617
- <UISelect
618
- style={{ width: '100%' }}
619
- placeholder="All plugins"
620
- allowClear
621
- value={pluginFilter ?? undefined}
622
- onChange={(v) => setPluginFilter(v as string | null)}
623
- options={uniquePlugins.map((plugin) => ({ label: String(plugin), value: String(plugin) }))}
624
- />
625
- </UISpace>
626
- </UICol>
627
-
628
- <UICol span={6}>
629
- <UISpace direction="vertical" style={{ width: '100%' }}>
630
- <UITypographyText type="secondary" style={{ fontSize: 12 }}>
631
- <UIIcon name="ClockCircleOutlined" /> Time Range
632
- </UITypographyText>
633
- <UISelect
634
- style={{ width: '100%' }}
635
- value={timeRange}
636
- onChange={(v) => setTimeRange(v as string)}
637
- options={Object.entries(TIME_RANGES).map(([key, { label }]) => ({
638
- label,
639
- value: key,
640
- }))}
641
- />
642
- </UISpace>
643
- </UICol>
644
-
645
- <UICol span={6}>
646
- <UISpace direction="vertical" style={{ width: '100%' }}>
647
- <UITypographyText type="secondary" style={{ fontSize: 12 }}>
648
- <UIIcon name="GroupOutlined" /> Group By
649
- </UITypographyText>
650
- <UISelect
651
- style={{ width: '100%' }}
652
- value={groupBy}
653
- onChange={(v) => setGroupBy(v as 'none' | 'trace' | 'plugin' | 'execution')}
654
- options={[
655
- { label: 'No grouping', value: 'none' },
656
- { label: 'Trace ID', value: 'trace' },
657
- { label: 'Execution ID', value: 'execution' },
658
- { label: 'Plugin', value: 'plugin' },
659
- ]}
660
- />
661
- </UISpace>
662
- </UICol>
663
-
664
- {timeRange === 'custom' && (
665
- <UICol span={24}>
666
- <UISpace direction="vertical" style={{ width: '100%' }}>
667
- <UITypographyText type="secondary" style={{ fontSize: 12 }}>
668
- Custom Time Range
669
- </UITypographyText>
670
- <UIRangePicker
671
- showTime
672
- style={{ width: '100%' }}
673
- onChange={(dates) =>
674
- setCustomTimeRange(dates as [Dayjs, Dayjs] | null)
675
- }
676
- />
677
- </UISpace>
678
- </UICol>
679
- )}
680
- </UIRow>
681
-
682
- {/* Statistics (for AI Summarization) */}
683
- <UIRow
684
- gutter={16}
685
- style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid #f0f0f0' }}
686
- >
687
- <UICol span={4}>
688
- <UITypographyText type="secondary" style={{ fontSize: 12 }}>
689
- Total Logs
690
- </UITypographyText>
691
- <div>
692
- <UITypographyText strong style={{ fontSize: 20 }}>
693
- {stats.total}
694
- </UITypographyText>
695
- </div>
696
- </UICol>
697
- <UICol span={4}>
698
- <UITypographyText type="secondary" style={{ fontSize: 12 }}>
699
- Errors
700
- </UITypographyText>
701
- <div>
702
- <UITypographyText type="danger" strong style={{ fontSize: 20 }}>
703
- {stats.errorCount}
704
- </UITypographyText>
705
- </div>
706
- </UICol>
707
- <UICol span={4}>
708
- <UITypographyText type="secondary" style={{ fontSize: 12 }}>
709
- Warnings
710
- </UITypographyText>
711
- <div>
712
- <UITypographyText type="warning" strong style={{ fontSize: 20 }}>
713
- {stats.warningCount}
714
- </UITypographyText>
715
- </div>
716
- </UICol>
717
- <UICol span={4}>
718
- <UITypographyText type="secondary" style={{ fontSize: 12 }}>
719
- Unique Traces
720
- </UITypographyText>
721
- <div>
722
- <UITypographyText strong style={{ fontSize: 20 }}>
723
- {stats.uniqueTraces.size}
724
- </UITypographyText>
725
- </div>
726
- </UICol>
727
- <UICol span={4}>
728
- <UITypographyText type="secondary" style={{ fontSize: 12 }}>
729
- Unique Executions
730
- </UITypographyText>
731
- <div>
732
- <UITypographyText strong style={{ fontSize: 20 }}>
733
- {stats.uniqueExecutions.size}
734
- </UITypographyText>
735
- </div>
736
- </UICol>
737
- <UICol span={4}>
738
- <UIButton
739
- variant="primary"
740
- icon={<UIIcon name="RobotOutlined" />}
741
- onClick={openSummarizeModal}
742
- disabled={filteredLogs.length === 0}
743
- >
744
- AI Summarize
745
- </UIButton>
746
- </UICol>
747
- </UIRow>
748
- </>
749
- ),
750
- },
751
- ]}
752
- />
753
- </UICard>
754
-
755
- {/* Logs List */}
756
- <UICard
757
- title={
758
- <UISpace>
759
- <span>{
760
- groupBy === 'none'
761
- ? (viewMode === 'history' ? 'Historical Logs' : 'Recent Logs')
762
- : `Logs grouped by ${groupBy}`
763
- }</span>
764
- <UIBadge
765
- count={viewMode === 'history' ? historyTotal : filteredLogs.length}
766
- showZero
767
- style={{ backgroundColor: '#52c41a' }}
768
- />
769
- </UISpace>
770
- }
771
- extra={
772
- <UISpace>
773
- {viewMode === 'live' ? (
774
- <>
775
- <UIButton
776
- size="small"
777
- icon={isPaused ? <UIIcon name="SyncOutlined" /> : <UIIcon name="ClockCircleOutlined" />}
778
- onClick={() => setIsPaused(!isPaused)}
779
- variant={isPaused ? 'primary' : 'default'}
780
- >
781
- {isPaused ? 'Resume' : 'Pause'}
782
- </UIButton>
783
- <UIButton size="small" onClick={clearLogs}>
784
- Clear
785
- </UIButton>
786
- </>
787
- ) : (
788
- <UIButton
789
- size="small"
790
- icon={<UIIcon name="ReloadOutlined" />}
791
- loading={historyLoading}
792
- onClick={() => setHistoryRefreshToken((v) => v + 1)}
793
- >
794
- Refresh
795
- </UIButton>
796
- )}
797
- </UISpace>
798
- }
799
- >
800
- {viewMode === 'history' && historyLoading ? (
801
- <div style={{ textAlign: 'center', padding: '40px 0' }}>
802
- <UISpin tip="Loading historical logs..." />
803
- </div>
804
- ) : filteredLogs.length === 0 ? (
805
- <UIAlert
806
- message="No logs match current filters"
807
- description={
808
- viewMode === 'history'
809
- ? 'Adjust filters or search query for historical data'
810
- : 'Adjust filters or wait for new logs to arrive'
811
- }
812
- variant="info"
813
- showIcon
814
- />
815
- ) : groupBy === 'none' ? (
816
- <div style={{ maxHeight: 600, overflow: 'auto' }}>
817
- <UIList
818
- dataSource={filteredLogs}
819
- renderItem={(log, index) => (
820
- <UIListItem style={{ padding: '8px 0' }}>{renderLogItem(log, index)}</UIListItem>
821
- )}
822
- pagination={
823
- viewMode === 'history'
824
- ? {
825
- current: historyPage,
826
- total: historyTotal,
827
- pageSize: historyPageSize,
828
- showSizeChanger: true,
829
- pageSizeOptions: ['25', '50', '100', '200'],
830
- showTotal: (total: number, range: [number, number]) =>
831
- `Showing ${range[0]}-${range[1]} of ${total} logs`,
832
- onChange: (page: number, pageSize: number) => {
833
- if (pageSize !== historyPageSize) {
834
- setHistoryPageSize(pageSize);
835
- setHistoryPage(1);
836
- } else {
837
- setHistoryPage(page);
838
- }
839
- },
840
- }
841
- : filteredLogs.length > 50
842
- ? {
843
- pageSize: 50,
844
- showSizeChanger: true,
845
- showTotal: (total: number) => `Total ${total} logs`,
846
- pageSizeOptions: ['25', '50', '100', '200'],
847
- }
848
- : false
849
- }
850
- />
851
- </div>
852
- ) : (
853
- renderGroupedLogs()
854
- )}
855
- </UICard>
856
-
857
- {/* AI Summarize Modal */}
858
- <UIModal
859
- title={
860
- <UISpace>
861
- <UIIcon name="RobotOutlined" />
862
- <span>AI Log Summarization</span>
863
- </UISpace>
864
- }
865
- open={summarizeModalOpen}
866
- onCancel={() => setSummarizeModalOpen(false)}
867
- width={800}
868
- footer={[
869
- <UIButton key="cancel" onClick={() => setSummarizeModalOpen(false)}>
870
- Cancel
871
- </UIButton>,
872
- <UIButton
873
- key="summarize"
874
- variant="primary"
875
- icon={<UIIcon name="ThunderboltOutlined" />}
876
- loading={summarizing}
877
- onClick={handleSummarize}
878
- disabled={!customQuestion.trim()}
879
- >
880
- Generate Summary
881
- </UIButton>,
882
- ]}
883
- >
884
- <UISpace direction="vertical" style={{ width: '100%' }} size="large">
885
- {/* Data Preview */}
886
- <UICard size="small" style={{ backgroundColor: '#fafafa' }}>
887
- <UIRow gutter={16}>
888
- <UICol span={6}>
889
- <UITypographyText type="secondary">Logs to analyze:</UITypographyText>
890
- <div>
891
- <UITypographyText strong>{filteredLogs.length}</UITypographyText>
892
- </div>
893
- </UICol>
894
- <UICol span={6}>
895
- <UITypographyText type="secondary">Errors:</UITypographyText>
896
- <div>
897
- <UITypographyText type="danger" strong>
898
- {stats.errorCount}
899
- </UITypographyText>
900
- </div>
901
- </UICol>
902
- <UICol span={6}>
903
- <UITypographyText type="secondary">Warnings:</UITypographyText>
904
- <div>
905
- <UITypographyText type="warning" strong>
906
- {stats.warningCount}
907
- </UITypographyText>
908
- </div>
909
- </UICol>
910
- <UICol span={6}>
911
- <UITypographyText type="secondary">Time range:</UITypographyText>
912
- <div>
913
- <UITypographyText strong>{TIME_RANGES[timeRange as keyof typeof TIME_RANGES]?.label}</UITypographyText>
914
- </div>
915
- </UICol>
916
- </UIRow>
917
- </UICard>
918
-
919
- {/* Quick Templates */}
920
- <div>
921
- <UITypographyText strong>Quick Questions:</UITypographyText>
922
- <div style={{ marginTop: 8 }}>
923
- <UISpace wrap>
924
- {QUESTION_TEMPLATES.map((template) => (
925
- <UIButton
926
- key={template.label}
927
- size="small"
928
- onClick={() => setCustomQuestion(template.value)}
929
- >
930
- {template.label}
931
- </UIButton>
932
- ))}
933
- </UISpace>
934
- </div>
935
- </div>
936
-
937
- <UIDivider style={{ margin: '8px 0' }} />
938
-
939
- {/* Custom Question */}
940
- <div>
941
- <UITypographyText strong>Your Question:</UITypographyText>
942
- <UIInputTextArea
943
- value={customQuestion}
944
- onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setCustomQuestion(e.target.value)}
945
- placeholder="Ask anything about these logs... (e.g., 'What went wrong in the last 5 minutes?')"
946
- rows={3}
947
- style={{ marginTop: 8 }}
948
- />
949
- </div>
950
-
951
- {/* Context Options */}
952
- <div>
953
- <UITypographyText strong>Include in context:</UITypographyText>
954
- <div style={{ marginTop: 8 }}>
955
- <UISpace direction="vertical">
956
- <UICheckbox
957
- checked={includeContext.errors}
958
- onChange={(checked) =>
959
- setIncludeContext({ ...includeContext, errors: checked })
960
- }
961
- >
962
- Error messages and stack traces
963
- </UICheckbox>
964
- <UICheckbox
965
- checked={includeContext.warnings}
966
- onChange={(checked) =>
967
- setIncludeContext({ ...includeContext, warnings: checked })
968
- }
969
- >
970
- Warning messages
971
- </UICheckbox>
972
- <UICheckbox
973
- checked={includeContext.info}
974
- onChange={(checked) =>
975
- setIncludeContext({ ...includeContext, info: checked })
976
- }
977
- >
978
- Info-level logs
979
- </UICheckbox>
980
- <UICheckbox
981
- checked={includeContext.metadata}
982
- onChange={(checked) =>
983
- setIncludeContext({ ...includeContext, metadata: checked })
984
- }
985
- >
986
- Metadata (traceId, executionId, etc.)
987
- </UICheckbox>
988
- <UICheckbox
989
- checked={includeContext.stackTraces}
990
- onChange={(checked) =>
991
- setIncludeContext({ ...includeContext, stackTraces: checked })
992
- }
993
- >
994
- Full stack traces
995
- </UICheckbox>
996
- </UISpace>
997
- </div>
998
- </div>
999
-
1000
- {/* Summary Result */}
1001
- {summarizing && (
1002
- <div style={{ textAlign: 'center', padding: '24px 0' }}>
1003
- <UISpin tip="Analyzing logs with AI..." size="large" />
1004
- </div>
1005
- )}
1006
-
1007
- {summaryResult && !summarizing && (
1008
- <UISpace direction="vertical" size="middle" style={{ width: '100%', marginTop: 16 }}>
1009
- {/* Statistics Card */}
1010
- <UICard
1011
- title={
1012
- <UISpace>
1013
- <UIIcon name="InfoCircleOutlined" />
1014
- <span>Statistics</span>
1015
- </UISpace>
1016
- }
1017
- size="small"
1018
- >
1019
- <UIDescriptions column={2} size="small">
1020
- <UIDescriptionsItem label="Total Logs">{summaryResult.summary.total}</UIDescriptionsItem>
1021
- <UIDescriptionsItem label="Time Range">
1022
- {summaryResult.summary.timeRange.from && summaryResult.summary.timeRange.to
1023
- ? `${summaryResult.summary.timeRange.from} to ${summaryResult.summary.timeRange.to}`
1024
- : 'All time'}
1025
- </UIDescriptionsItem>
1026
- </UIDescriptions>
1027
-
1028
- {/* Level breakdown */}
1029
- {summaryResult.summary.stats?.byLevel && Object.keys(summaryResult.summary.stats.byLevel).length > 0 && (
1030
- <>
1031
- <UIDivider style={{ margin: '12px 0' }} />
1032
- <UITypographyText strong>By Level:</UITypographyText>
1033
- <div style={{ marginTop: 8 }}>
1034
- <UISpace wrap>
1035
- {Object.entries(summaryResult.summary.stats.byLevel).map(([level, count]) => (
1036
- <UITag key={level} color={
1037
- level === 'error' ? 'red' :
1038
- level === 'warn' ? 'orange' :
1039
- level === 'info' ? 'blue' :
1040
- 'default'
1041
- }>
1042
- {level}: {count}
1043
- </UITag>
1044
- ))}
1045
- </UISpace>
1046
- </div>
1047
- </>
1048
- )}
1049
-
1050
- {/* Plugin breakdown */}
1051
- {summaryResult.summary.stats?.byPlugin && Object.keys(summaryResult.summary.stats.byPlugin).length > 0 && (
1052
- <>
1053
- <UIDivider style={{ margin: '12px 0' }} />
1054
- <UITypographyText strong>By Plugin (Top 5):</UITypographyText>
1055
- <div style={{ marginTop: 8 }}>
1056
- <UISpace wrap>
1057
- {Object.entries(summaryResult.summary.stats.byPlugin)
1058
- .sort((a: any, b: any) => b[1] - a[1])
1059
- .slice(0, 5)
1060
- .map(([plugin, count]) => (
1061
- <UITag key={plugin}>{plugin}: {count}</UITag>
1062
- ))}
1063
- </UISpace>
1064
- </div>
1065
- </>
1066
- )}
1067
- </UICard>
1068
-
1069
- {/* Top Errors Card */}
1070
- {summaryResult.summary.stats?.topErrors && summaryResult.summary.stats.topErrors.length > 0 && (
1071
- <UICard
1072
- title={
1073
- <UISpace>
1074
- <UIIcon name="BugOutlined" />
1075
- <span>Top Errors</span>
1076
- </UISpace>
1077
- }
1078
- size="small"
1079
- >
1080
- <UIList
1081
- size="small"
1082
- dataSource={summaryResult.summary.stats.topErrors.slice(0, 5)}
1083
- renderItem={(err: any, idx: number) => (
1084
- <UIListItem>
1085
- <UITypographyText>
1086
- <UITypographyText type="secondary">{idx + 1}.</UITypographyText>{' '}
1087
- <UITypographyText code>{err.message}</UITypographyText>{' '}
1088
- <UITypographyText type="secondary">({err.count} times)</UITypographyText>
1089
- </UITypographyText>
1090
- </UIListItem>
1091
- )}
1092
- />
1093
- </UICard>
1094
- )}
1095
-
1096
- {/* AI Analysis Card */}
1097
- {summaryResult.aiSummary && (
1098
- <UIAlert
1099
- variant="info"
1100
- showIcon
1101
- icon={<UIIcon name="RobotOutlined" />}
1102
- message="AI Analysis"
1103
- description={
1104
- <UISpace direction="vertical" size="small" style={{ width: '100%' }}>
1105
- {summaryResult.aiSummary.split('\n\n').map((paragraph, idx) => (
1106
- <UITypographyParagraph key={idx} style={{ marginBottom: 0 }}>
1107
- {paragraph}
1108
- </UITypographyParagraph>
1109
- ))}
1110
- </UISpace>
1111
- }
1112
- />
1113
- )}
1114
-
1115
- {/* Info message if AI not available */}
1116
- {!summaryResult.aiSummary && summaryResult.message && (
1117
- <UIAlert
1118
- message={summaryResult.message}
1119
- variant="info"
1120
- showIcon
1121
- />
1122
- )}
1123
- </UISpace>
1124
- )}
1125
- </UISpace>
1126
- </UIModal>
1127
- </UIPage>
1128
- );
1129
- }