page.mdx 7.4 KB

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