import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react'; import { Box, Flex, TableContainer, Table, Thead, Tr, Th, Td, Tbody, Image, MenuButton, useDisclosure, Button, Link, useTheme } from '@chakra-ui/react'; import { getDatasetCollections, delDatasetCollectionById, putDatasetCollectionById, postDatasetCollection, getDatasetCollectionPathById, postLinkCollectionSync } from '@/web/core/dataset/api'; import { useQuery } from '@tanstack/react-query'; import { debounce } from 'lodash'; import { useConfirm } from '@/web/common/hooks/useConfirm'; import { useTranslation } from 'next-i18next'; import MyIcon from '@fastgpt/web/components/common/Icon'; import MyInput from '@/components/MyInput'; import dayjs from 'dayjs'; import { useRequest } from '@/web/common/hooks/useRequest'; import { useLoading } from '@/web/common/hooks/useLoading'; import { useRouter } from 'next/router'; import { usePagination } from '@/web/common/hooks/usePagination'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import MyMenu from '@/components/MyMenu'; import { useEditTitle } from '@/web/common/hooks/useEditTitle'; import type { DatasetCollectionsListItemType } from '@/global/core/dataset/type.d'; import EmptyTip from '@/components/EmptyTip'; import { DatasetCollectionTypeEnum, TrainingModeEnum, DatasetTypeEnum, DatasetTypeMap, DatasetStatusEnum, DatasetCollectionSyncResultMap } from '@fastgpt/global/core/dataset/constants'; import { getCollectionIcon } from '@fastgpt/global/core/dataset/utils'; import EditFolderModal, { useEditFolder } from '../../component/EditFolderModal'; import { TabEnum } from '..'; import ParentPath from '@/components/common/ParentPaths'; import dynamic from 'next/dynamic'; import { useDrag } from '@/web/common/hooks/useDrag'; import SelectCollections from '@/web/core/dataset/components/SelectCollections'; import { useToast } from '@/web/common/hooks/useToast'; import MyTooltip from '@/components/MyTooltip'; import { useUserStore } from '@/web/support/user/useUserStore'; import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant'; import { useDatasetStore } from '@/web/core/dataset/store/dataset'; import { DatasetSchemaType } from '@fastgpt/global/core/dataset/type'; import { DatasetCollectionSyncResultEnum } from '@fastgpt/global/core/dataset/constants'; import MyBox from '@/components/common/MyBox'; import { ImportDataSourceEnum } from './Import'; const WebSiteConfigModal = dynamic(() => import('./Import/WebsiteConfig'), {}); const FileSourceSelector = dynamic(() => import('./Import/sourceSelector/FileSourceSelector'), {}); const CollectionCard = () => { const BoxRef = useRef(null); const lastSearch = useRef(''); const router = useRouter(); const theme = useTheme(); const { toast } = useToast(); const { parentId = '', datasetId } = router.query as { parentId: string; datasetId: string }; const { t } = useTranslation(); const { Loading } = useLoading(); const { isPc } = useSystemStore(); const { userInfo } = useUserStore(); const [searchText, setSearchText] = useState(''); const { datasetDetail, updateDataset, startWebsiteSync, loadDatasetDetail } = useDatasetStore(); const { openConfirm: openDeleteConfirm, ConfirmModal: ConfirmDeleteModal } = useConfirm({ content: t('dataset.Confirm to delete the file') }); const { openConfirm: openSyncConfirm, ConfirmModal: ConfirmSyncModal } = useConfirm({ content: t('core.dataset.collection.Start Sync Tip') }); const { isOpen: isOpenFileSourceSelector, onOpen: onOpenFileSourceSelector, onClose: onCloseFileSourceSelector } = useDisclosure(); const { isOpen: isOpenWebsiteModal, onOpen: onOpenWebsiteModal, onClose: onCloseWebsiteModal } = useDisclosure(); const { onOpenModal: onOpenCreateVirtualFileModal, EditModal: EditCreateVirtualFileModal } = useEditTitle({ title: t('dataset.Create manual collection'), tip: t('dataset.Manual collection Tip'), canEmpty: false }); const { onOpenModal: onOpenEditTitleModal, EditModal: EditTitleModal } = useEditTitle({ title: t('Rename') }); const { editFolderData, setEditFolderData } = useEditFolder(); const [moveCollectionData, setMoveCollectionData] = useState<{ collectionId: string }>(); const { data: collections, Pagination, total, getData, isLoading: isGetting, pageNum, pageSize } = usePagination({ api: getDatasetCollections, pageSize: 20, params: { datasetId, parentId, searchText }, defaultRequest: false, onChange() { if (BoxRef.current) { BoxRef.current.scrollTop = 0; } } }); const { dragStartId, setDragStartId, dragTargetId, setDragTargetId } = useDrag(); // change search const debounceRefetch = useCallback( debounce(() => { getData(1); lastSearch.current = searchText; }, 300), [] ); // add file icon const formatCollections = useMemo( () => collections.map((collection) => { const icon = getCollectionIcon(collection.type, collection.name); const status = (() => { if (collection.trainingAmount > 0) { return { statusText: t('dataset.collections.Collection Embedding', { total: collection.trainingAmount }), color: 'myGray.600', bg: 'myGray.50', borderColor: 'borderColor.low' }; } return { statusText: t('core.dataset.collection.status.active'), color: 'green.600', bg: 'green.50', borderColor: 'green.300' }; })(); return { ...collection, icon, ...status }; }), [collections, t] ); const { mutate: onCreateCollection, isLoading: isCreating } = useRequest({ mutationFn: async ({ name, type, callback, ...props }: { name: string; type: `${DatasetCollectionTypeEnum}`; callback?: (id: string) => void; trainingType?: `${TrainingModeEnum}`; rawLink?: string; chunkSize?: number; }) => { const id = await postDatasetCollection({ parentId, datasetId, name, type, ...props }); callback?.(id); return id; }, onSuccess() { getData(pageNum); }, successToast: t('common.Create Success'), errorToast: t('common.Create Failed') }); const { mutate: onUpdateCollectionName } = useRequest({ mutationFn: ({ collectionId, name }: { collectionId: string; name: string }) => { return putDatasetCollectionById({ id: collectionId, name }); }, onSuccess() { getData(pageNum); }, successToast: t('common.Rename Success'), errorToast: t('common.Rename Failed') }); const { mutate: onDelCollection, isLoading: isDeleting } = useRequest({ mutationFn: (collectionId: string) => { return delDatasetCollectionById({ id: collectionId }); }, onSuccess() { getData(pageNum); }, successToast: t('common.Delete Success'), errorToast: t('common.Delete Failed') }); const { mutate: onUpdateDatasetWebsiteConfig, isLoading: isUpdating } = useRequest({ mutationFn: async (websiteConfig: DatasetSchemaType['websiteConfig']) => { onCloseWebsiteModal(); await updateDataset({ id: datasetDetail._id, websiteConfig }); return startWebsiteSync(); }, errorToast: t('common.Update Failed') }); const { mutate: onclickStartSync, isLoading: isSyncing } = useRequest({ mutationFn: (collectionId: string) => { return postLinkCollectionSync(collectionId); }, onSuccess(res: DatasetCollectionSyncResultEnum) { getData(pageNum); toast({ status: 'success', title: t(DatasetCollectionSyncResultMap[res]?.label) }); }, errorToast: t('core.dataset.error.Start Sync Failed') }); const { data: paths = [] } = useQuery(['getDatasetCollectionPathById', parentId], () => getDatasetCollectionPathById(parentId) ); const hasTrainingData = useMemo( () => !!formatCollections.find((item) => item.trainingAmount > 0), [formatCollections] ); const isLoading = useMemo( () => isCreating || isDeleting || isUpdating || isSyncing || (isGetting && collections.length === 0), [collections.length, isCreating, isDeleting, isGetting, isSyncing, isUpdating] ); useQuery( ['refreshCollection'], () => { getData(1); if (datasetDetail.status === DatasetStatusEnum.syncing) { loadDatasetDetail(datasetId, true); } return null; }, { refetchInterval: 6000, enabled: hasTrainingData || datasetDetail.status === DatasetStatusEnum.syncing } ); useEffect(() => { getData(1); }, [parentId]); return ( {/* header */} ({ parentId: path.parentId, parentName: i === paths.length - 1 ? `${path.parentName}` : path.parentName }))} FirstPathDom={ <> {t(DatasetTypeMap[datasetDetail?.type]?.collectionLabel)}({total}) {datasetDetail?.websiteConfig?.url && ( {t('core.dataset.website.Base Url')}: {datasetDetail.websiteConfig.url} )} } onClick={(e) => { router.replace({ query: { ...router.query, parentId: e } }); }} /> {isPc && ( } onChange={(e) => { setSearchText(e.target.value); debounceRefetch(); }} onBlur={() => { if (searchText === lastSearch.current) return; getData(1); }} onKeyDown={(e) => { if (searchText === lastSearch.current) return; if (e.key === 'Enter') { getData(1); } }} /> )} {datasetDetail?.type === DatasetTypeEnum.dataset && ( <> {userInfo?.team?.role !== TeamMemberRoleEnum.visitor && ( {t('dataset.collections.Create And Import')} } menuList={[ { child: ( {t('Folder')} ), onClick: () => setEditFolderData({}) }, { child: ( {t('core.dataset.Manual collection')} ), onClick: () => { onOpenCreateVirtualFileModal({ defaultVal: '', onSuccess: (name) => { onCreateCollection({ name, type: DatasetCollectionTypeEnum.virtual }); } }); } }, { child: ( {t('core.dataset.Text collection')} ), onClick: onOpenFileSourceSelector }, { child: ( {t('core.dataset.Table collection')} ), onClick: () => router.replace({ query: { ...router.query, currentTab: TabEnum.import, source: ImportDataSourceEnum.tableLocal } }) } ]} /> )} )} {datasetDetail?.type === DatasetTypeEnum.websiteDataset && ( <> {datasetDetail?.websiteConfig?.url ? ( {datasetDetail.status === DatasetStatusEnum.active && ( )} {datasetDetail.status === DatasetStatusEnum.syncing && ( {t('core.dataset.status.syncing')} )} ) : ( )} )} {/* collection table */} {formatCollections.map((collection, index) => ( { setDragStartId(collection._id); }} onDragOver={(e) => { e.preventDefault(); const targetId = e.currentTarget.getAttribute('data-drag-id'); if (!targetId) return; DatasetCollectionTypeEnum.folder && setDragTargetId(targetId); }} onDragLeave={(e) => { e.preventDefault(); setDragTargetId(undefined); }} onDrop={async (e) => { e.preventDefault(); if (!dragTargetId || !dragStartId || dragTargetId === dragStartId) return; // update parentId try { await putDatasetCollectionById({ id: dragStartId, parentId: dragTargetId }); getData(pageNum); } catch (error) {} setDragTargetId(undefined); }} onClick={() => { if (collection.type === DatasetCollectionTypeEnum.folder) { router.replace({ query: { ...router.query, parentId: collection._id } }); } else { router.replace({ query: { ...router.query, collectionId: collection._id, currentTab: TabEnum.dataCard } }); } }} > ))}
# {t('common.Name')} {t('dataset.collections.Data Amount')} {t('core.dataset.Sync Time')} {t('common.Status')}
{index + 1} {collection.name} {collection.dataAmount || '-'} {dayjs(collection.updateTime).format('YYYY/MM/DD HH:mm')} {t(collection.statusText)} e.stopPropagation()}> {collection.canWrite && userInfo?.team?.role !== TeamMemberRoleEnum.visitor && ( } menuList={[ ...(collection.type === DatasetCollectionTypeEnum.link ? [ { child: ( {t('core.dataset.collection.Sync')} ), onClick: () => openSyncConfirm(() => { onclickStartSync(collection._id); })() } ] : []), { child: ( {t('Move')} ), onClick: () => setMoveCollectionData({ collectionId: collection._id }) }, { child: ( {t('Rename')} ), onClick: () => onOpenEditTitleModal({ defaultVal: collection.name, onSuccess: (newName) => { onUpdateCollectionName({ collectionId: collection._id, name: newName }); } }) }, { child: ( {t('common.Delete')} ), onClick: () => openDeleteConfirm( () => { onDelCollection(collection._id); }, undefined, collection.type === DatasetCollectionTypeEnum.folder ? t('dataset.collections.Confirm to delete the folder') : t('dataset.Confirm to delete the file') )() } ]} /> )}
{total > pageSize && ( )} {total === 0 && ( {datasetDetail.status === DatasetStatusEnum.syncing && ( <>{t('core.dataset.status.syncing')} )} {datasetDetail.status === DatasetStatusEnum.active && ( <> {!datasetDetail?.websiteConfig?.url ? ( <> {t('core.dataset.collection.Website Empty Tip')} {', '} {t('core.dataset.collection.Click top config website')} ) : ( <>{t('core.dataset.website.UnValid Website Tip')} )} )}
) } /> )} {/* {isOpenFileImportModal && ( { getData(1); onCloseFileImportModal(); }} onClose={onCloseFileImportModal} /> )} */} {isOpenFileSourceSelector && } {!!editFolderData && ( setEditFolderData(undefined)} editCallback={async (name) => { try { if (editFolderData.id) { await putDatasetCollectionById({ id: editFolderData.id, name }); getData(pageNum); } else { onCreateCollection({ name, type: DatasetCollectionTypeEnum.folder }); } } catch (error) { return Promise.reject(error); } }} isEdit={!!editFolderData.id} name={editFolderData.name} /> )} {!!moveCollectionData && ( setMoveCollectionData(undefined)} onSuccess={async ({ parentId }) => { await putDatasetCollectionById({ id: moveCollectionData.collectionId, parentId }); getData(pageNum); setMoveCollectionData(undefined); toast({ status: 'success', title: t('common.folder.Move Success') }); }} /> )} {isOpenWebsiteModal && ( )}
); }; export default React.memo(CollectionCard);