From 79e642ebfdf7433fab807e94d8db211cca1db15c Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Fri, 8 Sep 2023 19:48:04 +0800 Subject: [PATCH] feat: kb folder delete and path --- client/public/locales/en/common.json | 3 + client/public/locales/zh/common.json | 3 + client/src/api/plugins/kb.ts | 6 +- .../Icon/icons/light/rightArrow.svg | 3 + client/src/components/Icon/index.tsx | 3 +- client/src/pages/api/plugins/kb/delete.ts | 32 ++++-- client/src/pages/api/plugins/kb/detail.ts | 4 +- client/src/pages/api/plugins/kb/paths.ts | 36 +++++++ client/src/pages/kb/detail/index.tsx | 2 +- client/src/pages/kb/list/index.tsx | 98 ++++++++++++++----- client/src/types/plugin.d.ts | 5 + 11 files changed, 159 insertions(+), 36 deletions(-) create mode 100644 client/src/components/Icon/icons/light/rightArrow.svg create mode 100644 client/src/pages/api/plugins/kb/paths.ts diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 9eff60c16..ebfffff6d 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -78,6 +78,7 @@ "Copy Successful": "Copy Successful", "Course": "", "Delete": "Delete", + "Delete Success": "Delete Successful", "Delete Warning": "Warning", "Filed is repeat": "Filed is repeated", "Filed is repeated": "", @@ -156,8 +157,10 @@ }, "kb": { "Create Folder": "Create Folder", + "Delete Dataset Error": "Delete dataset failed", "Edit Folder": "Edit Folder", "Folder Name": "Input folder name", + "My Dataset": "My Dataset", "deleteDatasetTips": "Are you sure to delete the knowledge base? Data cannot be recovered after deletion, please confirm!", "deleteFolderTips": "Are you sure to delete this folder and all the knowledge bases it contains? Data cannot be recovered after deletion, please confirm!" }, diff --git a/client/public/locales/zh/common.json b/client/public/locales/zh/common.json index 5285d14c4..22e239d59 100644 --- a/client/public/locales/zh/common.json +++ b/client/public/locales/zh/common.json @@ -78,6 +78,7 @@ "Copy Successful": "复制成功", "Course": "", "Delete": "删除", + "Delete Success": "删除成功", "Delete Warning": "删除警告", "Filed is repeat": "", "Filed is repeated": "字段重复了", @@ -156,8 +157,10 @@ }, "kb": { "Create Folder": "创建文件夹", + "Delete Dataset Error": "删除知识库异常", "Edit Folder": "编辑文件夹", "Folder Name": "输入文件夹名称", + "My Dataset": "我的知识库", "deleteDatasetTips": "确认删除该知识库?删除后数据无法恢复,请确认!", "deleteFolderTips": "确认删除该文件夹及其包含的所有知识库?删除后数据无法恢复,请确认!" }, diff --git a/client/src/api/plugins/kb.ts b/client/src/api/plugins/kb.ts index f71f4903c..270bbdbc9 100644 --- a/client/src/api/plugins/kb.ts +++ b/client/src/api/plugins/kb.ts @@ -1,5 +1,5 @@ import { GET, POST, PUT, DELETE } from '../request'; -import type { DatasetItemType, KbItemType, KbListItemType } from '@/types/plugin'; +import type { DatasetItemType, KbItemType, KbListItemType, KbPathItemType } from '@/types/plugin'; import { RequestPaging } from '@/types/index'; import { TrainingModeEnum } from '@/constants/plugin'; import { @@ -10,7 +10,6 @@ import { Props as SearchTestProps, Response as SearchTestResponse } from '@/pages/api/openapi/kb/searchTest'; -import { Response as KbDataItemType } from '@/pages/api/plugins/kb/data/getDataById'; import { Props as UpdateDataProps } from '@/pages/api/openapi/kb/updateData'; import type { KbUpdateParams, CreateKbParams } from '../request/kb'; import { QuoteItemType } from '@/types/chat'; @@ -19,6 +18,9 @@ import { QuoteItemType } from '@/types/chat'; export const getKbList = (parentId?: string) => GET(`/plugins/kb/list`, { parentId }); +export const getKbPaths = (parentId?: string) => + GET('/plugins/kb/paths', { parentId }); + export const getKbById = (id: string) => GET(`/plugins/kb/detail?id=${id}`); export const postCreateKb = (data: CreateKbParams) => POST(`/plugins/kb/create`, data); diff --git a/client/src/components/Icon/icons/light/rightArrow.svg b/client/src/components/Icon/icons/light/rightArrow.svg new file mode 100644 index 000000000..3dc0a8598 --- /dev/null +++ b/client/src/components/Icon/icons/light/rightArrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/client/src/components/Icon/index.tsx b/client/src/components/Icon/index.tsx index 1f5edea13..e004f304f 100644 --- a/client/src/components/Icon/index.tsx +++ b/client/src/components/Icon/index.tsx @@ -80,7 +80,8 @@ const map = { logsLight: require('./icons/light/logs.svg').default, badLight: require('./icons/light/bad.svg').default, markLight: require('./icons/light/mark.svg').default, - retryLight: require('./icons/light/retry.svg').default + retryLight: require('./icons/light/retry.svg').default, + rightArrowLight: require('./icons/light/rightArrow.svg').default }; export type IconName = keyof typeof map; diff --git a/client/src/pages/api/plugins/kb/delete.ts b/client/src/pages/api/plugins/kb/delete.ts index 3b2d27d32..ad68a1ad4 100644 --- a/client/src/pages/api/plugins/kb/delete.ts +++ b/client/src/pages/api/plugins/kb/delete.ts @@ -3,12 +3,12 @@ import { jsonRes } from '@/service/response'; import { connectToDatabase, KB, App, TrainingData } from '@/service/mongo'; import { authUser } from '@/service/utils/auth'; import { PgClient } from '@/service/pg'; -import { Types } from 'mongoose'; import { PgTrainingTableName } from '@/constants/plugin'; import { GridFSStorage } from '@/service/lib/gridfs'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { + await connectToDatabase(); const { id } = req.query as { id: string; }; @@ -20,26 +20,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< // 凭证校验 const { userId } = await authUser({ req, authToken: true }); - await connectToDatabase(); + const deletedIds = [id, ...(await findAllChildrenIds(id))]; // delete training data await TrainingData.deleteMany({ userId, - kbId: id + kbId: { $in: deletedIds } }); // delete all pg data await PgClient.delete(PgTrainingTableName, { - where: [['user_id', userId], 'AND', ['kb_id', id]] + where: [ + ['user_id', userId], + 'AND', + `kb_id IN (${deletedIds.map((id) => `'${id}'`).join(',')})` + ] }); // delete related files const gridFs = new GridFSStorage('dataset', userId); - await gridFs.deleteFilesByKbId(id); + await Promise.all(deletedIds.map((id) => gridFs.deleteFilesByKbId(id))); // delete kb data - await KB.findOneAndDelete({ - _id: id, + await KB.deleteMany({ + _id: { $in: deletedIds }, userId }); @@ -51,3 +55,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< }); } } + +async function findAllChildrenIds(id: string) { + // find children + const children = await KB.find({ parentId: id }); + + let allChildrenIds = children.map((child) => String(child._id)); + + for (const child of children) { + const grandChildrenIds = await findAllChildrenIds(child._id); + allChildrenIds = allChildrenIds.concat(grandChildrenIds); + } + + return allChildrenIds; +} diff --git a/client/src/pages/api/plugins/kb/detail.ts b/client/src/pages/api/plugins/kb/detail.ts index bab0c9f9a..5bb3cb3ec 100644 --- a/client/src/pages/api/plugins/kb/detail.ts +++ b/client/src/pages/api/plugins/kb/detail.ts @@ -33,7 +33,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< _id: data._id, avatar: data.avatar, name: data.name, - userId: data.userId + userId: data.userId, + vectorModel: getVectorModel(data.vectorModel), + tags: data.tags.join(' ') } }); } catch (err) { diff --git a/client/src/pages/api/plugins/kb/paths.ts b/client/src/pages/api/plugins/kb/paths.ts new file mode 100644 index 000000000..db6b5e26d --- /dev/null +++ b/client/src/pages/api/plugins/kb/paths.ts @@ -0,0 +1,36 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/response'; +import { connectToDatabase, KB } from '@/service/mongo'; +import { KbPathItemType } from '@/types/plugin'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + await connectToDatabase(); + + const { parentId } = req.query as { parentId: string }; + + jsonRes(res, { + data: await getParents(parentId) + }); + } catch (err) { + jsonRes(res, { + code: 500, + error: err + }); + } +} + +async function getParents(parentId?: string): Promise { + if (!parentId) { + return []; + } + + const parent = await KB.findById(parentId, 'name parentId'); + + if (!parent) return []; + + const paths = await getParents(parent.parentId); + paths.push({ parentId, parentName: parent.name }); + + return paths; +} diff --git a/client/src/pages/kb/detail/index.tsx b/client/src/pages/kb/detail/index.tsx index c13d6836c..9e65d2784 100644 --- a/client/src/pages/kb/detail/index.tsx +++ b/client/src/pages/kb/detail/index.tsx @@ -138,7 +138,7 @@ const Detail = ({ kbId, currentTab }: { kbId: string; currentTab: `${TabEnum}` } px={3} borderRadius={'md'} _hover={{ bg: 'myGray.100' }} - onClick={() => router.replace('/kb/list')} + onClick={() => router.back()} > import('./component/CreateModal'), { ssr: false }); const EditFolderModal = dynamic(() => import('./component/EditFolderModal'), { ssr: false }); @@ -37,6 +38,7 @@ const Kb = () => { const router = useRouter(); const { parentId } = router.query as { parentId: string }; const { toast } = useToast(); + const { setLoading } = useGlobalStore(); const DeleteTipsMap = useRef({ [KbTypeEnum.folder]: t('kb.deleteFolderTips'), @@ -59,34 +61,81 @@ const Kb = () => { name?: string; }>(); - const { refetch } = useQuery(['loadKbList', parentId], () => loadKbList(parentId)); - /* 点击删除 */ - const onclickDelKb = useCallback( - async (id: string) => { - try { - delKbById(id); - toast({ - title: '删除成功', - status: 'success' - }); - setKbList(myKbList.filter((item) => item._id !== id)); - } catch (err: any) { - toast({ - title: getErrText(err, '删除失败'), - status: 'error' - }); - } + const { mutate: onclickDelKb } = useRequest({ + mutationFn: async (id: string) => { + setLoading(true); + await delKbById(id); + return id; }, - [toast, setKbList, myKbList] + onSuccess(id: string) { + setKbList(myKbList.filter((item) => item._id !== id)); + }, + onSettled() { + setLoading(false); + }, + successToast: t('common.Delete Success'), + errorToast: t('kb.Delete Dataset Error') + }); + + const { data, refetch } = useQuery(['loadKbList', parentId], () => { + return Promise.all([loadKbList(parentId), getKbPaths(parentId)]); + }); + + const paths = useMemo( + () => [ + { + parentId: '', + parentName: t('kb.My Dataset') + }, + ...(data?.[1] || []) + ], + [data, t] ); return ( - - 我的知识库 - + {/* url path */} + {!!parentId ? ( + + {paths.map((item, i) => ( + + { + router.push({ + query: { + parentId: item.parentId + } + }); + } + })} + > + {item.parentName} + + {i !== paths.length - 1 && } + + ))} + + ) : ( + + 我的知识库 + + )} + { {kb.name} + & { vectorModel: VectorModelItemType; }; +export type KbPathItemType = { + parentId: string; + parentName: string; +}; + /* kb type */ export interface KbItemType { _id: string;