# Streaming Messages

`StreamingMessageView` renders a reply as the agent writes it: Markdown, code, tables, images and charts, typed out letter by letter, as ChatGPT does. [`AIConversationView`](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-view/) shows every reply with it, and you can use it on its own in a widget of yours:

```dart
StreamingMessageView(
  text: reply.text, // updated in real time as chunks arrive
  expectMoreText: reply.isGenerating, // true while the reply is being generated
);
```

## Type the reply out

While `expectMoreText` is `true`, that is while the backend may still send more of the reply, the view types out the text that's new since the last update. When the text is replaced by something other than a continuation, the view keeps the part it shares with what is on screen and types only the rest. The update that turns `expectMoreText` to `false` is still typed out to the end, so a reply doesn't jump when its last chunk arrives. Only text that arrives after that shows in full at once, as for a reply loaded from history. That's why a stored reply doesn't type itself out again when the user scrolls back to it.

`typingSpeed` sets the speed of the typing, as the time per character. The default is 5 ms. `onTypewriterStateChanged` reports the typewriter's `TypewriterState`: `typing`, `idle`, `paused` or `stopped`.

```dart
StreamingMessageView(
  text: markdownText,
  expectMoreText: isGenerating,
  typingSpeed: const Duration(milliseconds: 5),
  onTypewriterStateChanged: (state) {
    // TypewriterState.typing | idle | paused | stopped
  },
);
```

The typewriter usually falls behind the backend, so the reply is still being revealed after generation ends. To know when it's completely on screen, use `onFinished`. It's called once `expectMoreText` is `false` and every character has been revealed:

```dart
StreamingMessageView(
  text: markdownText,
  expectMoreText: isGenerating,
  onFinished: () => setState(() => showActions = true),
);
```

With reduced motion turned on in the system settings, the view shows each new text in full instead of typing it out.

With Stream Chat, a reply is generating while its message's `generating` custom field is `true`, which you read as `message.extraData['generating'] == true`. [What your backend sends](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-state/#what-your-backend-sends) covers how to set it.

By default, the view renders the revealed text as Markdown. To put the typewriter effect on a layout of your own, drive it with a `TypewriterController` and a `TypewriterBuilder`:

```dart
final controller = TypewriterController(text: '');

// As new chunks arrive from the LLM:
controller.updateText(accumulatedText);

// In your widget tree:
TypewriterBuilder(
  controller: controller,
  builder: (context, value, child) => Text(value.text),
);
```

If you want markdown without the animation, use `AIMarkdownBody`, the renderer that `StreamingMessageView` is built on:

```dart
AIMarkdownBody(
  data: markdownText,
  selectable: true, // enables text selection on desktop and web
  onTapLink: (text, href, title) {
    // Handle the link tap.
  },
);
```

`StreamingMessageView` takes the same `onTapLink`. Links are ignored without it, and opening one needs a launcher of your choice, such as `url_launcher`.

>
> **Info:** `stream_chat_flutter` 10.5.0 deprecated its AI widgets, which moved to `stream_chat_flutter_ai`: `StreamingMessageView`, `AITypingIndicatorView`, `AnimatedDots`, `StreamTypewriterBuilder`, `TypewriterController`, `TypewriterValue`, `TypewriterState` and `TypewriterWidgetBuilder`. To move over, add `stream_chat_flutter_ai` to your app, as described in [Install the SDKs](https://getstream.io/chat/docs/sdk/flutter/ai-components/#step-2-install-the-sdks), and hide the old names when you import `stream_chat_flutter`, as shown in [Stream Chat integration](https://getstream.io/chat/docs/sdk/flutter/ai-components/stream-chat-integration/). Then rename `StreamTypewriterBuilder` to `TypewriterBuilder`, and pass `expectMoreText` to `StreamingMessageView`, from the message's `generating` field, since the view types text out only while it's `true`. Drop `AnimatedDots` and `AITypingIndicatorView.dotCount`: the indicator always shows three dots, styled by `dotColor`, `dotSize` and `dotSpacing`. The typewriter also types faster by default, 5 ms per character instead of 10 ms, and the dots are smaller, 5 pixels instead of 8. Pass `typingSpeed` and `dotSize` to keep the old look.
>

## Code and charts

Code fences are drawn with a `CodeBlockView`: a framed code block with a header with the code's language and a button that copies the code. A code fence whose language is `json`, `chart`, `chartjs`, `echarts`, `highcharts`, `plotly` or `vega`, and whose content is a chart spec, renders as a native chart with a `ChartView` instead. A fence is styled from its opening line onwards, so a block that is still streaming never shows the raw fence markers. A chart fence shows as code until its content reads as a chart spec.

Charts are drawn in Flutter with `fl_chart`, so they need no network. `fl_chart` and the Markdown renderer, `flutter_markdown_plus`, are dependencies of `stream_chat_flutter_ai`, so you don't add them to your app yourself. Syntax highlighting and LaTeX formulas each need a library that you add yourself, such as `re_highlight` and `flutter_math_fork`, or any other, since the package only asks for a callback. Without them, the components still work, but code and formulas render as plain text:

```yaml
dependencies:
  stream_chat_flutter_ai: ^0.2.0
  re_highlight: ^0.0.3 # code highlighting
  flutter_math_fork: ^0.7.4 # math rendering
```

### Charts

The charts read USpec, Chart.js, ECharts, Highcharts, Plotly heatmaps and (a subset of) Vega-Lite payloads. USpec is a simpler format of the package's own, which is handy to describe in your agent's instructions:

````md
```chart
{
  "kind": "bar",
  "title": "Signups per month",
  "xLabel": "Month",
  "yLabel": "Users",
  "series": [
    {
      "name": "2026",
      "points": [{ "x": "Jan", "y": 120 }, { "x": "Feb", "y": 180 }, { "x": "Mar", "y": 240 }]
    }
  ]
}
```
````

`ChartView` renders a `USpec` as a line, area, bar (vertical or horizontal, grouped or stacked), single stacked bar, scatter, bubble, pie (full, donut or semicircle), radar, radial, histogram or heatmap chart. `StreamingMessageView` already renders chart fences for you. `AIMarkdownBody` takes a `chartLanguages` set to change the languages of chart fences, or an empty set to turn charts off. Use `ChartView` directly when you parse the model output yourself, with `USpecParser`:

```dart
final spec = USpecParser.tryParse(jsonString);
if (spec != null) ChartView(spec: spec);
```

Pies, radial charts and single stacked bars, and any other chart except a heatmap or histogram that plots two or more series, get a legend below the plot. Pie slices big enough for it (8% by default, `piePercentageThreshold`) are labeled with their percentage. Turn the legend off or move it above the plot, and the percentages off, with the chart theme:

```dart
ChartView(
  spec: spec,
  theme: const ChartThemeData(
    legendPosition: ChartLegendPosition.top,
    showPiePercentages: false,
  ),
);
```

Each chart describes itself to screen readers, for example "Bar chart, Messages per day, 5 categories, values 8 to 24". Pass `semanticsLabel` to replace that sentence, or an empty string to describe the chart yourself.

### Syntax highlighting

Code blocks render as plain monospace text by default. Highlighting is opt-in, because a grammar set is several times the size of the whole package. Supply your tokenizer through `codeHighlighter`:

```dart
StreamingMessageView(
  text: message,
  codeHighlighter: highlightCode,
);
```

`AIMarkdownBody` takes the same callback as `codeHighlighter`, and `CodeBlockView` as `highlighter`. To highlight the replies of an `AIConversationView`, which takes no `codeHighlighter`, register a `codeBlock` builder with [`streamAIComponentBuilders`](https://getstream.io/chat/docs/sdk/flutter/ai-components/#replacing-components):

```dart
streamAIComponentBuilders(
  codeBlock: (context, props) => CodeBlockView(
    code: props.code,
    language: props.language,
    highlighter: props.highlighter ?? highlightCode,
    theme: props.theme,
  ),
);
```

The callback has a small contract:

```dart
TextSpan? highlightCode(CodeHighlightRequest request);
```

The request carries the `code`, its `language`, the `baseStyle` (the block's monospace style), the `brightness` of the block's background and its resolved `theme`. `language` arrives exactly as the fence wrote it, so case folding and alias resolution are yours to handle. Return `null` for a language you don't cover and the block renders as plain text. A highlighter that throws, or returns spans whose text doesn't match the code, is reported through `FlutterError.onError`, and the block falls back to plain text as well.

Pick the token colors from `request.brightness`, not from the app's brightness, since a code block can be dark in a light app. With `re_highlight`'s themes, for example:

```dart
final palette = request.brightness == Brightness.dark ? vs2015Theme : atomOneLightTheme;
```

The `highlightCode` function used on this page is yours to define. The package's [showcase app](https://github.com/GetStream/stream-chat-flutter-ai/tree/main/packages/stream_chat_flutter_ai/example) contains a complete implementation built on `re_highlight`. Every grammar you import adds to your app size, so keep only the languages your users are likely to see. Prefer a top-level function over an inline closure, so the view can tell a new highlighter from a new closure over the same one.

While a fence is streaming, your highlighter isn't called on every frame. It runs each time the code gains 64 characters, plus one final pass once the text stops changing. Blocks over 20,000 characters skip highlighting.

### Code block appearance

A code block's frame comes from `CodeBlockThemeData`: `brightness`, `backgroundColor`, `headerBackgroundColor`, `languageLabelStyle`, `copyIconColor`, `copiedIconColor` and `codeTextStyle`. By default the block is dark (`#1E1E1E` with `#D4D4D4` text) in light and dark mode alike. Set `brightness` to `Brightness.light` for a light frame, and every default color follows it:

```dart
StreamingMessageView(
  text: message,
  codeBlockTheme: const CodeBlockThemeData(brightness: Brightness.light),
);
```

`StreamingMessageView` and `AIMarkdownBody` pass their `codeBlockTheme` to every fence, and `CodeBlockView` takes it as `theme`. To set it once for the whole app, use `AITheme.codeBlockTheme`, as described in [Theming](https://getstream.io/chat/docs/sdk/flutter/ai-components/#theming).

### Math

The package recognizes LaTeX between `\(...\)` and `\[...\]`, but typesetting is left to you, so it doesn't pull in a TeX renderer. Provide a `mathBuilder`, and formulas render. Without one, the TeX source is shown as text:

```dart
// With `flutter_math_fork` in your pubspec.
StreamingMessageView(
  text: markdownText,
  mathBuilder: (context, tex, style, {required inline}) => Math.tex(
    tex,
    textStyle: style,
    mathStyle: inline ? MathStyle.text : MathStyle.display,
  ),
);
```

Pass `useDollarDelimitersForMath: true` to also accept `$...$` and `$$...$$`. It is off by default, because `$` collides with currency in ordinary prose.

To render formulas everywhere at once, including the replies of an `AIConversationView`, which takes no `mathBuilder`, register a `math` builder with [`streamAIComponentBuilders`](https://getstream.io/chat/docs/sdk/flutter/ai-components/#replacing-components) instead. A `mathBuilder` given to a widget still wins there:

```dart
streamAIComponentBuilders(
  math: (context, props) => Math.tex(
    props.tex,
    textStyle: props.style,
    mathStyle: props.inline ? MathStyle.text : MathStyle.display,
  ),
);
```

## Show what the agent is doing

Before the first word of a reply arrives, the agent may spend a while thinking or calling tools. `AITypingIndicatorView` shows what it's doing: a text of your choice, such as "Thinking" or "Checking sources", followed by three animated dots:

```dart
AITypingIndicatorView(
  text: 'Checking external sources',
  dotColor: Colors.blue, // optional; unset values come from the theme
  dotSize: 5,
  dotSpacing: 4,
);
```

By default the caption is the Stream theme's `bodyDefault` in `textSecondary`, and the dots match its color: three dots of 5 pixels, 4 pixels apart. `dotColor`, `dotSize` and `dotSpacing` are all optional, and `textStyle` is merged over the themed caption, so passing only a font size keeps the themed color. The indicator is a screen-reader live region labeled with its `text`, so keep it mounted and change the `text` to have each new state announced. With reduced motion turned on, the dots don't animate.

An [`AIConversation`](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-state/#show-what-the-agent-is-doing-and-errors) tells you what the agent says it's doing as its `activity`. Without one, [Showing the AI indicator](https://getstream.io/chat/docs/sdk/flutter/ai-components/stream-chat-integration/#showing-the-ai-indicator) shows where the agent says what it's doing. While the agent only thinks, `AIConversationView` shows "Thinking... 4s" by itself, as described in [Show the agent thinking](https://getstream.io/chat/docs/sdk/flutter/ai-components/conversation-view/#show-the-agent-thinking).

## Appearance

The view renders Markdown with the colors and text styles of the `StreamTheme`, and takes a `styleSheet` to override the markdown's styles. These theme data classes, on `AITheme` or in their scopes, as described in [Theming](https://getstream.io/chat/docs/sdk/flutter/ai-components/#theming), style the rest:

- **`CodeBlockThemeData`** (`codeBlockTheme`, `CodeBlockTheme`): the code blocks, as described in [Code and charts](#code-and-charts).
- **`ChartThemeData`** (`chartTheme`, `ChartTheme`): the charts, such as `seriesColors`, `height`, `axisLabelStyle`, `titleTextStyle`, `gridLineColor`, the heatmap's colors, the legend and the pie percentages.
- **`TypingIndicatorThemeData`** (`typingIndicatorTheme`, `TypingIndicatorTheme`): `textStyle`, `dotColor`, `dotSize` and `dotSpacing` of every `AITypingIndicatorView`.

The code blocks' copy button shows the `copy` icon of the `StreamTheme`'s `icons`, and `checkmark` once the code is copied.

Its texts are members of `AITranslations`, as described in [Localization](https://getstream.io/chat/docs/sdk/flutter/ai-components/#localization): `copyCode` and `codeCopied` for the code blocks' copy button, `unnamedChartSeries`, `unnamedChartSlice` and `chartLegendMore` for the charts, and `chartSemanticsLabel` for the sentence a chart is read out as.

---

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