page.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import Link from "next/link";
  2. import { Code } from "@/components/code";
  3. export const metadata = {
  4. title: "A2UI Integration | json-render",
  5. };
  6. export default function A2UIPage() {
  7. return (
  8. <article>
  9. <h1 className="text-3xl font-bold mb-4">A2UI Integration</h1>
  10. <p className="text-muted-foreground mb-8">
  11. Use <code className="text-foreground">@json-render/core</code> to
  12. support{" "}
  13. <a
  14. href="https://a2ui.org"
  15. target="_blank"
  16. rel="noopener noreferrer"
  17. className="text-foreground hover:underline"
  18. >
  19. A2UI
  20. </a>{" "}
  21. natively.
  22. </p>
  23. <div className="rounded-lg border border-amber-500/50 bg-amber-500/10 p-4 mb-8">
  24. <p className="text-sm text-amber-700 dark:text-amber-300">
  25. <strong>Concept:</strong> This page demonstrates how json-render can
  26. support A2UI. The examples are illustrative and may require adaptation
  27. for production use.
  28. </p>
  29. </div>
  30. <h2 className="text-xl font-semibold mt-12 mb-4">Native A2UI Support</h2>
  31. <p className="text-sm text-muted-foreground mb-4">
  32. <code className="text-foreground">@json-render/core</code> is
  33. schema-agnostic. Define a catalog that matches A2UI&apos;s format and
  34. build a renderer that understands it - no conversion layer needed.
  35. </p>
  36. <h2 className="text-xl font-semibold mt-12 mb-4">Example A2UI Message</h2>
  37. <p className="text-sm text-muted-foreground mb-4">
  38. A2UI uses an adjacency list model - a flat list of components with ID
  39. references. This makes it easy to patch individual components:
  40. </p>
  41. <Code lang="json">{`{
  42. "surfaceUpdate": {
  43. "surfaceId": "main",
  44. "components": [
  45. {
  46. "id": "header",
  47. "component": {
  48. "Text": {
  49. "text": {"literalString": "Book Your Table"},
  50. "usageHint": "h1"
  51. }
  52. }
  53. },
  54. {
  55. "id": "date-picker",
  56. "component": {
  57. "DateTimeInput": {
  58. "label": {"literalString": "Select Date"},
  59. "value": {"path": "/reservation/date"},
  60. "enableDate": true
  61. }
  62. }
  63. },
  64. {
  65. "id": "submit-btn",
  66. "component": {
  67. "Button": {
  68. "child": "submit-text",
  69. "action": {"name": "confirm_booking"}
  70. }
  71. }
  72. },
  73. {
  74. "id": "submit-text",
  75. "component": {
  76. "Text": {"text": {"literalString": "Confirm Reservation"}}
  77. }
  78. }
  79. ]
  80. }
  81. }`}</Code>
  82. <h2 className="text-xl font-semibold mt-12 mb-4">
  83. Define the A2UI Catalog
  84. </h2>
  85. <Code lang="typescript">{`import { createCatalog } from '@json-render/core';
  86. import { z } from 'zod';
  87. // A2UI BoundValue schema
  88. const BoundString = z.object({
  89. literalString: z.string().optional(),
  90. path: z.string().optional(),
  91. }).refine(d => d.literalString || d.path);
  92. // A2UI children schema
  93. const Children = z.object({
  94. explicitList: z.array(z.string()).optional(),
  95. template: z.object({
  96. dataBinding: z.string(),
  97. componentId: z.string(),
  98. }).optional(),
  99. }).refine(d => d.explicitList || d.template);
  100. export const a2uiCatalog = createCatalog({
  101. components: {
  102. Text: {
  103. description: 'Displays text content',
  104. props: z.object({
  105. text: BoundString,
  106. usageHint: z.enum(['h1', 'h2', 'h3', 'body', 'caption']).optional(),
  107. }),
  108. },
  109. Button: {
  110. description: 'Interactive button',
  111. props: z.object({
  112. child: z.string(),
  113. action: z.object({
  114. name: z.string(),
  115. context: z.array(z.object({
  116. key: z.string(),
  117. value: BoundString,
  118. })).optional(),
  119. }).optional(),
  120. }),
  121. },
  122. DateTimeInput: {
  123. description: 'Date/time picker',
  124. props: z.object({
  125. label: BoundString.optional(),
  126. value: BoundString.optional(),
  127. enableDate: z.boolean().optional(),
  128. enableTime: z.boolean().optional(),
  129. }),
  130. },
  131. Column: {
  132. description: 'Vertical layout',
  133. props: z.object({
  134. children: Children,
  135. }),
  136. },
  137. Row: {
  138. description: 'Horizontal layout',
  139. props: z.object({
  140. children: Children,
  141. }),
  142. },
  143. // Add more A2UI standard components...
  144. },
  145. });`}</Code>
  146. <h2 className="text-xl font-semibold mt-12 mb-4">
  147. Define the A2UI Schema
  148. </h2>
  149. <p className="text-sm text-muted-foreground mb-4">
  150. Define the schema for A2UI message types:
  151. </p>
  152. <Code lang="typescript">{`import { z } from 'zod';
  153. // Component instance in the adjacency list
  154. const A2UIComponent = z.object({
  155. id: z.string(),
  156. component: z.record(z.record(z.unknown())),
  157. });
  158. // Surface update message
  159. const SurfaceUpdate = z.object({
  160. surfaceId: z.string().optional(),
  161. components: z.array(A2UIComponent),
  162. });
  163. // Data model update message
  164. const DataModelUpdate = z.object({
  165. surfaceId: z.string().optional(),
  166. path: z.string().optional(),
  167. contents: z.array(z.object({
  168. key: z.string(),
  169. valueString: z.string().optional(),
  170. valueNumber: z.number().optional(),
  171. valueBoolean: z.boolean().optional(),
  172. valueMap: z.array(z.unknown()).optional(),
  173. })),
  174. });
  175. // Begin rendering message
  176. const BeginRendering = z.object({
  177. surfaceId: z.string().optional(),
  178. root: z.string(),
  179. catalogId: z.string().optional(),
  180. });
  181. // Complete A2UI message schema
  182. export const A2UIMessage = z.object({
  183. surfaceUpdate: SurfaceUpdate.optional(),
  184. dataModelUpdate: DataModelUpdate.optional(),
  185. beginRendering: BeginRendering.optional(),
  186. deleteSurface: z.object({ surfaceId: z.string() }).optional(),
  187. });`}</Code>
  188. <h2 className="text-xl font-semibold mt-12 mb-4">
  189. Build an A2UI Renderer
  190. </h2>
  191. <p className="text-sm text-muted-foreground mb-4">
  192. Create a renderer that processes the A2UI adjacency list format:
  193. </p>
  194. <Code lang="tsx">{`import { a2uiCatalog } from './catalog';
  195. // Component registry
  196. const components = {
  197. Text: ({ text, usageHint }) => {
  198. const Tag = usageHint?.startsWith('h') ? usageHint : 'p';
  199. return <Tag>{text}</Tag>;
  200. },
  201. Button: ({ children, action, onAction }) => (
  202. <button onClick={() => onAction?.(action)}>{children}</button>
  203. ),
  204. DateTimeInput: ({ label, value, onChange }) => (
  205. <label>
  206. {label}
  207. <input type="date" value={value} onChange={e => onChange?.(e.target.value)} />
  208. </label>
  209. ),
  210. Column: ({ children }) => <div className="flex flex-col gap-2">{children}</div>,
  211. Row: ({ children }) => <div className="flex gap-2">{children}</div>,
  212. };
  213. // Render A2UI surface
  214. export function renderA2UI(
  215. componentMap: Map<string, any>,
  216. dataModel: Record<string, any>,
  217. rootId: string,
  218. onAction?: (action: any) => void
  219. ) {
  220. function resolveBoundValue(bound: any) {
  221. if (!bound) return undefined;
  222. if (bound.literalString) return bound.literalString;
  223. if (bound.path) {
  224. const parts = bound.path.replace(/^\\//, '').split('/');
  225. let value = dataModel;
  226. for (const p of parts) value = value?.[p];
  227. return value;
  228. }
  229. }
  230. function render(id: string): React.ReactNode {
  231. const comp = componentMap.get(id);
  232. if (!comp) return null;
  233. const [type, props] = Object.entries(comp.component)[0];
  234. const Component = components[type];
  235. if (!Component) return null;
  236. // Resolve props
  237. const resolved: any = {};
  238. for (const [key, val] of Object.entries(props as any)) {
  239. if (key === 'child') {
  240. resolved.children = render(val as string);
  241. } else if (key === 'children' && val?.explicitList) {
  242. resolved.children = val.explicitList.map(render);
  243. } else if (val && typeof val === 'object' && ('literalString' in val || 'path' in val)) {
  244. resolved[key] = resolveBoundValue(val);
  245. } else {
  246. resolved[key] = val;
  247. }
  248. }
  249. return <Component key={id} {...resolved} onAction={onAction} />;
  250. }
  251. return render(rootId);
  252. }`}</Code>
  253. <h2 className="text-xl font-semibold mt-12 mb-4">Usage</h2>
  254. <Code lang="tsx">{`const [components] = useState(() => new Map());
  255. const [dataModel, setDataModel] = useState({});
  256. const [rootId, setRootId] = useState<string | null>(null);
  257. // Process A2UI messages
  258. function handleMessage(msg: any) {
  259. if (msg.surfaceUpdate) {
  260. for (const comp of msg.surfaceUpdate.components) {
  261. components.set(comp.id, comp);
  262. }
  263. }
  264. if (msg.dataModelUpdate) {
  265. setDataModel(prev => ({ ...prev, ...msg.dataModelUpdate.contents }));
  266. }
  267. if (msg.beginRendering) {
  268. setRootId(msg.beginRendering.root);
  269. }
  270. }
  271. // Render
  272. {rootId && renderA2UI(components, dataModel, rootId, handleAction)}`}</Code>
  273. <h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
  274. <p className="text-sm text-muted-foreground">
  275. Learn about{" "}
  276. <Link
  277. href="/docs/adaptive-cards"
  278. className="text-foreground hover:underline"
  279. >
  280. Adaptive Cards integration
  281. </Link>{" "}
  282. for another UI protocol.
  283. </p>
  284. </article>
  285. );
  286. }