Streaming messages

StreamingMessage renders a reply as the agent writes it: Markdown, code with syntax highlighting, tables and charts, typed out as the text grows, as ChatGPT does. AIConversation shows every reply with it, and you can use it on its own in a component of yours:

import { StreamingMessage } from "stream-chat-react/ai-components";

<StreamingMessage text={reply.text} />;
Info:

StreamingMessage, AIMarkdown and AIStateIndicator are part of the stream-chat-react/ai-components entry point. See the Quickstart to install them and load their stylesheet.

Type the reply out

Pass the reply's text as it grows. StreamingMessage types out what's new since the last update, from where it was. Whatever has arrived is written out within about 400 ms, so the text on screen is never far behind: a long chunk types fast, and a trickle at the pace it comes.

The text there when it mounts shows at once. That's why a stored reply doesn't type itself out again when the user scrolls back to it. Text that gets shorter, such as a half-written reply the backend replaces with an error, shows at once too.

Prop Description Default
text The Markdown to render. -
letterIntervalMs A fixed pace instead of catching up: the time between steps, in milliseconds. 30 when only renderingLetterCount is set
renderingLetterCount A fixed pace instead of catching up: the letters written each step. 2 when only letterIntervalMs is set
<StreamingMessage
  text={reply.text}
  letterIntervalMs={30} // a step every 30 ms
  renderingLetterCount={3} // 3 letters a step
/>

With Stream Chat, the reply's text arrives with every message.updated event while the backend streams it. What your backend sends covers how to write it.

Show the rest at once

StreamingMessage takes a ref, a StreamingMessageRef, whose skipAnimation() shows the whole text at once. Call it when the user stops the reply, so what has arrived shows without typing out:

import { useEffect, useRef } from "react";
import { useChannel } from "stream-chat-react";
import {
  StreamingMessage,
  type StreamingMessageRef,
} from "stream-chat-react/ai-components";

export const Reply = ({ text }: { text: string }) => {
  const channel = useChannel();
  const ref = useRef<StreamingMessageRef>(null);

  useEffect(() => {
    const subscription = channel.on("ai_indicator.stop", () =>
      ref.current?.skipAnimation(),
    );
    return () => subscription.unsubscribe();
  }, [channel]);

  return <StreamingMessage ref={ref} text={text} />;
};

In the message list

In the MessageList of stream-chat-react, a message that Chat's isMessageAIGenerated marks renders with StreamedMessageText, which types it out in the same way, and shows the rest at once when ai_indicator.stop reaches the channel. SDK integration shows how to turn it on, and how to use StreamingMessage in its place.

StreamedMessageText keeps showing what's on screen when the text gets shorter, until it mounts again. Swap in StreamingMessage if your backend replaces a reply with a shorter text, such as an error.

Type text out your own way

To type out text in a component of your own, use the useMessageTextStreaming hook from stream-chat-react, which StreamedMessageText is built on. It takes the text, and a fixed pace as streamingLetterIntervalMs (30 by default) and renderingLetterCount (2 by default). It returns the streamedMessageText typed out so far, and skipAnimation():

import { useMessageTextStreaming } from "stream-chat-react";
import { AIMarkdown } from "stream-chat-react/ai-components";

export const TypedReply = ({ text }: { text: string }) => {
  const { streamedMessageText } = useMessageTextStreaming({
    renderingLetterCount: 4,
    streamingLetterIntervalMs: 15,
    text,
  });

  return <AIMarkdown>{streamedMessageText}</AIMarkdown>;
};

The useMessageTextStreaming reference explains how the typing follows the text, and how to replace StreamedMessageText with a component built on it.

Code and charts

AIMarkdown is the Markdown renderer StreamingMessage uses, with GitHub-flavored Markdown such as tables. Render it directly when the text is complete and doesn't need typing out:

import { AIMarkdown } from "stream-chat-react/ai-components";

<AIMarkdown>{reply.text}</AIMarkdown>;

Code blocks with a language render with syntax highlighting and line numbers. A code block whose language is chartjs, or json that holds a Chart.js config, renders as a chart instead. The config needs a type and data with datasets, and the chart types are bar, line, pie, doughnut, bubble, scatter, radar and polarArea:

```chartjs
{
  "type": "bar",
  "data": {
    "labels": ["Jan", "Feb", "Mar"],
    "datasets": [{ "label": "Signups", "data": [120, 180, 240] }]
  }
}
```

The chart renderer loads the first time a chart shows, with a "Loading chart..." placeholder until it's ready. If it fails to load or the config is invalid, the block shows as code instead of breaking the message. Any other json block stays highlighted code.

Prop Description Default
children The Markdown to render. -
toolComponents Components that render code blocks by their language, as Record<string, ComponentType<ToolComponentProps>>. chartjs and json charts
markdownComponents Overrides for the Markdown elements, as the components of react-markdown, such as code or pre. -

Render your own blocks

To render a block your own way, such as a tool's output, register a component for its language in toolComponents. It receives the block's content as data, and a fallback, the block as code, to return when the data can't be rendered:

import {
  AIMarkdown,
  type ToolComponentProps,
} from "stream-chat-react/ai-components";

// Renders a block tagged `weather`, such as {"city":"Oslo","temperature":4}.
const WeatherTool = ({ data, fallback }: ToolComponentProps) => {
  try {
    const { city, temperature } = JSON.parse(data) as {
      city: string;
      temperature: number;
    };
    return (
      <div className="weather-tool">
        {city}: {temperature}°C
      </div>
    );
  } catch {
    return fallback;
  }
};

// Declared outside the component, so AIMarkdown gets the same object on every render.
const toolComponents = { weather: WeatherTool };

export const MessageContent = ({ text }: { text: string }) => (
  <AIMarkdown toolComponents={toolComponents}>{text}</AIMarkdown>
);

A component registered for chartjs or json takes the place of the built-in chart.

Show what the agent is doing

Before the first word of a reply arrives, the agent may spend a while thinking or calling tools. AIStateIndicator shows that it's working: a text with three pulsing dots. Pass the text, or let it pick a playful phrase, such as "Brewing up an answer":

import { AIStateIndicator } from "stream-chat-react/ai-components";

<AIStateIndicator text="Checking external sources" />;

It picks its phrase when it mounts, so change its key to pick a new one. It renders with role="status", so screen readers read the text.

With Stream Chat, the backend says what the agent is doing with ai_indicator.* events, and useAIState returns the channel's state. Show the indicator for the states you want, inside Channel:

import { AIStates, type Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  MessageComposer,
  MessageList,
  useAIState,
  useChannel,
} from "stream-chat-react";
import { AIStateIndicator as AIStateIndicatorView } from "stream-chat-react/ai-components";

const indicatorText: Record<string, string> = {
  [AIStates.Thinking]: "Thinking",
  [AIStates.ExternalSources]: "Checking external sources",
};

const AIIndicator = () => {
  const channel = useChannel();
  const { aiState } = useAIState(channel);
  const text = indicatorText[aiState];

  return text ? <AIStateIndicatorView text={text} /> : null;
};

export const ChannelView = ({ channel }: { channel: StreamChannel }) => (
  <Channel channel={channel}>
    <MessageList />
    <AIIndicator />
    <MessageComposer />
  </Channel>
);

Once the reply's text arrives, during AI_STATE_GENERATING, the text itself shows the agent is working, so the example hides the indicator. Say what the agent is doing lists the states and the events that set them.

stream-chat-react exports an AIStateIndicator of its own, from its main entry point, which is why the example renames this one on import. That one follows the channel's state by itself, but only shows "Thinking..." and "Generating...". This one only draws the indicator. See SDK integration.

In an AIConversation, show what the agent is doing in its footer with AITypingIndicator, and the agent thinking with isThinking, as Conversation view describes.

Styling and texts

The components follow the SDK's theme in light and dark mode, and their colors come from the --str-chat__ai-* variables described in Theming: syntax-* for the code highlighting, table-* for tables, inline-code-bg for inline code, and text-secondary for the indicator. Their class names start with str-chat__ai-, such as str-chat__ai-streaming-message and str-chat__ai-state-indicator.

Their texts go through the SDK's translations, such as aiComponents.chart.loading.text ("Loading chart...") and the indicator's phrases under aiComponents.stateIndicator.*. English is built in. To change or translate them, register your own dictionary, as described in Localization.