# 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.0.1
  stream_chat_flutter_ai: ^0.1.0
```

The AI package needs Dart 3.11 or later and Flutter 3.41 or later. To let users attach photos or dictate messages, also add the [platform permissions](https://getstream.io/chat/docs/sdk/flutter/guides/ai-integrations/#platform-requirements) described on the overview page.

`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,
        AnimatedDots,
        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(message: message);
    }
    return DefaultStreamMessageItem(props: defaultProps);
  },
);
```

`AIMessageItem` renders the message text without a bubble or avatar. Every time the backend updates the message, `StreamingMessageView` receives the longer text and animates the new part in:

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

  final Message message;
  final ValueChanged<TypewriterState>? onTypewriterStateChanged;

  @override
  Widget build(BuildContext context) {
    // Use the same text style as the regular message bubbles.
    final bodyStyle = context.streamTextTheme.bodyDefault.copyWith(
      color: context.streamColorScheme.textPrimary,
    );

    return Padding(
      padding: const EdgeInsets.all(16),
      child: StreamingMessageView(
        text: message.text ?? '',
        onTypewriterStateChanged: onTypewriterStateChanged,
        styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith(p: bodyStyle),
        codeHighlighter: highlightCode,
        onTapLink: (text, href, title) => openLink(href),
      ),
    );
  }
}
```

`highlightCode` and `openLink` are helpers you define yourself: `highlightCode` is the syntax highlighter described on the [overview](https://getstream.io/chat/docs/sdk/flutter/guides/ai-integrations/#syntax-highlighting) page, 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. See [Mark assistant messages with `ai_generated`](https://getstream.io/chat/docs/sdk/flutter/guides/ai-integrations/#mark-assistant-messages-with-ai_generated) for how the backend sets the flag.

## Showing the AI indicator

The backend reports what the LLM is doing with `ai_indicator.*` events. 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();
  }
}
```

Create one handler per channel, and dispose of it with the screen. Then show an `AITypingIndicatorView` below the message list while the state is `thinking` or `checkingSources`:

```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',
          _ => 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. The sample app keeps the indicator visible with a "Generating" caption for that state, which the snippet above leaves out.

The typewriter can still be revealing text after the backend has finished. To keep the "Generating" caption visible until the animation is done, forward `onTypewriterStateChanged` from `StreamingMessageView` and treat `TypewriterState.typing` as still generating. The callback is invoked while the message list builds, so schedule `setState` for after the frame with `WidgetsBinding.instance.addPostFrameCallback`. The sample app's `AITypingIndicatorStateView` combines both signals into one caption, and a complete version is in [`conversation_view.dart`](https://github.com/GetStream/chat-ai-samples/blob/main/flutter/lib/src/conversation_view.dart). `AITypingState.error` is a terminal state too. You can optionally show an error caption for it, and the backend has already written the error into the message.

## Sending messages and stopping a response

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: () => channel.stopAIResponse(),
);
```

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,
  };
});
```

See [Let users stop the response](https://getstream.io/chat/docs/sdk/flutter/guides/ai-integrations/#let-users-stop-the-response) for what the backend does with the stop event.

## Conversation starters

On a new chat screen, dock an `AISuggestionsView` above the composer and send the tapped suggestion as a message:

```dart
AISuggestionsView(
  suggestions: const [
    'What are the docs for the AI SDK?',
    'Summarize my last conversation',
  ],
  onSuggestionSelected: (text) => channel.sendMessage(Message(text: text)),
);
```

You can find a complete, working integration in the [Flutter AI 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/guides/ai-integrations/stream-chat-integration/](https://getstream.io/chat/docs/sdk/flutter/guides/ai-integrations/stream-chat-integration/).