Elements

03 / 37 · Reasoning

Reasoning panel

A collapsible trace that streams reasoning steps along a timeline, then settles into a summary.

  1. Reading the request

    The user wants drafts restored per thread, so the composer state has to move out of component state.

Installation

npx shadcn@latest add "@assistant-ui/elements-reasoning-panel"

Usage

import { ReasoningPanel } from "@/components/elements/reasoning-panel";

<ReasoningPanel
  steps={[{ title: "Scope", body: "Find the failing path." }]}
  visibleSteps={1}
  streaming
  open
  onOpenChange={setOpen}
  restingLabel="Reasoned for 4s"
  elapsed="2s"
/>

Props

steps*ReasoningStep[]Full list of reasoning steps available to reveal.
visibleSteps*numberHow many steps from the start are currently shown on the timeline.
streaming*booleanWhen true the trigger shows a shimmering Thinking label and the active step pulses.
open*booleanWhether the collapsible step list is expanded.
onOpenChange*(open: boolean) => voidCalled when the user expands or collapses the panel.
restingLabel*stringTrigger label once streaming finishes, for example a summary of how long it thought.
elapsedstringElapsed timer shown next to Thinking while streaming. Omit to hide it.
classNamestringExtra classes merged onto the root.

Source

reasoning-panel.tsx
"use client";

import { ChevronDownIcon } from "lucide-react";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { collapsePanel, mono, SwapLabel } from "./surfaces";

export interface ReasoningStep {
  title: string;
  body: string;
}

export interface ReasoningPanelProps {
  steps: ReasoningStep[];
  visibleSteps: number;
  streaming: boolean;
  open: boolean;
  onOpenChange: (open: boolean) => void;
  restingLabel: string;
  elapsed?: string;
  className?: string;
}

export function ReasoningPanel({
  steps,
  visibleSteps,
  streaming,
  open,
  onOpenChange,
  restingLabel,
  elapsed,
  className,
}: ReasoningPanelProps) {
  return (
    <Collapsible
      open={open}
      onOpenChange={onOpenChange}
      className={cn("w-full max-w-sm", className)}
    >
      <CollapsibleTrigger className="group/trigger text-foreground/55 hover:text-foreground/90 flex items-center gap-1.5 py-1 text-[13.5px] transition-[color,scale] outline-none active:scale-[0.98]">
        <SwapLabel active={streaming ? 0 : 1} className="text-start">
          <>
            <span className="relative inline-block leading-none">
              <span>Thinking</span>
              <span
                aria-hidden
                className="shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none"
              >
                Thinking
              </span>
            </span>
            {elapsed !== undefined && (
              <span className={cn(mono, "text-foreground/30 tabular-nums")}>
                {elapsed}
              </span>
            )}
          </>
          <>{restingLabel}</>
        </SwapLabel>
        <ChevronDownIcon className="size-3.5 shrink-0 opacity-60 transition-transform duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-open/trigger:rotate-180 group-data-panel-open/trigger:rotate-180 motion-reduce:transition-none" />
      </CollapsibleTrigger>
      <CollapsibleContent className={cn(collapsePanel, "outline-none")}>
        <ol className="flex flex-col gap-4 pt-3 pb-1">
          {steps.slice(0, visibleSteps).map((step, i) => {
            const active = streaming && i === visibleSteps - 1;
            return (
              <li
                key={step.title}
                className="fade-in slide-in-from-bottom-1 animate-in fill-mode-both flex gap-3 duration-300"
              >
                <span
                  aria-hidden
                  className={cn(
                    "mt-[7px] size-[5px] shrink-0 rounded-full transition-colors duration-300",
                    active
                      ? "animate-pulse bg-blue-500 dark:bg-blue-400"
                      : "bg-foreground/20",
                  )}
                />
                <span className="flex flex-col">
                  <p className="text-foreground/90 text-[13.5px] font-medium">
                    {step.title}
                  </p>
                  <p className="text-foreground/50 mt-0.5 text-[13px] leading-relaxed">
                    {step.body}
                  </p>
                </span>
              </li>
            );
          })}
        </ol>
      </CollapsibleContent>
    </Collapsible>
  );
}