page.mdx 7.3 KB

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