Generative UI

Let the model compose an interface at runtime from a component vocabulary you ship, using the present tool from @assistant-ui/generative-ui.

With generative UI, you ship a vocabulary of components and let the model assemble them, instead of writing one component per tool. The model calls a single present tool whose arguments are a JSON tree of component names and props, and assistant-ui renders that tree.

The package ships a default vocabulary of 35 components (cards, facts, tables, charts, tabs, steps, timelines, citations, diagrams, maps, forms, controls), so the shortest useful setup registers one tool and writes no components at all.

Which generative UI pattern?

assistant-ui uses "generative UI" for more than one thing. Two questions separate them: does the model compose the layout or do you bind it ahead of time, and does the UI originate from a tool call or from a part your backend emits. Pick the row that matches what you are building:

PatternAPIBest for
The present tool (this page)JSONGenerativeUI + presentThe model composes dashboards, cards, and layouts from a vocabulary you ship
Tool UItoolkit renderA widget tied to a tool you already know about (forms, pickers, charts)
Generative UI primitiveMessagePrimitive.GenerativeUI + allowlistA backend that already emits generative-ui message parts
LangGraph data UImakeAssistantDataUI + ui_messageLangGraph agents emitting UI on the LangGraph stream

The first two are tool-driven, so the model decides when UI appears; the last two are backend-driven, so your agent does. The backend-driven pair splits on transport rather than on either axis: the primitive reads a generative-ui message part, while LangGraph data UI reads push_ui_message off the LangGraph stream. present and the primitive both take a JSON component tree, but in non-interchangeable shapes. A third-party option also exists in the tool-driven space: OpenUI publishes an integration that streams OpenUI Lang through the same shape.

To decide between Markdown, present, spec mode, and a model-written widget for an answer, see Choosing an answer format.

Browse every component the default vocabulary offers in the component vocabulary reference, and see finished compositions in the Generative section of Elements.

Quick start

Install the package

npm install @assistant-ui/generative-ui zod

The vocabulary renders as unstyled semantic HTML with data-aui attributes. Add the styled library to get the shipped look:

npx shadcn@latest add @assistant-ui/generative-ui

The @assistant-ui namespace resolves the Radix or Base UI flavor from your project's style through the style-aware registry entry in components.json. Without that entry, add by direct URL instead:

npx shadcn@latest add https://r.assistant-ui.com/base/generative-ui.json

This merges the vocabulary stylesheet and theme variables into your CSS, which is what the rest of this page assumes, and lands components/assistant-ui/elements/generative-ui.tsx. That file exports styledGenerativeUILibrary, which differs from the default vocabulary in three entries: a real markdown renderer, and a drawn Diagram and Map (see Diagrams and maps); Styling wires it in.

Enable the compiler

The "use generative" directive lets one file declare tools that both the browser and your server route can import: the compiler strips the browser-only halves out of the server build and the schemas out of the client build.

next.config.ts
import { withAui } from "@assistant-ui/next";

export default withAui({
  /* your Next config */
});

Vite and TanStack Start use aui() from @assistant-ui/vite; Expo and bare React Native use withAui from @assistant-ui/metro.

Expose the vocabulary as a tool

JSONGenerativeUI turns a component library into the model-facing schema for present. Register the result on a toolkit like any other tool.

app/toolkit.tsx
"use generative";

import { defineToolkit } from "@assistant-ui/react";
import {
  JSONGenerativeUI,
  defaultGenerativeUILibrary,
} from "@assistant-ui/generative-ui/react";

const generative = new JSONGenerativeUI({
  library: defaultGenerativeUILibrary,
});

export default defineToolkit({
  present: generative.present({ display: "standalone" }),
});

display: "standalone" renders the result on its own surface, outside the chain-of-thought trace. Omit it to render inline.

Register the toolkit on the client

app/MyRuntimeProvider.tsx
"use client";

import {
  AssistantRuntimeProvider,
  AuiConfig,
  Tools,
} from "@assistant-ui/react";
import { useChatRuntime } from "@assistant-ui/ai-sdk";
import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
import toolkit from "./toolkit";

export function MyRuntimeProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const runtime = useChatRuntime({
    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
  });
  const config = AuiConfig({ tools: Tools({ toolkit }) });

  return (
    <AssistantRuntimeProvider runtime={runtime} config={config}>
      {children}
    </AssistantRuntimeProvider>
  );
}

Warning

present is a frontend tool: it resolves in the browser, and the run only continues once its result is sent back. Without sendAutomaticallyWhen, the UI renders and the conversation then stops.

Serve the schema

The route imports the same toolkit module. The compiler resolves that import to the server build, so only the schemas cross over and no browser code enters your server bundle.

app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { AISDKToolkit } from "@assistant-ui/ai-sdk";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import toolkit from "@/app/toolkit";

const aiToolkit = new AISDKToolkit({ toolkit });

export async function POST(req: Request) {
  const { messages, tools } = await req.json();

  const aiSDKTools = await aiToolkit.tools({ frontend: tools });

  const result = streamText({
    abortSignal: req.signal,
    model: openai("gpt-6-luna"),
    messages: await convertToModelMessages(messages, { tools: aiSDKTools }),
    stopWhen: stepCountIs(10),
    tools: aiSDKTools,
  });

  return result.toUIMessageStreamResponse();
}

stopWhen matters here for the same reason sendAutomaticallyWhen does: rendering the UI is one step, and the model needs another to say anything after it.

Ask for something the vocabulary can express, for example "show me a sales dashboard for the last six months", and the model will answer with a rendered composition. The complete setup runs in examples/with-generative-ui.

What the model emits

Every node is a flat object: _type names the component, children nests, and every other key is a prop.

{
  "_type": "Card",
  "title": "Q3 revenue",
  "children": [
    {
      "_type": "Row",
      "children": [
        { "_type": "Fact", "label": "Bookings", "value": "$1.2M" },
        { "_type": "Fact", "label": "Growth", "value": "+18%" }
      ]
    },
    {
      "_type": "Chart",
      "variant": "bar",
      "showAxis": true,
      "data": [
        { "label": "Jul", "value": 22 },
        { "label": "Aug", "value": 26 },
        { "label": "Sep", "value": 31 }
      ]
    }
  ]
}

_type, _key, and _action are reserved by the framework, as is every key beginning with $ (such as the injected $status), and children follows the JSX convention. The model writes the reserved keys with _ because Anthropic rejects tool schemas with $ in property names; trees built in code, such as converter output and the UIElement type, write them as $type, $key, and $action, and both spellings render the same. Every other key is yours, so a component is free to declare props named type, status, or variant without colliding.

Extending the vocabulary

defineGenerativeComponents adds your own components. Each one declares a zod schema for its props, a description the model reads, and a render function.

app/toolkit.tsx
"use generative";

import { z } from "zod";
import {
  JSONGenerativeUI,
  defaultGenerativeUILibrary,
  defineGenerativeComponents,
} from "@assistant-ui/generative-ui/react";
import { WeatherCard } from "@/components/weather-card";

const generative = new JSONGenerativeUI({
  library: {
    ...defaultGenerativeUILibrary,
    ...defineGenerativeComponents({
      Weather: {
        description: "Show a weather card for a `get_weather` result.",
        properties: z.object({
          id: z.string().describe("The `id` returned by `get_weather`."),
        }),
        render: (props) => <WeatherCard {...props} />,
      },
    }),
  },
});

Set streamProperties: true alongside properties to receive partially-filled props while the model is still writing them. render then sees Partial<P> and an injected $status of "streaming", and the full props once it turns "done". Components opt out by default and render only once their props are complete.

Warning

Inside a "use generative" module, a "use client" module may be referenced only as the render value of an inline defineGenerativeComponents literal. The compiler drops render and the imports only it uses from the server build; referencing such a module from properties, description, a spread, or a top-level constant leaks a client reference into the server graph and breaks schema generation. defaultGenerativeUILibrary is safe on both builds.

Actions

Pass an action registry to let rendered nodes call back into your app. The model puts an _action object on a node, and its type is matched against your handlers.

import {
  JSONGenerativeUI,
  createActionRegistry,
  defaultGenerativeUILibrary,
} from "@assistant-ui/generative-ui/react";

const actions = createActionRegistry({
  purchase: async ({ payload }) => {
    await checkout(payload);
  },
});

const generative = new JSONGenerativeUI({
  library: defaultGenerativeUILibrary,
  actions,
});
{
  "_type": "Button",
  "label": "Buy",
  "_action": { "type": "purchase", "sku": "pro-plan" }
}

Undo window

Set undoable for sends and other hard-to-reverse actions. The button waits five seconds, shows Undo with a countdown, and another click or Escape cancels it.

{
  "_type": "Button",
  "label": "Delete order",
  "undoable": true,
  "_action": { "type": "delete_order", "orderId": "48213" }
}

Select, Input, DatePicker, Checkbox, Slider, RadioGroup, and CheckboxGroup add the user's value as $input when they fire the action; Slider sends a number and a CheckboxGroup sends the array of its checked option values. Form, and a Card with asForm set, add an object keyed by each control's name instead. On a Card the collected object is dispatched through confirm._action, or through the card's own _action when confirm has none, while cancel._action always fires without $input. Unknown action types are ignored and warn in development.

Slider takes min, max, an optional step, defaultValue, and unit; it shows the live value and commits only when the adjustment finishes. Set Checkbox variant to "switch" for a switch presentation without changing its boolean value. RadioGroup and CheckboxGroup options can add a short description, rendered under the label.

Any value inside _action can be a { "$field": name } reference instead, which resolves to a control's current value when the action fires. It reads the control with that name in the nearest enclosing Form or Card with asForm, or else anywhere in the same JSONGenerativeUI render, collected the way a Form collects it. When no control holds a value, a reference written as { "$field": name, "fallback": value } resolves to its fallback, and one without a fallback is dropped. A component outside the default vocabulary that dispatches $action itself reads no control, so each reference in it resolves the same way. Such a component can resolve references against its own values before it dispatches: resolveFieldReferences(action, fields) from @assistant-ui/generative-ui replaces each reference with the matching value in fields, falling back the same way, and hasFieldReference(action) tells whether an action carries any. A tree rendered with renderGenerativeUI directly resolves references only inside one of those forms or an element marked data-aui="root", so they never read a host page's own form. Input and Select take a defaultValue, so a button can send an edited value without a form:

{
  "_type": "Col",
  "children": [
    {
      "_type": "Input",
      "name": "note",
      "label": "Delivery note",
      "defaultValue": "Leave at the door"
    },
    {
      "_type": "Button",
      "label": "Save",
      "_action": { "type": "save_note", "note": { "$field": "note" } }
    }
  ]
}

On Teams, decodeSubmitData resolves these references from the card's inputs. On Slack, pass the interaction payload's state.values alongside its action to decodeBlockAction to resolve references from named controls; a missing field uses its fallback when provided.

Input accepts an optional inputType of "text", "password", or "number" for a single line control, defaulting to text. A number input uses step="any" and keeps its value as a string, including decimal values such as "12.5" and "" when cleared. Setting multiline renders a textarea instead.

DatePicker accepts inputType "date" (the default), "time", or "datetime". Its own _action runs once per committed value: a pick from the native picker commits at once, and a typed value commits on blur or on Enter outside a form. Date values use YYYY-MM-DD; time values use HH:mm or HH:mm:ss; datetime values use YYYY-MM-DDTHH:mm with optional seconds, fractional seconds, and an offset (Z or ±HH:mm). A datetime with an offset displays in the viewer's time zone and submits with the same offset; one without an offset submits unchanged. The A2UI converter maps DateTimeInput to the matching type, and uses a text Input when its value cannot fit that type. A single choice A2UI ChoicePicker maps to RadioGroup even for a chips display hint, preserving an empty selection as [] and a selected value as a one item string array; multipleSelection maps to CheckboxGroup.

A control resets to its initial value (defaultValue, defaultChecked, or a DatePicker's value) whenever a re-render changes that value, and otherwise keeps what the user entered.

Each renderGenerativeUI or JSONGenerativeUI render scopes native radio names, so separate cards can reuse a logical name without clearing each other's selection. Inside the same render and form scope, radio groups with the same logical name share one selection. The logical name remains the key for form values and $field references. If you mount multiple independent React apps on the same page, configure distinct identifierPrefix values and use each app's same prefix for server rendering and hydration.

Each dispatched action, including its $input, is recorded on the tool call on a best-effort basis. Recording depends on the runtime and the interaction log's limits, and it never delays or prevents dispatch. When the UI is registered through promptUser(), a handler's non-undefined return value becomes the prompt_user tool result only when that call has no result yet. Whether the run then continues depends on the runtime, including how it treats human tools. present actions never create a tool result.

Once answered, a prompt_user locks its controls and shows the submitted values. Only values submitted through a Form or a Card with asForm are restored after a reload.

Without a registry, the tree still renders and model-emitted actions degrade to a no-op.

Spec mode

In generative-frame's spec mode, the model builds the UI as a flat spec, streamed as JSON Patch lines, with state, bindings, and validation. toSpecCatalog from @assistant-ui/generative-ui/spec turns a library into the catalog and components spec mode takes, so the default vocabulary, or a styled library, renders there too. Moving to spec mode maps a present setup onto it. Install generative-frame alongside it:

import { createWidgetToolkit } from "generative-frame/assistant-ui";
import { createSpecToolkit } from "generative-frame/spec/assistant-ui";
import { defaultGenerativeUILibrary } from "@assistant-ui/generative-ui/react";
import { toSpecCatalog } from "@assistant-ui/generative-ui/spec";

const { catalog, components } = toSpecCatalog(defaultGenerativeUILibrary, {
  actions: { purchase: { description: "Buys the plan in `sku`." } },
});

const widgets = createWidgetToolkit({
  spec: createSpecToolkit(catalog, {
    components,
    handlers: { purchase: (params) => checkout(params) },
  }),
});

A spec element binds behavior with on instead of carrying _action. A Button and a ListViewItem emit press, the value controls emit change (an Input emits submit), a Form emits submit, and a Card emits cancel from its cancel button and confirm from its confirm button or, with asForm, whenever it submits. The payload, which { "$event": "" } reads, is the control's value, or every named control's value when a Form emits submit or a Card with asForm emits confirm. A control whose value prop (defaultValue, defaultChecked, or a DatePicker's value) is bound with $bindState follows that state and writes every change back to it, inside a Form too. Declare host actions through actions so the model can bind them; setState is always available.

Your own components take part through the same library entry. Declare the events a component emits and the slots it renders: $dispatch({ type, $input }) emits a declared event, the element's $action carries the first one when the element binds it, default slot content arrives as children and any other slot in $slots, and slots: [] takes no children. A prop the spec binds with $bindState arrives as its current value, and $bindings holds a setter that writes a new one back to state. The present tool ignores events and slots, and present mode passes neither $slots nor $bindings. Declaring either on a vocabulary component replaces its spec events, slots, and description, so give the override its own description too, since the vocabulary's description covers present behavior.

import {
  defaultGenerativeUILibrary,
  type GenerativeUIComponent,
} from "@assistant-ui/generative-ui/react";
import { toSpecCatalog } from "@assistant-ui/generative-ui/spec";
import { z } from "zod";
import { Stars } from "@/components/stars";

const Rating: GenerativeUIComponent<{ value: number }> = {
  description: "A 1 to 5 star rating the user can change.",
  properties: z.object({ value: z.number().describe("Stars, from 1 to 5.") }),
  events: ["rate"],
  slots: [],
  render: ({ value, $bindings, $dispatch }) => (
    <Stars
      value={value}
      onChange={(stars) => {
        $bindings?.value?.(stars);
        $dispatch?.({ type: "rate", $input: stars });
      }}
    />
  ),
};

const { catalog, components } = toSpecCatalog({
  ...defaultGenerativeUILibrary,
  Rating,
});

With Vue

@assistant-ui/generative-ui/vue ships the vocabulary as Vue components for SpecRenderer from generative-frame/spec/vue, with vue as an optional peer. Its toSpecCatalog takes a Vue library and the same actions option, and every component renders the same data-aui markup, emits the same events with the same payloads, and writes bound values back the same way as the React vocabulary, so the stylesheet and the behavior above carry over:

import { defineComponent, h } from "vue";
import { SpecRenderer } from "generative-frame/spec/vue";
import {
  defaultGenerativeUILibrary,
  toSpecCatalog,
  type GenerativeUIComponent,
} from "@assistant-ui/generative-ui/vue";
import { z } from "zod";
import { Stars } from "@/components/stars";

const Rating: GenerativeUIComponent = {
  description: "A 1 to 5 star rating the user can change.",
  properties: z.object({ value: z.number().describe("Stars, from 1 to 5.") }),
  events: ["rate"],
  slots: [],
  component: defineComponent({
    props: ["value"],
    emits: ["rate", "update:value"],
    setup(props, { emit }) {
      return () =>
        h(Stars, {
          value: props.value,
          onChange: (stars: number) => {
            emit("update:value", stars);
            emit("rate", stars);
          },
        });
    },
  }),
};

const { catalog, components } = toSpecCatalog({
  ...defaultGenerativeUILibrary,
  Rating,
});

A Vue entry carries a component instead of a render function. The component follows the Vue component contract: it declares its props and the events it emits, receives children and named slots as Vue slots, and writes a bound prop by emitting update:<prop>. To render render_spec calls in an @assistant-ui/vue thread, pass the catalog and components to createSpecToolkit from generative-frame/spec/assistant-ui/vue (assistant-ui on Vue).

With React Native

On React Native, @assistant-ui/generative-ui/spec resolves to a React Native build through the react-native export condition: toSpecCatalog and defaultGenerativeUILibrary from it draw the same vocabulary with React Native's own components, with the same events, payloads, and bound values. Generative UI on React Native shows the setup and the components that take a different form on a phone.

Charts

Chart accepts positive and negative values. Its scale always includes zero, so bars grow upward or downward from the zero line, areas fill to that line, and stacked bars and areas keep positive and negative totals on their respective sides. A chart that contains both positive and negative values draws a zero baseline. When showAxis is set, the ticks include zero and use round values for a mixed domain.

Bars and data points in line and area charts expose a native tooltip with the point label and formatted value. A point without a label shows its position, counting from 1. Tooltips on named series also include the series label. Sparklines remain compact and do not add point targets.

Use series[].color to set an individual series to emphasis, secondary, alpha-70, white, white-70, or white-50. The same color fills that series' legend swatch, while the legend label stays muted.

Tables and facts

Table takes columns, rows, and optional sortable. Each column has a required label, an optional align of "start" or "end", and an optional format. A format has a kind of "number", "currency", "percent", or "date"; numeric kinds can set decimals from 0 to 20, currency also needs a currency code, and percent takes a fraction, so 0.25 renders as 25%. Number, currency, and percent columns end-align by default. Dates accept ISO strings or epoch numbers. Values with the wrong type, or a format the platform cannot apply, stay as their raw text. A sortable header cycles ascending, descending, then the original row order, and marks the sorted column with an arrow.

Fact takes label and value, plus optional delta, trend, and upIsGood. delta renders as a change chip beside the value. Set trend to "up", "down", or "flat" to choose its direction, or omit it to infer down from a leading minus sign. The arrow carries the direction, so a sign that agrees with it is dropped from the chip. upIsGood defaults to true and flips which direction is positive when set to false.

Carousels

Carousel paging controls appear only when cards overflow the available width. Each press moves one card, and motion follows the user's reduced motion preference.

Tabs, steps, and timelines

Tabs shows one child panel at a time under a row of tabs, labeled in order by its tabs strings, and label names the row for screen readers; a panel without a label is labeled with its number. Every panel stays mounted, so a control on a hidden tab keeps its value. On the web, the arrow keys, Home, and End move between tabs. Slack and Teams show every panel in order under its label.

Steps numbers its steps, each a title with an optional detail, for instructions to follow in order. Timeline lists its items, each a time label, a title, and an optional detail, for schedules, itineraries, and histories. A present tree skips an entry without a title; spec mode reports it to the model as an invalid prop instead.

Citations

Citation shows a source the answer draws on: its title, an optional snippet, and the source it comes from, which defaults to the link's domain. Only an http or https url to a plain host makes the title a link, which opens in a new tab on the web and through Linking on React Native; any other value leaves the title as plain text.

Checklists

Set progress on a CheckboxGroup to show how many of its options are checked, as a count and a bar, for a checklist the user works through. The count follows the boxes as the user checks them, also when spec mode binds the group's value. Slack and Teams show the options without the count.

Diagrams and maps

Diagram takes Mermaid source in code and an optional title. Map takes places, each a label with lat, lng, and an optional detail, and route to draw a line through them in order; a place without valid coordinates is skipped.

The default vocabulary draws neither, so it adds no dependency: Diagram shows its source, and Map lists its places, each linking to OpenStreetMap. Slack, Teams, and React Native show the same. The generative-ui registry item draws them with the Mermaid diagram and Geo map elements, which bring beautiful-mermaid and leaflet into your app, and the map loads OpenStreetMap tiles.

Styling

The vocabulary renders semantic HTML tagged with data-aui and data-aui-* attributes and ships no styles of its own, so it cannot conflict with your CSS. The generative-ui registry item above installs the shipped stylesheet, which is written entirely against your existing theme variables: change --radius or --primary in your own CSS and the rendered widgets follow.

A Card is a module the reader scans or acts on, such as a summary, a checklist, a form, or one option in a comparison, so it renders as a panel: a fill from the vocabulary's --aui-panel token, with no border. A card inside another card stays plain, so a module never nests a second panel. A card takes on a framed surface where one is warranted: when the model sets a background, when confirm or cancel add a footer whose buttons need a delimited target, or when it is a carousel slot.

To restyle a single component, target its attribute:

[data-aui="fact-value"] {
  font-variant-numeric: tabular-nums;
  font-size: 1.125rem;
}

Overriding a component's markup rather than its appearance is a library-level change: spread defaultGenerativeUILibrary and replace that one entry through defineGenerativeComponents. The default Markdown renders its source as plain text, and Diagram and Map draw nothing, which is why the registry item ships replacements. Wiring them is the same override:

app/toolkit.tsx
import { styledGenerativeUILibrary } from "@/components/assistant-ui/elements/generative-ui";

const markdown = defaultGenerativeUILibrary.Markdown!;
const diagram = defaultGenerativeUILibrary.Diagram!;
const map = defaultGenerativeUILibrary.Map!;

const generative = new JSONGenerativeUI({
  library: {
    ...defaultGenerativeUILibrary,
    ...defineGenerativeComponents({
      Markdown: {
        properties: markdown.properties,
        streamProperties: markdown.streamProperties,
        description: "A markdown string, rendered with GitHub-flavored markdown.",
        render: styledGenerativeUILibrary.Markdown!.render,
      },
      Diagram: {
        properties: diagram.properties,
        description: diagram.description,
        render: styledGenerativeUILibrary.Diagram!.render,
      },
      Map: {
        properties: map.properties,
        description: map.description,
        render: styledGenerativeUILibrary.Map!.render,
      },
    }),
  },
});

styledGenerativeUILibrary is a "use client" module, so this inline render is the only place a "use generative" file may name it. Passing it directly as library works only in a file without the directive.

Security

The library is the boundary on which components can render: the model can only name components you put in it, resolved by lookup with no eval and no dynamic import. Unknown names are dropped.

Props are a separate question. Each component's properties schema shapes the present tool parameters the model sees, and when several components declare a prop with different schemas, the parameters offer each one. The renderer checks props only to keep such a shared prop's value away from a component whose schema rejects it while another declaring component's schema accepts it, so declare every value a component can render: a value: z.string() component loses a number that a value: z.number() component in the same library accepts. Treat props as untrusted input: validate URLs and values passed to APIs, and sanitize content rendered as raw HTML. Components with streamProperties: true can also receive partial props while the model is streaming, so account for incomplete values.

Beyond the browser

The tree is plain JSON, so it is not tied to the browser. The package root, /slack, /teams, and /a2ui let a server action, queue worker, or webhook handler consume the tree without pulling React, while /react and /spec import it and /vue imports Vue:

  • @assistant-ui/generative-ui holds the tree types, normalization, token enums, and resolveFieldReferences.
  • @assistant-ui/generative-ui/spec exports toSpecCatalog and ToSpecCatalogOptions to build a generative-frame spec catalog from a generative UI library.
  • @assistant-ui/generative-ui/vue exports toSpecCatalog, defaultGenerativeUILibrary, and the GenerativeUIComponent and GenerativeUILibrary types for the same catalog with Vue components.
  • @assistant-ui/generative-ui/slack converts the tree into Block Kit and decodes block_actions back into $action. See Generative UI on Slack.
  • @assistant-ui/generative-ui/teams converts it into an Adaptive Card and decodes the submit payload. See Generative UI on Microsoft Teams.
  • @assistant-ui/generative-ui/a2ui consumes A2UI surfaces over AG-UI.