mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-18 02:35:56 +08:00
file upload limit
This commit is contained in:
parent
f652ae0d98
commit
80f167ca02
@ -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'
|
||||
|
@ -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
|
||||
|
@ -1 +1 @@
|
||||
export const FILE_LIMIT = 15 * 1024 * 1024
|
||||
export const FILE_SIZE_LIMIT = 15 * 1024 * 1024
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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'>
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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'],
|
||||
|
@ -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 && (
|
||||
|
@ -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}
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -562,6 +562,9 @@ const translation = {
|
||||
uploadFromComputer: '从本地上传',
|
||||
pasteFileLink: '粘贴文件链接',
|
||||
pasteFileLinkInputPlaceholder: '输入文件链接',
|
||||
uploadFromComputerReadError: '文件读取失败,请重新选择。',
|
||||
uploadFromComputerUploadError: '文件上传失败,请重新上传。',
|
||||
uploadFromComputerLimit: '上传文件不能超过 {{size}}',
|
||||
},
|
||||
tag: {
|
||||
placeholder: '全部标签',
|
||||
|
Loading…
x
Reference in New Issue
Block a user