Reasoning
Reasoning is a React and Tailwind CSS component from Prompt Kit, licensed MIT. Copy it and paste it into your project.
Prompt Kit
MIT
other
What it is
Reasoning is a React and Tailwind CSS component from Prompt Kit, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i lucide-react
Licence
This component comes from Prompt Kit and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Reasoning
"use client"
import { cn } from "@/lib/utils"
import { ChevronDownIcon } from "lucide-react"
import React, {
createContext,
useContext,
useEffect,
useRef,
useState,
} from "react"
import { Markdown } from "./markdown"
type ReasoningContextType = {
isOpen: boolean
onOpenChange: (open: boolean) => void
}
const ReasoningContext = createContext<ReasoningContextType | undefined>(
undefined
)
function useReasoningContext() {
const context = useContext(ReasoningContext)
if (!context) {
throw new Error(
"useReasoningContext must be used within a Reasoning provider"
)
}
return context
}
export type ReasoningProps = {
children: React.ReactNode
className?: string
open?: boolean
onOpenChange?: (open: boolean) => void
isStreaming?: boolean
}
function Reasoning({
children,
className,
open,
onOpenChange,
isStreaming,
}: ReasoningProps) {
const [internalOpen, setInternalOpen] = useState(false)
const [wasAutoOpened, setWasAutoOpened] = useState(false)
const isControlled = open !== undefined
const isOpen = isControlled ? open : internalOpen
const handleOpenChange = (newOpen: boolean) => {
if (!isControlled) {
setInternalOpen(newOpen)
}
onOpenChange?.(newOpen)
}
useEffect(() => {
if (isStreaming && !wasAutoOpened) {
if (!isControlled) setInternalOpen(true)
setWasAutoOpened(true)
}
if (!isStreaming && wasAutoOpened) {
if (!isControlled) setInternalOpen(false)
setWasAutoOpened(false)
}
}, [isStreaming, wasAutoOpened, isControlled])
return (
<ReasoningContext.Provider
value={{
isOpen,
onOpenChange: handleOpenChange,
}}
>
<div className={className}>{children}</div>
</ReasoningContext.Provider>
)
}
export type ReasoningTriggerProps = {
children: React.ReactNode
className?: string
} & React.HTMLAttributes<HTMLButtonElement>
function ReasoningTrigger({
children,
className,
...props
}: ReasoningTriggerProps) {
const { isOpen, onOpenChange } = useReasoningContext()
return (
<button
className={cn("flex cursor-pointer items-center gap-2", className)}
onClick={() => onOpenChange(!isOpen)}
{...props}
>
<span className="text-primary">{children}</span>
<div
className={cn(
"transform transition-transform",
isOpen ? "rotate-180" : ""
)}
>
<ChevronDownIcon className="size-4" />
</div>
</button>
)
}
export type ReasoningContentProps = {
children: React.ReactNode
className?: string
markdown?: boolean
contentClassName?: string
} & React.HTMLAttributes<HTMLDivElement>
function ReasoningContent({
children,
className,
contentClassName,
markdown = false,
...props
}: ReasoningContentProps) {
const contentRef = useRef<HTMLDivElement>(null)
const innerRef = useRef<HTMLDivElement>(null)
const { isOpen } = useReasoningContext()
useEffect(() => {
if (!contentRef.current || !innerRef.current) return
const observer = new ResizeObserver(() => {
if (contentRef.current && innerRef.current && isOpen) {
contentRef.current.style.maxHeight = `${innerRef.current.scrollHeight}px`
}
})
observer.observe(innerRef.current)
if (isOpen) {
contentRef.current.style.maxHeight = `${innerRef.current.scrollHeight}px`
}
return () => observer.disconnect()
}, [isOpen])
const content = markdown ? (
<Markdown>{children as string}</Markdown>
) : (
children
)
return (
<div
ref={contentRef}
className={cn(
"overflow-hidden transition-[max-height] duration-150 ease-out",
className
)}
style={{
maxHeight: isOpen ? contentRef.current?.scrollHeight : "0px",
}}
{...props}
>
<div
ref={innerRef}
className={cn(
"text-muted-foreground prose prose-sm dark:prose-invert",
contentClassName
)}
>
{content}
</div>
</div>
)
}
export { Reasoning, ReasoningTrigger, ReasoningContent }
import { cn } from "@/lib/utils"
import { marked } from "marked"
import { memo, useId, useMemo } from "react"
import ReactMarkdown, { Components } from "react-markdown"
import remarkBreaks from "remark-breaks"
import remarkGfm from "remark-gfm"
import { CodeBlock, CodeBlockCode } from "./code-block"
export type MarkdownProps = {
children: string
id?: string
className?: string
components?: Partial<Components>
}
function parseMarkdownIntoBlocks(markdown: string): string[] {
const tokens = marked.lexer(markdown)
return tokens.map((token) => token.raw)
}
function extractLanguage(className?: string): string {
if (!className) return "plaintext"
const match = className.match(/language-(\w+)/)
return match ? match[1] : "plaintext"
}
const INITIAL_COMPONENTS: Partial<Components> = {
code: function CodeComponent({ className, children, ...props }) {
const isInline =
!props.node?.position?.start.line ||
props.node?.position?.start.line === props.node?.position?.end.line
if (isInline) {
return (
<span
className={cn(
"bg-primary-foreground rounded-sm px-1 font-mono text-sm",
className
)}
{...props}
>
{children}
</span>
)
}
const language = extractLanguage(className)
return (
<CodeBlock className={className}>
<CodeBlockCode code={children as string} language={language} />
</CodeBlock>
)
},
pre: function PreComponent({ children }) {
return <>{children}</>
},
}
const MemoizedMarkdownBlock = memo(
function MarkdownBlock({
content,
components = INITIAL_COMPONENTS,
}: {
content: string
components?: Partial<Components>
}) {
return (
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkBreaks]}
components={components}
>
{content}
</ReactMarkdown>
)
},
function propsAreEqual(prevProps, nextProps) {
return prevProps.content === nextProps.content
}
)
MemoizedMarkdownBlock.displayName = "MemoizedMarkdownBlock"
function MarkdownComponent({
children,
id,
className,
components = INITIAL_COMPONENTS,
}: MarkdownProps) {
const generatedId = useId()
const blockId = id ?? generatedId
const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children])
return (
<div className={className}>
{blocks.map((block, index) => (
<MemoizedMarkdownBlock
key={`${blockId}-block-${index}`}
content={block}
components={components}
/>
))}
</div>
)
}
const Markdown = memo(MarkdownComponent)
Markdown.displayName = "Markdown"
export { Markdown }
"use client"
import { cn } from "@/lib/utils"
import React, { useCallback, useEffect, useRef, useState } from "react"
export type Mode = "typewriter" | "fade"
export type UseTextStreamOptions = {
textStream: string | AsyncIterable<string>
speed?: number
mode?: Mode
onComplete?: () => void
fadeDuration?: number
segmentDelay?: number
characterChunkSize?: number
onError?: (error: unknown) => void
}
export type UseTextStreamResult = {
displayedText: string
isComplete: boolean
segments: { text: string; index: number }[]
getFadeDuration: () => number
getSegmentDelay: () => number
reset: () => void
startStreaming: () => void
pause: () => void
resume: () => void
}
function useTextStream({
textStream,
speed = 20,
mode = "typewriter",
onComplete,
fadeDuration,
segmentDelay,
characterChunkSize,
onError,
}: UseTextStreamOptions): UseTextStreamResult {
const [displayedText, setDisplayedText] = useState("")
const [isComplete, setIsComplete] = useState(false)
const [segments, setSegments] = useState<{ text: string; index: number }[]>(
[]
)
const speedRef = useRef(speed)
const modeRef = useRef(mode)
const currentIndexRef = useRef(0)
const animationRef = useRef<number | null>(null)
const fadeDurationRef = useRef(fadeDuration)
const segmentDelayRef = useRef(segmentDelay)
const characterChunkSizeRef = useRef(characterChunkSize)
const streamRef = useRef<AbortController | null>(null)
const completedRef = useRef(false)
const onCompleteRef = useRef(onComplete)
useEffect(() => {
speedRef.current = speed
modeRef.current = mode
fadeDurationRef.current = fadeDuration
segmentDelayRef.current = segmentDelay
characterChunkSizeRef.current = characterChunkSize
}, [speed, mode, fadeDuration, segmentDelay, characterChunkSize])
useEffect(() => {
onCompleteRef.current = onComplete
}, [onComplete])
const getChunkSize = useCallback(() => {
if (typeof characterChunkSizeRef.current === "number") {
return Math.max(1, characterChunkSizeRef.current)
}
const normalizedSpeed = Math.min(100, Math.max(1, speedRef.current))
if (modeRef.current === "typewriter") {
if (normalizedSpeed < 25) return 1
return Math.max(1, Math.round((normalizedSpeed - 25) / 10))
} else if (modeRef.current === "fade") {
return 1
}
return 1
}, [])
const getProcessingDelay = useCallback(() => {
if (typeof segmentDelayRef.current === "number") {
return Math.max(0, segmentDelayRef.current)
}
const normalizedSpeed = Math.min(100, Math.max(1, speedRef.current))
return Math.max(1, Math.round(100 / Math.sqrt(normalizedSpeed)))
}, [])
const getFadeDuration = useCallback(() => {
if (typeof fadeDurationRef.current === "number")
return Math.max(10, fadeDurationRef.current)
const normalizedSpeed = Math.min(100, Math.max(1, speedRef.current))
return Math.round(1000 / Math.sqrt(normalizedSpeed))
}, [])
const getSegmentDelay = useCallback(() => {
if (typeof segmentDelayRef.current === "number")
return Math.max(0, segmentDelayRef.current)
const normalizedSpeed = Math.min(100, Math.max(1, speedRef.current))
return Math.max(1, Math.round(100 / Math.sqrt(normalizedSpeed)))
}, [])
const updateSegments = useCallback((text: string) => {
if (modeRef.current === "fade") {
try {
const segmenter = new Intl.Segmenter(navigator.language, {
granularity: "word",
})
const segmentIterator = segmenter.segment(text)
const newSegments = Array.from(segmentIterator).map(
(segment, index) => ({
text: segment.segment,
index,
})
)
setSegments(newSegments)
} catch (error) {
const newSegments = text
.split(/(\s+)/)
.filter(Boolean)
.map((word, index) => ({
text: word,
index,
}))
setSegments(newSegments)
onError?.(error)
}
}
}, [])
const markComplete = useCallback(() => {
if (!completedRef.current) {
completedRef.current = true
setIsComplete(true)
onCompleteRef.current?.()
}
}, [])
const reset = useCallback(() => {
currentIndexRef.current = 0
setDisplayedText("")
setSegments([])
setIsComplete(false)
completedRef.current = false
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
animationRef.current = null
}
}, [])
const processStringTypewriter = useCallback(
(text: string) => {
let lastFrameTime = 0
const streamContent = (timestamp: number) => {
const delay = getProcessingDelay()
if (delay > 0 && timestamp - lastFrameTime < delay) {
animationRef.current = requestAnimationFrame(streamContent)
return
}
lastFrameTime = timestamp
if (currentIndexRef.current >= text.length) {
markComplete()
return
}
const chunkSize = getChunkSize()
const endIndex = Math.min(
currentIndexRef.current + chunkSize,
text.length
)
const newDisplayedText = text.slice(0, endIndex)
setDisplayedText(newDisplayedText)
if (modeRef.current === "fade") {
updateSegments(newDisplayedText)
}
currentIndexRef.current = endIndex
if (endIndex < text.length) {
animationRef.current = requestAnimationFrame(streamContent)
} else {
markComplete()
}
}
animationRef.current = requestAnimationFrame(streamContent)
},
[getProcessingDelay, getChunkSize, updateSegments, markComplete]
)
const processAsyncIterable = useCallback(
async (stream: AsyncIterable<string>) => {
const controller = new AbortController()
streamRef.current = controller
let displayed = ""
try {
for await (const chunk of stream) {
if (controller.signal.aborted) return
displayed += chunk
setDisplayedText(displayed)
updateSegments(displayed)
}
markComplete()
} catch (error) {
console.error("Error processing text stream:", error)
markComplete()
onError?.(error)
}
},
[updateSegments, markComplete, onError]
)
const startStreaming = useCallback(() => {
reset()
if (typeof textStream === "string") {
processStringTypewriter(textStream)
} else if (textStream) {
processAsyncIterable(textStream)
}
}, [textStream, reset, processStringTypewriter, processAsyncIterable])
const pause = useCallback(() => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
animationRef.current = null
}
}, [])
const resume = useCallback(() => {
if (typeof textStream === "string" && !isComplete) {
processStringTypewriter(textStream)
}
}, [textStream, isComplete, processStringTypewriter])
useEffect(() => {
startStreaming()
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
if (streamRef.current) {
streamRef.current.abort()
}
}
}, [textStream, startStreaming])
return {
displayedText,
isComplete,
segments,
getFadeDuration,
getSegmentDelay,
reset,
startStreaming,
pause,
resume,
}
}
export type ResponseStreamProps = {
textStream: string | AsyncIterable<string>
mode?: Mode
speed?: number // 1-100, where 1 is slowest and 100 is fastest
className?: string
onComplete?: () => void
as?: keyof React.JSX.IntrinsicElements // Element type to render
fadeDuration?: number // Custom fade duration in ms (overrides speed)
segmentDelay?: number // Custom delay between segments in ms (overrides speed)
characterChunkSize?: number // Custom characters per frame for typewriter mode (overrides speed)
}
function ResponseStream({
textStream,
mode = "typewriter",
speed = 20,
className = "",
onComplete,
as = "div",
fadeDuration,
segmentDelay,
characterChunkSize,
}: ResponseStreamProps) {
const animationEndRef = useRef<(() => void) | null>(null)
const {
displayedText,
isComplete,
segments,
getFadeDuration,
getSegmentDelay,
} = useTextStream({
textStream,
speed,
mode,
onComplete,
fadeDuration,
segmentDelay,
characterChunkSize,
})
useEffect(() => {
animationEndRef.current = onComplete ?? null
}, [onComplete])
const handleLastSegmentAnimationEnd = useCallback(() => {
if (animationEndRef.current && isComplete) {
animationEndRef.current()
}
}, [isComplete])
// fadeStyle is the style for the fade animation
const fadeStyle = `
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-segment {
display: inline-block;
opacity: 0;
animation: fadeIn ${getFadeDuration()}ms ease-out forwards;
}
.fade-segment-space {
white-space: pre;
}
`
const renderContent = () => {
switch (mode) {
case "typewriter":
return <>{displayedText}</>
case "fade":
return (
<>
<style>{fadeStyle}</style>
<div className="relative">
{segments.map((segment, idx) => {
const isWhitespace = /^\s+$/.test(segment.text)
const isLastSegment = idx === segments.length - 1
return (
<span
key={`${segment.text}-${idx}`}
className={cn(
"fade-segment",
isWhitespace && "fade-segment-space"
)}
style={{
animationDelay: `${idx * getSegmentDelay()}ms`,
}}
onAnimationEnd={
isLastSegment ? handleLastSegmentAnimationEnd : undefined
}
>
{segment.text}
</span>
)
})}
</div>
</>
)
default:
return <>{displayedText}</>
}
}
const Container = as as keyof React.JSX.IntrinsicElements
return <Container className={className}>{renderContent()}</Container>
}
export { useTextStream, ResponseStream }