From d637a147ee8e9a38da7af62ec931e91ccb924dbe Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 21 Jun 2023 09:44:01 +0800 Subject: [PATCH] feat: support batch upload files (#419) --- .../datasets/create/file-preview/index.tsx | 6 +- .../create/file-uploader/index.module.css | 106 +++---- .../datasets/create/file-uploader/index.tsx | 287 ++++++++++-------- web/app/components/datasets/create/index.tsx | 33 +- .../datasets/create/step-one/index.module.css | 2 + .../datasets/create/step-one/index.tsx | 48 ++- .../datasets/create/step-two/index.tsx | 22 +- .../documents/detail/settings/index.tsx | 2 +- web/i18n/lang/dataset-creation.en.ts | 4 +- web/i18n/lang/dataset-creation.zh.ts | 4 +- 10 files changed, 288 insertions(+), 226 deletions(-) diff --git a/web/app/components/datasets/create/file-preview/index.tsx b/web/app/components/datasets/create/file-preview/index.tsx index 738013f196..b51f21c41a 100644 --- a/web/app/components/datasets/create/file-preview/index.tsx +++ b/web/app/components/datasets/create/file-preview/index.tsx @@ -9,7 +9,6 @@ import { fetchFilePreview } from '@/service/common' type IProps = { file?: File - notionPage?: any hidePreview: () => void } @@ -33,14 +32,15 @@ const FilePreview = ({ const getFileName = (currentFile?: File) => { if (!currentFile) return '' - const arr = currentFile.name.split('.') return arr.slice(0, -1).join() } useEffect(() => { - if (file) + if (file) { + setLoading(true) getPreviewContent(file.id) + } }, [file]) return ( diff --git a/web/app/components/datasets/create/file-uploader/index.module.css b/web/app/components/datasets/create/file-uploader/index.module.css index 95322a30c9..244ad328f9 100644 --- a/web/app/components/datasets/create/file-uploader/index.module.css +++ b/web/app/components/datasets/create/file-uploader/index.module.css @@ -1,5 +1,5 @@ .fileUploader { - @apply mb-9; + @apply mb-6; } .fileUploader .title { @apply mb-2; @@ -9,14 +9,14 @@ color: #344054; } .fileUploader .tip { - @apply mt-2; font-weight: 400; font-size: 12px; - line-height: 26px; + line-height: 18px; color: #667085; } .uploader { - @apply relative box-border flex justify-center items-center; + @apply relative box-border flex justify-center items-center mb-2; + flex-direction: column; max-width: 640px; height: 80px; background: #F9FAFB; @@ -38,7 +38,7 @@ width: 100%; height: 100%; } -.uploader::before { +.uploader .uploadIcon { content: ''; display: block; margin-right: 8px; @@ -51,16 +51,20 @@ @apply pl-1 cursor-pointer; color: #155eef; } - +.fileList { + @apply space-y-2; +} .file { - @apply box-border relative flex items-center; - padding: 21px 24px 21px 64px; + @apply box-border relative flex items-center justify-between; + padding: 8px 12px 8px 8px; max-width: 640px; - height: 80px; - background: #F9FAFB; - border: 1px solid #F2F4F7; - border-radius: 12px; + height: 40px; + background: #ffffff; + border: 0.5px solid #EAECF0; + box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05); + border-radius: 8px; overflow: hidden; + cursor: pointer; } .progressbar { position: absolute; @@ -69,36 +73,27 @@ height: 100%; background-color: #F2F4F7; } -.file:hover { - background: #F5F8FF; - border: 1px solid #D1E0FF; -} -.file:hover .actionWrapper .buttonWrapper { - display: flex; - align-items: center; -} -.file:hover .actionWrapper .divider { - display: block; -} + .file.uploading, .file.uploading:hover { background: #FCFCFD; - border: 1px solid #EAECF0; + border: 0.5px solid #EAECF0; } -.file.uploading:hover .actionWrapper .percent { - padding: 8px; +.file.active { + background: #F5F8FF; + border: 1px solid #D1E0FF; + box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05); } -.file.uploading:hover .actionWrapper .buttonWrapper { - display: flex; - align-items: center; +.file:hover { + background: #F5F8FF; + border: 1px solid #D1E0FF; + box-shadow: 0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06); } + .fileIcon { - @apply w-8 h-8 bg-center bg-no-repeat; - position: absolute; - top: 24px; - left: 24px; + @apply shrink-0 w-6 h-6 mr-2 bg-center bg-no-repeat; background-image: url(../assets/unknow.svg); - background-size: 32px; + background-size: 24px; } .fileIcon.csv { background-image: url(../assets/csv.svg); @@ -126,7 +121,7 @@ background-image: url(../assets/json.svg); } .fileInfo { - @apply grow; + @apply grow flex items-center; z-index: 1; overflow: hidden; text-overflow: ellipsis; @@ -134,46 +129,37 @@ } .filename { font-weight: 500; - font-size: 14px; - line-height: 20px; -} -.name { + font-size: 13px; + line-height: 18px; color: #1D2939; - line-height: 20px; } -.extension { - color: #667085; - line-height: 20px; -} -.fileExtraInfo { - color: #667085; + +.size { + @apply ml-3; + font-weight: 400; font-size: 12px; line-height: 18px; + color: #667085; } .actionWrapper { @apply flex items-center shrink-0; z-index: 1; } .actionWrapper .percent { - font-size: 16px; - line-height: 24px; + font-weight: 400; + font-size: 13px; + line-height: 18px; color: #344054; } -.actionWrapper .divider { - display: none; - margin: 0 8px; - width: 1px; - height: 16px; - background: #FEE4E2; -} + .actionWrapper .remove { - width: 32px; - height: 32px; + display: none; + width: 24px; + height: 24px; background: center no-repeat url(../assets/trash.svg); background-size: 16px; cursor: pointer; } -.actionWrapper .buttonWrapper { - @apply flex items-center; - display: none; +.file:hover .actionWrapper .remove { + display: block; } diff --git a/web/app/components/datasets/create/file-uploader/index.tsx b/web/app/components/datasets/create/file-uploader/index.tsx index 178bd237ec..3bf590fcc5 100644 --- a/web/app/components/datasets/create/file-uploader/index.tsx +++ b/web/app/components/datasets/create/file-uploader/index.tsx @@ -1,19 +1,21 @@ 'use client' -import React, { useCallback, useEffect, useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import cn from 'classnames' import s from './index.module.css' import type { File as FileEntity } from '@/models/datasets' import { ToastContext } from '@/app/components/base/toast' -import Button from '@/app/components/base/button' import { upload } from '@/service/base' type IFileUploaderProps = { - file?: FileEntity + fileList: any[] titleClassName?: string - onFileUpdate: (file?: FileEntity) => void + prepareFileList: (files: any[]) => void + onFileUpdate: (fileItem: any, progress: number, list: any[]) => void + onFileListUpdate?: (files: any) => void + onPreview: (file: FileEntity) => void } const ACCEPTS = [ @@ -28,19 +30,25 @@ const ACCEPTS = [ '.csv', ] -const MAX_SIZE = 15 * 1024 * 1024 +const MAX_SIZE = 10 * 1024 * 1024 +const BATCH_COUNT = 5 -const FileUploader = ({ file, onFileUpdate, titleClassName }: IFileUploaderProps) => { +const FileUploader = ({ + fileList, + titleClassName, + prepareFileList, + onFileUpdate, + onFileListUpdate, + onPreview, +}: IFileUploaderProps) => { const { t } = useTranslation() const { notify } = useContext(ToastContext) const [dragging, setDragging] = useState(false) const dropRef = useRef(null) const dragRef = useRef(null) const fileUploader = useRef(null) - const uploadPromise = useRef(null) - const [currentFile, setCurrentFile] = useState() - const [uploading, setUploading] = useState(false) - const [percent, setPercent] = useState(0) + + const fileListRef = useRef([]) // utils const getFileType = (currentFile: File) => { @@ -50,10 +58,7 @@ const FileUploader = ({ file, onFileUpdate, titleClassName }: IFileUploaderProps const arr = currentFile.name.split('.') return arr[arr.length - 1] } - const getFileName = (name: string) => { - const arr = name.split('.') - return arr.slice(0, -1).join() - } + const getFileSize = (size: number) => { if (size / 1024 < 10) return `${(size / 1024).toFixed(2)}KB` @@ -74,51 +79,75 @@ const FileUploader = ({ file, onFileUpdate, titleClassName }: IFileUploaderProps return isValidType && isValidSize } - const onProgress = useCallback((e: ProgressEvent) => { - if (e.lengthComputable) { - const percent = Math.floor(e.loaded / e.total * 100) - setPercent(percent) - } - }, [setPercent]) - const abort = () => { - const currentXHR = uploadPromise.current - currentXHR.abort() - } - const fileUpload = async (file?: File) => { - if (!file) - return - if (!isValid(file)) - return - - setCurrentFile(file) - setUploading(true) + const fileUpload = async (fileItem: any) => { const formData = new FormData() - formData.append('file', file) - // store for abort - const currentXHR = new XMLHttpRequest() - uploadPromise.current = currentXHR - try { - const result = await upload({ - xhr: currentXHR, - data: formData, - onprogress: onProgress, - }) as FileEntity - onFileUpdate(result) - setUploading(false) - } - catch (xhr: any) { - setUploading(false) - // abort handle - if (xhr.readyState === 0 && xhr.status === 0) { - if (fileUploader.current) - fileUploader.current.value = '' - - setCurrentFile(undefined) - return + formData.append('file', fileItem.file) + const onProgress = (e: ProgressEvent) => { + if (e.lengthComputable) { + const percent = Math.floor(e.loaded / e.total * 100) + onFileUpdate(fileItem, percent, fileListRef.current) } - notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.failed') }) } + + return upload({ + xhr: new XMLHttpRequest(), + data: formData, + onprogress: onProgress, + }) + .then((res: FileEntity) => { + const fileListCopy = fileListRef.current + + const completeFile = { + fileID: fileItem.fileID, + file: res, + } + const index = fileListCopy.findIndex((item: any) => item.fileID === fileItem.fileID) + fileListCopy[index] = completeFile + onFileUpdate(completeFile, 100, fileListCopy) + return Promise.resolve({ ...completeFile }) + }) + .catch(() => { + notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.failed') }) + onFileUpdate(fileItem, -2, fileListCopy) + return Promise.resolve({ ...fileItem }) + }) + .finally() + } + const uploadBatchFiles = (bFiles: any) => { + bFiles.forEach((bf: any) => (bf.progress = 0)) + return Promise.all(bFiles.map((bFile: any) => fileUpload(bFile))) + } + const uploadMultipleFiles = async (files: any) => { + const length = files.length + let start = 0 + let end = 0 + + while (start < length) { + if (start + BATCH_COUNT > length) + end = length + else + end = start + BATCH_COUNT + const bFiles = files.slice(start, end) + await uploadBatchFiles(bFiles) + start = end + } + } + const initialUpload = (files: any) => { + if (!files.length) + return false + const preparedFiles = files.map((file: any, index: number) => { + const fileItem = { + fileID: `file${index}-${Date.now()}`, + file, + progress: -1, + } + return fileItem + }) + const newFiles = [...fileListRef.current, ...preparedFiles] + prepareFileList(newFiles) + fileListRef.current = newFiles + uploadMultipleFiles(preparedFiles) } const handleDragEnter = (e: DragEvent) => { e.preventDefault() @@ -134,6 +163,7 @@ const FileUploader = ({ file, onFileUpdate, titleClassName }: IFileUploaderProps e.stopPropagation() e.target === dragRef.current && setDragging(false) } + const handleDrop = (e: DragEvent) => { e.preventDefault() e.stopPropagation() @@ -142,29 +172,26 @@ const FileUploader = ({ file, onFileUpdate, titleClassName }: IFileUploaderProps return const files = [...e.dataTransfer.files] - if (files.length > 1) { - notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.count') }) - return - } - onFileUpdate() - fileUpload(files[0]) + const validFiles = files.filter(file => isValid(file)) + // fileUpload(files[0]) + initialUpload(validFiles) } const selectHandle = () => { if (fileUploader.current) fileUploader.current.click() } - const removeFile = () => { + + const removeFile = (fileID: string) => { if (fileUploader.current) fileUploader.current.value = '' - setCurrentFile(undefined) - onFileUpdate() + fileListRef.current = fileListRef.current.filter((item: any) => item.fileID !== fileID) + onFileListUpdate?.([...fileListRef.current]) } const fileChangeHandle = (e: React.ChangeEvent) => { - const currentFile = e.target.files?.[0] - onFileUpdate() - fileUpload(currentFile) + const files = [...(e.target.files ?? [])].filter(file => isValid(file)) + initialUpload(files) } useEffect(() => { @@ -184,83 +211,83 @@ const FileUploader = ({ file, onFileUpdate, titleClassName }: IFileUploaderProps
{t('datasetCreation.stepOne.uploader.title')}
-
- {!currentFile && !file && ( -
- {t('datasetCreation.stepOne.uploader.button')} - - {dragging &&
} -
- )} +
+
+ + {t('datasetCreation.stepOne.uploader.button')} + +
+
{t('datasetCreation.stepOne.uploader.tip')}
+ {dragging &&
}
- {currentFile && ( -
- {uploading && ( -
- )} -
-
-
- {getFileName(currentFile.name)} - {`.${getFileType(currentFile)}`} +
+ {fileList.map((fileItem, index) => ( +
fileItem.file?.id && onPreview(fileItem.file)} + className={cn( + s.file, + fileItem.progress < 100 && s.uploading, + // s.active, + )} + > + {fileItem.progress < 100 && ( +
+ )} +
+
+
{fileItem.file.name}
+
{getFileSize(fileItem.file.size)}
-
- {getFileSize(currentFile.size)} - +
+ {(fileItem.progress < 100 && fileItem.progress >= 0) && ( +
{`${fileItem.progress}%`}
+ )} + {fileItem.progress === 100 && ( +
{ + e.stopPropagation() + removeFile(fileItem.fileID) + }}/> + )}
-
+ ))} + {/* {currentFile && ( +
onPreview(currentFile)} + className={cn( + s.file, + uploading && s.uploading, + // s.active, + )} + > {uploading && ( - <> +
+ )} +
+
+
{currentFile.name}
+
{getFileSize(currentFile.size)}
+
+
+ {uploading && (
{`${percent}%`}
-
-
- -
- - )} - {!uploading && ( - <> -
- -
-
-
- - )} -
-
- )} - {!currentFile && file && ( -
-
-
-
- {getFileName(file.name)} - {`.${file.extension}`} -
-
- {getFileSize(file.size)} - + )} + {!uploading && ( +
removeFile(index)}/> + )}
-
-
- -
-
-
-
-
- )} -
{t('datasetCreation.stepOne.uploader.tip')}
+ )} */} +
) } diff --git a/web/app/components/datasets/create/index.tsx b/web/app/components/datasets/create/index.tsx index 54e55e930b..dd33ae7b66 100644 --- a/web/app/components/datasets/create/index.tsx +++ b/web/app/components/datasets/create/index.tsx @@ -8,7 +8,7 @@ import StepOne from './step-one' import StepTwo from './step-two' import StepThree from './step-three' import { DataSourceType } from '@/models/datasets' -import type { DataSet, File, createDocumentResponse } from '@/models/datasets' +import type { DataSet, createDocumentResponse } from '@/models/datasets' import { fetchDataSource, fetchTenantInfo } from '@/service/common' import { fetchDataDetail } from '@/service/datasets' import type { DataSourceNotionPage } from '@/models/common' @@ -30,7 +30,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { const [dataSourceType, setDataSourceType] = useState(DataSourceType.FILE) const [step, setStep] = useState(1) const [indexingTypeCache, setIndexTypeCache] = useState('') - const [file, setFile] = useState() + const [fileList, setFiles] = useState([]) const [result, setResult] = useState() const [hasError, setHasError] = useState(false) @@ -39,8 +39,28 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { setNotionPages(value) } - const updateFile = (file?: File) => { - setFile(file) + const updateFileList = (preparedFiles: any) => { + setFiles(preparedFiles) + } + + const updateFile = (fileItem: any, progress: number, list: any[]) => { + const targetIndex = list.findIndex((file: any) => file.fileID === fileItem.fileID) + list[targetIndex] = { + ...list[targetIndex], + progress, + } + setFiles([...list]) + // use follow code would cause dirty list update problem + // const newList = list.map((file) => { + // if (file.fileID === fileItem.fileID) { + // return { + // ...fileItem, + // progress, + // } + // } + // return file + // }) + // setFiles(newList) } const updateIndexingTypeCache = (type: string) => { setIndexTypeCache(type) @@ -104,8 +124,9 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { dataSourceType={dataSourceType} dataSourceTypeDisable={!!detail?.data_source_type} changeType={setDataSourceType} - file={file} + files={fileList} updateFile={updateFile} + updateFileList={updateFileList} notionPages={notionPages} updateNotionPages={updateNotionPages} onStepChange={nextStep} @@ -116,7 +137,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { indexingType={detail?.indexing_technique || ''} datasetId={datasetId} dataSourceType={dataSourceType} - file={file} + files={fileList.map(file => file.file)} notionPages={notionPages} onStepChange={changeStep} updateIndexingTypeCache={updateIndexingTypeCache} diff --git a/web/app/components/datasets/create/step-one/index.module.css b/web/app/components/datasets/create/step-one/index.module.css index f2e2c85238..3e2035f4d4 100644 --- a/web/app/components/datasets/create/step-one/index.module.css +++ b/web/app/components/datasets/create/step-one/index.module.css @@ -10,7 +10,9 @@ } .form { + position: relative; padding: 12px 64px; + background-color: #fff; } .dataSourceTypeList { diff --git a/web/app/components/datasets/create/step-one/index.tsx b/web/app/components/datasets/create/step-one/index.tsx index aae31bbcfb..10d6030303 100644 --- a/web/app/components/datasets/create/step-one/index.tsx +++ b/web/app/components/datasets/create/step-one/index.tsx @@ -1,5 +1,5 @@ 'use client' -import React, { useState } from 'react' +import React, { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import cn from 'classnames' import FilePreview from '../file-preview' @@ -20,8 +20,9 @@ type IStepOneProps = { dataSourceTypeDisable: Boolean hasConnection: boolean onSetting: () => void - file?: File - updateFile: (file?: File) => void + files: any[] + updateFileList: (files: any[]) => void + updateFile: (fileItem: any, progress: number, list: any[]) => void notionPages?: any[] updateNotionPages: (value: any[]) => void onStepChange: () => void @@ -54,23 +55,28 @@ const StepOne = ({ hasConnection, onSetting, onStepChange, - file, + files, + updateFileList, updateFile, notionPages = [], updateNotionPages, }: IStepOneProps) => { const { dataset } = useDatasetDetailContext() const [showModal, setShowModal] = useState(false) - const [showFilePreview, setShowFilePreview] = useState(true) + const [currentFile, setCurrentFile] = useState() const [currentNotionPage, setCurrentNotionPage] = useState() const { t } = useTranslation() - const hidePreview = () => setShowFilePreview(false) - const modalShowHandle = () => setShowModal(true) - const modalCloseHandle = () => setShowModal(false) + const updateCurrentFile = (file: File) => { + setCurrentFile(file) + } + const hideFilePreview = () => { + setCurrentNotionPage(undefined) + } + const updateCurrentPage = (page: Page) => { setCurrentNotionPage(page) } @@ -81,6 +87,13 @@ const StepOne = ({ const shouldShowDataSourceTypeList = !datasetId || (datasetId && !dataset?.data_source_type) + const nextDisabled = useMemo(() => { + if (!files.length) + return true + if (files.some(file => !file.file.id)) + return true + return false + }, [files]) return (
@@ -103,7 +116,8 @@ const StepOne = ({ if (dataSourceTypeDisable) return changeType(DataSourceType.FILE) - hidePreview() + hideFilePreview() + hideNotionPagePreview() }} > @@ -119,7 +133,8 @@ const StepOne = ({ if (dataSourceTypeDisable) return changeType(DataSourceType.NOTION) - hidePreview() + hideFilePreview() + hideNotionPagePreview() }} > @@ -138,8 +153,15 @@ const StepOne = ({ } {dataSourceType === DataSourceType.FILE && ( <> - - + + )} {dataSourceType === DataSourceType.NOTION && ( @@ -164,7 +186,7 @@ const StepOne = ({
- {file && showFilePreview && } + {currentFile && } {currentNotionPage && }
) diff --git a/web/app/components/datasets/create/step-two/index.tsx b/web/app/components/datasets/create/step-two/index.tsx index 8635aefb3b..412fa4ba01 100644 --- a/web/app/components/datasets/create/step-two/index.tsx +++ b/web/app/components/datasets/create/step-two/index.tsx @@ -36,7 +36,7 @@ type StepTwoProps = { datasetId?: string indexingType?: string dataSourceType: DataSourceType - file?: File + files: File[] notionPages?: Page[] onStepChange?: (delta: number) => void updateIndexingTypeCache?: (type: string) => void @@ -62,7 +62,7 @@ const StepTwo = ({ datasetId, indexingType, dataSourceType, - file, + files, notionPages = [], onStepChange, updateIndexingTypeCache, @@ -212,8 +212,7 @@ const StepTwo = ({ info_list: { data_source_type: dataSourceType, file_info_list: { - // TODO multi files - file_ids: [file?.id || ''], + file_ids: files.map(file => file.id), }, }, indexing_technique: getIndexing_technique(), @@ -254,8 +253,7 @@ const StepTwo = ({ } as CreateDocumentReq if (dataSourceType === DataSourceType.FILE) { params.data_source.info_list.file_info_list = { - // TODO multi files - file_ids: [file?.id || ''], + file_ids: files.map(file => file.id), } } if (dataSourceType === DataSourceType.NOTION) @@ -529,15 +527,21 @@ const StepTwo = ({ {t('datasetCreation.stepTwo.datasetSettingLink')}
)} - {/* TODO multi files */}
{dataSourceType === DataSourceType.FILE && ( <>
{t('datasetCreation.stepTwo.fileSource')}
- - {getFileName(file?.name || '')} + + {getFileName(files[0].name || '')} + {files.length > 1 && ( + + {t('datasetCreation.stepTwo.other')} + {files.length - 1} + {t('datasetCreation.stepTwo.fileUnit')} + + )}
)} diff --git a/web/app/components/datasets/documents/detail/settings/index.tsx b/web/app/components/datasets/documents/detail/settings/index.tsx index 43d9c9491e..5bc4e8e517 100644 --- a/web/app/components/datasets/documents/detail/settings/index.tsx +++ b/web/app/components/datasets/documents/detail/settings/index.tsx @@ -85,7 +85,7 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => { indexingType={indexingTechnique || ''} isSetting documentDetail={documentDetail} - file={documentDetail.data_source_info.upload_file} + files={[documentDetail.data_source_info.upload_file]} onSave={saveHandler} onCancel={cancelHandler} /> diff --git a/web/i18n/lang/dataset-creation.en.ts b/web/i18n/lang/dataset-creation.en.ts index 04b815d33e..16d877c8cd 100644 --- a/web/i18n/lang/dataset-creation.en.ts +++ b/web/i18n/lang/dataset-creation.en.ts @@ -23,10 +23,10 @@ const translation = { title: 'Upload text file', button: 'Drag and drop file, or', browse: 'Browse', - tip: 'Supports txt, html, markdown, xlsx, and pdf.', + tip: 'Supports txt, html, markdown, xlsx, and pdf. Max 10MB each.', validation: { typeError: 'File type not supported', - size: 'File too large. Maximum is 15MB', + size: 'File too large. Maximum is 10MB', count: 'Multiple files not supported', }, cancel: 'Cancel', diff --git a/web/i18n/lang/dataset-creation.zh.ts b/web/i18n/lang/dataset-creation.zh.ts index e501334f1a..4ec0f8cdf9 100644 --- a/web/i18n/lang/dataset-creation.zh.ts +++ b/web/i18n/lang/dataset-creation.zh.ts @@ -23,10 +23,10 @@ const translation = { title: '上传文本文件', button: '拖拽文件至此,或者', browse: '选择文件', - tip: '已支持 TXT, HTML, Markdown, PDF, XLSX', + tip: '已支持 TXT、 HTML、 Markdown、 PDF、 XLSX,每个文件不超过 10 MB。', validation: { typeError: '文件类型不支持', - size: '文件太大了,不能超过 15MB', + size: '文件太大了,不能超过 10MB', count: '暂不支持多个文件', }, cancel: '取消',