From 0b0f184dd1e4615470e53ef6383b941cdc97ffd9 Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Fri, 8 Sep 2023 18:06:57 +0800 Subject: [PATCH] feat: dataset folder --- client/public/imgs/files/folder.svg | 1 + client/public/locales/en/common.json | 17 ++- client/public/locales/zh/common.json | 17 ++- client/src/api/plugins/kb.ts | 3 +- client/src/api/request/kb.d.ts | 11 +- client/src/components/MyMenu/index.tsx | 51 +++++++ client/src/constants/kb.ts | 16 +++ client/src/hooks/useConfirm.tsx | 14 +- client/src/hooks/useEditInfo.tsx | 2 +- client/src/pages/api/admin/initv44.ts | 62 ++++++++ client/src/pages/api/plugins/kb/create.ts | 10 +- client/src/pages/api/plugins/kb/detail.ts | 4 +- client/src/pages/api/plugins/kb/list.ts | 18 +-- client/src/pages/api/plugins/kb/update.ts | 6 +- .../app/detail/components/KBSelectModal.tsx | 3 +- .../pages/kb/list/component/CreateModal.tsx | 6 +- .../kb/list/component/EditFolderModal.tsx | 85 +++++++++++ client/src/pages/kb/list/index.tsx | 135 ++++++++++++++---- client/src/service/models/kb.ts | 11 ++ client/src/store/user.ts | 10 +- client/src/types/mongoSchema.d.ts | 7 +- client/src/types/plugin.d.ts | 7 +- 22 files changed, 417 insertions(+), 79 deletions(-) create mode 100644 client/public/imgs/files/folder.svg create mode 100644 client/src/components/MyMenu/index.tsx create mode 100644 client/src/pages/api/admin/initv44.ts create mode 100644 client/src/pages/kb/list/component/EditFolderModal.tsx diff --git a/client/public/imgs/files/folder.svg b/client/public/imgs/files/folder.svg new file mode 100644 index 000000000..602393396 --- /dev/null +++ b/client/public/imgs/files/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 3b7369b61..9eff60c16 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -2,6 +2,10 @@ "App": "App", "Cancel": "No", "Confirm": "Yes", + "Create New": "Create", + "Dataset": "Dataset", + "Folder": "Folder", + "Name": "Name", "Running": "Running", "Select value is empty": "Select value is empty", "UnKnow": "UnKnow", @@ -74,12 +78,14 @@ "Copy Successful": "Copy Successful", "Course": "", "Delete": "Delete", + "Delete Warning": "Warning", "Filed is repeat": "Filed is repeated", "Filed is repeated": "", "Input": "Input", "Output": "Output", - "export": "", - "Password inconsistency": "Password inconsistency" + "Password inconsistency": "Password inconsistency", + "Rename": "Rename", + "export": "" }, "dataset": { "Confirm to delete the data": "Confirm to delete the data?", @@ -148,6 +154,13 @@ "desc": "AI knowledge base question and answer platform based on LLM large model", "slogan": "Let the AI know more about you" }, + "kb": { + "Create Folder": "Create Folder", + "Edit Folder": "Edit Folder", + "Folder Name": "Input folder name", + "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!" + }, "navbar": { "Account": "Account", "Apps": "Apps", diff --git a/client/public/locales/zh/common.json b/client/public/locales/zh/common.json index 867bdb861..5285d14c4 100644 --- a/client/public/locales/zh/common.json +++ b/client/public/locales/zh/common.json @@ -2,6 +2,10 @@ "App": "应用", "Cancel": "取消", "Confirm": "确认", + "Create New": "新建", + "Dataset": "知识库", + "Folder": "文件夹", + "Name": "名称", "Running": "运行中", "Select value is empty": "选择的内容为空", "UnKnow": "未知", @@ -74,12 +78,14 @@ "Copy Successful": "复制成功", "Course": "", "Delete": "删除", + "Delete Warning": "删除警告", "Filed is repeat": "", "Filed is repeated": "字段重复了", "Input": "输入", "Output": "输出", - "export": "", - "Password inconsistency": "两次密码不一致" + "Password inconsistency": "两次密码不一致", + "Rename": "重命名", + "export": "" }, "dataset": { "Confirm to delete the data": "确认删除该数据?", @@ -148,6 +154,13 @@ "desc": "基于 LLM 大模型的 AI 知识库问答平台", "slogan": "让 AI 更懂你的知识" }, + "kb": { + "Create Folder": "创建文件夹", + "Edit Folder": "编辑文件夹", + "Folder Name": "输入文件夹名称", + "deleteDatasetTips": "确认删除该知识库?删除后数据无法恢复,请确认!", + "deleteFolderTips": "确认删除该文件夹及其包含的所有知识库?删除后数据无法恢复,请确认!" + }, "navbar": { "Account": "账号", "Apps": "应用", diff --git a/client/src/api/plugins/kb.ts b/client/src/api/plugins/kb.ts index ac24a5ef7..f71f4903c 100644 --- a/client/src/api/plugins/kb.ts +++ b/client/src/api/plugins/kb.ts @@ -16,7 +16,8 @@ import type { KbUpdateParams, CreateKbParams } from '../request/kb'; import { QuoteItemType } from '@/types/chat'; /* knowledge base */ -export const getKbList = () => GET(`/plugins/kb/list`); +export const getKbList = (parentId?: string) => + GET(`/plugins/kb/list`, { parentId }); export const getKbById = (id: string) => GET(`/plugins/kb/detail?id=${id}`); diff --git a/client/src/api/request/kb.d.ts b/client/src/api/request/kb.d.ts index 1ea227e7c..e139ca668 100644 --- a/client/src/api/request/kb.d.ts +++ b/client/src/api/request/kb.d.ts @@ -1,12 +1,15 @@ +import { KbTypeEnum } from '@/constants/kb'; export type KbUpdateParams = { id: string; - name: string; - tags: string; - avatar: string; + tags?: string; + name?: string; + avatar?: string; }; export type CreateKbParams = { + parentId?: string; name: string; tags: string[]; avatar: string; - vectorModel: string; + vectorModel?: string; + type: `${KbTypeEnum}`; }; diff --git a/client/src/components/MyMenu/index.tsx b/client/src/components/MyMenu/index.tsx new file mode 100644 index 000000000..76d35ff65 --- /dev/null +++ b/client/src/components/MyMenu/index.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Menu, MenuList, MenuItem } from '@chakra-ui/react'; + +interface Props { + width: number; + offset?: [number, number]; + Button: React.ReactNode; + menuList: { + isActive?: boolean; + child: React.ReactNode; + onClick: () => void; + }[]; +} + +const MyMenu = ({ width, offset = [0, 10], Button, menuList }: Props) => { + const menuItemStyles = { + borderRadius: 'sm', + py: 3, + display: 'flex', + alignItems: 'center', + _hover: { + backgroundColor: 'myWhite.600', + color: 'hover.blue' + } + }; + + return ( + + {Button} + + {menuList.map((item, i) => ( + + {item.child} + + ))} + + + ); +}; + +export default MyMenu; diff --git a/client/src/constants/kb.ts b/client/src/constants/kb.ts index 6d299566a..47742a1a5 100644 --- a/client/src/constants/kb.ts +++ b/client/src/constants/kb.ts @@ -14,3 +14,19 @@ export const defaultKbDetail: KbItemType = { maxToken: 3000 } }; + +export enum KbTypeEnum { + folder = 'folder', + dataset = 'dataset' +} + +export const KbTypeMap = { + [KbTypeEnum.folder]: { + name: 'folder' + }, + [KbTypeEnum.dataset]: { + name: 'dataset' + } +}; + +export const FolderAvatarSrc = '/imgs/files/folder.svg'; diff --git a/client/src/hooks/useConfirm.tsx b/client/src/hooks/useConfirm.tsx index ed08b7014..c8258b051 100644 --- a/client/src/hooks/useConfirm.tsx +++ b/client/src/hooks/useConfirm.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { AlertDialog, AlertDialogBody, @@ -11,21 +11,25 @@ import { } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; -export const useConfirm = (props: { title?: string; content: string }) => { +export const useConfirm = (props: { title?: string | null; content?: string | null }) => { const { t } = useTranslation(); const { title = t('Warning'), content } = props; + const [customContent, setCustomContent] = useState(content); const { isOpen, onOpen, onClose } = useDisclosure(); + const cancelRef = useRef(null); const confirmCb = useRef(); const cancelCb = useRef(); return { openConfirm: useCallback( - (confirm?: any, cancel?: any) => { + (confirm?: any, cancel?: any, customContent?: string) => { confirmCb.current = confirm; cancelCb.current = cancel; + customContent && setCustomContent(customContent); + return onOpen; }, [onOpen] @@ -44,7 +48,7 @@ export const useConfirm = (props: { title?: string; content: string }) => { {title} - {content} + {customContent} + + + + ); +}; + +export default EditFolderModal; diff --git a/client/src/pages/kb/list/index.tsx b/client/src/pages/kb/list/index.tsx index d145f8b5f..1b78eb546 100644 --- a/client/src/pages/kb/list/index.tsx +++ b/client/src/pages/kb/list/index.tsx @@ -1,13 +1,14 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import { Box, - Card, Flex, Grid, useTheme, - Button, + useDisclosure, + Card, IconButton, - useDisclosure + MenuButton, + Image } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import { useUserStore } from '@/store/user'; @@ -17,21 +18,34 @@ import { AddIcon } from '@chakra-ui/icons'; import { useQuery } from '@tanstack/react-query'; import { useToast } from '@/hooks/useToast'; import { delKbById } from '@/api/plugins/kb'; +import { useTranslation } from 'react-i18next'; import Avatar from '@/components/Avatar'; import MyIcon from '@/components/Icon'; -import Tag from '@/components/Tag'; import { serviceSideProps } from '@/utils/i18n'; import dynamic from 'next/dynamic'; +import { FolderAvatarSrc, KbTypeEnum } from '@/constants/kb'; +import Tag from '@/components/Tag'; +import MyMenu from '@/components/MyMenu'; +import { getErrText } from '@/utils/tools'; const CreateModal = dynamic(() => import('./component/CreateModal'), { ssr: false }); +const EditFolderModal = dynamic(() => import('./component/EditFolderModal'), { ssr: false }); const Kb = () => { + const { t } = useTranslation(); const theme = useTheme(); const router = useRouter(); + const { parentId } = router.query as { parentId: string }; const { toast } = useToast(); + + const DeleteTipsMap = useRef({ + [KbTypeEnum.folder]: t('kb.deleteFolderTips'), + [KbTypeEnum.dataset]: t('kb.deleteDatasetTips') + }); + const { openConfirm, ConfirmModal } = useConfirm({ - title: '删除提示', - content: '确认删除该知识库?知识库相关的文件、记录将永久删除,无法恢复!' + title: t('common.Delete Warning'), + content: '' }); const { myKbList, loadKbList, setKbList } = useUserStore(); @@ -40,8 +54,12 @@ const Kb = () => { onOpen: onOpenCreateModal, onClose: onCloseCreateModal } = useDisclosure(); + const [editFolderData, setEditFolderData] = useState<{ + id?: string; + name?: string; + }>(); - const { refetch } = useQuery(['loadKbList'], () => loadKbList()); + const { refetch } = useQuery(['loadKbList', parentId], () => loadKbList(parentId)); /* 点击删除 */ const onclickDelKb = useCallback( @@ -55,7 +73,7 @@ const Kb = () => { setKbList(myKbList.filter((item) => item._id !== id)); } catch (err: any) { toast({ - title: err?.message || '删除失败', + title: getErrText(err, '删除失败'), status: 'error' }); } @@ -69,9 +87,49 @@ const Kb = () => { 我的知识库 - + + + + {t('Create New')} + + + } + menuList={[ + { + child: ( + + {''} + {t('Folder')} + + ), + onClick: () => setEditFolderData({}) + }, + { + child: ( + + {''} + {t('Dataset')} + + ), + onClick: onOpenCreateModal + } + ]} + /> { py={4} px={5} cursor={'pointer'} - h={'140px'} + h={'130px'} border={theme.borders.md} boxShadow={'none'} userSelect={'none'} @@ -98,14 +156,23 @@ const Kb = () => { display: 'block' } }} - onClick={() => - router.push({ - pathname: '/kb/detail', - query: { - kbId: kb._id - } - }) - } + onClick={() => { + if (kb.type === KbTypeEnum.folder) { + router.push({ + pathname: '/kb/list', + query: { + parentId: kb._id + } + }); + } else if (kb.type === KbTypeEnum.dataset) { + router.push({ + pathname: '/kb/detail', + query: { + kbId: kb._id + } + }); + } + }} > @@ -126,7 +193,11 @@ const Kb = () => { }} onClick={(e) => { e.stopPropagation(); - openConfirm(() => onclickDelKb(kb._id))(); + openConfirm( + () => onclickDelKb(kb._id), + undefined, + DeleteTipsMap.current[kb.type] + )(); }} /> @@ -140,8 +211,14 @@ const Kb = () => { - - {kb.vectorModel.name} + {kb.type === KbTypeEnum.folder ? ( + {t('Folder')} + ) : ( + <> + + {kb.vectorModel.name} + + )} ))} @@ -155,7 +232,15 @@ const Kb = () => { )} - {isOpenCreateModal && } + {isOpenCreateModal && } + {!!editFolderData && ( + setEditFolderData(undefined)} + onSuccess={refetch} + parentId={parentId} + {...editFolderData} + /> + )} ); }; diff --git a/client/src/service/models/kb.ts b/client/src/service/models/kb.ts index 9c1856300..a1f9bea2a 100644 --- a/client/src/service/models/kb.ts +++ b/client/src/service/models/kb.ts @@ -1,7 +1,13 @@ import { Schema, model, models, Model } from 'mongoose'; import { kbSchema as SchemaType } from '@/types/mongoSchema'; +import { KbTypeMap } from '@/constants/kb'; const kbSchema = new Schema({ + parentId: { + type: Schema.Types.ObjectId, + ref: 'kb', + default: null + }, userId: { type: Schema.Types.ObjectId, ref: 'user', @@ -24,6 +30,11 @@ const kbSchema = new Schema({ required: true, default: 'text-embedding-ada-002' }, + type: { + type: String, + enum: Object.keys(KbTypeMap), + required: true + }, tags: { type: [String], default: [] diff --git a/client/src/store/user.ts b/client/src/store/user.ts index 41142d48d..b92b9c68b 100644 --- a/client/src/store/user.ts +++ b/client/src/store/user.ts @@ -8,7 +8,7 @@ import { getTokenLogin, putUserInfo } from '@/api/user'; import { defaultApp } from '@/constants/model'; import { AppListItemType, AppUpdateParams } from '@/types/app'; import type { KbItemType, KbListItemType } from '@/types/plugin'; -import { getKbList, getKbById } from '@/api/plugins/kb'; +import { getKbList, getKbById, putKbById } from '@/api/plugins/kb'; import { defaultKbDetail } from '@/constants/kb'; import type { AppSchema } from '@/types/mongoSchema'; @@ -26,7 +26,7 @@ type State = { clearAppModules(): void; // kb myKbList: KbListItemType[]; - loadKbList: () => Promise; + loadKbList: (parentId: string) => Promise; setKbList(val: KbListItemType[]): void; kbDetail: KbItemType; getKbDetail: (id: string, init?: boolean) => Promise; @@ -108,14 +108,14 @@ export const useUserStore = create()( }); }, myKbList: [], - async loadKbList() { - const res = await getKbList(); + async loadKbList(parentId) { + const res = await getKbList(parentId); set((state) => { state.myKbList = res; }); return res; }, - setKbList(val: KbListItemType[]) { + setKbList(val) { set((state) => { state.myKbList = val; }); diff --git a/client/src/types/mongoSchema.d.ts b/client/src/types/mongoSchema.d.ts index 5be07d351..38840e043 100644 --- a/client/src/types/mongoSchema.d.ts +++ b/client/src/types/mongoSchema.d.ts @@ -6,6 +6,7 @@ import { TrainingModeEnum } from '@/constants/plugin'; import type { AppModuleItemType } from './app'; import { ChatSourceEnum, OutLinkTypeEnum } from '@/constants/chat'; import { AppTypeEnum } from '@/constants/app'; +import { KbTypeEnum } from '@/constants/kb'; export interface UserModelSchema { _id: string; @@ -166,15 +167,17 @@ export interface OutLinkSchema { type: `${OutLinkTypeEnum}`; } -export interface kbSchema { +export type kbSchema = { _id: string; userId: string; + parentId: string; updateTime: Date; avatar: string; name: string; vectorModel: string; tags: string[]; -} + type: `${KbTypeEnum}`; +}; export interface informSchema { _id: string; diff --git a/client/src/types/plugin.d.ts b/client/src/types/plugin.d.ts index 3fcf3b788..d5090efb0 100644 --- a/client/src/types/plugin.d.ts +++ b/client/src/types/plugin.d.ts @@ -3,13 +3,10 @@ import type { kbSchema } from './mongoSchema'; export type SelectedKbType = { kbId: string; vectorModel: VectorModelItemType }[]; -export type KbListItemType = { - _id: string; - avatar: string; - name: string; - tags: string[]; +export type KbListItemType = Omit & { vectorModel: VectorModelItemType; }; + /* kb type */ export interface KbItemType { _id: string;