page.tsx 9.8 KB

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