"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 "@/hooks/use-auto-scroll" import { Button } from "@/components/ui/button" import { type Message } from "@/components/chat-playground/chat-message" import { CopyButton } from "@/components/ui/copy-button" import { MessageInput } from "@/components/chat-playground/message-input" import { MessageList } from "@/components/chat-playground/message-list" import { PromptSuggestions } from "@/components/chat-playground/prompt-suggestions" interface ChatPropsBase { handleSubmit: ( event?: { preventDefault?: () => void }, options?: { experimental_attachments?: FileList } ) => void messages: Array input: string className?: string handleInputChange: React.ChangeEventHandler isGenerating: boolean stop?: () => void onRateResponse?: ( messageId: string, rating: "thumbs-up" | "thumbs-down" ) => void setMessages?: (messages: any[]) => void transcribeAudio?: (blob: Blob) => Promise } 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 ? ( <>
) : ( ), }), [onRateResponse] ) return (
{isEmpty && append && suggestions ? (
) : null} {messages.length > 0 ? ( ) : null}
{({ files, setFiles }) => ( )}
) } Chat.displayName = "Chat" export function ChatMessages({ messages, children, }: React.PropsWithChildren<{ messages: Message[] }>) { const { containerRef, scrollToBottom, handleScroll, shouldAutoScroll, handleTouchStart, } = useAutoScroll([messages]) return (
{children}
{!shouldAutoScroll && (
)}
) } export const ChatContainer = forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => { return (
) }) 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> }) => ReactElement } export const ChatForm = forwardRef( ({ children, handleSubmit, isPending, className }, ref) => { const [files, setFiles] = useState(null) const onSubmit = (event: React.FormEvent) => { // if (isPending) { // event.preventDefault() // return // } if (!files) { handleSubmit(event) return } const fileList = createFileList(files) handleSubmit(event, { experimental_attachments: fileList }) setFiles(null) } return (
{children({ files, setFiles })}
) } ) 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 }