Shadcn Chatbot Kit
uichatsource: ok · hit
Chat
npx shadcn add "https://shadcn-chatbot-kit.vercel.app/r/chat.json"free
Where it goes
chat
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightRegistry dependencies
buttonhttps://shadcn-chatbot-kit.vercel.app/r/use-auto-scroll.jsonhttps://shadcn-chatbot-kit.vercel.app/r/chat-message.jsonhttps://shadcn-chatbot-kit.vercel.app/r/message-input.jsonhttps://shadcn-chatbot-kit.vercel.app/r/message-list.jsonhttps://shadcn-chatbot-kit.vercel.app/r/prompt-suggestions.json
Files1 · source cached 10/14/2026, 11:43:14 AM
ui/chat.tsx
"use client"
import {
forwardRef,
useCallback,
useRef,
useState,
type ReactElement,
} from "react"
import { ArrowDown, ThumbsDown, ThumbsUp } from "lucide-react"
import { cn } from "@/lib/utils"
import { useAutoScroll } from "@/registry/default/hooks/use-auto-scroll"
import { Button } from "@/registry/default/ui/button"
import { type Message } from "@/registry/default/ui/chat-message"
import { CopyButton } from "@/registry/default/ui/copy-button"
import { MessageInput } from "@/registry/default/ui/message-input"
import { MessageList } from "@/registry/default/ui/message-list"
import { PromptSuggestions } from "@/registry/default/ui/prompt-suggestions"
interface ChatPropsBase {
handleSubmit: (
event?: { preventDefault?: () => void },
options?: { experimental_attachments?: FileList }
) => void
messages: Array<Message>
input: string
className?: string
handleInputChange: React.ChangeEventHandler<HTMLTextAreaElement>
isGenerating: boolean
stop?: () => void
onRateResponse?: (
messageId: string,
rating: "thumbs-up" | "thumbs-down"
) => void
setMessages?: (messages: any[]) => void
transcribeAudio?: (blob: Blob) => Promise<string>
}
interface ChatPropsWithoutSuggestions extends ChatPropsBase {
append?: never
suggestions?: never
}
interface ChatPropsWithSuggestions extends ChatPropsBase {
append: (message: { role: "user"; content: string }) => void
suggestions: string[]
}
type ChatProps = ChatPropsWithoutSuggestions | ChatPropsWithSuggestions
export function Chat({
messages,
handleSubmit,
input,
handleInputChange,
stop,
isGenerating,
append,
suggestions,
className,
onRateResponse,
setMessages,
transcribeAudio,
}: ChatProps) {
const lastMessage = messages.at(-1)
const isEmpty = messages.length === 0
const isTyping = lastMessage?.role === "user"
const messagesRef = useRef(messages)
messagesRef.current = messages
// Enhanced stop function that marks pending tool calls as cancelled
const handleStop = useCallback(() => {
stop?.()
if (!setMessages) return
const latestMessages = [...messagesRef.current]
const lastAssistantMessage = latestMessages.findLast(
(m) => m.role === "assistant"
)
if (!lastAssistantMessage) return
let needsUpdate = false
let updatedMessage = { ...lastAssistantMessage }
if (lastAssistantMessage.toolInvocations) {
const updatedToolInvocations = lastAssistantMessage.toolInvocations.map(
(toolInvocation) => {
if (toolInvocation.state === "call") {
needsUpdate = true
return {
...toolInvocation,
state: "result",
result: {
content: "Tool execution was cancelled",
__cancelled: true, // Special marker to indicate cancellation
},
} as const
}
return toolInvocation
}
)
if (needsUpdate) {
updatedMessage = {
...updatedMessage,
toolInvocations: updatedToolInvocations,
}
}
}
if (lastAssistantMessage.parts && lastAssistantMessage.parts.length > 0) {
const updatedParts = lastAssistantMessage.parts.map((part: any) => {
if (
part.type === "tool-invocation" &&
part.toolInvocation &&
part.toolInvocation.state === "call"
) {
needsUpdate = true
return {
...part,
toolInvocation: {
...part.toolInvocation,
state: "result",
result: {
content: "Tool execution was cancelled",
__cancelled: true,
},
},
}
}
return part
})
if (needsUpdate) {
updatedMessage = {
...updatedMessage,
parts: updatedParts,
}
}
}
if (needsUpdate) {
const messageIndex = latestMessages.findIndex(
(m) => m.id === lastAssistantMessage.id
)
if (messageIndex !== -1) {
latestMessages[messageIndex] = updatedMessage
setMessages(latestMessages)
}
}
}, [stop, setMessages, messagesRef])
const messageOptions = useCallback(
(message: Message) => ({
actions: onRateResponse ? (
<>
<div className="border-r pr-1">
<CopyButton
content={message.content}
copyMessage="Copied response to clipboard!"
/>
</div>
<Button
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={() => onRateResponse(message.id, "thumbs-up")}
>
<ThumbsUp className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={() => onRateResponse(message.id, "thumbs-down")}
>
<ThumbsDown className="h-4 w-4" />
</Button>
</>
) : (
<CopyButton
content={message.content}
copyMessage="Copied response to clipboard!"
/>
),
}),
[onRateResponse]
)
return (
<ChatContainer className={className}>
{isEmpty && append && suggestions ? (
<PromptSuggestions
label="Try these prompts ✨"
append={append}
suggestions={suggestions}
/>
) : null}
{messages.length > 0 ? (
<ChatMessages messages={messages}>
<MessageList
messages={messages}
isTyping={isTyping}
messageOptions={messageOptions}
/>
</ChatMessages>
) : null}
<ChatForm
className="mt-auto"
isPending={isGenerating || isTyping}
handleSubmit={handleSubmit}
>
{({ files, setFiles }) => (
<MessageInput
value={input}
onChange={handleInputChange}
allowAttachments
files={files}
setFiles={setFiles}
stop={handleStop}
isGenerating={isGenerating}
transcribeAudio={transcribeAudio}
/>
)}
</ChatForm>
</ChatContainer>
)
}
Chat.displayName = "Chat"
export function ChatMessages({
messages,
children,
}: React.PropsWithChildren<{
messages: Message[]
}>) {
const {
containerRef,
scrollToBottom,
handleScroll,
shouldAutoScroll,
handleTouchStart,
} = useAutoScroll([messages])
return (
<div
className="grid grid-cols-1 overflow-y-auto pb-4"
ref={containerRef}
onScroll={handleScroll}
onTouchStart={handleTouchStart}
>
<div className="max-w-full [grid-column:1/1] [grid-row:1/1]">
{children}
</div>
{!shouldAutoScroll && (
<div className="pointer-events-none flex flex-1 items-end justify-end [grid-column:1/1] [grid-row:1/1]">
<div className="sticky bottom-0 left-0 flex w-full justify-end">
<Button
onClick={scrollToBottom}
className="pointer-events-auto h-8 w-8 rounded-full ease-in-out animate-in fade-in-0 slide-in-from-bottom-1"
size="icon"
variant="ghost"
>
<ArrowDown className="h-4 w-4" />
</Button>
</div>
</div>
)}
</div>
)
}
export const ChatContainer = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
className={cn("grid max-h-full w-full grid-rows-[1fr_auto]", className)}
{...props}
/>
)
})
ChatContainer.displayName = "ChatContainer"
interface ChatFormProps {
className?: string
isPending: boolean
handleSubmit: (
event?: { preventDefault?: () => void },
options?: { experimental_attachments?: FileList }
) => void
children: (props: {
files: File[] | null
setFiles: React.Dispatch<React.SetStateAction<File[] | null>>
}) => ReactElement
}
export const ChatForm = forwardRef<HTMLFormElement, ChatFormProps>(
({ children, handleSubmit, isPending, className }, ref) => {
const [files, setFiles] = useState<File[] | null>(null)
const onSubmit = (event: React.FormEvent) => {
if (!files) {
handleSubmit(event)
return
}
const fileList = createFileList(files)
handleSubmit(event, { experimental_attachments: fileList })
setFiles(null)
}
return (
<form ref={ref} onSubmit={onSubmit} className={className}>
{children({ files, setFiles })}
</form>
)
}
)
ChatForm.displayName = "ChatForm"
function createFileList(files: File[] | FileList): FileList {
const dataTransfer = new DataTransfer()
for (const file of Array.from(files)) {
dataTransfer.items.add(file)
}
return dataTransfer.files
}