Primitives for text, images, tool calls, and other message content.
Each message can have any number of message parts. Message parts are usually one of text, reasoning, audio, tool-call, or data.
Message part Types
Text
Standard text content, used for both user and assistant messages.
Reasoning
Exposes the assistant's reasoning process, showing how it arrived at its responses. This is typically used only in assistant messages.
Audio
Audio content that can be played back.
Tool Call
Interactive elements that represent tool operations.
Data
Custom data events that can be rendered as UI at their position in the message stream. Each data part has a name and a data payload.
You can use either the explicit format { type: "data", name: "workflow", data: {...} } or the shorthand data-* prefixed format { type: "data-workflow", data: {...} }. The prefixed format is automatically converted to a DataMessagePart (stripping the data- prefix as the name). Unknown message part types that don't match any built-in type are silently skipped with a console warning.
Anatomy
import { MessagePartPrimitive } from "@assistant-ui/react";
const TextMessagePart = () => {
return <MessagePartPrimitive.Text />;
};Primitives
Plain Text
import { MessagePartPrimitive } from "@assistant-ui/react";
<MessagePartPrimitive.Text />;Markdown Text
Renders the message's text as Markdown.
import { MarkdownTextPrimitive } from "@assistant-ui/react-markdown";
<MarkdownTextPrimitive />;Audio
Coming soon.
InProgress
Renders children only if the message part is in progress.
import { MessagePartPrimitive } from "@assistant-ui/react";
<MessagePartPrimitive.InProgress>
<LoadingIndicator />
</MessagePartPrimitive.InProgress>;Tool UI
You can map tool calls to UI components. We provide a few utility functions to make this easier, such as makeAssistantToolUI.
const MyWeatherToolUI = makeAssistantToolUI({
toolName: "get_weather",
render: function MyWeatherToolUI({ args, result }) {
return (
<div className="mb-4 flex flex-col items-center">
<pre className="whitespace-pre-wrap break-all text-center">
get_weather({JSON.stringify(args)})
</pre>
{"result" in result && (
<pre className="whitespace-pre-wrap break-all text-center">
{JSON.stringify(result.result)}
</pre>
)}
</div>
);
},
});Data UI
You can map data events to UI components, similar to tool UIs. There are two approaches:
Inline configuration
Pass a data config to MessagePrimitive.Parts:
<MessagePrimitive.Parts
components={{
data: {
by_name: {
my_chart: ({ name, data }) => <ChartComponent data={data} />,
},
Fallback: ({ name, data }) => (
<pre>{JSON.stringify(data, null, 2)}</pre>
),
},
}}
/>Global registration
Use makeAssistantDataUI or useAssistantDataUI to register data UIs globally. Global registrations take priority over inline configuration.
import { makeAssistantDataUI } from "@assistant-ui/react";
const MyChartUI = makeAssistantDataUI({
name: "my_chart",
render: ({ name, data }) => <ChartComponent data={data} />,
});
// Place inside AssistantRuntimeProvider
function App() {
return (
<AssistantRuntimeProvider runtime={runtime}>
<Thread />
<MyChartUI />
</AssistantRuntimeProvider>
);
}The hook variant allows access to component state:
import { useAssistantDataUI } from "@assistant-ui/react";
function MyComponent() {
useAssistantDataUI({
name: "my_chart",
render: ({ name, data }) => <ChartComponent data={data} />,
});
return null;
}Each data component receives the full data part as props: { type: "data", name: string, data: T, status: MessagePartStatus }.
Context Provider
Message part context is provided by MessagePrimitive.Parts or TextMessagePartProvider
MessagePrimitive.Parts
import { MessagePrimitive } from "@assistant-ui/react";
<MessagePrimitive.Parts
components={{
Text: MyText,
Reasoning: MyReasoning,
Audio: MyAudio,
tools: {
by_name: {
get_weather: MyWeatherToolUI,
},
Fallback: MyFallbackToolUI,
},
data: {
by_name: {
my_chart: MyChartComponent,
},
Fallback: GenericDataComponent,
},
}}
/>;TextMessagePartProvider
This is a helper context provider to allow you to reuse the message part components outside a message message part.
import { TextMessagePartProvider } from "@assistant-ui/react";
<TextMessagePartProvider text="Hello world" isRunning={false}>
<MessagePart.Text />
</TextMessagePartProvider>;Runtime API
useMessagePartRuntime
import { useMessagePartRuntime } from "@assistant-ui/react";
const MessagePartRuntime = useMessagePartRuntime();MessagePartRuntimeaddToolResultrequired: (result: any) => voidresumeToolCallrequired: (payload: unknown) => voidpathrequired: MessagePartRuntimePathgetStaterequired: () => MessagePartStatesubscriberequired: (callback: () => void) => UnsubscribeuseMessagePart
import { useMessagePart } from "@assistant-ui/react";
const MessagePart = useMessagePart();TextMessagePartStatetyperequired: "text"textrequired: stringparentId?: stringstatusrequired: { readonly type: "running"; } | { readonly type: "complete"; } | { readonly type: "incomplete"; readonly reason: "length" | "cancelled" | "content-filter" | "other" | "error"; readonly error?: unknown; } | { readonly type: "requires-action"; readonly reason: "interrupt"; }AudioMessagePartStatetyperequired: "audio"audiorequired: { readonly data: string; readonly format: "mp3" | "wav"; }statusrequired: { readonly type: "running"; } | { readonly type: "complete"; } | { readonly type: "incomplete"; readonly reason: "length" | "cancelled" | "content-filter" | "other" | "error"; readonly error?: unknown; } | { readonly type: "requires-action"; readonly reason: "interrupt"; }ToolCallMessagePartStatetyperequired: "tool-call"toolCallIdrequired: stringtoolNamerequired: stringargsrequired: ReadonlyJSONObjectresult?: unknownisError?: boolean | undefinedargsTextrequired: stringartifact?: unknowninterrupt?: { type: "human"; payload: unknown; }parentId?: stringmessages?: readonly ThreadMessage[]statusrequired: { readonly type: "running"; } | { readonly type: "complete"; } | { readonly type: "incomplete"; readonly reason: "length" | "cancelled" | "content-filter" | "other" | "error"; readonly error?: unknown; } | { readonly type: "requires-action"; readonly reason: "interrupt"; }useMessagePartText
import { useMessagePartText } from "@assistant-ui/react";
const MessagePartText = useMessagePartText();TextMessagePartStatetyperequired: "text"textrequired: stringparentId?: stringstatusrequired: { readonly type: "running"; } | { readonly type: "complete"; } | { readonly type: "incomplete"; readonly reason: "length" | "cancelled" | "content-filter" | "other" | "error"; readonly error?: unknown; } | { readonly type: "requires-action"; readonly reason: "interrupt"; }