Unverified Commit d637a147 authored by Joel's avatar Joel Committed by GitHub

feat: support batch upload files (#419)

parent 8a4d19d9
......@@ -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 (
......
.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;
}
......@@ -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>(DataSourceType.FILE)
const [step, setStep] = useState(1)
const [indexingTypeCache, setIndexTypeCache] = useState('')
const [file, setFile] = useState<File | undefined>()
const [fileList, setFiles] = useState<any[]>([])
const [result, setResult] = useState<createDocumentResponse | undefined>()
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}
......
......@@ -10,7 +10,9 @@
}
.form {
position: relative;
padding: 12px 64px;
background-color: #fff;
}
.dataSourceTypeList {
......
'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<File | undefined>()
const [currentNotionPage, setCurrentNotionPage] = useState<Page | undefined>()
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 (
<div className='flex w-full h-full'>
<div className='grow overflow-y-auto relative'>
......@@ -103,7 +116,8 @@ const StepOne = ({
if (dataSourceTypeDisable)
return
changeType(DataSourceType.FILE)
hidePreview()
hideFilePreview()
hideNotionPagePreview()
}}
>
<span className={cn(s.datasetIcon)} />
......@@ -119,7 +133,8 @@ const StepOne = ({
if (dataSourceTypeDisable)
return
changeType(DataSourceType.NOTION)
hidePreview()
hideFilePreview()
hideNotionPagePreview()
}}
>
<span className={cn(s.datasetIcon, s.notion)} />
......@@ -138,8 +153,15 @@ const StepOne = ({
}
{dataSourceType === DataSourceType.FILE && (
<>
<FileUploader onFileUpdate={updateFile} file={file} titleClassName={(!shouldShowDataSourceTypeList) ? 'mt-[30px] !mb-[44px] !text-lg !font-semibold !text-gray-900' : undefined} />
<Button disabled={!file} className={s.submitButton} type='primary' onClick={onStepChange}>{t('datasetCreation.stepOne.button')}</Button>
<FileUploader
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 && (
......@@ -164,7 +186,7 @@ const StepOne = ({
</div>
<EmptyDatasetCreationModal show={showModal} onHide={modalCloseHandle} />
</div>
{file && showFilePreview && <FilePreview file={file} hidePreview={hidePreview} />}
{currentFile && <FilePreview file={currentFile} hidePreview={hideFilePreview} />}
{currentNotionPage && <NotionPagePreview currentPage={currentNotionPage} hidePreview={hideNotionPagePreview} />}
</div>
)
......
......@@ -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 = ({
<Link className='text-[#155EEF]' href={`/datasets/${datasetId}/settings`}>{t('datasetCreation.stepTwo.datasetSettingLink')}</Link>
</div>
)}
{/* TODO multi files */}
<div className={s.source}>
<div className={s.sourceContent}>
{dataSourceType === DataSourceType.FILE && (
<>
<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'>
<span className={cn(s.fileIcon, file && s[file.extension])} />
{getFileName(file?.name || '')}
<span className={cn(s.fileIcon, files.length && s[files[0].extension])} />
{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>
</>
)}
......
......@@ -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}
/>
......
......@@ -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',
......
......@@ -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: '取消',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment