301 lines
9.9 KiB
TypeScript
301 lines
9.9 KiB
TypeScript
import React, { useMemo, useState } from 'react';
|
||
import {
|
||
Card,
|
||
Flex,
|
||
Box,
|
||
Button,
|
||
ModalBody,
|
||
ModalFooter,
|
||
useTheme,
|
||
Textarea,
|
||
Grid,
|
||
Divider
|
||
} from '@chakra-ui/react';
|
||
import Avatar from '@/components/Avatar';
|
||
import { useForm } from 'react-hook-form';
|
||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||
import type { SelectedKbType } from '@/types/plugin';
|
||
import { useToast } from '@/hooks/useToast';
|
||
import MySlider from '@/components/Slider';
|
||
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 { useDatasetStore } from '@/store/dataset';
|
||
import { feConfigs } from '@/store/static';
|
||
import DatasetSelectModal, { useDatasetSelect } from '@/components/core/dataset/SelectModal';
|
||
|
||
export type KbParamsType = {
|
||
searchSimilarity: number;
|
||
searchLimit: number;
|
||
searchEmptyText: string;
|
||
};
|
||
|
||
export const KBSelectModal = ({
|
||
isOpen,
|
||
activeKbs = [],
|
||
onChange,
|
||
onClose
|
||
}: {
|
||
isOpen: boolean;
|
||
activeKbs: SelectedKbType;
|
||
onChange: (e: SelectedKbType) => void;
|
||
onClose: () => void;
|
||
}) => {
|
||
const { t } = useTranslation();
|
||
const theme = useTheme();
|
||
const [selectedKbList, setSelectedKbList] = useState<SelectedKbType>(activeKbs);
|
||
const { toast } = useToast();
|
||
const { paths, parentId, setParentId, datasets } = useDatasetSelect();
|
||
const { allDatasets, loadAllDatasets } = useDatasetStore();
|
||
|
||
useQuery(['loadAllDatasets'], loadAllDatasets);
|
||
|
||
const filterKbList = useMemo(() => {
|
||
return {
|
||
selected: allDatasets.filter((item) => selectedKbList.find((kb) => kb.kbId === item._id)),
|
||
unSelected: datasets.filter((item) => !selectedKbList.find((kb) => kb.kbId === item._id))
|
||
};
|
||
}, [datasets, allDatasets, selectedKbList]);
|
||
|
||
return (
|
||
<DatasetSelectModal
|
||
isOpen={isOpen}
|
||
paths={paths}
|
||
parentId={parentId}
|
||
setParentId={setParentId}
|
||
tips={'仅能选择同一个索引模型的知识库'}
|
||
onClose={onClose}
|
||
>
|
||
<ModalBody flex={['1 0 0', '0 0 auto']} maxH={'80vh'} overflowY={'auto'} userSelect={'none'}>
|
||
<Grid gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']} gridGap={3}>
|
||
{filterKbList.selected.map((item) =>
|
||
(() => {
|
||
return (
|
||
<Card
|
||
key={item._id}
|
||
p={3}
|
||
border={theme.borders.base}
|
||
boxShadow={'sm'}
|
||
bg={'myBlue.300'}
|
||
>
|
||
<Flex alignItems={'center'} h={'38px'}>
|
||
<Avatar src={item.avatar} w={['24px', '28px']}></Avatar>
|
||
<Box flex={'1 0 0'} mx={3}>
|
||
{item.name}
|
||
</Box>
|
||
<MyIcon
|
||
name={'delete'}
|
||
w={'14px'}
|
||
cursor={'pointer'}
|
||
_hover={{ color: 'red.500' }}
|
||
onClick={() => {
|
||
setSelectedKbList((state) => state.filter((kb) => kb.kbId !== item._id));
|
||
}}
|
||
/>
|
||
</Flex>
|
||
</Card>
|
||
);
|
||
})()
|
||
)}
|
||
</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>
|
||
|
||
<ModalFooter>
|
||
<Button
|
||
onClick={() => {
|
||
// filter out the kb that is not in the kList
|
||
const filterKbList = selectedKbList.filter((kb) => {
|
||
return datasets.find((item) => item._id === kb.kbId);
|
||
});
|
||
|
||
onClose();
|
||
onChange(filterKbList);
|
||
}}
|
||
>
|
||
完成
|
||
</Button>
|
||
</ModalFooter>
|
||
</DatasetSelectModal>
|
||
);
|
||
};
|
||
|
||
export const KbParamsModal = ({
|
||
searchEmptyText,
|
||
searchLimit,
|
||
searchSimilarity,
|
||
onClose,
|
||
onChange
|
||
}: KbParamsType & { onClose: () => void; onChange: (e: KbParamsType) => void }) => {
|
||
const [refresh, setRefresh] = useState(false);
|
||
const { register, setValue, getValues, handleSubmit } = useForm<KbParamsType>({
|
||
defaultValues: {
|
||
searchEmptyText,
|
||
searchLimit,
|
||
searchSimilarity
|
||
}
|
||
});
|
||
|
||
return (
|
||
<MyModal isOpen={true} onClose={onClose} title={'搜索参数调整'} minW={['90vw', '600px']}>
|
||
<Flex flexDirection={'column'}>
|
||
<ModalBody>
|
||
<Box display={['block', 'flex']} py={5} pt={[0, 5]}>
|
||
<Box flex={'0 0 100px'} mb={[8, 0]}>
|
||
相似度
|
||
<MyTooltip
|
||
label={'不同索引模型的相似度有区别,请通过搜索测试来选择合适的数值'}
|
||
forceShow
|
||
>
|
||
<QuestionOutlineIcon ml={1} />
|
||
</MyTooltip>
|
||
</Box>
|
||
<MySlider
|
||
markList={[
|
||
{ label: '0', value: 0 },
|
||
{ label: '1', value: 1 }
|
||
]}
|
||
min={0}
|
||
max={1}
|
||
step={0.01}
|
||
value={getValues('searchSimilarity')}
|
||
onChange={(val) => {
|
||
setValue('searchSimilarity', val);
|
||
setRefresh(!refresh);
|
||
}}
|
||
/>
|
||
</Box>
|
||
<Box display={['block', 'flex']} py={8}>
|
||
<Box flex={'0 0 100px'} mb={[8, 0]}>
|
||
单次搜索数量
|
||
</Box>
|
||
<Box flex={1}>
|
||
<MySlider
|
||
markList={[
|
||
{ label: '1', value: 1 },
|
||
{ label: '20', value: 20 }
|
||
]}
|
||
min={1}
|
||
max={20}
|
||
value={getValues('searchLimit')}
|
||
onChange={(val) => {
|
||
setValue('searchLimit', val);
|
||
setRefresh(!refresh);
|
||
}}
|
||
/>
|
||
</Box>
|
||
</Box>
|
||
<Box display={['block', 'flex']} pt={3}>
|
||
<Box flex={'0 0 100px'} mb={[2, 0]}>
|
||
空搜索回复
|
||
</Box>
|
||
<Box flex={1}>
|
||
<Textarea
|
||
rows={5}
|
||
maxLength={500}
|
||
placeholder={`若填写该内容,没有搜索到对应内容时,将直接回复填写的内容。\n为了连贯上下文,${feConfigs?.systemTitle} 会取部分上一个聊天的搜索记录作为补充,因此在连续对话时,该功能可能会失效。`}
|
||
{...register('searchEmptyText')}
|
||
></Textarea>
|
||
</Box>
|
||
</Box>
|
||
</ModalBody>
|
||
<ModalFooter>
|
||
<Button variant={'base'} mr={3} onClick={onClose}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
onClick={() => {
|
||
onClose();
|
||
handleSubmit(onChange)();
|
||
}}
|
||
>
|
||
完成
|
||
</Button>
|
||
</ModalFooter>
|
||
</Flex>
|
||
</MyModal>
|
||
);
|
||
};
|
||
|
||
export default KBSelectModal;
|