@kopai/ui 0.0.5 → 0.1.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 +828 -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,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
+ }