catalog.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. import { z } from 'zod';
  2. import type {
  3. ComponentSchema,
  4. ValidationMode,
  5. UIElement,
  6. UITree,
  7. VisibilityCondition,
  8. } from './types';
  9. import { VisibilityConditionSchema } from './visibility';
  10. import { ActionSchema, type ActionDefinition } from './actions';
  11. import { ValidationConfigSchema, type ValidationFunction } from './validation';
  12. /**
  13. * Component definition with visibility and validation support
  14. */
  15. export interface ComponentDefinition<TProps extends ComponentSchema = ComponentSchema> {
  16. /** Zod schema for component props */
  17. props: TProps;
  18. /** Whether this component can have children */
  19. hasChildren?: boolean;
  20. /** Description for AI generation */
  21. description?: string;
  22. }
  23. /**
  24. * Catalog configuration
  25. */
  26. export interface CatalogConfig<
  27. TComponents extends Record<string, ComponentDefinition> = Record<string, ComponentDefinition>,
  28. TActions extends Record<string, ActionDefinition> = Record<string, ActionDefinition>,
  29. TFunctions extends Record<string, ValidationFunction> = Record<string, ValidationFunction>
  30. > {
  31. /** Catalog name */
  32. name?: string;
  33. /** Component definitions */
  34. components: TComponents;
  35. /** Action definitions with param schemas */
  36. actions?: TActions;
  37. /** Custom validation functions */
  38. functions?: TFunctions;
  39. /** Validation mode */
  40. validation?: ValidationMode;
  41. }
  42. /**
  43. * Catalog instance
  44. */
  45. export interface Catalog<
  46. TComponents extends Record<string, ComponentDefinition> = Record<string, ComponentDefinition>,
  47. TActions extends Record<string, ActionDefinition> = Record<string, ActionDefinition>,
  48. TFunctions extends Record<string, ValidationFunction> = Record<string, ValidationFunction>
  49. > {
  50. /** Catalog name */
  51. readonly name: string;
  52. /** Component names */
  53. readonly componentNames: (keyof TComponents)[];
  54. /** Action names */
  55. readonly actionNames: (keyof TActions)[];
  56. /** Function names */
  57. readonly functionNames: (keyof TFunctions)[];
  58. /** Validation mode */
  59. readonly validation: ValidationMode;
  60. /** Component definitions */
  61. readonly components: TComponents;
  62. /** Action definitions */
  63. readonly actions: TActions;
  64. /** Custom validation functions */
  65. readonly functions: TFunctions;
  66. /** Full element schema for AI generation */
  67. readonly elementSchema: z.ZodType<UIElement>;
  68. /** Full UI tree schema */
  69. readonly treeSchema: z.ZodType<UITree>;
  70. /** Check if component exists */
  71. hasComponent(type: string): boolean;
  72. /** Check if action exists */
  73. hasAction(name: string): boolean;
  74. /** Check if function exists */
  75. hasFunction(name: string): boolean;
  76. /** Validate an element */
  77. validateElement(element: unknown): { success: boolean; data?: UIElement; error?: z.ZodError };
  78. /** Validate a UI tree */
  79. validateTree(tree: unknown): { success: boolean; data?: UITree; error?: z.ZodError };
  80. }
  81. /**
  82. * Create a v2 catalog with visibility, actions, and validation support
  83. */
  84. export function createCatalog<
  85. TComponents extends Record<string, ComponentDefinition>,
  86. TActions extends Record<string, ActionDefinition> = Record<string, ActionDefinition>,
  87. TFunctions extends Record<string, ValidationFunction> = Record<string, ValidationFunction>
  88. >(
  89. config: CatalogConfig<TComponents, TActions, TFunctions>
  90. ): Catalog<TComponents, TActions, TFunctions> {
  91. const {
  92. name = 'unnamed',
  93. components,
  94. actions = {} as TActions,
  95. functions = {} as TFunctions,
  96. validation = 'strict',
  97. } = config;
  98. const componentNames = Object.keys(components) as (keyof TComponents)[];
  99. const actionNames = Object.keys(actions) as (keyof TActions)[];
  100. const functionNames = Object.keys(functions) as (keyof TFunctions)[];
  101. // Create element schema for each component type
  102. const componentSchemas = componentNames.map((componentName) => {
  103. const def = components[componentName]!;
  104. return z.object({
  105. key: z.string(),
  106. type: z.literal(componentName as string),
  107. props: def.props,
  108. children: z.array(z.string()).optional(),
  109. parentKey: z.string().nullable().optional(),
  110. visible: VisibilityConditionSchema.optional(),
  111. });
  112. });
  113. // Create union schema for all components
  114. let elementSchema: z.ZodType<UIElement>;
  115. if (componentSchemas.length === 0) {
  116. elementSchema = z.object({
  117. key: z.string(),
  118. type: z.string(),
  119. props: z.record(z.unknown()),
  120. children: z.array(z.string()).optional(),
  121. parentKey: z.string().nullable().optional(),
  122. visible: VisibilityConditionSchema.optional(),
  123. }) as unknown as z.ZodType<UIElement>;
  124. } else if (componentSchemas.length === 1) {
  125. elementSchema = componentSchemas[0] as unknown as z.ZodType<UIElement>;
  126. } else {
  127. elementSchema = z.discriminatedUnion('type', [
  128. componentSchemas[0] as z.ZodObject<any>,
  129. componentSchemas[1] as z.ZodObject<any>,
  130. ...componentSchemas.slice(2) as z.ZodObject<any>[],
  131. ]) as unknown as z.ZodType<UIElement>;
  132. }
  133. // Create tree schema
  134. const treeSchema = z.object({
  135. root: z.string(),
  136. elements: z.record(elementSchema),
  137. }) as unknown as z.ZodType<UITree>;
  138. return {
  139. name,
  140. componentNames,
  141. actionNames,
  142. functionNames,
  143. validation,
  144. components,
  145. actions,
  146. functions,
  147. elementSchema,
  148. treeSchema,
  149. hasComponent(type: string) {
  150. return type in components;
  151. },
  152. hasAction(name: string) {
  153. return name in actions;
  154. },
  155. hasFunction(name: string) {
  156. return name in functions;
  157. },
  158. validateElement(element: unknown) {
  159. const result = elementSchema.safeParse(element);
  160. if (result.success) {
  161. return { success: true, data: result.data };
  162. }
  163. return { success: false, error: result.error };
  164. },
  165. validateTree(tree: unknown) {
  166. const result = treeSchema.safeParse(tree);
  167. if (result.success) {
  168. return { success: true, data: result.data };
  169. }
  170. return { success: false, error: result.error };
  171. },
  172. };
  173. }
  174. /**
  175. * Generate a prompt for AI that describes the catalog
  176. */
  177. export function generateCatalogPrompt<
  178. TComponents extends Record<string, ComponentDefinition>,
  179. TActions extends Record<string, ActionDefinition>,
  180. TFunctions extends Record<string, ValidationFunction>
  181. >(catalog: Catalog<TComponents, TActions, TFunctions>): string {
  182. const lines: string[] = [
  183. `# ${catalog.name} Component Catalog`,
  184. '',
  185. '## Available Components',
  186. '',
  187. ];
  188. // Components
  189. for (const name of catalog.componentNames) {
  190. const def = catalog.components[name]!;
  191. lines.push(`### ${String(name)}`);
  192. if (def.description) {
  193. lines.push(def.description);
  194. }
  195. lines.push('');
  196. }
  197. // Actions
  198. if (catalog.actionNames.length > 0) {
  199. lines.push('## Available Actions');
  200. lines.push('');
  201. for (const name of catalog.actionNames) {
  202. const def = catalog.actions[name]!;
  203. lines.push(`- \`${String(name)}\`${def.description ? `: ${def.description}` : ''}`);
  204. }
  205. lines.push('');
  206. }
  207. // Visibility
  208. lines.push('## Visibility Conditions');
  209. lines.push('');
  210. lines.push('Components can have a `visible` property:');
  211. lines.push('- `true` / `false` - Always visible/hidden');
  212. lines.push('- `{ "path": "/data/path" }` - Visible when path is truthy');
  213. lines.push('- `{ "auth": "signedIn" }` - Visible when user is signed in');
  214. lines.push('- `{ "and": [...] }` - All conditions must be true');
  215. lines.push('- `{ "or": [...] }` - Any condition must be true');
  216. lines.push('- `{ "not": {...} }` - Negates a condition');
  217. lines.push('- `{ "eq": [a, b] }` - Equality check');
  218. lines.push('');
  219. // Validation
  220. lines.push('## Validation Functions');
  221. lines.push('');
  222. lines.push('Built-in: `required`, `email`, `minLength`, `maxLength`, `pattern`, `min`, `max`, `url`');
  223. if (catalog.functionNames.length > 0) {
  224. lines.push(`Custom: ${catalog.functionNames.map(String).join(', ')}`);
  225. }
  226. lines.push('');
  227. return lines.join('\n');
  228. }
  229. /**
  230. * Type helper to infer component props from catalog
  231. */
  232. export type InferCatalogComponentProps<
  233. C extends Catalog<Record<string, ComponentDefinition>>
  234. > = {
  235. [K in keyof C['components']]: z.infer<C['components'][K]['props']>;
  236. };