| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- export const metadata = { title: "Adaptive Cards Integration" }
- # Adaptive Cards Integration
- Use json-render to render [Microsoft Adaptive Cards](https://adaptivecards.io) natively.
- <div className="rounded-lg border border-amber-500/50 bg-amber-500/10 p-4 mb-8">
- <p className="text-sm text-amber-700 dark:text-amber-300">
- <strong>Concept:</strong> This page demonstrates how json-render can support Adaptive Cards. The examples are illustrative and may require adaptation for production use.
- </p>
- </div>
- ## 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 { createCatalog } from '@json-render/core';
- 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 = createCatalog({
- 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<string, unknown>) => {
- 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 <AdaptiveCardRenderer card={card} onAction={handleAction} />;
- }
- ```
- ## 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<string, unknown>;
- }
- 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.
|