4.8 preview (#1288)

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* perf: workflow ux

* system config

* Newflow (#89)

* docs: Add doc for Xinference (#1266)

Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* perf: workflow ux

* system config

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* rename code

* move code

* update flow

* input type selector

* perf: workflow runtime

* feat: node adapt newflow

* feat: adapt plugin

* feat: 360 connection

* check workflow

* perf: flow 性能

* change plugin input type (#81)

* change plugin input type

* plugin label mode

* perf: nodecard

* debug

* perf: debug ui

* connection ui

* change workflow ui (#82)

* feat: workflow debug

* adapt openAPI for new workflow (#83)

* adapt openAPI for new workflow

* i18n

* perf: plugin debug

* plugin input ui

* delete

* perf: global variable select

* fix rebase

* perf: workflow performance

* feat: input render type icon

* input icon

* adapt flow (#84)

* adapt newflow

* temp

* temp

* fix

* feat: app schedule trigger

* feat: app schedule trigger

* perf: schedule ui

* feat: ioslatevm run js code

* perf: workflow varialbe table ui

* feat: adapt simple mode

* feat: adapt input params

* output

* feat: adapt tamplate

* fix: ts

* add if-else module (#86)

* perf: worker

* if else node

* perf: tiktoken worker

* fix: ts

* perf: tiktoken

* fix if-else node (#87)

* fix if-else node

* type

* fix

* perf: audio render

* perf: Parallel worker

* log

* perf: if else node

* adapt plugin

* prompt

* perf: reference ui

* reference ui

* handle ux

* template ui and plugin tool

* adapt v1 workflow

* adapt v1 workflow completions

* perf: time variables

* feat: workflow keyboard shortcuts

* adapt v1 workflow

* update workflow example doc (#88)

* fix: simple mode select tool

---------

Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
Co-authored-by: Carson Yang <yangchuansheng33@gmail.com>
Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>

* doc

* perf: extract node

* extra node field

* update plugin version

* doc

* variable

* change doc & fix prompt editor (#90)

* fold workflow code

* value type label

---------

Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
Co-authored-by: Carson Yang <yangchuansheng33@gmail.com>
Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
This commit is contained in:
Archer
2024-04-25 17:51:20 +08:00
committed by GitHub
parent b08d81f887
commit 439c819ff1
505 changed files with 23570 additions and 18215 deletions

View File

@@ -14,7 +14,7 @@ const Avatar = ({ w = '30px', src, ...props }: ImageProps) => {
w={w}
h={w}
p={'1px'}
src={src}
src={src || LOGO_ICON}
{...props}
/>
);

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { ModalBody, Box, useTheme } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { DispatchNodeResponseType } from '@fastgpt/global/core/module/runtime/type.d';
import { DispatchNodeResponseType } from '@fastgpt/global/core/workflow/runtime/type.d';
const ContextModal = ({
context = [],

View File

@@ -1,8 +1,8 @@
import React, { useContext, createContext, useState, useMemo, useEffect, useCallback } from 'react';
import { useAudioPlay } from '@/web/common/utils/voice';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { ModuleItemType } from '@fastgpt/global/core/module/type';
import { splitGuideModule } from '@fastgpt/global/core/module/utils';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { splitGuideModule } from '@fastgpt/global/core/workflow/utils';
import {
AppTTSConfigType,
AppWhisperConfigType,
@@ -91,7 +91,7 @@ const StateContext = createContext<useChatStoreType>({
});
export type ChatProviderProps = OutLinkChatAuthProps & {
userGuideModule?: ModuleItemType;
userGuideModule?: StoreNodeItemType;
// not chat test params
chatId?: string;

View File

@@ -1,15 +1,14 @@
import React, { useMemo, useState } from 'react';
import { type ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { DispatchNodeResponseType } from '@fastgpt/global/core/module/runtime/type.d';
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
import { DispatchNodeResponseType } from '@fastgpt/global/core/workflow/runtime/type.d';
import { Flex, BoxProps, useDisclosure, useTheme, Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import dynamic from 'next/dynamic';
import Tag from '../Tag';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import MyTooltip from '../MyTooltip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import ChatBoxDivider from '@/components/core/chat/Divider';
import { strIsLink } from '@fastgpt/global/common/string/tools';
@@ -99,11 +98,6 @@ const ResponseTags = ({
};
}, [showDetail, flowResponses]);
const TagStyles: BoxProps = {
mr: 2,
bg: 'transparent'
};
return flowResponses.length === 0 ? null : (
<>
{sourceList.length > 0 && (
@@ -150,52 +144,52 @@ const ResponseTags = ({
</>
)}
{showDetail && (
<Flex alignItems={'center'} mt={3} flexWrap={'wrap'}>
<Flex alignItems={'center'} mt={3} flexWrap={'wrap'} gap={2}>
{quoteList.length > 0 && (
<MyTooltip label="查看引用">
<Tag
<MyTag
colorSchema="blue"
type="solid"
cursor={'pointer'}
{...TagStyles}
onClick={() => setQuoteModalData({ rawSearch: quoteList })}
>
{quoteList.length}
</Tag>
</MyTag>
</MyTooltip>
)}
{llmModuleAccount === 1 && (
<>
{historyPreview.length > 0 && (
<MyTooltip label={'点击查看上下文预览'}>
<Tag
<MyTag
colorSchema="green"
cursor={'pointer'}
{...TagStyles}
type="solid"
onClick={() => setContextModalData(historyPreview)}
>
{historyPreview.length}
</Tag>
</MyTag>
</MyTooltip>
)}
</>
)}
{llmModuleAccount > 1 && (
<Tag colorSchema="blue" {...TagStyles}>
<MyTag type="solid" colorSchema="blue">
AI
</Tag>
</MyTag>
)}
{isPc && runningTime > 0 && (
<MyTooltip label={'模块运行时间和'}>
<Tag colorSchema="purple" cursor={'default'} {...TagStyles}>
<MyTag colorSchema="purple" type="solid" cursor={'default'}>
{runningTime}s
</Tag>
</MyTag>
</MyTooltip>
)}
<MyTooltip label={t('core.chat.response.Read complete response tips')}>
<Tag colorSchema="gray" cursor={'pointer'} {...TagStyles} onClick={onOpenWholeModal}>
<MyTag colorSchema="gray" type="solid" cursor={'pointer'} onClick={onOpenWholeModal}>
{t('core.chat.response.Read complete response')}
</Tag>
</MyTag>
</MyTooltip>
</Flex>
)}

View File

@@ -2,7 +2,7 @@ import React, { useMemo, useState } from 'react';
import { Box, useTheme, Flex, Image } from '@chakra-ui/react';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { useTranslation } from 'next-i18next';
import { moduleTemplatesFlat } from '@fastgpt/global/core/module/template/constants';
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
import Tabs from '../Tabs';
import MyModal from '@fastgpt/web/components/common/MyModal';
@@ -34,12 +34,13 @@ function Row({
{t(label)}:
</Box>
<Box
borderRadius={'md'}
borderRadius={'sm'}
fontSize={'sm'}
bg={'myGray.50'}
{...(isCodeBlock
? { transform: 'translateY(-3px)' }
: value
? { px: 3, py: 1, border: theme.borders.base }
? { px: 3, py: 2, border: theme.borders.base }
: {})}
>
{value && <Markdown source={strValue} />}
@@ -86,12 +87,14 @@ const WholeResponseModal = ({
export default WholeResponseModal;
const ResponseBox = React.memo(function ResponseBox({
export const ResponseBox = React.memo(function ResponseBox({
response,
showDetail
showDetail,
hideTabs = false
}: {
response: ChatHistoryItemResType[];
showDetail: boolean;
hideTabs?: boolean;
}) {
const theme = useTheme();
const { t } = useTranslation();
@@ -105,7 +108,7 @@ const ResponseBox = React.memo(function ResponseBox({
mr={2}
src={
item.moduleLogo ||
moduleTemplatesFlat.find((template) => item.moduleType === template.flowType)
moduleTemplatesFlat.find((template) => item.moduleType === template.flowNodeType)
?.avatar
}
alt={''}
@@ -125,9 +128,11 @@ const ResponseBox = React.memo(function ResponseBox({
return (
<>
<Box>
<Tabs list={list} activeId={currentTab} onChange={setCurrentTab} />
</Box>
{!hideTabs && (
<Box>
<Tabs list={list} activeId={currentTab} onChange={setCurrentTab} />
</Box>
)}
<Box py={2} px={4} flex={'1 0 0'} overflow={'auto'}>
<>
<Row label={t('core.chat.response.module name')} value={t(activeModule.moduleName)} />
@@ -222,6 +227,7 @@ const ResponseBox = React.memo(function ResponseBox({
{/* classify question */}
<>
<Row label={t('core.chat.response.module cq result')} value={activeModule?.cqResult} />
<Row
label={t('core.chat.response.module cq')}
value={(() => {
@@ -229,7 +235,14 @@ const ResponseBox = React.memo(function ResponseBox({
return activeModule.cqList.map((item) => `* ${item.value}`).join('\n');
})()}
/>
<Row label={t('core.chat.response.module cq result')} value={activeModule?.cqResult} />
</>
{/* if-else */}
<>
<Row
label={t('core.chat.response.module if else Result')}
value={activeModule?.ifElseResult}
/>
</>
{/* extract */}

View File

@@ -1,5 +1,4 @@
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useAudioPlay } from '@/web/common/utils/voice';
import { Flex, FlexProps, Image, css, useTheme } from '@chakra-ui/react';
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';

View File

@@ -26,6 +26,7 @@ import {
} from '@fastgpt/global/core/chat/constants';
import FilesBlock from './FilesBox';
import { useChatProviderStore } from '../Provider';
import Avatar from '@/components/Avatar';
const colorMap = {
[ChatStatusEnum.loading]: {
@@ -157,7 +158,7 @@ ${JSON.stringify(questionGuides)}`;
color: 'primary.600'
}}
>
<Image src={tool.toolAvatar} alt={''} w={'14px'} mr={2} />
<Avatar src={tool.toolAvatar} borderRadius={'md'} w={'14px'} mr={2} />
<Box mr={1}>{tool.toolName}</Box>
{isChatting && !tool.response && (
<MyIcon name={'common/loading'} w={'14px'} />

View File

@@ -5,7 +5,7 @@ import { useTranslation } from 'next-i18next';
import { Box, Button, Card, Input, Textarea } from '@chakra-ui/react';
import ChatAvatar from './ChatAvatar';
import { MessageCardStyle } from '../constants';
import { VariableInputEnum } from '@fastgpt/global/core/module/constants';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ChatBoxInputFormType } from '../type.d';

View File

@@ -21,9 +21,9 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
import { Box, Flex, Checkbox } from '@chakra-ui/react';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { VariableInputEnum } from '@fastgpt/global/core/module/constants';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/module/runtime/constants';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { useForm } from 'react-hook-form';
import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore';
@@ -52,7 +52,7 @@ import { getNanoid } from '@fastgpt/global/common/string/tools';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { formatChatValue2InputType } from './utils';
import { textareaMinH } from './constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/module/runtime/constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import ChatProvider, { useChatProviderStore } from './Provider';
import ChatItem from './components/ChatItem';
@@ -79,7 +79,7 @@ type Props = OutLinkChatAuthProps & {
showEmptyIntro?: boolean;
appAvatar?: string;
userAvatar?: string;
userGuideModule?: ModuleItemType;
userGuideModule?: StoreNodeItemType;
showFileSelector?: boolean;
active?: boolean; // can use
appId: string;
@@ -588,7 +588,7 @@ const ChatBox = (
setLoading(false);
};
},
[chatHistories, onDelMessage, sendPrompt, setLoading, toast]
[chatHistories, onDelMessage, sendPrompt, setChatHistories, setLoading, toast]
);
// delete one message(One human and the ai response)
const delOneMessage = useCallback(

View File

@@ -5,7 +5,7 @@ import {
ChatSiteItemType,
ToolModuleResponseItemType
} from '@fastgpt/global/core/chat/type';
import { SseResponseEventEnum } from '@fastgpt/global/core/module/runtime/constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
export type generatingMessageProps = {
event: `${SseResponseEventEnum}`;

View File

@@ -1,22 +0,0 @@
import React from 'react';
import { Flex, Box, FlexProps } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
type Props = FlexProps & {
text?: string | React.ReactNode;
};
const EmptyTip = ({ text, ...props }: Props) => {
const { t } = useTranslation();
return (
<Flex mt={5} flexDirection={'column'} alignItems={'center'} pt={'10vh'} {...props}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
{text || t('common.empty.Common Tip')}
</Box>
</Flex>
);
};
export default EmptyTip;

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo } from 'react';
import { Box, useColorMode, Flex } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { useSystemStore } from '@/web/common/system/useSystemStore';
@@ -10,8 +10,8 @@ import { getUnreadCount } from '@/web/support/user/inform/api';
import dynamic from 'next/dynamic';
import Auth from './auth';
import Navbar from './navbar';
import NavbarPhone from './navbarPhone';
const Navbar = dynamic(() => import('./navbar'));
const NavbarPhone = dynamic(() => import('./navbarPhone'));
const UpdateInviteModal = dynamic(() => import('@/components/support/user/team/UpdateInviteModal'));
const NotSufficientModal = dynamic(() => import('@/components/support/wallet/NotSufficientModal'));
const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal'));
@@ -42,7 +42,6 @@ const phoneUnShowLayoutRoute: Record<string, boolean> = {
const Layout = ({ children }: { children: JSX.Element }) => {
const router = useRouter();
const { colorMode, setColorMode } = useColorMode();
const { Loading } = useLoading();
const { loading, setScreenWidth, isPc, feConfigs, isNotSufficientModal } = useSystemStore();
const { userInfo } = useUserStore();
@@ -52,12 +51,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
[router.pathname, router.query]
);
useEffect(() => {
if (colorMode === 'dark' && router.pathname !== '/chat') {
setColorMode('light');
}
}, [colorMode, router.pathname, setColorMode]);
// listen screen width
useEffect(() => {
const resize = throttle(() => {
setScreenWidth(document.documentElement.clientWidth);

View File

@@ -1,135 +0,0 @@
import React, { useRef, useState } from 'react';
import {
Menu,
MenuList,
MenuItem,
Box,
useOutsideClick,
MenuButton,
MenuItemProps
} from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
type MenuItemType = 'primary' | 'danger';
interface Props {
width?: number | string;
offset?: [number, number];
Button: React.ReactNode;
trigger?: 'hover' | 'click';
menuList: {
isActive?: boolean;
label: string | React.ReactNode;
icon?: string;
type?: MenuItemType;
onClick: () => any;
}[];
}
const MyMenu = ({
width = 'auto',
trigger = 'hover',
offset = [0, 5],
Button,
menuList
}: Props) => {
const typeMapStyle: Record<MenuItemType, MenuItemProps> = {
primary: {
_hover: {
backgroundColor: 'primary.50',
color: 'primary.600'
}
},
danger: {
_hover: {
color: 'red.600',
background: 'red.1'
}
}
};
const menuItemStyles: MenuItemProps = {
borderRadius: 'sm',
py: 3,
display: 'flex',
alignItems: 'center'
};
const ref = useRef<HTMLDivElement>(null);
const closeTimer = useRef<any>();
const [isOpen, setIsOpen] = useState(false);
useOutsideClick({
ref: ref,
handler: () => {
setIsOpen(false);
}
});
return (
<Menu offset={offset} isOpen={isOpen} autoSelect={false} direction={'ltr'} isLazy>
<Box
ref={ref}
onMouseEnter={() => {
if (trigger === 'hover') {
setIsOpen(true);
}
clearTimeout(closeTimer.current);
}}
onMouseLeave={() => {
if (trigger === 'hover') {
closeTimer.current = setTimeout(() => {
setIsOpen(false);
}, 100);
}
}}
>
<Box
position={'relative'}
onClickCapture={() => {
if (trigger === 'click') {
setIsOpen(!isOpen);
}
}}
>
<MenuButton
w={'100%'}
h={'100%'}
position={'absolute'}
top={0}
right={0}
bottom={0}
left={0}
/>
<Box position={'relative'}>{Button}</Box>
</Box>
<MenuList
minW={isOpen ? `${width}px !important` : 0}
p={'6px'}
border={'1px solid #fff'}
boxShadow={
'0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);'
}
>
{menuList.map((item, i) => (
<MenuItem
key={i}
{...menuItemStyles}
{...typeMapStyle[item.type || 'primary']}
onClick={(e) => {
e.stopPropagation();
setIsOpen(false);
item.onClick && item.onClick();
}}
color={item.isActive ? 'primary.700' : 'myGray.600'}
whiteSpace={'pre-wrap'}
>
{!!item.icon && <MyIcon name={item.icon as any} w={'16px'} mr={2} />}
{item.label}
</MenuItem>
))}
</MenuList>
</Box>
</Menu>
);
};
export default MyMenu;

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
@@ -9,7 +9,7 @@ import { HUGGING_FACE_ICON, LOGO_ICON } from '@fastgpt/global/common/system/cons
import { Box, Flex } from '@chakra-ui/react';
import Avatar from '../Avatar';
const AIModelSelector = ({ list, ...props }: SelectProps) => {
const AIModelSelector = ({ list, onchange, ...props }: SelectProps) => {
const { t } = useTranslation();
const { feConfigs, llmModelList, vectorModelList } = useSystemStore();
const router = useRouter();
@@ -50,19 +50,20 @@ const AIModelSelector = ({ list, ...props }: SelectProps) => {
: avatarList;
}, [feConfigs.show_pay, avatarList, t]);
const onSelect = useCallback(
(e: string) => {
if (e === 'price') {
router.push(AI_POINT_USAGE_CARD_ROUTE);
return;
}
onchange?.(e);
},
[onchange, router]
);
return (
<>
<MySelect
list={expandList}
{...props}
onchange={(e) => {
if (e === 'price') {
router.push(AI_POINT_USAGE_CARD_ROUTE);
return;
}
props.onchange?.(e);
}}
/>
<MySelect list={expandList} {...props} onchange={onSelect} />
</>
);
};

View File

@@ -1,53 +0,0 @@
import React, { useMemo } from 'react';
import { Flex, type FlexProps } from '@chakra-ui/react';
interface Props extends FlexProps {
children: React.ReactNode | React.ReactNode[];
colorSchema?: 'blue' | 'green' | 'gray' | 'purple';
}
const Tag = ({ children, colorSchema = 'blue', ...props }: Props) => {
const theme = useMemo(() => {
const map = {
blue: {
borderColor: 'primary.500',
bg: '#F2FBFF',
color: 'primary.600'
},
green: {
borderColor: '#67c13b',
bg: '#f8fff8',
color: '#67c13b'
},
purple: {
borderColor: '#A558C9',
bg: '#F6EEFA',
color: '#A558C9'
},
gray: {
borderColor: 'borderColor.base',
bg: 'myGray.50',
color: 'myGray.700'
}
};
return map[colorSchema];
}, [colorSchema]);
return (
<Flex
{...theme}
borderWidth={'1px'}
px={2}
lineHeight={1}
py={1}
borderRadius={'sm'}
fontSize={'xs'}
alignItems={'center'}
{...props}
>
{children}
</Flex>
);
};
export default Tag;

View File

@@ -14,8 +14,8 @@ import {
} from '@chakra-ui/react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MySlider from '@/components/Slider';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import type { SettingAIDataType } from '@fastgpt/global/core/module/node/type.d';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { SettingAIDataType } from '@fastgpt/global/core/app/type.d';
import { getDocPath } from '@/web/common/system/doc';
import AIModelSelector from '@/components/Select/AIModelSelector';
import { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
@@ -42,7 +42,7 @@ const AIChatSettingsModal = ({
defaultValues: defaultData
});
const model = watch('model');
const showResponseAnswerText = watch(ModuleInputKeyEnum.aiChatIsResponseText) !== undefined;
const showResponseAnswerText = watch(NodeInputKeyEnum.aiChatIsResponseText) !== undefined;
const showMaxHistoriesSlider = watch('maxHistories') !== undefined;
const selectedModel = llmModelList.find((item) => item.model === model) || llmModelList[0];
@@ -72,7 +72,7 @@ const AIChatSettingsModal = ({
return (
<MyModal
isOpen
iconSrc="/imgs/module/AI.png"
iconSrc="/imgs/workflow/AI.png"
onClose={onClose}
title={
<>
@@ -136,7 +136,7 @@ const AIChatSettingsModal = ({
<QuestionTip ml={1} label={t('core.module.template.AI support tool tip')} />
</Box>
<Box flex={1} ml={'10px'}>
{selectedModel?.usedInToolCall ? '支持' : '不支持'}
{selectedModel?.toolChoice || selectedModel?.functionCall ? '支持' : '不支持'}
</Box>
</Flex>
<Flex mt={8}>
@@ -152,9 +152,9 @@ const AIChatSettingsModal = ({
width={'95%'}
min={0}
max={10}
value={getValues(ModuleInputKeyEnum.aiChatTemperature)}
value={getValues(NodeInputKeyEnum.aiChatTemperature)}
onChange={(e) => {
setValue(ModuleInputKeyEnum.aiChatTemperature, e);
setValue(NodeInputKeyEnum.aiChatTemperature, e);
setRefresh(!refresh);
}}
/>
@@ -174,9 +174,9 @@ const AIChatSettingsModal = ({
min={100}
max={tokenLimit}
step={50}
value={getValues(ModuleInputKeyEnum.aiChatMaxToken)}
value={getValues(NodeInputKeyEnum.aiChatMaxToken)}
onChange={(val) => {
setValue(ModuleInputKeyEnum.aiChatMaxToken, val);
setValue(NodeInputKeyEnum.aiChatMaxToken, val);
setRefresh(!refresh);
}}
/>
@@ -215,11 +215,11 @@ const AIChatSettingsModal = ({
</Box>
<Box flex={1} ml={'10px'}>
<Switch
isChecked={getValues(ModuleInputKeyEnum.aiChatIsResponseText)}
isChecked={getValues(NodeInputKeyEnum.aiChatIsResponseText)}
size={'lg'}
onChange={(e) => {
const value = e.target.checked;
setValue(ModuleInputKeyEnum.aiChatIsResponseText, value);
setValue(NodeInputKeyEnum.aiChatIsResponseText, value);
setRefresh((state) => !state);
}}
/>

View File

@@ -1,11 +1,13 @@
import React, { useEffect } from 'react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { LLMModelTypeEnum, llmModelTypeFilterMap } from '@fastgpt/global/core/ai/constants';
import { Box, Button, useDisclosure } from '@chakra-ui/react';
import { SettingAIDataType } from '@fastgpt/global/core/module/node/type';
import { Box, Button, Flex, css, useDisclosure } from '@chakra-ui/react';
import type { SettingAIDataType } from '@fastgpt/global/core/app/type.d';
import AISettingModal from '@/components/core/ai/AISettingModal';
import Avatar from '@/components/Avatar';
import { HUGGING_FACE_ICON } from '@fastgpt/global/common/system/constants';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next';
type Props = {
llmModelType?: `${LLMModelTypeEnum}`;
@@ -14,6 +16,7 @@ type Props = {
};
const SettingLLMModel = ({ llmModelType = LLMModelTypeEnum.all, defaultData, onChange }: Props) => {
const { t } = useTranslation();
const { llmModelList } = useSystemStore();
const model = defaultData.model;
@@ -41,30 +44,39 @@ const SettingLLMModel = ({ llmModelType = LLMModelTypeEnum.all, defaultData, onC
model: modelList[0].model
});
}
}, [defaultData, model, modelList, onChange]);
}, []);
return (
<Box position={'relative'}>
<Button
w={'100%'}
justifyContent={'flex-start'}
variant={'whitePrimary'}
_active={{
transform: 'none'
}}
leftIcon={
<Avatar
borderRadius={'0'}
src={selectedModel?.avatar || HUGGING_FACE_ICON}
fallbackSrc={HUGGING_FACE_ICON}
w={'18px'}
/>
<Box
css={css({
span: {
display: 'block'
}
pl={4}
onClick={onOpenAIChatSetting}
>
{selectedModel?.name}
</Button>
})}
position={'relative'}
>
<MyTooltip label={t('core.app.Setting ai property')}>
<Button
w={'100%'}
justifyContent={'flex-start'}
variant={'whiteFlow'}
_active={{
transform: 'none'
}}
leftIcon={
<Avatar
borderRadius={'0'}
src={selectedModel?.avatar || HUGGING_FACE_ICON}
fallbackSrc={HUGGING_FACE_ICON}
w={'18px'}
/>
}
pl={4}
onClick={onOpenAIChatSetting}
>
{selectedModel?.name}
</Button>
</MyTooltip>
{isOpenAIChatSetting && (
<AISettingModal
onClose={onCloseAIChatSetting}

View File

@@ -20,7 +20,7 @@ import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constants';
import MyRadio from '@/components/common/MyRadio';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -133,7 +133,7 @@ const DatasetParamsModal = ({
{
label: t('core.module.template.Query extension'),
id: SearchSettingTabEnum.queryExtension,
icon: '/imgs/module/cfr.svg'
icon: '/imgs/workflow/cfr.svg'
}
]}
activeId={currentTabType}
@@ -223,9 +223,9 @@ const DatasetParamsModal = ({
min={100}
max={maxTokens}
step={50}
value={getValues(ModuleInputKeyEnum.datasetMaxTokens) ?? 1000}
value={getValues(NodeInputKeyEnum.datasetMaxTokens) ?? 1000}
onChange={(val) => {
setValue(ModuleInputKeyEnum.datasetMaxTokens, val);
setValue(NodeInputKeyEnum.datasetMaxTokens, val);
setRefresh(!refresh);
}}
/>
@@ -249,9 +249,9 @@ const DatasetParamsModal = ({
min={0}
max={1}
step={0.01}
value={getValues(ModuleInputKeyEnum.datasetSimilarity) ?? 0.5}
value={getValues(NodeInputKeyEnum.datasetSimilarity) ?? 0.5}
onChange={(val) => {
setValue(ModuleInputKeyEnum.datasetSimilarity, val);
setValue(NodeInputKeyEnum.datasetSimilarity, val);
setRefresh(!refresh);
}}
/>

View File

@@ -11,7 +11,7 @@ import {
Divider
} from '@chakra-ui/react';
import Avatar from '@/components/Avatar';
import type { SelectedDatasetType } from '@fastgpt/global/core/module/api.d';
import type { SelectedDatasetType } from '@fastgpt/global/core/workflow/api.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MyTooltip from '@/components/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -20,7 +20,7 @@ import { useTranslation } from 'next-i18next';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import DatasetSelectContainer, { useDatasetSelect } from '@/components/core/dataset/SelectModal';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import EmptyTip from '@/components/EmptyTip';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
export const DatasetSelectModal = ({
isOpen,

View File

@@ -11,7 +11,7 @@ const QGSwitch = (props: SwitchProps) => {
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/chat/QGFill'} mr={2} w={'20px'} />
<Box>{t('core.app.Question Guide')}</Box>
<Box fontWeight={'medium'}>{t('core.app.Question Guide')}</Box>
<MyTooltip label={t('core.app.Question Guide Tip')} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>

View File

@@ -0,0 +1,340 @@
import { Box, Button, Flex, ModalBody, useDisclosure, Switch, Textarea } from '@chakra-ui/react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { AppScheduledTriggerConfigType } from '@fastgpt/global/core/app/type';
import MyModal from '@fastgpt/web/components/common/MyModal';
import dynamic from 'next/dynamic';
import type { MultipleSelectProps } from '@fastgpt/web/components/common/MySelect/type.d';
import { useForm } from 'react-hook-form';
import { cronParser2Fields } from '@fastgpt/global/common/string/time';
import TimezoneSelect from '@fastgpt/web/components/common/MySelect/TimezoneSelect';
const MultipleRowSelect = dynamic(
() => import('@fastgpt/web/components/common/MySelect/MultipleRowSelect')
);
// options type:
enum CronJobTypeEnum {
month = 'month',
week = 'week',
day = 'day',
interval = 'interval'
}
type CronType = 'month' | 'week' | 'day' | 'interval';
const get24HoursOptions = () => {
return Array.from({ length: 24 }, (_, i) => ({
label: `${i < 10 ? '0' : ''}${i}:00`,
value: i
}));
};
const getWeekOptions = () => {
return Array.from({ length: 7 }, (_, i) => {
if (i === 0) {
return {
label: '星期日',
value: i,
children: get24HoursOptions()
};
}
return {
label: `星期${i}`,
value: i,
children: get24HoursOptions()
};
});
};
const getMonthOptions = () => {
return Array.from({ length: 28 }, (_, i) => ({
label: `${i + 1}`,
value: i,
children: get24HoursOptions()
}));
};
const getInterValOptions = () => {
// 每n小时
return [
{
label: `每小时`,
value: 1
},
{
label: `每2小时`,
value: 2
},
{
label: `每3小时`,
value: 3
},
{
label: `每4小时`,
value: 4
},
{
label: `每6小时`,
value: 6
},
{
label: `每12小时`,
value: 12
}
];
};
const defaultValue = ['day', 0, 0];
const defaultCronString = '0 0 * * *';
type CronFieldType = [CronType, number, number];
const ScheduledTriggerConfig = ({
value,
onChange
}: {
value: AppScheduledTriggerConfigType | null;
onChange: (e: AppScheduledTriggerConfigType | null) => void;
}) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const { register, setValue, watch } = useForm<AppScheduledTriggerConfigType>({
defaultValues: {
cronString: value?.cronString || '',
timezone: value?.timezone,
defaultPrompt: value?.defaultPrompt || ''
}
});
const timezone = watch('timezone');
const cronString = watch('cronString');
const cronSelectList = useRef<MultipleSelectProps['list']>([
{
label: '每天执行',
value: CronJobTypeEnum.day,
children: get24HoursOptions()
},
{
label: '每周执行',
value: CronJobTypeEnum.week,
children: getWeekOptions()
},
{
label: '每月执行',
value: CronJobTypeEnum.month,
children: getMonthOptions()
},
{
label: '间隔执行',
value: CronJobTypeEnum.interval,
children: getInterValOptions()
}
]);
/* cron string to config field */
const cronConfig = useMemo(() => {
if (!cronString) {
return null;
}
const cronField = cronParser2Fields(cronString);
if (!cronField) {
return null;
}
if (cronField.dayOfMonth.length !== 31) {
return {
isOpen: true,
cronField: [CronJobTypeEnum.month, cronField.dayOfMonth[0], cronField.hour[0]]
};
}
if (cronField.dayOfWeek.length !== 8) {
return {
isOpen: true,
cronField: [CronJobTypeEnum.week, cronField.dayOfWeek[0], cronField.hour[0]]
};
}
if (cronField.hour.length === 1) {
return {
isOpen: true,
cronField: [CronJobTypeEnum.day, cronField.hour[0], 0]
};
}
return {
isOpen: true,
cronField: [CronJobTypeEnum.interval, 24 / cronField.hour.length, 0]
};
}, [cronString]);
const isOpenSchedule = cronConfig?.isOpen;
const cronField = (cronConfig?.cronField || defaultValue) as CronFieldType;
const cronConfig2cronString = useCallback(
(e: CronFieldType) => {
if (e[0] === CronJobTypeEnum.month) {
setValue('cronString', `0 ${e[2]} ${e[1]} * *`);
} else if (e[0] === CronJobTypeEnum.week) {
setValue('cronString', `0 ${e[2]} * * ${e[1]}`);
} else if (e[0] === CronJobTypeEnum.day) {
setValue('cronString', `0 ${e[1]} * * *`);
} else if (e[0] === CronJobTypeEnum.interval) {
setValue('cronString', `0 */${e[1]} * * *`);
} else {
setValue('cronString', '');
}
},
[setValue]
);
// cron config to show label
const formatLabel = useMemo(() => {
if (!isOpenSchedule) {
return t('common.Not open');
}
if (cronField[0] === 'month') {
return t('core.app.schedule.Every month', {
day: cronField[1],
hour: cronField[2]
});
}
if (cronField[0] === 'week') {
return t('core.app.schedule.Every week', {
day: cronField[1] === 0 ? '日' : cronField[1],
hour: cronField[2]
});
}
if (cronField[0] === 'day') {
return t('core.app.schedule.Every day', {
hour: cronField[1]
});
}
if (cronField[0] === 'interval') {
return t('core.app.schedule.Interval', {
interval: cronField[1]
});
}
return t('common.Not open');
}, [cronField, isOpenSchedule, t]);
// update value
watch((data) => {
if (!data.cronString) {
onChange(null);
return;
}
onChange({
cronString: data.cronString,
timezone: data.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
defaultPrompt: data.defaultPrompt || ''
});
});
useEffect(() => {
if (!value?.timezone) {
setValue('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);
}
}, []);
const Render = useMemo(() => {
return (
<>
<Flex alignItems={'center'}>
<MyIcon name={'core/app/schedulePlan'} w={'20px'} />
<Flex alignItems={'center'} ml={2} flex={1}>
{t('core.app.Interval timer run')}
<QuestionTip ml={1} label={t('core.app.Interval timer tip')} />
</Flex>
<MyTooltip label={t('core.app.Config schedule plan')}>
<Button
variant={'transparentBase'}
iconSpacing={1}
size={'sm'}
mr={'-5px'}
onClick={onOpen}
>
{formatLabel}
</Button>
</MyTooltip>
</Flex>
<MyModal
isOpen={isOpen}
onClose={onClose}
iconSrc={'core/app/schedulePlan'}
title={t('core.app.Interval timer config')}
overflow={'unset'}
>
<ModalBody>
<Flex justifyContent={'space-between'} alignItems={'center'}>
<Box flex={'0 0 80px'}> {t('core.app.schedule.Open schedule')}</Box>
<Switch
size={'lg'}
isChecked={isOpenSchedule}
onChange={(e) => {
if (e.target.checked) {
setValue('cronString', defaultCronString);
} else {
setValue('cronString', '');
}
}}
/>
</Flex>
{isOpenSchedule && (
<>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'}></Box>
<Box flex={'1 0 0'}>
<MultipleRowSelect
label={formatLabel}
value={cronField}
list={cronSelectList.current}
onSelect={(e) => {
cronConfig2cronString(e as CronFieldType);
}}
/>
</Box>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 80px'}></Box>
<Box flex={'1 0 0'}>
<TimezoneSelect
value={timezone}
onChange={(e) => {
setValue('timezone', e);
}}
/>
</Box>
</Flex>
<Box mt={5}>
<Box>{t('core.app.schedule.Default prompt')}</Box>
<Textarea
{...register('defaultPrompt')}
rows={8}
bg={'myGray.50'}
placeholder={t('core.app.schedule.Default prompt placeholder')}
/>
</Box>
</>
)}
</ModalBody>
</MyModal>
</>
);
}, [
cronConfig2cronString,
cronField,
formatLabel,
isOpen,
isOpenSchedule,
onClose,
onOpen,
register,
setValue,
t,
timezone
]);
return Render;
};
export default React.memo(ScheduledTriggerConfig);

View File

@@ -80,7 +80,7 @@ const TTSSelect = ({
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/tts'} mr={2} w={'20px'} />
<Box>{t('core.app.TTS')}</Box>
<Box fontWeight={'medium'}>{t('core.app.TTS')}</Box>
<MyTooltip label={t('core.app.TTS Tip')} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
@@ -90,7 +90,6 @@ const TTSSelect = ({
variant={'transparentBase'}
iconSpacing={1}
size={'sm'}
fontSize={'md'}
mr={'-5px'}
onClick={onOpen}
>

View File

@@ -25,7 +25,7 @@ import {
useDisclosure
} from '@chakra-ui/react';
import { QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons';
import { VariableInputEnum, variableMap } from '@fastgpt/global/core/module/constants';
import { VariableInputEnum, variableMap } from '@fastgpt/global/core/workflow/constants';
import type { VariableItemType } from '@fastgpt/global/core/app/type.d';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useForm } from 'react-hook-form';
@@ -34,11 +34,11 @@ import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyTooltip from '@/components/MyTooltip';
import { variableTip } from '@fastgpt/global/core/module/template/tip';
import { variableTip } from '@fastgpt/global/core/workflow/template/tip';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MyRadio from '@/components/common/MyRadio';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/module/utils';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
const VariableEdit = ({
variables,
@@ -98,7 +98,7 @@ const VariableEdit = ({
<Box>
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/variable'} w={'20px'} />
<Box ml={2} flex={1}>
<Box ml={2} flex={1} fontWeight={'medium'}>
{t('core.module.Variable')}
<MyTooltip label={t(variableTip)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
@@ -110,7 +110,6 @@ const VariableEdit = ({
iconSpacing={1}
size={'sm'}
mr={'-5px'}
fontSize={'md'}
onClick={() => {
resetEdit({ variable: addVariable() });
onOpenEdit();

View File

@@ -32,14 +32,13 @@ const WhisperConfig = ({
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/whisper'} mr={2} w={'20px'} />
<Box>{t('core.app.Whisper')}</Box>
<Box fontWeight={'medium'}>{t('core.app.Whisper')}</Box>
<Box flex={1} />
<MyTooltip label={t('core.app.Config whisper')}>
<Button
variant={'transparentBase'}
iconSpacing={1}
size={'sm'}
fontSize={'md'}
mr={'-5px'}
onClick={onOpen}
>

View File

@@ -183,7 +183,9 @@ const QuoteItem = ({
w={'100%'}
size="sm"
borderRadius={'20px'}
colorScheme={scoreTheme[i]?.colorSchema}
{...(scoreTheme[i] && {
colorScheme: scoreTheme[i].colorSchema
})}
bg="#E8EBF0"
/>
)}
@@ -199,7 +201,14 @@ const QuoteItem = ({
</Box>
{canViewSource && (
<Flex alignItems={'center'} mt={3} gap={4} color={'myGray.500'} fontSize={'xs'}>
<Flex
alignItems={'center'}
flexWrap={'wrap'}
mt={3}
gap={4}
color={'myGray.500'}
fontSize={'xs'}
>
<MyTooltip label={t('core.dataset.Quote Length')}>
<Flex alignItems={'center'}>
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />

View File

@@ -33,7 +33,7 @@ const DatasetSelectContainer = ({
return (
<MyModal
iconSrc="/imgs/module/db.png"
iconSrc="/imgs/workflow/db.png"
title={
<Box fontWeight={'normal'}>
<ParentPaths

View File

@@ -1,572 +0,0 @@
import {
type Node,
type NodeChange,
type Edge,
type EdgeChange,
useNodesState,
useEdgesState,
Connection,
addEdge
} from 'reactflow';
import type { FlowModuleItemType, FlowNodeTemplateType } from '@fastgpt/global/core/module/type.d';
import type {
FlowNodeChangeProps,
FlowNodeInputItemType
} from '@fastgpt/global/core/module/node/type';
import React, {
type SetStateAction,
type Dispatch,
useContext,
useCallback,
createContext,
useRef,
useEffect,
useMemo
} from 'react';
import { customAlphabet } from 'nanoid';
import { appModule2FlowEdge, appModule2FlowNode } from '@/utils/adapt';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import {
ModuleIOValueTypeEnum,
ModuleInputKeyEnum,
ModuleOutputKeyEnum
} from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
import { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
type requestEventType =
| 'onChangeNode'
| 'onCopyNode'
| 'onResetNode'
| 'onDelNode'
| 'onDelConnect'
| 'setNodes';
export type useFlowProviderStoreType = {
reactFlowWrapper: null | React.RefObject<HTMLDivElement>;
mode: 'app' | 'plugin';
filterAppIds: string[];
nodes: Node<FlowModuleItemType, string | undefined>[];
setNodes: Dispatch<SetStateAction<Node<FlowModuleItemType, string | undefined>[]>>;
onNodesChange: OnChange<NodeChange>;
edges: Edge<any>[];
setEdges: Dispatch<SetStateAction<Edge<any>[]>>;
onEdgesChange: OnChange<EdgeChange>;
onFixView: () => void;
onDelNode: (nodeId: string) => void;
onChangeNode: (e: FlowNodeChangeProps) => void;
onCopyNode: (nodeId: string) => void;
onResetNode: (e: { id: string; module: FlowNodeTemplateType }) => void;
onDelEdge: (e: {
moduleId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}) => void;
onDelConnect: (id: string) => void;
onConnect: ({ connect }: { connect: Connection }) => any;
initData: (modules: ModuleItemType[]) => void;
splitToolInputs: (
inputs: FlowNodeInputItemType[],
moduleId: string
) => {
isTool: boolean;
toolInputs: FlowNodeInputItemType[];
commonInputs: FlowNodeInputItemType[];
};
hasToolNode: boolean;
};
const StateContext = createContext<useFlowProviderStoreType>({
reactFlowWrapper: null,
mode: 'app',
filterAppIds: [],
nodes: [],
setNodes: function (
value: React.SetStateAction<Node<FlowModuleItemType, string | undefined>[]>
): void {
return;
},
onNodesChange: function (changes: NodeChange[]): void {
return;
},
edges: [],
setEdges: function (value: React.SetStateAction<Edge<any>[]>): void {
return;
},
onEdgesChange: function (changes: EdgeChange[]): void {
return;
},
onFixView: function (): void {
return;
},
onDelNode: function (nodeId: string): void {
return;
},
onChangeNode: function (e: FlowNodeChangeProps): void {
return;
},
onCopyNode: function (nodeId: string): void {
return;
},
onDelEdge: function (e: {
moduleId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}): void {
return;
},
onDelConnect: function (id: string): void {
return;
},
onConnect: function ({ connect }: { connect: Connection }) {
return;
},
initData: function (modules: ModuleItemType[]): void {
throw new Error('Function not implemented.');
},
onResetNode: function (e): void {
throw new Error('Function not implemented.');
},
splitToolInputs: function (
inputs: FlowNodeInputItemType[],
moduleId: string
): {
isTool: boolean;
toolInputs: FlowNodeInputItemType[];
commonInputs: FlowNodeInputItemType[];
} {
throw new Error('Function not implemented.');
},
hasToolNode: false
});
export const useFlowProviderStore = () => useContext(StateContext);
export const FlowProvider = ({
mode,
filterAppIds = [],
children
}: {
mode: useFlowProviderStoreType['mode'];
filterAppIds?: string[];
children: React.ReactNode;
}) => {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const { toast } = useToast();
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const hasToolNode = useMemo(() => {
return !!nodes.find((node) => node.data.flowType === FlowNodeTypeEnum.tools);
}, [nodes]);
const onFixView = useCallback(() => {
const btn = document.querySelector('.custom-workflow-fix_view') as HTMLButtonElement;
setTimeout(() => {
btn && btn.click();
}, 100);
}, []);
const onDelEdge = useCallback(
({
moduleId,
sourceHandle,
targetHandle
}: {
moduleId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}) => {
if (!sourceHandle && !targetHandle) return;
setEdges((state) =>
state.filter((edge) => {
if (edge.source === moduleId && edge.sourceHandle === sourceHandle) return false;
if (edge.target === moduleId && edge.targetHandle === targetHandle) return false;
return true;
})
);
},
[setEdges]
);
const onDelConnect = useCallback(
(id: string) => {
setEdges((state) => state.filter((item) => item.id !== id));
},
[setEdges]
);
const onConnect = useCallback(
({ connect }: { connect: Connection }) => {
const source = nodes.find((node) => node.id === connect.source)?.data;
const sourceType = (() => {
const type = source?.outputs.find(
(output) => output.key === connect.sourceHandle
)?.valueType;
if (source?.flowType === FlowNodeTypeEnum.classifyQuestion && !type) {
return ModuleIOValueTypeEnum.boolean;
}
if (source?.flowType === FlowNodeTypeEnum.pluginInput) {
return source?.inputs.find((input) => input.key === connect.sourceHandle)?.valueType;
}
return source?.outputs.find((output) => output.key === connect.sourceHandle)?.valueType;
})();
const targetType = nodes
.find((node) => node.id === connect.target)
?.data?.inputs.find((input) => input.key === connect.targetHandle)?.valueType;
if (
connect.sourceHandle === ModuleOutputKeyEnum.selectedTools &&
connect.targetHandle === ModuleOutputKeyEnum.selectedTools
) {
} else if (!sourceType || !targetType) {
return toast({
status: 'warning',
title: t('app.Connection is invalid')
});
} else if (
sourceType !== ModuleIOValueTypeEnum.any &&
targetType !== ModuleIOValueTypeEnum.any &&
sourceType !== targetType
) {
return toast({
status: 'warning',
title: t('app.Connection type is different')
});
}
setEdges((state) =>
addEdge(
{
...connect,
type: EDGE_TYPE
},
state
)
);
},
[nodes, setEdges, t, toast]
);
const onDelNode = useCallback(
(nodeId: string) => {
setNodes((state) => state.filter((item) => item.id !== nodeId));
setEdges((state) => state.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
},
[setEdges, setNodes]
);
/* change */
const onChangeNode = useCallback(
({ moduleId, type, key, value, index }: FlowNodeChangeProps) => {
setNodes((nodes) =>
nodes.map((node) => {
if (node.id !== moduleId) return node;
const updateObj: Record<string, any> = {};
if (type === 'attr') {
if (key) {
updateObj[key] = value;
}
} else if (type === 'updateInput') {
updateObj.inputs = node.data.inputs.map((item) => (item.key === key ? value : item));
} else if (type === 'replaceInput') {
onDelEdge({ moduleId, targetHandle: key });
const oldInputIndex = node.data.inputs.findIndex((item) => item.key === key);
updateObj.inputs = node.data.inputs.filter((item) => item.key !== key);
setTimeout(() => {
onChangeNode({
moduleId,
type: 'addInput',
index: oldInputIndex,
value
});
});
} else if (type === 'addInput') {
const input = node.data.inputs.find((input) => input.key === value.key);
if (input) {
toast({
status: 'warning',
title: 'key 重复'
});
updateObj.inputs = node.data.inputs;
} else {
if (index !== undefined) {
const inputs = [...node.data.inputs];
inputs.splice(index, 0, value);
updateObj.inputs = inputs;
} else {
updateObj.inputs = node.data.inputs.concat(value);
}
}
} else if (type === 'delInput') {
onDelEdge({ moduleId, targetHandle: key });
updateObj.inputs = node.data.inputs.filter((item) => item.key !== key);
} else if (type === 'updateOutput') {
updateObj.outputs = node.data.outputs.map((item) => (item.key === key ? value : item));
} else if (type === 'replaceOutput') {
onDelEdge({ moduleId, sourceHandle: key });
const oldOutputIndex = node.data.outputs.findIndex((item) => item.key === key);
updateObj.outputs = node.data.outputs.filter((item) => item.key !== key);
setTimeout(() => {
onChangeNode({
moduleId,
type: 'addOutput',
index: oldOutputIndex,
value
});
});
} else if (type === 'addOutput') {
const output = node.data.outputs.find((output) => output.key === value.key);
if (output) {
toast({
status: 'warning',
title: 'key 重复'
});
updateObj.outputs = node.data.outputs;
} else {
if (index !== undefined) {
const outputs = [...node.data.outputs];
outputs.splice(index, 0, value);
updateObj.outputs = outputs;
} else {
updateObj.outputs = node.data.outputs.concat(value);
}
}
} else if (type === 'delOutput') {
onDelEdge({ moduleId, sourceHandle: key });
updateObj.outputs = node.data.outputs.filter((item) => item.key !== key);
}
return {
...node,
data: {
...node.data,
...updateObj
}
};
})
);
},
[onDelEdge, setNodes, toast]
);
const onCopyNode = useCallback(
(nodeId: string) => {
setNodes((nodes) => {
const node = nodes.find((node) => node.id === nodeId);
if (!node) return nodes;
const template = {
avatar: node.data.avatar,
name: node.data.name,
intro: node.data.intro,
flowType: node.data.flowType,
inputs: node.data.inputs,
outputs: node.data.outputs,
showStatus: node.data.showStatus
};
return nodes.concat(
appModule2FlowNode({
item: {
...template,
moduleId: nanoid(),
position: { x: node.position.x + 200, y: node.position.y + 50 }
}
})
);
});
},
[setNodes]
);
/* If the module is connected by a tool, the tool input and the normal input are separated */
const splitToolInputs = useCallback(
(inputs: FlowNodeInputItemType[], moduleId: string) => {
const isTool = !!edges.find(
(edge) =>
edge.targetHandle === ModuleOutputKeyEnum.selectedTools && edge.target === moduleId
);
return {
isTool,
toolInputs: inputs.filter((item) => isTool && item.toolDescription),
commonInputs: inputs.filter((item) => {
if (!isTool) return true;
return !item.toolDescription;
})
};
},
[edges]
);
// reset a node data. delete edge and replace it
const onResetNode = useCallback(
({ id, module }: { id: string; module: FlowNodeTemplateType }) => {
setNodes((state) =>
state.map((node) => {
if (node.id === id) {
// delete edge
node.data.inputs.forEach((item) => {
onDelEdge({ moduleId: id, targetHandle: item.key });
});
node.data.outputs.forEach((item) => {
onDelEdge({ moduleId: id, sourceHandle: item.key });
});
return {
...node,
data: {
...node.data,
...module
}
};
}
return node;
})
);
},
[onDelEdge, setNodes]
);
const initData = useCallback(
(modules: ModuleItemType[]) => {
const edges = appModule2FlowEdge({
modules
});
setEdges(edges);
setNodes(modules.map((item) => appModule2FlowNode({ item })));
onFixView();
},
[setEdges, setNodes, onFixView]
);
// use eventbus to avoid refresh ReactComponents
useEffect(() => {
eventBus.on(
EventNameEnum.requestFlowEvent,
({ type, data }: { type: requestEventType; data: any }) => {
switch (type) {
case 'onChangeNode':
onChangeNode(data);
return;
case 'onCopyNode':
onCopyNode(data);
return;
case 'onResetNode':
onResetNode(data);
return;
case 'onDelNode':
onDelNode(data);
return;
case 'onDelConnect':
onDelConnect(data);
return;
case 'setNodes':
setNodes(data);
return;
}
}
);
return () => {
eventBus.off(EventNameEnum.requestFlowEvent);
};
}, []);
useEffect(() => {
eventBus.on(EventNameEnum.requestFlowStore, () => {
eventBus.emit('receiveFlowStore', {
nodes,
edges,
mode,
filterAppIds,
reactFlowWrapper
});
});
return () => {
eventBus.off(EventNameEnum.requestFlowStore);
};
}, [edges, filterAppIds, mode, nodes]);
const value = {
reactFlowWrapper,
mode,
filterAppIds,
nodes,
setNodes,
onNodesChange,
edges,
setEdges,
onEdgesChange,
onFixView,
onDelNode,
onChangeNode,
onResetNode,
onCopyNode,
onDelEdge,
onDelConnect,
onConnect,
initData,
splitToolInputs,
hasToolNode
};
return <StateContext.Provider value={value}>{children}</StateContext.Provider>;
};
export default React.memo(FlowProvider);
export const onChangeNode = (e: FlowNodeChangeProps) => {
eventBus.emit(EventNameEnum.requestFlowEvent, {
type: 'onChangeNode',
data: e
});
};
export const onCopyNode = (nodeId: string) => {
eventBus.emit(EventNameEnum.requestFlowEvent, {
type: 'onCopyNode',
data: nodeId
});
};
export const onResetNode = (e: Parameters<useFlowProviderStoreType['onResetNode']>[0]) => {
eventBus.emit(EventNameEnum.requestFlowEvent, {
type: 'onResetNode',
data: e
});
};
export const onDelConnect = (e: Parameters<useFlowProviderStoreType['onDelConnect']>[0]) => {
eventBus.emit(EventNameEnum.requestFlowEvent, {
type: 'onDelConnect',
data: e
});
};
export const onSetNodes = (e: useFlowProviderStoreType['nodes']) => {
eventBus.emit(EventNameEnum.requestFlowEvent, {
type: 'setNodes',
data: e
});
};
export const getFlowStore = () =>
new Promise<{
nodes: useFlowProviderStoreType['nodes'];
edges: useFlowProviderStoreType['edges'];
mode: useFlowProviderStoreType['mode'];
filterAppIds: useFlowProviderStoreType['filterAppIds'];
reactFlowWrapper: useFlowProviderStoreType['reactFlowWrapper'];
}>((resolve) => {
eventBus.on('receiveFlowStore', (data: any) => {
resolve(data);
eventBus.off('receiveFlowStore');
});
eventBus.emit(EventNameEnum.requestFlowStore);
});

View File

@@ -1,30 +0,0 @@
import React from 'react';
import { useStoreApi, type ConnectionLineComponentProps } from 'reactflow';
const CustomConnection = ({ fromX, fromY, toX, toY }: ConnectionLineComponentProps) => {
const store = useStoreApi();
const { connectionHandleId } = store.getState();
console.log(fromX, fromY, toX, toY, connectionHandleId);
return (
<g>
<path
fill="none"
stroke={connectionHandleId || ''}
strokeWidth={1.5}
d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`}
/>
<circle
cx={toX}
cy={toY}
fill="#fff"
r={3}
stroke={connectionHandleId || ''}
strokeWidth={1.5}
/>
</g>
);
};
export default CustomConnection;

View File

@@ -1,116 +0,0 @@
import React, { useMemo } from 'react';
import { BezierEdge, getBezierPath, EdgeLabelRenderer, EdgeProps } from 'reactflow';
import { onDelConnect, useFlowProviderStore } from '../../FlowProvider';
import { Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
const ButtonEdge = (props: EdgeProps) => {
const { nodes } = useFlowProviderStore();
const {
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
selected,
sourceHandleId,
animated,
style = {}
} = props;
const active = (() => {
const connectNode = nodes.find((node) => {
return (node.id === props.source || node.id === props.target) && node.selected;
});
return !!(connectNode || selected);
})();
const [, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition
});
const isToolEdge = sourceHandleId === ModuleOutputKeyEnum.selectedTools;
const memoEdgeLabel = useMemo(() => {
return (
<EdgeLabelRenderer>
<Flex
alignItems={'center'}
justifyContent={'center'}
position={'absolute'}
transform={`translate(-50%, -50%) translate(${labelX}px,${labelY}px)`}
pointerEvents={'all'}
w={'20px'}
h={'20px'}
bg={'white'}
borderRadius={'20px'}
color={'black'}
cursor={'pointer'}
borderWidth={'1px'}
borderColor={'borderColor.low'}
zIndex={active ? 1000 : 0}
_hover={{
boxShadow: '0 0 6px 2px rgba(0, 0, 0, 0.08)'
}}
onClick={() => onDelConnect(id)}
>
<MyIcon
name="closeSolid"
w={'100%'}
color={active ? 'primary.700' : 'myGray.400'}
></MyIcon>
</Flex>
{!isToolEdge && (
<Flex
alignItems={'center'}
justifyContent={'center'}
position={'absolute'}
transform={`translate(-78%, -50%) translate(${targetX}px,${targetY}px)`}
pointerEvents={'all'}
w={'16px'}
h={'16px'}
bg={'white'}
zIndex={active ? 1000 : 0}
>
<MyIcon
name={'common/rightArrowLight'}
w={'100%'}
color={active ? 'primary.700' : 'myGray.400'}
></MyIcon>
</Flex>
)}
</EdgeLabelRenderer>
);
}, [labelX, labelY, active, isToolEdge, targetX, targetY, id]);
const memoBezierEdge = useMemo(() => {
const edgeStyle: React.CSSProperties = {
...style,
...(active
? {
strokeWidth: 5,
stroke: '#3370ff'
}
: { strokeWidth: 2, zIndex: 2, stroke: 'myGray.300' })
};
return <BezierEdge {...props} style={edgeStyle} />;
}, [style, active, props]);
return (
<>
{memoBezierEdge}
{memoEdgeLabel}
</>
);
};
export default React.memo(ButtonEdge);

View File

@@ -1,36 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
import { useFlowProviderStore } from '../../FlowProvider';
import Divider from '../modules/Divider';
import RenderToolInput from '../render/RenderToolInput';
import { useTranslation } from 'next-i18next';
const NodeAnswer = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
const { splitToolInputs } = useFlowProviderStore();
const { toolInputs, commonInputs } = splitToolInputs(inputs, moduleId);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
{toolInputs.length > 0 && (
<>
<Divider text={t('core.module.tool.Tool input')} />
<Container>
<RenderToolInput moduleId={moduleId} inputs={toolInputs} />
</Container>
</>
)}
<RenderInput moduleId={moduleId} flowInputList={commonInputs} />
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeAnswer);

View File

@@ -1,149 +0,0 @@
import React, { useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import { Box, Button, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { AddIcon } from '@chakra-ui/icons';
import {
ModuleIOValueTypeEnum,
ModuleInputKeyEnum,
ModuleOutputKeyEnum
} from '@fastgpt/global/core/module/constants';
import { getOneQuoteInputTemplate } from '@fastgpt/global/core/module/template/system/datasetConcat';
import { onChangeNode, useFlowProviderStore } from '../../FlowProvider';
import TargetHandle from '../render/TargetHandle';
import MyIcon from '@fastgpt/web/components/common/Icon';
import SourceHandle from '../render/SourceHandle';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MySlider from '@/components/Slider';
import { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { llmModelList } = useSystemStore();
const { nodes } = useFlowProviderStore();
const { moduleId, inputs, outputs } = data;
const quotes = useMemo(
() => inputs.filter((item) => item.valueType === ModuleIOValueTypeEnum.datasetQuote),
[inputs]
);
const tokenLimit = useMemo(() => {
let maxTokens = 3000;
nodes.forEach((item) => {
if (item.type === FlowNodeTypeEnum.chatNode) {
const model =
item.data.inputs.find((item) => item.key === ModuleInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken =
llmModelList.find((item) => item.model === model)?.quoteMaxToken || 3000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
return maxTokens;
}, [llmModelList, nodes]);
const RenderQuoteList = useMemo(() => {
return (
<Box>
<Box>
{quotes.map((quote, i) => (
<Flex key={quote.key} position={'relative'} mb={4} alignItems={'center'}>
<TargetHandle handleKey={quote.key} valueType={quote.valueType} />
<Box>
{t('core.chat.Quote')}
{i + 1}
</Box>
<MyIcon
ml={2}
w={'14px'}
name={'delete'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'delInput',
key: quote.key
});
}}
/>
</Flex>
))}
</Box>
<Button
leftIcon={<AddIcon />}
variant={'whiteBase'}
onClick={() => {
onChangeNode({
moduleId,
type: 'addInput',
value: getOneQuoteInputTemplate()
});
}}
>
{t('core.module.Dataset quote.Add quote')}
</Button>
</Box>
);
}, [moduleId, quotes, t]);
const CustomComponent = useMemo(() => {
console.log(111);
return {
[ModuleInputKeyEnum.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({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
</Box>
)
};
}, [moduleId, tokenLimit]);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'} position={'relative'}>
<RenderInput moduleId={moduleId} flowInputList={inputs} CustomComponent={CustomComponent} />
{/* render dataset select */}
{RenderQuoteList}
<Flex position={'absolute'} right={4} top={'60%'}>
<Box>{t('core.module.Dataset quote.Concat result')}</Box>
<SourceHandle
handleKey={ModuleOutputKeyEnum.datasetQuoteQA}
valueType={ModuleIOValueTypeEnum.datasetQuote}
// transform={'translate(-14px, -50%)'}
/>
</Flex>
{/* <RenderOutput moduleId={moduleId} flowOutputList={outputs} /> */}
</Container>
</NodeCard>
);
};
export default React.memo(NodeDatasetConcat);

View File

@@ -1,10 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
const NodeEmpty = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
return <NodeCard selected={selected} {...data}></NodeCard>;
};
export default React.memo(NodeEmpty);

View File

@@ -1,649 +0,0 @@
import React, { useCallback, useEffect, useMemo, useState, useTransition } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../../modules/Divider';
import Container from '../../modules/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 { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { onChangeNode, useFlowProviderStore } from '../../../FlowProvider';
import { useTranslation } from 'next-i18next';
import Tabs from '@/components/Tabs';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import JSONEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import {
formatEditorVariablePickerIcon,
getGuideModule,
splitGuideModule
} from '@fastgpt/global/core/module/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';
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({
moduleId,
inputs
}: {
moduleId: string;
inputs: FlowNodeInputItemType[];
}) {
const { t } = useTranslation();
const { toast } = useToast();
const [_, startSts] = useTransition();
const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure();
const requestMethods = inputs.find((item) => item.key === ModuleInputKeyEnum.httpMethod);
const requestUrl = inputs.find((item) => item.key === ModuleInputKeyEnum.httpReqUrl);
const onChangeUrl = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: e.target.value
}
});
},
[moduleId, requestUrl]
);
const onBlurUrl = useCallback(
(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 === ModuleInputKeyEnum.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({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpParams,
value: {
...inputParams,
value: concatParams
}
});
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: url
}
});
toast({
status: 'success',
title: t('core.module.http.Url and params have been split')
});
}
},
[inputs, moduleId, requestUrl, t, toast]
);
return (
<Box>
<Box mb={2} display={'flex'} justifyContent={'space-between'}>
<Box>{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={'myGray.50'}
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({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpMethod,
value: {
...requestMethods,
value: e
}
});
}}
/>
<Input
flex={'1 0 0'}
ml={2}
h={'34px'}
value={requestUrl?.value}
placeholder={t('core.module.input.label.Http Request Url')}
fontSize={'xs'}
onChange={onChangeUrl}
onBlur={onBlurUrl}
/>
</Flex>
{isOpenCurl && <CurlImportModal moduleId={moduleId} inputs={inputs} onClose={onCloseCurl} />}
</Box>
);
});
export function RenderHttpProps({
moduleId,
inputs
}: {
moduleId: string;
inputs: FlowNodeInputItemType[];
}) {
const { t } = useTranslation();
const [selectedTab, setSelectedTab] = useState(TabEnum.params);
const { nodes } = useFlowProviderStore();
const requestMethods = inputs.find((item) => item.key === ModuleInputKeyEnum.httpMethod)?.value;
const params = inputs.find((item) => item.key === ModuleInputKeyEnum.httpParams);
const headers = inputs.find((item) => item.key === ModuleInputKeyEnum.httpHeaders);
const jsonBody = inputs.find((item) => item.key === ModuleInputKeyEnum.httpJsonBody);
const paramsLength = params?.value?.length || 0;
const headersLength = headers?.value?.length || 0;
// get variable
const variables = useMemo(() => {
const globalVariables = formatEditorVariablePickerIcon(
splitGuideModule(getGuideModule(nodes.map((node) => node.data)))?.variableModules || []
);
const systemVariables = [
{
key: 'appId',
label: t('core.module.http.AppId')
},
{
key: 'chatId',
label: t('core.module.http.ChatId')
},
{
key: 'responseChatItemId',
label: t('core.module.http.ResponseChatItemId')
},
{
key: 'variables',
label: t('core.module.http.Variables')
},
{
key: 'histories',
label: t('core.module.http.Histories')
},
{
key: 'cTime',
label: t('core.module.http.Current time')
}
];
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.edit || input.toolDescription)
.map((item) => ({
key: item.key,
label: item.label
}))
);
return [...moduleVariables, ...globalVariables, ...systemVariables];
}, [inputs, nodes, t]);
const variableText = useMemo(() => {
return variables
.map((item) => `${item.key}${item.key !== item.label ? `(${item.label})` : ''}`)
.join('\n');
}, [variables]);
return (
<Box>
<Flex alignItems={'center'} mb={2}>
{t('core.module.Http request props')}
<MyTooltip label={t('core.module.http.Props tip', { variable: variableText })}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</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)}
/>
{params &&
headers &&
jsonBody &&
{
[TabEnum.params]: (
<RenderForm
moduleId={moduleId}
input={params}
variables={variables}
tabType={TabEnum.params}
/>
),
[TabEnum.body]: <RenderJson moduleId={moduleId} variables={variables} input={jsonBody} />,
[TabEnum.headers]: (
<RenderForm
moduleId={moduleId}
input={headers}
variables={variables}
tabType={TabEnum.headers}
/>
)
}[selectedTab]}
</Box>
);
}
const RenderForm = ({
moduleId,
input,
variables,
tabType
}: {
moduleId: string;
input: FlowNodeInputItemType;
variables: EditorVariablePickerType[];
tabType?: TabEnum;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
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({
moduleId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: list
}
});
setShouldUpdateNode(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [list]);
const handleKeyChange = (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);
};
const handleAddNewProps = (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);
};
return (
<TableContainer overflowY={'visible'} overflowX={'unset'}>
<Table>
<Thead>
<Tr>
<Th px={2}>{t('core.module.http.Props name')}</Th>
<Th px={2}>{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>
);
};
const RenderJson = ({
moduleId,
input,
variables
}: {
moduleId: string;
input: FlowNodeInputItemType;
variables: EditorVariablePickerType[];
}) => {
const { t } = useTranslation();
const [_, startSts] = useTransition();
return (
<Box mt={1}>
<JSONEditor
bg={'myGray.50'}
defaultHeight={200}
resize
value={input.value}
placeholder={t('core.module.template.http body placeholder')}
onChange={(e) => {
startSts(() => {
onChangeNode({
moduleId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: e
}
});
});
}}
variables={variables}
/>
</Box>
);
};
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<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
const { splitToolInputs, hasToolNode } = useFlowProviderStore();
const { toolInputs, commonInputs } = splitToolInputs(inputs, moduleId);
const CustomComponents = useMemo(
() => ({
[ModuleInputKeyEnum.httpMethod]: () => (
<RenderHttpMethodAndUrl moduleId={moduleId} inputs={inputs} />
),
[ModuleInputKeyEnum.httpHeaders]: () => (
<>
<RenderHttpProps moduleId={moduleId} inputs={inputs} />
<Box mt={2} transform={'translateY(10px)'}>
{t('core.module.Variable import')}
</Box>
</>
)
}),
[inputs, moduleId, t]
);
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
{hasToolNode && (
<>
<Divider text={t('core.module.tool.Tool input')} />
<Container>
<RenderToolInput moduleId={moduleId} inputs={toolInputs} canEdit />
</Container>
</>
)}
<>
<Divider text={t('common.Input')} />
<Container>
<RenderInput
moduleId={moduleId}
flowInputList={commonInputs}
CustomComponent={CustomComponents}
/>
</Container>
</>
<>
<Divider text={t('common.Output')} />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</>
</NodeCard>
);
};
export default React.memo(NodeHttp);

View File

@@ -1,266 +0,0 @@
import React, { useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import { onChangeNode } from '../../FlowProvider';
import dynamic from 'next/dynamic';
import { Box, Button, Flex } from '@chakra-ui/react';
import { QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import Container from '../modules/Container';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import SourceHandle from '../render/SourceHandle';
import type {
EditInputFieldMap,
EditNodeFieldType,
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/module/node/type.d';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
const FieldEditModal = dynamic(() => import('../render/FieldEditModal'));
const defaultCreateField: EditNodeFieldType = {
label: '',
key: '',
description: '',
inputType: FlowNodeInputTypeEnum.target,
valueType: ModuleIOValueTypeEnum.string,
required: true
};
const createEditField: EditInputFieldMap = {
key: true,
name: true,
description: true,
required: true,
dataType: true,
inputType: true,
isToolInput: true
};
const NodePluginInput = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
const [createField, setCreateField] = useState<EditNodeFieldType>();
const [editField, setEditField] = useState<EditNodeFieldType>();
return (
<NodeCard minW={'300px'} selected={selected} forbidMenu {...data}>
<Container mt={1} borderTop={'2px solid'} borderTopColor={'myGray.300'}>
{inputs.map((item) => (
<Flex
key={item.key}
className="nodrag"
cursor={'default'}
justifyContent={'right'}
alignItems={'center'}
position={'relative'}
mb={7}
>
{item.edit && (
<>
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'primary.500' }}
onClick={() =>
setEditField({
inputType: item.type,
valueType: item.valueType,
key: item.key,
label: item.label,
description: item.description,
required: item.required,
isToolInput: !!item.toolDescription
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'delInput',
key: item.key
});
onChangeNode({
moduleId,
type: 'delOutput',
key: item.key
});
}}
/>
</>
)}
{item.description && (
<MyTooltip label={t(item.description)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
</MyTooltip>
)}
<Box position={'relative'}>
{t(item.label)}
{item.required && (
<Box
position={'absolute'}
right={'-6px'}
top={'-3px'}
color={'red.500'}
fontWeight={'bold'}
>
*
</Box>
)}
</Box>
<SourceHandle handleKey={item.key} valueType={item.valueType} />
</Flex>
))}
<Box textAlign={'right'} mt={5}>
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
onClick={() => {
setCreateField(defaultCreateField);
}}
>
{t('core.module.input.Add Input')}
</Button>
</Box>
</Container>
{!!createField && (
<FieldEditModal
editField={createEditField}
defaultField={createField}
keys={inputs.map((input) => input.key)}
onClose={() => setCreateField(undefined)}
onSubmit={({ data }) => {
onChangeNode({
moduleId,
type: 'addInput',
value: {
key: data.key,
valueType: data.valueType,
label: data.label,
type: data.inputType,
required: data.required,
description: data.description,
toolDescription: data.isToolInput ? data.description : undefined,
edit: true,
editField: createEditField
}
});
onChangeNode({
moduleId,
type: 'addOutput',
value: {
key: data.key,
valueType: data.valueType,
label: data.label,
type: FlowNodeOutputTypeEnum.source,
edit: true,
targets: []
}
});
setCreateField(undefined);
}}
/>
)}
{!!editField?.key && (
<FieldEditModal
editField={createEditField}
defaultField={editField}
keys={[editField.key]}
onClose={() => setEditField(undefined)}
onSubmit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !data.label) return;
// check key valid
const memInput = inputs.find((item) => item.key === editField.key);
const memOutput = outputs.find((item) => item.key === editField.key);
if (!memInput || !memOutput) return setEditField(undefined);
const newInput: FlowNodeInputItemType = {
...memInput,
type: data.inputType,
valueType: data.valueType,
key: data.key,
required: data.required,
label: data.label,
description: data.description,
toolDescription: data.isToolInput ? data.description : undefined,
...(data.inputType === FlowNodeInputTypeEnum.addInputParam
? {
editField: {
key: true,
name: true,
description: true,
required: true,
dataType: true,
inputType: false
},
defaultEditField: {
label: '',
key: '',
description: '',
inputType: FlowNodeInputTypeEnum.target,
valueType: ModuleIOValueTypeEnum.string,
required: true
}
}
: {})
};
const newOutput: FlowNodeOutputItemType = {
...memOutput,
valueType: data.valueType,
key: data.key,
label: data.label
};
console.log(data);
if (changeKey) {
onChangeNode({
moduleId,
type: 'replaceInput',
key: editField.key,
value: newInput
});
onChangeNode({
moduleId,
type: 'replaceOutput',
key: editField.key,
value: newOutput
});
} else {
onChangeNode({
moduleId,
type: 'updateInput',
key: newInput.key,
value: newInput
});
onChangeNode({
moduleId,
type: 'updateOutput',
key: newOutput.key,
value: newOutput
});
}
setEditField(undefined);
}}
/>
)}
</NodeCard>
);
};
export default React.memo(NodePluginInput);

View File

@@ -1,238 +0,0 @@
import React, { useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import { onChangeNode } from '../../FlowProvider';
import dynamic from 'next/dynamic';
import { Box, Button, Flex } from '@chakra-ui/react';
import { QuestionOutlineIcon, SmallAddIcon } from '@chakra-ui/icons';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import Container from '../modules/Container';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import TargetHandle from '../render/TargetHandle';
import { useToast } from '@fastgpt/web/hooks/useToast';
import {
EditNodeFieldType,
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/module/node/type';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
const FieldEditModal = dynamic(() => import('../render/FieldEditModal'));
const defaultCreateField: EditNodeFieldType = {
label: '',
key: '',
description: '',
inputType: FlowNodeInputTypeEnum.target,
valueType: ModuleIOValueTypeEnum.string,
required: true
};
const createEditField = {
key: true,
name: true,
description: true,
required: false,
dataType: true,
inputType: false
};
const NodePluginOutput = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
const [createField, setCreateField] = useState<EditNodeFieldType>();
const [editField, setEditField] = useState<EditNodeFieldType>();
return (
<NodeCard minW={'300px'} selected={selected} forbidMenu {...data}>
<Container mt={1} borderTop={'2px solid'} borderTopColor={'myGray.300'}>
{inputs.map((item) => (
<Flex
key={item.key}
className="nodrag"
cursor={'default'}
justifyContent={'left'}
alignItems={'center'}
position={'relative'}
mb={7}
>
<TargetHandle handleKey={item.key} valueType={item.valueType} />
<Box position={'relative'}>
{t(item.label)}
<Box
position={'absolute'}
right={'-6px'}
top={'-3px'}
color={'red.500'}
fontWeight={'bold'}
>
*
</Box>
</Box>
{item.description && (
<MyTooltip label={t(item.description)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={2} />
</MyTooltip>
)}
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
_hover={{ color: 'primary.500' }}
onClick={() =>
setEditField({
inputType: item.type,
valueType: item.valueType,
key: item.key,
label: item.label,
description: item.description,
required: item.required
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'delInput',
key: item.key,
value: ''
});
onChangeNode({
moduleId,
type: 'delOutput',
key: item.key
});
}}
/>
</Flex>
))}
<Box textAlign={'left'} mt={5}>
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
onClick={() => {
setCreateField(defaultCreateField);
}}
>
{t('core.module.output.Add Output')}
</Button>
</Box>
</Container>
{!!createField && (
<FieldEditModal
editField={createEditField}
defaultField={createField}
keys={inputs.map((input) => input.key)}
onClose={() => setCreateField(undefined)}
onSubmit={({ data }) => {
onChangeNode({
moduleId,
type: 'addInput',
value: {
key: data.key,
valueType: data.valueType,
label: data.label,
type: data.inputType,
required: data.required,
description: data.description,
edit: true,
editField: createEditField
}
});
onChangeNode({
moduleId,
type: 'addOutput',
value: {
key: data.key,
valueType: data.valueType,
label: data.label,
type: FlowNodeOutputTypeEnum.source,
edit: true,
targets: []
}
});
setCreateField(undefined);
}}
/>
)}
{!!editField?.key && (
<FieldEditModal
editField={createEditField}
defaultField={editField}
keys={[editField.key]}
onClose={() => setEditField(undefined)}
onSubmit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !data.label) return;
// check key valid
const memInput = inputs.find((item) => item.key === editField.key);
const memOutput = outputs.find((item) => item.key === editField.key);
if (!memInput || !memOutput) return setEditField(undefined);
const newInput: FlowNodeInputItemType = {
...memInput,
type: data.inputType,
valueType: data.valueType,
key: data.key,
required: data.required,
label: data.label,
description: data.description
};
const newOutput: FlowNodeOutputItemType = {
...memOutput,
valueType: data.valueType,
key: data.key,
label: data.label
};
if (changeKey) {
onChangeNode({
moduleId,
type: 'replaceInput',
key: editField.key,
value: newInput
});
onChangeNode({
moduleId,
type: 'replaceOutput',
key: editField.key,
value: newOutput
});
} else {
onChangeNode({
moduleId,
type: 'updateInput',
key: newInput.key,
value: newInput
});
onChangeNode({
moduleId,
type: 'updateOutput',
key: newOutput.key,
value: newOutput
});
}
setEditField(undefined);
}}
/>
)}
</NodeCard>
);
};
export default React.memo(NodePluginOutput);

View File

@@ -1,21 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Container from '../modules/Container';
import RenderOutput from '../render/RenderOutput';
const QuestionInputNode = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const { moduleId, outputs } = data;
return (
<NodeCard minW={'240px'} selected={selected} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'} textAlign={'end'}>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(QuestionInputNode);

View File

@@ -1,59 +0,0 @@
import React, { useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
import RenderToolInput from '../render/RenderToolInput';
import { useTranslation } from 'next-i18next';
import { useFlowProviderStore } from '../../FlowProvider';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
const NodeSimple = ({
data,
selected,
minW = '350px',
maxW
}: NodeProps<FlowModuleItemType> & { minW?: string | number; maxW?: string | number }) => {
const { t } = useTranslation();
const { splitToolInputs } = useFlowProviderStore();
const { moduleId, inputs, outputs } = data;
const { toolInputs, commonInputs } = splitToolInputs(inputs, moduleId);
const filterHiddenInputs = useMemo(
() => commonInputs.filter((item) => item.type !== 'hidden'),
[commonInputs]
);
return (
<NodeCard minW={minW} maxW={maxW} selected={selected} {...data}>
{toolInputs.length > 0 && (
<>
<Divider text={t('core.module.tool.Tool input')} />
<Container>
<RenderToolInput moduleId={moduleId} inputs={toolInputs} />
</Container>
</>
)}
{filterHiddenInputs.length > 0 && (
<>
<Divider text={t('common.Input')} />
<Container>
<RenderInput moduleId={moduleId} flowInputList={commonInputs} />
</Container>
</>
)}
{outputs.filter((output) => output.type !== FlowNodeOutputTypeEnum.hidden).length > 0 && (
<>
<Divider text={t('common.Output')} />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</>
)}
</NodeCard>
);
};
export default React.memo(NodeSimple);

View File

@@ -1,33 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
import { useTranslation } from 'next-i18next';
import { ToolSourceHandle } from '../render/ToolHandle';
import { Box } from '@chakra-ui/react';
const NodeTools = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<Divider text={t('common.Input')} />
<Container>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
</Container>
<Box position={'relative'}>
<Box borderBottomLeftRadius={'md'} borderBottomRadius={'md'} overflow={'hidden'}>
<Divider showBorderBottom={false} text={t('core.module.template.Tool module')} />
</Box>
<ToolSourceHandle moduleId={moduleId} />
</Box>
</NodeCard>
);
};
export default React.memo(NodeTools);

View File

@@ -1,194 +0,0 @@
import React, { useCallback, useMemo, useTransition } from 'react';
import { NodeProps } from 'reactflow';
import { Box, Flex, Textarea, useTheme } from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { FlowModuleItemType, ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { welcomeTextTip } from '@fastgpt/global/core/module/template/tip';
import { onChangeNode } from '../../FlowProvider';
import VariableEdit from '../../../../app/VariableEdit';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import Container from '../modules/Container';
import NodeCard from '../render/NodeCard';
import type { VariableItemType } from '@fastgpt/global/core/app/type.d';
import QGSwitch from '@/components/core/app/QGSwitch';
import TTSSelect from '@/components/core/app/TTSSelect';
import WhisperConfig from '@/components/core/app/WhisperConfig';
import { splitGuideModule } from '@fastgpt/global/core/module/utils';
import { useTranslation } from 'next-i18next';
import { TTSTypeEnum } from '@/constants/app';
const NodeUserGuide = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const theme = useTheme();
return (
<>
<NodeCard minW={'300px'} selected={selected} forbidMenu {...data}>
<Container className="nodrag" borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<WelcomeText data={data} />
<Box pt={4} pb={2}>
<ChatStartVariable data={data} />
</Box>
<Box pt={3} borderTop={theme.borders.base}>
<TTSGuide data={data} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<WhisperGuide data={data} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<QuestionGuide data={data} />
</Box>
</Container>
</NodeCard>
</>
);
};
export default React.memo(NodeUserGuide);
function WelcomeText({ data }: { data: FlowModuleItemType }) {
const { t } = useTranslation();
const { inputs, moduleId } = data;
const [, startTst] = useTransition();
const welcomeText = inputs.find((item) => item.key === ModuleInputKeyEnum.welcomeText);
return (
<>
<Flex mb={1} alignItems={'center'}>
<MyIcon name={'core/modules/welcomeText'} mr={2} w={'16px'} color={'#E74694'} />
<Box>{t('core.app.Welcome Text')}</Box>
<MyTooltip label={t(welcomeTextTip)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
</Flex>
{welcomeText && (
<Textarea
className="nodrag"
rows={6}
resize={'both'}
defaultValue={welcomeText.value}
bg={'myWhite.500'}
placeholder={t(welcomeTextTip)}
onChange={(e) => {
startTst(() => {
onChangeNode({
moduleId,
key: ModuleInputKeyEnum.welcomeText,
type: 'updateInput',
value: {
...welcomeText,
value: e.target.value
}
});
});
}}
/>
)}
</>
);
}
function ChatStartVariable({ data }: { data: FlowModuleItemType }) {
const { inputs, moduleId } = data;
const variables = useMemo(
() =>
(inputs.find((item) => item.key === ModuleInputKeyEnum.variables)
?.value as VariableItemType[]) || [],
[inputs]
);
const updateVariables = useCallback(
(value: VariableItemType[]) => {
onChangeNode({
moduleId,
key: ModuleInputKeyEnum.variables,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === ModuleInputKeyEnum.variables),
value
}
});
},
[inputs, moduleId]
);
return <VariableEdit variables={variables} onChange={(e) => updateVariables(e)} />;
}
function QuestionGuide({ data }: { data: FlowModuleItemType }) {
const { inputs, moduleId } = data;
const questionGuide = useMemo(
() =>
(inputs.find((item) => item.key === ModuleInputKeyEnum.questionGuide)?.value as boolean) ||
false,
[inputs]
);
return (
<QGSwitch
isChecked={questionGuide}
size={'lg'}
onChange={(e) => {
const value = e.target.checked;
onChangeNode({
moduleId,
key: ModuleInputKeyEnum.questionGuide,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === ModuleInputKeyEnum.questionGuide),
value
}
});
}}
/>
);
}
function TTSGuide({ data }: { data: FlowModuleItemType }) {
const { inputs, moduleId } = data;
const { ttsConfig } = splitGuideModule({ inputs } as ModuleItemType);
return (
<TTSSelect
value={ttsConfig}
onChange={(e) => {
onChangeNode({
moduleId,
key: ModuleInputKeyEnum.tts,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === ModuleInputKeyEnum.tts),
value: e
}
});
}}
/>
);
}
function WhisperGuide({ data }: { data: FlowModuleItemType }) {
const { inputs, moduleId } = data;
const { ttsConfig, whisperConfig } = splitGuideModule({ inputs } as ModuleItemType);
return (
<WhisperConfig
isOpenAudio={ttsConfig.type !== TTSTypeEnum.none}
value={whisperConfig}
onChange={(e) => {
onChangeNode({
moduleId,
key: ModuleInputKeyEnum.whisper,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === ModuleInputKeyEnum.whisper),
value: e
}
});
}}
/>
);
}

View File

@@ -1,306 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import {
Box,
Button,
ModalFooter,
ModalBody,
Flex,
Switch,
Input,
Textarea
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { DYNAMIC_INPUT_KEY, ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/module/node/constant';
import { EditInputFieldMap, EditNodeFieldType } from '@fastgpt/global/core/module/node/type.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MySelect from '@fastgpt/web/components/common/MySelect';
const FieldEditModal = ({
editField = {
key: true,
name: true,
description: true,
dataType: true
},
defaultField,
keys = [],
onClose,
onSubmit
}: {
editField?: EditInputFieldMap;
defaultField: EditNodeFieldType;
keys: string[];
onClose: () => void;
onSubmit: (e: { data: EditNodeFieldType; changeKey: boolean }) => void;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const isCreate = useMemo(() => !defaultField.key, [defaultField.key]);
const showDynamicInputSelect =
!keys.includes(DYNAMIC_INPUT_KEY) || defaultField.key === DYNAMIC_INPUT_KEY;
const inputTypeList = [
{
label: t('core.module.inputType.target'),
value: FlowNodeInputTypeEnum.target,
valueType: ModuleIOValueTypeEnum.string
},
{
label: t('core.module.inputType.input'),
value: FlowNodeInputTypeEnum.input,
valueType: ModuleIOValueTypeEnum.string
},
{
label: t('core.module.inputType.textarea'),
value: FlowNodeInputTypeEnum.textarea,
valueType: ModuleIOValueTypeEnum.string
},
{
label: t('core.module.inputType.switch'),
value: FlowNodeInputTypeEnum.switch,
valueType: ModuleIOValueTypeEnum.boolean
},
{
label: t('core.module.inputType.selectDataset'),
value: FlowNodeInputTypeEnum.selectDataset,
valueType: ModuleIOValueTypeEnum.selectDataset
},
...(showDynamicInputSelect
? [
{
label: t('core.module.inputType.dynamicTargetInput'),
value: FlowNodeInputTypeEnum.addInputParam,
valueType: ModuleIOValueTypeEnum.any
}
]
: [])
];
const dataTypeSelectList = Object.values(FlowValueTypeMap)
.slice(0, -2)
.map((item) => ({
label: t(item.label),
value: item.value
}));
const { register, getValues, setValue, handleSubmit, watch } = useForm<EditNodeFieldType>({
defaultValues: defaultField
});
const inputType = watch('inputType');
const outputType = watch('outputType');
const required = watch('required');
const [refresh, setRefresh] = useState(false);
const showDataTypeSelect = useMemo(() => {
if (!editField.dataType) return false;
if (inputType === undefined) return true;
if (inputType === FlowNodeInputTypeEnum.target) return true;
if (outputType === FlowNodeOutputTypeEnum.source) return true;
return false;
}, [editField.dataType, inputType, outputType]);
const showRequired = useMemo(() => {
if (inputType === FlowNodeInputTypeEnum.addInputParam) return false;
return editField.required || editField.defaultValue;
}, [editField.defaultValue, editField.required, inputType]);
const showNameInput = useMemo(() => {
return editField.name;
}, [editField.name]);
const showKeyInput = useMemo(() => {
if (inputType === FlowNodeInputTypeEnum.addInputParam) return false;
return editField.key;
}, [editField.key, inputType]);
const showDescriptionInput = useMemo(() => {
return editField.description;
}, [editField.description]);
const onSubmitSuccess = useCallback(
(data: EditNodeFieldType) => {
if (!data.key) return;
if (isCreate && keys.includes(data.key)) {
return toast({
status: 'warning',
title: t('core.module.edit.Field Already Exist')
});
}
onSubmit({
data,
changeKey: !keys.includes(data.key)
});
},
[isCreate, keys, onSubmit, 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/module/extract.png"
title={t('core.module.edit.Field Edit')}
onClose={onClose}
>
<ModalBody overflow={'visible'}>
{/* input type select: target, input, textarea.... */}
{editField.inputType && (
<Flex alignItems={'center'} mb={5}>
<Box flex={'0 0 70px'}>{t('core.module.Input Type')}</Box>
<MySelect
w={'288px'}
list={inputTypeList}
value={getValues('inputType')}
onchange={(e: string) => {
const type = e as `${FlowNodeInputTypeEnum}`;
const selectedItem = inputTypeList.find((item) => item.value === type);
setValue('inputType', type);
setValue('valueType', selectedItem?.valueType);
if (type === FlowNodeInputTypeEnum.selectDataset) {
setValue('label', selectedItem?.label);
} else if (type === FlowNodeInputTypeEnum.addInputParam) {
setValue('label', t('core.module.valueType.dynamicTargetInput'));
setValue('key', DYNAMIC_INPUT_KEY);
setValue('required', false);
}
setRefresh(!refresh);
}}
/>
</Flex>
)}
{showRequired && (
<Flex alignItems={'center'} mb={5}>
<Box flex={'0 0 70px'}>{t('common.Require Input')}</Box>
<Switch
{...register('required', {
onChange(e) {
if (!e.target.checked) {
setValue('defaultValue', '');
}
}
})}
/>
</Flex>
)}
{showRequired && required && editField.defaultValue && (
<Flex alignItems={'center'} mb={5}>
<Box flex={['0 0 70px']}>{t('core.module.Default value')}</Box>
<Input
bg={'myGray.50'}
placeholder={t('core.module.Default value placeholder')}
{...register('defaultValue')}
/>
</Flex>
)}
{editField.isToolInput && (
<Flex alignItems={'center'} mb={5}>
<Box flex={'0 0 70px'}></Box>
<Switch {...register('isToolInput')} />
</Flex>
)}
{showDataTypeSelect && (
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Data Type')}</Box>
<MySelect
w={'288px'}
list={dataTypeSelectList}
value={getValues('valueType')}
onchange={(e: string) => {
const type = e as `${ModuleIOValueTypeEnum}`;
setValue('valueType', type);
if (
type === ModuleIOValueTypeEnum.chatHistory ||
type === ModuleIOValueTypeEnum.datasetQuote
) {
const label = dataTypeSelectList.find((item) => item.value === type)?.label;
setValue('label', label);
}
setRefresh(!refresh);
}}
/>
</Flex>
)}
{showNameInput && (
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Field Name')}</Box>
<Input
bg={'myGray.50'}
placeholder="预约字段/sql语句……"
{...register('label', { required: true })}
/>
</Flex>
)}
{showKeyInput && (
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Field key')}</Box>
<Input
bg={'myGray.50'}
placeholder="appointment/sql"
{...register('key', {
required: true,
onChange: (e) => {
const value = e.target.value;
// auto fill label
if (!showNameInput) {
setValue('label', value);
}
}
})}
/>
</Flex>
)}
{showDescriptionInput && (
<Box mb={5} alignItems={'flex-start'}>
<Box flex={'0 0 70px'} mb={'1px'}>
{t('core.module.Field Description')}
</Box>
<Textarea
bg={'myGray.50'}
placeholder={t('common.choosable')}
rows={5}
{...register('description')}
/>
</Box>
)}
</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);

View File

@@ -1,270 +0,0 @@
import React, { useMemo } from 'react';
import { Box, Button, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import type { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import { useTranslation } from 'next-i18next';
import { useEditTitle } from '@/web/common/hooks/useEditTitle';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { onChangeNode, onCopyNode, onResetNode, useFlowProviderStore } from '../../FlowProvider';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { getPreviewPluginModule } from '@/web/core/plugin/api';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
import { ToolTargetHandle } from './ToolHandle';
import { useEditTextarea } from '@fastgpt/web/hooks/useEditTextarea';
import TriggerAndFinish from './RenderInput/templates/TriggerAndFinish';
type Props = FlowModuleItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
minW?: string | number;
maxW?: string | number;
forbidMenu?: boolean;
selected?: boolean;
};
const NodeCard = (props: Props) => {
const { t } = useTranslation();
const {
children,
avatar = LOGO_ICON,
name = t('core.module.template.UnKnow Module'),
intro,
minW = '300px',
maxW = '600px',
moduleId,
flowType,
inputs,
selected,
forbidMenu,
isTool = false
} = props;
const { toast } = useToast();
const { setLoading } = useSystemStore();
const { nodes, splitToolInputs, onDelNode } = useFlowProviderStore();
// edit intro
const { onOpenModal: onOpenIntroModal, EditModal: EditIntroModal } = useEditTextarea({
title: t('core.module.Edit intro'),
tip: '调整该模块会对工具调用时机有影响。\n你可以通过精确的描述该模块功能引导模型进行工具调用。',
canEmpty: false
});
// custom title edit
const { onOpenModal, EditModal: EditTitleModal } = useEditTitle({
title: t('common.Custom Title'),
placeholder: t('app.module.Custom Title Tip') || ''
});
const { openConfirm: onOpenConfirmSync, ConfirmModal: ConfirmSyncModal } = useConfirm({
content: t('module.Confirm Sync Plugin')
});
const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({
content: t('core.module.Confirm Delete Node'),
type: 'delete'
});
const showToolHandle = useMemo(
() => isTool && !!nodes.find((item) => item.data?.flowType === FlowNodeTypeEnum.tools),
[isTool, nodes]
);
const moduleIsTool = useMemo(() => {
const { isTool } = splitToolInputs([], moduleId);
return isTool;
}, [moduleId, splitToolInputs]);
const Header = useMemo(() => {
const menuList = [
...(flowType === FlowNodeTypeEnum.pluginModule
? [
{
icon: 'common/refreshLight',
label: t('plugin.Synchronous version'),
variant: 'whiteBase',
onClick: () => {
const pluginId = inputs.find(
(item) => item.key === ModuleInputKeyEnum.pluginId
)?.value;
if (!pluginId) return;
onOpenConfirmSync(async () => {
try {
setLoading(true);
const pluginModule = await getPreviewPluginModule(pluginId);
onResetNode({
id: moduleId,
module: pluginModule
});
} catch (e) {
return toast({
status: 'error',
title: getErrText(e, t('plugin.Get Plugin Module Detail Failed'))
});
}
setLoading(false);
})();
}
}
]
: [
{
icon: 'edit',
label: t('common.Rename'),
variant: 'whiteBase',
onClick: () =>
onOpenModal({
defaultVal: name,
onSuccess: (e) => {
if (!e) {
return toast({
title: t('app.modules.Title is required'),
status: 'warning'
});
}
onChangeNode({
moduleId,
type: 'attr',
key: 'name',
value: e
});
}
})
}
]),
{
icon: 'copy',
label: t('common.Copy'),
variant: 'whiteBase',
onClick: () => onCopyNode(moduleId)
},
{
icon: 'delete',
label: t('common.Delete'),
variant: 'whiteDanger',
onClick: onOpenConfirmDeleteNode(() => onDelNode(moduleId))
}
];
return (
<Box className="custom-drag-handle" px={4} py={3} position={'relative'}>
{showToolHandle && <ToolTargetHandle moduleId={moduleId} />}
<Flex alignItems={'center'}>
<Avatar src={avatar} borderRadius={'0'} objectFit={'contain'} w={'30px'} h={'30px'} />
<Box ml={3} fontSize={'lg'}>
{t(name)}
</Box>
</Flex>
{!forbidMenu && (
<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={'12px'} />}
onClick={item.onClick}
>
{item.label}
</Button>
</Box>
))}
</Box>
)}
<Flex alignItems={'flex-end'} py={1}>
<Box fontSize={'xs'} color={'myGray.600'} flex={'1 0 0'}>
{t(intro)}
</Box>
{moduleIsTool && (
<Button
size={'xs'}
variant={'whiteBase'}
onClick={() => {
onOpenIntroModal({
defaultVal: intro,
onSuccess(e) {
onChangeNode({
moduleId,
type: 'attr',
key: 'intro',
value: e
});
}
});
}}
>
{t('core.module.Edit intro')}
</Button>
)}
</Flex>
{/* switch */}
<TriggerAndFinish moduleId={moduleId} isTool={moduleIsTool} />
</Box>
);
}, [
flowType,
t,
onOpenConfirmDeleteNode,
showToolHandle,
moduleId,
avatar,
name,
forbidMenu,
intro,
moduleIsTool,
inputs,
onOpenConfirmSync,
setLoading,
toast,
onOpenModal,
onDelNode,
onOpenIntroModal
]);
const RenderModal = useMemo(() => {
return (
<>
<EditTitleModal maxLength={20} />
{moduleIsTool && <EditIntroModal maxLength={500} />}
<ConfirmSyncModal />
<ConfirmDeleteModal />
</>
);
}, [ConfirmDeleteModal, ConfirmSyncModal, EditIntroModal, EditTitleModal, moduleIsTool]);
return (
<Box
minW={minW}
maxW={maxW}
bg={'white'}
borderWidth={'1px'}
borderColor={selected ? 'primary.600' : 'borderColor.base'}
borderRadius={'md'}
boxShadow={'1'}
_hover={{
boxShadow: '4',
'& .controller-menu': {
display: 'flex'
}
}}
>
{Header}
<Box className="nowheel">{children}</Box>
{RenderModal}
</Box>
);
};
export default React.memo(NodeCard);

View File

@@ -1,147 +0,0 @@
import { EditNodeFieldType, FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { onChangeNode, useFlowProviderStoreType } from '../../../FlowProvider';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { Box, Flex } from '@chakra-ui/react';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import TargetHandle from '../TargetHandle';
import MyIcon from '@fastgpt/web/components/common/Icon';
import dynamic from 'next/dynamic';
const FieldEditModal = dynamic(() => import('../FieldEditModal'));
type Props = FlowNodeInputItemType & {
moduleId: string;
inputKey: string;
mode: useFlowProviderStoreType['mode'];
};
const InputLabel = ({ moduleId, inputKey, mode, ...item }: Props) => {
const { t } = useTranslation();
const {
required = false,
description,
edit,
label,
type,
valueType,
showTargetInApp,
showTargetInPlugin
} = item;
const [editField, setEditField] = useState<EditNodeFieldType>();
const targetHandle = useMemo(() => {
if (type === FlowNodeInputTypeEnum.target) return true;
if (mode === 'app' && showTargetInApp) return true;
if (mode === 'plugin' && showTargetInPlugin) return true;
return false;
}, [mode, showTargetInApp, showTargetInPlugin, type]);
return (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Box position={'relative'}>
{t(label)}
{description && (
<MyTooltip label={t(description)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
)}
{required && (
<Box
position={'absolute'}
top={'-2px'}
right={'-8px'}
color={'red.500'}
fontWeight={'bold'}
>
*
</Box>
)}
</Box>
{targetHandle && <TargetHandle handleKey={inputKey} valueType={valueType} />}
{edit && (
<>
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
_hover={{ color: 'primary.500' }}
onClick={() =>
setEditField({
inputType: type,
valueType: valueType,
key: inputKey,
required,
label,
description
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'delInput',
key: inputKey,
value: ''
});
}}
/>
</>
)}
{!!editField?.key && (
<FieldEditModal
editField={item.editField}
keys={[editField.key]}
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !data.label) return;
const newInput: FlowNodeInputItemType = {
...item,
type: data.inputType,
valueType: data.valueType,
key: data.key,
required: data.required,
label: data.label,
description: data.description
};
if (changeKey) {
onChangeNode({
moduleId,
type: 'replaceInput',
key: editField.key,
value: newInput
});
} else {
onChangeNode({
moduleId,
type: 'updateInput',
key: newInput.key,
value: newInput
});
}
setEditField(undefined);
}}
/>
)}
</Flex>
);
};
export default React.memo(InputLabel);

View File

@@ -1,57 +0,0 @@
import React, { useState } from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { Button } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next';
import { EditNodeFieldType } from '@fastgpt/global/core/module/node/type';
import dynamic from 'next/dynamic';
const FieldEditModal = dynamic(() => import('../../FieldEditModal'));
const AddInputParam = ({ inputs = [], item, moduleId }: RenderInputProps) => {
const { t } = useTranslation();
const [editField, setEditField] = useState<EditNodeFieldType>();
return (
<>
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
onClick={() => {
setEditField(item.defaultEditField || {});
}}
>
{t('core.module.input.Add Input')}
</Button>
{!!editField && (
<FieldEditModal
editField={item.editField}
defaultField={editField}
keys={inputs.map((input) => input.key)}
onClose={() => setEditField(undefined)}
onSubmit={({ data }) => {
onChangeNode({
moduleId,
type: 'addInput',
key: data.key,
value: {
key: data.key,
valueType: data.valueType,
label: data.label,
type: data.inputType,
required: data.required,
description: data.description,
edit: true,
editField: item.editField
}
});
setEditField(undefined);
}}
/>
)}
</>
);
};
export default React.memo(AddInputParam);

View File

@@ -1,39 +0,0 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import {
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper
} from '@chakra-ui/react';
import { onChangeNode } from '../../../../FlowProvider';
const NumberInputRender = ({ item, moduleId }: RenderInputProps) => {
return (
<NumberInput
defaultValue={item.value}
min={item.min}
max={item.max}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: Number(e)
}
});
}}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
};
export default React.memo(NumberInputRender);

View File

@@ -1,27 +0,0 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import MySelect from '@fastgpt/web/components/common/MySelect';
const SelectRender = ({ item, moduleId }: RenderInputProps) => {
return (
<MySelect
width={'100%'}
value={item.value}
list={item.list || []}
onchange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
);
};
export default React.memo(SelectRender);

View File

@@ -1,175 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { getFlowStore, onChangeNode, useFlowProviderStoreType } from '../../../../FlowProvider';
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/module/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 { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon';
const DatasetSelectModal = dynamic(() => import('@/components/core/module/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/module/DatasetParamsModal'));
const SelectDatasetRender = ({ inputs = [], item, moduleId }: RenderInputProps) => {
const { t } = useTranslation();
const theme = useTheme();
const { llmModelList } = useSystemStore();
const [nodes, setNodes] = useState<useFlowProviderStoreType['nodes']>([]);
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]);
const tokenLimit = useMemo(() => {
let maxTokens = 3000;
nodes.forEach((item) => {
if (item.type === FlowNodeTypeEnum.chatNode) {
const model =
item.data.inputs.find((item) => item.key === ModuleInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken =
llmModelList.find((item) => item.model === model)?.quoteMaxToken || 3000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
return maxTokens;
}, [nodes]);
const {
isOpen: isOpenDatasetPrams,
onOpen: onOpenDatasetParams,
onClose: onCloseDatasetParams
} = useDisclosure();
useQuery(['loadAllDatasets'], loadAllDatasets);
useEffect(() => {
inputs.forEach((input) => {
// @ts-ignore
if (data[input.key] !== undefined) {
setData((state) => ({
...state,
[input.key]: input.value
}));
}
});
}, [inputs]);
useEffect(() => {
async () => {
const { nodes } = await getFlowStore();
setNodes(nodes);
};
}, []);
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>
{/* <Button
h={'36px'}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'common/settingLight'} w={'14px'} />}
onClick={onOpenDatasetParams}
>
{t('core.dataset.search.Params Setting')}
</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', 'xl']}
>
{item.name}
</Box>
</Flex>
))}
</Grid>
{isOpenDatasetSelect && (
<DatasetSelectModal
isOpen={isOpenDatasetSelect}
defaultSelectedDatasets={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
key: item.key,
type: 'updateInput',
value: {
...item,
value: e
}
});
}}
onClose={onCloseDatasetSelect}
/>
)}
{/* {isOpenDatasetPrams && (
<DatasetParamsModal
{...data}
maxTokens={tokenLimit}
onClose={onCloseDatasetParams}
onSuccess={(e) => {
for (let key in e) {
const item = inputs.find((input) => input.key === key);
if (!item) continue;
onChangeNode({
moduleId,
type: 'updateInput',
key,
value: {
...item,
//@ts-ignore
value: e[key]
}
});
}
}}
/>
)} */}
</>
);
};
export default React.memo(SelectDatasetRender);

View File

@@ -1,54 +0,0 @@
import React, { useCallback, useEffect } from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { llmModelTypeFilterMap } from '@fastgpt/global/core/ai/constants';
import AIModelSelector from '@/components/Select/AIModelSelector';
const SelectAiModelRender = ({ item, moduleId }: RenderInputProps) => {
const { llmModelList } = useSystemStore();
const modelList = llmModelList.filter((model) => {
if (!item.llmModelType) return true;
const filterField = llmModelTypeFilterMap[item.llmModelType];
if (!filterField) return true;
//@ts-ignore
return !!model[filterField];
});
const onChangeModel = useCallback(
(e: string) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
},
[item, moduleId]
);
useEffect(() => {
if (!item.value && modelList.length > 0) {
onChangeModel(modelList[0].model);
}
}, [item.value, modelList, onChangeModel]);
return (
<AIModelSelector
minW={'350px'}
width={'100%'}
value={item.value}
list={modelList.map((item) => ({
value: item.model,
label: item.name
}))}
onchange={onChangeModel}
/>
);
};
export default React.memo(SelectAiModelRender);

View File

@@ -1,49 +0,0 @@
import React, { useCallback } from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { SettingAIDataType } from '@fastgpt/global/core/module/node/type';
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
const SelectAiModelRender = ({ item, inputs = [], moduleId }: RenderInputProps) => {
const onChangeModel = useCallback(
(e: SettingAIDataType) => {
for (const key in e) {
const input = inputs.find((input) => input.key === key);
input &&
onChangeNode({
moduleId,
type: 'updateInput',
key,
value: {
...input,
// @ts-ignore
value: e[key]
}
});
}
},
[inputs, moduleId]
);
const llmModelData: SettingAIDataType = {
model: inputs.find((input) => input.key === ModuleInputKeyEnum.aiModel)?.value ?? '',
maxToken:
inputs.find((input) => input.key === ModuleInputKeyEnum.aiChatMaxToken)?.value ?? 2048,
temperature:
inputs.find((input) => input.key === ModuleInputKeyEnum.aiChatTemperature)?.value ?? 1,
isResponseAnswerText: inputs.find(
(input) => input.key === ModuleInputKeyEnum.aiChatIsResponseText
)?.value
};
return (
<SettingLLMModel
llmModelType={item.llmModelType}
defaultData={llmModelData}
onChange={onChangeModel}
/>
);
};
export default React.memo(SelectAiModelRender);

View File

@@ -1,243 +0,0 @@
import React, { useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { Box, BoxProps, Button, Flex, ModalFooter, useDisclosure } from '@chakra-ui/react';
import { onChangeNode, useFlowProviderStore } from '../../../../FlowProvider';
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 {
formatEditorVariablePickerIcon,
getGuideModule,
splitGuideModule
} from '@fastgpt/global/core/module/utils';
import { ModalBody } from '@chakra-ui/react';
import MyTooltip from '@/components/MyTooltip';
import {
Prompt_QuotePromptList,
Prompt_QuoteTemplateList
} from '@fastgpt/global/core/ai/prompt/AIChat';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import PromptTemplate from '@/components/PromptTemplate';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
const SettingQuotePrompt = ({ inputs = [], moduleId }: RenderInputProps) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const { nodes } = useFlowProviderStore();
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 variables = useMemo(() => {
const globalVariables = formatEditorVariablePickerIcon(
splitGuideModule(getGuideModule(nodes.map((node) => node.data)))?.variableModules || []
);
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.edit)
.map((item) => ({
key: item.key,
label: item.label
}))
);
const systemVariables = [
{
key: 'cTime',
label: t('core.module.http.Current time')
}
];
return [...globalVariables, ...moduleVariables, ...systemVariables];
}, [inputs, t]);
const [selectTemplateData, setSelectTemplateData] = useState<{
title: string;
templates: PromptTemplateItem[];
}>();
const quoteTemplateVariables = (() => [
{
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
])();
const quotePromptVariables = (() => [
{
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
])();
const LabelStyles: BoxProps = {
fontSize: ['sm', 'md']
};
const selectTemplateBtn: BoxProps = {
color: 'primary.500',
cursor: 'pointer'
};
const onSubmit = (data: { quoteTemplate: string; quotePrompt: string }) => {
const quoteTemplateInput = inputs.find(
(input) => input.key === ModuleInputKeyEnum.aiChatQuoteTemplate
);
const quotePromptInput = inputs.find(
(input) => input.key === ModuleInputKeyEnum.aiChatQuotePrompt
);
if (quoteTemplateInput) {
onChangeNode({
moduleId,
type: 'updateInput',
key: quoteTemplateInput.key,
value: {
...quoteTemplateInput,
value: data.quoteTemplate
}
});
}
if (quotePromptInput) {
onChangeNode({
moduleId,
type: 'updateInput',
key: quotePromptInput.key,
value: {
...quotePromptInput,
value: data.quotePrompt
}
});
}
onClose();
};
return (
<>
<Button variant={'whitePrimary'} size={'sm'} onClick={onOpen}>
{t('core.module.Setting quote prompt')}
</Button>
<MyModal
isOpen={isOpen}
iconSrc={'modal/edit'}
title={t('core.module.Quote prompt setting')}
w={'600px'}
>
<ModalBody>
<Box>
<Flex {...LabelStyles} mb={1}>
{t('core.app.Quote templates')}
<MyTooltip
label={t('template.Quote Content Tip', {
default: Prompt_QuoteTemplateList[0].value
})}
forceShow
>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
<Box flex={1} />
<Box
{...selectTemplateBtn}
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}>
{t('core.app.Quote prompt')}
<MyTooltip
label={t('template.Quote Prompt Tip', { default: Prompt_QuotePromptList[0].value })}
forceShow
>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
</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);
}}
/>
)}
</>
);
};
export default React.memo(SettingQuotePrompt);

View File

@@ -1,35 +0,0 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { useTranslation } from 'next-i18next';
import { Box } from '@chakra-ui/react';
import MySlider from '@/components/Slider';
const SliderRender = ({ item, moduleId }: RenderInputProps) => {
const { t } = useTranslation();
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({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
</Box>
);
};
export default React.memo(SliderRender);

View File

@@ -1,26 +0,0 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import { Switch } from '@chakra-ui/react';
import { onChangeNode } from '../../../../FlowProvider';
const SwitchRender = ({ item, moduleId }: RenderInputProps) => {
return (
<Switch
size={'lg'}
isChecked={item.value}
onChange={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e.target.checked
}
});
}}
/>
);
};
export default React.memo(SwitchRender);

View File

@@ -1,26 +0,0 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import { Input } from '@chakra-ui/react';
import { onChangeNode } from '../../../../FlowProvider';
const TextInput = ({ item, moduleId }: RenderInputProps) => {
return (
<Input
placeholder={item.placeholder}
defaultValue={item.value}
onBlur={(e) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e.target.value
}
});
}}
/>
);
};
export default React.memo(TextInput);

View File

@@ -1,66 +0,0 @@
import React, { useCallback, useMemo, useTransition } from 'react';
import type { RenderInputProps } from '../type';
import { useFlowProviderStore, onChangeNode } from '../../../../FlowProvider';
import { useTranslation } from 'next-i18next';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import {
formatEditorVariablePickerIcon,
getGuideModule,
splitGuideModule
} from '@fastgpt/global/core/module/utils';
const TextareaRender = ({ inputs = [], item, moduleId }: RenderInputProps) => {
const { t } = useTranslation();
const { nodes } = useFlowProviderStore();
// get variable
const variables = useMemo(() => {
const globalVariables = formatEditorVariablePickerIcon(
splitGuideModule(getGuideModule(nodes.map((node) => node.data)))?.variableModules || []
);
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.edit)
.map((item) => ({
key: item.key,
label: item.label
}))
);
const systemVariables = [
{
key: 'cTime',
label: t('core.module.http.Current time')
}
];
return [...globalVariables, ...moduleVariables, ...systemVariables];
}, [inputs, nodes, t]);
const onChange = useCallback(
(e: string) => {
onChangeNode({
moduleId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
},
[item, moduleId]
);
return (
<PromptEditor
variables={variables}
title={t(item.label)}
h={150}
placeholder={t(item.placeholder || '')}
value={item.value}
onChange={onChange}
/>
);
};
export default React.memo(TextareaRender);

View File

@@ -1,61 +0,0 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import TargetHandle from '../../TargetHandle';
import SourceHandle from '../../SourceHandle';
import { ModuleInputKeyEnum, ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import { useFlowProviderStore } from '../../../../FlowProvider';
const TriggerAndFinish = ({ moduleId, isTool }: { moduleId: string; isTool: boolean }) => {
const { t } = useTranslation();
const { nodes } = useFlowProviderStore();
const inputs = useMemo(
() => nodes.find((node) => node.data.moduleId === moduleId)?.data?.inputs || [],
[moduleId, nodes]
);
const hasSwitch = useMemo(
() => inputs.some((input) => input.key === ModuleInputKeyEnum.switch),
[inputs]
);
const outputs = useMemo(
() => nodes.find((node) => node.data.moduleId === moduleId)?.data?.outputs || [],
[moduleId, nodes]
);
const hasFinishOutput = useMemo(
() => outputs.some((output) => output.key === ModuleOutputKeyEnum.finish),
[outputs]
);
const Render = useMemo(() => {
return (
<Flex
className="nodrag"
cursor={'default'}
alignItems={'center'}
justifyContent={'space-between'}
position={'relative'}
>
<Box position={'relative'}>
{!isTool && (
<Box mt={2}>
<TargetHandle handleKey={ModuleInputKeyEnum.switch} valueType={'any'} />
{t('core.module.input.label.switch')}
</Box>
)}
</Box>
{hasFinishOutput && (
<Box position={'relative'} mt={2}>
{t('core.module.output.label.running done')}
<SourceHandle handleKey={ModuleOutputKeyEnum.finish} valueType={'boolean'} />
</Box>
)}
</Flex>
);
}, [hasFinishOutput, isTool, t]);
return hasSwitch ? Render : null;
};
export default React.memo(TriggerAndFinish);

View File

@@ -1,41 +0,0 @@
import React from 'react';
import type { RenderInputProps } from '../type';
import { Box, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import TargetHandle from '../../TargetHandle';
import SourceHandle from '../../SourceHandle';
import { ModuleInputKeyEnum, ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
const UserChatInput = ({ item }: RenderInputProps) => {
const { t } = useTranslation();
return (
<Flex
className="nodrag"
cursor={'default'}
alignItems={'center'}
justifyContent={'space-between'}
position={'relative'}
>
<Box position={'relative'}>
<TargetHandle handleKey={ModuleInputKeyEnum.userChatInput} valueType={item.valueType} />
{t('core.module.input.label.user question')}
<Box
position={'absolute'}
top={'-2px'}
right={'-8px'}
color={'red.500'}
fontWeight={'bold'}
>
*
</Box>
</Box>
<Box position={'relative'}>
{t('core.module.input.label.user question')}
<SourceHandle handleKey={ModuleOutputKeyEnum.userChatInput} valueType={item.valueType} />
</Box>
</Flex>
);
};
export default React.memo(UserChatInput);

View File

@@ -1,142 +0,0 @@
import { EditNodeFieldType, FlowNodeOutputItemType } from '@fastgpt/global/core/module/node/type';
import React, { useState } from 'react';
import { useTranslation } from 'next-i18next';
import { Box, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { onChangeNode } from '../../../FlowProvider';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import SourceHandle from '../SourceHandle';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import dynamic from 'next/dynamic';
const FieldEditModal = dynamic(() => import('../FieldEditModal'));
const OutputLabel = ({
moduleId,
outputKey,
outputs,
...item
}: FlowNodeOutputItemType & {
outputKey: string;
moduleId: string;
outputs: FlowNodeOutputItemType[];
}) => {
const { t } = useTranslation();
const { label = '', description, edit } = item;
const [editField, setEditField] = useState<EditNodeFieldType>();
return (
<Flex
className="nodrag"
cursor={'default'}
justifyContent={'right'}
alignItems={'center'}
position={'relative'}
>
{edit && (
<>
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'primary.500' }}
onClick={() =>
setEditField({
key: outputKey,
label: item.label,
description: item.description,
valueType: item.valueType,
outputType: item.type,
required: item.required,
defaultValue: item.defaultValue
})
}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
moduleId,
type: 'delOutput',
key: outputKey
});
}}
/>
</>
)}
{description && (
<MyTooltip label={t(description)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
</MyTooltip>
)}
<Box position={'relative'}>
{item.required && (
<Box
position={'absolute'}
top={'-2px'}
left={'-5px'}
color={'red.500'}
fontWeight={'bold'}
>
*
</Box>
)}
{t(label)}
</Box>
{item.type === FlowNodeOutputTypeEnum.source && (
<SourceHandle handleKey={outputKey} valueType={item.valueType} />
)}
{!!editField && (
<FieldEditModal
editField={item.editField}
defaultField={editField}
keys={[outputKey]}
onClose={() => setEditField(undefined)}
onSubmit={({ data, changeKey }) => {
if (!data.outputType || !data.key) return;
const newOutput: FlowNodeOutputItemType = {
...item,
type: data.outputType,
valueType: data.valueType,
key: data.key,
label: data.label,
description: data.description,
required: data.required,
defaultValue: data.defaultValue
};
if (changeKey) {
onChangeNode({
moduleId,
type: 'replaceOutput',
key: editField.key,
value: newOutput
});
} else {
onChangeNode({
moduleId,
type: 'updateOutput',
key: newOutput.key,
value: newOutput
});
}
setEditField(undefined);
}}
/>
)}
</Flex>
);
};
export default React.memo(OutputLabel);

View File

@@ -1,80 +0,0 @@
import React, { useMemo } from 'react';
import type { FlowNodeOutputItemType } from '@fastgpt/global/core/module/node/type';
import { Box } from '@chakra-ui/react';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import OutputLabel from './Label';
import { RenderOutputProps } from './type';
import dynamic from 'next/dynamic';
const RenderList: {
types: `${FlowNodeOutputTypeEnum}`[];
Component: React.ComponentType<RenderOutputProps>;
}[] = [
{
types: [FlowNodeOutputTypeEnum.addOutputParam],
Component: dynamic(() => import('./templates/AddOutputParam'))
}
];
const RenderToolOutput = ({
moduleId,
flowOutputList
}: {
moduleId: string;
flowOutputList: FlowNodeOutputItemType[];
}) => {
const sortOutputs = useMemo(
() =>
[...flowOutputList].sort((a, b) => {
if (a.type === FlowNodeOutputTypeEnum.addOutputParam) {
return 1;
}
if (b.type === FlowNodeOutputTypeEnum.addOutputParam) {
return -1;
}
if (a.key === ModuleOutputKeyEnum.finish) return -1;
if (b.key === ModuleOutputKeyEnum.finish) return 1;
return 0;
}),
[flowOutputList]
);
return (
<>
{sortOutputs.map((output) => {
const RenderComponent = (() => {
const Component = RenderList.find(
(item) => output.type && item.types.includes(output.type)
)?.Component;
if (!Component) return null;
return <Component outputs={sortOutputs} item={output} moduleId={moduleId} />;
})();
return (
output.type !== FlowNodeOutputTypeEnum.hidden && (
<Box key={output.key} _notLast={{ mb: 7 }} position={'relative'}>
{output.label && (
<OutputLabel
moduleId={moduleId}
outputKey={output.key}
outputs={sortOutputs}
{...output}
/>
)}
{!!RenderComponent && (
<Box mt={2} className={'nodrag'}>
{RenderComponent}
</Box>
)}
</Box>
)
);
})}
</>
);
};
export default React.memo(RenderToolOutput);

View File

@@ -1,60 +0,0 @@
import React, { useState } from 'react';
import type { RenderOutputProps } from '../type';
import { onChangeNode } from '../../../../FlowProvider';
import { Box, Button } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { EditNodeFieldType } from '@fastgpt/global/core/module/node/type';
const FieldEditModal = dynamic(() => import('../../FieldEditModal'));
const AddOutputParam = ({ outputs = [], item, moduleId }: RenderOutputProps) => {
const { t } = useTranslation();
const [editField, setEditField] = useState<EditNodeFieldType>();
return (
<Box textAlign={'right'}>
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
onClick={() => {
setEditField(item.defaultEditField || {});
}}
>
{t('core.module.output.Add Output')}
</Button>
{!!editField && (
<FieldEditModal
editField={item.editField}
defaultField={editField}
keys={outputs.map((output) => output.key)}
onClose={() => setEditField(undefined)}
onSubmit={({ data }) => {
onChangeNode({
moduleId,
type: 'addOutput',
key: data.key,
value: {
type: data.outputType,
valueType: data.valueType,
key: data.key,
label: data.label,
description: data.description,
required: data.required,
defaultValue: data.defaultValue,
edit: true,
editField: item.editField,
targets: []
}
});
setEditField(undefined);
}}
/>
)}
</Box>
);
};
export default React.memo(AddOutputParam);

View File

@@ -1,25 +0,0 @@
import { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
export const defaultEditFormData: FlowNodeInputItemType = {
valueType: 'string',
type: FlowNodeInputTypeEnum.target,
key: '',
label: '',
toolDescription: '',
required: true,
edit: true,
editField: {
key: true,
description: true,
dataType: true
},
defaultEditField: {
label: '',
key: '',
description: '',
inputType: FlowNodeInputTypeEnum.target,
valueType: ModuleIOValueTypeEnum.string
}
};

View File

@@ -1,58 +0,0 @@
import React, { useMemo } from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { Handle, Position } from 'reactflow';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import MyTooltip from '@/components/MyTooltip';
import { useTranslation } from 'next-i18next';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
interface Props extends BoxProps {
handleKey: string;
valueType?: `${ModuleIOValueTypeEnum}`;
}
const SourceHandle = ({ handleKey, valueType, ...props }: Props) => {
const { t } = useTranslation();
const valType = valueType ?? ModuleIOValueTypeEnum.any;
const valueStyle = useMemo(
() =>
valueType && FlowValueTypeMap[valueType]
? FlowValueTypeMap[valueType]?.handlerStyle
: FlowValueTypeMap[ModuleIOValueTypeEnum.any]?.handlerStyle,
[valueType]
);
return (
<Box
position={'absolute'}
top={'50%'}
right={'-18px'}
transform={'translate(0,-50%)'}
{...props}
>
<MyTooltip
label={t('app.module.type', {
type: t(FlowValueTypeMap[valType]?.label),
description: FlowValueTypeMap[valType]?.description
})}
>
<Handle
style={{
width: '14px',
height: '14px',
borderWidth: '3.5px',
backgroundColor: 'white',
...valueStyle
}}
type="source"
id={handleKey}
position={Position.Right}
/>
</MyTooltip>
</Box>
);
};
export default React.memo(SourceHandle);

View File

@@ -1,107 +0,0 @@
import MyTooltip from '@/components/MyTooltip';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import { Box, BoxProps } from '@chakra-ui/react';
import {
ModuleIOValueTypeEnum,
ModuleInputKeyEnum,
ModuleOutputKeyEnum
} from '@fastgpt/global/core/module/constants';
import { useTranslation } from 'next-i18next';
import { Connection, Handle, Position } from 'reactflow';
import { useFlowProviderStore } from '../../FlowProvider';
import { useCallback } from 'react';
type ToolHandleProps = BoxProps & {
moduleId: string;
};
export const ToolTargetHandle = ({ moduleId }: ToolHandleProps) => {
const { t } = useTranslation();
const valueTypeMap = FlowValueTypeMap[ModuleIOValueTypeEnum.tools];
return (
<MyTooltip
label={t('app.module.type', {
type: t(valueTypeMap?.label),
description: valueTypeMap?.description
})}
shouldWrapChildren={false}
>
<Handle
style={{
borderRadius: '0',
backgroundColor: 'transparent'
}}
type="target"
id={ModuleOutputKeyEnum.selectedTools}
position={Position.Top}
>
<Box
w={'14px'}
h={'14px'}
border={'4px solid #5E8FFF'}
transform={'translate(-40%,-30%) rotate(45deg)'}
pointerEvents={'none'}
/>
</Handle>
</MyTooltip>
);
};
export const ToolSourceHandle = ({ moduleId }: ToolHandleProps) => {
const { t } = useTranslation();
const { setEdges, nodes } = useFlowProviderStore();
const valueTypeMap = FlowValueTypeMap[ModuleIOValueTypeEnum.tools];
/* onConnect edge, delete tool input and switch */
const onConnect = useCallback(
(e: Connection) => {
const node = nodes.find((node) => node.id === e.target);
if (!node) return;
const inputs = node.data.inputs;
setEdges((edges) =>
edges.filter((edge) => {
const input = inputs.find((input) => input.key === edge.targetHandle);
if (
edge.target === node.id &&
(!!input?.toolDescription || input?.key === ModuleInputKeyEnum.switch)
) {
return false;
}
return true;
})
);
},
[nodes, setEdges]
);
return (
<MyTooltip
label={t('app.module.type', {
type: t(valueTypeMap?.label),
description: valueTypeMap?.description
})}
shouldWrapChildren={false}
>
<Handle
style={{
borderRadius: '0',
backgroundColor: 'transparent'
}}
type="source"
id={ModuleOutputKeyEnum.selectedTools}
position={Position.Bottom}
onConnect={onConnect}
>
<Box
w={'14px'}
h={'14px'}
border={'4px solid #5E8FFF'}
transform={'translate(-40%,-30%) rotate(45deg)'}
pointerEvents={'none'}
/>
</Handle>
</MyTooltip>
);
};

View File

@@ -1,203 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import ReactFlow, {
Background,
Connection,
Controls,
ControlButton,
MiniMap,
NodeProps,
ReactFlowProvider,
useReactFlow
} 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/module/node/constant';
import dynamic from 'next/dynamic';
import ButtonEdge from './components/modules/ButtonEdge';
import ModuleTemplateList from './ModuleTemplateList';
import { useFlowProviderStore } from './FlowProvider';
import 'reactflow/dist/style.css';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
const NodeSimple = dynamic(() => import('./components/nodes/NodeSimple'));
const nodeTypes: Record<`${FlowNodeTypeEnum}`, any> = {
[FlowNodeTypeEnum.userGuide]: dynamic(() => import('./components/nodes/NodeUserGuide')),
[FlowNodeTypeEnum.questionInput]: dynamic(() => import('./components/nodes/NodeQuestionInput')),
[FlowNodeTypeEnum.historyNode]: NodeSimple,
[FlowNodeTypeEnum.chatNode]: NodeSimple,
[FlowNodeTypeEnum.datasetSearchNode]: NodeSimple,
[FlowNodeTypeEnum.datasetConcatNode]: dynamic(
() => import('./components/nodes/NodeDatasetConcat')
),
[FlowNodeTypeEnum.answerNode]: dynamic(() => import('./components/nodes/NodeAnswer')),
[FlowNodeTypeEnum.classifyQuestion]: dynamic(() => import('./components/nodes/NodeCQNode')),
[FlowNodeTypeEnum.contentExtract]: dynamic(() => import('./components/nodes/NodeExtract')),
[FlowNodeTypeEnum.httpRequest468]: dynamic(() => import('./components/nodes/NodeHttp')),
[FlowNodeTypeEnum.httpRequest]: NodeSimple,
[FlowNodeTypeEnum.runApp]: NodeSimple,
[FlowNodeTypeEnum.pluginInput]: dynamic(() => import('./components/nodes/NodePluginInput')),
[FlowNodeTypeEnum.pluginOutput]: dynamic(() => import('./components/nodes/NodePluginOutput')),
[FlowNodeTypeEnum.pluginModule]: NodeSimple,
[FlowNodeTypeEnum.queryExtension]: NodeSimple,
[FlowNodeTypeEnum.tools]: dynamic(() => import('./components/nodes/NodeTools')),
[FlowNodeTypeEnum.stopTool]: (data: NodeProps<FlowModuleItemType>) => (
<NodeSimple {...data} minW={'100px'} maxW={'300px'} />
),
[FlowNodeTypeEnum.lafModule]: dynamic(() => import('./components/nodes/NodeLaf'))
};
const edgeTypes = {
[EDGE_TYPE]: ButtonEdge
};
const Container = React.memo(function Container() {
const { toast } = useToast();
const { t } = useTranslation();
const { reactFlowWrapper, nodes, onNodesChange, edges, onEdgesChange, onConnect } =
useFlowProviderStore();
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]
);
return (
<ReactFlow
ref={reactFlowWrapper}
fitView
nodes={nodes}
edges={edges}
minZoom={0.1}
maxZoom={1.5}
defaultEdgeOptions={{
animated: true,
zIndex: 0
}}
elevateEdgesOnSelect
connectionLineStyle={{ strokeWidth: 2, stroke: '#5A646Es' }}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={customOnConnect}
>
<FlowController />
</ReactFlow>
);
});
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} />
<ModuleTemplateList 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={'#fff'}>
{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 />
</>
);
});

View File

@@ -1,74 +0,0 @@
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { FlowNodeOutputTargetItemType } from '@fastgpt/global/core/module/node/type';
import { FlowModuleItemType, ModuleItemType } from '@fastgpt/global/core/module/type';
import { type Node, type Edge } from 'reactflow';
export const flowNode2Modules = ({
nodes,
edges
}: {
nodes: Node<FlowModuleItemType, string | undefined>[];
edges: Edge<any>[];
}) => {
const modules: ModuleItemType[] = nodes.map((item) => ({
moduleId: item.data.moduleId,
name: item.data.name,
intro: item.data.intro,
avatar: item.data.avatar,
flowType: item.data.flowType,
showStatus: item.data.showStatus,
position: item.position,
inputs: item.data.inputs.map((input) => ({
...input,
connected: false
})),
outputs: item.data.outputs.map((item) => ({
...item,
targets: [] as FlowNodeOutputTargetItemType[]
}))
}));
// update inputs and outputs
modules.forEach((module) => {
module.inputs.forEach((input) => {
input.connected = !!edges.find(
(edge) => edge.target === module.moduleId && edge.targetHandle === input.key
);
});
module.outputs.forEach((output) => {
output.targets = edges
.filter((edge) => {
if (
edge.source === module.moduleId &&
edge.sourceHandle === output.key &&
edge.targetHandle
) {
return true;
}
})
.map((edge) => ({
moduleId: edge.target,
key: edge.targetHandle || ''
}));
});
});
return modules;
};
export const filterExportModules = (modules: ModuleItemType[]) => {
modules.forEach((module) => {
// dataset - remove select dataset value
if (module.flowType === FlowNodeTypeEnum.datasetSearchNode) {
module.inputs.forEach((item) => {
if (item.key === ModuleInputKeyEnum.datasetSelectList) {
item.value = [];
}
});
}
});
return JSON.stringify(modules, null, 2);
};

View File

@@ -1,4 +1,4 @@
import type { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import React, {
useMemo,
@@ -16,10 +16,16 @@ import MyTooltip from '@/components/MyTooltip';
import { useUserStore } from '@/web/support/user/useUserStore';
import ChatBox from '@/components/ChatBox';
import type { ComponentRef, StartChatFnProps } from '@/components/ChatBox/type.d';
import { getGuideModule } from '@fastgpt/global/core/module/utils';
import { getGuideModule } from '@fastgpt/global/core/workflow/utils';
import { checkChatSupportSelectFileByModules } from '@/web/core/chat/utils';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import {
getDefaultEntryNodeIds,
initWorkflowEdgeStatus,
storeNodes2RuntimeNodes
} from '@fastgpt/global/core/workflow/runtime/utils';
export type ChatTestComponentRef = {
resetChatTest: () => void;
@@ -28,11 +34,13 @@ export type ChatTestComponentRef = {
const ChatTest = (
{
app,
modules = [],
nodes = [],
edges = [],
onClose
}: {
app: AppSchema;
modules?: ModuleItemType[];
nodes?: StoreNodeItemType[];
edges?: StoreEdgeItemType[];
onClose: () => void;
},
ref: ForwardedRef<ChatTestComponentRef>
@@ -40,16 +48,17 @@ const ChatTest = (
const { t } = useTranslation();
const ChatBoxRef = useRef<ComponentRef>(null);
const { userInfo } = useUserStore();
const isOpen = useMemo(() => modules && modules.length > 0, [modules]);
const isOpen = useMemo(() => nodes && nodes.length > 0, [nodes]);
const startChat = useCallback(
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
/* get histories */
let historyMaxLen = 6;
modules.forEach((module) => {
module.inputs.forEach((input) => {
nodes.forEach((node) => {
node.inputs.forEach((input) => {
if (
(input.key === ModuleInputKeyEnum.history ||
input.key === ModuleInputKeyEnum.historyMaxAmount) &&
(input.key === NodeInputKeyEnum.history ||
input.key === NodeInputKeyEnum.historyMaxAmount) &&
typeof input.value === 'number'
) {
historyMaxLen = Math.max(historyMaxLen, input.value);
@@ -64,10 +73,12 @@ const ChatTest = (
data: {
history,
prompt: chatList[chatList.length - 2].value,
modules,
nodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
edges: initWorkflowEdgeStatus(edges),
variables,
appId: app._id,
appName: `调试-${app.name}`
appName: `调试-${app.name}`,
mode: 'test'
},
onMessage: generatingMessage,
abortCtrl: controller
@@ -75,7 +86,7 @@ const ChatTest = (
return { responseText, responseData };
},
[app._id, app.name, modules]
[app._id, app.name, edges, nodes]
);
useImperativeHandle(ref, () => ({
@@ -138,14 +149,14 @@ const ChatTest = (
appAvatar={app.avatar}
userAvatar={userInfo?.avatar}
showMarkIcon
userGuideModule={getGuideModule(modules)}
showFileSelector={checkChatSupportSelectFileByModules(modules)}
userGuideModule={getGuideModule(nodes)}
showFileSelector={checkChatSupportSelectFileByModules(nodes)}
onStartChat={startChat}
onDelMessage={() => {}}
/>
</Box>
</Flex>
{/* <Box
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'fixed'}
@@ -154,7 +165,7 @@ const ChatTest = (
bottom={0}
right={0}
onClick={onClose}
/> */}
/>
</>
);
};

View File

@@ -0,0 +1,682 @@
import {
type Node,
type NodeChange,
type Edge,
type EdgeChange,
useNodesState,
useEdgesState,
OnConnectStartParams
} from 'reactflow';
import type {
FlowNodeItemType,
FlowNodeTemplateType
} from '@fastgpt/global/core/workflow/type/index.d';
import type { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe.d';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import React, {
type SetStateAction,
type Dispatch,
useContext,
useCallback,
createContext,
useRef,
useMemo,
useState,
useEffect
} from 'react';
import { storeEdgesRenderEdge, storeNode2FlowNode } from '@/web/core/workflow/utils';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/workflow/constants';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import { defaultRunningStatus } from '../constants';
import { postWorkflowDebug } from '@/web/core/workflow/api';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
export type useFlowProviderStoreType = {
// connect
connectingEdge: OnConnectStartParams | undefined;
setConnectingEdge: React.Dispatch<React.SetStateAction<OnConnectStartParams | undefined>>;
// nodes
basicNodeTemplates: FlowNodeTemplateType[];
reactFlowWrapper: null | React.RefObject<HTMLDivElement>;
mode: 'app' | 'plugin';
filterAppIds: string[];
nodes: Node<FlowNodeItemType, string | undefined>[];
nodeList: FlowNodeItemType[];
setNodes: Dispatch<SetStateAction<Node<FlowNodeItemType, string | undefined>[]>>;
onNodesChange: OnChange<NodeChange>;
// 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;
edges: Edge<any>[];
setEdges: Dispatch<SetStateAction<Edge<any>[]>>;
onEdgesChange: OnChange<EdgeChange>;
onFixView: () => void;
onChangeNode: (e: FlowNodeChangeProps) => void;
onResetNode: (e: { id: string; module: FlowNodeTemplateType }) => void;
onDelEdge: (e: {
nodeId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}) => void;
initData: (e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => void;
splitToolInputs: (
inputs: FlowNodeInputItemType[],
nodeId: string
) => {
isTool: boolean;
toolInputs: FlowNodeInputItemType[];
commonInputs: FlowNodeInputItemType[];
};
hasToolNode: boolean;
hoverNodeId: string | undefined;
setHoverNodeId: React.Dispatch<React.SetStateAction<string | undefined>>;
onUpdateNodeError: (node: string, isError: Boolean) => void;
};
const StateContext = createContext<useFlowProviderStoreType>({
reactFlowWrapper: null,
mode: 'app',
filterAppIds: [],
nodes: [],
setNodes: function (
value: React.SetStateAction<Node<FlowNodeItemType, string | undefined>[]>
): void {
return;
},
onNodesChange: function (changes: NodeChange[]): void {
return;
},
edges: [],
setEdges: function (value: React.SetStateAction<Edge<any>[]>): void {
return;
},
onEdgesChange: function (changes: EdgeChange[]): void {
return;
},
onFixView: function (): void {
return;
},
onChangeNode: function (e: FlowNodeChangeProps): void {
return;
},
onDelEdge: function (e: {
nodeId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}): void {
return;
},
onResetNode: function (e): 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.');
},
hasToolNode: false,
connectingEdge: undefined,
basicNodeTemplates: [],
initData: function (e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }): void {
throw new Error('Function not implemented.');
},
hoverNodeId: undefined,
setHoverNodeId: function (value: React.SetStateAction<string | undefined>): void {
throw new Error('Function not implemented.');
},
onUpdateNodeError: function (nodeId: string, isError: Boolean): void {
throw new Error('Function not implemented.');
},
nodeList: [],
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.');
},
setConnectingEdge: function (
value: React.SetStateAction<OnConnectStartParams | undefined>
): void {
throw new Error('Function not implemented.');
}
});
export const useFlowProviderStore = () => useContext(StateContext);
export const FlowProvider = ({
mode,
basicNodeTemplates = [],
filterAppIds = [],
children,
appId,
pluginId
}: {
mode: useFlowProviderStoreType['mode'];
basicNodeTemplates: FlowNodeTemplateType[];
filterAppIds?: string[];
children: React.ReactNode;
appId?: string;
pluginId?: string;
}) => {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { toast } = useToast();
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowNodeItemType>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [hoverNodeId, setHoverNodeId] = useState<string>();
const [connectingEdge, setConnectingEdge] = useState<OnConnectStartParams>();
const stringifyNodes = useMemo(() => JSON.stringify(nodes.map((node) => node.data)), [nodes]);
const nodeList = useMemo(
() => JSON.parse(stringifyNodes) as FlowNodeItemType[],
[stringifyNodes]
);
const hasToolNode = useMemo(() => {
return !!nodes.find((node) => node.data.flowNodeType === FlowNodeTypeEnum.tools);
}, [nodes]);
const onFixView = useCallback(() => {
const btn = document.querySelector('.custom-workflow-fix_view') as HTMLButtonElement;
setTimeout(() => {
btn && btn.click();
}, 100);
}, []);
/* edge */
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]
);
/* node */
// reset a node data. delete edge and replace it
const onResetNode = useCallback(
({ id, module }: { id: string; module: FlowNodeTemplateType }) => {
setNodes((state) =>
state.map((node) => {
if (node.id === id) {
// delete edge
node.data.inputs.forEach((item) => {
onDelEdge({ nodeId: id, targetHandle: item.key });
});
node.data.outputs.forEach((item) => {
onDelEdge({ nodeId: id, sourceHandle: item.key });
});
return {
...node,
data: {
...node.data,
...module
}
};
}
return node;
})
);
},
[onDelEdge, setNodes]
);
const onChangeNode = useCallback(
(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') {
onDelEdge({ nodeId, targetHandle: props.key });
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') {
onDelEdge({ nodeId, targetHandle: props.key });
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: 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: props.key });
updateObj.outputs = node.data.outputs.filter((item) => item.key !== props.key);
}
return {
...node,
data: {
...node.data,
...updateObj
}
};
})
);
},
[onDelEdge, setNodes, toast]
);
const onUpdateNodeError = useCallback(
(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;
});
});
},
[setNodes]
);
/* Run workflow debug and get next runtime data */
const [workflowDebugData, setWorkflowDebugData] = useState<{
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
nextRunNodes: RuntimeNodeItemType[];
}>();
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') {
console.log(node.name);
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]
);
/* If the module is connected by a tool, the tool input and the normal input are separated */
const splitToolInputs = useCallback(
(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;
})
};
},
[edges]
);
const initData = useCallback(
(e: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => {
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item })));
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })));
setTimeout(() => {
onFixView();
}, 100);
},
[setEdges, setNodes, onFixView]
);
useEffect(() => {
eventBus.on(EventNameEnum.requestWorkflowStore, () => {
eventBus.emit(EventNameEnum.receiveWorkflowStore, {
nodes
});
});
return () => {
eventBus.off(EventNameEnum.requestWorkflowStore);
};
}, [nodes]);
const value = {
reactFlowWrapper,
mode,
filterAppIds,
edges,
setEdges,
onEdgesChange,
// nodes
nodes,
nodeList,
setNodes,
onNodesChange,
hoverNodeId,
setHoverNodeId,
onUpdateNodeError,
workflowDebugData,
onNextNodeDebug,
onStartNodeDebug,
onStopNodeDebug,
basicNodeTemplates,
// connect
connectingEdge,
setConnectingEdge,
onFixView,
onChangeNode,
onResetNode,
onDelEdge,
initData,
splitToolInputs,
hasToolNode
};
return <StateContext.Provider value={value}>{children}</StateContext.Provider>;
};
export default React.memo(FlowProvider);
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);
});

View File

@@ -40,11 +40,7 @@ const ImportSettings = ({ onClose }: Props) => {
}
try {
const data = JSON.parse(value);
setEdges([]);
setNodes([]);
setTimeout(() => {
initData(data);
}, 10);
initData(data);
onClose();
} catch (error) {
toast({

View File

@@ -1,23 +1,31 @@
import React, { useCallback, useMemo, useState } from 'react';
import { Box, Flex, IconButton, Input, InputGroup, InputLeftElement } from '@chakra-ui/react';
import {
Box,
Card,
Flex,
IconButton,
Input,
InputGroup,
InputLeftElement,
css
} from '@chakra-ui/react';
import type {
FlowNodeTemplateType,
moduleTemplateListType
} from '@fastgpt/global/core/module/type.d';
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 { onSetNodes, useFlowProviderStore } from './FlowProvider';
import { useFlowProviderStore } from './FlowProvider';
import { customAlphabet } from 'nanoid';
import { appModule2FlowNode } from '@/utils/adapt';
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import EmptyTip from '@/components/EmptyTip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
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 { moduleTemplatesList } from '@fastgpt/global/core/module/template/constants';
import { moduleTemplatesList } from '@fastgpt/global/core/workflow/template/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';
@@ -27,6 +35,7 @@ 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';
type ModuleTemplateListProps = {
isOpen: boolean;
@@ -47,15 +56,15 @@ enum TemplateTypeEnum {
const sliderWidth = 380;
const ModuleTemplateList = ({ isOpen, onClose }: ModuleTemplateListProps) => {
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 { nodes, basicNodeTemplates, hasToolNode } = useFlowProviderStore();
const {
basicNodeTemplates,
systemNodeTemplates,
loadSystemNodeTemplates,
teamPluginNodeTemplates,
@@ -63,10 +72,22 @@ const ModuleTemplateList = ({ isOpen, onClose }: ModuleTemplateListProps) => {
} = useWorkflowStore();
const [templateType, setTemplateType] = useState(TemplateTypeEnum.basic);
const templates = useMemo(() => {
const templatesString = useMemo(() => {
const map = {
[TemplateTypeEnum.basic]: basicNodeTemplates.filter((item) => {
if (item.flowType === FlowNodeTypeEnum.lafModule && !feConfigs.lafEnv) {
// unique node filter
if (item.unique) {
const nodeExist = nodes.some((node) => node.data.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;
@@ -77,7 +98,16 @@ const ModuleTemplateList = ({ isOpen, onClose }: ModuleTemplateListProps) => {
)
};
return JSON.stringify(map[templateType]);
}, [basicNodeTemplates, searchKey, systemNodeTemplates, teamPluginNodeTemplates, templateType]);
}, [
basicNodeTemplates,
feConfigs.lafEnv,
hasToolNode,
nodes,
searchKey,
systemNodeTemplates,
teamPluginNodeTemplates,
templateType
]);
const { mutate: onChangeTab } = useRequest({
mutationFn: async (e: any) => {
@@ -103,7 +133,7 @@ const ModuleTemplateList = ({ isOpen, onClose }: ModuleTemplateListProps) => {
);
const Render = useMemo(() => {
const parseTemplates = JSON.parse(templates) as FlowNodeTemplateType[];
const parseTemplates = JSON.parse(templatesString) as FlowNodeTemplateType[];
return (
<>
<Box
@@ -218,12 +248,22 @@ const ModuleTemplateList = ({ isOpen, onClose }: ModuleTemplateListProps) => {
</Flex>
</>
);
}, [currentParent, isOpen, onChangeTab, onClose, router, searchKey, t, templateType, templates]);
}, [
currentParent,
isOpen,
onChangeTab,
onClose,
router,
searchKey,
t,
templateType,
templatesString
]);
return Render;
};
export default React.memo(ModuleTemplateList);
export default React.memo(NodeTemplatesModal);
const RenderList = React.memo(function RenderList({
templates,
@@ -236,10 +276,10 @@ const RenderList = React.memo(function RenderList({
const { x, y, zoom } = useViewport();
const { setLoading } = useSystemStore();
const { toast } = useToast();
const { reactFlowWrapper, nodes } = useFlowProviderStore();
const { reactFlowWrapper, setNodes } = useFlowProviderStore();
const formatTemplates = useMemo<moduleTemplateListType>(() => {
const copy: moduleTemplateListType = JSON.parse(JSON.stringify(moduleTemplatesList));
const formatTemplates = useMemo<nodeTemplateListType>(() => {
const copy: nodeTemplateListType = JSON.parse(JSON.stringify(moduleTemplatesList));
templates.forEach((item) => {
const index = copy.findIndex((template) => template.type === item.templateType);
if (index === -1) return;
@@ -253,10 +293,10 @@ const RenderList = React.memo(function RenderList({
async ({ template, position }: { template: FlowNodeTemplateType; position: XYPosition }) => {
if (!reactFlowWrapper?.current) return;
const templateModule = await (async () => {
const templateNode = await (async () => {
try {
// get plugin preview module
if (template.flowType === FlowNodeTypeEnum.pluginModule) {
if (template.flowNodeType === FlowNodeTypeEnum.pluginModule) {
setLoading(true);
const res = await getPreviewPluginModule(template.id);
@@ -278,91 +318,127 @@ const RenderList = React.memo(function RenderList({
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
onSetNodes(
nodes.concat(
appModule2FlowNode({
item: {
...templateModule,
moduleId: nanoid(),
position: { x: mouseX, y: mouseY - 20 }
}
})
)
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)
);
},
[nodes, reactFlowWrapper, setLoading, t, toast, x, y, zoom]
[reactFlowWrapper, setLoading, setNodes, t, toast, x, y, zoom]
);
return templates.length === 0 ? (
<EmptyTip text={t('app.module.No Modules')} />
) : (
<Box flex={'1 0 0'} overflow={'overlay'} px={'20px'}>
<Box mx={'auto'}>
{formatTemplates.map((item, i) => (
<Box key={item.type}>
{item.label && (
<Flex>
<Box fontWeight={'bold'} flex={1}>
{t(item.label)}
</Box>
</Flex>
)}
<>
{item.list.map((template) => (
<Flex
key={template.id}
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: 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={'34px'}
objectFit={'contain'}
borderRadius={'0'}
/>
<Box ml={5} flex={'1 0 0'}>
<Box color={'black'}>{t(template.name)}</Box>
<Box className="textEllipsis3" color={'myGray.500'} fontSize={'sm'}>
{t(template.intro)}
</Box>
const Render = useMemo(() => {
return templates.length === 0 ? (
<EmptyTip text={t('app.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 fontWeight={'bold'} flex={1}>
{t(item.label)}
</Box>
</Flex>
))}
</>
</Box>
))}
)}
<>
{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={'30px'}
objectFit={'contain'}
borderRadius={'0'}
/>
<Box color={'black'} ml={5} flex={'1 0 0'}>
{t(template.name)}
</Box>
</Flex>
</MyTooltip>
))}
</>
</Box>
))}
</Box>
</Box>
</Box>
);
);
}, [formatTemplates, isPc, onAddNode, onClose, setCurrentParent, t, templates.length]);
return Render;
});

View File

@@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
import { ModalBody, Flex, Box, useTheme, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useQuery } from '@tanstack/react-query';
import type { SelectAppItemType } from '@fastgpt/global/core/module/type';
import type { SelectAppItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Avatar from '@/components/Avatar';
import { useTranslation } from 'next-i18next';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
@@ -38,7 +38,7 @@ const SelectAppModal = ({
<MyModal
isOpen
title={`选择应用${max > 1 ? `(${selectedApps.length}/${max})` : ''}`}
iconSrc="/imgs/module/ai.svg"
iconSrc="/imgs/workflow/ai.svg"
onClose={onClose}
position={'relative'}
w={'600px'}

View File

@@ -0,0 +1,227 @@
import React, { useCallback, useMemo } from 'react';
import { BezierEdge, getBezierPath, EdgeLabelRenderer, EdgeProps } from 'reactflow';
import { useFlowProviderStore } from '../FlowProvider';
import { Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/workflow/constants';
const ButtonEdge = (props: EdgeProps) => {
const { nodes, setEdges, workflowDebugData } = useFlowProviderStore();
const {
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
selected,
sourceHandleId,
source,
target,
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 { 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.source === source && edge.target === target
);
if (!targetEdge) {
if (highlightEdge) return '#3370ff';
return '#94B5FF';
}
// debug mode
const colorMap = {
[RuntimeEdgeStatusEnum.active]: '#39CC83',
[RuntimeEdgeStatusEnum.waiting]: '#5E8FFF',
[RuntimeEdgeStatusEnum.skipped]: '#8A95A7'
};
return colorMap[targetEdge.status];
}, [highlightEdge, source, target, 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>
{highlightEdge && (
<Flex
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>
);
}, [
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);

View File

@@ -4,7 +4,17 @@ import { BoxProps } from '@chakra-ui/react';
const Container = ({ children, ...props }: BoxProps) => {
return (
<Box px={4} py={'10px'} position={'relative'} {...props}>
<Box
px={4}
mx={2}
mb={2}
py={'10px'}
position={'relative'}
bg={'myGray.50'}
border={'1px solid #F0F1F6'}
borderRadius={'md'}
{...props}
>
{children}
</Box>
);

View File

@@ -1,28 +1,32 @@
import React from 'react';
import { Box, useTheme } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
const Divider = ({
text,
showBorderBottom = true
showBorderBottom = true,
icon
}: {
text?: 'Input' | 'Output' | string;
showBorderBottom?: boolean;
icon?: React.ReactNode;
}) => {
const theme = useTheme();
const { t } = useTranslation();
const isDivider = !text;
return (
<Box
textAlign={'center'}
bg={'#f8f8f8'}
alignItems={'center'}
display={'flex'}
justifyContent={'center'}
bg={'myGray.25'}
py={isDivider ? '0' : 2}
borderTop={theme.borders.base}
borderBottom={showBorderBottom ? theme.borders.base : 0}
fontSize={'lg'}
fontWeight={'medium'}
>
{icon}
{icon && <Box w={1} />}
{text}
</Box>
);

View File

@@ -0,0 +1,13 @@
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);

View File

@@ -0,0 +1,233 @@
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 { getWorkflowStore, useFlowProviderStore } from '../FlowProvider';
import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { flowNode2StoreNodes } 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';
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 { edges, setNodes, onStartNodeDebug, onUpdateNodeError } = useFlowProviderStore();
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 = flowNode2StoreNodes({ 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 referenceInputs = runtimeNode.inputs.filter((input) => {
if (checkInputIsReference(input)) return true;
if (input.required && !input.value) return true;
});
const { register, getValues, setValue, handleSubmit } = useForm<Record<string, any>>({
defaultValues: referenceInputs.reduce((acc, input) => {
//@ts-ignore
acc[input.key] = undefined;
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) => ({
...input,
value: data[input.key] ?? input.value
}))
}
: node
),
runtimeEdges: runtimeEdges
});
onClose();
};
return (
<MyRightDrawer
onClose={onClose}
iconSrc="core/workflow/debugBlue"
title={t('core.workflow.Debug Node')}
maxW={['90vw', '35vw']}
>
<Flex flexDirection={'column'} h={'100%'}>
<Box flex={'1 0 0'} overflow={'auto'}>
{referenceInputs.map((input) => {
const required = input.required || false;
return (
<Box key={input.key} _notLast={{ mb: 4 }} px={1}>
<Box display={'inline-block'} position={'relative'} mb={1}>
{required && (
<Box position={'absolute'} right={-2} top={-1} color={'red.600'}>
*
</Box>
)}
{t(input.debugLabel || input.label)}
</Box>
{(() => {
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 <Switch size={'lg'} {...register(input.key)} />;
}
return (
<JsonEditor
bg={'myGray.50'}
placeholder={t(input.placeholder || '')}
resize
value={getValues(input.key)}
onChange={(e) => {
setValue(input.key, e);
}}
/>
);
})()}
</Box>
);
})}
</Box>
<Flex py={2} justifyContent={'flex-end'}>
<Button onClick={handleSubmit(onclickRun)}></Button>
</Flex>
</Flex>
</MyRightDrawer>
);
}, [onStartNodeDebug, runtimeEdges, runtimeNodeId, runtimeNodes, t]);
return {
DebugInputModal,
openDebugNode
};
};

View File

@@ -0,0 +1,115 @@
import { useCallback, useEffect, useState } from 'react';
import { getWorkflowStore, useFlowProviderStore } from '../FlowProvider';
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';
export const useKeyboard = () => {
const { t } = useTranslation();
const { setNodes } = useFlowProviderStore();
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) => ({
// reset id
...item,
id: getNanoid(),
data: {
...item.data,
nodeId: getNanoid()
},
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
};
};

View File

@@ -0,0 +1,283 @@
import React, { useCallback, useMemo } from 'react';
import ReactFlow, {
Background,
Connection,
Controls,
ControlButton,
MiniMap,
NodeProps,
ReactFlowProvider,
useReactFlow,
NodeChange,
OnConnectStartParams,
addEdge,
EdgeChange
} 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 { useFlowProviderStore } from './FlowProvider';
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 '@/components/MyTooltip';
import { connectionLineStyle, defaultEdgeOptions } from '../constants';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useKeyboard } from './hooks/useKeyboard';
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'))
};
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 {
reactFlowWrapper,
nodes,
onNodesChange,
edges,
setEdges,
onEdgesChange,
setConnectingEdge
} = useFlowProviderStore();
/* 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]
);
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}
>
<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 />
</>
);
});

View File

@@ -0,0 +1,35 @@
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 { useFlowProviderStore } from '../FlowProvider';
import RenderToolInput from './render/RenderToolInput';
import { useTranslation } from 'next-i18next';
import IOTitle from '../components/IOTitle';
const NodeAnswer = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
const { splitToolInputs } = useFlowProviderStore();
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);

View File

@@ -1,30 +1,30 @@
import React, { useMemo } from 'react';
import { NodeProps } from 'reactflow';
import { NodeProps, Position } from 'reactflow';
import { Box, Button, Flex, Textarea } from '@chakra-ui/react';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import type { ClassifyQuestionAgentItemType } from '@fastgpt/global/core/module/type.d';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 4);
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 { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleIOValueTypeEnum, ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import SourceHandle from '../render/SourceHandle';
import MyTooltip from '@/components/MyTooltip';
import { onChangeNode } from '../../FlowProvider';
import { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { useFlowProviderStore } from '../FlowProvider';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { SourceHandle } from './render/Handle';
import IOTitle from '../components/IOTitle';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
const NodeCQNode = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const NodeCQNode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs } = data;
const { nodeId, inputs } = data;
const { onChangeNode } = useFlowProviderStore();
const CustomComponent = useMemo(
() => ({
[ModuleInputKeyEnum.agents]: ({
[NodeInputKeyEnum.agents]: ({
key: agentKey,
value = [],
...props
@@ -40,13 +40,13 @@ const NodeCQNode = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
mt={1}
mr={2}
name={'minus'}
w={'14px'}
w={'12px'}
cursor={'pointer'}
color={'myGray.600'}
_hover={{ color: 'red.600' }}
onClick={() => {
onChangeNode({
moduleId,
nodeId,
type: 'updateInput',
key: agentKey,
value: {
@@ -56,20 +56,24 @@ const NodeCQNode = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
}
});
onChangeNode({
moduleId,
nodeId,
type: 'delOutput',
key: item.key
});
}}
/>
</MyTooltip>
<Box flex={1}>{i + 1}</Box>
<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
@@ -80,7 +84,7 @@ const NodeCQNode = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
: val
);
onChangeNode({
moduleId,
nodeId,
type: 'updateInput',
key: agentKey,
value: {
@@ -91,16 +95,22 @@ const NodeCQNode = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
});
}}
/>
<SourceHandle handleKey={item.key} valueType={ModuleIOValueTypeEnum.boolean} />
<SourceHandle
nodeId={nodeId}
handleId={getHandleId(nodeId, 'source', item.key)}
position={Position.Right}
translate={[26, 0]}
/>
</Box>
</Box>
))}
<Button
fontSize={'sm'}
onClick={() => {
const key = nanoid();
const key = getNanoid();
onChangeNode({
moduleId,
nodeId,
type: 'updateInput',
key: agentKey,
value: {
@@ -109,17 +119,6 @@ const NodeCQNode = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
value: agents.concat({ value: '', key })
}
});
onChangeNode({
moduleId,
type: 'addOutput',
value: {
key,
label: '',
type: FlowNodeOutputTypeEnum.hidden,
targets: []
}
});
}}
>
{t('core.module.Add question type')}
@@ -128,14 +127,13 @@ const NodeCQNode = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
);
}
}),
[moduleId, t]
[nodeId, onChangeNode, t]
);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Divider text={t('common.Input')} />
<Container>
<RenderInput moduleId={moduleId} flowInputList={inputs} CustomComponent={CustomComponent} />
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
</Container>
</NodeCard>
);

View File

@@ -0,0 +1,153 @@
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 { useFlowProviderStore } from '../FlowProvider';
import MyIcon from '@fastgpt/web/components/common/Icon';
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 Reference from './render/RenderInput/templates/Reference';
import IOTitle from '../components/IOTitle';
const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { llmModelList } = useSystemStore();
const { nodeList, onChangeNode } = useFlowProviderStore();
const { nodeId, inputs, outputs } = data;
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 RenderQuoteList = useMemo(() => {
return (
<Box mt={-2}>
{quotes.map((quote, i) => (
<Box key={quote.key} _notLast={{ mb: 4 }}>
<Flex alignItems={'center'}>
<Box fontWeight={'medium'} color={'myGray.600'}>
{t('core.chat.Quote')}
{i + 1}
</Box>
<MyIcon
ml={2}
w={'14px'}
name={'delete'}
cursor={'pointer'}
color={'myGray.600'}
_hover={{ color: 'red.600' }}
onClick={() => {
onChangeNode({
nodeId,
type: 'delInput',
key: quote.key
});
}}
/>
</Flex>
<Reference nodeId={nodeId} item={quote} />
</Box>
))}
</Box>
);
}, [nodeId, onChangeNode, quotes, t]);
const onAddField = useCallback(() => {
onChangeNode({
nodeId,
type: 'addInput',
value: getOneQuoteInputTemplate()
});
}, [nodeId, onChangeNode]);
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={'transparentBase'}
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);

View File

@@ -0,0 +1,10 @@
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);

View File

@@ -9,7 +9,7 @@ import {
Input,
Textarea
} from '@chakra-ui/react';
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/module/type';
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';
@@ -42,7 +42,7 @@ const ExtractFieldModal = ({
return (
<MyModal
isOpen={true}
iconSrc="/imgs/module/extract.png"
iconSrc="/imgs/workflow/extract.png"
title={t('core.module.extract.Field Setting Title')}
onClose={onClose}
w={['90vw', '500px']}

View File

@@ -12,34 +12,39 @@ import {
Flex
} from '@chakra-ui/react';
import { NodeProps } from 'reactflow';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { useTranslation } from 'next-i18next';
import NodeCard from '../../render/NodeCard';
import Container from '../../modules/Container';
import NodeCard from '../render/NodeCard';
import Container from '../../components/Container';
import { AddIcon } from '@chakra-ui/icons';
import RenderInput from '../../render/RenderInput';
import Divider from '../../modules/Divider';
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/module/type';
import RenderOutput from '../../render/RenderOutput';
import RenderInput from '../render/RenderInput';
import Divider from '../../components/Divider';
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 { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
import { onChangeNode, useFlowProviderStore } from '../../../FlowProvider';
import RenderToolInput from '../../render/RenderToolInput';
import { FlowNodeInputItemType } from '../../../../../../../../../../packages/global/core/module/node/type';
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 { useFlowProviderStore } from '../../FlowProvider';
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';
const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
const { inputs, outputs, moduleId } = data;
const { splitToolInputs } = useFlowProviderStore();
const { toolInputs, commonInputs } = splitToolInputs(inputs, moduleId);
const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
const { inputs, outputs, nodeId } = data;
const { splitToolInputs, onChangeNode } = useFlowProviderStore();
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
const { t } = useTranslation();
const [editExtractFiled, setEditExtractField] = useState<ContextExtractAgentItemType>();
const CustomComponent = useMemo(
() => ({
[ModuleInputKeyEnum.extractKeys]: ({
[NodeInputKeyEnum.extractKeys]: ({
value: extractKeys = [],
...props
}: Omit<FlowNodeInputItemType, 'value'> & {
@@ -47,7 +52,9 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
}) => (
<Box>
<Flex alignItems={'center'}>
<Box flex={'1 0 0'}>{t('core.module.extract.Target field')}</Box>
<Box flex={'1 0 0'} fontWeight={'medium'} color={'myGray.600'}>
{t('core.module.extract.Target field')}
</Box>
<Button
size={'sm'}
variant={'whitePrimary'}
@@ -101,9 +108,9 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
cursor={'pointer'}
onClick={() => {
onChangeNode({
moduleId,
nodeId,
type: 'updateInput',
key: ModuleInputKeyEnum.extractKeys,
key: NodeInputKeyEnum.extractKeys,
value: {
...props,
value: extractKeys.filter((extract) => item.key !== extract.key)
@@ -111,7 +118,7 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
});
onChangeNode({
moduleId,
nodeId,
type: 'delOutput',
key: item.key
});
@@ -127,33 +134,33 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
</Box>
)
}),
[moduleId, t]
[nodeId, onChangeNode, t]
);
return (
<NodeCard minW={'400px'} {...data}>
{toolInputs.length > 0 && (
<>
<Divider text={t('core.module.tool.Tool input')} />
<Container>
<RenderToolInput moduleId={moduleId} inputs={toolInputs} />
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
</Container>
</>
)}
<>
<Divider text={t('common.Input')} />
<Container>
<IOTitle text={t('common.Input')} />
<RenderInput
moduleId={moduleId}
nodeId={nodeId}
flowInputList={commonInputs}
CustomComponent={CustomComponent}
/>
</Container>
</>
<>
<Divider text={t('common.Output')} />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
<IOTitle text={t('common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</>
@@ -163,7 +170,7 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
onClose={() => setEditExtractField(undefined)}
onSubmit={(data) => {
const extracts: ContextExtractAgentItemType[] =
inputs.find((item) => item.key === ModuleInputKeyEnum.extractKeys)?.value || [];
inputs.find((item) => item.key === NodeInputKeyEnum.extractKeys)?.value || [];
const exists = extracts.find((item) => item.key === editExtractFiled.key);
@@ -172,21 +179,21 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
: extracts.concat(data);
onChangeNode({
moduleId,
nodeId,
type: 'updateInput',
key: ModuleInputKeyEnum.extractKeys,
key: NodeInputKeyEnum.extractKeys,
value: {
...inputs.find((input) => input.key === ModuleInputKeyEnum.extractKeys),
...inputs.find((input) => input.key === NodeInputKeyEnum.extractKeys),
value: newInputs
}
});
const newOutput = {
const newOutput: FlowNodeOutputItemType = {
id: getNanoid(),
key: data.key,
label: `提取结果-${data.desc}`,
valueType: ModuleIOValueTypeEnum.string,
type: FlowNodeOutputTypeEnum.source,
targets: []
valueType: WorkflowIOValueTypeEnum.string,
type: FlowNodeOutputTypeEnum.static
};
if (exists) {
@@ -194,7 +201,7 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
const output = outputs.find((output) => output.key === data.key);
// update
onChangeNode({
moduleId,
nodeId,
type: 'updateOutput',
key: data.key,
value: {
@@ -204,7 +211,7 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
});
} else {
onChangeNode({
moduleId,
nodeId,
type: 'replaceOutput',
key: editExtractFiled.key,
value: newOutput
@@ -212,7 +219,7 @@ const NodeExtract = ({ data }: NodeProps<FlowModuleItemType>) => {
}
} else {
onChangeNode({
moduleId,
nodeId,
type: 'addOutput',
value: newOutput
});

View File

@@ -2,12 +2,12 @@ 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 { onChangeNode } from '../../../FlowProvider';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
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 { useFlowProviderStore } from '../../FlowProvider';
type RequestMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';
const methodMap: { [K in RequestMethod]: string } = {
@@ -19,15 +19,16 @@ const methodMap: { [K in RequestMethod]: string } = {
};
const CurlImportModal = ({
moduleId,
nodeId,
inputs,
onClose
}: {
moduleId: string;
nodeId: string;
inputs: FlowNodeInputItemType[];
onClose: () => void;
}) => {
const { t } = useTranslation();
const { onChangeNode } = useFlowProviderStore();
const { register, handleSubmit } = useForm({
defaultValues: {
curlContent: ''
@@ -38,11 +39,11 @@ const CurlImportModal = ({
const handleFileProcessing = async (content: string) => {
try {
const requestUrl = inputs.find((item) => item.key === ModuleInputKeyEnum.httpReqUrl);
const requestMethod = inputs.find((item) => item.key === ModuleInputKeyEnum.httpMethod);
const params = inputs.find((item) => item.key === ModuleInputKeyEnum.httpParams);
const headers = inputs.find((item) => item.key === ModuleInputKeyEnum.httpHeaders);
const jsonBody = inputs.find((item) => item.key === ModuleInputKeyEnum.httpJsonBody);
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) {
@@ -62,9 +63,9 @@ const CurlImportModal = ({
const newBody = JSON.stringify(parsed.data, null, 2);
onChangeNode({
moduleId,
nodeId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpReqUrl,
key: NodeInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: parsed.url
@@ -72,9 +73,9 @@ const CurlImportModal = ({
});
onChangeNode({
moduleId,
nodeId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpMethod,
key: NodeInputKeyEnum.httpMethod,
value: {
...requestMethod,
value: methodMap[parsed.method?.toLowerCase() as RequestMethod] || 'GET'
@@ -82,9 +83,9 @@ const CurlImportModal = ({
});
onChangeNode({
moduleId,
nodeId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpParams,
key: NodeInputKeyEnum.httpParams,
value: {
...params,
value: newParams
@@ -92,9 +93,9 @@ const CurlImportModal = ({
});
onChangeNode({
moduleId,
nodeId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpHeaders,
key: NodeInputKeyEnum.httpHeaders,
value: {
...headers,
value: newHeaders
@@ -102,9 +103,9 @@ const CurlImportModal = ({
});
onChangeNode({
moduleId,
nodeId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpJsonBody,
key: NodeInputKeyEnum.httpJsonBody,
value: {
...jsonBody,
value: newBody

View File

@@ -0,0 +1,695 @@
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 { useFlowProviderStore } from '../../FlowProvider';
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 { QuestionOutlineIcon } from '@chakra-ui/icons';
import JSONEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import {
formatEditorVariablePickerIcon,
getGuideModule,
splitGuideModule
} 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 { getSystemVariables } from '@/web/core/app/utils';
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 [_, startSts] = useTransition();
const { onChangeNode } = useFlowProviderStore();
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 = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
startSts(() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: e.target.value
}
});
});
},
[nodeId, onChangeNode, requestUrl]
);
const onBlurUrl = useCallback(
(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')
});
}
},
[inputs, nodeId, onChangeNode, requestUrl, t, toast]
);
const Render = useMemo(() => {
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>
);
}, [
inputs,
isOpenCurl,
nodeId,
onBlurUrl,
onChangeNode,
onChangeUrl,
onCloseCurl,
onOpenCurl,
requestMethods,
requestUrl?.value,
t
]);
return Render;
});
export function RenderHttpProps({
nodeId,
inputs
}: {
nodeId: string;
inputs: FlowNodeInputItemType[];
}) {
const { t } = useTranslation();
const [selectedTab, setSelectedTab] = useState(TabEnum.params);
const { nodeList } = useFlowProviderStore();
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 = formatEditorVariablePickerIcon(
splitGuideModule(getGuideModule(nodeList))?.variableModules || []
);
const systemVariables = getSystemVariables(t);
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.canEdit || input.toolDescription)
.map((item) => ({
key: item.key,
label: item.label
}))
);
return [...moduleVariables, ...globalVariables, ...systemVariables];
}, [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')}
<MyTooltip label={t('core.module.http.Props tip', { variable: variableText })}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</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 } = useFlowProviderStore();
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 (
<TableContainer overflowY={'visible'} overflowX={'unset'}>
<Table>
<Thead>
<Tr>
<Th px={2}>{t('core.module.http.Props name')}</Th>
<Th px={2}>{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>
);
}, [
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 } = useFlowProviderStore();
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 } = useFlowProviderStore();
const { toolInputs, commonInputs, isTool } = splitToolInputs(inputs, nodeId);
const CustomComponents = useMemo(
() => ({
[NodeInputKeyEnum.httpMethod]: () => (
<RenderHttpMethodAndUrl nodeId={nodeId} inputs={inputs} />
),
[NodeInputKeyEnum.httpHeaders]: () => <RenderHttpProps nodeId={nodeId} inputs={inputs} />
}),
[inputs, nodeId]
);
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);

View File

@@ -0,0 +1,376 @@
import React, { useCallback, useMemo } from 'react';
import NodeCard from './render/NodeCard';
import { useTranslation } from 'next-i18next';
import { useFlowProviderStore } from '../FlowProvider';
import { Box, Button, Flex, background } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import MyIcon from '@fastgpt/web/components/common/Icon';
import RenderOutput from './render/RenderOutput';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeProps } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
import {
IfElseConditionType,
IfElseListItemType
} from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { ReferSelector, useReference } from './render/RenderInput/templates/Reference';
import {
VariableConditionEnum,
allConditionList,
arrayConditionList,
booleanConditionList,
numberConditionList
} from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
import { stringConditionList } from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyInput from '@/components/MyInput';
const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs = [], outputs } = data;
const { onChangeNode } = useFlowProviderStore();
const condition = useMemo(
() =>
(inputs.find((input) => input.key === NodeInputKeyEnum.condition)
?.value as IfElseConditionType) || 'OR',
[inputs]
);
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]
);
const RenderAddCondition = useMemo(() => {
return (
<Button
onClick={() => {
onUpdateIfElseList([
...ifElseList,
{
variable: undefined,
condition: undefined,
value: undefined
}
]);
}}
variant={'whiteBase'}
leftIcon={<SmallAddIcon />}
my={3}
w={'full'}
>
{t('core.module.input.add')}
</Button>
);
}, [ifElseList, onUpdateIfElseList, t]);
return (
<NodeCard selected={selected} maxW={'1000px'} {...data}>
<Box px={6}>
<RenderOutput nodeId={nodeId} flowOutputList={[outputs[0]]} />
</Box>
<Box py={3} px={4}>
<Box className="nowheel">
{ifElseList.map((item, i) => {
return (
<Box key={i}>
{/* border */}
{i !== 0 && (
<Flex alignItems={'center'} w={'full'} py={'5px'}>
<Box
w={'auto'}
flex={1}
height={'1px'}
style={{
background:
'linear-gradient(90deg, rgba(232, 235, 240, 0.00) 0%, #E8EBF0 100%)'
}}
></Box>
<Flex
px={'2.5'}
color={'primary.600'}
fontWeight={'medium'}
alignItems={'center'}
cursor={'pointer'}
rounded={'md'}
onClick={() => {
const conditionInput = inputs.find(
(input) => input.key === NodeInputKeyEnum.condition
);
if (!conditionInput) return;
onChangeNode({
nodeId,
type: 'updateInput',
key: 'condition',
value: {
...conditionInput,
value: conditionInput.value === 'OR' ? 'AND' : 'OR'
}
});
}}
>
{condition}
<MyIcon ml={1} boxSize={5} name="change" />
</Flex>
<Box
w={'auto'}
flex={1}
height={'1px'}
style={{
background:
'linear-gradient(90deg, #E8EBF0 0%, rgba(232, 235, 240, 0.00) 100%)'
}}
></Box>
</Flex>
)}
{/* condition list */}
<Flex gap={2} alignItems={'center'}>
{/* variable reference */}
<Box minW={'250px'}>
<Reference
nodeId={nodeId}
variable={item.variable}
onSelect={(e) => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === i) {
return {
...ifElse,
variable: e
};
}
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 === i) {
return {
...ifElse,
condition: e
};
}
return ifElse;
})
);
}}
/>
</Box>
{/* value */}
<Box w={'200px'}>
<ConditionValueInput
value={item.value}
condition={item.condition}
variable={item.variable}
onChange={(e) => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === i) {
return {
...ifElse,
value: e
};
}
return ifElse;
})
);
}}
/>
</Box>
{/* delete */}
{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 !== i));
}}
/>
)}
</Flex>
</Box>
);
})}
</Box>
{RenderAddCondition}
</Box>
<Box px={6} mb={4}>
<RenderOutput nodeId={nodeId} flowOutputList={[outputs[1]]} />
</Box>
</NodeCard>
);
};
export default React.memo(NodeIfElse);
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 { nodeList } = useFlowProviderStore();
// get condition 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 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.tools
)
return arrayConditionList;
if (valueType === WorkflowIOValueTypeEnum.any) return allConditionList;
return [];
}, [valueType]);
return (
<MySelect
w={'100%'}
list={conditionList}
value={condition}
onchange={onSelect}
placeholder="选择条件"
></MySelect>
);
};
/*
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 { nodeList } = useFlowProviderStore();
// 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]);
if (valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<MySelect
list={[
{ label: 'True', value: 'true' },
{ label: 'False', value: 'false' }
]}
onchange={onChange}
value={value}
placeholder={'选择值'}
/>
);
} else {
return (
<MyInput
value={value}
placeholder={'输入值'}
w={'100%'}
isDisabled={
condition === VariableConditionEnum.isEmpty ||
condition === VariableConditionEnum.isNotEmpty
}
onChange={(e) => onChange(e.target.value)}
/>
);
}
};

View File

@@ -1,11 +1,11 @@
import React, { useCallback, useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Container from '../modules/Container';
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 { ModuleIOValueTypeEnum, ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { onChangeNode, useFlowProviderStore } from '../../FlowProvider';
import { WorkflowIOValueTypeEnum, NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useFlowProviderStore } from '../FlowProvider';
import { useTranslation } from 'next-i18next';
import { getLafAppDetail } from '@/web/support/laf/api';
import MySelect from '@fastgpt/web/components/common/MySelect';
@@ -19,25 +19,32 @@ import dynamic from 'next/dynamic';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/module/node/constant';
} from '@fastgpt/global/core/workflow/node/constant';
import { useToast } from '@fastgpt/web/hooks/useToast';
import Divider from '../modules/Divider';
import RenderToolInput from '../render/RenderToolInput';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
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';
const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccountModal'));
const NodeLaf = (props: NodeProps<FlowModuleItemType>) => {
const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { toast } = useToast();
const { feConfigs } = useSystemStore();
const { data, selected } = props;
const { moduleId, inputs, outputs } = data;
const { nodeId, inputs, outputs } = data;
const requestUrl = inputs.find((item) => item.key === ModuleInputKeyEnum.httpReqUrl);
const { onChangeNode } = useFlowProviderStore();
const requestUrl = inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl);
const { userInfo } = useUserStore();
@@ -127,13 +134,14 @@ const NodeLaf = (props: NodeProps<FlowModuleItemType>) => {
// update intro
if (lafFunction.description) {
onChangeNode({
moduleId,
nodeId,
type: 'attr',
key: 'intro',
value: lafFunction.description
});
}
// add input variables
const bodyParams =
lafFunction?.request?.content?.['application/json']?.schema?.properties || {};
@@ -150,35 +158,29 @@ const NodeLaf = (props: NodeProps<FlowModuleItemType>) => {
}))
].filter((item) => !inputs.find((input) => input.key === item.name));
// add params
allParams.forEach((param) => {
onChangeNode({
moduleId,
type: 'addInput',
const newInput: FlowNodeInputItemType = {
key: param.name,
value: {
key: param.name,
valueType: ModuleIOValueTypeEnum.string,
label: param.name,
type: FlowNodeInputTypeEnum.target,
required: param.required,
description: param.desc || '',
toolDescription: param.desc || '未设置参数描述',
edit: true,
editField: {
key: true,
name: true,
description: true,
required: true,
dataType: true,
inputType: true,
isToolInput: true
},
connected: false
valueType: WorkflowIOValueTypeEnum.string,
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 =
@@ -193,26 +195,19 @@ const NodeLaf = (props: NodeProps<FlowModuleItemType>) => {
}))
].filter((item) => !outputs.find((output) => output.key === item.name));
allResponseParams.forEach((param) => {
onChangeNode({
moduleId,
type: 'addOutput',
const newOutput: FlowNodeOutputItemType = {
id: getNanoid(),
key: param.name,
value: {
key: param.name,
valueType: param.valueType,
label: param.name,
type: FlowNodeOutputTypeEnum.source,
required: param.required,
description: param.desc || '',
edit: true,
editField: {
key: true,
description: true,
dataType: true,
defaultValue: true
},
targets: []
}
valueType: param.valueType,
label: param.name,
type: FlowNodeOutputTypeEnum.dynamic,
required: param.required,
description: param.desc || ''
};
onChangeNode({
nodeId,
type: 'addOutput',
value: newOutput
});
});
},
@@ -229,9 +224,9 @@ const NodeLaf = (props: NodeProps<FlowModuleItemType>) => {
placeholder={t('core.module.laf.Select laf function')}
onchange={(e) => {
onChangeNode({
moduleId,
nodeId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpReqUrl,
key: NodeInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: e
@@ -295,33 +290,32 @@ const ConfigLaf = () => {
);
};
const RenderIO = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const RenderIO = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
const { splitToolInputs, hasToolNode } = useFlowProviderStore();
const { commonInputs, toolInputs } = splitToolInputs(inputs, moduleId);
const { nodeId, inputs, outputs } = data;
const { splitToolInputs } = useFlowProviderStore();
const { commonInputs, toolInputs, isTool } = splitToolInputs(inputs, nodeId);
return (
<>
{hasToolNode && (
{isTool && (
<>
<Divider text={t('core.module.tool.Tool input')} />
<Container>
<RenderToolInput moduleId={moduleId} inputs={toolInputs} canEdit />
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} canEdit />
</Container>
</>
)}
<>
<Divider text={t('common.Input')} />
<Container>
<Box mb={3}>Body参数</Box>
<RenderInput moduleId={moduleId} flowInputList={commonInputs} />
<IOTitle text={t('common.Input')} />
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
</Container>
</>
<>
<Divider text={t('common.Output')} />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
<IOTitle text={t('common.Output')} />
<RenderOutput flowOutputList={outputs} nodeId={nodeId} />
</Container>
</>
</>

View File

@@ -0,0 +1,239 @@
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 dynamic from 'next/dynamic';
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 { useFlowProviderStore } from '../FlowProvider';
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';
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 } = useFlowProviderStore();
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({
inputType: input.renderTypeList[0],
valueType: input.valueType,
key: input.key,
label: input.label,
description: input.description,
isToolInput: !!input.toolDescription,
defaultValue: input.defaultValue,
maxLength: input.maxLength,
max: input.max,
min: input.min,
dynamicParamDefaultValue: input.dynamicParamDefaultValue
});
}}
onEdit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !editField?.key) return;
const output = outputs.find((output) => output.key === editField.key);
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
};
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);

View File

@@ -0,0 +1,118 @@
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,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import Container from '../components/Container';
import { EditInputFieldMapType, EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
import {
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { useFlowProviderStore } from '../FlowProvider';
import RenderInput from './render/RenderInput';
import { getNanoid } from '@fastgpt/global/common/string/tools';
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, outputs } = data;
const { onChangeNode } = useFlowProviderStore();
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
};
const newOutput: FlowNodeOutputItemType = {
id: getNanoid(),
key: data.key,
valueType: data.valueType,
label: data.label,
type: FlowNodeOutputTypeEnum.static
};
onChangeNode({
nodeId,
type: 'addInput',
value: newInput
});
setCreateField(undefined);
}}
/>
)}
</NodeCard>
);
};
export default React.memo(NodePluginOutput);

View File

@@ -0,0 +1,56 @@
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 { useFlowProviderStore } from '../FlowProvider';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import IOTitle from '../components/IOTitle';
const NodeSimple = ({
data,
selected,
minW = '350px',
maxW
}: NodeProps<FlowNodeItemType> & { minW?: string | number; maxW?: string | number }) => {
const { t } = useTranslation();
const { splitToolInputs } = useFlowProviderStore();
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);

View File

@@ -0,0 +1,237 @@
import React, { useCallback, useMemo, useTransition } from 'react';
import { NodeProps } from 'reactflow';
import { Box, Flex, Textarea, useTheme } from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { FlowNodeItemType, StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { welcomeTextTip } from '@fastgpt/global/core/workflow/template/tip';
import type { VariableItemType } from '@fastgpt/global/core/app/type.d';
import QGSwitch from '@/components/core/app/QGSwitch';
import TTSSelect from '@/components/core/app/TTSSelect';
import WhisperConfig from '@/components/core/app/WhisperConfig';
import { splitGuideModule } from '@fastgpt/global/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import { TTSTypeEnum } from '@/constants/app';
import { useFlowProviderStore } from '../FlowProvider';
import VariableEdit from '../../../app/VariableEdit';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import NodeCard from './render/NodeCard';
import ScheduledTriggerConfig from '@/components/core/app/ScheduledTriggerConfig';
const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const theme = useTheme();
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 data={data} />
<Box pt={4} pb={2}>
<ChatStartVariable data={data} />
</Box>
<Box pt={3} borderTop={theme.borders.base}>
<TTSGuide data={data} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<WhisperGuide data={data} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<QuestionGuide data={data} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<ScheduledTrigger data={data} />
</Box>
</Box>
</NodeCard>
</>
);
};
export default React.memo(NodeUserGuide);
function WelcomeText({ data }: { data: FlowNodeItemType }) {
const { t } = useTranslation();
const { inputs, nodeId } = data;
const [, startTst] = useTransition();
const { onChangeNode } = useFlowProviderStore();
const welcomeText = inputs.find((item) => item.key === NodeInputKeyEnum.welcomeText);
return (
<>
<Flex mb={1} alignItems={'center'}>
<MyIcon name={'core/modules/welcomeText'} mr={2} w={'14px'} color={'#E74694'} />
<Box fontWeight={'medium'}>{t('core.app.Welcome Text')}</Box>
<MyTooltip label={t(welcomeTextTip)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
</Flex>
{welcomeText && (
<Textarea
className="nodrag"
rows={6}
fontSize={'12px'}
resize={'both'}
defaultValue={welcomeText.value}
bg={'myWhite.500'}
placeholder={t(welcomeTextTip)}
onChange={(e) => {
startTst(() => {
onChangeNode({
nodeId,
key: NodeInputKeyEnum.welcomeText,
type: 'updateInput',
value: {
...welcomeText,
value: e.target.value
}
});
});
}}
/>
)}
</>
);
}
function ChatStartVariable({ data }: { data: FlowNodeItemType }) {
const { inputs, nodeId } = data;
const { onChangeNode } = useFlowProviderStore();
const variables = useMemo(
() =>
(inputs.find((item) => item.key === NodeInputKeyEnum.variables)
?.value as VariableItemType[]) || [],
[inputs]
);
const updateVariables = useCallback(
(value: VariableItemType[]) => {
onChangeNode({
nodeId,
key: NodeInputKeyEnum.variables,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === NodeInputKeyEnum.variables),
value
}
});
},
[inputs, nodeId, onChangeNode]
);
return <VariableEdit variables={variables} onChange={(e) => updateVariables(e)} />;
}
function QuestionGuide({ data }: { data: FlowNodeItemType }) {
const { inputs, nodeId } = data;
const { onChangeNode } = useFlowProviderStore();
const questionGuide = useMemo(
() =>
(inputs.find((item) => item.key === NodeInputKeyEnum.questionGuide)?.value as boolean) ||
false,
[inputs]
);
return (
<QGSwitch
isChecked={questionGuide}
size={'md'}
onChange={(e) => {
const value = e.target.checked;
onChangeNode({
nodeId,
key: NodeInputKeyEnum.questionGuide,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === NodeInputKeyEnum.questionGuide),
value
}
});
}}
/>
);
}
function TTSGuide({ data }: { data: FlowNodeItemType }) {
const { inputs, nodeId } = data;
const { onChangeNode } = useFlowProviderStore();
const { ttsConfig } = splitGuideModule({ inputs } as StoreNodeItemType);
return (
<TTSSelect
value={ttsConfig}
onChange={(e) => {
onChangeNode({
nodeId,
key: NodeInputKeyEnum.tts,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === NodeInputKeyEnum.tts),
value: e
}
});
}}
/>
);
}
function WhisperGuide({ data }: { data: FlowNodeItemType }) {
const { inputs, nodeId } = data;
const { onChangeNode } = useFlowProviderStore();
const { ttsConfig, whisperConfig } = splitGuideModule({ inputs } as StoreNodeItemType);
return (
<WhisperConfig
isOpenAudio={ttsConfig.type !== TTSTypeEnum.none}
value={whisperConfig}
onChange={(e) => {
onChangeNode({
nodeId,
key: NodeInputKeyEnum.whisper,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === NodeInputKeyEnum.whisper),
value: e
}
});
}}
/>
);
}
function ScheduledTrigger({ data }: { data: FlowNodeItemType }) {
const { inputs, nodeId } = data;
const { onChangeNode } = useFlowProviderStore();
const { scheduledTriggerConfig } = splitGuideModule({ inputs } as StoreNodeItemType);
return (
<ScheduledTriggerConfig
value={scheduledTriggerConfig}
onChange={(e) => {
onChangeNode({
nodeId,
key: NodeInputKeyEnum.scheduleTrigger,
type: 'updateInput',
value: {
...inputs.find((item) => item.key === NodeInputKeyEnum.scheduleTrigger),
value: e
}
});
}}
/>
);
}

View File

@@ -0,0 +1,39 @@
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';
const NodeTools = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs } = data;
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<Container>
<IOTitle text={t('common.Input')} />
<RenderInput nodeId={nodeId} flowInputList={inputs} />
</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);

View File

@@ -0,0 +1,33 @@
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 RenderOutput from './render/RenderOutput';
import IOTitle from '../components/IOTitle';
import { useTranslation } from 'next-i18next';
const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, outputs } = data;
return (
<NodeCard
minW={'240px'}
selected={selected}
menuForbid={{
rename: true,
copy: true,
delete: true
}}
{...data}
>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'} textAlign={'end'}>
<IOTitle text={t('common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeStart);

View File

@@ -0,0 +1,502 @@
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';
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 { 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 dataTypeSelectList = Object.values(FlowValueTypeMap)
.slice(0, -2)
.map((item) => ({
label: t(item.label),
value: item.value
}));
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 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, keys, onSubmit, showValueTypeSelect, t, toast]
);
const onSubmitError = useCallback(
(e: Object) => {
console.log(e);
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')}
onClose={onClose}
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'}>
<Box flex={'0 0 70px'}>{t('core.module.Input Type')}</Box>
<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'}>
<Box flex={'0 0 70px'}>{t('core.module.Data Type')}</Box>
<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'}>
<Box flex={'0 0 70px'}>{t('core.module.Field Name')}</Box>
<Input
bg={'myGray.50'}
placeholder="appointment/sql"
{...register('key', {
required: true
})}
/>
</Flex>
)}
{showDescriptionInput && (
<Box alignItems={'flex-start'}>
<Box flex={'0 0 70px'} mb={'1px'}>
{t('core.module.Field Description')}
</Box>
<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}>
{showToolInput && (
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}></Box>
<Switch {...register('isToolInput')} />
</Flex>
)}
{showValueTypeSelect && (
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Data Type')}</Box>
<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'}>
<Box flex={'0 0 70px'}>{t('core.module.Default Value')}</Box>
{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'}>
<Box flex={'0 0 70px'}>{t('core.module.Max Length')}</Box>
<Input
bg={'myGray.50'}
placeholder={t('core.module.Max Length placeholder')}
{...register('maxLength')}
/>
</Flex>
)}
{showMinMaxInput && (
<>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Max Value')}</Box>
<Input bg={'myGray.50'} type={'number'} {...register('max')} />
</Flex>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Min Value')}</Box>
<Input bg={'myGray.50'} type={'number'} {...register('min')} />
</Flex>
</>
)}
{showDynamicInput && (
<Stack gap={5}>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Input Type')}</Box>
<Box flex={1} fontWeight={'bold'}>
{t('core.workflow.inputType.Reference')}
</Box>
</Flex>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Data Type')}</Box>
<Box flex={1}>
<MySelect
list={dataTypeSelectList}
value={defaultInputValueType}
onchange={(e) => {
setValue(
'dynamicParamDefaultValue.valueType',
e as WorkflowIOValueTypeEnum
);
}}
/>
</Box>
</Flex>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.workflow.inputType.Required')}</Box>
<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);

View File

@@ -0,0 +1,182 @@
import React, { useMemo } from 'react';
import { Position } from 'reactflow';
import { useFlowProviderStore } from '../../../FlowProvider';
import { SourceHandle, TargetHandle } from '.';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
export const ConnectionSourceHandle = ({ nodeId }: { nodeId: string }) => {
const { nodeList, edges, connectingEdge } = useFlowProviderStore();
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 { nodeList, connectingEdge } = useFlowProviderStore();
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 <></>;

View File

@@ -0,0 +1,126 @@
import MyTooltip from '@/components/MyTooltip';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import { Box, BoxProps } from '@chakra-ui/react';
import {
WorkflowIOValueTypeEnum,
NodeOutputKeyEnum
} from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { Connection, Handle, Position } from 'reactflow';
import { useFlowProviderStore } from '../../../FlowProvider';
import { useCallback, useMemo } from 'react';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
const handleSize = '14px';
type ToolHandleProps = BoxProps & {
nodeId: string;
};
export const ToolTargetHandle = ({ nodeId }: ToolHandleProps) => {
const { t } = useTranslation();
const { connectingEdge, edges } = useFlowProviderStore();
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 valueTypeMap = FlowValueTypeMap[WorkflowIOValueTypeEnum.tools];
const Render = useMemo(() => {
return (
<MyTooltip
label={t('app.module.type', {
type: t(valueTypeMap?.label),
description: valueTypeMap?.description
})}
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={hidden ? 'hidden' : 'visible'}
/>
</Handle>
</MyTooltip>
);
}, [handleId, hidden, t, valueTypeMap?.description, valueTypeMap?.label]);
return Render;
};
export const ToolSourceHandle = ({ nodeId }: ToolHandleProps) => {
const { t } = useTranslation();
const { setEdges } = useFlowProviderStore();
const valueTypeMap = FlowValueTypeMap[WorkflowIOValueTypeEnum.tools];
/* 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('app.module.type', {
type: t(valueTypeMap?.label),
description: valueTypeMap?.description
})}
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, valueTypeMap?.description, valueTypeMap?.label]);
return Render;
};

View File

@@ -0,0 +1,237 @@
import React, { useMemo } from 'react';
import { Handle, Position } from 'reactflow';
import { useFlowProviderStore } from '../../../FlowProvider';
import { SmallAddIcon } from '@chakra-ui/icons';
import { handleHighLightStyle, sourceCommonStyle, handleConnectedStyle, handleSize } from './style';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
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 { nodes, hoverNodeId, edges, connectingEdge } = useFlowProviderStore();
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 { nodeList, edges, connectingEdge } = useFlowProviderStore();
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;
// Same source node
if (connectedEdges.some((item) => item.sourceHandle === connectingEdge?.handleId)) 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 <></>;

View File

@@ -0,0 +1,25 @@
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'
};

View File

@@ -0,0 +1,589 @@
import React, { useCallback, useMemo } 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 { useFlowProviderStore } from '../../FlowProvider';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { useSystemStore } from '@/web/common/system/useSystemStore';
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/WholeResponseModal';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { getPreviewPluginModule } from '@/web/core/plugin/api';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { storeNode2FlowNode } from '@/web/core/workflow/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
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 {
children,
avatar = LOGO_ICON,
name = t('core.module.template.UnKnow Module'),
intro,
minW = '300px',
maxW = '600px',
nodeId,
flowNodeType,
inputs,
selected,
menuForbid,
isTool = false,
isError = false,
debugResult,
pluginId
} = props;
const { nodeList, setHoverNodeId, onUpdateNodeError } = useFlowProviderStore();
const showToolHandle = useMemo(
() => isTool && !!nodeList.find((item) => item?.flowNodeType === FlowNodeTypeEnum.tools),
[isTool, nodeList]
);
/* Node header */
const Header = useMemo(() => {
return (
<Box position={'relative'}>
{/* debug */}
<NodeDebugResponse nodeId={nodeId} debugResult={debugResult} />
<Box className="custom-drag-handle" 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={'lg'} fontWeight={'medium'}>
{t(name)}
</Box>
</Flex>
<MenuRender
name={name}
nodeId={nodeId}
pluginId={pluginId}
flowNodeType={flowNodeType}
inputs={inputs}
menuForbid={menuForbid}
/>
<NodeIntro nodeId={nodeId} intro={intro} />
</Box>
</Box>
);
}, [
nodeId,
debugResult,
showToolHandle,
avatar,
t,
name,
pluginId,
flowNodeType,
inputs,
menuForbid,
intro
]);
const Render = useMemo(() => {
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'
}
}}
onMouseEnter={() => setHoverNodeId(nodeId)}
onMouseLeave={() => setHoverNodeId(undefined)}
{...(isError
? {
borderColor: 'red.500',
onMouseDownCapture: () => onUpdateNodeError(nodeId, false)
}
: {
borderColor: selected ? 'primary.600' : 'borderColor.base'
})}
>
{Header}
{children}
<ConnectionSourceHandle nodeId={nodeId} />
<ConnectionTargetHandle nodeId={nodeId} />
</Box>
);
}, [Header, children, isError, maxW, minW, nodeId, onUpdateNodeError, selected, setHoverNodeId]);
return Render;
};
export default React.memo(NodeCard);
const MenuRender = React.memo(function MenuRender({
name,
nodeId,
pluginId,
flowNodeType,
inputs,
menuForbid
}: {
name: string;
nodeId: string;
pluginId?: string;
flowNodeType: Props['flowNodeType'];
inputs: Props['inputs'];
menuForbid?: Props['menuForbid'];
}) {
const { t } = useTranslation();
const { toast } = useToast();
const { setLoading } = useSystemStore();
const { openDebugNode, DebugInputModal } = useDebug();
const { openConfirm: onOpenConfirmSync, ConfirmModal: ConfirmSyncModal } = useConfirm({
content: t('module.Confirm Sync Plugin')
});
// custom title edit
const { onOpenModal: onOpenCustomTitleModal, EditModal: EditTitleModal } = useEditTitle({
title: t('common.Custom Title'),
placeholder: t('app.module.Custom Title Tip') || ''
});
const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({
content: t('core.module.Confirm Delete Node'),
type: 'delete'
});
const { setNodes, setEdges, onResetNode, onChangeNode } = useFlowProviderStore();
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
};
return state.concat(
storeNode2FlowNode({
item: {
name: template.name,
intro: template.intro,
nodeId: getNanoid(),
position: { x: node.position.x + 200, y: node.position.y + 50 },
flowNodeType: template.flowNodeType,
showStatus: template.showStatus,
inputs: template.inputs,
outputs: template.outputs
}
})
);
});
},
[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 })
}
]),
...(flowNodeType === FlowNodeTypeEnum.pluginModule
? [
{
icon: 'common/refreshLight',
label: t('plugin.Synchronous version'),
variant: 'whiteBase',
onClick: () => {
if (!pluginId) return;
onOpenConfirmSync(async () => {
try {
setLoading(true);
const pluginModule = await getPreviewPluginModule(pluginId);
onResetNode({
id: nodeId,
module: pluginModule
});
} catch (e) {
return toast({
status: 'error',
title: getErrText(e, t('plugin.Get Plugin Module Detail Failed'))
});
}
setLoading(false);
})();
}
}
]
: []),
...(menuForbid?.rename
? []
: [
{
icon: 'edit',
label: t('common.Rename'),
variant: 'whiteBase',
onClick: () =>
onOpenCustomTitleModal({
defaultVal: name,
onSuccess: (e) => {
if (!e) {
return toast({
title: t('app.modules.Title is required'),
status: 'warning'
});
}
onChangeNode({
nodeId,
type: 'attr',
key: 'name',
value: e
});
}
})
}
]),
...(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>
<EditTitleModal maxLength={20} />
<ConfirmSyncModal />
<ConfirmDeleteModal />
<DebugInputModal />
</>
);
}, [
ConfirmDeleteModal,
ConfirmSyncModal,
DebugInputModal,
EditTitleModal,
flowNodeType,
menuForbid?.copy,
menuForbid?.debug,
menuForbid?.delete,
menuForbid?.rename,
name,
nodeId,
onChangeNode,
onCopyNode,
onDelNode,
onOpenConfirmDeleteNode,
onOpenConfirmSync,
onOpenCustomTitleModal,
onResetNode,
openDebugNode,
pluginId,
setLoading,
t,
toast
]);
return Render;
});
const NodeIntro = React.memo(function NodeIntro({
nodeId,
intro = ''
}: {
nodeId: string;
intro?: string;
}) {
const { t } = useTranslation();
const { onChangeNode, splitToolInputs } = useFlowProviderStore();
const moduleIsTool = 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>
{moduleIsTool && (
<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, moduleIsTool, nodeId, onChangeNode, onOpenIntroModal, t]);
return Render;
});
const NodeDebugResponse = React.memo(function NodeDebugResponse({
nodeId,
debugResult
}: {
nodeId: string;
debugResult: FlowNodeItemType['debugResult'];
}) {
const { t } = useTranslation();
const { onChangeNode, onStopNodeDebug, onNextNodeDebug, workflowDebugData } =
useFlowProviderStore();
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
position={'absolute'}
right={'-430px'}
top={0}
zIndex={10}
w={'420px'}
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}</>;
});

View File

@@ -0,0 +1,217 @@
import {
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io.d';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useFlowProviderStore } from '../../../FlowProvider';
import { Box, Flex } from '@chakra-ui/react';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
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 { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import ValueTypeLabel from '../ValueTypeLabel';
const FieldEditModal = dynamic(() => import('../FieldEditModal'));
type Props = {
nodeId: string;
input: FlowNodeInputItemType;
mode?: 'app' | 'plugin';
};
const InputLabel = ({ nodeId, input }: Props) => {
const { t } = useTranslation();
const { onChangeNode } = useFlowProviderStore();
const {
description,
toolDescription,
required,
label,
selectedTypeIndex,
renderTypeList,
valueType,
canEdit,
key,
value
} = input;
const [editField, setEditField] = useState<EditNodeFieldType>();
const valueTypeLabel = useMemo(
() => (valueType ? t(FlowValueTypeMap[valueType]?.label) : ''),
[t, valueType]
);
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'}>
<Box position={'relative'} fontWeight={'medium'} color={'myGray.600'}>
{required && (
<Box position={'absolute'} left={-2} top={-1} color={'red.600'}>
*
</Box>
)}
{t(label)}
{description && (
<MyTooltip label={t(description)} forceShow>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</MyTooltip>
)}
</Box>
{/* value type */}
{renderType === FlowNodeInputTypeEnum.reference && !!valueTypeLabel && (
<ValueTypeLabel>{valueTypeLabel}</ValueTypeLabel>
)}
{/* edit config */}
{canEdit && (
<>
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
color={'myGray.600'}
_hover={{ color: 'primary.500' }}
onClick={() =>
setEditField({
inputType: renderTypeList[0],
valueType: valueType,
key,
label,
description,
isToolInput: !!toolDescription,
defaultValue: input.defaultValue,
maxLength: input.maxLength,
max: input.max,
min: input.min,
dynamicParamDefaultValue: input.dynamicParamDefaultValue
})
}
/>
<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,
valueTypeLabel
]);
return RenderLabel;
};
export default React.memo(InputLabel);

View File

@@ -1,18 +1,20 @@
import React, { useMemo } from 'react';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { Box } from '@chakra-ui/react';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import dynamic from 'next/dynamic';
import InputLabel from './Label';
import type { RenderInputProps } from './type.d';
import { useFlowProviderStore } from '../../../FlowProvider';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import type { RenderInputProps } from './type';
const RenderList: {
types: `${FlowNodeInputTypeEnum}`[];
types: FlowNodeInputTypeEnum[];
Component: React.ComponentType<RenderInputProps>;
}[] = [
{
types: [FlowNodeInputTypeEnum.reference],
Component: dynamic(() => import('./templates/Reference'))
},
{
types: [FlowNodeInputTypeEnum.input],
Component: dynamic(() => import('./templates/TextInput'))
@@ -29,14 +31,6 @@ const RenderList: {
types: [FlowNodeInputTypeEnum.textarea],
Component: dynamic(() => import('./templates/Textarea'))
},
{
types: [FlowNodeInputTypeEnum.select],
Component: dynamic(() => import('./templates/Select'))
},
{
types: [FlowNodeInputTypeEnum.slider],
Component: dynamic(() => import('./templates/Slider'))
},
{
types: [FlowNodeInputTypeEnum.selectApp],
Component: dynamic(() => import('./templates/SelectApp'))
@@ -70,67 +64,44 @@ const RenderList: {
Component: dynamic(() => import('./templates/SettingQuotePrompt'))
}
];
const UserChatInput = dynamic(() => import('./templates/UserChatInput'));
const hideLabelTypeList = [FlowNodeInputTypeEnum.addInputParam];
type Props = {
flowInputList: FlowNodeInputItemType[];
moduleId: string;
nodeId: string;
CustomComponent?: Record<string, (e: FlowNodeInputItemType) => React.ReactNode>;
mb?: number;
};
const RenderInput = ({ flowInputList, moduleId, CustomComponent }: Props) => {
const { mode } = useFlowProviderStore();
const sortInputs = useMemo(
() =>
JSON.stringify(
[...flowInputList].sort((a, b) => {
if (a.type === FlowNodeInputTypeEnum.addInputParam) {
return 1;
}
if (b.type === FlowNodeInputTypeEnum.addInputParam) {
return -1;
}
if (a.type === FlowNodeInputTypeEnum.switch) {
return -1;
}
return 0;
})
),
[flowInputList]
);
const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props) => {
const copyInputs = useMemo(() => JSON.stringify(flowInputList), [flowInputList]);
const filterInputs = useMemo(() => {
const parseSortInputs = JSON.parse(sortInputs) as FlowNodeInputItemType[];
const parseSortInputs = JSON.parse(copyInputs) as FlowNodeInputItemType[];
return parseSortInputs.filter((input) => {
if (mode === 'app' && input.hideInApp) return false;
if (mode === 'plugin' && input.hideInPlugin) return false;
return true;
});
}, [mode, sortInputs]);
}, [copyInputs]);
const memoCustomComponent = useMemo(() => CustomComponent || {}, [CustomComponent]);
const Render = useMemo(() => {
return filterInputs.map((input) => {
const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0];
const RenderComponent = (() => {
if (input.type === FlowNodeInputTypeEnum.custom && memoCustomComponent[input.key]) {
if (renderType === FlowNodeInputTypeEnum.custom && memoCustomComponent[input.key]) {
return <>{memoCustomComponent[input.key]({ ...input })}</>;
}
const Component = RenderList.find((item) => item.types.includes(input.type))?.Component;
const Component = RenderList.find((item) => item.types.includes(renderType))?.Component;
if (!Component) return null;
return <Component inputs={filterInputs} item={input} moduleId={moduleId} />;
return <Component inputs={filterInputs} item={input} nodeId={nodeId} />;
})();
return input.type !== FlowNodeInputTypeEnum.hidden ? (
<Box key={input.key} _notLast={{ mb: 7 }} position={'relative'}>
{input.key === ModuleInputKeyEnum.userChatInput && (
<UserChatInput inputs={filterInputs} item={input} moduleId={moduleId} />
)}
{!!input.label && (
<InputLabel moduleId={moduleId} inputKey={input.key} mode={mode} {...input} />
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'}>
@@ -140,7 +111,7 @@ const RenderInput = ({ flowInputList, moduleId, CustomComponent }: Props) => {
</Box>
) : null;
});
}, [filterInputs, memoCustomComponent, mode, moduleId]);
}, [filterInputs, mb, memoCustomComponent, nodeId]);
return <>{Render}</>;
};

View File

@@ -0,0 +1,110 @@
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 { useFlowProviderStore } from '../../../../FlowProvider';
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';
const FieldEditModal = dynamic(() => import('../../FieldEditModal'));
const AddInputParam = (props: RenderInputProps) => {
const { item, inputs, nodeId } = props;
const { t } = useTranslation();
const { onChangeNode, mode } = useFlowProviderStore();
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'}
mr={'-5px'}
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);

Some files were not shown because too many files have changed in this diff Show More