"use client"; import { useControllableState } from "@radix-ui/react-use-controllable-state"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "src/components/ui/collapsible"; import { cn } from "src/lib/utils"; import { cjk } from "@streamdown/cjk"; import { code } from "@streamdown/code"; import { math } from "@streamdown/math"; import { mermaid } from "@streamdown/mermaid"; import { BrainIcon, ChevronDownIcon } from "lucide-react"; import type { ComponentProps, ReactNode } from "react"; import { memo, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { Streamdown } from "streamdown"; import { ReasoningContext, useReasoning, AUTO_CLOSE_DELAY, MS_IN_S } from "./reasoning-context"; import { Shimmer } from "./shimmer"; export type ReasoningProps = ComponentProps & { isStreaming?: boolean; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; duration?: number; }; export const Reasoning = memo( ({ className, isStreaming = false, open, defaultOpen, onOpenChange, duration: durationProp, children, ...props }: ReasoningProps) => { const resolvedDefaultOpen = defaultOpen ?? isStreaming; const isExplicitlyClosed = defaultOpen === false; const [isOpen, setIsOpen] = useControllableState({ defaultProp: resolvedDefaultOpen, onChange: onOpenChange, prop: open, }); const [duration, setDuration] = useControllableState({ defaultProp: undefined, prop: durationProp, }); const hasEverStreamedRef = useRef(isStreaming); const [hasAutoClosed, setHasAutoClosed] = useState(false); const startTimeRef = useRef(null); useEffect(() => { if (isStreaming) { hasEverStreamedRef.current = true; if (startTimeRef.current === null) { startTimeRef.current = Date.now(); } } else if (startTimeRef.current !== null) { setDuration(Math.ceil((Date.now() - startTimeRef.current) / MS_IN_S)); startTimeRef.current = null; } }, [isStreaming, setDuration]); useEffect(() => { if (isStreaming && !isOpen && !isExplicitlyClosed) { setIsOpen(true); } }, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]); useEffect(() => { if ( hasEverStreamedRef.current && !isStreaming && isOpen && !hasAutoClosed ) { const timer = setTimeout(() => { setIsOpen(false); setHasAutoClosed(true); }, AUTO_CLOSE_DELAY); return () => clearTimeout(timer); } }, [isStreaming, isOpen, setIsOpen, hasAutoClosed]); const handleOpenChange = useCallback( (newOpen: boolean) => { setIsOpen(newOpen); }, [setIsOpen] ); const contextValue = useMemo( () => ({ duration, isOpen, isStreaming, setIsOpen }), [duration, isOpen, isStreaming, setIsOpen] ); return ( {children} ); } ); Reasoning.displayName = "Reasoning"; 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)} )} ); } ); ReasoningTrigger.displayName = "ReasoningTrigger"; export type ReasoningContentProps = ComponentProps< typeof CollapsibleContent > & { children: string; }; const streamdownPlugins = { cjk, code, math, mermaid }; export const ReasoningContent = memo( ({ className, children, ...props }: ReasoningContentProps) => ( {children} ) ); ReasoningContent.displayName = "ReasoningContent";