This commit is contained in:
Archer
2023-10-30 13:26:42 +08:00
committed by GitHub
parent 008d0af010
commit 60ee160131
216 changed files with 4429 additions and 2229 deletions

View File

@@ -0,0 +1,161 @@
import React, { useCallback } from 'react';
import { Box, Flex, IconButton, useTheme, useDisclosure } from '@chakra-ui/react';
import { PluginItemSchema } from '@fastgpt/global/core/plugin/type';
import { useRequest } from '@/web/common/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import dynamic from 'next/dynamic';
import MyIcon from '@/components/Icon';
import MyTooltip from '@/components/MyTooltip';
import { flowNode2Modules, useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
import { putUpdatePlugin } from '@/web/core/plugin/api';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleItemType } from '@fastgpt/global/core/module/type';
const ImportSettings = dynamic(() => import('@/components/core/module/Flow/ImportSettings'));
const PreviewPlugin = dynamic(() => import('./Preview'));
type Props = { plugin: PluginItemSchema; onClose: () => void };
const Header = ({ plugin, onClose }: Props) => {
const theme = useTheme();
const { t } = useTranslation();
const { copyData } = useCopyData();
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const { nodes, edges, onFixView } = useFlowProviderStore();
const [previewModules, setPreviewModules] = React.useState<ModuleItemType[]>();
const { mutate: onclickSave, isLoading } = useRequest({
mutationFn: () => {
const modules = flowNode2Modules({ nodes, edges });
// check required connect
for (let i = 0; i < modules.length; i++) {
const item = modules[i];
// update custom input connected
if (item.flowType === FlowNodeTypeEnum.pluginInput) {
item.inputs.forEach((item) => {
item.connected = true;
});
if (item.outputs.find((output) => output.targets.length === 0)) {
return Promise.reject(t('module.Plugin input must connect'));
}
}
if (item.inputs.find((input) => input.required && !input.connected)) {
return Promise.reject(`${item.name}】存在未连接的必填输入`);
}
if (item.inputs.find((input) => input.valueCheck && !input.valueCheck(input.value))) {
return Promise.reject(`${item.name}】存在为填写的必填项`);
}
}
// plugin must have input
const pluginInputModule = modules.find(
(item) => item.flowType === FlowNodeTypeEnum.pluginInput
);
if (!pluginInputModule) {
return Promise.reject(t('module.Plugin input is required'));
}
if (pluginInputModule.inputs.length < 1) {
return Promise.reject(t('module.Plugin input is not value'));
}
return putUpdatePlugin({
id: plugin._id,
modules
});
},
successToast: '保存配置成功',
errorToast: '保存配置异常'
});
return (
<>
<Flex
py={3}
px={[2, 5, 8]}
borderBottom={theme.borders.base}
alignItems={'center'}
userSelect={'none'}
>
<MyTooltip label={'返回'} offset={[10, 10]}>
<IconButton
size={'sm'}
icon={<MyIcon name={'back'} w={'14px'} />}
borderRadius={'md'}
borderColor={'myGray.300'}
variant={'base'}
aria-label={''}
onClick={() => {
onClose();
onFixView();
}}
/>
</MyTooltip>
<Box ml={[3, 6]} fontSize={['md', '2xl']} flex={1}>
{plugin.name}
</Box>
<MyTooltip label={t('app.Import Configs')}>
<IconButton
mr={[3, 6]}
icon={<MyIcon name={'importLight'} w={['14px', '16px']} />}
borderRadius={'lg'}
variant={'base'}
aria-label={'save'}
onClick={onOpenImport}
/>
</MyTooltip>
<MyTooltip label={t('app.Export Configs')}>
<IconButton
mr={[3, 6]}
icon={<MyIcon name={'export'} w={['14px', '16px']} />}
borderRadius={'lg'}
variant={'base'}
aria-label={'save'}
onClick={() =>
copyData(
JSON.stringify(flowNode2Modules({ nodes, edges }), null, 2),
t('app.Export Config Successful')
)
}
/>
</MyTooltip>
<MyTooltip label={t('module.Preview Plugin')}>
<IconButton
mr={[3, 6]}
icon={<MyIcon name={'core/module/previewLight'} w={['14px', '16px']} />}
borderRadius={'lg'}
aria-label={'save'}
variant={'base'}
onClick={() => {
setPreviewModules(flowNode2Modules({ nodes, edges }));
}}
/>
</MyTooltip>
<MyTooltip label={t('module.Save Config')}>
<IconButton
icon={<MyIcon name={'save'} w={['14px', '16px']} />}
borderRadius={'lg'}
isLoading={isLoading}
aria-label={'save'}
onClick={onclickSave}
/>
</MyTooltip>
</Flex>
{isOpenImport && <ImportSettings onClose={onCloseImport} />}
{!!previewModules && (
<PreviewPlugin
plugin={plugin}
modules={previewModules}
onClose={() => setPreviewModules(undefined)}
/>
)}
</>
);
};
export default React.memo(Header);

View File

@@ -0,0 +1,68 @@
import React, { useEffect } from 'react';
import ReactFlow, { Background, ReactFlowProvider, useNodesState } from 'reactflow';
import { FlowModuleItemType, ModuleItemType } from '@fastgpt/global/core/module/type';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import dynamic from 'next/dynamic';
import { formatPluginIOModules } from '@fastgpt/global/core/module/utils';
import MyModal from '@/components/MyModal';
import { Box } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { PluginItemSchema } from '@fastgpt/global/core/plugin/type';
import { appModule2FlowNode } from '@/utils/adapt';
const nodeTypes = {
[FlowNodeTypeEnum.pluginModule]: dynamic(
() => import('@/components/core/module/Flow/components/nodes/NodePreviewPlugin')
)
};
const PreviewPlugin = ({
plugin,
modules,
onClose
}: {
plugin: PluginItemSchema;
modules: ModuleItemType[];
onClose: () => void;
}) => {
const { t } = useTranslation();
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
useEffect(() => {
setNodes([
appModule2FlowNode({
item: {
moduleId: 'plugin',
flowType: FlowNodeTypeEnum.pluginModule,
logo: plugin.avatar,
name: plugin.name,
description: plugin.intro,
intro: plugin.intro,
...formatPluginIOModules(plugin._id, modules)
}
})
]);
}, [modules, plugin, setNodes]);
return (
<MyModal isOpen title={t('module.Preview Plugin')} onClose={onClose} isCentered>
<Box h={'400px'} w={'400px'}>
<ReactFlowProvider>
<ReactFlow
fitView
nodes={nodes}
edges={[]}
minZoom={0.1}
maxZoom={1.5}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
>
<Background />
</ReactFlow>
</ReactFlowProvider>
</Box>
</MyModal>
);
};
export default React.memo(PreviewPlugin);

View File

@@ -0,0 +1,96 @@
import React, { useMemo } from 'react';
import { useRouter } from 'next/router';
import Header from './Header';
import Flow from '@/components/core/module/Flow';
import FlowProvider, { useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
import { SystemModuleTemplateType } from '@fastgpt/global/core/module/type.d';
import { PluginModuleTemplates } from '@/constants/flow/ModuleTemplate';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useQuery } from '@tanstack/react-query';
import { getOnePlugin, getUserPlugs2ModuleTemplates } from '@/web/core/plugin/api';
import { useToast } from '@/web/common/hooks/useToast';
import Loading from '@/components/Loading';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useTranslation } from 'react-i18next';
import { usePluginStore } from '@/web/core/plugin/store/plugin';
type Props = { pluginId: string };
const Render = ({ pluginId }: Props) => {
const { t } = useTranslation();
const router = useRouter();
const { toast } = useToast();
const { nodes = [] } = useFlowProviderStore();
const { pluginModuleTemplates, loadPluginModuleTemplates } = usePluginStore();
const filterTemplates = useMemo(() => {
const copyTemplates: SystemModuleTemplateType = JSON.parse(
JSON.stringify(PluginModuleTemplates)
);
const filterType: Record<string, 1> = {
[FlowNodeTypeEnum.userGuide]: 1,
[FlowNodeTypeEnum.pluginInput]: 1,
[FlowNodeTypeEnum.pluginOutput]: 1
};
// filter some template
nodes.forEach((node) => {
if (node.type && filterType[node.type]) {
copyTemplates.forEach((item) => {
item.list.forEach((module, index) => {
if (module.flowType === node.type) {
item.list.splice(index, 1);
}
});
});
}
});
return copyTemplates;
}, [nodes]);
const { data } = useQuery(['getOnePlugin', pluginId], () => getOnePlugin(pluginId), {
onError: (error) => {
toast({
status: 'warning',
title: getErrText(error, t('plugin.Load Plugin Failed'))
});
router.replace('/plugin/list');
}
});
useQuery(['getUserPlugs2ModuleTemplates'], () => loadPluginModuleTemplates());
const filterPlugins = useMemo(
() => pluginModuleTemplates.filter((item) => item.id !== pluginId),
[pluginId, pluginModuleTemplates]
);
return data ? (
<Flow
systemTemplates={filterTemplates}
pluginTemplates={[{ label: '', list: filterPlugins }]}
modules={data?.modules || []}
Header={<Header plugin={data} onClose={() => router.back()} />}
/>
) : (
<Loading />
);
};
export default function AdEdit(props: any) {
return (
<FlowProvider filterAppIds={[]}>
<Render {...props} />
</FlowProvider>
);
}
export async function getServerSideProps(context: any) {
return {
props: {
pluginId: context?.query?.pluginId || '',
...(await serviceSideProps(context))
}
};
}

View File

@@ -0,0 +1,212 @@
import React, { useCallback, useState } from 'react';
import {
Box,
Flex,
Button,
ModalHeader,
ModalBody,
Input,
Textarea,
IconButton
} from '@chakra-ui/react';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useForm } from 'react-hook-form';
import { compressImg } from '@/web/common/file/utils';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useToast } from '@/web/common/hooks/useToast';
import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRequest } from '@/web/common/hooks/useRequest';
import { delOnePlugin, postCreatePlugin, putUpdatePlugin } from '@/web/core/plugin/api';
import Avatar from '@/components/Avatar';
import MyTooltip from '@/components/MyTooltip';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'react-i18next';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import MyIcon from '@/components/Icon';
export type FormType = {
id?: string;
avatar: string;
name: string;
intro: string;
};
export const defaultForm = {
avatar: '/icon/logo.svg',
name: '',
intro: ''
};
const CreateModal = ({
defaultValue = defaultForm,
onClose,
onSuccess,
onDelete
}: {
defaultValue?: FormType;
onClose: () => void;
onSuccess: () => void;
onDelete: () => void;
}) => {
const { t } = useTranslation();
const [refresh, setRefresh] = useState(false);
const { toast } = useToast();
const router = useRouter();
const { isPc } = useSystemStore();
const { openConfirm, ConfirmModal } = useConfirm({
title: t('common.Delete Tip'),
content: t('plugin.Confirm Delete')
});
const { register, setValue, getValues, handleSubmit } = useForm<FormType>({
defaultValues: defaultValue
});
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png,.svg',
multiple: false
});
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImg({
file,
maxW: 100,
maxH: 100
});
setValue('avatar', src);
setRefresh((state) => !state);
} catch (err: any) {
toast({
title: getErrText(err, t('common.Select File Failed')),
status: 'warning'
});
}
},
[setValue, t, toast]
);
const { mutate: onclickCreate, isLoading: creating } = useRequest({
mutationFn: async (data: FormType) => {
return postCreatePlugin(data);
},
onSuccess(id: string) {
router.push(`/plugin/edit?pluginId=${id}`);
onSuccess();
onClose();
},
successToast: t('common.Create Success'),
errorToast: t('common.Create Failed')
});
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
mutationFn: async (data: FormType) => {
if (!data.id) return Promise.resolve('');
// @ts-ignore
return putUpdatePlugin(data);
},
onSuccess() {
onSuccess();
onClose();
},
successToast: t('common.Update Success'),
errorToast: t('common.Update Failed')
});
const onclickDelApp = useCallback(async () => {
if (!defaultValue.id) return;
try {
await delOnePlugin(defaultValue.id);
toast({
title: t('common.Delete Success'),
status: 'success'
});
onDelete();
} catch (err: any) {
toast({
title: getErrText(err, t('common.Delete Failed')),
status: 'error'
});
}
onClose();
}, [defaultValue.id, onClose, toast, t, onDelete]);
return (
<MyModal isOpen onClose={onClose} isCentered={!isPc}>
<ModalHeader fontSize={'2xl'}>
{defaultValue.id ? t('plugin.Update Your Plugin') : t('plugin.Create Your Plugin')}
</ModalHeader>
<ModalBody>
<Box color={'myGray.800'} fontWeight={'bold'}>
{t('plugin.Set Name')}
</Box>
<Flex mt={3} alignItems={'center'}>
<MyTooltip label={t('common.Set Avatar')}>
<Avatar
flexShrink={0}
src={getValues('avatar')}
w={['28px', '32px']}
h={['28px', '32px']}
cursor={'pointer'}
borderRadius={'md'}
onClick={onOpenSelectFile}
/>
</MyTooltip>
<Input
flex={1}
ml={4}
autoFocus={!defaultValue.id}
bg={'myWhite.600'}
{...register('name', {
required: t("common.Name Can't Be Empty")
})}
/>
</Flex>
<Box mt={3}>
<Box mb={1}>{t('plugin.Intro')}</Box>
<Textarea {...register('intro')} bg={'myWhite.600'} rows={5} />
</Box>
</ModalBody>
<Flex px={5} py={4}>
{!!defaultValue.id && (
<IconButton
className="delete"
size={'sm'}
icon={<MyIcon name={'delete'} w={'14px'} />}
variant={'base'}
borderRadius={'md'}
aria-label={'delete'}
_hover={{
bg: 'red.100'
}}
onClick={(e) => {
e.stopPropagation();
openConfirm(onclickDelApp)();
}}
/>
)}
<Box flex={1} />
<Button variant={'base'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
{!!defaultValue.id ? (
<Button isLoading={updating} onClick={handleSubmit((data) => onclickUpdate(data))}>
{t('common.Confirm Update')}
</Button>
) : (
<Button isLoading={creating} onClick={handleSubmit((data) => onclickCreate(data))}>
{t('common.Confirm Create')}
</Button>
)}
</Flex>
<File onSelect={onSelectFile} />
<ConfirmModal />
</MyModal>
);
};
export default CreateModal;

View File

@@ -0,0 +1,137 @@
import React, { useState } from 'react';
import { Box, Grid, Card, useTheme, Flex, IconButton, Button, Image } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useQuery } from '@tanstack/react-query';
import { AddIcon } from '@chakra-ui/icons';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useTranslation } from 'next-i18next';
import MyIcon from '@/components/Icon';
import PageContainer from '@/components/PageContainer';
import Avatar from '@/components/Avatar';
import EditModal, { defaultForm, FormType } from './component/EditModal';
import { getUserPlugins } from '@/web/core/plugin/api';
import EmptyTip from '@/components/EmptyTip';
const MyModules = () => {
const { t } = useTranslation();
const theme = useTheme();
const router = useRouter();
const [editModalData, setEditModalData] = useState<FormType>();
/* load plugins */
const {
data = [],
isLoading,
refetch
} = useQuery(['loadModules'], () => getUserPlugins(), {
refetchOnMount: true
});
return (
<PageContainer isLoading={isLoading}>
<Flex pt={3} px={5} alignItems={'center'}>
<Flex flex={1} alignItems={'center'}>
<Image src={'/imgs/module/plugin.svg'} alt={''} mr={2} h={'24px'} />
<Box className="textlg" letterSpacing={1} fontSize={['20px', '24px']} fontWeight={'bold'}>
{t('plugin.My Plugins')}({t('common.Beta')})
</Box>
</Flex>
<Button
leftIcon={<AddIcon />}
variant={'base'}
onClick={() => setEditModalData(defaultForm)}
>
{t('common.New Create')}
</Button>
</Flex>
<Grid
p={5}
gridTemplateColumns={['1fr', 'repeat(3,1fr)', 'repeat(4,1fr)', 'repeat(5,1fr)']}
gridGap={5}
>
{data.map((plugin) => (
<Card
key={plugin._id}
py={4}
px={5}
cursor={'pointer'}
h={'140px'}
border={theme.borders.md}
boxShadow={'none'}
userSelect={'none'}
position={'relative'}
_hover={{
boxShadow: '1px 1px 10px rgba(0,0,0,0.2)',
borderColor: 'transparent',
'& .delete': {
display: 'block'
},
'& .chat': {
display: 'block'
}
}}
onClick={() => router.push(`/plugin/edit?pluginId=${plugin._id}`)}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={plugin.avatar} borderRadius={'md'} w={'28px'} />
<Box ml={3}>{plugin.name}</Box>
<IconButton
className="delete"
position={'absolute'}
top={4}
right={4}
size={'sm'}
icon={<MyIcon name={'edit'} w={'14px'} />}
variant={'base'}
borderRadius={'md'}
aria-label={'delete'}
display={['', 'none']}
_hover={{
bg: 'myBlue.200'
}}
onClick={(e) => {
e.stopPropagation();
setEditModalData({
id: plugin._id,
name: plugin.name,
avatar: plugin.avatar,
intro: plugin.intro
});
}}
/>
</Flex>
<Box
className={'textEllipsis3'}
py={2}
wordBreak={'break-all'}
fontSize={'sm'}
color={'myGray.600'}
>
{plugin.intro || t('plugin.No Intro')}
</Box>
</Card>
))}
</Grid>
{data.length === 0 && <EmptyTip />}
{!!editModalData && (
<EditModal
defaultValue={editModalData}
onClose={() => setEditModalData(undefined)}
onSuccess={refetch}
onDelete={refetch}
/>
)}
</PageContainer>
);
};
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content))
}
};
}
export default MyModules;