import { pageMetadata } from "@/lib/page-metadata" export const metadata = pageMetadata("docs/adaptive-cards") # Adaptive Cards Integration Use json-render to render [Microsoft Adaptive Cards](https://adaptivecards.io) natively.

Concept: This page demonstrates how json-render can support Adaptive Cards. The examples are illustrative and may require adaptation for production use.

## Adaptive Cards Overview 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. ### Example Adaptive Card ```json { "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.5", "body": [ { "type": "TextBlock", "text": "Hello, Adaptive Cards!", "size": "large", "weight": "bolder" }, { "type": "Image", "url": "https://example.com/image.png", "altText": "Example image" }, { "type": "Container", "items": [ { "type": "TextBlock", "text": "This is inside a container", "wrap": true } ] }, { "type": "ColumnSet", "columns": [ { "type": "Column", "width": "auto", "items": [ { "type": "TextBlock", "text": "Column 1" } ] }, { "type": "Column", "width": "stretch", "items": [ { "type": "TextBlock", "text": "Column 2" } ] } ] }, { "type": "Input.Text", "id": "userInput", "placeholder": "Enter your name", "label": "Name" } ], "actions": [ { "type": "Action.Submit", "title": "Submit" }, { "type": "Action.OpenUrl", "title": "Learn More", "url": "https://adaptivecards.io" } ] } ``` ## Creating an Adaptive Cards Catalog Define a catalog matching the Adaptive Cards element types: ```typescript import { defineCatalog } from '@json-render/core'; import { schema } from '@json-render/react/schema'; import { z } from 'zod'; // Common Adaptive Cards properties const Spacing = z.enum(['none', 'small', 'default', 'medium', 'large', 'extraLarge', 'padding']); const HorizontalAlignment = z.enum(['left', 'center', 'right']); const VerticalAlignment = z.enum(['top', 'center', 'bottom']); const FontSize = z.enum(['small', 'default', 'medium', 'large', 'extraLarge']); const FontWeight = z.enum(['lighter', 'default', 'bolder']); const ImageSize = z.enum(['auto', 'stretch', 'small', 'medium', 'large']); const ImageStyle = z.enum(['default', 'person']); // Base element properties shared by most elements const BaseElement = { id: z.string().optional(), isVisible: z.boolean().optional(), separator: z.boolean().optional(), spacing: Spacing.optional(), }; export const adaptiveCardsCatalog = defineCatalog(schema, { components: { // Root card AdaptiveCard: { description: 'Root Adaptive Card container', props: z.object({ version: z.string(), body: z.array(z.unknown()).optional(), actions: z.array(z.unknown()).optional(), fallbackText: z.string().optional(), minHeight: z.string().optional(), rtl: z.boolean().optional(), verticalContentAlignment: VerticalAlignment.optional(), }), }, // Elements TextBlock: { description: 'Displays text with formatting options', props: z.object({ ...BaseElement, text: z.string(), color: z.enum(['default', 'dark', 'light', 'accent', 'good', 'warning', 'attention']).optional(), fontType: z.enum(['default', 'monospace']).optional(), horizontalAlignment: HorizontalAlignment.optional(), isSubtle: z.boolean().optional(), maxLines: z.number().optional(), size: FontSize.optional(), weight: FontWeight.optional(), wrap: z.boolean().optional(), }), }, Image: { description: 'Displays an image', props: z.object({ ...BaseElement, url: z.string(), altText: z.string().optional(), backgroundColor: z.string().optional(), height: z.string().optional(), width: z.string().optional(), horizontalAlignment: HorizontalAlignment.optional(), size: ImageSize.optional(), style: ImageStyle.optional(), }), }, Container: { description: 'Groups elements together', props: z.object({ ...BaseElement, items: z.array(z.unknown()), style: z.enum(['default', 'emphasis', 'good', 'attention', 'warning', 'accent']).optional(), verticalContentAlignment: VerticalAlignment.optional(), bleed: z.boolean().optional(), minHeight: z.string().optional(), }), }, ColumnSet: { description: 'Arranges columns horizontally', props: z.object({ ...BaseElement, columns: z.array(z.unknown()), horizontalAlignment: HorizontalAlignment.optional(), minHeight: z.string().optional(), }), }, Column: { description: 'A column within a ColumnSet', props: z.object({ ...BaseElement, items: z.array(z.unknown()).optional(), width: z.union([z.string(), z.number()]).optional(), style: z.enum(['default', 'emphasis', 'good', 'attention', 'warning', 'accent']).optional(), verticalContentAlignment: VerticalAlignment.optional(), }), }, FactSet: { description: 'Displays a series of facts as key/value pairs', props: z.object({ ...BaseElement, facts: z.array(z.object({ title: z.string(), value: z.string(), })), }), }, // Inputs 'Input.Text': { description: 'Text input field', props: z.object({ ...BaseElement, id: z.string(), isMultiline: z.boolean().optional(), maxLength: z.number().optional(), placeholder: z.string().optional(), label: z.string().optional(), value: z.string().optional(), style: z.enum(['text', 'tel', 'url', 'email', 'password']).optional(), isRequired: z.boolean().optional(), errorMessage: z.string().optional(), }), }, 'Input.Number': { description: 'Number input field', props: z.object({ ...BaseElement, id: z.string(), max: z.number().optional(), min: z.number().optional(), placeholder: z.string().optional(), label: z.string().optional(), value: z.number().optional(), isRequired: z.boolean().optional(), errorMessage: z.string().optional(), }), }, 'Input.Toggle': { description: 'Toggle/checkbox input', props: z.object({ ...BaseElement, id: z.string(), title: z.string(), label: z.string().optional(), value: z.string().optional(), valueOff: z.string().optional(), valueOn: z.string().optional(), isRequired: z.boolean().optional(), }), }, 'Input.ChoiceSet': { description: 'Dropdown or radio/checkbox group', props: z.object({ ...BaseElement, id: z.string(), choices: z.array(z.object({ title: z.string(), value: z.string(), })), isMultiSelect: z.boolean().optional(), style: z.enum(['compact', 'expanded']).optional(), label: z.string().optional(), value: z.string().optional(), placeholder: z.string().optional(), isRequired: z.boolean().optional(), }), }, // Actions 'Action.OpenUrl': { description: 'Opens a URL', props: z.object({ title: z.string().optional(), url: z.string(), iconUrl: z.string().optional(), }), }, 'Action.Submit': { description: 'Submits input data', props: z.object({ title: z.string().optional(), data: z.unknown().optional(), iconUrl: z.string().optional(), }), }, 'Action.ShowCard': { description: 'Shows a card inline', props: z.object({ title: z.string().optional(), card: z.unknown(), iconUrl: z.string().optional(), }), }, 'Action.Execute': { description: 'Universal action for bots', props: z.object({ title: z.string().optional(), verb: z.string().optional(), data: z.unknown().optional(), iconUrl: z.string().optional(), }), }, }, }); ``` ## Building an Adaptive Cards Renderer 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. ## Usage Example Render an Adaptive Card and handle actions: ```tsx 'use client'; import { AdaptiveCardRenderer } from './adaptive-card-renderer'; const card = { type: 'AdaptiveCard' as const, version: '1.5', body: [ { type: 'TextBlock', text: 'Contact Form', size: 'large', weight: 'bolder', }, { type: 'Input.Text', id: 'name', label: 'Your Name', placeholder: 'Enter your name', }, { type: 'Input.Text', id: 'message', label: 'Message', placeholder: 'Enter your message', isMultiline: true, }, ], actions: [ { type: 'Action.Submit', title: 'Send', data: { action: 'submitForm' }, }, ], }; export function ContactCard() { const handleAction = (action: any, inputData: Record) => { console.log('Action:', action); console.log('Input data:', inputData); // Send to your backend fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, data: inputData }), }); }; return ; } ``` ## Handling Action.Execute for Bots For bot scenarios, handle `Action.Execute` with the verb and data: ```typescript interface ActionExecutePayload { action: { type: 'Action.Execute'; verb: string; data?: unknown; }; inputs: Record; } async function handleBotAction(payload: ActionExecutePayload) { const response = await fetch('/api/bot/action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ verb: payload.action.verb, data: payload.action.data, inputs: payload.inputs, }), }); // Bot may return a new card to render const result = await response.json(); if (result.card) { return result.card; // New AdaptiveCard to render } } ``` ## Next Learn about [A2UI integration](/docs/a2ui) for another agent-driven UI protocol.