page.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791
  1. import Link from "next/link";
  2. import { Code } from "@/components/code";
  3. export const metadata = {
  4. title: "Adaptive Cards Integration | json-render",
  5. };
  6. export default function AdaptiveCardsPage() {
  7. return (
  8. <article>
  9. <h1 className="text-3xl font-bold mb-4">Adaptive Cards Integration</h1>
  10. <p className="text-muted-foreground mb-8">
  11. Use json-render to render{" "}
  12. <a
  13. href="https://adaptivecards.io"
  14. target="_blank"
  15. rel="noopener noreferrer"
  16. className="text-foreground hover:underline"
  17. >
  18. Microsoft Adaptive Cards
  19. </a>{" "}
  20. natively.
  21. </p>
  22. <div className="rounded-lg border border-amber-500/50 bg-amber-500/10 p-4 mb-8">
  23. <p className="text-sm text-amber-700 dark:text-amber-300">
  24. <strong>Concept:</strong> This page demonstrates how json-render can
  25. support Adaptive Cards. The examples are illustrative and may require
  26. adaptation for production use.
  27. </p>
  28. </div>
  29. <h2 className="text-xl font-semibold mt-12 mb-4">
  30. Adaptive Cards Overview
  31. </h2>
  32. <p className="text-sm text-muted-foreground mb-4">
  33. Adaptive Cards is a JSON-based format for platform-agnostic UI snippets.
  34. Cards have a <code className="text-foreground">body</code> array of
  35. elements and an optional{" "}
  36. <code className="text-foreground">actions</code> array for interactive
  37. buttons.
  38. </p>
  39. <h3 className="text-lg font-medium mt-8 mb-3">Example Adaptive Card</h3>
  40. <Code lang="json">{`{
  41. "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
  42. "type": "AdaptiveCard",
  43. "version": "1.5",
  44. "body": [
  45. {
  46. "type": "TextBlock",
  47. "text": "Hello, Adaptive Cards!",
  48. "size": "large",
  49. "weight": "bolder"
  50. },
  51. {
  52. "type": "Image",
  53. "url": "https://example.com/image.png",
  54. "altText": "Example image"
  55. },
  56. {
  57. "type": "Container",
  58. "items": [
  59. {
  60. "type": "TextBlock",
  61. "text": "This is inside a container",
  62. "wrap": true
  63. }
  64. ]
  65. },
  66. {
  67. "type": "ColumnSet",
  68. "columns": [
  69. {
  70. "type": "Column",
  71. "width": "auto",
  72. "items": [
  73. { "type": "TextBlock", "text": "Column 1" }
  74. ]
  75. },
  76. {
  77. "type": "Column",
  78. "width": "stretch",
  79. "items": [
  80. { "type": "TextBlock", "text": "Column 2" }
  81. ]
  82. }
  83. ]
  84. },
  85. {
  86. "type": "Input.Text",
  87. "id": "userInput",
  88. "placeholder": "Enter your name",
  89. "label": "Name"
  90. }
  91. ],
  92. "actions": [
  93. {
  94. "type": "Action.Submit",
  95. "title": "Submit"
  96. },
  97. {
  98. "type": "Action.OpenUrl",
  99. "title": "Learn More",
  100. "url": "https://adaptivecards.io"
  101. }
  102. ]
  103. }`}</Code>
  104. <h2 className="text-xl font-semibold mt-12 mb-4">
  105. Creating an Adaptive Cards Catalog
  106. </h2>
  107. <p className="text-sm text-muted-foreground mb-4">
  108. Define a catalog matching the Adaptive Cards element types:
  109. </p>
  110. <Code lang="typescript">{`import { createCatalog } from '@json-render/core';
  111. import { z } from 'zod';
  112. // Common Adaptive Cards properties
  113. const Spacing = z.enum(['none', 'small', 'default', 'medium', 'large', 'extraLarge', 'padding']);
  114. const HorizontalAlignment = z.enum(['left', 'center', 'right']);
  115. const VerticalAlignment = z.enum(['top', 'center', 'bottom']);
  116. const FontSize = z.enum(['small', 'default', 'medium', 'large', 'extraLarge']);
  117. const FontWeight = z.enum(['lighter', 'default', 'bolder']);
  118. const ImageSize = z.enum(['auto', 'stretch', 'small', 'medium', 'large']);
  119. const ImageStyle = z.enum(['default', 'person']);
  120. // Base element properties shared by most elements
  121. const BaseElement = {
  122. id: z.string().optional(),
  123. isVisible: z.boolean().optional(),
  124. separator: z.boolean().optional(),
  125. spacing: Spacing.optional(),
  126. };
  127. export const adaptiveCardsCatalog = createCatalog({
  128. components: {
  129. // Root card
  130. AdaptiveCard: {
  131. description: 'Root Adaptive Card container',
  132. props: z.object({
  133. version: z.string(),
  134. body: z.array(z.unknown()).optional(),
  135. actions: z.array(z.unknown()).optional(),
  136. fallbackText: z.string().optional(),
  137. minHeight: z.string().optional(),
  138. rtl: z.boolean().optional(),
  139. verticalContentAlignment: VerticalAlignment.optional(),
  140. }),
  141. },
  142. // Elements
  143. TextBlock: {
  144. description: 'Displays text with formatting options',
  145. props: z.object({
  146. ...BaseElement,
  147. text: z.string(),
  148. color: z.enum(['default', 'dark', 'light', 'accent', 'good', 'warning', 'attention']).optional(),
  149. fontType: z.enum(['default', 'monospace']).optional(),
  150. horizontalAlignment: HorizontalAlignment.optional(),
  151. isSubtle: z.boolean().optional(),
  152. maxLines: z.number().optional(),
  153. size: FontSize.optional(),
  154. weight: FontWeight.optional(),
  155. wrap: z.boolean().optional(),
  156. }),
  157. },
  158. Image: {
  159. description: 'Displays an image',
  160. props: z.object({
  161. ...BaseElement,
  162. url: z.string(),
  163. altText: z.string().optional(),
  164. backgroundColor: z.string().optional(),
  165. height: z.string().optional(),
  166. width: z.string().optional(),
  167. horizontalAlignment: HorizontalAlignment.optional(),
  168. size: ImageSize.optional(),
  169. style: ImageStyle.optional(),
  170. }),
  171. },
  172. Container: {
  173. description: 'Groups elements together',
  174. props: z.object({
  175. ...BaseElement,
  176. items: z.array(z.unknown()),
  177. style: z.enum(['default', 'emphasis', 'good', 'attention', 'warning', 'accent']).optional(),
  178. verticalContentAlignment: VerticalAlignment.optional(),
  179. bleed: z.boolean().optional(),
  180. minHeight: z.string().optional(),
  181. }),
  182. },
  183. ColumnSet: {
  184. description: 'Arranges columns horizontally',
  185. props: z.object({
  186. ...BaseElement,
  187. columns: z.array(z.unknown()),
  188. horizontalAlignment: HorizontalAlignment.optional(),
  189. minHeight: z.string().optional(),
  190. }),
  191. },
  192. Column: {
  193. description: 'A column within a ColumnSet',
  194. props: z.object({
  195. ...BaseElement,
  196. items: z.array(z.unknown()).optional(),
  197. width: z.union([z.string(), z.number()]).optional(),
  198. style: z.enum(['default', 'emphasis', 'good', 'attention', 'warning', 'accent']).optional(),
  199. verticalContentAlignment: VerticalAlignment.optional(),
  200. }),
  201. },
  202. FactSet: {
  203. description: 'Displays a series of facts as key/value pairs',
  204. props: z.object({
  205. ...BaseElement,
  206. facts: z.array(z.object({
  207. title: z.string(),
  208. value: z.string(),
  209. })),
  210. }),
  211. },
  212. ImageSet: {
  213. description: 'Displays a collection of images',
  214. props: z.object({
  215. ...BaseElement,
  216. images: z.array(z.object({
  217. type: z.literal('Image'),
  218. url: z.string(),
  219. altText: z.string().optional(),
  220. })),
  221. imageSize: ImageSize.optional(),
  222. }),
  223. },
  224. ActionSet: {
  225. description: 'Displays a set of actions',
  226. props: z.object({
  227. ...BaseElement,
  228. actions: z.array(z.unknown()),
  229. }),
  230. },
  231. RichTextBlock: {
  232. description: 'Rich text with inline formatting',
  233. props: z.object({
  234. ...BaseElement,
  235. inlines: z.array(z.unknown()),
  236. horizontalAlignment: HorizontalAlignment.optional(),
  237. }),
  238. },
  239. // Inputs
  240. 'Input.Text': {
  241. description: 'Text input field',
  242. props: z.object({
  243. ...BaseElement,
  244. id: z.string(),
  245. isMultiline: z.boolean().optional(),
  246. maxLength: z.number().optional(),
  247. placeholder: z.string().optional(),
  248. label: z.string().optional(),
  249. value: z.string().optional(),
  250. style: z.enum(['text', 'tel', 'url', 'email', 'password']).optional(),
  251. isRequired: z.boolean().optional(),
  252. errorMessage: z.string().optional(),
  253. }),
  254. },
  255. 'Input.Number': {
  256. description: 'Number input field',
  257. props: z.object({
  258. ...BaseElement,
  259. id: z.string(),
  260. max: z.number().optional(),
  261. min: z.number().optional(),
  262. placeholder: z.string().optional(),
  263. label: z.string().optional(),
  264. value: z.number().optional(),
  265. isRequired: z.boolean().optional(),
  266. errorMessage: z.string().optional(),
  267. }),
  268. },
  269. 'Input.Date': {
  270. description: 'Date picker input',
  271. props: z.object({
  272. ...BaseElement,
  273. id: z.string(),
  274. max: z.string().optional(),
  275. min: z.string().optional(),
  276. placeholder: z.string().optional(),
  277. label: z.string().optional(),
  278. value: z.string().optional(),
  279. isRequired: z.boolean().optional(),
  280. }),
  281. },
  282. 'Input.Time': {
  283. description: 'Time picker input',
  284. props: z.object({
  285. ...BaseElement,
  286. id: z.string(),
  287. max: z.string().optional(),
  288. min: z.string().optional(),
  289. placeholder: z.string().optional(),
  290. label: z.string().optional(),
  291. value: z.string().optional(),
  292. isRequired: z.boolean().optional(),
  293. }),
  294. },
  295. 'Input.Toggle': {
  296. description: 'Toggle/checkbox input',
  297. props: z.object({
  298. ...BaseElement,
  299. id: z.string(),
  300. title: z.string(),
  301. label: z.string().optional(),
  302. value: z.string().optional(),
  303. valueOff: z.string().optional(),
  304. valueOn: z.string().optional(),
  305. isRequired: z.boolean().optional(),
  306. }),
  307. },
  308. 'Input.ChoiceSet': {
  309. description: 'Dropdown or radio/checkbox group',
  310. props: z.object({
  311. ...BaseElement,
  312. id: z.string(),
  313. choices: z.array(z.object({
  314. title: z.string(),
  315. value: z.string(),
  316. })),
  317. isMultiSelect: z.boolean().optional(),
  318. style: z.enum(['compact', 'expanded']).optional(),
  319. label: z.string().optional(),
  320. value: z.string().optional(),
  321. placeholder: z.string().optional(),
  322. isRequired: z.boolean().optional(),
  323. }),
  324. },
  325. // Actions
  326. 'Action.OpenUrl': {
  327. description: 'Opens a URL',
  328. props: z.object({
  329. title: z.string().optional(),
  330. url: z.string(),
  331. iconUrl: z.string().optional(),
  332. }),
  333. },
  334. 'Action.Submit': {
  335. description: 'Submits input data',
  336. props: z.object({
  337. title: z.string().optional(),
  338. data: z.unknown().optional(),
  339. iconUrl: z.string().optional(),
  340. }),
  341. },
  342. 'Action.ShowCard': {
  343. description: 'Shows a card inline',
  344. props: z.object({
  345. title: z.string().optional(),
  346. card: z.unknown(),
  347. iconUrl: z.string().optional(),
  348. }),
  349. },
  350. 'Action.ToggleVisibility': {
  351. description: 'Toggles visibility of elements',
  352. props: z.object({
  353. title: z.string().optional(),
  354. targetElements: z.array(z.union([
  355. z.string(),
  356. z.object({ elementId: z.string(), isVisible: z.boolean().optional() }),
  357. ])),
  358. iconUrl: z.string().optional(),
  359. }),
  360. },
  361. 'Action.Execute': {
  362. description: 'Universal action for bots',
  363. props: z.object({
  364. title: z.string().optional(),
  365. verb: z.string().optional(),
  366. data: z.unknown().optional(),
  367. iconUrl: z.string().optional(),
  368. }),
  369. },
  370. },
  371. });`}</Code>
  372. <h2 className="text-xl font-semibold mt-12 mb-4">
  373. Building an Adaptive Cards Renderer
  374. </h2>
  375. <p className="text-sm text-muted-foreground mb-4">
  376. Create a renderer that processes Adaptive Cards JSON:
  377. </p>
  378. <Code lang="tsx">{`'use client';
  379. import React from 'react';
  380. interface AdaptiveCardElement {
  381. type: string;
  382. [key: string]: unknown;
  383. }
  384. interface AdaptiveCard {
  385. type: 'AdaptiveCard';
  386. version: string;
  387. body?: AdaptiveCardElement[];
  388. actions?: AdaptiveCardElement[];
  389. }
  390. interface RenderContext {
  391. onAction: (action: AdaptiveCardElement, data: Record<string, unknown>) => void;
  392. inputs: Record<string, unknown>;
  393. setInput: (id: string, value: unknown) => void;
  394. }
  395. // Widget registry for Adaptive Cards elements
  396. const widgets: Record<string, React.FC<any>> = {
  397. TextBlock: ({ text, size, weight, color, isSubtle, wrap, horizontalAlignment }) => {
  398. const sizeClass = {
  399. small: 'text-xs',
  400. default: 'text-sm',
  401. medium: 'text-base',
  402. large: 'text-lg',
  403. extraLarge: 'text-2xl',
  404. }[size || 'default'];
  405. const weightClass = {
  406. lighter: 'font-light',
  407. default: 'font-normal',
  408. bolder: 'font-bold',
  409. }[weight || 'default'];
  410. const alignClass = {
  411. left: 'text-left',
  412. center: 'text-center',
  413. right: 'text-right',
  414. }[horizontalAlignment || 'left'];
  415. return (
  416. <p className={\`\${sizeClass} \${weightClass} \${alignClass} \${isSubtle ? 'text-muted-foreground' : ''} \${wrap !== false ? '' : 'truncate'}\`}>
  417. {text}
  418. </p>
  419. );
  420. },
  421. Image: ({ url, altText, size, style, horizontalAlignment }) => {
  422. const sizeClass = {
  423. auto: '',
  424. stretch: 'w-full',
  425. small: 'w-16',
  426. medium: 'w-32',
  427. large: 'w-48',
  428. }[size || 'auto'];
  429. return (
  430. <div className={\`flex \${horizontalAlignment === 'center' ? 'justify-center' : horizontalAlignment === 'right' ? 'justify-end' : ''}\`}>
  431. <img
  432. src={url}
  433. alt={altText || ''}
  434. className={\`\${sizeClass} \${style === 'person' ? 'rounded-full' : ''}\`}
  435. />
  436. </div>
  437. );
  438. },
  439. Container: ({ items, style, children, ctx }) => {
  440. const styleClass = {
  441. default: '',
  442. emphasis: 'bg-muted p-2 rounded',
  443. good: 'bg-green-50 p-2 rounded',
  444. attention: 'bg-red-50 p-2 rounded',
  445. warning: 'bg-yellow-50 p-2 rounded',
  446. accent: 'bg-blue-50 p-2 rounded',
  447. }[style || 'default'];
  448. return (
  449. <div className={\`\${styleClass} space-y-2\`}>
  450. {children || items?.map((item: any, i: number) => (
  451. <AdaptiveElement key={i} element={item} ctx={ctx} />
  452. ))}
  453. </div>
  454. );
  455. },
  456. ColumnSet: ({ columns, ctx }) => (
  457. <div className="flex gap-2">
  458. {columns?.map((col: any, i: number) => (
  459. <AdaptiveElement key={i} element={{ ...col, type: 'Column' }} ctx={ctx} />
  460. ))}
  461. </div>
  462. ),
  463. Column: ({ items, width, style, ctx }) => {
  464. const widthClass = width === 'auto' ? 'flex-none' :
  465. width === 'stretch' ? 'flex-1' :
  466. typeof width === 'number' ? \`flex-[\${width}]\` : 'flex-1';
  467. return (
  468. <div className={\`\${widthClass} space-y-2\`}>
  469. {items?.map((item: any, i: number) => (
  470. <AdaptiveElement key={i} element={item} ctx={ctx} />
  471. ))}
  472. </div>
  473. );
  474. },
  475. FactSet: ({ facts }) => (
  476. <div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
  477. {facts?.map((fact: any, i: number) => (
  478. <React.Fragment key={i}>
  479. <span className="font-medium">{fact.title}</span>
  480. <span>{fact.value}</span>
  481. </React.Fragment>
  482. ))}
  483. </div>
  484. ),
  485. ActionSet: ({ actions, ctx }) => (
  486. <div className="flex gap-2 pt-2">
  487. {actions?.map((action: any, i: number) => (
  488. <AdaptiveElement key={i} element={action} ctx={ctx} />
  489. ))}
  490. </div>
  491. ),
  492. 'Input.Text': ({ id, placeholder, label, isMultiline, value, ctx }) => (
  493. <div className="space-y-1">
  494. {label && <label className="text-sm font-medium">{label}</label>}
  495. {isMultiline ? (
  496. <textarea
  497. className="w-full px-3 py-2 border rounded text-sm"
  498. placeholder={placeholder}
  499. defaultValue={value}
  500. onChange={(e) => ctx.setInput(id, e.target.value)}
  501. />
  502. ) : (
  503. <input
  504. type="text"
  505. className="w-full px-3 py-2 border rounded text-sm"
  506. placeholder={placeholder}
  507. defaultValue={value}
  508. onChange={(e) => ctx.setInput(id, e.target.value)}
  509. />
  510. )}
  511. </div>
  512. ),
  513. 'Input.Number': ({ id, placeholder, label, min, max, value, ctx }) => (
  514. <div className="space-y-1">
  515. {label && <label className="text-sm font-medium">{label}</label>}
  516. <input
  517. type="number"
  518. className="w-full px-3 py-2 border rounded text-sm"
  519. placeholder={placeholder}
  520. min={min}
  521. max={max}
  522. defaultValue={value}
  523. onChange={(e) => ctx.setInput(id, parseFloat(e.target.value))}
  524. />
  525. </div>
  526. ),
  527. 'Input.Toggle': ({ id, title, label, valueOn = 'true', valueOff = 'false', value, ctx }) => (
  528. <div className="flex items-center gap-2">
  529. <input
  530. type="checkbox"
  531. id={id}
  532. defaultChecked={value === valueOn}
  533. onChange={(e) => ctx.setInput(id, e.target.checked ? valueOn : valueOff)}
  534. />
  535. <label htmlFor={id} className="text-sm">{title || label}</label>
  536. </div>
  537. ),
  538. 'Input.ChoiceSet': ({ id, choices, isMultiSelect, style, label, placeholder, ctx }) => (
  539. <div className="space-y-1">
  540. {label && <label className="text-sm font-medium">{label}</label>}
  541. {style === 'expanded' ? (
  542. <div className="space-y-1">
  543. {choices?.map((choice: any, i: number) => (
  544. <label key={i} className="flex items-center gap-2 text-sm">
  545. <input
  546. type={isMultiSelect ? 'checkbox' : 'radio'}
  547. name={id}
  548. value={choice.value}
  549. onChange={(e) => ctx.setInput(id, e.target.value)}
  550. />
  551. {choice.title}
  552. </label>
  553. ))}
  554. </div>
  555. ) : (
  556. <select
  557. className="w-full px-3 py-2 border rounded text-sm"
  558. onChange={(e) => ctx.setInput(id, e.target.value)}
  559. >
  560. {placeholder && <option value="">{placeholder}</option>}
  561. {choices?.map((choice: any, i: number) => (
  562. <option key={i} value={choice.value}>{choice.title}</option>
  563. ))}
  564. </select>
  565. )}
  566. </div>
  567. ),
  568. 'Action.Submit': ({ title, data, ctx }) => (
  569. <button
  570. className="px-4 py-2 bg-primary text-primary-foreground rounded text-sm"
  571. onClick={() => ctx.onAction({ type: 'Action.Submit', data }, ctx.inputs)}
  572. >
  573. {title || 'Submit'}
  574. </button>
  575. ),
  576. 'Action.OpenUrl': ({ title, url }) => (
  577. <a
  578. href={url}
  579. target="_blank"
  580. rel="noopener noreferrer"
  581. className="px-4 py-2 border rounded text-sm hover:bg-muted"
  582. >
  583. {title || 'Open'}
  584. </a>
  585. ),
  586. 'Action.Execute': ({ title, verb, data, ctx }) => (
  587. <button
  588. className="px-4 py-2 bg-primary text-primary-foreground rounded text-sm"
  589. onClick={() => ctx.onAction({ type: 'Action.Execute', verb, data }, ctx.inputs)}
  590. >
  591. {title || 'Execute'}
  592. </button>
  593. ),
  594. };
  595. function AdaptiveElement({ element, ctx }: { element: AdaptiveCardElement; ctx: RenderContext }) {
  596. const Widget = widgets[element.type];
  597. if (!Widget) {
  598. console.warn(\`Unknown Adaptive Card element: \${element.type}\`);
  599. return null;
  600. }
  601. return <Widget {...element} ctx={ctx} />;
  602. }
  603. export function AdaptiveCardRenderer({
  604. card,
  605. onAction,
  606. }: {
  607. card: AdaptiveCard;
  608. onAction?: (action: AdaptiveCardElement, data: Record<string, unknown>) => void;
  609. }) {
  610. const [inputs, setInputs] = React.useState<Record<string, unknown>>({});
  611. const ctx: RenderContext = {
  612. onAction: onAction || (() => {}),
  613. inputs,
  614. setInput: (id, value) => setInputs((prev) => ({ ...prev, [id]: value })),
  615. };
  616. return (
  617. <div className="rounded-lg border p-4 space-y-3 max-w-md">
  618. {card.body?.map((element, i) => (
  619. <AdaptiveElement key={i} element={element} ctx={ctx} />
  620. ))}
  621. {card.actions && card.actions.length > 0 && (
  622. <div className="flex gap-2 pt-2 border-t">
  623. {card.actions.map((action, i) => (
  624. <AdaptiveElement key={i} element={action} ctx={ctx} />
  625. ))}
  626. </div>
  627. )}
  628. </div>
  629. );
  630. }`}</Code>
  631. <h2 className="text-xl font-semibold mt-12 mb-4">Usage Example</h2>
  632. <p className="text-sm text-muted-foreground mb-4">
  633. Render an Adaptive Card and handle actions:
  634. </p>
  635. <Code lang="tsx">{`'use client';
  636. import { AdaptiveCardRenderer } from './adaptive-card-renderer';
  637. const card = {
  638. type: 'AdaptiveCard' as const,
  639. version: '1.5',
  640. body: [
  641. {
  642. type: 'TextBlock',
  643. text: 'Contact Form',
  644. size: 'large',
  645. weight: 'bolder',
  646. },
  647. {
  648. type: 'Input.Text',
  649. id: 'name',
  650. label: 'Your Name',
  651. placeholder: 'Enter your name',
  652. },
  653. {
  654. type: 'Input.Text',
  655. id: 'message',
  656. label: 'Message',
  657. placeholder: 'Enter your message',
  658. isMultiline: true,
  659. },
  660. ],
  661. actions: [
  662. {
  663. type: 'Action.Submit',
  664. title: 'Send',
  665. data: { action: 'submitForm' },
  666. },
  667. ],
  668. };
  669. export function ContactCard() {
  670. const handleAction = (action: any, inputData: Record<string, unknown>) => {
  671. console.log('Action:', action);
  672. console.log('Input data:', inputData);
  673. // Send to your backend
  674. fetch('/api/submit', {
  675. method: 'POST',
  676. headers: { 'Content-Type': 'application/json' },
  677. body: JSON.stringify({ action, data: inputData }),
  678. });
  679. };
  680. return <AdaptiveCardRenderer card={card} onAction={handleAction} />;
  681. }`}</Code>
  682. <h2 className="text-xl font-semibold mt-12 mb-4">
  683. Handling Action.Execute for Bots
  684. </h2>
  685. <p className="text-sm text-muted-foreground mb-4">
  686. For bot scenarios, handle{" "}
  687. <code className="text-foreground">Action.Execute</code> with the verb
  688. and data:
  689. </p>
  690. <Code lang="typescript">{`interface ActionExecutePayload {
  691. action: {
  692. type: 'Action.Execute';
  693. verb: string;
  694. data?: unknown;
  695. };
  696. inputs: Record<string, unknown>;
  697. }
  698. async function handleBotAction(payload: ActionExecutePayload) {
  699. const response = await fetch('/api/bot/action', {
  700. method: 'POST',
  701. headers: { 'Content-Type': 'application/json' },
  702. body: JSON.stringify({
  703. verb: payload.action.verb,
  704. data: payload.action.data,
  705. inputs: payload.inputs,
  706. }),
  707. });
  708. // Bot may return a new card to render
  709. const result = await response.json();
  710. if (result.card) {
  711. return result.card; // New AdaptiveCard to render
  712. }
  713. }`}</Code>
  714. <h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
  715. <p className="text-sm text-muted-foreground">
  716. Learn about{" "}
  717. <Link href="/docs/a2ui" className="text-foreground hover:underline">
  718. A2UI integration
  719. </Link>{" "}
  720. for another agent-driven UI protocol.
  721. </p>
  722. </article>
  723. );
  724. }