@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.
- package/README.md +137 -0
- package/dist/index.cjs +5069 -3
- package/dist/index.d.cts +301 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +302 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +5010 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +25 -7
- package/src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx +113 -0
- package/src/components/KeyboardShortcuts/ShortcutsHelpDialog.tsx +82 -0
- package/src/components/KeyboardShortcuts/context.ts +23 -0
- package/src/components/KeyboardShortcuts/index.ts +8 -0
- package/src/components/KeyboardShortcuts/types.ts +11 -0
- package/src/components/dashboard/Badge/Badge.stories.tsx +29 -0
- package/src/components/dashboard/Badge/index.tsx +32 -0
- package/src/components/dashboard/Button/Button.stories.tsx +107 -0
- package/src/components/dashboard/Button/index.tsx +63 -0
- package/src/components/dashboard/Card/Card.stories.tsx +81 -0
- package/src/components/dashboard/Card/index.tsx +58 -0
- package/src/components/dashboard/Chart/Chart.stories.tsx +48 -0
- package/src/components/dashboard/Chart/index.tsx +74 -0
- package/src/components/dashboard/DatePicker/DatePicker.stories.tsx +33 -0
- package/src/components/dashboard/DatePicker/index.tsx +41 -0
- package/src/components/dashboard/Divider/Divider.stories.tsx +17 -0
- package/src/components/dashboard/Divider/index.tsx +49 -0
- package/src/components/dashboard/Empty/Empty.stories.tsx +48 -0
- package/src/components/dashboard/Empty/index.tsx +46 -0
- package/src/components/dashboard/Grid/Grid.stories.tsx +52 -0
- package/src/components/dashboard/Grid/index.tsx +26 -0
- package/src/components/dashboard/Heading/Heading.stories.tsx +25 -0
- package/src/components/dashboard/Heading/index.tsx +27 -0
- package/src/components/dashboard/List/List.stories.tsx +37 -0
- package/src/components/dashboard/List/index.tsx +24 -0
- package/src/components/dashboard/Metric/Metric.stories.tsx +65 -0
- package/src/components/dashboard/Metric/index.tsx +36 -0
- package/src/components/dashboard/Stack/Stack.stories.tsx +61 -0
- package/src/components/dashboard/Stack/index.tsx +33 -0
- package/src/components/dashboard/Table/Table.stories.tsx +38 -0
- package/src/components/dashboard/Table/index.tsx +104 -0
- package/src/components/dashboard/Text/Text.stories.tsx +53 -0
- package/src/components/dashboard/Text/index.tsx +18 -0
- package/src/components/dashboard/index.ts +46 -0
- package/src/components/index.ts +17 -0
- package/src/components/observability/LogTimeline/LogDetailPane/AttributesTab.tsx +56 -0
- package/src/components/observability/LogTimeline/LogDetailPane/JsonTreeView.tsx +139 -0
- package/src/components/observability/LogTimeline/LogDetailPane/index.tsx +271 -0
- package/src/components/observability/LogTimeline/LogFilter.stories.tsx +66 -0
- package/src/components/observability/LogTimeline/LogFilter.test.tsx +696 -0
- package/src/components/observability/LogTimeline/LogFilter.tsx +674 -0
- package/src/components/observability/LogTimeline/LogRow.tsx +174 -0
- package/src/components/observability/LogTimeline/LogTimeline.stories.tsx +154 -0
- package/src/components/observability/LogTimeline/index.tsx +542 -0
- package/src/components/observability/LogTimeline/shortcuts.ts +18 -0
- package/src/components/observability/MetricHistogram/MetricHistogram.stories.tsx +20 -0
- package/src/components/observability/MetricHistogram/index.tsx +303 -0
- package/src/components/observability/MetricStat/MetricStat.stories.tsx +30 -0
- package/src/components/observability/MetricStat/index.tsx +281 -0
- package/src/components/observability/MetricTable/MetricTable.stories.tsx +20 -0
- package/src/components/observability/MetricTable/index.tsx +194 -0
- package/src/components/observability/MetricTimeSeries/MetricTimeSeries.stories.tsx +28 -0
- package/src/components/observability/MetricTimeSeries/index.tsx +462 -0
- package/src/components/observability/RawDataTable/RawDataTable.stories.tsx +27 -0
- package/src/components/observability/RawDataTable/index.tsx +131 -0
- package/src/components/observability/ServiceList/ServiceList.stories.tsx +20 -0
- package/src/components/observability/ServiceList/index.tsx +60 -0
- package/src/components/observability/ServiceList/shortcuts.ts +6 -0
- package/src/components/observability/TabBar/TabBar.stories.tsx +34 -0
- package/src/components/observability/TabBar/index.tsx +46 -0
- package/src/components/observability/TraceDetail/TraceDetail.stories.tsx +51 -0
- package/src/components/observability/TraceDetail/index.tsx +53 -0
- package/src/components/observability/TraceSearch/TraceSearch.stories.tsx +49 -0
- package/src/components/observability/TraceSearch/index.tsx +292 -0
- package/src/components/observability/TraceTimeline/DetailPane/AttributesTab.tsx +152 -0
- package/src/components/observability/TraceTimeline/DetailPane/EventsTab.tsx +128 -0
- package/src/components/observability/TraceTimeline/DetailPane/LinksTab.tsx +210 -0
- package/src/components/observability/TraceTimeline/DetailPane/index.tsx +174 -0
- package/src/components/observability/TraceTimeline/SpanRow.tsx +173 -0
- package/src/components/observability/TraceTimeline/TimelineBar.tsx +41 -0
- package/src/components/observability/TraceTimeline/Tooltip.tsx +42 -0
- package/src/components/observability/TraceTimeline/TraceHeader.tsx +88 -0
- package/src/components/observability/TraceTimeline/TraceTimeline.stories.tsx +25 -0
- package/src/components/observability/TraceTimeline/index.tsx +478 -0
- package/src/components/observability/TraceTimeline/shortcuts.ts +16 -0
- package/src/components/observability/__fixtures__/logs.ts +476 -0
- package/src/components/observability/__fixtures__/metrics.ts +216 -0
- package/src/components/observability/__fixtures__/raw-table.ts +204 -0
- package/src/components/observability/__fixtures__/services.ts +8 -0
- package/src/components/observability/__fixtures__/trace-summaries.ts +81 -0
- package/src/components/observability/__fixtures__/traces.ts +396 -0
- package/src/components/observability/index.ts +66 -0
- package/src/components/observability/renderers/OtelMetricDiscovery.tsx +77 -0
- package/src/components/observability/renderers/OtelMetricHistogram.tsx +29 -0
- package/src/components/observability/renderers/OtelMetricStat.tsx +44 -0
- package/src/components/observability/renderers/OtelMetricTable.tsx +29 -0
- package/src/components/observability/renderers/OtelMetricTimeSeries.tsx +30 -0
- package/src/components/observability/renderers/index.ts +5 -0
- package/src/components/observability/shared/LoadingSkeleton.tsx +43 -0
- package/src/components/observability/types.ts +113 -0
- package/src/components/observability/utils/attributes.ts +17 -0
- package/src/components/observability/utils/colors.ts +29 -0
- package/src/components/observability/utils/flatten-tree.ts +53 -0
- package/src/components/observability/utils/lttb.ts +121 -0
- package/src/components/observability/utils/time.ts +46 -0
- package/src/hooks/use-kopai-data.test.ts +296 -0
- package/src/hooks/use-kopai-data.ts +64 -0
- package/src/hooks/use-live-logs.test.ts +193 -0
- package/src/hooks/use-live-logs.ts +113 -0
- package/src/index.ts +15 -0
- package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +33 -0
- package/src/lib/catalog.ts +165 -0
- package/src/lib/component-catalog.test.ts +357 -0
- package/src/lib/component-catalog.ts +171 -0
- package/src/lib/dashboard-datasource.ts +76 -0
- package/src/lib/generate-prompt-instructions.test.ts +27 -0
- package/src/lib/generate-prompt-instructions.ts +185 -0
- package/src/lib/log-buffer.test.ts +88 -0
- package/src/lib/log-buffer.ts +62 -0
- package/src/lib/observability-catalog.ts +143 -0
- package/src/lib/renderer.test.tsx +693 -0
- package/src/lib/renderer.tsx +276 -0
- package/src/pages/observability.tsx +825 -0
- package/src/providers/kopai-provider.tsx +51 -0
- package/src/styles/globals.css +46 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { dataFilterSchemas } from "@kopai/core";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
// DataSource schema - discriminated union with type-safe params per method
|
|
6
|
+
export const dataSourceSchema = z.discriminatedUnion("method", [
|
|
7
|
+
z.object({
|
|
8
|
+
method: z.literal("searchTracesPage"),
|
|
9
|
+
params: dataFilterSchemas.tracesDataFilterSchema,
|
|
10
|
+
refetchIntervalMs: z.number().optional(),
|
|
11
|
+
}),
|
|
12
|
+
z.object({
|
|
13
|
+
method: z.literal("searchLogsPage"),
|
|
14
|
+
params: dataFilterSchemas.logsDataFilterSchema,
|
|
15
|
+
refetchIntervalMs: z.number().optional(),
|
|
16
|
+
}),
|
|
17
|
+
z.object({
|
|
18
|
+
method: z.literal("searchMetricsPage"),
|
|
19
|
+
params: dataFilterSchemas.metricsDataFilterSchema,
|
|
20
|
+
refetchIntervalMs: z.number().optional(),
|
|
21
|
+
}),
|
|
22
|
+
z.object({
|
|
23
|
+
method: z.literal("getTrace"),
|
|
24
|
+
params: z.object({ traceId: z.string() }),
|
|
25
|
+
refetchIntervalMs: z.number().optional(),
|
|
26
|
+
}),
|
|
27
|
+
z.object({
|
|
28
|
+
method: z.literal("discoverMetrics"),
|
|
29
|
+
params: z.object({}).optional(),
|
|
30
|
+
refetchIntervalMs: z.number().optional(),
|
|
31
|
+
}),
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
export type DataSource = z.infer<typeof dataSourceSchema>;
|
|
35
|
+
|
|
36
|
+
export const componentDefinitionSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
hasChildren: z.boolean(),
|
|
39
|
+
description: z
|
|
40
|
+
.string()
|
|
41
|
+
.describe(
|
|
42
|
+
"Component description to be displayed by the prompt generator"
|
|
43
|
+
),
|
|
44
|
+
props: z.unknown(),
|
|
45
|
+
})
|
|
46
|
+
.describe(
|
|
47
|
+
"All options and properties necessary to render the React component with renderer"
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export const catalogConfigSchema = z.object({
|
|
51
|
+
name: z.string().describe("catalog name"),
|
|
52
|
+
components: z.record(
|
|
53
|
+
z.string().describe("React component name"),
|
|
54
|
+
componentDefinitionSchema
|
|
55
|
+
),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Union of all element types with literal type discriminator
|
|
59
|
+
export type InferredElement<C extends Record<string, { props: unknown }>> = {
|
|
60
|
+
[K in keyof C & string]: {
|
|
61
|
+
key: string;
|
|
62
|
+
type: K;
|
|
63
|
+
children: string[];
|
|
64
|
+
parentKey: string;
|
|
65
|
+
dataSource?: z.infer<typeof dataSourceSchema>;
|
|
66
|
+
props: C[K]["props"] extends z.ZodTypeAny
|
|
67
|
+
? z.infer<C[K]["props"]>
|
|
68
|
+
: unknown;
|
|
69
|
+
};
|
|
70
|
+
}[keyof C & string];
|
|
71
|
+
|
|
72
|
+
// Zod schema type for a single element variant (preserves K-to-props mapping)
|
|
73
|
+
type ElementVariantSchema<
|
|
74
|
+
K extends string,
|
|
75
|
+
Props extends z.ZodTypeAny,
|
|
76
|
+
> = z.ZodObject<{
|
|
77
|
+
key: z.ZodString;
|
|
78
|
+
type: z.ZodLiteral<K>;
|
|
79
|
+
children: z.ZodArray<z.ZodString>;
|
|
80
|
+
parentKey: z.ZodString;
|
|
81
|
+
dataSource: z.ZodOptional<typeof dataSourceSchema>;
|
|
82
|
+
props: Props;
|
|
83
|
+
}>;
|
|
84
|
+
|
|
85
|
+
// Union of all element variant schemas
|
|
86
|
+
type ElementVariantSchemas<C extends Record<string, { props: unknown }>> = {
|
|
87
|
+
[K in keyof C & string]: ElementVariantSchema<
|
|
88
|
+
K,
|
|
89
|
+
C[K]["props"] extends z.ZodTypeAny ? C[K]["props"] : z.ZodUnknown
|
|
90
|
+
>;
|
|
91
|
+
}[keyof C & string];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Creates a component catalog with typed UI tree schema for validation.
|
|
95
|
+
*
|
|
96
|
+
* @param catalogConfig - Catalog configuration with name and component definitions
|
|
97
|
+
* @returns Catalog object with name, components, and generated uiTreeSchema
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* const catalog = createCatalog({
|
|
102
|
+
* name: "my-catalog",
|
|
103
|
+
* components: {
|
|
104
|
+
* Card: {
|
|
105
|
+
* hasChildren: true,
|
|
106
|
+
* description: "Container card",
|
|
107
|
+
* props: z.object({ title: z.string() }),
|
|
108
|
+
* },
|
|
109
|
+
* },
|
|
110
|
+
* });
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export function createCatalog<
|
|
114
|
+
C extends Record<string, z.infer<typeof componentDefinitionSchema>>,
|
|
115
|
+
>(catalogConfig: { name: string; components: C }) {
|
|
116
|
+
const elementVariants = (
|
|
117
|
+
Object.keys(catalogConfig.components) as (keyof C & string)[]
|
|
118
|
+
)
|
|
119
|
+
.map((catalogItemName) => ({
|
|
120
|
+
catalogItemName,
|
|
121
|
+
component: catalogConfig.components[catalogItemName],
|
|
122
|
+
}))
|
|
123
|
+
.filter(
|
|
124
|
+
(
|
|
125
|
+
itemConfig
|
|
126
|
+
): itemConfig is typeof itemConfig & { component: C[keyof C] } =>
|
|
127
|
+
!!itemConfig.component
|
|
128
|
+
)
|
|
129
|
+
.map(({ catalogItemName, component }) =>
|
|
130
|
+
z.object({
|
|
131
|
+
key: z.string(),
|
|
132
|
+
type: z.literal(catalogItemName),
|
|
133
|
+
children: z.array(z.string()),
|
|
134
|
+
parentKey: z.string(),
|
|
135
|
+
dataSource: dataSourceSchema.optional(),
|
|
136
|
+
props: component.props,
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
type Schemas = ElementVariantSchemas<C>;
|
|
141
|
+
const elementsUnion = z.discriminatedUnion(
|
|
142
|
+
"type",
|
|
143
|
+
elementVariants as unknown as [Schemas, ...Schemas[]]
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// TODO: implement a mechanism for validating there are no circular references
|
|
147
|
+
const uiTreeSchema = z.object({
|
|
148
|
+
root: z.string().describe("root uiElement key in the elements array"),
|
|
149
|
+
elements: z.record(
|
|
150
|
+
z.string().describe("equal to the element key"),
|
|
151
|
+
elementsUnion
|
|
152
|
+
),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
name: catalogConfig.name,
|
|
157
|
+
components: catalogConfig.components,
|
|
158
|
+
uiTreeSchema,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export type ComponentDefinition = z.infer<typeof componentDefinitionSchema>;
|
|
163
|
+
|
|
164
|
+
export type InferProps<P> = P extends z.ZodTypeAny ? z.infer<P> : P;
|
|
165
|
+
|
|
166
|
+
export type CatalogueComponentProps<CD extends ComponentDefinition> =
|
|
167
|
+
CD extends { hasChildren: true; props: infer P }
|
|
168
|
+
? { element: { props: InferProps<P> }; children: ReactNode }
|
|
169
|
+
: CD extends { props: infer P }
|
|
170
|
+
? { element: { props: InferProps<P> } }
|
|
171
|
+
: never;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { observabilityCatalog } from "./observability-catalog.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
type UiTree = z.infer<typeof observabilityCatalog.uiTreeSchema>;
|
|
5
|
+
|
|
6
|
+
export type DashboardId = string;
|
|
7
|
+
export type UiTreeId = string;
|
|
8
|
+
export type OwnerId = string;
|
|
9
|
+
|
|
10
|
+
export interface DashboardMeta {
|
|
11
|
+
dashboardId: DashboardId;
|
|
12
|
+
uiTreeId: UiTreeId;
|
|
13
|
+
name: string;
|
|
14
|
+
ownerId: OwnerId;
|
|
15
|
+
pinned: boolean;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
updatedAt: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface VersionMeta {
|
|
21
|
+
uiTreeId: UiTreeId;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DashboardWriteDatasource {
|
|
26
|
+
createDashboard(
|
|
27
|
+
uiTree: UiTree,
|
|
28
|
+
name: string,
|
|
29
|
+
ownerId: OwnerId
|
|
30
|
+
): Promise<DashboardMeta>;
|
|
31
|
+
|
|
32
|
+
// uiTrees are immutable, creates new one with ref to previous
|
|
33
|
+
updateDashboard(
|
|
34
|
+
dashboardId: DashboardId,
|
|
35
|
+
uiTree: UiTree,
|
|
36
|
+
pinned?: boolean
|
|
37
|
+
): Promise<DashboardMeta>;
|
|
38
|
+
|
|
39
|
+
deleteDashboard(dashboardId: DashboardId): Promise<void>;
|
|
40
|
+
|
|
41
|
+
// Creates new uiTree copying content from specified version
|
|
42
|
+
restoreVersion(
|
|
43
|
+
dashboardId: DashboardId,
|
|
44
|
+
uiTreeId: UiTreeId
|
|
45
|
+
): Promise<DashboardMeta>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DashboardReadDatasource {
|
|
49
|
+
searchDashboards(params: {
|
|
50
|
+
limit: number;
|
|
51
|
+
cursor?: string;
|
|
52
|
+
pinned?: boolean;
|
|
53
|
+
ownerId?: OwnerId;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
dashboards: (DashboardMeta & { uiTree: UiTree })[];
|
|
56
|
+
nextCursor?: string;
|
|
57
|
+
}>;
|
|
58
|
+
|
|
59
|
+
getDashboard(
|
|
60
|
+
dashboardId: DashboardId
|
|
61
|
+
): Promise<DashboardMeta & { uiTree: UiTree }>;
|
|
62
|
+
|
|
63
|
+
listVersions(params: {
|
|
64
|
+
dashboardId: DashboardId;
|
|
65
|
+
limit: number;
|
|
66
|
+
cursor?: string;
|
|
67
|
+
}): Promise<{
|
|
68
|
+
versions: VersionMeta[];
|
|
69
|
+
nextCursor?: string;
|
|
70
|
+
}>;
|
|
71
|
+
|
|
72
|
+
getDashboardAtVersion(uiTreeId: UiTreeId): Promise<{
|
|
73
|
+
uiTree: UiTree;
|
|
74
|
+
createdAt: string;
|
|
75
|
+
}>;
|
|
76
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createCatalog } from "./component-catalog.js";
|
|
4
|
+
import { generatePromptInstructions } from "./generate-prompt-instructions.js";
|
|
5
|
+
|
|
6
|
+
describe("generatePromptInstructions", () => {
|
|
7
|
+
it("generates full prompt instructions", () => {
|
|
8
|
+
const catalog = createCatalog({
|
|
9
|
+
name: "test",
|
|
10
|
+
components: {
|
|
11
|
+
Card: {
|
|
12
|
+
props: z.object({ title: z.string() }),
|
|
13
|
+
description: "A card container",
|
|
14
|
+
hasChildren: true,
|
|
15
|
+
},
|
|
16
|
+
Button: {
|
|
17
|
+
props: z.object({ label: z.string() }),
|
|
18
|
+
description: "Clickable button",
|
|
19
|
+
hasChildren: false,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const prompt = generatePromptInstructions(catalog);
|
|
25
|
+
expect(prompt).toMatchSnapshot();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { dataSourceSchema } from "./component-catalog.js";
|
|
3
|
+
|
|
4
|
+
type Catalog = {
|
|
5
|
+
name: string;
|
|
6
|
+
components: Record<
|
|
7
|
+
string,
|
|
8
|
+
{ hasChildren: boolean; description: string; props: unknown }
|
|
9
|
+
>;
|
|
10
|
+
uiTreeSchema: z.ZodTypeAny;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Helper to format prop type from JSON schema property
|
|
14
|
+
function formatPropType(prop: {
|
|
15
|
+
type?: string | string[];
|
|
16
|
+
enum?: string[];
|
|
17
|
+
items?: object;
|
|
18
|
+
}): string {
|
|
19
|
+
if (prop.enum) return prop.enum.map((v) => `"${v}"`).join(" | ");
|
|
20
|
+
if (Array.isArray(prop.type))
|
|
21
|
+
return prop.type.filter((t) => t !== "null").join(" | ");
|
|
22
|
+
if (prop.type === "array" && prop.items)
|
|
23
|
+
return `array of ${formatPropType(prop.items as Parameters<typeof formatPropType>[0])}`;
|
|
24
|
+
return prop.type ?? "unknown";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Helper to format props from JSON schema
|
|
28
|
+
function formatPropsFromJsonSchema(jsonSchema: object): string {
|
|
29
|
+
const schema = jsonSchema as {
|
|
30
|
+
properties?: Record<string, unknown>;
|
|
31
|
+
required?: string[];
|
|
32
|
+
};
|
|
33
|
+
if (!schema.properties) return "(no props)";
|
|
34
|
+
|
|
35
|
+
const required = new Set(schema.required ?? []);
|
|
36
|
+
const lines: string[] = [];
|
|
37
|
+
|
|
38
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
39
|
+
const prop = value as {
|
|
40
|
+
type?: string | string[];
|
|
41
|
+
enum?: string[];
|
|
42
|
+
description?: string;
|
|
43
|
+
items?: object;
|
|
44
|
+
};
|
|
45
|
+
const isRequired = required.has(key);
|
|
46
|
+
const typeStr = formatPropType(prop);
|
|
47
|
+
const reqStr = isRequired ? " (required)" : " (optional)";
|
|
48
|
+
const descStr = prop.description ? ` - ${prop.description}` : "";
|
|
49
|
+
lines.push(`- ${key}: ${typeStr}${reqStr}${descStr}`);
|
|
50
|
+
}
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Helper to build example UI tree
|
|
55
|
+
function buildExampleElements(
|
|
56
|
+
names: string[],
|
|
57
|
+
components: Record<string, { hasChildren: boolean }>
|
|
58
|
+
): { root: string; elements: Record<string, unknown> } {
|
|
59
|
+
const containerName =
|
|
60
|
+
names.find((n) => components[n]?.hasChildren) ?? names[0];
|
|
61
|
+
const containerKey = `${String(containerName).toLowerCase()}-1`;
|
|
62
|
+
|
|
63
|
+
const childKeys: string[] = [];
|
|
64
|
+
const elements: Record<string, unknown> = {};
|
|
65
|
+
|
|
66
|
+
elements[containerKey] = {
|
|
67
|
+
key: containerKey,
|
|
68
|
+
type: String(containerName),
|
|
69
|
+
props: {},
|
|
70
|
+
children: childKeys,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const otherNames = names.filter((n) => n !== containerName).slice(0, 2);
|
|
74
|
+
for (const name of otherNames) {
|
|
75
|
+
const key = `${String(name).toLowerCase()}-1`;
|
|
76
|
+
childKeys.push(key);
|
|
77
|
+
elements[key] = {
|
|
78
|
+
key,
|
|
79
|
+
type: String(name),
|
|
80
|
+
props: {},
|
|
81
|
+
parentKey: containerKey,
|
|
82
|
+
dataSource: {
|
|
83
|
+
method: "searchTracesPage",
|
|
84
|
+
params: { limit: 10 },
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { root: containerKey, elements };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Helper to check if a value looks like the dataSource schema
|
|
93
|
+
function isDataSourceSchema(value: unknown): boolean {
|
|
94
|
+
if (!value || typeof value !== "object") return false;
|
|
95
|
+
const obj = value as Record<string, unknown>;
|
|
96
|
+
if (!Array.isArray(obj.oneOf)) return false;
|
|
97
|
+
const first = obj.oneOf[0] as Record<string, unknown> | undefined;
|
|
98
|
+
if (!first?.properties) return false;
|
|
99
|
+
const props = first.properties as Record<string, unknown>;
|
|
100
|
+
const method = props.method as Record<string, unknown> | undefined;
|
|
101
|
+
return method?.const === "searchTracesPage";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Helper to recursively replace dataSource schema with $ref
|
|
105
|
+
function replaceDataSourceWithRef(obj: unknown): void {
|
|
106
|
+
if (!obj || typeof obj !== "object") return;
|
|
107
|
+
|
|
108
|
+
const record = obj as Record<string, unknown>;
|
|
109
|
+
for (const key of Object.keys(record)) {
|
|
110
|
+
const value = record[key];
|
|
111
|
+
if (key === "dataSource" && isDataSourceSchema(value)) {
|
|
112
|
+
record[key] = { $ref: "#/$defs/DataSource" };
|
|
113
|
+
} else if (value && typeof value === "object") {
|
|
114
|
+
replaceDataSourceWithRef(value);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Helper to build unified schema with $defs
|
|
120
|
+
function buildUnifiedSchema(treeSchema: z.ZodTypeAny): object {
|
|
121
|
+
const dataSourceJsonSchema = z.toJSONSchema(dataSourceSchema);
|
|
122
|
+
const treeJsonSchema = z.toJSONSchema(treeSchema) as Record<string, unknown>;
|
|
123
|
+
|
|
124
|
+
replaceDataSourceWithRef(treeJsonSchema);
|
|
125
|
+
treeJsonSchema.$defs = { DataSource: dataSourceJsonSchema };
|
|
126
|
+
|
|
127
|
+
return treeJsonSchema;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Generates LLM prompt instructions from a catalog.
|
|
132
|
+
* Includes component docs, JSON schema, and usage examples.
|
|
133
|
+
*
|
|
134
|
+
* @param catalog - The catalog created via createCatalog
|
|
135
|
+
* @returns Markdown string with component docs, schema, and examples
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* const instructions = generatePromptInstructions(catalog);
|
|
140
|
+
* const prompt = `Build a dashboard UI.\n\n${instructions}`;
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export function generatePromptInstructions(catalog: Catalog): string {
|
|
144
|
+
const componentNames = Object.keys(catalog.components);
|
|
145
|
+
|
|
146
|
+
const componentSections = componentNames
|
|
147
|
+
.map((name) => {
|
|
148
|
+
const def = catalog.components[name]!;
|
|
149
|
+
const propsSchema = z.toJSONSchema(def.props as z.ZodTypeAny);
|
|
150
|
+
const propsFormatted = formatPropsFromJsonSchema(propsSchema);
|
|
151
|
+
const roleLine = def.hasChildren
|
|
152
|
+
? "Accepts children: yes"
|
|
153
|
+
: "Accepts dataSource: yes";
|
|
154
|
+
|
|
155
|
+
return `### ${name}
|
|
156
|
+
${def.description ?? "No description"}
|
|
157
|
+
|
|
158
|
+
Props:
|
|
159
|
+
${propsFormatted}
|
|
160
|
+
${roleLine}`;
|
|
161
|
+
})
|
|
162
|
+
.join("\n\n---\n\n");
|
|
163
|
+
|
|
164
|
+
const unifiedSchema = buildUnifiedSchema(catalog.uiTreeSchema);
|
|
165
|
+
const exampleElements = buildExampleElements(
|
|
166
|
+
componentNames,
|
|
167
|
+
catalog.components
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return `## Available Components
|
|
171
|
+
|
|
172
|
+
${componentSections}
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Output Schema
|
|
177
|
+
|
|
178
|
+
${JSON.stringify(unifiedSchema)}
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Example
|
|
183
|
+
|
|
184
|
+
${JSON.stringify(exampleElements)}`;
|
|
185
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { LogBuffer } from "./log-buffer.js";
|
|
3
|
+
import type { denormalizedSignals } from "@kopai/core";
|
|
4
|
+
|
|
5
|
+
type OtelLogsRow = denormalizedSignals.OtelLogsRow;
|
|
6
|
+
|
|
7
|
+
const BASE_NS = 1700000000000000000n;
|
|
8
|
+
const ts = (offsetMs: number) =>
|
|
9
|
+
(BASE_NS + BigInt(offsetMs) * 1000000n).toString();
|
|
10
|
+
|
|
11
|
+
function makeRow(offsetMs: number, body: string, service = "svc"): OtelLogsRow {
|
|
12
|
+
return {
|
|
13
|
+
Timestamp: ts(offsetMs),
|
|
14
|
+
Body: body,
|
|
15
|
+
ServiceName: service,
|
|
16
|
+
SeverityText: "INFO",
|
|
17
|
+
SeverityNumber: 9,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("LogBuffer", () => {
|
|
22
|
+
it("stores and returns rows sorted by timestamp", () => {
|
|
23
|
+
const buf = new LogBuffer();
|
|
24
|
+
buf.merge([makeRow(200, "second"), makeRow(100, "first")]);
|
|
25
|
+
const all = buf.getAll();
|
|
26
|
+
expect(all).toHaveLength(2);
|
|
27
|
+
expect(all[0]!.Body).toBe("first");
|
|
28
|
+
expect(all[1]!.Body).toBe("second");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("deduplicates rows with same key", () => {
|
|
32
|
+
const buf = new LogBuffer();
|
|
33
|
+
const row = makeRow(100, "hello");
|
|
34
|
+
buf.merge([row]);
|
|
35
|
+
buf.merge([row]);
|
|
36
|
+
expect(buf.size).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("trims oldest rows when exceeding maxSize", () => {
|
|
40
|
+
const buf = new LogBuffer(3);
|
|
41
|
+
buf.merge([makeRow(10, "a"), makeRow(20, "b"), makeRow(30, "c")]);
|
|
42
|
+
expect(buf.size).toBe(3);
|
|
43
|
+
|
|
44
|
+
buf.merge([makeRow(40, "d")]);
|
|
45
|
+
expect(buf.size).toBe(3);
|
|
46
|
+
// oldest ("a" at ts=10) should be gone
|
|
47
|
+
const bodies = buf.getAll().map((r) => r.Body);
|
|
48
|
+
expect(bodies).toEqual(["b", "c", "d"]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("getNewestTimestamp returns latest timestamp", () => {
|
|
52
|
+
const buf = new LogBuffer();
|
|
53
|
+
buf.merge([makeRow(100, "a"), makeRow(300, "c"), makeRow(200, "b")]);
|
|
54
|
+
expect(buf.getNewestTimestamp()).toBe(ts(300));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("getNewestTimestamp returns undefined for empty buffer", () => {
|
|
58
|
+
const buf = new LogBuffer();
|
|
59
|
+
expect(buf.getNewestTimestamp()).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("clear resets the buffer", () => {
|
|
63
|
+
const buf = new LogBuffer();
|
|
64
|
+
buf.merge([makeRow(100, "a")]);
|
|
65
|
+
expect(buf.size).toBe(1);
|
|
66
|
+
buf.clear();
|
|
67
|
+
expect(buf.size).toBe(0);
|
|
68
|
+
expect(buf.getAll()).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("handles empty merge", () => {
|
|
72
|
+
const buf = new LogBuffer();
|
|
73
|
+
buf.merge([]);
|
|
74
|
+
expect(buf.size).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("distinguishes rows with same timestamp but different body", () => {
|
|
78
|
+
const buf = new LogBuffer();
|
|
79
|
+
buf.merge([makeRow(100, "hello"), makeRow(100, "world")]);
|
|
80
|
+
expect(buf.size).toBe(2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("distinguishes rows with same timestamp+body but different service", () => {
|
|
84
|
+
const buf = new LogBuffer();
|
|
85
|
+
buf.merge([makeRow(100, "hello", "svc-a"), makeRow(100, "hello", "svc-b")]);
|
|
86
|
+
expect(buf.size).toBe(2);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { denormalizedSignals } from "@kopai/core";
|
|
2
|
+
|
|
3
|
+
type OtelLogsRow = denormalizedSignals.OtelLogsRow;
|
|
4
|
+
|
|
5
|
+
function logKey(row: OtelLogsRow): string {
|
|
6
|
+
const body = row.Body ?? "";
|
|
7
|
+
let hash = 0;
|
|
8
|
+
for (let i = 0; i < body.length; i++) {
|
|
9
|
+
hash = (hash << 5) - hash + body.charCodeAt(i);
|
|
10
|
+
hash = hash & hash;
|
|
11
|
+
}
|
|
12
|
+
return `${row.Timestamp}-${row.ServiceName ?? ""}-${Math.abs(hash).toString(36)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class LogBuffer {
|
|
16
|
+
private readonly maxSize: number;
|
|
17
|
+
private rows: OtelLogsRow[] = [];
|
|
18
|
+
private keys = new Set<string>();
|
|
19
|
+
|
|
20
|
+
constructor(maxSize = 1000) {
|
|
21
|
+
this.maxSize = maxSize;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Merge new rows, dedup, sort by timestamp, trim oldest when over max. */
|
|
25
|
+
merge(incoming: OtelLogsRow[]): void {
|
|
26
|
+
for (const row of incoming) {
|
|
27
|
+
const k = logKey(row);
|
|
28
|
+
if (this.keys.has(k)) continue;
|
|
29
|
+
this.keys.add(k);
|
|
30
|
+
this.rows.push(row);
|
|
31
|
+
}
|
|
32
|
+
// Sort ascending by timestamp
|
|
33
|
+
this.rows.sort((a, b) => {
|
|
34
|
+
if (a.Timestamp < b.Timestamp) return -1;
|
|
35
|
+
if (a.Timestamp > b.Timestamp) return 1;
|
|
36
|
+
return 0;
|
|
37
|
+
});
|
|
38
|
+
// Trim oldest
|
|
39
|
+
if (this.rows.length > this.maxSize) {
|
|
40
|
+
const removed = this.rows.splice(0, this.rows.length - this.maxSize);
|
|
41
|
+
for (const r of removed) this.keys.delete(logKey(r));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getAll(): OtelLogsRow[] {
|
|
46
|
+
return this.rows.slice();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getNewestTimestamp(): string | undefined {
|
|
50
|
+
if (this.rows.length === 0) return undefined;
|
|
51
|
+
return this.rows[this.rows.length - 1]!.Timestamp;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get size(): number {
|
|
55
|
+
return this.rows.length;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
clear(): void {
|
|
59
|
+
this.rows = [];
|
|
60
|
+
this.keys.clear();
|
|
61
|
+
}
|
|
62
|
+
}
|