const isMessageAIGenerated = (message) => !!message.ai_generated;
const App = ({ children }) => (
<Chat client={client} isMessageAIGenerated={isMessageAIGenerated}>
{children}
</Chat>
);SDK Integration
The AI components work seamlessly with the React Chat SDK.
StreamedMessageText
The StreamedMessageText component comes already integrated within the SDK. It is rendered instead of the MessageText component if the isMessageAIGenerated function recognizes provided message object as AI generated.
It will display the message using a typewriter animation (similar to how ChatGPT does it) and it will automatically manage the rendering of the UI for you.
The component is overridable through the StreamedMessageText prop on the Channel component.
Example usage
The above example will make sure that all the messages with a custom field ai_generated equal to true will be rendered with StreamedMessageText.
AIStateIndicator
The component is not rendered by any other SDK component and thus needs to be imported and rendered by the integrators as a direct or indirect child of the Channel component. It is responsible for adding an indicator of the current AI state, and it will display if it is one of the following:
AI_STATE_GENERATINGAI_STATE_THINKING
Example usage
const isMessageAIGenerated = (message) => !!message.ai_generated;
const App = () => (
<Chat client={client} isMessageAIGenerated={isMessageAIGenerated}>
<Channel channel={channel}>
<MessageList />
<AIStateIndicator />
<MessageInput />
</Channel>
</Chat>
);The above example will make the indicator show right above the MessageInput component when the AI state matches one of the stated above.

StopAIGenerationButton
The purpose of the component is to allow a user to stop the AI response generation prematurely. It is rendered instead of SendMessage button if the AI state is one of the following:
AI_STATE_GENERATINGAI_STATE_THINKING
The component is overridable through the StopAIGenerationButton prop on the Channel component.
If you want to prevent rendering of the component altogether you can set the StopAIGenerationButton to null.
StreamingMessage
Integrating the StreamingMessage within the SDK is relatively easy. The SDK already comes prebuilt with a isMessageAIGenerated factory function that lets the underlying contexts know when we consider a message to be AI generated and renders its internal StreamingMessage whenever this is true. Let us say that a message is considered AI generated if the message.ai_generated property is set to true. In that case, our factory function would look something like this:
const isMessageAIGenerated = (message: LocalMessage) => !!message.ai_generated;Then, we can simply use our new StreamingMessage to like so:
import { StreamingMessage } from "@stream-io/chat-react-ai";
const CustomStreamingMessage = () => {
const { message } = useMessageContext();
return <StreamingMessage text={message.text ?? ""} />;
};
// ...
<Channel {...otherChannelProps} StreamedMessageText={CustomStreamingMessage} />;which will render a message that uses the StreamingMessage from @stream-io/chat-react-ai instead of the default one.
Now since this component is still rendered within our MessageSimple component, there are other components rendered alongside (reactions, read status, etc.) which might not suite your needs. For cleaner look, you can drop most of these by building your own custom message component while preserving default styles:
import {
type MessageUIComponentProps,
useMessageContext,
MessageSimple,
Attachment,
MessageErrorIcon,
Channel,
} from "stream-chat-react";
import { StreamingMessage } from "@stream-io/chat-react-ai";
const CustomMessage = (props: MessageUIComponentProps) => {
const { message, isMyMessage, highlighted, handleAction } =
useMessageContext();
const attachments = message.attachments ?? [];
const hasAttachments = attachments.length > 0;
const rootClassName = clsx(
"str-chat__message str-chat__message-simple",
`str-chat__message--${message.type}`,
`str-chat__message--${message.status}`,
{
"str-chat__message--me": isMyMessage(),
"str-chat__message--other": !isMyMessage(),
"str-chat__message--has-attachment": hasAttachments,
},
);
if (!isMessageAIGenerated(message)) return <MessageSimple {...props} />;
return (
<div className={rootClassName}>
<div className="str-chat__message-inner" data-testid="message-inner">
<div className="str-chat__message-bubble">
{hasAttachments && (
<Attachment
actionHandler={handleAction}
attachments={attachments}
/>
)}
<StreamingMessage text={message?.text || ""} />
<MessageErrorIcon />
</div>
</div>
</div>
);
};
// ...
<Channel {...otherChannelProps} Message={CustomMessage} />;AIMessageComposer
To use the AIMessageComposer, you’ll only really need to wire it to our existing MessageComposer utility.
That would look something like this:
import {
useMessageComposer,
useChatContext,
useChannelActionContext,
useChannelStateContext,
useAttachmentsForPreview,
} from "stream-chat-react";
import { AIMessageComposer } from "@stream-io/chat-react-native-ai";
const CustomMessageComposer = () => {
const composer = useMessageComposer();
const { client } = useChatContext();
const { updateMessage, sendMessage } = useChannelActionContext();
const { channel } = useChannelStateContext();
const { attachments } = useAttachmentsForPreview();
return (
<AIMessageComposer
onChange={(event) => {
const input = event.currentTarget.elements.namedItem(
"attachments",
) as HTMLInputElement | null;
const files = input?.files ?? null;
if (files) {
composer.attachmentManager.uploadFiles(files);
}
}}
onSubmit={async (event) => {
event.preventDefault();
const target = event.currentTarget;
const formData = new FormData(target);
const message = formData.get("message");
composer.textComposer.setText(message as string);
const composedData = await composer.compose();
if (!composedData) return;
target.reset();
composer.clear();
await sendMessage(composedData);
}}
>
<AIMessageComposer.AttachmentPreview>
{attachments.map((attachment) => (
<AIMessageComposer.AttachmentPreview.Item
key={attachment.localMetadata.id}
file={attachment.localMetadata.file}
state={attachment.localMetadata.uploadState}
imagePreviewSource={attachment.localMetadata.previewUri as string}
onDelete={() => {
composer.attachmentManager.removeAttachments([
attachment.localMetadata.id,
]);
}}
onRetry={() => {
composer.attachmentManager.uploadAttachment(attachment);
}}
/>
))}
</AIMessageComposer.AttachmentPreview>
<AIMessageComposer.TextInput />
<AIMessageComposer.FileInput />
<AIMessageComposer.SpeechToTextButton />
<AIMessageComposer.ModelSelect />
<AIMessageComposer.SubmitButton />
</AIMessageComposer>
);
};
//...
<MessageInput Input={CustomMessageComposer} />;You can pick and choose individual input components rendered within AIMessageComposer, you can also group and stylize them by providing own wrappers. Most of these are fairly primitive input components with a bunch of default properties applied for easier use. TextInput, FileInput and SpeechToTextButton control internal state which is accessible through useText or useAttachments.