From 8a39505ee667e7c17ee6f5663a3cdb2ff346d114 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 10 Jan 2026 06:22:59 -0800 Subject: [PATCH] feat(chat): add paste and drag-drop file upload support - Add clipboard paste handler for images and files (Ctrl+V / Cmd+V) - Add drag-and-drop zone with visual feedback - Update placeholder text to inform users about new features - Improve file upload UX with drop overlay Co-Authored-By: Claude Opus 4.5 --- .../presentation/components/InputArea.tsx | 111 +++++++++++++++++- 1 file changed, 106 insertions(+), 5 deletions(-) diff --git a/packages/web-client/src/features/chat/presentation/components/InputArea.tsx b/packages/web-client/src/features/chat/presentation/components/InputArea.tsx index 73157ff..f06233d 100644 --- a/packages/web-client/src/features/chat/presentation/components/InputArea.tsx +++ b/packages/web-client/src/features/chat/presentation/components/InputArea.tsx @@ -1,5 +1,5 @@ -import { useState, useRef, useEffect, KeyboardEvent, ChangeEvent } from 'react'; -import { Send, Paperclip, X, Image, FileText, Loader2 } from 'lucide-react'; +import { useState, useRef, useEffect, KeyboardEvent, ChangeEvent, ClipboardEvent, DragEvent } from 'react'; +import { Send, Paperclip, X, Image, FileText, Loader2, Upload } from 'lucide-react'; import { clsx } from 'clsx'; import { FileAttachment } from '../stores/chatStore'; @@ -49,8 +49,10 @@ export function InputArea({ uploadProgress = {}, }: InputAreaProps) { const [message, setMessage] = useState(''); + const [isDragging, setIsDragging] = useState(false); const textareaRef = useRef(null); const fileInputRef = useRef(null); + const dropZoneRef = useRef(null); // Auto-resize textarea useEffect(() => { @@ -105,6 +107,84 @@ export function InputArea({ fileInputRef.current?.click(); }; + // 处理文件验证和添加 + const processFiles = (files: File[]) => { + const validFiles: File[] = []; + files.forEach((file) => { + if (!ALLOWED_TYPES.includes(file.type)) { + console.warn(`不支持的文件类型: ${file.type}`); + return; + } + if (file.size > MAX_FILE_SIZE) { + console.warn(`文件过大: ${file.name}`); + return; + } + validFiles.push(file); + }); + + if (validFiles.length > 0) { + onFilesSelected?.(validFiles); + } + }; + + // 处理粘贴事件 + const handlePaste = (e: ClipboardEvent) => { + const items = e.clipboardData?.items; + if (!items) return; + + const files: File[] = []; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.kind === 'file') { + const file = item.getAsFile(); + if (file) { + files.push(file); + } + } + } + + if (files.length > 0) { + e.preventDefault(); // 阻止默认粘贴行为 + processFiles(files); + } + }; + + // 拖放事件处理 + const handleDragEnter = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (!disabled && !isUploading) { + setIsDragging(true); + } + }; + + const handleDragLeave = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + // 只有当离开整个drop zone时才取消高亮 + if (dropZoneRef.current && !dropZoneRef.current.contains(e.relatedTarget as Node)) { + setIsDragging(false); + } + }; + + const handleDragOver = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleDrop = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + + if (disabled || isUploading) return; + + const files = Array.from(e.dataTransfer?.files || []); + if (files.length > 0) { + processFiles(files); + } + }; + const formatFileSize = (bytes: number) => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; @@ -112,7 +192,27 @@ export function InputArea({ }; return ( -
+
+ {/* 拖放提示层 */} + {isDragging && ( +
+
+ + 释放以添加文件 +
+
+ )} + {/* 文件预览区域 */} {pendingFiles.length > 0 && (
@@ -208,10 +308,11 @@ export function InputArea({ value={message} onChange={(e) => setMessage(e.target.value)} onKeyDown={handleKeyDown} + onPaste={handlePaste} placeholder={ pendingFiles.length > 0 ? '添加说明文字(可选),或直接发送...' - : '输入您的问题...' + : '输入您的问题,可粘贴或拖放图片...' } disabled={disabled || isUploading} rows={1} @@ -244,7 +345,7 @@ export function InputArea({ {/* 提示文字 */}

- 支持图片 (JPEG, PNG, GIF) 和文档 (PDF, Word, Excel, 文本),最大 50MB + 支持粘贴、拖放图片 (JPEG, PNG, GIF) 和文档 (PDF, Word, Excel),最大 50MB

);