fix: use @tanstack/react-query to get knowledge base data #1306 (#1609)

### What problem does this PR solve?
fix: use @tanstack/react-query to get knowledge base data #1306

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
balibabu 2024-07-19 15:50:44 +08:00 committed by GitHub
parent 3fcdba1683
commit 1c90c39897
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 78 additions and 637 deletions

View File

@ -6,7 +6,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { message } from 'antd';
import { useCallback, useEffect } from 'react';
import { useDispatch, useSearchParams, useSelector } from 'umi';
import { useGetKnowledgeSearchParams } from './route-hook';
export const useKnowledgeBaseId = (): string => {
const [searchParams] = useSearchParams();
@ -41,44 +40,6 @@ export const useDeleteDocumentById = (): {
};
};
export const useFetchKnowledgeDetail = () => {
const dispatch = useDispatch();
const { knowledgeId } = useGetKnowledgeSearchParams();
const fetchKnowledgeDetail = useCallback(
(knowledgeId: string) => {
dispatch({
type: 'knowledgeModel/getKnowledgeDetail',
payload: { kb_id: knowledgeId },
});
},
[dispatch],
);
useEffect(() => {
fetchKnowledgeDetail(knowledgeId);
}, [fetchKnowledgeDetail, knowledgeId]);
return fetchKnowledgeDetail;
};
export const useSelectKnowledgeDetail = () => {
const knowledge: IKnowledge = useSelector(
(state: any) => state.knowledgeModel.knowledge,
);
return knowledge;
};
export const useGetDocumentDefaultParser = () => {
const item = useSelectKnowledgeDetail();
return {
defaultParserId: item?.parser_id ?? '',
parserConfig: item?.parser_config ?? '',
};
};
export const useDeleteChunkByIds = (): {
removeChunk: (chunkIds: string[], documentId: string) => Promise<number>;
} => {
@ -111,21 +72,21 @@ export const useDeleteChunkByIds = (): {
};
export const useFetchKnowledgeBaseConfiguration = () => {
const dispatch = useDispatch();
const knowledgeBaseId = useKnowledgeBaseId();
const fetchKnowledgeBaseConfiguration = useCallback(() => {
dispatch({
type: 'kSModel/getKbDetail',
payload: {
const { data, isFetching: loading } = useQuery({
queryKey: ['fetchKnowledgeDetail'],
initialData: {},
gcTime: 0,
queryFn: async () => {
const { data } = await kbService.get_kb_detail({
kb_id: knowledgeBaseId,
},
});
}, [dispatch, knowledgeBaseId]);
});
return data?.data ?? {};
},
});
useEffect(() => {
fetchKnowledgeBaseConfiguration();
}, [fetchKnowledgeBaseConfiguration]);
return { data, loading };
};
export const useNextFetchKnowledgeList = (
@ -228,27 +189,30 @@ export const useFetchFileThumbnails = (docIds?: Array<string>) => {
//#region knowledge configuration
export const useUpdateKnowledge = () => {
const dispatch = useDispatch();
const saveKnowledgeConfiguration = useCallback(
(payload: any) => {
dispatch({
type: 'kSModel/updateKb',
payload,
const knowledgeBaseId = useKnowledgeBaseId();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['saveKnowledge'],
mutationFn: async (params: Record<string, any>) => {
const { data = {} } = await kbService.updateKb({
kb_id: knowledgeBaseId,
...params,
});
if (data.retcode === 0) {
message.success(i18n.t(`message.updated`));
queryClient.invalidateQueries({ queryKey: ['fetchKnowledgeDetail'] });
}
return data;
},
[dispatch],
);
});
return saveKnowledgeConfiguration;
return { data, loading, saveKnowledgeConfiguration: mutateAsync };
};
export const useSelectKnowledgeDetails = () => {
const knowledgeDetails: IKnowledge = useSelector(
(state: any) => state.kSModel.knowledgeDetails,
);
return knowledgeDetails;
};
//#endregion
//#region Retrieval testing

View File

@ -1,86 +0,0 @@
.uploadWrapper {
display: flex;
flex-direction: column;
padding: 64px 32px 32px;
height: 100%;
.backToList {
display: none;
padding-bottom: 60px;
}
.footer {
text-align: center;
padding-top: 16px;
.nextButton {
// background-color: @purple;
font-weight: 700;
font-size: 24px;
display: inline-flex;
align-items: center;
padding: 26px 40px;
justify-content: center;
width: 10%;
}
}
.uploadContent {
flex: 1;
padding-top: 60px;
}
.uploader {
:global(.ant-upload) {
height: 126px;
}
}
.hiddenUploader {
:global(.ant-upload-drag) {
display: none;
}
}
.fileIcon {
font-size: 40px;
align-items: end;
}
.uploaderButton {
padding: 10px;
vertical-align: middle;
height: 40px;
}
.uploaderIcon {
svg {
width: 20px;
height: 20px;
}
}
.deleteIcon {
font-size: 20px;
}
.uploaderItem {
margin-top: 16px;
:global(.ant-card-body) {
padding: 16px;
}
}
.uploaderItemProgress {
padding-left: 50px;
}
.uploaderItemTextWrapper {
flex: 1;
text-align: left;
padding-left: 10px;
}
}
.progressWrapper {
text-align: center;
}
.progress {
width: 50%;
margin: 0;
}
.selectFilesText {
transform: translateX(10%);
}
.changeSpecificCategoryText {
transform: translateX(30%);
}

View File

@ -1,310 +0,0 @@
import { ReactComponent as SelectFilesEndIcon } from '@/assets/svg/select-files-end.svg';
import { ReactComponent as SelectFilesStartIcon } from '@/assets/svg/select-files-start.svg';
import ChunkMethodModal from '@/components/chunk-method-modal';
import { KnowledgeRouteKey } from '@/constants/knowledge';
import {
useRunDocument,
useSelectDocumentList,
useUploadDocument,
} from '@/hooks/document-hooks';
import {
useDeleteDocumentById,
useFetchKnowledgeDetail,
useKnowledgeBaseId,
} from '@/hooks/knowledge-hooks';
import {
useChangeDocumentParser,
useSetSelectedRecord,
} from '@/hooks/logic-hooks';
import { useFetchTenantInfo } from '@/hooks/user-setting-hooks';
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
import { getExtension, isFileUploadDone } from '@/utils/documentUtils';
import {
ArrowLeftOutlined,
CloudUploadOutlined,
DeleteOutlined,
EditOutlined,
FileDoneOutlined,
} from '@ant-design/icons';
import {
Button,
Card,
Flex,
Progress,
Space,
Upload,
UploadFile,
UploadProps,
} from 'antd';
import classNames from 'classnames';
import { ReactElement, useCallback, useMemo, useRef, useState } from 'react';
import { Link, useNavigate } from 'umi';
import { useTranslate } from '@/hooks/common-hooks';
import styles from './index.less';
const { Dragger } = Upload;
type UploadRequestOption = Parameters<
NonNullable<UploadProps['customRequest']>
>[0];
const UploaderItem = ({
file,
isUpload,
remove,
handleEdit,
}: {
isUpload: boolean;
originNode: ReactElement;
file: UploadFile;
fileList: object[];
showModal: () => void;
remove: (id: string) => void;
setRecord: (record: IKnowledgeFile) => void;
handleEdit: (id: string) => void;
}) => {
const { removeDocument } = useDeleteDocumentById();
const documentId = file?.response?.id;
const handleRemove = async () => {
if (file.status === 'error') {
remove(documentId);
} else {
const ret: any = await removeDocument(documentId);
if (ret === 0) {
remove(documentId);
}
}
};
const handleEditClick = () => {
if (file.status === 'done') {
handleEdit(documentId);
}
};
return (
<Card className={styles.uploaderItem}>
<Flex justify="space-between">
<FileDoneOutlined className={styles.fileIcon} />
<section className={styles.uploaderItemTextWrapper}>
<div>
<b>{file.name}</b>
</div>
<span>{file.size}</span>
</section>
{isUpload ? (
<DeleteOutlined
className={styles.deleteIcon}
onClick={handleRemove}
/>
) : (
<EditOutlined onClick={handleEditClick} />
)}
</Flex>
<Flex>
<Progress
showInfo={false}
percent={100}
className={styles.uploaderItemProgress}
strokeColor="
rgba(127, 86, 217, 1)
"
/>
<span>100%</span>
</Flex>
</Card>
);
};
const KnowledgeUploadFile = () => {
const knowledgeBaseId = useKnowledgeBaseId();
const [isUpload, setIsUpload] = useState(true);
const [uploadedFileIds, setUploadedFileIds] = useState<string[]>([]);
const fileListRef = useRef<UploadFile[]>([]);
const navigate = useNavigate();
const { currentRecord, setRecord } = useSetSelectedRecord();
const {
changeParserLoading,
onChangeParserOk,
changeParserVisible,
hideChangeParserModal,
showChangeParserModal,
} = useChangeDocumentParser(currentRecord.id);
const documentList = useSelectDocumentList();
const runDocumentByIds = useRunDocument();
const uploadDocument = useUploadDocument();
const { t } = useTranslate('knowledgeDetails');
const enabled = useMemo(() => {
if (isUpload) {
return (
uploadedFileIds.length > 0 &&
fileListRef.current.filter((x) => isFileUploadDone(x)).length ===
uploadedFileIds.length
);
}
return true;
}, [uploadedFileIds, isUpload]);
const createRequest: (props: UploadRequestOption) => void = async function ({
file,
onSuccess,
onError,
// onProgress,
}) {
const data = await uploadDocument(file as UploadFile);
if (data?.retcode === 0) {
setUploadedFileIds((pre) => {
return pre.concat(data.data.id);
});
if (onSuccess) {
onSuccess(data.data);
}
} else {
if (onError) {
onError(data?.data);
}
}
};
const removeIdFromUploadedIds = useCallback((id: string) => {
setUploadedFileIds((pre) => {
return pre.filter((x) => x !== id);
});
}, []);
const handleItemEdit = useCallback(
(id: string) => {
const document = documentList.find((x) => x.id === id);
if (document) {
setRecord(document);
}
showChangeParserModal();
},
[documentList, showChangeParserModal, setRecord],
);
const props: UploadProps = {
name: 'file',
multiple: true,
itemRender(originNode, file, fileList, actions) {
fileListRef.current = fileList;
const remove = (id: string) => {
if (isFileUploadDone(file)) {
removeIdFromUploadedIds(id);
}
actions.remove();
};
return (
<UploaderItem
isUpload={isUpload}
file={file}
fileList={fileList}
originNode={originNode}
remove={remove}
showModal={showChangeParserModal}
setRecord={setRecord}
handleEdit={handleItemEdit}
></UploaderItem>
);
},
customRequest: createRequest,
onDrop(e) {
console.log('Dropped files', e.dataTransfer.files);
},
};
const runSelectedDocument = () => {
const ids = fileListRef.current.map((x) => x.response.id);
runDocumentByIds({ doc_ids: ids, run: 1 });
};
const handleNextClick = () => {
if (!isUpload) {
runSelectedDocument();
navigate(`/knowledge/${KnowledgeRouteKey.Dataset}?id=${knowledgeBaseId}`);
} else {
setIsUpload(false);
}
};
useFetchTenantInfo();
useFetchKnowledgeDetail();
return (
<>
<div className={styles.uploadWrapper}>
<section>
<Space className={styles.backToList}>
<ArrowLeftOutlined />
<Link to={`/knowledge/dataset?id=${knowledgeBaseId}`}>
Back to select files
</Link>
</Space>
<div className={styles.progressWrapper}>
<Flex align="center" justify="center">
<SelectFilesStartIcon></SelectFilesStartIcon>
<Progress
percent={100}
showInfo={false}
className={styles.progress}
strokeColor="
rgba(127, 86, 217, 1)
"
/>
<SelectFilesEndIcon></SelectFilesEndIcon>
</Flex>
<Flex justify="space-around">
<p className={styles.selectFilesText}>
<b>{t('selectFiles')}</b>
</p>
<p className={styles.changeSpecificCategoryText}>
<b>{t('changeSpecificCategory')}</b>
</p>
</Flex>
</div>
</section>
<section className={styles.uploadContent}>
<Dragger
{...props}
className={classNames(styles.uploader, {
[styles.hiddenUploader]: !isUpload,
})}
>
<Button className={styles.uploaderButton}>
<CloudUploadOutlined className={styles.uploaderIcon} />
</Button>
<p className="ant-upload-text">{t('uploadTitle')}</p>
<p className="ant-upload-hint">{t('uploadDescription')}</p>
</Dragger>
</section>
<section className={styles.footer}>
<Button
type="primary"
className={styles.nextButton}
onClick={handleNextClick}
disabled={!enabled}
size="large"
>
{t('next', { keyPrefix: 'common' })}
</Button>
</section>
</div>
<ChunkMethodModal
documentId={currentRecord.id}
parserId={currentRecord.parser_id}
parserConfig={currentRecord.parser_config}
documentExtension={getExtension(currentRecord.name)}
onOk={onChangeParserOk}
visible={changeParserVisible}
hideModal={hideChangeParserModal}
loading={changeParserLoading}
/>
</>
);
};
export default KnowledgeUploadFile;

View File

@ -1,12 +1,9 @@
import {
useFetchKnowledgeBaseConfiguration,
useKnowledgeBaseId,
useSelectKnowledgeDetails,
useUpdateKnowledge,
} from '@/hooks/knowledge-hooks';
import { useFetchLlmList, useSelectLlmOptions } from '@/hooks/llm-hooks';
import { useNavigateToDataset } from '@/hooks/route-hook';
import { useOneNamespaceEffectsLoading } from '@/hooks/store-hooks';
import {
useFetchTenantInfo,
useSelectParserList,
@ -15,6 +12,7 @@ import {
getBase64FromUploadFileList,
getUploadFileListFromBase64,
} from '@/utils/fileUtil';
import { useIsFetching } from '@tanstack/react-query';
import { Form, UploadFile } from 'antd';
import { FormInstance } from 'antd/lib';
import pick from 'lodash/pick';
@ -22,32 +20,32 @@ import { useCallback, useEffect } from 'react';
import { LlmModelType } from '../../constant';
export const useSubmitKnowledgeConfiguration = (form: FormInstance) => {
const save = useUpdateKnowledge();
const knowledgeBaseId = useKnowledgeBaseId();
const submitLoading = useOneNamespaceEffectsLoading('kSModel', ['updateKb']);
const { saveKnowledgeConfiguration, loading } = useUpdateKnowledge();
const navigateToDataset = useNavigateToDataset();
const submitKnowledgeConfiguration = useCallback(async () => {
const values = await form.validateFields();
const avatar = await getBase64FromUploadFileList(values.avatar);
save({
saveKnowledgeConfiguration({
...values,
avatar,
kb_id: knowledgeBaseId,
});
navigateToDataset();
}, [save, knowledgeBaseId, form, navigateToDataset]);
}, [saveKnowledgeConfiguration, form, navigateToDataset]);
return { submitKnowledgeConfiguration, submitLoading, navigateToDataset };
return {
submitKnowledgeConfiguration,
submitLoading: loading,
navigateToDataset,
};
};
export const useFetchKnowledgeConfigurationOnMount = (form: FormInstance) => {
const knowledgeDetails = useSelectKnowledgeDetails();
const parserList = useSelectParserList();
const embeddingModelOptions = useSelectLlmOptions();
useFetchTenantInfo();
useFetchKnowledgeBaseConfiguration();
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration();
useFetchLlmList(LlmModelType.Embedding);
useEffect(() => {
@ -76,7 +74,7 @@ export const useFetchKnowledgeConfigurationOnMount = (form: FormInstance) => {
};
export const useSelectKnowledgeDetailsLoading = () =>
useOneNamespaceEffectsLoading('kSModel', ['getKbDetail']);
useIsFetching({ queryKey: ['fetchKnowledgeDetail'] }) > 0;
export const useHandleChunkMethodChange = () => {
const [form] = Form.useForm();

View File

@ -1,49 +0,0 @@
import { IKnowledge } from '@/interfaces/database/knowledge';
import i18n from '@/locales/config';
import kbService from '@/services/knowledge-service';
import { message } from 'antd';
import { DvaModel } from 'umi';
export interface KSModelState {
isShowPSwModal: boolean;
tenantIfo: any;
knowledgeDetails: IKnowledge;
}
const model: DvaModel<KSModelState> = {
namespace: 'kSModel',
state: {
isShowPSwModal: false,
tenantIfo: {},
knowledgeDetails: {} as any,
},
reducers: {
updateState(state, { payload }) {
return {
...state,
...payload,
};
},
setKnowledgeDetails(state, { payload }) {
return { ...state, knowledgeDetails: payload };
},
},
effects: {
*updateKb({ payload = {} }, { call, put }) {
const { data } = yield call(kbService.updateKb, payload);
const { retcode } = data;
if (retcode === 0) {
yield put({ type: 'getKbDetail', payload: { kb_id: payload.kb_id } });
message.success(i18n.t('message.updated'));
}
},
*getKbDetail({ payload = {} }, { call, put }) {
const { data } = yield call(kbService.get_kb_detail, payload);
if (data.retcode === 0) {
yield put({ type: 'setKnowledgeDetails', payload: data.data });
}
return data;
},
},
};
export default model;

View File

@ -3,7 +3,6 @@ import { ReactComponent as DatasetIcon } from '@/assets/svg/knowledge-dataset.sv
import { ReactComponent as TestingIcon } from '@/assets/svg/knowledge-testing.svg';
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/knowledge-hooks';
import { useSecondPathName } from '@/hooks/route-hook';
import { IKnowledge } from '@/interfaces/database/knowledge';
import { getWidth } from '@/utils';
import { Avatar, Menu, MenuProps, Space } from 'antd';
import classNames from 'classnames';
@ -11,6 +10,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useSelector } from 'umi';
import { KnowledgeRouteKey } from '../../constant';
import styles from './index.less';
const KnowledgeSidebar = () => {
@ -18,13 +18,11 @@ const KnowledgeSidebar = () => {
const { id } = kAModel;
let navigate = useNavigate();
const activeKey = useSecondPathName();
const knowledgeDetails: IKnowledge = useSelector(
(state: any) => state.kSModel.knowledgeDetails,
);
const [windowWidth, setWindowWidth] = useState(getWidth());
const [collapsed, setCollapsed] = useState(false);
const { t } = useTranslation();
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration();
const handleSelect: MenuProps['onSelect'] = (e) => {
navigate(`/knowledge/${e.key}?id=${id}`);
@ -94,8 +92,6 @@ const KnowledgeSidebar = () => {
};
}, []);
useFetchKnowledgeBaseConfiguration();
return (
<div className={styles.sidebarWrapper}>
<div className={styles.sidebarTop}>

View File

@ -1,63 +0,0 @@
import { IKnowledge } from '@/interfaces/database/knowledge';
import kbService from '@/services/knowledge-service';
import { DvaModel } from 'umi';
export interface KnowledgeModelState {
data: IKnowledge[];
knowledge: IKnowledge;
}
const model: DvaModel<KnowledgeModelState> = {
namespace: 'knowledgeModel',
state: {
data: [],
knowledge: {} as IKnowledge,
},
reducers: {
updateState(state, { payload }) {
return {
...state,
...payload,
};
},
setKnowledge(state, { payload }) {
return {
...state,
knowledge: payload,
};
},
},
effects: {
*rmKb({ payload = {} }, { call, put }) {
const { data } = yield call(kbService.rmKb, payload);
const { retcode } = data;
if (retcode === 0) {
yield put({
type: 'getList',
payload: {},
});
}
},
*getList({ payload = {} }, { call, put }) {
const { data } = yield call(kbService.getList, payload);
const { retcode, data: res } = data;
if (retcode === 0) {
yield put({
type: 'updateState',
payload: {
data: res,
},
});
}
},
*getKnowledgeDetail({ payload = {} }, { call, put }) {
const { data } = yield call(kbService.get_kb_detail, payload);
if (data.retcode === 0) {
yield put({ type: 'setKnowledge', payload: data.data });
}
return data.retcode;
},
},
};
export default model;

View File

@ -32,11 +32,6 @@ const routes = [
path: '/knowledge/dataset',
component: '@/pages/add-knowledge/components/knowledge-file',
},
{
path: '/knowledge/dataset/upload',
component:
'@/pages/add-knowledge/components/knowledge-dataset/knowledge-upload-file',
},
{
path: '/knowledge/dataset/chunk',
component: '@/pages/add-knowledge/components/knowledge-chunk',

74
web/typings.d.ts vendored
View File

@ -1,39 +1,35 @@
import { ChunkModelState } from '@/pages/add-knowledge/components/knowledge-chunk/model';
import { KFModelState } from '@/pages/add-knowledge/components/knowledge-file/model';
import { KSModelState } from '@/pages/add-knowledge/components/knowledge-setting/model';
import { TestingModelState } from '@/pages/add-knowledge/components/knowledge-testing/model';
import { kAModelState } from '@/pages/add-knowledge/model';
import { ChatModelState } from '@/pages/chat/model';
import { FileManagerModelState } from '@/pages/file-manager/model';
import { KnowledgeModelState } from '@/pages/knowledge/model';
import { LoginModelState } from '@/pages/login/model';
import { SettingModelState } from '@/pages/user-setting/model';
declare module 'lodash';
function useSelector<TState = RootState, TSelected = unknown>(
selector: (state: TState) => TSelected,
equalityFn?: (left: TSelected, right: TSelected) => boolean,
): TSelected;
export interface RootState {
// loading: Loading;
fileManager: FileManagerModelState;
chatModel: ChatModelState;
loginModel: LoginModelState;
knowledgeModel: KnowledgeModelState;
settingModel: SettingModelState;
kFModel: KFModelState;
kAModel: kAModelState;
chunkModel: ChunkModelState;
kSModel: KSModelState;
testingModel: TestingModelState;
}
declare global {
type Nullable<T> = T | null;
}
declare module 'umi' {
export { useSelector };
}
import { ChunkModelState } from '@/pages/add-knowledge/components/knowledge-chunk/model';
import { KFModelState } from '@/pages/add-knowledge/components/knowledge-file/model';
import { TestingModelState } from '@/pages/add-knowledge/components/knowledge-testing/model';
import { kAModelState } from '@/pages/add-knowledge/model';
import { ChatModelState } from '@/pages/chat/model';
import { FileManagerModelState } from '@/pages/file-manager/model';
import { LoginModelState } from '@/pages/login/model';
import { SettingModelState } from '@/pages/user-setting/model';
declare module 'lodash';
function useSelector<TState = RootState, TSelected = unknown>(
selector: (state: TState) => TSelected,
equalityFn?: (left: TSelected, right: TSelected) => boolean,
): TSelected;
export interface RootState {
// loading: Loading;
fileManager: FileManagerModelState;
chatModel: ChatModelState;
loginModel: LoginModelState;
settingModel: SettingModelState;
kFModel: KFModelState;
kAModel: kAModelState;
chunkModel: ChunkModelState;
testingModel: TestingModelState;
}
declare global {
type Nullable<T> = T | null;
}
declare module 'umi' {
export { useSelector };
}