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:
@@ -14,7 +14,7 @@ const Avatar = ({ w = '30px', src, ...props }: ImageProps) => {
|
||||
w={w}
|
||||
h={w}
|
||||
p={'1px'}
|
||||
src={src}
|
||||
src={src || LOGO_ICON}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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 = [],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'} />
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
@@ -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,
|
||||
@@ -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>
|
||||
|
||||
340
projects/app/src/components/core/app/ScheduledTriggerConfig.tsx
Normal file
340
projects/app/src/components/core/app/ScheduledTriggerConfig.tsx
Normal 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);
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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'} />
|
||||
|
||||
@@ -33,7 +33,7 @@ const DatasetSelectContainer = ({
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
iconSrc="/imgs/module/db.png"
|
||||
iconSrc="/imgs/workflow/db.png"
|
||||
title={
|
||||
<Box fontWeight={'normal'}>
|
||||
<ParentPaths
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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}
|
||||
/> */}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
682
projects/app/src/components/core/workflow/Flow/FlowProvider.tsx
Normal file
682
projects/app/src/components/core/workflow/Flow/FlowProvider.tsx
Normal 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);
|
||||
});
|
||||
@@ -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({
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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'}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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);
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
283
projects/app/src/components/core/workflow/Flow/index.tsx
Normal file
283
projects/app/src/components/core/workflow/Flow/index.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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']}
|
||||
@@ -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
|
||||
});
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
</>
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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 <></>;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 <></>;
|
||||
@@ -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'
|
||||
};
|
||||
@@ -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}</>;
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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}</>;
|
||||
};
|
||||
@@ -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
Reference in New Issue
Block a user