{ "name": "reasoning", "type": "registry:ui", "registryDependencies": [], "dependencies": [ "lucide-react" ], "devDependencies": [], "tailwind": {}, "cssVars": { "light": {}, "dark": {} }, "description": "A collapsible component for showing AI reasoning, explanations, or logic. You can control it manually or let it auto-close when the stream ends. Markdown is supported.", "files": [ { "path": "reasoning.tsx", "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport { ChevronDownIcon } from \"lucide-react\"\nimport React, {\n createContext,\n useContext,\n useEffect,\n useRef,\n useState,\n} from \"react\"\nimport { Markdown } from \"./markdown\"\n\ntype ReasoningContextType = {\n isOpen: boolean\n onOpenChange: (open: boolean) => void\n}\n\nconst ReasoningContext = createContext(\n undefined\n)\n\nfunction useReasoningContext() {\n const context = useContext(ReasoningContext)\n if (!context) {\n throw new Error(\n \"useReasoningContext must be used within a Reasoning provider\"\n )\n }\n return context\n}\n\nexport type ReasoningProps = {\n children: React.ReactNode\n className?: string\n open?: boolean\n onOpenChange?: (open: boolean) => void\n isStreaming?: boolean\n}\nfunction Reasoning({\n children,\n className,\n open,\n onOpenChange,\n isStreaming,\n}: ReasoningProps) {\n const [internalOpen, setInternalOpen] = useState(false)\n const [wasAutoOpened, setWasAutoOpened] = useState(false)\n\n const isControlled = open !== undefined\n const isOpen = isControlled ? open : internalOpen\n\n const handleOpenChange = (newOpen: boolean) => {\n if (!isControlled) {\n setInternalOpen(newOpen)\n }\n onOpenChange?.(newOpen)\n }\n\n useEffect(() => {\n if (isStreaming && !wasAutoOpened) {\n if (!isControlled) setInternalOpen(true)\n setWasAutoOpened(true)\n }\n\n if (!isStreaming && wasAutoOpened) {\n if (!isControlled) setInternalOpen(false)\n setWasAutoOpened(false)\n }\n }, [isStreaming, wasAutoOpened, isControlled])\n\n return (\n \n
{children}
\n \n )\n}\n\nexport type ReasoningTriggerProps = {\n children: React.ReactNode\n className?: string\n} & React.HTMLAttributes\n\nfunction ReasoningTrigger({\n children,\n className,\n ...props\n}: ReasoningTriggerProps) {\n const { isOpen, onOpenChange } = useReasoningContext()\n\n return (\n onOpenChange(!isOpen)}\n {...props}\n >\n {children}\n \n \n \n \n )\n}\n\nexport type ReasoningContentProps = {\n children: React.ReactNode\n className?: string\n markdown?: boolean\n contentClassName?: string\n} & React.HTMLAttributes\n\nfunction ReasoningContent({\n children,\n className,\n contentClassName,\n markdown = false,\n ...props\n}: ReasoningContentProps) {\n const contentRef = useRef(null)\n const innerRef = useRef(null)\n const { isOpen } = useReasoningContext()\n\n useEffect(() => {\n if (!contentRef.current || !innerRef.current) return\n\n const observer = new ResizeObserver(() => {\n if (contentRef.current && innerRef.current && isOpen) {\n contentRef.current.style.maxHeight = `${innerRef.current.scrollHeight}px`\n }\n })\n\n observer.observe(innerRef.current)\n\n if (isOpen) {\n contentRef.current.style.maxHeight = `${innerRef.current.scrollHeight}px`\n }\n\n return () => observer.disconnect()\n }, [isOpen])\n\n const content = markdown ? (\n {children as string}\n ) : (\n children\n )\n\n return (\n \n \n {content}\n \n \n )\n}\n\nexport { Reasoning, ReasoningTrigger, ReasoningContent }\n", "type": "registry:ui" }, { "path": "markdown.tsx", "content": "import { cn } from \"@/lib/utils\"\nimport { marked } from \"marked\"\nimport { memo, useId, useMemo } from \"react\"\nimport ReactMarkdown, { Components } from \"react-markdown\"\nimport remarkBreaks from \"remark-breaks\"\nimport remarkGfm from \"remark-gfm\"\nimport { CodeBlock, CodeBlockCode } from \"./code-block\"\n\nexport type MarkdownProps = {\n children: string\n id?: string\n className?: string\n components?: Partial\n}\n\nfunction parseMarkdownIntoBlocks(markdown: string): string[] {\n const tokens = marked.lexer(markdown)\n return tokens.map((token) => token.raw)\n}\n\nfunction extractLanguage(className?: string): string {\n if (!className) return \"plaintext\"\n const match = className.match(/language-(\\w+)/)\n return match ? match[1] : \"plaintext\"\n}\n\nconst INITIAL_COMPONENTS: Partial = {\n code: function CodeComponent({ className, children, ...props }) {\n const isInline =\n !props.node?.position?.start.line ||\n props.node?.position?.start.line === props.node?.position?.end.line\n\n if (isInline) {\n return (\n \n {children}\n \n )\n }\n\n const language = extractLanguage(className)\n\n return (\n \n \n \n )\n },\n pre: function PreComponent({ children }) {\n return <>{children}\n },\n}\n\nconst MemoizedMarkdownBlock = memo(\n function MarkdownBlock({\n content,\n components = INITIAL_COMPONENTS,\n }: {\n content: string\n components?: Partial\n }) {\n return (\n \n {content}\n \n )\n },\n function propsAreEqual(prevProps, nextProps) {\n return prevProps.content === nextProps.content\n }\n)\n\nMemoizedMarkdownBlock.displayName = \"MemoizedMarkdownBlock\"\n\nfunction MarkdownComponent({\n children,\n id,\n className,\n components = INITIAL_COMPONENTS,\n}: MarkdownProps) {\n const generatedId = useId()\n const blockId = id ?? generatedId\n const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children])\n\n return (\n
\n {blocks.map((block, index) => (\n \n ))}\n
\n )\n}\n\nconst Markdown = memo(MarkdownComponent)\nMarkdown.displayName = \"Markdown\"\n\nexport { Markdown }\n", "type": "registry:ui" }, { "path": "response-stream.tsx", "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport React, { useCallback, useEffect, useRef, useState } from \"react\"\n\nexport type Mode = \"typewriter\" | \"fade\"\n\nexport type UseTextStreamOptions = {\n textStream: string | AsyncIterable\n speed?: number\n mode?: Mode\n onComplete?: () => void\n fadeDuration?: number\n segmentDelay?: number\n characterChunkSize?: number\n onError?: (error: unknown) => void\n}\n\nexport type UseTextStreamResult = {\n displayedText: string\n isComplete: boolean\n segments: { text: string; index: number }[]\n getFadeDuration: () => number\n getSegmentDelay: () => number\n reset: () => void\n startStreaming: () => void\n pause: () => void\n resume: () => void\n}\n\nfunction useTextStream({\n textStream,\n speed = 20,\n mode = \"typewriter\",\n onComplete,\n fadeDuration,\n segmentDelay,\n characterChunkSize,\n onError,\n}: UseTextStreamOptions): UseTextStreamResult {\n const [displayedText, setDisplayedText] = useState(\"\")\n const [isComplete, setIsComplete] = useState(false)\n const [segments, setSegments] = useState<{ text: string; index: number }[]>(\n []\n )\n\n const speedRef = useRef(speed)\n const modeRef = useRef(mode)\n const currentIndexRef = useRef(0)\n const animationRef = useRef(null)\n const fadeDurationRef = useRef(fadeDuration)\n const segmentDelayRef = useRef(segmentDelay)\n const characterChunkSizeRef = useRef(characterChunkSize)\n const streamRef = useRef(null)\n const completedRef = useRef(false)\n const onCompleteRef = useRef(onComplete)\n\n useEffect(() => {\n speedRef.current = speed\n modeRef.current = mode\n fadeDurationRef.current = fadeDuration\n segmentDelayRef.current = segmentDelay\n characterChunkSizeRef.current = characterChunkSize\n }, [speed, mode, fadeDuration, segmentDelay, characterChunkSize])\n\n useEffect(() => {\n onCompleteRef.current = onComplete\n }, [onComplete])\n\n const getChunkSize = useCallback(() => {\n if (typeof characterChunkSizeRef.current === \"number\") {\n return Math.max(1, characterChunkSizeRef.current)\n }\n\n const normalizedSpeed = Math.min(100, Math.max(1, speedRef.current))\n\n if (modeRef.current === \"typewriter\") {\n if (normalizedSpeed < 25) return 1\n return Math.max(1, Math.round((normalizedSpeed - 25) / 10))\n } else if (modeRef.current === \"fade\") {\n return 1\n }\n\n return 1\n }, [])\n\n const getProcessingDelay = useCallback(() => {\n if (typeof segmentDelayRef.current === \"number\") {\n return Math.max(0, segmentDelayRef.current)\n }\n\n const normalizedSpeed = Math.min(100, Math.max(1, speedRef.current))\n return Math.max(1, Math.round(100 / Math.sqrt(normalizedSpeed)))\n }, [])\n\n const getFadeDuration = useCallback(() => {\n if (typeof fadeDurationRef.current === \"number\")\n return Math.max(10, fadeDurationRef.current)\n\n const normalizedSpeed = Math.min(100, Math.max(1, speedRef.current))\n return Math.round(1000 / Math.sqrt(normalizedSpeed))\n }, [])\n\n const getSegmentDelay = useCallback(() => {\n if (typeof segmentDelayRef.current === \"number\")\n return Math.max(0, segmentDelayRef.current)\n\n const normalizedSpeed = Math.min(100, Math.max(1, speedRef.current))\n return Math.max(1, Math.round(100 / Math.sqrt(normalizedSpeed)))\n }, [])\n\n const updateSegments = useCallback((text: string) => {\n if (modeRef.current === \"fade\") {\n try {\n const segmenter = new Intl.Segmenter(navigator.language, {\n granularity: \"word\",\n })\n const segmentIterator = segmenter.segment(text)\n const newSegments = Array.from(segmentIterator).map(\n (segment, index) => ({\n text: segment.segment,\n index,\n })\n )\n setSegments(newSegments)\n } catch (error) {\n const newSegments = text\n .split(/(\\s+)/)\n .filter(Boolean)\n .map((word, index) => ({\n text: word,\n index,\n }))\n setSegments(newSegments)\n onError?.(error)\n }\n }\n }, [])\n\n const markComplete = useCallback(() => {\n if (!completedRef.current) {\n completedRef.current = true\n setIsComplete(true)\n onCompleteRef.current?.()\n }\n }, [])\n\n const reset = useCallback(() => {\n currentIndexRef.current = 0\n setDisplayedText(\"\")\n setSegments([])\n setIsComplete(false)\n completedRef.current = false\n\n if (animationRef.current) {\n cancelAnimationFrame(animationRef.current)\n animationRef.current = null\n }\n }, [])\n\n const processStringTypewriter = useCallback(\n (text: string) => {\n let lastFrameTime = 0\n\n const streamContent = (timestamp: number) => {\n const delay = getProcessingDelay()\n if (delay > 0 && timestamp - lastFrameTime < delay) {\n animationRef.current = requestAnimationFrame(streamContent)\n return\n }\n lastFrameTime = timestamp\n\n if (currentIndexRef.current >= text.length) {\n markComplete()\n return\n }\n\n const chunkSize = getChunkSize()\n const endIndex = Math.min(\n currentIndexRef.current + chunkSize,\n text.length\n )\n const newDisplayedText = text.slice(0, endIndex)\n\n setDisplayedText(newDisplayedText)\n if (modeRef.current === \"fade\") {\n updateSegments(newDisplayedText)\n }\n\n currentIndexRef.current = endIndex\n\n if (endIndex < text.length) {\n animationRef.current = requestAnimationFrame(streamContent)\n } else {\n markComplete()\n }\n }\n\n animationRef.current = requestAnimationFrame(streamContent)\n },\n [getProcessingDelay, getChunkSize, updateSegments, markComplete]\n )\n\n const processAsyncIterable = useCallback(\n async (stream: AsyncIterable) => {\n const controller = new AbortController()\n streamRef.current = controller\n\n let displayed = \"\"\n\n try {\n for await (const chunk of stream) {\n if (controller.signal.aborted) return\n\n displayed += chunk\n setDisplayedText(displayed)\n updateSegments(displayed)\n }\n\n markComplete()\n } catch (error) {\n console.error(\"Error processing text stream:\", error)\n markComplete()\n onError?.(error)\n }\n },\n [updateSegments, markComplete, onError]\n )\n\n const startStreaming = useCallback(() => {\n reset()\n\n if (typeof textStream === \"string\") {\n processStringTypewriter(textStream)\n } else if (textStream) {\n processAsyncIterable(textStream)\n }\n }, [textStream, reset, processStringTypewriter, processAsyncIterable])\n\n const pause = useCallback(() => {\n if (animationRef.current) {\n cancelAnimationFrame(animationRef.current)\n animationRef.current = null\n }\n }, [])\n\n const resume = useCallback(() => {\n if (typeof textStream === \"string\" && !isComplete) {\n processStringTypewriter(textStream)\n }\n }, [textStream, isComplete, processStringTypewriter])\n\n useEffect(() => {\n startStreaming()\n\n return () => {\n if (animationRef.current) {\n cancelAnimationFrame(animationRef.current)\n }\n if (streamRef.current) {\n streamRef.current.abort()\n }\n }\n }, [textStream, startStreaming])\n\n return {\n displayedText,\n isComplete,\n segments,\n getFadeDuration,\n getSegmentDelay,\n reset,\n startStreaming,\n pause,\n resume,\n }\n}\n\nexport type ResponseStreamProps = {\n textStream: string | AsyncIterable\n mode?: Mode\n speed?: number // 1-100, where 1 is slowest and 100 is fastest\n className?: string\n onComplete?: () => void\n as?: keyof React.JSX.IntrinsicElements // Element type to render\n fadeDuration?: number // Custom fade duration in ms (overrides speed)\n segmentDelay?: number // Custom delay between segments in ms (overrides speed)\n characterChunkSize?: number // Custom characters per frame for typewriter mode (overrides speed)\n}\n\nfunction ResponseStream({\n textStream,\n mode = \"typewriter\",\n speed = 20,\n className = \"\",\n onComplete,\n as = \"div\",\n fadeDuration,\n segmentDelay,\n characterChunkSize,\n}: ResponseStreamProps) {\n const animationEndRef = useRef<(() => void) | null>(null)\n\n const {\n displayedText,\n isComplete,\n segments,\n getFadeDuration,\n getSegmentDelay,\n } = useTextStream({\n textStream,\n speed,\n mode,\n onComplete,\n fadeDuration,\n segmentDelay,\n characterChunkSize,\n })\n\n useEffect(() => {\n animationEndRef.current = onComplete ?? null\n }, [onComplete])\n\n const handleLastSegmentAnimationEnd = useCallback(() => {\n if (animationEndRef.current && isComplete) {\n animationEndRef.current()\n }\n }, [isComplete])\n\n // fadeStyle is the style for the fade animation\n const fadeStyle = `\n @keyframes fadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n }\n \n .fade-segment {\n display: inline-block;\n opacity: 0;\n animation: fadeIn ${getFadeDuration()}ms ease-out forwards;\n }\n\n .fade-segment-space {\n white-space: pre;\n }\n `\n\n const renderContent = () => {\n switch (mode) {\n case \"typewriter\":\n return <>{displayedText}\n\n case \"fade\":\n return (\n <>\n \n
\n {segments.map((segment, idx) => {\n const isWhitespace = /^\\s+$/.test(segment.text)\n const isLastSegment = idx === segments.length - 1\n\n return (\n \n {segment.text}\n \n )\n })}\n
\n \n )\n\n default:\n return <>{displayedText}\n }\n }\n\n const Container = as as keyof React.JSX.IntrinsicElements\n\n return {renderContent()}\n}\n\nexport { useTextStream, ResponseStream }\n", "type": "registry:ui" } ] }