schema.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import { defineSchema, type Spec } from "@json-render/core";
  2. /**
  3. * Ink terminal schema definition.
  4. *
  5. * Defines the spec shape (what the AI generates) and catalog shape
  6. * (what the developer provides as component + action definitions).
  7. */
  8. export const schema = defineSchema(
  9. (s) => ({
  10. // What the AI-generated SPEC looks like
  11. spec: s.object({
  12. /** Root element key */
  13. root: s.string(),
  14. /** Flat map of elements by key */
  15. elements: s.record(
  16. s.object({
  17. /** Component type from catalog */
  18. type: s.ref("catalog.components"),
  19. /** Component props */
  20. props: s.propsOf("catalog.components"),
  21. /** Child element keys (flat reference) */
  22. children: s.array(s.string()),
  23. /** Visibility condition */
  24. visible: s.any(),
  25. }),
  26. ),
  27. }),
  28. // What the CATALOG must provide
  29. catalog: s.object({
  30. /** Component definitions */
  31. components: s.map({
  32. /** Zod schema for component props */
  33. props: s.zod(),
  34. /** Slots for this component. Use ['default'] for children, or named slots like ['header', 'footer'] */
  35. slots: s.array(s.string()),
  36. /** Description for AI generation hints */
  37. description: s.string(),
  38. /** Example prop values used in prompt examples (auto-generated from Zod schema if omitted) */
  39. example: s.any(),
  40. }),
  41. /** Action definitions (optional) */
  42. actions: s.map({
  43. /** Zod schema for action params */
  44. params: s.zod(),
  45. /** Description for AI generation hints */
  46. description: s.string(),
  47. }),
  48. }),
  49. }),
  50. {
  51. builtInActions: [
  52. {
  53. name: "setState",
  54. description:
  55. "Update a value in the state model at the given statePath. Params: { statePath: string, value: any }",
  56. },
  57. {
  58. name: "pushState",
  59. description:
  60. 'Append an item to an array in state. Params: { statePath: string, value: any, clearStatePath?: string }. Value can contain {"$state":"/path"} refs and "$id" for auto IDs.',
  61. },
  62. {
  63. name: "removeState",
  64. description:
  65. "Remove an item from an array in state by index. Params: { statePath: string, index: number }",
  66. },
  67. ],
  68. defaultRules: [
  69. // Element integrity
  70. "CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.",
  71. "SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.",
  72. // Field placement
  73. 'CRITICAL: The "visible" field goes on the ELEMENT object, NOT inside "props". Correct: {"type":"<ComponentName>","props":{},"visible":{"$state":"/tab","eq":"home"},"children":[...]}.',
  74. 'CRITICAL: The "on" field goes on the ELEMENT object, NOT inside "props". Use on.press, on.change, on.submit etc. NEVER put action/actionParams inside props.',
  75. // State and data
  76. "When the user asks for a UI that displays data (e.g. logs, tasks, metrics), ALWAYS include a state field with realistic sample data. The state field is a top-level field on the spec (sibling of root/elements).",
  77. 'When building repeating content backed by a state array, use the "repeat" field on a container element. Example: { "type": "Box", "props": { "flexDirection": "column" }, "repeat": { "statePath": "/items", "key": "id" }, "children": ["item-row"] }. Inside repeated children, use { "$item": "field" } to read a field from the current item, and { "$index": true } for the current array index.',
  78. // Terminal UI design
  79. "This UI renders in a terminal using Ink. Use Box for layout (flexDirection, padding, gap), Text for text content. Keep designs compact and readable in monospace.",
  80. "Terminal UIs have limited width (~80-120 columns). Prefer vertical layouts (flexDirection: column) for main structure. Use horizontal layouts (flexDirection: row) for inline elements like badges, key-value pairs, and table rows.",
  81. "Use borderStyle on Box for visual grouping (single, double, round, bold). Use padding sparingly — 1 unit is usually enough.",
  82. "For color, use named terminal colors: red, green, yellow, blue, magenta, cyan, white, gray. Use hex colors sparingly.",
  83. "Always include realistic, professional-looking sample data. For lists include 3-5 items with varied content. Never leave data empty.",
  84. "Use Heading for section titles, Divider to separate sections, Badge for status indicators, KeyValue for labeled data, and Card for bordered groups.",
  85. "Use Tabs for multi-view UIs — bind the active tab to state and use visible conditions on child content to show/hide tab panels. Use MultiSelect for picking multiple items. Use ConfirmInput for yes/no prompts before destructive actions.",
  86. "Use Sparkline for inline trend visualization (compact, one line). Use BarChart for comparing values across categories (horizontal bars with labels). Both work well in dashboards alongside KeyValue and ProgressBar.",
  87. ],
  88. },
  89. );
  90. /**
  91. * Type alias for the Ink schema
  92. */
  93. export type InkSchema = typeof schema;
  94. /**
  95. * Spec type for Ink (parameterized by catalog)
  96. */
  97. export type InkSpec = Spec;