Skip to content

Chain of Thought UI

Show AI reasoning steps and tool calls in a collapsible thinking accordion. Build chain-of-thought visualizations in React chat with assistant-ui.

LLMs often produce reasoning steps and tool calls in succession. Chain of Thought groups these consecutive parts into a single collapsible "thinking" accordion.

Overview

When a reasoning model responds, it may emit a sequence of reasoning tokens and tool calls before producing its final text answer. Use MessagePrimitive.GroupedParts to group those adjacent reasoning and tool-call parts into a single collapsible "thinking" section.

Info

The older components.ChainOfThought prop on MessagePrimitive.Parts and components prop on ChainOfThoughtPrimitive.Parts are legacy APIs. They still work for existing code, but new code should use MessagePrimitive.GroupedParts.

Quick Start

Wire GroupedParts into your assistant message

Return the same top-level group for reasoning and tool calls, with nested groups for each type:

import {
  MessagePrimitive,
  groupPartByType,
} from "@assistant-ui/react";
import { MarkdownText } from "@/components/assistant-ui/elements/markdown-text";
import {
  Reasoning,
  ReasoningContent,
  ReasoningRoot,
  ReasoningText,
  ReasoningTrigger,
} from "@/components/assistant-ui/elements/reasoning.aui";
import { ToolFallback } from "@/components/assistant-ui/elements/tool-fallback.aui";
import {
  ToolGroupContent,
  ToolGroupRoot,
  ToolGroupTrigger,
} from "@/components/assistant-ui/elements/tool-group.aui";
import type { FC } from "react";

const AssistantMessage: FC = () => {
  return (
    <MessagePrimitive.Root>
      <MessagePrimitive.GroupedParts
        groupBy={groupPartByType({
          reasoning: ["group-chainOfThought", "group-reasoning"],
          "tool-call": ["group-chainOfThought", "group-tool"],
        })}
      >
        {({ part, children }) => {
          switch (part.type) {
            case "group-chainOfThought":
              return <div className="my-2">{children}</div>;
            case "group-reasoning": {
              const running = part.status.type === "running";
              return (
                <ReasoningRoot streaming={running}>
                  <ReasoningTrigger active={running} />
                  <ReasoningContent aria-busy={running}>
                    <ReasoningText>{children}</ReasoningText>
                  </ReasoningContent>
                </ReasoningRoot>
              );
            }
            case "group-tool":
              return (
                <ToolGroupRoot>
                  <ToolGroupTrigger
                    count={part.indices.length}
                    active={part.status.type === "running"}
                  />
                  <ToolGroupContent>{children}</ToolGroupContent>
                </ToolGroupRoot>
              );
            case "text":
              return <MarkdownText />;
            case "reasoning":
              return <Reasoning {...part} />;
            case "tool-call":
              return part.toolUI ?? <ToolFallback {...part} />;
            default:
              return null;
          }
        }}
      </MessagePrimitive.GroupedParts>
    </MessagePrimitive.Root>
  );
};

Use a Reasoning Model

Chain of Thought is most useful with models that produce reasoning tokens. This backend route uses the AI SDK:

app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { streamText, convertToModelMessages } from "ai";

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    abortSignal: req.signal,
    model: openai("gpt-6-luna"),
    messages: await convertToModelMessages(messages),
  });

  return result.toUIMessageStreamResponse();
}

LangGraph

The LangGraph runtime converts thinking and reasoning content blocks in assistant messages into reasoning parts, so GroupedParts groups them the same way. If your graph reports reasoning in another shape, emit it as a custom data part and render it with makeAssistantDataUI. See generative UI with LangGraph for details.

Legacy: ChainOfThoughtPrimitive

Reading Collapsed State

For existing ChainOfThoughtPrimitive code, use AuiIf to conditionally render based on the accordion state:

import { AuiIf, ChainOfThoughtPrimitive } from "@assistant-ui/react";
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";

const ChainOfThoughtAccordionTrigger = () => {
  return (
    <ChainOfThoughtPrimitive.AccordionTrigger className="flex w-full items-center gap-2 px-4 py-2 text-sm">
      <AuiIf condition={(s) => s.chainOfThought.collapsed}>
        <ChevronRightIcon className="size-4" />
      </AuiIf>
      <AuiIf condition={(s) => !s.chainOfThought.collapsed}>
        <ChevronDownIcon className="size-4" />
      </AuiIf>
      Thinking
    </ChainOfThoughtPrimitive.AccordionTrigger>
  );
};

API Reference

For lower-level legacy compatibility details, see the ChainOfThought primitive reference.

Full Example

See the complete with-chain-of-thought example for a working implementation with tool calls and reasoning.