# Stream Chat Integration

The AI components work seamlessly with the `stream_chat_flutter` SDK. Add both packages to your app, since `stream_chat_flutter_ai` doesn't depend on Stream Chat:

```yaml
dependencies:
  stream_chat_flutter: ^10.6.0
  stream_chat_flutter_ai: ^0.2.0
```

Together they need Dart 3.12 or later and Flutter 3.44 or later, as described in the [installation](https://getstream.io/chat/docs/sdk/flutter/ai-components/#step-2-install-the-sdks) section. To let users attach photos or dictate messages, also add the [permissions](https://getstream.io/chat/docs/sdk/flutter/ai-components/composer/#permissions) described in Composer.

This page adds the AI components to the `stream_chat_flutter` message list. If your app is an AI assistant rather than a chat, [`AIConversationView`](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-view/) lays out the whole conversation for you, without the message list.

`stream_chat_flutter` ships older versions of some AI widgets. When you import both packages, hide those names so the ones from `stream_chat_flutter_ai` are used:

```dart
import 'package:stream_chat_flutter/stream_chat_flutter.dart'
    hide
        StreamingMessageView,
        AITypingIndicatorView,
        TypewriterState,
        TypewriterValue,
        TypewriterController,
        TypewriterWidgetBuilder;
import 'package:stream_chat_flutter_ai/stream_chat_flutter_ai.dart';
```

## Rendering AI messages

`StreamMessageListView` accepts a `messageBuilder`. Use it to render messages marked with `ai_generated` with a `StreamingMessageView`, and keep the regular bubble for everything else:

```dart
StreamMessageListView(
  messageBuilder: (context, message, defaultProps) {
    if (message.extraData['ai_generated'] == true) {
      return AIMessageItem(key: ValueKey(message.id), message: message);
    }
    return DefaultStreamMessageItem(props: defaultProps);
  },
);
```

Key the item by the message's id, as the list does with its own items. Loading older messages shifts every item's position, and without the key a reply's typing state can move to a different message.

`AIMessageItem` renders the message text without a bubble or avatar. Every time the backend updates the message, `StreamingMessageView` receives the longer text and types the new part out. The backend sets the message's `generating` field to `true` while the reply streams and to `false` on its final update, which is still typed out to the end, so a live reply types itself out while one loaded from history shows in full at once:

```dart
class AIMessageItem extends StatelessWidget {
  const AIMessageItem({
    super.key,
    required this.message,
  });

  final Message message;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: StreamingMessageView(
        text: message.text ?? '',
        expectMoreText: message.extraData['generating'] == true,
        codeHighlighter: highlightCode,
        onTapLink: (text, href, title) => openLink(href),
      ),
    );
  }
}
```

`highlightCode` and `openLink` are helpers you define yourself: `highlightCode` is the syntax highlighter described in [Code and charts](https://getstream.io/chat/docs/sdk/flutter/ai-components/streaming-messages/#code-and-charts), and `openLink` opens the URL with a launcher such as `url_launcher`. The [sample app](https://github.com/GetStream/chat-ai-samples/blob/main/flutter/lib/src/ai_message_item.dart) shows both wired up. The `codeHighlighter`, `mathBuilder` and link handling are all optional. The markdown takes its styles from the same `StreamTheme` as the Stream Chat widgets, so it matches the regular message bubbles without a `styleSheet`. See [Mark the agent's replies](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-state/#mark-the-agents-replies) for how the backend sets the flag.

An item of your own replaces the whole default message item, so AI replies also have no reactions or reply counts, and it leaves out the default message footer, which would mark every AI reply as "Edited": each update the backend makes to the text sets the message's `messageTextUpdatedAt`, and the footer shows "Edited" whenever it's set.

The backend's placeholder message is empty until the first update arrives, so the item has nothing to type out yet. Show what the agent is doing meanwhile with the AI indicator, as described in [Showing the AI indicator](#showing-the-ai-indicator).

## Rendering reasoning and tool calls

If your agent stores its [reasoning and tool calls](https://getstream.io/chat/docs/sdk/flutter/ai-components/reasoning-and-tool-calls/) on the reply, they arrive as custom attachments of the message. Decode them into parts and show them above the text in the same item:

```dart
List<AIMessagePart> aiParts(Message message) => AIMessagePart.parseAll([
      for (final attachment in message.attachments)
        if (attachment.rawType case final type?)
          (type: type, payload: {'id': attachment.id, ...attachment.extraData}),
    ]);
```

Use `rawType`, since `type` can be rewritten, for example into a link preview. Spread `extraData` last, so the step's own `id` wins over `Attachment.id`: from the network the step's `id` is in `extraData`, while the offline database stores it as `Attachment.id`. Your backend must set an `id` on every step, or each update gets a new random `Attachment.id` and the step starts over.

```dart
final parts = aiParts(message);

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    if (parts.isNotEmpty) AIMessagePartsView(parts: parts, approver: approver),
    StreamingMessageView(
      text: message.text ?? '',
      expectMoreText: message.extraData['generating'] == true,
    ),
  ],
);
```

`approver` is an `AIToolApprover` you create once, for example in your screen's `State`. It sends the user's answers to tool calls that wait for their approval, and with an `AIToolAuthorizer` it also offers the [sign-ins](https://getstream.io/chat/docs/sdk/flutter/ai-components/reasoning-and-tool-calls/#tool-sign-ins) that tool calls wait for. Leave it out if your agent doesn't ask. Pass the same `parts` to [`AIClientToolRunner.handle`](https://getstream.io/chat/docs/sdk/flutter/ai-components/reasoning-and-tool-calls/#run-the-calls) on every update of the message if your agent runs tools on the user's device.

## Showing the AI indicator

The agent reports what it is doing with [`ai_indicator.*` events](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-state/#say-what-the-agent-is-doing). Listen for them on the channel, and keep the latest `AITypingState` in a `ValueNotifier`:

```dart
class TypingStateHandler extends ValueNotifier<AITypingState> {
  TypingStateHandler({required Channel channel}) : super(AITypingState.idle) {
    _subscription = channel.on().listen(_onEvent);
  }

  late final StreamSubscription<Event> _subscription;

  void _onEvent(Event event) {
    final state = switch (event.type) {
      // On failure the backend sends `ai_indicator.update` with
      // `AI_STATE_ERROR`, not a clear or stop, so `error` also ends a reply.
      EventType.aiIndicatorUpdate => event.aiState,
      EventType.aiIndicatorClear || EventType.aiIndicatorStop => AITypingState.idle,
      _ => null,
    };
    if (state != null) value = state;
  }

  @override
  void dispose() {
    unawaited(_subscription.cancel());
    super.dispose();
  }
}
```

For a reference implementation, check our [sample app](https://github.com/GetStream/chat-ai-samples/blob/main/flutter/lib/src/typing_state_handler.dart).

Handle `ai_indicator.clear` (and `ai_indicator.stop`) as well as the update: `clear` is what ends the indicator. Listening only to the update leaves the indicator on screen forever. `channel.on()` only delivers that channel's events, so an indicator from another conversation can't leak in. Create one handler per channel, and dispose of it with the screen.

Then show an `AITypingIndicatorView` below the message list while the agent is working:

```dart
Column(
  children: [
    Expanded(
      child: StreamMessageListView(messageBuilder: buildMessage),
    ),
    ValueListenableBuilder(
      valueListenable: typingStateHandler,
      builder: (context, state, _) {
        final text = switch (state) {
          AITypingState.thinking => 'Thinking',
          AITypingState.checkingSources => 'Checking sources',
          AITypingState.generating => 'Generating response',
          AITypingState.error => 'Something went wrong',
          _ => null,
        };
        if (text == null) return const SizedBox.shrink();
        return Padding(
          padding: const EdgeInsets.all(8),
          child: AITypingIndicatorView(text: text),
        );
      },
    ),
  ],
);
```

Once the state becomes `generating`, the text is streaming into the message itself. `AITypingState.error` is a terminal state too: the backend has already written the error into the message, and sends no `ai_indicator.clear` after it.

## Stopping generation

Wire `ChatComposer` to the channel. Set `controller.isGenerating` while the assistant is replying, so the send button turns into a stop button, and call `channel.stopAIResponse()` when it's tapped:

```dart
final controller = ChatComposerController();

ChatComposer(
  controller: controller,
  onSendPressed: (text, selectedOption, attachments) =>
      channel.sendMessage(Message(text: text)),
  onStopPressed: () {
    controller.isGenerating = false;
    channel.stopAIResponse();
  },
);
```

The stop button turns back into the send button as soon as it's tapped, without waiting for the backend. The snippet ignores the `attachments` the composer hands over. See [Show a message while it's sent](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-view/#show-a-message-while-its-sent) for uploading the picked photos with the message.

Drive `isGenerating` from the same events as the indicator:

```dart
typingStateHandler.addListener(() {
  controller.isGenerating = switch (typingStateHandler.value) {
    AITypingState.thinking || AITypingState.checkingSources || AITypingState.generating => true,
    _ => false,
  };
});
```

This turns the stop button back into the send button as soon as `ai_indicator.clear` arrives, while the typewriter may still be revealing the reply. To keep the stop button until the reply is on screen, set `isGenerating` to `false` from the reply's `StreamingMessageView.onFinished` instead, and on `AITypingState.error`.

See [Stop the reply](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-state/#stop-the-reply) for what the backend does with the stop event.

## Building a standalone AI screen

For a ChatGPT-style app, show an [`AIConversation`](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-state/) with [`AIConversationView`](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-view/) and the composer. The list has the reply's steps, attachments and actions built in:

```dart
class AiChatScreen extends StatefulWidget {
  const AiChatScreen({super.key, required this.channel});

  // A channel you've watched, with `channel.watch()`.
  final Channel channel;

  @override
  State<AiChatScreen> createState() => _AiChatScreenState();
}

class _AiChatScreenState extends State<AiChatScreen> {
  late final _conversation = AIConversation<StreamAIMessage>(transport: ChannelTransport(widget.channel));

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

  @override
  Widget build(BuildContext context) => Column(
    children: [
      Expanded(child: AIConversationView.conversation(conversation: _conversation)),
      ChatComposer.conversation(conversation: _conversation),
    ],
  );
}
```

`StreamAIMessage` is in [Messages from Stream Chat](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-view/#messages-from-stream-chat), and `ChannelTransport` in [A transport over a Stream Chat channel](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-state/#a-transport-over-a-stream-chat-channel). The transport sends what the user asks to the channel, and stops the reply with `channel.stopAIResponse()`. The conversation shows the stop button from the moment the user sends, while the agent thinks and while it writes.

The transport's `send` gets the picked files as `request.attachments`, and uploads them with the message. The [sample app](https://github.com/GetStream/chat-ai-samples/tree/main/flutter) shows a whole app.

## Replacing components

The AI components and the Stream Chat widgets read their builders from the same `StreamComponentFactory`. A nested factory replaces its parent's builders instead of adding to them, and `StreamChat` adds a factory of its own when you pass it `componentBuilders`. So pass the AI builders in that same list, next to the Stream Chat ones:

```dart
StreamChat(
  client: client,
  componentBuilders: StreamComponentBuilders(
    extensions: [
      ...streamChatComponentBuilders(),
      ...streamAIComponentBuilders(codeBlock: codeBlock),
    ],
  ),
  child: child,
);
```

[Replacing components](https://getstream.io/chat/docs/sdk/flutter/ai-components/#replacing-components) in the quickstart lists every AI builder.

## What the backend has to send

The app only renders what the agent writes, and it cannot tell when a field is missing. [What your backend sends](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-state/#what-your-backend-sends) covers each one:

- `ai_generated: true` on the reply when it is created, or it renders as an ordinary message with raw markdown.
- One message, updated as tokens arrive, not one message per chunk. Use `ephemeralUpdateMessage` for the interim updates and a regular partial update for the final one, as described in [Stream with ephemeral updates](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-state/#stream-with-ephemeral-updates).
- `generating: true` while streaming and `false` on the final update, on stop and on error.
- `ai_indicator.update` while working and `ai_indicator.clear` when done, or there is no indicator.

For a reference implementation showing how to integrate these components with Stream Chat's client API, please check our [sample app](https://github.com/GetStream/chat-ai-samples/tree/main/flutter).

---

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