Quickstart
The AI components give your React app the screens of an AI assistant: a conversation that streams the agent's replies, a prompt composer with attachments and dictation, the agent's reasoning and tool calls, and a face for a voice agent. They ship with stream-chat-react in its stream-chat-react/ai-components entry point. The agent behind them runs on Stream Agents: you describe it once, with its instructions, knowledge, tools and voice, and your app talks to it with the @stream-io/stream-agents SDK.
This page shows what you can build, how the pieces fit together, and takes you from an empty project to a working assistant.
What you can build
| You want to build | Start with |
|---|---|
| An AI-first app, like ChatGPT or Claude: one user, one assistant, many conversations | This page, then the Personal assistant use case |
| A support agent that answers from your help center, looks up orders and hands over to your team | The Customer support agent use case |
| A voice agent the user talks to, with a visual presence and a written transcript | The Voice agent use case |
| An agent that shows its work and asks before it acts | Reasoning and tool calls |
| An agent that runs tools in the browser, on the page the user has open | The Personal assistant use case |
| Replies that are UI, such as a trip plan with a map and a form | Generative UI |
AI replies in a chat you already built with MessageList |
SDK integration |
Voice runs through all of them. The composer has a microphone button that dictates into it, and the same agent can hold the conversation out loud: VoiceSession puts it on a call with the user, VoiceGlow or VoiceWave shows what it's doing, and AIConversation writes out what was said.
How it fits together
Your app never talks to the model, and it doesn't run the agent. It opens a session with your agent and asks it things. The agent runs on Stream's router, which calls the model, runs the agent's tools and writes the conversation into a Stream Chat channel as it goes. The components show that channel:
sequenceDiagram
participant App as React app
participant Router as Stream Agents
participant LLM as Model
participant Chat as Stream Chat
App->>Router: sessions.create() with @stream-io/stream-agents
App->>Router: responses.create("How do I...?")
Router->>LLM: The conversation so far
LLM-->>Router: Reasoning, tool calls, tokens
Router-->>Chat: The question, and the reply as it streams, with its steps
Chat-->>App: The channel's messages
Note over App: AIConversation shows them@stream-io/stream-agentssigns the user in, opens sessions, asks, stops a reply, answers the questions of tools that ask first, and runs the tools your page gives the agent.stream-chat-react/ai-componentsshows it all:AIConversationandAIMessageComposer, the reasoning and tool calls of each reply, the UI the agent writes, and the voice visualizations.- Your agent lives in a folder of plain files that you sync with the Stream CLI: its instructions, knowledge, skills, tools, connectors and models.
For voice, VoiceSession starts the agent on a call and joins it from the browser. The agent hears the user and answers out loud, and what is said is written into the session's channel.
Before you start
- A Stream app, from the Stream Dashboard.
- The beta Stream CLI, which has the
getstream agentscommands. - A React 18 or later app.
Step 1: Create the agent
Install the beta CLI, pick your Stream app, and make an agent from the text template:
curl -fsSL https://getstream.io/cli/beta/install.sh | bash
getstream init # pick or create a Stream app
getstream agents init my-agent --template text # or --template voiceThat creates agents/my-agent/, with the agent's settings in agent.yaml, its system prompt in instructions.md, and a guardrail in guardrail.md. The template's guardrail turns away anything that isn't about Acme Corp, its example company, so write what the agent is for in both files, then sync it to Stream and try it in the terminal:
getstream agents sync my-agent
getstream agents test my-agentThe Stream Agents quickstart covers the agent's folder: knowledge, skills, tools, connectors and guardrails.
Step 2: Install the SDKs
Add @stream-io/stream-agents, the client for your agent, and stream-chat-react, which has the views. stream-chat is the Stream Chat client that reads the conversation the agent writes:
npm install @stream-io/stream-agents stream-chat@9 stream-chat-react@14This page uses stream-chat-react v14, with stream-chat v9. The stream-chat-react/ai-components entry point ships from 14.13.0, VoiceWave, VoiceGlow and useAudioLevel from 14.14.0, AIConversation with the reply's steps, approvals, sign-ins, suggestions and reply actions from 14.15.0, and generative UI from 14.16.0, so this page needs 14.15.0 or later. The entry point needs no other package. Load its stylesheet next to the default SDK styles, once, where your app starts:
import "stream-chat-react/css/index.css";
import "stream-chat-react/css/ai-components.css";If your agent answers with generative UI, also install the packages of the format it writes, A2UI or OpenUI, as that guide describes. The composer's microphone button dictates with the browser's Web Speech API, so it needs no package, and it renders nothing in browsers without it, such as Firefox.
Is your agent on a backend of your own? Skip @stream-io/stream-agents, and have your backend write the agent's replies into a Stream Chat channel, by hand or with the Stream Chat AI SDK or the Stream Chat LangChain SDK. Conversation state describes what it sends: the ai_generated and generating fields of a reply, and the AI indicator events that say what the agent is doing. The client keeps the latest of those on the channel, as one of the states stream-chat-react exports as AIStates:
AIStates.Thinking,AI_STATE_THINKING: the model is working out an answer.AIStates.ExternalSources,AI_STATE_EXTERNAL_SOURCES: the model is checking external sources, such as with a tool call.AIStates.Generating,AI_STATE_GENERATING: the answer is being written into the message.AIStates.Error,AI_STATE_ERROR: the reply failed.AIStates.Stop,AI_STATE_STOP: the user asked to stop the reply.AIStates.Idle,AI_STATE_IDLE: nothing is happening.
The useAIState hook from stream-chat-react reads it, as Conversation state describes.
Step 3: Sign the user in
Create one Client for your app with your API key, and set the user once they sign in. The user's Stream token comes from your backend, which mints it with your API secret, as described in authentication. The client asks for it again when it expires:
import { Client } from "@stream-io/stream-agents";
export const api = new Client({ apiKey: "your_api_key" });
export async function signIn(user: { id: string; name: string }) {
await api.setUser(user, () =>
fetch("/api/stream-token").then((response) => response.text()),
);
}To try things out before you have a backend, mint a token for a test user with the CLI, getstream token jane --ttl 2h, and pass it as api.setUser({ id: "jane" }, "...").
Step 4: Connect a conversation to the agent
A session is one conversation with your agent, and its channel is where the agent writes it: the user's questions, each reply as it streams, and the reasoning and tool calls of each reply as the reply's attachments. Add a function to agent.ts that opens a session and its channel:
import type { Session } from "@stream-io/stream-agents";
// Lets session.chat() connect to Stream Chat. Import it once.
import "@stream-io/stream-agents/chat";
import type { Channel, StreamChat } from "stream-chat";
export type AgentChat = {
session: Session;
client: StreamChat;
channel: Channel;
};
/** Opens a conversation with the agent, or the channel of one already open. */
export async function openConversation(session?: Session): Promise<AgentChat> {
const opened = session ?? (await api.agent("my-agent").sessions.create());
// Connects to Stream Chat as the user signIn() set.
const { client, channel } = await opened.chat();
const agentChannel = channel as Channel;
await agentChannel.watch();
return {
session: opened,
client: client as StreamChat,
channel: agentChannel,
};
}The views show AIMessage values. fromStreamMessage turns each of the channel's messages into one: a reply is a message with ai_generated: true, it's being written while generating is true, and its steps are its ai_reasoning and ai_tool_call attachments. The channel's state lists its messages oldest first, which is the order the conversation wants:
import { useEffect, useMemo, useState } from "react";
import type { Channel, LocalMessage } from "stream-chat";
import { fromStreamMessage } from "stream-chat-react/ai-components";
export function useAgentMessages(channel: Channel) {
const [items, setItems] = useState<LocalMessage[]>([]);
useEffect(() => {
const update = () => setItems([...channel.state.messages]);
update();
// Every event of the channel, so new and updated messages show.
const subscription = channel.on(update);
return () => subscription.unsubscribe();
}, [channel]);
return useMemo(
() => items.map((message) => fromStreamMessage(message)),
[items],
);
}Step 5: Show the conversation
Show the conversation with AIConversation, and the composer below it. The composer asks through the session, and the router writes the question into the channel itself. The approver answers the tools that ask before they run:
import { useMemo, useSyncExternalStore, type FormEvent } from "react";
import { Button, Chat } from "stream-chat-react";
import {
AIClientIdentity,
AIConversation,
AIMessageComposer,
type AIToolApprover,
} from "stream-chat-react/ai-components";
import type { AgentChat } from "./agent";
import { useAgentMessages } from "./useAgentMessages";
export const AgentConversation = ({ chat }: { chat: AgentChat }) => {
const { session, client, channel } = chat;
const messages = useAgentMessages(channel);
// What the agent is doing: idle, listening, responding, working or ended.
const { kind } = useSyncExternalStore(
session.state.subscribe,
session.state.get,
);
const isAnswering = kind === "responding" || kind === "working";
const approver = useMemo<AIToolApprover>(
() => ({
userId: client.userID ?? "",
clientId: AIClientIdentity.installId(),
decide: async (call, allowed) => session.decide(call.id, { allowed }),
}),
[client, session],
);
const ask = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const text = String(new FormData(event.currentTarget).get("message") ?? "");
if (!text.trim() || isAnswering) return;
event.currentTarget.reset();
void session.responses.create(text);
};
return (
<Chat client={client}>
{/* `str-chat` is the SDK's theme scope: its fonts, colors and spacing. */}
<div
className="str-chat"
style={{
display: "flex",
flexDirection: "column",
height: "100dvh",
maxWidth: "48rem",
marginInline: "auto",
}}
>
<div style={{ flex: 1, minHeight: 0 }}>
<AIConversation approver={approver} messages={messages} />
</div>
<div style={{ padding: "0 1rem 1rem" }}>
<AIMessageComposer onSubmit={ask}>
<AIMessageComposer.TextInput name="message" />
{/* The composer stacks its children: put the buttons in a row. */}
<div
style={{
display: "flex",
gap: "0.25rem",
justifyContent: "flex-end",
}}
>
<AIMessageComposer.SpeechToTextButton />
{isAnswering ? (
<Button
appearance="outline"
onClick={() => session.interrupt()}
size="sm"
type="button"
variant="secondary"
>
Stop
</Button>
) : (
<AIMessageComposer.SubmitButton />
)}
</div>
</AIMessageComposer>
</div>
</div>
</Chat>
);
};AIConversation scrolls inside itself, so give it a height, here as the growing child of a column. The composer's form grows to fill a flex container it's placed in, so it sits in a div of its own below the conversation, and it stacks what's inside it, so its buttons go in a row of their own. The column is at most 48rem wide and centered, so lines stay short enough to read on a wide window.
The screen renders inside an element with the str-chat class, the SDK's theme scope. Inside it, the AI components and the SDK's Button take the SDK's fonts, colors and spacing. Chat provides the client and the translations but renders no element, so without one the page keeps the browser's default font. Chat gives the components the SDK's translations: outside it, their accessible names show as raw aria/ keys, as described in Localization.
Sign the user in, then open a conversation, for example from a button:
import { useState } from "react";
import { Button } from "stream-chat-react";
import { openConversation, signIn, type AgentChat } from "./agent";
import { AgentConversation } from "./AgentConversation";
export const App = () => {
const [chat, setChat] = useState<AgentChat>();
const start = async () => {
await signIn({ id: "jane", name: "Jane" });
setChat(await openConversation());
};
return chat ? (
<AgentConversation chat={chat} />
) : (
<div className="str-chat">
<Button
appearance="solid"
onClick={() => void start()}
size="md"
type="button"
variant="primary"
>
Ask the agent
</Button>
</div>
);
};Run the app and ask something. While the agent works on a reply that shows nothing yet, the conversation shows it thinking. Then the reply writes itself out, with Markdown, code and charts, and with the reasoning and tool calls that led to it. While it writes, the Stop button stops it, and under each finished reply the user can copy and share it.
The Conversation view guide covers what else the screen can show, such as the question while it's sent, older messages, and an empty state. The Composer guide adds attachments and chat options.
Step 6: Add voice
The composer already takes voice: its microphone button dictates into it.
To let the user talk to the agent instead, start a voice session. The agent joins a call right away, and the page joins it with the microphone on. VoiceSession lives in the @stream-io/stream-agents/video entry point, and the Video React SDK plays the agent's audio:
npm install @stream-io/video-react-sdkGive the agent a face with VoiceGlow, in stream-chat-react from 14.14.0, which moves differently while the agent is connecting, listening, thinking and speaking:
import { VoiceSession } from "@stream-io/stream-agents/video";
import {
ParticipantsAudio,
StreamCall,
StreamVideo,
useCallStateHooks,
} from "@stream-io/video-react-sdk";
import { useEffect, useSyncExternalStore } from "react";
import { Button } from "stream-chat-react";
import { VoiceGlow } from "stream-chat-react/ai-components";
import { api } from "./agent";
/** Starts a session with voice on. Call it from a click. */
export const startVoice = () => VoiceSession.start(api.agent("my-agent"));
export const AgentVoice = ({ voice }: { voice: VoiceSession }) => {
const { call, client, muted, status } = useSyncExternalStore(
voice.subscribe,
voice.get,
);
// Joins the call with the microphone on. Run twice, it joins once.
useEffect(() => {
voice.join().catch((error: unknown) => console.error(error));
}, [voice]);
return (
<div className="str-chat">
<VoiceGlow state={status} style={{ width: 240, height: 240 }} />
{call && client && (
<StreamVideo client={client}>
<StreamCall call={call}>
<AgentAudio />
</StreamCall>
</StreamVideo>
)}
<Button
appearance="outline"
onClick={() => void voice.setMuted(!muted)}
size="md"
type="button"
variant="secondary"
>
{muted ? "Unmute" : "Mute"}
</Button>
{/* Leaves the call and ends the session startVoice() opened. */}
<Button
appearance="solid"
onClick={() => void voice.leave()}
size="md"
type="button"
variant="danger"
>
Hang up
</Button>
</div>
);
};
/** Plays the agent. */
const AgentAudio = () => {
const { useRemoteParticipants } = useCallStateHooks();
return <ParticipantsAudio participants={useRemoteParticipants()} />;
};Call startVoice() from a click: a browser asks for the microphone and plays audio only after one. join() rejects when joining fails, for example when the user denies the microphone.
What is said on the call is written into the session's channel, so openConversation(voice.session) and the views from step 5 show it as a transcript. The Voice agent use case builds the whole screen: the agent's state, the visualizations, mute and hang up, and what was said written out as a conversation. An agent made with --template voice also lets you pick its speech-to-text and text-to-speech models, its voice and its greeting.
Theming
The AI components follow the stream-chat-react theme. Inside .str-chat they pick up the SDK's light and dark theme tokens, and outside of it they fall back to built-in light colors.
To change them, set the --str-chat__ai-* CSS variables. The stylesheet never declares these variables itself, so your values win at any scope, including :root:
:root {
--str-chat__ai-accent: #7c3aed;
--str-chat__ai-user-message-bg: #ede9fe;
--str-chat__ai-syntax-keyword: #c084fc;
--str-chat__ai-voice-color: #7c3aed;
}| Variable | Styles |
|---|---|
--str-chat__ai-text-primary, --str-chat__ai-text-secondary |
Text |
--str-chat__ai-accent |
Accents, such as the icons of a reply's steps |
--str-chat__ai-user-message-bg, --str-chat__ai-user-message-text |
The user's message bubbles |
--str-chat__ai-border |
Borders, such as the composer's |
--str-chat__ai-syntax-* |
Code highlighting |
--str-chat__ai-voice-color, --str-chat__ai-voice-backdrop |
VoiceGlow and VoiceWave, and the surface they sit on, which picks their look |
The stylesheet's _tokens.scss lists every one, with the SDK token it follows inside .str-chat and its color outside. Focus rings use the SDK's --str-chat__border-utility-focused token.
The rules use the str-chat__ai- class prefix (for example str-chat__ai-message-composer__form and str-chat__ai-streaming-message), so you can target them with your own CSS, in a CSS layer like the rest of the SDK styles. The components draw stream-chat-react icons, which you replace through WithComponents like any other SDK icon.
Localization
Every text the components show, such as the composer's "Ask a question..." placeholder or "Thought for 12s", goes through Streami18n like the rest of the SDK, which bundles 12 languages. Inside Chat (or a TranslationProvider) they follow the active language. Outside it, texts keyed by an English sentence still read correctly, but accessible names keyed with an aria/ prefix show the key itself, such as aria/Send, so render the components inside Chat.
The keys are the English texts, such as Ask a question... and Thought for {{ duration }}. To change some of them, pass Chat an instance whose translationsForLanguage lists the keys to change:
import { Chat, Streami18n } from "stream-chat-react";
const streamI18n = new Streami18n({
language: "en",
translationsForLanguage: {
"Ask a question...": "Message the assistant",
},
});
<Chat client={client} i18nInstance={streamI18n}>
{/* the conversation and the composer */}
</Chat>;Every other text stays as it is. The Localization guide covers switching and adding languages, and the library's en.json lists every key.
The components
Everything below is exported from stream-chat-react/ai-components.
| Component or hook | What it does | Guide |
|---|---|---|
AIConversation |
Lays out a conversation the way AI assistants do | Conversation view |
AIUserMessage, AIAssistantMessage, AIReplyActions, AIMessageAttachment |
Show the user's messages and the agent's replies, their images and files, and the actions under each reply | Conversation view |
AISuggestions |
Offers conversation starters | Conversation view |
fromStreamMessage |
Turns a Stream Chat message into the AIMessage the views show |
Conversation state |
AIMessageComposer, ChatOptionsList, SelectedChatOptionChip, useText, useAttachments, useIsDisabled, useAIMessageComposerContext |
Takes what the user writes, with attachments, chat options and a model picker | Composer |
useSpeechToText |
Dictates with the browser's Web Speech API | Composer |
StreamingMessage, AIMarkdown |
Types out Markdown, code, tables and charts as they stream | Streaming messages |
AIStateIndicator, AITypingIndicator |
Shows what the agent is doing before its reply has text | Streaming messages |
AIMessageParts, AIMessagePart, AIReasoning, AIThinking, AIToolCall |
Shows a reply's reasoning and tool calls | Reasoning and tool calls |
AIToolApproval, AIToolApprovalCard, AIToolArguments, AIToolApproverProvider, useAIToolApprover, AIClientIdentity |
Asks the user before a tool runs | Reasoning and tool calls |
AIToolAuthorization, AIToolAuthorizationCard, createPopupAuthorizer, completeConnectorSignIn |
Signs the user in to a connector a tool call waits for | Reasoning and tool calls |
AIUIHost, AIUIView, defineAIUIComponent, AIUIComponentView, AIUIRuntimeView, AIUIEntrance, AIUIEntranceStage, useAIUIEntrance, useAIUIHostRefresh |
Shows UI the agent writes in place of a reply's text | Generative UI |
VoiceGlow, VoiceWave, useAudioLevel |
Shows what a voice agent is doing | Voice visualizations |
useHeardText |
Reveals a spoken reply at the pace the user hears it | Voice agent |
--str-chat__ai-* CSS variables |
Set the colors of all of them | Theming |
The main stream-chat-react entry point has AI support of its own, for chats built with MessageList: StreamedMessageText, an AIStateIndicator that follows the channel's AI state, and the StopAIGenerationButton the default composer shows, described in SDK integration. Its useAIState hook is described in Conversation state, and its useMessageTextStreaming hook in Streaming messages.
Migrating from @stream-io/chat-react-ai
The components used to ship as the standalone @stream-io/chat-react-ai package, which they replace. To move to stream-chat-react/ai-components:
-
Dependencies. Remove
@stream-io/chat-react-ai, and upgradestream-chat-reactto 14.13.0 or later. Removematerial-symbolsas well if you only installed it for these components: they renderstream-chat-reacticons now. -
Imports. Replace the package name in imports, and the stylesheet path:
Before After @stream-io/chat-react-aistream-chat-react/ai-components@stream-io/chat-react-ai/styles/index.cssstream-chat-react/css/ai-components.css -
Class names and CSS variables. The
aicr__prefix becamestr-chat__ai-.aicr__ai-message-composer…becomesstr-chat__ai-message-composer…, every otheraicr__…class becomesstr-chat__ai-…, and--aicr__…variables become--str-chat__ai-…. The full mapping table lists every renamed class and variable. -
useMessageTextStreaming. The hook is no longer exported next to the AI components. Import it fromstream-chat-reactand rename itsletterIntervalMsoption tostreamingLetterIntervalMs. TheStreamingMessageprops haven't changed. -
SpeechToTextButton. It's only available asAIMessageComposer.SpeechToTextButton, and itsoptionsprop is now passed to the speech recognizer.
The component APIs are otherwise unchanged.
Next steps
- Customer support agent: answers from your help center, order lookups, a refund the user confirms, and a handover to your team.
- Personal assistant: a list of conversations, tools that run in the browser, and a voice mode.
- Voice agent: a voice conversation with the agent as a glowing globe, and what was said written out.
- The ChatGPT-style app in the
examples/ai-chatbotfolder of thestream-chat-reactrepository. - Stream Agents: the agent itself, its tools, knowledge, connectors and phone numbers.