page.mdx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. import { pageMetadata } from "@/lib/page-metadata"
  2. export const metadata = pageMetadata("docs/custom-schema")
  3. # Custom Schema & Renderer
  4. Build your own schema and renderer with `@json-render/core`.
  5. ## Overview
  6. `@json-render/core` is schema-agnostic. While `@json-render/react` provides a ready-to-use schema and renderer, you can create your own to match any JSON structure - whether it's a domain-specific format, an existing protocol, or something entirely custom.
  7. ## 1. Define Your Schema
  8. Start by defining the JSON structure your system will use. Here's an example of a simple dashboard schema:
  9. ```json
  10. {
  11. "layout": "grid",
  12. "columns": 2,
  13. "widgets": [
  14. {
  15. "type": "metric",
  16. "title": "Revenue",
  17. "value": "$12,345",
  18. "trend": "up"
  19. },
  20. {
  21. "type": "chart",
  22. "title": "Sales",
  23. "chartType": "line",
  24. "dataKey": "salesData"
  25. },
  26. {
  27. "type": "table",
  28. "title": "Recent Orders",
  29. "columns": ["id", "customer", "amount"],
  30. "dataKey": "orders"
  31. }
  32. ]
  33. }
  34. ```
  35. ## 2. Create the Catalog
  36. Define a catalog that describes your components and validates props using `defineCatalog` — see [Catalog](/docs/catalog).
  37. ```typescript
  38. import { defineCatalog } from '@json-render/core';
  39. import { z } from 'zod';
  40. export const dashboardCatalog = defineCatalog(mySchema, {
  41. components: {
  42. metric: {
  43. description: 'Displays a single metric value',
  44. props: z.object({
  45. title: z.string(),
  46. value: z.string(),
  47. trend: z.enum(['up', 'down', 'flat']).optional(),
  48. change: z.string().optional(),
  49. }),
  50. },
  51. chart: {
  52. description: 'Renders a chart visualization',
  53. props: z.object({
  54. title: z.string(),
  55. chartType: z.enum(['line', 'bar', 'pie', 'area']),
  56. dataKey: z.string(),
  57. height: z.number().optional(),
  58. }),
  59. },
  60. table: {
  61. description: 'Displays tabular data',
  62. props: z.object({
  63. title: z.string(),
  64. columns: z.array(z.string()),
  65. dataKey: z.string(),
  66. pageSize: z.number().optional(),
  67. }),
  68. },
  69. text: {
  70. description: 'Displays text content',
  71. props: z.object({
  72. content: z.string(),
  73. variant: z.enum(['heading', 'body', 'caption']).optional(),
  74. }),
  75. },
  76. },
  77. });
  78. ```
  79. ## 3. Define the Root Schema
  80. Create a schema for the overall document structure:
  81. ```typescript
  82. import { z } from 'zod';
  83. const WidgetSchema = z.object({
  84. type: z.string(),
  85. title: z.string().optional(),
  86. // Additional props validated by catalog
  87. }).passthrough();
  88. export const DashboardSchema = z.object({
  89. layout: z.enum(['grid', 'stack', 'tabs']),
  90. columns: z.number().optional(),
  91. widgets: z.array(WidgetSchema),
  92. });
  93. export type Dashboard = z.infer<typeof DashboardSchema>;
  94. export type Widget = z.infer<typeof WidgetSchema>;
  95. ```
  96. ## 4. Build the Renderer
  97. Create a renderer that maps your schema to React components:
  98. ```tsx
  99. import React from 'react';
  100. import { dashboardCatalog } from './catalog';
  101. import type { Dashboard, Widget } from './schema';
  102. // Widget component registry
  103. const widgetComponents: Record<string, React.FC<any>> = {
  104. metric: ({ title, value, trend, change }) => (
  105. <div className="p-4 rounded-lg border">
  106. <p className="text-sm text-muted-foreground">{title}</p>
  107. <p className="text-2xl font-bold">{value}</p>
  108. {trend && (
  109. <p className={`text-sm ${trend === 'up' ? 'text-green-500' : 'text-red-500'}`}>
  110. {trend === 'up' ? '+' : '-'}{change}
  111. </p>
  112. )}
  113. </div>
  114. ),
  115. chart: ({ title, chartType, data }) => (
  116. <div className="p-4 rounded-lg border">
  117. <p className="font-medium mb-2">{title}</p>
  118. <div className="h-48 bg-muted rounded flex items-center justify-center">
  119. {/* Your chart library here */}
  120. <span className="text-muted-foreground">{chartType} chart</span>
  121. </div>
  122. </div>
  123. ),
  124. table: ({ title, columns, data }) => (
  125. <div className="p-4 rounded-lg border">
  126. <p className="font-medium mb-2">{title}</p>
  127. <table className="w-full text-sm">
  128. <thead>
  129. <tr>
  130. {columns.map((col: string) => (
  131. <th key={col} className="text-left p-2 border-b">{col}</th>
  132. ))}
  133. </tr>
  134. </thead>
  135. <tbody>
  136. {data?.map((row: any, i: number) => (
  137. <tr key={i}>
  138. {columns.map((col: string) => (
  139. <td key={col} className="p-2 border-b">{row[col]}</td>
  140. ))}
  141. </tr>
  142. ))}
  143. </tbody>
  144. </table>
  145. </div>
  146. ),
  147. text: ({ content, variant = 'body' }) => {
  148. const className = {
  149. heading: 'text-xl font-bold',
  150. body: 'text-base',
  151. caption: 'text-sm text-muted-foreground',
  152. }[variant];
  153. return <p className={className}>{content}</p>;
  154. },
  155. };
  156. // Main renderer
  157. export function DashboardRenderer({
  158. spec,
  159. data = {},
  160. }: {
  161. spec: Dashboard;
  162. data?: Record<string, any>;
  163. }) {
  164. const layoutClass = {
  165. grid: `grid gap-4 ${spec.columns ? `grid-cols-${spec.columns}` : 'grid-cols-2'}`,
  166. stack: 'flex flex-col gap-4',
  167. tabs: 'space-y-4',
  168. }[spec.layout];
  169. return (
  170. <div className={layoutClass}>
  171. {spec.widgets.map((widget, index) => {
  172. const Component = widgetComponents[widget.type];
  173. if (!Component) {
  174. console.warn(`Unknown widget type: ${widget.type}`);
  175. return null;
  176. }
  177. // Resolve data references
  178. const widgetData = widget.dataKey ? data[widget.dataKey] : undefined;
  179. return (
  180. <Component
  181. key={index}
  182. {...widget}
  183. data={widgetData}
  184. />
  185. );
  186. })}
  187. </div>
  188. );
  189. }
  190. ```
  191. ## 5. Generate LLM Prompts
  192. Use the catalog to generate system prompts for AI:
  193. ```typescript
  194. const systemPrompt = dashboardCatalog.prompt({
  195. customRules: [
  196. 'Use metric widgets for single KPI values',
  197. 'Use chart widgets for time-series data',
  198. 'Use table widgets for lists of records',
  199. 'Limit dashboards to 6 widgets maximum',
  200. ],
  201. });
  202. // Use with any LLM
  203. const response = await generateText({
  204. model: 'gpt-4',
  205. system: systemPrompt,
  206. prompt: 'Create a sales dashboard with revenue, orders, and a chart',
  207. });
  208. ```
  209. ## 6. Validate Specs
  210. Validate incoming specs against your schema. Use `catalog.validate()` to check AI output against the catalog's Zod schema:
  211. ```typescript
  212. function validateDashboard(spec: unknown) {
  213. // Validate root structure
  214. const rootResult = DashboardSchema.safeParse(spec);
  215. if (!rootResult.success) {
  216. return { valid: false, errors: rootResult.error.errors };
  217. }
  218. // Validate each widget's props against the catalog
  219. const result = dashboardCatalog.validate(spec);
  220. if (!result.success) {
  221. return { valid: false, errors: result.error.errors };
  222. }
  223. return { valid: true, errors: [] };
  224. }
  225. ```
  226. ## Usage Example
  227. ```tsx
  228. 'use client';
  229. import { useState } from 'react';
  230. import { DashboardRenderer } from './renderer';
  231. import type { Dashboard } from './schema';
  232. const initialSpec: Dashboard = {
  233. layout: 'grid',
  234. columns: 2,
  235. widgets: [
  236. { type: 'metric', title: 'Revenue', value: '$12,345', trend: 'up' },
  237. { type: 'metric', title: 'Orders', value: '156', trend: 'up' },
  238. { type: 'chart', title: 'Sales Trend', chartType: 'line', dataKey: 'sales' },
  239. { type: 'table', title: 'Recent Orders', columns: ['id', 'customer', 'amount'], dataKey: 'orders' },
  240. ],
  241. };
  242. const data = {
  243. sales: [/* chart data */],
  244. orders: [
  245. { id: '001', customer: 'Acme Inc', amount: '$500' },
  246. { id: '002', customer: 'Globex', amount: '$750' },
  247. ],
  248. };
  249. export function MyDashboard() {
  250. const [spec, setSpec] = useState(initialSpec);
  251. return <DashboardRenderer spec={spec} data={data} />;
  252. }
  253. ```
  254. ## Next
  255. See how to integrate with [A2UI](/docs/a2ui) or [Adaptive Cards](/docs/adaptive-cards) protocols.