Reasoning
Reasoning é um componente para React e Tailwind CSS, da biblioteca Prompt Kit, com licença MIT. Copia e cola no teu projeto.
Prompt Kit
MIT
other
O que é
Reasoning é um componente para React e Tailwind CSS, da biblioteca Prompt Kit, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i lucide-react
Licença
Este componente vem de Prompt Kit e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
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 }