LLM index: /llms.txt
Structured timeline for assistant/tool execution traces. Use it to display sequential steps (web search, code search, file reads, tool calls) with per-step status, optional expandable output, and an auto-closing root when all steps complete.
import * as React from "react";
import {
AiBrain01Icon,
Analytics01Icon,
CheckmarkCircle01Icon,
FileSearchIcon,
IdeaIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import {
ChainOfThought,
ChainOfThoughtComplete,
ChainOfThoughtContent,
ChainOfThoughtStep,
ChainOfThoughtStepTitle,
ChainOfThoughtTrigger,
} from "@/components/nexus-ui/chain-of-thought";
function ChainOfThoughtDefault() {
return (
<div className="w-full">
<ChainOfThought autoCloseOnAllComplete={false}>
<ChainOfThoughtTrigger
icon={
<HugeiconsIcon
icon={AiBrain01Icon}
strokeWidth={1.75}
className="size-4"
/>
}
>
Triaged support ticket with account activity context
</ChainOfThoughtTrigger>
<ChainOfThoughtContent>
<ChainOfThoughtStep status="completed">
<ChainOfThoughtStepTitle
icon={
<HugeiconsIcon
icon={FileSearchIcon}
strokeWidth={1.75}
className="size-4"
/>
}
>
Pulled customer profile and recent events
</ChainOfThoughtStepTitle>
</ChainOfThoughtStep>
<ChainOfThoughtStep status="completed">
<ChainOfThoughtStepTitle
icon={
<HugeiconsIcon
icon={Analytics01Icon}
strokeWidth={1.75}
className="size-4"
/>
}
>
Reviewed billing history and usage spikes
</ChainOfThoughtStepTitle>
</ChainOfThoughtStep>
<ChainOfThoughtStep status="completed">
<ChainOfThoughtStepTitle
icon={
<HugeiconsIcon
icon={IdeaIcon}
strokeWidth={1.75}
className="size-4"
/>
}
>
Proposed resolution with confidence score
</ChainOfThoughtStepTitle>
</ChainOfThoughtStep>
<ChainOfThoughtComplete
label="Task complete"
icon={
<HugeiconsIcon
icon={CheckmarkCircle01Icon}
strokeWidth={1.75}
className="size-4"
/>
}
/>
</ChainOfThoughtContent>
</ChainOfThought>
</div>
);
}
export default ChainOfThoughtDefault;
Installation
npx shadcn@latest add @nexus-ui/chain-of-thoughtpnpm dlx shadcn@latest add @nexus-ui/chain-of-thoughtyarn dlx shadcn@latest add @nexus-ui/chain-of-thoughtbunx shadcn@latest add @nexus-ui/chain-of-thoughtInstall the following dependencies:
npx shadcn@latest add collapsible && npm install @hugeicons/react @hugeicons/core-free-icons tw-shimmerpnpm dlx shadcn@latest add collapsible && pnpm add @hugeicons/react @hugeicons/core-free-icons tw-shimmeryarn dlx shadcn@latest add collapsible && yarn add @hugeicons/react @hugeicons/core-free-icons tw-shimmerbunx shadcn@latest add collapsible && bun add @hugeicons/react @hugeicons/core-free-icons tw-shimmerCopy and paste the following code into your project.
"use client";
import * as React from "react";
import { Alert02Icon, ArrowDown01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { TextShimmer } from "@/components/nexus-ui/text-shimmer";
import { useOnChange } from "@/lib/use-on-change";
import { cn } from "@/lib/utils";
type ChainOfThoughtRootContextValue = {
registerStep: (id: string, status: ChainOfThoughtStepStatus) => void;
allStepsComplete: boolean;
hasAnyError: boolean;
};
const ChainOfThoughtRootContext =
React.createContext<ChainOfThoughtRootContextValue | null>(null);
function useChainOfThoughtRootContext(component: string) {
const ctx = React.useContext(ChainOfThoughtRootContext);
if (!ctx) {
throw new Error(`${component} must be used within <ChainOfThought>`);
}
return ctx;
}
type ChainOfThoughtStepContextValue = {
status: ChainOfThoughtStepStatus;
hasContent: boolean;
};
const ChainOfThoughtStepContext =
React.createContext<ChainOfThoughtStepContextValue | null>(null);
function useChainOfThoughtStepContext(component: string) {
const ctx = React.useContext(ChainOfThoughtStepContext);
if (!ctx) {
throw new Error(`${component} must be used within <ChainOfThoughtStep>`);
}
return ctx;
}
type ChainOfThoughtStepStatus = "pending" | "active" | "completed" | "error";
type ChainOfThoughtProps = Omit<
React.ComponentProps<typeof Collapsible>,
"open" | "defaultOpen" | "onOpenChange"
> & {
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
autoCloseOnAllComplete?: boolean;
};
function ChainOfThought({
className,
open: openProp,
defaultOpen = true,
onOpenChange,
autoCloseOnAllComplete = true,
children,
...props
}: ChainOfThoughtProps) {
const isControlled = openProp !== undefined;
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
const [stepStatuses, setStepStatuses] = React.useState<
Record<string, ChainOfThoughtStepStatus>
>({});
const { allStepsComplete, hasAnyError } = React.useMemo(() => {
const statuses = Object.values(stepStatuses);
return {
allStepsComplete:
statuses.length > 0 &&
statuses.every((status) => status === "completed"),
hasAnyError: statuses.some((status) => status === "error"),
};
}, [stepStatuses]);
const open = isControlled ? openProp : internalOpen;
const registerStep = React.useCallback(
(id: string, status: ChainOfThoughtStepStatus) => {
setStepStatuses((prev) => {
if (prev[id] === status) return prev;
return { ...prev, [id]: status };
});
},
[],
);
const contextValue = React.useMemo(
() => ({ registerStep, allStepsComplete, hasAnyError }),
[allStepsComplete, hasAnyError, registerStep],
);
const handleOpenChange = React.useCallback(
(nextOpen: boolean) => {
if (!isControlled) {
setInternalOpen(nextOpen);
}
onOpenChange?.(nextOpen);
},
[isControlled, onOpenChange],
);
useOnChange(allStepsComplete, (current, previous) => {
if (!autoCloseOnAllComplete || isControlled) return;
if (!previous && current) {
setInternalOpen(false);
onOpenChange?.(false);
}
});
return (
<ChainOfThoughtRootContext.Provider value={contextValue}>
<Collapsible
data-slot="chain-of-thought"
className={cn("not-prose w-full", className)}
open={open}
onOpenChange={handleOpenChange}
{...props}
>
{children}
</Collapsible>
</ChainOfThoughtRootContext.Provider>
);
}
type ChainOfThoughtTriggerProps = React.ComponentProps<
typeof CollapsibleTrigger
> & {
label?: React.ReactNode;
icon?: React.ReactNode;
};
function ChainOfThoughtTrigger({
className,
icon,
label,
children,
...props
}: ChainOfThoughtTriggerProps) {
const { allStepsComplete, hasAnyError } = useChainOfThoughtRootContext(
"ChainOfThoughtTrigger",
);
const isActive = !allStepsComplete && !hasAnyError;
return (
<CollapsibleTrigger
data-slot="chain-of-thought-trigger"
data-active={String(isActive)}
className={cn(
"group flex w-full cursor-pointer items-center gap-1.25 overflow-hidden text-muted-foreground transition-colors hover:text-foreground",
className,
)}
{...props}
>
{icon}
<div className="flex min-w-0 flex-1 items-start gap-1.25 overflow-hidden">
<TextShimmer
className="truncate text-left text-sm leading-4.5 text-ellipsis whitespace-nowrap"
spread={10}
invertLight
disableShimmer={!isActive}
>
{children ?? label}
</TextShimmer>
<HugeiconsIcon
icon={ArrowDown01Icon}
strokeWidth={2}
className="ml-0.5 size-4 shrink-0 opacity-0 transition-all group-hover:opacity-100 group-data-[state=open]:rotate-180 group-data-[state=open]:opacity-100"
/>
</div>
</CollapsibleTrigger>
);
}
type ChainOfThoughtContentProps = React.ComponentProps<
typeof CollapsibleContent
>;
function ChainOfThoughtContent({
className,
children,
...props
}: ChainOfThoughtContentProps) {
return (
<CollapsibleContent
data-slot="chain-of-thought-content"
className={cn(
"mt-3 space-y-3",
"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
className,
)}
{...props}
>
{children}
</CollapsibleContent>
);
}
function hasIconInStepTitle(children: React.ReactNode): boolean {
return React.Children.toArray(children).some(
(child) =>
React.isValidElement<{ icon?: React.ReactNode }>(child) &&
child.props.icon != null,
);
}
type ChainOfThoughtStepProps = Omit<
React.ComponentProps<typeof Collapsible>,
"open" | "defaultOpen" | "onOpenChange"
> & {
status?: ChainOfThoughtStepStatus;
hasContent?: boolean;
showConnector?: boolean;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
autoCloseOnComplete?: boolean;
};
function ChainOfThoughtStep({
className,
status = "pending",
hasContent = false,
showConnector,
open: openProp,
defaultOpen = true,
onOpenChange,
autoCloseOnComplete = true,
children,
...props
}: ChainOfThoughtStepProps) {
const { registerStep } = useChainOfThoughtRootContext("ChainOfThoughtStep");
const stepId = React.useId();
const showConnectorResolved = showConnector ?? hasIconInStepTitle(children);
const isControlled = openProp !== undefined;
const canAutoManageOpen = !isControlled && hasContent;
const [internalOpen, setInternalOpen] = React.useState(
() =>
defaultOpen ||
(hasContent && (status === "active" || status === "error")),
);
const open = isControlled ? openProp : internalOpen;
React.useEffect(() => {
registerStep(stepId, status);
}, [registerStep, status, stepId]);
const handleOpenChange = React.useCallback(
(nextOpen: boolean) => {
if (!isControlled) {
setInternalOpen(nextOpen);
}
onOpenChange?.(nextOpen);
},
[isControlled, onOpenChange],
);
useOnChange(status, (current, previous) => {
if (
canAutoManageOpen &&
(current === "active" || current === "error") &&
previous !== current
) {
setInternalOpen(true);
onOpenChange?.(true);
return;
}
if (!canAutoManageOpen || !autoCloseOnComplete || previous === undefined) {
return;
}
if (previous !== "completed" && current === "completed") {
setInternalOpen(false);
onOpenChange?.(false);
}
});
return (
<ChainOfThoughtStepContext.Provider value={{ status, hasContent }}>
<Collapsible
data-slot="chain-of-thought-step"
className={cn("relative w-full fade-in-0", className)}
open={open}
onOpenChange={handleOpenChange}
{...props}
>
{children}
{showConnectorResolved ? (
<div
className={cn(
"absolute top-4.75 -bottom-2.75 left-2 -mx-px w-px",
status === "error" ? "bg-destructive/20" : "bg-border/50",
)}
></div>
) : null}
</Collapsible>
</ChainOfThoughtStepContext.Provider>
);
}
type ChainOfThoughtStepTitleSharedProps = {
label?: React.ReactNode;
icon?: React.ReactNode;
children?: React.ReactNode;
collapsible?: boolean;
};
type ChainOfThoughtStepTitleCollapsibleProps =
ChainOfThoughtStepTitleSharedProps &
Omit<React.ComponentProps<typeof CollapsibleTrigger>, "children"> & {
collapsible: true;
};
type ChainOfThoughtStepTitleStaticProps = ChainOfThoughtStepTitleSharedProps &
React.HTMLAttributes<HTMLDivElement> & {
collapsible?: false;
};
type ChainOfThoughtStepTitleProps =
| ChainOfThoughtStepTitleCollapsibleProps
| ChainOfThoughtStepTitleStaticProps;
function ChainOfThoughtStepTitle({
className,
label: labelProp,
icon,
collapsible,
children,
...props
}: ChainOfThoughtStepTitleProps) {
const { hasContent, status } = useChainOfThoughtStepContext(
"ChainOfThoughtStepTitle",
);
const isCollapsible = collapsible ?? hasContent;
const isActive = status === "active";
const isError = status === "error";
const resolvedIcon =
isError && icon ? (
<HugeiconsIcon icon={Alert02Icon} strokeWidth={1.75} className="size-4" />
) : (
icon
);
const label = children ?? labelProp;
if (!isCollapsible) {
const staticProps = props as React.HTMLAttributes<HTMLDivElement>;
return (
<div
data-slot="chain-of-thought-step-title"
data-active={String(isActive)}
className={cn(
"group flex items-center text-sm leading-4.5 text-muted-foreground",
isError && "text-destructive",
resolvedIcon ? "gap-2" : "gap-0",
className,
)}
{...staticProps}
>
{resolvedIcon ? (
<div className="relative flex size-4 shrink-0 items-center justify-center">
{resolvedIcon}
</div>
) : null}
<TextShimmer
className="truncate text-left text-sm leading-4.5 text-ellipsis whitespace-nowrap"
spread={10}
invertLight
disableShimmer={!isActive}
>
{label}
</TextShimmer>
</div>
);
}
return (
<CollapsibleTrigger
data-slot="chain-of-thought-step-title"
data-active={String(isActive)}
className={cn(
"group flex w-full cursor-pointer items-center text-sm text-muted-foreground transition-colors hover:text-foreground",
isError && "text-destructive hover:text-destructive/90",
resolvedIcon ? "gap-2" : "gap-0",
className,
)}
{...(props as Omit<
React.ComponentProps<typeof CollapsibleTrigger>,
"children"
>)}
>
{resolvedIcon ? (
<div className="relative flex size-4 shrink-0 items-center justify-center">
{resolvedIcon}
</div>
) : null}
<div className="flex min-w-0 flex-1 items-start gap-1.25 overflow-hidden">
<TextShimmer
className="truncate text-left text-sm leading-4.5 text-ellipsis whitespace-nowrap"
spread={10}
invertLight
disableShimmer={!isActive}
>
{label}
</TextShimmer>
<HugeiconsIcon
icon={ArrowDown01Icon}
strokeWidth={2}
className="ml-0.5 size-4 shrink-0 opacity-0 transition-all group-hover:opacity-100 group-data-[state=open]:rotate-180 group-data-[state=open]:opacity-100"
/>
</div>
</CollapsibleTrigger>
);
}
type ChainOfThoughtStepContentProps = React.ComponentProps<
typeof CollapsibleContent
>;
function ChainOfThoughtStepContent({
className,
children,
...props
}: ChainOfThoughtStepContentProps) {
return (
<CollapsibleContent
data-slot="chain-of-thought-step-content"
className={cn(
"mt-2 ml-6",
"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
className,
)}
{...props}
>
{children}
</CollapsibleContent>
);
}
type ChainOfThoughtCompleteProps = React.HTMLAttributes<HTMLDivElement> & {
icon?: React.ReactNode;
label: React.ReactNode;
};
function ChainOfThoughtComplete({
className,
icon,
label,
...props
}: ChainOfThoughtCompleteProps) {
return (
<div
data-slot="chain-of-thought-complete"
className={cn(
"mt-0 flex items-center gap-2 text-sm leading-4.5 text-muted-foreground fade-in-0",
!icon && "gap-0",
className,
)}
{...props}
>
{icon}
<span>{label}</span>
</div>
);
}
export type { ChainOfThoughtStepStatus };
export {
ChainOfThought,
ChainOfThoughtTrigger,
ChainOfThoughtContent,
ChainOfThoughtStep,
ChainOfThoughtStepTitle,
ChainOfThoughtStepContent,
ChainOfThoughtComplete,
};
"use client";
import * as React from "react";
/**
* Runs `onChange` when `value` changes (compared against previous render).
*/
export function useOnChange<T>(
value: T,
onChange: (current: T, previous: T) => void,
isUpdated: (previous: T, current: T) => boolean = Object.is,
) {
const previousRef = React.useRef(value);
React.useEffect(() => {
const previous = previousRef.current;
if (!isUpdated(previous, value)) {
onChange(value, previous);
}
previousRef.current = value;
}, [value, onChange, isUpdated]);
}
Update import paths to match your project setup.
Usage
import {
ChainOfThought,
ChainOfThoughtTrigger,
ChainOfThoughtContent,
ChainOfThoughtStep,
ChainOfThoughtStepTitle,
ChainOfThoughtStepContent,
ChainOfThoughtComplete,
} from "@/components/nexus-ui/chain-of-thought";<ChainOfThought>
<ChainOfThoughtTrigger>
Thinking...
</ChainOfThoughtTrigger>
<ChainOfThoughtContent>
<ChainOfThoughtStep status="active" hasContent>
<ChainOfThoughtStepTitle icon={<SearchIcon />}>
Searching the web...
</ChainOfThoughtStepTitle>
<ChainOfThoughtStepContent>
{/* step output */}
</ChainOfThoughtStepContent>
</ChainOfThoughtStep>
<ChainOfThoughtStep status="pending">
<ChainOfThoughtStepTitle icon={<CodeIcon />}>
Search codebase for API handlers
</ChainOfThoughtStepTitle>
</ChainOfThoughtStep>
<ChainOfThoughtComplete label="Task complete" />
</ChainOfThoughtContent>
</ChainOfThought>Typical status progression per step is pending -> active -> completed (or error).
Examples
Basic
Simple timeline labels that mirror how coding agents report progress in tools like Cursor. Steps without icons hide connectors by default.
import * as React from "react";
import {
ChainOfThought,
ChainOfThoughtComplete,
ChainOfThoughtContent,
ChainOfThoughtStep,
ChainOfThoughtStepTitle,
ChainOfThoughtTrigger,
} from "@/components/nexus-ui/chain-of-thought";
function ChainOfThoughtBasic() {
return (
<div className="w-full">
<ChainOfThought autoCloseOnAllComplete={false}>
<ChainOfThoughtTrigger>
Explored 3 files, 2 searches, lints
</ChainOfThoughtTrigger>
<ChainOfThoughtContent>
<ChainOfThoughtStep status="completed">
<ChainOfThoughtStepTitle>
Grepped `chain-of-thought` in `nexus-ui`
</ChainOfThoughtStepTitle>
</ChainOfThoughtStep>
<ChainOfThoughtStep status="completed">
<ChainOfThoughtStepTitle>
Searched files `**/components/*` in `nexus-ui`
</ChainOfThoughtStepTitle>
</ChainOfThoughtStep>
<ChainOfThoughtStep status="completed">
<ChainOfThoughtStepTitle>
Read `registry.json` L1-206
</ChainOfThoughtStepTitle>
</ChainOfThoughtStep>
<ChainOfThoughtStep status="completed">
<ChainOfThoughtStepTitle>
Read `chain-of-thought.tsx` L1-80
</ChainOfThoughtStepTitle>
</ChainOfThoughtStep>
<ChainOfThoughtStep status="completed">
<ChainOfThoughtStepTitle>
Read `default.tsx` L1-96
</ChainOfThoughtStepTitle>
</ChainOfThoughtStep>
<ChainOfThoughtComplete label="No linter errors" />
</ChainOfThoughtContent>
</ChainOfThought>
</div>
);
}
export default ChainOfThoughtBasic;
With Content
Expandable step content for rich outputs like search queries, links, file matches, and analysis payloads.
import * as React from "react";
import {
AiBrain01Icon,
AiWebBrowsingIcon,
CheckmarkCircle01Icon,
Globe02Icon,
MapsIcon,
Sun03Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import {
ChainOfThought,
ChainOfThoughtComplete,
ChainOfThoughtContent,
ChainOfThoughtStep,
ChainOfThoughtStepContent,
ChainOfThoughtStepTitle,
ChainOfThoughtTrigger,
} from "@/components/nexus-ui/chain-of-thought";
const WEB_SEARCH_QUERIES = [
"3 days in lisbon best neighborhoods",
"must-visit places in lisbon for first timers",
"lisbon public transport tips 2026",
"best sunset viewpoints lisbon",
"food spots alfama bairro alto chiado",
];
const WEB_SOURCES = [
{
title: "Lisbon neighborhoods guide for first-time visitors",
domain: "visitlisboa.com",
url: "https://www.visitlisboa.com/en/",
},
{
title: "48 hours and 72 hours in Lisbon itinerary",
domain: "lonelyplanet.com",
url: "https://www.lonelyplanet.com/portugal/lisbon",
},
{
title: "Best miradouros (viewpoints) in Lisbon",
domain: "culturetrip.com",
url: "https://theculturetrip.com/europe/portugal/lisbon",
},
{
title: "Lisbon metro and tram guide",
domain: "carris.pt",
url: "https://www.carris.pt/en/",
},
];
const WEATHER_CARDS = [
{
day: "Fri",
condition: "Sunny",
high: "24C",
low: "16C",
rainChance: "5%",
},
{
day: "Sat",
condition: "Partly cloudy",
high: "23C",
low: "15C",
rainChance: "15%",
},
{
day: "Sun",
condition: "Breezy",
high: "21C",
low: "14C",
rainChance: "20%",
},
];
const MAP_ROUTES = [
{ from: "Hotel -> Belem Tower", eta: "24 min", mode: "Tram + walk" },
{ from: "Alfama -> Time Out Market", eta: "18 min", mode: "Metro + walk" },
{ from: "Chiado -> Miradouro da Senhora", eta: "22 min", mode: "Taxi" },
];
function ChainOfThoughtWithContent() {
return (
<div className="w-full">
<ChainOfThought autoCloseOnAllComplete={false}>
<ChainOfThoughtTrigger
icon={
<HugeiconsIcon
icon={AiBrain01Icon}
strokeWidth={1.75}
className="size-4"
/>
}
>
Planned a 3-day Lisbon itinerary
</ChainOfThoughtTrigger>
<ChainOfThoughtContent>
<ChainOfThoughtStep
status="completed"
hasContent
autoCloseOnComplete={false}
>
<ChainOfThoughtStepTitle
icon={
<HugeiconsIcon
icon={AiWebBrowsingIcon}
strokeWidth={1.75}
className="size-4"
/>
}
>
Web search
</ChainOfThoughtStepTitle>
<ChainOfThoughtStepContent>
<div className="mt-1 space-y-2">
<div className="-mx-1 mt-1 no-scrollbar flex gap-1.5 overflow-x-auto overscroll-x-contain px-1 pb-0.5 [-webkit-overflow-scrolling:touch] sm:mx-0 sm:flex-wrap sm:overflow-x-visible sm:px-0 sm:pb-0">
{WEB_SEARCH_QUERIES.map((query) => (
<span
key={query}
className="inline-flex h-6.5 shrink-0 items-center gap-1 rounded-full bg-muted px-2 text-xs leading-4.5 whitespace-nowrap text-muted-foreground sm:max-w-[187.8px] sm:whitespace-normal"
>
<HugeiconsIcon
icon={Globe02Icon}
strokeWidth={1.75}
className="size-4 shrink-0 text-muted-foreground/50"
/>
<span className="min-w-0 sm:truncate">{query}</span>
</span>
))}
</div>
<div className="mt-1.5 no-scrollbar flex max-h-[180px] w-full max-w-full min-w-0 flex-col gap-2 overflow-x-hidden overflow-y-auto rounded-[12px] border border-border/50 bg-secondary p-3">
{WEB_SOURCES.map((source) => (
<a
key={source.url}
href={source.url}
target="_blank"
rel="noopener noreferrer"
className="grid w-full max-w-full min-w-0 grid-cols-[auto_minmax(0,1fr)_minmax(0,7.5rem)] items-center gap-2 rounded-md px-1.5 py-1 text-xs leading-4.5 transition-colors hover:bg-border/50 sm:grid-cols-[auto_minmax(0,1fr)_minmax(0,9rem)] dark:hover:bg-border/40"
>
<img
alt=""
loading="lazy"
width={16}
height={16}
className="size-4 shrink-0 rounded"
src={`https://www.google.com/s2/favicons?domain=${source.domain}&sz=128`}
/>
<div className="min-w-0 truncate text-primary">
{source.title}
</div>
<div
className="min-w-0 truncate text-right text-muted-foreground tabular-nums"
title={source.domain}
>
{source.domain}
</div>
</a>
))}
</div>
</div>
</ChainOfThoughtStepContent>
</ChainOfThoughtStep>
<ChainOfThoughtStep
status="completed"
hasContent
autoCloseOnComplete={false}
>
<ChainOfThoughtStepTitle
icon={
<HugeiconsIcon
icon={Sun03Icon}
strokeWidth={1.75}
className="size-4"
/>
}
>
Get weather tool
</ChainOfThoughtStepTitle>
<ChainOfThoughtStepContent>
<div className="mt-1.5 grid grid-cols-1 gap-2 sm:grid-cols-3">
{WEATHER_CARDS.map((item) => (
<div
key={item.day}
className="rounded-[12px] border border-border/50 bg-secondary p-3"
>
<div className="text-xs text-muted-foreground">
{item.day}
</div>
<div className="mt-1 text-sm font-medium text-primary">
{item.condition}
</div>
<div className="mt-1 text-xs text-muted-foreground">
H: {item.high} - L: {item.low}
</div>
<div className="mt-1 text-xs text-muted-foreground">
Rain: {item.rainChance}
</div>
</div>
))}
</div>
</ChainOfThoughtStepContent>
</ChainOfThoughtStep>
<ChainOfThoughtStep
status="completed"
hasContent
autoCloseOnComplete={false}
className="animate-in"
>
<ChainOfThoughtStepTitle
icon={
<HugeiconsIcon
icon={MapsIcon}
strokeWidth={1.75}
className="size-4"
/>
}
>
Route planner tool
</ChainOfThoughtStepTitle>
<ChainOfThoughtStepContent>
<div className="mt-1.5 no-scrollbar flex max-h-[180px] w-full max-w-full min-w-0 flex-col gap-2 overflow-x-hidden overflow-y-auto rounded-[12px] border border-border/50 bg-secondary p-3">
{MAP_ROUTES.map((route) => (
<div
key={route.from}
className="grid w-full max-w-full min-w-0 grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-2 rounded-md px-1.5 py-1 text-xs leading-4.5"
>
<div className="min-w-0 truncate text-primary">
{route.from}
</div>
<div className="text-muted-foreground tabular-nums">
{route.eta}
</div>
<div className="text-muted-foreground">{route.mode}</div>
</div>
))}
<div className="mt-1 rounded-md border border-border/50 bg-background/50 px-2 py-1.5 text-xs text-muted-foreground">
Suggested pass: 24-hour transit card for day 1 and day 2.
</div>
</div>
</ChainOfThoughtStepContent>
</ChainOfThoughtStep>
<ChainOfThoughtComplete
label="Research complete"
icon={
<HugeiconsIcon
icon={CheckmarkCircle01Icon}
strokeWidth={1.75}
className="size-4"
/>
}
/>
</ChainOfThoughtContent>
</ChainOfThought>
</div>
);
}
export default ChainOfThoughtWithContent;
Error
Show a failed step with status="error" and include contextual details in step content.
import * as React from "react";
import {
AiBrain01Icon,
AiWebBrowsingIcon,
FolderOpenIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import {
ChainOfThought,
ChainOfThoughtContent,
ChainOfThoughtStep,
ChainOfThoughtStepContent,
ChainOfThoughtStepTitle,
ChainOfThoughtTrigger,
} from "@/components/nexus-ui/chain-of-thought";
function ChainOfThoughtError() {
return (
<div className="w-full">
<ChainOfThought autoCloseOnAllComplete={false}>
<ChainOfThoughtTrigger
icon={<HugeiconsIcon icon={AiBrain01Icon} strokeWidth={1.75} className="size-4" />}
>
Executing plan...
</ChainOfThoughtTrigger>
<ChainOfThoughtContent>
<ChainOfThoughtStep status="completed">
<ChainOfThoughtStepTitle
icon={<HugeiconsIcon icon={FolderOpenIcon} strokeWidth={1.75} className="size-4" />}
>
Connected to workspace
</ChainOfThoughtStepTitle>
</ChainOfThoughtStep>
<ChainOfThoughtStep status="error" hasContent autoCloseOnComplete={false}>
<ChainOfThoughtStepTitle
icon={
<HugeiconsIcon
icon={AiWebBrowsingIcon}
strokeWidth={1.75}
className="size-4"
/>
}
>
Failed to fetch external API schema
</ChainOfThoughtStepTitle>
<ChainOfThoughtStepContent>
<div className="mt-1 rounded-[12px] border border-destructive/20 bg-destructive/5 p-3 text-sm leading-4.5 text-destructive">
Request to api.example.com timed out after 10s. Retry with fallback
endpoint or cached schema.
</div>
</ChainOfThoughtStepContent>
</ChainOfThoughtStep>
</ChainOfThoughtContent>
</ChainOfThought>
</div>
);
}
export default ChainOfThoughtError;
Without Header
Render only the step timeline by omitting ChainOfThoughtTrigger. Root stays open and autoCloseOnAllComplete is disabled.
import * as React from "react";
import {
Analytics01Icon,
CheckmarkCircle01Icon,
FolderOpenIcon,
Globe02Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import {
ChainOfThought,
ChainOfThoughtComplete,
ChainOfThoughtContent,
ChainOfThoughtStep,
ChainOfThoughtStepTitle,
} from "@/components/nexus-ui/chain-of-thought";
function ChainOfThoughtWithoutHeader() {
return (
<div className="w-full">
<ChainOfThought defaultOpen autoCloseOnAllComplete={false}>
<ChainOfThoughtContent>
<ChainOfThoughtStep status="completed">
<ChainOfThoughtStepTitle
icon={
<HugeiconsIcon
icon={Globe02Icon}
strokeWidth={1.75}
className="size-4"
/>
}
>
Fetched customer chat history from CRM
</ChainOfThoughtStepTitle>
</ChainOfThoughtStep>
<ChainOfThoughtStep status="completed">
<ChainOfThoughtStepTitle
icon={
<HugeiconsIcon
icon={FolderOpenIcon}
strokeWidth={1.75}
className="size-4"
/>
}
>
Retrieved orders, refunds, and delivery events
</ChainOfThoughtStepTitle>
</ChainOfThoughtStep>
<ChainOfThoughtStep status="completed">
<ChainOfThoughtStepTitle
icon={
<HugeiconsIcon
icon={Analytics01Icon}
strokeWidth={1.75}
className="size-4"
/>
}
>
Generated escalation summary and next actions
</ChainOfThoughtStepTitle>
</ChainOfThoughtStep>
<ChainOfThoughtComplete
label="Escalation draft ready"
icon={
<HugeiconsIcon
icon={CheckmarkCircle01Icon}
strokeWidth={1.75}
className="size-4"
/>
}
/>
</ChainOfThoughtContent>
</ChainOfThought>
</div>
);
}
export default ChainOfThoughtWithoutHeader;
Vercel AI SDK Integration
ChainOfThought works well with useChat by mapping assistant tool parts into timeline steps.
Use it with Vercel AI SDK by:
- deriving step status (
pending/active/completed/error) from tool part state - rendering tool results inside
ChainOfThoughtStepContentwhen available - letting root auto-close after all steps are completed
Install the AI SDK
npm install ai @ai-sdk/reactCreate a chat API route that streams tool and text parts
You can use any streamText(...).toUIMessageStreamResponse() route shape used in your app. See Prompt Input docs for a minimal route scaffold.
Map assistant parts to Chain of Thought steps
"use client";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport, type UIMessage } from "ai";
import {
ChainOfThought,
ChainOfThoughtTrigger,
ChainOfThoughtContent,
ChainOfThoughtStep,
ChainOfThoughtStepTitle,
ChainOfThoughtStepContent,
ChainOfThoughtComplete,
type ChainOfThoughtStepStatus,
} from "@/components/nexus-ui/chain-of-thought";
type StepVM = {
id: string;
label: string;
status: ChainOfThoughtStepStatus;
hasContent?: boolean;
content?: React.ReactNode;
};
function stepsFromAssistant(message: UIMessage): StepVM[] {
return message.parts.flatMap((part, index) => {
// AI SDK UI emits tool-specific part types: tool-<toolName>
if (part.type === "tool-displayWeather") {
switch (part.state) {
case "input-available":
return [
{
id: `weather-${index}`,
label: "Running weather tool...",
status: "active" as const,
hasContent: true,
content: <div className="text-xs text-muted-foreground">Fetching forecast...</div>,
},
];
case "output-available":
return [
{
id: `weather-${index}`,
label: "Weather tool",
status: "completed" as const,
hasContent: true,
content: <pre>{JSON.stringify(part.output, null, 2)}</pre>,
},
];
case "output-error":
return [
{
id: `weather-${index}`,
label: "Weather tool failed",
status: "error" as const,
hasContent: true,
content: <div className="text-xs text-destructive">{part.errorText}</div>,
},
];
}
}
if (part.type === "tool-getStockPrice") {
switch (part.state) {
case "input-available":
return [
{
id: `stock-${index}`,
label: "Running stock tool...",
status: "active" as const,
},
];
case "output-available":
return [
{
id: `stock-${index}`,
label: "Stock price tool",
status: "completed" as const,
hasContent: true,
content: <pre>{JSON.stringify(part.output, null, 2)}</pre>,
},
];
case "output-error":
return [
{
id: `stock-${index}`,
label: "Stock price tool failed",
status: "error" as const,
hasContent: true,
content: <div className="text-xs text-destructive">{part.errorText}</div>,
},
];
}
}
return [];
});
}
export default function ChainOfThoughtWithUseChat() {
const { messages } = useChat({
transport: new DefaultChatTransport({ api: "/api/chat" }),
});
const assistant = [...messages].reverse().find((m) => m.role === "assistant");
if (!assistant) return null;
const steps = stepsFromAssistant(assistant);
if (steps.length === 0) return null;
return (
<ChainOfThought autoCloseOnAllComplete>
<ChainOfThoughtTrigger>Thinking...</ChainOfThoughtTrigger>
<ChainOfThoughtContent>
{steps.map((step) => (
<ChainOfThoughtStep
key={step.id}
status={step.status}
hasContent={step.hasContent}
>
<ChainOfThoughtStepTitle collapsible={Boolean(step.hasContent)}>
{step.label}
</ChainOfThoughtStepTitle>
{step.hasContent ? (
<ChainOfThoughtStepContent>{step.content}</ChainOfThoughtStepContent>
) : null}
</ChainOfThoughtStep>
))}
<ChainOfThoughtComplete label="Task complete" />
</ChainOfThoughtContent>
</ChainOfThought>
);
}API Reference
ChainOfThought
Root container for the thought timeline. Tracks child step statuses to drive trigger activity and optional auto-close behavior. Wraps Collapsible Root.
Prop
Type
ChainOfThoughtTrigger
Top trigger row for the thought timeline. Label shimmer is active while the flow is in progress and no step is in error. Wraps Collapsible Trigger.
Prop
Type
ChainOfThoughtContent
Content container for step rows. Wraps Collapsible Content.
Prop
Type
ChainOfThoughtStep
A single timeline step with status-aware open behavior and optional expandable content. Wraps Collapsible.
Prop
Type
ChainOfThoughtStepTitle
Step label row. Can render as static text row or collapsible trigger row. Wraps Collapsible Trigger.
Prop
Type
ChainOfThoughtStepContent
Expandable area for tool output/results. Keep result rendering consumer-defined. Wraps Collapsible Content.
Prop
Type
ChainOfThoughtComplete
Terminal row for completion state (Task complete, Done, etc.).
Prop
Type