2023-12-03 20:45:57 +08:00

349 lines
12 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import {
Box,
Textarea,
Button,
Flex,
useTheme,
Grid,
Progress,
Switch,
useDisclosure
} from '@chakra-ui/react';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { useSearchTestStore, SearchTestStoreItemType } from '@/web/core/dataset/store/searchTest';
import { getDatasetDataItemById, postSearchText } from '@/web/core/dataset/api';
import MyIcon from '@/components/Icon';
import { useRequest } from '@/web/common/hooks/useRequest';
import { formatTimeToChatTime } from '@/utils/tools';
import InputDataModal, { type InputDataType } from './InputDataModal';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useToast } from '@/web/common/hooks/useToast';
import { customAlphabet } from 'nanoid';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next';
import { SearchTestResponse } from '@/global/core/dataset/api';
import { DatasetSearchModeEnum, DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constant';
import dynamic from 'next/dynamic';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
const DatasetParamsModal = dynamic(() => import('@/components/core/module/DatasetParamsModal'));
const Test = ({ datasetId }: { datasetId: string }) => {
const { t } = useTranslation();
const theme = useTheme();
const { toast } = useToast();
const { setLoading } = useSystemStore();
const { datasetDetail } = useDatasetStore();
const { datasetTestList, pushDatasetTestItem, delDatasetTestItemById, updateDatasetItemById } =
useSearchTestStore();
const [inputText, setInputText] = useState('');
const [datasetTestItem, setDatasetTestItem] = useState<SearchTestStoreItemType>();
const [editInputData, setEditInputData] = useState<InputDataType & { collectionId: string }>();
const [searchMode, setSearchMode] = useState<`${DatasetSearchModeEnum}`>(
DatasetSearchModeEnum.embedding
);
const searchModeData = DatasetSearchModeMap[searchMode];
const {
isOpen: isOpenSelectMode,
onOpen: onOpenSelectMode,
onClose: onCloseSelectMode
} = useDisclosure();
const testHistories = useMemo(
() => datasetTestList.filter((item) => item.datasetId === datasetId),
[datasetId, datasetTestList]
);
const { mutate, isLoading } = useRequest({
mutationFn: () => postSearchText({ datasetId, text: inputText.trim(), searchMode, limit: 30 }),
onSuccess(res: SearchTestResponse) {
if (!res || res.list.length === 0) {
return toast({
status: 'warning',
title: t('dataset.test.noResult')
});
}
const testItem: SearchTestStoreItemType = {
id: nanoid(),
datasetId,
text: inputText.trim(),
time: new Date(),
results: res.list,
duration: res.duration
};
pushDatasetTestItem(testItem);
setDatasetTestItem(testItem);
},
onError(err) {
toast({
title: getErrText(err),
status: 'error'
});
}
});
useEffect(() => {
setDatasetTestItem(undefined);
}, [datasetId]);
return (
<Box h={'100%'} display={['block', 'flex']}>
{/* input */}
<Box
h={['auto', '100%']}
display={['block', 'flex']}
flexDirection={'column'}
flex={1}
maxW={'500px'}
py={4}
borderRight={['none', theme.borders.base]}
>
<Box border={'2px solid'} borderColor={'myBlue.600'} p={3} mx={4} borderRadius={'md'}>
<Flex alignItems={'center'}>
<Box fontSize={'sm'} fontWeight={'bold'} flex={1}>
<MyIcon mr={2} name={'text'} w={'18px'} h={'18px'} color={'myBlue.700'} />
{t('core.dataset.test.Test Text')}
</Box>
<Button
variant={'base'}
leftIcon={<MyIcon name={searchModeData.icon as any} w={'14px'} />}
size={'sm'}
onClick={onOpenSelectMode}
>
{t(searchModeData.title)}
</Button>
</Flex>
<Textarea
rows={6}
resize={'none'}
variant={'unstyled'}
maxLength={datasetDetail.vectorModel.maxToken}
placeholder={t('core.dataset.test.Test Text Placeholder')}
value={inputText}
onChange={(e) => setInputText(e.target.value)}
/>
<Flex alignItems={'center'} justifyContent={'flex-end'}>
<Box mx={3} color={'myGray.500'}>
{inputText.length}
</Box>
<Button isDisabled={inputText === ''} isLoading={isLoading} onClick={mutate}>
{t('core.dataset.test.Test')}
</Button>
</Flex>
</Box>
<Box mt={5} flex={'1 0 0'} px={4} overflow={'overlay'} display={['none', 'block']}>
<Flex alignItems={'center'} color={'myGray.600'}>
<MyIcon mr={2} name={'history'} w={'16px'} h={'16px'} />
<Box fontSize={'2xl'}>{t('core.dataset.test.test history')}</Box>
</Flex>
<Box mt={2}>
<Flex py={2} fontWeight={'bold'} borderBottom={theme.borders.sm}>
<Box flex={1}>{t('core.dataset.test.Test Text')}</Box>
<Box w={'80px'}>{t('common.Time')}</Box>
<Box w={'14px'}></Box>
</Flex>
{testHistories.map((item) => (
<Flex
key={item.id}
p={1}
alignItems={'center'}
borderBottom={theme.borders.base}
_hover={{
bg: '#f4f4f4',
'& .delete': {
display: 'block'
}
}}
cursor={'pointer'}
onClick={() => setDatasetTestItem(item)}
>
<Box flex={1} mr={2}>
{item.text}
</Box>
<Box w={'80px'}>{formatTimeToChatTime(item.time)}</Box>
<MyTooltip label={t('core.dataset.test.delete test history')}>
<Box w={'14px'} h={'14px'}>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
display={'none'}
_hover={{ color: 'red.600' }}
onClick={(e) => {
e.stopPropagation();
delDatasetTestItemById(item.id);
datasetTestItem?.id === item.id && setDatasetTestItem(undefined);
}}
/>
</Box>
</MyTooltip>
</Flex>
))}
</Box>
</Box>
</Box>
{/* result show */}
<Box p={4} h={['auto', '100%']} overflow={'overlay'} flex={'1 0 0'}>
{!datasetTestItem?.results || datasetTestItem.results.length === 0 ? (
<Flex
mt={[10, 0]}
h={'100%'}
flexDirection={'column'}
alignItems={'center'}
justifyContent={'center'}
>
<MyIcon name={'empty'} color={'transparent'} w={'54px'} />
<Box mt={3} color={'myGray.600'}>
{t('core.dataset.test.test result placeholder')}
</Box>
</Flex>
) : (
<>
<Flex alignItems={'center'}>
<Box fontSize={'3xl'} color={'myGray.600'}>
{t('core.dataset.test.Test Result')}
</Box>
<MyTooltip label={t('core.dataset.test.test result tip')} forceShow>
<QuestionOutlineIcon
mx={2}
color={'myGray.600'}
cursor={'pointer'}
fontSize={'lg'}
/>
</MyTooltip>
<Box>({datasetTestItem.duration})</Box>
</Flex>
<Grid
mt={1}
gridTemplateColumns={[
'repeat(1,1fr)',
'repeat(1,1fr)',
'repeat(1,1fr)',
'repeat(1,1fr)',
'repeat(2,1fr)'
]}
gridGap={4}
>
{datasetTestItem?.results.map((item, index) => (
<Box
key={item.id}
pb={2}
borderRadius={'sm'}
border={theme.borders.base}
_notLast={{ mb: 2 }}
cursor={'pointer'}
title={'编辑'}
onClick={async () => {
try {
setLoading(true);
const data = await getDatasetDataItemById(item.id);
if (!data) {
throw new Error(t('core.dataset.data.data is deleted'));
}
setEditInputData({
id: data.id,
collectionId: data.collectionId,
q: data.q,
a: data.a,
indexes: data.indexes
});
} catch (err) {
toast({
status: 'warning',
title: getErrText(err)
});
}
setLoading(false);
}}
>
<Flex p={3} alignItems={'center'} color={'myGray.500'}>
<Box
border={theme.borders.base}
px={2}
fontSize={'sm'}
mr={1}
borderRadius={'md'}
>
# {index + 1}
</Box>
<MyIcon name={'kbTest'} w={'14px'} />
<Progress
mx={2}
flex={'1 0 0'}
value={item.score * 100}
size="sm"
borderRadius={'20px'}
colorScheme="gray"
/>
<Box>{item.score.toFixed(4)}</Box>
</Flex>
<Box px={2} fontSize={'xs'} color={'myGray.600'} wordBreak={'break-word'}>
<Box>{item.q}</Box>
<Box>{item.a}</Box>
</Box>
</Box>
))}
</Grid>
</>
)}
</Box>
{!!editInputData && (
<InputDataModal
collectionId={editInputData.collectionId}
defaultValue={editInputData}
onClose={() => setEditInputData(undefined)}
onSuccess={(data) => {
if (datasetTestItem && editInputData.id) {
const newTestItem: SearchTestStoreItemType = {
...datasetTestItem,
results: datasetTestItem.results.map((item) =>
item.id === editInputData.id
? {
...item,
q: data.q || '',
a: data.a || ''
}
: item
)
};
updateDatasetItemById(newTestItem);
setDatasetTestItem(newTestItem);
}
setEditInputData(undefined);
}}
onDelete={() => {
if (datasetTestItem && editInputData.id) {
const newTestItem = {
...datasetTestItem,
results: datasetTestItem.results.filter((item) => item.id !== editInputData.id)
};
updateDatasetItemById(newTestItem);
setDatasetTestItem(newTestItem);
}
setEditInputData(undefined);
}}
/>
)}
{isOpenSelectMode && (
<DatasetParamsModal
searchMode={searchMode}
onClose={onCloseSelectMode}
onSuccess={(e) => {
setSearchMode(e.searchMode);
}}
/>
)}
</Box>
);
};
export default Test;