diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index 43819cc25..8476aca77 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -332,6 +332,41 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { } } + const handlePaste = (e: React.ClipboardEvent) => { + const clipboardItems = e.clipboardData?.items + if (!clipboardItems) return + + const imageItems = Array.from(clipboardItems).filter(item => + item.type.startsWith('image/') + ) + + if (imageItems.length > 0) { + e.preventDefault() + + const files: File[] = [] + let processedCount = 0 + + imageItems.forEach((item) => { + const file = item.getAsFile() + if (file) { + files.push(file) + processedCount++ + + // When all files are collected, process them + if (processedCount === imageItems.length) { + const syntheticEvent = { + target: { + files: files, + }, + } as unknown as React.ChangeEvent + + handleFileChange(syntheticEvent) + } + } + }) + } + } + return (
@@ -359,6 +394,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { isFocused && 'ring-1 ring-main-view-fg/10', isDragOver && 'ring-2 ring-accent border-accent' )} + data-drop-zone="true" onDragEnter={handleDragEnter} onDragLeave={handleDragLeave} onDragOver={handleDragOver} @@ -423,6 +459,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { // When Shift+Enter is pressed, a new line is added (default behavior) } }} + onPaste={handlePaste} placeholder={t('common:placeholder.chatInput')} autoFocus spellCheck={spellCheckChatInput} diff --git a/web-app/src/routes/__root.tsx b/web-app/src/routes/__root.tsx index 5278f73fc..77d9f9d2b 100644 --- a/web-app/src/routes/__root.tsx +++ b/web-app/src/routes/__root.tsx @@ -26,7 +26,7 @@ import { ResizablePanel, ResizableHandle, } from '@/components/ui/resizable' -import { useCallback } from 'react' +import { useCallback, useEffect } from 'react' import GlobalError from '@/containers/GlobalError' import { GlobalEventHandler } from '@/providers/GlobalEventHandler' @@ -65,6 +65,41 @@ const AppLayout = () => { [setLeftPanelSize, setLeftPanel] ) + // Prevent default drag and drop behavior globally + useEffect(() => { + const preventDefaults = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + } + + const handleGlobalDrop = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + + // Only prevent if the target is not within a chat input or other valid drop zone + const target = e.target as Element + const isValidDropZone = target?.closest('[data-drop-zone="true"]') || + target?.closest('.chat-input-drop-zone') || + target?.closest('[data-tauri-drag-region]') + + if (!isValidDropZone) { + // Prevent the file from opening in the window + return false + } + } + + // Add event listeners to prevent default drag/drop behavior + window.addEventListener('dragenter', preventDefaults) + window.addEventListener('dragover', preventDefaults) + window.addEventListener('drop', handleGlobalDrop) + + return () => { + window.removeEventListener('dragenter', preventDefaults) + window.removeEventListener('dragover', preventDefaults) + window.removeEventListener('drop', handleGlobalDrop) + } + }, []) + return (