V4.9.6 feature (#4565)
* Dashboard submenu (#4545) * add app submenu (#4452) * add app submenu * fix * width & i18n * optimize submenu code (#4515) * optimize submenu code * fix * fix * fix * fix ts * perf: dashboard sub menu * doc --------- Co-authored-by: heheer <heheer@sealos.io> * feat: value format test * doc * Mcp export (#4555) * feat: mcp server * feat: mcp server * feat: mcp server build * update doc * perf: path selector (#4556) * perf: path selector * fix: docker file path * perf: add image endpoint to dataset search (#4557) * perf: add image endpoint to dataset search * fix: mcp_server url * human in loop (#4558) * Support interactive nodes for loops, and enhance the function of merging nested and loop node history messages. (#4552) * feat: add LoopInteractive definition * feat: Support LoopInteractive type and update related logic * fix: Refactor loop handling logic and improve output value initialization * feat: Add mergeSignId to dispatchLoop and dispatchRunAppNode responses * feat: Enhance mergeChatResponseData to recursively merge plugin details and improve response handling * refactor: Remove redundant comments in mergeChatResponseData for clarity * perf: loop interactive * perf: human in loop --------- Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com> * mcp server ui * integrate mcp (#4549) * integrate mcp * delete unused code * fix ts * bug fix * fix * support whole mcp tools * add try catch * fix * fix * fix ts * fix test * fix ts * fix: interactive in v1 completions * doc * fix: router path * fix mcp integrate (#4563) * fix mcp integrate * fix ui * fix: mcp ux * feat: mcp call title * remove repeat loading * fix mcp tools avatar (#4564) * fix * fix avatar * fix update version * update doc * fix: value format * close server and remove cache * perf: avatar --------- Co-authored-by: heheer <heheer@sealos.io> Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com>
This commit is contained in:
339
projects/app/src/pageComponents/dashboard/Container.tsx
Normal file
339
projects/app/src/pageComponents/dashboard/Container.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
import { Box, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMemo } from 'react';
|
||||
import { AppTemplateTypeEnum, AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useRouter } from 'next/router';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { navbarWidth } from '@/components/Layout';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getTemplateMarketItemList, getTemplateTagList } from '@/web/core/app/api/template';
|
||||
import { AppTemplateSchemaType, TemplateTypeSchemaType } from '@fastgpt/global/core/app/type';
|
||||
import { getPluginGroups } from '@/web/core/app/api/plugin';
|
||||
import { PluginGroupSchemaType } from '@fastgpt/service/core/app/plugin/type';
|
||||
|
||||
export enum TabEnum {
|
||||
apps = 'apps',
|
||||
app_templates = 'templateMarket',
|
||||
mcp_server = 'mcpServer'
|
||||
}
|
||||
type TabEnumType = `${keyof typeof TabEnum}` | string;
|
||||
|
||||
const DashboardContainer = ({
|
||||
children
|
||||
}: {
|
||||
children: (e: {
|
||||
templateTags: TemplateTypeSchemaType[];
|
||||
templateList: AppTemplateSchemaType[];
|
||||
pluginGroups: PluginGroupSchemaType[];
|
||||
MenuIcon: JSX.Element;
|
||||
}) => React.ReactNode;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useSystem();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const { isOpen: isOpenSidebar, onOpen: onOpenSidebar, onClose: onCloseSidebar } = useDisclosure();
|
||||
|
||||
// First tab
|
||||
const currentTab = useMemo(() => {
|
||||
const path = router.asPath.split('?')[0]; // 移除查询参数
|
||||
const segments = path.split('/').filter(Boolean); // 过滤空字符串
|
||||
|
||||
return (segments.pop() as TabEnumType) || TabEnum.apps;
|
||||
}, [router.asPath]);
|
||||
|
||||
// Sub tab
|
||||
const { type: currentType, appType } = router.query as {
|
||||
type: string;
|
||||
appType?: AppTypeEnum | 'all';
|
||||
};
|
||||
|
||||
// Template market
|
||||
const { data: templateTags = [], loading: isLoadingTemplatesTags } = useRequest2(
|
||||
() =>
|
||||
currentTab === TabEnum.app_templates
|
||||
? getTemplateTagList().then((res) => [
|
||||
{
|
||||
typeId: AppTemplateTypeEnum.recommendation,
|
||||
typeName: t('app:templateMarket.templateTags.Recommendation'),
|
||||
typeOrder: 0
|
||||
},
|
||||
...res
|
||||
])
|
||||
: Promise.resolve([]),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [currentTab]
|
||||
}
|
||||
);
|
||||
const { data: templateList = [], loading: isLoadingTemplates } = useRequest2(
|
||||
() =>
|
||||
currentTab === TabEnum.app_templates
|
||||
? getTemplateMarketItemList({ type: appType })
|
||||
: Promise.resolve([]),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [currentTab, appType]
|
||||
}
|
||||
);
|
||||
|
||||
// System tools
|
||||
const { data: pluginGroups = [], loading: isLoadingToolGroups } = useRequest2(
|
||||
() =>
|
||||
getPluginGroups().then((res) =>
|
||||
res.map((item) => ({
|
||||
...item,
|
||||
groupTypes: [
|
||||
{
|
||||
typeId: 'all',
|
||||
typeName: t('app:type.All')
|
||||
},
|
||||
...item.groupTypes
|
||||
]
|
||||
}))
|
||||
),
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
|
||||
const groupList = useMemo<
|
||||
{
|
||||
groupId: string;
|
||||
groupAvatar: string;
|
||||
groupName: string;
|
||||
children: {
|
||||
typeId: string;
|
||||
typeName: string;
|
||||
isActive?: boolean;
|
||||
onClick?: () => void;
|
||||
}[];
|
||||
}[]
|
||||
>(() => {
|
||||
return [
|
||||
{
|
||||
groupId: TabEnum.apps,
|
||||
groupAvatar: 'common/app',
|
||||
groupName: t('common:core.module.template.Team app'),
|
||||
children: [
|
||||
{
|
||||
isActive: !currentType,
|
||||
typeId: 'all',
|
||||
typeName: t('app:type.All')
|
||||
},
|
||||
{
|
||||
typeId: AppTypeEnum.simple,
|
||||
typeName: t('app:type.Simple bot')
|
||||
},
|
||||
{
|
||||
typeId: AppTypeEnum.workflow,
|
||||
typeName: t('app:type.Workflow bot')
|
||||
},
|
||||
{
|
||||
typeId: AppTypeEnum.plugin,
|
||||
typeName: t('app:type.Plugin')
|
||||
}
|
||||
]
|
||||
},
|
||||
...pluginGroups.map((group) => ({
|
||||
groupId: group.groupId,
|
||||
groupAvatar: group.groupAvatar,
|
||||
groupName: t(group.groupName as any),
|
||||
children: group.groupTypes.map((type, index) => ({
|
||||
typeId: type.typeId,
|
||||
typeName: t(type.typeName as any),
|
||||
isActive: index === 0 && !currentType
|
||||
}))
|
||||
})),
|
||||
{
|
||||
groupId: TabEnum.app_templates,
|
||||
groupAvatar: 'common/templateMarket',
|
||||
groupName: t('common:template_market'),
|
||||
children: [
|
||||
...templateTags
|
||||
.map((tag) => {
|
||||
const templates = templateList.filter((template) =>
|
||||
template.tags.includes(tag.typeId)
|
||||
);
|
||||
return {
|
||||
...tag,
|
||||
templates
|
||||
};
|
||||
})
|
||||
.filter((tag) => tag.templates.length > 0)
|
||||
.map((tag, index) => ({
|
||||
typeId: tag.typeId,
|
||||
typeName: t(tag.typeName as any),
|
||||
isActive: index === 0 && !currentType
|
||||
})),
|
||||
...(feConfigs?.appTemplateCourse
|
||||
? [
|
||||
{
|
||||
typeId: AppTemplateTypeEnum.contribute,
|
||||
typeName: t('common:contribute_app_template'),
|
||||
onClick: () => {
|
||||
window.open(feConfigs.appTemplateCourse);
|
||||
}
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
},
|
||||
...(feConfigs?.mcpServerProxyEndpoint
|
||||
? [
|
||||
{
|
||||
groupId: TabEnum.mcp_server,
|
||||
groupAvatar: 'key',
|
||||
groupName: t('common:mcp_server'),
|
||||
children: []
|
||||
}
|
||||
]
|
||||
: [])
|
||||
];
|
||||
}, [currentType, feConfigs.appTemplateCourse, pluginGroups, t, templateList, templateTags]);
|
||||
|
||||
const MenuIcon = useMemo(
|
||||
() => (
|
||||
<Flex alignItems={'center'}>
|
||||
{isOpenSidebar && (
|
||||
<Box
|
||||
position="fixed"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
bg="blackAlpha.600"
|
||||
onClick={onCloseSidebar}
|
||||
zIndex={99}
|
||||
/>
|
||||
)}
|
||||
<MyIcon name="menu" w={'1.25rem'} mr={1.5} onClick={onOpenSidebar} />
|
||||
</Flex>
|
||||
),
|
||||
[isOpenSidebar, onCloseSidebar, onOpenSidebar]
|
||||
);
|
||||
|
||||
const isLoading = isLoadingTemplatesTags || isLoadingTemplates || isLoadingToolGroups;
|
||||
|
||||
return (
|
||||
<Box h={'100%'}>
|
||||
{/* Side bar */}
|
||||
{(isPc || isOpenSidebar) && (
|
||||
<MyBox
|
||||
isLoading={isLoading}
|
||||
position={'fixed'}
|
||||
left={isPc ? navbarWidth : 0}
|
||||
top={0}
|
||||
bg={'myGray.25'}
|
||||
w={`220px`}
|
||||
h={'full'}
|
||||
borderLeft={'1px solid'}
|
||||
borderRight={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
pt={4}
|
||||
px={2.5}
|
||||
pb={2.5}
|
||||
zIndex={100}
|
||||
userSelect={'none'}
|
||||
>
|
||||
{groupList.map((group) => {
|
||||
const selected = currentTab === group.groupId;
|
||||
|
||||
return (
|
||||
<Box key={group.groupId}>
|
||||
<Flex
|
||||
p={2}
|
||||
fontSize={'sm'}
|
||||
rounded={'md'}
|
||||
color={'myGray.700'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'primary.50'
|
||||
}}
|
||||
mb={0.5}
|
||||
onClick={() => {
|
||||
router.push(`/dashboard/${group.groupId}`);
|
||||
onCloseSidebar();
|
||||
}}
|
||||
{...(group.children.length === 0 &&
|
||||
selected && { bg: 'primary.100', color: 'primary.600' })}
|
||||
>
|
||||
<Avatar src={group.groupAvatar} w={'1rem'} mr={1.5} />
|
||||
<Box fontWeight={'medium'}>{group.groupName}</Box>
|
||||
<Box flex={1} />
|
||||
{group.children.length > 0 && (
|
||||
<MyIcon
|
||||
name={selected ? 'core/chat/chevronDown' : 'core/chat/chevronUp'}
|
||||
w={'1rem'}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
{selected && (
|
||||
<Box>
|
||||
{group.children.map((child) => {
|
||||
const isActive = child.isActive || child.typeId === currentType;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
key={child.typeId}
|
||||
fontSize={'sm'}
|
||||
fontWeight={500}
|
||||
rounded={'md'}
|
||||
py={2}
|
||||
pl={'30px'}
|
||||
cursor={'pointer'}
|
||||
mb={0.5}
|
||||
_hover={{ bg: 'primary.50' }}
|
||||
{...(isActive
|
||||
? {
|
||||
bg: 'primary.50',
|
||||
color: 'primary.600'
|
||||
}
|
||||
: {
|
||||
bg: 'transparent',
|
||||
color: 'myGray.500'
|
||||
})}
|
||||
onClick={() => {
|
||||
if (child.onClick) {
|
||||
child.onClick();
|
||||
} else {
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
type: child.typeId
|
||||
}
|
||||
});
|
||||
onCloseSidebar();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{child.typeName}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</MyBox>
|
||||
)}
|
||||
|
||||
<Box h={'100%'} pl={isPc ? `220px` : 0} position={'relative'}>
|
||||
{children({
|
||||
templateTags,
|
||||
templateList,
|
||||
pluginGroups,
|
||||
MenuIcon
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardContainer;
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { Box, Flex, HStack } from '@chakra-ui/react';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { PluginGroupSchemaType } from '@fastgpt/service/core/app/plugin/type';
|
||||
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
|
||||
|
||||
const PluginCard = ({
|
||||
item,
|
||||
groups
|
||||
}: {
|
||||
item: NodeTemplateListItemType;
|
||||
groups: PluginGroupSchemaType[];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const type = groups.reduce<string | undefined>((acc, group) => {
|
||||
const foundType = group.groupTypes.find((type) => type.typeId === item.templateType);
|
||||
return foundType ? foundType.typeName : acc;
|
||||
}, undefined);
|
||||
|
||||
return (
|
||||
<MyBox
|
||||
key={item.id}
|
||||
lineHeight={1.5}
|
||||
h="100%"
|
||||
pt={4}
|
||||
pb={3}
|
||||
px={4}
|
||||
border={'base'}
|
||||
boxShadow={'2'}
|
||||
bg={'white'}
|
||||
borderRadius={'10px'}
|
||||
position={'relative'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
_hover={{
|
||||
borderColor: 'primary.300',
|
||||
boxShadow: '1.5'
|
||||
}}
|
||||
>
|
||||
<HStack>
|
||||
<Avatar src={item.avatar} borderRadius={'sm'} w={'1.5rem'} h={'1.5rem'} />
|
||||
<Box flex={'1 0 0'} color={'myGray.900'} fontWeight={500}>
|
||||
{item.name}
|
||||
</Box>
|
||||
<Box mr={'-1rem'}>
|
||||
<Flex
|
||||
bg={'myGray.100'}
|
||||
color={'myGray.600'}
|
||||
py={0.5}
|
||||
pl={2}
|
||||
pr={3}
|
||||
borderLeftRadius={'sm'}
|
||||
whiteSpace={'nowrap'}
|
||||
>
|
||||
<Box ml={1} fontSize={'mini'}>
|
||||
{t(type as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
</HStack>
|
||||
<Box
|
||||
flex={['1 0 48px', '1 0 56px']}
|
||||
mt={3}
|
||||
pr={1}
|
||||
textAlign={'justify'}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'xs'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
<Box className={'textEllipsis2'}>{item.intro || t('app:templateMarket.no_intro')}</Box>
|
||||
</Box>
|
||||
|
||||
<Flex w={'full'} fontSize={'mini'}>
|
||||
<Flex flex={1}>
|
||||
{(item.instructions || item.courseUrl) && (
|
||||
<UseGuideModal
|
||||
title={item.name}
|
||||
iconSrc={item.avatar}
|
||||
text={item.instructions}
|
||||
link={item.courseUrl}
|
||||
>
|
||||
{({ onClick }) => (
|
||||
<Flex
|
||||
color={'primary.700'}
|
||||
alignItems={'center'}
|
||||
gap={1}
|
||||
cursor={'pointer'}
|
||||
onClick={onClick}
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
>
|
||||
<MyIcon name={'book'} w={'14px'} />
|
||||
{t('app:plugin.Instructions')}
|
||||
</Flex>
|
||||
)}
|
||||
</UseGuideModal>
|
||||
)}
|
||||
</Flex>
|
||||
<Box color={'myGray.500'}>{`by ${item.author || feConfigs.systemTitle}`}</Box>
|
||||
</Flex>
|
||||
</MyBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(PluginCard);
|
||||
336
projects/app/src/pageComponents/dashboard/apps/CreateModal.tsx
Normal file
336
projects/app/src/pageComponents/dashboard/apps/CreateModal.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
ModalBody,
|
||||
Input,
|
||||
Grid,
|
||||
Card,
|
||||
Textarea,
|
||||
ModalFooter
|
||||
} from '@chakra-ui/react';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { postCreateApp } from '@/web/core/app/api';
|
||||
import { useRouter } from 'next/router';
|
||||
import { emptyTemplates, parsePluginFromCurlString } from '@/web/core/app/templates';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppListContext } from './context';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { ChevronRightIcon } from '@chakra-ui/icons';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import {
|
||||
getTemplateMarketItemDetail,
|
||||
getTemplateMarketItemList
|
||||
} from '@/web/core/app/api/template';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
||||
import { appTypeMap } from '@/pageComponents/app/constants';
|
||||
|
||||
type FormType = {
|
||||
avatar: string;
|
||||
name: string;
|
||||
curlContent: string;
|
||||
};
|
||||
|
||||
export type CreateAppType = AppTypeEnum.simple | AppTypeEnum.workflow | AppTypeEnum.plugin;
|
||||
|
||||
const CreateModal = ({ onClose, type }: { type: CreateAppType; onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
|
||||
const { isPc } = useSystem();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const [currentCreateType, setCurrentCreateType] = useState<'template' | 'curl'>('template');
|
||||
const isTemplateMode = currentCreateType === 'template';
|
||||
|
||||
const typeData = appTypeMap[type];
|
||||
const { data: templateList = [], loading: isRequestTemplates } = useRequest2(
|
||||
() => getTemplateMarketItemList({ isQuickTemplate: true, type }),
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
|
||||
const { register, setValue, watch, handleSubmit } = useForm<FormType>({
|
||||
defaultValues: {
|
||||
avatar: typeData.avatar,
|
||||
name: '',
|
||||
curlContent: ''
|
||||
}
|
||||
});
|
||||
|
||||
const avatar = watch('avatar');
|
||||
const {
|
||||
File,
|
||||
onOpen: onOpenSelectFile,
|
||||
onSelectImage
|
||||
} = useSelectFile({
|
||||
fileType: '.jpg,.png',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
const { runAsync: onclickCreate, loading: isCreating } = useRequest2(
|
||||
async ({ avatar, name, curlContent }: FormType, templateId?: string) => {
|
||||
// From empty template
|
||||
if (!templateId && currentCreateType !== 'curl') {
|
||||
return postCreateApp({
|
||||
parentId,
|
||||
avatar: avatar,
|
||||
name: name,
|
||||
type,
|
||||
modules: emptyTemplates[type].nodes,
|
||||
edges: emptyTemplates[type].edges,
|
||||
chatConfig: emptyTemplates[type].chatConfig
|
||||
});
|
||||
}
|
||||
|
||||
const { workflow, appAvatar } = await (async () => {
|
||||
if (templateId) {
|
||||
const templateDetail = await getTemplateMarketItemDetail(templateId);
|
||||
return {
|
||||
appAvatar: templateDetail.avatar,
|
||||
workflow: templateDetail.workflow
|
||||
};
|
||||
}
|
||||
if (curlContent) {
|
||||
return {
|
||||
appAvatar: avatar,
|
||||
workflow: parsePluginFromCurlString(curlContent)
|
||||
};
|
||||
}
|
||||
return Promise.reject('No template or curl content');
|
||||
})();
|
||||
|
||||
return postCreateApp({
|
||||
parentId,
|
||||
avatar: appAvatar,
|
||||
name: name,
|
||||
type,
|
||||
modules: workflow.nodes || [],
|
||||
edges: workflow.edges || [],
|
||||
chatConfig: workflow.chatConfig || {}
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess(id: string) {
|
||||
router.push(`/app/detail?appId=${id}`);
|
||||
loadMyApps();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
iconSrc={typeData.icon}
|
||||
title={t(typeData.title)}
|
||||
isOpen
|
||||
isCentered={!isPc}
|
||||
maxW={['90vw', '40rem']}
|
||||
isLoading={isCreating || isRequestTemplates}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box color={'myGray.800'} fontWeight={'bold'}>
|
||||
{t('common:common.Set Name')}
|
||||
</Box>
|
||||
<Flex mt={2} alignItems={'center'}>
|
||||
<MyTooltip label={t('common:common.Set Avatar')}>
|
||||
<Avatar
|
||||
flexShrink={0}
|
||||
src={avatar}
|
||||
w={['28px', '36px']}
|
||||
h={['28px', '36px']}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={onOpenSelectFile}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Input
|
||||
flex={1}
|
||||
ml={3}
|
||||
autoFocus
|
||||
bg={'myWhite.600'}
|
||||
{...register('name', {
|
||||
required: t('common:core.app.error.App name can not be empty')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex mt={[4, 7]} mb={3}>
|
||||
{type === AppTypeEnum.plugin ? (
|
||||
<FillRowTabs
|
||||
list={[
|
||||
{ label: t('app:create_by_template'), value: 'template' },
|
||||
{ label: t('app:create_by_curl'), value: 'curl' }
|
||||
]}
|
||||
value={currentCreateType}
|
||||
fontSize={'xs'}
|
||||
onChange={(e) => setCurrentCreateType(e as 'template' | 'curl')}
|
||||
/>
|
||||
) : (
|
||||
<Box color={'myGray.900'} fontWeight={'bold'} fontSize={'sm'}>
|
||||
{t('app:create_by_template')}
|
||||
</Box>
|
||||
)}
|
||||
<Box flex={1} />
|
||||
{isTemplateMode && (
|
||||
<Flex
|
||||
onClick={() => {
|
||||
router.push({
|
||||
pathname: '/dashboard/templateMarket',
|
||||
query: {
|
||||
appType: type,
|
||||
parentId
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.600'}
|
||||
fontSize={'xs'}
|
||||
_hover={{ color: 'blue.700' }}
|
||||
>
|
||||
{t('common:core.app.switch_to_template_market')}
|
||||
<ChevronRightIcon w={4} h={4} />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{isTemplateMode ? (
|
||||
<Grid
|
||||
userSelect={'none'}
|
||||
gridTemplateColumns={
|
||||
templateList.length > 0 ? ['repeat(1,1fr)', 'repeat(2,1fr)'] : '1fr'
|
||||
}
|
||||
gridGap={[2, 4]}
|
||||
>
|
||||
<Card
|
||||
borderWidth={'1px'}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
boxShadow={'3'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
color={'myGray.500'}
|
||||
borderColor={'myGray.200'}
|
||||
h={'8.25rem'}
|
||||
_hover={{
|
||||
color: 'primary.700',
|
||||
borderColor: 'primary.300'
|
||||
}}
|
||||
onClick={handleSubmit((data) => onclickCreate(data))}
|
||||
>
|
||||
<MyIcon name={'common/addLight'} w={'1.5rem'} />
|
||||
<Box fontSize={'sm'} mt={2}>
|
||||
{t(typeData.emptyCreateText)}
|
||||
</Box>
|
||||
</Card>
|
||||
{templateList.map((item) => (
|
||||
<Card
|
||||
key={item.templateId}
|
||||
p={4}
|
||||
borderRadius={'md'}
|
||||
borderWidth={'1px'}
|
||||
borderColor={'myGray.200'}
|
||||
boxShadow={'3'}
|
||||
h={'8.25rem'}
|
||||
_hover={{
|
||||
borderColor: 'primary.300',
|
||||
'& .buttons': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
>
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar src={item.avatar} borderRadius={'sm'} w={'1.5rem'} />
|
||||
<Box ml={3} color={'myGray.900'} fontWeight={500}>
|
||||
{t(item.name as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box fontSize={'xs'} mt={2} color={'myGray.600'} flex={1}>
|
||||
{t(item.intro as any)}
|
||||
</Box>
|
||||
<Box w={'full'} fontSize={'mini'}>
|
||||
<Box color={'myGray.500'}>{`By ${item.author || feConfigs.systemTitle}`}</Box>
|
||||
<Box
|
||||
className="buttons"
|
||||
display={'none'}
|
||||
justifyContent={'center'}
|
||||
alignItems={'center'}
|
||||
position={'absolute'}
|
||||
borderRadius={'lg'}
|
||||
w={'full'}
|
||||
h={'full'}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={1}
|
||||
height={'40px'}
|
||||
bg={'white'}
|
||||
zIndex={1}
|
||||
>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
h={6}
|
||||
borderRadius={'sm'}
|
||||
w={'40%'}
|
||||
onClick={handleSubmit((data) => onclickCreate(data, item.templateId))}
|
||||
>
|
||||
{t('app:templateMarket.Use')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Box>
|
||||
<Textarea
|
||||
placeholder={t('app:oaste_curl_string')}
|
||||
w={'560px'}
|
||||
h={'260px'}
|
||||
bg={'myGray.50'}
|
||||
{...register('curlContent')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter gap={4}>
|
||||
<Button variant={'whiteBase'} onClick={onClose}>
|
||||
{t('common:common.Cancel')}
|
||||
</Button>
|
||||
<Button variant={'primary'} onClick={handleSubmit((data) => onclickCreate(data))}>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
<File
|
||||
onSelect={(e) =>
|
||||
onSelectImage(e, {
|
||||
maxH: 300,
|
||||
maxW: 300,
|
||||
callback: (e) => setValue('avatar', e)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateModal;
|
||||
@@ -0,0 +1,469 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
ModalBody,
|
||||
Input,
|
||||
Textarea,
|
||||
TableContainer,
|
||||
Table,
|
||||
Thead,
|
||||
Th,
|
||||
Tbody,
|
||||
Tr,
|
||||
Td,
|
||||
ModalFooter
|
||||
} from '@chakra-ui/react';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { HttpPluginImgUrl } from '@fastgpt/global/common/file/image/constants';
|
||||
import {
|
||||
postCreateHttpPlugin,
|
||||
putUpdateHttpPlugin,
|
||||
getApiSchemaByUrl
|
||||
} from '@/web/core/app/api/plugin';
|
||||
import { str2OpenApiSchema } from '@fastgpt/global/core/app/httpPlugin/utils';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import HttpInput from '@fastgpt/web/components/common/Input/HttpInput';
|
||||
import { OpenApiJsonSchema } from '@fastgpt/global/core/app/httpPlugin/type';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppListContext } from './context';
|
||||
|
||||
export type EditHttpPluginProps = {
|
||||
id?: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
intro?: string;
|
||||
pluginData?: AppSchema['pluginData'];
|
||||
};
|
||||
export const defaultHttpPlugin: EditHttpPluginProps = {
|
||||
avatar: HttpPluginImgUrl,
|
||||
name: '',
|
||||
intro: '',
|
||||
pluginData: {
|
||||
apiSchemaStr: '',
|
||||
customHeaders: '{"Authorization":"Bearer"}'
|
||||
}
|
||||
};
|
||||
|
||||
const HttpPluginEditModal = ({
|
||||
defaultPlugin = defaultHttpPlugin,
|
||||
onClose
|
||||
}: {
|
||||
defaultPlugin?: EditHttpPluginProps;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const isEdit = !!defaultPlugin.id;
|
||||
|
||||
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
|
||||
|
||||
const [schemaUrl, setSchemaUrl] = useState('');
|
||||
const [customHeaders, setCustomHeaders] = useState<{ key: string; value: string }[]>(() => {
|
||||
const keyValue = JSON.parse(defaultPlugin.pluginData?.customHeaders || '{}');
|
||||
return Object.keys(keyValue).map((key) => ({ key, value: keyValue[key] }));
|
||||
});
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
|
||||
const { register, setValue, handleSubmit, watch } = useForm<EditHttpPluginProps>({
|
||||
defaultValues: defaultPlugin
|
||||
});
|
||||
const avatar = watch('avatar');
|
||||
const apiSchemaStr = watch('pluginData.apiSchemaStr');
|
||||
const [apiData, setApiData] = useState<OpenApiJsonSchema>({ pathData: [], serverPath: '' });
|
||||
|
||||
const { mutate: onCreate, isLoading: isCreating } = useRequest({
|
||||
mutationFn: async (data: EditHttpPluginProps) => {
|
||||
return postCreateHttpPlugin({
|
||||
parentId,
|
||||
name: data.name,
|
||||
intro: data.intro,
|
||||
avatar: data.avatar,
|
||||
pluginData: {
|
||||
apiSchemaStr: data.pluginData?.apiSchemaStr,
|
||||
customHeaders: data.pluginData?.customHeaders
|
||||
}
|
||||
});
|
||||
},
|
||||
onSuccess() {
|
||||
loadMyApps();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed')
|
||||
});
|
||||
|
||||
const { mutate: updatePlugins, isLoading: isUpdating } = useRequest({
|
||||
mutationFn: async (data: EditHttpPluginProps) => {
|
||||
if (!data.id || !data.pluginData) return Promise.resolve('');
|
||||
|
||||
return putUpdateHttpPlugin({
|
||||
appId: data.id,
|
||||
name: data.name,
|
||||
intro: data.intro,
|
||||
avatar: data.avatar,
|
||||
pluginData: data.pluginData
|
||||
});
|
||||
},
|
||||
onSuccess() {
|
||||
loadMyApps();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common:common.Update Success'),
|
||||
errorToast: t('common:common.Update Failed')
|
||||
});
|
||||
|
||||
const {
|
||||
File,
|
||||
onOpen: onOpenSelectFile,
|
||||
onSelectImage
|
||||
} = useSelectFile({
|
||||
fileType: 'image/*',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
/* load api from url */
|
||||
const { mutate: onClickUrlLoadApi, isLoading: isLoadingUrlApi } = useRequest({
|
||||
mutationFn: async () => {
|
||||
if (!schemaUrl || (!schemaUrl.startsWith('https://') && !schemaUrl.startsWith('http://'))) {
|
||||
return toast({
|
||||
title: t('common:plugin.Invalid URL'),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
|
||||
const schema = await getApiSchemaByUrl(schemaUrl);
|
||||
setValue('pluginData.apiSchemaStr', JSON.stringify(schema, null, 2));
|
||||
},
|
||||
errorToast: t('common:plugin.Invalid Schema')
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!apiSchemaStr) {
|
||||
return setApiData({ pathData: [], serverPath: '' });
|
||||
}
|
||||
try {
|
||||
setApiData(await str2OpenApiSchema(apiSchemaStr));
|
||||
} catch (err) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('common:plugin.Invalid Schema')
|
||||
});
|
||||
setApiData({ pathData: [], serverPath: '' });
|
||||
}
|
||||
})();
|
||||
}, [apiSchemaStr, t, toast]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyModal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
iconSrc="core/app/type/httpPluginFill"
|
||||
title={isEdit ? t('common:plugin.Edit Http Plugin') : t('common:plugin.Import Plugin')}
|
||||
w={['90vw', '600px']}
|
||||
h={['90vh', '80vh']}
|
||||
position={'relative'}
|
||||
>
|
||||
<ModalBody flex={'1 0 0'} overflow={'auto'}>
|
||||
<>
|
||||
<Box color={'myGray.800'} fontWeight={'bold'}>
|
||||
{t('common:plugin.Set Name')}
|
||||
</Box>
|
||||
<Flex mt={3} alignItems={'center'}>
|
||||
<MyTooltip label={t('common:common.Set Avatar')}>
|
||||
<Avatar
|
||||
flexShrink={0}
|
||||
src={avatar}
|
||||
w={['28px', '32px']}
|
||||
h={['28px', '32px']}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={onOpenSelectFile}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Input
|
||||
flex={1}
|
||||
ml={4}
|
||||
bg={'myWhite.600'}
|
||||
{...register('name', {
|
||||
required: t('common:common.name_is_empty')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<>
|
||||
<Box color={'myGray.800'} fontWeight={'bold'} mt={3}>
|
||||
{t('common:plugin.Intro')}
|
||||
</Box>
|
||||
<Textarea
|
||||
{...register('intro')}
|
||||
bg={'myWhite.600'}
|
||||
rows={3}
|
||||
mt={3}
|
||||
placeholder={t('common:core.plugin.Http plugin intro placeholder')}
|
||||
/>
|
||||
</>
|
||||
</>
|
||||
{/* import */}
|
||||
<Box mt={4}>
|
||||
<Box
|
||||
color={'myGray.800'}
|
||||
fontWeight={'bold'}
|
||||
justifyContent={'space-between'}
|
||||
display={'flex'}
|
||||
>
|
||||
<Box my={'auto'}>{'OpenAPI Schema'}</Box>
|
||||
|
||||
<Box>
|
||||
<Flex alignItems={'center'}>
|
||||
<Input
|
||||
mr={2}
|
||||
placeholder={t('common:plugin.Import from URL')}
|
||||
h={'30px'}
|
||||
w={['150px', '250px']}
|
||||
fontSize={'sm'}
|
||||
onBlur={(e) => setSchemaUrl(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'whitePrimary'}
|
||||
isLoading={isLoadingUrlApi}
|
||||
onClick={onClickUrlLoadApi}
|
||||
>
|
||||
{t('common:common.Import')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
<Textarea
|
||||
{...register('pluginData.apiSchemaStr')}
|
||||
bg={'myWhite.600'}
|
||||
rows={10}
|
||||
mt={3}
|
||||
onBlur={(e) => {
|
||||
const content = e.target.value;
|
||||
if (!content) return;
|
||||
setValue('pluginData.apiSchemaStr', content);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<>
|
||||
<Box color={'myGray.800'} fontWeight={'bold'} mt={3}>
|
||||
{t('common:core.plugin.Custom headers')}
|
||||
</Box>
|
||||
<Box
|
||||
mt={1}
|
||||
borderRadius={'md'}
|
||||
overflow={'hidden'}
|
||||
borderWidth={'1px'}
|
||||
borderBottom={'none'}
|
||||
>
|
||||
<TableContainer overflowY={'visible'} overflowX={'unset'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th px={2} borderRadius="none !important">
|
||||
{t('common:core.module.http.Props name')}
|
||||
</Th>
|
||||
<Th px={2} borderRadius="none !important">
|
||||
{t('common:core.module.http.Props value')}
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{customHeaders.map((item, index) => (
|
||||
<Tr key={`${index}`}>
|
||||
<Td p={0} w={'150px'}>
|
||||
<HttpInput
|
||||
placeholder={t('common:core.module.http.Props name')}
|
||||
value={item.key}
|
||||
onBlur={(val) => {
|
||||
setCustomHeaders((prev) => {
|
||||
const newHeaders = prev.map((item, i) =>
|
||||
i === index ? { ...item, key: val } : item
|
||||
);
|
||||
setValue(
|
||||
'pluginData.customHeaders',
|
||||
'{\n' +
|
||||
newHeaders
|
||||
.map((item) => `"${item.key}":"${item.value}"`)
|
||||
.join(',\n') +
|
||||
'\n}'
|
||||
);
|
||||
return newHeaders;
|
||||
});
|
||||
}}
|
||||
updateTrigger={updateTrigger}
|
||||
/>
|
||||
</Td>
|
||||
<Td p={0}>
|
||||
<Box display={'flex'} alignItems={'center'}>
|
||||
<HttpInput
|
||||
placeholder={t('common:core.module.http.Props value')}
|
||||
value={item.value}
|
||||
onBlur={(val) =>
|
||||
setCustomHeaders((prev) => {
|
||||
const newHeaders = prev.map((item, i) =>
|
||||
i === index ? { ...item, value: val } : item
|
||||
);
|
||||
setValue(
|
||||
'pluginData.customHeaders',
|
||||
'{\n' +
|
||||
newHeaders
|
||||
.map((item) => `"${item.key}":"${item.value}"`)
|
||||
.join(',\n') +
|
||||
'\n}'
|
||||
);
|
||||
return newHeaders;
|
||||
})
|
||||
}
|
||||
/>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
w={'14px'}
|
||||
onClick={() =>
|
||||
setCustomHeaders((prev) => {
|
||||
const newHeaders = prev.filter((val) => val.key !== item.key);
|
||||
setValue(
|
||||
'pluginData.customHeaders',
|
||||
'{\n' +
|
||||
newHeaders
|
||||
.map((item) => `"${item.key}":"${item.value}"`)
|
||||
.join(',\n') +
|
||||
'\n}'
|
||||
);
|
||||
return newHeaders;
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
<Tr>
|
||||
<Td p={0} w={'150px'}>
|
||||
<HttpInput
|
||||
placeholder={t('common:core.module.http.Add props')}
|
||||
value={''}
|
||||
updateTrigger={updateTrigger}
|
||||
onBlur={(val) => {
|
||||
if (!val) return;
|
||||
setCustomHeaders((prev) => {
|
||||
const newHeaders = [...prev, { key: val, value: '' }];
|
||||
setValue(
|
||||
'pluginData.customHeaders',
|
||||
'{\n' +
|
||||
newHeaders
|
||||
.map((item) => `"${item.key}":"${item.value}"`)
|
||||
.join(',\n') +
|
||||
'\n}'
|
||||
);
|
||||
return newHeaders;
|
||||
});
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
<Td p={0}>
|
||||
<Box display={'flex'} alignItems={'center'}>
|
||||
<HttpInput />
|
||||
</Box>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
</>
|
||||
<>
|
||||
<Box color={'myGray.800'} fontWeight={'bold'} mt={3}>
|
||||
{t('common:plugin.Plugin List')}
|
||||
</Box>
|
||||
<Box
|
||||
mt={3}
|
||||
borderRadius={'md'}
|
||||
overflow={'hidden'}
|
||||
borderWidth={'1px'}
|
||||
borderBottom={'none'}
|
||||
>
|
||||
<TableContainer maxH={400} overflowY={'auto'}>
|
||||
<Table bg={'white'}>
|
||||
<Thead bg={'myGray.50'}>
|
||||
<Th>{t('common:Name')}</Th>
|
||||
<Th>{t('common:plugin.Description')}</Th>
|
||||
<Th>{t('common:plugin.Method')}</Th>
|
||||
<Th>{t('common:plugin.Path')}</Th>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{apiData.pathData?.map((item, index) => (
|
||||
<Tr key={index}>
|
||||
<Td>{item.name}</Td>
|
||||
<Td
|
||||
fontSize={'sm'}
|
||||
textColor={'gray.600'}
|
||||
w={'auto'}
|
||||
maxW={80}
|
||||
whiteSpace={'pre-wrap'}
|
||||
>
|
||||
{item.description}
|
||||
</Td>
|
||||
<Td>{item.method}</Td>
|
||||
<Td>{item.path}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
</>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
|
||||
{t('common:common.Close')}
|
||||
</Button>
|
||||
{!isEdit ? (
|
||||
<Button
|
||||
isDisabled={apiData.pathData.length === 0}
|
||||
onClick={handleSubmit((data) => onCreate(data))}
|
||||
isLoading={isCreating}
|
||||
>
|
||||
{t('common:common.Confirm Create')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
isDisabled={apiData.pathData.length === 0}
|
||||
isLoading={isUpdating}
|
||||
onClick={handleSubmit((data) => updatePlugins(data))}
|
||||
>
|
||||
{t('common:common.Confirm Update')}
|
||||
</Button>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
<File
|
||||
onSelect={(e) =>
|
||||
onSelectImage(e, {
|
||||
maxH: 300,
|
||||
maxW: 300,
|
||||
callback: (e) => setValue('avatar', e)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HttpPluginEditModal;
|
||||
@@ -0,0 +1,178 @@
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { Box, Button, Flex, Input, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { appTypeMap } from '@/pageComponents/app/constants';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { useMemo } from 'react';
|
||||
import { getAppType } from '@fastgpt/global/core/app/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppListContext } from './context';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { postCreateApp } from '@/web/core/app/api';
|
||||
import { useRouter } from 'next/router';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import ImportAppConfigEditor from '@/pageComponents/app/ImportAppConfigEditor';
|
||||
|
||||
type FormType = {
|
||||
avatar: string;
|
||||
name: string;
|
||||
workflowStr: string;
|
||||
};
|
||||
|
||||
const JsonImportModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
|
||||
const router = useRouter();
|
||||
|
||||
const { register, setValue, watch, handleSubmit } = useForm<FormType>({
|
||||
defaultValues: {
|
||||
avatar: '',
|
||||
name: '',
|
||||
workflowStr: ''
|
||||
}
|
||||
});
|
||||
const workflowStr = watch('workflowStr');
|
||||
|
||||
const avatar = watch('avatar');
|
||||
const {
|
||||
File,
|
||||
onOpen: onOpenSelectFile,
|
||||
onSelectImage
|
||||
} = useSelectFile({
|
||||
fileType: '.jpg,.png',
|
||||
multiple: false
|
||||
});
|
||||
// If the user does not select an avatar, it will follow the type to change
|
||||
const selectedAvatar = useMemo(() => {
|
||||
if (avatar) return avatar;
|
||||
|
||||
const defaultVal = appTypeMap[AppTypeEnum.simple].avatar;
|
||||
if (!workflowStr) return defaultVal;
|
||||
|
||||
try {
|
||||
const workflow = JSON.parse(workflowStr);
|
||||
const type = getAppType(workflow);
|
||||
if (type) return appTypeMap[type].avatar;
|
||||
return defaultVal;
|
||||
} catch (err) {
|
||||
return defaultVal;
|
||||
}
|
||||
}, [avatar, workflowStr]);
|
||||
|
||||
const { runAsync: onSubmit, loading: isCreating } = useRequest2(
|
||||
async ({ name, workflowStr }: FormType) => {
|
||||
const { workflow, appType } = await (async () => {
|
||||
try {
|
||||
const workflow = JSON.parse(workflowStr);
|
||||
const appType = getAppType(workflow);
|
||||
|
||||
if (!appType) {
|
||||
return Promise.reject(t('app:type_not_recognized'));
|
||||
}
|
||||
|
||||
if (appType === AppTypeEnum.simple) {
|
||||
return {
|
||||
workflow: form2AppWorkflow(workflow, t),
|
||||
appType
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
workflow,
|
||||
appType
|
||||
};
|
||||
} catch (err) {
|
||||
return Promise.reject(t('app:invalid_json_format'));
|
||||
}
|
||||
})();
|
||||
|
||||
return postCreateApp({
|
||||
parentId,
|
||||
avatar: selectedAvatar,
|
||||
name,
|
||||
type: appType,
|
||||
modules: workflow.nodes,
|
||||
edges: workflow.edges,
|
||||
chatConfig: workflow.chatConfig
|
||||
});
|
||||
},
|
||||
{
|
||||
refreshDeps: [selectedAvatar],
|
||||
onSuccess(id: string) {
|
||||
router.push(`/app/detail?appId=${id}`);
|
||||
loadMyApps();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common:common.Create Success')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyModal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
isLoading={isCreating}
|
||||
title={t('app:type.Import from json')}
|
||||
iconSrc="common/importLight"
|
||||
iconColor={'primary.600'}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box color={'myGray.800'} fontWeight={'bold'}>
|
||||
{t('common:common.Set Name')}
|
||||
</Box>
|
||||
<Flex mt={2} alignItems={'center'}>
|
||||
<MyTooltip label={t('common:common.Set Avatar')}>
|
||||
<Avatar
|
||||
flexShrink={0}
|
||||
src={selectedAvatar}
|
||||
w={['1.75rem', '2.25rem']}
|
||||
h={['1.75rem', '2.25rem']}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={onOpenSelectFile}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Input
|
||||
flex={1}
|
||||
ml={3}
|
||||
autoFocus
|
||||
bg={'myWhite.600'}
|
||||
{...register('name', {
|
||||
required: t('common:core.app.error.App name can not be empty')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Box mt={5}>
|
||||
<ImportAppConfigEditor
|
||||
value={workflowStr}
|
||||
onChange={(e) => setValue('workflowStr', e)}
|
||||
rows={10}
|
||||
/>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter gap={4}>
|
||||
<Button variant={'whiteBase'} onClick={onClose}>
|
||||
{t('common:common.Cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleSubmit(onSubmit)}>{t('common:common.Confirm')}</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
<File
|
||||
onSelect={(e) =>
|
||||
onSelectImage(e, {
|
||||
maxH: 300,
|
||||
maxW: 300,
|
||||
callback: (e) => setValue('avatar', e)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default JsonImportModal;
|
||||
468
projects/app/src/pageComponents/dashboard/apps/List.tsx
Normal file
468
projects/app/src/pageComponents/dashboard/apps/List.tsx
Normal file
@@ -0,0 +1,468 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Box, Grid, Flex, IconButton, HStack } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { delAppById, putAppById, resumeInheritPer, changeOwner } from '@/web/core/app/api';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import PermissionIconText from '@/components/support/permission/IconText';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppListContext } from './context';
|
||||
import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { useFolderDrag } from '@/components/common/folder/useFolderDrag';
|
||||
import dynamic from 'next/dynamic';
|
||||
import type { EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal';
|
||||
import MyMenu, { MenuItemType } from '@fastgpt/web/components/common/MyMenu';
|
||||
import { AppPermissionList } from '@fastgpt/global/support/permission/app/constant';
|
||||
import {
|
||||
deleteAppCollaborators,
|
||||
getCollaboratorList,
|
||||
postUpdateAppCollaborators
|
||||
} from '@/web/core/app/api/collaborator';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import AppTypeTag from './TypeTag';
|
||||
|
||||
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
|
||||
const ConfigPerModal = dynamic(() => import('@/components/support/permission/ConfigPerModal'));
|
||||
|
||||
import type { EditHttpPluginProps } from './HttpPluginEditModal';
|
||||
import { postCopyApp } from '@/web/core/app/api/app';
|
||||
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
|
||||
import UserBox from '@fastgpt/web/components/common/UserBox';
|
||||
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
|
||||
const HttpEditModal = dynamic(() => import('./HttpPluginEditModal'));
|
||||
|
||||
const ListItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { parentId = null } = router.query;
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const { openConfirm: openMoveConfirm, ConfirmModal: MoveConfirmModal } = useConfirm({
|
||||
type: 'common',
|
||||
title: t('common:move.confirm'),
|
||||
content: t('app:move.hint')
|
||||
});
|
||||
|
||||
const { myApps, loadMyApps, onUpdateApp, setMoveAppId, folderDetail } = useContextSelector(
|
||||
AppListContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
const [editedApp, setEditedApp] = useState<EditResourceInfoFormType>();
|
||||
const [editHttpPlugin, setEditHttpPlugin] = useState<EditHttpPluginProps>();
|
||||
const [editPerAppIndex, setEditPerAppIndex] = useState<number>();
|
||||
|
||||
const editPerApp = useMemo(
|
||||
() => (editPerAppIndex !== undefined ? myApps[editPerAppIndex] : undefined),
|
||||
[editPerAppIndex, myApps]
|
||||
);
|
||||
|
||||
const parentApp = useMemo(() => myApps.find((item) => item._id === parentId), [parentId, myApps]);
|
||||
|
||||
const { runAsync: onPutAppById } = useRequest2(putAppById, {
|
||||
onSuccess() {
|
||||
loadMyApps();
|
||||
}
|
||||
});
|
||||
|
||||
const { getBoxProps } = useFolderDrag({
|
||||
activeStyles: {
|
||||
borderColor: 'primary.600'
|
||||
},
|
||||
onDrop: (dragId: string, targetId: string) => {
|
||||
openMoveConfirm(async () => onPutAppById(dragId, { parentId: targetId }))();
|
||||
}
|
||||
});
|
||||
|
||||
const { openConfirm: openConfirmDel, ConfirmModal: DelConfirmModal } = useConfirm({
|
||||
type: 'delete'
|
||||
});
|
||||
|
||||
const { lastChatAppId, setLastChatAppId } = useChatStore();
|
||||
const { runAsync: onclickDelApp } = useRequest2(
|
||||
(id: string) => {
|
||||
if (id === lastChatAppId) {
|
||||
setLastChatAppId('');
|
||||
}
|
||||
return delAppById(id);
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
loadMyApps();
|
||||
},
|
||||
successToast: t('common:common.Delete Success'),
|
||||
errorToast: t('common:common.Delete Failed')
|
||||
}
|
||||
);
|
||||
|
||||
const { openConfirm: openConfirmCopy, ConfirmModal: ConfirmCopyModal } = useConfirm({
|
||||
content: t('app:confirm_copy_app_tip')
|
||||
});
|
||||
const { runAsync: onclickCopy } = useRequest2(postCopyApp, {
|
||||
onSuccess({ appId }) {
|
||||
router.push(`/app/detail?appId=${appId}`);
|
||||
loadMyApps();
|
||||
},
|
||||
successToast: t('app:create_copy_success')
|
||||
});
|
||||
|
||||
const { runAsync: onResumeInheritPermission } = useRequest2(
|
||||
() => {
|
||||
return resumeInheritPer(editPerApp!._id);
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
errorToast: t('common:permission.Resume InheritPermission Failed'),
|
||||
onSuccess() {
|
||||
loadMyApps();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
py={4}
|
||||
gridTemplateColumns={
|
||||
folderDetail
|
||||
? ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']
|
||||
: ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']
|
||||
}
|
||||
gridGap={5}
|
||||
alignItems={'stretch'}
|
||||
>
|
||||
{myApps.map((app, index) => {
|
||||
return (
|
||||
<MyTooltip
|
||||
key={app._id}
|
||||
h="100%"
|
||||
label={
|
||||
app.type === AppTypeEnum.folder
|
||||
? t('common:common.folder.Open folder')
|
||||
: app.permission.hasWritePer
|
||||
? t('app:edit_app')
|
||||
: t('app:go_to_chat')
|
||||
}
|
||||
>
|
||||
<MyBox
|
||||
lineHeight={1.5}
|
||||
h="100%"
|
||||
pt={5}
|
||||
pb={3}
|
||||
px={5}
|
||||
cursor={'pointer'}
|
||||
border={'base'}
|
||||
boxShadow={'2'}
|
||||
bg={'white'}
|
||||
borderRadius={'lg'}
|
||||
position={'relative'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
_hover={{
|
||||
borderColor: 'primary.300',
|
||||
boxShadow: '1.5',
|
||||
'& .more': {
|
||||
display: 'flex'
|
||||
},
|
||||
'& .time': {
|
||||
display: ['flex', 'none']
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (AppFolderTypeList.includes(app.type)) {
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
parentId: app._id
|
||||
}
|
||||
});
|
||||
} else if (app.permission.hasWritePer) {
|
||||
router.push(`/app/detail?appId=${app._id}`);
|
||||
} else {
|
||||
router.push(`/chat?appId=${app._id}`);
|
||||
}
|
||||
}}
|
||||
{...getBoxProps({
|
||||
dataId: app._id,
|
||||
isFolder: app.type === AppTypeEnum.folder
|
||||
})}
|
||||
>
|
||||
<HStack>
|
||||
<Avatar src={app.avatar} borderRadius={'sm'} w={'1.5rem'} />
|
||||
<Box flex={'1 0 0'} color={'myGray.900'}>
|
||||
{app.name}
|
||||
</Box>
|
||||
<Box mr={'-1.25rem'}>
|
||||
<AppTypeTag type={app.type} />
|
||||
</Box>
|
||||
</HStack>
|
||||
<Box
|
||||
flex={['1 0 60px', '1 0 72px']}
|
||||
mt={3}
|
||||
pr={8}
|
||||
textAlign={'justify'}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'xs'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
<Box className={'textEllipsis2'} whiteSpace={'pre-wrap'}>
|
||||
{app.intro || t('common:common.no_intro')}
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex
|
||||
h={'24px'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
fontSize={'mini'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
<HStack spacing={3.5}>
|
||||
<UserBox
|
||||
sourceMember={app.sourceMember}
|
||||
fontSize="xs"
|
||||
avatarSize="1rem"
|
||||
spacing={0.5}
|
||||
/>
|
||||
<PermissionIconText
|
||||
private={app.private}
|
||||
color={'myGray.500'}
|
||||
iconColor={'myGray.400'}
|
||||
w={'0.875rem'}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
<HStack>
|
||||
{isPc && (
|
||||
<HStack spacing={0.5} className="time">
|
||||
<MyIcon name={'history'} w={'0.85rem'} color={'myGray.400'} />
|
||||
<Box color={'myGray.500'}>
|
||||
{t(formatTimeToChatTime(app.updateTime) as any).replace('#', ':')}
|
||||
</Box>
|
||||
</HStack>
|
||||
)}
|
||||
{(AppFolderTypeList.includes(app.type)
|
||||
? app.permission.hasManagePer
|
||||
: app.permission.hasWritePer) && (
|
||||
<Box className="more" display={['', 'none']}>
|
||||
<MyMenu
|
||||
size={'xs'}
|
||||
Button={
|
||||
<IconButton
|
||||
size={'xsSquare'}
|
||||
variant={'transparentBase'}
|
||||
icon={<MyIcon name={'more'} w={'0.875rem'} color={'myGray.500'} />}
|
||||
aria-label={''}
|
||||
/>
|
||||
}
|
||||
menuList={[
|
||||
...([AppTypeEnum.simple, AppTypeEnum.workflow].includes(app.type)
|
||||
? [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'core/chat/chatLight',
|
||||
type: 'grayBg' as MenuItemType,
|
||||
label: t('app:go_to_chat'),
|
||||
onClick: () => {
|
||||
router.push(`/chat?appId=${app._id}`);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...([AppTypeEnum.plugin].includes(app.type)
|
||||
? [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'core/chat/chatLight',
|
||||
type: 'grayBg' as MenuItemType,
|
||||
label: t('app:go_to_run'),
|
||||
onClick: () => {
|
||||
router.push(`/chat?appId=${app._id}`);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(app.permission.hasManagePer
|
||||
? [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'edit',
|
||||
type: 'grayBg' as MenuItemType,
|
||||
label: t('common:dataset.Edit Info'),
|
||||
onClick: () => {
|
||||
if (app.type === AppTypeEnum.httpPlugin) {
|
||||
setEditHttpPlugin({
|
||||
id: app._id,
|
||||
name: app.name,
|
||||
avatar: app.avatar,
|
||||
intro: app.intro,
|
||||
pluginData: app.pluginData
|
||||
});
|
||||
} else {
|
||||
setEditedApp({
|
||||
id: app._id,
|
||||
avatar: app.avatar,
|
||||
name: app.name,
|
||||
intro: app.intro
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
...(folderDetail?.type === AppTypeEnum.httpPlugin &&
|
||||
!(parentApp ? parentApp.permission : app.permission)
|
||||
.hasManagePer
|
||||
? []
|
||||
: [
|
||||
{
|
||||
icon: 'common/file/move',
|
||||
type: 'grayBg' as MenuItemType,
|
||||
label: t('common:common.folder.Move to'),
|
||||
onClick: () => setMoveAppId(app._id)
|
||||
}
|
||||
]),
|
||||
...(app.permission.hasManagePer
|
||||
? [
|
||||
{
|
||||
icon: 'key',
|
||||
type: 'grayBg' as MenuItemType,
|
||||
label: t('common:permission.Permission'),
|
||||
onClick: () => setEditPerAppIndex(index)
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(app.type === AppTypeEnum.toolSet ||
|
||||
app.type === AppTypeEnum.folder ||
|
||||
app.type === AppTypeEnum.httpPlugin
|
||||
? []
|
||||
: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'copy',
|
||||
type: 'grayBg' as MenuItemType,
|
||||
label: t('app:copy_one_app'),
|
||||
onClick: () =>
|
||||
openConfirmCopy(() => onclickCopy({ appId: app._id }))()
|
||||
}
|
||||
]
|
||||
}
|
||||
]),
|
||||
...(app.permission.isOwner
|
||||
? [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
type: 'danger' as 'danger',
|
||||
icon: 'delete',
|
||||
label: t('common:common.Delete'),
|
||||
onClick: () =>
|
||||
openConfirmDel(
|
||||
() => onclickDelApp(app._id),
|
||||
undefined,
|
||||
app.type === AppTypeEnum.folder
|
||||
? t('app:confirm_delete_folder_tip')
|
||||
: t('app:confirm_del_app_tip', { name: app.name })
|
||||
)()
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</HStack>
|
||||
</Flex>
|
||||
</MyBox>
|
||||
</MyTooltip>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
{myApps.length === 0 && <EmptyTip text={t('common:core.app.no_app')} pt={'30vh'} />}
|
||||
<DelConfirmModal />
|
||||
<ConfirmCopyModal />
|
||||
{!!editedApp && (
|
||||
<EditResourceModal
|
||||
{...editedApp}
|
||||
title={t('common:core.app.edit_content')}
|
||||
onClose={() => {
|
||||
setEditedApp(undefined);
|
||||
}}
|
||||
onEdit={({ id, ...data }) => onUpdateApp(id, data)}
|
||||
/>
|
||||
)}
|
||||
{!!editPerApp && (
|
||||
<ConfigPerModal
|
||||
{...(editPerApp.permission.isOwner && {
|
||||
onChangeOwner: (tmbId: string) =>
|
||||
changeOwner({
|
||||
appId: editPerApp._id,
|
||||
ownerId: tmbId
|
||||
}).then(() => loadMyApps())
|
||||
})}
|
||||
refetchResource={loadMyApps}
|
||||
hasParent={Boolean(parentId)}
|
||||
resumeInheritPermission={onResumeInheritPermission}
|
||||
isInheritPermission={editPerApp.inheritPermission}
|
||||
avatar={editPerApp.avatar}
|
||||
name={editPerApp.name}
|
||||
managePer={{
|
||||
permission: editPerApp.permission,
|
||||
onGetCollaboratorList: () => getCollaboratorList(editPerApp._id),
|
||||
permissionList: AppPermissionList,
|
||||
onUpdateCollaborators: (props: {
|
||||
members?: string[];
|
||||
groups?: string[];
|
||||
orgs?: string[];
|
||||
permission: PermissionValueType;
|
||||
}) =>
|
||||
postUpdateAppCollaborators({
|
||||
...props,
|
||||
appId: editPerApp._id
|
||||
}),
|
||||
onDelOneCollaborator: async (
|
||||
props: RequireOnlyOne<{
|
||||
tmbId?: string;
|
||||
groupId?: string;
|
||||
orgId?: string;
|
||||
}>
|
||||
) =>
|
||||
deleteAppCollaborators({
|
||||
...props,
|
||||
appId: editPerApp._id
|
||||
}),
|
||||
refreshDeps: [editPerApp.inheritPermission]
|
||||
}}
|
||||
onClose={() => setEditPerAppIndex(undefined)}
|
||||
/>
|
||||
)}
|
||||
{!!editHttpPlugin && (
|
||||
<HttpEditModal
|
||||
defaultPlugin={editHttpPlugin}
|
||||
onClose={() => setEditHttpPlugin(undefined)}
|
||||
/>
|
||||
)}
|
||||
<MoveConfirmModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ListItem;
|
||||
@@ -0,0 +1,253 @@
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { getMCPTools, postCreateMCPTools } from '@/web/core/app/api/plugin';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Flex,
|
||||
Input,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr
|
||||
} from '@chakra-ui/react';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AppListContext } from './context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ToolType } from '@fastgpt/global/core/app/type';
|
||||
import { getMCPToolsBody } from '@/pages/api/core/app/mcpTools/getMCPTools';
|
||||
|
||||
export type MCPToolSetData = {
|
||||
url: string;
|
||||
toolList: ToolType[];
|
||||
};
|
||||
|
||||
export type EditMCPToolsProps = {
|
||||
avatar: string;
|
||||
name: string;
|
||||
mcpData: MCPToolSetData;
|
||||
};
|
||||
|
||||
const MCPToolsEditModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
|
||||
|
||||
const { register, setValue, handleSubmit, watch } = useForm<EditMCPToolsProps>({
|
||||
defaultValues: {
|
||||
avatar: 'core/app/type/mcpToolsFill',
|
||||
name: '',
|
||||
mcpData: {
|
||||
url: '',
|
||||
toolList: []
|
||||
}
|
||||
}
|
||||
});
|
||||
const avatar = watch('avatar');
|
||||
const mcpData = watch('mcpData');
|
||||
|
||||
const { runAsync: onCreate, loading: isCreating } = useRequest2(
|
||||
async (data: EditMCPToolsProps) => {
|
||||
return postCreateMCPTools({
|
||||
name: data.name,
|
||||
avatar: data.avatar,
|
||||
toolList: data.mcpData.toolList,
|
||||
url: data.mcpData.url,
|
||||
parentId
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
onClose();
|
||||
loadMyApps();
|
||||
},
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed')
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: runGetMCPTools, loading: isGettingTools } = useRequest2(
|
||||
(data: getMCPToolsBody) => getMCPTools(data),
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
setValue('mcpData.toolList', res);
|
||||
},
|
||||
errorToast: t('app:MCP_tools_parse_failed')
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
File,
|
||||
onOpen: onOpenSelectFile,
|
||||
onSelectImage
|
||||
} = useSelectFile({
|
||||
fileType: 'image/*',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
iconSrc="core/app/type/mcpToolsFill"
|
||||
title={t('app:type.MCP tools')}
|
||||
w={['90vw', '530px']}
|
||||
position={'relative'}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box color={'myGray.900'} fontSize={'14px'} fontWeight={'medium'}>
|
||||
{t('common:common.Set Name')}
|
||||
</Box>
|
||||
<Flex mt={2} alignItems={'center'}>
|
||||
<MyTooltip label={t('common:common.Set Avatar')}>
|
||||
<Avatar
|
||||
flexShrink={0}
|
||||
src={avatar}
|
||||
w={['28px', '32px']}
|
||||
h={['28px', '32px']}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={onOpenSelectFile}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Input
|
||||
flex={1}
|
||||
ml={4}
|
||||
bg={'myWhite.600'}
|
||||
{...register('name', {
|
||||
required: t('common:common.name_is_empty')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Box color={'myGray.900'} fontSize={'14px'} fontWeight={'medium'} mt={6}>
|
||||
{t('app:MCP_tools_url')}
|
||||
</Box>
|
||||
<Flex alignItems={'center'} gap={2} mt={2}>
|
||||
<Input
|
||||
h={8}
|
||||
placeholder={t('app:MCP_tools_url_placeholder')}
|
||||
{...register('mcpData.url', {
|
||||
required: t('app:MCP_tools_url_is_empty')
|
||||
})}
|
||||
/>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'whitePrimary'}
|
||||
h={8}
|
||||
isLoading={isGettingTools}
|
||||
onClick={() => {
|
||||
runGetMCPTools({ url: mcpData.url });
|
||||
}}
|
||||
>
|
||||
{t('common:common.Parse')}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Box color={'myGray.900'} fontSize={'14px'} fontWeight={'medium'} mt={6}>
|
||||
{t('app:MCP_tools_list')}
|
||||
</Box>
|
||||
<Box
|
||||
mt={2}
|
||||
borderRadius={'md'}
|
||||
overflow={'hidden'}
|
||||
borderWidth={'1px'}
|
||||
position={'relative'}
|
||||
>
|
||||
<TableContainer maxH={360} minH={200} overflowY={'auto'}>
|
||||
<Table bg={'white'}>
|
||||
<Thead bg={'myGray.50'}>
|
||||
<Th fontSize={'mini'} py={0} h={'34px'}>
|
||||
{t('common:Name')}
|
||||
</Th>
|
||||
<Th fontSize={'mini'} py={0} h={'34px'}>
|
||||
{t('common:plugin.Description')}
|
||||
</Th>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{mcpData.toolList.map((item) => (
|
||||
<Tr key={item.name} height={'28px'}>
|
||||
<Td
|
||||
fontSize={'mini'}
|
||||
color={'myGray.900'}
|
||||
fontWeight={'medium'}
|
||||
py={2}
|
||||
maxW={1 / 2}
|
||||
overflow={'hidden'}
|
||||
textOverflow={'ellipsis'}
|
||||
whiteSpace={'nowrap'}
|
||||
>
|
||||
{item.name}
|
||||
</Td>
|
||||
<Td
|
||||
fontSize={'mini'}
|
||||
color={'myGray.900'}
|
||||
fontWeight={'medium'}
|
||||
py={2}
|
||||
maxW={1 / 2}
|
||||
overflow={'hidden'}
|
||||
textOverflow={'ellipsis'}
|
||||
whiteSpace={'nowrap'}
|
||||
>
|
||||
{item.description}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{mcpData.toolList.length === 0 && (
|
||||
<Center
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
fontSize={'mini'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
{t('app:no_mcp_tools_list')}
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter gap={2}>
|
||||
<Button variant={'whitePrimary'} onClick={onClose}>
|
||||
{t('common:common.Close')}
|
||||
</Button>
|
||||
<Button
|
||||
isDisabled={mcpData.toolList.length === 0}
|
||||
isLoading={isCreating}
|
||||
onClick={handleSubmit(onCreate)}
|
||||
>
|
||||
{t('common:common.Confirm Create')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
<File
|
||||
onSelect={(e) =>
|
||||
onSelectImage(e, {
|
||||
maxH: 300,
|
||||
maxW: 300,
|
||||
callback: (e) => setValue('avatar', e)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MCPToolsEditModal;
|
||||
66
projects/app/src/pageComponents/dashboard/apps/TypeTag.tsx
Normal file
66
projects/app/src/pageComponents/dashboard/apps/TypeTag.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
const AppTypeTag = ({ type }: { type: AppTypeEnum }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const map = useRef({
|
||||
[AppTypeEnum.simple]: {
|
||||
label: t('app:type.Simple bot'),
|
||||
icon: 'core/app/type/simple',
|
||||
bg: '#DBF3FF',
|
||||
color: '#0884DD'
|
||||
},
|
||||
[AppTypeEnum.workflow]: {
|
||||
label: t('app:type.Workflow bot'),
|
||||
icon: 'core/app/type/workflow',
|
||||
bg: '#E4E1FC',
|
||||
color: '#6F5DD7'
|
||||
},
|
||||
[AppTypeEnum.plugin]: {
|
||||
label: t('app:type.Plugin'),
|
||||
icon: 'core/app/type/plugin',
|
||||
bg: '#D0F5EE',
|
||||
color: '#007E7C'
|
||||
},
|
||||
[AppTypeEnum.httpPlugin]: {
|
||||
label: t('app:type.Http plugin'),
|
||||
icon: 'core/app/type/httpPlugin',
|
||||
bg: '#FFE4EE',
|
||||
color: '#E82F72'
|
||||
},
|
||||
[AppTypeEnum.toolSet]: {
|
||||
label: t('app:type.MCP tools'),
|
||||
icon: 'core/app/type/mcpTools',
|
||||
bg: '',
|
||||
color: ''
|
||||
},
|
||||
[AppTypeEnum.tool]: undefined,
|
||||
[AppTypeEnum.folder]: undefined
|
||||
});
|
||||
|
||||
const data = map.current[type];
|
||||
|
||||
return data ? (
|
||||
<Flex
|
||||
bg={'myGray.100'}
|
||||
color={'myGray.600'}
|
||||
py={0.5}
|
||||
pl={2}
|
||||
pr={3}
|
||||
borderLeftRadius={'sm'}
|
||||
whiteSpace={'nowrap'}
|
||||
>
|
||||
<MyIcon name={data.icon as any} w={'0.8rem'} color={'myGray.500'} />
|
||||
<Box ml={1} fontSize={'mini'}>
|
||||
{data.label}
|
||||
</Box>
|
||||
</Flex>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default AppTypeTag;
|
||||
177
projects/app/src/pageComponents/dashboard/apps/context.tsx
Normal file
177
projects/app/src/pageComponents/dashboard/apps/context.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import React, { ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getAppDetailById, getMyApps, putAppById } from '@/web/core/app/api';
|
||||
import { AppDetailType, AppListItemType } from '@fastgpt/global/core/app/type';
|
||||
import { getAppFolderPath } from '@/web/core/app/api/app';
|
||||
import {
|
||||
GetResourceFolderListProps,
|
||||
ParentIdType,
|
||||
ParentTreePathItemType
|
||||
} from '@fastgpt/global/common/parentFolder/type';
|
||||
import { AppUpdateParams } from '@/global/core/app/api';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal'));
|
||||
|
||||
type AppListContextType = {
|
||||
parentId?: string | null;
|
||||
appType: AppTypeEnum | 'all';
|
||||
myApps: AppListItemType[];
|
||||
loadMyApps: () => Promise<AppListItemType[]>;
|
||||
isFetchingApps: boolean;
|
||||
folderDetail: AppDetailType | undefined | null;
|
||||
paths: ParentTreePathItemType[];
|
||||
onUpdateApp: (id: string, data: AppUpdateParams) => Promise<any>;
|
||||
setMoveAppId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
refetchFolderDetail: () => Promise<AppDetailType | null>;
|
||||
searchKey: string;
|
||||
setSearchKey: React.Dispatch<React.SetStateAction<string>>;
|
||||
};
|
||||
|
||||
export const AppListContext = createContext<AppListContextType>({
|
||||
parentId: undefined,
|
||||
myApps: [],
|
||||
loadMyApps: async function (): Promise<AppListItemType[]> {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
isFetchingApps: false,
|
||||
folderDetail: undefined,
|
||||
paths: [],
|
||||
onUpdateApp: function (id: string, data: AppUpdateParams): Promise<any> {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
setMoveAppId: function (value: React.SetStateAction<string | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
appType: 'all',
|
||||
refetchFolderDetail: async function (): Promise<AppDetailType | null> {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
searchKey: '',
|
||||
setSearchKey: function (value: React.SetStateAction<string>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
});
|
||||
|
||||
const AppListContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { parentId = null, type = 'all' } = router.query as {
|
||||
parentId?: string | null;
|
||||
type: AppTypeEnum;
|
||||
};
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
|
||||
const {
|
||||
data = [],
|
||||
runAsync: loadMyApps,
|
||||
loading: isFetchingApps
|
||||
} = useRequest2(
|
||||
() => {
|
||||
const formatType = (() => {
|
||||
if (!type || type === 'all') return undefined;
|
||||
if (type === AppTypeEnum.plugin)
|
||||
return [AppTypeEnum.folder, AppTypeEnum.plugin, AppTypeEnum.httpPlugin];
|
||||
|
||||
return [AppTypeEnum.folder, type];
|
||||
})();
|
||||
return getMyApps({ parentId, type: formatType, searchKey });
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [searchKey, parentId, type],
|
||||
throttleWait: 500,
|
||||
refreshOnWindowFocus: true
|
||||
}
|
||||
);
|
||||
|
||||
const { data: paths = [], runAsync: refetchPaths } = useRequest2(
|
||||
() => getAppFolderPath({ sourceId: parentId, type: 'current' }),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [parentId]
|
||||
}
|
||||
);
|
||||
|
||||
const { data: folderDetail, runAsync: refetchFolderDetail } = useRequest2(
|
||||
() => {
|
||||
if (parentId) return getAppDetailById(parentId);
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [parentId]
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onUpdateApp } = useRequest2((id: string, data: AppUpdateParams) =>
|
||||
putAppById(id, data).then(async (res) => {
|
||||
await Promise.all([refetchFolderDetail(), refetchPaths(), loadMyApps()]);
|
||||
return res;
|
||||
})
|
||||
);
|
||||
|
||||
const [moveAppId, setMoveAppId] = useState<string>();
|
||||
const onMoveApp = useCallback(
|
||||
async (parentId: ParentIdType) => {
|
||||
if (!moveAppId) return;
|
||||
await onUpdateApp(moveAppId, { parentId });
|
||||
},
|
||||
[moveAppId, onUpdateApp]
|
||||
);
|
||||
|
||||
const getAppFolderList = useCallback(({ parentId }: GetResourceFolderListProps) => {
|
||||
return getMyApps({
|
||||
parentId,
|
||||
type: AppTypeEnum.folder
|
||||
}).then((res) =>
|
||||
res
|
||||
.filter((item) => item.permission.hasWritePer)
|
||||
.map((item) => ({
|
||||
id: item._id,
|
||||
name: item.name
|
||||
}))
|
||||
);
|
||||
}, []);
|
||||
|
||||
const { setLastAppListRouteType } = useSystemStore();
|
||||
useEffect(() => {
|
||||
setLastAppListRouteType(type);
|
||||
}, [setLastAppListRouteType, type]);
|
||||
|
||||
const contextValue: AppListContextType = {
|
||||
parentId,
|
||||
appType: type,
|
||||
myApps: data,
|
||||
loadMyApps,
|
||||
refetchFolderDetail,
|
||||
isFetchingApps,
|
||||
folderDetail,
|
||||
paths,
|
||||
onUpdateApp,
|
||||
setMoveAppId,
|
||||
searchKey,
|
||||
setSearchKey
|
||||
};
|
||||
return (
|
||||
<AppListContext.Provider value={contextValue}>
|
||||
{children}
|
||||
{!!moveAppId && (
|
||||
<MoveModal
|
||||
moveResourceId={moveAppId}
|
||||
server={getAppFolderList}
|
||||
title={t('app:move_app')}
|
||||
onClose={() => setMoveAppId(undefined)}
|
||||
onConfirm={onMoveApp}
|
||||
moveHint={t('app:move.hint')}
|
||||
/>
|
||||
)}
|
||||
</AppListContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppListContextProvider;
|
||||
414
projects/app/src/pageComponents/dashboard/mcp/EditModal.tsx
Normal file
414
projects/app/src/pageComponents/dashboard/mcp/EditModal.tsx
Normal file
@@ -0,0 +1,414 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Flex,
|
||||
Grid,
|
||||
HStack,
|
||||
Input,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { McpAppType } from '@fastgpt/global/support/mcp/type';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import Path from '@/components/common/folder/Path';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getAppBasicInfoByIds, getMyApps } from '@/web/core/app/api';
|
||||
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { getAppFolderPath } from '@/web/core/app/api/app';
|
||||
import { AppFolderTypeList } from '@fastgpt/global/core/app/constants';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { postCreateMcpServer, putUpdateMcpServer } from '../../../web/support/mcp/api';
|
||||
|
||||
export type EditMcForm = {
|
||||
id?: string;
|
||||
name: string;
|
||||
apps: McpAppType[];
|
||||
};
|
||||
|
||||
export const defaultForm: EditMcForm = {
|
||||
name: '',
|
||||
apps: []
|
||||
};
|
||||
|
||||
const SelectAppModal = ({
|
||||
selectedApps,
|
||||
onClose,
|
||||
onConfirm
|
||||
}: {
|
||||
selectedApps: McpAppType[];
|
||||
onClose: () => void;
|
||||
onConfirm: (e: McpAppType[]) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [selectedList, setSelectedList] = useState<
|
||||
{
|
||||
appId: string;
|
||||
toolName: string;
|
||||
avatar: string;
|
||||
description: string;
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
// Load selected app
|
||||
useRequest2(() => getAppBasicInfoByIds(selectedApps.map((item) => item.appId)), {
|
||||
manual: false,
|
||||
onSuccess: (data) => {
|
||||
setSelectedList(
|
||||
data.map((item) => ({
|
||||
appId: item.id,
|
||||
toolName: item.name,
|
||||
avatar: item.avatar,
|
||||
description: selectedApps.find((app) => app.appId === item.id)?.description || ''
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Load all apps
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const [parentId, setParentId] = useState<ParentIdType>('');
|
||||
|
||||
const { data: apps = [], loading: loadingApps } = useRequest2(
|
||||
() =>
|
||||
getMyApps({
|
||||
searchKey,
|
||||
parentId
|
||||
}),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [searchKey, parentId],
|
||||
throttleWait: 200
|
||||
}
|
||||
);
|
||||
const { data: paths = [] } = useRequest2(
|
||||
() => getAppFolderPath({ sourceId: parentId, type: 'current' }),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [parentId]
|
||||
}
|
||||
);
|
||||
|
||||
const isLoading = loadingApps;
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
iconSrc={'modal/AddClb'}
|
||||
title={t('dashboard_mcp:select_app')}
|
||||
minW="800px"
|
||||
maxW={'60vw'}
|
||||
h={'100%'}
|
||||
maxH={'90vh'}
|
||||
isCentered
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<ModalBody flex={'1'}>
|
||||
<Grid
|
||||
border="1px solid"
|
||||
borderColor="myGray.200"
|
||||
borderRadius="0.5rem"
|
||||
gridTemplateColumns="1fr 1fr"
|
||||
h={'100%'}
|
||||
>
|
||||
<Flex
|
||||
h={'100%'}
|
||||
flexDirection="column"
|
||||
borderRight="1px solid"
|
||||
borderColor="myGray.200"
|
||||
p="4"
|
||||
>
|
||||
<SearchInput
|
||||
placeholder={t('dashboard_mcp:search_app')}
|
||||
bgColor="myGray.50"
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
|
||||
{paths.length > 0 && !searchKey && (
|
||||
<Box mt={3}>
|
||||
<Path paths={paths} hoverStyle={{ bg: 'myGray.200' }} onClick={setParentId} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box mt="3" overflow={'auto'} flex={'1 0 0'} h={0}>
|
||||
{apps.map((item) => {
|
||||
const selected = selectedList.some((app) => app.appId === item._id);
|
||||
const isFolder = AppFolderTypeList.includes(item.type);
|
||||
|
||||
return (
|
||||
<HStack
|
||||
key={item._id}
|
||||
py={2}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'myGray.100'
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isFolder) {
|
||||
setParentId(item._id);
|
||||
} else if (selected) {
|
||||
setSelectedList((state) => state.filter((app) => app.appId !== item._id));
|
||||
} else {
|
||||
setSelectedList((state) => [
|
||||
...state,
|
||||
{
|
||||
appId: item._id,
|
||||
toolName: item.name,
|
||||
avatar: item.avatar,
|
||||
description: item.intro
|
||||
}
|
||||
]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} w={'1.25rem'}>
|
||||
{!isFolder && <Checkbox isChecked={selected} />}
|
||||
</Flex>
|
||||
<Avatar src={item.avatar} w="1.5rem" borderRadius={'sm'} />
|
||||
<Box>{item.name}</Box>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Flex h={'100%'} p="4" flexDirection="column">
|
||||
<Box>
|
||||
{`${t('dashboard_mcp:has_chosen')}: `}
|
||||
{selectedList.length}
|
||||
</Box>
|
||||
<Flex flexDirection="column" mt="2" gap={1} overflow={'auto'} flex={'1 0 0'} h={0}>
|
||||
{selectedList.map((item) => {
|
||||
return (
|
||||
<HStack
|
||||
key={item.appId}
|
||||
py={2}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'myGray.100'
|
||||
}}
|
||||
>
|
||||
<Avatar src={item.avatar} w="1.5rem" borderRadius={'sm'} />
|
||||
<Box ml="2" flex={'1 0 0'}>
|
||||
{item.toolName}
|
||||
</Box>
|
||||
<MyIcon
|
||||
name="common/closeLight"
|
||||
w="1rem"
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'red.600'
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedList((state) => state.filter((app) => app.appId !== item.appId));
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button ml="4" h={'32px'} onClick={() => onConfirm(selectedList)}>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
const EditMcpModal = ({
|
||||
editMcp,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
editMcp: EditMcForm;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const isEdit = !!editMcp.id;
|
||||
console.log(editMcp);
|
||||
const {
|
||||
isOpen: isOpenSelectApp,
|
||||
onOpen: onOpenSelectApp,
|
||||
onClose: onCloseSelectApp
|
||||
} = useDisclosure();
|
||||
|
||||
const { register, handleSubmit, control } = useForm({
|
||||
defaultValues: editMcp
|
||||
});
|
||||
|
||||
const {
|
||||
fields: apps,
|
||||
replace: replaceSelectedApps,
|
||||
remove
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: 'apps'
|
||||
});
|
||||
|
||||
const { runAsync: createMcp, loading: loadingCreate } = useRequest2(
|
||||
(data: EditMcForm) =>
|
||||
postCreateMcpServer({
|
||||
name: data.name,
|
||||
apps: data.apps.map((item) => ({
|
||||
appId: item.appId,
|
||||
toolName: item.toolName,
|
||||
description: item.description
|
||||
}))
|
||||
}),
|
||||
{
|
||||
manual: true,
|
||||
successToast: t('common:common.Create Success'),
|
||||
onSuccess
|
||||
}
|
||||
);
|
||||
const { runAsync: updateMcp, loading: loadingUpdate } = useRequest2(
|
||||
(data: EditMcForm) =>
|
||||
putUpdateMcpServer({
|
||||
id: data.id!,
|
||||
name: data.name,
|
||||
apps: data.apps.map((item) => ({
|
||||
appId: item.appId,
|
||||
toolName: item.toolName,
|
||||
description: item.description
|
||||
}))
|
||||
}),
|
||||
{
|
||||
manual: true,
|
||||
successToast: t('common:common.Update Success'),
|
||||
onSuccess
|
||||
}
|
||||
);
|
||||
const isConfirming = loadingCreate || loadingUpdate;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyModal
|
||||
iconSrc="key"
|
||||
title={isEdit ? '编辑MCP' : '创建MCP'}
|
||||
w={'100%'}
|
||||
maxW={['90vw', '600px']}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box>
|
||||
<FormLabel required mb={0.5}>
|
||||
{t('common:common.Input name')}
|
||||
</FormLabel>
|
||||
<Input {...register('name', { required: true })} bg={'myGray.50'} />
|
||||
</Box>
|
||||
<Box mt={6}>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||
<FormLabel>{t('dashboard_mcp:apps')}</FormLabel>
|
||||
<Button variant={'whiteBase'} size={'sm'} onClick={onOpenSelectApp}>
|
||||
{t('dashboard_mcp:manage_app')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<TableContainer mt={2} position={'relative'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('dashboard_mcp:app_name')}</Th>
|
||||
<Th>{t('dashboard_mcp:app_description')}</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{apps.map((app, index) => {
|
||||
return (
|
||||
<Tr key={app.id} fontWeight={500} fontSize={'mini'} color={'myGray.900'}>
|
||||
<Td>{app.toolName}</Td>
|
||||
<Td>
|
||||
<Input
|
||||
{...register(`apps.${index}.description`, { required: true })}
|
||||
bg={'myGray.50'}
|
||||
w={'100%'}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Flex justifyContent={'flex-end'}>
|
||||
<MyIconButton
|
||||
icon="delete"
|
||||
hoverColor={'red.600'}
|
||||
onClick={() => remove(index)}
|
||||
color={'myGray.600'}
|
||||
/>
|
||||
</Flex>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
{apps.length === 0 && <EmptyTip />}
|
||||
</TableContainer>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} mr={4} onClick={onClose}>
|
||||
{t('common:common.Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isConfirming}
|
||||
variant={'primary'}
|
||||
isDisabled={apps.length === 0}
|
||||
onClick={handleSubmit((data) => {
|
||||
if (isEdit) {
|
||||
return updateMcp(data);
|
||||
}
|
||||
return createMcp(data);
|
||||
})}
|
||||
>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
|
||||
{isOpenSelectApp && (
|
||||
<SelectAppModal
|
||||
selectedApps={apps}
|
||||
onClose={onCloseSelectApp}
|
||||
onConfirm={(e) => {
|
||||
replaceSelectedApps(
|
||||
e.map((item) => ({
|
||||
appId: item.appId,
|
||||
toolName: item.toolName,
|
||||
description: item.description
|
||||
}))
|
||||
);
|
||||
onCloseSelectApp();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditMcpModal;
|
||||
62
projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx
Normal file
62
projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { McpKeyType } from '@fastgpt/global/support/mcp/type';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Flex, HStack, ModalBody } from '@chakra-ui/react';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import CopyBox from '@fastgpt/web/components/common/String/CopyBox';
|
||||
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
|
||||
|
||||
const UsageWay = ({ mcp, onClose }: { mcp: McpKeyType; onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const sseUrl = `${feConfigs?.mcpServerProxyEndpoint}/${mcp.key}/sse`;
|
||||
const jsonConfig = `{
|
||||
"mcpServers": {
|
||||
"${feConfigs?.systemTitle}-mcp-${mcp._id}": {
|
||||
"url": "${sseUrl}"
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
return (
|
||||
<MyModal isOpen title={t('dashboard_mcp:usage_way')} onClose={onClose}>
|
||||
<ModalBody>
|
||||
<Box>
|
||||
<FormLabel>{t('dashboard_mcp:mcp_endpoints')}</FormLabel>
|
||||
<HStack mt={0.5} bg={'myGray.50'} px={2} py={1} borderRadius={'md'} fontSize={'sm'}>
|
||||
<Box userSelect={'all'} flex={'1 0 0'} whiteSpace={'pre-wrap'} wordBreak={'break-all'}>
|
||||
{sseUrl}
|
||||
</Box>
|
||||
<CopyBox value={sseUrl}>
|
||||
<MyIconButton icon="copy" />
|
||||
</CopyBox>
|
||||
</HStack>
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'}>
|
||||
<Flex
|
||||
p={3}
|
||||
bg={'myWhite.500'}
|
||||
border={'base'}
|
||||
borderTopLeftRadius={'md'}
|
||||
borderTopRightRadius={'md'}
|
||||
>
|
||||
<Box flex={1}>{t('dashboard_mcp:mcp_json_config')}</Box>
|
||||
<CopyBox value={jsonConfig}>
|
||||
<MyIconButton icon="copy" />
|
||||
</CopyBox>
|
||||
</Flex>
|
||||
<Box whiteSpace={'pre-wrap'} wordBreak={'break-all'} p={3} overflowX={'auto'}>
|
||||
{jsonConfig}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(UsageWay);
|
||||
Reference in New Issue
Block a user