{ "$schema": "https://ui.shadcn.com/schema/registry.json", "name": "prompt-kit", "homepage": "https://prompt-kit.com", "items": [ { "name": "prompt-input", "type": "registry:ui", "title": "Prompt Input", "description": "An input field designed for chat interfaces, allowing users to enter and submit text prompts to an AI model", "dependencies": [], "devDependencies": [], "registryDependencies": [ "textarea", "tooltip" ], "files": [ { "path": "components/prompt-kit/prompt-input.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport { Textarea } from \"@/components/ui/textarea\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { cn } from \"@/lib/utils\"\nimport React, {\n createContext,\n useContext,\n useEffect,\n useRef,\n useState,\n} from \"react\"\n\ntype PromptInputContextType = {\n isLoading: boolean\n value: string\n setValue: (value: string) => void\n maxHeight: number | string\n onSubmit?: () => void\n disabled?: boolean\n}\n\nconst PromptInputContext = createContext({\n isLoading: false,\n value: \"\",\n setValue: () => {},\n maxHeight: 240,\n onSubmit: undefined,\n disabled: false,\n})\n\nfunction usePromptInput() {\n const context = useContext(PromptInputContext)\n if (!context) {\n throw new Error(\"usePromptInput must be used within a PromptInput\")\n }\n return context\n}\n\ntype PromptInputProps = {\n isLoading?: boolean\n value?: string\n onValueChange?: (value: string) => void\n maxHeight?: number | string\n onSubmit?: () => void\n children: React.ReactNode\n className?: string\n}\n\nfunction PromptInput({\n className,\n isLoading = false,\n maxHeight = 240,\n value,\n onValueChange,\n onSubmit,\n children,\n}: PromptInputProps) {\n const [internalValue, setInternalValue] = useState(value || \"\")\n\n const handleChange = (newValue: string) => {\n setInternalValue(newValue)\n onValueChange?.(newValue)\n }\n\n return (\n \n \n \n {children}\n \n \n \n )\n}\n\nexport type PromptInputTextareaProps = {\n disableAutosize?: boolean\n} & React.ComponentProps\n\nfunction PromptInputTextarea({\n className,\n onKeyDown,\n disableAutosize = false,\n ...props\n}: PromptInputTextareaProps) {\n const { value, setValue, maxHeight, onSubmit, disabled } = usePromptInput()\n const textareaRef = useRef(null)\n\n useEffect(() => {\n if (disableAutosize) return\n\n if (!textareaRef.current) return\n textareaRef.current.style.height = \"auto\"\n textareaRef.current.style.height =\n typeof maxHeight === \"number\"\n ? `${Math.min(textareaRef.current.scrollHeight, maxHeight)}px`\n : `min(${textareaRef.current.scrollHeight}px, ${maxHeight})`\n }, [value, maxHeight, disableAutosize])\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault()\n onSubmit?.()\n }\n onKeyDown?.(e)\n }\n\n return (\n setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n className={cn(\n \"text-primary min-h-[44px] w-full resize-none border-none bg-transparent shadow-none outline-none focus-visible:ring-0 focus-visible:ring-offset-0\",\n className\n )}\n rows={1}\n disabled={disabled}\n {...props}\n />\n )\n}\n\ntype PromptInputActionsProps = React.HTMLAttributes\n\nfunction PromptInputActions({\n children,\n className,\n ...props\n}: PromptInputActionsProps) {\n return (\n
\n {children}\n
\n )\n}\n\ntype PromptInputActionProps = {\n className?: string\n tooltip: React.ReactNode\n children: React.ReactNode\n side?: \"top\" | \"bottom\" | \"left\" | \"right\"\n} & React.ComponentProps\n\nfunction PromptInputAction({\n tooltip,\n children,\n className,\n side = \"top\",\n ...props\n}: PromptInputActionProps) {\n const { disabled } = usePromptInput()\n\n return (\n \n \n {children}\n \n \n {tooltip}\n \n \n )\n}\n\nexport {\n PromptInput,\n PromptInputTextarea,\n PromptInputActions,\n PromptInputAction,\n}\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "code-block", "type": "registry:ui", "title": "Code Block", "description": "A component for displaying code snippets with syntax highlighting and customizable styling", "dependencies": [ "shiki" ], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/prompt-kit/code-block.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport React, { useEffect, useState } from \"react\"\nimport { codeToHtml } from \"@/lib/shiki\"\n\nexport type CodeBlockProps = {\n children?: React.ReactNode\n className?: string\n} & React.HTMLProps\n\nfunction CodeBlock({ children, className, ...props }: CodeBlockProps) {\n return (\n \n {children}\n \n )\n}\n\nexport type CodeBlockCodeProps = {\n code: string\n language?: string\n theme?: string\n className?: string\n} & React.HTMLProps\n\nfunction CodeBlockCode({\n code,\n language = \"tsx\",\n theme = \"github-light\",\n className,\n ...props\n}: CodeBlockCodeProps) {\n const [highlightedHtml, setHighlightedHtml] = useState(null)\n\n useEffect(() => {\n async function highlight() {\n if (!code) {\n setHighlightedHtml(\"
\")\n return\n }\n\n const html = await codeToHtml({ code, lang: language })\n setHighlightedHtml(html)\n }\n highlight()\n }, [code, language])\n\n const classNames = cn(\n \"w-full overflow-x-auto text-[13px] [&>pre]:px-4 [&>pre]:py-4\",\n className\n )\n\n // SSR fallback: render plain code if not hydrated yet\n return highlightedHtml ? (\n \n ) : (\n
\n
\n        {code}\n      
\n
\n )\n}\n\nexport type CodeBlockGroupProps = React.HTMLAttributes\n\nfunction CodeBlockGroup({\n children,\n className,\n ...props\n}: CodeBlockGroupProps) {\n return (\n \n {children}\n \n )\n}\n\nexport { CodeBlockGroup, CodeBlockCode, CodeBlock }\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "markdown", "type": "registry:ui", "title": "Markdown", "description": "A component for rendering Markdown content with support for code blocks, GFM, and custom styling", "dependencies": [ "react-markdown", "remark-gfm", "shiki", "marked" ], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/prompt-kit/markdown.tsx", "type": "registry:component", "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" }, { "path": "components/prompt-kit/code-block.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport React, { useEffect, useState } from \"react\"\nimport { codeToHtml } from \"@/lib/shiki\"\n\nexport type CodeBlockProps = {\n children?: React.ReactNode\n className?: string\n} & React.HTMLProps\n\nfunction CodeBlock({ children, className, ...props }: CodeBlockProps) {\n return (\n \n {children}\n \n )\n}\n\nexport type CodeBlockCodeProps = {\n code: string\n language?: string\n theme?: string\n className?: string\n} & React.HTMLProps\n\nfunction CodeBlockCode({\n code,\n language = \"tsx\",\n theme = \"github-light\",\n className,\n ...props\n}: CodeBlockCodeProps) {\n const [highlightedHtml, setHighlightedHtml] = useState(null)\n\n useEffect(() => {\n async function highlight() {\n if (!code) {\n setHighlightedHtml(\"
\")\n return\n }\n\n const html = await codeToHtml({ code, lang: language })\n setHighlightedHtml(html)\n }\n highlight()\n }, [code, language])\n\n const classNames = cn(\n \"w-full overflow-x-auto text-[13px] [&>pre]:px-4 [&>pre]:py-4\",\n className\n )\n\n // SSR fallback: render plain code if not hydrated yet\n return highlightedHtml ? (\n \n ) : (\n
\n
\n        {code}\n      
\n
\n )\n}\n\nexport type CodeBlockGroupProps = React.HTMLAttributes\n\nfunction CodeBlockGroup({\n children,\n className,\n ...props\n}: CodeBlockGroupProps) {\n return (\n \n {children}\n \n )\n}\n\nexport { CodeBlockGroup, CodeBlockCode, CodeBlock }\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "message", "type": "registry:ui", "title": "Message", "description": "A component for displaying chat messages with support for avatars, markdown content, and interactive actions", "dependencies": [ "react-markdown", "remark-gfm", "shiki", "marked", "remark-breaks" ], "devDependencies": [], "registryDependencies": [ "avatar", "tooltip" ], "files": [ { "path": "components/prompt-kit/message.tsx", "type": "registry:component", "content": "import { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { cn } from \"@/lib/utils\"\nimport { Markdown } from \"./markdown\"\n\nexport type MessageProps = {\n children: React.ReactNode\n className?: string\n} & React.HTMLProps\n\nconst Message = ({ children, className, ...props }: MessageProps) => (\n
\n {children}\n
\n)\n\nexport type MessageAvatarProps = {\n src: string\n alt: string\n fallback?: string\n delayMs?: number\n className?: string\n}\n\nconst MessageAvatar = ({\n src,\n alt,\n fallback,\n delayMs,\n className,\n}: MessageAvatarProps) => {\n return (\n \n \n {fallback && (\n {fallback}\n )}\n \n )\n}\n\nexport type MessageContentProps = {\n children: React.ReactNode\n markdown?: boolean\n className?: string\n} & React.ComponentProps &\n React.HTMLProps\n\nconst MessageContent = ({\n children,\n markdown = false,\n className,\n ...props\n}: MessageContentProps) => {\n const classNames = cn(\n \"rounded-lg p-2 text-foreground bg-secondary prose break-words whitespace-normal\",\n className\n )\n\n return markdown ? (\n \n {children as string}\n \n ) : (\n
\n {children}\n
\n )\n}\n\nexport type MessageActionsProps = {\n children: React.ReactNode\n className?: string\n} & React.HTMLProps\n\nconst MessageActions = ({\n children,\n className,\n ...props\n}: MessageActionsProps) => (\n \n {children}\n \n)\n\nexport type MessageActionProps = {\n className?: string\n tooltip: React.ReactNode\n children: React.ReactNode\n side?: \"top\" | \"bottom\" | \"left\" | \"right\"\n} & React.ComponentProps\n\nconst MessageAction = ({\n tooltip,\n children,\n className,\n side = \"top\",\n ...props\n}: MessageActionProps) => {\n return (\n \n \n {children}\n \n {tooltip}\n \n \n \n )\n}\n\nexport { Message, MessageAvatar, MessageContent, MessageActions, MessageAction }\n" }, { "path": "components/prompt-kit/markdown.tsx", "type": "registry:component", "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" }, { "path": "components/prompt-kit/code-block.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport React, { useEffect, useState } from \"react\"\nimport { codeToHtml } from \"@/lib/shiki\"\n\nexport type CodeBlockProps = {\n children?: React.ReactNode\n className?: string\n} & React.HTMLProps\n\nfunction CodeBlock({ children, className, ...props }: CodeBlockProps) {\n return (\n \n {children}\n \n )\n}\n\nexport type CodeBlockCodeProps = {\n code: string\n language?: string\n theme?: string\n className?: string\n} & React.HTMLProps\n\nfunction CodeBlockCode({\n code,\n language = \"tsx\",\n theme = \"github-light\",\n className,\n ...props\n}: CodeBlockCodeProps) {\n const [highlightedHtml, setHighlightedHtml] = useState(null)\n\n useEffect(() => {\n async function highlight() {\n if (!code) {\n setHighlightedHtml(\"
\")\n return\n }\n\n const html = await codeToHtml({ code, lang: language })\n setHighlightedHtml(html)\n }\n highlight()\n }, [code, language])\n\n const classNames = cn(\n \"w-full overflow-x-auto text-[13px] [&>pre]:px-4 [&>pre]:py-4\",\n className\n )\n\n // SSR fallback: render plain code if not hydrated yet\n return highlightedHtml ? (\n \n ) : (\n
\n
\n        {code}\n      
\n
\n )\n}\n\nexport type CodeBlockGroupProps = React.HTMLAttributes\n\nfunction CodeBlockGroup({\n children,\n className,\n ...props\n}: CodeBlockGroupProps) {\n return (\n \n {children}\n \n )\n}\n\nexport { CodeBlockGroup, CodeBlockCode, CodeBlock }\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "chat-container", "type": "registry:ui", "title": "Chat Container", "description": "A component for creating chat interfaces with intelligent auto-scrolling behavior, designed to provide a smooth and responsive user experience", "dependencies": [ "use-stick-to-bottom" ], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/prompt-kit/chat-container.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport { StickToBottom } from \"use-stick-to-bottom\"\n\nexport type ChatContainerRootProps = {\n children: React.ReactNode\n className?: string\n} & React.HTMLAttributes\n\nexport type ChatContainerContentProps = {\n children: React.ReactNode\n className?: string\n} & React.HTMLAttributes\n\nexport type ChatContainerScrollAnchorProps = {\n className?: string\n ref?: React.RefObject\n} & React.HTMLAttributes\n\nfunction ChatContainerRoot({\n children,\n className,\n ...props\n}: ChatContainerRootProps) {\n return (\n \n {children}\n \n )\n}\n\nfunction ChatContainerContent({\n children,\n className,\n ...props\n}: ChatContainerContentProps) {\n return (\n \n {children}\n \n )\n}\n\nfunction ChatContainerScrollAnchor({\n className,\n ...props\n}: ChatContainerScrollAnchorProps) {\n return (\n \n )\n}\n\nexport { ChatContainerRoot, ChatContainerContent, ChatContainerScrollAnchor }\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "scroll-button", "type": "registry:ui", "title": "Scroll Button", "description": "A floating button component that appears when users scroll up in a container, allowing them to quickly return to the bottom of the content", "dependencies": [ "class-variance-authority", "lucide-react" ], "devDependencies": [], "registryDependencies": [ "button" ], "files": [ { "path": "components/prompt-kit/scroll-button.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport { Button, buttonVariants } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { ChevronDown } from \"lucide-react\"\nimport { useStickToBottomContext } from \"use-stick-to-bottom\"\n\nexport type ScrollButtonProps = {\n className?: string\n variant?: VariantProps[\"variant\"]\n size?: VariantProps[\"size\"]\n} & React.ButtonHTMLAttributes\n\nfunction ScrollButton({\n className,\n variant = \"outline\",\n size = \"sm\",\n ...props\n}: ScrollButtonProps) {\n const { isAtBottom, scrollToBottom } = useStickToBottomContext()\n\n return (\n scrollToBottom()}\n {...props}\n >\n \n \n )\n}\n\nexport { ScrollButton }\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "loader", "type": "registry:ui", "title": "Loader", "description": "A component for displaying a loading indicator with multiple variants and customizable styling", "dependencies": [], "devDependencies": [], "registryDependencies": [ "button" ], "files": [ { "path": "components/prompt-kit/loader.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport React from \"react\"\n\nexport interface LoaderProps {\n variant?:\n | \"circular\"\n | \"classic\"\n | \"pulse\"\n | \"pulse-dot\"\n | \"dots\"\n | \"typing\"\n | \"wave\"\n | \"bars\"\n | \"terminal\"\n | \"text-blink\"\n | \"text-shimmer\"\n | \"loading-dots\"\n size?: \"sm\" | \"md\" | \"lg\"\n text?: string\n className?: string\n}\n\nexport function CircularLoader({\n className,\n size = \"md\",\n}: {\n className?: string\n size?: \"sm\" | \"md\" | \"lg\"\n}) {\n const sizeClasses = {\n sm: \"size-4\",\n md: \"size-5\",\n lg: \"size-6\",\n }\n\n return (\n \n Loading\n \n )\n}\n\nexport function ClassicLoader({\n className,\n size = \"md\",\n}: {\n className?: string\n size?: \"sm\" | \"md\" | \"lg\"\n}) {\n const sizeClasses = {\n sm: \"size-4\",\n md: \"size-5\",\n lg: \"size-6\",\n }\n\n const barSizes = {\n sm: { height: \"6px\", width: \"1.5px\" },\n md: { height: \"8px\", width: \"2px\" },\n lg: { height: \"10px\", width: \"2.5px\" },\n }\n\n return (\n
\n
\n {[...Array(12)].map((_, i) => (\n \n ))}\n
\n Loading\n
\n )\n}\n\nexport function PulseLoader({\n className,\n size = \"md\",\n}: {\n className?: string\n size?: \"sm\" | \"md\" | \"lg\"\n}) {\n const sizeClasses = {\n sm: \"size-4\",\n md: \"size-5\",\n lg: \"size-6\",\n }\n\n return (\n
\n
\n Loading\n
\n )\n}\n\nexport function PulseDotLoader({\n className,\n size = \"md\",\n}: {\n className?: string\n size?: \"sm\" | \"md\" | \"lg\"\n}) {\n const sizeClasses = {\n sm: \"size-1\",\n md: \"size-2\",\n lg: \"size-3\",\n }\n\n return (\n \n Loading\n
\n )\n}\n\nexport function DotsLoader({\n className,\n size = \"md\",\n}: {\n className?: string\n size?: \"sm\" | \"md\" | \"lg\"\n}) {\n const dotSizes = {\n sm: \"h-1.5 w-1.5\",\n md: \"h-2 w-2\",\n lg: \"h-2.5 w-2.5\",\n }\n\n const containerSizes = {\n sm: \"h-4\",\n md: \"h-5\",\n lg: \"h-6\",\n }\n\n return (\n \n {[...Array(3)].map((_, i) => (\n \n ))}\n Loading\n \n )\n}\n\nexport function TypingLoader({\n className,\n size = \"md\",\n}: {\n className?: string\n size?: \"sm\" | \"md\" | \"lg\"\n}) {\n const dotSizes = {\n sm: \"h-1 w-1\",\n md: \"h-1.5 w-1.5\",\n lg: \"h-2 w-2\",\n }\n\n const containerSizes = {\n sm: \"h-4\",\n md: \"h-5\",\n lg: \"h-6\",\n }\n\n return (\n \n {[...Array(3)].map((_, i) => (\n \n ))}\n Loading\n \n )\n}\n\nexport function WaveLoader({\n className,\n size = \"md\",\n}: {\n className?: string\n size?: \"sm\" | \"md\" | \"lg\"\n}) {\n const barWidths = {\n sm: \"w-0.5\",\n md: \"w-0.5\",\n lg: \"w-1\",\n }\n\n const containerSizes = {\n sm: \"h-4\",\n md: \"h-5\",\n lg: \"h-6\",\n }\n\n const heights = {\n sm: [\"6px\", \"9px\", \"12px\", \"9px\", \"6px\"],\n md: [\"8px\", \"12px\", \"16px\", \"12px\", \"8px\"],\n lg: [\"10px\", \"15px\", \"20px\", \"15px\", \"10px\"],\n }\n\n return (\n \n {[...Array(5)].map((_, i) => (\n \n ))}\n Loading\n \n )\n}\n\nexport function BarsLoader({\n className,\n size = \"md\",\n}: {\n className?: string\n size?: \"sm\" | \"md\" | \"lg\"\n}) {\n const barWidths = {\n sm: \"w-1\",\n md: \"w-1.5\",\n lg: \"w-2\",\n }\n\n const containerSizes = {\n sm: \"h-4 gap-1\",\n md: \"h-5 gap-1.5\",\n lg: \"h-6 gap-2\",\n }\n\n return (\n
\n {[...Array(3)].map((_, i) => (\n \n ))}\n Loading\n
\n )\n}\n\nexport function TerminalLoader({\n className,\n size = \"md\",\n}: {\n className?: string\n size?: \"sm\" | \"md\" | \"lg\"\n}) {\n const cursorSizes = {\n sm: \"h-3 w-1.5\",\n md: \"h-4 w-2\",\n lg: \"h-5 w-2.5\",\n }\n\n const textSizes = {\n sm: \"text-xs\",\n md: \"text-sm\",\n lg: \"text-base\",\n }\n\n const containerSizes = {\n sm: \"h-4\",\n md: \"h-5\",\n lg: \"h-6\",\n }\n\n return (\n \n \n {\">\"}\n \n \n Loading\n \n )\n}\n\nexport function TextBlinkLoader({\n text = \"Thinking\",\n className,\n size = \"md\",\n}: {\n text?: string\n className?: string\n size?: \"sm\" | \"md\" | \"lg\"\n}) {\n const textSizes = {\n sm: \"text-xs\",\n md: \"text-sm\",\n lg: \"text-base\",\n }\n\n return (\n \n {text}\n \n )\n}\n\nexport function TextShimmerLoader({\n text = \"Thinking\",\n className,\n size = \"md\",\n}: {\n text?: string\n className?: string\n size?: \"sm\" | \"md\" | \"lg\"\n}) {\n const textSizes = {\n sm: \"text-xs\",\n md: \"text-sm\",\n lg: \"text-base\",\n }\n\n return (\n \n {text}\n \n )\n}\n\nexport function TextDotsLoader({\n className,\n text = \"Thinking\",\n size = \"md\",\n}: {\n className?: string\n text?: string\n size?: \"sm\" | \"md\" | \"lg\"\n}) {\n const textSizes = {\n sm: \"text-xs\",\n md: \"text-sm\",\n lg: \"text-base\",\n }\n\n return (\n \n \n {text}\n \n \n \n .\n \n \n .\n \n \n .\n \n \n \n )\n}\n\nfunction Loader({\n variant = \"circular\",\n size = \"md\",\n text,\n className,\n}: LoaderProps) {\n switch (variant) {\n case \"circular\":\n return \n case \"classic\":\n return \n case \"pulse\":\n return \n case \"pulse-dot\":\n return \n case \"dots\":\n return \n case \"typing\":\n return \n case \"wave\":\n return \n case \"bars\":\n return \n case \"terminal\":\n return \n case \"text-blink\":\n return \n case \"text-shimmer\":\n return \n case \"loading-dots\":\n return \n default:\n return \n }\n}\n\nexport { Loader }\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "prompt-suggestion", "type": "registry:ui", "title": "Prompt Suggestion", "description": "A component for implementing interactive prompt suggestions in AI interfaces. The PromptSuggestion component offers two distinct modes: Normal Mode and Highlight Mode.", "dependencies": [ "class-variance-authority", "lucide-react" ], "devDependencies": [], "registryDependencies": [ "button" ], "files": [ { "path": "components/prompt-kit/prompt-suggestion.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport { Button, buttonVariants } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport { VariantProps } from \"class-variance-authority\"\n\nexport type PromptSuggestionProps = {\n children: React.ReactNode\n variant?: VariantProps[\"variant\"]\n size?: VariantProps[\"size\"]\n className?: string\n highlight?: string\n} & React.ButtonHTMLAttributes\n\nfunction PromptSuggestion({\n children,\n variant,\n size,\n className,\n highlight,\n ...props\n}: PromptSuggestionProps) {\n const isHighlightMode = highlight !== undefined && highlight.trim() !== \"\"\n const content = typeof children === \"string\" ? children : \"\"\n\n if (!isHighlightMode) {\n return (\n \n {children}\n \n )\n }\n\n if (!content) {\n return (\n \n {children}\n \n )\n }\n\n const trimmedHighlight = highlight.trim()\n const contentLower = content.toLowerCase()\n const highlightLower = trimmedHighlight.toLowerCase()\n const shouldHighlight = contentLower.includes(highlightLower)\n\n return (\n \n {shouldHighlight ? (\n (() => {\n const index = contentLower.indexOf(highlightLower)\n if (index === -1)\n return (\n \n {content}\n \n )\n\n const actualHighlightedText = content.substring(\n index,\n index + highlightLower.length\n )\n\n const before = content.substring(0, index)\n const after = content.substring(index + actualHighlightedText.length)\n\n return (\n <>\n {before && (\n \n {before}\n \n )}\n \n {actualHighlightedText}\n \n {after && (\n \n {after}\n \n )}\n \n )\n })()\n ) : (\n \n {content}\n \n )}\n \n )\n}\n\nexport { PromptSuggestion }\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "response-stream", "type": "registry:ui", "title": "Response Stream", "description": "A component to simulate streaming text on the client side, perfect for fake responses, or any controlled progressive text display.", "dependencies": [], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/prompt-kit/response-stream.tsx", "type": "registry:component", "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" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "reasoning", "type": "registry:ui", "title": "Reasoning", "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.", "dependencies": [ "lucide-react" ], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/prompt-kit/reasoning.tsx", "type": "registry:component", "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" }, { "path": "components/prompt-kit/markdown.tsx", "type": "registry:component", "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" }, { "path": "components/prompt-kit/response-stream.tsx", "type": "registry:component", "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" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "file-upload", "type": "registry:ui", "title": "File Upload", "description": "A component for creating drag-and-drop file upload interfaces with support for single or multiple files, custom triggers, and visual feedback during file dragging operations.", "dependencies": [], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/prompt-kit/file-upload.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n Children,\n cloneElement,\n createContext,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n} from \"react\"\nimport { createPortal } from \"react-dom\"\n\ntype FileUploadContextValue = {\n isDragging: boolean\n inputRef: React.RefObject\n multiple?: boolean\n disabled?: boolean\n}\n\nconst FileUploadContext = createContext(null)\n\nexport type FileUploadProps = {\n onFilesAdded: (files: File[]) => void\n children: React.ReactNode\n multiple?: boolean\n accept?: string\n disabled?: boolean\n}\n\nfunction FileUpload({\n onFilesAdded,\n children,\n multiple = true,\n accept,\n disabled = false,\n}: FileUploadProps) {\n const inputRef = useRef(null)\n const [isDragging, setIsDragging] = useState(false)\n const dragCounter = useRef(0)\n\n const handleFiles = useCallback(\n (files: FileList) => {\n const newFiles = Array.from(files)\n if (multiple) {\n onFilesAdded(newFiles)\n } else {\n onFilesAdded(newFiles.slice(0, 1))\n }\n },\n [multiple, onFilesAdded]\n )\n\n useEffect(() => {\n const handleDrag = (e: DragEvent) => {\n e.preventDefault()\n e.stopPropagation()\n }\n\n const handleDragIn = (e: DragEvent) => {\n handleDrag(e)\n dragCounter.current++\n if (e.dataTransfer?.items.length) setIsDragging(true)\n }\n\n const handleDragOut = (e: DragEvent) => {\n handleDrag(e)\n dragCounter.current--\n if (dragCounter.current === 0) setIsDragging(false)\n }\n\n const handleDrop = (e: DragEvent) => {\n handleDrag(e)\n setIsDragging(false)\n dragCounter.current = 0\n if (e.dataTransfer?.files.length) {\n handleFiles(e.dataTransfer.files)\n }\n }\n\n window.addEventListener(\"dragenter\", handleDragIn)\n window.addEventListener(\"dragleave\", handleDragOut)\n window.addEventListener(\"dragover\", handleDrag)\n window.addEventListener(\"drop\", handleDrop)\n\n return () => {\n window.removeEventListener(\"dragenter\", handleDragIn)\n window.removeEventListener(\"dragleave\", handleDragOut)\n window.removeEventListener(\"dragover\", handleDrag)\n window.removeEventListener(\"drop\", handleDrop)\n }\n }, [handleFiles, onFilesAdded, multiple])\n\n const handleFileSelect = (e: React.ChangeEvent) => {\n if (e.target.files?.length) {\n handleFiles(e.target.files)\n e.target.value = \"\"\n }\n }\n\n return (\n \n \n {children}\n \n )\n}\n\nexport type FileUploadTriggerProps =\n React.ComponentPropsWithoutRef<\"button\"> & {\n asChild?: boolean\n }\n\nfunction FileUploadTrigger({\n asChild = false,\n className,\n children,\n ...props\n}: FileUploadTriggerProps) {\n const context = useContext(FileUploadContext)\n const handleClick = () => context?.inputRef.current?.click()\n\n if (asChild) {\n const child = Children.only(children) as React.ReactElement<\n React.HTMLAttributes\n >\n return cloneElement(child, {\n ...props,\n role: \"button\",\n className: cn(className, child.props.className),\n onClick: (e: React.MouseEvent) => {\n handleClick()\n child.props.onClick?.(e as React.MouseEvent)\n },\n })\n }\n\n return (\n \n {children}\n \n )\n}\n\ntype FileUploadContentProps = React.HTMLAttributes\n\nfunction FileUploadContent({ className, ...props }: FileUploadContentProps) {\n const context = useContext(FileUploadContext)\n const [mounted, setMounted] = useState(false)\n\n useEffect(() => {\n setMounted(true)\n return () => setMounted(false)\n }, [])\n\n if (!context?.isDragging || !mounted || context?.disabled) {\n return null\n }\n\n const content = (\n \n )\n\n return createPortal(content, document.body)\n}\n\nexport { FileUpload, FileUploadTrigger, FileUploadContent }\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "jsx-preview", "type": "registry:ui", "title": "Jsx Preview", "description": "A component for rendering JSX strings as React components, with support for streaming content and automatic tag completion.", "dependencies": [ "react-jsx-parser" ], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/prompt-kit/jsx-preview.tsx", "type": "registry:component", "content": "import * as React from \"react\"\nimport JsxParser from \"react-jsx-parser\"\nimport type { TProps as JsxParserProps } from \"react-jsx-parser\"\n\nfunction matchJsxTag(code: string) {\n if (code.trim() === \"\") {\n return null\n }\n\n const tagRegex = /<\\/?([a-zA-Z][a-zA-Z0-9]*)\\s*([^>]*?)(\\/)?>/\n const match = code.match(tagRegex)\n\n if (!match || typeof match.index === \"undefined\") {\n return null\n }\n\n const [fullMatch, tagName, attributes, selfClosing] = match\n\n const type = selfClosing\n ? \"self-closing\"\n : fullMatch.startsWith(\" ``)\n .join(\"\")\n )\n}\n\nexport type JSXPreviewProps = {\n jsx: string\n isStreaming?: boolean\n} & JsxParserProps\n\nfunction JSXPreview({ jsx, isStreaming = false, ...props }: JSXPreviewProps) {\n const processedJsx = React.useMemo(\n () => (isStreaming ? completeJsxTag(jsx) : jsx),\n [jsx, isStreaming]\n )\n\n // Cast JsxParser to any to work around the type incompatibility\n const Parser = JsxParser as unknown as React.ComponentType\n\n return \n}\n\nexport { JSXPreview }\n" } ], "categories": [ "ai", "prompt-kit" ] } ] }