Radix UI Primitives

MessagePartPrimitive

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();
MessagePartRuntime
addToolResultrequired: (result: any) => void

resumeToolCallrequired: (payload: unknown) => void

pathrequired: MessagePartRuntimePath

getStaterequired: () => MessagePartState

subscriberequired: (callback: () => void) => Unsubscribe

useMessagePart

import { useMessagePart } from "@assistant-ui/react";

const MessagePart = useMessagePart();
TextMessagePartState
typerequired: "text"

textrequired: string

parentId?: string

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"; }

AudioMessagePartState
typerequired: "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"; }

ToolCallMessagePartState
typerequired: "tool-call"

toolCallIdrequired: string

toolNamerequired: string

argsrequired: ReadonlyJSONObject

result?: unknown

isError?: boolean | undefined

argsTextrequired: string

artifact?: unknown

interrupt?: { type: "human"; payload: unknown; }

parentId?: string

messages?: 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();
TextMessagePartState
typerequired: "text"

textrequired: string

parentId?: string

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"; }