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'>
<FileListInChatInput />
<FileListInChatInput fileConfig={visionConfig!} />
<div
ref={wrapperRef}
className='flex items-center justify-between'

View File

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

View File

@ -1,19 +1,37 @@
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 = () => {
const { handleLocalFileUpload } = useFile()
type FileInputProps = {
fileConfig: FileUpload
}
const FileInput = ({
fileConfig,
}: FileInputProps) => {
const files = useStore(s => s.files)
const { handleLocalFileUpload } = useFile(fileConfig)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (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 (
<input
className='absolute block inset-0 opacity-0 text-[0] w-full disabled:cursor-not-allowed cursor-pointer'
onClick={e => ((e.target as HTMLInputElement).value = '')}
type='file'
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 Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
import type { FileUpload } from '@/app/components/base/features/types'
type Option = {
value: string
label: string
icon: JSX.Element
}
const FileUploaderInAttachment = () => {
type FileUploaderInAttachmentProps = {
fileConfig: FileUpload
}
const FileUploaderInAttachment = ({
fileConfig,
}: FileUploaderInAttachmentProps) => {
const { t } = useTranslation()
const files = useStore(s => s.files)
const {
handleRemoveFile,
handleReUploadFile,
} = useFile()
} = useFile(fileConfig)
const options = [
{
value: 'local',
@ -49,17 +55,18 @@ const FileUploaderInAttachment = () => {
key={option.value}
variant='tertiary'
className={cn('basis-1/2 relative', open && 'bg-components-button-tertiary-bg-hover')}
disabled={!!(fileConfig.number_limits && files.length >= fileConfig.number_limits)}
>
{option.icon}
<span className='ml-1'>{option.label}</span>
{
option.value === 'local' && (
<FileInput />
<FileInput fileConfig={fileConfig} />
)
}
</Button>
)
}, [])
}, [fileConfig, files.length])
const renderTrigger = useCallback((option: Option) => {
return (open: boolean) => renderButton(option, open)
}, [renderButton])
@ -73,10 +80,11 @@ const FileUploaderInAttachment = () => {
key={option.value}
showFromLocal={false}
trigger={renderTrigger(option)}
fileConfig={fileConfig}
/>
)
}
}, [renderButton, renderTrigger])
}, [renderButton, renderTrigger, fileConfig])
return (
<div>
@ -106,13 +114,15 @@ const FileUploaderInAttachment = () => {
type FileUploaderInAttachmentWrapperProps = {
onChange: (files: FileEntity[]) => void
fileConfig: FileUpload
}
const FileUploaderInAttachmentWrapper = ({
onChange,
fileConfig,
}: FileUploaderInAttachmentWrapperProps) => {
return (
<FileContextProvider onChange={onChange}>
<FileUploaderInAttachment />
<FileUploaderInAttachment fileConfig={fileConfig} />
</FileContextProvider>
)
}

View File

@ -3,13 +3,19 @@ import { useFile } from '../hooks'
import { useStore } from '../store'
import FileImageItem from './file-image-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 {
handleRemoveFile,
handleReUploadFile,
} = useFile()
} = useFile(fileConfig)
return (
<div className='flex flex-wrap gap-2'>

View File

@ -8,8 +8,14 @@ import {
import FileFromLinkOrLocal from '../file-from-link-or-local'
import ActionButton from '@/app/components/base/action-button'
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) => {
return (
<ActionButton
@ -24,6 +30,7 @@ const FileUploaderInChatInput = () => {
return (
<FileFromLinkOrLocal
trigger={renderTrigger}
fileConfig={fileConfig}
/>
)
}

View File

@ -13,17 +13,18 @@ import {
fileUpload,
getFileType,
} from './utils'
import { FILE_SIZE_LIMIT } from './constants'
import { useToastContext } from '@/app/components/base/toast'
import { TransferMethod } from '@/types/app'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
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 { notify } = useToastContext()
const fileStore = useFileStore()
const params = useParams()
const featuresStore = useFeaturesStore()
const handleAddOrUpdateFiles = useCallback((newFile: FileEntity) => {
const {
@ -95,9 +96,13 @@ export const useFile = () => {
}, [fileStore])
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 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)
reader.addEventListener(
@ -122,7 +127,7 @@ export const useFile = () => {
handleAddOrUpdateFiles({ ...uploadingFile, fileStorageId: res.id, progress: 100 })
},
onErrorCallback: () => {
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') })
handleAddOrUpdateFiles({ ...uploadingFile, progress: -1 })
},
}, !!params.token)
@ -132,12 +137,12 @@ export const useFile = () => {
reader.addEventListener(
'error',
() => {
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerReadError') })
notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerReadError') })
},
false,
)
reader.readAsDataURL(file)
}, [notify, t, handleAddOrUpdateFiles, params.token])
}, [notify, t, handleAddOrUpdateFiles, params.token, fileConfig?.allowed_file_types])
const handleClipboardPasteFile = useCallback((e: ClipboardEvent<HTMLTextAreaElement>) => {
const file = e.clipboardData?.files[0]

View File

@ -50,7 +50,7 @@ export const getInputVars = (text: string): ValueSelector[] => {
return []
}
export const FILE_EXTS = {
export const FILE_EXTS: Record<string, string[]> = {
[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.audio]: ['MP3', 'M4A', 'WAV', 'WEBM', 'AMR'],

View File

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

View File

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

View File

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

View File

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

View File

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