"use client" import { useControllableState } from "@radix-ui/react-use-controllable-state" import { BrainIcon, ChevronDownIcon } from "lucide-react" import type { ComponentProps, ReactNode } from "react" import { createContext, memo, useContext, useEffect, useState } from "react" import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible" import { cn } from "@/lib/utils" import { Shimmer } from "./shimmer" type ReasoningContextValue = { isStreaming: boolean isOpen: boolean setIsOpen: (open: boolean) => void duration: number | undefined } const ReasoningContext = createContext(null) export const useReasoning = () => { const context = useContext(ReasoningContext) if (!context) { throw new Error("Reasoning components must be used within Reasoning") } return context } export type ReasoningProps = ComponentProps & { isStreaming?: boolean open?: boolean defaultOpen?: boolean onOpenChange?: (open: boolean) => void duration?: number } const AUTO_CLOSE_DELAY = 1000 const MS_IN_S = 1000 export const Reasoning = memo( ({ className, isStreaming = false, open, defaultOpen = true, onOpenChange, duration: durationProp, children, ...props }: ReasoningProps) => { const [isOpen, setIsOpen] = useControllableState({ prop: open, defaultProp: defaultOpen, onChange: onOpenChange, }) const [duration, setDuration] = useControllableState({ prop: durationProp, defaultProp: undefined, }) const [hasAutoClosed, setHasAutoClosed] = useState(false) const [startTime, setStartTime] = useState(null) // Track duration when streaming starts and ends useEffect(() => { if (isStreaming) { if (startTime === null) { setStartTime(Date.now()) } } else if (startTime !== null) { setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S)) setStartTime(null) } }, [isStreaming, startTime, setDuration]) // Auto-open when streaming starts, auto-close when streaming ends (once only) useEffect(() => { if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) { // Add a small delay before closing to allow user to see the content const timer = setTimeout(() => { setIsOpen(false) setHasAutoClosed(true) }, AUTO_CLOSE_DELAY) return () => clearTimeout(timer) } }, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed]) const handleOpenChange = (newOpen: boolean) => { setIsOpen(newOpen) } return ( {children} ) }, ) export type ReasoningTriggerProps = ComponentProps< typeof CollapsibleTrigger > & { getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode } const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => { if (isStreaming || duration === 0) { return Thinking... } if (duration === undefined) { return

Thought for a few seconds

} return

Thought for {duration} seconds

} export const ReasoningTrigger = memo( ({ className, children, getThinkingMessage = defaultGetThinkingMessage, ...props }: ReasoningTriggerProps) => { const { isStreaming, isOpen, duration } = useReasoning() return ( {children ?? ( <> {getThinkingMessage(isStreaming, duration)} )} ) }, ) export type ReasoningContentProps = ComponentProps< typeof CollapsibleContent > & { children: string } export const ReasoningContent = memo( ({ className, children, ...props }: ReasoningContentProps) => (
{children}
), ) Reasoning.displayName = "Reasoning" ReasoningTrigger.displayName = "ReasoningTrigger" ReasoningContent.displayName = "ReasoningContent"