feat: kb folder delete and path
This commit is contained in:
parent
0b0f184dd1
commit
79e642ebfd
@ -78,6 +78,7 @@
|
|||||||
"Copy Successful": "Copy Successful",
|
"Copy Successful": "Copy Successful",
|
||||||
"Course": "",
|
"Course": "",
|
||||||
"Delete": "Delete",
|
"Delete": "Delete",
|
||||||
|
"Delete Success": "Delete Successful",
|
||||||
"Delete Warning": "Warning",
|
"Delete Warning": "Warning",
|
||||||
"Filed is repeat": "Filed is repeated",
|
"Filed is repeat": "Filed is repeated",
|
||||||
"Filed is repeated": "",
|
"Filed is repeated": "",
|
||||||
@ -156,8 +157,10 @@
|
|||||||
},
|
},
|
||||||
"kb": {
|
"kb": {
|
||||||
"Create Folder": "Create Folder",
|
"Create Folder": "Create Folder",
|
||||||
|
"Delete Dataset Error": "Delete dataset failed",
|
||||||
"Edit Folder": "Edit Folder",
|
"Edit Folder": "Edit Folder",
|
||||||
"Folder Name": "Input folder name",
|
"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!",
|
"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!"
|
"deleteFolderTips": "Are you sure to delete this folder and all the knowledge bases it contains? Data cannot be recovered after deletion, please confirm!"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -78,6 +78,7 @@
|
|||||||
"Copy Successful": "复制成功",
|
"Copy Successful": "复制成功",
|
||||||
"Course": "",
|
"Course": "",
|
||||||
"Delete": "删除",
|
"Delete": "删除",
|
||||||
|
"Delete Success": "删除成功",
|
||||||
"Delete Warning": "删除警告",
|
"Delete Warning": "删除警告",
|
||||||
"Filed is repeat": "",
|
"Filed is repeat": "",
|
||||||
"Filed is repeated": "字段重复了",
|
"Filed is repeated": "字段重复了",
|
||||||
@ -156,8 +157,10 @@
|
|||||||
},
|
},
|
||||||
"kb": {
|
"kb": {
|
||||||
"Create Folder": "创建文件夹",
|
"Create Folder": "创建文件夹",
|
||||||
|
"Delete Dataset Error": "删除知识库异常",
|
||||||
"Edit Folder": "编辑文件夹",
|
"Edit Folder": "编辑文件夹",
|
||||||
"Folder Name": "输入文件夹名称",
|
"Folder Name": "输入文件夹名称",
|
||||||
|
"My Dataset": "我的知识库",
|
||||||
"deleteDatasetTips": "确认删除该知识库?删除后数据无法恢复,请确认!",
|
"deleteDatasetTips": "确认删除该知识库?删除后数据无法恢复,请确认!",
|
||||||
"deleteFolderTips": "确认删除该文件夹及其包含的所有知识库?删除后数据无法恢复,请确认!"
|
"deleteFolderTips": "确认删除该文件夹及其包含的所有知识库?删除后数据无法恢复,请确认!"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { GET, POST, PUT, DELETE } from '../request';
|
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 { RequestPaging } from '@/types/index';
|
||||||
import { TrainingModeEnum } from '@/constants/plugin';
|
import { TrainingModeEnum } from '@/constants/plugin';
|
||||||
import {
|
import {
|
||||||
@ -10,7 +10,6 @@ import {
|
|||||||
Props as SearchTestProps,
|
Props as SearchTestProps,
|
||||||
Response as SearchTestResponse
|
Response as SearchTestResponse
|
||||||
} from '@/pages/api/openapi/kb/searchTest';
|
} 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 { Props as UpdateDataProps } from '@/pages/api/openapi/kb/updateData';
|
||||||
import type { KbUpdateParams, CreateKbParams } from '../request/kb';
|
import type { KbUpdateParams, CreateKbParams } from '../request/kb';
|
||||||
import { QuoteItemType } from '@/types/chat';
|
import { QuoteItemType } from '@/types/chat';
|
||||||
@ -19,6 +18,9 @@ import { QuoteItemType } from '@/types/chat';
|
|||||||
export const getKbList = (parentId?: string) =>
|
export const getKbList = (parentId?: string) =>
|
||||||
GET<KbListItemType[]>(`/plugins/kb/list`, { parentId });
|
GET<KbListItemType[]>(`/plugins/kb/list`, { parentId });
|
||||||
|
|
||||||
|
export const getKbPaths = (parentId?: string) =>
|
||||||
|
GET<KbPathItemType[]>('/plugins/kb/paths', { parentId });
|
||||||
|
|
||||||
export const getKbById = (id: string) => GET<KbItemType>(`/plugins/kb/detail?id=${id}`);
|
export const getKbById = (id: string) => GET<KbItemType>(`/plugins/kb/detail?id=${id}`);
|
||||||
|
|
||||||
export const postCreateKb = (data: CreateKbParams) => POST<string>(`/plugins/kb/create`, data);
|
export const postCreateKb = (data: CreateKbParams) => POST<string>(`/plugins/kb/create`, data);
|
||||||
|
|||||||
3
client/src/components/Icon/icons/light/rightArrow.svg
Normal file
3
client/src/components/Icon/icons/light/rightArrow.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M8.3 5.7a1 1 0 011.4-1.4l7.71 7.7-7.7 7.7a1 1 0 11-1.42-1.4l6.3-6.3-6.3-6.3z" fill-rule="nonzero"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 174 B |
@ -80,7 +80,8 @@ const map = {
|
|||||||
logsLight: require('./icons/light/logs.svg').default,
|
logsLight: require('./icons/light/logs.svg').default,
|
||||||
badLight: require('./icons/light/bad.svg').default,
|
badLight: require('./icons/light/bad.svg').default,
|
||||||
markLight: require('./icons/light/mark.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;
|
export type IconName = keyof typeof map;
|
||||||
|
|||||||
@ -3,12 +3,12 @@ import { jsonRes } from '@/service/response';
|
|||||||
import { connectToDatabase, KB, App, TrainingData } from '@/service/mongo';
|
import { connectToDatabase, KB, App, TrainingData } from '@/service/mongo';
|
||||||
import { authUser } from '@/service/utils/auth';
|
import { authUser } from '@/service/utils/auth';
|
||||||
import { PgClient } from '@/service/pg';
|
import { PgClient } from '@/service/pg';
|
||||||
import { Types } from 'mongoose';
|
|
||||||
import { PgTrainingTableName } from '@/constants/plugin';
|
import { PgTrainingTableName } from '@/constants/plugin';
|
||||||
import { GridFSStorage } from '@/service/lib/gridfs';
|
import { GridFSStorage } from '@/service/lib/gridfs';
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
try {
|
try {
|
||||||
|
await connectToDatabase();
|
||||||
const { id } = req.query as {
|
const { id } = req.query as {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
@ -20,26 +20,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
// 凭证校验
|
// 凭证校验
|
||||||
const { userId } = await authUser({ req, authToken: true });
|
const { userId } = await authUser({ req, authToken: true });
|
||||||
|
|
||||||
await connectToDatabase();
|
const deletedIds = [id, ...(await findAllChildrenIds(id))];
|
||||||
|
|
||||||
// delete training data
|
// delete training data
|
||||||
await TrainingData.deleteMany({
|
await TrainingData.deleteMany({
|
||||||
userId,
|
userId,
|
||||||
kbId: id
|
kbId: { $in: deletedIds }
|
||||||
});
|
});
|
||||||
|
|
||||||
// delete all pg data
|
// delete all pg data
|
||||||
await PgClient.delete(PgTrainingTableName, {
|
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
|
// delete related files
|
||||||
const gridFs = new GridFSStorage('dataset', userId);
|
const gridFs = new GridFSStorage('dataset', userId);
|
||||||
await gridFs.deleteFilesByKbId(id);
|
await Promise.all(deletedIds.map((id) => gridFs.deleteFilesByKbId(id)));
|
||||||
|
|
||||||
// delete kb data
|
// delete kb data
|
||||||
await KB.findOneAndDelete({
|
await KB.deleteMany({
|
||||||
_id: id,
|
_id: { $in: deletedIds },
|
||||||
userId
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -33,7 +33,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
_id: data._id,
|
_id: data._id,
|
||||||
avatar: data.avatar,
|
avatar: data.avatar,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
userId: data.userId
|
userId: data.userId,
|
||||||
|
vectorModel: getVectorModel(data.vectorModel),
|
||||||
|
tags: data.tags.join(' ')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
36
client/src/pages/api/plugins/kb/paths.ts
Normal file
36
client/src/pages/api/plugins/kb/paths.ts
Normal file
@ -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<any>) {
|
||||||
|
try {
|
||||||
|
await connectToDatabase();
|
||||||
|
|
||||||
|
const { parentId } = req.query as { parentId: string };
|
||||||
|
|
||||||
|
jsonRes<KbPathItemType[]>(res, {
|
||||||
|
data: await getParents(parentId)
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getParents(parentId?: string): Promise<KbPathItemType[]> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@ -138,7 +138,7 @@ const Detail = ({ kbId, currentTab }: { kbId: string; currentTab: `${TabEnum}` }
|
|||||||
px={3}
|
px={3}
|
||||||
borderRadius={'md'}
|
borderRadius={'md'}
|
||||||
_hover={{ bg: 'myGray.100' }}
|
_hover={{ bg: 'myGray.100' }}
|
||||||
onClick={() => router.replace('/kb/list')}
|
onClick={() => router.back()}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
mr={3}
|
mr={3}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Flex,
|
Flex,
|
||||||
@ -17,7 +17,7 @@ import { useConfirm } from '@/hooks/useConfirm';
|
|||||||
import { AddIcon } from '@chakra-ui/icons';
|
import { AddIcon } from '@chakra-ui/icons';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { delKbById } from '@/api/plugins/kb';
|
import { delKbById, getKbPaths } from '@/api/plugins/kb';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Avatar from '@/components/Avatar';
|
import Avatar from '@/components/Avatar';
|
||||||
import MyIcon from '@/components/Icon';
|
import MyIcon from '@/components/Icon';
|
||||||
@ -26,7 +26,8 @@ import dynamic from 'next/dynamic';
|
|||||||
import { FolderAvatarSrc, KbTypeEnum } from '@/constants/kb';
|
import { FolderAvatarSrc, KbTypeEnum } from '@/constants/kb';
|
||||||
import Tag from '@/components/Tag';
|
import Tag from '@/components/Tag';
|
||||||
import MyMenu from '@/components/MyMenu';
|
import MyMenu from '@/components/MyMenu';
|
||||||
import { getErrText } from '@/utils/tools';
|
import { useRequest } from '@/hooks/useRequest';
|
||||||
|
import { useGlobalStore } from '@/store/global';
|
||||||
|
|
||||||
const CreateModal = dynamic(() => import('./component/CreateModal'), { ssr: false });
|
const CreateModal = dynamic(() => import('./component/CreateModal'), { ssr: false });
|
||||||
const EditFolderModal = dynamic(() => import('./component/EditFolderModal'), { ssr: false });
|
const EditFolderModal = dynamic(() => import('./component/EditFolderModal'), { ssr: false });
|
||||||
@ -37,6 +38,7 @@ const Kb = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { parentId } = router.query as { parentId: string };
|
const { parentId } = router.query as { parentId: string };
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { setLoading } = useGlobalStore();
|
||||||
|
|
||||||
const DeleteTipsMap = useRef({
|
const DeleteTipsMap = useRef({
|
||||||
[KbTypeEnum.folder]: t('kb.deleteFolderTips'),
|
[KbTypeEnum.folder]: t('kb.deleteFolderTips'),
|
||||||
@ -59,34 +61,81 @@ const Kb = () => {
|
|||||||
name?: string;
|
name?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { refetch } = useQuery(['loadKbList', parentId], () => loadKbList(parentId));
|
|
||||||
|
|
||||||
/* 点击删除 */
|
/* 点击删除 */
|
||||||
const onclickDelKb = useCallback(
|
const { mutate: onclickDelKb } = useRequest({
|
||||||
async (id: string) => {
|
mutationFn: async (id: string) => {
|
||||||
try {
|
setLoading(true);
|
||||||
delKbById(id);
|
await delKbById(id);
|
||||||
toast({
|
return id;
|
||||||
title: '删除成功',
|
|
||||||
status: 'success'
|
|
||||||
});
|
|
||||||
setKbList(myKbList.filter((item) => item._id !== id));
|
|
||||||
} catch (err: any) {
|
|
||||||
toast({
|
|
||||||
title: getErrText(err, '删除失败'),
|
|
||||||
status: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[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 (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<Flex pt={3} px={5} alignItems={'center'}>
|
<Flex pt={3} px={5} alignItems={'center'}>
|
||||||
<Box flex={1} className="textlg" letterSpacing={1} fontSize={'24px'} fontWeight={'bold'}>
|
{/* url path */}
|
||||||
我的知识库
|
{!!parentId ? (
|
||||||
</Box>
|
<Flex flex={1}>
|
||||||
|
{paths.map((item, i) => (
|
||||||
|
<Flex key={item.parentId} mr={2} alignItems={'center'}>
|
||||||
|
<Box
|
||||||
|
fontSize={'lg'}
|
||||||
|
px={2}
|
||||||
|
py={1}
|
||||||
|
borderRadius={'md'}
|
||||||
|
{...(i === paths.length - 1
|
||||||
|
? {
|
||||||
|
cursor: 'default'
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
_hover: {
|
||||||
|
bg: 'myGray.100'
|
||||||
|
},
|
||||||
|
onClick: () => {
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
parentId: item.parentId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item.parentName}
|
||||||
|
</Box>
|
||||||
|
{i !== paths.length - 1 && <MyIcon name={'rightArrowLight'} color={'myGray.500'} />}
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Box flex={1} className="textlg" letterSpacing={1} fontSize={'24px'} fontWeight={'bold'}>
|
||||||
|
我的知识库
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<MyMenu
|
<MyMenu
|
||||||
offset={[-30, 10]}
|
offset={[-30, 10]}
|
||||||
width={120}
|
width={120}
|
||||||
@ -177,6 +226,7 @@ const Kb = () => {
|
|||||||
<Flex alignItems={'center'} h={'38px'}>
|
<Flex alignItems={'center'} h={'38px'}>
|
||||||
<Avatar src={kb.avatar} borderRadius={'lg'} w={'28px'} />
|
<Avatar src={kb.avatar} borderRadius={'lg'} w={'28px'} />
|
||||||
<Box ml={3}>{kb.name}</Box>
|
<Box ml={3}>{kb.name}</Box>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
className="delete"
|
className="delete"
|
||||||
position={'absolute'}
|
position={'absolute'}
|
||||||
|
|||||||
5
client/src/types/plugin.d.ts
vendored
5
client/src/types/plugin.d.ts
vendored
@ -7,6 +7,11 @@ export type KbListItemType = Omit<kbSchema, 'vectorModel'> & {
|
|||||||
vectorModel: VectorModelItemType;
|
vectorModel: VectorModelItemType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type KbPathItemType = {
|
||||||
|
parentId: string;
|
||||||
|
parentName: string;
|
||||||
|
};
|
||||||
|
|
||||||
/* kb type */
|
/* kb type */
|
||||||
export interface KbItemType {
|
export interface KbItemType {
|
||||||
_id: string;
|
_id: string;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user