Concat plugin to app (#1799)
This commit is contained in:
@@ -29,10 +29,12 @@ export default function InputGuideBox({
|
||||
const { data = [] } = useRequest2(
|
||||
async () => {
|
||||
if (!text) return [];
|
||||
// More than 20 characters, it's basically meaningless
|
||||
if (text.length > 20) return [];
|
||||
return await queryChatInputGuideList(
|
||||
{
|
||||
appId,
|
||||
searchKey: text.slice(0, 50),
|
||||
searchKey: text,
|
||||
...outLinkAuthData
|
||||
},
|
||||
chatInputGuide.customUrl ? chatInputGuide.customUrl : undefined
|
||||
|
||||
@@ -79,7 +79,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
return (
|
||||
<>
|
||||
<Box h={'100%'} bg={'myGray.100'}>
|
||||
{isPc === true && (
|
||||
{isPc ? (
|
||||
<>
|
||||
{isHideNavbar ? (
|
||||
<Auth>{children}</Auth>
|
||||
@@ -94,8 +94,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isPc === false && (
|
||||
) : (
|
||||
<>
|
||||
<Box h={'100%'} display={['block', 'none']}>
|
||||
{phoneUnShowLayoutRoute[router.pathname] || isChatPage ? (
|
||||
|
||||
@@ -40,13 +40,6 @@ const Navbar = ({ unread }: { unread: number }) => {
|
||||
link: `/app/list`,
|
||||
activeLink: ['/app/list', '/app/detail']
|
||||
},
|
||||
{
|
||||
label: t('navbar.Plugin'),
|
||||
icon: 'common/navbar/pluginLight',
|
||||
activeIcon: 'common/navbar/pluginFill',
|
||||
link: `/plugin/list`,
|
||||
activeLink: ['/plugin/list', '/plugin/edit']
|
||||
},
|
||||
{
|
||||
label: t('navbar.Datasets'),
|
||||
icon: 'core/dataset/datasetLight',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Flex, Grid, Image } from '@chakra-ui/react';
|
||||
import type { GridProps } from '@chakra-ui/react';
|
||||
import type { FlexProps, GridProps } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
@@ -9,10 +9,11 @@ interface Props extends GridProps {
|
||||
list: { id: string; icon?: string; label: string | React.ReactNode }[];
|
||||
activeId: string;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
inlineStyles?: FlexProps;
|
||||
onChange: (id: string) => void;
|
||||
}
|
||||
|
||||
const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
|
||||
const Tabs = ({ list, size = 'md', activeId, onChange, inlineStyles, ...props }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const sizeMap = useMemo(() => {
|
||||
switch (size) {
|
||||
@@ -55,6 +56,7 @@ const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
|
||||
borderBottom={'2px solid transparent'}
|
||||
px={3}
|
||||
whiteSpace={'nowrap'}
|
||||
{...inlineStyles}
|
||||
{...(activeId === item.id
|
||||
? {
|
||||
color: 'primary.600',
|
||||
|
||||
@@ -91,7 +91,7 @@ const MyRadio = ({
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Radio isChecked={value === item.value} />
|
||||
{!hiddenCircle && <Radio isChecked={value === item.value} />}
|
||||
</Flex>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Flex, Box, BoxProps, border } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
type Props = BoxProps & {
|
||||
list: {
|
||||
icon?: string;
|
||||
label: string | React.ReactNode;
|
||||
value: string;
|
||||
}[];
|
||||
value: string;
|
||||
onChange: (e: string) => void;
|
||||
};
|
||||
|
||||
const RowTabs = ({ list, value, onChange, py = '7px', px = '12px', ...props }: Props) => {
|
||||
return (
|
||||
<Box display={'inline-flex'} px={'3px'} {...props}>
|
||||
{list.map((item) => (
|
||||
<Flex
|
||||
key={item.value}
|
||||
flex={'1 0 0'}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
px={px}
|
||||
py={py}
|
||||
userSelect={'none'}
|
||||
whiteSpace={'noWrap'}
|
||||
borderBottom={'2px solid'}
|
||||
{...(value === item.value
|
||||
? {
|
||||
bg: 'white',
|
||||
color: 'primary.600',
|
||||
borderColor: 'primary.600'
|
||||
}
|
||||
: {
|
||||
borderColor: 'myGray.100',
|
||||
onClick: () => onChange(item.value)
|
||||
})}
|
||||
>
|
||||
{item.icon && <MyIcon name={item.icon as any} mr={1} w={'14px'} />}
|
||||
<Box fontSize={'sm'}>{item.label}</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default RowTabs;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { Box, BoxProps, Flex } from '@chakra-ui/react';
|
||||
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
@@ -9,9 +9,17 @@ const FolderPath = (props: {
|
||||
FirstPathDom?: React.ReactNode;
|
||||
onClick: (parentId: string) => void;
|
||||
fontSize?: string;
|
||||
hoverStyle?: BoxProps;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { paths, rootName = t('common.folder.Root Path'), FirstPathDom, onClick, fontSize } = props;
|
||||
const {
|
||||
paths,
|
||||
rootName = t('common.folder.Root Path'),
|
||||
FirstPathDom,
|
||||
onClick,
|
||||
fontSize,
|
||||
hoverStyle
|
||||
} = props;
|
||||
|
||||
const concatPaths = useMemo(
|
||||
() => [
|
||||
@@ -43,9 +51,10 @@ const FolderPath = (props: {
|
||||
}
|
||||
: {
|
||||
cursor: 'pointer',
|
||||
color: 'myGray.600',
|
||||
color: 'myGray.500',
|
||||
_hover: {
|
||||
bg: 'myGray.100'
|
||||
bg: 'myGray.100',
|
||||
...hoverStyle
|
||||
},
|
||||
onClick: () => {
|
||||
onClick(item.parentId);
|
||||
|
||||
@@ -13,10 +13,18 @@ const AppTypeTag = ({ type }: { type: AppTypeEnum }) => {
|
||||
label: appT('type.Simple bot'),
|
||||
icon: 'core/app/type/simple'
|
||||
},
|
||||
[AppTypeEnum.advanced]: {
|
||||
[AppTypeEnum.workflow]: {
|
||||
label: appT('type.Workflow bot'),
|
||||
icon: 'core/app/type/workflow'
|
||||
},
|
||||
[AppTypeEnum.plugin]: {
|
||||
label: appT('type.Plugin'),
|
||||
icon: 'core/app/type/plugin'
|
||||
},
|
||||
[AppTypeEnum.httpPlugin]: {
|
||||
label: appT('type.Http plugin'),
|
||||
icon: 'core/app/type/httpPlugin'
|
||||
},
|
||||
[AppTypeEnum.folder]: undefined
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Flex, TextareaProps } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import React, { useTransition } from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import ChatFunctionTip from './Tip';
|
||||
import MyTextarea from '@/components/common/Textarea/MyTextarea';
|
||||
@@ -8,6 +8,7 @@ import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
|
||||
const WelcomeTextConfig = (props: TextareaProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems={'center'}>
|
||||
@@ -27,4 +28,4 @@ const WelcomeTextConfig = (props: TextareaProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default WelcomeTextConfig;
|
||||
export default React.memo(WelcomeTextConfig);
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type.d';
|
||||
import React, {
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
ForwardedRef
|
||||
} from 'react';
|
||||
import { SmallCloseIcon } from '@chakra-ui/icons';
|
||||
import { Box, Flex, IconButton } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { streamFetch } from '@/web/common/api/fetch';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import ChatBox from '@/components/ChatBox';
|
||||
import type { ComponentRef, StartChatFnProps } from '@/components/ChatBox/type.d';
|
||||
import {
|
||||
checkChatSupportSelectFileByModules,
|
||||
getAppQuestionGuidesByModules
|
||||
} from '@/web/core/chat/utils';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import {
|
||||
getDefaultEntryNodeIds,
|
||||
getMaxHistoryLimitFromNodes,
|
||||
initWorkflowEdgeStatus,
|
||||
storeNodes2RuntimeNodes
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
|
||||
export type ChatTestComponentRef = {
|
||||
resetChatTest: () => void;
|
||||
};
|
||||
|
||||
const ChatTest = (
|
||||
{
|
||||
isOpen,
|
||||
nodes = [],
|
||||
edges = [],
|
||||
onClose
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
nodes?: StoreNodeItemType[];
|
||||
edges?: StoreEdgeItemType[];
|
||||
onClose: () => void;
|
||||
},
|
||||
ref: ForwardedRef<ChatTestComponentRef>
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const ChatBoxRef = useRef<ComponentRef>(null);
|
||||
const { userInfo } = useUserStore();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const startChat = useCallback(
|
||||
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
|
||||
/* get histories */
|
||||
let historyMaxLen = getMaxHistoryLimitFromNodes(nodes);
|
||||
const history = chatList.slice(-historyMaxLen - 2, -2);
|
||||
|
||||
// 流请求,获取数据
|
||||
const { responseText, responseData } = await streamFetch({
|
||||
url: '/api/core/chat/chatTest',
|
||||
data: {
|
||||
history,
|
||||
prompt: chatList[chatList.length - 2].value,
|
||||
nodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
|
||||
edges: initWorkflowEdgeStatus(edges),
|
||||
variables,
|
||||
appId: appDetail._id,
|
||||
appName: `调试-${appDetail.name}`,
|
||||
mode: 'test'
|
||||
},
|
||||
onMessage: generatingMessage,
|
||||
abortCtrl: controller
|
||||
});
|
||||
|
||||
return { responseText, responseData };
|
||||
},
|
||||
[appDetail._id, appDetail.name, edges, nodes]
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetChatTest() {
|
||||
ChatBoxRef.current?.resetHistory([]);
|
||||
ChatBoxRef.current?.resetVariables();
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
zIndex={101}
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
right={0}
|
||||
h={isOpen ? '95%' : '0'}
|
||||
w={isOpen ? ['100%', '460px'] : '0'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'md'}
|
||||
overflow={'hidden'}
|
||||
transition={'.2s ease'}
|
||||
>
|
||||
<Flex py={4} px={5} whiteSpace={'nowrap'}>
|
||||
<Box fontSize={'lg'} fontWeight={'bold'} flex={1}>
|
||||
{t('core.chat.Debug test')}
|
||||
</Box>
|
||||
<MyTooltip label={t('core.chat.Restart')}>
|
||||
<IconButton
|
||||
className="chat"
|
||||
size={'smSquare'}
|
||||
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
|
||||
variant={'whiteDanger'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
ChatBoxRef.current?.resetHistory([]);
|
||||
ChatBoxRef.current?.resetVariables();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={t('common.Close')}>
|
||||
<IconButton
|
||||
ml={[3, 6]}
|
||||
icon={<SmallCloseIcon fontSize={'22px'} />}
|
||||
variant={'grayBase'}
|
||||
size={'smSquare'}
|
||||
aria-label={''}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box flex={1}>
|
||||
<ChatBox
|
||||
ref={ChatBoxRef}
|
||||
appId={appDetail._id}
|
||||
appAvatar={appDetail.avatar}
|
||||
userAvatar={userInfo?.avatar}
|
||||
showMarkIcon
|
||||
chatConfig={appDetail.chatConfig}
|
||||
showFileSelector={checkChatSupportSelectFileByModules(nodes)}
|
||||
onStartChat={startChat}
|
||||
onDelMessage={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box
|
||||
zIndex={2}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
position={'fixed'}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(forwardRef(ChatTest));
|
||||
@@ -1,60 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Textarea, Button, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../context';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const ImportSettings = ({ onClose }: Props) => {
|
||||
const { appT } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const initData = useContextSelector(WorkflowContext, (v) => v.initData);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
w={'600px'}
|
||||
onClose={onClose}
|
||||
iconSrc="/imgs/modal/params.svg"
|
||||
title={appT('Import Configs')}
|
||||
>
|
||||
<ModalBody>
|
||||
<Textarea
|
||||
placeholder={appT('Paste Config')}
|
||||
defaultValue={value}
|
||||
rows={16}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant="whiteBase"
|
||||
onClick={() => {
|
||||
if (!value) {
|
||||
return onClose();
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(value);
|
||||
initData(data);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: appT('Import Configs Failed')
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ImportSettings);
|
||||
@@ -1,427 +0,0 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Box, Flex, IconButton, Input, InputGroup, InputLeftElement, css } from '@chakra-ui/react';
|
||||
import type {
|
||||
FlowNodeTemplateType,
|
||||
nodeTemplateListType
|
||||
} from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { useViewport, XYPosition } from 'reactflow';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { getPreviewPluginModule } from '@/web/core/plugin/api';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { workflowNodeTemplateList } from '@fastgpt/web/core/workflow/constants';
|
||||
import RowTabs from '@fastgpt/web/components/common/Tabs/RowTabs';
|
||||
import { useWorkflowStore } from '@/web/core/workflow/store/workflow';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import ParentPaths from '@/components/common/ParentPaths';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useRouter } from 'next/router';
|
||||
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { debounce } from 'lodash';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../context';
|
||||
import { useCreation } from 'ahooks';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
|
||||
type ModuleTemplateListProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
type RenderListProps = {
|
||||
templates: FlowNodeTemplateType[];
|
||||
onClose: () => void;
|
||||
currentParent?: { parentId: string; parentName: string };
|
||||
setCurrentParent: (e: { parentId: string; parentName: string }) => void;
|
||||
};
|
||||
|
||||
enum TemplateTypeEnum {
|
||||
'basic' = 'basic',
|
||||
'systemPlugin' = 'systemPlugin',
|
||||
'teamPlugin' = 'teamPlugin'
|
||||
}
|
||||
|
||||
const sliderWidth = 390;
|
||||
|
||||
const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [currentParent, setCurrentParent] = useState<RenderListProps['currentParent']>();
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const { feConfigs } = useSystemStore();
|
||||
const basicNodeTemplates = useContextSelector(WorkflowContext, (v) => v.basicNodeTemplates);
|
||||
const hasToolNode = useContextSelector(WorkflowContext, (v) => v.hasToolNode);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
|
||||
const {
|
||||
systemNodeTemplates,
|
||||
loadSystemNodeTemplates,
|
||||
teamPluginNodeTemplates,
|
||||
loadTeamPluginNodeTemplates
|
||||
} = useWorkflowStore();
|
||||
const [templateType, setTemplateType] = useState(TemplateTypeEnum.basic);
|
||||
|
||||
const templates = useCreation(() => {
|
||||
const map = {
|
||||
[TemplateTypeEnum.basic]: basicNodeTemplates.filter((item) => {
|
||||
// unique node filter
|
||||
if (item.unique) {
|
||||
const nodeExist = nodeList.some((node) => node.flowNodeType === item.flowNodeType);
|
||||
if (nodeExist) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// special node filter
|
||||
if (item.flowNodeType === FlowNodeTypeEnum.lafModule && !feConfigs.lafEnv) {
|
||||
return false;
|
||||
}
|
||||
// tool stop
|
||||
if (!hasToolNode && item.flowNodeType === FlowNodeTypeEnum.stopTool) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
[TemplateTypeEnum.systemPlugin]: systemNodeTemplates,
|
||||
[TemplateTypeEnum.teamPlugin]: teamPluginNodeTemplates.filter((item) =>
|
||||
searchKey ? item.pluginType !== PluginTypeEnum.folder : true
|
||||
)
|
||||
};
|
||||
return map[templateType];
|
||||
}, [
|
||||
basicNodeTemplates,
|
||||
feConfigs.lafEnv,
|
||||
hasToolNode,
|
||||
nodeList,
|
||||
searchKey,
|
||||
systemNodeTemplates,
|
||||
teamPluginNodeTemplates,
|
||||
templateType
|
||||
]);
|
||||
|
||||
const { mutate: onChangeTab } = useRequest({
|
||||
mutationFn: async (e: any) => {
|
||||
const val = e as TemplateTypeEnum;
|
||||
if (val === TemplateTypeEnum.systemPlugin) {
|
||||
await loadSystemNodeTemplates();
|
||||
} else if (val === TemplateTypeEnum.teamPlugin) {
|
||||
await loadTeamPluginNodeTemplates({
|
||||
parentId: currentParent?.parentId
|
||||
});
|
||||
}
|
||||
setTemplateType(val);
|
||||
},
|
||||
errorToast: t('core.module.templates.Load plugin error')
|
||||
});
|
||||
|
||||
useQuery(['teamNodeTemplate', currentParent?.parentId, searchKey], () =>
|
||||
loadTeamPluginNodeTemplates({
|
||||
parentId: currentParent?.parentId,
|
||||
searchKey,
|
||||
init: true
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
zIndex={2}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
w={`${sliderWidth}px`}
|
||||
onClick={onClose}
|
||||
fontSize={'sm'}
|
||||
/>
|
||||
<Flex
|
||||
zIndex={3}
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={'10px'}
|
||||
left={0}
|
||||
pt={'20px'}
|
||||
pb={4}
|
||||
h={isOpen ? 'calc(100% - 20px)' : '0'}
|
||||
w={isOpen ? ['100%', `${sliderWidth}px`] : '0'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'0 20px 20px 0'}
|
||||
transition={'.2s ease'}
|
||||
userSelect={'none'}
|
||||
overflow={isOpen ? 'none' : 'hidden'}
|
||||
>
|
||||
<Box mb={2} pl={'20px'} pr={'10px'} whiteSpace={'nowrap'} overflow={'hidden'}>
|
||||
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
|
||||
<RowTabs
|
||||
list={[
|
||||
{
|
||||
icon: 'core/modules/basicNode',
|
||||
label: t('core.module.template.Basic Node'),
|
||||
value: TemplateTypeEnum.basic
|
||||
},
|
||||
{
|
||||
icon: 'core/modules/systemPlugin',
|
||||
label: t('core.module.template.System Plugin'),
|
||||
value: TemplateTypeEnum.systemPlugin
|
||||
},
|
||||
{
|
||||
icon: 'core/modules/teamPlugin',
|
||||
label: t('core.module.template.Team Plugin'),
|
||||
value: TemplateTypeEnum.teamPlugin
|
||||
}
|
||||
]}
|
||||
py={'5px'}
|
||||
value={templateType}
|
||||
onChange={onChangeTab}
|
||||
/>
|
||||
{/* close icon */}
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.700'} />}
|
||||
borderColor={'myGray.300'}
|
||||
variant={'grayBase'}
|
||||
aria-label={''}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Flex>
|
||||
{templateType === TemplateTypeEnum.teamPlugin && (
|
||||
<Flex mt={2} alignItems={'center'} h={10}>
|
||||
<InputGroup mr={4} h={'full'}>
|
||||
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
|
||||
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
h={'full'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={t('plugin.Search plugin')}
|
||||
onChange={debounce((e) => setSearchKey(e.target.value), 200)}
|
||||
/>
|
||||
</InputGroup>
|
||||
<Box flex={1} />
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
onClick={() => router.push('/plugin/list')}
|
||||
>
|
||||
<Box>去创建</Box>
|
||||
<MyIcon name={'common/rightArrowLight'} w={'14px'} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
{templateType === TemplateTypeEnum.teamPlugin && !searchKey && currentParent && (
|
||||
<Flex alignItems={'center'} mt={2}>
|
||||
<ParentPaths
|
||||
paths={[currentParent]}
|
||||
FirstPathDom={null}
|
||||
onClick={() => {
|
||||
setCurrentParent(undefined);
|
||||
}}
|
||||
fontSize="md"
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
<RenderList
|
||||
templates={templates}
|
||||
onClose={onClose}
|
||||
currentParent={currentParent}
|
||||
setCurrentParent={setCurrentParent}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NodeTemplatesModal);
|
||||
|
||||
const RenderList = React.memo(function RenderList({
|
||||
templates,
|
||||
onClose,
|
||||
currentParent,
|
||||
setCurrentParent
|
||||
}: RenderListProps) {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
|
||||
const { isPc } = useSystemStore();
|
||||
const { x, y, zoom } = useViewport();
|
||||
const { setLoading } = useSystemStore();
|
||||
const { toast } = useToast();
|
||||
const reactFlowWrapper = useContextSelector(WorkflowContext, (v) => v.reactFlowWrapper);
|
||||
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
|
||||
|
||||
const formatTemplates = useMemo<nodeTemplateListType>(() => {
|
||||
const copy: nodeTemplateListType = JSON.parse(JSON.stringify(workflowNodeTemplateList(t)));
|
||||
templates.forEach((item) => {
|
||||
const index = copy.findIndex((template) => template.type === item.templateType);
|
||||
if (index === -1) return;
|
||||
copy[index].list.push(item);
|
||||
});
|
||||
return copy.filter((item) => item.list.length > 0);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [templates, currentParent]);
|
||||
|
||||
const onAddNode = useCallback(
|
||||
async ({ template, position }: { template: FlowNodeTemplateType; position: XYPosition }) => {
|
||||
if (!reactFlowWrapper?.current) return;
|
||||
|
||||
const templateNode = await (async () => {
|
||||
try {
|
||||
// get plugin preview module
|
||||
if (template.flowNodeType === FlowNodeTypeEnum.pluginModule) {
|
||||
setLoading(true);
|
||||
const res = await getPreviewPluginModule(template.id);
|
||||
|
||||
setLoading(false);
|
||||
return res;
|
||||
}
|
||||
return { ...template };
|
||||
} catch (e) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: getErrText(e, t('core.plugin.Get Plugin Module Detail Failed'))
|
||||
});
|
||||
setLoading(false);
|
||||
return Promise.reject(e);
|
||||
}
|
||||
})();
|
||||
|
||||
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
|
||||
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
|
||||
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
|
||||
|
||||
const node = nodeTemplate2FlowNode({
|
||||
template: {
|
||||
...templateNode,
|
||||
name: t(templateNode.name),
|
||||
intro: t(templateNode.intro || '')
|
||||
},
|
||||
position: { x: mouseX, y: mouseY - 20 },
|
||||
selected: true
|
||||
});
|
||||
|
||||
setNodes((state) =>
|
||||
state
|
||||
.map((node) => ({
|
||||
...node,
|
||||
selected: false
|
||||
}))
|
||||
// @ts-ignore
|
||||
.concat(node)
|
||||
);
|
||||
},
|
||||
[reactFlowWrapper, setLoading, setNodes, t, toast, x, y, zoom]
|
||||
);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return templates.length === 0 ? (
|
||||
<EmptyTip text={appT('module.No Modules')} />
|
||||
) : (
|
||||
<Box flex={'1 0 0'} overflow={'overlay'} px={'20px'}>
|
||||
<Box mx={'auto'}>
|
||||
{formatTemplates.map((item, i) => (
|
||||
<Box
|
||||
key={item.type}
|
||||
css={css({
|
||||
span: {
|
||||
display: 'block'
|
||||
}
|
||||
})}
|
||||
>
|
||||
{item.label && (
|
||||
<Flex>
|
||||
<Box fontSize={'sm'} fontWeight={'bold'} flex={1}>
|
||||
{t(item.label)}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<>
|
||||
{item.list.map((template) => (
|
||||
<MyTooltip
|
||||
key={template.id}
|
||||
placement={'right'}
|
||||
label={
|
||||
<Box>
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar
|
||||
src={template.avatar}
|
||||
w={'24px'}
|
||||
objectFit={'contain'}
|
||||
borderRadius={'0'}
|
||||
/>
|
||||
<Box fontWeight={'bold'} ml={3}>
|
||||
{t(template.name)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box mt={2}>{t(template.intro || 'core.workflow.Not intro')}</Box>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
p={5}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
borderRadius={'sm'}
|
||||
draggable={template.pluginType !== PluginTypeEnum.folder}
|
||||
onDragEnd={(e) => {
|
||||
if (e.clientX < sliderWidth) return;
|
||||
onAddNode({
|
||||
template,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (template.pluginType === PluginTypeEnum.folder) {
|
||||
return setCurrentParent({
|
||||
parentId: template.id,
|
||||
parentName: template.name
|
||||
});
|
||||
}
|
||||
if (isPc) {
|
||||
return onAddNode({
|
||||
template,
|
||||
position: { x: sliderWidth * 1.5, y: 200 }
|
||||
});
|
||||
}
|
||||
onAddNode({
|
||||
template: template,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src={template.avatar}
|
||||
w={'1.7rem'}
|
||||
objectFit={'contain'}
|
||||
borderRadius={'0'}
|
||||
/>
|
||||
<Box color={'black'} fontSize={'sm'} ml={5} flex={'1 0 0'}>
|
||||
{t(template.name)}
|
||||
</Box>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
))}
|
||||
</>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}, [appT, formatTemplates, isPc, onAddNode, onClose, setCurrentParent, t, templates.length]);
|
||||
|
||||
return Render;
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { ModalBody, ModalFooter, Button } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import type { SelectAppItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import SelectOneResource from '@/components/common/folder/SelectOneResource';
|
||||
import {
|
||||
GetResourceFolderListProps,
|
||||
GetResourceListItemResponse
|
||||
} from '@fastgpt/global/common/parentFolder/type';
|
||||
import { getMyApps } from '@/web/core/app/api';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
|
||||
const SelectAppModal = ({
|
||||
value,
|
||||
filterAppIds = [],
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
value?: SelectAppItemType;
|
||||
filterAppIds?: string[];
|
||||
onClose: () => void;
|
||||
onSuccess: (e: SelectAppItemType) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedApp, setSelectedApp] = useState<SelectAppItemType | undefined>(value);
|
||||
|
||||
const getAppList = useCallback(
|
||||
async ({ parentId }: GetResourceFolderListProps) => {
|
||||
return getMyApps({ parentId }).then((res) =>
|
||||
res
|
||||
.filter((item) => !filterAppIds.includes(item._id))
|
||||
.map<GetResourceListItemResponse>((item) => ({
|
||||
id: item._id,
|
||||
name: item.name,
|
||||
avatar: item.avatar,
|
||||
isFolder: item.type === AppTypeEnum.folder
|
||||
}))
|
||||
);
|
||||
},
|
||||
[filterAppIds]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
title={`选择应用`}
|
||||
iconSrc="/imgs/workflow/ai.svg"
|
||||
onClose={onClose}
|
||||
position={'relative'}
|
||||
w={'600px'}
|
||||
>
|
||||
<ModalBody flex={'1 0 0'} overflow={'auto'} minH={'400px'} position={'relative'}>
|
||||
<SelectOneResource
|
||||
value={selectedApp?.id}
|
||||
onSelect={(id) => setSelectedApp(id ? { id } : undefined)}
|
||||
server={getAppList}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} onClick={onClose}>
|
||||
{t('common.Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
ml={2}
|
||||
isDisabled={!selectedApp}
|
||||
onClick={() => {
|
||||
if (!selectedApp) return;
|
||||
onSuccess(selectedApp);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t('common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SelectAppModal);
|
||||
@@ -1,235 +0,0 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { BezierEdge, getBezierPath, EdgeLabelRenderer, EdgeProps } from 'reactflow';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
|
||||
const ButtonEdge = (props: EdgeProps) => {
|
||||
const { nodes, setEdges, workflowDebugData, hoverEdgeId } = useContextSelector(
|
||||
WorkflowContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
const {
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
selected,
|
||||
source,
|
||||
sourceHandleId,
|
||||
target,
|
||||
targetHandleId,
|
||||
style
|
||||
} = props;
|
||||
|
||||
const onDelConnect = useCallback(
|
||||
(id: string) => {
|
||||
setEdges((state) => state.filter((item) => item.id !== id));
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const highlightEdge = useMemo(() => {
|
||||
const connectNode = nodes.find((node) => {
|
||||
return node.selected && (node.id === props.source || node.id === props.target);
|
||||
});
|
||||
return !!(connectNode || selected);
|
||||
}, [nodes, props.source, props.target, selected]);
|
||||
|
||||
const [, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition
|
||||
});
|
||||
|
||||
const isToolEdge = sourceHandleId === NodeOutputKeyEnum.selectedTools;
|
||||
const isHover = hoverEdgeId === id;
|
||||
|
||||
const { newTargetX, newTargetY } = useMemo(() => {
|
||||
if (targetPosition === 'left') {
|
||||
return {
|
||||
newTargetX: targetX - 3,
|
||||
newTargetY: targetY
|
||||
};
|
||||
}
|
||||
if (targetPosition === 'right') {
|
||||
return {
|
||||
newTargetX: targetX + 3,
|
||||
newTargetY: targetY
|
||||
};
|
||||
}
|
||||
if (targetPosition === 'bottom') {
|
||||
return {
|
||||
newTargetX: targetX,
|
||||
newTargetY: targetY + 3
|
||||
};
|
||||
}
|
||||
if (targetPosition === 'top') {
|
||||
return {
|
||||
newTargetX: targetX,
|
||||
newTargetY: targetY - 3
|
||||
};
|
||||
}
|
||||
return {
|
||||
newTargetX: targetX,
|
||||
newTargetY: targetY
|
||||
};
|
||||
}, [targetPosition, targetX, targetY]);
|
||||
|
||||
const edgeColor = useMemo(() => {
|
||||
const targetEdge = workflowDebugData?.runtimeEdges.find(
|
||||
(edge) => edge.sourceHandle === sourceHandleId && edge.targetHandle === targetHandleId
|
||||
);
|
||||
if (!targetEdge) {
|
||||
if (highlightEdge) return '#3370ff';
|
||||
return '#94B5FF';
|
||||
}
|
||||
console.log(targetEdge);
|
||||
// debug mode
|
||||
const colorMap = {
|
||||
[RuntimeEdgeStatusEnum.active]: '#39CC83',
|
||||
[RuntimeEdgeStatusEnum.waiting]: '#5E8FFF',
|
||||
[RuntimeEdgeStatusEnum.skipped]: '#8A95A7'
|
||||
};
|
||||
return colorMap[targetEdge.status];
|
||||
}, [highlightEdge, sourceHandleId, targetHandleId, workflowDebugData?.runtimeEdges]);
|
||||
|
||||
const memoEdgeLabel = useMemo(() => {
|
||||
const arrowTransform = (() => {
|
||||
if (targetPosition === 'left') {
|
||||
return `translate(-85%, -47%) translate(${newTargetX}px,${newTargetY}px) rotate(0deg)`;
|
||||
}
|
||||
if (targetPosition === 'right') {
|
||||
return `translate(-10%, -50%) translate(${newTargetX}px,${newTargetY}px) rotate(-180deg)`;
|
||||
}
|
||||
if (targetPosition === 'bottom') {
|
||||
return `translate(-50%, -20%) translate(${newTargetX}px,${newTargetY}px) rotate(-90deg)`;
|
||||
}
|
||||
if (targetPosition === 'top') {
|
||||
return `translate(-50%, -90%) translate(${newTargetX}px,${newTargetY}px) rotate(90deg)`;
|
||||
}
|
||||
})();
|
||||
return (
|
||||
<EdgeLabelRenderer>
|
||||
<Flex
|
||||
display={isHover || highlightEdge ? 'flex' : 'none'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
position={'absolute'}
|
||||
transform={`translate(-55%, -50%) translate(${labelX}px,${labelY}px)`}
|
||||
pointerEvents={'all'}
|
||||
w={'17px'}
|
||||
h={'17px'}
|
||||
bg={'white'}
|
||||
borderRadius={'17px'}
|
||||
cursor={'pointer'}
|
||||
zIndex={1000}
|
||||
onClick={() => onDelConnect(id)}
|
||||
>
|
||||
<MyIcon name={'core/workflow/closeEdge'} w={'100%'}></MyIcon>
|
||||
</Flex>
|
||||
{!isToolEdge && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
position={'absolute'}
|
||||
transform={arrowTransform}
|
||||
pointerEvents={'all'}
|
||||
w={highlightEdge ? '14px' : '10px'}
|
||||
h={highlightEdge ? '14px' : '10px'}
|
||||
// bg={'white'}
|
||||
zIndex={highlightEdge ? 1000 : 0}
|
||||
>
|
||||
<MyIcon
|
||||
name={'core/workflow/edgeArrow'}
|
||||
w={'100%'}
|
||||
color={edgeColor}
|
||||
{...(highlightEdge
|
||||
? {
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
: {})}
|
||||
></MyIcon>
|
||||
</Flex>
|
||||
)}
|
||||
</EdgeLabelRenderer>
|
||||
);
|
||||
}, [
|
||||
isHover,
|
||||
highlightEdge,
|
||||
labelX,
|
||||
labelY,
|
||||
isToolEdge,
|
||||
edgeColor,
|
||||
targetPosition,
|
||||
newTargetX,
|
||||
newTargetY,
|
||||
onDelConnect,
|
||||
id
|
||||
]);
|
||||
|
||||
const memoBezierEdge = useMemo(() => {
|
||||
const targetEdge = workflowDebugData?.runtimeEdges.find(
|
||||
(edge) => edge.source === source && edge.target === target
|
||||
);
|
||||
|
||||
const edgeStyle: React.CSSProperties = (() => {
|
||||
if (!targetEdge) {
|
||||
return {
|
||||
...style,
|
||||
...(highlightEdge
|
||||
? {
|
||||
strokeWidth: 5
|
||||
}
|
||||
: { strokeWidth: 3, zIndex: 2 })
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...style,
|
||||
strokeWidth: 3,
|
||||
zIndex: 2
|
||||
};
|
||||
})();
|
||||
|
||||
return (
|
||||
<BezierEdge
|
||||
{...props}
|
||||
targetX={newTargetX}
|
||||
targetY={newTargetY}
|
||||
style={{
|
||||
...edgeStyle,
|
||||
stroke: edgeColor
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
workflowDebugData?.runtimeEdges,
|
||||
props,
|
||||
newTargetX,
|
||||
newTargetY,
|
||||
edgeColor,
|
||||
source,
|
||||
target,
|
||||
style,
|
||||
highlightEdge
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{memoBezierEdge}
|
||||
{memoEdgeLabel}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ButtonEdge);
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { BoxProps } from '@chakra-ui/react';
|
||||
|
||||
const Container = ({ children, ...props }: BoxProps) => {
|
||||
return (
|
||||
<Box
|
||||
px={4}
|
||||
mx={2}
|
||||
mb={2}
|
||||
py={'10px'}
|
||||
position={'relative'}
|
||||
bg={'myGray.50'}
|
||||
border={'1px solid #F0F1F6'}
|
||||
borderRadius={'md'}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Container);
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, useTheme } from '@chakra-ui/react';
|
||||
|
||||
const Divider = ({
|
||||
text,
|
||||
showBorderBottom = true,
|
||||
icon
|
||||
}: {
|
||||
text?: 'Input' | 'Output' | string;
|
||||
showBorderBottom?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const isDivider = !text;
|
||||
|
||||
return (
|
||||
<Box
|
||||
alignItems={'center'}
|
||||
display={'flex'}
|
||||
justifyContent={'center'}
|
||||
bg={'myGray.25'}
|
||||
py={isDivider ? '0' : 2}
|
||||
borderTop={theme.borders.base}
|
||||
borderBottom={showBorderBottom ? theme.borders.base : 0}
|
||||
fontWeight={'medium'}
|
||||
>
|
||||
{icon}
|
||||
{icon && <Box w={1} />}
|
||||
{text}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Divider);
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
|
||||
const IOTitle = ({ text }: { text?: 'Input' | 'Output' | string }) => {
|
||||
return (
|
||||
<Flex fontSize={'md'} alignItems={'center'} fontWeight={'medium'} mb={3}>
|
||||
<Box w={'3px'} h={'14px'} borderRadius={'13px'} bg={'primary.600'} mr={1.5} />
|
||||
{text}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(IOTitle);
|
||||
@@ -1,274 +0,0 @@
|
||||
import { storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
|
||||
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { uiWorkflow2StoreWorkflow } from '../../utils';
|
||||
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Textarea,
|
||||
NumberDecrementStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
Switch
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { checkInputIsReference } from '@fastgpt/global/core/workflow/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext, getWorkflowStore } from '../../context';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
const MyRightDrawer = dynamic(
|
||||
() => import('@fastgpt/web/components/common/MyDrawer/MyRightDrawer')
|
||||
);
|
||||
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
|
||||
|
||||
export const useDebug = () => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
|
||||
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
|
||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
||||
const onStartNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStartNodeDebug);
|
||||
|
||||
const [runtimeNodeId, setRuntimeNodeId] = useState<string>();
|
||||
const [runtimeNodes, setRuntimeNodes] = useState<RuntimeNodeItemType[]>();
|
||||
const [runtimeEdges, setRuntimeEdges] = useState<RuntimeEdgeItemType[]>();
|
||||
|
||||
const flowData2StoreDataAndCheck = useCallback(async () => {
|
||||
const { nodes } = await getWorkflowStore();
|
||||
|
||||
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
|
||||
if (!checkResults) {
|
||||
const storeNodes = uiWorkflow2StoreWorkflow({ nodes, edges });
|
||||
|
||||
return JSON.stringify(storeNodes);
|
||||
} else {
|
||||
checkResults.forEach((nodeId) => onUpdateNodeError(nodeId, true));
|
||||
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('core.workflow.Check Failed')
|
||||
});
|
||||
return Promise.reject();
|
||||
}
|
||||
}, [edges, onUpdateNodeError, t, toast]);
|
||||
|
||||
const openDebugNode = useCallback(
|
||||
async ({ entryNodeId }: { entryNodeId: string }) => {
|
||||
setNodes((state) =>
|
||||
state.map((node) => ({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
debugResult: undefined
|
||||
}
|
||||
}))
|
||||
);
|
||||
const {
|
||||
nodes,
|
||||
edges
|
||||
}: {
|
||||
nodes: StoreNodeItemType[];
|
||||
edges: StoreEdgeItemType[];
|
||||
} = JSON.parse(await flowData2StoreDataAndCheck());
|
||||
|
||||
const runtimeNodes = storeNodes2RuntimeNodes(nodes, [entryNodeId]);
|
||||
const runtimeEdges: RuntimeEdgeItemType[] = edges.map((edge) =>
|
||||
edge.target === entryNodeId
|
||||
? {
|
||||
...edge,
|
||||
status: 'active'
|
||||
}
|
||||
: {
|
||||
...edge,
|
||||
status: 'waiting'
|
||||
}
|
||||
);
|
||||
|
||||
setRuntimeNodeId(entryNodeId);
|
||||
setRuntimeNodes(runtimeNodes);
|
||||
setRuntimeEdges(runtimeEdges);
|
||||
},
|
||||
[flowData2StoreDataAndCheck, setNodes]
|
||||
);
|
||||
|
||||
const DebugInputModal = useCallback(() => {
|
||||
if (!runtimeNodes || !runtimeEdges) return <></>;
|
||||
|
||||
const runtimeNode = runtimeNodes.find((node) => node.nodeId === runtimeNodeId);
|
||||
|
||||
if (!runtimeNode) return <></>;
|
||||
const renderInputs = runtimeNode.inputs.filter((input) => {
|
||||
if (runtimeNode.flowNodeType === FlowNodeTypeEnum.pluginInput) return true;
|
||||
if (checkInputIsReference(input)) return true;
|
||||
if (input.required && !input.value) return true;
|
||||
});
|
||||
|
||||
const { register, getValues, setValue, handleSubmit } = useForm<Record<string, any>>({
|
||||
defaultValues: renderInputs.reduce((acc: Record<string, any>, input) => {
|
||||
const isReference = checkInputIsReference(input);
|
||||
if (isReference) {
|
||||
acc[input.key] = undefined;
|
||||
} else if (typeof input.value === 'object') {
|
||||
acc[input.key] = JSON.stringify(input.value, null, 2);
|
||||
} else {
|
||||
acc[input.key] = input.value;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
setRuntimeNodeId(undefined);
|
||||
setRuntimeNodes(undefined);
|
||||
setRuntimeEdges(undefined);
|
||||
};
|
||||
|
||||
const onclickRun = (data: Record<string, any>) => {
|
||||
onStartNodeDebug({
|
||||
entryNodeId: runtimeNode.nodeId,
|
||||
runtimeNodes: runtimeNodes.map((node) =>
|
||||
node.nodeId === runtimeNode.nodeId
|
||||
? {
|
||||
...runtimeNode,
|
||||
inputs: runtimeNode.inputs.map((input) => {
|
||||
let parseValue = (() => {
|
||||
try {
|
||||
if (
|
||||
input.valueType === WorkflowIOValueTypeEnum.string ||
|
||||
input.valueType === WorkflowIOValueTypeEnum.number ||
|
||||
input.valueType === WorkflowIOValueTypeEnum.boolean
|
||||
)
|
||||
return data[input.key];
|
||||
|
||||
return JSON.parse(data[input.key]);
|
||||
} catch (e) {
|
||||
return data[input.key];
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
...input,
|
||||
value: parseValue ?? input.value
|
||||
};
|
||||
})
|
||||
}
|
||||
: node
|
||||
),
|
||||
runtimeEdges: runtimeEdges
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<MyRightDrawer
|
||||
onClose={onClose}
|
||||
iconSrc="core/workflow/debugBlue"
|
||||
title={t('core.workflow.Debug Node')}
|
||||
maxW={['90vw', '35vw']}
|
||||
px={0}
|
||||
>
|
||||
<Box flex={'1 0 0'} overflow={'auto'} px={6}>
|
||||
{renderInputs.map((input) => {
|
||||
const required = input.required || false;
|
||||
|
||||
const RenderInput = (() => {
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.string) {
|
||||
return (
|
||||
<Textarea
|
||||
{...register(input.key, {
|
||||
required
|
||||
})}
|
||||
placeholder={t(input.placeholder || '')}
|
||||
bg={'myGray.50'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.number) {
|
||||
return (
|
||||
<NumberInput step={input.step} min={input.min} max={input.max} bg={'myGray.50'}>
|
||||
<NumberInputField
|
||||
{...register(input.key, {
|
||||
required: input.required,
|
||||
min: input.min,
|
||||
max: input.max,
|
||||
valueAsNumber: true
|
||||
})}
|
||||
/>
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
);
|
||||
}
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.boolean) {
|
||||
return (
|
||||
<Box>
|
||||
<Switch {...register(input.key)} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
let value = getValues(input.key) || '';
|
||||
if (typeof value !== 'string') {
|
||||
value = JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
return (
|
||||
<JsonEditor
|
||||
bg={'myGray.50'}
|
||||
placeholder={t(input.placeholder || '')}
|
||||
resize
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(input.key, e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})();
|
||||
|
||||
return !!RenderInput ? (
|
||||
<Box key={input.key} _notLast={{ mb: 4 }} px={1}>
|
||||
<Flex alignItems={'center'} mb={1}>
|
||||
<Box position={'relative'}>
|
||||
{required && (
|
||||
<Box position={'absolute'} right={-2} top={'-1px'} color={'red.600'}>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
{t(input.debugLabel || input.label)}
|
||||
</Box>
|
||||
{input.description && <QuestionTip ml={2} label={input.description} />}
|
||||
</Flex>
|
||||
{RenderInput}
|
||||
</Box>
|
||||
) : null;
|
||||
})}
|
||||
</Box>
|
||||
<Flex py={2} justifyContent={'flex-end'} px={6}>
|
||||
<Button onClick={handleSubmit(onclickRun)}>运行</Button>
|
||||
</Flex>
|
||||
</MyRightDrawer>
|
||||
);
|
||||
}, [onStartNodeDebug, runtimeEdges, runtimeNodeId, runtimeNodes, t]);
|
||||
|
||||
return {
|
||||
DebugInputModal,
|
||||
openDebugNode
|
||||
};
|
||||
};
|
||||
@@ -1,120 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Node } from 'reactflow';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext, getWorkflowStore } from '../../context';
|
||||
|
||||
export const useKeyboard = () => {
|
||||
const { t } = useTranslation();
|
||||
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
|
||||
const { copyData } = useCopyData();
|
||||
|
||||
const [isDowningCtrl, setIsDowningCtrl] = useState(false);
|
||||
|
||||
const hasInputtingElement = useCallback(() => {
|
||||
const activeElement = document.activeElement;
|
||||
|
||||
if (activeElement) {
|
||||
const tagName = activeElement.tagName.toLowerCase();
|
||||
const className = activeElement.className.toLowerCase();
|
||||
if (tagName === 'input' || tagName === 'textarea') return true;
|
||||
if (className.includes('prompteditor')) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
const onCopy = useCallback(async () => {
|
||||
if (hasInputtingElement()) return;
|
||||
const { nodes } = await getWorkflowStore();
|
||||
|
||||
const selectedNodes = nodes.filter(
|
||||
(node) => node.selected && !node.data?.isError && node.data?.unique !== true
|
||||
);
|
||||
if (selectedNodes.length === 0) return;
|
||||
copyData(JSON.stringify(selectedNodes), t('core.workflow.Copy node'));
|
||||
}, [copyData, hasInputtingElement, t]);
|
||||
|
||||
const onParse = useCallback(async () => {
|
||||
if (hasInputtingElement()) return;
|
||||
const copyResult = await navigator.clipboard.readText();
|
||||
try {
|
||||
const parseData = JSON.parse(copyResult) as Node<FlowNodeItemType, string | undefined>[];
|
||||
// check is array
|
||||
if (!Array.isArray(parseData)) return;
|
||||
// filter workflow data
|
||||
const newNodes = parseData
|
||||
.filter((item) => !!item.type && item.data?.unique !== true)
|
||||
.map((item) => {
|
||||
const nodeId = getNanoid();
|
||||
return {
|
||||
// reset id
|
||||
...item,
|
||||
id: nodeId,
|
||||
data: {
|
||||
...item.data,
|
||||
nodeId
|
||||
},
|
||||
position: {
|
||||
x: item.position.x + 100,
|
||||
y: item.position.y + 100
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
setNodes((prev) =>
|
||||
prev
|
||||
.map((node) => ({
|
||||
...node,
|
||||
selected: false
|
||||
}))
|
||||
//@ts-ignore
|
||||
.concat(newNodes)
|
||||
);
|
||||
} catch (error) {}
|
||||
}, [hasInputtingElement, setNodes]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
setIsDowningCtrl(true);
|
||||
switch (event.key) {
|
||||
case 'c':
|
||||
onCopy();
|
||||
break;
|
||||
case 'v':
|
||||
onParse();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
[onCopy, onParse]
|
||||
);
|
||||
|
||||
const handleKeyUp = useCallback((event: KeyboardEvent) => {
|
||||
setIsDowningCtrl(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [handleKeyDown]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keyup', handleKeyUp);
|
||||
return () => {
|
||||
document.removeEventListener('keyup', handleKeyUp);
|
||||
};
|
||||
}, [handleKeyUp]);
|
||||
|
||||
return {
|
||||
isDowningCtrl
|
||||
};
|
||||
};
|
||||
@@ -1,300 +0,0 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Connection,
|
||||
Controls,
|
||||
ControlButton,
|
||||
MiniMap,
|
||||
NodeProps,
|
||||
ReactFlowProvider,
|
||||
useReactFlow,
|
||||
NodeChange,
|
||||
OnConnectStartParams,
|
||||
addEdge,
|
||||
EdgeChange,
|
||||
Edge
|
||||
} from 'reactflow';
|
||||
import { Box, Flex, IconButton, useDisclosure } from '@chakra-ui/react';
|
||||
import { SmallCloseIcon } from '@chakra-ui/icons';
|
||||
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import ButtonEdge from './components/ButtonEdge';
|
||||
import NodeTemplatesModal from './NodeTemplatesModal';
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { connectionLineStyle, defaultEdgeOptions } from '../constants';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useKeyboard } from './hooks/useKeyboard';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../context';
|
||||
|
||||
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
|
||||
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
|
||||
[FlowNodeTypeEnum.emptyNode]: NodeSimple,
|
||||
[FlowNodeTypeEnum.globalVariable]: NodeSimple,
|
||||
[FlowNodeTypeEnum.systemConfig]: dynamic(() => import('./nodes/NodeSystemConfig')),
|
||||
[FlowNodeTypeEnum.workflowStart]: dynamic(() => import('./nodes/NodeWorkflowStart')),
|
||||
[FlowNodeTypeEnum.chatNode]: NodeSimple,
|
||||
[FlowNodeTypeEnum.datasetSearchNode]: NodeSimple,
|
||||
[FlowNodeTypeEnum.datasetConcatNode]: dynamic(() => import('./nodes/NodeDatasetConcat')),
|
||||
[FlowNodeTypeEnum.answerNode]: dynamic(() => import('./nodes/NodeAnswer')),
|
||||
[FlowNodeTypeEnum.classifyQuestion]: dynamic(() => import('./nodes/NodeCQNode')),
|
||||
[FlowNodeTypeEnum.contentExtract]: dynamic(() => import('./nodes/NodeExtract')),
|
||||
[FlowNodeTypeEnum.httpRequest468]: dynamic(() => import('./nodes/NodeHttp')),
|
||||
[FlowNodeTypeEnum.runApp]: NodeSimple,
|
||||
[FlowNodeTypeEnum.pluginInput]: dynamic(() => import('./nodes/NodePluginInput')),
|
||||
[FlowNodeTypeEnum.pluginOutput]: dynamic(() => import('./nodes/NodePluginOutput')),
|
||||
[FlowNodeTypeEnum.pluginModule]: NodeSimple,
|
||||
[FlowNodeTypeEnum.queryExtension]: NodeSimple,
|
||||
[FlowNodeTypeEnum.tools]: dynamic(() => import('./nodes/NodeTools')),
|
||||
[FlowNodeTypeEnum.stopTool]: (data: NodeProps<FlowNodeItemType>) => (
|
||||
<NodeSimple {...data} minW={'100px'} maxW={'300px'} />
|
||||
),
|
||||
[FlowNodeTypeEnum.lafModule]: dynamic(() => import('./nodes/NodeLaf')),
|
||||
[FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse')),
|
||||
[FlowNodeTypeEnum.variableUpdate]: dynamic(() => import('./nodes/NodeVariableUpdate')),
|
||||
[FlowNodeTypeEnum.code]: dynamic(() => import('./nodes/NodeCode'))
|
||||
};
|
||||
const edgeTypes = {
|
||||
[EDGE_TYPE]: ButtonEdge
|
||||
};
|
||||
|
||||
const Container = React.memo(function Container() {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({
|
||||
content: t('core.module.Confirm Delete Node'),
|
||||
type: 'delete'
|
||||
});
|
||||
|
||||
const { isDowningCtrl } = useKeyboard();
|
||||
const {
|
||||
setConnectingEdge,
|
||||
reactFlowWrapper,
|
||||
nodes,
|
||||
onNodesChange,
|
||||
edges,
|
||||
setEdges,
|
||||
onEdgesChange,
|
||||
setHoverEdgeId
|
||||
} = useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
/* node */
|
||||
const handleNodesChange = useCallback(
|
||||
(changes: NodeChange[]) => {
|
||||
for (const change of changes) {
|
||||
if (change.type === 'remove') {
|
||||
const node = nodes.find((n) => n.id === change.id);
|
||||
if (node && node.data.forbidDelete) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('core.workflow.Can not delete node')
|
||||
});
|
||||
} else {
|
||||
return onOpenConfirmDeleteNode(() => {
|
||||
onNodesChange(changes);
|
||||
setEdges((state) =>
|
||||
state.filter((edge) => edge.source !== change.id && edge.target !== change.id)
|
||||
);
|
||||
})();
|
||||
}
|
||||
} else if (change.type === 'select' && change.selected === false && isDowningCtrl) {
|
||||
change.selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
onNodesChange(changes);
|
||||
},
|
||||
[isDowningCtrl, nodes, onNodesChange, onOpenConfirmDeleteNode, setEdges, t, toast]
|
||||
);
|
||||
const handleEdgeChange = useCallback(
|
||||
(changes: EdgeChange[]) => {
|
||||
onEdgesChange(changes.filter((change) => change.type !== 'remove'));
|
||||
},
|
||||
[onEdgesChange]
|
||||
);
|
||||
|
||||
/* connect */
|
||||
const onConnectStart = useCallback(
|
||||
(event: any, params: OnConnectStartParams) => {
|
||||
setConnectingEdge(params);
|
||||
},
|
||||
[setConnectingEdge]
|
||||
);
|
||||
const onConnectEnd = useCallback(() => {
|
||||
setConnectingEdge(undefined);
|
||||
}, [setConnectingEdge]);
|
||||
const onConnect = useCallback(
|
||||
({ connect }: { connect: Connection }) => {
|
||||
setEdges((state) =>
|
||||
addEdge(
|
||||
{
|
||||
...connect,
|
||||
type: EDGE_TYPE
|
||||
},
|
||||
state
|
||||
)
|
||||
);
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
const customOnConnect = useCallback(
|
||||
(connect: Connection) => {
|
||||
if (!connect.sourceHandle || !connect.targetHandle) {
|
||||
return;
|
||||
}
|
||||
if (connect.source === connect.target) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('core.module.Can not connect self')
|
||||
});
|
||||
}
|
||||
onConnect({
|
||||
connect
|
||||
});
|
||||
},
|
||||
[onConnect, t, toast]
|
||||
);
|
||||
|
||||
/* edge */
|
||||
const onEdgeMouseEnter = useCallback(
|
||||
(e: any, edge: Edge) => {
|
||||
setHoverEdgeId(edge.id);
|
||||
},
|
||||
[setHoverEdgeId]
|
||||
);
|
||||
const onEdgeMouseLeave = useCallback(() => {
|
||||
setHoverEdgeId(undefined);
|
||||
}, [setHoverEdgeId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReactFlow
|
||||
ref={reactFlowWrapper}
|
||||
fitView
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
minZoom={0.1}
|
||||
maxZoom={1.5}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
elevateEdgesOnSelect
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={handleEdgeChange}
|
||||
onConnect={customOnConnect}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
onEdgeMouseEnter={onEdgeMouseEnter}
|
||||
onEdgeMouseLeave={onEdgeMouseLeave}
|
||||
>
|
||||
<FlowController />
|
||||
</ReactFlow>
|
||||
<ConfirmDeleteModal />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const Flow = ({ Header, ...data }: { Header: React.ReactNode }) => {
|
||||
const {
|
||||
isOpen: isOpenTemplate,
|
||||
onOpen: onOpenTemplate,
|
||||
onClose: onCloseTemplate
|
||||
} = useDisclosure();
|
||||
|
||||
const memoRenderContainer = useMemo(() => {
|
||||
return (
|
||||
<Box
|
||||
minH={'400px'}
|
||||
flex={'1 0 0'}
|
||||
w={'100%'}
|
||||
h={0}
|
||||
position={'relative'}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{/* open module template */}
|
||||
<IconButton
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
left={5}
|
||||
size={'mdSquare'}
|
||||
borderRadius={'50%'}
|
||||
icon={<SmallCloseIcon fontSize={'26px'} />}
|
||||
transform={isOpenTemplate ? '' : 'rotate(135deg)'}
|
||||
transition={'0.2s ease'}
|
||||
aria-label={''}
|
||||
zIndex={1}
|
||||
boxShadow={'2px 2px 6px #85b1ff'}
|
||||
onClick={() => {
|
||||
isOpenTemplate ? onCloseTemplate() : onOpenTemplate();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Container {...data} />
|
||||
|
||||
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
|
||||
</Box>
|
||||
);
|
||||
}, [data, isOpenTemplate, onCloseTemplate, onOpenTemplate]);
|
||||
|
||||
return (
|
||||
<Box h={'100%'} position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
|
||||
<ReactFlowProvider>
|
||||
<Flex h={'100%'} flexDirection={'column'} bg={'myGray.50'}>
|
||||
{Header}
|
||||
{memoRenderContainer}
|
||||
</Flex>
|
||||
</ReactFlowProvider>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Flow);
|
||||
|
||||
const FlowController = React.memo(function FlowController() {
|
||||
const { fitView } = useReactFlow();
|
||||
return (
|
||||
<>
|
||||
<MiniMap
|
||||
style={{
|
||||
height: 78,
|
||||
width: 126,
|
||||
marginBottom: 35
|
||||
}}
|
||||
pannable
|
||||
/>
|
||||
<Controls
|
||||
position={'bottom-right'}
|
||||
style={{
|
||||
display: 'flex',
|
||||
marginBottom: 5,
|
||||
background: 'white',
|
||||
borderRadius: '6px',
|
||||
overflow: 'hidden',
|
||||
boxShadow:
|
||||
'0px 0px 1px 0px rgba(19, 51, 107, 0.20), 0px 12px 16px -4px rgba(19, 51, 107, 0.20)'
|
||||
}}
|
||||
showInteractive={false}
|
||||
showFitView={false}
|
||||
>
|
||||
<MyTooltip label={'页面居中'}>
|
||||
<ControlButton className="custom-workflow-fix_view" onClick={() => fitView()}>
|
||||
<MyIcon name={'core/modules/fixview'} w={'14px'} />
|
||||
</ControlButton>
|
||||
</MyTooltip>
|
||||
</Controls>
|
||||
<Background />
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import Container from '../components/Container';
|
||||
import RenderInput from './render/RenderInput';
|
||||
import RenderToolInput from './render/RenderToolInput';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import IOTitle from '../components/IOTitle';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
|
||||
const NodeAnswer = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { nodeId, inputs } = data;
|
||||
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
|
||||
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
|
||||
|
||||
return (
|
||||
<NodeCard minW={'400px'} selected={selected} {...data}>
|
||||
<Container>
|
||||
{toolInputs.length > 0 && (
|
||||
<>
|
||||
<IOTitle text={t('core.module.tool.Tool input')} />
|
||||
<Container>
|
||||
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
|
||||
{/* <RenderOutput nodeId={nodeId} flowOutputList={outputs} /> */}
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeAnswer);
|
||||
@@ -1,141 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { NodeProps, Position } from 'reactflow';
|
||||
import { Box, Button, Flex, Textarea } from '@chakra-ui/react';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import Container from '../components/Container';
|
||||
import RenderInput from './render/RenderInput';
|
||||
import type { ClassifyQuestionAgentItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { SourceHandle } from './render/Handle';
|
||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
|
||||
const NodeCQNode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { nodeId, inputs } = data;
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const CustomComponent = useMemo(
|
||||
() => ({
|
||||
[NodeInputKeyEnum.agents]: ({
|
||||
key: agentKey,
|
||||
value = [],
|
||||
...props
|
||||
}: FlowNodeInputItemType) => {
|
||||
const agents = value as ClassifyQuestionAgentItemType[];
|
||||
return (
|
||||
<Box>
|
||||
{agents.map((item, i) => (
|
||||
<Box key={item.key} mb={4}>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyTooltip label={t('common.Delete')}>
|
||||
<MyIcon
|
||||
mt={1}
|
||||
mr={2}
|
||||
name={'minus'}
|
||||
w={'12px'}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.600'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: agentKey,
|
||||
value: {
|
||||
...props,
|
||||
key: agentKey,
|
||||
value: agents.filter((input) => input.key !== item.key)
|
||||
}
|
||||
});
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'delOutput',
|
||||
key: item.key
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Box flex={1} color={'myGray.600'} fontWeight={'medium'}>
|
||||
分类{i + 1}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box position={'relative'}>
|
||||
<Textarea
|
||||
rows={2}
|
||||
mt={1}
|
||||
defaultValue={item.value}
|
||||
bg={'white'}
|
||||
fontSize={'sm'}
|
||||
onChange={(e) => {
|
||||
const newVal = agents.map((val) =>
|
||||
val.key === item.key
|
||||
? {
|
||||
...val,
|
||||
value: e.target.value
|
||||
}
|
||||
: val
|
||||
);
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: agentKey,
|
||||
value: {
|
||||
...props,
|
||||
key: agentKey,
|
||||
value: newVal
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={getHandleId(nodeId, 'source', item.key)}
|
||||
position={Position.Right}
|
||||
translate={[26, 0]}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
<Button
|
||||
fontSize={'sm'}
|
||||
onClick={() => {
|
||||
const key = getNanoid();
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: agentKey,
|
||||
value: {
|
||||
...props,
|
||||
key: agentKey,
|
||||
value: agents.concat({ value: '', key })
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('core.module.Add question type')}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}),
|
||||
[nodeId, onChangeNode, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<NodeCard minW={'400px'} selected={selected} {...data}>
|
||||
<Container>
|
||||
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeCQNode);
|
||||
@@ -1,106 +0,0 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import Container from '../components/Container';
|
||||
import RenderInput from './render/RenderInput';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
import IOTitle from '../components/IOTitle';
|
||||
import RenderToolInput from './render/RenderToolInput';
|
||||
import RenderOutput from './render/RenderOutput';
|
||||
import CodeEditor from '@fastgpt/web/components/common/Textarea/CodeEditor';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { JS_TEMPLATE } from '@fastgpt/global/core/workflow/template/system/sandbox/constants';
|
||||
|
||||
const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { workflowT } = useI18n();
|
||||
const { nodeId, inputs, outputs } = data;
|
||||
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
|
||||
const { ConfirmModal, openConfirm } = useConfirm({
|
||||
content: workflowT('code.Reset template confirm')
|
||||
});
|
||||
|
||||
const CustomComponent = useMemo(() => {
|
||||
return {
|
||||
[NodeInputKeyEnum.code]: (item: FlowNodeInputItemType) => {
|
||||
return (
|
||||
<Box>
|
||||
<Flex mb={1} alignItems={'flex-end'}>
|
||||
<Box flex={'1'}>Javascript{workflowT('Code')}</Box>
|
||||
<Box
|
||||
cursor={'pointer'}
|
||||
color={'primary.500'}
|
||||
fontSize={'xs'}
|
||||
onClick={openConfirm(() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: JS_TEMPLATE
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
{workflowT('code.Reset template')}
|
||||
</Box>
|
||||
</Flex>
|
||||
<CodeEditor
|
||||
bg={'white'}
|
||||
borderRadius={'sm'}
|
||||
value={item.value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [nodeId, onChangeNode, openConfirm, workflowT]);
|
||||
|
||||
return (
|
||||
<NodeCard minW={'400px'} selected={selected} {...data}>
|
||||
{toolInputs.length > 0 && (
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle text={t('core.module.tool.Tool input')} />
|
||||
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
<Container>
|
||||
<IOTitle text={t('common.Input')} />
|
||||
<RenderInput
|
||||
nodeId={nodeId}
|
||||
flowInputList={commonInputs}
|
||||
CustomComponent={CustomComponent}
|
||||
/>
|
||||
</Container>
|
||||
<Container>
|
||||
<IOTitle text={t('common.Output')} />
|
||||
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
<ConfirmModal />
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeCode);
|
||||
@@ -1,120 +0,0 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import Container from '../components/Container';
|
||||
import RenderInput from './render/RenderInput';
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import { WorkflowIOValueTypeEnum, NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { getOneQuoteInputTemplate } from '@fastgpt/global/core/workflow/template/system/datasetConcat';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import MySlider from '@/components/Slider';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import RenderOutput from './render/RenderOutput';
|
||||
import IOTitle from '../components/IOTitle';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
|
||||
const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { llmModelList } = useSystemStore();
|
||||
const { nodeId, inputs, outputs } = data;
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const quotes = useMemo(
|
||||
() => inputs.filter((item) => item.valueType === WorkflowIOValueTypeEnum.datasetQuote),
|
||||
[inputs]
|
||||
);
|
||||
|
||||
const tokenLimit = useMemo(() => {
|
||||
let maxTokens = 3000;
|
||||
|
||||
nodeList.forEach((item) => {
|
||||
if (item.flowNodeType === FlowNodeTypeEnum.chatNode) {
|
||||
const model =
|
||||
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
|
||||
const quoteMaxToken =
|
||||
llmModelList.find((item) => item.model === model)?.quoteMaxToken || 3000;
|
||||
|
||||
maxTokens = Math.max(maxTokens, quoteMaxToken);
|
||||
}
|
||||
});
|
||||
|
||||
return maxTokens;
|
||||
}, [llmModelList, nodeList]);
|
||||
|
||||
const onAddField = useCallback(() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'addInput',
|
||||
value: getOneQuoteInputTemplate({ index: quotes.length + 1 })
|
||||
});
|
||||
}, [nodeId, onChangeNode, quotes.length]);
|
||||
|
||||
const CustomComponent = useMemo(() => {
|
||||
return {
|
||||
[NodeInputKeyEnum.datasetMaxTokens]: (item: FlowNodeInputItemType) => (
|
||||
<Box px={2}>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '100', value: 100 },
|
||||
{ label: tokenLimit, value: tokenLimit }
|
||||
]}
|
||||
width={'100%'}
|
||||
min={100}
|
||||
max={tokenLimit}
|
||||
step={50}
|
||||
value={item.value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
customComponent: (item: FlowNodeInputItemType) => (
|
||||
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
|
||||
<Box position={'relative'} fontWeight={'medium'} color={'myGray.600'}>
|
||||
{t('core.workflow.Dataset quote')}
|
||||
</Box>
|
||||
<Box flex={'1 0 0'} />
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
iconSpacing={1}
|
||||
size={'sm'}
|
||||
mr={'-5px'}
|
||||
onClick={onAddField}
|
||||
>
|
||||
{t('common.Add New')}
|
||||
</Button>
|
||||
</Flex>
|
||||
)
|
||||
};
|
||||
}, [nodeId, onAddField, onChangeNode, t, tokenLimit]);
|
||||
|
||||
return (
|
||||
<NodeCard minW={'400px'} selected={selected} {...data}>
|
||||
<Container position={'relative'}>
|
||||
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
|
||||
{/* {RenderQuoteList} */}
|
||||
</Container>
|
||||
<Container>
|
||||
<IOTitle text={t('common.Output')} />
|
||||
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeDatasetConcat);
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
|
||||
const NodeEmpty = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
return <NodeCard selected={selected} {...data}></NodeCard>;
|
||||
};
|
||||
|
||||
export default React.memo(NodeEmpty);
|
||||
@@ -1,131 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
Flex,
|
||||
Switch,
|
||||
Input,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { fnValueTypeSelect } from '@/web/core/workflow/constants/dataType';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
|
||||
export const defaultField: ContextExtractAgentItemType = {
|
||||
valueType: 'string',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
desc: '',
|
||||
key: '',
|
||||
enum: ''
|
||||
};
|
||||
|
||||
const ExtractFieldModal = ({
|
||||
defaultField,
|
||||
onClose,
|
||||
onSubmit
|
||||
}: {
|
||||
defaultField: ContextExtractAgentItemType;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: ContextExtractAgentItemType) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { register, setValue, handleSubmit, watch } = useForm<ContextExtractAgentItemType>({
|
||||
defaultValues: defaultField
|
||||
});
|
||||
const required = watch('required');
|
||||
const valueType = watch('valueType');
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
iconSrc="/imgs/workflow/extract.png"
|
||||
title={t('core.module.extract.Field Setting Title')}
|
||||
onClose={onClose}
|
||||
w={['90vw', '500px']}
|
||||
>
|
||||
<ModalBody>
|
||||
<Flex mt={2} alignItems={'center'}>
|
||||
<Flex alignItems={'center'} flex={['1 0 80px', '1 0 100px']}>
|
||||
<FormLabel>{t('core.module.extract.Required')}</FormLabel>
|
||||
<QuestionTip ml={1} label={t('core.module.extract.Required Description')}></QuestionTip>
|
||||
</Flex>
|
||||
<Switch {...register('required')} />
|
||||
</Flex>
|
||||
{required && (
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<FormLabel flex={['0 0 80px', '0 0 100px']}>{t('core.module.Default value')}</FormLabel>
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
placeholder={t('core.module.Default value placeholder')}
|
||||
{...register('defaultValue')}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<FormLabel flex={['0 0 80px', '0 0 100px']}>{t('core.module.Data Type')}</FormLabel>
|
||||
<Box flex={'1 0 0'}>
|
||||
<MySelect
|
||||
list={fnValueTypeSelect}
|
||||
value={valueType}
|
||||
onchange={(e: any) => {
|
||||
setValue('valueType', e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<FormLabel flex={['0 0 80px', '0 0 100px']}>{t('Field name')}</FormLabel>
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
placeholder="name/age/sql"
|
||||
{...register('key', { required: true })}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<FormLabel flex={['0 0 80px', '0 0 100px']}>
|
||||
{t('core.module.Field Description')}
|
||||
</FormLabel>
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
placeholder={t('core.module.extract.Field Description Placeholder')}
|
||||
{...register('desc', { required: true })}
|
||||
/>
|
||||
</Flex>
|
||||
{(valueType === 'string' || valueType === 'number') && (
|
||||
<Box mt={5}>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel>
|
||||
{t('core.module.extract.Enum Value')}({t('common.choosable')})
|
||||
</FormLabel>
|
||||
<QuestionTip ml={1} label={t('core.module.extract.Enum Description')}></QuestionTip>
|
||||
</Flex>
|
||||
|
||||
<Textarea
|
||||
rows={5}
|
||||
bg={'myGray.50'}
|
||||
placeholder={'apple\npeach\nwatermelon'}
|
||||
{...register('enum')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onClick={handleSubmit(onSubmit)}>{t('common.Confirm')}</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ExtractFieldModal);
|
||||
@@ -1,241 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Flex
|
||||
} from '@chakra-ui/react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import NodeCard from '../render/NodeCard';
|
||||
import Container from '../../components/Container';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import RenderOutput from '../render/RenderOutput';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import ExtractFieldModal, { defaultField } from './ExtractFieldModal';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import RenderToolInput from '../render/RenderToolInput';
|
||||
import {
|
||||
FlowNodeInputItemType,
|
||||
FlowNodeOutputItemType
|
||||
} from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import IOTitle from '../../components/IOTitle';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../../context';
|
||||
|
||||
const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
|
||||
const { inputs, outputs, nodeId } = data;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
|
||||
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
|
||||
const [editExtractFiled, setEditExtractField] = useState<ContextExtractAgentItemType>();
|
||||
|
||||
const CustomComponent = useMemo(
|
||||
() => ({
|
||||
[NodeInputKeyEnum.extractKeys]: ({
|
||||
value: extractKeys = [],
|
||||
...props
|
||||
}: Omit<FlowNodeInputItemType, 'value'> & {
|
||||
value?: ContextExtractAgentItemType[];
|
||||
}) => (
|
||||
<Box>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'1 0 0'} fontWeight={'medium'} color={'myGray.600'}>
|
||||
{t('core.module.extract.Target field')}
|
||||
</Box>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'whitePrimary'}
|
||||
leftIcon={<AddIcon fontSize={'10px'} />}
|
||||
onClick={() => setEditExtractField(defaultField)}
|
||||
>
|
||||
{t('core.module.extract.Add field')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Box
|
||||
mt={2}
|
||||
borderRadius={'md'}
|
||||
overflow={'hidden'}
|
||||
borderWidth={'1px'}
|
||||
borderBottom="none"
|
||||
>
|
||||
<TableContainer>
|
||||
<Table bg={'white'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th bg={'myGray.50'} borderRadius={'none !important'}>
|
||||
字段名
|
||||
</Th>
|
||||
<Th bg={'myGray.50'}>字段描述</Th>
|
||||
<Th bg={'myGray.50'}>必须</Th>
|
||||
<Th bg={'myGray.50'} borderRadius={'none !important'}></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{extractKeys.map((item, index) => (
|
||||
<Tr
|
||||
key={index}
|
||||
position={'relative'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
wordBreak={'break-all'}
|
||||
>
|
||||
<Td>{item.key}</Td>
|
||||
<Td>{item.desc}</Td>
|
||||
<Td>{item.required ? '✔' : ''}</Td>
|
||||
<Td whiteSpace={'nowrap'}>
|
||||
<MyIcon
|
||||
mr={3}
|
||||
name={'common/settingLight'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
setEditExtractField(item);
|
||||
}}
|
||||
/>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.extractKeys,
|
||||
value: {
|
||||
...props,
|
||||
value: extractKeys.filter((extract) => item.key !== extract.key)
|
||||
}
|
||||
});
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'delOutput',
|
||||
key: item.key
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}),
|
||||
[nodeId, onChangeNode, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<NodeCard minW={'400px'} {...data}>
|
||||
{toolInputs.length > 0 && (
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle text={t('core.module.tool.Tool input')} />
|
||||
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle text={t('common.Input')} />
|
||||
<RenderInput
|
||||
nodeId={nodeId}
|
||||
flowInputList={commonInputs}
|
||||
CustomComponent={CustomComponent}
|
||||
/>
|
||||
</Container>
|
||||
</>
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle text={t('common.Output')} />
|
||||
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
</>
|
||||
|
||||
{!!editExtractFiled && (
|
||||
<ExtractFieldModal
|
||||
defaultField={editExtractFiled}
|
||||
onClose={() => setEditExtractField(undefined)}
|
||||
onSubmit={(data) => {
|
||||
const extracts: ContextExtractAgentItemType[] =
|
||||
inputs.find((item) => item.key === NodeInputKeyEnum.extractKeys)?.value || [];
|
||||
|
||||
const exists = extracts.find((item) => item.key === editExtractFiled.key);
|
||||
|
||||
const newInputs = exists
|
||||
? extracts.map((item) => (item.key === editExtractFiled.key ? data : item))
|
||||
: extracts.concat(data);
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.extractKeys,
|
||||
value: {
|
||||
...inputs.find((input) => input.key === NodeInputKeyEnum.extractKeys),
|
||||
value: newInputs
|
||||
}
|
||||
});
|
||||
|
||||
const newOutput: FlowNodeOutputItemType = {
|
||||
id: getNanoid(),
|
||||
key: data.key,
|
||||
label: `提取结果-${data.desc}`,
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
};
|
||||
|
||||
if (exists) {
|
||||
if (editExtractFiled.key === data.key) {
|
||||
const output = outputs.find((output) => output.key === data.key);
|
||||
// update
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateOutput',
|
||||
key: data.key,
|
||||
value: {
|
||||
...output,
|
||||
label: `提取结果-${data.desc}`
|
||||
}
|
||||
});
|
||||
} else {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'replaceOutput',
|
||||
key: editExtractFiled.key,
|
||||
value: newOutput
|
||||
});
|
||||
}
|
||||
} else {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'addOutput',
|
||||
value: newOutput
|
||||
});
|
||||
}
|
||||
|
||||
setEditExtractField(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NodeExtract);
|
||||
@@ -1,158 +0,0 @@
|
||||
import React from 'react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { ModalBody, Button, ModalFooter, useDisclosure, Textarea, Box } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import parse from '@bany/curl-to-json';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../../context';
|
||||
|
||||
type RequestMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';
|
||||
const methodMap: { [K in RequestMethod]: string } = {
|
||||
get: 'GET',
|
||||
post: 'POST',
|
||||
put: 'PUT',
|
||||
delete: 'DELETE',
|
||||
patch: 'PATCH'
|
||||
};
|
||||
|
||||
const CurlImportModal = ({
|
||||
nodeId,
|
||||
inputs,
|
||||
onClose
|
||||
}: {
|
||||
nodeId: string;
|
||||
inputs: FlowNodeInputItemType[];
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const { register, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
curlContent: ''
|
||||
}
|
||||
});
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleFileProcessing = async (content: string) => {
|
||||
try {
|
||||
const requestUrl = inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl);
|
||||
const requestMethod = inputs.find((item) => item.key === NodeInputKeyEnum.httpMethod);
|
||||
const params = inputs.find((item) => item.key === NodeInputKeyEnum.httpParams);
|
||||
const headers = inputs.find((item) => item.key === NodeInputKeyEnum.httpHeaders);
|
||||
const jsonBody = inputs.find((item) => item.key === NodeInputKeyEnum.httpJsonBody);
|
||||
|
||||
const parsed = parse(content);
|
||||
if (!parsed.url) {
|
||||
throw new Error('url not found');
|
||||
}
|
||||
|
||||
const newParams = Object.keys(parsed.params || {}).map((key) => ({
|
||||
key,
|
||||
value: parsed.params?.[key],
|
||||
type: 'string'
|
||||
}));
|
||||
const newHeaders = Object.keys(parsed.header || {}).map((key) => ({
|
||||
key,
|
||||
value: parsed.header?.[key],
|
||||
type: 'string'
|
||||
}));
|
||||
const newBody = JSON.stringify(parsed.data, null, 2);
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.httpReqUrl,
|
||||
value: {
|
||||
...requestUrl,
|
||||
value: parsed.url
|
||||
}
|
||||
});
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.httpMethod,
|
||||
value: {
|
||||
...requestMethod,
|
||||
value: methodMap[parsed.method?.toLowerCase() as RequestMethod] || 'GET'
|
||||
}
|
||||
});
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.httpParams,
|
||||
value: {
|
||||
...params,
|
||||
value: newParams
|
||||
}
|
||||
});
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.httpHeaders,
|
||||
value: {
|
||||
...headers,
|
||||
value: newHeaders
|
||||
}
|
||||
});
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.httpJsonBody,
|
||||
value: {
|
||||
...jsonBody,
|
||||
value: newBody
|
||||
}
|
||||
});
|
||||
|
||||
onClose();
|
||||
|
||||
toast({
|
||||
title: t('common.Import success'),
|
||||
status: 'success'
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: t('common.Import failed'),
|
||||
description: error.message,
|
||||
status: 'error'
|
||||
});
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
iconSrc="modal/edit"
|
||||
title={t('core.module.http.curl import')}
|
||||
w={600}
|
||||
>
|
||||
<ModalBody>
|
||||
<Textarea
|
||||
rows={20}
|
||||
mt={2}
|
||||
{...register('curlContent')}
|
||||
placeholder={t('core.module.http.curl import placeholder')}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onClick={handleSubmit((data) => handleFileProcessing(data.curlContent))}>
|
||||
{t('common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CurlImportModal);
|
||||
@@ -1,681 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState, useTransition } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from '../render/NodeCard';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import Container from '../../components/Container';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import RenderOutput from '../render/RenderOutput';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Input,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Button,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Tabs from '@/components/Tabs';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import JSONEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
|
||||
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
|
||||
import { EditorVariablePickerType } from '@fastgpt/web/components/common/Textarea/PromptEditor/type';
|
||||
import HttpInput from '@fastgpt/web/components/common/Input/HttpInput';
|
||||
import dynamic from 'next/dynamic';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import RenderToolInput from '../render/RenderToolInput';
|
||||
import IOTitle from '../../components/IOTitle';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../../context';
|
||||
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
const CurlImportModal = dynamic(() => import('./CurlImportModal'));
|
||||
|
||||
export const HttpHeaders = [
|
||||
{ key: 'A-IM', label: 'A-IM' },
|
||||
{ key: 'Accept', label: 'Accept' },
|
||||
{ key: 'Accept-Charset', label: 'Accept-Charset' },
|
||||
{ key: 'Accept-Encoding', label: 'Accept-Encoding' },
|
||||
{ key: 'Accept-Language', label: 'Accept-Language' },
|
||||
{ key: 'Accept-Datetime', label: 'Accept-Datetime' },
|
||||
{ key: 'Access-Control-Request-Method', label: 'Access-Control-Request-Method' },
|
||||
{ key: 'Access-Control-Request-Headers', label: 'Access-Control-Request-Headers' },
|
||||
{ key: 'Authorization', label: 'Authorization' },
|
||||
{ key: 'Cache-Control', label: 'Cache-Control' },
|
||||
{ key: 'Connection', label: 'Connection' },
|
||||
{ key: 'Content-Length', label: 'Content-Length' },
|
||||
{ key: 'Content-Type', label: 'Content-Type' },
|
||||
{ key: 'Cookie', label: 'Cookie' },
|
||||
{ key: 'Date', label: 'Date' },
|
||||
{ key: 'Expect', label: 'Expect' },
|
||||
{ key: 'Forwarded', label: 'Forwarded' },
|
||||
{ key: 'From', label: 'From' },
|
||||
{ key: 'Host', label: 'Host' },
|
||||
{ key: 'If-Match', label: 'If-Match' },
|
||||
{ key: 'If-Modified-Since', label: 'If-Modified-Since' },
|
||||
{ key: 'If-None-Match', label: 'If-None-Match' },
|
||||
{ key: 'If-Range', label: 'If-Range' },
|
||||
{ key: 'If-Unmodified-Since', label: 'If-Unmodified-Since' },
|
||||
{ key: 'Max-Forwards', label: 'Max-Forwards' },
|
||||
{ key: 'Origin', label: 'Origin' },
|
||||
{ key: 'Pragma', label: 'Pragma' },
|
||||
{ key: 'Proxy-Authorization', label: 'Proxy-Authorization' },
|
||||
{ key: 'Range', label: 'Range' },
|
||||
{ key: 'Referer', label: 'Referer' },
|
||||
{ key: 'TE', label: 'TE' },
|
||||
{ key: 'User-Agent', label: 'User-Agent' },
|
||||
{ key: 'Upgrade', label: 'Upgrade' },
|
||||
{ key: 'Via', label: 'Via' },
|
||||
{ key: 'Warning', label: 'Warning' },
|
||||
{ key: 'Dnt', label: 'Dnt' },
|
||||
{ key: 'X-Requested-With', label: 'X-Requested-With' },
|
||||
{ key: 'X-CSRF-Token', label: 'X-CSRF-Token' }
|
||||
];
|
||||
|
||||
enum TabEnum {
|
||||
params = 'params',
|
||||
headers = 'headers',
|
||||
body = 'body'
|
||||
}
|
||||
export type PropsArrType = {
|
||||
key: string;
|
||||
type: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
|
||||
nodeId,
|
||||
inputs
|
||||
}: {
|
||||
nodeId: string;
|
||||
inputs: FlowNodeInputItemType[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure();
|
||||
|
||||
const requestMethods = inputs.find((item) => item.key === NodeInputKeyEnum.httpMethod);
|
||||
const requestUrl = inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl);
|
||||
|
||||
const onChangeUrl = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.httpReqUrl,
|
||||
value: {
|
||||
...requestUrl,
|
||||
value: e.target.value
|
||||
}
|
||||
});
|
||||
};
|
||||
const onBlurUrl = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
// 拆分params和url
|
||||
const url = val.split('?')[0];
|
||||
const params = val.split('?')[1];
|
||||
if (params) {
|
||||
const paramsArr = params.split('&');
|
||||
const paramsObj = paramsArr.reduce((acc, cur) => {
|
||||
const [key, value] = cur.split('=');
|
||||
return {
|
||||
...acc,
|
||||
[key]: value
|
||||
};
|
||||
}, {});
|
||||
const inputParams = inputs.find((item) => item.key === NodeInputKeyEnum.httpParams);
|
||||
|
||||
if (!inputParams || Object.keys(paramsObj).length === 0) return;
|
||||
|
||||
const concatParams: PropsArrType[] = inputParams?.value || [];
|
||||
Object.entries(paramsObj).forEach(([key, value]) => {
|
||||
if (!concatParams.find((item) => item.key === key)) {
|
||||
concatParams.push({ key, value: value as string, type: 'string' });
|
||||
}
|
||||
});
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.httpParams,
|
||||
value: {
|
||||
...inputParams,
|
||||
value: concatParams
|
||||
}
|
||||
});
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.httpReqUrl,
|
||||
value: {
|
||||
...requestUrl,
|
||||
value: url
|
||||
}
|
||||
});
|
||||
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('core.module.http.Url and params have been split')
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={2} display={'flex'} justifyContent={'space-between'}>
|
||||
<Box fontWeight={'medium'} color={'myGray.600'}>
|
||||
{t('core.module.Http request settings')}
|
||||
</Box>
|
||||
<Button variant={'link'} onClick={onOpenCurl}>
|
||||
{t('core.module.http.curl import')}
|
||||
</Button>
|
||||
</Box>
|
||||
<Flex alignItems={'center'} className="nodrag">
|
||||
<MySelect
|
||||
h={'34px'}
|
||||
w={'88px'}
|
||||
bg={'white'}
|
||||
width={'100%'}
|
||||
value={requestMethods?.value}
|
||||
list={[
|
||||
{
|
||||
label: 'GET',
|
||||
value: 'GET'
|
||||
},
|
||||
{
|
||||
label: 'POST',
|
||||
value: 'POST'
|
||||
},
|
||||
{
|
||||
label: 'PUT',
|
||||
value: 'PUT'
|
||||
},
|
||||
{
|
||||
label: 'DELETE',
|
||||
value: 'DELETE'
|
||||
},
|
||||
{
|
||||
label: 'PATCH',
|
||||
value: 'PATCH'
|
||||
}
|
||||
]}
|
||||
onchange={(e) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.httpMethod,
|
||||
value: {
|
||||
...requestMethods,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
flex={'1 0 0'}
|
||||
ml={2}
|
||||
h={'34px'}
|
||||
bg={'white'}
|
||||
value={requestUrl?.value || ''}
|
||||
placeholder={t('core.module.input.label.Http Request Url')}
|
||||
fontSize={'xs'}
|
||||
onChange={onChangeUrl}
|
||||
onBlur={onBlurUrl}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{isOpenCurl && <CurlImportModal nodeId={nodeId} inputs={inputs} onClose={onCloseCurl} />}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
export function RenderHttpProps({
|
||||
nodeId,
|
||||
inputs
|
||||
}: {
|
||||
nodeId: string;
|
||||
inputs: FlowNodeInputItemType[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedTab, setSelectedTab] = useState(TabEnum.params);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const requestMethods = inputs.find((item) => item.key === NodeInputKeyEnum.httpMethod)?.value;
|
||||
const params = inputs.find((item) => item.key === NodeInputKeyEnum.httpParams);
|
||||
const headers = inputs.find((item) => item.key === NodeInputKeyEnum.httpHeaders);
|
||||
const jsonBody = inputs.find((item) => item.key === NodeInputKeyEnum.httpJsonBody);
|
||||
|
||||
const paramsLength = params?.value?.length || 0;
|
||||
const headersLength = headers?.value?.length || 0;
|
||||
|
||||
// get variable
|
||||
const variables = useMemo(() => {
|
||||
const globalVariables = getWorkflowGlobalVariables({
|
||||
nodes: nodeList,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
t
|
||||
});
|
||||
|
||||
const moduleVariables = formatEditorVariablePickerIcon(
|
||||
inputs
|
||||
.filter((input) => input.canEdit || input.toolDescription)
|
||||
.map((item) => ({
|
||||
key: item.key,
|
||||
label: item.label
|
||||
}))
|
||||
);
|
||||
|
||||
return [...moduleVariables, ...globalVariables];
|
||||
}, [appDetail.chatConfig, inputs, nodeList, t]);
|
||||
|
||||
const variableText = useMemo(() => {
|
||||
return variables
|
||||
.map((item) => `${item.key}${item.key !== item.label ? `(${item.label})` : ''}`)
|
||||
.join('\n');
|
||||
}, [variables]);
|
||||
|
||||
const stringifyVariables = useMemo(
|
||||
() =>
|
||||
JSON.stringify({
|
||||
params,
|
||||
headers,
|
||||
jsonBody,
|
||||
variables
|
||||
}),
|
||||
[headers, jsonBody, params, variables]
|
||||
);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
const { params, headers, jsonBody, variables } = JSON.parse(stringifyVariables);
|
||||
return (
|
||||
<Box>
|
||||
<Flex alignItems={'center'} mb={2} fontWeight={'medium'} color={'myGray.600'}>
|
||||
{t('core.module.Http request props')}
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('core.module.http.Props tip', { variable: variableText })}
|
||||
></QuestionTip>
|
||||
</Flex>
|
||||
<Tabs
|
||||
list={[
|
||||
{ label: <RenderPropsItem text="Params" num={paramsLength} />, id: TabEnum.params },
|
||||
...(!['GET', 'DELETE'].includes(requestMethods)
|
||||
? [
|
||||
{
|
||||
label: (
|
||||
<Flex alignItems={'center'}>
|
||||
Body
|
||||
{jsonBody?.value && <Box ml={1}>✅</Box>}
|
||||
</Flex>
|
||||
),
|
||||
id: TabEnum.body
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{ label: <RenderPropsItem text="Headers" num={headersLength} />, id: TabEnum.headers }
|
||||
]}
|
||||
activeId={selectedTab}
|
||||
onChange={(e) => setSelectedTab(e as any)}
|
||||
/>
|
||||
<Box bg={'white'} borderRadius={'md'}>
|
||||
{params &&
|
||||
headers &&
|
||||
jsonBody &&
|
||||
{
|
||||
[TabEnum.params]: (
|
||||
<RenderForm
|
||||
nodeId={nodeId}
|
||||
input={params}
|
||||
variables={variables}
|
||||
tabType={TabEnum.params}
|
||||
/>
|
||||
),
|
||||
[TabEnum.body]: <RenderJson nodeId={nodeId} variables={variables} input={jsonBody} />,
|
||||
[TabEnum.headers]: (
|
||||
<RenderForm
|
||||
nodeId={nodeId}
|
||||
input={headers}
|
||||
variables={variables}
|
||||
tabType={TabEnum.headers}
|
||||
/>
|
||||
)
|
||||
}[selectedTab]}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}, [
|
||||
headersLength,
|
||||
nodeId,
|
||||
paramsLength,
|
||||
requestMethods,
|
||||
selectedTab,
|
||||
stringifyVariables,
|
||||
t,
|
||||
variableText
|
||||
]);
|
||||
|
||||
return Render;
|
||||
}
|
||||
const RenderForm = ({
|
||||
nodeId,
|
||||
input,
|
||||
variables,
|
||||
tabType
|
||||
}: {
|
||||
nodeId: string;
|
||||
input: FlowNodeInputItemType;
|
||||
variables: EditorVariablePickerType[];
|
||||
tabType?: TabEnum;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const [list, setList] = useState<PropsArrType[]>(input.value || []);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
const [shouldUpdateNode, setShouldUpdateNode] = useState(false);
|
||||
|
||||
const leftVariables = useMemo(() => {
|
||||
return (tabType === TabEnum.headers ? HttpHeaders : variables).filter((variable) => {
|
||||
const existVariables = list.map((item) => item.key);
|
||||
return !existVariables.includes(variable.key);
|
||||
});
|
||||
}, [list, tabType, variables]);
|
||||
|
||||
useEffect(() => {
|
||||
setList(input.value || []);
|
||||
}, [input.value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldUpdateNode) {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: input.key,
|
||||
value: {
|
||||
...input,
|
||||
value: list
|
||||
}
|
||||
});
|
||||
setShouldUpdateNode(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [list]);
|
||||
|
||||
const handleKeyChange = useCallback(
|
||||
(index: number, newKey: string) => {
|
||||
setList((prevList) => {
|
||||
if (!newKey) {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('core.module.http.Key cannot be empty')
|
||||
});
|
||||
return prevList;
|
||||
}
|
||||
const checkExist = prevList.find((item, i) => i !== index && item.key == newKey);
|
||||
if (checkExist) {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('core.module.http.Key already exists')
|
||||
});
|
||||
return prevList;
|
||||
}
|
||||
return prevList.map((item, i) => (i === index ? { ...item, key: newKey } : item));
|
||||
});
|
||||
setShouldUpdateNode(true);
|
||||
},
|
||||
[t, toast]
|
||||
);
|
||||
|
||||
const handleAddNewProps = useCallback(
|
||||
(key: string, value: string = '') => {
|
||||
setList((prevList) => {
|
||||
if (!key) {
|
||||
return prevList;
|
||||
}
|
||||
|
||||
const checkExist = prevList.find((item) => item.key === key);
|
||||
if (checkExist) {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('core.module.http.Key already exists')
|
||||
});
|
||||
return prevList;
|
||||
}
|
||||
return [...prevList, { key, type: 'string', value }];
|
||||
});
|
||||
|
||||
setShouldUpdateNode(true);
|
||||
},
|
||||
[t, toast]
|
||||
);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<Box mt={2} borderRadius={'md'} overflow={'hidden'} borderWidth={'1px'} borderBottom={'none'}>
|
||||
<TableContainer overflowY={'visible'} overflowX={'unset'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th px={2} borderBottomLeftRadius={'none !important'}>
|
||||
{t('core.module.http.Props name')}
|
||||
</Th>
|
||||
<Th px={2} borderBottomRadius={'none !important'}>
|
||||
{t('core.module.http.Props value')}
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{list.map((item, index) => (
|
||||
<Tr key={`${input.key}${index}`}>
|
||||
<Td p={0} w={'150px'}>
|
||||
<HttpInput
|
||||
hasVariablePlugin={false}
|
||||
hasDropDownPlugin={tabType === TabEnum.headers}
|
||||
setDropdownValue={(value) => {
|
||||
handleKeyChange(index, value);
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
}}
|
||||
placeholder={t('core.module.http.Props name')}
|
||||
value={item.key}
|
||||
variables={leftVariables}
|
||||
onBlur={(val) => {
|
||||
handleKeyChange(index, val);
|
||||
}}
|
||||
updateTrigger={updateTrigger}
|
||||
/>
|
||||
</Td>
|
||||
<Td p={0}>
|
||||
<Box display={'flex'} alignItems={'center'}>
|
||||
<HttpInput
|
||||
placeholder={t('core.module.http.Props value')}
|
||||
value={item.value}
|
||||
variables={variables}
|
||||
onBlur={(val) => {
|
||||
setList((prevList) =>
|
||||
prevList.map((item, i) =>
|
||||
i === index ? { ...item, value: val } : item
|
||||
)
|
||||
);
|
||||
setShouldUpdateNode(true);
|
||||
}}
|
||||
/>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
w={'14px'}
|
||||
onClick={() => {
|
||||
setList((prevlist) => prevlist.filter((val) => val.key !== item.key));
|
||||
setShouldUpdateNode(true);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
<Tr>
|
||||
<Td p={0} w={'150px'}>
|
||||
<HttpInput
|
||||
hasVariablePlugin={false}
|
||||
hasDropDownPlugin={tabType === TabEnum.headers}
|
||||
setDropdownValue={(val) => {
|
||||
handleAddNewProps(val);
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
}}
|
||||
placeholder={t('core.module.http.Add props')}
|
||||
value={''}
|
||||
variables={leftVariables}
|
||||
updateTrigger={updateTrigger}
|
||||
onBlur={(val) => {
|
||||
handleAddNewProps(val);
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
<Td p={0}>
|
||||
<Box display={'flex'} alignItems={'center'}>
|
||||
<HttpInput />
|
||||
</Box>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
);
|
||||
}, [
|
||||
handleAddNewProps,
|
||||
handleKeyChange,
|
||||
input.key,
|
||||
leftVariables,
|
||||
list,
|
||||
t,
|
||||
tabType,
|
||||
updateTrigger,
|
||||
variables
|
||||
]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
const RenderJson = ({
|
||||
nodeId,
|
||||
input,
|
||||
variables
|
||||
}: {
|
||||
nodeId: string;
|
||||
input: FlowNodeInputItemType;
|
||||
variables: EditorVariablePickerType[];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const [_, startSts] = useTransition();
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<Box mt={1}>
|
||||
<JSONEditor
|
||||
bg={'white'}
|
||||
defaultHeight={200}
|
||||
resize
|
||||
value={input.value}
|
||||
placeholder={t('core.module.template.http body placeholder')}
|
||||
onChange={(e) => {
|
||||
startSts(() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: input.key,
|
||||
value: {
|
||||
...input,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
});
|
||||
}}
|
||||
variables={variables}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}, [input, nodeId, onChangeNode, t, variables]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
const RenderPropsItem = ({ text, num }: { text: string; num: number }) => {
|
||||
return (
|
||||
<Flex alignItems={'center'}>
|
||||
<Box>{text}</Box>
|
||||
{num > 0 && (
|
||||
<Box ml={1} borderRadius={'50%'} bg={'myGray.200'} px={2} py={'1px'}>
|
||||
{num}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeHttp = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { nodeId, inputs, outputs } = data;
|
||||
const splitToolInputs = useContextSelector(WorkflowContext, (v) => v.splitToolInputs);
|
||||
const { toolInputs, commonInputs, isTool } = splitToolInputs(inputs, nodeId);
|
||||
|
||||
const HttpMethodAndUrl = useMemoizedFn(() => (
|
||||
<RenderHttpMethodAndUrl nodeId={nodeId} inputs={inputs} />
|
||||
));
|
||||
const Headers = useMemoizedFn(() => <RenderHttpProps nodeId={nodeId} inputs={inputs} />);
|
||||
|
||||
const CustomComponents = useMemo(() => {
|
||||
return {
|
||||
[NodeInputKeyEnum.httpMethod]: HttpMethodAndUrl,
|
||||
[NodeInputKeyEnum.httpHeaders]: Headers
|
||||
};
|
||||
}, [Headers, HttpMethodAndUrl]);
|
||||
|
||||
return (
|
||||
<NodeCard minW={'350px'} selected={selected} {...data}>
|
||||
{isTool && (
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle text={t('core.module.tool.Tool input')} />
|
||||
<RenderToolInput nodeId={nodeId} inputs={toolInputs} canEdit />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle text={t('common.Input')} />
|
||||
<RenderInput
|
||||
nodeId={nodeId}
|
||||
flowInputList={commonInputs}
|
||||
CustomComponent={CustomComponents}
|
||||
/>
|
||||
</Container>
|
||||
</>
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle text={t('common.Output')} />
|
||||
<RenderOutput flowOutputList={outputs} nodeId={nodeId} />
|
||||
</Container>
|
||||
</>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeHttp);
|
||||
@@ -1,470 +0,0 @@
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import {
|
||||
DraggableProvided,
|
||||
DraggableStateSnapshot
|
||||
} from '@fastgpt/web/components/common/DndDrag/index';
|
||||
import Container from '../../components/Container';
|
||||
import { MinusIcon, SmallAddIcon } from '@chakra-ui/icons';
|
||||
import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ReferSelector, useReference } from '../render/RenderInput/templates/Reference';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import {
|
||||
VariableConditionEnum,
|
||||
allConditionList,
|
||||
arrayConditionList,
|
||||
booleanConditionList,
|
||||
numberConditionList,
|
||||
objectConditionList,
|
||||
stringConditionList
|
||||
} from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import React, { useMemo } from 'react';
|
||||
import { WorkflowContext } from '../../../context';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import MyInput from '@/components/MyInput';
|
||||
import { getElseIFLabel, getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||
import { SourceHandle } from '../render/Handle';
|
||||
import { Position, useReactFlow } from 'reactflow';
|
||||
import { getRefData } from '@/web/core/workflow/utils';
|
||||
import DragIcon from '@fastgpt/web/components/common/DndDrag/DragIcon';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
|
||||
const ListItem = ({
|
||||
provided,
|
||||
snapshot,
|
||||
conditionIndex,
|
||||
conditionItem,
|
||||
ifElseList,
|
||||
onUpdateIfElseList,
|
||||
nodeId
|
||||
}: {
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
conditionIndex: number;
|
||||
conditionItem: IfElseListItemType;
|
||||
ifElseList: IfElseListItemType[];
|
||||
onUpdateIfElseList: (value: IfElseListItemType[]) => void;
|
||||
nodeId: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { getZoom } = useReactFlow();
|
||||
const onDelEdge = useContextSelector(WorkflowContext, (v) => v.onDelEdge);
|
||||
const handleId = getHandleId(nodeId, 'source', getElseIFLabel(conditionIndex));
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
position={'relative'}
|
||||
transform={snapshot.isDragging ? `scale(${getZoom()})` : ''}
|
||||
transformOrigin={'top left'}
|
||||
>
|
||||
<Container w={snapshot.isDragging ? '' : 'full'} className="nodrag">
|
||||
<Flex mb={4} alignItems={'center'}>
|
||||
{ifElseList.length > 1 && <DragIcon provided={provided} />}
|
||||
<Box color={'black'} fontSize={'md'} ml={2}>
|
||||
{getElseIFLabel(conditionIndex)}
|
||||
</Box>
|
||||
{conditionItem.list?.length > 1 && (
|
||||
<Flex
|
||||
px={'2.5'}
|
||||
color={'primary.600'}
|
||||
fontWeight={'medium'}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
rounded={'md'}
|
||||
onClick={() => {
|
||||
onUpdateIfElseList(
|
||||
ifElseList.map((ifElse, index) => {
|
||||
if (index === conditionIndex) {
|
||||
return {
|
||||
...ifElse,
|
||||
condition: ifElse.condition === 'AND' ? 'OR' : 'AND'
|
||||
};
|
||||
}
|
||||
return ifElse;
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{conditionItem.condition}
|
||||
<MyIcon ml={1} boxSize={5} name="change" />
|
||||
</Flex>
|
||||
)}
|
||||
<Box flex={1}></Box>
|
||||
{ifElseList.length > 1 && (
|
||||
<MyIcon
|
||||
ml={2}
|
||||
boxSize={5}
|
||||
name="delete"
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
color={'myGray.400'}
|
||||
onClick={() => {
|
||||
onUpdateIfElseList(ifElseList.filter((_, index) => index !== conditionIndex));
|
||||
onDelEdge({
|
||||
nodeId,
|
||||
sourceHandle: handleId
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<Box>
|
||||
{conditionItem.list?.map((item, i) => {
|
||||
return (
|
||||
<Box key={i}>
|
||||
{/* condition list */}
|
||||
<Flex gap={2} mb={2} alignItems={'center'}>
|
||||
{/* variable reference */}
|
||||
<Box minW={'250px'}>
|
||||
<Reference
|
||||
nodeId={nodeId}
|
||||
variable={item.variable}
|
||||
onSelect={(e) => {
|
||||
onUpdateIfElseList(
|
||||
ifElseList.map((ifElse, index) => {
|
||||
if (index === conditionIndex) {
|
||||
return {
|
||||
...ifElse,
|
||||
list: ifElse.list.map((item, index) => {
|
||||
if (index === i) {
|
||||
return {
|
||||
...item,
|
||||
variable: e,
|
||||
condition: undefined
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
};
|
||||
}
|
||||
return ifElse;
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{/* condition select */}
|
||||
<Box w={'130px'} flex={1}>
|
||||
<ConditionSelect
|
||||
condition={item.condition}
|
||||
variable={item.variable}
|
||||
onSelect={(e) => {
|
||||
onUpdateIfElseList(
|
||||
ifElseList.map((ifElse, index) => {
|
||||
if (index === conditionIndex) {
|
||||
return {
|
||||
...ifElse,
|
||||
list: ifElse.list.map((item, index) => {
|
||||
if (index === i) {
|
||||
return {
|
||||
...item,
|
||||
condition: e
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
};
|
||||
}
|
||||
return ifElse;
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{/* value */}
|
||||
<Box w={'200px'}>
|
||||
<ConditionValueInput
|
||||
value={item.value}
|
||||
condition={item.condition}
|
||||
variable={item.variable}
|
||||
onChange={(e) => {
|
||||
onUpdateIfElseList(
|
||||
ifElseList.map((ifElse, index) => {
|
||||
return {
|
||||
...ifElse,
|
||||
list:
|
||||
index === conditionIndex
|
||||
? ifElse.list.map((item, index) => {
|
||||
if (index === i) {
|
||||
return {
|
||||
...item,
|
||||
value: e
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
: ifElse.list
|
||||
};
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{/* delete */}
|
||||
{conditionItem.list.length > 1 && (
|
||||
<MinusIcon
|
||||
ml={2}
|
||||
boxSize={3}
|
||||
name="delete"
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
color={'myGray.400'}
|
||||
onClick={() => {
|
||||
onUpdateIfElseList(
|
||||
ifElseList.map((ifElse, index) => {
|
||||
if (index === conditionIndex) {
|
||||
return {
|
||||
...ifElse,
|
||||
list: ifElse.list.filter((_, index) => index !== i)
|
||||
};
|
||||
}
|
||||
return ifElse;
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onUpdateIfElseList(
|
||||
ifElseList.map((ifElse, index) => {
|
||||
if (index === conditionIndex) {
|
||||
return {
|
||||
...ifElse,
|
||||
list: ifElse.list.concat({
|
||||
variable: undefined,
|
||||
condition: undefined,
|
||||
value: undefined
|
||||
})
|
||||
};
|
||||
}
|
||||
return ifElse;
|
||||
})
|
||||
);
|
||||
}}
|
||||
variant={'link'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
my={3}
|
||||
color={'primary.600'}
|
||||
>
|
||||
{t('core.module.input.add')}
|
||||
</Button>
|
||||
</Container>
|
||||
{!snapshot.isDragging && (
|
||||
<SourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={handleId}
|
||||
position={Position.Right}
|
||||
translate={[18, 0]}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}, [
|
||||
conditionIndex,
|
||||
conditionItem.condition,
|
||||
conditionItem.list,
|
||||
getZoom,
|
||||
handleId,
|
||||
ifElseList,
|
||||
nodeId,
|
||||
onDelEdge,
|
||||
onUpdateIfElseList,
|
||||
provided,
|
||||
snapshot.isDragging,
|
||||
t
|
||||
]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
style={{
|
||||
...provided.draggableProps.style,
|
||||
opacity: snapshot.isDragging ? 0.8 : 1
|
||||
}}
|
||||
>
|
||||
{Render}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ListItem);
|
||||
|
||||
const Reference = ({
|
||||
nodeId,
|
||||
variable,
|
||||
onSelect
|
||||
}: {
|
||||
nodeId: string;
|
||||
variable?: ReferenceValueProps;
|
||||
onSelect: (e: ReferenceValueProps) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { referenceList, formatValue } = useReference({
|
||||
nodeId,
|
||||
valueType: WorkflowIOValueTypeEnum.any,
|
||||
value: variable
|
||||
});
|
||||
|
||||
return (
|
||||
<ReferSelector
|
||||
placeholder={t('选择引用变量')}
|
||||
list={referenceList}
|
||||
value={formatValue}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/* Different data types have different options */
|
||||
const ConditionSelect = ({
|
||||
condition,
|
||||
variable,
|
||||
onSelect
|
||||
}: {
|
||||
condition?: VariableConditionEnum;
|
||||
variable?: ReferenceValueProps;
|
||||
onSelect: (e: VariableConditionEnum) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||
|
||||
// get condition type
|
||||
const { valueType, required } = useMemo(() => {
|
||||
return getRefData({
|
||||
variable,
|
||||
nodeList,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
t
|
||||
});
|
||||
}, [appDetail.chatConfig, nodeList, t, variable]);
|
||||
|
||||
const conditionList = useMemo(() => {
|
||||
if (valueType === WorkflowIOValueTypeEnum.string) return stringConditionList;
|
||||
if (valueType === WorkflowIOValueTypeEnum.number) return numberConditionList;
|
||||
if (valueType === WorkflowIOValueTypeEnum.boolean) return booleanConditionList;
|
||||
if (
|
||||
valueType === WorkflowIOValueTypeEnum.chatHistory ||
|
||||
valueType === WorkflowIOValueTypeEnum.datasetQuote ||
|
||||
valueType === WorkflowIOValueTypeEnum.dynamic ||
|
||||
valueType === WorkflowIOValueTypeEnum.selectApp ||
|
||||
valueType === WorkflowIOValueTypeEnum.arrayBoolean ||
|
||||
valueType === WorkflowIOValueTypeEnum.arrayNumber ||
|
||||
valueType === WorkflowIOValueTypeEnum.arrayObject ||
|
||||
valueType === WorkflowIOValueTypeEnum.arrayString
|
||||
)
|
||||
return arrayConditionList;
|
||||
if (valueType === WorkflowIOValueTypeEnum.object) return objectConditionList;
|
||||
|
||||
if (valueType === WorkflowIOValueTypeEnum.any) return allConditionList;
|
||||
|
||||
return [];
|
||||
}, [valueType]);
|
||||
const filterQuiredConditionList = useMemo(() => {
|
||||
if (required) {
|
||||
return conditionList.filter(
|
||||
(item) =>
|
||||
item.value !== VariableConditionEnum.isEmpty &&
|
||||
item.value !== VariableConditionEnum.isNotEmpty
|
||||
);
|
||||
}
|
||||
return conditionList;
|
||||
}, [conditionList, required]);
|
||||
|
||||
return (
|
||||
<MySelect
|
||||
className="nowheel"
|
||||
w={'100%'}
|
||||
list={filterQuiredConditionList}
|
||||
value={condition}
|
||||
onchange={onSelect}
|
||||
placeholder="选择条件"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
Different condition can be entered differently
|
||||
empty, notEmpty: forbid input
|
||||
boolean type: select true/false
|
||||
*/
|
||||
const ConditionValueInput = ({
|
||||
value = '',
|
||||
variable,
|
||||
condition,
|
||||
onChange
|
||||
}: {
|
||||
value?: string;
|
||||
variable?: ReferenceValueProps;
|
||||
condition?: VariableConditionEnum;
|
||||
onChange: (e: string) => void;
|
||||
}) => {
|
||||
const { workflowT } = useI18n();
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
|
||||
// get value type
|
||||
const valueType = useMemo(() => {
|
||||
if (!variable) return;
|
||||
const node = nodeList.find((node) => node.nodeId === variable[0]);
|
||||
|
||||
if (!node) return WorkflowIOValueTypeEnum.any;
|
||||
const output = node.outputs.find((item) => item.id === variable[1]);
|
||||
|
||||
if (!output) return WorkflowIOValueTypeEnum.any;
|
||||
return output.valueType;
|
||||
}, [nodeList, variable]);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
if (valueType === WorkflowIOValueTypeEnum.boolean) {
|
||||
return (
|
||||
<MySelect
|
||||
list={[
|
||||
{ label: 'True', value: 'true' },
|
||||
{ label: 'False', value: 'false' }
|
||||
]}
|
||||
onchange={onChange}
|
||||
value={value}
|
||||
placeholder={workflowT('ifelse.Select value')}
|
||||
isDisabled={
|
||||
condition === VariableConditionEnum.isEmpty ||
|
||||
condition === VariableConditionEnum.isNotEmpty
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<MyInput
|
||||
value={value}
|
||||
placeholder={
|
||||
condition === VariableConditionEnum.reg
|
||||
? '/^((+|00)86)?1[3-9]d{9}$/'
|
||||
: workflowT('ifelse.Input value')
|
||||
}
|
||||
w={'100%'}
|
||||
bg={'white'}
|
||||
isDisabled={
|
||||
condition === VariableConditionEnum.isEmpty ||
|
||||
condition === VariableConditionEnum.isNotEmpty
|
||||
}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, [condition, onChange, value, valueType, workflowT]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
@@ -1,137 +0,0 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import NodeCard from '../render/NodeCard';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { NodeProps, Position } from 'reactflow';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
|
||||
import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../../context';
|
||||
import Container from '../../components/Container';
|
||||
import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag/index';
|
||||
import { SourceHandle } from '../render/Handle';
|
||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||
import ListItem from './ListItem';
|
||||
import { IfElseResultEnum } from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
|
||||
|
||||
const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { nodeId, inputs = [] } = data;
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const elseHandleId = getHandleId(nodeId, 'source', IfElseResultEnum.ELSE);
|
||||
|
||||
const ifElseList = useMemo(
|
||||
() =>
|
||||
(inputs.find((input) => input.key === NodeInputKeyEnum.ifElseList)
|
||||
?.value as IfElseListItemType[]) || [],
|
||||
[inputs]
|
||||
);
|
||||
|
||||
const onUpdateIfElseList = useCallback(
|
||||
(value: IfElseListItemType[]) => {
|
||||
const ifElseListInput = inputs.find((input) => input.key === NodeInputKeyEnum.ifElseList);
|
||||
if (!ifElseListInput) return;
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.ifElseList,
|
||||
value: {
|
||||
...ifElseListInput,
|
||||
value
|
||||
}
|
||||
});
|
||||
},
|
||||
[inputs, nodeId, onChangeNode]
|
||||
);
|
||||
|
||||
return (
|
||||
<NodeCard selected={selected} maxW={'1000px'} {...data}>
|
||||
<Box px={4} cursor={'default'}>
|
||||
<DndDrag<IfElseListItemType>
|
||||
onDragEndCb={(list: IfElseListItemType[]) => onUpdateIfElseList(list)}
|
||||
dataList={ifElseList}
|
||||
renderClone={(provided, snapshot, rubric) => (
|
||||
<ListItem
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
conditionItem={ifElseList[rubric.source.index]}
|
||||
conditionIndex={rubric.source.index}
|
||||
ifElseList={ifElseList}
|
||||
onUpdateIfElseList={onUpdateIfElseList}
|
||||
nodeId={nodeId}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{(provided) => (
|
||||
<Box {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{ifElseList.map((conditionItem, conditionIndex) => (
|
||||
<Draggable
|
||||
key={conditionIndex}
|
||||
draggableId={conditionIndex.toString()}
|
||||
index={conditionIndex}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<ListItem
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
conditionItem={conditionItem}
|
||||
conditionIndex={conditionIndex}
|
||||
ifElseList={ifElseList}
|
||||
onUpdateIfElseList={onUpdateIfElseList}
|
||||
nodeId={nodeId}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</DndDrag>
|
||||
|
||||
<Container position={'relative'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box color={'black'} fontSize={'md'} ml={2}>
|
||||
{IfElseResultEnum.ELSE}
|
||||
</Box>
|
||||
<SourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={elseHandleId}
|
||||
position={Position.Right}
|
||||
translate={[26, 0]}
|
||||
/>
|
||||
</Flex>
|
||||
</Container>
|
||||
</Box>
|
||||
<Box py={3} px={6}>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
w={'full'}
|
||||
onClick={() => {
|
||||
const ifElseListInput = inputs.find(
|
||||
(input) => input.key === NodeInputKeyEnum.ifElseList
|
||||
);
|
||||
if (!ifElseListInput) return;
|
||||
|
||||
onUpdateIfElseList([
|
||||
...ifElseList,
|
||||
{
|
||||
condition: 'AND',
|
||||
list: [
|
||||
{
|
||||
variable: undefined,
|
||||
condition: undefined,
|
||||
value: undefined
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
}}
|
||||
>
|
||||
{t('core.module.input.Add Branch')}
|
||||
</Button>
|
||||
</Box>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeIfElse);
|
||||
@@ -1,332 +0,0 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import Container from '../components/Container';
|
||||
import { Box, Button, Center, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import { WorkflowIOValueTypeEnum, NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { getLafAppDetail } from '@/web/support/laf/api';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { getApiSchemaByUrl } from '@/web/core/plugin/api';
|
||||
import { getType, str2OpenApiSchema } from '@fastgpt/global/core/plugin/httpPlugin/utils';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { ChevronRightIcon } from '@chakra-ui/icons';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
import {
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeOutputTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import RenderToolInput from './render/RenderToolInput';
|
||||
import RenderInput from './render/RenderInput';
|
||||
import RenderOutput from './render/RenderOutput';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import {
|
||||
FlowNodeInputItemType,
|
||||
FlowNodeOutputItemType
|
||||
} from '@fastgpt/global/core/workflow/type/io';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import IOTitle from '../components/IOTitle';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
import { putUpdateTeam } from '@/web/support/user/team/api';
|
||||
|
||||
const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccountModal'));
|
||||
|
||||
const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { feConfigs } = useSystemStore();
|
||||
const { data, selected } = props;
|
||||
const { nodeId, inputs, outputs } = data;
|
||||
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const requestUrl = inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl);
|
||||
|
||||
const { userInfo, initUserInfo } = useUserStore();
|
||||
|
||||
const token = userInfo?.team.lafAccount?.token;
|
||||
const appid = userInfo?.team.lafAccount?.appid;
|
||||
|
||||
const {
|
||||
data: lafData,
|
||||
isLoading: isLoadingFunctions,
|
||||
refetch: refetchFunction
|
||||
} = useQuery(
|
||||
['getLafFunctionList'],
|
||||
async () => {
|
||||
// load laf app detail
|
||||
try {
|
||||
const appDetail = await getLafAppDetail(appid || '');
|
||||
// load laf app functions
|
||||
const schemaUrl = `https://${appDetail?.domain.domain}/_/api-docs?token=${appDetail?.openapi_token}`;
|
||||
|
||||
const schema = await getApiSchemaByUrl(schemaUrl);
|
||||
const openApiSchema = await str2OpenApiSchema(JSON.stringify(schema));
|
||||
const filterPostSchema = openApiSchema.pathData.filter((item) => item.method === 'post');
|
||||
|
||||
return {
|
||||
lafApp: appDetail,
|
||||
lafFunctions: filterPostSchema.map((item) => ({
|
||||
...item,
|
||||
requestUrl: `https://${appDetail?.domain.domain}${item.path}`
|
||||
}))
|
||||
};
|
||||
} catch (err) {
|
||||
await putUpdateTeam({
|
||||
lafAccount: { token: '', appid: '', pat: '' }
|
||||
});
|
||||
initUserInfo();
|
||||
}
|
||||
},
|
||||
{
|
||||
enabled: !!token && !!appid,
|
||||
onError(err) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: getErrText(err, '获取Laf函数列表失败')
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const lafFunctionSelectList = useMemo(
|
||||
() =>
|
||||
lafData?.lafFunctions.map((item) => {
|
||||
const functionName = item.path.slice(1);
|
||||
return {
|
||||
alias: functionName,
|
||||
label: item.description ? (
|
||||
<Box>
|
||||
<Box>{functionName}</Box>
|
||||
<Box fontSize={'xs'} color={'gray.500'}>
|
||||
{item.description}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
functionName
|
||||
),
|
||||
value: item.requestUrl
|
||||
};
|
||||
}) || [],
|
||||
[lafData?.lafFunctions]
|
||||
);
|
||||
|
||||
const selectedFunction = useMemo(
|
||||
() => lafFunctionSelectList.find((item) => item.value === requestUrl?.value)?.value,
|
||||
[lafFunctionSelectList, requestUrl?.value]
|
||||
);
|
||||
|
||||
const { mutate: onSyncParams, isLoading: isSyncing } = useRequest({
|
||||
mutationFn: async () => {
|
||||
await refetchFunction();
|
||||
const lafFunction = lafData?.lafFunctions.find(
|
||||
(item) => item.requestUrl === selectedFunction
|
||||
);
|
||||
|
||||
if (!lafFunction) return;
|
||||
|
||||
// update intro
|
||||
if (lafFunction.description) {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'attr',
|
||||
key: 'intro',
|
||||
value: lafFunction.description
|
||||
});
|
||||
}
|
||||
|
||||
// add input variables
|
||||
const bodyParams =
|
||||
lafFunction?.request?.content?.['application/json']?.schema?.properties || {};
|
||||
|
||||
const requiredParams =
|
||||
lafFunction?.request?.content?.['application/json']?.schema?.required || [];
|
||||
|
||||
const allParams = [
|
||||
...Object.keys(bodyParams).map((key) => ({
|
||||
name: key,
|
||||
desc: bodyParams[key].description,
|
||||
required: requiredParams?.includes(key) || false,
|
||||
value: `{{${key}}}`,
|
||||
type: getType(bodyParams[key])
|
||||
}))
|
||||
].filter((item) => !inputs.find((input) => input.key === item.name));
|
||||
|
||||
allParams.forEach((param) => {
|
||||
const newInput: FlowNodeInputItemType = {
|
||||
key: param.name,
|
||||
valueType: param.type,
|
||||
label: param.name,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.reference],
|
||||
required: param.required,
|
||||
description: param.desc || '',
|
||||
toolDescription: param.desc || '未设置参数描述',
|
||||
canEdit: true,
|
||||
editField: {
|
||||
key: true,
|
||||
valueType: true
|
||||
}
|
||||
};
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'addInput',
|
||||
value: newInput
|
||||
});
|
||||
});
|
||||
|
||||
/* add output variables */
|
||||
const responseParams =
|
||||
lafFunction?.response?.default.content?.['application/json'].schema.properties || {};
|
||||
const requiredResponseParams =
|
||||
lafFunction?.response?.default.content?.['application/json'].schema.required || [];
|
||||
|
||||
const allResponseParams = [
|
||||
...Object.keys(responseParams).map((key) => ({
|
||||
valueType: getType(responseParams[key]),
|
||||
name: key,
|
||||
desc: responseParams[key].description,
|
||||
required: requiredResponseParams?.includes(key) || false
|
||||
}))
|
||||
].filter((item) => !outputs.find((output) => output.key === item.name));
|
||||
allResponseParams.forEach((param) => {
|
||||
const newOutput: FlowNodeOutputItemType = {
|
||||
id: getNanoid(),
|
||||
key: param.name,
|
||||
valueType: param.valueType,
|
||||
label: param.name,
|
||||
type: FlowNodeOutputTypeEnum.dynamic,
|
||||
required: param.required,
|
||||
description: param.desc || ''
|
||||
};
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'addOutput',
|
||||
value: newOutput
|
||||
});
|
||||
});
|
||||
},
|
||||
successToast: t('common.Sync success')
|
||||
});
|
||||
|
||||
// not config laf
|
||||
if (!token || !appid) {
|
||||
return (
|
||||
<NodeCard minW={'350px'} selected={selected} {...data}>
|
||||
<ConfigLaf />
|
||||
</NodeCard>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<NodeCard minW={'350px'} selected={selected} {...data}>
|
||||
<Container>
|
||||
{/* select function */}
|
||||
<MySelect
|
||||
isLoading={isLoadingFunctions}
|
||||
list={lafFunctionSelectList}
|
||||
placeholder={t('core.module.laf.Select laf function')}
|
||||
onchange={(e) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.httpReqUrl,
|
||||
value: {
|
||||
...requestUrl,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
value={selectedFunction}
|
||||
/>
|
||||
{/* auto set params and go to edit */}
|
||||
{!!selectedFunction && (
|
||||
<Flex justifyContent={'flex-end'} mt={2} gap={2}>
|
||||
<Button isLoading={isSyncing} variant={'grayBase'} size={'sm'} onClick={onSyncParams}>
|
||||
{t('core.module.Laf sync params')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={'grayBase'}
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
const lafFunction = lafData?.lafFunctions.find(
|
||||
(item) => item.requestUrl === selectedFunction
|
||||
);
|
||||
|
||||
if (!lafFunction) return;
|
||||
const url = `${feConfigs.lafEnv}/app/${lafData?.lafApp?.appid}/function${lafFunction?.path}?templateid=FastGPT_Laf`;
|
||||
window.open(url, '_blank');
|
||||
}}
|
||||
>
|
||||
{t('plugin.go to laf')}
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</Container>
|
||||
{!!selectedFunction && <RenderIO {...props} />}
|
||||
</NodeCard>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default React.memo(NodeLaf);
|
||||
|
||||
const ConfigLaf = () => {
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
const { feConfigs } = useSystemStore();
|
||||
const {
|
||||
isOpen: isOpenLafConfig,
|
||||
onOpen: onOpenLafConfig,
|
||||
onClose: onCloseLafConfig
|
||||
} = useDisclosure();
|
||||
|
||||
return !!feConfigs?.lafEnv ? (
|
||||
<Center minH={150}>
|
||||
<Button onClick={onOpenLafConfig} variant={'whitePrimary'}>
|
||||
{t('plugin.Please bind laf accout first')} <ChevronRightIcon />
|
||||
</Button>
|
||||
|
||||
{isOpenLafConfig && feConfigs?.lafEnv && (
|
||||
<LafAccountModal defaultData={userInfo?.team.lafAccount} onClose={onCloseLafConfig} />
|
||||
)}
|
||||
</Center>
|
||||
) : (
|
||||
<Box>系统未配置Laf环境</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const RenderIO = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { nodeId, inputs, outputs } = data;
|
||||
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
|
||||
const { commonInputs, toolInputs, isTool } = splitToolInputs(inputs, nodeId);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isTool && (
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle text={t('core.module.tool.Tool input')} />
|
||||
<RenderToolInput nodeId={nodeId} inputs={toolInputs} canEdit />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle text={t('common.Input')} />
|
||||
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
|
||||
</Container>
|
||||
</>
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle text={t('common.Output')} />
|
||||
<RenderOutput flowOutputList={outputs} nodeId={nodeId} />
|
||||
</Container>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,225 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
FlowNodeInputItemType,
|
||||
FlowNodeOutputItemType
|
||||
} from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import Container from '../components/Container';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
|
||||
import type {
|
||||
EditInputFieldMapType,
|
||||
EditNodeFieldType
|
||||
} from '@fastgpt/global/core/workflow/node/type.d';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import {
|
||||
FlowNodeInputMap,
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeOutputTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
|
||||
import VariableTable from '../nodes/render/VariableTable';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
|
||||
const defaultCreateField: EditNodeFieldType = {
|
||||
label: '',
|
||||
key: '',
|
||||
description: '',
|
||||
inputType: FlowNodeInputTypeEnum.reference,
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
required: true
|
||||
};
|
||||
const createEditField: EditInputFieldMapType = {
|
||||
key: true,
|
||||
description: true,
|
||||
required: true,
|
||||
valueType: true,
|
||||
inputType: true
|
||||
};
|
||||
const dynamicInputEditField: EditInputFieldMapType = {
|
||||
key: true
|
||||
};
|
||||
|
||||
const NodePluginInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { nodeId, inputs, outputs } = data;
|
||||
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const [createField, setCreateField] = useState<EditNodeFieldType>();
|
||||
const [editField, setEditField] = useState<EditNodeFieldType>();
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<NodeCard
|
||||
minW={'300px'}
|
||||
selected={selected}
|
||||
menuForbid={{
|
||||
rename: true,
|
||||
copy: true,
|
||||
delete: true
|
||||
}}
|
||||
{...data}
|
||||
>
|
||||
<Container mt={1}>
|
||||
<Flex className="nodrag" cursor={'default'} alignItems={'center'} mb={3}>
|
||||
<Box>{t('core.workflow.Custom inputs')}</Box>
|
||||
<Box flex={'1 0 0'} />
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
iconSpacing={1}
|
||||
size={'sm'}
|
||||
onClick={() => setCreateField(defaultCreateField)}
|
||||
>
|
||||
{t('common.Add New')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<VariableTable
|
||||
fieldEditType={createEditField}
|
||||
keys={inputs.map((input) => input.key)}
|
||||
onCloseFieldEdit={() => {
|
||||
setCreateField(undefined);
|
||||
setEditField(undefined);
|
||||
}}
|
||||
variables={inputs.map((input) => {
|
||||
const inputType = input.renderTypeList[0];
|
||||
return {
|
||||
icon: FlowNodeInputMap[inputType]?.icon as string,
|
||||
label: t(input.label),
|
||||
type: input.valueType ? t(FlowValueTypeMap[input.valueType]?.label) : '-',
|
||||
key: input.key
|
||||
};
|
||||
})}
|
||||
createField={createField}
|
||||
onCreate={({ data }) => {
|
||||
if (!data.key || !data.inputType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newInput: FlowNodeInputItemType = {
|
||||
key: data.key,
|
||||
valueType: data.valueType,
|
||||
label: data.label || '',
|
||||
renderTypeList: [data.inputType],
|
||||
required: data.required,
|
||||
description: data.description,
|
||||
toolDescription: data.isToolInput ? data.description : undefined,
|
||||
canEdit: true,
|
||||
value: data.defaultValue,
|
||||
editField: dynamicInputEditField,
|
||||
maxLength: data.maxLength,
|
||||
max: data.max,
|
||||
min: data.min,
|
||||
dynamicParamDefaultValue: data.dynamicParamDefaultValue
|
||||
};
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'addInput',
|
||||
value: newInput
|
||||
});
|
||||
|
||||
const newOutput: FlowNodeOutputItemType = {
|
||||
id: getNanoid(),
|
||||
key: data.key,
|
||||
valueType: data.valueType,
|
||||
label: data.label,
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
};
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'addOutput',
|
||||
value: newOutput
|
||||
});
|
||||
setCreateField(undefined);
|
||||
}}
|
||||
editField={editField}
|
||||
onStartEdit={(key) => {
|
||||
const input = inputs.find((input) => input.key === key);
|
||||
if (!input) return;
|
||||
|
||||
setEditField({
|
||||
...input,
|
||||
inputType: input.renderTypeList[0],
|
||||
isToolInput: !!input.toolDescription
|
||||
});
|
||||
}}
|
||||
onEdit={({ data, changeKey }) => {
|
||||
if (!data.inputType || !data.key || !editField?.key) return;
|
||||
|
||||
const output = outputs.find((output) => output.key === editField.key);
|
||||
|
||||
const newInput: FlowNodeInputItemType = {
|
||||
...data,
|
||||
key: data.key,
|
||||
label: data.label || '',
|
||||
renderTypeList: [data.inputType],
|
||||
toolDescription: data.isToolInput ? data.description : undefined,
|
||||
canEdit: true,
|
||||
value: data.defaultValue,
|
||||
editField: dynamicInputEditField
|
||||
};
|
||||
const newOutput: FlowNodeOutputItemType = {
|
||||
...(output as FlowNodeOutputItemType),
|
||||
valueType: data.valueType,
|
||||
key: data.key,
|
||||
label: data.label
|
||||
};
|
||||
|
||||
if (changeKey) {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'replaceInput',
|
||||
key: editField.key,
|
||||
value: newInput
|
||||
});
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'replaceOutput',
|
||||
key: editField.key,
|
||||
value: newOutput
|
||||
});
|
||||
} else {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: newInput.key,
|
||||
value: newInput
|
||||
});
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateOutput',
|
||||
key: newOutput.key,
|
||||
value: newOutput
|
||||
});
|
||||
}
|
||||
setEditField(undefined);
|
||||
}}
|
||||
onDelete={(key) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'delInput',
|
||||
key
|
||||
});
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'delOutput',
|
||||
key
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
}, [createField, data, editField, inputs, nodeId, onChangeNode, outputs, selected, t]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
export default React.memo(NodePluginInput);
|
||||
@@ -1,105 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import Container from '../components/Container';
|
||||
import { EditInputFieldMapType, EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import RenderInput from './render/RenderInput';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
|
||||
const FieldEditModal = dynamic(() => import('./render/FieldEditModal'));
|
||||
|
||||
const defaultCreateField: EditNodeFieldType = {
|
||||
inputType: FlowNodeInputTypeEnum.reference,
|
||||
key: '',
|
||||
description: '',
|
||||
valueType: WorkflowIOValueTypeEnum.string
|
||||
};
|
||||
const createEditField: EditInputFieldMapType = {
|
||||
key: true,
|
||||
description: true,
|
||||
valueType: true
|
||||
};
|
||||
|
||||
const NodePluginOutput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { nodeId, inputs } = data;
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const [createField, setCreateField] = useState<EditNodeFieldType>();
|
||||
|
||||
return (
|
||||
<NodeCard
|
||||
minW={'300px'}
|
||||
selected={selected}
|
||||
menuForbid={{
|
||||
debug: true,
|
||||
rename: true,
|
||||
copy: true,
|
||||
delete: true
|
||||
}}
|
||||
{...data}
|
||||
>
|
||||
<Container mt={1}>
|
||||
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
|
||||
<Box position={'relative'} fontWeight={'medium'}>
|
||||
{t('core.workflow.Custom outputs')}
|
||||
</Box>
|
||||
<Box flex={'1 0 0'} />
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
iconSpacing={1}
|
||||
size={'sm'}
|
||||
onClick={() => setCreateField(defaultCreateField)}
|
||||
>
|
||||
{t('common.Add New')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<RenderInput nodeId={nodeId} flowInputList={inputs} />
|
||||
</Container>
|
||||
{!!createField && (
|
||||
<FieldEditModal
|
||||
editField={createEditField}
|
||||
defaultField={createField}
|
||||
keys={inputs.map((input) => input.key)}
|
||||
onClose={() => setCreateField(undefined)}
|
||||
onSubmit={({ data }) => {
|
||||
if (!data.key || !data.label) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newInput: FlowNodeInputItemType = {
|
||||
key: data.key,
|
||||
valueType: data.valueType,
|
||||
label: data.label,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.reference],
|
||||
required: false,
|
||||
description: data.description,
|
||||
canEdit: true,
|
||||
editField: createEditField
|
||||
};
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'addInput',
|
||||
value: newInput
|
||||
});
|
||||
|
||||
setCreateField(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NodePluginOutput);
|
||||
@@ -1,57 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import Container from '../components/Container';
|
||||
import RenderInput from './render/RenderInput';
|
||||
import RenderOutput from './render/RenderOutput';
|
||||
import RenderToolInput from './render/RenderToolInput';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import IOTitle from '../components/IOTitle';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
|
||||
const NodeSimple = ({
|
||||
data,
|
||||
selected,
|
||||
minW = '350px',
|
||||
maxW
|
||||
}: NodeProps<FlowNodeItemType> & { minW?: string | number; maxW?: string | number }) => {
|
||||
const { t } = useTranslation();
|
||||
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
|
||||
const { nodeId, inputs, outputs } = data;
|
||||
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
|
||||
|
||||
const filterHiddenInputs = useMemo(() => commonInputs.filter((item) => true), [commonInputs]);
|
||||
|
||||
return (
|
||||
<NodeCard minW={minW} maxW={maxW} selected={selected} {...data}>
|
||||
{toolInputs.length > 0 && (
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle text={t('core.module.tool.Tool input')} />
|
||||
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
{filterHiddenInputs.length > 0 && (
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle text={t('common.Input')} />
|
||||
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
{outputs.filter((output) => output.type !== FlowNodeOutputTypeEnum.hidden).length > 0 && (
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle text={t('common.Output')} />
|
||||
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeSimple);
|
||||
@@ -1,221 +0,0 @@
|
||||
import React, { Dispatch, useMemo, useTransition } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { Box, useTheme } from '@chakra-ui/react';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
|
||||
import QGSwitch from '@/components/core/app/QGSwitch';
|
||||
import TTSSelect from '@/components/core/app/TTSSelect';
|
||||
import WhisperConfig from '@/components/core/app/WhisperConfig';
|
||||
import InputGuideConfig from '@/components/core/app/InputGuideConfig';
|
||||
import { getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { TTSTypeEnum } from '@/web/core/app/constants';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import ScheduledTriggerConfig from '@/components/core/app/ScheduledTriggerConfig';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
import { AppChatConfigType, AppDetailType, VariableItemType } from '@fastgpt/global/core/app/type';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import VariableEdit from '@/components/core/app/VariableEdit';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
import WelcomeTextConfig from '@/components/core/app/WelcomeTextConfig';
|
||||
|
||||
type ComponentProps = {
|
||||
chatConfig: AppChatConfigType;
|
||||
setAppDetail: Dispatch<React.SetStateAction<AppDetailType>>;
|
||||
};
|
||||
|
||||
const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const theme = useTheme();
|
||||
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const chatConfig = useMemo<AppChatConfigType>(() => {
|
||||
return getAppChatConfig({
|
||||
chatConfig: appDetail.chatConfig,
|
||||
systemConfigNode: data,
|
||||
isPublicFetch: true
|
||||
});
|
||||
}, [data, appDetail]);
|
||||
|
||||
const componentsProps = useMemo(
|
||||
() => ({
|
||||
chatConfig,
|
||||
setAppDetail
|
||||
}),
|
||||
[chatConfig, setAppDetail]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeCard
|
||||
minW={'300px'}
|
||||
selected={selected}
|
||||
menuForbid={{
|
||||
debug: true,
|
||||
rename: true,
|
||||
copy: true,
|
||||
delete: true
|
||||
}}
|
||||
{...data}
|
||||
>
|
||||
<Box px={4} py={'10px'} position={'relative'} borderRadius={'md'} className="nodrag">
|
||||
<WelcomeText {...componentsProps} />
|
||||
<Box pt={4}>
|
||||
<ChatStartVariable {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={3} borderTop={theme.borders.base}>
|
||||
<TTSGuide {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={3} borderTop={theme.borders.base}>
|
||||
<WhisperGuide {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={3} borderTop={theme.borders.base}>
|
||||
<QuestionGuide {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={3} borderTop={theme.borders.base}>
|
||||
<ScheduledTrigger {...componentsProps} />
|
||||
</Box>
|
||||
<Box mt={3} pt={3} borderTop={theme.borders.base}>
|
||||
<QuestionInputGuide {...componentsProps} />
|
||||
</Box>
|
||||
</Box>
|
||||
</NodeCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NodeUserGuide);
|
||||
|
||||
function WelcomeText({ chatConfig: { welcomeText }, setAppDetail }: ComponentProps) {
|
||||
const { t } = useTranslation();
|
||||
const [, startTst] = useTransition();
|
||||
|
||||
return (
|
||||
<Box className="nodrag">
|
||||
<WelcomeTextConfig
|
||||
resize={'both'}
|
||||
defaultValue={welcomeText}
|
||||
onChange={(e) => {
|
||||
startTst(() => {
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
welcomeText: e.target.value
|
||||
}
|
||||
}));
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatStartVariable({ chatConfig: { variables = [] }, setAppDetail }: ComponentProps) {
|
||||
const updateVariables = useMemoizedFn((value: VariableItemType[]) => {
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
variables: value
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
return <VariableEdit variables={variables} onChange={(e) => updateVariables(e)} />;
|
||||
}
|
||||
|
||||
function QuestionGuide({ chatConfig: { questionGuide = false }, setAppDetail }: ComponentProps) {
|
||||
return (
|
||||
<QGSwitch
|
||||
isChecked={questionGuide}
|
||||
onChange={(e) => {
|
||||
const value = e.target.checked;
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
questionGuide: value
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TTSGuide({ chatConfig: { ttsConfig }, setAppDetail }: ComponentProps) {
|
||||
return (
|
||||
<TTSSelect
|
||||
value={ttsConfig}
|
||||
onChange={(e) => {
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
ttsConfig: e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function WhisperGuide({ chatConfig: { whisperConfig, ttsConfig }, setAppDetail }: ComponentProps) {
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
return (
|
||||
<WhisperConfig
|
||||
isOpenAudio={ttsConfig?.type !== TTSTypeEnum.none}
|
||||
value={whisperConfig}
|
||||
onChange={(e) => {
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
whisperConfig: e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ScheduledTrigger({
|
||||
chatConfig: { scheduledTriggerConfig },
|
||||
setAppDetail
|
||||
}: ComponentProps) {
|
||||
return (
|
||||
<ScheduledTriggerConfig
|
||||
value={scheduledTriggerConfig}
|
||||
onChange={(e) => {
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
scheduledTriggerConfig: e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function QuestionInputGuide({ chatConfig: { chatInputGuide }, setAppDetail }: ComponentProps) {
|
||||
const appId = useContextSelector(WorkflowContext, (v) => v.appId);
|
||||
|
||||
return appId ? (
|
||||
<InputGuideConfig
|
||||
appId={appId}
|
||||
value={chatInputGuide}
|
||||
onChange={(e) => {
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
chatInputGuide: e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import Divider from '../components/Divider';
|
||||
import Container from '../components/Container';
|
||||
import RenderInput from './render/RenderInput';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ToolSourceHandle } from './render/Handle/ToolHandle';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import IOTitle from '../components/IOTitle';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import RenderOutput from './render/RenderOutput';
|
||||
|
||||
const NodeTools = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { nodeId, inputs, outputs } = data;
|
||||
|
||||
return (
|
||||
<NodeCard minW={'350px'} selected={selected} {...data}>
|
||||
<Container>
|
||||
<IOTitle text={t('common.Input')} />
|
||||
<RenderInput nodeId={nodeId} flowInputList={inputs} />
|
||||
</Container>
|
||||
<Container>
|
||||
<IOTitle text={t('common.Output')} />
|
||||
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
<Box position={'relative'}>
|
||||
<Box borderBottomLeftRadius={'md'} borderBottomRadius={'md'} overflow={'hidden'}>
|
||||
<Divider
|
||||
showBorderBottom={false}
|
||||
icon={<MyIcon name="phoneTabbar/tool" w={'16px'} h={'16px'} />}
|
||||
text={t('core.workflow.tool.Select Tool')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<ToolSourceHandle nodeId={nodeId} />
|
||||
</Box>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeTools);
|
||||
@@ -1,311 +0,0 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
NumberDecrementStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
Switch,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type';
|
||||
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
import {
|
||||
FlowNodeInputMap,
|
||||
FlowNodeInputTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import Container from '../components/Container';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { ReferSelector, useReference } from './render/RenderInput/templates/Reference';
|
||||
import { getRefData } from '@/web/core/workflow/utils';
|
||||
import { isReferenceValue } from '@fastgpt/global/core/workflow/utils';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
|
||||
const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { inputs = [], nodeId } = data;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||
|
||||
const updateList = useMemo(
|
||||
() =>
|
||||
(inputs.find((input) => input.key === NodeInputKeyEnum.updateList)
|
||||
?.value as TUpdateListItem[]) || [],
|
||||
[inputs]
|
||||
);
|
||||
|
||||
const onUpdateList = useCallback(
|
||||
(value: TUpdateListItem[]) => {
|
||||
const updateListInput = inputs.find((input) => input.key === NodeInputKeyEnum.updateList);
|
||||
if (!updateListInput) return;
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: NodeInputKeyEnum.updateList,
|
||||
value: {
|
||||
...updateListInput,
|
||||
value
|
||||
}
|
||||
});
|
||||
},
|
||||
[inputs, nodeId, onChangeNode]
|
||||
);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
const menuList = [
|
||||
{
|
||||
renderType: FlowNodeInputTypeEnum.input,
|
||||
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.input].icon,
|
||||
label: t('core.workflow.inputType.Manual input')
|
||||
},
|
||||
{
|
||||
renderType: FlowNodeInputTypeEnum.reference,
|
||||
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.reference].icon,
|
||||
label: t('core.workflow.inputType.Reference')
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{updateList.map((updateItem, index) => {
|
||||
const { valueType } = getRefData({
|
||||
variable: updateItem.variable,
|
||||
nodeList,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
t
|
||||
});
|
||||
|
||||
const renderTypeData = menuList.find((item) => item.renderType === updateItem.renderType);
|
||||
const handleUpdate = (newValue: ReferenceValueProps | string) => {
|
||||
if (isReferenceValue(newValue)) {
|
||||
onUpdateList(
|
||||
updateList.map((update, i) =>
|
||||
i === index ? { ...update, value: newValue as ReferenceValueProps } : update
|
||||
)
|
||||
);
|
||||
} else {
|
||||
onUpdateList(
|
||||
updateList.map((update, i) =>
|
||||
i === index ? { ...update, value: ['', newValue as string] } : update
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container key={index} mt={4} w={'full'} mx={0}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Flex w={'60px'}>{t('core.workflow.variable')}</Flex>
|
||||
<Reference
|
||||
nodeId={nodeId}
|
||||
variable={updateItem.variable}
|
||||
onSelect={(value) => {
|
||||
onUpdateList(
|
||||
updateList.map((update, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...update,
|
||||
value: ['', ''],
|
||||
valueType,
|
||||
variable: value
|
||||
};
|
||||
}
|
||||
return update;
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Box flex={1} />
|
||||
{updateList.length > 1 && (
|
||||
<MyIcon
|
||||
className="delete"
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
color={'myGray.600'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.500' }}
|
||||
position={'absolute'}
|
||||
top={3}
|
||||
right={3}
|
||||
onClick={() => {
|
||||
onUpdateList(updateList.filter((_, i) => i !== index));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex mt={2} w={'full'} alignItems={'center'} className="nodrag">
|
||||
<Flex w={'60px'}>
|
||||
<Box>{t('core.workflow.value')}</Box>
|
||||
<MyTooltip
|
||||
label={
|
||||
menuList.find((item) => item.renderType === updateItem.renderType)?.label
|
||||
}
|
||||
>
|
||||
<Button
|
||||
size={'xs'}
|
||||
bg={'white'}
|
||||
borderRadius={'xs'}
|
||||
mx={2}
|
||||
onClick={() => {
|
||||
onUpdateList(
|
||||
updateList.map((update, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...update,
|
||||
value: ['', ''],
|
||||
renderType:
|
||||
updateItem.renderType === FlowNodeInputTypeEnum.input
|
||||
? FlowNodeInputTypeEnum.reference
|
||||
: FlowNodeInputTypeEnum.input
|
||||
};
|
||||
}
|
||||
return update;
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<MyIcon name={renderTypeData?.icon as any} w={'14px'} />
|
||||
</Button>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
|
||||
{/* Render input components */}
|
||||
{(() => {
|
||||
if (updateItem.renderType === FlowNodeInputTypeEnum.reference) {
|
||||
return (
|
||||
<Reference
|
||||
nodeId={nodeId}
|
||||
variable={updateItem.value}
|
||||
valueType={valueType}
|
||||
onSelect={handleUpdate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (valueType === WorkflowIOValueTypeEnum.string) {
|
||||
return (
|
||||
<Textarea
|
||||
bg="white"
|
||||
value={updateItem.value?.[1] || ''}
|
||||
w="300px"
|
||||
onChange={(e) => handleUpdate(e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (valueType === WorkflowIOValueTypeEnum.number) {
|
||||
return (
|
||||
<NumberInput value={Number(updateItem.value?.[1]) || 0}>
|
||||
<NumberInputField
|
||||
bg="white"
|
||||
onChange={(e) => handleUpdate(e.target.value)}
|
||||
/>
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
);
|
||||
}
|
||||
if (valueType === WorkflowIOValueTypeEnum.boolean) {
|
||||
return (
|
||||
<Switch
|
||||
defaultChecked={updateItem.value?.[1] === 'true'}
|
||||
onChange={(e) => handleUpdate(String(e.target.checked))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<JsonEditor
|
||||
bg="white"
|
||||
resize
|
||||
w="300px"
|
||||
value={String(updateItem.value?.[1] || '')}
|
||||
onChange={(e) => {
|
||||
handleUpdate(e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</Flex>
|
||||
</Container>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}, [nodeId, nodeList, onUpdateList, t, updateList]);
|
||||
|
||||
return (
|
||||
<NodeCard selected={selected} maxW={'1000px'} {...data}>
|
||||
<Box px={4} pb={4}>
|
||||
{Render}
|
||||
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
iconSpacing={1}
|
||||
w={'full'}
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
onUpdateList([
|
||||
...updateList,
|
||||
{
|
||||
variable: ['', ''],
|
||||
value: ['', ''],
|
||||
renderType: FlowNodeInputTypeEnum.input
|
||||
}
|
||||
]);
|
||||
}}
|
||||
>
|
||||
{t('common.Add New')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeVariableUpdate);
|
||||
|
||||
const Reference = ({
|
||||
nodeId,
|
||||
variable,
|
||||
valueType,
|
||||
onSelect
|
||||
}: {
|
||||
nodeId: string;
|
||||
variable?: ReferenceValueProps;
|
||||
valueType?: WorkflowIOValueTypeEnum;
|
||||
onSelect: (e: ReferenceValueProps) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { referenceList, formatValue } = useReference({
|
||||
nodeId,
|
||||
valueType,
|
||||
value: variable
|
||||
});
|
||||
|
||||
return (
|
||||
<ReferSelector
|
||||
placeholder={t('选择引用变量')}
|
||||
list={referenceList}
|
||||
value={formatValue}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import Container from '../components/Container';
|
||||
import RenderOutput from './render/RenderOutput';
|
||||
import IOTitle from '../components/IOTitle';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
import { useCreation } from 'ahooks';
|
||||
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
|
||||
import { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
|
||||
const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { nodeId, outputs } = data;
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const variablesOutputs = useCreation(() => {
|
||||
const variables = getWorkflowGlobalVariables({
|
||||
nodes: nodeList,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
t
|
||||
});
|
||||
|
||||
return variables.map<FlowNodeOutputItemType>((item) => ({
|
||||
id: item.key,
|
||||
type: FlowNodeOutputTypeEnum.static,
|
||||
key: item.key,
|
||||
required: item.required,
|
||||
valueType: item.valueType || WorkflowIOValueTypeEnum.any,
|
||||
label: item.label
|
||||
}));
|
||||
}, [nodeList, t]);
|
||||
|
||||
return (
|
||||
<NodeCard
|
||||
minW={'240px'}
|
||||
selected={selected}
|
||||
menuForbid={{
|
||||
rename: true,
|
||||
copy: true,
|
||||
delete: true
|
||||
}}
|
||||
{...data}
|
||||
>
|
||||
<Container>
|
||||
<IOTitle text={t('common.Output')} />
|
||||
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
<Container>
|
||||
<IOTitle text={t('core.module.Variable')} />
|
||||
<RenderOutput nodeId={nodeId} flowOutputList={variablesOutputs} />
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NodeStart);
|
||||
@@ -1,530 +0,0 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
Flex,
|
||||
Switch,
|
||||
Input,
|
||||
Textarea,
|
||||
Stack
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import {
|
||||
EditInputFieldMapType,
|
||||
EditNodeFieldType
|
||||
} from '@fastgpt/global/core/workflow/node/type.d';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput/index';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
|
||||
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
|
||||
const EmptyTip = dynamic(() => import('@fastgpt/web/components/common/EmptyTip'));
|
||||
|
||||
const defaultValue: EditNodeFieldType = {
|
||||
inputType: FlowNodeInputTypeEnum.reference,
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
key: '',
|
||||
label: '',
|
||||
description: '',
|
||||
isToolInput: false,
|
||||
defaultValue: '',
|
||||
maxLength: undefined,
|
||||
max: undefined,
|
||||
min: undefined,
|
||||
editField: {},
|
||||
dynamicParamDefaultValue: {
|
||||
inputType: FlowNodeInputTypeEnum.reference,
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
required: true
|
||||
}
|
||||
};
|
||||
|
||||
const FieldEditModal = ({
|
||||
editField = {
|
||||
key: true
|
||||
},
|
||||
defaultField,
|
||||
keys = [],
|
||||
onClose,
|
||||
onSubmit
|
||||
}: {
|
||||
editField?: EditInputFieldMapType;
|
||||
defaultField: EditNodeFieldType;
|
||||
keys: string[];
|
||||
onClose: () => void;
|
||||
onSubmit: (e: { data: EditNodeFieldType; changeKey: boolean }) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { workflowT } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const showDynamicInputSelect =
|
||||
!keys.includes(NodeInputKeyEnum.addInputParam) ||
|
||||
defaultField.key === NodeInputKeyEnum.addInputParam;
|
||||
|
||||
const inputTypeList = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t('core.workflow.inputType.Reference'),
|
||||
value: FlowNodeInputTypeEnum.reference,
|
||||
defaultValue: {}
|
||||
},
|
||||
{
|
||||
label: t('core.workflow.inputType.input'),
|
||||
value: FlowNodeInputTypeEnum.input,
|
||||
defaultValue: {
|
||||
valueType: WorkflowIOValueTypeEnum.string
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('core.workflow.inputType.textarea'),
|
||||
value: FlowNodeInputTypeEnum.textarea,
|
||||
defaultValue: {
|
||||
valueType: WorkflowIOValueTypeEnum.string
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('core.workflow.inputType.JSON Editor'),
|
||||
value: FlowNodeInputTypeEnum.JSONEditor,
|
||||
defaultValue: {
|
||||
valueType: WorkflowIOValueTypeEnum.string
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('core.workflow.inputType.number input'),
|
||||
value: FlowNodeInputTypeEnum.numberInput,
|
||||
defaultValue: {
|
||||
valueType: WorkflowIOValueTypeEnum.number
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('core.workflow.inputType.switch'),
|
||||
value: FlowNodeInputTypeEnum.switch,
|
||||
defaultValue: {
|
||||
valueType: WorkflowIOValueTypeEnum.boolean
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('core.workflow.inputType.selectApp'),
|
||||
value: FlowNodeInputTypeEnum.selectApp,
|
||||
defaultValue: {
|
||||
valueType: WorkflowIOValueTypeEnum.selectApp
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('core.workflow.inputType.selectLLMModel'),
|
||||
value: FlowNodeInputTypeEnum.selectLLMModel,
|
||||
defaultValue: {
|
||||
valueType: WorkflowIOValueTypeEnum.string
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('core.workflow.inputType.selectDataset'),
|
||||
value: FlowNodeInputTypeEnum.selectDataset,
|
||||
defaultValue: {
|
||||
valueType: WorkflowIOValueTypeEnum.selectDataset
|
||||
}
|
||||
},
|
||||
...(showDynamicInputSelect
|
||||
? [
|
||||
{
|
||||
label: t('core.workflow.inputType.dynamicTargetInput'),
|
||||
value: FlowNodeInputTypeEnum.addInputParam,
|
||||
defaultValue: {
|
||||
label: t('core.workflow.inputType.dynamicTargetInput'),
|
||||
valueType: WorkflowIOValueTypeEnum.dynamic,
|
||||
key: NodeInputKeyEnum.addInputParam,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
]
|
||||
: [])
|
||||
],
|
||||
[showDynamicInputSelect, t]
|
||||
);
|
||||
|
||||
const { register, getValues, setValue, handleSubmit, watch } = useForm<EditNodeFieldType>({
|
||||
defaultValues: {
|
||||
...defaultValue,
|
||||
...defaultField,
|
||||
valueType: defaultField.valueType ?? WorkflowIOValueTypeEnum.string
|
||||
}
|
||||
});
|
||||
const inputType = watch('inputType');
|
||||
const valueType = watch('valueType');
|
||||
|
||||
const isToolInput = watch('isToolInput');
|
||||
const maxLength = watch('maxLength');
|
||||
const max = watch('max');
|
||||
const min = watch('min');
|
||||
const defaultInputValueType = watch('dynamicParamDefaultValue.valueType');
|
||||
|
||||
const showKeyInput = useMemo(() => {
|
||||
if (inputType === FlowNodeInputTypeEnum.addInputParam) return false;
|
||||
|
||||
return editField.key;
|
||||
}, [editField.key, inputType]);
|
||||
|
||||
const showInputTypeSelect = useMemo(() => {
|
||||
return editField.inputType;
|
||||
}, [editField.inputType]);
|
||||
|
||||
const showDescriptionInput = useMemo(() => {
|
||||
return editField.description;
|
||||
}, [editField.description]);
|
||||
|
||||
const showValueTypeSelect = useMemo(() => {
|
||||
if (!editField.valueType) return false;
|
||||
if (inputType !== FlowNodeInputTypeEnum.reference) return false;
|
||||
|
||||
return true;
|
||||
}, [editField.valueType, inputType]);
|
||||
|
||||
// input type config
|
||||
const showToolInput = useMemo(() => {
|
||||
return inputType === FlowNodeInputTypeEnum.reference;
|
||||
}, [inputType]);
|
||||
|
||||
const showDefaultValue = useMemo(() => {
|
||||
if (inputType === FlowNodeInputTypeEnum.input) return true;
|
||||
if (inputType === FlowNodeInputTypeEnum.textarea) return true;
|
||||
if (inputType === FlowNodeInputTypeEnum.JSONEditor) return true;
|
||||
if (inputType === FlowNodeInputTypeEnum.numberInput) return true;
|
||||
if (inputType === FlowNodeInputTypeEnum.switch) return true;
|
||||
|
||||
return false;
|
||||
}, [inputType]);
|
||||
|
||||
const showMaxLenInput = useMemo(() => {
|
||||
if (inputType === FlowNodeInputTypeEnum.input) return true;
|
||||
if (inputType === FlowNodeInputTypeEnum.textarea) return true;
|
||||
|
||||
return false;
|
||||
}, [inputType]);
|
||||
|
||||
const showMinMaxInput = useMemo(
|
||||
() => inputType === FlowNodeInputTypeEnum.numberInput,
|
||||
[inputType]
|
||||
);
|
||||
|
||||
const showDynamicInput = useMemo(() => {
|
||||
return inputType === FlowNodeInputTypeEnum.addInputParam;
|
||||
}, [inputType]);
|
||||
|
||||
const slicedTypeMap = Object.values(FlowValueTypeMap).slice(0, -1);
|
||||
|
||||
const dataTypeSelectList = slicedTypeMap.map((item) => ({
|
||||
label: t(item.label),
|
||||
value: item.value
|
||||
}));
|
||||
|
||||
const onSubmitSuccess = useCallback(
|
||||
(data: EditNodeFieldType) => {
|
||||
data.key = data?.key?.trim();
|
||||
// add default value
|
||||
const inputTypeConfig = inputTypeList.find((item) => item.value === data.inputType);
|
||||
if (inputTypeConfig?.defaultValue) {
|
||||
data.label = data.key;
|
||||
for (const key in inputTypeConfig.defaultValue) {
|
||||
// @ts-ignore
|
||||
data[key] = inputTypeConfig.defaultValue[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.key) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('core.module.edit.Field Name Cannot Be Empty')
|
||||
});
|
||||
}
|
||||
|
||||
// create check key
|
||||
if (!defaultField.key && keys.includes(data.key)) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('core.module.edit.Field Already Exist')
|
||||
});
|
||||
}
|
||||
// edit check repeat key
|
||||
if (defaultField.key && defaultField.key !== data.key && keys.includes(data.key)) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('core.module.edit.Field Already Exist')
|
||||
});
|
||||
}
|
||||
if (showValueTypeSelect && !data.valueType) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: '数据类型不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit({
|
||||
data,
|
||||
changeKey: !keys.includes(data.key)
|
||||
});
|
||||
},
|
||||
[defaultField.key, inputTypeList, keys, onSubmit, showValueTypeSelect, t, toast]
|
||||
);
|
||||
const onSubmitError = useCallback(
|
||||
(e: Object) => {
|
||||
for (const item of Object.values(e)) {
|
||||
if (item.message) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: item.message
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
iconSrc="/imgs/workflow/extract.png"
|
||||
title={t('core.module.edit.Field Edit')}
|
||||
maxW={['90vw', showInputTypeSelect ? '800px' : '400px']}
|
||||
w={'100%'}
|
||||
overflow={'unset'}
|
||||
>
|
||||
<ModalBody overflow={'visible'}>
|
||||
<Flex gap={8} flexDirection={['column', 'row']}>
|
||||
<Stack flex={1} gap={5}>
|
||||
{showInputTypeSelect && (
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 70px'}>{t('core.module.Input Type')}</FormLabel>
|
||||
<Box flex={1}>
|
||||
<MySelect
|
||||
list={inputTypeList}
|
||||
value={inputType}
|
||||
onchange={(e: string) => {
|
||||
const type = e as FlowNodeInputTypeEnum;
|
||||
|
||||
setValue('inputType', type);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{showValueTypeSelect && !showInputTypeSelect && (
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 70px'}>{t('core.module.Data Type')}</FormLabel>
|
||||
<Box flex={1}>
|
||||
<MySelect
|
||||
w={'full'}
|
||||
list={dataTypeSelectList}
|
||||
value={valueType}
|
||||
onchange={(e: string) => {
|
||||
const type = e as WorkflowIOValueTypeEnum;
|
||||
setValue('valueType', type);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{showKeyInput && (
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 70px'}>{t('core.module.Field Name')}</FormLabel>
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
placeholder="appointment/sql"
|
||||
{...register('key', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
{showDescriptionInput && (
|
||||
<Box alignItems={'flex-start'}>
|
||||
<FormLabel flex={'0 0 70px'} mb={'1px'}>
|
||||
{t('core.module.Field Description')}
|
||||
</FormLabel>
|
||||
<Textarea
|
||||
bg={'myGray.50'}
|
||||
placeholder={
|
||||
isToolInput ? t('core.module.Plugin tool Description') : t('common.choosable')
|
||||
}
|
||||
rows={5}
|
||||
{...register('description', { required: isToolInput ? true : false })}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
{/* input type config */}
|
||||
{showInputTypeSelect && (
|
||||
<Stack flex={1} gap={5}>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 70px'}>{workflowT('Field required')}</FormLabel>
|
||||
<Switch {...register('required')} />
|
||||
</Flex>
|
||||
{showToolInput && (
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 70px'}>工具参数</FormLabel>
|
||||
<Switch {...register('isToolInput')} />
|
||||
</Flex>
|
||||
)}
|
||||
{showValueTypeSelect && (
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 70px'}>{t('core.module.Data Type')}</FormLabel>
|
||||
<Box flex={1}>
|
||||
<MySelect
|
||||
w={'full'}
|
||||
list={dataTypeSelectList}
|
||||
value={valueType}
|
||||
onchange={(e: string) => {
|
||||
const type = e as WorkflowIOValueTypeEnum;
|
||||
setValue('valueType', type);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{showDefaultValue && (
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 70px'}>{t('core.module.Default Value')}</FormLabel>
|
||||
{inputType === FlowNodeInputTypeEnum.numberInput && (
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
max={max}
|
||||
min={min}
|
||||
type={'number'}
|
||||
{...register('defaultValue')}
|
||||
/>
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.input && (
|
||||
<Input bg={'myGray.50'} maxLength={maxLength} {...register('defaultValue')} />
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.textarea && (
|
||||
<Textarea
|
||||
bg={'myGray.50'}
|
||||
maxLength={maxLength}
|
||||
{...register('defaultValue')}
|
||||
/>
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.JSONEditor && (
|
||||
<JsonEditor
|
||||
resize
|
||||
w={'full'}
|
||||
onChange={(e) => {
|
||||
setValue('defaultValue', e);
|
||||
}}
|
||||
defaultValue={String(getValues('defaultValue'))}
|
||||
/>
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.switch && (
|
||||
<Switch {...register('defaultValue')} />
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
{showMaxLenInput && (
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 70px'}>{t('core.module.Max Length')}</FormLabel>
|
||||
<MyNumberInput
|
||||
flex={'1 0 0'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={t('core.module.Max Length placeholder')}
|
||||
value={maxLength}
|
||||
onChange={(e) => {
|
||||
// @ts-ignore
|
||||
setValue('maxLength', e);
|
||||
}}
|
||||
// {...register('maxLength')}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
{showMinMaxInput && (
|
||||
<>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 70px'}>{t('core.module.Max Value')}</FormLabel>
|
||||
<MyNumberInput
|
||||
flex={'1 0 0'}
|
||||
bg={'myGray.50'}
|
||||
value={watch('max')}
|
||||
onChange={(e) => {
|
||||
// @ts-ignore
|
||||
setValue('max', e);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 70px'}>{t('core.module.Min Value')}</FormLabel>
|
||||
<MyNumberInput
|
||||
flex={'1 0 0'}
|
||||
bg={'myGray.50'}
|
||||
value={watch('min')}
|
||||
onChange={(e) => {
|
||||
// @ts-ignore
|
||||
setValue('min', e);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
{showDynamicInput && (
|
||||
<Stack gap={5}>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 70px'}>{t('core.module.Input Type')}</FormLabel>
|
||||
<Box flex={1} fontWeight={'bold'}>
|
||||
{t('core.workflow.inputType.Reference')}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 70px'}>{t('core.module.Data Type')}</FormLabel>
|
||||
<Box flex={1}>
|
||||
<MySelect
|
||||
list={dataTypeSelectList}
|
||||
value={defaultInputValueType}
|
||||
onchange={(e) => {
|
||||
setValue(
|
||||
'dynamicParamDefaultValue.valueType',
|
||||
e as WorkflowIOValueTypeEnum
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 70px'}>{t('core.workflow.inputType.Required')}</FormLabel>
|
||||
<Box flex={1}>
|
||||
<Switch {...register('dynamicParamDefaultValue.required')} />
|
||||
</Box>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)}
|
||||
{!showToolInput &&
|
||||
!showValueTypeSelect &&
|
||||
!showDefaultValue &&
|
||||
!showMaxLenInput &&
|
||||
!showMinMaxInput &&
|
||||
!showDynamicInput && <EmptyTip text={t('core.module.No Config Tips')} />}
|
||||
</Stack>
|
||||
)}
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
|
||||
{t('common.Close')}
|
||||
</Button>
|
||||
<Button onClick={handleSubmit(onSubmitSuccess, onSubmitError)}>
|
||||
{t('common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(FieldEditModal);
|
||||
@@ -1,186 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Position } from 'reactflow';
|
||||
import { SourceHandle, TargetHandle } from '.';
|
||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
|
||||
export const ConnectionSourceHandle = ({ nodeId }: { nodeId: string }) => {
|
||||
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
||||
|
||||
const node = useMemo(() => nodeList.find((node) => node.nodeId === nodeId), [nodeList, nodeId]);
|
||||
|
||||
/* not node/not connecting node, hidden */
|
||||
const showSourceHandle = useMemo(() => {
|
||||
if (!node) return false;
|
||||
if (connectingEdge && connectingEdge.nodeId !== nodeId) return false;
|
||||
return true;
|
||||
}, [connectingEdge, node, nodeId]);
|
||||
|
||||
const RightHandle = useMemo(() => {
|
||||
const handleId = getHandleId(nodeId, 'source', Position.Right);
|
||||
const rightTargetConnected = edges.some(
|
||||
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Right)
|
||||
);
|
||||
|
||||
if (!node || !node?.sourceHandle?.right || rightTargetConnected) return null;
|
||||
|
||||
return (
|
||||
<SourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={handleId}
|
||||
position={Position.Right}
|
||||
translate={[2, 0]}
|
||||
/>
|
||||
);
|
||||
}, [edges, node, nodeId]);
|
||||
const LeftHandlee = useMemo(() => {
|
||||
const leftTargetConnected = edges.some(
|
||||
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Left)
|
||||
);
|
||||
if (!node || !node?.sourceHandle?.left || leftTargetConnected) return null;
|
||||
|
||||
const handleId = getHandleId(nodeId, 'source', Position.Left);
|
||||
|
||||
return (
|
||||
<SourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={handleId}
|
||||
position={Position.Left}
|
||||
translate={[-6, 0]}
|
||||
/>
|
||||
);
|
||||
}, [edges, node, nodeId]);
|
||||
const TopHandlee = useMemo(() => {
|
||||
if (
|
||||
edges.some(
|
||||
(edge) => edge.target === nodeId && edge.targetHandle === NodeOutputKeyEnum.selectedTools
|
||||
)
|
||||
)
|
||||
return null;
|
||||
|
||||
const handleId = getHandleId(nodeId, 'source', Position.Top);
|
||||
const topTargetConnected = edges.some(
|
||||
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Top)
|
||||
);
|
||||
if (!node || !node?.sourceHandle?.top || topTargetConnected) return null;
|
||||
|
||||
return (
|
||||
<SourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={handleId}
|
||||
position={Position.Top}
|
||||
translate={[0, -2]}
|
||||
/>
|
||||
);
|
||||
}, [edges, node, nodeId]);
|
||||
const BottomHandlee = useMemo(() => {
|
||||
const handleId = getHandleId(nodeId, 'source', Position.Bottom);
|
||||
const targetConnected = edges.some(
|
||||
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Bottom)
|
||||
);
|
||||
if (!node || !node?.sourceHandle?.bottom || targetConnected) return null;
|
||||
|
||||
return (
|
||||
<SourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={handleId}
|
||||
position={Position.Bottom}
|
||||
translate={[0, 2]}
|
||||
/>
|
||||
);
|
||||
}, [edges, node, nodeId]);
|
||||
|
||||
return showSourceHandle ? (
|
||||
<>
|
||||
{RightHandle}
|
||||
{LeftHandlee}
|
||||
{TopHandlee}
|
||||
{BottomHandlee}
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const ConnectionTargetHandle = ({ nodeId }: { nodeId: string }) => {
|
||||
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
|
||||
const node = useMemo(() => nodeList.find((node) => node.nodeId === nodeId), [nodeList, nodeId]);
|
||||
|
||||
const showHandle = useMemo(() => {
|
||||
if (!node) return false;
|
||||
if (connectingEdge && connectingEdge.nodeId === nodeId) return false;
|
||||
return true;
|
||||
}, [connectingEdge, node, nodeId]);
|
||||
|
||||
const LeftHandle = useMemo(() => {
|
||||
if (!node || !node?.targetHandle?.left) return null;
|
||||
|
||||
const handleId = getHandleId(nodeId, 'target', Position.Left);
|
||||
|
||||
return (
|
||||
<TargetHandle
|
||||
nodeId={nodeId}
|
||||
handleId={handleId}
|
||||
position={Position.Left}
|
||||
translate={[-2, 0]}
|
||||
/>
|
||||
);
|
||||
}, [node, nodeId]);
|
||||
const rightHandle = useMemo(() => {
|
||||
if (!node || !node?.targetHandle?.right) return null;
|
||||
|
||||
const handleId = getHandleId(nodeId, 'target', Position.Right);
|
||||
|
||||
return (
|
||||
<TargetHandle
|
||||
nodeId={nodeId}
|
||||
handleId={handleId}
|
||||
position={Position.Right}
|
||||
translate={[2, 0]}
|
||||
/>
|
||||
);
|
||||
}, [node, nodeId]);
|
||||
const topHandle = useMemo(() => {
|
||||
if (!node || !node?.targetHandle?.top) return null;
|
||||
|
||||
const handleId = getHandleId(nodeId, 'target', Position.Top);
|
||||
|
||||
return (
|
||||
<TargetHandle
|
||||
nodeId={nodeId}
|
||||
handleId={handleId}
|
||||
position={Position.Top}
|
||||
translate={[0, -2]}
|
||||
/>
|
||||
);
|
||||
}, [node, nodeId]);
|
||||
const bottomHandle = useMemo(() => {
|
||||
if (!node || !node?.targetHandle?.bottom) return null;
|
||||
|
||||
const handleId = getHandleId(nodeId, 'target', Position.Bottom);
|
||||
|
||||
return (
|
||||
<TargetHandle
|
||||
nodeId={nodeId}
|
||||
handleId={handleId}
|
||||
position={Position.Bottom}
|
||||
translate={[0, 2]}
|
||||
/>
|
||||
);
|
||||
}, [node, nodeId]);
|
||||
|
||||
return showHandle ? (
|
||||
<>
|
||||
{LeftHandle}
|
||||
{rightHandle}
|
||||
{topHandle}
|
||||
{bottomHandle}
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default <></>;
|
||||
@@ -1,109 +0,0 @@
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { Box, BoxProps } from '@chakra-ui/react';
|
||||
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Connection, Handle, Position } from 'reactflow';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
const handleSize = '14px';
|
||||
|
||||
type ToolHandleProps = BoxProps & {
|
||||
nodeId: string;
|
||||
};
|
||||
export const ToolTargetHandle = ({ nodeId }: ToolHandleProps) => {
|
||||
const { t } = useTranslation();
|
||||
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
||||
|
||||
const handleId = NodeOutputKeyEnum.selectedTools;
|
||||
|
||||
const connected = edges.some((edge) => edge.target === nodeId && edge.targetHandle === handleId);
|
||||
|
||||
// if top handle is connected, return null
|
||||
const hidden =
|
||||
!connected &&
|
||||
(connectingEdge?.handleId !== NodeOutputKeyEnum.selectedTools ||
|
||||
edges.some((edge) => edge.targetHandle === getHandleId(nodeId, 'target', 'top')));
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return hidden ? null : (
|
||||
<MyTooltip label={t('core.workflow.tool.Handle')} shouldWrapChildren={false}>
|
||||
<Handle
|
||||
style={{
|
||||
borderRadius: '0',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
width: handleSize,
|
||||
height: handleSize
|
||||
}}
|
||||
type="target"
|
||||
id={handleId}
|
||||
position={Position.Top}
|
||||
>
|
||||
<Box
|
||||
className="flow-handle"
|
||||
w={handleSize}
|
||||
h={handleSize}
|
||||
border={'4px solid #8774EE'}
|
||||
transform={'translate(0,-30%) rotate(45deg)'}
|
||||
pointerEvents={'none'}
|
||||
visibility={'visible'}
|
||||
/>
|
||||
</Handle>
|
||||
</MyTooltip>
|
||||
);
|
||||
}, [handleId, hidden, t]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export const ToolSourceHandle = ({ nodeId }: ToolHandleProps) => {
|
||||
const { t } = useTranslation();
|
||||
const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges);
|
||||
|
||||
/* onConnect edge, delete tool input and switch */
|
||||
const onConnect = useCallback(
|
||||
(e: Connection) => {
|
||||
setEdges((edges) =>
|
||||
edges.filter((edge) => {
|
||||
if (edge.target !== e.target) return true;
|
||||
if (edge.targetHandle === NodeOutputKeyEnum.selectedTools) return true;
|
||||
return false;
|
||||
})
|
||||
);
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<MyTooltip label={t('core.workflow.tool.Handle')} shouldWrapChildren={false}>
|
||||
<Handle
|
||||
style={{
|
||||
borderRadius: '0',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
width: handleSize,
|
||||
height: handleSize
|
||||
}}
|
||||
type="source"
|
||||
id={NodeOutputKeyEnum.selectedTools}
|
||||
position={Position.Bottom}
|
||||
onConnect={onConnect}
|
||||
>
|
||||
<Box
|
||||
w={handleSize}
|
||||
h={handleSize}
|
||||
border={'4px solid #8774EE'}
|
||||
transform={'translate(0,30%) rotate(45deg)'}
|
||||
pointerEvents={'none'}
|
||||
/>
|
||||
</Handle>
|
||||
</MyTooltip>
|
||||
);
|
||||
}, [onConnect, t]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
@@ -1,250 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import { handleHighLightStyle, sourceCommonStyle, handleConnectedStyle, handleSize } from './style';
|
||||
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
handleId: string;
|
||||
position: Position;
|
||||
translate?: [number, number];
|
||||
};
|
||||
|
||||
const MySourceHandle = React.memo(function MySourceHandle({
|
||||
nodeId,
|
||||
translate,
|
||||
handleId,
|
||||
position,
|
||||
highlightStyle,
|
||||
connectedStyle
|
||||
}: Props & {
|
||||
highlightStyle: Record<string, any>;
|
||||
connectedStyle: Record<string, any>;
|
||||
}) {
|
||||
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
||||
|
||||
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes);
|
||||
const hoverNodeId = useContextSelector(WorkflowContext, (v) => v.hoverNodeId);
|
||||
|
||||
const node = useMemo(() => nodes.find((node) => node.data.nodeId === nodeId), [nodes, nodeId]);
|
||||
const connected = edges.some((edge) => edge.sourceHandle === handleId);
|
||||
const nodeIsHover = hoverNodeId === nodeId;
|
||||
|
||||
const active = useMemo(
|
||||
() => nodeIsHover || node?.selected || connectingEdge?.handleId === handleId,
|
||||
[nodeIsHover, node?.selected, connectingEdge, handleId]
|
||||
);
|
||||
|
||||
const translateStr = useMemo(() => {
|
||||
if (!translate) return '';
|
||||
if (position === Position.Right) {
|
||||
return `${active ? translate[0] + 2 : translate[0]}px, -50%`;
|
||||
}
|
||||
if (position === Position.Left) {
|
||||
return `${active ? translate[0] + 2 : translate[0]}px, -50%`;
|
||||
}
|
||||
if (position === Position.Top) {
|
||||
return `-50%, ${active ? translate[1] - 2 : translate[1]}px`;
|
||||
}
|
||||
if (position === Position.Bottom) {
|
||||
return `-50%, ${active ? translate[1] + 2 : translate[1]}px`;
|
||||
}
|
||||
}, [active, position, translate]);
|
||||
|
||||
const transform = useMemo(
|
||||
() => (translateStr ? `translate(${translateStr})` : ''),
|
||||
[translateStr]
|
||||
);
|
||||
|
||||
const { styles, showAddIcon } = useMemo(() => {
|
||||
if (active) {
|
||||
return {
|
||||
styles: {
|
||||
...highlightStyle,
|
||||
...(translateStr && {
|
||||
transform
|
||||
})
|
||||
},
|
||||
showAddIcon: true
|
||||
};
|
||||
}
|
||||
|
||||
if (connected) {
|
||||
return {
|
||||
styles: {
|
||||
...connectedStyle,
|
||||
...(translateStr && {
|
||||
transform
|
||||
})
|
||||
},
|
||||
showAddIcon: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
styles: undefined,
|
||||
showAddIcon: false
|
||||
};
|
||||
}, [active, connected, highlightStyle, translateStr, transform, connectedStyle]);
|
||||
|
||||
const RenderHandle = useMemo(() => {
|
||||
return (
|
||||
<Handle
|
||||
style={
|
||||
!!styles
|
||||
? styles
|
||||
: {
|
||||
visibility: 'hidden',
|
||||
transform,
|
||||
...handleSize
|
||||
}
|
||||
}
|
||||
type="source"
|
||||
id={handleId}
|
||||
position={position}
|
||||
isConnectableEnd={false}
|
||||
>
|
||||
{showAddIcon && (
|
||||
<SmallAddIcon pointerEvents={'none'} color={'primary.600'} fontWeight={'bold'} />
|
||||
)}
|
||||
</Handle>
|
||||
);
|
||||
}, [handleId, position, showAddIcon, styles, transform]);
|
||||
|
||||
if (!node) return null;
|
||||
if (connectingEdge?.handleId === NodeOutputKeyEnum.selectedTools) return null;
|
||||
return <>{RenderHandle}</>;
|
||||
});
|
||||
|
||||
export const SourceHandle = (props: Props) => {
|
||||
return (
|
||||
<MySourceHandle
|
||||
{...props}
|
||||
highlightStyle={{ ...sourceCommonStyle, ...handleHighLightStyle }}
|
||||
connectedStyle={{ ...sourceCommonStyle, ...handleConnectedStyle }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const MyTargetHandle = React.memo(function MyTargetHandle({
|
||||
nodeId,
|
||||
handleId,
|
||||
position,
|
||||
translate,
|
||||
highlightStyle,
|
||||
connectedStyle
|
||||
}: Props & {
|
||||
highlightStyle: Record<string, any>;
|
||||
connectedStyle: Record<string, any>;
|
||||
}) {
|
||||
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
||||
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const node = useMemo(() => nodeList.find((node) => node.nodeId === nodeId), [nodeList, nodeId]);
|
||||
const connected = edges.some((edge) => edge.targetHandle === handleId);
|
||||
const connectedEdges = edges.filter((edge) => edge.target === nodeId);
|
||||
|
||||
const translateStr = useMemo(() => {
|
||||
if (!translate) return '';
|
||||
|
||||
if (position === Position.Right) {
|
||||
return `${connectingEdge ? translate[0] + 2 : translate[0]}px, -50%`;
|
||||
}
|
||||
if (position === Position.Left) {
|
||||
return `${connectingEdge ? translate[0] - 2 : translate[0]}px, -50%`;
|
||||
}
|
||||
if (position === Position.Top) {
|
||||
return `-50%, ${connectingEdge ? translate[1] - 2 : translate[1]}px`;
|
||||
}
|
||||
if (position === Position.Bottom) {
|
||||
return `-50%, ${connectingEdge ? translate[1] + 2 : translate[1]}px`;
|
||||
}
|
||||
}, [connectingEdge, position, translate]);
|
||||
|
||||
const transform = useMemo(
|
||||
() => (translateStr ? `translate(${translateStr})` : ''),
|
||||
[translateStr]
|
||||
);
|
||||
|
||||
const styles = useMemo(() => {
|
||||
if (!connectingEdge && !connected) return;
|
||||
|
||||
if (connectingEdge) {
|
||||
return {
|
||||
...highlightStyle,
|
||||
transform
|
||||
};
|
||||
}
|
||||
|
||||
if (connected) {
|
||||
return {
|
||||
...connectedStyle,
|
||||
transform
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [connected, connectingEdge, connectedStyle, highlightStyle, transform]);
|
||||
|
||||
const showHandle = useMemo(() => {
|
||||
if (!node) return false;
|
||||
// check tool connected
|
||||
if (
|
||||
edges.some(
|
||||
(edge) => edge.target === nodeId && edge.targetHandle === NodeOutputKeyEnum.selectedTools
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (connectingEdge?.handleId && !connectingEdge.handleId?.includes('source')) return false;
|
||||
|
||||
// From same source node and same handle
|
||||
if (
|
||||
connectedEdges.some(
|
||||
(item) => item.sourceHandle === connectingEdge?.handleId && item.target === nodeId
|
||||
)
|
||||
)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}, [connectedEdges, connectingEdge?.handleId, edges, node, nodeId]);
|
||||
|
||||
const RenderHandle = useMemo(() => {
|
||||
return (
|
||||
<Handle
|
||||
style={
|
||||
!!styles && showHandle
|
||||
? styles
|
||||
: {
|
||||
visibility: 'hidden',
|
||||
transform,
|
||||
...handleSize
|
||||
}
|
||||
}
|
||||
type="target"
|
||||
id={handleId}
|
||||
position={position}
|
||||
isConnectableStart={false}
|
||||
></Handle>
|
||||
);
|
||||
}, [styles, showHandle, transform, handleId, position]);
|
||||
|
||||
return RenderHandle;
|
||||
});
|
||||
|
||||
export const TargetHandle = (props: Props) => {
|
||||
return (
|
||||
<MyTargetHandle
|
||||
{...props}
|
||||
highlightStyle={{ ...sourceCommonStyle, ...handleHighLightStyle }}
|
||||
connectedStyle={{ ...sourceCommonStyle, ...handleConnectedStyle }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default <></>;
|
||||
@@ -1,25 +0,0 @@
|
||||
export const primaryColor = '#3370FF';
|
||||
export const lowPrimaryColor = '#94B5FF';
|
||||
export const handleSize = {
|
||||
width: '18px',
|
||||
height: '18px'
|
||||
};
|
||||
|
||||
export const sourceCommonStyle = {
|
||||
backgroundColor: 'white',
|
||||
borderWidth: '3px',
|
||||
borderRadius: '50%'
|
||||
};
|
||||
export const handleConnectedStyle = {
|
||||
borderColor: lowPrimaryColor,
|
||||
width: '14px',
|
||||
height: '14px'
|
||||
};
|
||||
export const handleHighLightStyle = {
|
||||
borderColor: primaryColor,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '18px',
|
||||
height: '18px'
|
||||
};
|
||||
@@ -1,645 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Box, Button, Card, Flex } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useEditTitle } from '@/web/common/hooks/useEditTitle';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
|
||||
import { ToolTargetHandle } from './Handle/ToolHandle';
|
||||
import { useEditTextarea } from '@fastgpt/web/hooks/useEditTextarea';
|
||||
import { ConnectionSourceHandle, ConnectionTargetHandle } from './Handle/ConnectionHandle';
|
||||
import { useDebug } from '../../hooks/useDebug';
|
||||
import { ResponseBox } from '@/components/ChatBox/components/WholeResponseModal';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { getPreviewPluginModule } from '@/web/core/plugin/api';
|
||||
import { storeNode2FlowNode, updateFlowNodeVersion } from '@/web/core/workflow/utils';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../../context';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
|
||||
type Props = FlowNodeItemType & {
|
||||
children?: React.ReactNode | React.ReactNode[] | string;
|
||||
minW?: string | number;
|
||||
maxW?: string | number;
|
||||
selected?: boolean;
|
||||
menuForbid?: {
|
||||
debug?: boolean;
|
||||
rename?: boolean;
|
||||
copy?: boolean;
|
||||
delete?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const NodeCard = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const {
|
||||
children,
|
||||
avatar = LOGO_ICON,
|
||||
name = t('core.module.template.UnKnow Module'),
|
||||
intro,
|
||||
minW = '300px',
|
||||
maxW = '600px',
|
||||
nodeId,
|
||||
flowNodeType,
|
||||
selected,
|
||||
menuForbid,
|
||||
isTool = false,
|
||||
isError = false,
|
||||
debugResult,
|
||||
pluginId
|
||||
} = props;
|
||||
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const setHoverNodeId = useContextSelector(WorkflowContext, (v) => v.setHoverNodeId);
|
||||
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode);
|
||||
|
||||
const [hasNewVersion, setHasNewVersion] = useState(false);
|
||||
const { setLoading } = useSystemStore();
|
||||
|
||||
// custom title edit
|
||||
const { onOpenModal: onOpenCustomTitleModal, EditModal: EditTitleModal } = useEditTitle({
|
||||
title: t('common.Custom Title'),
|
||||
placeholder: appT('module.Custom Title Tip') || ''
|
||||
});
|
||||
|
||||
const showToolHandle = useMemo(
|
||||
() => isTool && !!nodeList.find((item) => item?.flowNodeType === FlowNodeTypeEnum.tools),
|
||||
[isTool, nodeList]
|
||||
);
|
||||
|
||||
const node = nodeList.find((node) => node.nodeId === nodeId);
|
||||
const { openConfirm: onOpenConfirmSync, ConfirmModal: ConfirmSyncModal } = useConfirm({
|
||||
content: appT('module.Confirm Sync')
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPluginModule = async () => {
|
||||
if (node?.flowNodeType === FlowNodeTypeEnum.pluginModule) {
|
||||
if (!node?.pluginId) return;
|
||||
const template = await getPreviewPluginModule(node.pluginId);
|
||||
setHasNewVersion(!!template.nodeVersion && node.nodeVersion !== template.nodeVersion);
|
||||
} else {
|
||||
const template = moduleTemplatesFlat.find(
|
||||
(item) => item.flowNodeType === node?.flowNodeType
|
||||
);
|
||||
setHasNewVersion(node?.version !== template?.version);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPluginModule();
|
||||
}, [node]);
|
||||
|
||||
const template = moduleTemplatesFlat.find((item) => item.flowNodeType === node?.flowNodeType);
|
||||
|
||||
const onClickSyncVersion = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (!node || !template) return;
|
||||
if (node?.flowNodeType === 'pluginModule') {
|
||||
if (!node.pluginId) return;
|
||||
onResetNode({
|
||||
id: nodeId,
|
||||
node: await getPreviewPluginModule(node.pluginId)
|
||||
});
|
||||
} else {
|
||||
onResetNode({
|
||||
id: nodeId,
|
||||
node: updateFlowNodeVersion(node, template)
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching plugin module:', error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [node, nodeId, onResetNode, setLoading, template]);
|
||||
|
||||
/* Node header */
|
||||
const Header = useMemo(() => {
|
||||
return (
|
||||
<Box position={'relative'}>
|
||||
{/* debug */}
|
||||
<Box px={4} py={3}>
|
||||
{/* tool target handle */}
|
||||
{showToolHandle && <ToolTargetHandle nodeId={nodeId} />}
|
||||
|
||||
{/* avatar and name */}
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar src={avatar} borderRadius={'0'} objectFit={'contain'} w={'30px'} h={'30px'} />
|
||||
<Box ml={3} fontSize={'md'} fontWeight={'medium'}>
|
||||
{t(name)}
|
||||
</Box>
|
||||
{!menuForbid?.rename && (
|
||||
<MyIcon
|
||||
className="controller-rename"
|
||||
display={'none'}
|
||||
name={'edit'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
ml={1}
|
||||
color={'myGray.500'}
|
||||
_hover={{ color: 'primary.600' }}
|
||||
onClick={() => {
|
||||
onOpenCustomTitleModal({
|
||||
defaultVal: name,
|
||||
onSuccess: (e) => {
|
||||
if (!e) {
|
||||
return toast({
|
||||
title: appT('modules.Title is required'),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'attr',
|
||||
key: 'name',
|
||||
value: e
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box flex={1} />
|
||||
{hasNewVersion && (
|
||||
<MyTooltip label={appT('app.modules.click to update')}>
|
||||
<Button
|
||||
bg={'yellow.50'}
|
||||
color={'yellow.600'}
|
||||
variant={'ghost'}
|
||||
h={8}
|
||||
px={2}
|
||||
rounded={'6px'}
|
||||
fontSize={'xs'}
|
||||
fontWeight={'medium'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'yellow.100' }}
|
||||
onClick={onOpenConfirmSync(onClickSyncVersion)}
|
||||
>
|
||||
<Box>{appT('app.modules.has new version')}</Box>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</Button>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
<MenuRender
|
||||
nodeId={nodeId}
|
||||
pluginId={pluginId}
|
||||
flowNodeType={flowNodeType}
|
||||
menuForbid={menuForbid}
|
||||
/>
|
||||
<NodeIntro nodeId={nodeId} intro={intro} />
|
||||
</Box>
|
||||
<ConfirmSyncModal />
|
||||
</Box>
|
||||
);
|
||||
}, [
|
||||
showToolHandle,
|
||||
nodeId,
|
||||
avatar,
|
||||
t,
|
||||
name,
|
||||
menuForbid,
|
||||
hasNewVersion,
|
||||
appT,
|
||||
onOpenConfirmSync,
|
||||
onClickSyncVersion,
|
||||
pluginId,
|
||||
flowNodeType,
|
||||
intro,
|
||||
ConfirmSyncModal,
|
||||
onOpenCustomTitleModal,
|
||||
onChangeNode,
|
||||
toast
|
||||
]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
minW={minW}
|
||||
maxW={maxW}
|
||||
bg={'white'}
|
||||
borderWidth={'1px'}
|
||||
borderRadius={'md'}
|
||||
boxShadow={'1'}
|
||||
_hover={{
|
||||
boxShadow: '4',
|
||||
'& .controller-menu': {
|
||||
display: 'flex'
|
||||
},
|
||||
'& .controller-debug': {
|
||||
display: 'block'
|
||||
},
|
||||
'& .controller-rename': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setHoverNodeId(nodeId)}
|
||||
onMouseLeave={() => setHoverNodeId(undefined)}
|
||||
{...(isError
|
||||
? {
|
||||
borderColor: 'red.500',
|
||||
onMouseDownCapture: () => onUpdateNodeError(nodeId, false)
|
||||
}
|
||||
: {
|
||||
borderColor: selected ? 'primary.600' : 'borderColor.base'
|
||||
})}
|
||||
>
|
||||
<NodeDebugResponse nodeId={nodeId} debugResult={debugResult} />
|
||||
{Header}
|
||||
{children}
|
||||
<ConnectionSourceHandle nodeId={nodeId} />
|
||||
<ConnectionTargetHandle nodeId={nodeId} />
|
||||
|
||||
<EditTitleModal maxLength={20} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NodeCard);
|
||||
|
||||
const MenuRender = React.memo(function MenuRender({
|
||||
nodeId,
|
||||
pluginId,
|
||||
flowNodeType,
|
||||
menuForbid
|
||||
}: {
|
||||
nodeId: string;
|
||||
pluginId?: string;
|
||||
flowNodeType: Props['flowNodeType'];
|
||||
menuForbid?: Props['menuForbid'];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { openDebugNode, DebugInputModal } = useDebug();
|
||||
|
||||
const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({
|
||||
content: t('core.module.Confirm Delete Node'),
|
||||
type: 'delete'
|
||||
});
|
||||
|
||||
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
|
||||
const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges);
|
||||
|
||||
const onCopyNode = useCallback(
|
||||
(nodeId: string) => {
|
||||
setNodes((state) => {
|
||||
const node = state.find((node) => node.id === nodeId);
|
||||
if (!node) return state;
|
||||
const template = {
|
||||
avatar: node.data.avatar,
|
||||
name: node.data.name,
|
||||
intro: node.data.intro,
|
||||
flowNodeType: node.data.flowNodeType,
|
||||
inputs: node.data.inputs,
|
||||
outputs: node.data.outputs,
|
||||
showStatus: node.data.showStatus,
|
||||
pluginId: node.data.pluginId,
|
||||
version: node.data.version
|
||||
};
|
||||
return state.concat(
|
||||
storeNode2FlowNode({
|
||||
item: {
|
||||
flowNodeType: template.flowNodeType,
|
||||
avatar: template.avatar,
|
||||
name: template.name,
|
||||
intro: template.intro,
|
||||
nodeId: getNanoid(),
|
||||
position: { x: node.position.x + 200, y: node.position.y + 50 },
|
||||
showStatus: template.showStatus,
|
||||
pluginId: template.pluginId,
|
||||
inputs: template.inputs,
|
||||
outputs: template.outputs,
|
||||
version: template.version
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
[setNodes]
|
||||
);
|
||||
const onDelNode = useCallback(
|
||||
(nodeId: string) => {
|
||||
setNodes((state) => state.filter((item) => item.data.nodeId !== nodeId));
|
||||
setEdges((state) => state.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
|
||||
},
|
||||
[setEdges, setNodes]
|
||||
);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
const menuList = [
|
||||
...(menuForbid?.debug
|
||||
? []
|
||||
: [
|
||||
{
|
||||
icon: 'core/workflow/debug',
|
||||
label: t('core.workflow.Debug'),
|
||||
variant: 'whiteBase',
|
||||
onClick: () => openDebugNode({ entryNodeId: nodeId })
|
||||
}
|
||||
]),
|
||||
...(menuForbid?.copy
|
||||
? []
|
||||
: [
|
||||
{
|
||||
icon: 'copy',
|
||||
label: t('common.Copy'),
|
||||
variant: 'whiteBase',
|
||||
onClick: () => onCopyNode(nodeId)
|
||||
}
|
||||
]),
|
||||
...(menuForbid?.delete
|
||||
? []
|
||||
: [
|
||||
{
|
||||
icon: 'delete',
|
||||
label: t('common.Delete'),
|
||||
variant: 'whiteDanger',
|
||||
onClick: onOpenConfirmDeleteNode(() => onDelNode(nodeId))
|
||||
}
|
||||
])
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
className="nodrag controller-menu"
|
||||
display={'none'}
|
||||
flexDirection={'column'}
|
||||
gap={3}
|
||||
position={'absolute'}
|
||||
top={'-20px'}
|
||||
right={0}
|
||||
transform={'translateX(90%)'}
|
||||
pl={'20px'}
|
||||
pr={'10px'}
|
||||
pb={'20px'}
|
||||
pt={'20px'}
|
||||
>
|
||||
{menuList.map((item) => (
|
||||
<Box key={item.icon}>
|
||||
<Button
|
||||
size={'xs'}
|
||||
variant={item.variant}
|
||||
leftIcon={<MyIcon name={item.icon as any} w={'13px'} />}
|
||||
onClick={item.onClick}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<ConfirmDeleteModal />
|
||||
<DebugInputModal />
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
menuForbid?.debug,
|
||||
menuForbid?.copy,
|
||||
menuForbid?.delete,
|
||||
t,
|
||||
onOpenConfirmDeleteNode,
|
||||
ConfirmDeleteModal,
|
||||
DebugInputModal,
|
||||
openDebugNode,
|
||||
nodeId,
|
||||
onCopyNode,
|
||||
onDelNode
|
||||
]);
|
||||
|
||||
return Render;
|
||||
});
|
||||
|
||||
const NodeIntro = React.memo(function NodeIntro({
|
||||
nodeId,
|
||||
intro = ''
|
||||
}: {
|
||||
nodeId: string;
|
||||
intro?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const NodeIsTool = useMemo(() => {
|
||||
const { isTool } = splitToolInputs([], nodeId);
|
||||
return isTool;
|
||||
}, [nodeId, splitToolInputs]);
|
||||
|
||||
// edit intro
|
||||
const { onOpenModal: onOpenIntroModal, EditModal: EditIntroModal } = useEditTextarea({
|
||||
title: t('core.module.Edit intro'),
|
||||
tip: '调整该模块会对工具调用时机有影响。\n你可以通过精确的描述该模块功能,引导模型进行工具调用。',
|
||||
canEmpty: false
|
||||
});
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems={'flex-end'} py={1}>
|
||||
<Box fontSize={'xs'} color={'myGray.600'} flex={'1 0 0'}>
|
||||
{t(intro)}
|
||||
</Box>
|
||||
{NodeIsTool && (
|
||||
<Button
|
||||
size={'xs'}
|
||||
variant={'whiteBase'}
|
||||
onClick={() => {
|
||||
onOpenIntroModal({
|
||||
defaultVal: intro,
|
||||
onSuccess(e) {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'attr',
|
||||
key: 'intro',
|
||||
value: e
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('core.module.Edit intro')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<EditIntroModal maxLength={500} />
|
||||
</>
|
||||
);
|
||||
}, [EditIntroModal, intro, NodeIsTool, nodeId, onChangeNode, onOpenIntroModal, t]);
|
||||
|
||||
return Render;
|
||||
});
|
||||
|
||||
const NodeDebugResponse = React.memo(function NodeDebugResponse({
|
||||
nodeId,
|
||||
debugResult
|
||||
}: {
|
||||
nodeId: string;
|
||||
debugResult: FlowNodeItemType['debugResult'];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const onStopNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStopNodeDebug);
|
||||
const onNextNodeDebug = useContextSelector(WorkflowContext, (v) => v.onNextNodeDebug);
|
||||
const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData);
|
||||
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
content: t('core.workflow.Confirm stop debug')
|
||||
});
|
||||
|
||||
const RenderStatus = useMemo(() => {
|
||||
const map = {
|
||||
running: {
|
||||
bg: 'primary.50',
|
||||
text: t('core.workflow.Running'),
|
||||
icon: 'core/workflow/running'
|
||||
},
|
||||
success: {
|
||||
bg: 'green.50',
|
||||
text: t('core.workflow.Success'),
|
||||
icon: 'core/workflow/runSuccess'
|
||||
},
|
||||
failed: {
|
||||
bg: 'red.50',
|
||||
text: t('core.workflow.Failed'),
|
||||
icon: 'core/workflow/runError'
|
||||
},
|
||||
skipped: {
|
||||
bg: 'myGray.50',
|
||||
text: t('core.workflow.Skipped'),
|
||||
icon: 'core/workflow/runSkip'
|
||||
}
|
||||
};
|
||||
|
||||
const statusData = map[debugResult?.status || 'running'];
|
||||
|
||||
const response = debugResult?.response;
|
||||
|
||||
const onStop = () => {
|
||||
openConfirm(onStopNodeDebug)();
|
||||
};
|
||||
|
||||
return !!debugResult && !!statusData ? (
|
||||
<>
|
||||
<Flex px={4} bg={statusData.bg} borderTopRadius={'md'} py={3}>
|
||||
<MyIcon name={statusData.icon as any} w={'16px'} mr={2} />
|
||||
<Box color={'myGray.900'} fontWeight={'bold'} flex={'1 0 0'}>
|
||||
{statusData.text}
|
||||
</Box>
|
||||
{debugResult.status !== 'running' && (
|
||||
<Box
|
||||
color={'primary.700'}
|
||||
cursor={'pointer'}
|
||||
fontSize={'sm'}
|
||||
onClick={() =>
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'attr',
|
||||
key: 'debugResult',
|
||||
value: {
|
||||
...debugResult,
|
||||
showResult: !debugResult.showResult
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
{debugResult.showResult
|
||||
? t('core.workflow.debug.Hide result')
|
||||
: t('core.workflow.debug.Show result')}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
{/* result */}
|
||||
{debugResult.showResult && (
|
||||
<Card
|
||||
className="nowheel"
|
||||
position={'absolute'}
|
||||
right={'-430px'}
|
||||
top={0}
|
||||
zIndex={10}
|
||||
w={'420px'}
|
||||
maxH={'100%'}
|
||||
minH={'300px'}
|
||||
overflowY={'auto'}
|
||||
border={'base'}
|
||||
>
|
||||
{/* Status header */}
|
||||
<Flex px={4} mb={1} py={3} alignItems={'center'} borderBottom={'base'}>
|
||||
<MyIcon mr={1} name={'core/workflow/debugResult'} w={'20px'} color={'primary.600'} />
|
||||
<Box fontWeight={'bold'} flex={'1'}>
|
||||
{t('core.workflow.debug.Run result')}
|
||||
</Box>
|
||||
{workflowDebugData?.nextRunNodes.length !== 0 && (
|
||||
<Button
|
||||
size={'sm'}
|
||||
leftIcon={<MyIcon name={'core/chat/stopSpeech'} w={'16px'} />}
|
||||
variant={'whiteDanger'}
|
||||
onClick={onStop}
|
||||
>
|
||||
{t('core.workflow.Stop debug')}
|
||||
</Button>
|
||||
)}
|
||||
{(debugResult.status === 'success' || debugResult.status === 'skipped') &&
|
||||
!debugResult.isExpired &&
|
||||
workflowDebugData?.nextRunNodes &&
|
||||
workflowDebugData.nextRunNodes.length > 0 && (
|
||||
<Button
|
||||
ml={2}
|
||||
size={'sm'}
|
||||
leftIcon={<MyIcon name={'core/workflow/debugNext'} w={'16px'} />}
|
||||
variant={'primary'}
|
||||
onClick={() => onNextNodeDebug()}
|
||||
>
|
||||
{t('common.Next Step')}
|
||||
</Button>
|
||||
)}
|
||||
{workflowDebugData?.nextRunNodes && workflowDebugData?.nextRunNodes.length === 0 && (
|
||||
<Button ml={2} size={'sm'} variant={'primary'} onClick={onStopNodeDebug}>
|
||||
{t('core.workflow.debug.Done')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
{/* Show result */}
|
||||
<Box maxH={'100%'} overflow={'auto'}>
|
||||
{!debugResult.message && !response && (
|
||||
<EmptyTip text={t('core.workflow.debug.Not result')} pt={2} pb={5} />
|
||||
)}
|
||||
{debugResult.message && (
|
||||
<Box color={'red.600'} px={3} py={4}>
|
||||
{debugResult.message}
|
||||
</Box>
|
||||
)}
|
||||
{response && <ResponseBox response={[response]} showDetail hideTabs />}
|
||||
</Box>
|
||||
</Card>
|
||||
)}
|
||||
<ConfirmModal />
|
||||
</>
|
||||
) : null;
|
||||
}, [
|
||||
ConfirmModal,
|
||||
debugResult,
|
||||
nodeId,
|
||||
onChangeNode,
|
||||
onNextNodeDebug,
|
||||
onStopNodeDebug,
|
||||
openConfirm,
|
||||
t,
|
||||
workflowDebugData?.nextRunNodes
|
||||
]);
|
||||
|
||||
return <>{RenderStatus}</>;
|
||||
});
|
||||
@@ -1,205 +0,0 @@
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
|
||||
import NodeInputSelect from '@fastgpt/web/components/core/workflow/NodeInputSelect';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import ValueTypeLabel from '../ValueTypeLabel';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
const FieldEditModal = dynamic(() => import('../FieldEditModal'));
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
input: FlowNodeInputItemType;
|
||||
};
|
||||
|
||||
const InputLabel = ({ nodeId, input }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const {
|
||||
description,
|
||||
toolDescription,
|
||||
required,
|
||||
label,
|
||||
selectedTypeIndex,
|
||||
renderTypeList,
|
||||
valueType,
|
||||
canEdit,
|
||||
key
|
||||
} = input;
|
||||
const [editField, setEditField] = useState<EditNodeFieldType>();
|
||||
|
||||
const onChangeRenderType = useCallback(
|
||||
(e: string) => {
|
||||
const index = renderTypeList.findIndex((item) => item === e) || 0;
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: input.key,
|
||||
value: {
|
||||
...input,
|
||||
selectedTypeIndex: index,
|
||||
value: undefined
|
||||
}
|
||||
});
|
||||
},
|
||||
[input, nodeId, onChangeNode, renderTypeList]
|
||||
);
|
||||
|
||||
const RenderLabel = useMemo(() => {
|
||||
const renderType = renderTypeList?.[selectedTypeIndex || 0];
|
||||
|
||||
return (
|
||||
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
position={'relative'}
|
||||
fontWeight={'medium'}
|
||||
color={'myGray.600'}
|
||||
>
|
||||
{required && (
|
||||
<Box position={'absolute'} left={-2} top={-1} color={'red.600'}>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
{t(label)}
|
||||
{description && <QuestionTip ml={1} label={t(description)}></QuestionTip>}
|
||||
</Flex>
|
||||
{/* value type */}
|
||||
{renderType === FlowNodeInputTypeEnum.reference && <ValueTypeLabel valueType={valueType} />}
|
||||
{/* edit config */}
|
||||
{canEdit && (
|
||||
<>
|
||||
{input.editField && Object.keys(input.editField).length > 0 && (
|
||||
<MyIcon
|
||||
name={'common/settingLight'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
ml={3}
|
||||
color={'myGray.600'}
|
||||
_hover={{ color: 'primary.500' }}
|
||||
onClick={() =>
|
||||
setEditField({
|
||||
...input,
|
||||
inputType: renderTypeList[0],
|
||||
valueType: valueType,
|
||||
key,
|
||||
label,
|
||||
description,
|
||||
isToolInput: !!toolDescription
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<MyIcon
|
||||
className="delete"
|
||||
name={'delete'}
|
||||
w={'14px'}
|
||||
color={'myGray.600'}
|
||||
cursor={'pointer'}
|
||||
ml={2}
|
||||
_hover={{ color: 'red.500' }}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'delInput',
|
||||
key: key
|
||||
});
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'delOutput',
|
||||
key: key
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{/* input type select */}
|
||||
{renderTypeList && renderTypeList.length > 1 && (
|
||||
<Box ml={2}>
|
||||
<NodeInputSelect
|
||||
renderTypeList={renderTypeList}
|
||||
renderTypeIndex={selectedTypeIndex}
|
||||
onChange={onChangeRenderType}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!!editField?.key && (
|
||||
<FieldEditModal
|
||||
editField={input.editField}
|
||||
keys={[editField.key]}
|
||||
defaultField={editField}
|
||||
onClose={() => setEditField(undefined)}
|
||||
onSubmit={({ data, changeKey }) => {
|
||||
if (!data.inputType || !data.key || !data.label || !editField.key) return;
|
||||
|
||||
const newInput: FlowNodeInputItemType = {
|
||||
...input,
|
||||
renderTypeList: [data.inputType],
|
||||
valueType: data.valueType,
|
||||
key: data.key,
|
||||
required: data.required,
|
||||
label: data.label,
|
||||
description: data.description,
|
||||
toolDescription: data.isToolInput ? data.description : undefined,
|
||||
maxLength: data.maxLength,
|
||||
value: data.defaultValue,
|
||||
max: data.max,
|
||||
min: data.min
|
||||
};
|
||||
|
||||
if (changeKey) {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'replaceInput',
|
||||
key: editField.key,
|
||||
value: newInput
|
||||
});
|
||||
} else {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: newInput.key,
|
||||
value: newInput
|
||||
});
|
||||
}
|
||||
setEditField(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}, [
|
||||
canEdit,
|
||||
description,
|
||||
editField,
|
||||
input,
|
||||
key,
|
||||
label,
|
||||
nodeId,
|
||||
onChangeNode,
|
||||
onChangeRenderType,
|
||||
renderTypeList,
|
||||
required,
|
||||
selectedTypeIndex,
|
||||
t,
|
||||
toolDescription,
|
||||
valueType
|
||||
]);
|
||||
|
||||
return RenderLabel;
|
||||
};
|
||||
|
||||
export default React.memo(InputLabel);
|
||||
@@ -1,119 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import InputLabel from './Label';
|
||||
import type { RenderInputProps } from './type';
|
||||
|
||||
const RenderList: {
|
||||
types: FlowNodeInputTypeEnum[];
|
||||
Component: React.ComponentType<RenderInputProps>;
|
||||
}[] = [
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.reference],
|
||||
Component: dynamic(() => import('./templates/Reference'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.input],
|
||||
Component: dynamic(() => import('./templates/TextInput'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.numberInput],
|
||||
Component: dynamic(() => import('./templates/NumberInput'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.switch],
|
||||
Component: dynamic(() => import('./templates/Switch'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.textarea],
|
||||
Component: dynamic(() => import('./templates/Textarea'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.selectApp],
|
||||
Component: dynamic(() => import('./templates/SelectApp'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.selectLLMModel],
|
||||
Component: dynamic(() => import('./templates/SelectLLMModel'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.settingLLMModel],
|
||||
Component: dynamic(() => import('./templates/SettingLLMModel'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.selectDataset],
|
||||
Component: dynamic(() => import('./templates/SelectDataset'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.selectDatasetParamsModal],
|
||||
Component: dynamic(() => import('./templates/SelectDatasetParams'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.addInputParam],
|
||||
Component: dynamic(() => import('./templates/AddInputParam'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.JSONEditor],
|
||||
Component: dynamic(() => import('./templates/JsonEditor'))
|
||||
},
|
||||
{
|
||||
types: [FlowNodeInputTypeEnum.settingDatasetQuotePrompt],
|
||||
Component: dynamic(() => import('./templates/SettingQuotePrompt'))
|
||||
}
|
||||
];
|
||||
|
||||
const hideLabelTypeList = [FlowNodeInputTypeEnum.addInputParam];
|
||||
|
||||
type Props = {
|
||||
flowInputList: FlowNodeInputItemType[];
|
||||
nodeId: string;
|
||||
CustomComponent?: Record<string, (e: FlowNodeInputItemType) => React.ReactNode>;
|
||||
mb?: number;
|
||||
};
|
||||
const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props) => {
|
||||
const copyInputs = useMemo(() => JSON.stringify(flowInputList), [flowInputList]);
|
||||
const filterInputs = useMemo(() => {
|
||||
const parseSortInputs = JSON.parse(copyInputs) as FlowNodeInputItemType[];
|
||||
return parseSortInputs.filter((input) => {
|
||||
return true;
|
||||
});
|
||||
}, [copyInputs]);
|
||||
|
||||
const memoCustomComponent = useMemo(() => CustomComponent || {}, [CustomComponent]);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return filterInputs.map((input) => {
|
||||
const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0];
|
||||
const RenderComponent = (() => {
|
||||
if (renderType === FlowNodeInputTypeEnum.custom && memoCustomComponent[input.key]) {
|
||||
return <>{memoCustomComponent[input.key]({ ...input })}</>;
|
||||
}
|
||||
|
||||
const Component = RenderList.find((item) => item.types.includes(renderType))?.Component;
|
||||
|
||||
if (!Component) return null;
|
||||
return <Component inputs={filterInputs} item={input} nodeId={nodeId} />;
|
||||
})();
|
||||
|
||||
return renderType !== FlowNodeInputTypeEnum.hidden ? (
|
||||
<Box key={input.key} _notLast={{ mb }} position={'relative'}>
|
||||
{!!input.label && !hideLabelTypeList.includes(renderType) && (
|
||||
<InputLabel nodeId={nodeId} input={input} />
|
||||
)}
|
||||
{!!RenderComponent && (
|
||||
<Box mt={2} className={'nodrag'}>
|
||||
{RenderComponent}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : null;
|
||||
});
|
||||
}, [filterInputs, mb, memoCustomComponent, nodeId]);
|
||||
|
||||
return <>{Render}</>;
|
||||
};
|
||||
|
||||
export default React.memo(RenderInput);
|
||||
@@ -1,112 +0,0 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
|
||||
import dynamic from 'next/dynamic';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import Reference from './Reference';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
|
||||
const FieldEditModal = dynamic(() => import('../../FieldEditModal'));
|
||||
|
||||
const AddInputParam = (props: RenderInputProps) => {
|
||||
const { item, inputs, nodeId } = props;
|
||||
const { t } = useTranslation();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const mode = useContextSelector(WorkflowContext, (ctx) => ctx.mode);
|
||||
|
||||
const inputValue = useMemo(() => (item.value || []) as FlowNodeInputItemType[], [item.value]);
|
||||
|
||||
const [editField, setEditField] = useState<EditNodeFieldType>();
|
||||
const inputIndex = useMemo(
|
||||
() => inputs?.findIndex((input) => input.key === item.key),
|
||||
[inputs, item.key]
|
||||
);
|
||||
|
||||
const onAddField = useCallback(
|
||||
({ data }: { data: EditNodeFieldType }) => {
|
||||
if (!data.key) return;
|
||||
|
||||
const newInput: FlowNodeInputItemType = {
|
||||
key: data.key,
|
||||
valueType: data.valueType,
|
||||
label: data.label || '',
|
||||
renderTypeList: [FlowNodeInputTypeEnum.reference],
|
||||
required: data.required,
|
||||
description: data.description,
|
||||
canEdit: true,
|
||||
editField: item.editField
|
||||
};
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'addInput',
|
||||
index: inputIndex ? inputIndex + 1 : 1,
|
||||
value: newInput
|
||||
});
|
||||
setEditField(undefined);
|
||||
},
|
||||
[inputIndex, item, nodeId, onChangeNode]
|
||||
);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
position={'relative'}
|
||||
fontWeight={'medium'}
|
||||
color={'myGray.600'}
|
||||
>
|
||||
{t('core.workflow.Custom variable')}
|
||||
{item.description && <QuestionTip ml={1} label={t(item.description)} />}
|
||||
</Flex>
|
||||
<Box flex={'1 0 0'} />
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
iconSpacing={1}
|
||||
size={'sm'}
|
||||
onClick={() => setEditField(item.dynamicParamDefaultValue ?? {})}
|
||||
>
|
||||
{t('common.Add New')}
|
||||
</Button>
|
||||
</Flex>
|
||||
{mode === 'plugin' && (
|
||||
<Box mt={1}>
|
||||
<Reference {...props} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!!editField && (
|
||||
<FieldEditModal
|
||||
editField={item.editField}
|
||||
defaultField={editField}
|
||||
keys={inputValue.map((input) => input.key)}
|
||||
onClose={() => setEditField(undefined)}
|
||||
onSubmit={onAddField}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
editField,
|
||||
inputValue,
|
||||
item.description,
|
||||
item.dynamicParamDefaultValue,
|
||||
item.editField,
|
||||
mode,
|
||||
onAddField,
|
||||
props,
|
||||
t
|
||||
]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(AddInputParam);
|
||||
@@ -1,79 +0,0 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import JSONEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
|
||||
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
|
||||
import { useCreation } from 'ahooks';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
|
||||
const JsonEditor = ({ inputs = [], item, nodeId }: RenderInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
// get variable
|
||||
const variables = useCreation(() => {
|
||||
const globalVariables = getWorkflowGlobalVariables({
|
||||
nodes: nodeList,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
t
|
||||
});
|
||||
|
||||
const moduleVariables = formatEditorVariablePickerIcon(
|
||||
inputs
|
||||
.filter((input) => input.canEdit)
|
||||
.map((item) => ({
|
||||
key: item.key,
|
||||
label: item.label
|
||||
}))
|
||||
);
|
||||
|
||||
return [...globalVariables, ...moduleVariables];
|
||||
}, [inputs, nodeList]);
|
||||
|
||||
const update = useCallback(
|
||||
(value: string) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value
|
||||
}
|
||||
});
|
||||
},
|
||||
[item, nodeId, onChangeNode]
|
||||
);
|
||||
|
||||
const value = useMemo(() => {
|
||||
if (typeof item.value === 'string') {
|
||||
return item.value;
|
||||
}
|
||||
return JSON.stringify(item.value, null, 2);
|
||||
}, [item.value]);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<JSONEditor
|
||||
bg={'white'}
|
||||
borderRadius={'sm'}
|
||||
placeholder={item.placeholder}
|
||||
resize
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
update(e);
|
||||
}}
|
||||
variables={variables}
|
||||
/>
|
||||
);
|
||||
}, [item.placeholder, update, value, variables]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(JsonEditor);
|
||||
@@ -1,46 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import {
|
||||
NumberDecrementStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper
|
||||
} from '@chakra-ui/react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
|
||||
const NumberInputRender = ({ item, nodeId }: RenderInputProps) => {
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<NumberInput
|
||||
defaultValue={item.value}
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: Number(e)
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<NumberInputField bg={'white'} px={3} borderRadius={'sm'} />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
);
|
||||
}, [item, nodeId, onChangeNode]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(NumberInputRender);
|
||||
@@ -1,206 +0,0 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import { Flex, Box, ButtonProps } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { computedNodeInputReference } from '@/web/core/workflow/utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import {
|
||||
NodeOutputKeyEnum,
|
||||
VARIABLE_NODE_ID,
|
||||
WorkflowIOValueTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/constants';
|
||||
import type { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
|
||||
const MultipleRowSelect = dynamic(
|
||||
() => import('@fastgpt/web/components/common/MySelect/MultipleRowSelect')
|
||||
);
|
||||
const Avatar = dynamic(() => import('@/components/Avatar'));
|
||||
|
||||
type SelectProps = {
|
||||
value?: ReferenceValueProps;
|
||||
placeholder?: string;
|
||||
list: {
|
||||
label: string | React.ReactNode;
|
||||
value: string;
|
||||
children: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
}[];
|
||||
onSelect: (val: ReferenceValueProps) => void;
|
||||
styles?: ButtonProps;
|
||||
};
|
||||
|
||||
const Reference = ({ item, nodeId }: RenderInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
|
||||
const onSelect = useCallback(
|
||||
(e: ReferenceValueProps) => {
|
||||
const workflowStartNode = nodeList.find(
|
||||
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
|
||||
);
|
||||
if (e[0] === workflowStartNode?.id && e[1] !== NodeOutputKeyEnum.userChatInput) {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: [VARIABLE_NODE_ID, e[1]]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
[item, nodeId, nodeList, onChangeNode]
|
||||
);
|
||||
|
||||
const { referenceList, formatValue } = useReference({
|
||||
nodeId,
|
||||
valueType: item.valueType,
|
||||
value: item.value
|
||||
});
|
||||
|
||||
return (
|
||||
<ReferSelector
|
||||
placeholder={t(item.referencePlaceholder || '选择引用变量')}
|
||||
list={referenceList}
|
||||
value={formatValue}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Reference);
|
||||
|
||||
export const useReference = ({
|
||||
nodeId,
|
||||
valueType = WorkflowIOValueTypeEnum.any,
|
||||
value
|
||||
}: {
|
||||
nodeId: string;
|
||||
valueType?: WorkflowIOValueTypeEnum;
|
||||
value?: any;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
||||
|
||||
const referenceList = useMemo(() => {
|
||||
const sourceNodes = computedNodeInputReference({
|
||||
nodeId,
|
||||
nodes: nodeList,
|
||||
edges: edges,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
t
|
||||
});
|
||||
|
||||
if (!sourceNodes) return [];
|
||||
|
||||
// 转换为 select 的数据结构
|
||||
const list: SelectProps['list'] = sourceNodes
|
||||
.map((node) => {
|
||||
return {
|
||||
label: (
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar mr={1} src={node.avatar} w={'14px'} borderRadius={'ms'} />
|
||||
<Box>{t(node.name)}</Box>
|
||||
</Flex>
|
||||
),
|
||||
value: node.nodeId,
|
||||
children: node.outputs
|
||||
.filter(
|
||||
(output) =>
|
||||
valueType === WorkflowIOValueTypeEnum.any ||
|
||||
output.valueType === WorkflowIOValueTypeEnum.any ||
|
||||
output.valueType === valueType
|
||||
)
|
||||
.filter((output) => output.id !== NodeOutputKeyEnum.addOutputParam)
|
||||
.map((output) => {
|
||||
return {
|
||||
label: t(output.label || ''),
|
||||
value: output.id
|
||||
};
|
||||
})
|
||||
};
|
||||
})
|
||||
.filter((item) => item.children.length > 0);
|
||||
|
||||
return list;
|
||||
}, [appDetail.chatConfig, edges, nodeId, nodeList, t, valueType]);
|
||||
|
||||
const formatValue = useMemo(() => {
|
||||
if (
|
||||
Array.isArray(value) &&
|
||||
value.length === 2 &&
|
||||
typeof value[0] === 'string' &&
|
||||
typeof value[1] === 'string'
|
||||
) {
|
||||
return value as ReferenceValueProps;
|
||||
}
|
||||
return undefined;
|
||||
}, [value]);
|
||||
|
||||
return {
|
||||
referenceList,
|
||||
formatValue
|
||||
};
|
||||
};
|
||||
export const ReferSelector = ({ placeholder, value, list = [], onSelect }: SelectProps) => {
|
||||
const selectItemLabel = useMemo(() => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const firstColumn = list.find((item) => item.value === value[0]);
|
||||
if (!firstColumn) {
|
||||
return;
|
||||
}
|
||||
const secondColumn = firstColumn.children.find((item) => item.value === value[1]);
|
||||
if (!secondColumn) {
|
||||
return;
|
||||
}
|
||||
return [firstColumn, secondColumn];
|
||||
}, [list, value]);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<MultipleRowSelect
|
||||
label={
|
||||
selectItemLabel ? (
|
||||
<Flex alignItems={'center'}>
|
||||
{selectItemLabel[0].label}
|
||||
<MyIcon name={'common/rightArrowLight'} mx={1} w={'14px'}></MyIcon>
|
||||
{selectItemLabel[1].label}
|
||||
</Flex>
|
||||
) : (
|
||||
<Box>{placeholder}</Box>
|
||||
)
|
||||
}
|
||||
value={value as any[]}
|
||||
list={list}
|
||||
onSelect={(e) => {
|
||||
onSelect(e as ReferenceValueProps);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}, [list, onSelect, placeholder, selectItemLabel, value]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
|
||||
const SelectRender = ({ item, nodeId }: RenderInputProps) => {
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<MySelect
|
||||
width={'100%'}
|
||||
value={item.value}
|
||||
list={item.list || []}
|
||||
onchange={(e) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}, [item, nodeId, onChangeNode]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(SelectRender);
|
||||
@@ -1,107 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import { Box, Button, useDisclosure } from '@chakra-ui/react';
|
||||
import { SelectAppItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import SelectAppModal from '../../../../SelectAppModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getAppDetailById } from '@/web/core/app/api';
|
||||
|
||||
const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
const filterAppIds = useContextSelector(WorkflowContext, (ctx) => ctx.filterAppIds);
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const {
|
||||
isOpen: isOpenSelectApp,
|
||||
onOpen: onOpenSelectApp,
|
||||
onClose: onCloseSelectApp
|
||||
} = useDisclosure();
|
||||
|
||||
const value = item.value as SelectAppItemType | undefined;
|
||||
const { data: appDetail, loading } = useRequest2(
|
||||
() => {
|
||||
if (value?.id) return getAppDetailById(value.id);
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [value?.id],
|
||||
errorToast: 'Error',
|
||||
onError() {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: 'app',
|
||||
value: {
|
||||
...item,
|
||||
value: undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<Box onClick={onOpenSelectApp}>
|
||||
{!value ? (
|
||||
<Button variant={'whiteFlow'} w={'100%'}>
|
||||
{t('core.module.Select app')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
isLoading={loading}
|
||||
w={'100%'}
|
||||
justifyContent={loading ? 'center' : 'flex-start'}
|
||||
variant={'whiteFlow'}
|
||||
leftIcon={<Avatar src={appDetail?.avatar} w={6} />}
|
||||
>
|
||||
{appDetail?.name}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{isOpenSelectApp && (
|
||||
<SelectAppModal
|
||||
value={item.value}
|
||||
filterAppIds={filterAppIds}
|
||||
onClose={onCloseSelectApp}
|
||||
onSuccess={(e) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: 'app',
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
appDetail?.avatar,
|
||||
appDetail?.name,
|
||||
filterAppIds,
|
||||
isOpenSelectApp,
|
||||
item,
|
||||
loading,
|
||||
nodeId,
|
||||
onChangeNode,
|
||||
onCloseSelectApp,
|
||||
onOpenSelectApp,
|
||||
t,
|
||||
value
|
||||
]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(SelectAppRender);
|
||||
@@ -1,130 +0,0 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import { Box, Button, Flex, Grid, useDisclosure, useTheme } from '@chakra-ui/react';
|
||||
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
|
||||
import { SelectedDatasetType } from '@fastgpt/global/core/workflow/api';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
|
||||
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
|
||||
|
||||
const SelectDatasetRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const [data, setData] = useState({
|
||||
searchMode: DatasetSearchModeEnum.embedding,
|
||||
limit: 5,
|
||||
similarity: 0.5,
|
||||
usingReRank: false
|
||||
});
|
||||
|
||||
const { allDatasets, loadAllDatasets } = useDatasetStore();
|
||||
const {
|
||||
isOpen: isOpenDatasetSelect,
|
||||
onOpen: onOpenDatasetSelect,
|
||||
onClose: onCloseDatasetSelect
|
||||
} = useDisclosure();
|
||||
|
||||
const selectedDatasets = useMemo(() => {
|
||||
const value = item.value as SelectedDatasetType;
|
||||
return allDatasets.filter((dataset) => value?.find((item) => item.datasetId === dataset._id));
|
||||
}, [allDatasets, item.value]);
|
||||
|
||||
useQuery(['loadAllDatasets'], loadAllDatasets);
|
||||
|
||||
useEffect(() => {
|
||||
inputs.forEach((input) => {
|
||||
// @ts-ignore
|
||||
if (data[input.key] !== undefined) {
|
||||
setData((state) => ({
|
||||
...state,
|
||||
[input.key]: input.value
|
||||
}));
|
||||
}
|
||||
});
|
||||
}, [inputs]);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
gridTemplateColumns={'repeat(2, minmax(0, 1fr))'}
|
||||
gridGap={4}
|
||||
minW={'350px'}
|
||||
w={'100%'}
|
||||
>
|
||||
<Button
|
||||
h={'36px'}
|
||||
leftIcon={<MyIcon name={'common/selectLight'} w={'14px'} />}
|
||||
onClick={onOpenDatasetSelect}
|
||||
>
|
||||
{t('common.Choose')}
|
||||
</Button>
|
||||
{selectedDatasets.map((item) => (
|
||||
<Flex
|
||||
key={item._id}
|
||||
alignItems={'center'}
|
||||
h={'36px'}
|
||||
border={theme.borders.base}
|
||||
px={2}
|
||||
borderRadius={'md'}
|
||||
>
|
||||
<Avatar src={item.avatar} w={'24px'}></Avatar>
|
||||
<Box
|
||||
ml={3}
|
||||
flex={'1 0 0'}
|
||||
w={0}
|
||||
className="textEllipsis"
|
||||
fontWeight={'bold'}
|
||||
fontSize={['md', 'lg']}
|
||||
>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Grid>
|
||||
{isOpenDatasetSelect && (
|
||||
<DatasetSelectModal
|
||||
isOpen={isOpenDatasetSelect}
|
||||
defaultSelectedDatasets={item.value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
key: item.key,
|
||||
type: 'updateInput',
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
onClose={onCloseDatasetSelect}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
isOpenDatasetSelect,
|
||||
item,
|
||||
nodeId,
|
||||
onChangeNode,
|
||||
onCloseDatasetSelect,
|
||||
onOpenDatasetSelect,
|
||||
selectedDatasets,
|
||||
t,
|
||||
theme.borders.base
|
||||
]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(SelectDatasetRender);
|
||||
@@ -1,122 +0,0 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import DatasetParamsModal, { DatasetParamsProps } from '@/components/core/app/DatasetParamsModal';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
|
||||
const SelectDatasetParam = ({ inputs = [], nodeId }: RenderInputProps) => {
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { llmModelList } = useSystemStore();
|
||||
|
||||
const [data, setData] = useState<DatasetParamsProps>({
|
||||
searchMode: DatasetSearchModeEnum.embedding,
|
||||
limit: 5,
|
||||
similarity: 0.5,
|
||||
usingReRank: false,
|
||||
datasetSearchUsingExtensionQuery: true,
|
||||
datasetSearchExtensionModel: llmModelList[0]?.model,
|
||||
datasetSearchExtensionBg: ''
|
||||
});
|
||||
|
||||
const tokenLimit = useMemo(() => {
|
||||
let maxTokens = 3000;
|
||||
|
||||
nodeList.forEach((item) => {
|
||||
if (item.flowNodeType === FlowNodeTypeEnum.chatNode) {
|
||||
const model =
|
||||
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
|
||||
const quoteMaxToken =
|
||||
llmModelList.find((item) => item.model === model)?.quoteMaxToken || 3000;
|
||||
|
||||
maxTokens = Math.max(maxTokens, quoteMaxToken);
|
||||
}
|
||||
});
|
||||
|
||||
return maxTokens;
|
||||
}, [llmModelList, nodeList]);
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
useEffect(() => {
|
||||
inputs.forEach((input) => {
|
||||
// @ts-ignore
|
||||
if (data[input.key] !== undefined) {
|
||||
setData((state) => ({
|
||||
...state,
|
||||
[input.key]: input.value
|
||||
}));
|
||||
}
|
||||
});
|
||||
}, [inputs]);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
{/* label */}
|
||||
<Flex alignItems={'center'} mb={3} fontWeight={'medium'} color={'myGray.600'}>
|
||||
{t('core.dataset.search.Params Setting')}
|
||||
<MyIcon
|
||||
name={'common/settingLight'}
|
||||
ml={2}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}}
|
||||
onClick={onOpen}
|
||||
/>
|
||||
</Flex>
|
||||
<SearchParamsTip
|
||||
searchMode={data.searchMode}
|
||||
similarity={data.similarity}
|
||||
limit={data.limit}
|
||||
usingReRank={data.usingReRank}
|
||||
queryExtensionModel={data.datasetSearchExtensionModel}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}, [data, onOpen, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Render}
|
||||
{isOpen && (
|
||||
<DatasetParamsModal
|
||||
{...data}
|
||||
maxTokens={tokenLimit}
|
||||
onClose={onClose}
|
||||
onSuccess={(e) => {
|
||||
setData(e);
|
||||
for (let key in e) {
|
||||
const item = inputs.find((input) => input.key === key);
|
||||
if (!item) continue;
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key,
|
||||
value: {
|
||||
...item,
|
||||
//@ts-ignore
|
||||
value: e[key]
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SelectDatasetParam);
|
||||
@@ -1,64 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { llmModelTypeFilterMap } from '@fastgpt/global/core/ai/constants';
|
||||
import AIModelSelector from '@/components/Select/AIModelSelector';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
|
||||
const SelectAiModelRender = ({ item, nodeId }: RenderInputProps) => {
|
||||
const { llmModelList } = useSystemStore();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const modelList = useMemo(
|
||||
() =>
|
||||
llmModelList.filter((model) => {
|
||||
if (!item.llmModelType) return true;
|
||||
const filterField = llmModelTypeFilterMap[item.llmModelType];
|
||||
if (!filterField) return true;
|
||||
//@ts-ignore
|
||||
return !!model[filterField];
|
||||
}),
|
||||
[llmModelList, item.llmModelType]
|
||||
);
|
||||
|
||||
const onChangeModel = useCallback(
|
||||
(e: string) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
},
|
||||
[item, nodeId, onChangeNode]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!item.value && modelList.length > 0) {
|
||||
onChangeModel(modelList[0].model);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<AIModelSelector
|
||||
minW={'350px'}
|
||||
width={'100%'}
|
||||
value={item.value}
|
||||
list={modelList.map((item) => ({
|
||||
value: item.model,
|
||||
label: item.name
|
||||
}))}
|
||||
onchange={onChangeModel}
|
||||
/>
|
||||
);
|
||||
}, [item.value, modelList, onChangeModel]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(SelectAiModelRender);
|
||||
@@ -1,59 +0,0 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import type { SettingAIDataType } from '@fastgpt/global/core/app/type.d';
|
||||
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
|
||||
const SelectAiModelRender = ({ item, inputs = [], nodeId }: RenderInputProps) => {
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const onChangeModel = useCallback(
|
||||
(e: SettingAIDataType) => {
|
||||
for (const key in e) {
|
||||
const input = inputs.find((input) => input.key === key);
|
||||
input &&
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key,
|
||||
value: {
|
||||
...input,
|
||||
// @ts-ignore
|
||||
value: e[key]
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
[inputs, nodeId, onChangeNode]
|
||||
);
|
||||
|
||||
const llmModelData: SettingAIDataType = useMemo(
|
||||
() => ({
|
||||
model: inputs.find((input) => input.key === NodeInputKeyEnum.aiModel)?.value ?? '',
|
||||
maxToken:
|
||||
inputs.find((input) => input.key === NodeInputKeyEnum.aiChatMaxToken)?.value ?? 2048,
|
||||
temperature:
|
||||
inputs.find((input) => input.key === NodeInputKeyEnum.aiChatTemperature)?.value ?? 1,
|
||||
isResponseAnswerText: inputs.find(
|
||||
(input) => input.key === NodeInputKeyEnum.aiChatIsResponseText
|
||||
)?.value
|
||||
}),
|
||||
[inputs]
|
||||
);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<SettingLLMModel
|
||||
llmModelType={item.llmModelType}
|
||||
defaultData={llmModelData}
|
||||
onChange={onChangeModel}
|
||||
/>
|
||||
);
|
||||
}, [item.llmModelType, llmModelData, onChangeModel]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(SelectAiModelRender);
|
||||
@@ -1,284 +0,0 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import { Box, BoxProps, Button, Flex, ModalFooter, useDisclosure } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { PromptTemplateItem } from '@fastgpt/global/core/ai/type';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ModalBody } from '@chakra-ui/react';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import {
|
||||
Prompt_QuotePromptList,
|
||||
Prompt_QuoteTemplateList
|
||||
} from '@fastgpt/global/core/ai/prompt/AIChat';
|
||||
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
|
||||
import PromptTemplate from '@/components/PromptTemplate';
|
||||
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Reference from './Reference';
|
||||
import ValueTypeLabel from '../../ValueTypeLabel';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
|
||||
import { useCreation } from 'ahooks';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
|
||||
const LabelStyles: BoxProps = {
|
||||
fontSize: ['sm', 'md']
|
||||
};
|
||||
const selectTemplateBtn: BoxProps = {
|
||||
color: 'primary.500',
|
||||
cursor: 'pointer'
|
||||
};
|
||||
|
||||
const SettingQuotePrompt = (props: RenderInputProps) => {
|
||||
const { inputs = [], nodeId } = props;
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
|
||||
const { watch, setValue, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
quoteTemplate: inputs.find((input) => input.key === 'quoteTemplate')?.value || '',
|
||||
quotePrompt: inputs.find((input) => input.key === 'quotePrompt')?.value || ''
|
||||
}
|
||||
});
|
||||
const aiChatQuoteTemplate = watch('quoteTemplate');
|
||||
const aiChatQuotePrompt = watch('quotePrompt');
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const variables = useCreation(() => {
|
||||
const globalVariables = getWorkflowGlobalVariables({
|
||||
nodes: nodeList,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
t
|
||||
});
|
||||
|
||||
return globalVariables;
|
||||
}, [nodeList, t]);
|
||||
|
||||
const [selectTemplateData, setSelectTemplateData] = useState<{
|
||||
title: string;
|
||||
templates: PromptTemplateItem[];
|
||||
}>();
|
||||
const quoteTemplateVariables = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'q',
|
||||
label: 'q',
|
||||
icon: 'core/app/simpleMode/variable'
|
||||
},
|
||||
{
|
||||
key: 'a',
|
||||
label: 'a',
|
||||
icon: 'core/app/simpleMode/variable'
|
||||
},
|
||||
{
|
||||
key: 'source',
|
||||
label: t('core.dataset.search.Source name'),
|
||||
icon: 'core/app/simpleMode/variable'
|
||||
},
|
||||
{
|
||||
key: 'sourceId',
|
||||
label: t('core.dataset.search.Source id'),
|
||||
icon: 'core/app/simpleMode/variable'
|
||||
},
|
||||
{
|
||||
key: 'index',
|
||||
label: t('core.dataset.search.Quote index'),
|
||||
icon: 'core/app/simpleMode/variable'
|
||||
},
|
||||
...variables
|
||||
],
|
||||
[t, variables]
|
||||
);
|
||||
const quotePromptVariables = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'quote',
|
||||
label: t('core.app.Quote templates'),
|
||||
icon: 'core/app/simpleMode/variable'
|
||||
},
|
||||
{
|
||||
key: 'question',
|
||||
label: t('core.module.input.label.user question'),
|
||||
icon: 'core/app/simpleMode/variable'
|
||||
},
|
||||
...variables
|
||||
],
|
||||
[t, variables]
|
||||
);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(data: { quoteTemplate: string; quotePrompt: string }) => {
|
||||
const quoteTemplateInput = inputs.find(
|
||||
(input) => input.key === NodeInputKeyEnum.aiChatQuoteTemplate
|
||||
);
|
||||
const quotePromptInput = inputs.find(
|
||||
(input) => input.key === NodeInputKeyEnum.aiChatQuotePrompt
|
||||
);
|
||||
if (quoteTemplateInput) {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: quoteTemplateInput.key,
|
||||
value: {
|
||||
...quoteTemplateInput,
|
||||
value: data.quoteTemplate
|
||||
}
|
||||
});
|
||||
}
|
||||
if (quotePromptInput) {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: quotePromptInput.key,
|
||||
value: {
|
||||
...quotePromptInput,
|
||||
value: data.quotePrompt
|
||||
}
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
},
|
||||
[inputs, nodeId, onChangeNode, onClose]
|
||||
);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
|
||||
<Box position={'relative'} color={'myGray.600'} fontWeight={'medium'}>
|
||||
{t('core.module.Dataset quote.label')}
|
||||
</Box>
|
||||
<ValueTypeLabel valueType={WorkflowIOValueTypeEnum.datasetQuote} />
|
||||
|
||||
<MyTooltip label={t('core.module.Setting quote prompt')}>
|
||||
<MyIcon
|
||||
ml={1}
|
||||
name={'common/settingLight'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
onClick={onOpen}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box mt={1}>
|
||||
<Reference {...props} />
|
||||
</Box>
|
||||
|
||||
<MyModal
|
||||
isOpen={isOpen}
|
||||
iconSrc={'modal/edit'}
|
||||
title={t('core.module.Quote prompt setting')}
|
||||
w={'600px'}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box>
|
||||
<Flex {...LabelStyles} mb={1}>
|
||||
<FormLabel>{t('core.app.Quote templates')}</FormLabel>
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('template.Quote Content Tip', {
|
||||
default: Prompt_QuoteTemplateList[0].value
|
||||
})}
|
||||
></QuestionTip>
|
||||
<Box flex={1} />
|
||||
<Box
|
||||
{...selectTemplateBtn}
|
||||
fontSize={'sm'}
|
||||
onClick={() =>
|
||||
setSelectTemplateData({
|
||||
title: t('core.app.Select quote template'),
|
||||
templates: Prompt_QuoteTemplateList
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('common.Select template')}
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<PromptEditor
|
||||
variables={quoteTemplateVariables}
|
||||
h={160}
|
||||
title={t('core.app.Quote templates')}
|
||||
placeholder={t('template.Quote Content Tip', {
|
||||
default: Prompt_QuoteTemplateList[0].value
|
||||
})}
|
||||
value={aiChatQuoteTemplate}
|
||||
onChange={(e) => {
|
||||
setValue('quoteTemplate', e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<Flex {...LabelStyles} mb={1}>
|
||||
<FormLabel>{t('core.app.Quote prompt')}</FormLabel>
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('template.Quote Prompt Tip', {
|
||||
default: Prompt_QuotePromptList[0].value
|
||||
})}
|
||||
></QuestionTip>
|
||||
</Flex>
|
||||
<PromptEditor
|
||||
variables={quotePromptVariables}
|
||||
title={t('core.app.Quote prompt')}
|
||||
h={280}
|
||||
placeholder={t('template.Quote Prompt Tip', {
|
||||
default: Prompt_QuotePromptList[0].value
|
||||
})}
|
||||
value={aiChatQuotePrompt}
|
||||
onChange={(e) => {
|
||||
setValue('quotePrompt', e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} mr={2} onClick={onClose}>
|
||||
{t('common.Close')}
|
||||
</Button>
|
||||
<Button onClick={handleSubmit(onSubmit)}>{t('common.Confirm')}</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
{!!selectTemplateData && (
|
||||
<PromptTemplate
|
||||
title={selectTemplateData.title}
|
||||
templates={selectTemplateData.templates}
|
||||
onClose={() => setSelectTemplateData(undefined)}
|
||||
onSuccess={(e) => {
|
||||
const quoteVal = e.value;
|
||||
const promptVal = Prompt_QuotePromptList.find(
|
||||
(item) => item.title === e.title
|
||||
)?.value;
|
||||
setValue('quoteTemplate', quoteVal);
|
||||
setValue('quotePrompt', promptVal);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
aiChatQuotePrompt,
|
||||
aiChatQuoteTemplate,
|
||||
handleSubmit,
|
||||
isOpen,
|
||||
onClose,
|
||||
onOpen,
|
||||
onSubmit,
|
||||
props,
|
||||
quotePromptVariables,
|
||||
quoteTemplateVariables,
|
||||
selectTemplateData,
|
||||
setValue,
|
||||
t
|
||||
]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(SettingQuotePrompt);
|
||||
@@ -1,42 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import MySlider from '@/components/Slider';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
|
||||
const SliderRender = ({ item, nodeId }: RenderInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<Box px={2}>
|
||||
<MySlider
|
||||
markList={item.markList}
|
||||
width={'100%'}
|
||||
min={item.min || 0}
|
||||
max={item.max}
|
||||
step={item.step || 1}
|
||||
value={item.value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}, [item, nodeId, onChangeNode]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(SliderRender);
|
||||
@@ -1,33 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import { Switch } from '@chakra-ui/react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
|
||||
const SwitchRender = ({ item, nodeId }: RenderInputProps) => {
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<Switch
|
||||
size={'md'}
|
||||
isChecked={item.value}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e.target.checked
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}, [item, nodeId, onChangeNode]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(SwitchRender);
|
||||
@@ -1,36 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
|
||||
const TextInput = ({ item, nodeId }: RenderInputProps) => {
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<Input
|
||||
placeholder={item.placeholder}
|
||||
defaultValue={item.value}
|
||||
bg={'white'}
|
||||
px={3}
|
||||
borderRadius={'sm'}
|
||||
onBlur={(e) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e.target.value
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}, [item, nodeId, onChangeNode]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(TextInput);
|
||||
@@ -1,70 +0,0 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
|
||||
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
|
||||
import { useCreation } from 'ahooks';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
|
||||
const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
// get variable
|
||||
const variables = useCreation(() => {
|
||||
const globalVariables = getWorkflowGlobalVariables({
|
||||
nodes: nodeList,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
t
|
||||
});
|
||||
|
||||
const moduleVariables = formatEditorVariablePickerIcon(
|
||||
inputs
|
||||
.filter((input) => input.canEdit)
|
||||
.map((item) => ({
|
||||
key: item.key,
|
||||
label: item.label
|
||||
}))
|
||||
);
|
||||
|
||||
return [...globalVariables, ...moduleVariables];
|
||||
}, [nodeList, inputs, t]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: string) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: item.key,
|
||||
value: {
|
||||
...item,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
},
|
||||
[item, nodeId, onChangeNode]
|
||||
);
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<PromptEditor
|
||||
variables={variables}
|
||||
title={t(item.label)}
|
||||
maxLength={item.maxLength}
|
||||
h={150}
|
||||
placeholder={t(item.placeholder || '')}
|
||||
value={item.value}
|
||||
onChange={onChange}
|
||||
isFlow={true}
|
||||
/>
|
||||
);
|
||||
}, [item.label, item.maxLength, item.placeholder, item.value, onChange, t, variables]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(TextareaRender);
|
||||
@@ -1,7 +0,0 @@
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
|
||||
export type RenderInputProps = {
|
||||
inputs?: FlowNodeInputItemType[];
|
||||
item: FlowNodeInputItemType;
|
||||
nodeId: string;
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
import { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { SourceHandle } from '../Handle';
|
||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||
import { Position } from 'reactflow';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import ValueTypeLabel from '../ValueTypeLabel';
|
||||
|
||||
const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutputItemType }) => {
|
||||
const { t } = useTranslation();
|
||||
const { label = '', description, valueType } = output;
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<Box position={'relative'}>
|
||||
<Flex
|
||||
className="nodrag"
|
||||
cursor={'default'}
|
||||
alignItems={'center'}
|
||||
fontWeight={'medium'}
|
||||
color={'myGray.600'}
|
||||
{...(output.type === FlowNodeOutputTypeEnum.source
|
||||
? {
|
||||
flexDirection: 'row-reverse'
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<Box
|
||||
position={'relative'}
|
||||
mr={1}
|
||||
ml={output.type === FlowNodeOutputTypeEnum.source ? 1 : 0}
|
||||
>
|
||||
{t(label)}
|
||||
</Box>
|
||||
{description && <QuestionTip label={t(description)} />}
|
||||
<ValueTypeLabel valueType={valueType} />
|
||||
</Flex>
|
||||
{output.type === FlowNodeOutputTypeEnum.source && (
|
||||
<SourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={getHandleId(nodeId, 'source', output.key)}
|
||||
translate={[26, 0]}
|
||||
position={Position.Right}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}, [output.type, output.key, t, label, description, valueType, nodeId]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(OutputLabel);
|
||||
@@ -1,197 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import type { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import OutputLabel from './Label';
|
||||
import { RenderOutputProps } from './type';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import VariableTable from '../VariableTable';
|
||||
import { EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
|
||||
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
|
||||
const RenderList: {
|
||||
types: FlowNodeOutputTypeEnum[];
|
||||
Component: React.ComponentType<RenderOutputProps>;
|
||||
}[] = [];
|
||||
|
||||
const RenderOutput = ({
|
||||
nodeId,
|
||||
flowOutputList
|
||||
}: {
|
||||
nodeId: string;
|
||||
flowOutputList: FlowNodeOutputItemType[];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const outputString = useMemo(() => JSON.stringify(flowOutputList), [flowOutputList]);
|
||||
const copyOutputs = useMemo(() => {
|
||||
const parseOutputs = JSON.parse(outputString) as FlowNodeOutputItemType[];
|
||||
|
||||
return parseOutputs;
|
||||
}, [outputString]);
|
||||
|
||||
const [createField, setCreateField] = useState<EditNodeFieldType>();
|
||||
const [editField, setEditField] = useState<EditNodeFieldType>();
|
||||
|
||||
const RenderDynamicOutputs = useMemo(() => {
|
||||
const dynamicOutputs = copyOutputs.filter(
|
||||
(item) => item.type === FlowNodeOutputTypeEnum.dynamic
|
||||
);
|
||||
|
||||
const addOutput = dynamicOutputs.find((item) => item.key === NodeOutputKeyEnum.addOutputParam);
|
||||
const filterAddOutput = dynamicOutputs.filter(
|
||||
(item) => item.key !== NodeOutputKeyEnum.addOutputParam
|
||||
);
|
||||
|
||||
return dynamicOutputs.length === 0 || !addOutput ? null : (
|
||||
<Box mb={5}>
|
||||
<Flex
|
||||
mb={2}
|
||||
className="nodrag"
|
||||
cursor={'default'}
|
||||
alignItems={'center'}
|
||||
position={'relative'}
|
||||
>
|
||||
<Box position={'relative'} fontWeight={'medium'}>
|
||||
{t('core.workflow.Custom outputs')}
|
||||
</Box>
|
||||
<QuestionTip ml={1} label={addOutput.description} />
|
||||
<Box flex={'1 0 0'} />
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
iconSpacing={1}
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
setCreateField({});
|
||||
}}
|
||||
>
|
||||
{t('common.Add New')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<VariableTable
|
||||
fieldEditType={addOutput.editField}
|
||||
keys={copyOutputs.map((output) => output.key)}
|
||||
onCloseFieldEdit={() => {
|
||||
setCreateField(undefined);
|
||||
setEditField(undefined);
|
||||
}}
|
||||
variables={filterAddOutput.map((output) => ({
|
||||
label: output.label || '-',
|
||||
type: output.valueType ? t(FlowValueTypeMap[output.valueType]?.label) : '-',
|
||||
key: output.key
|
||||
}))}
|
||||
createField={createField}
|
||||
onCreate={({ data }) => {
|
||||
if (!data.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOutput: FlowNodeOutputItemType = {
|
||||
id: getNanoid(),
|
||||
type: FlowNodeOutputTypeEnum.dynamic,
|
||||
key: data.key,
|
||||
valueType: data.valueType,
|
||||
label: data.key
|
||||
};
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'addOutput',
|
||||
value: newOutput
|
||||
});
|
||||
setCreateField(undefined);
|
||||
}}
|
||||
editField={editField}
|
||||
onStartEdit={(e) => {
|
||||
const output = copyOutputs.find((output) => output.key === e);
|
||||
if (!output) return;
|
||||
setEditField({
|
||||
valueType: output.valueType,
|
||||
required: output.required,
|
||||
key: output.key,
|
||||
label: output.label,
|
||||
description: output.description
|
||||
});
|
||||
}}
|
||||
onEdit={({ data, changeKey }) => {
|
||||
if (!data.key || !editField?.key) return;
|
||||
|
||||
const output = copyOutputs.find((output) => output.key === editField.key);
|
||||
|
||||
const newOutput: FlowNodeOutputItemType = {
|
||||
...(output as FlowNodeOutputItemType),
|
||||
valueType: data.valueType,
|
||||
key: data.key,
|
||||
label: data.label,
|
||||
description: data.description
|
||||
};
|
||||
|
||||
if (changeKey) {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'replaceOutput',
|
||||
key: editField.key,
|
||||
value: newOutput
|
||||
});
|
||||
} else {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateOutput',
|
||||
key: newOutput.key,
|
||||
value: newOutput
|
||||
});
|
||||
}
|
||||
setEditField(undefined);
|
||||
}}
|
||||
onDelete={(key) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'delOutput',
|
||||
key
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}, [copyOutputs, createField, editField, nodeId, onChangeNode, t]);
|
||||
|
||||
const RenderCommonOutputs = useMemo(() => {
|
||||
const renderOutputs = copyOutputs.filter(
|
||||
(item) =>
|
||||
item.type !== FlowNodeOutputTypeEnum.dynamic && item.type !== FlowNodeOutputTypeEnum.hidden
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{renderOutputs.map((output) => {
|
||||
return output.label ? (
|
||||
<Box key={output.key} _notLast={{ mb: 5 }} position={'relative'}>
|
||||
{output.required && (
|
||||
<Box position={'absolute'} left={'-6px'} top={-1} color={'red.600'}>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
<OutputLabel nodeId={nodeId} output={output} />
|
||||
</Box>
|
||||
) : null;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}, [copyOutputs, nodeId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{RenderDynamicOutputs}
|
||||
{RenderCommonOutputs}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(RenderOutput);
|
||||
@@ -1,7 +0,0 @@
|
||||
import { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
|
||||
export type RenderOutputProps = {
|
||||
outputs?: FlowNodeOutputItemType[];
|
||||
item: FlowNodeOutputItemType;
|
||||
nodeId: string;
|
||||
};
|
||||
@@ -1,138 +0,0 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import type { EditFieldModalProps } from './type';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Input,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Switch,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { defaultEditFormData } from './constants';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
import { fnValueTypeSelect } from '@/web/core/workflow/constants/dataType';
|
||||
|
||||
const EditFieldModal = ({
|
||||
defaultValue = defaultEditFormData,
|
||||
nodeId,
|
||||
onClose
|
||||
}: EditFieldModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const { register, setValue, handleSubmit, watch } = useForm<FlowNodeInputItemType>({
|
||||
defaultValues: defaultValue
|
||||
});
|
||||
const valueType = watch('valueType');
|
||||
|
||||
const { mutate: onclickSubmit } = useRequest({
|
||||
mutationFn: async (e: FlowNodeInputItemType) => {
|
||||
const inputConfig: FlowNodeInputItemType = {
|
||||
...e,
|
||||
label: e.key
|
||||
};
|
||||
if (defaultValue.key) {
|
||||
// edit
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'replaceInput',
|
||||
key: defaultValue.key,
|
||||
value: inputConfig
|
||||
});
|
||||
} else {
|
||||
// create
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'addInput',
|
||||
index: 1,
|
||||
value: {
|
||||
...e,
|
||||
label: e.key
|
||||
}
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
const onclickSubmitError = useCallback(
|
||||
(e: Object) => {
|
||||
for (const item of Object.values(e)) {
|
||||
if (item.message) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: item.message
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal isOpen iconSrc="modal/edit" title={'工具字段参数配置'} onClose={onClose}>
|
||||
<ModalBody>
|
||||
<Flex alignItems={'center'} mb={5}>
|
||||
<Box flex={'0 0 80px'}>{t('common.Require Input')}</Box>
|
||||
<Switch {...register('required')} />
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mb={5}>
|
||||
<Box flex={'0 0 80px'}>{t('core.module.Data Type')}</Box>
|
||||
<Box flex={'1 0 0'}>
|
||||
<MySelect
|
||||
list={fnValueTypeSelect}
|
||||
value={valueType}
|
||||
onchange={(e: any) => {
|
||||
setValue('valueType', e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mb={5}>
|
||||
<Box flex={'0 0 80px'}>{t('core.module.Field Name')}</Box>
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
{...register('key', {
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z]+[0-9]*$/,
|
||||
message: '字段key必须是纯英文字母或数字,并且不能以数字开头。'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Box mb={5}>
|
||||
<Box flex={'0 0 80px'}>{t('core.module.Field Description')}</Box>
|
||||
<Textarea
|
||||
bg={'myGray.50'}
|
||||
rows={5}
|
||||
{...register('toolDescription', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} mr={2} onClick={onClose}>
|
||||
{t('common.Close')}
|
||||
</Button>
|
||||
<Button onClick={handleSubmit((data) => onclickSubmit(data), onclickSubmitError)}>
|
||||
{t('common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(EditFieldModal);
|
||||
@@ -1,17 +0,0 @@
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
|
||||
export const defaultEditFormData: FlowNodeInputItemType = {
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.reference],
|
||||
key: '',
|
||||
label: '',
|
||||
toolDescription: '',
|
||||
required: true,
|
||||
canEdit: true,
|
||||
editField: {
|
||||
key: true,
|
||||
description: true
|
||||
}
|
||||
};
|
||||
@@ -1,124 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import type {
|
||||
FlowNodeInputItemType,
|
||||
FlowNodeOutputItemType
|
||||
} from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Flex
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { defaultEditFormData } from './constants';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
const EditFieldModal = dynamic(() => import('./EditFieldModal'));
|
||||
|
||||
const RenderToolInput = ({
|
||||
nodeId,
|
||||
inputs,
|
||||
canEdit = false
|
||||
}: {
|
||||
nodeId: string;
|
||||
inputs: FlowNodeInputItemType[];
|
||||
canEdit?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const [editField, setEditField] = useState<FlowNodeInputItemType>();
|
||||
|
||||
return (
|
||||
<>
|
||||
{canEdit && (
|
||||
<Flex mb={2} alignItems={'center'}>
|
||||
<Box flex={'1 0 0'} fontWeight={'medium'} color={'myGray.600'}>
|
||||
{t('common.Field')}
|
||||
</Box>
|
||||
<Button
|
||||
variant={'unstyled'}
|
||||
leftIcon={<MyIcon name={'common/addLight'} w={'14px'} />}
|
||||
size={'sm'}
|
||||
px={3}
|
||||
_hover={{
|
||||
bg: 'myGray.150'
|
||||
}}
|
||||
onClick={() => setEditField(defaultEditFormData)}
|
||||
>
|
||||
{t('core.module.extract.Add field')}
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
<Box borderRadius={'md'} overflow={'hidden'} borderWidth={'1px'} borderBottom="none">
|
||||
<TableContainer>
|
||||
<Table bg={'white'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>字段名</Th>
|
||||
<Th>字段描述</Th>
|
||||
<Th>必须</Th>
|
||||
{canEdit && <Th></Th>}
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{inputs.map((item, index) => (
|
||||
<Tr
|
||||
key={index}
|
||||
position={'relative'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
wordBreak={'break-all'}
|
||||
>
|
||||
<Td>{item.key}</Td>
|
||||
<Td>{item.toolDescription}</Td>
|
||||
<Td>{item.required ? '✔' : ''}</Td>
|
||||
{canEdit && (
|
||||
<Td whiteSpace={'nowrap'}>
|
||||
<MyIcon
|
||||
mr={3}
|
||||
name={'common/settingLight'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => setEditField(item)}
|
||||
/>
|
||||
<MyIcon
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'delInput',
|
||||
key: item.key
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
)}
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
|
||||
{!!editField && (
|
||||
<EditFieldModal
|
||||
defaultValue={editField}
|
||||
nodeId={nodeId}
|
||||
onClose={() => setEditField(undefined)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(RenderToolInput);
|
||||
@@ -1,5 +0,0 @@
|
||||
export type EditFieldModalProps = {
|
||||
defaultValue?: EditFieldFormProps;
|
||||
nodeId: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import React from 'react';
|
||||
|
||||
const ValueTypeLabel = ({ valueType }: { valueType?: WorkflowIOValueTypeEnum }) => {
|
||||
const valueTypeData = valueType ? FlowValueTypeMap[valueType] : undefined;
|
||||
|
||||
const label = valueTypeData?.label || '';
|
||||
const description = valueTypeData?.description || '';
|
||||
|
||||
return !!label ? (
|
||||
<MyTooltip label={description}>
|
||||
<Box
|
||||
bg={'myGray.100'}
|
||||
color={'myGray.500'}
|
||||
border={'base'}
|
||||
borderRadius={'sm'}
|
||||
ml={2}
|
||||
px={1}
|
||||
h={6}
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
fontSize={'11px'}
|
||||
>
|
||||
{label}
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default React.memo(ValueTypeLabel);
|
||||
@@ -1,123 +0,0 @@
|
||||
import React from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { Box, Table, Thead, Tbody, Tr, Th, Td, TableContainer, Flex } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type {
|
||||
EditInputFieldMapType,
|
||||
EditNodeFieldType
|
||||
} from '@fastgpt/global/core/workflow/node/type';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const FieldEditModal = dynamic(() => import('./FieldEditModal'));
|
||||
|
||||
const VariableTable = ({
|
||||
fieldEditType,
|
||||
variables = [],
|
||||
keys,
|
||||
|
||||
createField,
|
||||
onCreate,
|
||||
|
||||
editField,
|
||||
onStartEdit,
|
||||
onEdit,
|
||||
|
||||
onCloseFieldEdit,
|
||||
onDelete
|
||||
}: {
|
||||
fieldEditType?: EditInputFieldMapType;
|
||||
variables: { icon?: string; label: string; type: string; key: string }[];
|
||||
keys: string[];
|
||||
|
||||
createField?: EditNodeFieldType;
|
||||
onCreate?: (e: { data: EditNodeFieldType }) => void;
|
||||
|
||||
editField?: EditNodeFieldType;
|
||||
onStartEdit: (key: string) => void;
|
||||
onEdit?: (e: { data: EditNodeFieldType; changeKey: boolean }) => void;
|
||||
|
||||
onCloseFieldEdit: () => void;
|
||||
onDelete?: (key: string) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const fileEditData = (createField || editField) as EditNodeFieldType | undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
bg={'white'}
|
||||
borderRadius={'md'}
|
||||
overflow={'hidden'}
|
||||
borderWidth={'1px'}
|
||||
borderBottom={'none'}
|
||||
>
|
||||
<TableContainer>
|
||||
<Table bg={'white'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th borderBottomLeftRadius={'none !important'}>
|
||||
{t('core.module.variable.variable name')}
|
||||
</Th>
|
||||
<Th>{t('core.workflow.Value type')}</Th>
|
||||
<Th borderBottomRightRadius={'none !important'}></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{variables.map((item) => (
|
||||
<Tr key={item.key}>
|
||||
<Td>
|
||||
<Flex alignItems={'center'}>
|
||||
{!!item.icon && <MyIcon name={item.icon as any} w={'14px'} mr={1} />}
|
||||
{item.label || item.key}
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td>{item.type}</Td>
|
||||
<Td>
|
||||
<MyIcon
|
||||
mr={3}
|
||||
name={'common/settingLight'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => onStartEdit(item.key)}
|
||||
/>
|
||||
<MyIcon
|
||||
className="delete"
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
color={'myGray.600'}
|
||||
cursor={'pointer'}
|
||||
ml={2}
|
||||
_hover={{ color: 'red.500' }}
|
||||
onClick={() => {
|
||||
onDelete?.(item.key);
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
{!!fileEditData && (
|
||||
<FieldEditModal
|
||||
editField={fieldEditType}
|
||||
defaultField={fileEditData}
|
||||
keys={keys}
|
||||
onClose={onCloseFieldEdit}
|
||||
onSubmit={(e) => {
|
||||
if (!!createField && onCreate) {
|
||||
onCreate(e);
|
||||
} else if (!!editField && onEdit) {
|
||||
onEdit(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(VariableTable);
|
||||
@@ -1,186 +0,0 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { getPublishList, postRevertVersion } from '@/web/core/app/versionApi';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../context';
|
||||
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
|
||||
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
|
||||
const PublishHistoriesSlider = () => {
|
||||
const { t } = useTranslation();
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
content: t('core.workflow.publish.OnRevert version confirm')
|
||||
});
|
||||
|
||||
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const appId = useContextSelector(WorkflowContext, (e) => e.appId);
|
||||
const setIsShowVersionHistories = useContextSelector(
|
||||
WorkflowContext,
|
||||
(e) => e.setIsShowVersionHistories
|
||||
);
|
||||
const initData = useContextSelector(WorkflowContext, (e) => e.initData);
|
||||
|
||||
const [selectedHistoryId, setSelectedHistoryId] = useState<string>();
|
||||
|
||||
const { list, ScrollList, isLoading } = useScrollPagination(getPublishList, {
|
||||
itemHeight: 49,
|
||||
overscan: 20,
|
||||
|
||||
pageSize: 30,
|
||||
defaultParams: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
const onClose = useMemoizedFn(() => {
|
||||
setIsShowVersionHistories(false);
|
||||
});
|
||||
|
||||
const onPreview = useCallback((data: AppVersionSchemaType) => {
|
||||
setSelectedHistoryId(data._id);
|
||||
|
||||
initData({
|
||||
nodes: data.nodes,
|
||||
edges: data.edges
|
||||
});
|
||||
}, []);
|
||||
const onCloseSlider = useCallback(
|
||||
(data: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => {
|
||||
setSelectedHistoryId(undefined);
|
||||
initData(data);
|
||||
onClose();
|
||||
},
|
||||
[appDetail]
|
||||
);
|
||||
|
||||
const { mutate: onRevert, isLoading: isReverting } = useRequest({
|
||||
mutationFn: async (data: AppVersionSchemaType) => {
|
||||
if (!appId) return;
|
||||
await postRevertVersion(appId, {
|
||||
versionId: data._id,
|
||||
editNodes: appDetail.modules, // old workflow
|
||||
editEdges: appDetail.edges
|
||||
});
|
||||
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
modules: data.nodes,
|
||||
edges: data.edges
|
||||
}));
|
||||
|
||||
onCloseSlider(data);
|
||||
}
|
||||
});
|
||||
|
||||
const showLoading = isLoading || isReverting;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomRightDrawer
|
||||
onClose={() =>
|
||||
onCloseSlider({
|
||||
nodes: appDetail.modules,
|
||||
edges: appDetail.edges
|
||||
})
|
||||
}
|
||||
iconSrc="core/workflow/versionHistories"
|
||||
title={t('core.workflow.publish.histories')}
|
||||
maxW={'300px'}
|
||||
px={0}
|
||||
showMask={false}
|
||||
mt={'60px'}
|
||||
overflow={'unset'}
|
||||
>
|
||||
<Button
|
||||
mx={'20px'}
|
||||
variant={'whitePrimary'}
|
||||
mb={2}
|
||||
isDisabled={!selectedHistoryId}
|
||||
onClick={() => {
|
||||
setSelectedHistoryId(undefined);
|
||||
initData({
|
||||
nodes: appDetail.modules,
|
||||
edges: appDetail.edges
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('core.workflow.Current workflow')}
|
||||
</Button>
|
||||
<ScrollList isLoading={showLoading} flex={'1 0 0'} px={5}>
|
||||
{list.map((data, index) => {
|
||||
const item = data.data;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
key={data.index}
|
||||
alignItems={'center'}
|
||||
py={3}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
fontWeight={500}
|
||||
_hover={{
|
||||
bg: 'primary.50'
|
||||
}}
|
||||
{...(selectedHistoryId === item._id && {
|
||||
color: 'primary.600'
|
||||
})}
|
||||
onClick={() => onPreview(item)}
|
||||
>
|
||||
<Box
|
||||
w={'12px'}
|
||||
h={'12px'}
|
||||
borderWidth={'2px'}
|
||||
borderColor={'primary.600'}
|
||||
borderRadius={'50%'}
|
||||
position={'relative'}
|
||||
{...(index !== list.length - 1 && {
|
||||
_after: {
|
||||
content: '""',
|
||||
height: '40px',
|
||||
width: '2px',
|
||||
bgColor: 'myGray.250',
|
||||
position: 'absolute',
|
||||
top: '10px',
|
||||
left: '3px'
|
||||
}
|
||||
})}
|
||||
></Box>
|
||||
<Box ml={3} flex={'1 0 0'}>
|
||||
{formatTime2YMDHM(item.time)}
|
||||
</Box>
|
||||
{item._id === selectedHistoryId && (
|
||||
<MyTooltip label={t('core.workflow.publish.OnRevert version')}>
|
||||
<MyIcon
|
||||
name={'core/workflow/revertVersion'}
|
||||
w={'20px'}
|
||||
color={'primary.600'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openConfirm(() => onRevert(item))();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</ScrollList>
|
||||
</CustomRightDrawer>
|
||||
<ConfirmModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(PublishHistoriesSlider);
|
||||
@@ -1,23 +0,0 @@
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
|
||||
import React from 'react';
|
||||
import { DefaultEdgeOptions } from 'reactflow';
|
||||
|
||||
export const connectionLineStyle: React.CSSProperties = {
|
||||
strokeWidth: 2,
|
||||
stroke: '#487FFF'
|
||||
};
|
||||
|
||||
export const defaultEdgeOptions: DefaultEdgeOptions = {
|
||||
zIndex: 0
|
||||
};
|
||||
|
||||
export const defaultRunningStatus: FlowNodeItemType['debugResult'] = {
|
||||
status: 'running',
|
||||
message: '',
|
||||
showResult: false
|
||||
};
|
||||
export const defaultSkippedStatus: FlowNodeItemType['debugResult'] = {
|
||||
status: 'skipped',
|
||||
message: '',
|
||||
showResult: false
|
||||
};
|
||||
@@ -1,722 +0,0 @@
|
||||
import { postWorkflowDebug } from '@/web/core/workflow/api';
|
||||
import { storeEdgesRenderEdge, storeNode2FlowNode } from '@/web/core/workflow/utils';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import {
|
||||
FlowNodeItemType,
|
||||
FlowNodeTemplateType,
|
||||
StoreNodeItemType
|
||||
} from '@fastgpt/global/core/workflow/type';
|
||||
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react';
|
||||
import {
|
||||
Edge,
|
||||
EdgeChange,
|
||||
Node,
|
||||
NodeChange,
|
||||
OnConnectStartParams,
|
||||
useEdgesState,
|
||||
useNodesState
|
||||
} from 'reactflow';
|
||||
import { createContext, useContextSelector } from 'use-context-selector';
|
||||
import { defaultRunningStatus } from './constants';
|
||||
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
|
||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
|
||||
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
|
||||
|
||||
type WorkflowContextType = {
|
||||
appId?: string;
|
||||
mode: 'app' | 'plugin';
|
||||
basicNodeTemplates: FlowNodeTemplateType[];
|
||||
filterAppIds?: string[];
|
||||
reactFlowWrapper: React.RefObject<HTMLDivElement> | null;
|
||||
|
||||
// nodes
|
||||
nodes: Node<FlowNodeItemType, string | undefined>[];
|
||||
nodeList: FlowNodeItemType[];
|
||||
setNodes: Dispatch<SetStateAction<Node<FlowNodeItemType, string | undefined>[]>>;
|
||||
onNodesChange: OnChange<NodeChange>;
|
||||
hasToolNode: boolean;
|
||||
hoverNodeId?: string;
|
||||
setHoverNodeId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
onUpdateNodeError: (node: string, isError: Boolean) => void;
|
||||
onResetNode: (e: { id: string; node: FlowNodeTemplateType }) => void;
|
||||
onChangeNode: (e: FlowNodeChangeProps) => void;
|
||||
|
||||
// edges
|
||||
edges: Edge<any>[];
|
||||
setEdges: Dispatch<SetStateAction<Edge<any>[]>>;
|
||||
onEdgesChange: OnChange<EdgeChange>;
|
||||
onDelEdge: (e: {
|
||||
nodeId: string;
|
||||
sourceHandle?: string | undefined;
|
||||
targetHandle?: string | undefined;
|
||||
}) => void;
|
||||
hoverEdgeId?: string;
|
||||
setHoverEdgeId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
|
||||
// connect
|
||||
connectingEdge?: OnConnectStartParams;
|
||||
setConnectingEdge: React.Dispatch<React.SetStateAction<OnConnectStartParams | undefined>>;
|
||||
|
||||
// common function
|
||||
onFixView: () => void;
|
||||
splitToolInputs: (
|
||||
inputs: FlowNodeInputItemType[],
|
||||
nodeId: string
|
||||
) => {
|
||||
isTool: boolean;
|
||||
toolInputs: FlowNodeInputItemType[];
|
||||
commonInputs: FlowNodeInputItemType[];
|
||||
};
|
||||
initData: (e: {
|
||||
nodes: StoreNodeItemType[];
|
||||
edges: StoreEdgeItemType[];
|
||||
chatConfig?: AppChatConfigType;
|
||||
}) => Promise<void>;
|
||||
|
||||
// debug
|
||||
workflowDebugData:
|
||||
| {
|
||||
runtimeNodes: RuntimeNodeItemType[];
|
||||
runtimeEdges: RuntimeEdgeItemType[];
|
||||
nextRunNodes: RuntimeNodeItemType[];
|
||||
}
|
||||
| undefined;
|
||||
onNextNodeDebug: () => Promise<void>;
|
||||
onStartNodeDebug: ({
|
||||
entryNodeId,
|
||||
runtimeNodes,
|
||||
runtimeEdges
|
||||
}: {
|
||||
entryNodeId: string;
|
||||
runtimeNodes: RuntimeNodeItemType[];
|
||||
runtimeEdges: RuntimeEdgeItemType[];
|
||||
}) => Promise<void>;
|
||||
onStopNodeDebug: () => void;
|
||||
|
||||
// version history
|
||||
isShowVersionHistories: boolean;
|
||||
setIsShowVersionHistories: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
type ContextValueProps = Pick<
|
||||
WorkflowContextType,
|
||||
'mode' | 'basicNodeTemplates' | 'filterAppIds'
|
||||
> & {
|
||||
appId?: string;
|
||||
pluginId?: string;
|
||||
};
|
||||
|
||||
type DebugDataType = {
|
||||
runtimeNodes: RuntimeNodeItemType[];
|
||||
runtimeEdges: RuntimeEdgeItemType[];
|
||||
nextRunNodes: RuntimeNodeItemType[];
|
||||
};
|
||||
|
||||
export const WorkflowContext = createContext<WorkflowContextType>({
|
||||
mode: 'app',
|
||||
setConnectingEdge: function (
|
||||
value: React.SetStateAction<OnConnectStartParams | undefined>
|
||||
): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
onFixView: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
basicNodeTemplates: [],
|
||||
reactFlowWrapper: null,
|
||||
nodes: [],
|
||||
nodeList: [],
|
||||
setNodes: function (
|
||||
value: React.SetStateAction<Node<FlowNodeItemType, string | undefined>[]>
|
||||
): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
onNodesChange: function (changes: NodeChange[]): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
hasToolNode: false,
|
||||
setHoverNodeId: function (value: React.SetStateAction<string | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
onUpdateNodeError: function (node: string, isError: Boolean): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
edges: [],
|
||||
setEdges: function (value: React.SetStateAction<Edge<any>[]>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
onEdgesChange: function (changes: EdgeChange[]): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
onResetNode: function (e: { id: string; node: FlowNodeTemplateType }): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
onDelEdge: function (e: {
|
||||
nodeId: string;
|
||||
sourceHandle?: string | undefined;
|
||||
targetHandle?: string | undefined;
|
||||
}): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
splitToolInputs: function (
|
||||
inputs: FlowNodeInputItemType[],
|
||||
nodeId: string
|
||||
): {
|
||||
isTool: boolean;
|
||||
toolInputs: FlowNodeInputItemType[];
|
||||
commonInputs: FlowNodeInputItemType[];
|
||||
} {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
initData: function (e: {
|
||||
nodes: StoreNodeItemType[];
|
||||
edges: StoreEdgeItemType[];
|
||||
}): Promise<void> {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
workflowDebugData: undefined,
|
||||
onNextNodeDebug: function (): Promise<void> {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
onStartNodeDebug: function ({
|
||||
entryNodeId,
|
||||
runtimeNodes,
|
||||
runtimeEdges
|
||||
}: {
|
||||
entryNodeId: string;
|
||||
runtimeNodes: RuntimeNodeItemType[];
|
||||
runtimeEdges: RuntimeEdgeItemType[];
|
||||
}): Promise<void> {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
onStopNodeDebug: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
onChangeNode: function (e: FlowNodeChangeProps): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
isShowVersionHistories: false,
|
||||
setIsShowVersionHistories: function (value: React.SetStateAction<boolean>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
setHoverEdgeId: function (value: React.SetStateAction<string | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
});
|
||||
|
||||
const WorkflowContextProvider = ({
|
||||
children,
|
||||
value
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
value: ContextValueProps;
|
||||
}) => {
|
||||
const { appId, pluginId } = value;
|
||||
const { toast } = useToast();
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
const setAppDetail = useContextSelector(AppContext, (v) => v.setAppDetail);
|
||||
|
||||
/* edge */
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const [hoverEdgeId, setHoverEdgeId] = useState<string>();
|
||||
const onDelEdge = useCallback(
|
||||
({
|
||||
nodeId,
|
||||
sourceHandle,
|
||||
targetHandle
|
||||
}: {
|
||||
nodeId: string;
|
||||
sourceHandle?: string | undefined;
|
||||
targetHandle?: string | undefined;
|
||||
}) => {
|
||||
if (!sourceHandle && !targetHandle) return;
|
||||
setEdges((state) =>
|
||||
state.filter((edge) => {
|
||||
if (edge.source === nodeId && edge.sourceHandle === sourceHandle) return false;
|
||||
if (edge.target === nodeId && edge.targetHandle === targetHandle) return false;
|
||||
|
||||
return true;
|
||||
})
|
||||
);
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
/* connect */
|
||||
const [connectingEdge, setConnectingEdge] = useState<OnConnectStartParams>();
|
||||
|
||||
/* node */
|
||||
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowNodeItemType>([]);
|
||||
const [hoverNodeId, setHoverNodeId] = useState<string>();
|
||||
|
||||
const nodeListString = JSON.stringify(nodes.map((node) => node.data));
|
||||
const nodeList = useMemo(
|
||||
() => JSON.parse(nodeListString) as FlowNodeItemType[],
|
||||
[nodeListString]
|
||||
);
|
||||
|
||||
const hasToolNode = useMemo(() => {
|
||||
return !!nodes.find((node) => node.data.flowNodeType === FlowNodeTypeEnum.tools);
|
||||
}, [nodes]);
|
||||
|
||||
const onUpdateNodeError = useMemoizedFn((nodeId: string, isError: Boolean) => {
|
||||
setNodes((nodes) => {
|
||||
return nodes.map((item) => {
|
||||
if (item.data?.nodeId === nodeId) {
|
||||
item.selected = true;
|
||||
//@ts-ignore
|
||||
item.data.isError = isError;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// reset a node data. delete edge and replace it
|
||||
const onResetNode = useMemoizedFn(({ id, node }: { id: string; node: FlowNodeTemplateType }) => {
|
||||
setNodes((state) =>
|
||||
state.map((item) => {
|
||||
if (item.id === id) {
|
||||
return {
|
||||
...item,
|
||||
data: {
|
||||
...item.data,
|
||||
...node,
|
||||
inputs: node.inputs.map((input) => {
|
||||
const value =
|
||||
item.data.inputs.find((i) => i.key === input.key)?.value ?? input.value;
|
||||
return {
|
||||
...input,
|
||||
value
|
||||
};
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const onChangeNode = useMemoizedFn((props: FlowNodeChangeProps) => {
|
||||
const { nodeId, type } = props;
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
if (node.id !== nodeId) return node;
|
||||
|
||||
const updateObj: Record<string, any> = {};
|
||||
|
||||
if (type === 'attr') {
|
||||
if (props.key) {
|
||||
updateObj[props.key] = props.value;
|
||||
}
|
||||
} else if (type === 'updateInput') {
|
||||
updateObj.inputs = node.data.inputs.map((item) =>
|
||||
item.key === props.key ? props.value : item
|
||||
);
|
||||
} else if (type === 'replaceInput') {
|
||||
const oldInputIndex = node.data.inputs.findIndex((item) => item.key === props.key);
|
||||
updateObj.inputs = node.data.inputs.filter((item) => item.key !== props.key);
|
||||
setTimeout(() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'addInput',
|
||||
index: oldInputIndex,
|
||||
value: props.value
|
||||
});
|
||||
});
|
||||
} else if (type === 'addInput') {
|
||||
const input = node.data.inputs.find((input) => input.key === props.value.key);
|
||||
if (input) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: 'key 重复'
|
||||
});
|
||||
updateObj.inputs = node.data.inputs;
|
||||
} else {
|
||||
if (props.index !== undefined) {
|
||||
const inputs = [...node.data.inputs];
|
||||
inputs.splice(props.index, 0, props.value);
|
||||
updateObj.inputs = inputs;
|
||||
} else {
|
||||
updateObj.inputs = node.data.inputs.concat(props.value);
|
||||
}
|
||||
}
|
||||
} else if (type === 'delInput') {
|
||||
updateObj.inputs = node.data.inputs.filter((item) => item.key !== props.key);
|
||||
} else if (type === 'updateOutput') {
|
||||
updateObj.outputs = node.data.outputs.map((item) =>
|
||||
item.key === props.key ? props.value : item
|
||||
);
|
||||
} else if (type === 'replaceOutput') {
|
||||
onDelEdge({ nodeId, sourceHandle: getHandleId(nodeId, 'source', props.key) });
|
||||
const oldOutputIndex = node.data.outputs.findIndex((item) => item.key === props.key);
|
||||
updateObj.outputs = node.data.outputs.filter((item) => item.key !== props.key);
|
||||
console.log(props.value);
|
||||
setTimeout(() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'addOutput',
|
||||
index: oldOutputIndex,
|
||||
value: props.value
|
||||
});
|
||||
});
|
||||
} else if (type === 'addOutput') {
|
||||
const output = node.data.outputs.find((output) => output.key === props.value.key);
|
||||
if (output) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: 'key 重复'
|
||||
});
|
||||
updateObj.outputs = node.data.outputs;
|
||||
} else {
|
||||
if (props.index !== undefined) {
|
||||
const outputs = [...node.data.outputs];
|
||||
outputs.splice(props.index, 0, props.value);
|
||||
updateObj.outputs = outputs;
|
||||
} else {
|
||||
updateObj.outputs = node.data.outputs.concat(props.value);
|
||||
}
|
||||
}
|
||||
} else if (type === 'delOutput') {
|
||||
onDelEdge({ nodeId, sourceHandle: getHandleId(nodeId, 'source', props.key) });
|
||||
updateObj.outputs = node.data.outputs.filter((item) => item.key !== props.key);
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
...updateObj
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/* function */
|
||||
const onFixView = useMemoizedFn(() => {
|
||||
const btn = document.querySelector('.custom-workflow-fix_view') as HTMLButtonElement;
|
||||
|
||||
setTimeout(() => {
|
||||
btn && btn.click();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
/* If the module is connected by a tool, the tool input and the normal input are separated */
|
||||
const splitToolInputs = (inputs: FlowNodeInputItemType[], nodeId: string) => {
|
||||
const isTool = !!edges.find(
|
||||
(edge) => edge.targetHandle === NodeOutputKeyEnum.selectedTools && edge.target === nodeId
|
||||
);
|
||||
|
||||
return {
|
||||
isTool,
|
||||
toolInputs: inputs.filter((item) => isTool && item.toolDescription),
|
||||
commonInputs: inputs.filter((item) => {
|
||||
if (!isTool) return true;
|
||||
return !item.toolDescription;
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
const initData = useMemoizedFn(async (e: Parameters<WorkflowContextType['initData']>[0]) => {
|
||||
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item })) || []);
|
||||
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []);
|
||||
|
||||
const chatConfig = e.chatConfig;
|
||||
if (chatConfig) {
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
chatConfig
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
/* debug */
|
||||
const [workflowDebugData, setWorkflowDebugData] = useState<DebugDataType>();
|
||||
const onNextNodeDebug = useCallback(
|
||||
async (debugData = workflowDebugData) => {
|
||||
if (!debugData) return;
|
||||
// 1. Cancel node selected status and debugResult.showStatus
|
||||
setNodes((state) =>
|
||||
state.map((node) => ({
|
||||
...node,
|
||||
selected: false,
|
||||
data: {
|
||||
...node.data,
|
||||
debugResult: node.data.debugResult
|
||||
? {
|
||||
...node.data.debugResult,
|
||||
showResult: false,
|
||||
isExpired: true
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
}))
|
||||
);
|
||||
|
||||
// 2. Set isEntry field and get entryNodes
|
||||
const runtimeNodes = debugData.runtimeNodes.map((item) => ({
|
||||
...item,
|
||||
isEntry: debugData.nextRunNodes.some((node) => node.nodeId === item.nodeId)
|
||||
}));
|
||||
const entryNodes = runtimeNodes.filter((item) => item.isEntry);
|
||||
|
||||
const runtimeNodeStatus: Record<string, string> = entryNodes
|
||||
.map((node) => {
|
||||
const status = checkNodeRunStatus({
|
||||
node,
|
||||
runtimeEdges: debugData?.runtimeEdges || []
|
||||
});
|
||||
|
||||
return {
|
||||
nodeId: node.nodeId,
|
||||
status
|
||||
};
|
||||
})
|
||||
.reduce(
|
||||
(acc, cur) => ({
|
||||
...acc,
|
||||
[cur.nodeId]: cur.status
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
// 3. Set entry node status to running
|
||||
entryNodes.forEach((node) => {
|
||||
if (runtimeNodeStatus[node.nodeId] !== 'wait') {
|
||||
onChangeNode({
|
||||
nodeId: node.nodeId,
|
||||
type: 'attr',
|
||||
key: 'debugResult',
|
||||
value: defaultRunningStatus
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// 4. Run one step
|
||||
const { finishedEdges, finishedNodes, nextStepRunNodes, flowResponses } =
|
||||
await postWorkflowDebug({
|
||||
nodes: runtimeNodes,
|
||||
edges: debugData.runtimeEdges,
|
||||
variables: {},
|
||||
appId,
|
||||
pluginId
|
||||
});
|
||||
// console.log({ finishedEdges, finishedNodes, nextStepRunNodes, flowResponses });
|
||||
// 5. Store debug result
|
||||
const newStoreDebugData = {
|
||||
runtimeNodes: finishedNodes,
|
||||
// edges need to save status
|
||||
runtimeEdges: finishedEdges.map((edge) => {
|
||||
const oldEdge = debugData.runtimeEdges.find(
|
||||
(item) => item.source === edge.source && item.target === edge.target
|
||||
);
|
||||
const status =
|
||||
oldEdge?.status && oldEdge.status !== RuntimeEdgeStatusEnum.waiting
|
||||
? oldEdge.status
|
||||
: edge.status;
|
||||
return {
|
||||
...edge,
|
||||
status
|
||||
};
|
||||
}),
|
||||
nextRunNodes: nextStepRunNodes
|
||||
};
|
||||
setWorkflowDebugData(newStoreDebugData);
|
||||
|
||||
// 6. selected entry node and Update entry node debug result
|
||||
setNodes((state) =>
|
||||
state.map((node) => {
|
||||
const isEntryNode = entryNodes.some((item) => item.nodeId === node.data.nodeId);
|
||||
|
||||
if (!isEntryNode || runtimeNodeStatus[node.data.nodeId] === 'wait') return node;
|
||||
|
||||
const result = flowResponses.find((item) => item.nodeId === node.data.nodeId);
|
||||
|
||||
if (runtimeNodeStatus[node.data.nodeId] === 'skip') {
|
||||
return {
|
||||
...node,
|
||||
selected: isEntryNode,
|
||||
data: {
|
||||
...node.data,
|
||||
debugResult: {
|
||||
status: 'skipped',
|
||||
showResult: true,
|
||||
isExpired: false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
selected: isEntryNode,
|
||||
data: {
|
||||
...node.data,
|
||||
debugResult: {
|
||||
status: 'success',
|
||||
response: result,
|
||||
showResult: true,
|
||||
isExpired: false
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Check for an empty response
|
||||
if (flowResponses.length === 0 && nextStepRunNodes.length > 0) {
|
||||
onNextNodeDebug(newStoreDebugData);
|
||||
}
|
||||
} catch (error) {
|
||||
entryNodes.forEach((node) => {
|
||||
onChangeNode({
|
||||
nodeId: node.nodeId,
|
||||
type: 'attr',
|
||||
key: 'debugResult',
|
||||
value: {
|
||||
status: 'failed',
|
||||
message: getErrText(error, 'Debug failed'),
|
||||
showResult: true
|
||||
}
|
||||
});
|
||||
});
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
[appId, onChangeNode, pluginId, setNodes, workflowDebugData]
|
||||
);
|
||||
const onStopNodeDebug = useCallback(() => {
|
||||
setWorkflowDebugData(undefined);
|
||||
setNodes((state) =>
|
||||
state.map((node) => ({
|
||||
...node,
|
||||
selected: false,
|
||||
data: {
|
||||
...node.data,
|
||||
debugResult: undefined
|
||||
}
|
||||
}))
|
||||
);
|
||||
}, [setNodes]);
|
||||
const onStartNodeDebug = useCallback(
|
||||
async ({
|
||||
entryNodeId,
|
||||
runtimeNodes,
|
||||
runtimeEdges
|
||||
}: {
|
||||
entryNodeId: string;
|
||||
runtimeNodes: RuntimeNodeItemType[];
|
||||
runtimeEdges: RuntimeEdgeItemType[];
|
||||
}) => {
|
||||
const data = {
|
||||
runtimeNodes,
|
||||
runtimeEdges,
|
||||
nextRunNodes: runtimeNodes.filter((node) => node.nodeId === entryNodeId)
|
||||
};
|
||||
onStopNodeDebug();
|
||||
setWorkflowDebugData(data);
|
||||
|
||||
onNextNodeDebug(data);
|
||||
},
|
||||
[onNextNodeDebug, onStopNodeDebug]
|
||||
);
|
||||
|
||||
/* Version histories */
|
||||
const [isShowVersionHistories, setIsShowVersionHistories] = useState(false);
|
||||
|
||||
/* event bus */
|
||||
useEffect(() => {
|
||||
eventBus.on(EventNameEnum.requestWorkflowStore, () => {
|
||||
eventBus.emit(EventNameEnum.receiveWorkflowStore, {
|
||||
nodes
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
eventBus.off(EventNameEnum.requestWorkflowStore);
|
||||
};
|
||||
}, [nodes]);
|
||||
|
||||
return (
|
||||
<WorkflowContext.Provider
|
||||
value={{
|
||||
appId,
|
||||
reactFlowWrapper,
|
||||
...value,
|
||||
// node
|
||||
nodes,
|
||||
setNodes,
|
||||
onNodesChange,
|
||||
nodeList,
|
||||
hasToolNode,
|
||||
hoverNodeId,
|
||||
setHoverNodeId,
|
||||
onUpdateNodeError,
|
||||
onResetNode,
|
||||
onChangeNode,
|
||||
|
||||
// edge
|
||||
edges,
|
||||
setEdges,
|
||||
hoverEdgeId,
|
||||
setHoverEdgeId,
|
||||
onEdgesChange,
|
||||
connectingEdge,
|
||||
setConnectingEdge,
|
||||
onDelEdge,
|
||||
|
||||
// function
|
||||
onFixView,
|
||||
splitToolInputs,
|
||||
initData,
|
||||
|
||||
// debug
|
||||
workflowDebugData,
|
||||
onNextNodeDebug,
|
||||
onStartNodeDebug,
|
||||
onStopNodeDebug,
|
||||
|
||||
// version history
|
||||
isShowVersionHistories,
|
||||
setIsShowVersionHistories
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</WorkflowContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowContextProvider;
|
||||
|
||||
type GetWorkflowStoreResponse = {
|
||||
nodes: Node<FlowNodeItemType>[];
|
||||
};
|
||||
export const getWorkflowStore = () =>
|
||||
new Promise<GetWorkflowStoreResponse>((resolve) => {
|
||||
eventBus.on(EventNameEnum.receiveWorkflowStore, (data: GetWorkflowStoreResponse) => {
|
||||
resolve(data);
|
||||
eventBus.off(EventNameEnum.receiveWorkflowStore);
|
||||
});
|
||||
eventBus.emit(EventNameEnum.requestWorkflowStore);
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import { FlowNodeItemType, StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { type Node, type Edge } from 'reactflow';
|
||||
|
||||
export const uiWorkflow2StoreWorkflow = ({
|
||||
nodes,
|
||||
edges
|
||||
}: {
|
||||
nodes: Node<FlowNodeItemType, string | undefined>[];
|
||||
edges: Edge<any>[];
|
||||
}) => {
|
||||
const formatNodes: StoreNodeItemType[] = nodes.map((item) => ({
|
||||
nodeId: item.data.nodeId,
|
||||
name: item.data.name,
|
||||
intro: item.data.intro,
|
||||
avatar: item.data.avatar,
|
||||
flowNodeType: item.data.flowNodeType,
|
||||
showStatus: item.data.showStatus,
|
||||
position: item.position,
|
||||
version: item.data.version,
|
||||
inputs: item.data.inputs,
|
||||
outputs: item.data.outputs,
|
||||
pluginId: item.data.pluginId,
|
||||
nodeVersion: item.data.nodeVersion
|
||||
}));
|
||||
|
||||
// get all handle
|
||||
const reactFlowViewport = document.querySelector('.react-flow__viewport');
|
||||
// Gets the value of data-handleid on all elements below it whose data-handleid is not empty
|
||||
const handleList =
|
||||
reactFlowViewport?.querySelectorAll('[data-handleid]:not([data-handleid=""])') || [];
|
||||
const handleIdList = Array.from(handleList).map(
|
||||
(item) => item.getAttribute('data-handleid') || ''
|
||||
);
|
||||
const formatEdges: StoreEdgeItemType[] = edges
|
||||
.map((item) => ({
|
||||
source: item.source,
|
||||
target: item.target,
|
||||
sourceHandle: item.sourceHandle || '',
|
||||
targetHandle: item.targetHandle || ''
|
||||
}))
|
||||
.filter((item) => item.sourceHandle && item.targetHandle)
|
||||
.filter(
|
||||
// Filter out edges that do not have both sourceHandle and targetHandle
|
||||
(item) => handleIdList.includes(item.sourceHandle) && handleIdList.includes(item.targetHandle)
|
||||
);
|
||||
|
||||
return {
|
||||
nodes: formatNodes,
|
||||
edges: formatEdges
|
||||
};
|
||||
};
|
||||
|
||||
export const filterExportModules = (modules: StoreNodeItemType[]) => {
|
||||
modules.forEach((module) => {
|
||||
// dataset - remove select dataset value
|
||||
if (module.flowNodeType === FlowNodeTypeEnum.datasetSearchNode) {
|
||||
module.inputs.forEach((item) => {
|
||||
if (item.key === NodeInputKeyEnum.datasetSelectList) {
|
||||
item.value = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return JSON.stringify(modules, null, 2);
|
||||
};
|
||||
@@ -41,6 +41,7 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
|
||||
type EditProps = EditApiKeyProps & { _id?: string };
|
||||
const defaultEditData: EditProps = {
|
||||
@@ -84,8 +85,14 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'} position={'relative'}>
|
||||
<Box display={['block', 'flex']} py={[0, 3]} px={5} alignItems={'center'}>
|
||||
<MyBox
|
||||
isLoading={isGetting || isDeleting}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
h={'100%'}
|
||||
position={'relative'}
|
||||
>
|
||||
<Box display={['block', 'flex']} alignItems={'center'}>
|
||||
<Box flex={1}>
|
||||
<Flex alignItems={'flex-end'}>
|
||||
<Box color={'myGray.900'} fontSize={'lg'}>
|
||||
@@ -103,7 +110,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
<Box fontSize={'xs'} color={'myGray.600'}>
|
||||
<Box fontSize={'mini'} color={'myGray.600'}>
|
||||
{tips}
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -140,7 +147,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<TableContainer mt={2} position={'relative'} minH={'300px'}>
|
||||
<TableContainer mt={3} position={'relative'} minH={'300px'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
@@ -225,7 +232,6 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<Loading loading={isGetting || isDeleting} fixed={false} />
|
||||
</TableContainer>
|
||||
|
||||
{!!editData && (
|
||||
@@ -279,7 +285,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
</Flex>
|
||||
</MyBox>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ const ConfigPerModal = ({
|
||||
<ModalBody>
|
||||
<HStack>
|
||||
<Avatar src={avatar} w={'1.75rem'} />
|
||||
<Box fontSize={'lg'}>{name}</Box>
|
||||
<Box>{name}</Box>
|
||||
</HStack>
|
||||
<Box mt={6}>
|
||||
<Box fontSize={'sm'}>{t('permission.Default permission')}</Box>
|
||||
|
||||
@@ -3,7 +3,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
import RowTabs from '../../../../common/Rowtabs';
|
||||
import RowTabs from '@fastgpt/web/components/common/Tabs/RowTabs';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
|
||||
Reference in New Issue
Block a user