{ "$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 useLayoutEffect,\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 textareaRef: React.RefObject\n}\n\nconst PromptInputContext = createContext({\n isLoading: false,\n value: \"\",\n setValue: () => {},\n maxHeight: 240,\n onSubmit: undefined,\n disabled: false,\n textareaRef: React.createRef(),\n})\n\nfunction usePromptInput() {\n return useContext(PromptInputContext)\n}\n\nexport type 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 disabled?: boolean\n} & React.ComponentProps<\"div\">\n\nfunction PromptInput({\n className,\n isLoading = false,\n maxHeight = 240,\n value,\n onValueChange,\n onSubmit,\n children,\n disabled = false,\n onClick,\n ...props\n}: PromptInputProps) {\n const [internalValue, setInternalValue] = useState(value || \"\")\n const textareaRef = useRef(null)\n\n const handleChange = (newValue: string) => {\n setInternalValue(newValue)\n onValueChange?.(newValue)\n }\n\n const handleClick: React.MouseEventHandler = (e) => {\n if (!disabled) textareaRef.current?.focus()\n onClick?.(e)\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, textareaRef } =\n usePromptInput()\n\n const adjustHeight = (el: HTMLTextAreaElement | null) => {\n if (!el || disableAutosize) return\n\n el.style.height = \"auto\"\n\n if (typeof maxHeight === \"number\") {\n el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`\n } else {\n el.style.height = `min(${el.scrollHeight}px, ${maxHeight})`\n }\n }\n\n const handleRef = (el: HTMLTextAreaElement | null) => {\n textareaRef.current = el\n adjustHeight(el)\n }\n\n useLayoutEffect(() => {\n if (!textareaRef.current || disableAutosize) return\n\n const el = textareaRef.current\n el.style.height = \"auto\"\n\n if (typeof maxHeight === \"number\") {\n el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`\n } else {\n el.style.height = `min(${el.scrollHeight}px, ${maxHeight})`\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [value, maxHeight, disableAutosize])\n\n const handleChange = (e: React.ChangeEvent) => {\n adjustHeight(e.target)\n setValue(e.target.value)\n }\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 \n )\n}\n\nexport type PromptInputActionsProps = React.HTMLAttributes\n\nfunction PromptInputActions({\n children,\n className,\n ...props\n}: PromptInputActionsProps) {\n return (\n
\n {children}\n
\n )\n}\n\nexport type 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 event.stopPropagation()}\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 \"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, theme })\n setHighlightedHtml(html)\n }\n highlight()\n }, [code, language, theme])\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", "remark-breaks" ], "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 \"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, theme })\n setHighlightedHtml(html)\n }\n highlight()\n }, [code, language, theme])\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 \"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, theme })\n setHighlightedHtml(html)\n }\n highlight()\n }, [code, language, theme])\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" ], "tailwind": { "config": { "theme": { "keyframes": { "typing": { "0%, 100%": { "transform": "translateY(0)", "opacity": "0.5" }, "50%": { "transform": "translateY(-2px)", "opacity": "1" } }, "loading-dots": { "0%, 100%": { "opacity": "0" }, "50%": { "opacity": "1" } }, "wave": { "0%, 100%": { "transform": "scaleY(1)" }, "50%": { "transform": "scaleY(0.6)" } }, "blink": { "0%, 100%": { "opacity": "1" }, "50%": { "opacity": "0" } } }, "text-blink": { "0%, 100%": { "color": "var(--primary)" }, "50%": { "color": "var(--muted-foreground)" } }, "bounce-dots": { "0%, 100%": { "transform": "scale(0.8)", "opacity": "0.5" }, "50%": { "transform": "scale(1.2)", "opacity": "1" } }, "thin-pulse": { "0%, 100%": { "transform": "scale(0.95)", "opacity": "0.8" }, "50%": { "transform": "scale(1.05)", "opacity": "0.4" } }, "pulse-dot": { "0%, 100%": { "transform": "scale(1)", "opacity": "0.8" }, "50%": { "transform": "scale(1.5)", "opacity": "1" } }, "shimmer-text": { "0%": { "backgroundPosition": "150% center" }, "100%": { "backgroundPosition": "-150% center" } }, "wave-bars": { "0%, 100%": { "transform": "scaleY(1)", "opacity": "0.5" }, "50%": { "transform": "scaleY(0.6)", "opacity": "1" } }, "shimmer": { "0%": { "backgroundPosition": "200% 50%" }, "100%": { "backgroundPosition": "-200% 50%" } }, "spinner-fade": { "0%": { "opacity": "0" }, "100%": { "opacity": "1" } } } } }, "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 e.stopPropagation()\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" ] }, { "name": "tool", "type": "registry:ui", "title": "Tool", "description": "Displays tool call details including input, output, status, and errors. Ideal for visualizing AI tool usage in chat UIs.", "dependencies": [ "lucide-react" ], "devDependencies": [], "registryDependencies": [ "collapsible", "button" ], "files": [ { "path": "components/prompt-kit/tool.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/components/ui/collapsible\"\nimport { cn } from \"@/lib/utils\"\nimport {\n CheckCircle,\n ChevronDown,\n Loader2,\n Settings,\n XCircle,\n} from \"lucide-react\"\nimport { useState } from \"react\"\n\nexport type ToolPart = {\n type: string\n state:\n | \"input-streaming\"\n | \"input-available\"\n | \"output-available\"\n | \"output-error\"\n input?: Record\n output?: Record\n toolCallId?: string\n errorText?: string\n}\n\nexport type ToolProps = {\n toolPart: ToolPart\n defaultOpen?: boolean\n className?: string\n}\n\nconst Tool = ({ toolPart, defaultOpen = false, className }: ToolProps) => {\n const [isOpen, setIsOpen] = useState(defaultOpen)\n\n const { state, input, output, toolCallId } = toolPart\n\n const getStateIcon = () => {\n switch (state) {\n case \"input-streaming\":\n return \n case \"input-available\":\n return \n case \"output-available\":\n return \n case \"output-error\":\n return \n default:\n return \n }\n }\n\n const getStateBadge = () => {\n const baseClasses = \"px-2 py-1 rounded-full text-xs font-medium\"\n switch (state) {\n case \"input-streaming\":\n return (\n \n Processing\n \n )\n case \"input-available\":\n return (\n \n Ready\n \n )\n case \"output-available\":\n return (\n \n Completed\n \n )\n case \"output-error\":\n return (\n \n Error\n \n )\n default:\n return (\n \n Pending\n \n )\n }\n }\n\n const formatValue = (value: unknown): string => {\n if (value === null) return \"null\"\n if (value === undefined) return \"undefined\"\n if (typeof value === \"string\") return value\n if (typeof value === \"object\") {\n return JSON.stringify(value, null, 2)\n }\n return String(value)\n }\n\n return (\n \n \n \n \n
\n {getStateIcon()}\n \n {toolPart.type}\n \n {getStateBadge()}\n
\n \n \n
\n \n
\n {input && Object.keys(input).length > 0 && (\n
\n

\n Input\n

\n
\n {Object.entries(input).map(([key, value]) => (\n
\n {key}:{\" \"}\n {formatValue(value)}\n
\n ))}\n
\n
\n )}\n\n {output && (\n
\n

\n Output\n

\n
\n
\n                    {formatValue(output)}\n                  
\n
\n
\n )}\n\n {state === \"output-error\" && toolPart.errorText && (\n
\n

Error

\n
\n {toolPart.errorText}\n
\n
\n )}\n\n {state === \"input-streaming\" && (\n
\n Processing tool call...\n
\n )}\n\n {toolCallId && (\n
\n Call ID: {toolCallId}\n
\n )}\n
\n \n
\n \n )\n}\n\nexport { Tool }\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "source", "type": "registry:ui", "title": "Source", "description": "Displays website sources used by AI-generated content, showing URL details, titles, and descriptions on hover.", "dependencies": [], "devDependencies": [], "registryDependencies": [ "hover-card" ], "files": [ { "path": "components/prompt-kit/source.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport {\n HoverCard,\n HoverCardContent,\n HoverCardTrigger,\n} from \"@/components/ui/hover-card\"\nimport { cn } from \"@/lib/utils\"\nimport { createContext, useContext } from \"react\"\n\nconst SourceContext = createContext<{\n href: string\n domain: string\n} | null>(null)\n\nfunction useSourceContext() {\n const ctx = useContext(SourceContext)\n if (!ctx) throw new Error(\"Source.* must be used inside \")\n return ctx\n}\n\nexport type SourceProps = {\n href: string\n children: React.ReactNode\n}\n\nexport function Source({ href, children }: SourceProps) {\n let domain = \"\"\n try {\n domain = new URL(href).hostname\n } catch {\n domain = href.split(\"/\").pop() || href\n }\n\n return (\n \n \n {children}\n \n \n )\n}\n\nexport type SourceTriggerProps = {\n label?: string | number\n showFavicon?: boolean\n className?: string\n}\n\nexport function SourceTrigger({\n label,\n showFavicon = false,\n className,\n}: SourceTriggerProps) {\n const { href, domain } = useSourceContext()\n const labelToShow = label ?? domain.replace(\"www.\", \"\")\n\n return (\n \n \n {showFavicon && (\n \n )}\n {labelToShow}\n \n \n )\n}\n\nexport type SourceContentProps = {\n title: string\n description: string\n className?: string\n}\n\nexport function SourceContent({\n title,\n description,\n className,\n}: SourceContentProps) {\n const { href, domain } = useSourceContext()\n\n return (\n \n \n
\n \n
\n {domain.replace(\"www.\", \"\")}\n
\n
\n
{title}
\n
\n {description}\n
\n \n
\n )\n}\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "image", "type": "registry:ui", "title": "Image", "description": "A component for displaying images from base64 or Uint8Array data, with full accessibility and responsive styling. Perfect for AI-generated or user-uploaded images.", "dependencies": [], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/prompt-kit/image.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useEffect, useState, type ImgHTMLAttributes } from \"react\"\n\nexport type GeneratedImageLike = {\n base64?: string\n uint8Array?: Uint8Array\n mediaType?: string\n}\n\nexport type ImageProps = GeneratedImageLike &\n Omit, \"src\"> & {\n alt: string\n }\n\nfunction getImageSrc({\n base64,\n mediaType,\n}: Pick) {\n if (base64 && mediaType) {\n return `data:${mediaType};base64,${base64}`\n }\n return undefined\n}\n\nexport const Image = ({\n base64,\n uint8Array,\n mediaType = \"image/png\",\n className,\n alt,\n ...props\n}: ImageProps) => {\n const [objectUrl, setObjectUrl] = useState(undefined)\n\n useEffect(() => {\n if (uint8Array && mediaType) {\n const blob = new Blob([uint8Array as BlobPart], { type: mediaType })\n const url = URL.createObjectURL(blob)\n setObjectUrl(url)\n return () => {\n URL.revokeObjectURL(url)\n }\n }\n setObjectUrl(undefined)\n return\n }, [uint8Array, mediaType])\n\n const base64Src = getImageSrc({ base64, mediaType })\n const src = base64Src ?? objectUrl\n\n if (!src) {\n return (\n \n )\n }\n\n return (\n \n )\n}\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "steps", "type": "registry:ui", "title": "Steps", "description": "A component for displaying a sequence of operations in a collapsible layout. Each step can include details and an optional vertical bar. Useful for showing AI steps like reasoning traces, tool calls, or process logs.", "dependencies": [], "devDependencies": [], "registryDependencies": [ "collapsible" ], "files": [ { "path": "components/prompt-kit/steps.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/components/ui/collapsible\"\nimport { cn } from \"@/lib/utils\"\nimport { ChevronDown } from \"lucide-react\"\n\nexport type StepsItemProps = React.ComponentProps<\"div\">\n\nexport const StepsItem = ({\n children,\n className,\n ...props\n}: StepsItemProps) => (\n
\n {children}\n
\n)\n\nexport type StepsTriggerProps = React.ComponentProps<\n typeof CollapsibleTrigger\n> & {\n leftIcon?: React.ReactNode\n swapIconOnHover?: boolean\n}\n\nexport const StepsTrigger = ({\n children,\n className,\n leftIcon,\n swapIconOnHover = true,\n ...props\n}: StepsTriggerProps) => (\n \n
\n {leftIcon ? (\n \n \n {leftIcon}\n \n {swapIconOnHover && (\n \n )}\n \n ) : null}\n {children}\n
\n {!leftIcon && (\n \n )}\n \n)\n\nexport type StepsContentProps = React.ComponentProps<\n typeof CollapsibleContent\n> & {\n bar?: React.ReactNode\n}\n\nexport const StepsContent = ({\n children,\n className,\n bar,\n ...props\n}: StepsContentProps) => {\n return (\n \n
\n
{bar ?? }
\n
{children}
\n
\n \n )\n}\n\nexport type StepsBarProps = React.HTMLAttributes\n\nexport const StepsBar = ({ className, ...props }: StepsBarProps) => (\n \n)\n\nexport type StepsProps = React.ComponentProps\n\nexport function Steps({ defaultOpen = true, className, ...props }: StepsProps) {\n return (\n \n )\n}\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "system-message", "type": "registry:ui", "title": "System Message", "description": "A banner-style component for surfacing contextual information, warnings, or instructions within AI interfaces.", "dependencies": [], "devDependencies": [], "registryDependencies": [ "button" ], "files": [ { "path": "components/prompt-kit/system-message.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { AlertCircle, AlertTriangle, Info } from \"lucide-react\"\nimport React from \"react\"\n\nconst systemMessageVariants = cva(\n \"flex flex-row items-center gap-3 rounded-[12px] border py-2 pr-2 pl-3\",\n {\n variants: {\n variant: {\n action: \"text-zinc-700 dark:text-zinc-300\",\n error: \"text-red-700 dark:text-red-800\",\n warning: \"text-amber-700 dark:text-amber-700\",\n },\n fill: {\n true: \"bg-background\",\n false: \"\",\n },\n },\n compoundVariants: [\n {\n variant: \"action\",\n fill: true,\n class: \"bg-zinc-100 dark:bg-zinc-900 border-transparent\",\n },\n {\n variant: \"error\",\n fill: true,\n class: \"bg-red-100 dark:bg-red-900/20 border-transparent\",\n },\n {\n variant: \"warning\",\n fill: true,\n class: \"bg-amber-100 dark:bg-amber-900/20 border-transparent\",\n },\n {\n variant: \"action\",\n fill: false,\n class: \"border-zinc-200 dark:border-zinc-800\",\n },\n {\n variant: \"error\",\n fill: false,\n class: \"border-red-600 dark:border-red-900\",\n },\n {\n variant: \"warning\",\n fill: false,\n class: \"border-amber-600 dark:border-amber-900\",\n },\n ],\n defaultVariants: {\n variant: \"action\",\n fill: false,\n },\n }\n)\n\nexport type SystemMessageProps = React.ComponentProps<\"div\"> &\n VariantProps & {\n icon?: React.ReactNode\n isIconHidden?: boolean\n cta?: {\n label: string\n onClick?: () => void\n variant?: \"solid\" | \"outline\" | \"ghost\"\n }\n }\n\nexport function SystemMessage({\n children,\n variant = \"action\",\n fill = false,\n icon,\n isIconHidden = false,\n cta,\n className,\n ...props\n}: SystemMessageProps) {\n const getDefaultIcon = () => {\n if (isIconHidden) return null\n\n switch (variant) {\n case \"error\":\n return \n case \"warning\":\n return \n default:\n return \n }\n }\n\n const getIconToShow = () => {\n if (isIconHidden) return null\n if (icon) return icon\n return getDefaultIcon()\n }\n\n const shouldShowIcon = getIconToShow() !== null\n\n return (\n \n
\n {shouldShowIcon && (\n
\n {getIconToShow()}\n
\n )}\n\n \n
{children}
\n
\n \n\n {cta && (\n \n )}\n \n )\n}\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "chain-of-thought", "type": "registry:ui", "title": "Chain Of Thought", "description": "A component for displaying a chain of thought process with collapsible steps and triggers.", "dependencies": [ "lucide-react" ], "devDependencies": [], "registryDependencies": [ "collapsible" ], "files": [ { "path": "components/prompt-kit/chain-of-thought.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/components/ui/collapsible\"\nimport { cn } from \"@/lib/utils\"\nimport { ChevronDown, Circle } from \"lucide-react\"\nimport React from \"react\"\n\nexport type ChainOfThoughtItemProps = React.ComponentProps<\"div\">\n\nexport const ChainOfThoughtItem = ({\n children,\n className,\n ...props\n}: ChainOfThoughtItemProps) => (\n
\n {children}\n
\n)\n\nexport type ChainOfThoughtTriggerProps = React.ComponentProps<\n typeof CollapsibleTrigger\n> & {\n leftIcon?: React.ReactNode\n swapIconOnHover?: boolean\n}\n\nexport const ChainOfThoughtTrigger = ({\n children,\n className,\n leftIcon,\n swapIconOnHover = true,\n ...props\n}: ChainOfThoughtTriggerProps) => (\n \n
\n {leftIcon ? (\n \n \n {leftIcon}\n \n {swapIconOnHover && (\n \n )}\n \n ) : (\n \n \n \n )}\n {children}\n
\n {!leftIcon && (\n \n )}\n \n)\n\nexport type ChainOfThoughtContentProps = React.ComponentProps<\n typeof CollapsibleContent\n>\n\nexport const ChainOfThoughtContent = ({\n children,\n className,\n ...props\n}: ChainOfThoughtContentProps) => {\n return (\n \n
\n
\n
\n
{children}
\n
\n \n )\n}\n\nexport type ChainOfThoughtProps = {\n children: React.ReactNode\n className?: string\n}\n\nexport function ChainOfThought({ children, className }: ChainOfThoughtProps) {\n const childrenArray = React.Children.toArray(children)\n\n return (\n
\n {childrenArray.map((child, index) => (\n \n {React.isValidElement(child) &&\n React.cloneElement(\n child as React.ReactElement,\n {\n isLast: index === childrenArray.length - 1,\n }\n )}\n \n ))}\n
\n )\n}\n\nexport type ChainOfThoughtStepProps = {\n children: React.ReactNode\n className?: string\n isLast?: boolean\n}\n\nexport const ChainOfThoughtStep = ({\n children,\n className,\n isLast = false,\n ...props\n}: ChainOfThoughtStepProps & React.ComponentProps) => {\n return (\n \n {children}\n
\n
\n
\n \n )\n}\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "text-shimmer", "type": "registry:ui", "title": "Text Shimmer", "description": "A component for displaying a shimmer effect on text, perfect for loading states or highlighting text.", "dependencies": [], "devDependencies": [], "registryDependencies": [], "tailwind": { "config": { "theme": { "keyframes": { "shimmer": { "0%": { "backgroundPosition": "200% 50%" }, "100%": { "backgroundPosition": "-200% 50%" } } } } } }, "files": [ { "path": "components/prompt-kit/text-shimmer.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type TextShimmerProps = {\n as?: string\n duration?: number\n spread?: number\n children: React.ReactNode\n} & React.HTMLAttributes\n\nexport function TextShimmer({\n as = \"span\",\n className,\n duration = 4,\n spread = 20,\n children,\n ...props\n}: TextShimmerProps) {\n const dynamicSpread = Math.min(Math.max(spread, 5), 45)\n const Component = as as React.ElementType\n\n return (\n \n {children}\n \n )\n}\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "thinking-bar", "type": "registry:ui", "title": "Thinking Bar", "description": "A component to display the thinking state of an AI model with optional actions.", "dependencies": [ "lucide-react" ], "devDependencies": [], "registryDependencies": [ "text-shimmer" ], "files": [ { "path": "components/prompt-kit/thinking-bar.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport { TextShimmer } from \"@/components/prompt-kit/text-shimmer\"\nimport { cn } from \"@/lib/utils\"\nimport { ChevronRight } from \"lucide-react\"\n\ntype ThinkingBarProps = {\n className?: string\n text?: string\n onStop?: () => void\n stopLabel?: string\n onClick?: () => void\n}\n\nexport function ThinkingBar({\n className,\n text = \"Thinking\",\n onStop,\n stopLabel = \"Answer now\",\n onClick,\n}: ThinkingBarProps) {\n return (\n
\n {onClick ? (\n \n {text}\n \n \n ) : (\n {text}\n )}\n {onStop ? (\n \n {stopLabel}\n \n ) : null}\n
\n )\n}\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "feedback-bar", "type": "registry:ui", "title": "Feedback Bar", "description": "A component to collect user feedback on AI responses.", "dependencies": [ "lucide-react" ], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/prompt-kit/feedback-bar.tsx", "type": "registry:component", "content": "import { cn } from \"@/lib/utils\"\nimport { ThumbsDown, ThumbsUp, X } from \"lucide-react\"\n\ntype FeedbackBarProps = {\n className?: string\n title?: string\n icon?: React.ReactNode\n onHelpful?: () => void\n onNotHelpful?: () => void\n onClose?: () => void\n}\n\nexport function FeedbackBar({\n className,\n title,\n icon,\n onHelpful,\n onNotHelpful,\n onClose,\n}: FeedbackBarProps) {\n return (\n \n
\n
\n {icon}\n {title}\n
\n
\n \n \n \n \n \n \n
\n
\n \n \n \n
\n
\n
\n )\n}\n" } ], "categories": [ "ai", "prompt-kit" ] }, { "name": "chatbot", "type": "registry:item", "title": "Chatbot", "description": "A chatbot component that allows users to chat with an AI model. It uses prompt-kit, shadcn/ui, and AI SDK V5.", "dependencies": [ "ai", "@ai-sdk/openai", "zod", "@ai-sdk/react", "use-stick-to-bottom", "react-markdown", "remark-gfm", "shiki", "marked", "remark-breaks" ], "devDependencies": [], "registryDependencies": [ "avatar", "tooltip", "textarea" ], "files": [ { "path": "components/primitives/chatbot.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport {\n ChatContainerContent,\n ChatContainerRoot,\n} from \"@/components/prompt-kit/chat-container\"\nimport { DotsLoader } from \"@/components/prompt-kit/loader\"\nimport {\n Message,\n MessageAction,\n MessageActions,\n MessageContent,\n} from \"@/components/prompt-kit/message\"\nimport {\n PromptInput,\n PromptInputActions,\n PromptInputTextarea,\n} from \"@/components/prompt-kit/prompt-input\"\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport { useChat } from \"@ai-sdk/react\"\nimport { DefaultChatTransport } from \"ai\"\nimport type { UIMessage } from \"ai\"\nimport {\n AlertTriangle,\n ArrowUp,\n Copy,\n ThumbsDown,\n ThumbsUp,\n} from \"lucide-react\"\nimport { memo, useState } from \"react\"\n\ntype MessageComponentProps = {\n message: UIMessage\n isLastMessage: boolean\n}\n\nexport const MessageComponent = memo(\n ({ message, isLastMessage }: MessageComponentProps) => {\n const isAssistant = message.role === \"assistant\"\n\n return (\n \n {isAssistant ? (\n
\n \n {message.parts\n .map((part) => (part.type === \"text\" ? part.text : null))\n .join(\"\")}\n \n \n \n \n \n \n \n \n \n \n \n \n
\n ) : (\n
\n \n {message.parts\n .map((part) => (part.type === \"text\" ? part.text : null))\n .join(\"\")}\n \n \n \n \n \n \n
\n )}\n \n )\n }\n)\n\nMessageComponent.displayName = \"MessageComponent\"\n\nconst LoadingMessage = memo(() => (\n \n
\n
\n \n
\n
\n
\n))\n\nLoadingMessage.displayName = \"LoadingMessage\"\n\nconst ErrorMessage = memo(({ error }: { error: Error }) => (\n \n
\n
\n \n

{error.message}

\n
\n
\n
\n))\n\nErrorMessage.displayName = \"ErrorMessage\"\n\nfunction ConversationPromptInput() {\n const [input, setInput] = useState(\"\")\n\n const { messages, sendMessage, status, error } = useChat({\n transport: new DefaultChatTransport({\n api: \"/api/primitives/chatbot\",\n }),\n })\n\n const handleSubmit = () => {\n if (!input.trim()) return\n\n sendMessage({ text: input })\n setInput(\"\")\n }\n\n return (\n
\n \n \n {messages.map((message, index) => {\n const isLastMessage = index === messages.length - 1\n\n return (\n \n )\n })}\n\n {status === \"submitted\" && }\n {status === \"error\" && error && }\n \n \n
\n \n
\n \n\n \n
\n
\n \n {status === \"ready\" || status === \"error\" ? (\n \n ) : (\n \n )}\n \n
\n \n
\n \n
\n
\n )\n}\n\nexport default ConversationPromptInput\n" }, { "path": "app/api/primitives/chatbot/route.ts", "type": "registry:file", "content": "import { openai } from \"@ai-sdk/openai\"\nimport { convertToModelMessages, streamText, tool, UIMessage } from \"ai\"\nimport { z } from \"zod\"\n\nexport const maxDuration = 30\n\nexport async function POST(req: Request) {\n const { messages }: { messages: UIMessage[] } = await req.json()\n\n const result = streamText({\n model: openai(\"gpt-4.1-nano\"),\n system:\n \"You are a helpful assistant with access to tools. Use the getCurrentDate tool when users ask about dates, time, or current information. You are also able to use the getTime tool to get the current time in a specific timezone.\",\n messages: convertToModelMessages(messages),\n tools: {\n getTime: tool({\n description: \"Get the current time in a specific timezone\",\n inputSchema: z.object({\n timezone: z\n .string()\n .describe(\"A valid IANA timezone, e.g. 'Europe/Paris'\"),\n }),\n execute: async ({ timezone }) => {\n try {\n const now = new Date()\n const time = now.toLocaleString(\"en-US\", {\n timeZone: timezone,\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n })\n\n return { time, timezone }\n } catch {\n return { error: \"Invalid timezone format.\" }\n }\n },\n }),\n getCurrentDate: tool({\n description: \"Get the current date and time with timezone information\",\n inputSchema: z.object({}),\n execute: async () => {\n const now = new Date()\n return {\n timestamp: now.getTime(),\n iso: now.toISOString(),\n local: now.toLocaleString(\"en-US\", {\n weekday: \"long\",\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n timeZoneName: \"short\",\n }),\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n utc: now.toUTCString(),\n }\n },\n }),\n },\n })\n\n return result.toUIMessageStreamResponse()\n}\n", "target": "app/api/primitives/chatbot/route.ts" }, { "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" }, { "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" }, { "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/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 useLayoutEffect,\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 textareaRef: React.RefObject\n}\n\nconst PromptInputContext = createContext({\n isLoading: false,\n value: \"\",\n setValue: () => {},\n maxHeight: 240,\n onSubmit: undefined,\n disabled: false,\n textareaRef: React.createRef(),\n})\n\nfunction usePromptInput() {\n return useContext(PromptInputContext)\n}\n\nexport type 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 disabled?: boolean\n} & React.ComponentProps<\"div\">\n\nfunction PromptInput({\n className,\n isLoading = false,\n maxHeight = 240,\n value,\n onValueChange,\n onSubmit,\n children,\n disabled = false,\n onClick,\n ...props\n}: PromptInputProps) {\n const [internalValue, setInternalValue] = useState(value || \"\")\n const textareaRef = useRef(null)\n\n const handleChange = (newValue: string) => {\n setInternalValue(newValue)\n onValueChange?.(newValue)\n }\n\n const handleClick: React.MouseEventHandler = (e) => {\n if (!disabled) textareaRef.current?.focus()\n onClick?.(e)\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, textareaRef } =\n usePromptInput()\n\n const adjustHeight = (el: HTMLTextAreaElement | null) => {\n if (!el || disableAutosize) return\n\n el.style.height = \"auto\"\n\n if (typeof maxHeight === \"number\") {\n el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`\n } else {\n el.style.height = `min(${el.scrollHeight}px, ${maxHeight})`\n }\n }\n\n const handleRef = (el: HTMLTextAreaElement | null) => {\n textareaRef.current = el\n adjustHeight(el)\n }\n\n useLayoutEffect(() => {\n if (!textareaRef.current || disableAutosize) return\n\n const el = textareaRef.current\n el.style.height = \"auto\"\n\n if (typeof maxHeight === \"number\") {\n el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`\n } else {\n el.style.height = `min(${el.scrollHeight}px, ${maxHeight})`\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [value, maxHeight, disableAutosize])\n\n const handleChange = (e: React.ChangeEvent) => {\n adjustHeight(e.target)\n setValue(e.target.value)\n }\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 \n )\n}\n\nexport type PromptInputActionsProps = React.HTMLAttributes\n\nfunction PromptInputActions({\n children,\n className,\n ...props\n}: PromptInputActionsProps) {\n return (\n
\n {children}\n
\n )\n}\n\nexport type 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 event.stopPropagation()}\n >\n {children}\n \n \n {tooltip}\n \n \n )\n}\n\nexport {\n PromptInput,\n PromptInputTextarea,\n PromptInputActions,\n PromptInputAction,\n}\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 \"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, theme })\n setHighlightedHtml(html)\n }\n highlight()\n }, [code, language, theme])\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" } ], "envVars": { "OPENAI_API_KEY": "" }, "categories": [ "ai", "prompt-kit" ] }, { "name": "tool-calling", "type": "registry:item", "title": "Tool calling", "description": "A chatbot with tool calling feature. It uses prompt-kit, shadcn/ui, and AI SDK V5.", "dependencies": [ "ai", "@ai-sdk/openai", "zod", "@ai-sdk/react", "use-stick-to-bottom", "react-markdown", "remark-gfm", "shiki", "marked", "remark-breaks" ], "devDependencies": [], "registryDependencies": [ "avatar", "tooltip", "textarea", "collapsible", "button" ], "files": [ { "path": "components/primitives/tool-calling.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport {\n ChatContainerContent,\n ChatContainerRoot,\n} from \"@/components/prompt-kit/chat-container\"\nimport { DotsLoader } from \"@/components/prompt-kit/loader\"\nimport {\n Message,\n MessageAction,\n MessageActions,\n MessageContent,\n} from \"@/components/prompt-kit/message\"\nimport {\n PromptInput,\n PromptInputActions,\n PromptInputTextarea,\n} from \"@/components/prompt-kit/prompt-input\"\nimport { Tool } from \"@/components/prompt-kit/tool\"\nimport type { ToolPart } from \"@/components/prompt-kit/tool\"\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport { useChat } from \"@ai-sdk/react\"\nimport { DefaultChatTransport } from \"ai\"\nimport type { UIMessage, UIMessagePart } from \"ai\"\nimport {\n AlertTriangle,\n ArrowUp,\n Copy,\n ThumbsDown,\n ThumbsUp,\n} from \"lucide-react\"\nimport { memo, useState } from \"react\"\n\ntype MessageComponentProps = {\n message: UIMessage\n isLastMessage: boolean\n}\n\nconst renderToolPart = (\n part: UIMessagePart,\n index: number\n): React.ReactNode => {\n if (!part.type?.startsWith(\"tool-\")) return null\n\n return \n}\n\nexport const MessageComponent = memo(\n ({ message, isLastMessage }: MessageComponentProps) => {\n const isAssistant = message?.role === \"assistant\"\n\n return (\n \n {isAssistant ? (\n
\n
\n {message?.parts\n .filter(\n (part: any) => part.type && part.type.startsWith(\"tool-\")\n )\n .map((part: any, index: number) => renderToolPart(part, index))}\n
\n \n {message?.parts\n .filter((part: any) => part.type === \"text\")\n .map((part: any) => part.text)\n .join(\"\")}\n \n\n \n \n \n \n \n \n \n \n \n \n \n
\n ) : (\n
\n \n {message?.parts\n .map((part: any) => (part.type === \"text\" ? part.text : null))\n .join(\"\")}\n \n \n \n \n \n \n
\n )}\n \n )\n }\n)\n\nMessageComponent.displayName = \"MessageComponent\"\n\nconst LoadingMessage = memo(() => (\n \n
\n
\n \n
\n
\n
\n))\n\nLoadingMessage.displayName = \"LoadingMessage\"\n\nconst ErrorMessage = memo(({ error }: { error: Error }) => (\n \n
\n
\n \n

{error.message}

\n
\n
\n
\n))\n\nErrorMessage.displayName = \"ErrorMessage\"\n\nfunction ToolCallingChatbot() {\n const [input, setInput] = useState(\"\")\n\n const { messages, sendMessage, status, error } = useChat({\n transport: new DefaultChatTransport({\n api: \"/api/primitives/tool-calling\",\n }),\n })\n\n const handleSubmit = () => {\n if (!input.trim()) return\n\n sendMessage({ text: input })\n setInput(\"\")\n }\n\n return (\n
\n \n \n {messages.length === 0 && (\n
\n
\n Try asking:\n
\n
    \n
  • what's the current date?
  • \n
  • what time is it in Tokyo?
  • \n
  • give me the current time in Europe/Paris
  • \n
\n
\n )}\n\n {messages?.map((message, index) => {\n const isLastMessage = index === messages.length - 1\n\n return (\n \n )\n })}\n\n {status === \"submitted\" && }\n {status === \"error\" && error && }\n
\n
\n\n
\n \n
\n \n\n \n
\n
\n \n {status === \"ready\" || status === \"error\" ? (\n \n ) : (\n \n )}\n \n
\n \n
\n \n
\n
\n )\n}\n\nexport default ToolCallingChatbot\n" }, { "path": "app/api/primitives/tool-calling/route.ts", "type": "registry:file", "content": "import { openai } from \"@ai-sdk/openai\"\nimport {\n convertToModelMessages,\n stepCountIs,\n streamText,\n tool,\n UIMessage,\n} from \"ai\"\nimport { z } from \"zod\"\n\nexport const maxDuration = 30\n\nexport async function POST(req: Request) {\n const { messages }: { messages: UIMessage[] } = await req.json()\n\n const result = streamText({\n model: openai(\"gpt-4.1-nano\"),\n system:\n \"You are a helpful assistant with access to tools. Use the getCurrentDate tool when users ask about dates, time, or current information. You are also able to use the getTime tool to get the current time in a specific timezone.\",\n messages: convertToModelMessages(messages),\n stopWhen: stepCountIs(5),\n tools: {\n getTime: tool({\n description: \"Get the current time in a specific timezone\",\n inputSchema: z.object({\n timezone: z\n .string()\n .describe(\"A valid IANA timezone, e.g. 'Europe/Paris'\"),\n }),\n execute: async ({ timezone }) => {\n try {\n const now = new Date()\n const time = now.toLocaleString(\"en-US\", {\n timeZone: timezone,\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n })\n\n return { time, timezone }\n } catch {\n return { error: \"Invalid timezone format.\" }\n }\n },\n }),\n getCurrentDate: tool({\n description: \"Get the current date and time with timezone information\",\n inputSchema: z.object({}),\n execute: async () => {\n const now = new Date()\n return {\n timestamp: now.getTime(),\n iso: now.toISOString(),\n local: now.toLocaleString(\"en-US\", {\n weekday: \"long\",\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n timeZoneName: \"short\",\n }),\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n utc: now.toUTCString(),\n }\n },\n }),\n },\n })\n\n return result.toUIMessageStreamResponse()\n}\n", "target": "app/api/primitives/tool-calling/route.ts" }, { "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" }, { "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" }, { "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/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 useLayoutEffect,\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 textareaRef: React.RefObject\n}\n\nconst PromptInputContext = createContext({\n isLoading: false,\n value: \"\",\n setValue: () => {},\n maxHeight: 240,\n onSubmit: undefined,\n disabled: false,\n textareaRef: React.createRef(),\n})\n\nfunction usePromptInput() {\n return useContext(PromptInputContext)\n}\n\nexport type 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 disabled?: boolean\n} & React.ComponentProps<\"div\">\n\nfunction PromptInput({\n className,\n isLoading = false,\n maxHeight = 240,\n value,\n onValueChange,\n onSubmit,\n children,\n disabled = false,\n onClick,\n ...props\n}: PromptInputProps) {\n const [internalValue, setInternalValue] = useState(value || \"\")\n const textareaRef = useRef(null)\n\n const handleChange = (newValue: string) => {\n setInternalValue(newValue)\n onValueChange?.(newValue)\n }\n\n const handleClick: React.MouseEventHandler = (e) => {\n if (!disabled) textareaRef.current?.focus()\n onClick?.(e)\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, textareaRef } =\n usePromptInput()\n\n const adjustHeight = (el: HTMLTextAreaElement | null) => {\n if (!el || disableAutosize) return\n\n el.style.height = \"auto\"\n\n if (typeof maxHeight === \"number\") {\n el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`\n } else {\n el.style.height = `min(${el.scrollHeight}px, ${maxHeight})`\n }\n }\n\n const handleRef = (el: HTMLTextAreaElement | null) => {\n textareaRef.current = el\n adjustHeight(el)\n }\n\n useLayoutEffect(() => {\n if (!textareaRef.current || disableAutosize) return\n\n const el = textareaRef.current\n el.style.height = \"auto\"\n\n if (typeof maxHeight === \"number\") {\n el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`\n } else {\n el.style.height = `min(${el.scrollHeight}px, ${maxHeight})`\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [value, maxHeight, disableAutosize])\n\n const handleChange = (e: React.ChangeEvent) => {\n adjustHeight(e.target)\n setValue(e.target.value)\n }\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 \n )\n}\n\nexport type PromptInputActionsProps = React.HTMLAttributes\n\nfunction PromptInputActions({\n children,\n className,\n ...props\n}: PromptInputActionsProps) {\n return (\n
\n {children}\n
\n )\n}\n\nexport type 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 event.stopPropagation()}\n >\n {children}\n \n \n {tooltip}\n \n \n )\n}\n\nexport {\n PromptInput,\n PromptInputTextarea,\n PromptInputActions,\n PromptInputAction,\n}\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 \"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, theme })\n setHighlightedHtml(html)\n }\n highlight()\n }, [code, language, theme])\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" }, { "path": "components/prompt-kit/tool.tsx", "type": "registry:component", "content": "\"use client\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/components/ui/collapsible\"\nimport { cn } from \"@/lib/utils\"\nimport {\n CheckCircle,\n ChevronDown,\n Loader2,\n Settings,\n XCircle,\n} from \"lucide-react\"\nimport { useState } from \"react\"\n\nexport type ToolPart = {\n type: string\n state:\n | \"input-streaming\"\n | \"input-available\"\n | \"output-available\"\n | \"output-error\"\n input?: Record\n output?: Record\n toolCallId?: string\n errorText?: string\n}\n\nexport type ToolProps = {\n toolPart: ToolPart\n defaultOpen?: boolean\n className?: string\n}\n\nconst Tool = ({ toolPart, defaultOpen = false, className }: ToolProps) => {\n const [isOpen, setIsOpen] = useState(defaultOpen)\n\n const { state, input, output, toolCallId } = toolPart\n\n const getStateIcon = () => {\n switch (state) {\n case \"input-streaming\":\n return \n case \"input-available\":\n return \n case \"output-available\":\n return \n case \"output-error\":\n return \n default:\n return \n }\n }\n\n const getStateBadge = () => {\n const baseClasses = \"px-2 py-1 rounded-full text-xs font-medium\"\n switch (state) {\n case \"input-streaming\":\n return (\n \n Processing\n \n )\n case \"input-available\":\n return (\n \n Ready\n \n )\n case \"output-available\":\n return (\n \n Completed\n \n )\n case \"output-error\":\n return (\n \n Error\n \n )\n default:\n return (\n \n Pending\n \n )\n }\n }\n\n const formatValue = (value: unknown): string => {\n if (value === null) return \"null\"\n if (value === undefined) return \"undefined\"\n if (typeof value === \"string\") return value\n if (typeof value === \"object\") {\n return JSON.stringify(value, null, 2)\n }\n return String(value)\n }\n\n return (\n \n \n \n \n
\n {getStateIcon()}\n \n {toolPart.type}\n \n {getStateBadge()}\n
\n \n \n
\n \n
\n {input && Object.keys(input).length > 0 && (\n
\n

\n Input\n

\n
\n {Object.entries(input).map(([key, value]) => (\n
\n {key}:{\" \"}\n {formatValue(value)}\n
\n ))}\n
\n
\n )}\n\n {output && (\n
\n

\n Output\n

\n
\n
\n                    {formatValue(output)}\n                  
\n
\n
\n )}\n\n {state === \"output-error\" && toolPart.errorText && (\n
\n

Error

\n
\n {toolPart.errorText}\n
\n
\n )}\n\n {state === \"input-streaming\" && (\n
\n Processing tool call...\n
\n )}\n\n {toolCallId && (\n
\n Call ID: {toolCallId}\n
\n )}\n
\n \n
\n \n )\n}\n\nexport { Tool }\n" } ], "envVars": { "OPENAI_API_KEY": "" }, "categories": [ "ai", "prompt-kit" ] } ] }