page.mdx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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 { createCatalog } from '@json-render/core';
  59. import { z } from 'zod';
  60. // A2UI BoundValue schema
  61. const BoundString = z.object({
  62. literalString: z.string().optional(),
  63. path: z.string().optional(),
  64. }).refine(d => d.literalString || d.path);
  65. // A2UI children schema
  66. const Children = z.object({
  67. explicitList: z.array(z.string()).optional(),
  68. template: z.object({
  69. dataBinding: z.string(),
  70. componentId: z.string(),
  71. }).optional(),
  72. }).refine(d => d.explicitList || d.template);
  73. export const a2uiCatalog = createCatalog({
  74. components: {
  75. Text: {
  76. description: 'Displays text content',
  77. props: z.object({
  78. text: BoundString,
  79. usageHint: z.enum(['h1', 'h2', 'h3', 'body', 'caption']).optional(),
  80. }),
  81. },
  82. Button: {
  83. description: 'Interactive button',
  84. props: z.object({
  85. child: z.string(),
  86. action: z.object({
  87. name: z.string(),
  88. context: z.array(z.object({
  89. key: z.string(),
  90. value: BoundString,
  91. })).optional(),
  92. }).optional(),
  93. }),
  94. },
  95. DateTimeInput: {
  96. description: 'Date/time picker',
  97. props: z.object({
  98. label: BoundString.optional(),
  99. value: BoundString.optional(),
  100. enableDate: z.boolean().optional(),
  101. enableTime: z.boolean().optional(),
  102. }),
  103. },
  104. Column: {
  105. description: 'Vertical layout',
  106. props: z.object({
  107. children: Children,
  108. }),
  109. },
  110. Row: {
  111. description: 'Horizontal layout',
  112. props: z.object({
  113. children: Children,
  114. }),
  115. },
  116. // Add more A2UI standard components...
  117. },
  118. });
  119. ```
  120. ## Define the A2UI Schema
  121. Define the schema for A2UI message types:
  122. ```typescript
  123. import { z } from 'zod';
  124. // Component instance in the adjacency list
  125. const A2UIComponent = z.object({
  126. id: z.string(),
  127. component: z.record(z.record(z.unknown())),
  128. });
  129. // Surface update message
  130. const SurfaceUpdate = z.object({
  131. surfaceId: z.string().optional(),
  132. components: z.array(A2UIComponent),
  133. });
  134. // State model update message
  135. const StateModelUpdate = z.object({
  136. surfaceId: z.string().optional(),
  137. path: z.string().optional(),
  138. contents: z.array(z.object({
  139. key: z.string(),
  140. valueString: z.string().optional(),
  141. valueNumber: z.number().optional(),
  142. valueBoolean: z.boolean().optional(),
  143. valueMap: z.array(z.unknown()).optional(),
  144. })),
  145. });
  146. // Begin rendering message
  147. const BeginRendering = z.object({
  148. surfaceId: z.string().optional(),
  149. root: z.string(),
  150. catalogId: z.string().optional(),
  151. });
  152. // Complete A2UI message schema
  153. export const A2UIMessage = z.object({
  154. surfaceUpdate: SurfaceUpdate.optional(),
  155. dataModelUpdate: StateModelUpdate.optional(),
  156. beginRendering: BeginRendering.optional(),
  157. deleteSurface: z.object({ surfaceId: z.string() }).optional(),
  158. });
  159. ```
  160. ## Build an A2UI Renderer
  161. Create a renderer that processes the A2UI adjacency list format:
  162. ```tsx
  163. import { a2uiCatalog } from './catalog';
  164. // Component registry
  165. const components = {
  166. Text: ({ text, usageHint }) => {
  167. const Tag = usageHint?.startsWith('h') ? usageHint : 'p';
  168. return <Tag>{text}</Tag>;
  169. },
  170. Button: ({ children, action, onAction }) => (
  171. <button onClick={() => onAction?.(action)}>{children}</button>
  172. ),
  173. DateTimeInput: ({ label, value, onChange }) => (
  174. <label>
  175. {label}
  176. <input type="date" value={value} onChange={e => onChange?.(e.target.value)} />
  177. </label>
  178. ),
  179. Column: ({ children }) => <div className="flex flex-col gap-2">{children}</div>,
  180. Row: ({ children }) => <div className="flex gap-2">{children}</div>,
  181. };
  182. // Render A2UI surface
  183. export function renderA2UI(
  184. componentMap: Map<string, any>,
  185. dataModel: Record<string, any>,
  186. rootId: string,
  187. onAction?: (action: any) => void
  188. ) {
  189. function resolveBoundValue(bound: any) {
  190. if (!bound) return undefined;
  191. if (bound.literalString) return bound.literalString;
  192. if (bound.path) {
  193. const parts = bound.path.replace(/^\//, '').split('/');
  194. let value = dataModel;
  195. for (const p of parts) value = value?.[p];
  196. return value;
  197. }
  198. }
  199. function render(id: string): React.ReactNode {
  200. const comp = componentMap.get(id);
  201. if (!comp) return null;
  202. const [type, props] = Object.entries(comp.component)[0];
  203. const Component = components[type];
  204. if (!Component) return null;
  205. // Resolve props
  206. const resolved: any = {};
  207. for (const [key, val] of Object.entries(props as any)) {
  208. if (key === 'child') {
  209. resolved.children = render(val as string);
  210. } else if (key === 'children' && val?.explicitList) {
  211. resolved.children = val.explicitList.map(render);
  212. } else if (val && typeof val === 'object' && ('literalString' in val || 'path' in val)) {
  213. resolved[key] = resolveBoundValue(val);
  214. } else {
  215. resolved[key] = val;
  216. }
  217. }
  218. return <Component key={id} {...resolved} onAction={onAction} />;
  219. }
  220. return render(rootId);
  221. }
  222. ```
  223. ## Usage
  224. ```tsx
  225. const [components] = useState(() => new Map());
  226. const [dataModel, setDataModel] = useState({});
  227. const [rootId, setRootId] = useState<string | null>(null);
  228. // Process A2UI messages
  229. function handleMessage(msg: any) {
  230. if (msg.surfaceUpdate) {
  231. for (const comp of msg.surfaceUpdate.components) {
  232. components.set(comp.id, comp);
  233. }
  234. }
  235. if (msg.dataModelUpdate) {
  236. setDataModel(prev => ({ ...prev, ...msg.dataModelUpdate.contents }));
  237. }
  238. if (msg.beginRendering) {
  239. setRootId(msg.beginRendering.root);
  240. }
  241. }
  242. // Render
  243. {rootId && renderA2UI(components, dataModel, rootId, handleAction)}
  244. ```
  245. ## Next
  246. Learn about [Adaptive Cards integration](/docs/adaptive-cards) for another UI protocol.