page.mdx 11 KB

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