* perf: redirect request and err log replace perf: dataset openapi feat: session fix: retry input error feat: 468 doc sub page feat: standard sub perf: rerank tip perf: rerank tip perf: api sdk perf: openapi sub plan perf: sub ui fix: ts * perf: init log * fix: variable select * sub page * icon * perf: llm model config * perf: menu ux * perf: system store * perf: publish app name * fix: init data * perf: flow edit ux * fix: value type format and ux * fix prompt editor default value (#13) * fix prompt editor default value * fix prompt editor update when not focus * add key with variable --------- Co-authored-by: Archer <545436317@qq.com> * fix: value type * doc * i18n * import path * home page * perf: mongo session running * fix: ts * perf: use toast * perf: flow edit * perf: sse response * slider ui * fetch error * fix prompt editor rerender when not focus by key defaultvalue (#14) * perf: prompt editor * feat: dataset search concat * perf: doc * fix:ts * perf: doc * fix json editor onblur value (#15) * faq * vector model default config * ipv6 --------- Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
445 lines
14 KiB
TypeScript
445 lines
14 KiB
TypeScript
import React, { useCallback, useState, useRef, useMemo } from 'react';
|
|
import {
|
|
Box,
|
|
Card,
|
|
IconButton,
|
|
Flex,
|
|
Grid,
|
|
Button,
|
|
useTheme,
|
|
Drawer,
|
|
DrawerBody,
|
|
DrawerFooter,
|
|
DrawerHeader,
|
|
DrawerOverlay,
|
|
DrawerContent,
|
|
useDisclosure
|
|
} from '@chakra-ui/react';
|
|
import { usePagination } from '@/web/common/hooks/usePagination';
|
|
import {
|
|
getDatasetDataList,
|
|
delOneDatasetDataById,
|
|
getDatasetCollectionById
|
|
} from '@/web/core/dataset/api';
|
|
import { DeleteIcon } from '@chakra-ui/icons';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useToast } from '@fastgpt/web/hooks/useToast';
|
|
import { debounce } from 'lodash';
|
|
import { getErrText } from '@fastgpt/global/common/error/utils';
|
|
import { useConfirm } from '@/web/common/hooks/useConfirm';
|
|
import { useTranslation } from 'next-i18next';
|
|
import { useRouter } from 'next/router';
|
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
|
import MyInput from '@/components/MyInput';
|
|
import { useLoading } from '@/web/common/hooks/useLoading';
|
|
import InputDataModal from '../components/InputDataModal';
|
|
import RawSourceBox from '@/components/core/dataset/RawSourceBox';
|
|
import type { DatasetDataListItemType } from '@/global/core/dataset/type.d';
|
|
import { TabEnum } from '..';
|
|
import { useUserStore } from '@/web/support/user/useUserStore';
|
|
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
|
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
|
import {
|
|
DatasetCollectionTypeMap,
|
|
TrainingModeEnum,
|
|
TrainingTypeMap
|
|
} from '@fastgpt/global/core/dataset/constants';
|
|
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
|
|
import { formatFileSize } from '@fastgpt/global/common/file/tools';
|
|
import { getFileAndOpen } from '@/web/core/dataset/utils';
|
|
import MyTooltip from '@/components/MyTooltip';
|
|
|
|
const DataCard = () => {
|
|
const BoxRef = useRef<HTMLDivElement>(null);
|
|
const theme = useTheme();
|
|
const lastSearch = useRef('');
|
|
const router = useRouter();
|
|
const { userInfo } = useUserStore();
|
|
const { isPc } = useSystemStore();
|
|
const { collectionId = '', datasetId } = router.query as {
|
|
collectionId: string;
|
|
datasetId: string;
|
|
};
|
|
const { Loading, setIsLoading } = useLoading({ defaultLoading: true });
|
|
const { t } = useTranslation();
|
|
const [searchText, setSearchText] = useState('');
|
|
const { toast } = useToast();
|
|
const { openConfirm, ConfirmModal } = useConfirm({
|
|
content: t('dataset.Confirm to delete the data'),
|
|
type: 'delete'
|
|
});
|
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
|
|
const {
|
|
data: datasetDataList,
|
|
Pagination,
|
|
total,
|
|
getData,
|
|
pageNum,
|
|
pageSize
|
|
} = usePagination<DatasetDataListItemType>({
|
|
api: getDatasetDataList,
|
|
pageSize: 24,
|
|
params: {
|
|
collectionId,
|
|
searchText
|
|
},
|
|
onChange() {
|
|
setIsLoading(false);
|
|
if (BoxRef.current) {
|
|
BoxRef.current.scrollTop = 0;
|
|
}
|
|
}
|
|
});
|
|
|
|
const [editDataId, setEditDataId] = useState<string>();
|
|
|
|
// get first page data
|
|
const getFirstData = useCallback(
|
|
debounce(() => {
|
|
getData(1);
|
|
lastSearch.current = searchText;
|
|
}, 300),
|
|
[]
|
|
);
|
|
|
|
// get file info
|
|
const { data: collection } = useQuery(
|
|
['getDatasetCollectionById', collectionId],
|
|
() => getDatasetCollectionById(collectionId),
|
|
{
|
|
onError: () => {
|
|
router.replace({
|
|
query: {
|
|
datasetId
|
|
}
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
const canWrite = useMemo(
|
|
() => userInfo?.team?.role !== TeamMemberRoleEnum.visitor && !!collection?.canWrite,
|
|
[collection?.canWrite, userInfo?.team?.role]
|
|
);
|
|
|
|
const metadataList = useMemo(() => {
|
|
if (!collection) return [];
|
|
|
|
const webSelector =
|
|
collection?.datasetId?.websiteConfig?.selector || collection?.metadata?.webPageSelector;
|
|
|
|
return [
|
|
{
|
|
label: t('core.dataset.collection.metadata.source'),
|
|
value: t(DatasetCollectionTypeMap[collection.type]?.name)
|
|
},
|
|
{
|
|
label: t('core.dataset.collection.metadata.source name'),
|
|
value: collection.file?.filename || collection?.rawLink || collection?.name
|
|
},
|
|
{
|
|
label: t('core.dataset.collection.metadata.source size'),
|
|
value: collection.file ? formatFileSize(collection.file.length) : '-'
|
|
},
|
|
{
|
|
label: t('core.dataset.collection.metadata.Createtime'),
|
|
value: formatTime2YMDHM(collection.createTime)
|
|
},
|
|
{
|
|
label: t('core.dataset.collection.metadata.Updatetime'),
|
|
value: formatTime2YMDHM(collection.updateTime)
|
|
},
|
|
{
|
|
label: t('core.dataset.collection.metadata.Raw text length'),
|
|
value: collection.rawTextLength ?? '-'
|
|
},
|
|
{
|
|
label: t('core.dataset.collection.metadata.Training Type'),
|
|
value: t(TrainingTypeMap[collection.trainingType]?.label)
|
|
},
|
|
{
|
|
label: t('core.dataset.collection.metadata.Chunk Size'),
|
|
value: collection.chunkSize || '-'
|
|
},
|
|
...(webSelector
|
|
? [
|
|
{
|
|
label: t('core.dataset.collection.metadata.Web page selector'),
|
|
value: webSelector
|
|
}
|
|
]
|
|
: [])
|
|
];
|
|
}, [collection, t]);
|
|
|
|
return (
|
|
<Box position={'relative'} py={[1, 5]} h={'100%'}>
|
|
<Flex ref={BoxRef} flexDirection={'column'} h={'100%'}>
|
|
<Flex alignItems={'center'} px={5}>
|
|
<IconButton
|
|
mr={3}
|
|
icon={<MyIcon name={'common/backFill'} w={['14px', '18px']} color={'primary.500'} />}
|
|
variant={'whitePrimary'}
|
|
size={'smSquare'}
|
|
borderRadius={'50%'}
|
|
aria-label={''}
|
|
onClick={() =>
|
|
router.replace({
|
|
query: {
|
|
datasetId: router.query.datasetId,
|
|
parentId: router.query.parentId,
|
|
currentTab: TabEnum.collectionCard
|
|
}
|
|
})
|
|
}
|
|
/>
|
|
<Flex className="textEllipsis" flex={'1 0 0'} mr={[3, 5]} alignItems={'center'}>
|
|
<Box lineHeight={1.2}>
|
|
<RawSourceBox
|
|
sourceName={collection?.name}
|
|
sourceId={collection?.fileId || collection?.rawLink}
|
|
fontSize={['md', 'lg']}
|
|
color={'black'}
|
|
textDecoration={'none'}
|
|
/>
|
|
<Box fontSize={'sm'} color={'myGray.500'}>
|
|
{t('core.dataset.collection.id')}:{' '}
|
|
<Box as={'span'} userSelect={'all'}>
|
|
{collection?._id}
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Flex>
|
|
{canWrite && (
|
|
<Box>
|
|
<Button
|
|
mx={2}
|
|
variant={'whitePrimary'}
|
|
size={['sm', 'md']}
|
|
onClick={() => {
|
|
if (!collection) return;
|
|
setEditDataId('');
|
|
}}
|
|
>
|
|
{t('dataset.Insert Data')}
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
{isPc && (
|
|
<MyTooltip label={t('core.dataset.collection.metadata.Read Metadata')}>
|
|
<IconButton
|
|
variant={'whiteBase'}
|
|
size={['sm', 'md']}
|
|
icon={<MyIcon name={'menu'} w={'18px'} />}
|
|
aria-label={''}
|
|
onClick={onOpen}
|
|
/>
|
|
</MyTooltip>
|
|
)}
|
|
</Flex>
|
|
<Flex my={3} alignItems={'center'} px={5}>
|
|
<Box>
|
|
<Box as={'span'} fontSize={['md', 'lg']}>
|
|
{t('core.dataset.data.Total Amount', { total })}
|
|
</Box>
|
|
</Box>
|
|
<Box flex={1} mr={1} />
|
|
<MyInput
|
|
leftIcon={
|
|
<MyIcon
|
|
name="common/searchLight"
|
|
position={'absolute'}
|
|
w={'14px'}
|
|
color={'myGray.500'}
|
|
/>
|
|
}
|
|
w={['200px', '300px']}
|
|
placeholder={t('core.dataset.data.Search data placeholder')}
|
|
value={searchText}
|
|
onChange={(e) => {
|
|
setSearchText(e.target.value);
|
|
getFirstData();
|
|
}}
|
|
onBlur={() => {
|
|
if (searchText === lastSearch.current) return;
|
|
getFirstData();
|
|
}}
|
|
onKeyDown={(e) => {
|
|
if (searchText === lastSearch.current) return;
|
|
if (e.key === 'Enter') {
|
|
getFirstData();
|
|
}
|
|
}}
|
|
/>
|
|
</Flex>
|
|
<Box flex={'1 0 0'} overflow={'auto'} px={5}>
|
|
<Grid
|
|
gridTemplateColumns={['1fr', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']}
|
|
gridGap={4}
|
|
>
|
|
{datasetDataList.map((item, index) => (
|
|
<Card
|
|
key={item._id}
|
|
cursor={'pointer'}
|
|
p={3}
|
|
userSelect={'none'}
|
|
boxShadow={'none'}
|
|
bg={'myWhite.500'}
|
|
border={theme.borders.sm}
|
|
position={'relative'}
|
|
overflow={'hidden'}
|
|
_hover={{
|
|
borderColor: 'myGray.200',
|
|
boxShadow: 'lg',
|
|
bg: 'white',
|
|
'& .footer': { h: 'auto', p: 3 }
|
|
}}
|
|
onClick={() => {
|
|
if (!collection) return;
|
|
setEditDataId(item._id);
|
|
}}
|
|
>
|
|
<Flex zIndex={1} alignItems={'center'} justifyContent={'space-between'}>
|
|
<Box
|
|
border={theme.borders.base}
|
|
px={2}
|
|
fontSize={'sm'}
|
|
mr={1}
|
|
borderRadius={'md'}
|
|
>
|
|
# {item.chunkIndex ?? '-'}
|
|
</Box>
|
|
<Box className={'textEllipsis'} color={'myGray.500'} fontSize={'xs'}>
|
|
ID:{item._id}
|
|
</Box>
|
|
</Flex>
|
|
<Box
|
|
maxH={'135px'}
|
|
minH={'90px'}
|
|
overflow={'hidden'}
|
|
wordBreak={'break-all'}
|
|
pt={1}
|
|
pb={3}
|
|
fontSize={'13px'}
|
|
>
|
|
<Box color={'black'} mb={1}>
|
|
{item.q}
|
|
</Box>
|
|
<Box color={'myGray.700'}>{item.a}</Box>
|
|
|
|
<Flex
|
|
className="footer"
|
|
position={'absolute'}
|
|
top={0}
|
|
bottom={0}
|
|
left={0}
|
|
right={0}
|
|
h={'0'}
|
|
overflow={'hidden'}
|
|
p={0}
|
|
bg={'linear-gradient(to top, white,white 20%, rgba(255,255,255,0) 60%)'}
|
|
alignItems={'flex-end'}
|
|
fontSize={'sm'}
|
|
>
|
|
<Flex alignItems={'center'}>
|
|
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />
|
|
{item.q.length + (item.a?.length || 0)}
|
|
</Flex>
|
|
<Box flex={1} />
|
|
{canWrite && (
|
|
<IconButton
|
|
display={'flex'}
|
|
icon={<DeleteIcon />}
|
|
variant={'whiteDanger'}
|
|
size={'xsSquare'}
|
|
aria-label={'delete'}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
openConfirm(async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
await delOneDatasetDataById(item._id);
|
|
getData(pageNum);
|
|
} catch (error) {
|
|
toast({
|
|
title: getErrText(error),
|
|
status: 'error'
|
|
});
|
|
}
|
|
setIsLoading(false);
|
|
})();
|
|
}}
|
|
/>
|
|
)}
|
|
</Flex>
|
|
</Box>
|
|
</Card>
|
|
))}
|
|
</Grid>
|
|
{total > pageSize && (
|
|
<Flex mt={2} justifyContent={'center'}>
|
|
<Pagination />
|
|
</Flex>
|
|
)}
|
|
{total === 0 && (
|
|
<Flex flexDirection={'column'} alignItems={'center'} pt={'10vh'}>
|
|
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
|
<Box mt={2} color={'myGray.500'}>
|
|
{t('core.dataset.data.Empty Tip')}
|
|
</Box>
|
|
</Flex>
|
|
)}
|
|
</Box>
|
|
</Flex>
|
|
|
|
{/* metadata drawer */}
|
|
<Drawer isOpen={isOpen} placement="right" size={'md'} onClose={onClose}>
|
|
<DrawerOverlay />
|
|
<DrawerContent>
|
|
<DrawerHeader>{t('core.dataset.collection.metadata.metadata')}</DrawerHeader>
|
|
|
|
<DrawerBody>
|
|
{metadataList.map((item) => (
|
|
<Flex key={item.label} alignItems={'center'} mb={5} wordBreak={'break-all'}>
|
|
<Box color={'myGray.500'} flex={'0 0 100px'}>
|
|
{item.label}
|
|
</Box>
|
|
<Box>{item.value}</Box>
|
|
</Flex>
|
|
))}
|
|
{collection?.sourceId && (
|
|
<Button
|
|
variant={'whitePrimary'}
|
|
onClick={() => collection.sourceId && getFileAndOpen(collection.sourceId)}
|
|
>
|
|
{t('core.dataset.collection.metadata.read source')}
|
|
</Button>
|
|
)}
|
|
</DrawerBody>
|
|
|
|
<DrawerFooter>
|
|
<Button variant={'whitePrimary'} onClick={onClose}>
|
|
{t('common.Close')}
|
|
</Button>
|
|
</DrawerFooter>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
|
|
{editDataId !== undefined && collection && (
|
|
<InputDataModal
|
|
collectionId={collection._id}
|
|
dataId={editDataId}
|
|
onClose={() => setEditDataId(undefined)}
|
|
onSuccess={() => getData(pageNum)}
|
|
onDelete={() => getData(pageNum)}
|
|
/>
|
|
)}
|
|
<ConfirmModal />
|
|
<Loading fixed={false} />
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default React.memo(DataCard);
|