diff --git a/web/app/components/app/chat/index.tsx b/web/app/components/app/chat/index.tsx index 94eeb955c4..86f46c2f9e 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 { useImageFiles } from '@/app/components/base/image-uploader/hooks' +import { useClipboardUploader, useImageFiles } from '@/app/components/base/image-uploader/hooks' export type IChatProps = { configElem?: React.ReactNode @@ -101,6 +101,7 @@ const Chat: FC = ({ onImageLinkLoadSuccess, onClear, } = useImageFiles() + const { onPaste } = useClipboardUploader({ onUpload, visionConfig, files }) const isUseInputMethod = useRef(false) const [query, setQuery] = React.useState('') @@ -305,6 +306,7 @@ const Chat: FC = ({ onChange={handleContentChange} onKeyUp={handleKeyUp} onKeyDown={handleKeyDown} + onPaste={onPaste} autoSize />
diff --git a/web/app/components/base/image-uploader/hooks.ts b/web/app/components/base/image-uploader/hooks.ts index 24a5af1b86..09c6a87941 100644 --- a/web/app/components/base/image-uploader/hooks.ts +++ b/web/app/components/base/image-uploader/hooks.ts @@ -1,9 +1,11 @@ -import { useMemo, useRef, useState } from 'react' +import { useCallback, useMemo, useRef, useState } from 'react' +import type { ClipboardEvent } from 'react' import { useParams } from 'next/navigation' import { useTranslation } from 'react-i18next' import { imageUpload } from './utils' import { useToastContext } from '@/app/components/base/toast' -import type { ImageFile } from '@/types/app' +import { ALLOW_FILE_EXTENSIONS, TransferMethod } from '@/types/app' +import type { ImageFile, VisionSettings } from '@/types/app' export const useImageFiles = () => { const params = useParams() @@ -108,3 +110,81 @@ export const useImageFiles = () => { onClear: handleClear, } } + +type useClipboardUploaderProps = { + files: ImageFile[] + visionConfig?: VisionSettings + onUpload: (imageFile: ImageFile) => void +} + +export const useClipboardUploader = ({ visionConfig, onUpload, files }: useClipboardUploaderProps) => { + 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) + // TODO: leave some warnings? + return + + const file = e.clipboardData?.files[0] + + if (!file || !ALLOW_FILE_EXTENSIONS.includes(file.type.split('/')[1])) + return + + const limit = +visionConfig.image_file_size_limit! + + if (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) + }, [visionConfig, files.length, notify, t, onUpload, params.token]) + + return { + onPaste: handleClipboardPaste, + } +} diff --git a/web/app/components/base/image-uploader/uploader.tsx b/web/app/components/base/image-uploader/uploader.tsx index a0e420e7bf..2dd032198c 100644 --- a/web/app/components/base/image-uploader/uploader.tsx +++ b/web/app/components/base/image-uploader/uploader.tsx @@ -4,7 +4,7 @@ import { useParams } from 'next/navigation' import { useTranslation } from 'react-i18next' import { imageUpload } from './utils' import type { ImageFile } from '@/types/app' -import { TransferMethod } from '@/types/app' +import { ALLOW_FILE_EXTENSIONS, TransferMethod } from '@/types/app' import { useToastContext } from '@/app/components/base/toast' type UploaderProps = { @@ -90,7 +90,7 @@ const Uploader: FC = ({ `} onClick={e => (e.target as HTMLInputElement).value = ''} type='file' - accept='.png, .jpg, .jpeg, .webp, .gif' + accept={ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',')} onChange={handleChange} disabled={disabled} /> diff --git a/web/types/app.ts b/web/types/app.ts index 6b53e2b87e..0dfe321392 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -297,6 +297,8 @@ export enum TransferMethod { remote_url = 'remote_url', } +export const ALLOW_FILE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'webp', 'gif'] + export type VisionSettings = { enabled: boolean number_limits: number