file upload limit

This commit is contained in:
StyleZhang 2024-09-18 18:11:43 +08:00
parent f652ae0d98
commit 80f167ca02
15 changed files with 103 additions and 33 deletions

View File

@ -125,7 +125,7 @@ const ChatInputArea = ({
)} )}
> >
<div className='relative px-[9px] max-h-[158px] overflow-x-hidden overflow-y-auto'> <div className='relative px-[9px] max-h-[158px] overflow-x-hidden overflow-y-auto'>
<FileListInChatInput /> <FileListInChatInput fileConfig={visionConfig!} />
<div <div
ref={wrapperRef} ref={wrapperRef}
className='flex items-center justify-between' className='flex items-center justify-between'

View File

@ -38,7 +38,7 @@ const Operation = forwardRef<HTMLDivElement, OperationProps>(({
ref={ref} ref={ref}
> >
<div className='flex items-center space-x-1'> <div className='flex items-center space-x-1'>
{visionConfig?.enabled && <FileUploaderInChatInput />} {visionConfig?.enabled && <FileUploaderInChatInput fileConfig={visionConfig} />}
{ {
speechToTextConfig?.enabled && ( speechToTextConfig?.enabled && (
<ActionButton <ActionButton

View File

@ -1 +1 @@
export const FILE_LIMIT = 15 * 1024 * 1024 export const FILE_SIZE_LIMIT = 15 * 1024 * 1024

View File

@ -6,27 +6,33 @@ import { useTranslation } from 'react-i18next'
import { RiUploadCloud2Line } from '@remixicon/react' import { RiUploadCloud2Line } from '@remixicon/react'
import FileInput from '../file-input' import FileInput from '../file-input'
import { useFile } from '../hooks' import { useFile } from '../hooks'
import { useStore } from '../store'
import { import {
PortalToFollowElem, PortalToFollowElem,
PortalToFollowElemContent, PortalToFollowElemContent,
PortalToFollowElemTrigger, PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem' } from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import type { FileUpload } from '@/app/components/base/features/types'
type FileFromLinkOrLocalProps = { type FileFromLinkOrLocalProps = {
showFromLink?: boolean showFromLink?: boolean
showFromLocal?: boolean showFromLocal?: boolean
trigger: (open: boolean) => React.ReactNode trigger: (open: boolean) => React.ReactNode
fileConfig: FileUpload
} }
const FileFromLinkOrLocal = ({ const FileFromLinkOrLocal = ({
showFromLink = true, showFromLink = true,
showFromLocal = true, showFromLocal = true,
trigger, trigger,
fileConfig,
}: FileFromLinkOrLocalProps) => { }: FileFromLinkOrLocalProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const files = useStore(s => s.files)
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [url, setUrl] = useState('') const [url, setUrl] = useState('')
const { handleLoadFileFromLink } = useFile() const { handleLoadFileFromLink } = useFile(fileConfig)
const disabled = !!fileConfig.number_limits && files.length >= fileConfig.number_limits
return ( return (
<PortalToFollowElem <PortalToFollowElem
@ -48,12 +54,13 @@ const FileFromLinkOrLocal = ({
placeholder={t('common.fileUploader.pasteFileLinkInputPlaceholder') || ''} placeholder={t('common.fileUploader.pasteFileLinkInputPlaceholder') || ''}
value={url} value={url}
onChange={e => setUrl(e.target.value)} onChange={e => setUrl(e.target.value)}
disabled={disabled}
/> />
<Button <Button
className='shrink-0' className='shrink-0'
size='small' size='small'
variant='primary' variant='primary'
disabled={!url} disabled={!url || disabled}
onClick={() => handleLoadFileFromLink()} onClick={() => handleLoadFileFromLink()}
> >
{t('common.operation.ok')} {t('common.operation.ok')}
@ -75,10 +82,11 @@ const FileFromLinkOrLocal = ({
<Button <Button
className='relative w-full' className='relative w-full'
variant='secondary-accent' variant='secondary-accent'
disabled={disabled}
> >
<RiUploadCloud2Line className='mr-1 w-4 h-4' /> <RiUploadCloud2Line className='mr-1 w-4 h-4' />
{t('common.fileUploader.uploadFromComputer')} {t('common.fileUploader.uploadFromComputer')}
<FileInput /> <FileInput fileConfig={fileConfig} />
</Button> </Button>
) )
} }

View File

@ -1,19 +1,37 @@
import { useFile } from './hooks' import { useFile } from './hooks'
import { useStore } from './store'
import type { FileUpload } from '@/app/components/base/features/types'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
const FileInput = () => { type FileInputProps = {
const { handleLocalFileUpload } = useFile() fileConfig: FileUpload
}
const FileInput = ({
fileConfig,
}: FileInputProps) => {
const files = useStore(s => s.files)
const { handleLocalFileUpload } = useFile(fileConfig)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] const file = e.target.files?.[0]
if (file) if (file)
handleLocalFileUpload(file) handleLocalFileUpload(file)
} }
const allowedFileTypes = fileConfig.allowed_file_types
const isCustom = allowedFileTypes?.includes(SupportUploadFileTypes.custom)
const exts = isCustom ? (fileConfig.allowed_file_extensions || []) : (allowedFileTypes?.map(type => FILE_EXTS[type]) || []).flat().map(item => `.${item}`)
const accept = exts.join(',')
return ( return (
<input <input
className='absolute block inset-0 opacity-0 text-[0] w-full disabled:cursor-not-allowed cursor-pointer' className='absolute block inset-0 opacity-0 text-[0] w-full disabled:cursor-not-allowed cursor-pointer'
onClick={e => ((e.target as HTMLInputElement).value = '')} onClick={e => ((e.target as HTMLInputElement).value = '')}
type='file' type='file'
onChange={handleChange} onChange={handleChange}
accept={accept}
disabled={!!(fileConfig.number_limits && files.length >= fileConfig?.number_limits)}
/> />
) )
} }

View File

@ -17,19 +17,25 @@ import { useFile } from '../hooks'
import FileItem from './file-item' import FileItem from './file-item'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { FileUpload } from '@/app/components/base/features/types'
type Option = { type Option = {
value: string value: string
label: string label: string
icon: JSX.Element icon: JSX.Element
} }
const FileUploaderInAttachment = () => { type FileUploaderInAttachmentProps = {
fileConfig: FileUpload
}
const FileUploaderInAttachment = ({
fileConfig,
}: FileUploaderInAttachmentProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const files = useStore(s => s.files) const files = useStore(s => s.files)
const { const {
handleRemoveFile, handleRemoveFile,
handleReUploadFile, handleReUploadFile,
} = useFile() } = useFile(fileConfig)
const options = [ const options = [
{ {
value: 'local', value: 'local',
@ -49,17 +55,18 @@ const FileUploaderInAttachment = () => {
key={option.value} key={option.value}
variant='tertiary' variant='tertiary'
className={cn('basis-1/2 relative', open && 'bg-components-button-tertiary-bg-hover')} className={cn('basis-1/2 relative', open && 'bg-components-button-tertiary-bg-hover')}
disabled={!!(fileConfig.number_limits && files.length >= fileConfig.number_limits)}
> >
{option.icon} {option.icon}
<span className='ml-1'>{option.label}</span> <span className='ml-1'>{option.label}</span>
{ {
option.value === 'local' && ( option.value === 'local' && (
<FileInput /> <FileInput fileConfig={fileConfig} />
) )
} }
</Button> </Button>
) )
}, []) }, [fileConfig, files.length])
const renderTrigger = useCallback((option: Option) => { const renderTrigger = useCallback((option: Option) => {
return (open: boolean) => renderButton(option, open) return (open: boolean) => renderButton(option, open)
}, [renderButton]) }, [renderButton])
@ -73,10 +80,11 @@ const FileUploaderInAttachment = () => {
key={option.value} key={option.value}
showFromLocal={false} showFromLocal={false}
trigger={renderTrigger(option)} trigger={renderTrigger(option)}
fileConfig={fileConfig}
/> />
) )
} }
}, [renderButton, renderTrigger]) }, [renderButton, renderTrigger, fileConfig])
return ( return (
<div> <div>
@ -106,13 +114,15 @@ const FileUploaderInAttachment = () => {
type FileUploaderInAttachmentWrapperProps = { type FileUploaderInAttachmentWrapperProps = {
onChange: (files: FileEntity[]) => void onChange: (files: FileEntity[]) => void
fileConfig: FileUpload
} }
const FileUploaderInAttachmentWrapper = ({ const FileUploaderInAttachmentWrapper = ({
onChange, onChange,
fileConfig,
}: FileUploaderInAttachmentWrapperProps) => { }: FileUploaderInAttachmentWrapperProps) => {
return ( return (
<FileContextProvider onChange={onChange}> <FileContextProvider onChange={onChange}>
<FileUploaderInAttachment /> <FileUploaderInAttachment fileConfig={fileConfig} />
</FileContextProvider> </FileContextProvider>
) )
} }

View File

@ -3,13 +3,19 @@ import { useFile } from '../hooks'
import { useStore } from '../store' import { useStore } from '../store'
import FileImageItem from './file-image-item' import FileImageItem from './file-image-item'
import FileItem from './file-item' import FileItem from './file-item'
import type { FileUpload } from '@/app/components/base/features/types'
const FileList = () => { type FileListProps = {
fileConfig: FileUpload
}
const FileList = ({
fileConfig,
}: FileListProps) => {
const files = useStore(s => s.files) const files = useStore(s => s.files)
const { const {
handleRemoveFile, handleRemoveFile,
handleReUploadFile, handleReUploadFile,
} = useFile() } = useFile(fileConfig)
return ( return (
<div className='flex flex-wrap gap-2'> <div className='flex flex-wrap gap-2'>

View File

@ -8,8 +8,14 @@ import {
import FileFromLinkOrLocal from '../file-from-link-or-local' import FileFromLinkOrLocal from '../file-from-link-or-local'
import ActionButton from '@/app/components/base/action-button' import ActionButton from '@/app/components/base/action-button'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { FileUpload } from '@/app/components/base/features/types'
const FileUploaderInChatInput = () => { type FileUploaderInChatInputProps = {
fileConfig: FileUpload
}
const FileUploaderInChatInput = ({
fileConfig,
}: FileUploaderInChatInputProps) => {
const renderTrigger = useCallback((open: boolean) => { const renderTrigger = useCallback((open: boolean) => {
return ( return (
<ActionButton <ActionButton
@ -24,6 +30,7 @@ const FileUploaderInChatInput = () => {
return ( return (
<FileFromLinkOrLocal <FileFromLinkOrLocal
trigger={renderTrigger} trigger={renderTrigger}
fileConfig={fileConfig}
/> />
) )
} }

View File

@ -13,17 +13,18 @@ import {
fileUpload, fileUpload,
getFileType, getFileType,
} from './utils' } from './utils'
import { FILE_SIZE_LIMIT } from './constants'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import { SupportUploadFileTypes } from '@/app/components/workflow/types' import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import type { FileUpload } from '@/app/components/base/features/types'
import { formatFileSize } from '@/utils/format'
export const useFile = () => { export const useFile = (fileConfig: FileUpload) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext() const { notify } = useToastContext()
const fileStore = useFileStore() const fileStore = useFileStore()
const params = useParams() const params = useParams()
const featuresStore = useFeaturesStore()
const handleAddOrUpdateFiles = useCallback((newFile: FileEntity) => { const handleAddOrUpdateFiles = useCallback((newFile: FileEntity) => {
const { const {
@ -95,9 +96,13 @@ export const useFile = () => {
}, [fileStore]) }, [fileStore])
const handleLocalFileUpload = useCallback((file: File) => { const handleLocalFileUpload = useCallback((file: File) => {
if (file.size > FILE_SIZE_LIMIT) {
notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerLimit', { size: formatFileSize(FILE_SIZE_LIMIT) }) })
return
}
const reader = new FileReader() const reader = new FileReader()
const isImage = file.type.startsWith('image') const isImage = file.type.startsWith('image')
const allowedFileTypes = featuresStore?.getState().features.file?.allowed_file_types const allowedFileTypes = fileConfig.allowed_file_types
const isCustomFileType = allowedFileTypes?.includes(SupportUploadFileTypes.custom) const isCustomFileType = allowedFileTypes?.includes(SupportUploadFileTypes.custom)
reader.addEventListener( reader.addEventListener(
@ -122,7 +127,7 @@ export const useFile = () => {
handleAddOrUpdateFiles({ ...uploadingFile, fileStorageId: res.id, progress: 100 }) handleAddOrUpdateFiles({ ...uploadingFile, fileStorageId: res.id, progress: 100 })
}, },
onErrorCallback: () => { onErrorCallback: () => {
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') }) notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') })
handleAddOrUpdateFiles({ ...uploadingFile, progress: -1 }) handleAddOrUpdateFiles({ ...uploadingFile, progress: -1 })
}, },
}, !!params.token) }, !!params.token)
@ -132,12 +137,12 @@ export const useFile = () => {
reader.addEventListener( reader.addEventListener(
'error', 'error',
() => { () => {
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerReadError') }) notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerReadError') })
}, },
false, false,
) )
reader.readAsDataURL(file) reader.readAsDataURL(file)
}, [notify, t, handleAddOrUpdateFiles, params.token]) }, [notify, t, handleAddOrUpdateFiles, params.token, fileConfig?.allowed_file_types])
const handleClipboardPasteFile = useCallback((e: ClipboardEvent<HTMLTextAreaElement>) => { const handleClipboardPasteFile = useCallback((e: ClipboardEvent<HTMLTextAreaElement>) => {
const file = e.clipboardData?.files[0] const file = e.clipboardData?.files[0]

View File

@ -50,7 +50,7 @@ export const getInputVars = (text: string): ValueSelector[] => {
return [] return []
} }
export const FILE_EXTS = { export const FILE_EXTS: Record<string, string[]> = {
[SupportUploadFileTypes.image]: ['JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG'], [SupportUploadFileTypes.image]: ['JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG'],
[SupportUploadFileTypes.document]: ['TXT', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB'], [SupportUploadFileTypes.document]: ['TXT', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB'],
[SupportUploadFileTypes.audio]: ['MP3', 'M4A', 'WAV', 'WEBM', 'AMR'], [SupportUploadFileTypes.audio]: ['MP3', 'M4A', 'WAV', 'WEBM', 'AMR'],

View File

@ -159,12 +159,20 @@ const FormItem: FC<Props> = ({
{/* #TODO# file upload */} {/* #TODO# file upload */}
{(type === InputVarType.singleFile || type === InputVarType.multiFiles) && ( {(type === InputVarType.singleFile || type === InputVarType.multiFiles) && (
<FileUploaderInAttachmentWrapper onChange={files => onChange(files.filter(file => file.progress !== -1).map(fileItem => ({ <FileUploaderInAttachmentWrapper
onChange={files => onChange(files.filter(file => file.progress !== -1).map(fileItem => ({
type: fileItem.fileType, type: fileItem.fileType,
transfer_method: fileItem.type, transfer_method: fileItem.type,
url: fileItem.url, url: fileItem.url,
upload_file_id: fileItem.fileId, upload_file_id: fileItem.fileId,
})))} /> })))}
fileConfig={{
allowed_file_types: payload.allowed_file_types,
allowed_file_extensions: payload.allowed_file_extensions,
allowed_file_upload_methods: payload.allowed_file_upload_methods,
number_limits: payload.max_length,
}}
/>
)} )}
{ {
type === InputVarType.files && ( type === InputVarType.files && (

View File

@ -10,6 +10,8 @@ import FileTypeItem from './file-type-item'
import InputNumberWithSlider from './input-number-with-slider' import InputNumberWithSlider from './input-number-with-slider'
import Field from '@/app/components/app/configuration/config-var/config-modal/field' import Field from '@/app/components/app/configuration/config-var/config-modal/field'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import { FILE_SIZE_LIMIT } from '@/app/components/base/file-uploader/constants'
import { formatFileSize } from '@/utils/format'
type Props = { type Props = {
payload: UploadFileSetting payload: UploadFileSetting
@ -138,7 +140,7 @@ const FileUploadSetting: FC<Props> = ({
title={t('appDebug.variableConfig.maxNumberOfUploads')!} title={t('appDebug.variableConfig.maxNumberOfUploads')!}
> >
<div> <div>
<div className='mb-1.5 text-text-tertiary body-xs-regular'>{t('appDebug.variableConfig.maxNumberTip')}</div> <div className='mb-1.5 text-text-tertiary body-xs-regular'>{t('appDebug.variableConfig.maxNumberTip', { size: formatFileSize(FILE_SIZE_LIMIT) })}</div>
<InputNumberWithSlider <InputNumberWithSlider
value={max_length} value={max_length}
min={1} min={1}

View File

@ -361,7 +361,7 @@ const translation = {
}, },
}, },
'maxNumberOfUploads': 'Max number of uploads', 'maxNumberOfUploads': 'Max number of uploads',
'maxNumberTip': 'Max 15MB each', 'maxNumberTip': 'Max {{size}} each',
'errorMsg': { 'errorMsg': {
labelNameRequired: 'Label name is required', labelNameRequired: 'Label name is required',
varNameCanBeRepeat: 'Variable name can not be repeated', varNameCanBeRepeat: 'Variable name can not be repeated',

View File

@ -562,6 +562,9 @@ const translation = {
uploadFromComputer: 'Local upload', uploadFromComputer: 'Local upload',
pasteFileLink: 'Paste file link', pasteFileLink: 'Paste file link',
pasteFileLinkInputPlaceholder: 'Enter URL...', pasteFileLinkInputPlaceholder: 'Enter URL...',
uploadFromComputerReadError: 'File reading failed, please try again.',
uploadFromComputerUploadError: 'File upload failed, please upload again.',
uploadFromComputerLimit: 'Upload File cannot exceed {{size}}',
}, },
tag: { tag: {
placeholder: 'All Tags', placeholder: 'All Tags',

View File

@ -562,6 +562,9 @@ const translation = {
uploadFromComputer: '从本地上传', uploadFromComputer: '从本地上传',
pasteFileLink: '粘贴文件链接', pasteFileLink: '粘贴文件链接',
pasteFileLinkInputPlaceholder: '输入文件链接', pasteFileLinkInputPlaceholder: '输入文件链接',
uploadFromComputerReadError: '文件读取失败,请重新选择。',
uploadFromComputerUploadError: '文件上传失败,请重新上传。',
uploadFromComputerLimit: '上传文件不能超过 {{size}}',
}, },
tag: { tag: {
placeholder: '全部标签', placeholder: '全部标签',