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:
Archer
2025-02-05 00:10:47 +08:00
committed by GitHub
parent c393002f1d
commit db2c0a0bdb
496 changed files with 9031 additions and 4726 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
</>
);
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);