V4.8.20 feature (#3686)
* Aiproxy (#3649) * model config * feat: model config ui * perf: rename variable * feat: custom request url * perf: model buffer * perf: init model * feat: json model config * auto login * fix: ts * update packages * package * fix: dockerfile * feat: usage filter & export & dashbord (#3538) * feat: usage filter & export & dashbord * adjust ui * fix tmb scroll * fix code & selecte all * merge * perf: usages list;perf: move components (#3654) * perf: usages list * team sub plan load * perf: usage dashboard code * perf: dashboard ui * perf: move components * add default model config (#3653) * 4.8.20 test (#3656) * provider * perf: model config * model perf (#3657) * fix: model * dataset quote * perf: model config * model tag * doubao model config * perf: config model * feat: model test * fix: POST 500 error on dingtalk bot (#3655) * feat: default model (#3662) * move model config * feat: default model * fix: false triggerd org selection (#3661) * export usage csv i18n (#3660) * export usage csv i18n * fix build * feat: markdown extension (#3663) * feat: markdown extension * media cros * rerank test * default price * perf: default model * fix: cannot custom provider * fix: default model select * update bg * perf: default model selector * fix: usage export * i18n * fix: rerank * update init extension * perf: ip limit check * doubao model order * web default modle * perf: tts selector * perf: tts error * qrcode package * reload buffer (#3665) * reload buffer * reload buffer * tts selector * fix: err tip (#3666) * fix: err tip * perf: training queue * doc * fix interactive edge (#3659) * fix interactive edge * fix * comment * add gemini model * fix: chat model select * perf: supplement assistant empty response (#3669) * perf: supplement assistant empty response * check array * perf: max_token count;feat: support resoner output;fix: member scroll (#3681) * perf: supplement assistant empty response * check array * perf: max_token count * feat: support resoner output * member scroll * update provider order * i18n * fix: stream response (#3682) * perf: supplement assistant empty response * check array * fix: stream response * fix: model config cannot set to null * fix: reasoning response (#3684) * perf: supplement assistant empty response * check array * fix: reasoning response * fix: reasoning response * doc (#3685) * perf: supplement assistant empty response * check array * doc * lock * animation * update doc * update compose * doc * doc --------- Co-authored-by: heheer <heheer@sealos.io> Co-authored-by: a.e. <49438478+I-Info@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { Dispatch, ReactNode, SetStateAction, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { createContext, useContextSelector } from 'use-context-selector';
|
||||
import { DatasetStatusEnum, DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { DatasetSchemaType } from '@fastgpt/global/core/dataset/type';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { checkTeamWebSyncLimit } from '@/web/support/user/team/api';
|
||||
import { postCreateTrainingUsage } from '@/web/support/wallet/usage/api';
|
||||
import { getDatasetCollections, postWebsiteSync } from '@/web/core/dataset/api';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { usePagination } from '@fastgpt/web/hooks/usePagination';
|
||||
import { DatasetCollectionsListItemType } from '@/global/core/dataset/type';
|
||||
import { useRouter } from 'next/router';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
|
||||
const WebSiteConfigModal = dynamic(() => import('./WebsiteConfig'));
|
||||
|
||||
type CollectionPageContextType = {
|
||||
openWebSyncConfirm: () => void;
|
||||
onOpenWebsiteModal: () => void;
|
||||
collections: DatasetCollectionsListItemType[];
|
||||
Pagination: () => JSX.Element;
|
||||
total: number;
|
||||
getData: (e: number) => void;
|
||||
isGetting: boolean;
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
searchText: string;
|
||||
setSearchText: Dispatch<SetStateAction<string>>;
|
||||
filterTags: string[];
|
||||
setFilterTags: Dispatch<SetStateAction<string[]>>;
|
||||
};
|
||||
|
||||
export const CollectionPageContext = createContext<CollectionPageContextType>({
|
||||
openWebSyncConfirm: function (): () => void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
onOpenWebsiteModal: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
collections: [],
|
||||
Pagination: function (): JSX.Element {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
total: 0,
|
||||
getData: function (e: number): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
isGetting: false,
|
||||
pageNum: 0,
|
||||
pageSize: 0,
|
||||
searchText: '',
|
||||
setSearchText: function (value: SetStateAction<string>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
filterTags: [],
|
||||
setFilterTags: function (value: SetStateAction<string[]>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
});
|
||||
|
||||
const CollectionPageContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { parentId = '' } = router.query as { parentId: string };
|
||||
|
||||
const { datasetDetail, datasetId, updateDataset } = useContextSelector(
|
||||
DatasetPageContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
// website config
|
||||
const { openConfirm: openWebSyncConfirm, ConfirmModal: ConfirmWebSyncModal } = useConfirm({
|
||||
content: t('dataset:start_sync_website_tip')
|
||||
});
|
||||
const {
|
||||
isOpen: isOpenWebsiteModal,
|
||||
onOpen: onOpenWebsiteModal,
|
||||
onClose: onCloseWebsiteModal
|
||||
} = useDisclosure();
|
||||
const { mutate: onUpdateDatasetWebsiteConfig } = useRequest({
|
||||
mutationFn: async (websiteConfig: DatasetSchemaType['websiteConfig']) => {
|
||||
onCloseWebsiteModal();
|
||||
await checkTeamWebSyncLimit();
|
||||
await updateDataset({
|
||||
id: datasetId,
|
||||
websiteConfig,
|
||||
status: DatasetStatusEnum.syncing
|
||||
});
|
||||
const billId = await postCreateTrainingUsage({
|
||||
name: t('common:core.dataset.training.Website Sync'),
|
||||
datasetId: datasetId
|
||||
});
|
||||
await postWebsiteSync({ datasetId: datasetId, billId });
|
||||
|
||||
return;
|
||||
},
|
||||
errorToast: t('common:common.Update Failed')
|
||||
});
|
||||
|
||||
// collection list
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [filterTags, setFilterTags] = useState<string[]>([]);
|
||||
const {
|
||||
data: collections,
|
||||
Pagination,
|
||||
total,
|
||||
getData,
|
||||
isLoading: isGetting,
|
||||
pageNum,
|
||||
pageSize
|
||||
} = usePagination(getDatasetCollections, {
|
||||
pageSize: 20,
|
||||
params: {
|
||||
datasetId,
|
||||
parentId,
|
||||
searchText,
|
||||
filterTags
|
||||
},
|
||||
// defaultRequest: false,
|
||||
refreshDeps: [parentId, searchText, filterTags]
|
||||
});
|
||||
|
||||
const contextValue: CollectionPageContextType = {
|
||||
openWebSyncConfirm: openWebSyncConfirm(onUpdateDatasetWebsiteConfig),
|
||||
onOpenWebsiteModal,
|
||||
|
||||
searchText,
|
||||
setSearchText,
|
||||
filterTags,
|
||||
setFilterTags,
|
||||
collections,
|
||||
Pagination,
|
||||
total,
|
||||
getData,
|
||||
isGetting,
|
||||
pageNum,
|
||||
pageSize
|
||||
};
|
||||
|
||||
return (
|
||||
<CollectionPageContext.Provider value={contextValue}>
|
||||
{children}
|
||||
{datasetDetail.type === DatasetTypeEnum.websiteDataset && (
|
||||
<>
|
||||
{isOpenWebsiteModal && (
|
||||
<WebSiteConfigModal
|
||||
onClose={onCloseWebsiteModal}
|
||||
onSuccess={onUpdateDatasetWebsiteConfig}
|
||||
defaultValue={{
|
||||
url: datasetDetail?.websiteConfig?.url,
|
||||
selector: datasetDetail?.websiteConfig?.selector
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ConfirmWebSyncModal />
|
||||
</>
|
||||
)}
|
||||
</CollectionPageContext.Provider>
|
||||
);
|
||||
};
|
||||
export default CollectionPageContextProvider;
|
||||
@@ -0,0 +1,55 @@
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { DatasetStatusEnum, DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { CollectionPageContext } from './Context';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
|
||||
const EmptyCollectionTip = () => {
|
||||
const { t } = useTranslation();
|
||||
const onOpenWebsiteModal = useContextSelector(CollectionPageContext, (v) => v.onOpenWebsiteModal);
|
||||
const datasetDetail = useContextSelector(DatasetPageContext, (v) => v.datasetDetail);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(datasetDetail.type === DatasetTypeEnum.dataset ||
|
||||
datasetDetail.type === DatasetTypeEnum.externalFile) && (
|
||||
<EmptyTip text={t('common:core.dataset.collection.Empty Tip')} />
|
||||
)}
|
||||
{datasetDetail.type === DatasetTypeEnum.websiteDataset && (
|
||||
<EmptyTip
|
||||
text={
|
||||
<Flex>
|
||||
{datasetDetail.status === DatasetStatusEnum.syncing && (
|
||||
<>{t('common:core.dataset.status.syncing')}</>
|
||||
)}
|
||||
{datasetDetail.status === DatasetStatusEnum.active && (
|
||||
<>
|
||||
{!datasetDetail?.websiteConfig?.url ? (
|
||||
<>
|
||||
{t('common:core.dataset.collection.Website Empty Tip')}
|
||||
{', '}
|
||||
<Box
|
||||
textDecoration={'underline'}
|
||||
cursor={'pointer'}
|
||||
onClick={onOpenWebsiteModal}
|
||||
>
|
||||
{t('common:core.dataset.collection.Click top config website')}
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>{t('common:core.dataset.website.UnValid Website Tip')}</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyCollectionTip;
|
||||
@@ -0,0 +1,453 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
MenuButton,
|
||||
Button,
|
||||
Link,
|
||||
useTheme,
|
||||
useDisclosure,
|
||||
HStack
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
getDatasetCollectionPathById,
|
||||
postDatasetCollection,
|
||||
putDatasetCollectionById
|
||||
} from '@/web/core/dataset/api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyInput from '@/components/MyInput';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import { useEditTitle } from '@/web/common/hooks/useEditTitle';
|
||||
import {
|
||||
DatasetCollectionTypeEnum,
|
||||
TrainingModeEnum,
|
||||
DatasetTypeEnum,
|
||||
DatasetTypeMap,
|
||||
DatasetStatusEnum
|
||||
} from '@fastgpt/global/core/dataset/constants';
|
||||
import EditFolderModal, { useEditFolder } from '../../EditFolderModal';
|
||||
import { TabEnum } from '../../../../pages/dataset/detail/index';
|
||||
import ParentPath from '@/components/common/ParentPaths';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import { ImportDataSourceEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { CollectionPageContext } from './Context';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import HeaderTagPopOver from './HeaderTagPopOver';
|
||||
|
||||
const FileSourceSelector = dynamic(() => import('../Import/components/FileSourceSelector'));
|
||||
|
||||
const Header = ({}: {}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const { setLoading, feConfigs } = useSystemStore();
|
||||
const datasetDetail = useContextSelector(DatasetPageContext, (v) => v.datasetDetail);
|
||||
|
||||
const router = useRouter();
|
||||
const { parentId = '' } = router.query as { parentId: string };
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const { searchText, setSearchText, total, getData, pageNum, onOpenWebsiteModal } =
|
||||
useContextSelector(CollectionPageContext, (v) => v);
|
||||
|
||||
const { data: paths = [] } = useQuery(['getDatasetCollectionPathById', parentId], () =>
|
||||
getDatasetCollectionPathById(parentId)
|
||||
);
|
||||
|
||||
const { editFolderData, setEditFolderData } = useEditFolder();
|
||||
const { onOpenModal: onOpenCreateVirtualFileModal, EditModal: EditCreateVirtualFileModal } =
|
||||
useEditTitle({
|
||||
title: t('common:dataset.Create manual collection'),
|
||||
tip: t('common:dataset.Manual collection Tip'),
|
||||
canEmpty: false
|
||||
});
|
||||
const {
|
||||
isOpen: isOpenFileSourceSelector,
|
||||
onOpen: onOpenFileSourceSelector,
|
||||
onClose: onCloseFileSourceSelector
|
||||
} = useDisclosure();
|
||||
const { mutate: onCreateCollection } = useRequest({
|
||||
mutationFn: async ({
|
||||
name,
|
||||
type,
|
||||
callback,
|
||||
...props
|
||||
}: {
|
||||
name: string;
|
||||
type: DatasetCollectionTypeEnum;
|
||||
callback?: (id: string) => void;
|
||||
trainingType?: TrainingModeEnum;
|
||||
rawLink?: string;
|
||||
chunkSize?: number;
|
||||
}) => {
|
||||
setLoading(true);
|
||||
const id = await postDatasetCollection({
|
||||
parentId,
|
||||
datasetId: datasetDetail._id,
|
||||
name,
|
||||
type,
|
||||
...props
|
||||
});
|
||||
callback?.(id);
|
||||
return id;
|
||||
},
|
||||
onSuccess() {
|
||||
getData(pageNum);
|
||||
},
|
||||
onSettled() {
|
||||
setLoading(false);
|
||||
},
|
||||
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed')
|
||||
});
|
||||
const isWebSite = datasetDetail?.type === DatasetTypeEnum.websiteDataset;
|
||||
|
||||
return (
|
||||
<Box display={['block', 'flex']} alignItems={'center'} gap={2}>
|
||||
<HStack flex={1}>
|
||||
<Box flex={1} fontWeight={'500'} color={'myGray.900'} whiteSpace={'nowrap'}>
|
||||
<ParentPath
|
||||
paths={paths.map((path, i) => ({
|
||||
parentId: path.parentId,
|
||||
parentName: i === paths.length - 1 ? `${path.parentName}` : path.parentName
|
||||
}))}
|
||||
FirstPathDom={
|
||||
<Flex
|
||||
flexDir={'column'}
|
||||
justify={'center'}
|
||||
h={'100%'}
|
||||
fontSize={isWebSite ? 'sm' : 'md'}
|
||||
fontWeight={'500'}
|
||||
color={'myGray.600'}
|
||||
>
|
||||
<Flex align={'center'}>
|
||||
{!isWebSite && <MyIcon name="common/list" mr={2} w={'20px'} color={'black'} />}
|
||||
{t(DatasetTypeMap[datasetDetail?.type]?.collectionLabel as any)}({total})
|
||||
</Flex>
|
||||
{datasetDetail?.websiteConfig?.url && (
|
||||
<Flex fontSize={'mini'}>
|
||||
{t('common:core.dataset.website.Base Url')}:
|
||||
<Link
|
||||
href={datasetDetail.websiteConfig.url}
|
||||
target="_blank"
|
||||
mr={2}
|
||||
color={'blue.700'}
|
||||
>
|
||||
{datasetDetail.websiteConfig.url}
|
||||
</Link>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
}
|
||||
onClick={(e) => {
|
||||
router.replace({
|
||||
query: {
|
||||
...router.query,
|
||||
parentId: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* search input */}
|
||||
{isPc && (
|
||||
<MyInput
|
||||
maxW={'250px'}
|
||||
flex={1}
|
||||
size={'sm'}
|
||||
h={'36px'}
|
||||
placeholder={t('common:common.Search') || ''}
|
||||
value={searchText}
|
||||
leftIcon={
|
||||
<MyIcon
|
||||
name="common/searchLight"
|
||||
position={'absolute'}
|
||||
w={'16px'}
|
||||
color={'myGray.500'}
|
||||
/>
|
||||
}
|
||||
onChange={(e) => {
|
||||
setSearchText(e.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tag */}
|
||||
{datasetDetail.permission.hasWritePer && feConfigs?.isPlus && <HeaderTagPopOver />}
|
||||
</HStack>
|
||||
|
||||
{/* diff collection button */}
|
||||
{datasetDetail.permission.hasWritePer && (
|
||||
<Box textAlign={'end'} mt={[3, 0]}>
|
||||
{datasetDetail?.type === DatasetTypeEnum.dataset && (
|
||||
<MyMenu
|
||||
offset={[0, 5]}
|
||||
Button={
|
||||
<MenuButton
|
||||
_hover={{
|
||||
color: 'primary.500'
|
||||
}}
|
||||
fontSize={['sm', 'md']}
|
||||
>
|
||||
<Flex
|
||||
px={3.5}
|
||||
py={2}
|
||||
borderRadius={'sm'}
|
||||
cursor={'pointer'}
|
||||
bg={'primary.500'}
|
||||
overflow={'hidden'}
|
||||
color={'white'}
|
||||
>
|
||||
<Flex h={'20px'} alignItems={'center'}>
|
||||
<MyIcon
|
||||
name={'common/folderImport'}
|
||||
mr={2}
|
||||
w={'18px'}
|
||||
h={'18px'}
|
||||
color={'white'}
|
||||
/>
|
||||
</Flex>
|
||||
<Box h={'20px'} fontSize={'sm'} fontWeight={'500'}>
|
||||
{t('common:dataset.collections.Create And Import')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</MenuButton>
|
||||
}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
label: (
|
||||
<Flex>
|
||||
<MyIcon name={'common/folderFill'} w={'20px'} mr={2} />
|
||||
{t('common:Folder')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: () => setEditFolderData({})
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Flex>
|
||||
<MyIcon name={'core/dataset/manualCollection'} mr={2} w={'20px'} />
|
||||
{t('common:core.dataset.Manual collection')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: () => {
|
||||
onOpenCreateVirtualFileModal({
|
||||
defaultVal: '',
|
||||
onSuccess: (name) => {
|
||||
onCreateCollection({ name, type: DatasetCollectionTypeEnum.virtual });
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Flex>
|
||||
<MyIcon name={'core/dataset/fileCollection'} mr={2} w={'20px'} />
|
||||
{t('common:core.dataset.Text collection')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: onOpenFileSourceSelector
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Flex>
|
||||
<MyIcon name={'core/dataset/tableCollection'} mr={2} w={'20px'} />
|
||||
{t('common:core.dataset.Table collection')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: () =>
|
||||
router.replace({
|
||||
query: {
|
||||
...router.query,
|
||||
currentTab: TabEnum.import,
|
||||
source: ImportDataSourceEnum.csvTable
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{datasetDetail?.type === DatasetTypeEnum.websiteDataset && (
|
||||
<>
|
||||
{datasetDetail?.websiteConfig?.url ? (
|
||||
<Flex alignItems={'center'}>
|
||||
{datasetDetail.status === DatasetStatusEnum.active && (
|
||||
<Button onClick={onOpenWebsiteModal}>{t('common:common.Config')}</Button>
|
||||
)}
|
||||
{datasetDetail.status === DatasetStatusEnum.syncing && (
|
||||
<Flex
|
||||
ml={3}
|
||||
alignItems={'center'}
|
||||
px={3}
|
||||
py={1}
|
||||
borderRadius="md"
|
||||
border={theme.borders.base}
|
||||
>
|
||||
<Box
|
||||
animation={'zoomStopIcon 0.5s infinite alternate'}
|
||||
bg={'myGray.700'}
|
||||
w="8px"
|
||||
h="8px"
|
||||
borderRadius={'50%'}
|
||||
mt={'1px'}
|
||||
></Box>
|
||||
<Box ml={2} color={'myGray.600'}>
|
||||
{t('common:core.dataset.status.syncing')}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
) : (
|
||||
<Button onClick={onOpenWebsiteModal}>
|
||||
{t('common:core.dataset.Set Website Config')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{datasetDetail?.type === DatasetTypeEnum.externalFile && (
|
||||
<MyMenu
|
||||
offset={[0, 5]}
|
||||
Button={
|
||||
<MenuButton
|
||||
_hover={{
|
||||
color: 'primary.500'
|
||||
}}
|
||||
fontSize={['sm', 'md']}
|
||||
>
|
||||
<Flex
|
||||
px={3.5}
|
||||
py={2}
|
||||
borderRadius={'sm'}
|
||||
cursor={'pointer'}
|
||||
bg={'primary.500'}
|
||||
overflow={'hidden'}
|
||||
color={'white'}
|
||||
>
|
||||
<Flex h={'20px'} alignItems={'center'}>
|
||||
<MyIcon
|
||||
name={'common/folderImport'}
|
||||
mr={2}
|
||||
w={'18px'}
|
||||
h={'18px'}
|
||||
color={'white'}
|
||||
/>
|
||||
</Flex>
|
||||
<Box h={'20px'} fontSize={'sm'} fontWeight={'500'}>
|
||||
{t('common:dataset.collections.Create And Import')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</MenuButton>
|
||||
}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
label: (
|
||||
<Flex>
|
||||
<MyIcon name={'common/folderFill'} w={'20px'} mr={2} />
|
||||
{t('common:Folder')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: () => setEditFolderData({})
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Flex>
|
||||
<MyIcon name={'core/dataset/fileCollection'} mr={2} w={'20px'} />
|
||||
{t('common:core.dataset.Text collection')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: () =>
|
||||
router.replace({
|
||||
query: {
|
||||
...router.query,
|
||||
currentTab: TabEnum.import,
|
||||
source: ImportDataSourceEnum.externalFile
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{/* apiDataset */}
|
||||
{(datasetDetail?.type === DatasetTypeEnum.apiDataset ||
|
||||
datasetDetail?.type === DatasetTypeEnum.feishu ||
|
||||
datasetDetail?.type === DatasetTypeEnum.yuque) && (
|
||||
<Flex
|
||||
px={3.5}
|
||||
py={2}
|
||||
borderRadius={'sm'}
|
||||
cursor={'pointer'}
|
||||
bg={'primary.500'}
|
||||
overflow={'hidden'}
|
||||
color={'white'}
|
||||
onClick={() =>
|
||||
router.replace({
|
||||
query: {
|
||||
...router.query,
|
||||
currentTab: TabEnum.import,
|
||||
source: ImportDataSourceEnum.apiDataset
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Flex h={'20px'} alignItems={'center'}>
|
||||
<MyIcon name={'common/folderImport'} mr={2} w={'18px'} h={'18px'} color={'white'} />
|
||||
</Flex>
|
||||
<Box h={'20px'} fontSize={'sm'} fontWeight={'500'}>
|
||||
{t('dataset:add_file')}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* modal */}
|
||||
{!!editFolderData && (
|
||||
<EditFolderModal
|
||||
onClose={() => 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}
|
||||
/>
|
||||
)}
|
||||
<EditCreateVirtualFileModal iconSrc={'modal/manualDataset'} closeBtnText={''} />
|
||||
{isOpenFileSourceSelector && <FileSourceSelector onClose={onCloseFileSourceSelector} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,211 @@
|
||||
import { Box, Button, Checkbox, Flex, Input, useDisclosure } from '@chakra-ui/react';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { CollectionPageContext } from './Context';
|
||||
import { isEqual } from 'lodash';
|
||||
import TagManageModal from './TagManageModal';
|
||||
import { DatasetTagType } from '@fastgpt/global/core/dataset/type';
|
||||
|
||||
const HeaderTagPopOver = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
searchDatasetTagsResult,
|
||||
searchTagKey,
|
||||
setSearchTagKey,
|
||||
checkedDatasetTag,
|
||||
setCheckedDatasetTag,
|
||||
onCreateCollectionTag,
|
||||
isCreateCollectionTagLoading
|
||||
} = useContextSelector(DatasetPageContext, (v) => v);
|
||||
|
||||
const { filterTags, setFilterTags, getData } = useContextSelector(
|
||||
CollectionPageContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
const checkedTags = filterTags;
|
||||
|
||||
const {
|
||||
isOpen: isTagManageModalOpen,
|
||||
onOpen: onOpenTagManageModal,
|
||||
onClose: onCloseTagManageModal
|
||||
} = useDisclosure();
|
||||
|
||||
const checkTags = (tag: DatasetTagType) => {
|
||||
let currentCheckedTags = [];
|
||||
if (checkedTags.includes(tag._id)) {
|
||||
currentCheckedTags = checkedTags.filter((t) => t !== tag._id);
|
||||
setCheckedDatasetTag(checkedDatasetTag.filter((t) => t._id !== tag._id));
|
||||
} else {
|
||||
currentCheckedTags = [...checkedTags, tag._id];
|
||||
setCheckedDatasetTag([...checkedDatasetTag, tag]);
|
||||
}
|
||||
if (isEqual(currentCheckedTags, filterTags)) return;
|
||||
setFilterTags(currentCheckedTags);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyPopover
|
||||
placement="bottom"
|
||||
hasArrow={false}
|
||||
offset={[2, 2]}
|
||||
w={'180px'}
|
||||
closeOnBlur={true}
|
||||
trigger={'click'}
|
||||
Trigger={
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
px={3}
|
||||
py={2}
|
||||
w={['140px', '180px']}
|
||||
borderRadius={'md'}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.250'}
|
||||
cursor={'pointer'}
|
||||
overflow={'hidden'}
|
||||
h={['28px', '36px']}
|
||||
fontSize={'sm'}
|
||||
_hover={{
|
||||
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)',
|
||||
borderColor: 'primary.300'
|
||||
}}
|
||||
>
|
||||
<Flex flex={'1 0 0'}>
|
||||
{t('dataset:tag.tags')}
|
||||
<Box as={'span'}>
|
||||
{checkedTags.length > 0 && (
|
||||
<Box ml={1} fontSize={'xs'} color={'myGray.600'}>
|
||||
{`(${checkedTags.length})`}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<MyIcon name={'core/chat/chevronDown'} w={'14px'} />
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
{({ onClose }) => (
|
||||
<MyBox isLoading={isCreateCollectionTagLoading} onClick={(e) => e.stopPropagation()}>
|
||||
<Box px={1.5} pt={1.5}>
|
||||
<Input
|
||||
pl={2}
|
||||
h={8}
|
||||
borderRadius={'xs'}
|
||||
value={searchTagKey}
|
||||
placeholder={t('dataset:tag.searchOrAddTag')}
|
||||
onChange={(e) => setSearchTagKey(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box my={1} px={1.5} maxH={'240px'} overflow={'auto'}>
|
||||
{searchTagKey &&
|
||||
!searchDatasetTagsResult.map((item) => item.tag).includes(searchTagKey) && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'sm'}
|
||||
px={1}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: '#1118240D', color: 'primary.700' }}
|
||||
borderRadius={'xs'}
|
||||
onClick={() => onCreateCollectionTag(searchTagKey)}
|
||||
>
|
||||
<MyIcon name={'common/addLight'} w={'16px'} />
|
||||
<Box ml={2} py={2}>
|
||||
{t('dataset:tag.add') + ` "${searchTagKey}"`}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{[
|
||||
...new Map(
|
||||
[...checkedDatasetTag, ...searchDatasetTagsResult].map((item) => [item._id, item])
|
||||
).values()
|
||||
].map((item) => {
|
||||
const checked = checkedTags.includes(item._id);
|
||||
return (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'sm'}
|
||||
px={1}
|
||||
py={1}
|
||||
my={1}
|
||||
cursor={'pointer'}
|
||||
color={checked ? 'primary.700' : 'myGray.600'}
|
||||
_hover={{
|
||||
bg: '#1118240D',
|
||||
color: 'primary.700',
|
||||
...(checked ? {} : { svg: { color: '#F3F3F4' } })
|
||||
}}
|
||||
borderRadius={'xs'}
|
||||
key={item._id}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
checkTags(item);
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={checkedTags.includes(item._id)}
|
||||
onChange={(e) => {
|
||||
checkTags(item);
|
||||
}}
|
||||
size={'md'}
|
||||
icon={<MyIcon name={'common/check'} w={'12px'} />}
|
||||
/>
|
||||
<Box ml={2}>{item.tag}</Box>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<Flex borderTop={'1px solid #E8EBF0'} color={'myGray.600'}>
|
||||
<Button
|
||||
w={'full'}
|
||||
fontSize={'sm'}
|
||||
_hover={{ bg: '#1118240D', color: 'primary.700' }}
|
||||
borderRadius={'none'}
|
||||
borderBottomLeftRadius={'md'}
|
||||
variant={'unstyled'}
|
||||
onClick={() => {
|
||||
setSearchTagKey('');
|
||||
setFilterTags([]);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t('dataset:tag.cancel')}
|
||||
</Button>
|
||||
<Box w={'1px'} bg={'myGray.200'}></Box>
|
||||
<Button
|
||||
w={'full'}
|
||||
fontSize={'sm'}
|
||||
_hover={{ bg: '#1118240D', color: 'primary.700' }}
|
||||
borderRadius={'none'}
|
||||
borderBottomRightRadius={'md'}
|
||||
variant={'unstyled'}
|
||||
onClick={() => {
|
||||
onOpenTagManageModal();
|
||||
}}
|
||||
>
|
||||
{t('dataset:tag.manage')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</MyBox>
|
||||
)}
|
||||
</MyPopover>
|
||||
{isTagManageModalOpen && (
|
||||
<TagManageModal
|
||||
onClose={() => {
|
||||
onCloseTagManageModal();
|
||||
getData(1);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderTagPopOver;
|
||||
@@ -0,0 +1,546 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Input, Button, Flex, Box, Checkbox } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
import { CollectionPageContext } from './Context';
|
||||
import { getCollectionIcon } from '@fastgpt/global/core/dataset/utils';
|
||||
import {
|
||||
delDatasetCollectionTag,
|
||||
getDatasetCollectionTags,
|
||||
getScrollCollectionList,
|
||||
getTagUsage,
|
||||
postAddTagsToCollections,
|
||||
updateDatasetCollectionTag
|
||||
} from '@/web/core/dataset/api';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import MyInput from '@/components/MyInput';
|
||||
import { DatasetTagType } from '@fastgpt/global/core/dataset/type';
|
||||
import { ScrollListType, useVirtualScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
|
||||
import { DatasetCollectionsListItemType } from '@/global/core/dataset/type';
|
||||
|
||||
const TagManageModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
datasetDetail,
|
||||
onCreateCollectionTag,
|
||||
isCreateCollectionTagLoading,
|
||||
loadAllDatasetTags,
|
||||
setSearchTagKey
|
||||
} = useContextSelector(DatasetPageContext, (v) => v);
|
||||
const { getData, pageNum, collections } = useContextSelector(CollectionPageContext, (v) => v);
|
||||
|
||||
const tagInputRef = useRef<HTMLInputElement>(null);
|
||||
const editInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [currentAddTag, setCurrentAddTag] = useState<
|
||||
(DatasetTagType & { collections: string[] }) | undefined
|
||||
>(undefined);
|
||||
|
||||
const [newTag, setNewTag] = useState<string | undefined>(undefined);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const [currentEditTagContent, setCurrentEditTagContent] = useState<string | undefined>(undefined);
|
||||
const [currentEditTag, setCurrentEditTag] = useState<DatasetTagType | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (newTag !== undefined && tagInputRef.current) {
|
||||
tagInputRef.current?.focus();
|
||||
}
|
||||
}, [newTag]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentEditTag !== undefined && editInputRef.current) {
|
||||
editInputRef.current?.focus();
|
||||
}
|
||||
}, [currentEditTag]);
|
||||
|
||||
const { runAsync: onDeleteCollectionTag, loading: isDeleteCollectionTagLoading } = useRequest2(
|
||||
(tag: string) =>
|
||||
delDatasetCollectionTag({
|
||||
datasetId: datasetDetail._id,
|
||||
id: tag
|
||||
}),
|
||||
{
|
||||
onSuccess() {
|
||||
fetchData(1);
|
||||
setSearchTagKey('');
|
||||
loadAllDatasetTags();
|
||||
},
|
||||
successToast: t('common:common.Delete Success'),
|
||||
errorToast: t('common:common.Delete Failed')
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onUpdateCollectionTag, loading: isUpdateCollectionTagLoading } = useRequest2(
|
||||
async (tag: DatasetTagType) => {
|
||||
return updateDatasetCollectionTag({
|
||||
datasetId: datasetDetail._id,
|
||||
tagId: tag._id,
|
||||
tag: tag.tag
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
fetchData(1);
|
||||
setSearchTagKey('');
|
||||
loadAllDatasetTags();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onSaveCollectionTag, loading: isSaveCollectionTagLoading } = useRequest2(
|
||||
async ({
|
||||
tag,
|
||||
originCollectionIds,
|
||||
collectionIds
|
||||
}: {
|
||||
tag: string;
|
||||
originCollectionIds: string[];
|
||||
collectionIds: string[];
|
||||
}) => {
|
||||
return postAddTagsToCollections({
|
||||
tag,
|
||||
originCollectionIds,
|
||||
collectionIds,
|
||||
datasetId: datasetDetail._id
|
||||
});
|
||||
},
|
||||
{
|
||||
onFinally() {
|
||||
getData(pageNum);
|
||||
},
|
||||
successToast: t('common:common.Save Success'),
|
||||
errorToast: t('common:common.Save Failed')
|
||||
}
|
||||
);
|
||||
|
||||
// Tags list
|
||||
const {
|
||||
scrollDataList: renderTags,
|
||||
totalData: collectionTags,
|
||||
ScrollList,
|
||||
isLoading: isRequesting,
|
||||
fetchData,
|
||||
total: tagsTotal
|
||||
} = useVirtualScrollPagination(getDatasetCollectionTags, {
|
||||
refreshDeps: [''],
|
||||
// debounceWait: 300,
|
||||
|
||||
itemHeight: 56,
|
||||
overscan: 10,
|
||||
|
||||
pageSize: 10,
|
||||
defaultParams: {
|
||||
datasetId: datasetDetail._id,
|
||||
searchText: ''
|
||||
}
|
||||
});
|
||||
|
||||
// Collections list
|
||||
const {
|
||||
scrollDataList: collectionsList,
|
||||
ScrollList: ScrollListCollections,
|
||||
isLoading: collectionsListLoading
|
||||
} = useVirtualScrollPagination(getScrollCollectionList, {
|
||||
refreshDeps: [searchText],
|
||||
// debounceWait: 300,
|
||||
|
||||
itemHeight: 37,
|
||||
overscan: 10,
|
||||
|
||||
pageSize: 30,
|
||||
defaultParams: {
|
||||
datasetId: datasetDetail._id,
|
||||
searchText
|
||||
}
|
||||
});
|
||||
|
||||
const { data: tagUsages } = useRequest2(() => getTagUsage(datasetDetail._id), {
|
||||
manual: false,
|
||||
refreshDeps: [collections]
|
||||
});
|
||||
|
||||
const isLoading =
|
||||
isRequesting ||
|
||||
isCreateCollectionTagLoading ||
|
||||
isDeleteCollectionTagLoading ||
|
||||
isUpdateCollectionTagLoading ||
|
||||
isSaveCollectionTagLoading ||
|
||||
collectionsListLoading;
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
iconSrc="core/dataset/tag"
|
||||
iconColor={'primary.600'}
|
||||
title={t('dataset:tag.manage')}
|
||||
w={'580px'}
|
||||
h={'600px'}
|
||||
closeOnOverlayClick={false}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{currentAddTag === undefined ? (
|
||||
<>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
color={'myGray.900'}
|
||||
pb={2}
|
||||
borderBottom={'1px solid #E8EBF0'}
|
||||
mx={8}
|
||||
pt={6}
|
||||
>
|
||||
<MyIcon name="menu" w={5} />
|
||||
<Box ml={2} fontWeight={'semibold'} flex={'1 0 0'}>
|
||||
{t('dataset:tag.total_tags', {
|
||||
total: tagsTotal
|
||||
})}
|
||||
</Box>
|
||||
<Button
|
||||
size={'sm'}
|
||||
leftIcon={<MyIcon name="common/addLight" w={4} />}
|
||||
variant={'outline'}
|
||||
fontSize={'xs'}
|
||||
onClick={() => {
|
||||
setNewTag('');
|
||||
}}
|
||||
>
|
||||
{t('dataset:tag.Add New')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex px={8} w={'full'}>
|
||||
{newTag !== undefined && (
|
||||
<Flex py={3} px={2} w={'full'} borderBottom={'1px solid #E8EBF0'}>
|
||||
<Input
|
||||
placeholder={t('dataset:tag.Add_new_tag')}
|
||||
value={newTag}
|
||||
onChange={(e) => setNewTag(e.target.value)}
|
||||
ref={tagInputRef}
|
||||
w={'200px'}
|
||||
onBlur={async () => {
|
||||
if (newTag && !collectionTags.map((item) => item.tag).includes(newTag)) {
|
||||
await onCreateCollectionTag(newTag);
|
||||
fetchData(1);
|
||||
}
|
||||
setNewTag(undefined);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
<ScrollList
|
||||
px={8}
|
||||
flex={'1 0 0'}
|
||||
fontSize={'sm'}
|
||||
EmptyChildren={<EmptyTip text={t('dataset:dataset.no_tags')} />}
|
||||
>
|
||||
{renderTags.map((listItem) => {
|
||||
const item = listItem.data;
|
||||
const tagUsage = tagUsages?.find((tagUsage) => tagUsage.tagId === item._id);
|
||||
const collections = tagUsage?.collections || [];
|
||||
const usage = collections.length;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
py={2}
|
||||
borderBottom={'1px solid #E8EBF0'}
|
||||
sx={{
|
||||
'&:hover .icon-box': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
key={item._id}
|
||||
>
|
||||
<Flex
|
||||
px={2}
|
||||
py={1}
|
||||
flex={'1'}
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
alignItems={'center'}
|
||||
borderRadius={'xs'}
|
||||
>
|
||||
<Flex
|
||||
flex={'1 0 0'}
|
||||
alignItems={'center'}
|
||||
onClick={() => {
|
||||
setCurrentAddTag({ ...item, collections });
|
||||
}}
|
||||
cursor={'pointer'}
|
||||
>
|
||||
{currentEditTag?._id !== item._id ? (
|
||||
<Box
|
||||
px={3}
|
||||
py={1.5}
|
||||
bg={'#DBF3FF'}
|
||||
color={'#0884DD'}
|
||||
fontSize={'xs'}
|
||||
borderRadius={'sm'}
|
||||
>
|
||||
{item.tag}
|
||||
</Box>
|
||||
) : (
|
||||
<Input
|
||||
placeholder={t('dataset:tag.Edit_tag')}
|
||||
value={
|
||||
currentEditTagContent !== undefined ? currentEditTagContent : item.tag
|
||||
}
|
||||
onChange={(e) => setCurrentEditTagContent(e.target.value)}
|
||||
ref={editInputRef}
|
||||
w={'200px'}
|
||||
onBlur={() => {
|
||||
if (
|
||||
currentEditTagContent &&
|
||||
!collectionTags
|
||||
.map((item) => item.tag)
|
||||
.includes(currentEditTagContent)
|
||||
) {
|
||||
onUpdateCollectionTag({
|
||||
tag: currentEditTagContent,
|
||||
_id: item._id
|
||||
});
|
||||
}
|
||||
setCurrentEditTag(undefined);
|
||||
setCurrentEditTagContent(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box as={'span'} color={'myGray.500'} ml={2}>{`(${usage})`}</Box>
|
||||
</Flex>
|
||||
<Box
|
||||
className="icon-box"
|
||||
display="none"
|
||||
_hover={{ bg: '#1118240D' }}
|
||||
mr={2}
|
||||
p={1}
|
||||
borderRadius={'sm'}
|
||||
onClick={() => {
|
||||
setCurrentAddTag({ ...item, collections });
|
||||
}}
|
||||
cursor={'pointer'}
|
||||
>
|
||||
<MyIcon name="common/add2" w={4} />
|
||||
</Box>
|
||||
<Box
|
||||
className="icon-box"
|
||||
display="none"
|
||||
_hover={{ bg: '#1118240D' }}
|
||||
mr={2}
|
||||
p={1}
|
||||
borderRadius={'sm'}
|
||||
cursor={'pointer'}
|
||||
onClick={(e) => {
|
||||
setCurrentEditTag(item);
|
||||
editInputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<MyIcon name="edit" w={4} />
|
||||
</Box>
|
||||
<PopoverConfirm
|
||||
showCancel
|
||||
content={t('dataset:tag.delete_tag_confirm')}
|
||||
type="delete"
|
||||
Trigger={
|
||||
<Box
|
||||
className="icon-box"
|
||||
display="none"
|
||||
_hover={{ bg: '#1118240D' }}
|
||||
p={1}
|
||||
borderRadius={'sm'}
|
||||
cursor={'pointer'}
|
||||
>
|
||||
<MyIcon name="delete" w={4} />
|
||||
</Box>
|
||||
}
|
||||
onConfirm={() => onDeleteCollectionTag(item._id)}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</ScrollList>
|
||||
</>
|
||||
) : (
|
||||
<AddTagToCollections
|
||||
currentAddTag={currentAddTag}
|
||||
setCurrentAddTag={setCurrentAddTag}
|
||||
onSaveCollectionTag={onSaveCollectionTag}
|
||||
setSearchText={setSearchText}
|
||||
collectionsList={collectionsList}
|
||||
ScrollListCollections={ScrollListCollections}
|
||||
/>
|
||||
)}
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagManageModal;
|
||||
|
||||
const AddTagToCollections = ({
|
||||
currentAddTag,
|
||||
setCurrentAddTag,
|
||||
onSaveCollectionTag,
|
||||
setSearchText,
|
||||
collectionsList,
|
||||
ScrollListCollections
|
||||
}: {
|
||||
currentAddTag: DatasetTagType & { collections: string[] };
|
||||
setCurrentAddTag: (tag: (DatasetTagType & { collections: string[] }) | undefined) => void;
|
||||
onSaveCollectionTag: ({
|
||||
tag,
|
||||
originCollectionIds,
|
||||
collectionIds
|
||||
}: {
|
||||
tag: string;
|
||||
originCollectionIds: string[];
|
||||
collectionIds: string[];
|
||||
}) => void;
|
||||
setSearchText: (text: string) => void;
|
||||
collectionsList: {
|
||||
index: number;
|
||||
data: DatasetCollectionsListItemType;
|
||||
}[];
|
||||
ScrollListCollections: ScrollListType;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [selectedCollections, setSelectedCollections] = useState<string[]>(
|
||||
currentAddTag.collections
|
||||
);
|
||||
const [originCollections, setOriginCollections] = useState<string[]>(currentAddTag.collections);
|
||||
|
||||
const formatCollections = useMemo(
|
||||
() =>
|
||||
collectionsList.map((item) => {
|
||||
const collection = item.data;
|
||||
const icon = getCollectionIcon(collection.type, collection.name);
|
||||
return {
|
||||
id: collection._id,
|
||||
tags: collection.tags,
|
||||
name: collection.name,
|
||||
icon
|
||||
};
|
||||
}),
|
||||
[collectionsList]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems={'center'} pb={2} mx={8} pt={6} borderBottom={'1px solid #E8EBF0'}>
|
||||
<MyIcon
|
||||
name="common/backFill"
|
||||
w={4}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
setCurrentAddTag(undefined);
|
||||
setSearchText('');
|
||||
}}
|
||||
/>
|
||||
{
|
||||
<Flex alignItems={'center'}>
|
||||
<Box
|
||||
ml={2}
|
||||
px={3}
|
||||
py={1.5}
|
||||
bg={'#DBF3FF'}
|
||||
color={'#0884DD'}
|
||||
fontSize={'sm'}
|
||||
borderRadius={'sm'}
|
||||
>
|
||||
{currentAddTag.tag}
|
||||
</Box>
|
||||
<Box
|
||||
as={'span'}
|
||||
fontSize={'sm'}
|
||||
color={'myGray.500'}
|
||||
ml={2}
|
||||
>{`(${selectedCollections.length})`}</Box>
|
||||
</Flex>
|
||||
}
|
||||
<Box flex={'1 0 0'}></Box>
|
||||
<MyInput
|
||||
placeholder={t('common:common.Search')}
|
||||
w={'200px'}
|
||||
mr={2}
|
||||
onChange={(e) => {
|
||||
setSearchText(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
leftIcon={<MyIcon name="save" w={4} />}
|
||||
onClick={() => {
|
||||
onSaveCollectionTag({
|
||||
tag: currentAddTag._id,
|
||||
originCollectionIds: originCollections,
|
||||
collectionIds: selectedCollections
|
||||
});
|
||||
setOriginCollections(selectedCollections);
|
||||
}}
|
||||
>
|
||||
{t('common:common.Save')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<ScrollListCollections
|
||||
px={8}
|
||||
mt={2}
|
||||
flex={'1 0 0'}
|
||||
fontSize={'sm'}
|
||||
EmptyChildren={<EmptyTip text={t('dataset:dataset.no_collections')} />}
|
||||
>
|
||||
{formatCollections.map((collection) => {
|
||||
return (
|
||||
<Flex
|
||||
px={2}
|
||||
py={1}
|
||||
mb={2}
|
||||
flex={'1'}
|
||||
_hover={{
|
||||
bg: 'myGray.100',
|
||||
...(!selectedCollections.includes(collection.id)
|
||||
? { svg: { color: 'myGray.100' } }
|
||||
: {})
|
||||
}}
|
||||
alignItems={'center'}
|
||||
borderRadius={'xs'}
|
||||
key={collection.id}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
setSelectedCollections((prev) => {
|
||||
if (prev.includes(collection.id)) {
|
||||
return prev.filter((id) => id !== collection.id);
|
||||
} else {
|
||||
return [...prev, collection.id];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
size={'md'}
|
||||
mr={2}
|
||||
icon={<MyIcon name="common/check" w={'12px'} />}
|
||||
onChange={() => {
|
||||
setSelectedCollections((prev) => {
|
||||
if (prev.includes(collection.id)) {
|
||||
return prev.filter((id) => id !== collection.id);
|
||||
} else {
|
||||
return [...prev, collection.id];
|
||||
}
|
||||
});
|
||||
}}
|
||||
isChecked={selectedCollections.includes(collection.id)}
|
||||
/>
|
||||
<MyIcon name={collection.icon as any} w={'20px'} mr={2} />
|
||||
<Box fontSize={'sm'} borderRadius={'sm'} color={'myGray.900'}>
|
||||
{collection.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</ScrollListCollections>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,262 @@
|
||||
import { Box, Checkbox, Flex, Input } from '@chakra-ui/react';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { putDatasetCollectionById } from '@/web/core/dataset/api';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useDeepCompareEffect } from 'ahooks';
|
||||
import { DatasetCollectionItemType, DatasetTagType } from '@fastgpt/global/core/dataset/type';
|
||||
import { isEqual } from 'lodash';
|
||||
import { DatasetCollectionsListItemType } from '@/global/core/dataset/type';
|
||||
|
||||
const TagsPopOver = ({
|
||||
currentCollection
|
||||
}: {
|
||||
currentCollection: DatasetCollectionItemType | DatasetCollectionsListItemType;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
searchTagKey,
|
||||
setSearchTagKey,
|
||||
searchDatasetTagsResult,
|
||||
allDatasetTags,
|
||||
onCreateCollectionTag,
|
||||
isCreateCollectionTagLoading
|
||||
} = useContextSelector(DatasetPageContext, (v) => v);
|
||||
|
||||
const [collectionTags, setCollectionTags] = useState<string[]>(currentCollection.tags ?? []);
|
||||
const [checkedTags, setCheckedTags] = useState<DatasetTagType[]>([]);
|
||||
const [showTagManage, setShowTagManage] = useState(false);
|
||||
const [isUpdateLoading, setIsUpdateLoading] = useState(false);
|
||||
|
||||
const tagList = useMemo(
|
||||
() =>
|
||||
(collectionTags
|
||||
?.map((item) => {
|
||||
const tagObject = allDatasetTags.find((tag) => tag.tag === item);
|
||||
return tagObject ? { _id: tagObject._id, tag: tagObject.tag } : null;
|
||||
})
|
||||
.filter((tag) => tag !== null) as {
|
||||
_id: string;
|
||||
tag: string;
|
||||
}[]) || [],
|
||||
[collectionTags, allDatasetTags]
|
||||
);
|
||||
|
||||
const [visibleTags, setVisibleTags] = useState<DatasetTagType[]>(tagList);
|
||||
const [overflowTags, setOverflowTags] = useState<DatasetTagType[]>([]);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
const calculateTags = () => {
|
||||
if (!containerRef.current || !tagList) return;
|
||||
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
const tagWidth = 11;
|
||||
let totalWidth = 30;
|
||||
let visibleCount = 0;
|
||||
|
||||
for (let i = 0; i < tagList.length; i++) {
|
||||
const tag = tagList[i];
|
||||
const estimatedWidth = tag.tag.length * tagWidth + 16; // 加上左右 padding 的宽度
|
||||
if (totalWidth + estimatedWidth <= containerWidth) {
|
||||
totalWidth += estimatedWidth;
|
||||
visibleCount++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setVisibleTags(tagList.slice(0, visibleCount));
|
||||
setOverflowTags(tagList.slice(visibleCount));
|
||||
};
|
||||
|
||||
setTimeout(calculateTags, 100);
|
||||
setCheckedTags(tagList);
|
||||
|
||||
window.addEventListener('resize', calculateTags);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', calculateTags);
|
||||
};
|
||||
}, [tagList]);
|
||||
|
||||
return (
|
||||
<MyPopover
|
||||
placement={showTagManage ? 'bottom' : 'bottom-end'}
|
||||
hasArrow={false}
|
||||
offset={[2, 2]}
|
||||
w={'180px'}
|
||||
trigger={'hover'}
|
||||
Trigger={
|
||||
<MyBox
|
||||
ref={containerRef}
|
||||
display={'flex'}
|
||||
isLoading={isUpdateLoading}
|
||||
size={'xs'}
|
||||
mt={1}
|
||||
py={0.5}
|
||||
px={0.25}
|
||||
_hover={{
|
||||
bg: 'myGray.50',
|
||||
borderRadius: '3px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!e.currentTarget.parentElement || !e.currentTarget.parentElement.parentElement)
|
||||
return;
|
||||
e.currentTarget.parentElement.parentElement.style.backgroundColor = 'white';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!e.currentTarget.parentElement || !e.currentTarget.parentElement.parentElement)
|
||||
return;
|
||||
e.currentTarget.parentElement.parentElement.style.backgroundColor = '';
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowTagManage(true);
|
||||
}}
|
||||
cursor={'pointer'}
|
||||
>
|
||||
<Flex>
|
||||
{visibleTags.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
h={5}
|
||||
mr={2}
|
||||
px={2}
|
||||
fontSize={'11px'}
|
||||
fontWeight={'500'}
|
||||
bg={'#F0FBFF'}
|
||||
color={'#0884DD'}
|
||||
borderRadius={'xs'}
|
||||
>
|
||||
{item.tag}
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
{overflowTags.length > 0 && (
|
||||
<Box h={5} px={2} bg={'#1118240D'} borderRadius={'33px'} fontSize={'11px'}>
|
||||
{`+${overflowTags.length}`}
|
||||
</Box>
|
||||
)}
|
||||
</MyBox>
|
||||
}
|
||||
onCloseFunc={async () => {
|
||||
setSearchTagKey('');
|
||||
|
||||
setShowTagManage(false);
|
||||
if (isEqual(checkedTags, tagList) || !showTagManage) return;
|
||||
setIsUpdateLoading(true);
|
||||
await putDatasetCollectionById({
|
||||
id: currentCollection._id,
|
||||
tags: checkedTags.map((tag) => tag.tag)
|
||||
});
|
||||
setCollectionTags(checkedTags.map((tag) => tag.tag));
|
||||
setIsUpdateLoading(false);
|
||||
}}
|
||||
display={showTagManage || overflowTags.length > 0 ? 'block' : 'none'}
|
||||
>
|
||||
{({}) => (
|
||||
<>
|
||||
{showTagManage ? (
|
||||
<MyBox isLoading={isCreateCollectionTagLoading} onClick={(e) => e.stopPropagation()}>
|
||||
<Box px={1.5} pt={1.5}>
|
||||
<Input
|
||||
pl={2}
|
||||
h={7}
|
||||
borderRadius={'xs'}
|
||||
value={searchTagKey}
|
||||
placeholder={t('dataset:tag.searchOrAddTag')}
|
||||
onChange={(e) => setSearchTagKey(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
<Box my={1} px={1.5} maxH={'200px'} overflow={'auto'}>
|
||||
{searchTagKey &&
|
||||
!searchDatasetTagsResult.map((item) => item.tag).includes(searchTagKey) && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'xs'}
|
||||
px={1}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: '#1118240D', color: '#2B5FD9' }}
|
||||
borderRadius={'xs'}
|
||||
onClick={() => onCreateCollectionTag(searchTagKey)}
|
||||
>
|
||||
<MyIcon name={'common/addLight'} w={'1rem'} />
|
||||
<Box ml={1} py={1}>
|
||||
{t('dataset:tag.add') + ` "${searchTagKey}"`}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{searchDatasetTagsResult?.map((item) => {
|
||||
const tagsList = checkedTags.map((tag) => tag.tag);
|
||||
return (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'xs'}
|
||||
px={1}
|
||||
py={1}
|
||||
my={1}
|
||||
key={item._id}
|
||||
cursor={'pointer'}
|
||||
color={tagsList.includes(item.tag) ? '#2B5FD9' : 'myGray.600'}
|
||||
_hover={{
|
||||
bg: '#1118240D',
|
||||
color: '#2B5FD9',
|
||||
...(tagsList.includes(item.tag) ? {} : { svg: { color: '#F3F3F4' } })
|
||||
}}
|
||||
borderRadius={'xs'}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (tagsList.includes(item.tag)) {
|
||||
setCheckedTags(checkedTags.filter((t) => t.tag !== item.tag));
|
||||
} else {
|
||||
setCheckedTags([...checkedTags, item]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={tagsList.includes(item.tag)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setCheckedTags([...checkedTags, item]);
|
||||
} else {
|
||||
setCheckedTags(checkedTags.filter((t) => t._id !== item._id));
|
||||
}
|
||||
}}
|
||||
icon={<MyIcon name={'common/check'} w={'12px'} />}
|
||||
/>
|
||||
<Box ml={2}>{item.tag}</Box>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</MyBox>
|
||||
) : (
|
||||
<Flex gap={1} p={3} flexWrap={'wrap'}>
|
||||
{overflowTags.map((tag, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
h={5}
|
||||
px={2}
|
||||
fontSize={'11px'}
|
||||
bg={'#F0FBFF'}
|
||||
color={'#0884DD'}
|
||||
borderRadius={'xs'}
|
||||
>
|
||||
{tag.tag}
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</MyPopover>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagsPopOver;
|
||||
@@ -0,0 +1,114 @@
|
||||
import React from 'react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Button, Input, Link, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import { strIsLink } from '@fastgpt/global/common/string/tools';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { getDocPath } from '@/web/common/system/doc';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
|
||||
type FormType = {
|
||||
url?: string | undefined;
|
||||
selector?: string | undefined;
|
||||
};
|
||||
|
||||
const WebsiteConfigModal = ({
|
||||
onClose,
|
||||
onSuccess,
|
||||
defaultValue = {
|
||||
url: '',
|
||||
selector: ''
|
||||
}
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onSuccess: (data: FormType) => void;
|
||||
defaultValue?: FormType;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
const { toast } = useToast();
|
||||
const { register, handleSubmit } = useForm({
|
||||
defaultValues: defaultValue
|
||||
});
|
||||
const isEdit = !!defaultValue.url;
|
||||
const confirmTip = isEdit
|
||||
? t('common:core.dataset.website.Confirm Update Tips')
|
||||
: t('common:core.dataset.website.Confirm Create Tips');
|
||||
|
||||
const { ConfirmModal, openConfirm } = useConfirm({
|
||||
type: 'common'
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
iconSrc="core/dataset/websiteDataset"
|
||||
title={t('common:core.dataset.website.Config')}
|
||||
onClose={onClose}
|
||||
maxW={'500px'}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box fontSize={'sm'} color={'myGray.600'}>
|
||||
{t('common:core.dataset.website.Config Description')}
|
||||
{feConfigs?.docUrl && (
|
||||
<Link
|
||||
href={getDocPath('/docs/guide/knowledge_base/websync/')}
|
||||
target="_blank"
|
||||
textDecoration={'underline'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
{t('common:common.course.Read Course')}
|
||||
</Link>
|
||||
)}
|
||||
</Box>
|
||||
<Box mt={2}>
|
||||
<Box>{t('common:core.dataset.website.Base Url')}</Box>
|
||||
<Input
|
||||
placeholder={t('common:core.dataset.collection.Website Link')}
|
||||
{...register('url', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
<Box mt={3}>
|
||||
<Box>
|
||||
{t('common:core.dataset.website.Selector')}({t('common:common.choosable')})
|
||||
</Box>
|
||||
<Input {...register('selector')} placeholder="body .content #document" />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} onClick={onClose}>
|
||||
{t('common:common.Close')}
|
||||
</Button>
|
||||
<Button
|
||||
ml={2}
|
||||
onClick={handleSubmit((data) => {
|
||||
if (!data.url) return;
|
||||
// check is link
|
||||
if (!strIsLink(data.url)) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('common:common.link.UnValid')
|
||||
});
|
||||
}
|
||||
openConfirm(
|
||||
() => {
|
||||
onSuccess(data);
|
||||
},
|
||||
undefined,
|
||||
confirmTip
|
||||
)();
|
||||
})}
|
||||
>
|
||||
{t('common:core.dataset.website.Start Sync')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
<ConfirmModal />
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebsiteConfigModal;
|
||||
@@ -0,0 +1,436 @@
|
||||
import React, { useState, useRef, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
TableContainer,
|
||||
Table,
|
||||
Thead,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
Tbody,
|
||||
MenuButton,
|
||||
Switch
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
delDatasetCollectionById,
|
||||
putDatasetCollectionById,
|
||||
postLinkCollectionSync
|
||||
} from '@/web/core/dataset/api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useRouter } from 'next/router';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import { useEditTitle } from '@/web/common/hooks/useEditTitle';
|
||||
import {
|
||||
DatasetCollectionTypeEnum,
|
||||
DatasetStatusEnum,
|
||||
DatasetCollectionSyncResultMap,
|
||||
DatasetTypeEnum
|
||||
} from '@fastgpt/global/core/dataset/constants';
|
||||
import { getCollectionIcon } from '@fastgpt/global/core/dataset/utils';
|
||||
import { TabEnum } from '../../../../pages/dataset/detail/index';
|
||||
import dynamic from 'next/dynamic';
|
||||
import SelectCollections from '@/web/core/dataset/components/SelectCollections';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { DatasetCollectionSyncResultEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { CollectionPageContext } from './Context';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import {
|
||||
checkCollectionIsFolder,
|
||||
getTrainingTypeLabel
|
||||
} from '@fastgpt/global/core/dataset/collection/utils';
|
||||
import { useFolderDrag } from '@/components/common/folder/useFolderDrag';
|
||||
import TagsPopOver from './TagsPopOver';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
|
||||
const Header = dynamic(() => import('./Header'));
|
||||
const EmptyCollectionTip = dynamic(() => import('./EmptyCollectionTip'));
|
||||
|
||||
const CollectionCard = () => {
|
||||
const BoxRef = useRef<HTMLDivElement>(null);
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { datasetDetail, loadDatasetDetail } = useContextSelector(DatasetPageContext, (v) => v);
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const { openConfirm: openDeleteConfirm, ConfirmModal: ConfirmDeleteModal } = useConfirm({
|
||||
content: t('common:dataset.Confirm to delete the file'),
|
||||
type: 'delete'
|
||||
});
|
||||
|
||||
const { onOpenModal: onOpenEditTitleModal, EditModal: EditTitleModal } = useEditTitle({
|
||||
title: t('common:Rename')
|
||||
});
|
||||
|
||||
const [moveCollectionData, setMoveCollectionData] = useState<{ collectionId: string }>();
|
||||
|
||||
const { collections, Pagination, total, getData, isGetting, pageNum, pageSize } =
|
||||
useContextSelector(CollectionPageContext, (v) => v);
|
||||
|
||||
// Ad file status icon
|
||||
const formatCollections = useMemo(
|
||||
() =>
|
||||
collections.map((collection) => {
|
||||
const icon = getCollectionIcon(collection.type, collection.name);
|
||||
const status = (() => {
|
||||
if (collection.trainingAmount > 0) {
|
||||
return {
|
||||
statusText: t('common:dataset.collections.Collection Embedding', {
|
||||
total: collection.trainingAmount
|
||||
}),
|
||||
colorSchema: 'gray'
|
||||
};
|
||||
}
|
||||
return {
|
||||
statusText: t('common:core.dataset.collection.status.active'),
|
||||
colorSchema: 'green'
|
||||
};
|
||||
})();
|
||||
|
||||
return {
|
||||
...collection,
|
||||
icon,
|
||||
...status
|
||||
};
|
||||
}),
|
||||
[collections, t]
|
||||
);
|
||||
|
||||
const { runAsync: onUpdateCollection, loading: isUpdating } = useRequest2(
|
||||
putDatasetCollectionById,
|
||||
{
|
||||
onSuccess() {
|
||||
getData(pageNum);
|
||||
},
|
||||
successToast: t('common:common.Update Success')
|
||||
}
|
||||
);
|
||||
const { runAsync: onDelCollection, loading: isDeleting } = useRequest2(
|
||||
(collectionId: string) => {
|
||||
return delDatasetCollectionById({
|
||||
id: collectionId
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
getData(pageNum);
|
||||
},
|
||||
successToast: t('common:common.Delete Success'),
|
||||
errorToast: t('common:common.Delete Failed')
|
||||
}
|
||||
);
|
||||
|
||||
const { openConfirm: openSyncConfirm, ConfirmModal: ConfirmSyncModal } = useConfirm({
|
||||
content: t('dataset:collection_sync_confirm_tip')
|
||||
});
|
||||
const { runAsync: onclickStartSync, loading: isSyncing } = useRequest2(postLinkCollectionSync, {
|
||||
onSuccess(res: DatasetCollectionSyncResultEnum) {
|
||||
getData(pageNum);
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t(DatasetCollectionSyncResultMap[res]?.label as any)
|
||||
});
|
||||
},
|
||||
errorToast: t('common:core.dataset.error.Start Sync Failed')
|
||||
});
|
||||
|
||||
const hasTrainingData = useMemo(
|
||||
() => !!formatCollections.find((item) => item.trainingAmount > 0),
|
||||
[formatCollections]
|
||||
);
|
||||
|
||||
useQuery(
|
||||
['refreshCollection'],
|
||||
() => {
|
||||
getData(pageNum);
|
||||
if (datasetDetail.status === DatasetStatusEnum.syncing) {
|
||||
loadDatasetDetail(datasetDetail._id);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
{
|
||||
refetchInterval: 6000,
|
||||
enabled: hasTrainingData || datasetDetail.status === DatasetStatusEnum.syncing
|
||||
}
|
||||
);
|
||||
|
||||
const { getBoxProps, isDropping } = useFolderDrag({
|
||||
activeStyles: {
|
||||
bg: 'primary.100'
|
||||
},
|
||||
onDrop: async (dragId: string, targetId: string) => {
|
||||
try {
|
||||
await putDatasetCollectionById({
|
||||
id: dragId,
|
||||
parentId: targetId
|
||||
});
|
||||
getData(pageNum);
|
||||
} catch (error) {}
|
||||
}
|
||||
});
|
||||
|
||||
const isLoading =
|
||||
isUpdating || isDeleting || isSyncing || (isGetting && collections.length === 0) || isDropping;
|
||||
|
||||
return (
|
||||
<MyBox isLoading={isLoading} h={'100%'} py={[2, 4]}>
|
||||
<Flex ref={BoxRef} flexDirection={'column'} py={[1, 0]} h={'100%'} px={[2, 6]}>
|
||||
{/* header */}
|
||||
<Header />
|
||||
|
||||
{/* collection table */}
|
||||
<TableContainer mt={3} overflowY={'auto'} fontSize={'sm'}>
|
||||
<Table variant={'simple'} draggable={false}>
|
||||
<Thead draggable={false}>
|
||||
<Tr>
|
||||
<Th py={4}>{t('common:common.Name')}</Th>
|
||||
<Th py={4}>{t('dataset:collection.Training type')}</Th>
|
||||
<Th py={4}>{t('common:dataset.collections.Data Amount')}</Th>
|
||||
<Th py={4}>{t('dataset:collection.Create update time')}</Th>
|
||||
<Th py={4}>{t('common:common.Status')}</Th>
|
||||
<Th py={4}>{t('dataset:Enable')}</Th>
|
||||
<Th py={4} />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
<Tr h={'5px'} />
|
||||
{formatCollections.map((collection) => (
|
||||
<Tr
|
||||
key={collection._id}
|
||||
_hover={{ bg: 'myGray.50' }}
|
||||
cursor={'pointer'}
|
||||
{...getBoxProps({
|
||||
dataId: collection._id,
|
||||
isFolder: collection.type === DatasetCollectionTypeEnum.folder
|
||||
})}
|
||||
draggable={false}
|
||||
onClick={() => {
|
||||
if (collection.type === DatasetCollectionTypeEnum.folder) {
|
||||
router.push({
|
||||
query: {
|
||||
datasetId: datasetDetail._id,
|
||||
parentId: collection._id
|
||||
}
|
||||
});
|
||||
} else {
|
||||
router.push({
|
||||
query: {
|
||||
datasetId: datasetDetail._id,
|
||||
collectionId: collection._id,
|
||||
currentTab: TabEnum.dataCard
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Td minW={'150px'} maxW={['200px', '300px']} draggable py={2}>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={collection.icon as any} w={'1.25rem'} mr={2} />
|
||||
<MyTooltip
|
||||
label={t('common:common.folder.Drag Tip')}
|
||||
shouldWrapChildren={false}
|
||||
>
|
||||
<Box color={'myGray.900'} fontWeight={'500'} className="textEllipsis">
|
||||
{collection.name}
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
{feConfigs?.isPlus && !!collection.tags?.length && (
|
||||
<TagsPopOver currentCollection={collection} />
|
||||
)}
|
||||
</Td>
|
||||
<Td py={2}>
|
||||
{!checkCollectionIsFolder(collection.type) ? (
|
||||
<>{t((getTrainingTypeLabel(collection.trainingType) || '-') as any)}</>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Td>
|
||||
<Td py={2}>{collection.dataAmount || '-'}</Td>
|
||||
<Td fontSize={'xs'} py={2} color={'myGray.500'}>
|
||||
<Box>{formatTime2YMDHM(collection.createTime)}</Box>
|
||||
<Box>{formatTime2YMDHM(collection.updateTime)}</Box>
|
||||
</Td>
|
||||
<Td py={2}>
|
||||
<MyTag showDot colorSchema={collection.colorSchema as any} type={'borderFill'}>
|
||||
{t(collection.statusText as any)}
|
||||
</MyTag>
|
||||
</Td>
|
||||
<Td py={2} onClick={(e) => e.stopPropagation()}>
|
||||
<Switch
|
||||
isChecked={!collection.forbid}
|
||||
size={'sm'}
|
||||
onChange={(e) =>
|
||||
onUpdateCollection({
|
||||
id: collection._id,
|
||||
forbid: !e.target.checked
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Td>
|
||||
<Td py={2} onClick={(e) => e.stopPropagation()}>
|
||||
{collection.permission.hasWritePer && (
|
||||
<MyMenu
|
||||
width={100}
|
||||
offset={[-70, 5]}
|
||||
Button={
|
||||
<MenuButton
|
||||
w={'1.5rem'}
|
||||
h={'1.5rem'}
|
||||
borderRadius={'md'}
|
||||
_hover={{
|
||||
color: 'primary.500',
|
||||
'& .icon': {
|
||||
bg: 'myGray.200'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MyIcon
|
||||
className="icon"
|
||||
name={'more'}
|
||||
h={'1rem'}
|
||||
w={'1rem'}
|
||||
px={1}
|
||||
py={1}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
/>
|
||||
</MenuButton>
|
||||
}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
...(collection.type === DatasetCollectionTypeEnum.link ||
|
||||
datasetDetail.type === DatasetTypeEnum.apiDataset
|
||||
? [
|
||||
{
|
||||
label: (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon
|
||||
name={'common/refreshLight'}
|
||||
w={'0.9rem'}
|
||||
mr={2}
|
||||
/>
|
||||
{t('dataset:collection_sync')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: () =>
|
||||
openSyncConfirm(() => {
|
||||
onclickStartSync(collection._id);
|
||||
})()
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'common/file/move'} w={'0.9rem'} mr={2} />
|
||||
{t('common:Move')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: () =>
|
||||
setMoveCollectionData({ collectionId: collection._id })
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'edit'} w={'0.9rem'} mr={2} />
|
||||
{t('common:Rename')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: () =>
|
||||
onOpenEditTitleModal({
|
||||
defaultVal: collection.name,
|
||||
onSuccess: (newName) =>
|
||||
onUpdateCollection({
|
||||
id: collection._id,
|
||||
name: newName
|
||||
})
|
||||
})
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
label: (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon
|
||||
mr={1}
|
||||
name={'delete'}
|
||||
w={'0.9rem'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
/>
|
||||
<Box>{t('common:common.Delete')}</Box>
|
||||
</Flex>
|
||||
),
|
||||
type: 'danger',
|
||||
onClick: () =>
|
||||
openDeleteConfirm(
|
||||
() => {
|
||||
onDelCollection(collection._id);
|
||||
},
|
||||
undefined,
|
||||
collection.type === DatasetCollectionTypeEnum.folder
|
||||
? t('common:dataset.collections.Confirm to delete the folder')
|
||||
: t('common:dataset.Confirm to delete the file')
|
||||
)()
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
{total > pageSize && (
|
||||
<Flex mt={2} justifyContent={'center'}>
|
||||
<Pagination />
|
||||
</Flex>
|
||||
)}
|
||||
{total === 0 && <EmptyCollectionTip />}
|
||||
</TableContainer>
|
||||
|
||||
<ConfirmDeleteModal />
|
||||
<ConfirmSyncModal />
|
||||
<EditTitleModal />
|
||||
|
||||
{!!moveCollectionData && (
|
||||
<SelectCollections
|
||||
datasetId={datasetDetail._id}
|
||||
type="folder"
|
||||
defaultSelectedId={[moveCollectionData.collectionId]}
|
||||
onClose={() => setMoveCollectionData(undefined)}
|
||||
onSuccess={async ({ parentId }) => {
|
||||
await putDatasetCollectionById({
|
||||
id: moveCollectionData.collectionId,
|
||||
parentId
|
||||
});
|
||||
getData(pageNum);
|
||||
setMoveCollectionData(undefined);
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('common:common.folder.Move Success')
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</MyBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CollectionCard);
|
||||
Reference in New Issue
Block a user