"use client" import type { ComponentProps, ReactNode } from "react" import { useControllableState } from "@radix-ui/react-use-controllable-state" import { useTranslations } from "next-intl" import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible" import { cn } from "@/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 { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react" import { Streamdown } from "streamdown" import { Shimmer } from "./shimmer" import { useStreamdownLinkSafety } from "./link-safety" interface 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, onOpenChange, duration: durationProp, children, ...props }: ReasoningProps) => { const resolvedDefaultOpen = defaultOpen ?? isStreaming // Track if defaultOpen was explicitly set to false (to prevent auto-open) 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) // Track when streaming starts and compute duration 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]) // Auto-open when streaming starts (unless explicitly closed) useEffect(() => { if (isStreaming && !isOpen && !isExplicitlyClosed) { setIsOpen(true) } }, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]) // Auto-close when streaming ends (once only, and only if it ever streamed) 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} ) } ) export type ReasoningTriggerProps = ComponentProps< typeof CollapsibleTrigger > & { getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode } export const ReasoningTrigger = memo( ({ className, children, getThinkingMessage, ...props }: ReasoningTriggerProps) => { const t = useTranslations("Folder.chat.reasoning") const { isStreaming, isOpen, duration } = useReasoning() const defaultGetThinkingMessage = useCallback( (nextIsStreaming: boolean, nextDuration?: number) => { if (nextIsStreaming || nextDuration === 0) { return {t("thinking")} } if (nextDuration === undefined) { return

{t("thoughtForFewSeconds")}

} return

{t("thoughtForSeconds", { duration: nextDuration })}

}, [t] ) const thinkingMessageBuilder = getThinkingMessage ?? defaultGetThinkingMessage return ( {children ?? ( <> {thinkingMessageBuilder(isStreaming, duration)} )} ) } ) export type ReasoningContentProps = ComponentProps< typeof CollapsibleContent > & { children: string } const streamdownPlugins = { cjk, code, math, mermaid } export const ReasoningContent = memo( ({ className, children, ...props }: ReasoningContentProps) => { const linkSafety = useStreamdownLinkSafety() return ( {children} ) } ) Reasoning.displayName = "Reasoning" ReasoningTrigger.displayName = "ReasoningTrigger" ReasoningContent.displayName = "ReasoningContent"