page.mdx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. export const metadata = { title: "Adaptive Cards Integration" }
  2. # Adaptive Cards Integration
  3. Use json-render to render [Microsoft Adaptive Cards](https://adaptivecards.io) 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 Adaptive Cards. The examples are illustrative and may require adaptation for production use.
  7. </p>
  8. </div>
  9. ## Adaptive Cards Overview
  10. Adaptive Cards is a JSON-based format for platform-agnostic UI snippets. Cards have a `body` array of elements and an optional `actions` array for interactive buttons.
  11. ### Example Adaptive Card
  12. ```json
  13. {
  14. "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
  15. "type": "AdaptiveCard",
  16. "version": "1.5",
  17. "body": [
  18. {
  19. "type": "TextBlock",
  20. "text": "Hello, Adaptive Cards!",
  21. "size": "large",
  22. "weight": "bolder"
  23. },
  24. {
  25. "type": "Image",
  26. "url": "https://example.com/image.png",
  27. "altText": "Example image"
  28. },
  29. {
  30. "type": "Container",
  31. "items": [
  32. {
  33. "type": "TextBlock",
  34. "text": "This is inside a container",
  35. "wrap": true
  36. }
  37. ]
  38. },
  39. {
  40. "type": "ColumnSet",
  41. "columns": [
  42. {
  43. "type": "Column",
  44. "width": "auto",
  45. "items": [
  46. { "type": "TextBlock", "text": "Column 1" }
  47. ]
  48. },
  49. {
  50. "type": "Column",
  51. "width": "stretch",
  52. "items": [
  53. { "type": "TextBlock", "text": "Column 2" }
  54. ]
  55. }
  56. ]
  57. },
  58. {
  59. "type": "Input.Text",
  60. "id": "userInput",
  61. "placeholder": "Enter your name",
  62. "label": "Name"
  63. }
  64. ],
  65. "actions": [
  66. {
  67. "type": "Action.Submit",
  68. "title": "Submit"
  69. },
  70. {
  71. "type": "Action.OpenUrl",
  72. "title": "Learn More",
  73. "url": "https://adaptivecards.io"
  74. }
  75. ]
  76. }
  77. ```
  78. ## Creating an Adaptive Cards Catalog
  79. Define a catalog matching the Adaptive Cards element types:
  80. ```typescript
  81. import { createCatalog } from '@json-render/core';
  82. import { z } from 'zod';
  83. // Common Adaptive Cards properties
  84. const Spacing = z.enum(['none', 'small', 'default', 'medium', 'large', 'extraLarge', 'padding']);
  85. const HorizontalAlignment = z.enum(['left', 'center', 'right']);
  86. const VerticalAlignment = z.enum(['top', 'center', 'bottom']);
  87. const FontSize = z.enum(['small', 'default', 'medium', 'large', 'extraLarge']);
  88. const FontWeight = z.enum(['lighter', 'default', 'bolder']);
  89. const ImageSize = z.enum(['auto', 'stretch', 'small', 'medium', 'large']);
  90. const ImageStyle = z.enum(['default', 'person']);
  91. // Base element properties shared by most elements
  92. const BaseElement = {
  93. id: z.string().optional(),
  94. isVisible: z.boolean().optional(),
  95. separator: z.boolean().optional(),
  96. spacing: Spacing.optional(),
  97. };
  98. export const adaptiveCardsCatalog = createCatalog({
  99. components: {
  100. // Root card
  101. AdaptiveCard: {
  102. description: 'Root Adaptive Card container',
  103. props: z.object({
  104. version: z.string(),
  105. body: z.array(z.unknown()).optional(),
  106. actions: z.array(z.unknown()).optional(),
  107. fallbackText: z.string().optional(),
  108. minHeight: z.string().optional(),
  109. rtl: z.boolean().optional(),
  110. verticalContentAlignment: VerticalAlignment.optional(),
  111. }),
  112. },
  113. // Elements
  114. TextBlock: {
  115. description: 'Displays text with formatting options',
  116. props: z.object({
  117. ...BaseElement,
  118. text: z.string(),
  119. color: z.enum(['default', 'dark', 'light', 'accent', 'good', 'warning', 'attention']).optional(),
  120. fontType: z.enum(['default', 'monospace']).optional(),
  121. horizontalAlignment: HorizontalAlignment.optional(),
  122. isSubtle: z.boolean().optional(),
  123. maxLines: z.number().optional(),
  124. size: FontSize.optional(),
  125. weight: FontWeight.optional(),
  126. wrap: z.boolean().optional(),
  127. }),
  128. },
  129. Image: {
  130. description: 'Displays an image',
  131. props: z.object({
  132. ...BaseElement,
  133. url: z.string(),
  134. altText: z.string().optional(),
  135. backgroundColor: z.string().optional(),
  136. height: z.string().optional(),
  137. width: z.string().optional(),
  138. horizontalAlignment: HorizontalAlignment.optional(),
  139. size: ImageSize.optional(),
  140. style: ImageStyle.optional(),
  141. }),
  142. },
  143. Container: {
  144. description: 'Groups elements together',
  145. props: z.object({
  146. ...BaseElement,
  147. items: z.array(z.unknown()),
  148. style: z.enum(['default', 'emphasis', 'good', 'attention', 'warning', 'accent']).optional(),
  149. verticalContentAlignment: VerticalAlignment.optional(),
  150. bleed: z.boolean().optional(),
  151. minHeight: z.string().optional(),
  152. }),
  153. },
  154. ColumnSet: {
  155. description: 'Arranges columns horizontally',
  156. props: z.object({
  157. ...BaseElement,
  158. columns: z.array(z.unknown()),
  159. horizontalAlignment: HorizontalAlignment.optional(),
  160. minHeight: z.string().optional(),
  161. }),
  162. },
  163. Column: {
  164. description: 'A column within a ColumnSet',
  165. props: z.object({
  166. ...BaseElement,
  167. items: z.array(z.unknown()).optional(),
  168. width: z.union([z.string(), z.number()]).optional(),
  169. style: z.enum(['default', 'emphasis', 'good', 'attention', 'warning', 'accent']).optional(),
  170. verticalContentAlignment: VerticalAlignment.optional(),
  171. }),
  172. },
  173. FactSet: {
  174. description: 'Displays a series of facts as key/value pairs',
  175. props: z.object({
  176. ...BaseElement,
  177. facts: z.array(z.object({
  178. title: z.string(),
  179. value: z.string(),
  180. })),
  181. }),
  182. },
  183. // Inputs
  184. 'Input.Text': {
  185. description: 'Text input field',
  186. props: z.object({
  187. ...BaseElement,
  188. id: z.string(),
  189. isMultiline: z.boolean().optional(),
  190. maxLength: z.number().optional(),
  191. placeholder: z.string().optional(),
  192. label: z.string().optional(),
  193. value: z.string().optional(),
  194. style: z.enum(['text', 'tel', 'url', 'email', 'password']).optional(),
  195. isRequired: z.boolean().optional(),
  196. errorMessage: z.string().optional(),
  197. }),
  198. },
  199. 'Input.Number': {
  200. description: 'Number input field',
  201. props: z.object({
  202. ...BaseElement,
  203. id: z.string(),
  204. max: z.number().optional(),
  205. min: z.number().optional(),
  206. placeholder: z.string().optional(),
  207. label: z.string().optional(),
  208. value: z.number().optional(),
  209. isRequired: z.boolean().optional(),
  210. errorMessage: z.string().optional(),
  211. }),
  212. },
  213. 'Input.Toggle': {
  214. description: 'Toggle/checkbox input',
  215. props: z.object({
  216. ...BaseElement,
  217. id: z.string(),
  218. title: z.string(),
  219. label: z.string().optional(),
  220. value: z.string().optional(),
  221. valueOff: z.string().optional(),
  222. valueOn: z.string().optional(),
  223. isRequired: z.boolean().optional(),
  224. }),
  225. },
  226. 'Input.ChoiceSet': {
  227. description: 'Dropdown or radio/checkbox group',
  228. props: z.object({
  229. ...BaseElement,
  230. id: z.string(),
  231. choices: z.array(z.object({
  232. title: z.string(),
  233. value: z.string(),
  234. })),
  235. isMultiSelect: z.boolean().optional(),
  236. style: z.enum(['compact', 'expanded']).optional(),
  237. label: z.string().optional(),
  238. value: z.string().optional(),
  239. placeholder: z.string().optional(),
  240. isRequired: z.boolean().optional(),
  241. }),
  242. },
  243. // Actions
  244. 'Action.OpenUrl': {
  245. description: 'Opens a URL',
  246. props: z.object({
  247. title: z.string().optional(),
  248. url: z.string(),
  249. iconUrl: z.string().optional(),
  250. }),
  251. },
  252. 'Action.Submit': {
  253. description: 'Submits input data',
  254. props: z.object({
  255. title: z.string().optional(),
  256. data: z.unknown().optional(),
  257. iconUrl: z.string().optional(),
  258. }),
  259. },
  260. 'Action.ShowCard': {
  261. description: 'Shows a card inline',
  262. props: z.object({
  263. title: z.string().optional(),
  264. card: z.unknown(),
  265. iconUrl: z.string().optional(),
  266. }),
  267. },
  268. 'Action.Execute': {
  269. description: 'Universal action for bots',
  270. props: z.object({
  271. title: z.string().optional(),
  272. verb: z.string().optional(),
  273. data: z.unknown().optional(),
  274. iconUrl: z.string().optional(),
  275. }),
  276. },
  277. },
  278. });
  279. ```
  280. ## Building an Adaptive Cards Renderer
  281. Create a renderer that processes Adaptive Cards JSON. See the [A2UI integration](/docs/a2ui) page for a similar pattern. The key is mapping each Adaptive Card element type to a React component, resolving nested `items` and `columns` arrays recursively.
  282. ## Usage Example
  283. Render an Adaptive Card and handle actions:
  284. ```tsx
  285. 'use client';
  286. import { AdaptiveCardRenderer } from './adaptive-card-renderer';
  287. const card = {
  288. type: 'AdaptiveCard' as const,
  289. version: '1.5',
  290. body: [
  291. {
  292. type: 'TextBlock',
  293. text: 'Contact Form',
  294. size: 'large',
  295. weight: 'bolder',
  296. },
  297. {
  298. type: 'Input.Text',
  299. id: 'name',
  300. label: 'Your Name',
  301. placeholder: 'Enter your name',
  302. },
  303. {
  304. type: 'Input.Text',
  305. id: 'message',
  306. label: 'Message',
  307. placeholder: 'Enter your message',
  308. isMultiline: true,
  309. },
  310. ],
  311. actions: [
  312. {
  313. type: 'Action.Submit',
  314. title: 'Send',
  315. data: { action: 'submitForm' },
  316. },
  317. ],
  318. };
  319. export function ContactCard() {
  320. const handleAction = (action: any, inputData: Record<string, unknown>) => {
  321. console.log('Action:', action);
  322. console.log('Input data:', inputData);
  323. // Send to your backend
  324. fetch('/api/submit', {
  325. method: 'POST',
  326. headers: { 'Content-Type': 'application/json' },
  327. body: JSON.stringify({ action, data: inputData }),
  328. });
  329. };
  330. return <AdaptiveCardRenderer card={card} onAction={handleAction} />;
  331. }
  332. ```
  333. ## Handling Action.Execute for Bots
  334. For bot scenarios, handle `Action.Execute` with the verb and data:
  335. ```typescript
  336. interface ActionExecutePayload {
  337. action: {
  338. type: 'Action.Execute';
  339. verb: string;
  340. data?: unknown;
  341. };
  342. inputs: Record<string, unknown>;
  343. }
  344. async function handleBotAction(payload: ActionExecutePayload) {
  345. const response = await fetch('/api/bot/action', {
  346. method: 'POST',
  347. headers: { 'Content-Type': 'application/json' },
  348. body: JSON.stringify({
  349. verb: payload.action.verb,
  350. data: payload.action.data,
  351. inputs: payload.inputs,
  352. }),
  353. });
  354. // Bot may return a new card to render
  355. const result = await response.json();
  356. if (result.card) {
  357. return result.card; // New AdaptiveCard to render
  358. }
  359. }
  360. ```
  361. ## Next
  362. Learn about [A2UI integration](/docs/a2ui) for another agent-driven UI protocol.