From efa16dbb44595bd183c0a816a9bfa436f523ef30 Mon Sep 17 00:00:00 2001 From: Yuhao Date: Fri, 1 Dec 2023 16:50:22 +0800 Subject: [PATCH] feat: drag to upload image (#1666) --- web/app/components/app/chat/index.tsx | 9 +- .../components/base/image-uploader/hooks.ts | 116 +++++++++++++++--- .../base/image-uploader/uploader.tsx | 55 +-------- 3 files changed, 109 insertions(+), 71 deletions(-) diff --git a/web/app/components/app/chat/index.tsx b/web/app/components/app/chat/index.tsx index 86f46c2f9ecd50..a7b5db3cc21d9b 100644 --- a/web/app/components/app/chat/index.tsx +++ b/web/app/components/app/chat/index.tsx @@ -23,7 +23,7 @@ import type { DataSet } from '@/models/datasets' import ChatImageUploader from '@/app/components/base/image-uploader/chat-image-uploader' import ImageList from '@/app/components/base/image-uploader/image-list' import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app' -import { useClipboardUploader, useImageFiles } from '@/app/components/base/image-uploader/hooks' +import { useClipboardUploader, useDraggableUploader, useImageFiles } from '@/app/components/base/image-uploader/hooks' export type IChatProps = { configElem?: React.ReactNode @@ -102,6 +102,7 @@ const Chat: FC = ({ onClear, } = useImageFiles() const { onPaste } = useClipboardUploader({ onUpload, visionConfig, files }) + const { onDragEnter, onDragLeave, onDragOver, onDrop, isDragActive } = useDraggableUploader({ onUpload, files, visionConfig }) const isUseInputMethod = useRef(false) const [query, setQuery] = React.useState('') @@ -273,7 +274,7 @@ const Chat: FC = ({ ) } -
+
{ visionConfig?.enabled && ( <> @@ -307,6 +308,10 @@ const Chat: FC = ({ onKeyUp={handleKeyUp} onKeyDown={handleKeyDown} onPaste={onPaste} + onDragEnter={onDragEnter} + onDragLeave={onDragLeave} + onDragOver={onDragOver} + onDrop={onDrop} autoSize />
diff --git a/web/app/components/base/image-uploader/hooks.ts b/web/app/components/base/image-uploader/hooks.ts index 09c6a87941f083..54b08b6ac0fd61 100644 --- a/web/app/components/base/image-uploader/hooks.ts +++ b/web/app/components/base/image-uploader/hooks.ts @@ -111,35 +111,27 @@ export const useImageFiles = () => { } } -type useClipboardUploaderProps = { - files: ImageFile[] - visionConfig?: VisionSettings +type useLocalUploaderProps = { + disabled?: boolean + limit?: number onUpload: (imageFile: ImageFile) => void } -export const useClipboardUploader = ({ visionConfig, onUpload, files }: useClipboardUploaderProps) => { +export const useLocalFileUploader = ({ limit, disabled = false, onUpload }: useLocalUploaderProps) => { const { notify } = useToastContext() const params = useParams() const { t } = useTranslation() - const handleClipboardPaste = useCallback((e: ClipboardEvent) => { - if (!visionConfig || !visionConfig.enabled) - return - - const disabled = files.length >= visionConfig.number_limits - - if (disabled) + const handleLocalFileUpload = useCallback((file: File) => { + if (disabled) { // TODO: leave some warnings? return + } - const file = e.clipboardData?.files[0] - - if (!file || !ALLOW_FILE_EXTENSIONS.includes(file.type.split('/')[1])) + if (!ALLOW_FILE_EXTENSIONS.includes(file.type.split('/')[1])) return - const limit = +visionConfig.image_file_size_limit! - - if (file.size > limit * 1024 * 1024) { + if (limit && file.size > limit * 1024 * 1024) { notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerLimit', { size: limit }) }) return } @@ -182,9 +174,97 @@ export const useClipboardUploader = ({ visionConfig, onUpload, files }: useClipb false, ) reader.readAsDataURL(file) - }, [visionConfig, files.length, notify, t, onUpload, params.token]) + }, [disabled, limit, notify, t, onUpload, params.token]) + + return { disabled, handleLocalFileUpload } +} + +type useClipboardUploaderProps = { + files: ImageFile[] + visionConfig?: VisionSettings + onUpload: (imageFile: ImageFile) => void +} + +export const useClipboardUploader = ({ visionConfig, onUpload, files }: useClipboardUploaderProps) => { + const allowLocalUpload = visionConfig?.transfer_methods?.includes(TransferMethod.local_file) + const disabled = useMemo(() => + !visionConfig + || !visionConfig?.enabled + || !allowLocalUpload + || files.length >= visionConfig.number_limits!, + [allowLocalUpload, files.length, visionConfig]) + const limit = useMemo(() => visionConfig ? +visionConfig.image_file_size_limit! : 0, [visionConfig]) + const { handleLocalFileUpload } = useLocalFileUploader({ limit, onUpload, disabled }) + + const handleClipboardPaste = useCallback((e: ClipboardEvent) => { + e.preventDefault() + const file = e.clipboardData?.files[0] + + if (!file) + return + + handleLocalFileUpload(file) + }, [handleLocalFileUpload]) return { onPaste: handleClipboardPaste, } } + +type useDraggableUploaderProps = { + files: ImageFile[] + visionConfig?: VisionSettings + onUpload: (imageFile: ImageFile) => void +} + +export const useDraggableUploader = ({ visionConfig, onUpload, files }: useDraggableUploaderProps) => { + const allowLocalUpload = visionConfig?.transfer_methods?.includes(TransferMethod.local_file) + const disabled = useMemo(() => + !visionConfig + || !visionConfig?.enabled + || !allowLocalUpload + || files.length >= visionConfig.number_limits!, + [allowLocalUpload, files.length, visionConfig]) + const limit = useMemo(() => visionConfig ? +visionConfig.image_file_size_limit! : 0, [visionConfig]) + const { handleLocalFileUpload } = useLocalFileUploader({ disabled, onUpload, limit }) + const [isDragActive, setIsDragActive] = useState(false) + + const handleDragEnter = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + if (!disabled) + setIsDragActive(true) + }, [disabled]) + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + }, []) + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragActive(false) + }, []) + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragActive(false) + + const file = e.dataTransfer.files[0] + + if (!file) + return + + handleLocalFileUpload(file) + }, [handleLocalFileUpload]) + + return { + onDragEnter: handleDragEnter, + onDragOver: handleDragOver, + onDragLeave: handleDragLeave, + onDrop: handleDrop, + isDragActive, + } +} diff --git a/web/app/components/base/image-uploader/uploader.tsx b/web/app/components/base/image-uploader/uploader.tsx index 2dd032198c40b0..f43c24c3f6e130 100644 --- a/web/app/components/base/image-uploader/uploader.tsx +++ b/web/app/components/base/image-uploader/uploader.tsx @@ -1,11 +1,8 @@ import type { ChangeEvent, FC } from 'react' import { useState } from 'react' -import { useParams } from 'next/navigation' -import { useTranslation } from 'react-i18next' -import { imageUpload } from './utils' +import { useLocalFileUploader } from './hooks' import type { ImageFile } from '@/types/app' -import { ALLOW_FILE_EXTENSIONS, TransferMethod } from '@/types/app' -import { useToastContext } from '@/app/components/base/toast' +import { ALLOW_FILE_EXTENSIONS } from '@/types/app' type UploaderProps = { children: (hovering: boolean) => JSX.Element @@ -21,9 +18,7 @@ const Uploader: FC = ({ disabled, }) => { const [hovering, setHovering] = useState(false) - const params = useParams() - const { notify } = useToastContext() - const { t } = useTranslation() + const { handleLocalFileUpload } = useLocalFileUploader({ limit, onUpload, disabled }) const handleChange = (e: ChangeEvent) => { const file = e.target.files?.[0] @@ -31,49 +26,7 @@ const Uploader: FC = ({ if (!file) return - if (limit && file.size > limit * 1024 * 1024) { - notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerLimit', { size: limit }) }) - return - } - - const reader = new FileReader() - reader.addEventListener( - 'load', - () => { - const imageFile = { - type: TransferMethod.local_file, - _id: `${Date.now()}`, - fileId: '', - file, - url: reader.result as string, - base64Url: reader.result as string, - progress: 0, - } - onUpload(imageFile) - imageUpload({ - file: imageFile.file, - onProgressCallback: (progress) => { - onUpload({ ...imageFile, progress }) - }, - onSuccessCallback: (res) => { - onUpload({ ...imageFile, fileId: res.id, progress: 100 }) - }, - onErrorCallback: () => { - notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') }) - onUpload({ ...imageFile, progress: -1 }) - }, - }, !!params.token) - }, - false, - ) - reader.addEventListener( - 'error', - () => { - notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerReadError') }) - }, - false, - ) - reader.readAsDataURL(file) + handleLocalFileUpload(file) } return (