"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: Message[]) => 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: { type: string; toolInvocation?: { state: string; toolName: string }; }) => { 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; }