"use client"; import { useControllableState } from "@radix-ui/react-use-controllable-state"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { cn } from "@/lib/utils"; import { BrainIcon, ChevronDownIcon } from "lucide-react"; import type { ComponentProps } from "react"; import { createContext, memo, useContext, useEffect, useRef, useState } from "react"; import { Response } from "./response"; import { Shimmer } from "./shimmer"; type ReasoningContextValue = { isStreaming: boolean; isOpen: boolean; setIsOpen: (open: boolean) => void; duration: number; }; const ReasoningContext = createContext(null); 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; onReasoningDurationChange?: (duration: number) => void; }; const AUTO_CLOSE_DELAY = 1000; const MS_IN_S = 1000; export const Reasoning = memo( ({ className, isStreaming = false, open, defaultOpen = true, onOpenChange, duration: durationProp, onReasoningDurationChange, children, ...props }: ReasoningProps) => { const [isOpen, setIsOpen] = useControllableState({ prop: open, defaultProp: defaultOpen, onChange: onOpenChange, }); const [duration, setDuration] = useControllableState({ prop: durationProp, defaultProp: 0, }); const [hasAutoClosed, setHasAutoClosed] = useState(false); const [startTime, setStartTime] = useState(null); const onReasoningDurationChangeRef = useRef(onReasoningDurationChange); useEffect(() => { onReasoningDurationChangeRef.current = onReasoningDurationChange; }, [onReasoningDurationChange]); // Track duration when streaming starts and ends useEffect(() => { if (isStreaming) { if (startTime === null) { setStartTime(Date.now()); } } else if (startTime !== null) { const computedDuration = Math.ceil((Date.now() - startTime) / MS_IN_S); setDuration(computedDuration); onReasoningDurationChangeRef.current?.(computedDuration); 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; const getThinkingMessage = (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, ...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) => { const { isStreaming } = useReasoning(); const contentRef = useRef(null); useEffect(() => { if (contentRef.current) { contentRef.current.scrollTop = contentRef.current.scrollHeight; } }, [children, isStreaming]); return (
{children}
); } ); Reasoning.displayName = "Reasoning"; ReasoningTrigger.displayName = "ReasoningTrigger"; ReasoningContent.displayName = "ReasoningContent";