page.mdx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  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 { defineCatalog } from '@json-render/core';
  82. import { schema } from '@json-render/react';
  83. import { z } from 'zod';
  84. // Common Adaptive Cards properties
  85. const Spacing = z.enum(['none', 'small', 'default', 'medium', 'large', 'extraLarge', 'padding']);
  86. const HorizontalAlignment = z.enum(['left', 'center', 'right']);
  87. const VerticalAlignment = z.enum(['top', 'center', 'bottom']);
  88. const FontSize = z.enum(['small', 'default', 'medium', 'large', 'extraLarge']);
  89. const FontWeight = z.enum(['lighter', 'default', 'bolder']);
  90. const ImageSize = z.enum(['auto', 'stretch', 'small', 'medium', 'large']);
  91. const ImageStyle = z.enum(['default', 'person']);
  92. // Base element properties shared by most elements
  93. const BaseElement = {
  94. id: z.string().optional(),
  95. isVisible: z.boolean().optional(),
  96. separator: z.boolean().optional(),
  97. spacing: Spacing.optional(),
  98. };
  99. export const adaptiveCardsCatalog = defineCatalog(schema, {
  100. components: {
  101. // Root card
  102. AdaptiveCard: {
  103. description: 'Root Adaptive Card container',
  104. props: z.object({
  105. version: z.string(),
  106. body: z.array(z.unknown()).optional(),
  107. actions: z.array(z.unknown()).optional(),
  108. fallbackText: z.string().optional(),
  109. minHeight: z.string().optional(),
  110. rtl: z.boolean().optional(),
  111. verticalContentAlignment: VerticalAlignment.optional(),
  112. }),
  113. },
  114. // Elements
  115. TextBlock: {
  116. description: 'Displays text with formatting options',
  117. props: z.object({
  118. ...BaseElement,
  119. text: z.string(),
  120. color: z.enum(['default', 'dark', 'light', 'accent', 'good', 'warning', 'attention']).optional(),
  121. fontType: z.enum(['default', 'monospace']).optional(),
  122. horizontalAlignment: HorizontalAlignment.optional(),
  123. isSubtle: z.boolean().optional(),
  124. maxLines: z.number().optional(),
  125. size: FontSize.optional(),
  126. weight: FontWeight.optional(),
  127. wrap: z.boolean().optional(),
  128. }),
  129. },
  130. Image: {
  131. description: 'Displays an image',
  132. props: z.object({
  133. ...BaseElement,
  134. url: z.string(),
  135. altText: z.string().optional(),
  136. backgroundColor: z.string().optional(),
  137. height: z.string().optional(),
  138. width: z.string().optional(),
  139. horizontalAlignment: HorizontalAlignment.optional(),
  140. size: ImageSize.optional(),
  141. style: ImageStyle.optional(),
  142. }),
  143. },
  144. Container: {
  145. description: 'Groups elements together',
  146. props: z.object({
  147. ...BaseElement,
  148. items: z.array(z.unknown()),
  149. style: z.enum(['default', 'emphasis', 'good', 'attention', 'warning', 'accent']).optional(),
  150. verticalContentAlignment: VerticalAlignment.optional(),
  151. bleed: z.boolean().optional(),
  152. minHeight: z.string().optional(),
  153. }),
  154. },
  155. ColumnSet: {
  156. description: 'Arranges columns horizontally',
  157. props: z.object({
  158. ...BaseElement,
  159. columns: z.array(z.unknown()),
  160. horizontalAlignment: HorizontalAlignment.optional(),
  161. minHeight: z.string().optional(),
  162. }),
  163. },
  164. Column: {
  165. description: 'A column within a ColumnSet',
  166. props: z.object({
  167. ...BaseElement,
  168. items: z.array(z.unknown()).optional(),
  169. width: z.union([z.string(), z.number()]).optional(),
  170. style: z.enum(['default', 'emphasis', 'good', 'attention', 'warning', 'accent']).optional(),
  171. verticalContentAlignment: VerticalAlignment.optional(),
  172. }),
  173. },
  174. FactSet: {
  175. description: 'Displays a series of facts as key/value pairs',
  176. props: z.object({
  177. ...BaseElement,
  178. facts: z.array(z.object({
  179. title: z.string(),
  180. value: z.string(),
  181. })),
  182. }),
  183. },
  184. // Inputs
  185. 'Input.Text': {
  186. description: 'Text input field',
  187. props: z.object({
  188. ...BaseElement,
  189. id: z.string(),
  190. isMultiline: z.boolean().optional(),
  191. maxLength: z.number().optional(),
  192. placeholder: z.string().optional(),
  193. label: z.string().optional(),
  194. value: z.string().optional(),
  195. style: z.enum(['text', 'tel', 'url', 'email', 'password']).optional(),
  196. isRequired: z.boolean().optional(),
  197. errorMessage: z.string().optional(),
  198. }),
  199. },
  200. 'Input.Number': {
  201. description: 'Number input field',
  202. props: z.object({
  203. ...BaseElement,
  204. id: z.string(),
  205. max: z.number().optional(),
  206. min: z.number().optional(),
  207. placeholder: z.string().optional(),
  208. label: z.string().optional(),
  209. value: z.number().optional(),
  210. isRequired: z.boolean().optional(),
  211. errorMessage: z.string().optional(),
  212. }),
  213. },
  214. 'Input.Toggle': {
  215. description: 'Toggle/checkbox input',
  216. props: z.object({
  217. ...BaseElement,
  218. id: z.string(),
  219. title: z.string(),
  220. label: z.string().optional(),
  221. value: z.string().optional(),
  222. valueOff: z.string().optional(),
  223. valueOn: z.string().optional(),
  224. isRequired: z.boolean().optional(),
  225. }),
  226. },
  227. 'Input.ChoiceSet': {
  228. description: 'Dropdown or radio/checkbox group',
  229. props: z.object({
  230. ...BaseElement,
  231. id: z.string(),
  232. choices: z.array(z.object({
  233. title: z.string(),
  234. value: z.string(),
  235. })),
  236. isMultiSelect: z.boolean().optional(),
  237. style: z.enum(['compact', 'expanded']).optional(),
  238. label: z.string().optional(),
  239. value: z.string().optional(),
  240. placeholder: z.string().optional(),
  241. isRequired: z.boolean().optional(),
  242. }),
  243. },
  244. // Actions
  245. 'Action.OpenUrl': {
  246. description: 'Opens a URL',
  247. props: z.object({
  248. title: z.string().optional(),
  249. url: z.string(),
  250. iconUrl: z.string().optional(),
  251. }),
  252. },
  253. 'Action.Submit': {
  254. description: 'Submits input data',
  255. props: z.object({
  256. title: z.string().optional(),
  257. data: z.unknown().optional(),
  258. iconUrl: z.string().optional(),
  259. }),
  260. },
  261. 'Action.ShowCard': {
  262. description: 'Shows a card inline',
  263. props: z.object({
  264. title: z.string().optional(),
  265. card: z.unknown(),
  266. iconUrl: z.string().optional(),
  267. }),
  268. },
  269. 'Action.Execute': {
  270. description: 'Universal action for bots',
  271. props: z.object({
  272. title: z.string().optional(),
  273. verb: z.string().optional(),
  274. data: z.unknown().optional(),
  275. iconUrl: z.string().optional(),
  276. }),
  277. },
  278. },
  279. });
  280. ```
  281. ## Building an Adaptive Cards Renderer
  282. 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.
  283. ## Usage Example
  284. Render an Adaptive Card and handle actions:
  285. ```tsx
  286. 'use client';
  287. import { AdaptiveCardRenderer } from './adaptive-card-renderer';
  288. const card = {
  289. type: 'AdaptiveCard' as const,
  290. version: '1.5',
  291. body: [
  292. {
  293. type: 'TextBlock',
  294. text: 'Contact Form',
  295. size: 'large',
  296. weight: 'bolder',
  297. },
  298. {
  299. type: 'Input.Text',
  300. id: 'name',
  301. label: 'Your Name',
  302. placeholder: 'Enter your name',
  303. },
  304. {
  305. type: 'Input.Text',
  306. id: 'message',
  307. label: 'Message',
  308. placeholder: 'Enter your message',
  309. isMultiline: true,
  310. },
  311. ],
  312. actions: [
  313. {
  314. type: 'Action.Submit',
  315. title: 'Send',
  316. data: { action: 'submitForm' },
  317. },
  318. ],
  319. };
  320. export function ContactCard() {
  321. const handleAction = (action: any, inputData: Record<string, unknown>) => {
  322. console.log('Action:', action);
  323. console.log('Input data:', inputData);
  324. // Send to your backend
  325. fetch('/api/submit', {
  326. method: 'POST',
  327. headers: { 'Content-Type': 'application/json' },
  328. body: JSON.stringify({ action, data: inputData }),
  329. });
  330. };
  331. return <AdaptiveCardRenderer card={card} onAction={handleAction} />;
  332. }
  333. ```
  334. ## Handling Action.Execute for Bots
  335. For bot scenarios, handle `Action.Execute` with the verb and data:
  336. ```typescript
  337. interface ActionExecutePayload {
  338. action: {
  339. type: 'Action.Execute';
  340. verb: string;
  341. data?: unknown;
  342. };
  343. inputs: Record<string, unknown>;
  344. }
  345. async function handleBotAction(payload: ActionExecutePayload) {
  346. const response = await fetch('/api/bot/action', {
  347. method: 'POST',
  348. headers: { 'Content-Type': 'application/json' },
  349. body: JSON.stringify({
  350. verb: payload.action.verb,
  351. data: payload.action.data,
  352. inputs: payload.inputs,
  353. }),
  354. });
  355. // Bot may return a new card to render
  356. const result = await response.json();
  357. if (result.card) {
  358. return result.card; // New AdaptiveCard to render
  359. }
  360. }
  361. ```
  362. ## Next
  363. Learn about [A2UI integration](/docs/a2ui) for another agent-driven UI protocol.