fix: mark modal cannot select folder (#327)

This commit is contained in:
Archer
2023-09-20 11:26:17 +08:00
committed by GitHub
parent 858117f8c0
commit 63b183a9fe
7 changed files with 323 additions and 279 deletions

View File

@@ -1,58 +1,43 @@
import React, { useRef, useState } from 'react'; import React, { useState } from 'react';
import { import { ModalBody, useTheme, ModalFooter, Button, Box, Card, Flex, Grid } from '@chakra-ui/react';
ModalBody,
useTheme,
ModalFooter,
Button,
ModalHeader,
Box,
Card,
Flex
} from '@chakra-ui/react';
import MyModal from '../MyModal';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { useDatasetStore } from '@/store/dataset';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import Avatar from '../Avatar'; import Avatar from '../Avatar';
import MyIcon from '@/components/Icon'; import MyIcon from '@/components/Icon';
import { useGlobalStore } from '@/store/global'; import { KbTypeEnum } from '@/constants/kb';
import DatasetSelectModal, { useDatasetSelect } from '@/components/core/dataset/SelectModal';
const SelectDataset = ({ const SelectDataset = ({
isOpen,
onSuccess, onSuccess,
onClose onClose
}: { }: {
isOpen: boolean;
onSuccess: (kbId: string) => void; onSuccess: (kbId: string) => void;
onClose: () => void; onClose: () => void;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const { isPc } = useGlobalStore();
const { toast } = useToast(); const { toast } = useToast();
const { myKbList, loadKbList } = useDatasetStore();
const [selectedId, setSelectedId] = useState<string>(); const [selectedId, setSelectedId] = useState<string>();
const { paths, parentId, setParentId, datasets } = useDatasetSelect();
useQuery(['loadKbList'], () => loadKbList());
return ( return (
<MyModal isOpen={true} onClose={onClose} w={'100%'} maxW={['90vw', '900px']} isCentered={!isPc}> <DatasetSelectModal
<Flex flexDirection={'column'} h={['90vh', 'auto']}> isOpen={isOpen}
<ModalHeader> paths={paths}
<Box>{t('chat.Select Mark Kb')}</Box> onClose={onClose}
<Box fontSize={'sm'} color={'myGray.500'} fontWeight={'normal'}> parentId={parentId}
{t('chat.Select Mark Kb Desc')} setParentId={setParentId}
</Box> tips={t('chat.Select Mark Kb Desc')}
</ModalHeader> >
<ModalBody <ModalBody flex={['1 0 0', '0 0 auto']} maxH={'80vh'} overflowY={'auto'}>
flex={['1 0 0', '0 0 auto']} <Grid
maxH={'80vh'}
overflowY={'auto'}
display={'grid'}
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']} gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
gridGap={3} gridGap={3}
userSelect={'none'} userSelect={'none'}
> >
{myKbList.map((item) => {datasets.map((item) =>
(() => { (() => {
const selected = selectedId === item._id; const selected = selectedId === item._id;
return ( return (
@@ -72,7 +57,11 @@ const SelectDataset = ({
} }
: {})} : {})}
onClick={() => { onClick={() => {
setSelectedId(item._id); if (item.type === KbTypeEnum.folder) {
setParentId(item._id);
} else {
setSelectedId(item._id);
}
}} }}
> >
<Flex alignItems={'center'} h={'38px'}> <Flex alignItems={'center'} h={'38px'}>
@@ -89,28 +78,36 @@ const SelectDataset = ({
); );
})() })()
)} )}
</ModalBody> </Grid>
<ModalFooter> {datasets.length === 0 && (
<Button variant={'base'} mr={2} onClick={onClose}> <Flex mt={5} flexDirection={'column'} alignItems={'center'}>
{t('Cancel')} <MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
</Button> <Box mt={2} color={'myGray.500'}>
<Button 西~
onClick={() => { </Box>
if (!selectedId) { </Flex>
return toast({ )}
status: 'warning', </ModalBody>
title: t('Select value is empty') <ModalFooter>
}); <Button variant={'base'} mr={2} onClick={onClose}>
} {t('Cancel')}
</Button>
<Button
onClick={() => {
if (!selectedId) {
return toast({
status: 'warning',
title: t('Select value is empty')
});
}
onSuccess(selectedId); onSuccess(selectedId);
}} }}
> >
{t('Confirm')} {t('Confirm')}
</Button> </Button>
</ModalFooter> </ModalFooter>
</Flex> </DatasetSelectModal>
</MyModal>
); );
}; };

View File

@@ -985,13 +985,12 @@ const ChatBox = (
/> />
)} )}
{/* select one dataset to insert markData */} {/* select one dataset to insert markData */}
{adminMarkData && !adminMarkData.kbId && ( <SelectDataset
<SelectDataset isOpen={!!adminMarkData && !adminMarkData.kbId}
onClose={() => setAdminMarkData(undefined)} onClose={() => setAdminMarkData(undefined)}
// @ts-ignore // @ts-ignore
onSuccess={(kbId) => setAdminMarkData((state) => ({ ...state, kbId }))} onSuccess={(kbId) => setAdminMarkData((state) => ({ ...state, kbId }))}
/> />
)}
{/* edit markData modal */} {/* edit markData modal */}
{adminMarkData && adminMarkData.kbId && ( {adminMarkData && adminMarkData.kbId && (
<InputDataModal <InputDataModal

View File

@@ -101,7 +101,7 @@ const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconName } &
.catch((error) => console.log(error)); .catch((error) => console.log(error));
}, [name]); }, [name]);
return name ? ( return !!name && !!iconPaths[name] ? (
<Icon <Icon
{...IconComponent} {...IconComponent}
w={w} w={w}

View File

@@ -0,0 +1,122 @@
import { getKbList, getKbPaths } from '@/api/plugins/kb';
import MyModal from '@/components/MyModal';
import { useQuery } from '@tanstack/react-query';
import React, { Dispatch, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useGlobalStore } from '@/store/global';
import { Box, Flex, ModalHeader } from '@chakra-ui/react';
import MyIcon from '@/components/Icon';
type PathItemType = {
parentId: string;
parentName: string;
};
const DatasetSelectModal = ({
isOpen,
parentId,
setParentId,
paths,
onClose,
tips,
children
}: {
isOpen: boolean;
parentId?: string;
setParentId: Dispatch<string>;
paths: PathItemType[];
onClose: () => void;
tips?: string | null;
children: React.ReactNode;
}) => {
const { t } = useTranslation();
const { isPc } = useGlobalStore();
return (
<MyModal
isOpen={isOpen}
onClose={onClose}
w={'100%'}
maxW={['90vw', '900px']}
isCentered={!isPc}
>
<Flex flexDirection={'column'} h={['90vh', 'auto']}>
<ModalHeader>
{!!parentId ? (
<Flex
flex={1}
userSelect={'none'}
fontSize={['sm', 'lg']}
fontWeight={'normal'}
color={'myGray.900'}
>
{paths.map((item, i) => (
<Flex key={item.parentId} mr={2} alignItems={'center'}>
<Box
fontSize={'lg'}
borderRadius={'md'}
{...(i === paths.length - 1
? {
cursor: 'default'
}
: {
cursor: 'pointer',
_hover: {
color: 'myBlue.600'
},
onClick: () => {
setParentId(item.parentId);
}
})}
>
{item.parentName}
</Box>
{i !== paths.length - 1 && (
<MyIcon name={'rightArrowLight'} color={'myGray.500'} w={['18px', '24px']} />
)}
</Flex>
))}
</Flex>
) : (
<Box>{t('chat.Select Mark Kb')}</Box>
)}
{!!tips && (
<Box fontSize={'sm'} color={'myGray.500'} fontWeight={'normal'}>
{tips}
</Box>
)}
</ModalHeader>
{children}
</Flex>
</MyModal>
);
};
export const useDatasetSelect = () => {
const { t } = useTranslation();
const [parentId, setParentId] = useState<string>();
const { data } = useQuery(['loadDatasetData', parentId], () =>
Promise.all([getKbList({ parentId }), getKbPaths(parentId)])
);
const paths = useMemo(
() => [
{
parentId: '',
parentName: t('kb.My Dataset')
},
...(data?.[1] || [])
],
[data, t]
);
return {
parentId,
setParentId,
datasets: data?.[0] || [],
paths
};
};
export default DatasetSelectModal;

View File

@@ -71,7 +71,7 @@ const Settings = ({ appId }: { appId: string }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const { appDetail, updateAppDetail } = useUserStore(); const { appDetail, updateAppDetail } = useUserStore();
const { loadAllDatasets, datasets } = useDatasetStore(); const { loadAllDatasets, allDatasets } = useDatasetStore();
const { isPc } = useGlobalStore(); const { isPc } = useGlobalStore();
const [editVariable, setEditVariable] = useState<VariableItemType>(); const [editVariable, setEditVariable] = useState<VariableItemType>();
@@ -131,8 +131,8 @@ const Settings = ({ appId }: { appId: string }) => {
); );
}, [getValues, refresh]); }, [getValues, refresh]);
const selectedKbList = useMemo( const selectedKbList = useMemo(
() => datasets.filter((item) => kbList.find((kb) => kb.kbId === item._id)), () => allDatasets.filter((item) => kbList.find((kb) => kb.kbId === item._id)),
[datasets, kbList] [allDatasets, kbList]
); );
/* 点击删除 */ /* 点击删除 */
@@ -564,16 +564,15 @@ const Settings = ({ appId }: { appId: string }) => {
defaultData={getValues('chatModel')} defaultData={getValues('chatModel')}
/> />
)} )}
{isOpenKbSelect && ( <KBSelectModal
<KBSelectModal isOpen={isOpenKbSelect}
activeKbs={selectedKbList.map((item) => ({ activeKbs={selectedKbList.map((item) => ({
kbId: item._id, kbId: item._id,
vectorModel: item.vectorModel vectorModel: item.vectorModel
}))} }))}
onClose={onCloseKbSelect} onClose={onCloseKbSelect}
onChange={replaceKbList} onChange={replaceKbList}
/> />
)}
{isOpenKbParams && ( {isOpenKbParams && (
<KbParamsModal <KbParamsModal
searchEmptyText={getValues('kb.searchEmptyText')} searchEmptyText={getValues('kb.searchEmptyText')}

View File

@@ -5,19 +5,16 @@ import {
Box, Box,
Button, Button,
ModalBody, ModalBody,
ModalHeader,
ModalFooter, ModalFooter,
useTheme, useTheme,
Textarea, Textarea,
Grid, Grid,
Divider Divider
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { getKbPaths } from '@/api/plugins/kb';
import Avatar from '@/components/Avatar'; import Avatar from '@/components/Avatar';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { QuestionOutlineIcon } from '@chakra-ui/icons'; import { QuestionOutlineIcon } from '@chakra-ui/icons';
import type { SelectedKbType } from '@/types/plugin'; import type { SelectedKbType } from '@/types/plugin';
import { useGlobalStore } from '@/store/global';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import MySlider from '@/components/Slider'; import MySlider from '@/components/Slider';
import MyTooltip from '@/components/MyTooltip'; import MyTooltip from '@/components/MyTooltip';
@@ -28,6 +25,7 @@ import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useDatasetStore } from '@/store/dataset'; import { useDatasetStore } from '@/store/dataset';
import { feConfigs } from '@/store/static'; import { feConfigs } from '@/store/static';
import DatasetSelectModal, { useDatasetSelect } from '@/components/core/dataset/SelectModal';
export type KbParamsType = { export type KbParamsType = {
searchSimilarity: number; searchSimilarity: number;
@@ -36,10 +34,12 @@ export type KbParamsType = {
}; };
export const KBSelectModal = ({ export const KBSelectModal = ({
isOpen,
activeKbs = [], activeKbs = [],
onChange, onChange,
onClose onClose
}: { }: {
isOpen: boolean;
activeKbs: SelectedKbType; activeKbs: SelectedKbType;
onChange: (e: SelectedKbType) => void; onChange: (e: SelectedKbType) => void;
onClose: () => void; onClose: () => void;
@@ -47,229 +47,156 @@ export const KBSelectModal = ({
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const [selectedKbList, setSelectedKbList] = useState<SelectedKbType>(activeKbs); const [selectedKbList, setSelectedKbList] = useState<SelectedKbType>(activeKbs);
const { isPc } = useGlobalStore();
const { toast } = useToast(); const { toast } = useToast();
const [parentId, setParentId] = useState<string>(); const { paths, parentId, setParentId, datasets } = useDatasetSelect();
const { myKbList, loadKbList, datasets, loadAllDatasets } = useDatasetStore(); const { allDatasets, loadAllDatasets } = useDatasetStore();
const { data } = useQuery(['loadKbList', parentId], () => {
return Promise.all([loadKbList(parentId), getKbPaths(parentId)]);
});
useQuery(['loadAllDatasets'], loadAllDatasets); useQuery(['loadAllDatasets'], loadAllDatasets);
const paths = useMemo(
() => [
{
parentId: '',
parentName: t('kb.My Dataset')
},
...(data?.[1] || [])
],
[data, t]
);
const filterKbList = useMemo(() => { const filterKbList = useMemo(() => {
return { return {
selected: datasets.filter((item) => selectedKbList.find((kb) => kb.kbId === item._id)), selected: allDatasets.filter((item) => selectedKbList.find((kb) => kb.kbId === item._id)),
unSelected: myKbList.filter((item) => !selectedKbList.find((kb) => kb.kbId === item._id)) unSelected: datasets.filter((item) => !selectedKbList.find((kb) => kb.kbId === item._id))
}; };
}, [myKbList, datasets, selectedKbList]); }, [datasets, allDatasets, selectedKbList]);
return ( return (
<MyModal <DatasetSelectModal
isOpen={true} isOpen={isOpen}
isCentered={!isPc} paths={paths}
maxW={['90vw', '800px']} parentId={parentId}
w={'800px'} setParentId={setParentId}
tips={'仅能选择同一个索引模型的知识库'}
onClose={onClose} onClose={onClose}
> >
<Flex flexDirection={'column'} h={['90vh', 'auto']}> <ModalBody flex={['1 0 0', '0 0 auto']} maxH={'80vh'} overflowY={'auto'} userSelect={'none'}>
<ModalHeader> <Grid gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']} gridGap={3}>
{!!parentId ? ( {filterKbList.selected.map((item) =>
<Flex (() => {
flex={1} return (
userSelect={'none'} <Card
fontSize={['sm', 'lg']} key={item._id}
fontWeight={'normal'} p={3}
color={'myGray.900'} border={theme.borders.base}
> boxShadow={'sm'}
{paths.map((item, i) => ( bg={'myBlue.300'}
<Flex key={item.parentId} mr={2} alignItems={'center'}> >
<Box <Flex alignItems={'center'} h={'38px'}>
fontSize={'lg'} <Avatar src={item.avatar} w={['24px', '28px']}></Avatar>
borderRadius={'md'} <Box flex={'1 0 0'} mx={3}>
{...(i === paths.length - 1 {item.name}
? { </Box>
cursor: 'default' <MyIcon
} name={'delete'}
: { w={'14px'}
cursor: 'pointer', cursor={'pointer'}
_hover: { _hover={{ color: 'red.500' }}
color: 'myBlue.600' onClick={() => {
}, setSelectedKbList((state) => state.filter((kb) => kb.kbId !== item._id));
onClick: () => { }}
setParentId(item.parentId); />
} </Flex>
})} </Card>
> );
{item.parentName} })()
</Box>
{i !== paths.length - 1 && (
<MyIcon name={'rightArrowLight'} color={'myGray.500'} w={['18px', '24px']} />
)}
</Flex>
))}
</Flex>
) : (
<Box>({selectedKbList.length})</Box>
)} )}
{isPc && ( </Grid>
<Box fontSize={'sm'} color={'myGray.500'} fontWeight={'normal'}>
</Box>
)}
</ModalHeader>
<ModalBody {filterKbList.selected.length > 0 && <Divider my={3} />}
flex={['1 0 0', '0 0 auto']}
maxH={'80vh'} <Grid gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']} gridGap={3}>
overflowY={'auto'} {filterKbList.unSelected.map((item) =>
display={'grid'} (() => {
userSelect={'none'} return (
> <MyTooltip
<Grid key={item._id}
h={'auto'} label={
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']} item.type === KbTypeEnum.dataset
gridGap={3} ? t('kb.Select Dataset')
> : t('kb.Select Folder')
{filterKbList.selected.map((item) => }
(() => { >
return (
<Card <Card
key={item._id}
p={3} p={3}
border={theme.borders.base} border={theme.borders.base}
boxShadow={'sm'} boxShadow={'sm'}
bg={'myBlue.300'} h={'80px'}
cursor={'pointer'}
_hover={{
boxShadow: 'md'
}}
onClick={() => {
if (item.type === KbTypeEnum.folder) {
setParentId(item._id);
} else if (item.type === KbTypeEnum.dataset) {
const vectorModel = selectedKbList[0]?.vectorModel?.model;
if (vectorModel && vectorModel !== item.vectorModel.model) {
return toast({
status: 'warning',
title: '仅能选择同一个索引模型的知识库'
});
}
setSelectedKbList((state) => [
...state,
{ kbId: item._id, vectorModel: item.vectorModel }
]);
}
}}
> >
<Flex alignItems={'center'} h={'38px'}> <Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['24px', '28px']}></Avatar> <Avatar src={item.avatar} w={['24px', '28px']}></Avatar>
<Box flex={'1 0 0'} mx={3}> <Box
className="textEllipsis"
ml={3}
fontWeight={'bold'}
fontSize={['md', 'lg', 'xl']}
>
{item.name} {item.name}
</Box> </Box>
<MyIcon </Flex>
name={'delete'} <Flex justifyContent={'flex-end'} alignItems={'center'} fontSize={'sm'}>
w={'14px'} {item.type === KbTypeEnum.folder ? (
cursor={'pointer'} <Box color={'myGray.500'}>{t('Folder')}</Box>
_hover={{ color: 'red.500' }} ) : (
onClick={() => { <>
setSelectedKbList((state) => state.filter((kb) => kb.kbId !== item._id)); <MyIcon mr={1} name="kbTest" w={'12px'} />
}} <Box color={'myGray.500'}>{item.vectorModel.name}</Box>
/> </>
)}
</Flex> </Flex>
</Card> </Card>
); </MyTooltip>
})() );
)} })()
</Grid>
{filterKbList.selected.length > 0 && <Divider my={3} />}
<Grid
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
gridGap={3}
>
{filterKbList.unSelected.map((item) =>
(() => {
return (
<MyTooltip
key={item._id}
label={
item.type === KbTypeEnum.dataset
? t('kb.Select Dataset')
: t('kb.Select Folder')
}
>
<Card
p={3}
border={theme.borders.base}
boxShadow={'sm'}
h={'80px'}
cursor={'pointer'}
_hover={{
boxShadow: 'md'
}}
onClick={() => {
if (item.type === KbTypeEnum.folder) {
setParentId(item._id);
} else if (item.type === KbTypeEnum.dataset) {
const vectorModel = selectedKbList[0]?.vectorModel?.model;
if (vectorModel && vectorModel !== item.vectorModel.model) {
return toast({
status: 'warning',
title: '仅能选择同一个索引模型的知识库'
});
}
setSelectedKbList((state) => [
...state,
{ kbId: item._id, vectorModel: item.vectorModel }
]);
}
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['24px', '28px']}></Avatar>
<Box
className="textEllipsis"
ml={3}
fontWeight={'bold'}
fontSize={['md', 'lg', 'xl']}
>
{item.name}
</Box>
</Flex>
<Flex justifyContent={'flex-end'} alignItems={'center'} fontSize={'sm'}>
{item.type === KbTypeEnum.folder ? (
<Box color={'myGray.500'}>{t('Folder')}</Box>
) : (
<>
<MyIcon mr={1} name="kbTest" w={'12px'} />
<Box color={'myGray.500'}>{item.vectorModel.name}</Box>
</>
)}
</Flex>
</Card>
</MyTooltip>
);
})()
)}
</Grid>
{filterKbList.unSelected.length === 0 && (
<Flex mt={5} flexDirection={'column'} alignItems={'center'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
西~
</Box>
</Flex>
)} )}
</ModalBody> </Grid>
{filterKbList.unSelected.length === 0 && (
<Flex mt={5} flexDirection={'column'} alignItems={'center'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
西~
</Box>
</Flex>
)}
</ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button
onClick={() => { onClick={() => {
// filter out the kb that is not in the kList // filter out the kb that is not in the kList
const filterKbList = selectedKbList.filter((kb) => { const filterKbList = selectedKbList.filter((kb) => {
return datasets.find((item) => item._id === kb.kbId); return datasets.find((item) => item._id === kb.kbId);
}); });
onClose(); onClose();
onChange(filterKbList); onChange(filterKbList);
}} }}
> >
</Button> </Button>
</ModalFooter> </ModalFooter>
</Flex> </DatasetSelectModal>
</MyModal>
); );
}; };

View File

@@ -8,7 +8,7 @@ import { defaultKbDetail } from '@/constants/kb';
import { KbUpdateParams } from '@/api/request/kb'; import { KbUpdateParams } from '@/api/request/kb';
type State = { type State = {
datasets: KbListItemType[]; allDatasets: KbListItemType[];
loadAllDatasets: () => Promise<KbListItemType[]>; loadAllDatasets: () => Promise<KbListItemType[]>;
myKbList: KbListItemType[]; myKbList: KbListItemType[];
loadKbList: (parentId?: string) => Promise<any>; loadKbList: (parentId?: string) => Promise<any>;
@@ -27,11 +27,11 @@ export const useDatasetStore = create<State>()(
devtools( devtools(
persist( persist(
immer((set, get) => ({ immer((set, get) => ({
datasets: [], allDatasets: [],
async loadAllDatasets() { async loadAllDatasets() {
const res = await getAllDataset(); const res = await getAllDataset();
set((state) => { set((state) => {
state.datasets = res; state.allDatasets = res;
}); });
return res; return res;
}, },