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} />;StreamingMessage, AIMarkdown and AIStateIndicator are available from version 14.13.0 of stream-chat-react, in 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 { useChannelStateContext } from "stream-chat-react";
import {
StreamingMessage,
type StreamingMessageRef,
} from "stream-chat-react/ai-components";
export const Reply = ({ text }: { text: string }) => {
const { channel } = useChannelStateContext();
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 typing follows the text this way:
- The text there when the component mounts shows at once. Only text added later is typed out.
- When
textgrows, typing goes on from where it was. - When
textgets shorter than what's on screen, the old text stays untilskipAnimation()is called or the component mounts again. Give the component a newkeywhen it shows another message.
To type out the message list's AI replies your own way, replace StreamedMessageText with a component built on the hook, through WithComponents:
import type { PropsWithChildren } from "react";
import {
MessageText,
useMessageContext,
useMessageTextStreaming,
WithComponents,
} from "stream-chat-react";
import type { StreamedMessageTextProps } from "stream-chat-react";
const TypedMessageText = ({
message: messageFromProps,
renderText,
}: StreamedMessageTextProps) => {
const { message: messageFromContext } = useMessageContext();
const message = messageFromProps ?? messageFromContext;
const text = message.text ?? "";
const { skipAnimation, streamedMessageText } = useMessageTextStreaming({
renderingLetterCount: 4,
streamingLetterIntervalMs: 15,
text,
});
return (
<div className="typed-message">
<MessageText
message={{ ...message, text: streamedMessageText }}
renderText={renderText}
/>
{streamedMessageText.length < text.length && (
<button onClick={skipAnimation} type="button">
Show all
</button>
)}
</div>
);
};
const overrides = { StreamedMessageText: TypedMessageText };
export const WithTypedAIMessages = ({ children }: PropsWithChildren) => (
<WithComponents overrides={overrides}>{children}</WithComponents>
);Show all appears while the text is still being typed out. To change only the speed, give the default component other values instead: <StreamedMessageText {...props} streamingLetterIntervalMs={15} />. Keep the speed readable, and type out only the messages isMessageAIGenerated marks.
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,
Channel,
MessageComposer,
MessageList,
useAIState,
useChannelStateContext,
} 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 } = useChannelStateContext();
const { aiState } = useAIState(channel);
const text = indicatorText[aiState];
return text ? <AIStateIndicatorView text={text} /> : null;
};
export const ChannelView = () => (
<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 listens for the channel's events 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. Their keys are the English texts, such as Loading chart... and the indicator's phrases, such as Brewing up an answer, and the SDK's bundled languages translate them. To change them, register your own translations, as described in Localization.