@kopai/ui 0.0.5 → 0.2.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 (125) hide show
  1. package/README.md +137 -0
  2. package/dist/index.cjs +5069 -3
  3. package/dist/index.d.cts +301 -3
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +302 -3
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +5010 -3
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +25 -7
  10. package/src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx +113 -0
  11. package/src/components/KeyboardShortcuts/ShortcutsHelpDialog.tsx +82 -0
  12. package/src/components/KeyboardShortcuts/context.ts +23 -0
  13. package/src/components/KeyboardShortcuts/index.ts +8 -0
  14. package/src/components/KeyboardShortcuts/types.ts +11 -0
  15. package/src/components/dashboard/Badge/Badge.stories.tsx +29 -0
  16. package/src/components/dashboard/Badge/index.tsx +32 -0
  17. package/src/components/dashboard/Button/Button.stories.tsx +107 -0
  18. package/src/components/dashboard/Button/index.tsx +63 -0
  19. package/src/components/dashboard/Card/Card.stories.tsx +81 -0
  20. package/src/components/dashboard/Card/index.tsx +58 -0
  21. package/src/components/dashboard/Chart/Chart.stories.tsx +48 -0
  22. package/src/components/dashboard/Chart/index.tsx +74 -0
  23. package/src/components/dashboard/DatePicker/DatePicker.stories.tsx +33 -0
  24. package/src/components/dashboard/DatePicker/index.tsx +41 -0
  25. package/src/components/dashboard/Divider/Divider.stories.tsx +17 -0
  26. package/src/components/dashboard/Divider/index.tsx +49 -0
  27. package/src/components/dashboard/Empty/Empty.stories.tsx +48 -0
  28. package/src/components/dashboard/Empty/index.tsx +46 -0
  29. package/src/components/dashboard/Grid/Grid.stories.tsx +52 -0
  30. package/src/components/dashboard/Grid/index.tsx +26 -0
  31. package/src/components/dashboard/Heading/Heading.stories.tsx +25 -0
  32. package/src/components/dashboard/Heading/index.tsx +27 -0
  33. package/src/components/dashboard/List/List.stories.tsx +37 -0
  34. package/src/components/dashboard/List/index.tsx +24 -0
  35. package/src/components/dashboard/Metric/Metric.stories.tsx +65 -0
  36. package/src/components/dashboard/Metric/index.tsx +36 -0
  37. package/src/components/dashboard/Stack/Stack.stories.tsx +61 -0
  38. package/src/components/dashboard/Stack/index.tsx +33 -0
  39. package/src/components/dashboard/Table/Table.stories.tsx +38 -0
  40. package/src/components/dashboard/Table/index.tsx +104 -0
  41. package/src/components/dashboard/Text/Text.stories.tsx +53 -0
  42. package/src/components/dashboard/Text/index.tsx +18 -0
  43. package/src/components/dashboard/index.ts +46 -0
  44. package/src/components/index.ts +17 -0
  45. package/src/components/observability/LogTimeline/LogDetailPane/AttributesTab.tsx +56 -0
  46. package/src/components/observability/LogTimeline/LogDetailPane/JsonTreeView.tsx +139 -0
  47. package/src/components/observability/LogTimeline/LogDetailPane/index.tsx +271 -0
  48. package/src/components/observability/LogTimeline/LogFilter.stories.tsx +66 -0
  49. package/src/components/observability/LogTimeline/LogFilter.test.tsx +696 -0
  50. package/src/components/observability/LogTimeline/LogFilter.tsx +674 -0
  51. package/src/components/observability/LogTimeline/LogRow.tsx +174 -0
  52. package/src/components/observability/LogTimeline/LogTimeline.stories.tsx +154 -0
  53. package/src/components/observability/LogTimeline/index.tsx +542 -0
  54. package/src/components/observability/LogTimeline/shortcuts.ts +18 -0
  55. package/src/components/observability/MetricHistogram/MetricHistogram.stories.tsx +20 -0
  56. package/src/components/observability/MetricHistogram/index.tsx +303 -0
  57. package/src/components/observability/MetricStat/MetricStat.stories.tsx +30 -0
  58. package/src/components/observability/MetricStat/index.tsx +281 -0
  59. package/src/components/observability/MetricTable/MetricTable.stories.tsx +20 -0
  60. package/src/components/observability/MetricTable/index.tsx +194 -0
  61. package/src/components/observability/MetricTimeSeries/MetricTimeSeries.stories.tsx +28 -0
  62. package/src/components/observability/MetricTimeSeries/index.tsx +462 -0
  63. package/src/components/observability/RawDataTable/RawDataTable.stories.tsx +27 -0
  64. package/src/components/observability/RawDataTable/index.tsx +131 -0
  65. package/src/components/observability/ServiceList/ServiceList.stories.tsx +20 -0
  66. package/src/components/observability/ServiceList/index.tsx +60 -0
  67. package/src/components/observability/ServiceList/shortcuts.ts +6 -0
  68. package/src/components/observability/TabBar/TabBar.stories.tsx +34 -0
  69. package/src/components/observability/TabBar/index.tsx +46 -0
  70. package/src/components/observability/TraceDetail/TraceDetail.stories.tsx +51 -0
  71. package/src/components/observability/TraceDetail/index.tsx +53 -0
  72. package/src/components/observability/TraceSearch/TraceSearch.stories.tsx +49 -0
  73. package/src/components/observability/TraceSearch/index.tsx +292 -0
  74. package/src/components/observability/TraceTimeline/DetailPane/AttributesTab.tsx +152 -0
  75. package/src/components/observability/TraceTimeline/DetailPane/EventsTab.tsx +128 -0
  76. package/src/components/observability/TraceTimeline/DetailPane/LinksTab.tsx +210 -0
  77. package/src/components/observability/TraceTimeline/DetailPane/index.tsx +174 -0
  78. package/src/components/observability/TraceTimeline/SpanRow.tsx +173 -0
  79. package/src/components/observability/TraceTimeline/TimelineBar.tsx +41 -0
  80. package/src/components/observability/TraceTimeline/Tooltip.tsx +42 -0
  81. package/src/components/observability/TraceTimeline/TraceHeader.tsx +88 -0
  82. package/src/components/observability/TraceTimeline/TraceTimeline.stories.tsx +25 -0
  83. package/src/components/observability/TraceTimeline/index.tsx +478 -0
  84. package/src/components/observability/TraceTimeline/shortcuts.ts +16 -0
  85. package/src/components/observability/__fixtures__/logs.ts +476 -0
  86. package/src/components/observability/__fixtures__/metrics.ts +216 -0
  87. package/src/components/observability/__fixtures__/raw-table.ts +204 -0
  88. package/src/components/observability/__fixtures__/services.ts +8 -0
  89. package/src/components/observability/__fixtures__/trace-summaries.ts +81 -0
  90. package/src/components/observability/__fixtures__/traces.ts +396 -0
  91. package/src/components/observability/index.ts +66 -0
  92. package/src/components/observability/renderers/OtelMetricDiscovery.tsx +77 -0
  93. package/src/components/observability/renderers/OtelMetricHistogram.tsx +29 -0
  94. package/src/components/observability/renderers/OtelMetricStat.tsx +44 -0
  95. package/src/components/observability/renderers/OtelMetricTable.tsx +29 -0
  96. package/src/components/observability/renderers/OtelMetricTimeSeries.tsx +30 -0
  97. package/src/components/observability/renderers/index.ts +5 -0
  98. package/src/components/observability/shared/LoadingSkeleton.tsx +43 -0
  99. package/src/components/observability/types.ts +113 -0
  100. package/src/components/observability/utils/attributes.ts +17 -0
  101. package/src/components/observability/utils/colors.ts +29 -0
  102. package/src/components/observability/utils/flatten-tree.ts +53 -0
  103. package/src/components/observability/utils/lttb.ts +121 -0
  104. package/src/components/observability/utils/time.ts +46 -0
  105. package/src/hooks/use-kopai-data.test.ts +296 -0
  106. package/src/hooks/use-kopai-data.ts +64 -0
  107. package/src/hooks/use-live-logs.test.ts +193 -0
  108. package/src/hooks/use-live-logs.ts +113 -0
  109. package/src/index.ts +15 -0
  110. package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +33 -0
  111. package/src/lib/catalog.ts +165 -0
  112. package/src/lib/component-catalog.test.ts +357 -0
  113. package/src/lib/component-catalog.ts +171 -0
  114. package/src/lib/dashboard-datasource.ts +76 -0
  115. package/src/lib/generate-prompt-instructions.test.ts +27 -0
  116. package/src/lib/generate-prompt-instructions.ts +185 -0
  117. package/src/lib/log-buffer.test.ts +88 -0
  118. package/src/lib/log-buffer.ts +62 -0
  119. package/src/lib/observability-catalog.ts +143 -0
  120. package/src/lib/renderer.test.tsx +693 -0
  121. package/src/lib/renderer.tsx +276 -0
  122. package/src/pages/observability.tsx +825 -0
  123. package/src/providers/kopai-provider.tsx +51 -0
  124. package/src/styles/globals.css +46 -0
  125. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,61 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Stack } from "./index.js";
3
+
4
+ const meta: Meta<typeof Stack> = {
5
+ title: "Dashboard/Stack",
6
+ component: Stack,
7
+ };
8
+ export default meta;
9
+ type Story = StoryObj<typeof Stack>;
10
+
11
+ const StackItem = ({ label }: { label: string }) => (
12
+ <div
13
+ style={{
14
+ padding: 12,
15
+ background: "hsl(var(--card))",
16
+ border: "1px solid hsl(var(--border))",
17
+ borderRadius: "var(--radius)",
18
+ }}
19
+ >
20
+ {label}
21
+ </div>
22
+ );
23
+
24
+ export const Vertical: Story = {
25
+ render: () => (
26
+ <Stack
27
+ element={{
28
+ props: { direction: "vertical", gap: "md", align: "stretch" },
29
+ }}
30
+ >
31
+ <StackItem label="Item 1" />
32
+ <StackItem label="Item 2" />
33
+ <StackItem label="Item 3" />
34
+ </Stack>
35
+ ),
36
+ };
37
+
38
+ export const Horizontal: Story = {
39
+ render: () => (
40
+ <Stack
41
+ element={{
42
+ props: { direction: "horizontal", gap: "md", align: "center" },
43
+ }}
44
+ >
45
+ <StackItem label="Item 1" />
46
+ <StackItem label="Item 2" />
47
+ <StackItem label="Item 3" />
48
+ </Stack>
49
+ ),
50
+ };
51
+
52
+ export const SmallGap: Story = {
53
+ render: () => (
54
+ <Stack
55
+ element={{ props: { direction: "vertical", gap: "sm", align: "start" } }}
56
+ >
57
+ <StackItem label="Item 1" />
58
+ <StackItem label="Item 2" />
59
+ </Stack>
60
+ ),
61
+ };
@@ -0,0 +1,33 @@
1
+ import { dashboardCatalog } from "../../../lib/catalog.js";
2
+ import type { CatalogueComponentProps } from "../../../lib/component-catalog.js";
3
+
4
+ export function Stack({
5
+ element,
6
+ children,
7
+ }: CatalogueComponentProps<typeof dashboardCatalog.components.Stack>) {
8
+ const { direction, gap, align } = element.props;
9
+ const gaps: Record<string, string> = {
10
+ sm: "8px",
11
+ md: "16px",
12
+ lg: "24px",
13
+ };
14
+ const alignments: Record<string, string> = {
15
+ start: "flex-start",
16
+ center: "center",
17
+ end: "flex-end",
18
+ stretch: "stretch",
19
+ };
20
+
21
+ return (
22
+ <div
23
+ style={{
24
+ display: "flex",
25
+ flexDirection: direction === "horizontal" ? "row" : "column",
26
+ gap: gaps[gap || "md"],
27
+ alignItems: alignments[align || "stretch"],
28
+ }}
29
+ >
30
+ {children}
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,38 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Table } from "./index.js";
3
+
4
+ const meta: Meta<typeof Table> = {
5
+ title: "Dashboard/Table",
6
+ component: Table,
7
+ };
8
+ export default meta;
9
+ type Story = StoryObj<typeof Table>;
10
+
11
+ export const Default: Story = {
12
+ args: {
13
+ element: {
14
+ props: {
15
+ dataPath: "users",
16
+ columns: [
17
+ { key: "name", label: "Name", format: "text" },
18
+ { key: "status", label: "Status", format: "badge" },
19
+ { key: "amount", label: "Amount", format: "currency" },
20
+ ],
21
+ },
22
+ },
23
+ },
24
+ };
25
+
26
+ export const TwoColumns: Story = {
27
+ args: {
28
+ element: {
29
+ props: {
30
+ dataPath: "items",
31
+ columns: [
32
+ { key: "name", label: "Name", format: "text" },
33
+ { key: "amount", label: "Amount", format: "currency" },
34
+ ],
35
+ },
36
+ },
37
+ },
38
+ };
@@ -0,0 +1,104 @@
1
+ import { dashboardCatalog } from "../../../lib/catalog.js";
2
+ import type { CatalogueComponentProps } from "../../../lib/component-catalog.js";
3
+
4
+ export function Table({
5
+ element,
6
+ }: CatalogueComponentProps<typeof dashboardCatalog.components.Table>) {
7
+ const { dataPath, columns } = element.props;
8
+
9
+ // Static mock data for example page
10
+ const mockData = [
11
+ { id: 1, name: "Item A", status: "Active", amount: 1250 },
12
+ { id: 2, name: "Item B", status: "Pending", amount: 830 },
13
+ { id: 3, name: "Item C", status: "Completed", amount: 2100 },
14
+ ];
15
+
16
+ const formatCell = (
17
+ value: unknown,
18
+ format?: "text" | "currency" | "date" | "badge" | null
19
+ ) => {
20
+ if (value === null || value === undefined) return "-";
21
+ if (format === "currency" && typeof value === "number") {
22
+ return new Intl.NumberFormat("en-US", {
23
+ style: "currency",
24
+ currency: "USD",
25
+ }).format(value);
26
+ }
27
+ if (format === "date" && typeof value === "string") {
28
+ const d = new Date(value);
29
+ return isNaN(d.getTime()) ? String(value) : d.toLocaleDateString();
30
+ }
31
+ if (format === "badge") {
32
+ return (
33
+ <span
34
+ style={{
35
+ padding: "2px 8px",
36
+ borderRadius: 12,
37
+ fontSize: 12,
38
+ fontWeight: 500,
39
+ background: "hsl(var(--border))",
40
+ color: "hsl(var(--foreground))",
41
+ }}
42
+ >
43
+ {String(value)}
44
+ </span>
45
+ );
46
+ }
47
+ return String(value);
48
+ };
49
+
50
+ return (
51
+ <div>
52
+ <table style={{ width: "100%", borderCollapse: "collapse" }}>
53
+ <thead>
54
+ <tr>
55
+ {columns.map((col) => (
56
+ <th
57
+ key={col.key}
58
+ style={{
59
+ textAlign: "left",
60
+ padding: "12px 8px",
61
+ borderBottom: "1px solid hsl(var(--border))",
62
+ fontSize: 12,
63
+ fontWeight: 500,
64
+ color: "hsl(var(--muted-foreground))",
65
+ textTransform: "uppercase",
66
+ letterSpacing: "0.05em",
67
+ }}
68
+ >
69
+ {col.label}
70
+ </th>
71
+ ))}
72
+ </tr>
73
+ </thead>
74
+ <tbody>
75
+ {mockData.map((row, i) => (
76
+ <tr key={i}>
77
+ {columns.map((col) => (
78
+ <td
79
+ key={col.key}
80
+ style={{
81
+ padding: "12px 8px",
82
+ borderBottom: "1px solid hsl(var(--border))",
83
+ fontSize: 14,
84
+ }}
85
+ >
86
+ {formatCell(row[col.key as keyof typeof row], col.format)}
87
+ </td>
88
+ ))}
89
+ </tr>
90
+ ))}
91
+ </tbody>
92
+ </table>
93
+ <p
94
+ style={{
95
+ margin: "8px 0 0",
96
+ fontSize: 12,
97
+ color: "hsl(var(--muted-foreground))",
98
+ }}
99
+ >
100
+ Data: {dataPath}
101
+ </p>
102
+ </div>
103
+ );
104
+ }
@@ -0,0 +1,53 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Text } from "./index.js";
3
+
4
+ const meta: Meta<typeof Text> = {
5
+ title: "Dashboard/Text",
6
+ component: Text,
7
+ };
8
+ export default meta;
9
+ type Story = StoryObj<typeof Text>;
10
+
11
+ export const Default: Story = {
12
+ args: {
13
+ element: {
14
+ props: {
15
+ content: "Default text content",
16
+ variant: null,
17
+ color: "default",
18
+ },
19
+ },
20
+ },
21
+ };
22
+
23
+ export const Muted: Story = {
24
+ args: {
25
+ element: {
26
+ props: { content: "Muted text content", variant: null, color: "muted" },
27
+ },
28
+ },
29
+ };
30
+
31
+ export const Success: Story = {
32
+ args: {
33
+ element: {
34
+ props: { content: "Success message", variant: null, color: "success" },
35
+ },
36
+ },
37
+ };
38
+
39
+ export const Warning: Story = {
40
+ args: {
41
+ element: {
42
+ props: { content: "Warning message", variant: null, color: "warning" },
43
+ },
44
+ },
45
+ };
46
+
47
+ export const Danger: Story = {
48
+ args: {
49
+ element: {
50
+ props: { content: "Error message", variant: null, color: "danger" },
51
+ },
52
+ },
53
+ };
@@ -0,0 +1,18 @@
1
+ import { dashboardCatalog } from "../../../lib/catalog.js";
2
+ import type { CatalogueComponentProps } from "../../../lib/component-catalog.js";
3
+
4
+ export function Text({
5
+ element,
6
+ }: CatalogueComponentProps<typeof dashboardCatalog.components.Text>) {
7
+ const { content, color } = element.props;
8
+ const colors: Record<string, string> = {
9
+ default: "hsl(var(--foreground))",
10
+ muted: "hsl(var(--muted-foreground))",
11
+ success: "#22c55e",
12
+ warning: "#eab308",
13
+ danger: "#ef4444",
14
+ };
15
+ return (
16
+ <p style={{ margin: 0, color: colors[color || "default"] }}>{content}</p>
17
+ );
18
+ }
@@ -0,0 +1,46 @@
1
+ export { Badge } from "./Badge/index.js";
2
+ export { Button } from "./Button/index.js";
3
+ export { Card } from "./Card/index.js";
4
+ export { Chart } from "./Chart/index.js";
5
+ export { DatePicker } from "./DatePicker/index.js";
6
+ export { Divider } from "./Divider/index.js";
7
+ export { Empty } from "./Empty/index.js";
8
+ export { Grid } from "./Grid/index.js";
9
+ export { Heading } from "./Heading/index.js";
10
+ export { List } from "./List/index.js";
11
+ export { Metric } from "./Metric/index.js";
12
+ export { Stack } from "./Stack/index.js";
13
+ export { Table } from "./Table/index.js";
14
+ export { Text } from "./Text/index.js";
15
+
16
+ import { Badge } from "./Badge/index.js";
17
+ import { Button } from "./Button/index.js";
18
+ import { Card } from "./Card/index.js";
19
+ import { Chart } from "./Chart/index.js";
20
+ import { DatePicker } from "./DatePicker/index.js";
21
+ import { Divider } from "./Divider/index.js";
22
+ import { Empty } from "./Empty/index.js";
23
+ import { Grid } from "./Grid/index.js";
24
+ import { Heading } from "./Heading/index.js";
25
+ import { List } from "./List/index.js";
26
+ import { Metric } from "./Metric/index.js";
27
+ import { Stack } from "./Stack/index.js";
28
+ import { Table } from "./Table/index.js";
29
+ import { Text } from "./Text/index.js";
30
+
31
+ export const componentRegistry = {
32
+ Badge,
33
+ Button,
34
+ Card,
35
+ Chart,
36
+ DatePicker,
37
+ Divider,
38
+ Empty,
39
+ Grid,
40
+ Heading,
41
+ List,
42
+ Metric,
43
+ Stack,
44
+ Table,
45
+ Text,
46
+ };
@@ -0,0 +1,17 @@
1
+ export {
2
+ Badge,
3
+ Button,
4
+ Card,
5
+ Chart,
6
+ DatePicker,
7
+ Divider,
8
+ Empty,
9
+ Grid,
10
+ Heading,
11
+ List,
12
+ Metric,
13
+ Stack,
14
+ Table,
15
+ Text,
16
+ componentRegistry,
17
+ } from "./dashboard/index.js";
@@ -0,0 +1,56 @@
1
+ import { useMemo } from "react";
2
+ import type { LogEntry } from "../../types.js";
3
+ import {
4
+ formatAttributeValue,
5
+ isComplexValue,
6
+ } from "../../utils/attributes.js";
7
+
8
+ export interface AttributesTabProps {
9
+ log: LogEntry;
10
+ }
11
+
12
+ export function AttributesTab({ log }: AttributesTabProps) {
13
+ const sortedAttributes = useMemo(() => {
14
+ return Object.entries(log.attributes).sort(([a], [b]) =>
15
+ a.localeCompare(b)
16
+ );
17
+ }, [log.attributes]);
18
+
19
+ if (sortedAttributes.length === 0) {
20
+ return (
21
+ <div className="text-sm text-muted-foreground text-center py-8">
22
+ No attributes available
23
+ </div>
24
+ );
25
+ }
26
+
27
+ return (
28
+ <div className="space-y-2">
29
+ {sortedAttributes.map(([key, value]) => {
30
+ const isComplex = isComplexValue(value);
31
+ const formattedValue = formatAttributeValue(value);
32
+ return (
33
+ <div key={key} className="p-2 rounded bg-muted">
34
+ <div
35
+ className="font-mono font-medium text-xs text-foreground mb-1"
36
+ title={key}
37
+ >
38
+ {key}
39
+ </div>
40
+ <div>
41
+ {isComplex ? (
42
+ <pre className="text-xs text-foreground bg-background p-2 rounded border border-border overflow-x-auto">
43
+ {formattedValue}
44
+ </pre>
45
+ ) : (
46
+ <span className="text-sm text-foreground break-words">
47
+ {formattedValue}
48
+ </span>
49
+ )}
50
+ </div>
51
+ </div>
52
+ );
53
+ })}
54
+ </div>
55
+ );
56
+ }
@@ -0,0 +1,139 @@
1
+ import { useState } from "react";
2
+
3
+ export interface JsonTreeViewProps {
4
+ data: Record<string, unknown> | unknown[];
5
+ level?: number;
6
+ }
7
+
8
+ export function JsonTreeView({ data, level = 0 }: JsonTreeViewProps) {
9
+ return (
10
+ <div className="font-mono text-sm">
11
+ {Array.isArray(data) ? (
12
+ <ArrayView items={data} level={level} />
13
+ ) : (
14
+ <ObjectView obj={data} level={level} />
15
+ )}
16
+ </div>
17
+ );
18
+ }
19
+
20
+ function ObjectView({
21
+ obj,
22
+ level,
23
+ }: {
24
+ obj: Record<string, unknown>;
25
+ level: number;
26
+ }) {
27
+ const entries = Object.entries(obj);
28
+ if (entries.length === 0)
29
+ return <span className="text-muted-foreground">{"{}"}</span>;
30
+ return (
31
+ <div>
32
+ {entries.map(([key, value]) => (
33
+ <JsonTreeNode key={key} objKey={key} value={value} level={level} />
34
+ ))}
35
+ </div>
36
+ );
37
+ }
38
+
39
+ function ArrayView({ items, level }: { items: unknown[]; level: number }) {
40
+ if (items.length === 0)
41
+ return <span className="text-muted-foreground">[]</span>;
42
+ return (
43
+ <div>
44
+ {items.map((item, index) => (
45
+ <JsonTreeNode
46
+ key={index}
47
+ objKey={String(index)}
48
+ value={item}
49
+ level={level}
50
+ isArrayItem
51
+ />
52
+ ))}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ function JsonTreeNode({
58
+ objKey,
59
+ value,
60
+ level,
61
+ isArrayItem = false,
62
+ }: {
63
+ objKey: string;
64
+ value: unknown;
65
+ level: number;
66
+ isArrayItem?: boolean;
67
+ }) {
68
+ const [isExpanded, setIsExpanded] = useState(level < 2);
69
+
70
+ const isExpandable =
71
+ value !== null &&
72
+ typeof value === "object" &&
73
+ (Array.isArray(value) ? value.length > 0 : Object.keys(value).length > 0);
74
+
75
+ const indent = level * 16;
76
+
77
+ if (!isExpandable) {
78
+ return (
79
+ <div
80
+ style={{ paddingLeft: `${indent}px` }}
81
+ className="py-0.5 hover:bg-muted"
82
+ >
83
+ {!isArrayItem && (
84
+ <span className="text-blue-600 dark:text-blue-400">
85
+ {objKey}
86
+ {": "}
87
+ </span>
88
+ )}
89
+ <span className="text-foreground">{formatPrimitiveValue(value)}</span>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ return (
95
+ <div>
96
+ <div
97
+ style={{ paddingLeft: `${indent}px` }}
98
+ className="py-0.5 hover:bg-muted cursor-pointer"
99
+ onClick={() => setIsExpanded(!isExpanded)}
100
+ >
101
+ <span className="inline-block w-4 text-muted-foreground">
102
+ {isExpanded ? "▼" : "▶"}
103
+ </span>
104
+ {!isArrayItem && (
105
+ <span className="text-blue-600 dark:text-blue-400">
106
+ {objKey}
107
+ {": "}
108
+ </span>
109
+ )}
110
+ <span className="text-muted-foreground">
111
+ {Array.isArray(value)
112
+ ? `Array(${value.length})`
113
+ : `Object(${Object.keys(value).length})`}
114
+ </span>
115
+ </div>
116
+ {isExpanded && (
117
+ <div>
118
+ {Array.isArray(value) ? (
119
+ <ArrayView items={value} level={level + 1} />
120
+ ) : (
121
+ <ObjectView
122
+ obj={value as Record<string, unknown>}
123
+ level={level + 1}
124
+ />
125
+ )}
126
+ </div>
127
+ )}
128
+ </div>
129
+ );
130
+ }
131
+
132
+ function formatPrimitiveValue(value: unknown): string {
133
+ if (value === null) return "null";
134
+ if (value === undefined) return "undefined";
135
+ if (typeof value === "string") return `"${value}"`;
136
+ if (typeof value === "boolean") return value ? "true" : "false";
137
+ if (typeof value === "number") return String(value);
138
+ return String(value);
139
+ }