fix: #567 use modal to upload files in the knowledge base (#601)

### What problem does this PR solve?

fix:  #567 use modal to upload files in the knowledge base

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
balibabu 2024-04-29 15:45:19 +08:00 committed by GitHub
parent 6874c6f3a7
commit 38f0cc016f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 262 additions and 20 deletions

View File

@ -0,0 +1,8 @@
.uploader {
:global {
.ant-upload-list {
max-height: 40vh;
overflow-y: auto;
}
}
}

View File

@ -0,0 +1,135 @@
import { useTranslate } from '@/hooks/commonHooks';
import { IModalProps } from '@/interfaces/common';
import { InboxOutlined } from '@ant-design/icons';
import {
Flex,
Modal,
Segmented,
Tabs,
TabsProps,
Upload,
UploadFile,
UploadProps,
} from 'antd';
import { Dispatch, SetStateAction, useState } from 'react';
import styles from './index.less';
const { Dragger } = Upload;
const FileUpload = ({
directory,
fileList,
setFileList,
}: {
directory: boolean;
fileList: UploadFile[];
setFileList: Dispatch<SetStateAction<UploadFile[]>>;
}) => {
const { t } = useTranslate('fileManager');
const props: UploadProps = {
multiple: true,
onRemove: (file) => {
const index = fileList.indexOf(file);
const newFileList = fileList.slice();
newFileList.splice(index, 1);
setFileList(newFileList);
},
beforeUpload: (file) => {
setFileList((pre) => {
return [...pre, file];
});
return false;
},
directory,
fileList,
};
return (
<Dragger {...props} className={styles.uploader}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">{t('uploadTitle')}</p>
<p className="ant-upload-hint">{t('uploadDescription')}</p>
</Dragger>
);
};
const FileUploadModal = ({
visible,
hideModal,
loading,
onOk: onFileUploadOk,
}: IModalProps<UploadFile[]>) => {
const { t } = useTranslate('fileManager');
const [value, setValue] = useState<string | number>('local');
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [directoryFileList, setDirectoryFileList] = useState<UploadFile[]>([]);
const onOk = async () => {
const ret = await onFileUploadOk?.([...fileList, ...directoryFileList]);
if (ret !== undefined && ret === 0) {
setFileList([]);
setDirectoryFileList([]);
}
return ret;
};
const items: TabsProps['items'] = [
{
key: '1',
label: t('file'),
children: (
<FileUpload
directory={false}
fileList={fileList}
setFileList={setFileList}
></FileUpload>
),
},
{
key: '2',
label: t('directory'),
children: (
<FileUpload
directory
fileList={directoryFileList}
setFileList={setDirectoryFileList}
></FileUpload>
),
},
];
return (
<>
<Modal
title={t('uploadFile')}
open={visible}
onOk={onOk}
onCancel={hideModal}
confirmLoading={loading}
>
<Flex gap={'large'} vertical>
<Segmented
options={[
{ label: t('local'), value: 'local' },
{ label: t('s3'), value: 's3' },
]}
block
value={value}
onChange={setValue}
/>
{value === 'local' ? (
<Tabs defaultActiveKey="1" items={items} />
) : (
t('comingSoon', { keyPrefix: 'common' })
)}
</Flex>
</Modal>
</>
);
};
export default FileUploadModal;

View File

@ -184,12 +184,12 @@ export const useUploadDocument = () => {
const { knowledgeId } = useGetKnowledgeSearchParams(); const { knowledgeId } = useGetKnowledgeSearchParams();
const uploadDocument = useCallback( const uploadDocument = useCallback(
(file: UploadFile) => { (fileList: UploadFile[]) => {
try { try {
return dispatch<any>({ return dispatch<any>({
type: 'kFModel/upload_document', type: 'kFModel/upload_document',
payload: { payload: {
file, fileList,
kb_id: knowledgeId, kb_id: knowledgeId,
}, },
}); });

View File

@ -5,6 +5,7 @@ import {
IThirdOAIModelCollection, IThirdOAIModelCollection,
} from '@/interfaces/database/llm'; } from '@/interfaces/database/llm';
import { IAddLlmRequestBody } from '@/interfaces/request/llm'; import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { sortLLmFactoryListBySpecifiedOrder } from '@/utils/commonUtil';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'umi'; import { useDispatch, useSelector } from 'umi';
@ -110,13 +111,12 @@ export const useFetchLlmFactoryListOnMount = () => {
const factoryList = useSelectLlmFactoryList(); const factoryList = useSelectLlmFactoryList();
const myLlmList = useSelectMyLlmList(); const myLlmList = useSelectMyLlmList();
const list = useMemo( const list = useMemo(() => {
() => const currentList = factoryList.filter((x) =>
factoryList.filter((x) => Object.keys(myLlmList).every((y) => y !== x.name),
Object.keys(myLlmList).every((y) => y !== x.name), );
), return sortLLmFactoryListBySpecifiedOrder(currentList);
[factoryList, myLlmList], }, [factoryList, myLlmList]);
);
const fetchLlmFactoryList = useCallback(() => { const fetchLlmFactoryList = useCallback(() => {
dispatch({ dispatch({

View File

@ -30,9 +30,14 @@ import styles from './index.less';
interface IProps { interface IProps {
selectedRowKeys: string[]; selectedRowKeys: string[];
showCreateModal(): void; showCreateModal(): void;
showDocumentUploadModal(): void;
} }
const DocumentToolbar = ({ selectedRowKeys, showCreateModal }: IProps) => { const DocumentToolbar = ({
selectedRowKeys,
showCreateModal,
showDocumentUploadModal,
}: IProps) => {
const { t } = useTranslate('knowledgeDetails'); const { t } = useTranslate('knowledgeDetails');
const { fetchDocumentList } = useFetchDocumentListOnMount(); const { fetchDocumentList } = useFetchDocumentListOnMount();
const { setPagination, searchString } = useGetPagination(fetchDocumentList); const { setPagination, searchString } = useGetPagination(fetchDocumentList);
@ -48,7 +53,7 @@ const DocumentToolbar = ({ selectedRowKeys, showCreateModal }: IProps) => {
return [ return [
{ {
key: '1', key: '1',
onClick: linkToUploadPage, onClick: showDocumentUploadModal,
label: ( label: (
<div> <div>
<Button type="link"> <Button type="link">
@ -75,7 +80,7 @@ const DocumentToolbar = ({ selectedRowKeys, showCreateModal }: IProps) => {
// disabled: true, // disabled: true,
}, },
]; ];
}, [linkToUploadPage, showCreateModal, t]); }, [showDocumentUploadModal, showCreateModal, t]);
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {
showDeleteConfirm({ showDeleteConfirm({

View File

@ -4,13 +4,15 @@ import {
useFetchDocumentList, useFetchDocumentList,
useSaveDocumentName, useSaveDocumentName,
useSetDocumentParser, useSetDocumentParser,
useUploadDocument,
} from '@/hooks/documentHooks'; } from '@/hooks/documentHooks';
import { useGetKnowledgeSearchParams } from '@/hooks/routeHook'; import { useGetKnowledgeSearchParams } from '@/hooks/routeHook';
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
import { useFetchTenantInfo } from '@/hooks/userSettingHook'; import { useFetchTenantInfo } from '@/hooks/userSettingHook';
import { Pagination } from '@/interfaces/common'; import { Pagination } from '@/interfaces/common';
import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
import { PaginationProps } from 'antd'; import { getUnSupportedFilesCount } from '@/utils/documentUtils';
import { PaginationProps, UploadFile } from 'antd';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useNavigate, useSelector } from 'umi'; import { useDispatch, useNavigate, useSelector } from 'umi';
import { KnowledgeRouteKey } from './constant'; import { KnowledgeRouteKey } from './constant';
@ -242,3 +244,42 @@ export const useGetRowSelection = () => {
return rowSelection; return rowSelection;
}; };
export const useHandleUploadDocument = () => {
const {
visible: documentUploadVisible,
hideModal: hideDocumentUploadModal,
showModal: showDocumentUploadModal,
} = useSetModalState();
const uploadDocument = useUploadDocument();
const onDocumentUploadOk = useCallback(
async (fileList: UploadFile[]): Promise<number | undefined> => {
if (fileList.length > 0) {
const ret: any = await uploadDocument(fileList);
const count = getUnSupportedFilesCount(ret.retmsg);
/// 500 error code indicates that some file types are not supported
let retcode = ret.retcode;
if (
ret.retcode === 0 ||
(ret.retcode === 500 && count !== fileList.length) // Some files were not uploaded successfully, but some were uploaded successfully.
) {
retcode = 0;
hideDocumentUploadModal();
}
return retcode;
}
},
[uploadDocument, hideDocumentUploadModal],
);
const loading = useOneNamespaceEffectsLoading('kFModel', ['upload_document']);
return {
documentUploadLoading: loading,
onDocumentUploadOk,
documentUploadVisible,
hideDocumentUploadModal,
showDocumentUploadModal,
};
};

View File

@ -19,6 +19,7 @@ import {
useFetchDocumentListOnMount, useFetchDocumentListOnMount,
useGetPagination, useGetPagination,
useGetRowSelection, useGetRowSelection,
useHandleUploadDocument,
useNavigateToOtherPage, useNavigateToOtherPage,
useRenameDocument, useRenameDocument,
} from './hooks'; } from './hooks';
@ -26,6 +27,7 @@ import ParsingActionCell from './parsing-action-cell';
import ParsingStatusCell from './parsing-status-cell'; import ParsingStatusCell from './parsing-status-cell';
import RenameModal from './rename-modal'; import RenameModal from './rename-modal';
import FileUploadModal from '@/components/file-upload-modal';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import styles from './index.less'; import styles from './index.less';
@ -58,6 +60,13 @@ const KnowledgeFile = () => {
hideChangeParserModal, hideChangeParserModal,
showChangeParserModal, showChangeParserModal,
} = useChangeDocumentParser(currentRecord.id); } = useChangeDocumentParser(currentRecord.id);
const {
documentUploadVisible,
hideDocumentUploadModal,
showDocumentUploadModal,
onDocumentUploadOk,
documentUploadLoading,
} = useHandleUploadDocument();
const { t } = useTranslation('translation', { const { t } = useTranslation('translation', {
keyPrefix: 'knowledgeDetails', keyPrefix: 'knowledgeDetails',
}); });
@ -157,6 +166,7 @@ const KnowledgeFile = () => {
<DocumentToolbar <DocumentToolbar
selectedRowKeys={rowSelection.selectedRowKeys as string[]} selectedRowKeys={rowSelection.selectedRowKeys as string[]}
showCreateModal={showCreateModal} showCreateModal={showCreateModal}
showDocumentUploadModal={showDocumentUploadModal}
></DocumentToolbar> ></DocumentToolbar>
<Table <Table
rowKey="id" rowKey="id"
@ -190,6 +200,12 @@ const KnowledgeFile = () => {
hideModal={hideRenameModal} hideModal={hideRenameModal}
initialName={currentRecord.name} initialName={currentRecord.name}
></RenameModal> ></RenameModal>
<FileUploadModal
visible={documentUploadVisible}
hideModal={hideDocumentUploadModal}
loading={documentUploadLoading}
onOk={onDocumentUploadOk}
></FileUploadModal>
</div> </div>
); );
}; };

View File

@ -210,11 +210,15 @@ const model: DvaModel<KFModelState> = {
} }
}, },
*upload_document({ payload = {} }, { call, put }) { *upload_document({ payload = {} }, { call, put }) {
const fileList = payload.fileList;
const formData = new FormData(); const formData = new FormData();
formData.append('file', payload.file);
formData.append('kb_id', payload.kb_id); formData.append('kb_id', payload.kb_id);
fileList.forEach((file: any) => {
formData.append('file', file);
});
const { data } = yield call(kbService.document_upload, formData); const { data } = yield call(kbService.document_upload, formData);
if (data.retcode === 0) { if (data.retcode === 0 || data.retcode === 500) {
yield put({ yield put({
type: 'getKfList', type: 'getKfList',
payload: { kb_id: payload.kb_id }, payload: { kb_id: payload.kb_id },

View File

@ -85,8 +85,6 @@ const model: DvaModel<FileManagerModelState> = {
const pathList = payload.path; const pathList = payload.path;
const formData = new FormData(); const formData = new FormData();
formData.append('parent_id', payload.parentId); formData.append('parent_id', payload.parentId);
// formData.append('file', payload.file);
// formData.append('path', payload.path);
fileList.forEach((file: any, index: number) => { fileList.forEach((file: any, index: number) => {
formData.append('file', file); formData.append('file', file);
formData.append('path', pathList[index]); formData.append('path', pathList[index]);

View File

@ -1,5 +1,9 @@
.modelWrapper { .modelWrapper {
width: 100%; width: 100%;
}
.modelContainer {
width: 100%;
.factoryOperationWrapper { .factoryOperationWrapper {
text-align: right; text-align: right;
} }

View File

@ -223,9 +223,9 @@ const UserSettingModel = () => {
]; ];
return ( return (
<> <section id="xx" className={styles.modelWrapper}>
<Spin spinning={loading}> <Spin spinning={loading}>
<section className={styles.modelWrapper}> <section className={styles.modelContainer}>
<SettingTitle <SettingTitle
title={t('model')} title={t('model')}
description={t('modelDescription')} description={t('modelDescription')}
@ -257,7 +257,7 @@ const UserSettingModel = () => {
loading={llmAddingLoading} loading={llmAddingLoading}
llmFactory={selectedLlmFactory} llmFactory={selectedLlmFactory}
></OllamaModal> ></OllamaModal>
</> </section>
); );
}; };

View File

@ -1,3 +1,4 @@
import { IFactory } from '@/interfaces/database/llm';
import isObject from 'lodash/isObject'; import isObject from 'lodash/isObject';
import snakeCase from 'lodash/snakeCase'; import snakeCase from 'lodash/snakeCase';
@ -33,3 +34,29 @@ export const formatNumberWithThousandsSeparator = (numberStr: string) => {
const formattedNumber = numberStr.replace(/\B(?=(\d{3})+(?!\d))/g, ','); const formattedNumber = numberStr.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return formattedNumber; return formattedNumber;
}; };
const orderFactoryList = [
'OpenAI',
'Moonshot',
'ZHIPU-AI',
'Ollama',
'Xinference',
];
export const sortLLmFactoryListBySpecifiedOrder = (list: IFactory[]) => {
const finalList: IFactory[] = [];
orderFactoryList.forEach((orderItem) => {
const index = list.findIndex((item) => item.name === orderItem);
if (index !== -1) {
finalList.push(list[index]);
}
});
list.forEach((item) => {
if (finalList.every((x) => x.name !== item.name)) {
finalList.push(item);
}
});
return finalList;
};

View File

@ -42,3 +42,7 @@ export const getExtension = (name: string) =>
export const isPdf = (name: string) => { export const isPdf = (name: string) => {
return getExtension(name) === 'pdf'; return getExtension(name) === 'pdf';
}; };
export const getUnSupportedFilesCount = (message: string) => {
return message.split('\n').length;
};