page.mdx 7.7 KB

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