From b46048609c155a277166be3529bb412592de29c9 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Mon, 11 Sep 2023 18:23:51 +0800 Subject: [PATCH] feat: move dataset (#277) --- client/public/locales/en/common.json | 5 + client/public/locales/zh/common.json | 5 + client/src/api/plugins/kb.ts | 5 +- client/src/api/request/kb.d.ts | 1 + .../src/components/Icon/icons/light/move.svg | 1 + .../Icon/icons/light/rightArrow.svg | 2 +- client/src/components/Icon/index.tsx | 3 +- client/src/components/MyMenu/index.tsx | 7 +- client/src/hooks/useEditInfo.tsx | 3 +- client/src/pages/api/plugins/kb/list.ts | 6 +- client/src/pages/api/plugins/kb/update.ts | 9 +- .../app/detail/components/KBSelectModal.tsx | 10 +- client/src/pages/components/Hero.tsx | 45 ++++- .../pages/kb/detail/components/FileCard.tsx | 2 +- .../src/pages/kb/list/component/MoveModal.tsx | 181 ++++++++++++++++++ client/src/pages/kb/list/index.tsx | 167 ++++++++++++---- client/src/store/dataset.ts | 30 ++- 17 files changed, 422 insertions(+), 60 deletions(-) create mode 100644 client/src/components/Icon/icons/light/move.svg create mode 100644 client/src/pages/kb/list/component/MoveModal.tsx diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index e32eb5db1..3e143fdce 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -5,7 +5,9 @@ "Create New": "Create", "Dataset": "Dataset", "Folder": "Folder", + "Move": "Move", "Name": "Name", + "Rename": "Rename", "Running": "Running", "Select value is empty": "Select value is empty", "UnKnow": "UnKnow", @@ -163,6 +165,7 @@ }, "kb": { "Chunk Length": "Chunk Length", + "Confirm move the folder": "Confirm Move", "Confirm to delete the file": "Are you sure to delete the file and all its data?", "Create Folder": "Create Folder", "Delete Dataset Error": "Delete dataset failed", @@ -171,7 +174,9 @@ "Filename": "Filename", "Files": "{{total}} Files", "Folder Name": "Input folder name", + "Move Failed": "Move Failed", "My Dataset": "My Dataset", + "No Folder": "No Folder", "Other Data": "Other Data", "Select Dataset": "Select Dataset", "Select Folder": "Enter folder", diff --git a/client/public/locales/zh/common.json b/client/public/locales/zh/common.json index 5bde238de..b12dafebb 100644 --- a/client/public/locales/zh/common.json +++ b/client/public/locales/zh/common.json @@ -5,7 +5,9 @@ "Create New": "新建", "Dataset": "知识库", "Folder": "文件夹", + "Move": "移动", "Name": "名称", + "Rename": "重命名", "Running": "运行中", "Select value is empty": "选择的内容为空", "UnKnow": "未知", @@ -163,6 +165,7 @@ }, "kb": { "Chunk Length": "数据总量", + "Confirm move the folder": "确认移动到该目录", "Confirm to delete the file": "确认删除该文件及其所有数据?", "Create Folder": "创建文件夹", "Delete Dataset Error": "删除知识库异常", @@ -171,7 +174,9 @@ "Filename": "文件名", "Files": "文件: {{total}}个", "Folder Name": "输入文件夹名称", + "Move Failed": "移动出现错误~", "My Dataset": "我的知识库", + "No Folder": "没有子目录了~", "Other Data": "其他数据", "Select Dataset": "选择该知识库", "Select Folder": "进入文件夹", diff --git a/client/src/api/plugins/kb.ts b/client/src/api/plugins/kb.ts index 7e93698ce..1836b5213 100644 --- a/client/src/api/plugins/kb.ts +++ b/client/src/api/plugins/kb.ts @@ -19,10 +19,11 @@ import { import { Props as UpdateDataProps } from '@/pages/api/openapi/kb/updateData'; import type { KbUpdateParams, CreateKbParams, GetKbDataListProps } from '../request/kb'; import { QuoteItemType } from '@/types/chat'; +import { KbTypeEnum } from '@/constants/kb'; /* knowledge base */ -export const getKbList = (parentId?: string) => - GET(`/plugins/kb/list`, { parentId }); +export const getKbList = (data: { parentId?: string; type?: `${KbTypeEnum}` }) => + GET(`/plugins/kb/list`, data); export const getAllDataset = () => GET(`/plugins/kb/allDataset`); export const getKbPaths = (parentId?: string) => diff --git a/client/src/api/request/kb.d.ts b/client/src/api/request/kb.d.ts index a9d698394..b382a0e49 100644 --- a/client/src/api/request/kb.d.ts +++ b/client/src/api/request/kb.d.ts @@ -3,6 +3,7 @@ import type { RequestPaging } from '@/types'; export type KbUpdateParams = { id: string; + parentId?: string; tags?: string; name?: string; avatar?: string; diff --git a/client/src/components/Icon/icons/light/move.svg b/client/src/components/Icon/icons/light/move.svg new file mode 100644 index 000000000..7e23ef30f --- /dev/null +++ b/client/src/components/Icon/icons/light/move.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/components/Icon/icons/light/rightArrow.svg b/client/src/components/Icon/icons/light/rightArrow.svg index 3dc0a8598..892a8149e 100644 --- a/client/src/components/Icon/icons/light/rightArrow.svg +++ b/client/src/components/Icon/icons/light/rightArrow.svg @@ -1,3 +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 cd1167aba..b6c9a893f 100644 --- a/client/src/components/Icon/index.tsx +++ b/client/src/components/Icon/index.tsx @@ -83,7 +83,8 @@ const map = { retryLight: require('./icons/light/retry.svg').default, rightArrowLight: require('./icons/light/rightArrow.svg').default, searchLight: require('./icons/light/search.svg').default, - plusFill: require('./icons/fill/plus.svg').default + plusFill: require('./icons/fill/plus.svg').default, + moveLight: require('./icons/light/move.svg').default }; export type IconName = keyof typeof map; diff --git a/client/src/components/MyMenu/index.tsx b/client/src/components/MyMenu/index.tsx index 76d35ff65..f3a33ad93 100644 --- a/client/src/components/MyMenu/index.tsx +++ b/client/src/components/MyMenu/index.tsx @@ -8,7 +8,7 @@ interface Props { menuList: { isActive?: boolean; child: React.ReactNode; - onClick: () => void; + onClick: () => any; }[]; } @@ -37,7 +37,10 @@ const MyMenu = ({ width, offset = [0, 10], Button, menuList }: Props) => { { + e.stopPropagation(); + item.onClick && item.onClick(); + }} color={item.isActive ? 'hover.blue' : ''} > {item.child} diff --git a/client/src/hooks/useEditInfo.tsx b/client/src/hooks/useEditInfo.tsx index e824b69b5..364b47c22 100644 --- a/client/src/hooks/useEditInfo.tsx +++ b/client/src/hooks/useEditInfo.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useRef } from 'react'; import { ModalFooter, ModalBody, Input, useDisclosure, Button } from '@chakra-ui/react'; import MyModal from '@/components/MyModal'; @@ -39,6 +39,7 @@ export const useEditInfo = ({ try { const val = inputRef.current.value; await onSuccessCb.current?.(val); + onClose(); } catch (err) { onErrorCb.current?.(err); diff --git a/client/src/pages/api/plugins/kb/list.ts b/client/src/pages/api/plugins/kb/list.ts index c9f150e17..9957411ec 100644 --- a/client/src/pages/api/plugins/kb/list.ts +++ b/client/src/pages/api/plugins/kb/list.ts @@ -4,10 +4,11 @@ import { connectToDatabase, KB } from '@/service/mongo'; import { authUser } from '@/service/utils/auth'; import { getVectorModel } from '@/service/utils/data'; import { KbListItemType } from '@/types/plugin'; +import { KbTypeEnum } from '@/constants/kb'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - const { parentId } = req.query as { parentId: string }; + const { parentId, type } = req.query as { parentId?: string; type?: `${KbTypeEnum}` }; // 凭证校验 const { userId } = await authUser({ req, authToken: true }); @@ -15,7 +16,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< const kbList = await KB.find({ userId, - parentId: parentId || null + ...(parentId !== undefined && { parentId: parentId || null }), + ...(type && { type }) }).sort({ updateTime: -1 }); const data = await Promise.all( diff --git a/client/src/pages/api/plugins/kb/update.ts b/client/src/pages/api/plugins/kb/update.ts index e3d8aa4d3..5492a1065 100644 --- a/client/src/pages/api/plugins/kb/update.ts +++ b/client/src/pages/api/plugins/kb/update.ts @@ -6,9 +6,9 @@ import type { KbUpdateParams } from '@/api/request/kb'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - const { id, name, avatar, tags = '' } = req.body as KbUpdateParams; + const { id, parentId, name, avatar, tags } = req.body as KbUpdateParams; - if (!id || !name) { + if (!id) { throw new Error('缺少参数'); } @@ -23,9 +23,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< userId }, { + ...(parentId !== undefined && { parentId: parentId || null }), ...(name && { name }), ...(avatar && { avatar }), - tags: tags.split(' ').filter((item) => item) + ...(typeof tags === 'string' && { + tags: tags.split(' ').filter((item) => item) + }) } ); diff --git a/client/src/pages/app/detail/components/KBSelectModal.tsx b/client/src/pages/app/detail/components/KBSelectModal.tsx index 2cb4519ad..2e0a223e2 100644 --- a/client/src/pages/app/detail/components/KBSelectModal.tsx +++ b/client/src/pages/app/detail/components/KBSelectModal.tsx @@ -83,7 +83,13 @@ export const KBSelectModal = ({ {!!parentId ? ( - + {paths.map((item, i) => ( {i !== paths.length - 1 && ( - + )} ))} diff --git a/client/src/pages/components/Hero.tsx b/client/src/pages/components/Hero.tsx index f0d1c1d74..33ab35c12 100644 --- a/client/src/pages/components/Hero.tsx +++ b/client/src/pages/components/Hero.tsx @@ -1,17 +1,16 @@ import { Box, Flex, Button, Image } from '@chakra-ui/react'; -import React from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { feConfigs } from '@/store/static'; import { useGlobalStore } from '@/store/global'; import MyIcon from '@/components/Icon'; import { useRouter } from 'next/router'; -import { useToast } from '@/hooks/useToast'; const Hero = () => { const router = useRouter(); - const { toast } = useToast(); const { t } = useTranslation(); const { isPc, gitStar } = useGlobalStore(); + const [showVideo, setShowVide] = useState(false); return ( @@ -62,6 +61,7 @@ const Hero = () => { mx={['-10%', 'auto']} maxW={['120%', '1000px']} alt="" + draggable={false} /> { top={'50%'} color={'#363c42b8'} transform={['translate(-50%,5px)', 'translate(-50%,40px)']} - onClick={() => { - toast({ - title: '录制中~' - }); - }} + onClick={() => setShowVide(true)} /> + {showVideo && ( + setShowVide(false)} + > + e.preventDefault()} + > + + + )} ); }; diff --git a/client/src/pages/kb/detail/components/FileCard.tsx b/client/src/pages/kb/detail/components/FileCard.tsx index 902391643..bac8b5321 100644 --- a/client/src/pages/kb/detail/components/FileCard.tsx +++ b/client/src/pages/kb/detail/components/FileCard.tsx @@ -145,7 +145,7 @@ const FileCard = ({ kbId }: { kbId: string }) => { cursor={'pointer'} title={'点击查看数据详情'} onClick={() => - router.push({ + router.replace({ query: { kbId, fileId: file.id, diff --git a/client/src/pages/kb/list/component/MoveModal.tsx b/client/src/pages/kb/list/component/MoveModal.tsx new file mode 100644 index 000000000..0d33d227d --- /dev/null +++ b/client/src/pages/kb/list/component/MoveModal.tsx @@ -0,0 +1,181 @@ +import React, { useMemo, useState } from 'react'; +import { + Card, + Flex, + Box, + Button, + ModalBody, + ModalHeader, + ModalFooter, + useTheme, + Grid +} from '@chakra-ui/react'; +import { getKbPaths } from '@/api/plugins/kb'; +import Avatar from '@/components/Avatar'; +import MyTooltip from '@/components/MyTooltip'; +import MyModal from '@/components/MyModal'; +import MyIcon from '@/components/Icon'; +import { KbTypeEnum } from '@/constants/kb'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from '@tanstack/react-query'; +import { getKbList, putKbById } from '@/api/plugins/kb'; +import { useRequest } from '@/hooks/useRequest'; + +const MoveModal = ({ + onClose, + onSuccess, + moveDataId +}: { + onClose: () => void; + onSuccess: () => void; + moveDataId: string; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + + const [parentId, setParentId] = useState(''); + + const { data } = useQuery(['getKbList', parentId], () => { + return Promise.all([getKbList({ parentId, type: 'folder' }), getKbPaths(parentId)]); + }); + const paths = useMemo( + () => [ + { + parentId: '', + parentName: t('kb.My Dataset') + }, + ...(data?.[1] || []) + ], + [data, t] + ); + const folderList = useMemo( + () => (data?.[0] || []).filter((item) => item._id !== moveDataId), + [moveDataId, data] + ); + + const { mutate, isLoading } = useRequest({ + mutationFn: () => putKbById({ id: moveDataId, parentId }), + onSuccess, + errorToast: t('kb.Move Failed') + }); + + return ( + + + + {!!parentId ? ( + + {paths.map((item, i) => ( + + { + setParentId(item.parentId); + } + })} + > + {item.parentName} + + {i !== paths.length - 1 && ( + + )} + + ))} + + ) : ( + 我的知识库 + )} + + + + + {folderList.map((item) => + (() => { + return ( + + { + setParentId(item._id); + }} + > + + + + {item.name} + + + + {item.type === KbTypeEnum.folder ? ( + {t('Folder')} + ) : ( + <> + + {item.vectorModel.name} + + )} + + + + ); + })() + )} + + {folderList.length === 0 && ( + + + + {t('kb.No Folder')} + + + )} + + + + + + + + ); +}; + +export default MoveModal; diff --git a/client/src/pages/kb/list/index.tsx b/client/src/pages/kb/list/index.tsx index 5df73a52f..a6e623e10 100644 --- a/client/src/pages/kb/list/index.tsx +++ b/client/src/pages/kb/list/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; import { Box, Flex, @@ -6,7 +6,6 @@ import { useTheme, useDisclosure, Card, - IconButton, MenuButton, Image } from '@chakra-ui/react'; @@ -16,8 +15,7 @@ import PageContainer from '@/components/PageContainer'; import { useConfirm } from '@/hooks/useConfirm'; import { AddIcon } from '@chakra-ui/icons'; import { useQuery } from '@tanstack/react-query'; -import { useToast } from '@/hooks/useToast'; -import { delKbById, getKbPaths } from '@/api/plugins/kb'; +import { delKbById, getKbPaths, putKbById } from '@/api/plugins/kb'; import { useTranslation } from 'react-i18next'; import Avatar from '@/components/Avatar'; import MyIcon from '@/components/Icon'; @@ -28,16 +26,17 @@ import Tag from '@/components/Tag'; import MyMenu from '@/components/MyMenu'; import { useRequest } from '@/hooks/useRequest'; import { useGlobalStore } from '@/store/global'; +import { useEditInfo } from '@/hooks/useEditInfo'; const CreateModal = dynamic(() => import('./component/CreateModal'), { ssr: false }); const EditFolderModal = dynamic(() => import('./component/EditFolderModal'), { ssr: false }); +const MoveModal = dynamic(() => import('./component/MoveModal'), { ssr: false }); const Kb = () => { const { t } = useTranslation(); const theme = useTheme(); const router = useRouter(); const { parentId } = router.query as { parentId: string }; - const { toast } = useToast(); const { setLoading } = useGlobalStore(); const DeleteTipsMap = useRef({ @@ -49,7 +48,13 @@ const Kb = () => { title: t('common.Delete Warning'), content: '' }); - const { myKbList, loadKbList, setKbList } = useDatasetStore(); + const { myKbList, loadKbList, setKbList, updateDataset } = useDatasetStore(); + const { onOpenModal: onOpenTitleModal, EditModal: EditTitleModal } = useEditInfo({ + title: t('Rename') + }); + const [moveDataId, setMoveDataId] = useState(); + const [dragStartId, setDragStartId] = useState(); + const [dragTargetId, setDragTargetId] = useState(); const { isOpen: isOpenCreateModal, @@ -102,8 +107,8 @@ const Kb = () => { {paths.map((item, i) => ( { > {item.parentName} - {i !== paths.length - 1 && } + {i !== paths.length - 1 && ( + + )} ))} @@ -184,6 +191,7 @@ const Kb = () => { p={5} gridTemplateColumns={['1fr', 'repeat(3,1fr)', 'repeat(4,1fr)', 'repeat(5,1fr)']} gridGap={5} + userSelect={'none'} > {myKbList.map((kb) => ( { h={'130px'} border={theme.borders.md} boxShadow={'none'} - userSelect={'none'} position={'relative'} + data-drag-id={kb.type === KbTypeEnum.folder ? kb._id : undefined} + borderColor={dragTargetId === kb._id ? 'myBlue.600' : ''} + draggable + onDragStart={(e) => { + setDragStartId(kb._id); + }} + onDragOver={(e) => { + e.preventDefault(); + const targetId = e.currentTarget.getAttribute('data-drag-id'); + if (!targetId) return; + KbTypeEnum.folder && setDragTargetId(targetId); + }} + onDragLeave={(e) => { + e.preventDefault(); + setDragTargetId(undefined); + }} + onDrop={async (e) => { + e.preventDefault(); + if (!dragTargetId || !dragStartId || dragTargetId === dragStartId) return; + // update parentId + try { + await putKbById({ + id: dragStartId, + parentId: dragTargetId + }); + refetch(); + } catch (error) {} + setDragTargetId(undefined); + }} _hover={{ boxShadow: '1px 1px 10px rgba(0,0,0,0.2)', borderColor: 'transparent', @@ -223,33 +259,85 @@ const Kb = () => { } }} > + { + e.stopPropagation(); + }} + > + + + } + menuList={[ + { + child: ( + + + {t('Rename')} + + ), + onClick: () => + onOpenTitleModal({ + defaultVal: kb.name, + onSuccess: (val) => { + if (val === kb.name || !val) return; + updateDataset({ id: kb._id, name: val }); + } + }) + }, + { + child: ( + + + {t('Move')} + + ), + onClick: () => setMoveDataId(kb._id) + }, + { + child: ( + + + {t('common.Delete')} + + ), + onClick: () => { + openConfirm( + () => onclickDelKb(kb._id), + undefined, + DeleteTipsMap.current[kb.type] + )(); + } + } + ]} + /> {kb.name} - - } - variant={'base'} - borderRadius={'md'} - aria-label={'delete'} - display={['', 'none']} - _hover={{ - bg: 'red.100' - }} - onClick={(e) => { - e.stopPropagation(); - openConfirm( - () => onclickDelKb(kb._id), - undefined, - DeleteTipsMap.current[kb.type] - )(); - }} - /> @@ -282,6 +370,7 @@ const Kb = () => { )} + {isOpenCreateModal && } {!!editFolderData && ( { {...editFolderData} /> )} + {!!moveDataId && ( + setMoveDataId('')} + onSuccess={() => { + refetch(); + setMoveDataId(''); + }} + /> + )} ); }; diff --git a/client/src/store/dataset.ts b/client/src/store/dataset.ts index f6df0396c..28c045fbc 100644 --- a/client/src/store/dataset.ts +++ b/client/src/store/dataset.ts @@ -3,8 +3,9 @@ import { devtools, persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; import { type KbTestItemType } from '@/types/plugin'; import type { KbItemType, KbListItemType } from '@/types/plugin'; -import { getKbList, getKbById, getAllDataset } from '@/api/plugins/kb'; +import { getKbList, getKbById, getAllDataset, putKbById } from '@/api/plugins/kb'; import { defaultKbDetail } from '@/constants/kb'; +import { KbUpdateParams } from '@/api/request/kb'; type State = { datasets: KbListItemType[]; @@ -14,6 +15,7 @@ type State = { setKbList(val: KbListItemType[]): void; kbDetail: KbItemType; getKbDetail: (id: string, init?: boolean) => Promise; + updateDataset: (data: KbUpdateParams) => Promise; kbTestList: KbTestItemType[]; pushKbTestItem: (data: KbTestItemType) => void; @@ -34,8 +36,8 @@ export const useDatasetStore = create()( return res; }, myKbList: [], - async loadKbList(parentId) { - const res = await getKbList(parentId); + async loadKbList(parentId = '') { + const res = await getKbList({ parentId }); set((state) => { state.myKbList = res; }); @@ -58,6 +60,28 @@ export const useDatasetStore = create()( return data; }, + async updateDataset(data) { + if (get().kbDetail._id === data.id) { + set((state) => { + state.kbDetail = { + ...state.kbDetail, + ...data + }; + }); + } + set((state) => { + state.myKbList = state.myKbList = state.myKbList.map((item) => + item._id === data.id + ? { + ...item, + ...data, + tags: data.tags?.split(' ') || [] + } + : item + ); + }); + await putKbById(data); + }, kbTestList: [], pushKbTestItem(data) { set((state) => {