mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 11:42:29 +08:00
feat: support batch upload files (#419)
This commit is contained in:
parent
8a4d19d9ba
commit
d637a147ee
|
@ -9,7 +9,6 @@ import { fetchFilePreview } from '@/service/common'
|
||||||
|
|
||||||
type IProps = {
|
type IProps = {
|
||||||
file?: File
|
file?: File
|
||||||
notionPage?: any
|
|
||||||
hidePreview: () => void
|
hidePreview: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,14 +32,15 @@ const FilePreview = ({
|
||||||
const getFileName = (currentFile?: File) => {
|
const getFileName = (currentFile?: File) => {
|
||||||
if (!currentFile)
|
if (!currentFile)
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
const arr = currentFile.name.split('.')
|
const arr = currentFile.name.split('.')
|
||||||
return arr.slice(0, -1).join()
|
return arr.slice(0, -1).join()
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (file)
|
if (file) {
|
||||||
|
setLoading(true)
|
||||||
getPreviewContent(file.id)
|
getPreviewContent(file.id)
|
||||||
|
}
|
||||||
}, [file])
|
}, [file])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
.fileUploader {
|
.fileUploader {
|
||||||
@apply mb-9;
|
@apply mb-6;
|
||||||
}
|
}
|
||||||
.fileUploader .title {
|
.fileUploader .title {
|
||||||
@apply mb-2;
|
@apply mb-2;
|
||||||
|
@ -9,14 +9,14 @@
|
||||||
color: #344054;
|
color: #344054;
|
||||||
}
|
}
|
||||||
.fileUploader .tip {
|
.fileUploader .tip {
|
||||||
@apply mt-2;
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 26px;
|
line-height: 18px;
|
||||||
color: #667085;
|
color: #667085;
|
||||||
}
|
}
|
||||||
.uploader {
|
.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;
|
max-width: 640px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
background: #F9FAFB;
|
background: #F9FAFB;
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.uploader::before {
|
.uploader .uploadIcon {
|
||||||
content: '';
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
@ -51,16 +51,20 @@
|
||||||
@apply pl-1 cursor-pointer;
|
@apply pl-1 cursor-pointer;
|
||||||
color: #155eef;
|
color: #155eef;
|
||||||
}
|
}
|
||||||
|
.fileList {
|
||||||
|
@apply space-y-2;
|
||||||
|
}
|
||||||
.file {
|
.file {
|
||||||
@apply box-border relative flex items-center;
|
@apply box-border relative flex items-center justify-between;
|
||||||
padding: 21px 24px 21px 64px;
|
padding: 8px 12px 8px 8px;
|
||||||
max-width: 640px;
|
max-width: 640px;
|
||||||
height: 80px;
|
height: 40px;
|
||||||
background: #F9FAFB;
|
background: #ffffff;
|
||||||
border: 1px solid #F2F4F7;
|
border: 0.5px solid #EAECF0;
|
||||||
border-radius: 12px;
|
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.progressbar {
|
.progressbar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -69,36 +73,27 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #F2F4F7;
|
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,
|
||||||
.file.uploading:hover {
|
.file.uploading:hover {
|
||||||
background: #FCFCFD;
|
background: #FCFCFD;
|
||||||
border: 1px solid #EAECF0;
|
border: 0.5px solid #EAECF0;
|
||||||
}
|
}
|
||||||
.file.uploading:hover .actionWrapper .percent {
|
.file.active {
|
||||||
padding: 8px;
|
background: #F5F8FF;
|
||||||
|
border: 1px solid #D1E0FF;
|
||||||
|
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||||
}
|
}
|
||||||
.file.uploading:hover .actionWrapper .buttonWrapper {
|
.file:hover {
|
||||||
display: flex;
|
background: #F5F8FF;
|
||||||
align-items: center;
|
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 {
|
.fileIcon {
|
||||||
@apply w-8 h-8 bg-center bg-no-repeat;
|
@apply shrink-0 w-6 h-6 mr-2 bg-center bg-no-repeat;
|
||||||
position: absolute;
|
|
||||||
top: 24px;
|
|
||||||
left: 24px;
|
|
||||||
background-image: url(../assets/unknow.svg);
|
background-image: url(../assets/unknow.svg);
|
||||||
background-size: 32px;
|
background-size: 24px;
|
||||||
}
|
}
|
||||||
.fileIcon.csv {
|
.fileIcon.csv {
|
||||||
background-image: url(../assets/csv.svg);
|
background-image: url(../assets/csv.svg);
|
||||||
|
@ -126,7 +121,7 @@
|
||||||
background-image: url(../assets/json.svg);
|
background-image: url(../assets/json.svg);
|
||||||
}
|
}
|
||||||
.fileInfo {
|
.fileInfo {
|
||||||
@apply grow;
|
@apply grow flex items-center;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -134,46 +129,37 @@
|
||||||
}
|
}
|
||||||
.filename {
|
.filename {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
line-height: 20px;
|
line-height: 18px;
|
||||||
}
|
|
||||||
.name {
|
|
||||||
color: #1D2939;
|
color: #1D2939;
|
||||||
line-height: 20px;
|
|
||||||
}
|
}
|
||||||
.extension {
|
|
||||||
color: #667085;
|
.size {
|
||||||
line-height: 20px;
|
@apply ml-3;
|
||||||
}
|
font-weight: 400;
|
||||||
.fileExtraInfo {
|
|
||||||
color: #667085;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
|
color: #667085;
|
||||||
}
|
}
|
||||||
.actionWrapper {
|
.actionWrapper {
|
||||||
@apply flex items-center shrink-0;
|
@apply flex items-center shrink-0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.actionWrapper .percent {
|
.actionWrapper .percent {
|
||||||
font-size: 16px;
|
font-weight: 400;
|
||||||
line-height: 24px;
|
font-size: 13px;
|
||||||
|
line-height: 18px;
|
||||||
color: #344054;
|
color: #344054;
|
||||||
}
|
}
|
||||||
.actionWrapper .divider {
|
|
||||||
display: none;
|
|
||||||
margin: 0 8px;
|
|
||||||
width: 1px;
|
|
||||||
height: 16px;
|
|
||||||
background: #FEE4E2;
|
|
||||||
}
|
|
||||||
.actionWrapper .remove {
|
.actionWrapper .remove {
|
||||||
width: 32px;
|
display: none;
|
||||||
height: 32px;
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
background: center no-repeat url(../assets/trash.svg);
|
background: center no-repeat url(../assets/trash.svg);
|
||||||
background-size: 16px;
|
background-size: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.actionWrapper .buttonWrapper {
|
.file:hover .actionWrapper .remove {
|
||||||
@apply flex items-center;
|
display: block;
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
'use client'
|
'use client'
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useContext } from 'use-context-selector'
|
import { useContext } from 'use-context-selector'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import s from './index.module.css'
|
import s from './index.module.css'
|
||||||
import type { File as FileEntity } from '@/models/datasets'
|
import type { File as FileEntity } from '@/models/datasets'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import Button from '@/app/components/base/button'
|
|
||||||
|
|
||||||
import { upload } from '@/service/base'
|
import { upload } from '@/service/base'
|
||||||
|
|
||||||
type IFileUploaderProps = {
|
type IFileUploaderProps = {
|
||||||
file?: FileEntity
|
fileList: any[]
|
||||||
titleClassName?: string
|
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 = [
|
const ACCEPTS = [
|
||||||
|
@ -28,19 +30,25 @@ const ACCEPTS = [
|
||||||
'.csv',
|
'.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 { t } = useTranslation()
|
||||||
const { notify } = useContext(ToastContext)
|
const { notify } = useContext(ToastContext)
|
||||||
const [dragging, setDragging] = useState(false)
|
const [dragging, setDragging] = useState(false)
|
||||||
const dropRef = useRef<HTMLDivElement>(null)
|
const dropRef = useRef<HTMLDivElement>(null)
|
||||||
const dragRef = useRef<HTMLDivElement>(null)
|
const dragRef = useRef<HTMLDivElement>(null)
|
||||||
const fileUploader = useRef<HTMLInputElement>(null)
|
const fileUploader = useRef<HTMLInputElement>(null)
|
||||||
const uploadPromise = useRef<any>(null)
|
|
||||||
const [currentFile, setCurrentFile] = useState<File>()
|
const fileListRef = useRef<any>([])
|
||||||
const [uploading, setUploading] = useState(false)
|
|
||||||
const [percent, setPercent] = useState(0)
|
|
||||||
|
|
||||||
// utils
|
// utils
|
||||||
const getFileType = (currentFile: File) => {
|
const getFileType = (currentFile: File) => {
|
||||||
|
@ -50,10 +58,7 @@ const FileUploader = ({ file, onFileUpdate, titleClassName }: IFileUploaderProps
|
||||||
const arr = currentFile.name.split('.')
|
const arr = currentFile.name.split('.')
|
||||||
return arr[arr.length - 1]
|
return arr[arr.length - 1]
|
||||||
}
|
}
|
||||||
const getFileName = (name: string) => {
|
|
||||||
const arr = name.split('.')
|
|
||||||
return arr.slice(0, -1).join()
|
|
||||||
}
|
|
||||||
const getFileSize = (size: number) => {
|
const getFileSize = (size: number) => {
|
||||||
if (size / 1024 < 10)
|
if (size / 1024 < 10)
|
||||||
return `${(size / 1024).toFixed(2)}KB`
|
return `${(size / 1024).toFixed(2)}KB`
|
||||||
|
@ -74,51 +79,75 @@ const FileUploader = ({ file, onFileUpdate, titleClassName }: IFileUploaderProps
|
||||||
|
|
||||||
return isValidType && isValidSize
|
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))
|
const fileUpload = async (fileItem: any) => {
|
||||||
return
|
|
||||||
|
|
||||||
setCurrentFile(file)
|
|
||||||
setUploading(true)
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', fileItem.file)
|
||||||
// store for abort
|
const onProgress = (e: ProgressEvent) => {
|
||||||
const currentXHR = new XMLHttpRequest()
|
if (e.lengthComputable) {
|
||||||
uploadPromise.current = currentXHR
|
const percent = Math.floor(e.loaded / e.total * 100)
|
||||||
try {
|
onFileUpdate(fileItem, percent, fileListRef.current)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
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) => {
|
const handleDragEnter = (e: DragEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
@ -134,6 +163,7 @@ const FileUploader = ({ file, onFileUpdate, titleClassName }: IFileUploaderProps
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.target === dragRef.current && setDragging(false)
|
e.target === dragRef.current && setDragging(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDrop = (e: DragEvent) => {
|
const handleDrop = (e: DragEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
@ -142,29 +172,26 @@ const FileUploader = ({ file, onFileUpdate, titleClassName }: IFileUploaderProps
|
||||||
return
|
return
|
||||||
|
|
||||||
const files = [...e.dataTransfer.files]
|
const files = [...e.dataTransfer.files]
|
||||||
if (files.length > 1) {
|
const validFiles = files.filter(file => isValid(file))
|
||||||
notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.count') })
|
// fileUpload(files[0])
|
||||||
return
|
initialUpload(validFiles)
|
||||||
}
|
|
||||||
onFileUpdate()
|
|
||||||
fileUpload(files[0])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectHandle = () => {
|
const selectHandle = () => {
|
||||||
if (fileUploader.current)
|
if (fileUploader.current)
|
||||||
fileUploader.current.click()
|
fileUploader.current.click()
|
||||||
}
|
}
|
||||||
const removeFile = () => {
|
|
||||||
|
const removeFile = (fileID: string) => {
|
||||||
if (fileUploader.current)
|
if (fileUploader.current)
|
||||||
fileUploader.current.value = ''
|
fileUploader.current.value = ''
|
||||||
|
|
||||||
setCurrentFile(undefined)
|
fileListRef.current = fileListRef.current.filter((item: any) => item.fileID !== fileID)
|
||||||
onFileUpdate()
|
onFileListUpdate?.([...fileListRef.current])
|
||||||
}
|
}
|
||||||
const fileChangeHandle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const fileChangeHandle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const currentFile = e.target.files?.[0]
|
const files = [...(e.target.files ?? [])].filter(file => isValid(file))
|
||||||
onFileUpdate()
|
initialUpload(files)
|
||||||
fileUpload(currentFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -184,83 +211,83 @@ const FileUploader = ({ file, onFileUpdate, titleClassName }: IFileUploaderProps
|
||||||
<div className={s.fileUploader}>
|
<div className={s.fileUploader}>
|
||||||
<input
|
<input
|
||||||
ref={fileUploader}
|
ref={fileUploader}
|
||||||
|
id="fileUploader"
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
type="file"
|
type="file"
|
||||||
id="fileUploader"
|
multiple
|
||||||
accept={ACCEPTS.join(',')}
|
accept={ACCEPTS.join(',')}
|
||||||
onChange={fileChangeHandle}
|
onChange={fileChangeHandle}
|
||||||
/>
|
/>
|
||||||
<div className={cn(s.title, titleClassName)}>{t('datasetCreation.stepOne.uploader.title')}</div>
|
<div className={cn(s.title, titleClassName)}>{t('datasetCreation.stepOne.uploader.title')}</div>
|
||||||
<div ref={dropRef}>
|
<div ref={dropRef} className={cn(s.uploader, dragging && s.dragging)}>
|
||||||
{!currentFile && !file && (
|
<div className='flex justify-center items-center h-6 mb-2'>
|
||||||
<div className={cn(s.uploader, dragging && s.dragging)}>
|
<span className={s.uploadIcon}/>
|
||||||
<span>{t('datasetCreation.stepOne.uploader.button')}</span>
|
<span>{t('datasetCreation.stepOne.uploader.button')}</span>
|
||||||
<label className={s.browse} onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.browse')}</label>
|
<label className={s.browse} onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.browse')}</label>
|
||||||
{dragging && <div ref={dragRef} className={s.draggingCover}/>}
|
</div>
|
||||||
</div>
|
<div className={s.tip}>{t('datasetCreation.stepOne.uploader.tip')}</div>
|
||||||
)}
|
{dragging && <div ref={dragRef} className={s.draggingCover}/>}
|
||||||
</div>
|
</div>
|
||||||
{currentFile && (
|
<div className={s.fileList}>
|
||||||
<div className={cn(s.file, uploading && s.uploading)}>
|
{fileList.map((fileItem, index) => (
|
||||||
{uploading && (
|
<div
|
||||||
<div className={s.progressbar} style={{ width: `${percent}%` }}/>
|
key={`${fileItem.fileID}-${index}`}
|
||||||
)}
|
onClick={() => fileItem.file?.id && onPreview(fileItem.file)}
|
||||||
<div className={cn(s.fileIcon, s[getFileType(currentFile)])}/>
|
className={cn(
|
||||||
<div className={s.fileInfo}>
|
s.file,
|
||||||
<div className={s.filename}>
|
fileItem.progress < 100 && s.uploading,
|
||||||
<span className={s.name}>{getFileName(currentFile.name)}</span>
|
// s.active,
|
||||||
<span className={s.extension}>{`.${getFileType(currentFile)}`}</span>
|
)}
|
||||||
|
>
|
||||||
|
{fileItem.progress < 100 && (
|
||||||
|
<div className={s.progressbar} style={{ width: `${fileItem.progress}%` }}/>
|
||||||
|
)}
|
||||||
|
<div className={s.fileInfo}>
|
||||||
|
<div className={cn(s.fileIcon, s[getFileType(fileItem.file)])}/>
|
||||||
|
<div className={s.filename}>{fileItem.file.name}</div>
|
||||||
|
<div className={s.size}>{getFileSize(fileItem.file.size)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={s.fileExtraInfo}>
|
<div className={s.actionWrapper}>
|
||||||
<span className={s.size}>{getFileSize(currentFile.size)}</span>
|
{(fileItem.progress < 100 && fileItem.progress >= 0) && (
|
||||||
<span className={s.error}></span>
|
<div className={s.percent}>{`${fileItem.progress}%`}</div>
|
||||||
|
)}
|
||||||
|
{fileItem.progress === 100 && (
|
||||||
|
<div className={s.remove} onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
removeFile(fileItem.fileID)
|
||||||
|
}}/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={s.actionWrapper}>
|
))}
|
||||||
|
{/* {currentFile && (
|
||||||
|
<div
|
||||||
|
// onClick={() => onPreview(currentFile)}
|
||||||
|
className={cn(
|
||||||
|
s.file,
|
||||||
|
uploading && s.uploading,
|
||||||
|
// s.active,
|
||||||
|
)}
|
||||||
|
>
|
||||||
{uploading && (
|
{uploading && (
|
||||||
<>
|
<div className={s.progressbar} style={{ width: `${percent}%` }}/>
|
||||||
|
)}
|
||||||
|
<div className={s.fileInfo}>
|
||||||
|
<div className={cn(s.fileIcon, s[getFileType(currentFile)])}/>
|
||||||
|
<div className={s.filename}>{currentFile.name}</div>
|
||||||
|
<div className={s.size}>{getFileSize(currentFile.size)}</div>
|
||||||
|
</div>
|
||||||
|
<div className={s.actionWrapper}>
|
||||||
|
{uploading && (
|
||||||
<div className={s.percent}>{`${percent}%`}</div>
|
<div className={s.percent}>{`${percent}%`}</div>
|
||||||
<div className={s.divider}/>
|
)}
|
||||||
<div className={s.buttonWrapper}>
|
{!uploading && (
|
||||||
<Button className={cn(s.button, 'ml-2 !h-8 bg-white')} onClick={abort}>{t('datasetCreation.stepOne.uploader.cancel')}</Button>
|
<div className={s.remove} onClick={() => removeFile(index)}/>
|
||||||
</div>
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!uploading && (
|
|
||||||
<>
|
|
||||||
<div className={s.buttonWrapper}>
|
|
||||||
<Button className={cn(s.button, 'ml-2 !h-8 bg-white')} onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
|
|
||||||
<div className={s.divider}/>
|
|
||||||
<div className={s.remove} onClick={removeFile}/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!currentFile && file && (
|
|
||||||
<div className={cn(s.file)}>
|
|
||||||
<div className={cn(s.fileIcon, s[file.extension])}/>
|
|
||||||
<div className={s.fileInfo}>
|
|
||||||
<div className={s.filename}>
|
|
||||||
<span className={s.name}>{getFileName(file.name)}</span>
|
|
||||||
<span className={s.extension}>{`.${file.extension}`}</span>
|
|
||||||
</div>
|
|
||||||
<div className={s.fileExtraInfo}>
|
|
||||||
<span className={s.size}>{getFileSize(file.size)}</span>
|
|
||||||
<span className={s.error}></span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={s.actionWrapper}>
|
)} */}
|
||||||
<div className={s.buttonWrapper}>
|
</div>
|
||||||
<Button className={cn(s.button, 'ml-2 !h-8 bg-white')} onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
|
|
||||||
<div className={s.divider}/>
|
|
||||||
<div className={s.remove} onClick={removeFile}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={s.tip}>{t('datasetCreation.stepOne.uploader.tip')}</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import StepOne from './step-one'
|
||||||
import StepTwo from './step-two'
|
import StepTwo from './step-two'
|
||||||
import StepThree from './step-three'
|
import StepThree from './step-three'
|
||||||
import { DataSourceType } from '@/models/datasets'
|
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 { fetchDataSource, fetchTenantInfo } from '@/service/common'
|
||||||
import { fetchDataDetail } from '@/service/datasets'
|
import { fetchDataDetail } from '@/service/datasets'
|
||||||
import type { DataSourceNotionPage } from '@/models/common'
|
import type { DataSourceNotionPage } from '@/models/common'
|
||||||
|
@ -30,7 +30,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
|
||||||
const [dataSourceType, setDataSourceType] = useState<DataSourceType>(DataSourceType.FILE)
|
const [dataSourceType, setDataSourceType] = useState<DataSourceType>(DataSourceType.FILE)
|
||||||
const [step, setStep] = useState(1)
|
const [step, setStep] = useState(1)
|
||||||
const [indexingTypeCache, setIndexTypeCache] = useState('')
|
const [indexingTypeCache, setIndexTypeCache] = useState('')
|
||||||
const [file, setFile] = useState<File | undefined>()
|
const [fileList, setFiles] = useState<any[]>([])
|
||||||
const [result, setResult] = useState<createDocumentResponse | undefined>()
|
const [result, setResult] = useState<createDocumentResponse | undefined>()
|
||||||
const [hasError, setHasError] = useState(false)
|
const [hasError, setHasError] = useState(false)
|
||||||
|
|
||||||
|
@ -39,8 +39,28 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
|
||||||
setNotionPages(value)
|
setNotionPages(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFile = (file?: File) => {
|
const updateFileList = (preparedFiles: any) => {
|
||||||
setFile(file)
|
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) => {
|
const updateIndexingTypeCache = (type: string) => {
|
||||||
setIndexTypeCache(type)
|
setIndexTypeCache(type)
|
||||||
|
@ -104,8 +124,9 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
|
||||||
dataSourceType={dataSourceType}
|
dataSourceType={dataSourceType}
|
||||||
dataSourceTypeDisable={!!detail?.data_source_type}
|
dataSourceTypeDisable={!!detail?.data_source_type}
|
||||||
changeType={setDataSourceType}
|
changeType={setDataSourceType}
|
||||||
file={file}
|
files={fileList}
|
||||||
updateFile={updateFile}
|
updateFile={updateFile}
|
||||||
|
updateFileList={updateFileList}
|
||||||
notionPages={notionPages}
|
notionPages={notionPages}
|
||||||
updateNotionPages={updateNotionPages}
|
updateNotionPages={updateNotionPages}
|
||||||
onStepChange={nextStep}
|
onStepChange={nextStep}
|
||||||
|
@ -116,7 +137,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
|
||||||
indexingType={detail?.indexing_technique || ''}
|
indexingType={detail?.indexing_technique || ''}
|
||||||
datasetId={datasetId}
|
datasetId={datasetId}
|
||||||
dataSourceType={dataSourceType}
|
dataSourceType={dataSourceType}
|
||||||
file={file}
|
files={fileList.map(file => file.file)}
|
||||||
notionPages={notionPages}
|
notionPages={notionPages}
|
||||||
onStepChange={changeStep}
|
onStepChange={changeStep}
|
||||||
updateIndexingTypeCache={updateIndexingTypeCache}
|
updateIndexingTypeCache={updateIndexingTypeCache}
|
||||||
|
|
|
@ -10,7 +10,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
|
position: relative;
|
||||||
padding: 12px 64px;
|
padding: 12px 64px;
|
||||||
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dataSourceTypeList {
|
.dataSourceTypeList {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
'use client'
|
'use client'
|
||||||
import React, { useState } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import FilePreview from '../file-preview'
|
import FilePreview from '../file-preview'
|
||||||
|
@ -20,8 +20,9 @@ type IStepOneProps = {
|
||||||
dataSourceTypeDisable: Boolean
|
dataSourceTypeDisable: Boolean
|
||||||
hasConnection: boolean
|
hasConnection: boolean
|
||||||
onSetting: () => void
|
onSetting: () => void
|
||||||
file?: File
|
files: any[]
|
||||||
updateFile: (file?: File) => void
|
updateFileList: (files: any[]) => void
|
||||||
|
updateFile: (fileItem: any, progress: number, list: any[]) => void
|
||||||
notionPages?: any[]
|
notionPages?: any[]
|
||||||
updateNotionPages: (value: any[]) => void
|
updateNotionPages: (value: any[]) => void
|
||||||
onStepChange: () => void
|
onStepChange: () => void
|
||||||
|
@ -54,23 +55,28 @@ const StepOne = ({
|
||||||
hasConnection,
|
hasConnection,
|
||||||
onSetting,
|
onSetting,
|
||||||
onStepChange,
|
onStepChange,
|
||||||
file,
|
files,
|
||||||
|
updateFileList,
|
||||||
updateFile,
|
updateFile,
|
||||||
notionPages = [],
|
notionPages = [],
|
||||||
updateNotionPages,
|
updateNotionPages,
|
||||||
}: IStepOneProps) => {
|
}: IStepOneProps) => {
|
||||||
const { dataset } = useDatasetDetailContext()
|
const { dataset } = useDatasetDetailContext()
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [showModal, setShowModal] = useState(false)
|
||||||
const [showFilePreview, setShowFilePreview] = useState(true)
|
const [currentFile, setCurrentFile] = useState<File | undefined>()
|
||||||
const [currentNotionPage, setCurrentNotionPage] = useState<Page | undefined>()
|
const [currentNotionPage, setCurrentNotionPage] = useState<Page | undefined>()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const hidePreview = () => setShowFilePreview(false)
|
|
||||||
|
|
||||||
const modalShowHandle = () => setShowModal(true)
|
const modalShowHandle = () => setShowModal(true)
|
||||||
|
|
||||||
const modalCloseHandle = () => setShowModal(false)
|
const modalCloseHandle = () => setShowModal(false)
|
||||||
|
|
||||||
|
const updateCurrentFile = (file: File) => {
|
||||||
|
setCurrentFile(file)
|
||||||
|
}
|
||||||
|
const hideFilePreview = () => {
|
||||||
|
setCurrentNotionPage(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
const updateCurrentPage = (page: Page) => {
|
const updateCurrentPage = (page: Page) => {
|
||||||
setCurrentNotionPage(page)
|
setCurrentNotionPage(page)
|
||||||
}
|
}
|
||||||
|
@ -81,6 +87,13 @@ const StepOne = ({
|
||||||
|
|
||||||
const shouldShowDataSourceTypeList = !datasetId || (datasetId && !dataset?.data_source_type)
|
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 (
|
return (
|
||||||
<div className='flex w-full h-full'>
|
<div className='flex w-full h-full'>
|
||||||
<div className='grow overflow-y-auto relative'>
|
<div className='grow overflow-y-auto relative'>
|
||||||
|
@ -103,7 +116,8 @@ const StepOne = ({
|
||||||
if (dataSourceTypeDisable)
|
if (dataSourceTypeDisable)
|
||||||
return
|
return
|
||||||
changeType(DataSourceType.FILE)
|
changeType(DataSourceType.FILE)
|
||||||
hidePreview()
|
hideFilePreview()
|
||||||
|
hideNotionPagePreview()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className={cn(s.datasetIcon)} />
|
<span className={cn(s.datasetIcon)} />
|
||||||
|
@ -119,7 +133,8 @@ const StepOne = ({
|
||||||
if (dataSourceTypeDisable)
|
if (dataSourceTypeDisable)
|
||||||
return
|
return
|
||||||
changeType(DataSourceType.NOTION)
|
changeType(DataSourceType.NOTION)
|
||||||
hidePreview()
|
hideFilePreview()
|
||||||
|
hideNotionPagePreview()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className={cn(s.datasetIcon, s.notion)} />
|
<span className={cn(s.datasetIcon, s.notion)} />
|
||||||
|
@ -138,8 +153,15 @@ const StepOne = ({
|
||||||
}
|
}
|
||||||
{dataSourceType === DataSourceType.FILE && (
|
{dataSourceType === DataSourceType.FILE && (
|
||||||
<>
|
<>
|
||||||
<FileUploader onFileUpdate={updateFile} file={file} titleClassName={(!shouldShowDataSourceTypeList) ? 'mt-[30px] !mb-[44px] !text-lg !font-semibold !text-gray-900' : undefined} />
|
<FileUploader
|
||||||
<Button disabled={!file} className={s.submitButton} type='primary' onClick={onStepChange}>{t('datasetCreation.stepOne.button')}</Button>
|
fileList={files}
|
||||||
|
titleClassName={!shouldShowDataSourceTypeList ? 'mt-[30px] !mb-[44px] !text-lg !font-semibold !text-gray-900' : undefined}
|
||||||
|
prepareFileList={updateFileList}
|
||||||
|
onFileListUpdate={updateFileList}
|
||||||
|
onFileUpdate={updateFile}
|
||||||
|
onPreview={updateCurrentFile}
|
||||||
|
/>
|
||||||
|
<Button disabled={nextDisabled} className={s.submitButton} type='primary' onClick={onStepChange}>{t('datasetCreation.stepOne.button')}</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{dataSourceType === DataSourceType.NOTION && (
|
{dataSourceType === DataSourceType.NOTION && (
|
||||||
|
@ -164,7 +186,7 @@ const StepOne = ({
|
||||||
</div>
|
</div>
|
||||||
<EmptyDatasetCreationModal show={showModal} onHide={modalCloseHandle} />
|
<EmptyDatasetCreationModal show={showModal} onHide={modalCloseHandle} />
|
||||||
</div>
|
</div>
|
||||||
{file && showFilePreview && <FilePreview file={file} hidePreview={hidePreview} />}
|
{currentFile && <FilePreview file={currentFile} hidePreview={hideFilePreview} />}
|
||||||
{currentNotionPage && <NotionPagePreview currentPage={currentNotionPage} hidePreview={hideNotionPagePreview} />}
|
{currentNotionPage && <NotionPagePreview currentPage={currentNotionPage} hidePreview={hideNotionPagePreview} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -36,7 +36,7 @@ type StepTwoProps = {
|
||||||
datasetId?: string
|
datasetId?: string
|
||||||
indexingType?: string
|
indexingType?: string
|
||||||
dataSourceType: DataSourceType
|
dataSourceType: DataSourceType
|
||||||
file?: File
|
files: File[]
|
||||||
notionPages?: Page[]
|
notionPages?: Page[]
|
||||||
onStepChange?: (delta: number) => void
|
onStepChange?: (delta: number) => void
|
||||||
updateIndexingTypeCache?: (type: string) => void
|
updateIndexingTypeCache?: (type: string) => void
|
||||||
|
@ -62,7 +62,7 @@ const StepTwo = ({
|
||||||
datasetId,
|
datasetId,
|
||||||
indexingType,
|
indexingType,
|
||||||
dataSourceType,
|
dataSourceType,
|
||||||
file,
|
files,
|
||||||
notionPages = [],
|
notionPages = [],
|
||||||
onStepChange,
|
onStepChange,
|
||||||
updateIndexingTypeCache,
|
updateIndexingTypeCache,
|
||||||
|
@ -212,8 +212,7 @@ const StepTwo = ({
|
||||||
info_list: {
|
info_list: {
|
||||||
data_source_type: dataSourceType,
|
data_source_type: dataSourceType,
|
||||||
file_info_list: {
|
file_info_list: {
|
||||||
// TODO multi files
|
file_ids: files.map(file => file.id),
|
||||||
file_ids: [file?.id || ''],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
indexing_technique: getIndexing_technique(),
|
indexing_technique: getIndexing_technique(),
|
||||||
|
@ -254,8 +253,7 @@ const StepTwo = ({
|
||||||
} as CreateDocumentReq
|
} as CreateDocumentReq
|
||||||
if (dataSourceType === DataSourceType.FILE) {
|
if (dataSourceType === DataSourceType.FILE) {
|
||||||
params.data_source.info_list.file_info_list = {
|
params.data_source.info_list.file_info_list = {
|
||||||
// TODO multi files
|
file_ids: files.map(file => file.id),
|
||||||
file_ids: [file?.id || ''],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dataSourceType === DataSourceType.NOTION)
|
if (dataSourceType === DataSourceType.NOTION)
|
||||||
|
@ -529,15 +527,21 @@ const StepTwo = ({
|
||||||
<Link className='text-[#155EEF]' href={`/datasets/${datasetId}/settings`}>{t('datasetCreation.stepTwo.datasetSettingLink')}</Link>
|
<Link className='text-[#155EEF]' href={`/datasets/${datasetId}/settings`}>{t('datasetCreation.stepTwo.datasetSettingLink')}</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* TODO multi files */}
|
|
||||||
<div className={s.source}>
|
<div className={s.source}>
|
||||||
<div className={s.sourceContent}>
|
<div className={s.sourceContent}>
|
||||||
{dataSourceType === DataSourceType.FILE && (
|
{dataSourceType === DataSourceType.FILE && (
|
||||||
<>
|
<>
|
||||||
<div className='mb-2 text-xs font-medium text-gray-500'>{t('datasetCreation.stepTwo.fileSource')}</div>
|
<div className='mb-2 text-xs font-medium text-gray-500'>{t('datasetCreation.stepTwo.fileSource')}</div>
|
||||||
<div className='flex items-center text-sm leading-6 font-medium text-gray-800'>
|
<div className='flex items-center text-sm leading-6 font-medium text-gray-800'>
|
||||||
<span className={cn(s.fileIcon, file && s[file.extension])} />
|
<span className={cn(s.fileIcon, files.length && s[files[0].extension])} />
|
||||||
{getFileName(file?.name || '')}
|
{getFileName(files[0].name || '')}
|
||||||
|
{files.length > 1 && (
|
||||||
|
<span className={s.sourceCount}>
|
||||||
|
<span>{t('datasetCreation.stepTwo.other')}</span>
|
||||||
|
<span>{files.length - 1}</span>
|
||||||
|
<span>{t('datasetCreation.stepTwo.fileUnit')}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -85,7 +85,7 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => {
|
||||||
indexingType={indexingTechnique || ''}
|
indexingType={indexingTechnique || ''}
|
||||||
isSetting
|
isSetting
|
||||||
documentDetail={documentDetail}
|
documentDetail={documentDetail}
|
||||||
file={documentDetail.data_source_info.upload_file}
|
files={[documentDetail.data_source_info.upload_file]}
|
||||||
onSave={saveHandler}
|
onSave={saveHandler}
|
||||||
onCancel={cancelHandler}
|
onCancel={cancelHandler}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -23,10 +23,10 @@ const translation = {
|
||||||
title: 'Upload text file',
|
title: 'Upload text file',
|
||||||
button: 'Drag and drop file, or',
|
button: 'Drag and drop file, or',
|
||||||
browse: 'Browse',
|
browse: 'Browse',
|
||||||
tip: 'Supports txt, html, markdown, xlsx, and pdf.',
|
tip: 'Supports txt, html, markdown, xlsx, and pdf. Max 10MB each.',
|
||||||
validation: {
|
validation: {
|
||||||
typeError: 'File type not supported',
|
typeError: 'File type not supported',
|
||||||
size: 'File too large. Maximum is 15MB',
|
size: 'File too large. Maximum is 10MB',
|
||||||
count: 'Multiple files not supported',
|
count: 'Multiple files not supported',
|
||||||
},
|
},
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
|
|
|
@ -23,10 +23,10 @@ const translation = {
|
||||||
title: '上传文本文件',
|
title: '上传文本文件',
|
||||||
button: '拖拽文件至此,或者',
|
button: '拖拽文件至此,或者',
|
||||||
browse: '选择文件',
|
browse: '选择文件',
|
||||||
tip: '已支持 TXT, HTML, Markdown, PDF, XLSX',
|
tip: '已支持 TXT、 HTML、 Markdown、 PDF、 XLSX,每个文件不超过 10 MB。',
|
||||||
validation: {
|
validation: {
|
||||||
typeError: '文件类型不支持',
|
typeError: '文件类型不支持',
|
||||||
size: '文件太大了,不能超过 15MB',
|
size: '文件太大了,不能超过 10MB',
|
||||||
count: '暂不支持多个文件',
|
count: '暂不支持多个文件',
|
||||||
},
|
},
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user