# Generative UI

An agent can answer with UI rather than text: a weekend plan with a map of the places, the user's own calendar for those days and a form to change the pace. The AI components show a reply's UI in place of its text, bring it in as the agent writes it, and send what the user does in it back to the agent as their next message.

The AI components know no UI format of their own. You give a conversation a renderer for each format your agent writes, and the widgets of your app the agent can place, such as a map, a calendar or a card over your own data. The SDK ships a renderer of [A2UI](https://a2ui.org), and you can write one for any other format. A component works whichever format the agent writes.

>
> **Info:** Generative UI is part of `stream_chat_flutter_ai`, and the A2UI renderer is the `stream_chat_flutter_ai_a2ui` package, both available from version 0.2.0. See [Renderers](#renderers).
>

## How a reply carries its UI

The agent's backend writes a reply's UI in the message's `ai_ui` custom field. The reply's text stays readable on its own, for wherever the UI can't show, such as a push notification or an app without a renderer for the format:

```json
{
  "text": "Here's a weekend in Ohrid, with a map and a plan for each day.",
  "ai_ui": {
    "v": 1,
    "format": "a2ui",
    "status": "streaming",
    "content": "{\"version\":\"1.0\",\"createSurface\":{\"surfaceId\":\"plan\",\"catalogId\":\"getstream.io:app-catalog\"}}\n{\"version\":\"1.0\",\"updateComponents\":{\"surfaceId\":\"plan\",\"components\":[{\"id\":\"root\",\"component\":\"Text\",\"text\":\"A weekend in Ohrid\"}]}}"
  }
}
```

| Field     | Description                                                                                     |
| --------- | ----------------------------------------------------------------------------------------------- |
| `v`       | The version of the field, `1`. An app that doesn't know a newer version shows the reply's text. |
| `format`  | The format the UI is written in, such as `a2ui` or `openui`.                                    |
| `version` | The format's version, such as `1.0`, when the agent names one. Optional.                        |
| `status`  | `streaming` while the agent is still writing the UI, then `complete`.                           |
| `content` | The UI in its format: text, such as an OpenUI program or A2UI messages one per line, or JSON.   |

A model usually writes the UI into its reply, for example in a fenced code block. Have your backend move it out of the text into `ai_ui` as the reply streams, so the text keeps only the words around it. A message's custom data holds at most 50 KB in a channel of the [`agent` type](https://getstream.io/chat/docs/node/channel-features/#the-agent-channel-type), and 5 KB in the other types, so leave the UI out when it doesn't fit beside the rest of it.

Read the field with `AIUIDocument.fromCustomData`, as the `uiDocument` of your `AIMessage`, such as the `StreamAIMessage` from [Messages from Stream Chat](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-view/#messages-from-stream-chat):

```dart
class StreamAIMessage with AIMessage {
  StreamAIMessage(this.message) : uiDocument = AIUIDocument.fromCustomData(message.extraData);

  final Message message;

  // The id, role, text and the rest, as in the Conversation View guide.

  @override
  final AIUIDocument? uiDocument;
}
```

It returns `null` when the message carries no `ai_ui` field the SDK can read, and the reply shows its text.

## Show it in a conversation

Make an `AIUIHost` with the renderers of the formats your agent writes and your components, and pass it to `AIConversationView` as `ui`. Keep one host for as long as its conversation shows, for example in the `State` behind your screen, so each reply's UI keeps its state, such as what the user typed into a form, across scrolling and rebuilds. Call `dispose` when the conversation closes:

```dart
class _AssistantScreenState extends State<AssistantScreen> {
  late final AIUIHost _ui = AIUIHost(
    renderers: [A2UIRenderer()],
    components: const [PlaceCardComponent()],
    send: (action) => widget.channel.sendMessage(
      Message(text: action.label, extraData: action.toCustomData()),
    ),
  );

  @override
  void dispose() {
    _ui.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<List<Message>>(
      stream: widget.channel.state!.messagesStream,
      initialData: widget.channel.state!.messages,
      builder: (context, snapshot) => AIConversationView(
        messages: [
          for (final message in snapshot.data!)
            if (message.type == MessageType.regular) StreamAIMessage(message),
        ],
        ui: _ui,
      ),
    );
  }
}
```

`PlaceCardComponent` is one of your [components](#components). A reply shows its UI in place of its text, after its [steps](https://getstream.io/chat/docs/sdk/flutter/ai-components/reasoning-and-tool-calls/), when one of the host's renderers reads its format and version, the first one listed asked first, and builds something for it. Otherwise, it shows its text. The UI comes in as the agent writes it: each part fades in, rising into place, after the parts before it. A reply written before it showed, such as one loaded from history, shows at once.

A reply with UI the host can show counts as showing something, so the conversation stops showing the agent thinking as soon as its UI starts. The [reply actions](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-view/#reply-actions) show under a reply once its UI is written, as they do under text. Their message's `text` is still the reply's text, which the reply doesn't show while it shows its UI. A reply that only shows UI and has no text offers no copying or sharing.

With an [`AIConversation`](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-state/#generative-ui), pass the host the same way:

```dart
AIConversationView.conversation(conversation: conversation, ui: ui)
```

## Renderers

A renderer reads one format, and shows each reply's UI written in it. Add the package of each format your agent writes next to `stream_chat_flutter_ai`, which each depends on:

```yaml
dependencies:
  stream_chat_flutter_ai: ^0.2.0
  stream_chat_flutter_ai_a2ui: ^0.2.0
```

| Package                       | Renderer       | Format   |
| ----------------------------- | -------------- | -------- |
| `stream_chat_flutter_ai_a2ui` | `A2UIRenderer` | A2UI 1.0 |

It requires Dart 3.12 or later and Flutter 3.44 or later, like the AI components.

The SDK's renderers place your app's [components](#components) beside their own, run your app's [tools](#run-your-apps-tools), and style their widgets with the app's `StreamTheme`, so a reply's UI looks like the rest of the conversation. They take these options:

| Parameter      | Description                                                                                                                                                                                                                                                                                            |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `instructions` | What the agent's model is told ahead of the format's own instructions, such as which answers to write as UI and when to use your components. See [Tell the agent what the app shows](#tell-the-agent-what-the-app-shows).                                                                              |
| `theme`        | The look of the format's own components, such as an `A2UIThemeData`. What it leaves unset comes from the `StreamTheme`. See [Theming](#theming).                                                                                                                                                       |
| `mediaUrl`     | The URL an image (and with A2UI, a video or an audio clip) loads from, given the one the agent wrote, or `null` to not load it, such as to allow only your own image hosts. By default, only web links load.                                                                                           |
| `onError`      | Gets the problems with a reply's UI, such as a component the format doesn't know or a tool that failed, with the reply's id. A chat has no channel for them, so forward them to your backend if the agent should hear about them. They are reported in debug builds by default. See [Errors](#errors). |

`A2UIRenderer` also takes a `catalog` and a `catalogId`, as described below.

The renderers treat what the agent writes as untrusted: links in a reply's UI open through the host, which opens only web and email links by default, and images load only from the web.

### A2UI

`A2UIRenderer` reads [A2UI 1.0](https://a2ui.org), with every component and function of its basic catalog drawn in Flutter. The agent writes with the basic catalog, which your components join under the `getstream.io:app-catalog` id (`catalogId` changes it). A surface that names the basic catalog's id shows them as well. Each reply has surfaces of its own, kept while the host is, so what the user enters in them survives scrolling.

A button reaches the host as an `AIUIAction` named after its event, labeled with the button's text, or else with the event's name as words (`submit_form` is "Submit form"). Its payload holds the `surfaceId`, the `sourceComponentId`, the event's resolved `context`, the event's `userMessage` when it has one, and the surface's `dataModel` when the agent created the surface with `sendDataModel`. A function the catalog doesn't define runs as one of the host's [tools](#run-your-apps-tools), with the tool's output as the function's value.

By default, only web links load as images, video and audio. Pass `mediaUrl` to return the URL an image, video or audio clip loads from, given the one the agent wrote, or `null` to not load it, such as to load media through your own CDN. The agent writes these URLs, so return only ones your app trusts:

```dart
final renderer = A2UIRenderer(
  mediaUrl: (url) => url.scheme == 'https' && url.host == 'media.example.com' ? url : null,
);
```

To change how one of the basic catalog's components looks, give the renderer a catalog in which your widget draws it. The builder reads the component through an `A2UIComponentScope`, with its bindings resolved, and draws its children with the same catalog:

```dart
// In a `static final` field: a catalog made in `build` is a new one each time,
// so everything drawn with it is built again.
static final catalog = A2UICatalog.basic.replacing('Card', (context, scope) {
  final child = scope.child();
  return BrandCard(child: child == null ? const SizedBox.shrink() : scope.build(child));
});

final renderer = A2UIRenderer(catalog: catalog);
```

Here `BrandCard` is a widget of your app.

The scope reads properties with `value`, `string`, `number`, `boolean` and `stringList`, writes an input's value to the surface's data model with `write`, and runs what the component does with `performAction`. A builder draws with the surface's theme through `A2UITheme.of(context)`, whose fields are `null` where nothing overrides them, so fall back to the `StreamTheme`, such as `A2UITheme.of(context).border ?? context.streamColorScheme.borderDefault`. A `Row` or `Column` measures its children before laying them out, so a child can't be a widget that can't measure itself, such as a `LayoutBuilder`.

`removing` leaves out components you don't want the agent to use. The agent isn't told about them, and a reply that still uses one shows nothing for it:

```dart
final renderer = A2UIRenderer(catalog: A2UICatalog.basic.removing(['Video', 'AudioPlayer']));
```

### Write a renderer

For any other format, write a renderer of your own. A renderer makes an engine for each conversation's host, which makes the widget of each reply's UI:

```dart
class CardsRenderer extends AIUIRenderer {
  const CardsRenderer();

  @override
  AIUIFormat get format => const AIUIFormat('cards');

  @override
  AIUIEngine createEngine(AIUIHosting host) => CardsEngine();
}

class CardsEngine extends AIUIEngine {
  @override
  Widget? view(AIUIDocument document, String messageId) {
    final cards = document.content;
    if (cards is! List<Object?>) return null;
    return Cards(cards: cards);
  }
}

class Cards extends StatelessWidget {
  const Cards({super.key, required this.cards});

  final List<Object?> cards;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        for (final card in cards)
          AIUIEntrance(child: Text(card is Map<String, Object?> ? '${card['title'] ?? ''}' : '')),
      ],
    );
  }
}
```

List it in the host like any other renderer. A document's `content` is the agent's text, which `text` reads, or JSON as plain Dart values: maps, lists, strings, numbers, booleans and `null`.

- `view(document, messageId)` is called for each new version of a reply's document, such as while the agent writes it. Return `null` to show the reply's text instead. It isn't called again when other replies change, so a widget that other replies can update should listen to the engine's own state, such as a `Listenable`.
- The host makes one engine per renderer and keeps it until `AIUIHost.dispose`, so an engine can hold each reply's state by its `messageId`. The host reports what an engine throws to `FlutterError.onError`, and the reply shows its text.
- `receive(document, messageId)` gets each reply's document as it arrives or changes, oldest first, including replies that aren't on screen. Use it for a format whose later replies change an earlier one's UI. `remove(messageId)` tells the engine a reply, or its UI, left the conversation, so it can let go of what it kept for it. `dispose()` releases the engine's resources.
- `supports(version)` on the renderer says which versions of the format it reads. It reads every version by default.
- `capabilities(components, tools)` on the renderer says what the agent's backend needs to write the format, such as the instructions for its model. See [Tell the agent what the app shows](#tell-the-agent-what-the-app-shows).

Extend `AIUIRenderer` and `AIUIEngine` rather than implementing them, so members added in later versions don't break your code.

A renderer places the app's components by name, with `host.component(name)`, among the ones its format has. A component's `view(arguments, context)` makes its widget for the arguments the agent wrote, or returns `null` while they don't fit its parameters yet. Give it an `AIUIComponentContext` whose `emit` sends its events the way your format reports what the user did.

## Components

A component is a widget of your app the agent can place in a reply's UI. You describe it once, with the parameters the agent fills in and the events it reports, say how to read the arguments the agent wrote, and how it looks for them:

```dart
class PlaceCardComponent extends AIUIComponent<({String title, DateTime? visitDate})> {
  const PlaceCardComponent();

  @override
  AIUIComponentDefinition get definition => const AIUIComponentDefinition(
    name: 'place_card',
    description: 'A photo and a short summary of a place.',
    parameters: [
      AIUIParameter.required('title', AIUIParameterType.string, description: "The place's Wikipedia title."),
      AIUIParameter.optional('visit_date', AIUIParameterType.date),
    ],
    events: [AIUIComponentEvent(name: 'ask_about', description: 'The user asks about the place.')],
  );

  @override
  ({String title, DateTime? visitDate}) readArguments(AIUIArguments arguments) => (
    title: arguments.string('title'),
    visitDate: arguments.optional('visit_date', arguments.date),
  );

  @override
  Widget build(BuildContext context, ({String title, DateTime? visitDate}) arguments, AIUIComponentContext ui) {
    return PlaceCard(
      title: arguments.title,
      date: arguments.visitDate,
      onTap: () => ui.emit('ask_about', label: 'Tell me more about ${arguments.title}'),
    );
  }
}
```

`PlaceCard` is a widget of your app. Every renderer places it by its name, so it works whichever format the agent writes. The SDK's renderers place it under its name in `PascalCase`, so `place_card` is `PlaceCard`. A component named like one of the format's own, such as `Card` in A2UI, would replace it, so the renderer leaves it out and reports it in debug builds. A2UI also leaves out a component with a parameter named `id`, `component`, `weight` or `accessibility`, or whose name isn't an identifier, such as `visit-date`, and an app component holds no other components and no `action` there.

The arguments are the JSON the agent wrote, which `readArguments` reads through the getters of `AIUIArguments`, such as `string`, `number`, `integer`, `boolean`, `date` (a day or a date and time), `url`, `object`, their lists (`strings`, `numbers`, `integers`, `booleans`, `dates`, `urls` and `objects`) and `optional`. A getter throws a `FormatException` when a value is missing or doesn't fit, and the component shows nothing, as it does while the agent is still writing the arguments. A date arrives as the text the agent wrote, such as `2026-11-14`, which `date` reads. `ui.isStreaming` tells whether the arguments may still change, and `ui.messageId` which reply the component is in. An event's `payload` carries what it reports, and the renderer adds which component it came from as `component`, and A2UI also `surfaceId` and `sourceComponentId`. An event can describe what it carries with `parameters`, as a component does.

Extend `AIUIComponent` rather than implementing it, so members added in later versions don't break your code.

The parameter types are `AIUIParameterType.string`, `number`, `integer`, `boolean`, `date`, `dateTime` and `url`, and `AIUIParameterType.enumeration(values)`, `array(items)` and `object(fields)`. Those three aren't constants, so a definition that uses one isn't `const`. `AIUIParameterType(schema)` takes any other JSON Schema.

A component can load what it shows on the device, such as photos or the user's calendar, which then never reaches the agent. `ui.host.open(url)` opens a link through the host, which decides the kinds of link that open.

### One description for every platform

A component's definition reads and writes JSON, so every platform your app runs on, and the agent's backend, can share one description of it. Its parameters are a JSON Schema object, as a tool's are, which keeps their order in `x-order`:

```json
{
  "name": "place_card",
  "description": "A photo and a short summary of a place.",
  "parameters": {
    "type": "object",
    "properties": {
      "title": {
        "type": "string",
        "description": "The place's Wikipedia title."
      },
      "visit_date": { "type": "string", "format": "date" }
    },
    "required": ["title"],
    "x-order": ["title", "visit_date"]
  },
  "events": [
    { "name": "ask_about", "description": "The user asks about the place." }
  ]
}
```

Read it with `AIUIComponentDefinition.fromJson` to use the same file in your app. It throws a `FormatException` for a definition without a name. `toJson()` writes it:

```dart
final json = jsonDecode(await rootBundle.loadString('assets/place_card.json')) as Map<String, Object?>;
final definition = AIUIComponentDefinition.fromJson(json);
```

## Run your app's tools

A reply's UI can run your app's [tools](https://getstream.io/chat/docs/sdk/flutter/ai-components/reasoning-and-tool-calls/#tools-on-the-device) on the device, such as to load the user's calendar for the days of a plan. Pass them to the host as `tools`, as `AIResultClientTool`s; when two share a name, the first is used. They are apart from the tools the agent calls, and they run without asking the user, so the host runs a tool by itself only when its definition says it's [read-only](https://getstream.io/chat/docs/sdk/flutter/ai-components/reasoning-and-tool-calls/#define-a-tool), with `isReadOnly`:

```dart
class CalendarEventsTool extends AIResultClientTool {
  const CalendarEventsTool();

  @override
  AIToolDefinition get definition => const AIToolDefinition(
    name: 'calendar_events',
    description: "The user's calendar events between two days.",
    parameters: {
      'type': 'object',
      'properties': {
        'from': {'type': 'string', 'format': 'date'},
        'to': {'type': 'string', 'format': 'date'},
      },
      'required': ['from', 'to'],
    },
    isReadOnly: true,
  );

  @override
  Future<AIClientToolResult> run(AIToolCallPart call) async {
    final from = call.arguments?['from'];
    final to = call.arguments?['to'];
    if (from is! String || to is! String) return const AIClientToolResult.failed('The days are missing');
    final events = await DeviceCalendar.eventsJson(from, to);
    return AIClientToolResult.completed(events, summary: 'Read the calendar');
  }
}
```

Here `DeviceCalendar` stands for your app's own access to the calendar, and `sendToAgent` for sending an action as shown in [Show it in a conversation](#show-it-in-a-conversation).

```dart
final ui = AIUIHost(
  renderers: [A2UIRenderer()],
  components: const [PlaceCardComponent()],
  tools: const [CalendarEventsTool()],
  send: sendToAgent,
);
```

A read-only tool runs whenever the UI asks, such as while it shows. Any other tool runs only when the user taps something that runs it, and fails otherwise. The renderers follow this: an A2UI function runs a tool that isn't read-only only from a tap. The model's instructions list the host's tools, with their descriptions and arguments, and say which ones are read-only.

A component of yours runs a tool through its context's host, and gets the tool's output, JSON, as plain Dart values. `runTool` is asynchronous, so call it from a `State`, such as in `initState`. Pass `userInitiated: true` when the user asked for it, such as with a tap, and `false` when the component loads data while it shows:

```dart
class _CalendarStripState extends State<CalendarStrip> {
  late final Future<Object?> _events;

  @override
  void initState() {
    super.initState();
    _events = _load();
  }

  Future<Object?> _load() async {
    try {
      return await widget.ui.host.runTool(
        'calendar_events',
        arguments: {'from': '2026-11-14', 'to': '2026-11-15'},
        userInitiated: false,
      );
    } on AIUIToolException {
      return null;
    }
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Object?>(
      future: _events,
      builder: (context, snapshot) => EventsList(events: snapshot.data),
    );
  }
}
```

Here `CalendarStrip` takes the `AIUIComponentContext` its component's `build` got, as `ui`, and `EventsList` is a widget of your app. `runTool` throws an `AIUIToolException` when the host has no such tool, the tool isn't read-only and the user didn't ask for it, or it fails.

The host's `open` callback opens the links of a reply's UI. By default, web and email links open and others don't, since the agent writes the links, and another kind could call someone or act in another app. Pass your own `open` only when your app wants another kind of link.

## What the user does

When the user does something in a reply's UI that's for the agent, such as tapping a place, choosing a follow-up or submitting a form, the host calls your `send` function with an `AIUIAction`. Send it as the user's next message. The host sends a reply's actions one after another, in the order they happened.

| Property    | Description                                                                                                                          |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `messageId` | The reply whose UI it came from.                                                                                                     |
| `format`    | The format of that UI.                                                                                                               |
| `name`      | What happened, such as a component's event (`ask_about`) or an action of the format, such as an A2UI button's event (`submit_form`). |
| `label`     | What the user did, in their words, such as "Tell me more about St. Naum". Use it as the message's text.                              |
| `payload`   | The details, in the format's own shape, such as what was chosen and a form's values.                                                 |

`toCustomData()` holds the action under the `ai_ui_action` key (`AIUIAction.customDataKey`), for the message's custom data, as above. `toJson()` is the action alone, for a backend that takes it beside the question. Have your backend tell the model what the user did after their words, and which reply it was in, since the same UI can show in several replies, such as a plan for each of two trips.

The SDK's renderers label an action with the control the user tapped, never with text the agent wrote for it, so the conversation shows only what the user chose. An A2UI text field that's `obscured` only hides its text on screen. What the user types into it is data like any other, so it reaches the agent when the action asks for it, but it never becomes a button's label.

## Tell the agent what the app shows

The model has to know which formats and components your app shows, and which tools its UI can run. The host's `capabilities` describes them, as plain Dart values: each format, with what its renderer adds, and your components' definitions:

```dart
final capabilities = ui.capabilities;
// {"v": 1, "renderers": [{"format": "a2ui", "fence": ..., "catalog_id": ..., "instructions": ...}, ...], "components": [...]}
```

Each renderer adds the `fence` the model writes its UI in, such as `ui:a2ui@1.0`, and the `instructions` for the model: the `instructions` you gave the renderer, then the format's own, written from its components, yours and the host's tools. A2UI also adds the `catalog_id` of the catalog with your components.

Send the capabilities to your backend, for example when a conversation starts, and add the instructions to what the model is told. You can also write the model's instructions on your backend, from the same component definitions.

### Write the instructions from a test

An agent on [Stream Agents](https://getstream.io/agents/docs/generative-ui/?client=flutter) is told what to do by the `instructions.md` in its folder, which you sync with the Stream CLI, so the app can't send it the capabilities. Write the instructions into a file from a test of your app instead. Run it with `flutter test` from your app's folder, which holds the agent's, so the file lands in your project:

```dart title="test/agent_instructions_test.dart"
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter_ai/stream_chat_flutter_ai.dart';
import 'package:stream_chat_flutter_ai_a2ui/stream_chat_flutter_ai_a2ui.dart';
import 'package:your_app/place_card_component.dart';

void main() {
  // Writes what the agent is told about the app's UI into the agent's folder.
  test('writes the UI instructions', () {
    // The same components and renderers as the app's host.
    final ui = AIUIHost(
      components: const [PlaceCardComponent()],
      renderers: const [
        A2UIRenderer(
          instructions:
              'Write a trip plan as UI, with a place card for each place. Answer everything else in Markdown.',
        ),
      ],
      send: (_) async {},
    );
    addTearDown(ui.dispose);
    final renderers = ui.capabilities['renderers']! as List<Object?>;
    final instructions = [
      for (final renderer in renderers.cast<Map<String, Object?>>())
        if (renderer['instructions'] case final String text) text,
    ].join('\n\n');

    File('agents/my-agent/ui-instructions.md').writeAsStringSync(instructions);
  });
}
```

Add the file's text to the agent's `instructions.md`, after your own, and sync the agent with `getstream agents sync my-agent`. Run the test again whenever a component changes. Build the host in one function that the app and the test both call, so the two never drift apart.

A2UI names your components in its own style, `PlaceCard` for `place_card`, so use that name when your renderer's `instructions` mention one.

The instructions describe every component of the renderer's catalog, with its properties, so with the whole basic catalog they run to tens of kilobytes, which the model reads on every turn. Give the renderer a catalog of only the components your agent uses, with `removing` as in [A2UI](#a2ui), in the app and in the test alike.

## Theming

The renderers' own components follow the app's `StreamTheme`, in light and dark mode. To restyle one of them, replace its builder, with [A2UI's catalog](#a2ui).

A2UI's components read their colors, corner radii and text styles from an `A2UIThemeData`. Every field is nullable, and one left unset comes from the `StreamTheme`. To change it, give the renderer a theme:

```dart
final a2ui = A2UIRenderer(
  theme: A2UIThemeData(accent: brandOrange, cornerRadius: 8),
);
```

Here `brandOrange` is a color of your app. To theme the replies of a whole subtree instead, wrap it in an `A2UITheme`. The renderer's `theme` is layered over the nearest one, and `A2UITheme.merge` adds to the enclosing theme rather than replace it.

The renderers' texts that have no text of their own in the reply, such as the buttons that play and pause a video or an audio clip, are members of `AITranslations` (`playMedia` and `pauseMedia`), which you translate as described in [Localization](https://getstream.io/chat/docs/sdk/flutter/ai-components/#localization).

Buttons, check boxes, fields and images in a reply are the Stream design system's own, so the builders your app registers with `StreamComponentFactory` draw them there too.

## Errors

A reply's UI can't show everything the agent wrote, such as a component the format doesn't know, a message that doesn't read or a tool that failed. Each renderer passes these problems to its `onError`, with the reply's id. A chat has no channel for them, so forward them to your backend if the agent should hear about them, for example to have the model correct its UI. Without `onError`, they are reported in debug builds only:

```dart
final renderer = A2UIRenderer(
  onError: (A2UIError error, String messageId) {
    agentBackend.reportUIError(messageId, error.toString());
  },
);
```

Here `agentBackend` stands for your app's connection to the agent's backend.

- `A2UIRenderer` passes an `A2UIError`, the problems A2UI reports to the agent: its `code` in A2UI 1.0, such as `A2UIErrorCode.validationFailed`, the `message`, written for the developer of the agent, and the `surfaceId` and the `path`, a JSON Pointer to the part of the agent's message at fault, when they apply.

## Show it in your own views

To show replies with views of your own rather than with `AIConversationView`, show a reply's UI with `AIUIView`, below an `AIAssistantMessageView` whose text is empty, and the reply's text when `canShow` is false:

```dart
Widget reply(AIMessage message, AIUIHost ui) {
  final document = message.uiDocument;
  final view = document == null ? null : AIUIView(host: ui, document: document, messageId: message.id);
  final showsUI = view != null && view.canShow;
  return Column(
    children: [
      AIAssistantMessageView(text: showsUI ? '' : message.text, parts: message.parts),
      if (view != null && showsUI) view,
    ],
  );
}
```

`AIUIHost.canShow(document)` tells whether one of the host's renderers reads a document's format and version, and `AIUIView.canShow` whether it also built something to show for the reply. Each time your messages change, pass them to `ui.receive(messages)`, oldest first, before the replies show, so the renderers get the UI of every reply, also of those that aren't on screen. `AIConversationView` does it for you.

## Motion

`AIUIEntrance` brings a part of a reply's UI in when it shows: the part fades in, rising into place, growing to full size and coming into focus, after the parts that came in just before it. Parts that show together come in together, so a reply written before it showed shows at once. With the system's animations off, parts only fade in.

The SDK's renderers give it to each part they place, and to every component. Use it on the parts of your own renderer's widgets, as `Cards` does above. Outside a reply's UI, it shows its child as it is.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/flutter/ai-components/generative-ui/](https://getstream.io/chat/docs/sdk/flutter/ai-components/generative-ui/).