From a2a1b065a2fffe2cc2b1e3e670bcdf5be3427801 Mon Sep 17 00:00:00 2001 From: ishaanxgupta <124028055+ishaanxgupta@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:55:57 +0000 Subject: [PATCH] expand chat file drop zone (#1085) ## What changed - Added a chat-shell-level file drag/drop target so the whole chat area, including the conversation area, shows the drop overlay and accepts files. - Disabled the nested composer drop handler for the sidebar chat path to avoid duplicate drops or stuck overlay state. - Kept the existing local input drop behavior available for other ChatInput usages like the home composer. --- apps/web/components/chat/index.tsx | 103 ++++++++++++++++++++++- apps/web/components/chat/input/index.tsx | 31 ++++--- 2 files changed, 120 insertions(+), 14 deletions(-) diff --git a/apps/web/components/chat/index.tsx b/apps/web/components/chat/index.tsx index a19daa1e..a2d8e305 100644 --- a/apps/web/components/chat/index.tsx +++ b/apps/web/components/chat/index.tsx @@ -201,6 +201,7 @@ export function ChatSidebar({ const [attachmentDrafts, setAttachmentDrafts] = useState< ChatAttachmentDraft[] >([]) + const [isChatDraggingFiles, setIsChatDraggingFiles] = useState(false) const [selectedModel, setSelectedModel] = useState( initialSelectedModel ?? "grok-4.3", ) @@ -246,6 +247,7 @@ export function ChatSidebar({ null, ) const messagesContainerRef = useRef(null) + const chatDragDepthRef = useRef(0) const isScrolledToBottomRef = useRef(true) const userJustSentRef = useRef(false) const sentQueuedMessageRef = useRef(null) @@ -725,6 +727,77 @@ export function ChatSidebar({ [attachmentDrafts, currentChatId, uploadAttachmentDraft], ) + const hasDraggedFiles = useCallback( + (event: React.DragEvent) => + Array.from(event.dataTransfer.types).includes("Files"), + [], + ) + + const resetChatFileDrag = useCallback(() => { + chatDragDepthRef.current = 0 + setIsChatDraggingFiles(false) + }, []) + + const handleChatDragEnter = useCallback( + (event: React.DragEvent) => { + if (!hasDraggedFiles(event)) return + event.preventDefault() + event.stopPropagation() + + chatDragDepthRef.current += 1 + if (status !== "submitted" && status !== "streaming") { + setIsChatDraggingFiles(true) + } + }, + [hasDraggedFiles, status], + ) + + const handleChatDragOver = useCallback( + (event: React.DragEvent) => { + if (!hasDraggedFiles(event)) return + event.preventDefault() + event.stopPropagation() + event.dataTransfer.dropEffect = + status === "submitted" || status === "streaming" ? "none" : "copy" + }, + [hasDraggedFiles, status], + ) + + const handleChatDragLeave = useCallback( + (event: React.DragEvent) => { + if (!hasDraggedFiles(event)) return + event.preventDefault() + event.stopPropagation() + + chatDragDepthRef.current = Math.max(0, chatDragDepthRef.current - 1) + if (chatDragDepthRef.current === 0) { + setIsChatDraggingFiles(false) + } + }, + [hasDraggedFiles], + ) + + const handleChatDrop = useCallback( + (event: React.DragEvent) => { + if (!hasDraggedFiles(event)) return + event.preventDefault() + event.stopPropagation() + + resetChatFileDrag() + const files = event.dataTransfer.files + if (status !== "submitted" && status !== "streaming" && files.length) { + handleAddAttachmentFiles(files) + } + }, + [handleAddAttachmentFiles, hasDraggedFiles, resetChatFileDrag, status], + ) + + useEffect(() => { + if (status === "submitted" || status === "streaming") { + resetChatFileDrag() + } + }, [resetChatFileDrag, status]) + useEffect(() => { if (pendingThreadLoad && currentChatId === pendingThreadLoad.id) { setMessages(pendingThreadLoad.messages) @@ -1765,6 +1838,27 @@ export function ChatSidebar({ ) : null + const chatDropOverlay = isChatDraggingFiles ? ( + + ) : null + const chatDropTargetProps = { + onDragEnter: handleChatDragEnter, + onDragOver: handleChatDragOver, + onDragLeave: handleChatDragLeave, + onDrop: handleChatDrop, + onDragEnd: resetChatFileDrag, + } + const shell = ( <> {showHeaderRow ? ( @@ -1983,6 +2077,7 @@ export function ChatSidebar({ onRetryAttachment={handleRetryAttachment} canSend={canSendMessage} attachmentAccept={CHAT_ATTACHMENT_ACCEPT} + disableFileDropZone sendDisabled={isResponding && isQueueFull} sendDisabledTooltip={`Queue is full (${CHAT_QUEUE_LIMIT} max)`} activeStatus={ @@ -2063,7 +2158,9 @@ export function ChatSidebar({ layout === "page" ? { opacity: 0, y: 12 } : { x: "100px", opacity: 0 } } transition={{ duration: 0.3, ease: "easeOut", bounce: 0 }} + {...(!isPageDesktop ? chatDropTargetProps : {})} > + {!isPageDesktop && chatDropOverlay} {chatHistorySheet} {isPageDesktop ? (
@@ -2073,7 +2170,11 @@ export function ChatSidebar({ chatProject === AUTO_CHAT_SPACE_ID ? null : [chatProject] } /> -
+
+ {chatDropOverlay} {pageDesktopToolbarRow}
{shell} diff --git a/apps/web/components/chat/input/index.tsx b/apps/web/components/chat/input/index.tsx index d655d657..0926af53 100644 --- a/apps/web/components/chat/input/index.tsx +++ b/apps/web/components/chat/input/index.tsx @@ -59,6 +59,7 @@ interface ChatInputProps { onRetryAttachment?: (id: string) => void canSend?: boolean attachmentAccept?: string + disableFileDropZone?: boolean } export default function ChatInput({ @@ -84,6 +85,7 @@ export default function ChatInput({ onRetryAttachment, canSend, attachmentAccept = CHAT_ATTACHMENT_ACCEPT, + disableFileDropZone = false, }: ChatInputProps) { const [isMultiline, setIsMultiline] = useState(false) const [isExpanded, setIsExpanded] = useState(false) @@ -200,11 +202,20 @@ export default function ChatInput({ ) : null - const dropOverlay = isDraggingFiles ? ( -
- Drop files to attach -
- ) : null + const dropOverlay = + !disableFileDropZone && isDraggingFiles ? ( +
+ Drop files to attach +
+ ) : null + const dropZoneProps = disableFileDropZone + ? {} + : { + onDragEnter: handleDragEnter, + onDragOver: handleDragOver, + onDragLeave: handleDragLeave, + onDrop: handleDrop, + } return ( {dropOverlay} @@ -368,10 +376,7 @@ export default function ChatInput({ ) : (