Concat plugin to app (#1799)

This commit is contained in:
Archer
2024-06-19 14:38:21 +08:00
committed by GitHub
parent b17d14bb7d
commit 565bfc8486
220 changed files with 5018 additions and 4667 deletions

View File

@@ -29,10 +29,12 @@ export default function InputGuideBox({
const { data = [] } = useRequest2(
async () => {
if (!text) return [];
// More than 20 characters, it's basically meaningless
if (text.length > 20) return [];
return await queryChatInputGuideList(
{
appId,
searchKey: text.slice(0, 50),
searchKey: text,
...outLinkAuthData
},
chatInputGuide.customUrl ? chatInputGuide.customUrl : undefined

View File

@@ -79,7 +79,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
return (
<>
<Box h={'100%'} bg={'myGray.100'}>
{isPc === true && (
{isPc ? (
<>
{isHideNavbar ? (
<Auth>{children}</Auth>
@@ -94,8 +94,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
</>
)}
</>
)}
{isPc === false && (
) : (
<>
<Box h={'100%'} display={['block', 'none']}>
{phoneUnShowLayoutRoute[router.pathname] || isChatPage ? (

View File

@@ -40,13 +40,6 @@ const Navbar = ({ unread }: { unread: number }) => {
link: `/app/list`,
activeLink: ['/app/list', '/app/detail']
},
{
label: t('navbar.Plugin'),
icon: 'common/navbar/pluginLight',
activeIcon: 'common/navbar/pluginFill',
link: `/plugin/list`,
activeLink: ['/plugin/list', '/plugin/edit']
},
{
label: t('navbar.Datasets'),
icon: 'core/dataset/datasetLight',

View File

@@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import { Box, Flex, Grid, Image } from '@chakra-ui/react';
import type { GridProps } from '@chakra-ui/react';
import type { FlexProps, GridProps } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -9,10 +9,11 @@ interface Props extends GridProps {
list: { id: string; icon?: string; label: string | React.ReactNode }[];
activeId: string;
size?: 'sm' | 'md' | 'lg';
inlineStyles?: FlexProps;
onChange: (id: string) => void;
}
const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
const Tabs = ({ list, size = 'md', activeId, onChange, inlineStyles, ...props }: Props) => {
const { t } = useTranslation();
const sizeMap = useMemo(() => {
switch (size) {
@@ -55,6 +56,7 @@ const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
borderBottom={'2px solid transparent'}
px={3}
whiteSpace={'nowrap'}
{...inlineStyles}
{...(activeId === item.id
? {
color: 'primary.600',

View File

@@ -91,7 +91,7 @@ const MyRadio = ({
</Box>
)}
</Box>
<Radio isChecked={value === item.value} />
{!hiddenCircle && <Radio isChecked={value === item.value} />}
</Flex>
))}
</Grid>

View File

@@ -1,48 +0,0 @@
import React from 'react';
import { Flex, Box, BoxProps, border } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
type Props = BoxProps & {
list: {
icon?: string;
label: string | React.ReactNode;
value: string;
}[];
value: string;
onChange: (e: string) => void;
};
const RowTabs = ({ list, value, onChange, py = '7px', px = '12px', ...props }: Props) => {
return (
<Box display={'inline-flex'} px={'3px'} {...props}>
{list.map((item) => (
<Flex
key={item.value}
flex={'1 0 0'}
alignItems={'center'}
cursor={'pointer'}
px={px}
py={py}
userSelect={'none'}
whiteSpace={'noWrap'}
borderBottom={'2px solid'}
{...(value === item.value
? {
bg: 'white',
color: 'primary.600',
borderColor: 'primary.600'
}
: {
borderColor: 'myGray.100',
onClick: () => onChange(item.value)
})}
>
{item.icon && <MyIcon name={item.icon as any} mr={1} w={'14px'} />}
<Box fontSize={'sm'}>{item.label}</Box>
</Flex>
))}
</Box>
);
};
export default RowTabs;

View File

@@ -1,4 +1,4 @@
import { Box, Flex } from '@chakra-ui/react';
import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import React, { useMemo } from 'react';
import { useTranslation } from 'next-i18next';
@@ -9,9 +9,17 @@ const FolderPath = (props: {
FirstPathDom?: React.ReactNode;
onClick: (parentId: string) => void;
fontSize?: string;
hoverStyle?: BoxProps;
}) => {
const { t } = useTranslation();
const { paths, rootName = t('common.folder.Root Path'), FirstPathDom, onClick, fontSize } = props;
const {
paths,
rootName = t('common.folder.Root Path'),
FirstPathDom,
onClick,
fontSize,
hoverStyle
} = props;
const concatPaths = useMemo(
() => [
@@ -43,9 +51,10 @@ const FolderPath = (props: {
}
: {
cursor: 'pointer',
color: 'myGray.600',
color: 'myGray.500',
_hover: {
bg: 'myGray.100'
bg: 'myGray.100',
...hoverStyle
},
onClick: () => {
onClick(item.parentId);

View File

@@ -13,10 +13,18 @@ const AppTypeTag = ({ type }: { type: AppTypeEnum }) => {
label: appT('type.Simple bot'),
icon: 'core/app/type/simple'
},
[AppTypeEnum.advanced]: {
[AppTypeEnum.workflow]: {
label: appT('type.Workflow bot'),
icon: 'core/app/type/workflow'
},
[AppTypeEnum.plugin]: {
label: appT('type.Plugin'),
icon: 'core/app/type/plugin'
},
[AppTypeEnum.httpPlugin]: {
label: appT('type.Http plugin'),
icon: 'core/app/type/httpPlugin'
},
[AppTypeEnum.folder]: undefined
});

View File

@@ -1,5 +1,5 @@
import { Box, Flex, TextareaProps } from '@chakra-ui/react';
import React from 'react';
import React, { useTransition } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import ChatFunctionTip from './Tip';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
@@ -8,6 +8,7 @@ import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const WelcomeTextConfig = (props: TextareaProps) => {
const { t } = useTranslation();
return (
<>
<Flex alignItems={'center'}>
@@ -27,4 +28,4 @@ const WelcomeTextConfig = (props: TextareaProps) => {
);
};
export default WelcomeTextConfig;
export default React.memo(WelcomeTextConfig);

View File

@@ -1,167 +0,0 @@
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import React, {
useMemo,
useCallback,
useRef,
forwardRef,
useImperativeHandle,
ForwardedRef
} from 'react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { Box, Flex, IconButton } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { streamFetch } from '@/web/common/api/fetch';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useUserStore } from '@/web/support/user/useUserStore';
import ChatBox from '@/components/ChatBox';
import type { ComponentRef, StartChatFnProps } from '@/components/ChatBox/type.d';
import {
checkChatSupportSelectFileByModules,
getAppQuestionGuidesByModules
} from '@/web/core/chat/utils';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import {
getDefaultEntryNodeIds,
getMaxHistoryLimitFromNodes,
initWorkflowEdgeStatus,
storeNodes2RuntimeNodes
} from '@fastgpt/global/core/workflow/runtime/utils';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
export type ChatTestComponentRef = {
resetChatTest: () => void;
};
const ChatTest = (
{
isOpen,
nodes = [],
edges = [],
onClose
}: {
isOpen: boolean;
nodes?: StoreNodeItemType[];
edges?: StoreEdgeItemType[];
onClose: () => void;
},
ref: ForwardedRef<ChatTestComponentRef>
) => {
const { t } = useTranslation();
const ChatBoxRef = useRef<ComponentRef>(null);
const { userInfo } = useUserStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const startChat = useCallback(
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
/* get histories */
let historyMaxLen = getMaxHistoryLimitFromNodes(nodes);
const history = chatList.slice(-historyMaxLen - 2, -2);
// 流请求,获取数据
const { responseText, responseData } = await streamFetch({
url: '/api/core/chat/chatTest',
data: {
history,
prompt: chatList[chatList.length - 2].value,
nodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
edges: initWorkflowEdgeStatus(edges),
variables,
appId: appDetail._id,
appName: `调试-${appDetail.name}`,
mode: 'test'
},
onMessage: generatingMessage,
abortCtrl: controller
});
return { responseText, responseData };
},
[appDetail._id, appDetail.name, edges, nodes]
);
useImperativeHandle(ref, () => ({
resetChatTest() {
ChatBoxRef.current?.resetHistory([]);
ChatBoxRef.current?.resetVariables();
}
}));
return (
<>
<Flex
zIndex={101}
flexDirection={'column'}
position={'absolute'}
top={5}
right={0}
h={isOpen ? '95%' : '0'}
w={isOpen ? ['100%', '460px'] : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'md'}
overflow={'hidden'}
transition={'.2s ease'}
>
<Flex py={4} px={5} whiteSpace={'nowrap'}>
<Box fontSize={'lg'} fontWeight={'bold'} flex={1}>
{t('core.chat.Debug test')}
</Box>
<MyTooltip label={t('core.chat.Restart')}>
<IconButton
className="chat"
size={'smSquare'}
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
variant={'whiteDanger'}
borderRadius={'md'}
aria-label={'delete'}
onClick={(e) => {
e.stopPropagation();
ChatBoxRef.current?.resetHistory([]);
ChatBoxRef.current?.resetVariables();
}}
/>
</MyTooltip>
<MyTooltip label={t('common.Close')}>
<IconButton
ml={[3, 6]}
icon={<SmallCloseIcon fontSize={'22px'} />}
variant={'grayBase'}
size={'smSquare'}
aria-label={''}
onClick={onClose}
/>
</MyTooltip>
</Flex>
<Box flex={1}>
<ChatBox
ref={ChatBoxRef}
appId={appDetail._id}
appAvatar={appDetail.avatar}
userAvatar={userInfo?.avatar}
showMarkIcon
chatConfig={appDetail.chatConfig}
showFileSelector={checkChatSupportSelectFileByModules(nodes)}
onStartChat={startChat}
onDelMessage={() => {}}
/>
</Box>
</Flex>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'fixed'}
top={0}
left={0}
bottom={0}
right={0}
onClick={onClose}
/>
</>
);
};
export default React.memo(forwardRef(ChatTest));

View File

@@ -1,60 +0,0 @@
import React, { useState } from 'react';
import { Textarea, Button, ModalBody, ModalFooter } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { useI18n } from '@/web/context/I18n';
type Props = {
onClose: () => void;
};
const ImportSettings = ({ onClose }: Props) => {
const { appT } = useI18n();
const { toast } = useToast();
const initData = useContextSelector(WorkflowContext, (v) => v.initData);
const [value, setValue] = useState('');
return (
<MyModal
isOpen
w={'600px'}
onClose={onClose}
iconSrc="/imgs/modal/params.svg"
title={appT('Import Configs')}
>
<ModalBody>
<Textarea
placeholder={appT('Paste Config')}
defaultValue={value}
rows={16}
onChange={(e) => setValue(e.target.value)}
/>
</ModalBody>
<ModalFooter>
<Button
variant="whiteBase"
onClick={() => {
if (!value) {
return onClose();
}
try {
const data = JSON.parse(value);
initData(data);
onClose();
} catch (error) {
toast({
title: appT('Import Configs Failed')
});
}
}}
>
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(ImportSettings);

View File

@@ -1,427 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import { Box, Flex, IconButton, Input, InputGroup, InputLeftElement, css } from '@chakra-ui/react';
import type {
FlowNodeTemplateType,
nodeTemplateListType
} from '@fastgpt/global/core/workflow/type/index.d';
import { useViewport, XYPosition } from 'reactflow';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import Avatar from '@/components/Avatar';
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getPreviewPluginModule } from '@/web/core/plugin/api';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { workflowNodeTemplateList } from '@fastgpt/web/core/workflow/constants';
import RowTabs from '@fastgpt/web/components/common/Tabs/RowTabs';
import { useWorkflowStore } from '@/web/core/workflow/store/workflow';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import ParentPaths from '@/components/common/ParentPaths';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRouter } from 'next/router';
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
import { useQuery } from '@tanstack/react-query';
import { debounce } from 'lodash';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { useCreation } from 'ahooks';
import { useI18n } from '@/web/context/I18n';
type ModuleTemplateListProps = {
isOpen: boolean;
onClose: () => void;
};
type RenderListProps = {
templates: FlowNodeTemplateType[];
onClose: () => void;
currentParent?: { parentId: string; parentName: string };
setCurrentParent: (e: { parentId: string; parentName: string }) => void;
};
enum TemplateTypeEnum {
'basic' = 'basic',
'systemPlugin' = 'systemPlugin',
'teamPlugin' = 'teamPlugin'
}
const sliderWidth = 390;
const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
const { t } = useTranslation();
const router = useRouter();
const [currentParent, setCurrentParent] = useState<RenderListProps['currentParent']>();
const [searchKey, setSearchKey] = useState('');
const { feConfigs } = useSystemStore();
const basicNodeTemplates = useContextSelector(WorkflowContext, (v) => v.basicNodeTemplates);
const hasToolNode = useContextSelector(WorkflowContext, (v) => v.hasToolNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const {
systemNodeTemplates,
loadSystemNodeTemplates,
teamPluginNodeTemplates,
loadTeamPluginNodeTemplates
} = useWorkflowStore();
const [templateType, setTemplateType] = useState(TemplateTypeEnum.basic);
const templates = useCreation(() => {
const map = {
[TemplateTypeEnum.basic]: basicNodeTemplates.filter((item) => {
// unique node filter
if (item.unique) {
const nodeExist = nodeList.some((node) => node.flowNodeType === item.flowNodeType);
if (nodeExist) {
return false;
}
}
// special node filter
if (item.flowNodeType === FlowNodeTypeEnum.lafModule && !feConfigs.lafEnv) {
return false;
}
// tool stop
if (!hasToolNode && item.flowNodeType === FlowNodeTypeEnum.stopTool) {
return false;
}
return true;
}),
[TemplateTypeEnum.systemPlugin]: systemNodeTemplates,
[TemplateTypeEnum.teamPlugin]: teamPluginNodeTemplates.filter((item) =>
searchKey ? item.pluginType !== PluginTypeEnum.folder : true
)
};
return map[templateType];
}, [
basicNodeTemplates,
feConfigs.lafEnv,
hasToolNode,
nodeList,
searchKey,
systemNodeTemplates,
teamPluginNodeTemplates,
templateType
]);
const { mutate: onChangeTab } = useRequest({
mutationFn: async (e: any) => {
const val = e as TemplateTypeEnum;
if (val === TemplateTypeEnum.systemPlugin) {
await loadSystemNodeTemplates();
} else if (val === TemplateTypeEnum.teamPlugin) {
await loadTeamPluginNodeTemplates({
parentId: currentParent?.parentId
});
}
setTemplateType(val);
},
errorToast: t('core.module.templates.Load plugin error')
});
useQuery(['teamNodeTemplate', currentParent?.parentId, searchKey], () =>
loadTeamPluginNodeTemplates({
parentId: currentParent?.parentId,
searchKey,
init: true
})
);
return (
<>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'absolute'}
top={0}
left={0}
bottom={0}
w={`${sliderWidth}px`}
onClick={onClose}
fontSize={'sm'}
/>
<Flex
zIndex={3}
flexDirection={'column'}
position={'absolute'}
top={'10px'}
left={0}
pt={'20px'}
pb={4}
h={isOpen ? 'calc(100% - 20px)' : '0'}
w={isOpen ? ['100%', `${sliderWidth}px`] : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'0 20px 20px 0'}
transition={'.2s ease'}
userSelect={'none'}
overflow={isOpen ? 'none' : 'hidden'}
>
<Box mb={2} pl={'20px'} pr={'10px'} whiteSpace={'nowrap'} overflow={'hidden'}>
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
<RowTabs
list={[
{
icon: 'core/modules/basicNode',
label: t('core.module.template.Basic Node'),
value: TemplateTypeEnum.basic
},
{
icon: 'core/modules/systemPlugin',
label: t('core.module.template.System Plugin'),
value: TemplateTypeEnum.systemPlugin
},
{
icon: 'core/modules/teamPlugin',
label: t('core.module.template.Team Plugin'),
value: TemplateTypeEnum.teamPlugin
}
]}
py={'5px'}
value={templateType}
onChange={onChangeTab}
/>
{/* close icon */}
<IconButton
size={'sm'}
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.700'} />}
borderColor={'myGray.300'}
variant={'grayBase'}
aria-label={''}
onClick={onClose}
/>
</Flex>
{templateType === TemplateTypeEnum.teamPlugin && (
<Flex mt={2} alignItems={'center'} h={10}>
<InputGroup mr={4} h={'full'}>
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
</InputLeftElement>
<Input
h={'full'}
bg={'myGray.50'}
placeholder={t('plugin.Search plugin')}
onChange={debounce((e) => setSearchKey(e.target.value), 200)}
/>
</InputGroup>
<Box flex={1} />
<Flex
alignItems={'center'}
cursor={'pointer'}
_hover={{
color: 'primary.600'
}}
fontSize={'sm'}
onClick={() => router.push('/plugin/list')}
>
<Box></Box>
<MyIcon name={'common/rightArrowLight'} w={'14px'} />
</Flex>
</Flex>
)}
{templateType === TemplateTypeEnum.teamPlugin && !searchKey && currentParent && (
<Flex alignItems={'center'} mt={2}>
<ParentPaths
paths={[currentParent]}
FirstPathDom={null}
onClick={() => {
setCurrentParent(undefined);
}}
fontSize="md"
/>
</Flex>
)}
</Box>
<RenderList
templates={templates}
onClose={onClose}
currentParent={currentParent}
setCurrentParent={setCurrentParent}
/>
</Flex>
</>
);
};
export default React.memo(NodeTemplatesModal);
const RenderList = React.memo(function RenderList({
templates,
onClose,
currentParent,
setCurrentParent
}: RenderListProps) {
const { t } = useTranslation();
const { appT } = useI18n();
const { isPc } = useSystemStore();
const { x, y, zoom } = useViewport();
const { setLoading } = useSystemStore();
const { toast } = useToast();
const reactFlowWrapper = useContextSelector(WorkflowContext, (v) => v.reactFlowWrapper);
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
const formatTemplates = useMemo<nodeTemplateListType>(() => {
const copy: nodeTemplateListType = JSON.parse(JSON.stringify(workflowNodeTemplateList(t)));
templates.forEach((item) => {
const index = copy.findIndex((template) => template.type === item.templateType);
if (index === -1) return;
copy[index].list.push(item);
});
return copy.filter((item) => item.list.length > 0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [templates, currentParent]);
const onAddNode = useCallback(
async ({ template, position }: { template: FlowNodeTemplateType; position: XYPosition }) => {
if (!reactFlowWrapper?.current) return;
const templateNode = await (async () => {
try {
// get plugin preview module
if (template.flowNodeType === FlowNodeTypeEnum.pluginModule) {
setLoading(true);
const res = await getPreviewPluginModule(template.id);
setLoading(false);
return res;
}
return { ...template };
} catch (e) {
toast({
status: 'error',
title: getErrText(e, t('core.plugin.Get Plugin Module Detail Failed'))
});
setLoading(false);
return Promise.reject(e);
}
})();
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
const node = nodeTemplate2FlowNode({
template: {
...templateNode,
name: t(templateNode.name),
intro: t(templateNode.intro || '')
},
position: { x: mouseX, y: mouseY - 20 },
selected: true
});
setNodes((state) =>
state
.map((node) => ({
...node,
selected: false
}))
// @ts-ignore
.concat(node)
);
},
[reactFlowWrapper, setLoading, setNodes, t, toast, x, y, zoom]
);
const Render = useMemo(() => {
return templates.length === 0 ? (
<EmptyTip text={appT('module.No Modules')} />
) : (
<Box flex={'1 0 0'} overflow={'overlay'} px={'20px'}>
<Box mx={'auto'}>
{formatTemplates.map((item, i) => (
<Box
key={item.type}
css={css({
span: {
display: 'block'
}
})}
>
{item.label && (
<Flex>
<Box fontSize={'sm'} fontWeight={'bold'} flex={1}>
{t(item.label)}
</Box>
</Flex>
)}
<>
{item.list.map((template) => (
<MyTooltip
key={template.id}
placement={'right'}
label={
<Box>
<Flex alignItems={'center'}>
<Avatar
src={template.avatar}
w={'24px'}
objectFit={'contain'}
borderRadius={'0'}
/>
<Box fontWeight={'bold'} ml={3}>
{t(template.name)}
</Box>
</Flex>
<Box mt={2}>{t(template.intro || 'core.workflow.Not intro')}</Box>
</Box>
}
>
<Flex
alignItems={'center'}
p={5}
cursor={'pointer'}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'sm'}
draggable={template.pluginType !== PluginTypeEnum.folder}
onDragEnd={(e) => {
if (e.clientX < sliderWidth) return;
onAddNode({
template,
position: { x: e.clientX, y: e.clientY }
});
}}
onClick={(e) => {
if (template.pluginType === PluginTypeEnum.folder) {
return setCurrentParent({
parentId: template.id,
parentName: template.name
});
}
if (isPc) {
return onAddNode({
template,
position: { x: sliderWidth * 1.5, y: 200 }
});
}
onAddNode({
template: template,
position: { x: e.clientX, y: e.clientY }
});
onClose();
}}
>
<Avatar
src={template.avatar}
w={'1.7rem'}
objectFit={'contain'}
borderRadius={'0'}
/>
<Box color={'black'} fontSize={'sm'} ml={5} flex={'1 0 0'}>
{t(template.name)}
</Box>
</Flex>
</MyTooltip>
))}
</>
</Box>
))}
</Box>
</Box>
);
}, [appT, formatTemplates, isPc, onAddNode, onClose, setCurrentParent, t, templates.length]);
return Render;
});

View File

@@ -1,80 +0,0 @@
import React, { useCallback, useState } from 'react';
import { ModalBody, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import type { SelectAppItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { useTranslation } from 'next-i18next';
import SelectOneResource from '@/components/common/folder/SelectOneResource';
import {
GetResourceFolderListProps,
GetResourceListItemResponse
} from '@fastgpt/global/common/parentFolder/type';
import { getMyApps } from '@/web/core/app/api';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const SelectAppModal = ({
value,
filterAppIds = [],
onClose,
onSuccess
}: {
value?: SelectAppItemType;
filterAppIds?: string[];
onClose: () => void;
onSuccess: (e: SelectAppItemType) => void;
}) => {
const { t } = useTranslation();
const [selectedApp, setSelectedApp] = useState<SelectAppItemType | undefined>(value);
const getAppList = useCallback(
async ({ parentId }: GetResourceFolderListProps) => {
return getMyApps({ parentId }).then((res) =>
res
.filter((item) => !filterAppIds.includes(item._id))
.map<GetResourceListItemResponse>((item) => ({
id: item._id,
name: item.name,
avatar: item.avatar,
isFolder: item.type === AppTypeEnum.folder
}))
);
},
[filterAppIds]
);
return (
<MyModal
isOpen
title={`选择应用`}
iconSrc="/imgs/workflow/ai.svg"
onClose={onClose}
position={'relative'}
w={'600px'}
>
<ModalBody flex={'1 0 0'} overflow={'auto'} minH={'400px'} position={'relative'}>
<SelectOneResource
value={selectedApp?.id}
onSelect={(id) => setSelectedApp(id ? { id } : undefined)}
server={getAppList}
/>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} onClick={onClose}>
{t('common.Cancel')}
</Button>
<Button
ml={2}
isDisabled={!selectedApp}
onClick={() => {
if (!selectedApp) return;
onSuccess(selectedApp);
onClose();
}}
>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(SelectAppModal);

View File

@@ -1,235 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import { BezierEdge, getBezierPath, EdgeLabelRenderer, EdgeProps } from 'reactflow';
import { Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const ButtonEdge = (props: EdgeProps) => {
const { nodes, setEdges, workflowDebugData, hoverEdgeId } = useContextSelector(
WorkflowContext,
(v) => v
);
const {
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
selected,
source,
sourceHandleId,
target,
targetHandleId,
style
} = props;
const onDelConnect = useCallback(
(id: string) => {
setEdges((state) => state.filter((item) => item.id !== id));
},
[setEdges]
);
const highlightEdge = useMemo(() => {
const connectNode = nodes.find((node) => {
return node.selected && (node.id === props.source || node.id === props.target);
});
return !!(connectNode || selected);
}, [nodes, props.source, props.target, selected]);
const [, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition
});
const isToolEdge = sourceHandleId === NodeOutputKeyEnum.selectedTools;
const isHover = hoverEdgeId === id;
const { newTargetX, newTargetY } = useMemo(() => {
if (targetPosition === 'left') {
return {
newTargetX: targetX - 3,
newTargetY: targetY
};
}
if (targetPosition === 'right') {
return {
newTargetX: targetX + 3,
newTargetY: targetY
};
}
if (targetPosition === 'bottom') {
return {
newTargetX: targetX,
newTargetY: targetY + 3
};
}
if (targetPosition === 'top') {
return {
newTargetX: targetX,
newTargetY: targetY - 3
};
}
return {
newTargetX: targetX,
newTargetY: targetY
};
}, [targetPosition, targetX, targetY]);
const edgeColor = useMemo(() => {
const targetEdge = workflowDebugData?.runtimeEdges.find(
(edge) => edge.sourceHandle === sourceHandleId && edge.targetHandle === targetHandleId
);
if (!targetEdge) {
if (highlightEdge) return '#3370ff';
return '#94B5FF';
}
console.log(targetEdge);
// debug mode
const colorMap = {
[RuntimeEdgeStatusEnum.active]: '#39CC83',
[RuntimeEdgeStatusEnum.waiting]: '#5E8FFF',
[RuntimeEdgeStatusEnum.skipped]: '#8A95A7'
};
return colorMap[targetEdge.status];
}, [highlightEdge, sourceHandleId, targetHandleId, workflowDebugData?.runtimeEdges]);
const memoEdgeLabel = useMemo(() => {
const arrowTransform = (() => {
if (targetPosition === 'left') {
return `translate(-85%, -47%) translate(${newTargetX}px,${newTargetY}px) rotate(0deg)`;
}
if (targetPosition === 'right') {
return `translate(-10%, -50%) translate(${newTargetX}px,${newTargetY}px) rotate(-180deg)`;
}
if (targetPosition === 'bottom') {
return `translate(-50%, -20%) translate(${newTargetX}px,${newTargetY}px) rotate(-90deg)`;
}
if (targetPosition === 'top') {
return `translate(-50%, -90%) translate(${newTargetX}px,${newTargetY}px) rotate(90deg)`;
}
})();
return (
<EdgeLabelRenderer>
<Flex
display={isHover || highlightEdge ? 'flex' : 'none'}
alignItems={'center'}
justifyContent={'center'}
position={'absolute'}
transform={`translate(-55%, -50%) translate(${labelX}px,${labelY}px)`}
pointerEvents={'all'}
w={'17px'}
h={'17px'}
bg={'white'}
borderRadius={'17px'}
cursor={'pointer'}
zIndex={1000}
onClick={() => onDelConnect(id)}
>
<MyIcon name={'core/workflow/closeEdge'} w={'100%'}></MyIcon>
</Flex>
{!isToolEdge && (
<Flex
alignItems={'center'}
justifyContent={'center'}
position={'absolute'}
transform={arrowTransform}
pointerEvents={'all'}
w={highlightEdge ? '14px' : '10px'}
h={highlightEdge ? '14px' : '10px'}
// bg={'white'}
zIndex={highlightEdge ? 1000 : 0}
>
<MyIcon
name={'core/workflow/edgeArrow'}
w={'100%'}
color={edgeColor}
{...(highlightEdge
? {
fontWeight: 'bold'
}
: {})}
></MyIcon>
</Flex>
)}
</EdgeLabelRenderer>
);
}, [
isHover,
highlightEdge,
labelX,
labelY,
isToolEdge,
edgeColor,
targetPosition,
newTargetX,
newTargetY,
onDelConnect,
id
]);
const memoBezierEdge = useMemo(() => {
const targetEdge = workflowDebugData?.runtimeEdges.find(
(edge) => edge.source === source && edge.target === target
);
const edgeStyle: React.CSSProperties = (() => {
if (!targetEdge) {
return {
...style,
...(highlightEdge
? {
strokeWidth: 5
}
: { strokeWidth: 3, zIndex: 2 })
};
}
return {
...style,
strokeWidth: 3,
zIndex: 2
};
})();
return (
<BezierEdge
{...props}
targetX={newTargetX}
targetY={newTargetY}
style={{
...edgeStyle,
stroke: edgeColor
}}
/>
);
}, [
workflowDebugData?.runtimeEdges,
props,
newTargetX,
newTargetY,
edgeColor,
source,
target,
style,
highlightEdge
]);
return (
<>
{memoBezierEdge}
{memoEdgeLabel}
</>
);
};
export default React.memo(ButtonEdge);

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
import React from 'react';
import { Box, Flex } from '@chakra-ui/react';
const IOTitle = ({ text }: { text?: 'Input' | 'Output' | string }) => {
return (
<Flex fontSize={'md'} alignItems={'center'} fontWeight={'medium'} mb={3}>
<Box w={'3px'} h={'14px'} borderRadius={'13px'} bg={'primary.600'} mr={1.5} />
{text}
</Flex>
);
};
export default React.memo(IOTitle);

View File

@@ -1,274 +0,0 @@
import { storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { useCallback, useState } from 'react';
import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { uiWorkflow2StoreWorkflow } from '../../utils';
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import dynamic from 'next/dynamic';
import {
Box,
Button,
Flex,
Textarea,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Switch
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { checkInputIsReference } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../../context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
const MyRightDrawer = dynamic(
() => import('@fastgpt/web/components/common/MyDrawer/MyRightDrawer')
);
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
export const useDebug = () => {
const { t } = useTranslation();
const { toast } = useToast();
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const onStartNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStartNodeDebug);
const [runtimeNodeId, setRuntimeNodeId] = useState<string>();
const [runtimeNodes, setRuntimeNodes] = useState<RuntimeNodeItemType[]>();
const [runtimeEdges, setRuntimeEdges] = useState<RuntimeEdgeItemType[]>();
const flowData2StoreDataAndCheck = useCallback(async () => {
const { nodes } = await getWorkflowStore();
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
if (!checkResults) {
const storeNodes = uiWorkflow2StoreWorkflow({ nodes, edges });
return JSON.stringify(storeNodes);
} else {
checkResults.forEach((nodeId) => onUpdateNodeError(nodeId, true));
toast({
status: 'warning',
title: t('core.workflow.Check Failed')
});
return Promise.reject();
}
}, [edges, onUpdateNodeError, t, toast]);
const openDebugNode = useCallback(
async ({ entryNodeId }: { entryNodeId: string }) => {
setNodes((state) =>
state.map((node) => ({
...node,
data: {
...node.data,
debugResult: undefined
}
}))
);
const {
nodes,
edges
}: {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
} = JSON.parse(await flowData2StoreDataAndCheck());
const runtimeNodes = storeNodes2RuntimeNodes(nodes, [entryNodeId]);
const runtimeEdges: RuntimeEdgeItemType[] = edges.map((edge) =>
edge.target === entryNodeId
? {
...edge,
status: 'active'
}
: {
...edge,
status: 'waiting'
}
);
setRuntimeNodeId(entryNodeId);
setRuntimeNodes(runtimeNodes);
setRuntimeEdges(runtimeEdges);
},
[flowData2StoreDataAndCheck, setNodes]
);
const DebugInputModal = useCallback(() => {
if (!runtimeNodes || !runtimeEdges) return <></>;
const runtimeNode = runtimeNodes.find((node) => node.nodeId === runtimeNodeId);
if (!runtimeNode) return <></>;
const renderInputs = runtimeNode.inputs.filter((input) => {
if (runtimeNode.flowNodeType === FlowNodeTypeEnum.pluginInput) return true;
if (checkInputIsReference(input)) return true;
if (input.required && !input.value) return true;
});
const { register, getValues, setValue, handleSubmit } = useForm<Record<string, any>>({
defaultValues: renderInputs.reduce((acc: Record<string, any>, input) => {
const isReference = checkInputIsReference(input);
if (isReference) {
acc[input.key] = undefined;
} else if (typeof input.value === 'object') {
acc[input.key] = JSON.stringify(input.value, null, 2);
} else {
acc[input.key] = input.value;
}
return acc;
}, {})
});
const onClose = () => {
setRuntimeNodeId(undefined);
setRuntimeNodes(undefined);
setRuntimeEdges(undefined);
};
const onclickRun = (data: Record<string, any>) => {
onStartNodeDebug({
entryNodeId: runtimeNode.nodeId,
runtimeNodes: runtimeNodes.map((node) =>
node.nodeId === runtimeNode.nodeId
? {
...runtimeNode,
inputs: runtimeNode.inputs.map((input) => {
let parseValue = (() => {
try {
if (
input.valueType === WorkflowIOValueTypeEnum.string ||
input.valueType === WorkflowIOValueTypeEnum.number ||
input.valueType === WorkflowIOValueTypeEnum.boolean
)
return data[input.key];
return JSON.parse(data[input.key]);
} catch (e) {
return data[input.key];
}
})();
return {
...input,
value: parseValue ?? input.value
};
})
}
: node
),
runtimeEdges: runtimeEdges
});
onClose();
};
return (
<MyRightDrawer
onClose={onClose}
iconSrc="core/workflow/debugBlue"
title={t('core.workflow.Debug Node')}
maxW={['90vw', '35vw']}
px={0}
>
<Box flex={'1 0 0'} overflow={'auto'} px={6}>
{renderInputs.map((input) => {
const required = input.required || false;
const RenderInput = (() => {
if (input.valueType === WorkflowIOValueTypeEnum.string) {
return (
<Textarea
{...register(input.key, {
required
})}
placeholder={t(input.placeholder || '')}
bg={'myGray.50'}
/>
);
}
if (input.valueType === WorkflowIOValueTypeEnum.number) {
return (
<NumberInput step={input.step} min={input.min} max={input.max} bg={'myGray.50'}>
<NumberInputField
{...register(input.key, {
required: input.required,
min: input.min,
max: input.max,
valueAsNumber: true
})}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
}
if (input.valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<Box>
<Switch {...register(input.key)} />
</Box>
);
}
let value = getValues(input.key) || '';
if (typeof value !== 'string') {
value = JSON.stringify(value, null, 2);
}
return (
<JsonEditor
bg={'myGray.50'}
placeholder={t(input.placeholder || '')}
resize
value={value}
onChange={(e) => {
setValue(input.key, e);
}}
/>
);
})();
return !!RenderInput ? (
<Box key={input.key} _notLast={{ mb: 4 }} px={1}>
<Flex alignItems={'center'} mb={1}>
<Box position={'relative'}>
{required && (
<Box position={'absolute'} right={-2} top={'-1px'} color={'red.600'}>
*
</Box>
)}
{t(input.debugLabel || input.label)}
</Box>
{input.description && <QuestionTip ml={2} label={input.description} />}
</Flex>
{RenderInput}
</Box>
) : null;
})}
</Box>
<Flex py={2} justifyContent={'flex-end'} px={6}>
<Button onClick={handleSubmit(onclickRun)}></Button>
</Flex>
</MyRightDrawer>
);
}, [onStartNodeDebug, runtimeEdges, runtimeNodeId, runtimeNodes, t]);
return {
DebugInputModal,
openDebugNode
};
};

View File

@@ -1,120 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useTranslation } from 'next-i18next';
import { Node } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../../context';
export const useKeyboard = () => {
const { t } = useTranslation();
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
const { copyData } = useCopyData();
const [isDowningCtrl, setIsDowningCtrl] = useState(false);
const hasInputtingElement = useCallback(() => {
const activeElement = document.activeElement;
if (activeElement) {
const tagName = activeElement.tagName.toLowerCase();
const className = activeElement.className.toLowerCase();
if (tagName === 'input' || tagName === 'textarea') return true;
if (className.includes('prompteditor')) return true;
}
return false;
}, []);
const onCopy = useCallback(async () => {
if (hasInputtingElement()) return;
const { nodes } = await getWorkflowStore();
const selectedNodes = nodes.filter(
(node) => node.selected && !node.data?.isError && node.data?.unique !== true
);
if (selectedNodes.length === 0) return;
copyData(JSON.stringify(selectedNodes), t('core.workflow.Copy node'));
}, [copyData, hasInputtingElement, t]);
const onParse = useCallback(async () => {
if (hasInputtingElement()) return;
const copyResult = await navigator.clipboard.readText();
try {
const parseData = JSON.parse(copyResult) as Node<FlowNodeItemType, string | undefined>[];
// check is array
if (!Array.isArray(parseData)) return;
// filter workflow data
const newNodes = parseData
.filter((item) => !!item.type && item.data?.unique !== true)
.map((item) => {
const nodeId = getNanoid();
return {
// reset id
...item,
id: nodeId,
data: {
...item.data,
nodeId
},
position: {
x: item.position.x + 100,
y: item.position.y + 100
}
};
});
setNodes((prev) =>
prev
.map((node) => ({
...node,
selected: false
}))
//@ts-ignore
.concat(newNodes)
);
} catch (error) {}
}, [hasInputtingElement, setNodes]);
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.ctrlKey || event.metaKey) {
setIsDowningCtrl(true);
switch (event.key) {
case 'c':
onCopy();
break;
case 'v':
onParse();
break;
default:
break;
}
}
},
[onCopy, onParse]
);
const handleKeyUp = useCallback((event: KeyboardEvent) => {
setIsDowningCtrl(false);
}, []);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);
useEffect(() => {
document.addEventListener('keyup', handleKeyUp);
return () => {
document.removeEventListener('keyup', handleKeyUp);
};
}, [handleKeyUp]);
return {
isDowningCtrl
};
};

View File

@@ -1,300 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import ReactFlow, {
Background,
Connection,
Controls,
ControlButton,
MiniMap,
NodeProps,
ReactFlowProvider,
useReactFlow,
NodeChange,
OnConnectStartParams,
addEdge,
EdgeChange,
Edge
} from 'reactflow';
import { Box, Flex, IconButton, useDisclosure } from '@chakra-ui/react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import dynamic from 'next/dynamic';
import ButtonEdge from './components/ButtonEdge';
import NodeTemplatesModal from './NodeTemplatesModal';
import 'reactflow/dist/style.css';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { connectionLineStyle, defaultEdgeOptions } from '../constants';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useKeyboard } from './hooks/useKeyboard';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
[FlowNodeTypeEnum.emptyNode]: NodeSimple,
[FlowNodeTypeEnum.globalVariable]: NodeSimple,
[FlowNodeTypeEnum.systemConfig]: dynamic(() => import('./nodes/NodeSystemConfig')),
[FlowNodeTypeEnum.workflowStart]: dynamic(() => import('./nodes/NodeWorkflowStart')),
[FlowNodeTypeEnum.chatNode]: NodeSimple,
[FlowNodeTypeEnum.datasetSearchNode]: NodeSimple,
[FlowNodeTypeEnum.datasetConcatNode]: dynamic(() => import('./nodes/NodeDatasetConcat')),
[FlowNodeTypeEnum.answerNode]: dynamic(() => import('./nodes/NodeAnswer')),
[FlowNodeTypeEnum.classifyQuestion]: dynamic(() => import('./nodes/NodeCQNode')),
[FlowNodeTypeEnum.contentExtract]: dynamic(() => import('./nodes/NodeExtract')),
[FlowNodeTypeEnum.httpRequest468]: dynamic(() => import('./nodes/NodeHttp')),
[FlowNodeTypeEnum.runApp]: NodeSimple,
[FlowNodeTypeEnum.pluginInput]: dynamic(() => import('./nodes/NodePluginInput')),
[FlowNodeTypeEnum.pluginOutput]: dynamic(() => import('./nodes/NodePluginOutput')),
[FlowNodeTypeEnum.pluginModule]: NodeSimple,
[FlowNodeTypeEnum.queryExtension]: NodeSimple,
[FlowNodeTypeEnum.tools]: dynamic(() => import('./nodes/NodeTools')),
[FlowNodeTypeEnum.stopTool]: (data: NodeProps<FlowNodeItemType>) => (
<NodeSimple {...data} minW={'100px'} maxW={'300px'} />
),
[FlowNodeTypeEnum.lafModule]: dynamic(() => import('./nodes/NodeLaf')),
[FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse')),
[FlowNodeTypeEnum.variableUpdate]: dynamic(() => import('./nodes/NodeVariableUpdate')),
[FlowNodeTypeEnum.code]: dynamic(() => import('./nodes/NodeCode'))
};
const edgeTypes = {
[EDGE_TYPE]: ButtonEdge
};
const Container = React.memo(function Container() {
const { toast } = useToast();
const { t } = useTranslation();
const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({
content: t('core.module.Confirm Delete Node'),
type: 'delete'
});
const { isDowningCtrl } = useKeyboard();
const {
setConnectingEdge,
reactFlowWrapper,
nodes,
onNodesChange,
edges,
setEdges,
onEdgesChange,
setHoverEdgeId
} = useContextSelector(WorkflowContext, (v) => v);
/* node */
const handleNodesChange = useCallback(
(changes: NodeChange[]) => {
for (const change of changes) {
if (change.type === 'remove') {
const node = nodes.find((n) => n.id === change.id);
if (node && node.data.forbidDelete) {
return toast({
status: 'warning',
title: t('core.workflow.Can not delete node')
});
} else {
return onOpenConfirmDeleteNode(() => {
onNodesChange(changes);
setEdges((state) =>
state.filter((edge) => edge.source !== change.id && edge.target !== change.id)
);
})();
}
} else if (change.type === 'select' && change.selected === false && isDowningCtrl) {
change.selected = true;
}
}
onNodesChange(changes);
},
[isDowningCtrl, nodes, onNodesChange, onOpenConfirmDeleteNode, setEdges, t, toast]
);
const handleEdgeChange = useCallback(
(changes: EdgeChange[]) => {
onEdgesChange(changes.filter((change) => change.type !== 'remove'));
},
[onEdgesChange]
);
/* connect */
const onConnectStart = useCallback(
(event: any, params: OnConnectStartParams) => {
setConnectingEdge(params);
},
[setConnectingEdge]
);
const onConnectEnd = useCallback(() => {
setConnectingEdge(undefined);
}, [setConnectingEdge]);
const onConnect = useCallback(
({ connect }: { connect: Connection }) => {
setEdges((state) =>
addEdge(
{
...connect,
type: EDGE_TYPE
},
state
)
);
},
[setEdges]
);
const customOnConnect = useCallback(
(connect: Connection) => {
if (!connect.sourceHandle || !connect.targetHandle) {
return;
}
if (connect.source === connect.target) {
return toast({
status: 'warning',
title: t('core.module.Can not connect self')
});
}
onConnect({
connect
});
},
[onConnect, t, toast]
);
/* edge */
const onEdgeMouseEnter = useCallback(
(e: any, edge: Edge) => {
setHoverEdgeId(edge.id);
},
[setHoverEdgeId]
);
const onEdgeMouseLeave = useCallback(() => {
setHoverEdgeId(undefined);
}, [setHoverEdgeId]);
return (
<>
<ReactFlow
ref={reactFlowWrapper}
fitView
nodes={nodes}
edges={edges}
minZoom={0.1}
maxZoom={1.5}
defaultEdgeOptions={defaultEdgeOptions}
elevateEdgesOnSelect
connectionLineStyle={connectionLineStyle}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={handleNodesChange}
onEdgesChange={handleEdgeChange}
onConnect={customOnConnect}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseLeave={onEdgeMouseLeave}
>
<FlowController />
</ReactFlow>
<ConfirmDeleteModal />
</>
);
});
const Flow = ({ Header, ...data }: { Header: React.ReactNode }) => {
const {
isOpen: isOpenTemplate,
onOpen: onOpenTemplate,
onClose: onCloseTemplate
} = useDisclosure();
const memoRenderContainer = useMemo(() => {
return (
<Box
minH={'400px'}
flex={'1 0 0'}
w={'100%'}
h={0}
position={'relative'}
onContextMenu={(e) => {
e.preventDefault();
return false;
}}
>
{/* open module template */}
<IconButton
position={'absolute'}
top={5}
left={5}
size={'mdSquare'}
borderRadius={'50%'}
icon={<SmallCloseIcon fontSize={'26px'} />}
transform={isOpenTemplate ? '' : 'rotate(135deg)'}
transition={'0.2s ease'}
aria-label={''}
zIndex={1}
boxShadow={'2px 2px 6px #85b1ff'}
onClick={() => {
isOpenTemplate ? onCloseTemplate() : onOpenTemplate();
}}
/>
<Container {...data} />
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
</Box>
);
}, [data, isOpenTemplate, onCloseTemplate, onOpenTemplate]);
return (
<Box h={'100%'} position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
<ReactFlowProvider>
<Flex h={'100%'} flexDirection={'column'} bg={'myGray.50'}>
{Header}
{memoRenderContainer}
</Flex>
</ReactFlowProvider>
</Box>
);
};
export default React.memo(Flow);
const FlowController = React.memo(function FlowController() {
const { fitView } = useReactFlow();
return (
<>
<MiniMap
style={{
height: 78,
width: 126,
marginBottom: 35
}}
pannable
/>
<Controls
position={'bottom-right'}
style={{
display: 'flex',
marginBottom: 5,
background: 'white',
borderRadius: '6px',
overflow: 'hidden',
boxShadow:
'0px 0px 1px 0px rgba(19, 51, 107, 0.20), 0px 12px 16px -4px rgba(19, 51, 107, 0.20)'
}}
showInteractive={false}
showFitView={false}
>
<MyTooltip label={'页面居中'}>
<ControlButton className="custom-workflow-fix_view" onClick={() => fitView()}>
<MyIcon name={'core/modules/fixview'} w={'14px'} />
</ControlButton>
</MyTooltip>
</Controls>
<Background />
</>
);
});

View File

@@ -1,36 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import RenderToolInput from './render/RenderToolInput';
import { useTranslation } from 'next-i18next';
import IOTitle from '../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const NodeAnswer = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container>
{toolInputs.length > 0 && (
<>
<IOTitle text={t('core.module.tool.Tool input')} />
<Container>
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
</Container>
</>
)}
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
{/* <RenderOutput nodeId={nodeId} flowOutputList={outputs} /> */}
</Container>
</NodeCard>
);
};
export default React.memo(NodeAnswer);

View File

@@ -1,141 +0,0 @@
import React, { useMemo } from 'react';
import { NodeProps, Position } from 'reactflow';
import { Box, Button, Flex, Textarea } from '@chakra-ui/react';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import type { ClassifyQuestionAgentItemType } from '@fastgpt/global/core/workflow/type/index.d';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { SourceHandle } from './render/Handle';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const NodeCQNode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const CustomComponent = useMemo(
() => ({
[NodeInputKeyEnum.agents]: ({
key: agentKey,
value = [],
...props
}: FlowNodeInputItemType) => {
const agents = value as ClassifyQuestionAgentItemType[];
return (
<Box>
{agents.map((item, i) => (
<Box key={item.key} mb={4}>
<Flex alignItems={'center'}>
<MyTooltip label={t('common.Delete')}>
<MyIcon
mt={1}
mr={2}
name={'minus'}
w={'12px'}
cursor={'pointer'}
color={'myGray.600'}
_hover={{ color: 'red.600' }}
onClick={() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: agentKey,
value: {
...props,
key: agentKey,
value: agents.filter((input) => input.key !== item.key)
}
});
onChangeNode({
nodeId,
type: 'delOutput',
key: item.key
});
}}
/>
</MyTooltip>
<Box flex={1} color={'myGray.600'} fontWeight={'medium'}>
{i + 1}
</Box>
</Flex>
<Box position={'relative'}>
<Textarea
rows={2}
mt={1}
defaultValue={item.value}
bg={'white'}
fontSize={'sm'}
onChange={(e) => {
const newVal = agents.map((val) =>
val.key === item.key
? {
...val,
value: e.target.value
}
: val
);
onChangeNode({
nodeId,
type: 'updateInput',
key: agentKey,
value: {
...props,
key: agentKey,
value: newVal
}
});
}}
/>
<SourceHandle
nodeId={nodeId}
handleId={getHandleId(nodeId, 'source', item.key)}
position={Position.Right}
translate={[26, 0]}
/>
</Box>
</Box>
))}
<Button
fontSize={'sm'}
onClick={() => {
const key = getNanoid();
onChangeNode({
nodeId,
type: 'updateInput',
key: agentKey,
value: {
...props,
key: agentKey,
value: agents.concat({ value: '', key })
}
});
}}
>
{t('core.module.Add question type')}
</Button>
</Box>
);
}
}),
[nodeId, onChangeNode, t]
);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container>
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeCQNode);

View File

@@ -1,106 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import IOTitle from '../components/IOTitle';
import RenderToolInput from './render/RenderToolInput';
import RenderOutput from './render/RenderOutput';
import CodeEditor from '@fastgpt/web/components/common/Textarea/CodeEditor';
import { Box, Flex } from '@chakra-ui/react';
import { useI18n } from '@/web/context/I18n';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { JS_TEMPLATE } from '@fastgpt/global/core/workflow/template/system/sandbox/constants';
const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { workflowT } = useI18n();
const { nodeId, inputs, outputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
const { ConfirmModal, openConfirm } = useConfirm({
content: workflowT('code.Reset template confirm')
});
const CustomComponent = useMemo(() => {
return {
[NodeInputKeyEnum.code]: (item: FlowNodeInputItemType) => {
return (
<Box>
<Flex mb={1} alignItems={'flex-end'}>
<Box flex={'1'}>Javascript{workflowT('Code')}</Box>
<Box
cursor={'pointer'}
color={'primary.500'}
fontSize={'xs'}
onClick={openConfirm(() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: JS_TEMPLATE
}
});
})}
>
{workflowT('code.Reset template')}
</Box>
</Flex>
<CodeEditor
bg={'white'}
borderRadius={'sm'}
value={item.value}
onChange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
</Box>
);
}
};
}, [nodeId, onChangeNode, openConfirm, workflowT]);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
{toolInputs.length > 0 && (
<>
<Container>
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
</Container>
</>
)}
<Container>
<IOTitle text={t('common.Input')} />
<RenderInput
nodeId={nodeId}
flowInputList={commonInputs}
CustomComponent={CustomComponent}
/>
</Container>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<ConfirmModal />
</NodeCard>
);
};
export default React.memo(NodeCode);

View File

@@ -1,120 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import { Box, Button, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { SmallAddIcon } from '@chakra-ui/icons';
import { WorkflowIOValueTypeEnum, NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { getOneQuoteInputTemplate } from '@fastgpt/global/core/workflow/template/system/datasetConcat';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MySlider from '@/components/Slider';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import RenderOutput from './render/RenderOutput';
import IOTitle from '../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { llmModelList } = useSystemStore();
const { nodeId, inputs, outputs } = data;
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const quotes = useMemo(
() => inputs.filter((item) => item.valueType === WorkflowIOValueTypeEnum.datasetQuote),
[inputs]
);
const tokenLimit = useMemo(() => {
let maxTokens = 3000;
nodeList.forEach((item) => {
if (item.flowNodeType === FlowNodeTypeEnum.chatNode) {
const model =
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken =
llmModelList.find((item) => item.model === model)?.quoteMaxToken || 3000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
return maxTokens;
}, [llmModelList, nodeList]);
const onAddField = useCallback(() => {
onChangeNode({
nodeId,
type: 'addInput',
value: getOneQuoteInputTemplate({ index: quotes.length + 1 })
});
}, [nodeId, onChangeNode, quotes.length]);
const CustomComponent = useMemo(() => {
return {
[NodeInputKeyEnum.datasetMaxTokens]: (item: FlowNodeInputItemType) => (
<Box px={2}>
<MySlider
markList={[
{ label: '100', value: 100 },
{ label: tokenLimit, value: tokenLimit }
]}
width={'100%'}
min={100}
max={tokenLimit}
step={50}
value={item.value}
onChange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
</Box>
),
customComponent: (item: FlowNodeInputItemType) => (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Box position={'relative'} fontWeight={'medium'} color={'myGray.600'}>
{t('core.workflow.Dataset quote')}
</Box>
<Box flex={'1 0 0'} />
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
mr={'-5px'}
onClick={onAddField}
>
{t('common.Add New')}
</Button>
</Flex>
)
};
}, [nodeId, onAddField, onChangeNode, t, tokenLimit]);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container position={'relative'}>
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
{/* {RenderQuoteList} */}
</Container>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeDatasetConcat);

View File

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

View File

@@ -1,131 +0,0 @@
import React from 'react';
import {
Box,
Button,
ModalFooter,
ModalBody,
Flex,
Switch,
Input,
Textarea
} from '@chakra-ui/react';
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { useForm } from 'react-hook-form';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { fnValueTypeSelect } from '@/web/core/workflow/constants/dataType';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
export const defaultField: ContextExtractAgentItemType = {
valueType: 'string',
required: false,
defaultValue: '',
desc: '',
key: '',
enum: ''
};
const ExtractFieldModal = ({
defaultField,
onClose,
onSubmit
}: {
defaultField: ContextExtractAgentItemType;
onClose: () => void;
onSubmit: (data: ContextExtractAgentItemType) => void;
}) => {
const { t } = useTranslation();
const { register, setValue, handleSubmit, watch } = useForm<ContextExtractAgentItemType>({
defaultValues: defaultField
});
const required = watch('required');
const valueType = watch('valueType');
return (
<MyModal
isOpen={true}
iconSrc="/imgs/workflow/extract.png"
title={t('core.module.extract.Field Setting Title')}
onClose={onClose}
w={['90vw', '500px']}
>
<ModalBody>
<Flex mt={2} alignItems={'center'}>
<Flex alignItems={'center'} flex={['1 0 80px', '1 0 100px']}>
<FormLabel>{t('core.module.extract.Required')}</FormLabel>
<QuestionTip ml={1} label={t('core.module.extract.Required Description')}></QuestionTip>
</Flex>
<Switch {...register('required')} />
</Flex>
{required && (
<Flex mt={5} alignItems={'center'}>
<FormLabel flex={['0 0 80px', '0 0 100px']}>{t('core.module.Default value')}</FormLabel>
<Input
bg={'myGray.50'}
placeholder={t('core.module.Default value placeholder')}
{...register('defaultValue')}
/>
</Flex>
)}
<Flex alignItems={'center'} mt={5}>
<FormLabel flex={['0 0 80px', '0 0 100px']}>{t('core.module.Data Type')}</FormLabel>
<Box flex={'1 0 0'}>
<MySelect
list={fnValueTypeSelect}
value={valueType}
onchange={(e: any) => {
setValue('valueType', e);
}}
/>
</Box>
</Flex>
<Flex mt={5} alignItems={'center'}>
<FormLabel flex={['0 0 80px', '0 0 100px']}>{t('Field name')}</FormLabel>
<Input
bg={'myGray.50'}
placeholder="name/age/sql"
{...register('key', { required: true })}
/>
</Flex>
<Flex mt={5} alignItems={'center'}>
<FormLabel flex={['0 0 80px', '0 0 100px']}>
{t('core.module.Field Description')}
</FormLabel>
<Input
bg={'myGray.50'}
placeholder={t('core.module.extract.Field Description Placeholder')}
{...register('desc', { required: true })}
/>
</Flex>
{(valueType === 'string' || valueType === 'number') && (
<Box mt={5}>
<Flex alignItems={'center'}>
<FormLabel>
{t('core.module.extract.Enum Value')}({t('common.choosable')})
</FormLabel>
<QuestionTip ml={1} label={t('core.module.extract.Enum Description')}></QuestionTip>
</Flex>
<Textarea
rows={5}
bg={'myGray.50'}
placeholder={'apple\npeach\nwatermelon'}
{...register('enum')}
/>
</Box>
)}
</ModalBody>
<ModalFooter>
<Button onClick={handleSubmit(onSubmit)}>{t('common.Confirm')}</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(ExtractFieldModal);

View File

@@ -1,241 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Box,
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Flex
} from '@chakra-ui/react';
import { NodeProps } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { useTranslation } from 'next-i18next';
import NodeCard from '../render/NodeCard';
import Container from '../../components/Container';
import { AddIcon } from '@chakra-ui/icons';
import RenderInput from '../render/RenderInput';
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/type/index.d';
import RenderOutput from '../render/RenderOutput';
import MyIcon from '@fastgpt/web/components/common/Icon';
import ExtractFieldModal, { defaultField } from './ExtractFieldModal';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import RenderToolInput from '../render/RenderToolInput';
import {
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io.d';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import IOTitle from '../../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
const { inputs, outputs, nodeId } = data;
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { toolInputs, commonInputs } = splitToolInputs(inputs, nodeId);
const [editExtractFiled, setEditExtractField] = useState<ContextExtractAgentItemType>();
const CustomComponent = useMemo(
() => ({
[NodeInputKeyEnum.extractKeys]: ({
value: extractKeys = [],
...props
}: Omit<FlowNodeInputItemType, 'value'> & {
value?: ContextExtractAgentItemType[];
}) => (
<Box>
<Flex alignItems={'center'}>
<Box flex={'1 0 0'} fontWeight={'medium'} color={'myGray.600'}>
{t('core.module.extract.Target field')}
</Box>
<Button
size={'sm'}
variant={'whitePrimary'}
leftIcon={<AddIcon fontSize={'10px'} />}
onClick={() => setEditExtractField(defaultField)}
>
{t('core.module.extract.Add field')}
</Button>
</Flex>
<Box
mt={2}
borderRadius={'md'}
overflow={'hidden'}
borderWidth={'1px'}
borderBottom="none"
>
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th bg={'myGray.50'} borderRadius={'none !important'}>
</Th>
<Th bg={'myGray.50'}></Th>
<Th bg={'myGray.50'}></Th>
<Th bg={'myGray.50'} borderRadius={'none !important'}></Th>
</Tr>
</Thead>
<Tbody>
{extractKeys.map((item, index) => (
<Tr
key={index}
position={'relative'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
>
<Td>{item.key}</Td>
<Td>{item.desc}</Td>
<Td>{item.required ? '✔' : ''}</Td>
<Td whiteSpace={'nowrap'}>
<MyIcon
mr={3}
name={'common/settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
setEditExtractField(item);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.extractKeys,
value: {
...props,
value: extractKeys.filter((extract) => item.key !== extract.key)
}
});
onChangeNode({
nodeId,
type: 'delOutput',
key: item.key
});
}}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
</Box>
)
}),
[nodeId, onChangeNode, t]
);
return (
<NodeCard minW={'400px'} {...data}>
{toolInputs.length > 0 && (
<>
<Container>
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} />
</Container>
</>
)}
<>
<Container>
<IOTitle text={t('common.Input')} />
<RenderInput
nodeId={nodeId}
flowInputList={commonInputs}
CustomComponent={CustomComponent}
/>
</Container>
</>
<>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</>
{!!editExtractFiled && (
<ExtractFieldModal
defaultField={editExtractFiled}
onClose={() => setEditExtractField(undefined)}
onSubmit={(data) => {
const extracts: ContextExtractAgentItemType[] =
inputs.find((item) => item.key === NodeInputKeyEnum.extractKeys)?.value || [];
const exists = extracts.find((item) => item.key === editExtractFiled.key);
const newInputs = exists
? extracts.map((item) => (item.key === editExtractFiled.key ? data : item))
: extracts.concat(data);
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.extractKeys,
value: {
...inputs.find((input) => input.key === NodeInputKeyEnum.extractKeys),
value: newInputs
}
});
const newOutput: FlowNodeOutputItemType = {
id: getNanoid(),
key: data.key,
label: `提取结果-${data.desc}`,
valueType: WorkflowIOValueTypeEnum.string,
type: FlowNodeOutputTypeEnum.static
};
if (exists) {
if (editExtractFiled.key === data.key) {
const output = outputs.find((output) => output.key === data.key);
// update
onChangeNode({
nodeId,
type: 'updateOutput',
key: data.key,
value: {
...output,
label: `提取结果-${data.desc}`
}
});
} else {
onChangeNode({
nodeId,
type: 'replaceOutput',
key: editExtractFiled.key,
value: newOutput
});
}
} else {
onChangeNode({
nodeId,
type: 'addOutput',
value: newOutput
});
}
setEditExtractField(undefined);
}}
/>
)}
</NodeCard>
);
};
export default React.memo(NodeExtract);

View File

@@ -1,158 +0,0 @@
import React from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { ModalBody, Button, ModalFooter, useDisclosure, Textarea, Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useForm } from 'react-hook-form';
import parse from '@bany/curl-to-json';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
type RequestMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';
const methodMap: { [K in RequestMethod]: string } = {
get: 'GET',
post: 'POST',
put: 'PUT',
delete: 'DELETE',
patch: 'PATCH'
};
const CurlImportModal = ({
nodeId,
inputs,
onClose
}: {
nodeId: string;
inputs: FlowNodeInputItemType[];
onClose: () => void;
}) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { register, handleSubmit } = useForm({
defaultValues: {
curlContent: ''
}
});
const { toast } = useToast();
const handleFileProcessing = async (content: string) => {
try {
const requestUrl = inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl);
const requestMethod = inputs.find((item) => item.key === NodeInputKeyEnum.httpMethod);
const params = inputs.find((item) => item.key === NodeInputKeyEnum.httpParams);
const headers = inputs.find((item) => item.key === NodeInputKeyEnum.httpHeaders);
const jsonBody = inputs.find((item) => item.key === NodeInputKeyEnum.httpJsonBody);
const parsed = parse(content);
if (!parsed.url) {
throw new Error('url not found');
}
const newParams = Object.keys(parsed.params || {}).map((key) => ({
key,
value: parsed.params?.[key],
type: 'string'
}));
const newHeaders = Object.keys(parsed.header || {}).map((key) => ({
key,
value: parsed.header?.[key],
type: 'string'
}));
const newBody = JSON.stringify(parsed.data, null, 2);
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: parsed.url
}
});
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpMethod,
value: {
...requestMethod,
value: methodMap[parsed.method?.toLowerCase() as RequestMethod] || 'GET'
}
});
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpParams,
value: {
...params,
value: newParams
}
});
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpHeaders,
value: {
...headers,
value: newHeaders
}
});
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpJsonBody,
value: {
...jsonBody,
value: newBody
}
});
onClose();
toast({
title: t('common.Import success'),
status: 'success'
});
} catch (error: any) {
toast({
title: t('common.Import failed'),
description: error.message,
status: 'error'
});
console.error(error);
}
};
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="modal/edit"
title={t('core.module.http.curl import')}
w={600}
>
<ModalBody>
<Textarea
rows={20}
mt={2}
{...register('curlContent')}
placeholder={t('core.module.http.curl import placeholder')}
/>
</ModalBody>
<ModalFooter>
<Button onClick={handleSubmit((data) => handleFileProcessing(data.curlContent))}>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(CurlImportModal);

View File

@@ -1,681 +0,0 @@
import React, { useCallback, useEffect, useMemo, useState, useTransition } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Container from '../../components/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
import {
Box,
Flex,
Input,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Button,
useDisclosure
} from '@chakra-ui/react';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import Tabs from '@/components/Tabs';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import JSONEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
import { EditorVariablePickerType } from '@fastgpt/web/components/common/Textarea/PromptEditor/type';
import HttpInput from '@fastgpt/web/components/common/Input/HttpInput';
import dynamic from 'next/dynamic';
import MySelect from '@fastgpt/web/components/common/MySelect';
import RenderToolInput from '../render/RenderToolInput';
import IOTitle from '../../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useMemoizedFn } from 'ahooks';
import { AppContext } from '@/web/core/app/context/appContext';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
const CurlImportModal = dynamic(() => import('./CurlImportModal'));
export const HttpHeaders = [
{ key: 'A-IM', label: 'A-IM' },
{ key: 'Accept', label: 'Accept' },
{ key: 'Accept-Charset', label: 'Accept-Charset' },
{ key: 'Accept-Encoding', label: 'Accept-Encoding' },
{ key: 'Accept-Language', label: 'Accept-Language' },
{ key: 'Accept-Datetime', label: 'Accept-Datetime' },
{ key: 'Access-Control-Request-Method', label: 'Access-Control-Request-Method' },
{ key: 'Access-Control-Request-Headers', label: 'Access-Control-Request-Headers' },
{ key: 'Authorization', label: 'Authorization' },
{ key: 'Cache-Control', label: 'Cache-Control' },
{ key: 'Connection', label: 'Connection' },
{ key: 'Content-Length', label: 'Content-Length' },
{ key: 'Content-Type', label: 'Content-Type' },
{ key: 'Cookie', label: 'Cookie' },
{ key: 'Date', label: 'Date' },
{ key: 'Expect', label: 'Expect' },
{ key: 'Forwarded', label: 'Forwarded' },
{ key: 'From', label: 'From' },
{ key: 'Host', label: 'Host' },
{ key: 'If-Match', label: 'If-Match' },
{ key: 'If-Modified-Since', label: 'If-Modified-Since' },
{ key: 'If-None-Match', label: 'If-None-Match' },
{ key: 'If-Range', label: 'If-Range' },
{ key: 'If-Unmodified-Since', label: 'If-Unmodified-Since' },
{ key: 'Max-Forwards', label: 'Max-Forwards' },
{ key: 'Origin', label: 'Origin' },
{ key: 'Pragma', label: 'Pragma' },
{ key: 'Proxy-Authorization', label: 'Proxy-Authorization' },
{ key: 'Range', label: 'Range' },
{ key: 'Referer', label: 'Referer' },
{ key: 'TE', label: 'TE' },
{ key: 'User-Agent', label: 'User-Agent' },
{ key: 'Upgrade', label: 'Upgrade' },
{ key: 'Via', label: 'Via' },
{ key: 'Warning', label: 'Warning' },
{ key: 'Dnt', label: 'Dnt' },
{ key: 'X-Requested-With', label: 'X-Requested-With' },
{ key: 'X-CSRF-Token', label: 'X-CSRF-Token' }
];
enum TabEnum {
params = 'params',
headers = 'headers',
body = 'body'
}
export type PropsArrType = {
key: string;
type: string;
value: string;
};
const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
nodeId,
inputs
}: {
nodeId: string;
inputs: FlowNodeInputItemType[];
}) {
const { t } = useTranslation();
const { toast } = useToast();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure();
const requestMethods = inputs.find((item) => item.key === NodeInputKeyEnum.httpMethod);
const requestUrl = inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl);
const onChangeUrl = (e: React.ChangeEvent<HTMLInputElement>) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: e.target.value
}
});
};
const onBlurUrl = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
// 拆分params和url
const url = val.split('?')[0];
const params = val.split('?')[1];
if (params) {
const paramsArr = params.split('&');
const paramsObj = paramsArr.reduce((acc, cur) => {
const [key, value] = cur.split('=');
return {
...acc,
[key]: value
};
}, {});
const inputParams = inputs.find((item) => item.key === NodeInputKeyEnum.httpParams);
if (!inputParams || Object.keys(paramsObj).length === 0) return;
const concatParams: PropsArrType[] = inputParams?.value || [];
Object.entries(paramsObj).forEach(([key, value]) => {
if (!concatParams.find((item) => item.key === key)) {
concatParams.push({ key, value: value as string, type: 'string' });
}
});
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpParams,
value: {
...inputParams,
value: concatParams
}
});
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: url
}
});
toast({
status: 'success',
title: t('core.module.http.Url and params have been split')
});
}
};
return (
<Box>
<Box mb={2} display={'flex'} justifyContent={'space-between'}>
<Box fontWeight={'medium'} color={'myGray.600'}>
{t('core.module.Http request settings')}
</Box>
<Button variant={'link'} onClick={onOpenCurl}>
{t('core.module.http.curl import')}
</Button>
</Box>
<Flex alignItems={'center'} className="nodrag">
<MySelect
h={'34px'}
w={'88px'}
bg={'white'}
width={'100%'}
value={requestMethods?.value}
list={[
{
label: 'GET',
value: 'GET'
},
{
label: 'POST',
value: 'POST'
},
{
label: 'PUT',
value: 'PUT'
},
{
label: 'DELETE',
value: 'DELETE'
},
{
label: 'PATCH',
value: 'PATCH'
}
]}
onchange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpMethod,
value: {
...requestMethods,
value: e
}
});
}}
/>
<Input
flex={'1 0 0'}
ml={2}
h={'34px'}
bg={'white'}
value={requestUrl?.value || ''}
placeholder={t('core.module.input.label.Http Request Url')}
fontSize={'xs'}
onChange={onChangeUrl}
onBlur={onBlurUrl}
/>
</Flex>
{isOpenCurl && <CurlImportModal nodeId={nodeId} inputs={inputs} onClose={onCloseCurl} />}
</Box>
);
});
export function RenderHttpProps({
nodeId,
inputs
}: {
nodeId: string;
inputs: FlowNodeInputItemType[];
}) {
const { t } = useTranslation();
const [selectedTab, setSelectedTab] = useState(TabEnum.params);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const requestMethods = inputs.find((item) => item.key === NodeInputKeyEnum.httpMethod)?.value;
const params = inputs.find((item) => item.key === NodeInputKeyEnum.httpParams);
const headers = inputs.find((item) => item.key === NodeInputKeyEnum.httpHeaders);
const jsonBody = inputs.find((item) => item.key === NodeInputKeyEnum.httpJsonBody);
const paramsLength = params?.value?.length || 0;
const headersLength = headers?.value?.length || 0;
// get variable
const variables = useMemo(() => {
const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.canEdit || input.toolDescription)
.map((item) => ({
key: item.key,
label: item.label
}))
);
return [...moduleVariables, ...globalVariables];
}, [appDetail.chatConfig, inputs, nodeList, t]);
const variableText = useMemo(() => {
return variables
.map((item) => `${item.key}${item.key !== item.label ? `(${item.label})` : ''}`)
.join('\n');
}, [variables]);
const stringifyVariables = useMemo(
() =>
JSON.stringify({
params,
headers,
jsonBody,
variables
}),
[headers, jsonBody, params, variables]
);
const Render = useMemo(() => {
const { params, headers, jsonBody, variables } = JSON.parse(stringifyVariables);
return (
<Box>
<Flex alignItems={'center'} mb={2} fontWeight={'medium'} color={'myGray.600'}>
{t('core.module.Http request props')}
<QuestionTip
ml={1}
label={t('core.module.http.Props tip', { variable: variableText })}
></QuestionTip>
</Flex>
<Tabs
list={[
{ label: <RenderPropsItem text="Params" num={paramsLength} />, id: TabEnum.params },
...(!['GET', 'DELETE'].includes(requestMethods)
? [
{
label: (
<Flex alignItems={'center'}>
Body
{jsonBody?.value && <Box ml={1}></Box>}
</Flex>
),
id: TabEnum.body
}
]
: []),
{ label: <RenderPropsItem text="Headers" num={headersLength} />, id: TabEnum.headers }
]}
activeId={selectedTab}
onChange={(e) => setSelectedTab(e as any)}
/>
<Box bg={'white'} borderRadius={'md'}>
{params &&
headers &&
jsonBody &&
{
[TabEnum.params]: (
<RenderForm
nodeId={nodeId}
input={params}
variables={variables}
tabType={TabEnum.params}
/>
),
[TabEnum.body]: <RenderJson nodeId={nodeId} variables={variables} input={jsonBody} />,
[TabEnum.headers]: (
<RenderForm
nodeId={nodeId}
input={headers}
variables={variables}
tabType={TabEnum.headers}
/>
)
}[selectedTab]}
</Box>
</Box>
);
}, [
headersLength,
nodeId,
paramsLength,
requestMethods,
selectedTab,
stringifyVariables,
t,
variableText
]);
return Render;
}
const RenderForm = ({
nodeId,
input,
variables,
tabType
}: {
nodeId: string;
input: FlowNodeInputItemType;
variables: EditorVariablePickerType[];
tabType?: TabEnum;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [list, setList] = useState<PropsArrType[]>(input.value || []);
const [updateTrigger, setUpdateTrigger] = useState(false);
const [shouldUpdateNode, setShouldUpdateNode] = useState(false);
const leftVariables = useMemo(() => {
return (tabType === TabEnum.headers ? HttpHeaders : variables).filter((variable) => {
const existVariables = list.map((item) => item.key);
return !existVariables.includes(variable.key);
});
}, [list, tabType, variables]);
useEffect(() => {
setList(input.value || []);
}, [input.value]);
useEffect(() => {
if (shouldUpdateNode) {
onChangeNode({
nodeId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: list
}
});
setShouldUpdateNode(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [list]);
const handleKeyChange = useCallback(
(index: number, newKey: string) => {
setList((prevList) => {
if (!newKey) {
setUpdateTrigger((prev) => !prev);
toast({
status: 'warning',
title: t('core.module.http.Key cannot be empty')
});
return prevList;
}
const checkExist = prevList.find((item, i) => i !== index && item.key == newKey);
if (checkExist) {
setUpdateTrigger((prev) => !prev);
toast({
status: 'warning',
title: t('core.module.http.Key already exists')
});
return prevList;
}
return prevList.map((item, i) => (i === index ? { ...item, key: newKey } : item));
});
setShouldUpdateNode(true);
},
[t, toast]
);
const handleAddNewProps = useCallback(
(key: string, value: string = '') => {
setList((prevList) => {
if (!key) {
return prevList;
}
const checkExist = prevList.find((item) => item.key === key);
if (checkExist) {
setUpdateTrigger((prev) => !prev);
toast({
status: 'warning',
title: t('core.module.http.Key already exists')
});
return prevList;
}
return [...prevList, { key, type: 'string', value }];
});
setShouldUpdateNode(true);
},
[t, toast]
);
const Render = useMemo(() => {
return (
<Box mt={2} borderRadius={'md'} overflow={'hidden'} borderWidth={'1px'} borderBottom={'none'}>
<TableContainer overflowY={'visible'} overflowX={'unset'}>
<Table>
<Thead>
<Tr>
<Th px={2} borderBottomLeftRadius={'none !important'}>
{t('core.module.http.Props name')}
</Th>
<Th px={2} borderBottomRadius={'none !important'}>
{t('core.module.http.Props value')}
</Th>
</Tr>
</Thead>
<Tbody>
{list.map((item, index) => (
<Tr key={`${input.key}${index}`}>
<Td p={0} w={'150px'}>
<HttpInput
hasVariablePlugin={false}
hasDropDownPlugin={tabType === TabEnum.headers}
setDropdownValue={(value) => {
handleKeyChange(index, value);
setUpdateTrigger((prev) => !prev);
}}
placeholder={t('core.module.http.Props name')}
value={item.key}
variables={leftVariables}
onBlur={(val) => {
handleKeyChange(index, val);
}}
updateTrigger={updateTrigger}
/>
</Td>
<Td p={0}>
<Box display={'flex'} alignItems={'center'}>
<HttpInput
placeholder={t('core.module.http.Props value')}
value={item.value}
variables={variables}
onBlur={(val) => {
setList((prevList) =>
prevList.map((item, i) =>
i === index ? { ...item, value: val } : item
)
);
setShouldUpdateNode(true);
}}
/>
<MyIcon
name={'delete'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
w={'14px'}
onClick={() => {
setList((prevlist) => prevlist.filter((val) => val.key !== item.key));
setShouldUpdateNode(true);
}}
/>
</Box>
</Td>
</Tr>
))}
<Tr>
<Td p={0} w={'150px'}>
<HttpInput
hasVariablePlugin={false}
hasDropDownPlugin={tabType === TabEnum.headers}
setDropdownValue={(val) => {
handleAddNewProps(val);
setUpdateTrigger((prev) => !prev);
}}
placeholder={t('core.module.http.Add props')}
value={''}
variables={leftVariables}
updateTrigger={updateTrigger}
onBlur={(val) => {
handleAddNewProps(val);
setUpdateTrigger((prev) => !prev);
}}
/>
</Td>
<Td p={0}>
<Box display={'flex'} alignItems={'center'}>
<HttpInput />
</Box>
</Td>
</Tr>
</Tbody>
</Table>
</TableContainer>
</Box>
);
}, [
handleAddNewProps,
handleKeyChange,
input.key,
leftVariables,
list,
t,
tabType,
updateTrigger,
variables
]);
return Render;
};
const RenderJson = ({
nodeId,
input,
variables
}: {
nodeId: string;
input: FlowNodeInputItemType;
variables: EditorVariablePickerType[];
}) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [_, startSts] = useTransition();
const Render = useMemo(() => {
return (
<Box mt={1}>
<JSONEditor
bg={'white'}
defaultHeight={200}
resize
value={input.value}
placeholder={t('core.module.template.http body placeholder')}
onChange={(e) => {
startSts(() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: e
}
});
});
}}
variables={variables}
/>
</Box>
);
}, [input, nodeId, onChangeNode, t, variables]);
return Render;
};
const RenderPropsItem = ({ text, num }: { text: string; num: number }) => {
return (
<Flex alignItems={'center'}>
<Box>{text}</Box>
{num > 0 && (
<Box ml={1} borderRadius={'50%'} bg={'myGray.200'} px={2} py={'1px'}>
{num}
</Box>
)}
</Flex>
);
};
const NodeHttp = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (v) => v.splitToolInputs);
const { toolInputs, commonInputs, isTool } = splitToolInputs(inputs, nodeId);
const HttpMethodAndUrl = useMemoizedFn(() => (
<RenderHttpMethodAndUrl nodeId={nodeId} inputs={inputs} />
));
const Headers = useMemoizedFn(() => <RenderHttpProps nodeId={nodeId} inputs={inputs} />);
const CustomComponents = useMemo(() => {
return {
[NodeInputKeyEnum.httpMethod]: HttpMethodAndUrl,
[NodeInputKeyEnum.httpHeaders]: Headers
};
}, [Headers, HttpMethodAndUrl]);
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
{isTool && (
<>
<Container>
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} canEdit />
</Container>
</>
)}
<>
<Container>
<IOTitle text={t('common.Input')} />
<RenderInput
nodeId={nodeId}
flowInputList={commonInputs}
CustomComponent={CustomComponents}
/>
</Container>
</>
<>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput flowOutputList={outputs} nodeId={nodeId} />
</Container>
</>
</NodeCard>
);
};
export default React.memo(NodeHttp);

View File

@@ -1,470 +0,0 @@
import { Box, Button, Flex } from '@chakra-ui/react';
import {
DraggableProvided,
DraggableStateSnapshot
} from '@fastgpt/web/components/common/DndDrag/index';
import Container from '../../components/Container';
import { MinusIcon, SmallAddIcon } from '@chakra-ui/icons';
import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { useTranslation } from 'next-i18next';
import { ReferSelector, useReference } from '../render/RenderInput/templates/Reference';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import {
VariableConditionEnum,
allConditionList,
arrayConditionList,
booleanConditionList,
numberConditionList,
objectConditionList,
stringConditionList
} from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
import { useContextSelector } from 'use-context-selector';
import React, { useMemo } from 'react';
import { WorkflowContext } from '../../../context';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyInput from '@/components/MyInput';
import { getElseIFLabel, getHandleId } from '@fastgpt/global/core/workflow/utils';
import { SourceHandle } from '../render/Handle';
import { Position, useReactFlow } from 'reactflow';
import { getRefData } from '@/web/core/workflow/utils';
import DragIcon from '@fastgpt/web/components/common/DndDrag/DragIcon';
import { AppContext } from '@/web/core/app/context/appContext';
import { useI18n } from '@/web/context/I18n';
const ListItem = ({
provided,
snapshot,
conditionIndex,
conditionItem,
ifElseList,
onUpdateIfElseList,
nodeId
}: {
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
conditionIndex: number;
conditionItem: IfElseListItemType;
ifElseList: IfElseListItemType[];
onUpdateIfElseList: (value: IfElseListItemType[]) => void;
nodeId: string;
}) => {
const { t } = useTranslation();
const { getZoom } = useReactFlow();
const onDelEdge = useContextSelector(WorkflowContext, (v) => v.onDelEdge);
const handleId = getHandleId(nodeId, 'source', getElseIFLabel(conditionIndex));
const Render = useMemo(() => {
return (
<Flex
alignItems={'center'}
position={'relative'}
transform={snapshot.isDragging ? `scale(${getZoom()})` : ''}
transformOrigin={'top left'}
>
<Container w={snapshot.isDragging ? '' : 'full'} className="nodrag">
<Flex mb={4} alignItems={'center'}>
{ifElseList.length > 1 && <DragIcon provided={provided} />}
<Box color={'black'} fontSize={'md'} ml={2}>
{getElseIFLabel(conditionIndex)}
</Box>
{conditionItem.list?.length > 1 && (
<Flex
px={'2.5'}
color={'primary.600'}
fontWeight={'medium'}
alignItems={'center'}
cursor={'pointer'}
rounded={'md'}
onClick={() => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
condition: ifElse.condition === 'AND' ? 'OR' : 'AND'
};
}
return ifElse;
})
);
}}
>
{conditionItem.condition}
<MyIcon ml={1} boxSize={5} name="change" />
</Flex>
)}
<Box flex={1}></Box>
{ifElseList.length > 1 && (
<MyIcon
ml={2}
boxSize={5}
name="delete"
cursor={'pointer'}
_hover={{ color: 'red.600' }}
color={'myGray.400'}
onClick={() => {
onUpdateIfElseList(ifElseList.filter((_, index) => index !== conditionIndex));
onDelEdge({
nodeId,
sourceHandle: handleId
});
}}
/>
)}
</Flex>
<Box>
{conditionItem.list?.map((item, i) => {
return (
<Box key={i}>
{/* condition list */}
<Flex gap={2} mb={2} alignItems={'center'}>
{/* variable reference */}
<Box minW={'250px'}>
<Reference
nodeId={nodeId}
variable={item.variable}
onSelect={(e) => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
list: ifElse.list.map((item, index) => {
if (index === i) {
return {
...item,
variable: e,
condition: undefined
};
}
return item;
})
};
}
return ifElse;
})
);
}}
/>
</Box>
{/* condition select */}
<Box w={'130px'} flex={1}>
<ConditionSelect
condition={item.condition}
variable={item.variable}
onSelect={(e) => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
list: ifElse.list.map((item, index) => {
if (index === i) {
return {
...item,
condition: e
};
}
return item;
})
};
}
return ifElse;
})
);
}}
/>
</Box>
{/* value */}
<Box w={'200px'}>
<ConditionValueInput
value={item.value}
condition={item.condition}
variable={item.variable}
onChange={(e) => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
return {
...ifElse,
list:
index === conditionIndex
? ifElse.list.map((item, index) => {
if (index === i) {
return {
...item,
value: e
};
}
return item;
})
: ifElse.list
};
})
);
}}
/>
</Box>
{/* delete */}
{conditionItem.list.length > 1 && (
<MinusIcon
ml={2}
boxSize={3}
name="delete"
cursor={'pointer'}
_hover={{ color: 'red.600' }}
color={'myGray.400'}
onClick={() => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
list: ifElse.list.filter((_, index) => index !== i)
};
}
return ifElse;
})
);
}}
/>
)}
</Flex>
</Box>
);
})}
</Box>
<Button
onClick={() => {
onUpdateIfElseList(
ifElseList.map((ifElse, index) => {
if (index === conditionIndex) {
return {
...ifElse,
list: ifElse.list.concat({
variable: undefined,
condition: undefined,
value: undefined
})
};
}
return ifElse;
})
);
}}
variant={'link'}
leftIcon={<SmallAddIcon />}
my={3}
color={'primary.600'}
>
{t('core.module.input.add')}
</Button>
</Container>
{!snapshot.isDragging && (
<SourceHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Right}
translate={[18, 0]}
/>
)}
</Flex>
);
}, [
conditionIndex,
conditionItem.condition,
conditionItem.list,
getZoom,
handleId,
ifElseList,
nodeId,
onDelEdge,
onUpdateIfElseList,
provided,
snapshot.isDragging,
t
]);
return (
<Box
ref={provided.innerRef}
{...provided.draggableProps}
style={{
...provided.draggableProps.style,
opacity: snapshot.isDragging ? 0.8 : 1
}}
>
{Render}
</Box>
);
};
export default React.memo(ListItem);
const Reference = ({
nodeId,
variable,
onSelect
}: {
nodeId: string;
variable?: ReferenceValueProps;
onSelect: (e: ReferenceValueProps) => void;
}) => {
const { t } = useTranslation();
const { referenceList, formatValue } = useReference({
nodeId,
valueType: WorkflowIOValueTypeEnum.any,
value: variable
});
return (
<ReferSelector
placeholder={t('选择引用变量')}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
);
};
/* Different data types have different options */
const ConditionSelect = ({
condition,
variable,
onSelect
}: {
condition?: VariableConditionEnum;
variable?: ReferenceValueProps;
onSelect: (e: VariableConditionEnum) => void;
}) => {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
// get condition type
const { valueType, required } = useMemo(() => {
return getRefData({
variable,
nodeList,
chatConfig: appDetail.chatConfig,
t
});
}, [appDetail.chatConfig, nodeList, t, variable]);
const conditionList = useMemo(() => {
if (valueType === WorkflowIOValueTypeEnum.string) return stringConditionList;
if (valueType === WorkflowIOValueTypeEnum.number) return numberConditionList;
if (valueType === WorkflowIOValueTypeEnum.boolean) return booleanConditionList;
if (
valueType === WorkflowIOValueTypeEnum.chatHistory ||
valueType === WorkflowIOValueTypeEnum.datasetQuote ||
valueType === WorkflowIOValueTypeEnum.dynamic ||
valueType === WorkflowIOValueTypeEnum.selectApp ||
valueType === WorkflowIOValueTypeEnum.arrayBoolean ||
valueType === WorkflowIOValueTypeEnum.arrayNumber ||
valueType === WorkflowIOValueTypeEnum.arrayObject ||
valueType === WorkflowIOValueTypeEnum.arrayString
)
return arrayConditionList;
if (valueType === WorkflowIOValueTypeEnum.object) return objectConditionList;
if (valueType === WorkflowIOValueTypeEnum.any) return allConditionList;
return [];
}, [valueType]);
const filterQuiredConditionList = useMemo(() => {
if (required) {
return conditionList.filter(
(item) =>
item.value !== VariableConditionEnum.isEmpty &&
item.value !== VariableConditionEnum.isNotEmpty
);
}
return conditionList;
}, [conditionList, required]);
return (
<MySelect
className="nowheel"
w={'100%'}
list={filterQuiredConditionList}
value={condition}
onchange={onSelect}
placeholder="选择条件"
/>
);
};
/*
Different condition can be entered differently
empty, notEmpty: forbid input
boolean type: select true/false
*/
const ConditionValueInput = ({
value = '',
variable,
condition,
onChange
}: {
value?: string;
variable?: ReferenceValueProps;
condition?: VariableConditionEnum;
onChange: (e: string) => void;
}) => {
const { workflowT } = useI18n();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
// get value type
const valueType = useMemo(() => {
if (!variable) return;
const node = nodeList.find((node) => node.nodeId === variable[0]);
if (!node) return WorkflowIOValueTypeEnum.any;
const output = node.outputs.find((item) => item.id === variable[1]);
if (!output) return WorkflowIOValueTypeEnum.any;
return output.valueType;
}, [nodeList, variable]);
const Render = useMemo(() => {
if (valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<MySelect
list={[
{ label: 'True', value: 'true' },
{ label: 'False', value: 'false' }
]}
onchange={onChange}
value={value}
placeholder={workflowT('ifelse.Select value')}
isDisabled={
condition === VariableConditionEnum.isEmpty ||
condition === VariableConditionEnum.isNotEmpty
}
/>
);
} else {
return (
<MyInput
value={value}
placeholder={
condition === VariableConditionEnum.reg
? '/^((+|00)86)?1[3-9]d{9}$/'
: workflowT('ifelse.Input value')
}
w={'100%'}
bg={'white'}
isDisabled={
condition === VariableConditionEnum.isEmpty ||
condition === VariableConditionEnum.isNotEmpty
}
onChange={(e) => onChange(e.target.value)}
/>
);
}
}, [condition, onChange, value, valueType, workflowT]);
return Render;
};

View File

@@ -1,137 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import NodeCard from '../render/NodeCard';
import { useTranslation } from 'next-i18next';
import { Box, Button, Flex } from '@chakra-ui/react';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeProps, Position } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import Container from '../../components/Container';
import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag/index';
import { SourceHandle } from '../render/Handle';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import ListItem from './ListItem';
import { IfElseResultEnum } from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs = [] } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const elseHandleId = getHandleId(nodeId, 'source', IfElseResultEnum.ELSE);
const ifElseList = useMemo(
() =>
(inputs.find((input) => input.key === NodeInputKeyEnum.ifElseList)
?.value as IfElseListItemType[]) || [],
[inputs]
);
const onUpdateIfElseList = useCallback(
(value: IfElseListItemType[]) => {
const ifElseListInput = inputs.find((input) => input.key === NodeInputKeyEnum.ifElseList);
if (!ifElseListInput) return;
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.ifElseList,
value: {
...ifElseListInput,
value
}
});
},
[inputs, nodeId, onChangeNode]
);
return (
<NodeCard selected={selected} maxW={'1000px'} {...data}>
<Box px={4} cursor={'default'}>
<DndDrag<IfElseListItemType>
onDragEndCb={(list: IfElseListItemType[]) => onUpdateIfElseList(list)}
dataList={ifElseList}
renderClone={(provided, snapshot, rubric) => (
<ListItem
provided={provided}
snapshot={snapshot}
conditionItem={ifElseList[rubric.source.index]}
conditionIndex={rubric.source.index}
ifElseList={ifElseList}
onUpdateIfElseList={onUpdateIfElseList}
nodeId={nodeId}
/>
)}
>
{(provided) => (
<Box {...provided.droppableProps} ref={provided.innerRef}>
{ifElseList.map((conditionItem, conditionIndex) => (
<Draggable
key={conditionIndex}
draggableId={conditionIndex.toString()}
index={conditionIndex}
>
{(provided, snapshot) => (
<ListItem
provided={provided}
snapshot={snapshot}
conditionItem={conditionItem}
conditionIndex={conditionIndex}
ifElseList={ifElseList}
onUpdateIfElseList={onUpdateIfElseList}
nodeId={nodeId}
/>
)}
</Draggable>
))}
</Box>
)}
</DndDrag>
<Container position={'relative'}>
<Flex alignItems={'center'}>
<Box color={'black'} fontSize={'md'} ml={2}>
{IfElseResultEnum.ELSE}
</Box>
<SourceHandle
nodeId={nodeId}
handleId={elseHandleId}
position={Position.Right}
translate={[26, 0]}
/>
</Flex>
</Container>
</Box>
<Box py={3} px={6}>
<Button
variant={'whiteBase'}
w={'full'}
onClick={() => {
const ifElseListInput = inputs.find(
(input) => input.key === NodeInputKeyEnum.ifElseList
);
if (!ifElseListInput) return;
onUpdateIfElseList([
...ifElseList,
{
condition: 'AND',
list: [
{
variable: undefined,
condition: undefined,
value: undefined
}
]
}
]);
}}
>
{t('core.module.input.Add Branch')}
</Button>
</Box>
</NodeCard>
);
};
export default React.memo(NodeIfElse);

View File

@@ -1,332 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Container from '../components/Container';
import { Box, Button, Center, Flex, useDisclosure } from '@chakra-ui/react';
import { WorkflowIOValueTypeEnum, NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { getLafAppDetail } from '@/web/support/laf/api';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { getApiSchemaByUrl } from '@/web/core/plugin/api';
import { getType, str2OpenApiSchema } from '@fastgpt/global/core/plugin/httpPlugin/utils';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { ChevronRightIcon } from '@chakra-ui/icons';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import {
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { useToast } from '@fastgpt/web/hooks/useToast';
import RenderToolInput from './render/RenderToolInput';
import RenderInput from './render/RenderInput';
import RenderOutput from './render/RenderOutput';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import {
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import IOTitle from '../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { putUpdateTeam } from '@/web/support/user/team/api';
const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccountModal'));
const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { toast } = useToast();
const { feConfigs } = useSystemStore();
const { data, selected } = props;
const { nodeId, inputs, outputs } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const requestUrl = inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl);
const { userInfo, initUserInfo } = useUserStore();
const token = userInfo?.team.lafAccount?.token;
const appid = userInfo?.team.lafAccount?.appid;
const {
data: lafData,
isLoading: isLoadingFunctions,
refetch: refetchFunction
} = useQuery(
['getLafFunctionList'],
async () => {
// load laf app detail
try {
const appDetail = await getLafAppDetail(appid || '');
// load laf app functions
const schemaUrl = `https://${appDetail?.domain.domain}/_/api-docs?token=${appDetail?.openapi_token}`;
const schema = await getApiSchemaByUrl(schemaUrl);
const openApiSchema = await str2OpenApiSchema(JSON.stringify(schema));
const filterPostSchema = openApiSchema.pathData.filter((item) => item.method === 'post');
return {
lafApp: appDetail,
lafFunctions: filterPostSchema.map((item) => ({
...item,
requestUrl: `https://${appDetail?.domain.domain}${item.path}`
}))
};
} catch (err) {
await putUpdateTeam({
lafAccount: { token: '', appid: '', pat: '' }
});
initUserInfo();
}
},
{
enabled: !!token && !!appid,
onError(err) {
toast({
status: 'error',
title: getErrText(err, '获取Laf函数列表失败')
});
}
}
);
const lafFunctionSelectList = useMemo(
() =>
lafData?.lafFunctions.map((item) => {
const functionName = item.path.slice(1);
return {
alias: functionName,
label: item.description ? (
<Box>
<Box>{functionName}</Box>
<Box fontSize={'xs'} color={'gray.500'}>
{item.description}
</Box>
</Box>
) : (
functionName
),
value: item.requestUrl
};
}) || [],
[lafData?.lafFunctions]
);
const selectedFunction = useMemo(
() => lafFunctionSelectList.find((item) => item.value === requestUrl?.value)?.value,
[lafFunctionSelectList, requestUrl?.value]
);
const { mutate: onSyncParams, isLoading: isSyncing } = useRequest({
mutationFn: async () => {
await refetchFunction();
const lafFunction = lafData?.lafFunctions.find(
(item) => item.requestUrl === selectedFunction
);
if (!lafFunction) return;
// update intro
if (lafFunction.description) {
onChangeNode({
nodeId,
type: 'attr',
key: 'intro',
value: lafFunction.description
});
}
// add input variables
const bodyParams =
lafFunction?.request?.content?.['application/json']?.schema?.properties || {};
const requiredParams =
lafFunction?.request?.content?.['application/json']?.schema?.required || [];
const allParams = [
...Object.keys(bodyParams).map((key) => ({
name: key,
desc: bodyParams[key].description,
required: requiredParams?.includes(key) || false,
value: `{{${key}}}`,
type: getType(bodyParams[key])
}))
].filter((item) => !inputs.find((input) => input.key === item.name));
allParams.forEach((param) => {
const newInput: FlowNodeInputItemType = {
key: param.name,
valueType: param.type,
label: param.name,
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: param.required,
description: param.desc || '',
toolDescription: param.desc || '未设置参数描述',
canEdit: true,
editField: {
key: true,
valueType: true
}
};
onChangeNode({
nodeId,
type: 'addInput',
value: newInput
});
});
/* add output variables */
const responseParams =
lafFunction?.response?.default.content?.['application/json'].schema.properties || {};
const requiredResponseParams =
lafFunction?.response?.default.content?.['application/json'].schema.required || [];
const allResponseParams = [
...Object.keys(responseParams).map((key) => ({
valueType: getType(responseParams[key]),
name: key,
desc: responseParams[key].description,
required: requiredResponseParams?.includes(key) || false
}))
].filter((item) => !outputs.find((output) => output.key === item.name));
allResponseParams.forEach((param) => {
const newOutput: FlowNodeOutputItemType = {
id: getNanoid(),
key: param.name,
valueType: param.valueType,
label: param.name,
type: FlowNodeOutputTypeEnum.dynamic,
required: param.required,
description: param.desc || ''
};
onChangeNode({
nodeId,
type: 'addOutput',
value: newOutput
});
});
},
successToast: t('common.Sync success')
});
// not config laf
if (!token || !appid) {
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<ConfigLaf />
</NodeCard>
);
} else {
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<Container>
{/* select function */}
<MySelect
isLoading={isLoadingFunctions}
list={lafFunctionSelectList}
placeholder={t('core.module.laf.Select laf function')}
onchange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: e
}
});
}}
value={selectedFunction}
/>
{/* auto set params and go to edit */}
{!!selectedFunction && (
<Flex justifyContent={'flex-end'} mt={2} gap={2}>
<Button isLoading={isSyncing} variant={'grayBase'} size={'sm'} onClick={onSyncParams}>
{t('core.module.Laf sync params')}
</Button>
<Button
variant={'grayBase'}
size={'sm'}
onClick={() => {
const lafFunction = lafData?.lafFunctions.find(
(item) => item.requestUrl === selectedFunction
);
if (!lafFunction) return;
const url = `${feConfigs.lafEnv}/app/${lafData?.lafApp?.appid}/function${lafFunction?.path}?templateid=FastGPT_Laf`;
window.open(url, '_blank');
}}
>
{t('plugin.go to laf')}
</Button>
</Flex>
)}
</Container>
{!!selectedFunction && <RenderIO {...props} />}
</NodeCard>
);
}
};
export default React.memo(NodeLaf);
const ConfigLaf = () => {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { feConfigs } = useSystemStore();
const {
isOpen: isOpenLafConfig,
onOpen: onOpenLafConfig,
onClose: onCloseLafConfig
} = useDisclosure();
return !!feConfigs?.lafEnv ? (
<Center minH={150}>
<Button onClick={onOpenLafConfig} variant={'whitePrimary'}>
{t('plugin.Please bind laf accout first')} <ChevronRightIcon />
</Button>
{isOpenLafConfig && feConfigs?.lafEnv && (
<LafAccountModal defaultData={userInfo?.team.lafAccount} onClose={onCloseLafConfig} />
)}
</Center>
) : (
<Box>Laf环境</Box>
);
};
const RenderIO = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { commonInputs, toolInputs, isTool } = splitToolInputs(inputs, nodeId);
return (
<>
{isTool && (
<>
<Container>
<IOTitle text={t('core.module.tool.Tool input')} />
<RenderToolInput nodeId={nodeId} inputs={toolInputs} canEdit />
</Container>
</>
)}
<>
<Container>
<IOTitle text={t('common.Input')} />
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
</Container>
</>
<>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput flowOutputList={outputs} nodeId={nodeId} />
</Container>
</>
</>
);
};

View File

@@ -1,225 +0,0 @@
import React, { useMemo, useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { Box, Button, Flex } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import {
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io.d';
import Container from '../components/Container';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import type {
EditInputFieldMapType,
EditNodeFieldType
} from '@fastgpt/global/core/workflow/node/type.d';
import { useTranslation } from 'next-i18next';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import {
FlowNodeInputMap,
FlowNodeInputTypeEnum,
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import VariableTable from '../nodes/render/VariableTable';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const defaultCreateField: EditNodeFieldType = {
label: '',
key: '',
description: '',
inputType: FlowNodeInputTypeEnum.reference,
valueType: WorkflowIOValueTypeEnum.string,
required: true
};
const createEditField: EditInputFieldMapType = {
key: true,
description: true,
required: true,
valueType: true,
inputType: true
};
const dynamicInputEditField: EditInputFieldMapType = {
key: true
};
const NodePluginInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [createField, setCreateField] = useState<EditNodeFieldType>();
const [editField, setEditField] = useState<EditNodeFieldType>();
const Render = useMemo(() => {
return (
<NodeCard
minW={'300px'}
selected={selected}
menuForbid={{
rename: true,
copy: true,
delete: true
}}
{...data}
>
<Container mt={1}>
<Flex className="nodrag" cursor={'default'} alignItems={'center'} mb={3}>
<Box>{t('core.workflow.Custom inputs')}</Box>
<Box flex={'1 0 0'} />
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
onClick={() => setCreateField(defaultCreateField)}
>
{t('common.Add New')}
</Button>
</Flex>
<VariableTable
fieldEditType={createEditField}
keys={inputs.map((input) => input.key)}
onCloseFieldEdit={() => {
setCreateField(undefined);
setEditField(undefined);
}}
variables={inputs.map((input) => {
const inputType = input.renderTypeList[0];
return {
icon: FlowNodeInputMap[inputType]?.icon as string,
label: t(input.label),
type: input.valueType ? t(FlowValueTypeMap[input.valueType]?.label) : '-',
key: input.key
};
})}
createField={createField}
onCreate={({ data }) => {
if (!data.key || !data.inputType) {
return;
}
const newInput: FlowNodeInputItemType = {
key: data.key,
valueType: data.valueType,
label: data.label || '',
renderTypeList: [data.inputType],
required: data.required,
description: data.description,
toolDescription: data.isToolInput ? data.description : undefined,
canEdit: true,
value: data.defaultValue,
editField: dynamicInputEditField,
maxLength: data.maxLength,
max: data.max,
min: data.min,
dynamicParamDefaultValue: data.dynamicParamDefaultValue
};
onChangeNode({
nodeId,
type: 'addInput',
value: newInput
});
const newOutput: FlowNodeOutputItemType = {
id: getNanoid(),
key: data.key,
valueType: data.valueType,
label: data.label,
type: FlowNodeOutputTypeEnum.static
};
onChangeNode({
nodeId,
type: 'addOutput',
value: newOutput
});
setCreateField(undefined);
}}
editField={editField}
onStartEdit={(key) => {
const input = inputs.find((input) => input.key === key);
if (!input) return;
setEditField({
...input,
inputType: input.renderTypeList[0],
isToolInput: !!input.toolDescription
});
}}
onEdit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !editField?.key) return;
const output = outputs.find((output) => output.key === editField.key);
const newInput: FlowNodeInputItemType = {
...data,
key: data.key,
label: data.label || '',
renderTypeList: [data.inputType],
toolDescription: data.isToolInput ? data.description : undefined,
canEdit: true,
value: data.defaultValue,
editField: dynamicInputEditField
};
const newOutput: FlowNodeOutputItemType = {
...(output as FlowNodeOutputItemType),
valueType: data.valueType,
key: data.key,
label: data.label
};
if (changeKey) {
onChangeNode({
nodeId,
type: 'replaceInput',
key: editField.key,
value: newInput
});
onChangeNode({
nodeId,
type: 'replaceOutput',
key: editField.key,
value: newOutput
});
} else {
onChangeNode({
nodeId,
type: 'updateInput',
key: newInput.key,
value: newInput
});
onChangeNode({
nodeId,
type: 'updateOutput',
key: newOutput.key,
value: newOutput
});
}
setEditField(undefined);
}}
onDelete={(key) => {
onChangeNode({
nodeId,
type: 'delInput',
key
});
onChangeNode({
nodeId,
type: 'delOutput',
key
});
}}
/>
</Container>
</NodeCard>
);
}, [createField, data, editField, inputs, nodeId, onChangeNode, outputs, selected, t]);
return Render;
};
export default React.memo(NodePluginInput);

View File

@@ -1,105 +0,0 @@
import React, { useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import dynamic from 'next/dynamic';
import { Box, Button, Flex } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import Container from '../components/Container';
import { EditInputFieldMapType, EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import RenderInput from './render/RenderInput';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const FieldEditModal = dynamic(() => import('./render/FieldEditModal'));
const defaultCreateField: EditNodeFieldType = {
inputType: FlowNodeInputTypeEnum.reference,
key: '',
description: '',
valueType: WorkflowIOValueTypeEnum.string
};
const createEditField: EditInputFieldMapType = {
key: true,
description: true,
valueType: true
};
const NodePluginOutput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs } = data;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [createField, setCreateField] = useState<EditNodeFieldType>();
return (
<NodeCard
minW={'300px'}
selected={selected}
menuForbid={{
debug: true,
rename: true,
copy: true,
delete: true
}}
{...data}
>
<Container mt={1}>
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Box position={'relative'} fontWeight={'medium'}>
{t('core.workflow.Custom outputs')}
</Box>
<Box flex={'1 0 0'} />
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
onClick={() => setCreateField(defaultCreateField)}
>
{t('common.Add New')}
</Button>
</Flex>
<RenderInput nodeId={nodeId} flowInputList={inputs} />
</Container>
{!!createField && (
<FieldEditModal
editField={createEditField}
defaultField={createField}
keys={inputs.map((input) => input.key)}
onClose={() => setCreateField(undefined)}
onSubmit={({ data }) => {
if (!data.key || !data.label) {
return;
}
const newInput: FlowNodeInputItemType = {
key: data.key,
valueType: data.valueType,
label: data.label,
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: false,
description: data.description,
canEdit: true,
editField: createEditField
};
onChangeNode({
nodeId,
type: 'addInput',
value: newInput
});
setCreateField(undefined);
}}
/>
)}
</NodeCard>
);
};
export default React.memo(NodePluginOutput);

View File

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

View File

@@ -1,221 +0,0 @@
import React, { Dispatch, useMemo, useTransition } from 'react';
import { NodeProps } from 'reactflow';
import { Box, useTheme } from '@chakra-ui/react';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import QGSwitch from '@/components/core/app/QGSwitch';
import TTSSelect from '@/components/core/app/TTSSelect';
import WhisperConfig from '@/components/core/app/WhisperConfig';
import InputGuideConfig from '@/components/core/app/InputGuideConfig';
import { getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import { TTSTypeEnum } from '@/web/core/app/constants';
import NodeCard from './render/NodeCard';
import ScheduledTriggerConfig from '@/components/core/app/ScheduledTriggerConfig';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { AppChatConfigType, AppDetailType, VariableItemType } from '@fastgpt/global/core/app/type';
import { useMemoizedFn } from 'ahooks';
import VariableEdit from '@/components/core/app/VariableEdit';
import { AppContext } from '@/web/core/app/context/appContext';
import WelcomeTextConfig from '@/components/core/app/WelcomeTextConfig';
type ComponentProps = {
chatConfig: AppChatConfigType;
setAppDetail: Dispatch<React.SetStateAction<AppDetailType>>;
};
const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const theme = useTheme();
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
const chatConfig = useMemo<AppChatConfigType>(() => {
return getAppChatConfig({
chatConfig: appDetail.chatConfig,
systemConfigNode: data,
isPublicFetch: true
});
}, [data, appDetail]);
const componentsProps = useMemo(
() => ({
chatConfig,
setAppDetail
}),
[chatConfig, setAppDetail]
);
return (
<>
<NodeCard
minW={'300px'}
selected={selected}
menuForbid={{
debug: true,
rename: true,
copy: true,
delete: true
}}
{...data}
>
<Box px={4} py={'10px'} position={'relative'} borderRadius={'md'} className="nodrag">
<WelcomeText {...componentsProps} />
<Box pt={4}>
<ChatStartVariable {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<TTSGuide {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<WhisperGuide {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<QuestionGuide {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<ScheduledTrigger {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={theme.borders.base}>
<QuestionInputGuide {...componentsProps} />
</Box>
</Box>
</NodeCard>
</>
);
};
export default React.memo(NodeUserGuide);
function WelcomeText({ chatConfig: { welcomeText }, setAppDetail }: ComponentProps) {
const { t } = useTranslation();
const [, startTst] = useTransition();
return (
<Box className="nodrag">
<WelcomeTextConfig
resize={'both'}
defaultValue={welcomeText}
onChange={(e) => {
startTst(() => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
welcomeText: e.target.value
}
}));
});
}}
/>
</Box>
);
}
function ChatStartVariable({ chatConfig: { variables = [] }, setAppDetail }: ComponentProps) {
const updateVariables = useMemoizedFn((value: VariableItemType[]) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
variables: value
}
}));
});
return <VariableEdit variables={variables} onChange={(e) => updateVariables(e)} />;
}
function QuestionGuide({ chatConfig: { questionGuide = false }, setAppDetail }: ComponentProps) {
return (
<QGSwitch
isChecked={questionGuide}
onChange={(e) => {
const value = e.target.checked;
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
questionGuide: value
}
}));
}}
/>
);
}
function TTSGuide({ chatConfig: { ttsConfig }, setAppDetail }: ComponentProps) {
return (
<TTSSelect
value={ttsConfig}
onChange={(e) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
ttsConfig: e
}
}));
}}
/>
);
}
function WhisperGuide({ chatConfig: { whisperConfig, ttsConfig }, setAppDetail }: ComponentProps) {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
return (
<WhisperConfig
isOpenAudio={ttsConfig?.type !== TTSTypeEnum.none}
value={whisperConfig}
onChange={(e) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
whisperConfig: e
}
}));
}}
/>
);
}
function ScheduledTrigger({
chatConfig: { scheduledTriggerConfig },
setAppDetail
}: ComponentProps) {
return (
<ScheduledTriggerConfig
value={scheduledTriggerConfig}
onChange={(e) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
scheduledTriggerConfig: e
}
}));
}}
/>
);
}
function QuestionInputGuide({ chatConfig: { chatInputGuide }, setAppDetail }: ComponentProps) {
const appId = useContextSelector(WorkflowContext, (v) => v.appId);
return appId ? (
<InputGuideConfig
appId={appId}
value={chatInputGuide}
onChange={(e) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
chatInputGuide: e
}
}));
}}
/>
) : null;
}

View File

@@ -1,43 +0,0 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Divider from '../components/Divider';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import { useTranslation } from 'next-i18next';
import { ToolSourceHandle } from './render/Handle/ToolHandle';
import { Box } from '@chakra-ui/react';
import IOTitle from '../components/IOTitle';
import MyIcon from '@fastgpt/web/components/common/Icon';
import RenderOutput from './render/RenderOutput';
const NodeTools = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<Container>
<IOTitle text={t('common.Input')} />
<RenderInput nodeId={nodeId} flowInputList={inputs} />
</Container>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<Box position={'relative'}>
<Box borderBottomLeftRadius={'md'} borderBottomRadius={'md'} overflow={'hidden'}>
<Divider
showBorderBottom={false}
icon={<MyIcon name="phoneTabbar/tool" w={'16px'} h={'16px'} />}
text={t('core.workflow.tool.Select Tool')}
/>
</Box>
<ToolSourceHandle nodeId={nodeId} />
</Box>
</NodeCard>
);
};
export default React.memo(NodeTools);

View File

@@ -1,311 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import NodeCard from './render/NodeCard';
import { NodeProps } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
import { useTranslation } from 'next-i18next';
import {
Box,
Button,
Flex,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Switch,
Textarea
} from '@chakra-ui/react';
import { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import {
FlowNodeInputMap,
FlowNodeInputTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import Container from '../components/Container';
import MyIcon from '@fastgpt/web/components/common/Icon';
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import { SmallAddIcon } from '@chakra-ui/icons';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { ReferSelector, useReference } from './render/RenderInput/templates/Reference';
import { getRefData } from '@/web/core/workflow/utils';
import { isReferenceValue } from '@fastgpt/global/core/workflow/utils';
import { AppContext } from '@/web/core/app/context/appContext';
const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { inputs = [], nodeId } = data;
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const updateList = useMemo(
() =>
(inputs.find((input) => input.key === NodeInputKeyEnum.updateList)
?.value as TUpdateListItem[]) || [],
[inputs]
);
const onUpdateList = useCallback(
(value: TUpdateListItem[]) => {
const updateListInput = inputs.find((input) => input.key === NodeInputKeyEnum.updateList);
if (!updateListInput) return;
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.updateList,
value: {
...updateListInput,
value
}
});
},
[inputs, nodeId, onChangeNode]
);
const Render = useMemo(() => {
const menuList = [
{
renderType: FlowNodeInputTypeEnum.input,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.input].icon,
label: t('core.workflow.inputType.Manual input')
},
{
renderType: FlowNodeInputTypeEnum.reference,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.reference].icon,
label: t('core.workflow.inputType.Reference')
}
];
return (
<>
{updateList.map((updateItem, index) => {
const { valueType } = getRefData({
variable: updateItem.variable,
nodeList,
chatConfig: appDetail.chatConfig,
t
});
const renderTypeData = menuList.find((item) => item.renderType === updateItem.renderType);
const handleUpdate = (newValue: ReferenceValueProps | string) => {
if (isReferenceValue(newValue)) {
onUpdateList(
updateList.map((update, i) =>
i === index ? { ...update, value: newValue as ReferenceValueProps } : update
)
);
} else {
onUpdateList(
updateList.map((update, i) =>
i === index ? { ...update, value: ['', newValue as string] } : update
)
);
}
};
return (
<Container key={index} mt={4} w={'full'} mx={0}>
<Flex alignItems={'center'}>
<Flex w={'60px'}>{t('core.workflow.variable')}</Flex>
<Reference
nodeId={nodeId}
variable={updateItem.variable}
onSelect={(value) => {
onUpdateList(
updateList.map((update, i) => {
if (i === index) {
return {
...update,
value: ['', ''],
valueType,
variable: value
};
}
return update;
})
);
}}
/>
<Box flex={1} />
{updateList.length > 1 && (
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'red.500' }}
position={'absolute'}
top={3}
right={3}
onClick={() => {
onUpdateList(updateList.filter((_, i) => i !== index));
}}
/>
)}
</Flex>
<Flex mt={2} w={'full'} alignItems={'center'} className="nodrag">
<Flex w={'60px'}>
<Box>{t('core.workflow.value')}</Box>
<MyTooltip
label={
menuList.find((item) => item.renderType === updateItem.renderType)?.label
}
>
<Button
size={'xs'}
bg={'white'}
borderRadius={'xs'}
mx={2}
onClick={() => {
onUpdateList(
updateList.map((update, i) => {
if (i === index) {
return {
...update,
value: ['', ''],
renderType:
updateItem.renderType === FlowNodeInputTypeEnum.input
? FlowNodeInputTypeEnum.reference
: FlowNodeInputTypeEnum.input
};
}
return update;
})
);
}}
>
<MyIcon name={renderTypeData?.icon as any} w={'14px'} />
</Button>
</MyTooltip>
</Flex>
{/* Render input components */}
{(() => {
if (updateItem.renderType === FlowNodeInputTypeEnum.reference) {
return (
<Reference
nodeId={nodeId}
variable={updateItem.value}
valueType={valueType}
onSelect={handleUpdate}
/>
);
}
if (valueType === WorkflowIOValueTypeEnum.string) {
return (
<Textarea
bg="white"
value={updateItem.value?.[1] || ''}
w="300px"
onChange={(e) => handleUpdate(e.target.value)}
/>
);
}
if (valueType === WorkflowIOValueTypeEnum.number) {
return (
<NumberInput value={Number(updateItem.value?.[1]) || 0}>
<NumberInputField
bg="white"
onChange={(e) => handleUpdate(e.target.value)}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
}
if (valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<Switch
defaultChecked={updateItem.value?.[1] === 'true'}
onChange={(e) => handleUpdate(String(e.target.checked))}
/>
);
}
return (
<JsonEditor
bg="white"
resize
w="300px"
value={String(updateItem.value?.[1] || '')}
onChange={(e) => {
handleUpdate(e);
}}
/>
);
})()}
</Flex>
</Container>
);
})}
</>
);
}, [nodeId, nodeList, onUpdateList, t, updateList]);
return (
<NodeCard selected={selected} maxW={'1000px'} {...data}>
<Box px={4} pb={4}>
{Render}
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Button
variant={'whiteBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
w={'full'}
size={'sm'}
onClick={() => {
onUpdateList([
...updateList,
{
variable: ['', ''],
value: ['', ''],
renderType: FlowNodeInputTypeEnum.input
}
]);
}}
>
{t('common.Add New')}
</Button>
</Flex>
</Box>
</NodeCard>
);
};
export default React.memo(NodeVariableUpdate);
const Reference = ({
nodeId,
variable,
valueType,
onSelect
}: {
nodeId: string;
variable?: ReferenceValueProps;
valueType?: WorkflowIOValueTypeEnum;
onSelect: (e: ReferenceValueProps) => void;
}) => {
const { t } = useTranslation();
const { referenceList, formatValue } = useReference({
nodeId,
valueType,
value: variable
});
return (
<ReferSelector
placeholder={t('选择引用变量')}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
);
};

View File

@@ -1,64 +0,0 @@
import React, { useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Container from '../components/Container';
import RenderOutput from './render/RenderOutput';
import IOTitle from '../components/IOTitle';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { useCreation } from 'ahooks';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { AppContext } from '@/web/core/app/context/appContext';
const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, outputs } = data;
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const variablesOutputs = useCreation(() => {
const variables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
return variables.map<FlowNodeOutputItemType>((item) => ({
id: item.key,
type: FlowNodeOutputTypeEnum.static,
key: item.key,
required: item.required,
valueType: item.valueType || WorkflowIOValueTypeEnum.any,
label: item.label
}));
}, [nodeList, t]);
return (
<NodeCard
minW={'240px'}
selected={selected}
menuForbid={{
rename: true,
copy: true,
delete: true
}}
{...data}
>
<Container>
<IOTitle text={t('common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<Container>
<IOTitle text={t('core.module.Variable')} />
<RenderOutput nodeId={nodeId} flowOutputList={variablesOutputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeStart);

View File

@@ -1,530 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import {
Box,
Button,
ModalFooter,
ModalBody,
Flex,
Switch,
Input,
Textarea,
Stack
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import {
EditInputFieldMapType,
EditNodeFieldType
} from '@fastgpt/global/core/workflow/node/type.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import dynamic from 'next/dynamic';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput/index';
import { useI18n } from '@/web/context/I18n';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
const EmptyTip = dynamic(() => import('@fastgpt/web/components/common/EmptyTip'));
const defaultValue: EditNodeFieldType = {
inputType: FlowNodeInputTypeEnum.reference,
valueType: WorkflowIOValueTypeEnum.string,
key: '',
label: '',
description: '',
isToolInput: false,
defaultValue: '',
maxLength: undefined,
max: undefined,
min: undefined,
editField: {},
dynamicParamDefaultValue: {
inputType: FlowNodeInputTypeEnum.reference,
valueType: WorkflowIOValueTypeEnum.string,
required: true
}
};
const FieldEditModal = ({
editField = {
key: true
},
defaultField,
keys = [],
onClose,
onSubmit
}: {
editField?: EditInputFieldMapType;
defaultField: EditNodeFieldType;
keys: string[];
onClose: () => void;
onSubmit: (e: { data: EditNodeFieldType; changeKey: boolean }) => void;
}) => {
const { t } = useTranslation();
const { workflowT } = useI18n();
const { toast } = useToast();
const showDynamicInputSelect =
!keys.includes(NodeInputKeyEnum.addInputParam) ||
defaultField.key === NodeInputKeyEnum.addInputParam;
const inputTypeList = useMemo(
() => [
{
label: t('core.workflow.inputType.Reference'),
value: FlowNodeInputTypeEnum.reference,
defaultValue: {}
},
{
label: t('core.workflow.inputType.input'),
value: FlowNodeInputTypeEnum.input,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.string
}
},
{
label: t('core.workflow.inputType.textarea'),
value: FlowNodeInputTypeEnum.textarea,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.string
}
},
{
label: t('core.workflow.inputType.JSON Editor'),
value: FlowNodeInputTypeEnum.JSONEditor,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.string
}
},
{
label: t('core.workflow.inputType.number input'),
value: FlowNodeInputTypeEnum.numberInput,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.number
}
},
{
label: t('core.workflow.inputType.switch'),
value: FlowNodeInputTypeEnum.switch,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.boolean
}
},
{
label: t('core.workflow.inputType.selectApp'),
value: FlowNodeInputTypeEnum.selectApp,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.selectApp
}
},
{
label: t('core.workflow.inputType.selectLLMModel'),
value: FlowNodeInputTypeEnum.selectLLMModel,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.string
}
},
{
label: t('core.workflow.inputType.selectDataset'),
value: FlowNodeInputTypeEnum.selectDataset,
defaultValue: {
valueType: WorkflowIOValueTypeEnum.selectDataset
}
},
...(showDynamicInputSelect
? [
{
label: t('core.workflow.inputType.dynamicTargetInput'),
value: FlowNodeInputTypeEnum.addInputParam,
defaultValue: {
label: t('core.workflow.inputType.dynamicTargetInput'),
valueType: WorkflowIOValueTypeEnum.dynamic,
key: NodeInputKeyEnum.addInputParam,
required: false
}
}
]
: [])
],
[showDynamicInputSelect, t]
);
const { register, getValues, setValue, handleSubmit, watch } = useForm<EditNodeFieldType>({
defaultValues: {
...defaultValue,
...defaultField,
valueType: defaultField.valueType ?? WorkflowIOValueTypeEnum.string
}
});
const inputType = watch('inputType');
const valueType = watch('valueType');
const isToolInput = watch('isToolInput');
const maxLength = watch('maxLength');
const max = watch('max');
const min = watch('min');
const defaultInputValueType = watch('dynamicParamDefaultValue.valueType');
const showKeyInput = useMemo(() => {
if (inputType === FlowNodeInputTypeEnum.addInputParam) return false;
return editField.key;
}, [editField.key, inputType]);
const showInputTypeSelect = useMemo(() => {
return editField.inputType;
}, [editField.inputType]);
const showDescriptionInput = useMemo(() => {
return editField.description;
}, [editField.description]);
const showValueTypeSelect = useMemo(() => {
if (!editField.valueType) return false;
if (inputType !== FlowNodeInputTypeEnum.reference) return false;
return true;
}, [editField.valueType, inputType]);
// input type config
const showToolInput = useMemo(() => {
return inputType === FlowNodeInputTypeEnum.reference;
}, [inputType]);
const showDefaultValue = useMemo(() => {
if (inputType === FlowNodeInputTypeEnum.input) return true;
if (inputType === FlowNodeInputTypeEnum.textarea) return true;
if (inputType === FlowNodeInputTypeEnum.JSONEditor) return true;
if (inputType === FlowNodeInputTypeEnum.numberInput) return true;
if (inputType === FlowNodeInputTypeEnum.switch) return true;
return false;
}, [inputType]);
const showMaxLenInput = useMemo(() => {
if (inputType === FlowNodeInputTypeEnum.input) return true;
if (inputType === FlowNodeInputTypeEnum.textarea) return true;
return false;
}, [inputType]);
const showMinMaxInput = useMemo(
() => inputType === FlowNodeInputTypeEnum.numberInput,
[inputType]
);
const showDynamicInput = useMemo(() => {
return inputType === FlowNodeInputTypeEnum.addInputParam;
}, [inputType]);
const slicedTypeMap = Object.values(FlowValueTypeMap).slice(0, -1);
const dataTypeSelectList = slicedTypeMap.map((item) => ({
label: t(item.label),
value: item.value
}));
const onSubmitSuccess = useCallback(
(data: EditNodeFieldType) => {
data.key = data?.key?.trim();
// add default value
const inputTypeConfig = inputTypeList.find((item) => item.value === data.inputType);
if (inputTypeConfig?.defaultValue) {
data.label = data.key;
for (const key in inputTypeConfig.defaultValue) {
// @ts-ignore
data[key] = inputTypeConfig.defaultValue[key];
}
}
if (!data.key) {
return toast({
status: 'warning',
title: t('core.module.edit.Field Name Cannot Be Empty')
});
}
// create check key
if (!defaultField.key && keys.includes(data.key)) {
return toast({
status: 'warning',
title: t('core.module.edit.Field Already Exist')
});
}
// edit check repeat key
if (defaultField.key && defaultField.key !== data.key && keys.includes(data.key)) {
return toast({
status: 'warning',
title: t('core.module.edit.Field Already Exist')
});
}
if (showValueTypeSelect && !data.valueType) {
return toast({
status: 'warning',
title: '数据类型不能为空'
});
}
onSubmit({
data,
changeKey: !keys.includes(data.key)
});
},
[defaultField.key, inputTypeList, keys, onSubmit, showValueTypeSelect, t, toast]
);
const onSubmitError = useCallback(
(e: Object) => {
for (const item of Object.values(e)) {
if (item.message) {
toast({
status: 'warning',
title: item.message
});
break;
}
}
},
[toast]
);
return (
<MyModal
isOpen={true}
iconSrc="/imgs/workflow/extract.png"
title={t('core.module.edit.Field Edit')}
maxW={['90vw', showInputTypeSelect ? '800px' : '400px']}
w={'100%'}
overflow={'unset'}
>
<ModalBody overflow={'visible'}>
<Flex gap={8} flexDirection={['column', 'row']}>
<Stack flex={1} gap={5}>
{showInputTypeSelect && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Input Type')}</FormLabel>
<Box flex={1}>
<MySelect
list={inputTypeList}
value={inputType}
onchange={(e: string) => {
const type = e as FlowNodeInputTypeEnum;
setValue('inputType', type);
}}
/>
</Box>
</Flex>
)}
{showValueTypeSelect && !showInputTypeSelect && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Data Type')}</FormLabel>
<Box flex={1}>
<MySelect
w={'full'}
list={dataTypeSelectList}
value={valueType}
onchange={(e: string) => {
const type = e as WorkflowIOValueTypeEnum;
setValue('valueType', type);
}}
/>
</Box>
</Flex>
)}
{showKeyInput && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Field Name')}</FormLabel>
<Input
bg={'myGray.50'}
placeholder="appointment/sql"
{...register('key', {
required: true
})}
/>
</Flex>
)}
{showDescriptionInput && (
<Box alignItems={'flex-start'}>
<FormLabel flex={'0 0 70px'} mb={'1px'}>
{t('core.module.Field Description')}
</FormLabel>
<Textarea
bg={'myGray.50'}
placeholder={
isToolInput ? t('core.module.Plugin tool Description') : t('common.choosable')
}
rows={5}
{...register('description', { required: isToolInput ? true : false })}
/>
</Box>
)}
</Stack>
{/* input type config */}
{showInputTypeSelect && (
<Stack flex={1} gap={5}>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{workflowT('Field required')}</FormLabel>
<Switch {...register('required')} />
</Flex>
{showToolInput && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}></FormLabel>
<Switch {...register('isToolInput')} />
</Flex>
)}
{showValueTypeSelect && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Data Type')}</FormLabel>
<Box flex={1}>
<MySelect
w={'full'}
list={dataTypeSelectList}
value={valueType}
onchange={(e: string) => {
const type = e as WorkflowIOValueTypeEnum;
setValue('valueType', type);
}}
/>
</Box>
</Flex>
)}
{showDefaultValue && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Default Value')}</FormLabel>
{inputType === FlowNodeInputTypeEnum.numberInput && (
<Input
bg={'myGray.50'}
max={max}
min={min}
type={'number'}
{...register('defaultValue')}
/>
)}
{inputType === FlowNodeInputTypeEnum.input && (
<Input bg={'myGray.50'} maxLength={maxLength} {...register('defaultValue')} />
)}
{inputType === FlowNodeInputTypeEnum.textarea && (
<Textarea
bg={'myGray.50'}
maxLength={maxLength}
{...register('defaultValue')}
/>
)}
{inputType === FlowNodeInputTypeEnum.JSONEditor && (
<JsonEditor
resize
w={'full'}
onChange={(e) => {
setValue('defaultValue', e);
}}
defaultValue={String(getValues('defaultValue'))}
/>
)}
{inputType === FlowNodeInputTypeEnum.switch && (
<Switch {...register('defaultValue')} />
)}
</Flex>
)}
{showMaxLenInput && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Max Length')}</FormLabel>
<MyNumberInput
flex={'1 0 0'}
bg={'myGray.50'}
placeholder={t('core.module.Max Length placeholder')}
value={maxLength}
onChange={(e) => {
// @ts-ignore
setValue('maxLength', e);
}}
// {...register('maxLength')}
/>
</Flex>
)}
{showMinMaxInput && (
<>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Max Value')}</FormLabel>
<MyNumberInput
flex={'1 0 0'}
bg={'myGray.50'}
value={watch('max')}
onChange={(e) => {
// @ts-ignore
setValue('max', e);
}}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Min Value')}</FormLabel>
<MyNumberInput
flex={'1 0 0'}
bg={'myGray.50'}
value={watch('min')}
onChange={(e) => {
// @ts-ignore
setValue('min', e);
}}
/>
</Flex>
</>
)}
{showDynamicInput && (
<Stack gap={5}>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Input Type')}</FormLabel>
<Box flex={1} fontWeight={'bold'}>
{t('core.workflow.inputType.Reference')}
</Box>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.module.Data Type')}</FormLabel>
<Box flex={1}>
<MySelect
list={dataTypeSelectList}
value={defaultInputValueType}
onchange={(e) => {
setValue(
'dynamicParamDefaultValue.valueType',
e as WorkflowIOValueTypeEnum
);
}}
/>
</Box>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{t('core.workflow.inputType.Required')}</FormLabel>
<Box flex={1}>
<Switch {...register('dynamicParamDefaultValue.required')} />
</Box>
</Flex>
</Stack>
)}
{!showToolInput &&
!showValueTypeSelect &&
!showDefaultValue &&
!showMaxLenInput &&
!showMinMaxInput &&
!showDynamicInput && <EmptyTip text={t('core.module.No Config Tips')} />}
</Stack>
)}
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmitSuccess, onSubmitError)}>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(FieldEditModal);

View File

@@ -1,186 +0,0 @@
import React, { useMemo } from 'react';
import { Position } from 'reactflow';
import { SourceHandle, TargetHandle } from '.';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
export const ConnectionSourceHandle = ({ nodeId }: { nodeId: string }) => {
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const node = useMemo(() => nodeList.find((node) => node.nodeId === nodeId), [nodeList, nodeId]);
/* not node/not connecting node, hidden */
const showSourceHandle = useMemo(() => {
if (!node) return false;
if (connectingEdge && connectingEdge.nodeId !== nodeId) return false;
return true;
}, [connectingEdge, node, nodeId]);
const RightHandle = useMemo(() => {
const handleId = getHandleId(nodeId, 'source', Position.Right);
const rightTargetConnected = edges.some(
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Right)
);
if (!node || !node?.sourceHandle?.right || rightTargetConnected) return null;
return (
<SourceHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Right}
translate={[2, 0]}
/>
);
}, [edges, node, nodeId]);
const LeftHandlee = useMemo(() => {
const leftTargetConnected = edges.some(
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Left)
);
if (!node || !node?.sourceHandle?.left || leftTargetConnected) return null;
const handleId = getHandleId(nodeId, 'source', Position.Left);
return (
<SourceHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Left}
translate={[-6, 0]}
/>
);
}, [edges, node, nodeId]);
const TopHandlee = useMemo(() => {
if (
edges.some(
(edge) => edge.target === nodeId && edge.targetHandle === NodeOutputKeyEnum.selectedTools
)
)
return null;
const handleId = getHandleId(nodeId, 'source', Position.Top);
const topTargetConnected = edges.some(
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Top)
);
if (!node || !node?.sourceHandle?.top || topTargetConnected) return null;
return (
<SourceHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Top}
translate={[0, -2]}
/>
);
}, [edges, node, nodeId]);
const BottomHandlee = useMemo(() => {
const handleId = getHandleId(nodeId, 'source', Position.Bottom);
const targetConnected = edges.some(
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Bottom)
);
if (!node || !node?.sourceHandle?.bottom || targetConnected) return null;
return (
<SourceHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Bottom}
translate={[0, 2]}
/>
);
}, [edges, node, nodeId]);
return showSourceHandle ? (
<>
{RightHandle}
{LeftHandlee}
{TopHandlee}
{BottomHandlee}
</>
) : null;
};
export const ConnectionTargetHandle = ({ nodeId }: { nodeId: string }) => {
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const node = useMemo(() => nodeList.find((node) => node.nodeId === nodeId), [nodeList, nodeId]);
const showHandle = useMemo(() => {
if (!node) return false;
if (connectingEdge && connectingEdge.nodeId === nodeId) return false;
return true;
}, [connectingEdge, node, nodeId]);
const LeftHandle = useMemo(() => {
if (!node || !node?.targetHandle?.left) return null;
const handleId = getHandleId(nodeId, 'target', Position.Left);
return (
<TargetHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Left}
translate={[-2, 0]}
/>
);
}, [node, nodeId]);
const rightHandle = useMemo(() => {
if (!node || !node?.targetHandle?.right) return null;
const handleId = getHandleId(nodeId, 'target', Position.Right);
return (
<TargetHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Right}
translate={[2, 0]}
/>
);
}, [node, nodeId]);
const topHandle = useMemo(() => {
if (!node || !node?.targetHandle?.top) return null;
const handleId = getHandleId(nodeId, 'target', Position.Top);
return (
<TargetHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Top}
translate={[0, -2]}
/>
);
}, [node, nodeId]);
const bottomHandle = useMemo(() => {
if (!node || !node?.targetHandle?.bottom) return null;
const handleId = getHandleId(nodeId, 'target', Position.Bottom);
return (
<TargetHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Bottom}
translate={[0, 2]}
/>
);
}, [node, nodeId]);
return showHandle ? (
<>
{LeftHandle}
{rightHandle}
{topHandle}
{bottomHandle}
</>
) : null;
};
export default <></>;

View File

@@ -1,109 +0,0 @@
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { Box, BoxProps } from '@chakra-ui/react';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { Connection, Handle, Position } from 'reactflow';
import { useCallback, useMemo } from 'react';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
const handleSize = '14px';
type ToolHandleProps = BoxProps & {
nodeId: string;
};
export const ToolTargetHandle = ({ nodeId }: ToolHandleProps) => {
const { t } = useTranslation();
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const handleId = NodeOutputKeyEnum.selectedTools;
const connected = edges.some((edge) => edge.target === nodeId && edge.targetHandle === handleId);
// if top handle is connected, return null
const hidden =
!connected &&
(connectingEdge?.handleId !== NodeOutputKeyEnum.selectedTools ||
edges.some((edge) => edge.targetHandle === getHandleId(nodeId, 'target', 'top')));
const Render = useMemo(() => {
return hidden ? null : (
<MyTooltip label={t('core.workflow.tool.Handle')} shouldWrapChildren={false}>
<Handle
style={{
borderRadius: '0',
backgroundColor: 'transparent',
border: 'none',
width: handleSize,
height: handleSize
}}
type="target"
id={handleId}
position={Position.Top}
>
<Box
className="flow-handle"
w={handleSize}
h={handleSize}
border={'4px solid #8774EE'}
transform={'translate(0,-30%) rotate(45deg)'}
pointerEvents={'none'}
visibility={'visible'}
/>
</Handle>
</MyTooltip>
);
}, [handleId, hidden, t]);
return Render;
};
export const ToolSourceHandle = ({ nodeId }: ToolHandleProps) => {
const { t } = useTranslation();
const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges);
/* onConnect edge, delete tool input and switch */
const onConnect = useCallback(
(e: Connection) => {
setEdges((edges) =>
edges.filter((edge) => {
if (edge.target !== e.target) return true;
if (edge.targetHandle === NodeOutputKeyEnum.selectedTools) return true;
return false;
})
);
},
[setEdges]
);
const Render = useMemo(() => {
return (
<MyTooltip label={t('core.workflow.tool.Handle')} shouldWrapChildren={false}>
<Handle
style={{
borderRadius: '0',
backgroundColor: 'transparent',
border: 'none',
width: handleSize,
height: handleSize
}}
type="source"
id={NodeOutputKeyEnum.selectedTools}
position={Position.Bottom}
onConnect={onConnect}
>
<Box
w={handleSize}
h={handleSize}
border={'4px solid #8774EE'}
transform={'translate(0,30%) rotate(45deg)'}
pointerEvents={'none'}
/>
</Handle>
</MyTooltip>
);
}, [onConnect, t]);
return Render;
};

View File

@@ -1,250 +0,0 @@
import React, { useMemo } from 'react';
import { Handle, Position } from 'reactflow';
import { SmallAddIcon } from '@chakra-ui/icons';
import { handleHighLightStyle, sourceCommonStyle, handleConnectedStyle, handleSize } from './style';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
type Props = {
nodeId: string;
handleId: string;
position: Position;
translate?: [number, number];
};
const MySourceHandle = React.memo(function MySourceHandle({
nodeId,
translate,
handleId,
position,
highlightStyle,
connectedStyle
}: Props & {
highlightStyle: Record<string, any>;
connectedStyle: Record<string, any>;
}) {
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes);
const hoverNodeId = useContextSelector(WorkflowContext, (v) => v.hoverNodeId);
const node = useMemo(() => nodes.find((node) => node.data.nodeId === nodeId), [nodes, nodeId]);
const connected = edges.some((edge) => edge.sourceHandle === handleId);
const nodeIsHover = hoverNodeId === nodeId;
const active = useMemo(
() => nodeIsHover || node?.selected || connectingEdge?.handleId === handleId,
[nodeIsHover, node?.selected, connectingEdge, handleId]
);
const translateStr = useMemo(() => {
if (!translate) return '';
if (position === Position.Right) {
return `${active ? translate[0] + 2 : translate[0]}px, -50%`;
}
if (position === Position.Left) {
return `${active ? translate[0] + 2 : translate[0]}px, -50%`;
}
if (position === Position.Top) {
return `-50%, ${active ? translate[1] - 2 : translate[1]}px`;
}
if (position === Position.Bottom) {
return `-50%, ${active ? translate[1] + 2 : translate[1]}px`;
}
}, [active, position, translate]);
const transform = useMemo(
() => (translateStr ? `translate(${translateStr})` : ''),
[translateStr]
);
const { styles, showAddIcon } = useMemo(() => {
if (active) {
return {
styles: {
...highlightStyle,
...(translateStr && {
transform
})
},
showAddIcon: true
};
}
if (connected) {
return {
styles: {
...connectedStyle,
...(translateStr && {
transform
})
},
showAddIcon: false
};
}
return {
styles: undefined,
showAddIcon: false
};
}, [active, connected, highlightStyle, translateStr, transform, connectedStyle]);
const RenderHandle = useMemo(() => {
return (
<Handle
style={
!!styles
? styles
: {
visibility: 'hidden',
transform,
...handleSize
}
}
type="source"
id={handleId}
position={position}
isConnectableEnd={false}
>
{showAddIcon && (
<SmallAddIcon pointerEvents={'none'} color={'primary.600'} fontWeight={'bold'} />
)}
</Handle>
);
}, [handleId, position, showAddIcon, styles, transform]);
if (!node) return null;
if (connectingEdge?.handleId === NodeOutputKeyEnum.selectedTools) return null;
return <>{RenderHandle}</>;
});
export const SourceHandle = (props: Props) => {
return (
<MySourceHandle
{...props}
highlightStyle={{ ...sourceCommonStyle, ...handleHighLightStyle }}
connectedStyle={{ ...sourceCommonStyle, ...handleConnectedStyle }}
/>
);
};
const MyTargetHandle = React.memo(function MyTargetHandle({
nodeId,
handleId,
position,
translate,
highlightStyle,
connectedStyle
}: Props & {
highlightStyle: Record<string, any>;
connectedStyle: Record<string, any>;
}) {
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const node = useMemo(() => nodeList.find((node) => node.nodeId === nodeId), [nodeList, nodeId]);
const connected = edges.some((edge) => edge.targetHandle === handleId);
const connectedEdges = edges.filter((edge) => edge.target === nodeId);
const translateStr = useMemo(() => {
if (!translate) return '';
if (position === Position.Right) {
return `${connectingEdge ? translate[0] + 2 : translate[0]}px, -50%`;
}
if (position === Position.Left) {
return `${connectingEdge ? translate[0] - 2 : translate[0]}px, -50%`;
}
if (position === Position.Top) {
return `-50%, ${connectingEdge ? translate[1] - 2 : translate[1]}px`;
}
if (position === Position.Bottom) {
return `-50%, ${connectingEdge ? translate[1] + 2 : translate[1]}px`;
}
}, [connectingEdge, position, translate]);
const transform = useMemo(
() => (translateStr ? `translate(${translateStr})` : ''),
[translateStr]
);
const styles = useMemo(() => {
if (!connectingEdge && !connected) return;
if (connectingEdge) {
return {
...highlightStyle,
transform
};
}
if (connected) {
return {
...connectedStyle,
transform
};
}
return;
}, [connected, connectingEdge, connectedStyle, highlightStyle, transform]);
const showHandle = useMemo(() => {
if (!node) return false;
// check tool connected
if (
edges.some(
(edge) => edge.target === nodeId && edge.targetHandle === NodeOutputKeyEnum.selectedTools
)
) {
return false;
}
if (connectingEdge?.handleId && !connectingEdge.handleId?.includes('source')) return false;
// From same source node and same handle
if (
connectedEdges.some(
(item) => item.sourceHandle === connectingEdge?.handleId && item.target === nodeId
)
)
return false;
return true;
}, [connectedEdges, connectingEdge?.handleId, edges, node, nodeId]);
const RenderHandle = useMemo(() => {
return (
<Handle
style={
!!styles && showHandle
? styles
: {
visibility: 'hidden',
transform,
...handleSize
}
}
type="target"
id={handleId}
position={position}
isConnectableStart={false}
></Handle>
);
}, [styles, showHandle, transform, handleId, position]);
return RenderHandle;
});
export const TargetHandle = (props: Props) => {
return (
<MyTargetHandle
{...props}
highlightStyle={{ ...sourceCommonStyle, ...handleHighLightStyle }}
connectedStyle={{ ...sourceCommonStyle, ...handleConnectedStyle }}
/>
);
};
export default <></>;

View File

@@ -1,25 +0,0 @@
export const primaryColor = '#3370FF';
export const lowPrimaryColor = '#94B5FF';
export const handleSize = {
width: '18px',
height: '18px'
};
export const sourceCommonStyle = {
backgroundColor: 'white',
borderWidth: '3px',
borderRadius: '50%'
};
export const handleConnectedStyle = {
borderColor: lowPrimaryColor,
width: '14px',
height: '14px'
};
export const handleHighLightStyle = {
borderColor: primaryColor,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '18px',
height: '18px'
};

View File

@@ -1,645 +0,0 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Box, Button, Card, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { useTranslation } from 'next-i18next';
import { useEditTitle } from '@/web/common/hooks/useEditTitle';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
import { ToolTargetHandle } from './Handle/ToolHandle';
import { useEditTextarea } from '@fastgpt/web/hooks/useEditTextarea';
import { ConnectionSourceHandle, ConnectionTargetHandle } from './Handle/ConnectionHandle';
import { useDebug } from '../../hooks/useDebug';
import { ResponseBox } from '@/components/ChatBox/components/WholeResponseModal';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { getPreviewPluginModule } from '@/web/core/plugin/api';
import { storeNode2FlowNode, updateFlowNodeVersion } from '@/web/core/workflow/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import { useI18n } from '@/web/context/I18n';
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useSystemStore } from '@/web/common/system/useSystemStore';
type Props = FlowNodeItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
minW?: string | number;
maxW?: string | number;
selected?: boolean;
menuForbid?: {
debug?: boolean;
rename?: boolean;
copy?: boolean;
delete?: boolean;
};
};
const NodeCard = (props: Props) => {
const { t } = useTranslation();
const { appT } = useI18n();
const { toast } = useToast();
const {
children,
avatar = LOGO_ICON,
name = t('core.module.template.UnKnow Module'),
intro,
minW = '300px',
maxW = '600px',
nodeId,
flowNodeType,
selected,
menuForbid,
isTool = false,
isError = false,
debugResult,
pluginId
} = props;
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const setHoverNodeId = useContextSelector(WorkflowContext, (v) => v.setHoverNodeId);
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode);
const [hasNewVersion, setHasNewVersion] = useState(false);
const { setLoading } = useSystemStore();
// custom title edit
const { onOpenModal: onOpenCustomTitleModal, EditModal: EditTitleModal } = useEditTitle({
title: t('common.Custom Title'),
placeholder: appT('module.Custom Title Tip') || ''
});
const showToolHandle = useMemo(
() => isTool && !!nodeList.find((item) => item?.flowNodeType === FlowNodeTypeEnum.tools),
[isTool, nodeList]
);
const node = nodeList.find((node) => node.nodeId === nodeId);
const { openConfirm: onOpenConfirmSync, ConfirmModal: ConfirmSyncModal } = useConfirm({
content: appT('module.Confirm Sync')
});
useEffect(() => {
const fetchPluginModule = async () => {
if (node?.flowNodeType === FlowNodeTypeEnum.pluginModule) {
if (!node?.pluginId) return;
const template = await getPreviewPluginModule(node.pluginId);
setHasNewVersion(!!template.nodeVersion && node.nodeVersion !== template.nodeVersion);
} else {
const template = moduleTemplatesFlat.find(
(item) => item.flowNodeType === node?.flowNodeType
);
setHasNewVersion(node?.version !== template?.version);
}
};
fetchPluginModule();
}, [node]);
const template = moduleTemplatesFlat.find((item) => item.flowNodeType === node?.flowNodeType);
const onClickSyncVersion = useCallback(async () => {
try {
setLoading(true);
if (!node || !template) return;
if (node?.flowNodeType === 'pluginModule') {
if (!node.pluginId) return;
onResetNode({
id: nodeId,
node: await getPreviewPluginModule(node.pluginId)
});
} else {
onResetNode({
id: nodeId,
node: updateFlowNodeVersion(node, template)
});
}
} catch (error) {
console.error('Error fetching plugin module:', error);
}
setLoading(false);
}, [node, nodeId, onResetNode, setLoading, template]);
/* Node header */
const Header = useMemo(() => {
return (
<Box position={'relative'}>
{/* debug */}
<Box px={4} py={3}>
{/* tool target handle */}
{showToolHandle && <ToolTargetHandle nodeId={nodeId} />}
{/* avatar and name */}
<Flex alignItems={'center'}>
<Avatar src={avatar} borderRadius={'0'} objectFit={'contain'} w={'30px'} h={'30px'} />
<Box ml={3} fontSize={'md'} fontWeight={'medium'}>
{t(name)}
</Box>
{!menuForbid?.rename && (
<MyIcon
className="controller-rename"
display={'none'}
name={'edit'}
w={'14px'}
cursor={'pointer'}
ml={1}
color={'myGray.500'}
_hover={{ color: 'primary.600' }}
onClick={() => {
onOpenCustomTitleModal({
defaultVal: name,
onSuccess: (e) => {
if (!e) {
return toast({
title: appT('modules.Title is required'),
status: 'warning'
});
}
onChangeNode({
nodeId,
type: 'attr',
key: 'name',
value: e
});
}
});
}}
/>
)}
<Box flex={1} />
{hasNewVersion && (
<MyTooltip label={appT('app.modules.click to update')}>
<Button
bg={'yellow.50'}
color={'yellow.600'}
variant={'ghost'}
h={8}
px={2}
rounded={'6px'}
fontSize={'xs'}
fontWeight={'medium'}
cursor={'pointer'}
_hover={{ bg: 'yellow.100' }}
onClick={onOpenConfirmSync(onClickSyncVersion)}
>
<Box>{appT('app.modules.has new version')}</Box>
<QuestionOutlineIcon ml={1} />
</Button>
</MyTooltip>
)}
</Flex>
<MenuRender
nodeId={nodeId}
pluginId={pluginId}
flowNodeType={flowNodeType}
menuForbid={menuForbid}
/>
<NodeIntro nodeId={nodeId} intro={intro} />
</Box>
<ConfirmSyncModal />
</Box>
);
}, [
showToolHandle,
nodeId,
avatar,
t,
name,
menuForbid,
hasNewVersion,
appT,
onOpenConfirmSync,
onClickSyncVersion,
pluginId,
flowNodeType,
intro,
ConfirmSyncModal,
onOpenCustomTitleModal,
onChangeNode,
toast
]);
return (
<Box
minW={minW}
maxW={maxW}
bg={'white'}
borderWidth={'1px'}
borderRadius={'md'}
boxShadow={'1'}
_hover={{
boxShadow: '4',
'& .controller-menu': {
display: 'flex'
},
'& .controller-debug': {
display: 'block'
},
'& .controller-rename': {
display: 'block'
}
}}
onMouseEnter={() => setHoverNodeId(nodeId)}
onMouseLeave={() => setHoverNodeId(undefined)}
{...(isError
? {
borderColor: 'red.500',
onMouseDownCapture: () => onUpdateNodeError(nodeId, false)
}
: {
borderColor: selected ? 'primary.600' : 'borderColor.base'
})}
>
<NodeDebugResponse nodeId={nodeId} debugResult={debugResult} />
{Header}
{children}
<ConnectionSourceHandle nodeId={nodeId} />
<ConnectionTargetHandle nodeId={nodeId} />
<EditTitleModal maxLength={20} />
</Box>
);
};
export default React.memo(NodeCard);
const MenuRender = React.memo(function MenuRender({
nodeId,
pluginId,
flowNodeType,
menuForbid
}: {
nodeId: string;
pluginId?: string;
flowNodeType: Props['flowNodeType'];
menuForbid?: Props['menuForbid'];
}) {
const { t } = useTranslation();
const { openDebugNode, DebugInputModal } = useDebug();
const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({
content: t('core.module.Confirm Delete Node'),
type: 'delete'
});
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges);
const onCopyNode = useCallback(
(nodeId: string) => {
setNodes((state) => {
const node = state.find((node) => node.id === nodeId);
if (!node) return state;
const template = {
avatar: node.data.avatar,
name: node.data.name,
intro: node.data.intro,
flowNodeType: node.data.flowNodeType,
inputs: node.data.inputs,
outputs: node.data.outputs,
showStatus: node.data.showStatus,
pluginId: node.data.pluginId,
version: node.data.version
};
return state.concat(
storeNode2FlowNode({
item: {
flowNodeType: template.flowNodeType,
avatar: template.avatar,
name: template.name,
intro: template.intro,
nodeId: getNanoid(),
position: { x: node.position.x + 200, y: node.position.y + 50 },
showStatus: template.showStatus,
pluginId: template.pluginId,
inputs: template.inputs,
outputs: template.outputs,
version: template.version
}
})
);
});
},
[setNodes]
);
const onDelNode = useCallback(
(nodeId: string) => {
setNodes((state) => state.filter((item) => item.data.nodeId !== nodeId));
setEdges((state) => state.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
},
[setEdges, setNodes]
);
const Render = useMemo(() => {
const menuList = [
...(menuForbid?.debug
? []
: [
{
icon: 'core/workflow/debug',
label: t('core.workflow.Debug'),
variant: 'whiteBase',
onClick: () => openDebugNode({ entryNodeId: nodeId })
}
]),
...(menuForbid?.copy
? []
: [
{
icon: 'copy',
label: t('common.Copy'),
variant: 'whiteBase',
onClick: () => onCopyNode(nodeId)
}
]),
...(menuForbid?.delete
? []
: [
{
icon: 'delete',
label: t('common.Delete'),
variant: 'whiteDanger',
onClick: onOpenConfirmDeleteNode(() => onDelNode(nodeId))
}
])
];
return (
<>
<Box
className="nodrag controller-menu"
display={'none'}
flexDirection={'column'}
gap={3}
position={'absolute'}
top={'-20px'}
right={0}
transform={'translateX(90%)'}
pl={'20px'}
pr={'10px'}
pb={'20px'}
pt={'20px'}
>
{menuList.map((item) => (
<Box key={item.icon}>
<Button
size={'xs'}
variant={item.variant}
leftIcon={<MyIcon name={item.icon as any} w={'13px'} />}
onClick={item.onClick}
>
{item.label}
</Button>
</Box>
))}
</Box>
<ConfirmDeleteModal />
<DebugInputModal />
</>
);
}, [
menuForbid?.debug,
menuForbid?.copy,
menuForbid?.delete,
t,
onOpenConfirmDeleteNode,
ConfirmDeleteModal,
DebugInputModal,
openDebugNode,
nodeId,
onCopyNode,
onDelNode
]);
return Render;
});
const NodeIntro = React.memo(function NodeIntro({
nodeId,
intro = ''
}: {
nodeId: string;
intro?: string;
}) {
const { t } = useTranslation();
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const NodeIsTool = useMemo(() => {
const { isTool } = splitToolInputs([], nodeId);
return isTool;
}, [nodeId, splitToolInputs]);
// edit intro
const { onOpenModal: onOpenIntroModal, EditModal: EditIntroModal } = useEditTextarea({
title: t('core.module.Edit intro'),
tip: '调整该模块会对工具调用时机有影响。\n你可以通过精确的描述该模块功能引导模型进行工具调用。',
canEmpty: false
});
const Render = useMemo(() => {
return (
<>
<Flex alignItems={'flex-end'} py={1}>
<Box fontSize={'xs'} color={'myGray.600'} flex={'1 0 0'}>
{t(intro)}
</Box>
{NodeIsTool && (
<Button
size={'xs'}
variant={'whiteBase'}
onClick={() => {
onOpenIntroModal({
defaultVal: intro,
onSuccess(e) {
onChangeNode({
nodeId,
type: 'attr',
key: 'intro',
value: e
});
}
});
}}
>
{t('core.module.Edit intro')}
</Button>
)}
</Flex>
<EditIntroModal maxLength={500} />
</>
);
}, [EditIntroModal, intro, NodeIsTool, nodeId, onChangeNode, onOpenIntroModal, t]);
return Render;
});
const NodeDebugResponse = React.memo(function NodeDebugResponse({
nodeId,
debugResult
}: {
nodeId: string;
debugResult: FlowNodeItemType['debugResult'];
}) {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const onStopNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStopNodeDebug);
const onNextNodeDebug = useContextSelector(WorkflowContext, (v) => v.onNextNodeDebug);
const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData);
const { openConfirm, ConfirmModal } = useConfirm({
content: t('core.workflow.Confirm stop debug')
});
const RenderStatus = useMemo(() => {
const map = {
running: {
bg: 'primary.50',
text: t('core.workflow.Running'),
icon: 'core/workflow/running'
},
success: {
bg: 'green.50',
text: t('core.workflow.Success'),
icon: 'core/workflow/runSuccess'
},
failed: {
bg: 'red.50',
text: t('core.workflow.Failed'),
icon: 'core/workflow/runError'
},
skipped: {
bg: 'myGray.50',
text: t('core.workflow.Skipped'),
icon: 'core/workflow/runSkip'
}
};
const statusData = map[debugResult?.status || 'running'];
const response = debugResult?.response;
const onStop = () => {
openConfirm(onStopNodeDebug)();
};
return !!debugResult && !!statusData ? (
<>
<Flex px={4} bg={statusData.bg} borderTopRadius={'md'} py={3}>
<MyIcon name={statusData.icon as any} w={'16px'} mr={2} />
<Box color={'myGray.900'} fontWeight={'bold'} flex={'1 0 0'}>
{statusData.text}
</Box>
{debugResult.status !== 'running' && (
<Box
color={'primary.700'}
cursor={'pointer'}
fontSize={'sm'}
onClick={() =>
onChangeNode({
nodeId,
type: 'attr',
key: 'debugResult',
value: {
...debugResult,
showResult: !debugResult.showResult
}
})
}
>
{debugResult.showResult
? t('core.workflow.debug.Hide result')
: t('core.workflow.debug.Show result')}
</Box>
)}
</Flex>
{/* result */}
{debugResult.showResult && (
<Card
className="nowheel"
position={'absolute'}
right={'-430px'}
top={0}
zIndex={10}
w={'420px'}
maxH={'100%'}
minH={'300px'}
overflowY={'auto'}
border={'base'}
>
{/* Status header */}
<Flex px={4} mb={1} py={3} alignItems={'center'} borderBottom={'base'}>
<MyIcon mr={1} name={'core/workflow/debugResult'} w={'20px'} color={'primary.600'} />
<Box fontWeight={'bold'} flex={'1'}>
{t('core.workflow.debug.Run result')}
</Box>
{workflowDebugData?.nextRunNodes.length !== 0 && (
<Button
size={'sm'}
leftIcon={<MyIcon name={'core/chat/stopSpeech'} w={'16px'} />}
variant={'whiteDanger'}
onClick={onStop}
>
{t('core.workflow.Stop debug')}
</Button>
)}
{(debugResult.status === 'success' || debugResult.status === 'skipped') &&
!debugResult.isExpired &&
workflowDebugData?.nextRunNodes &&
workflowDebugData.nextRunNodes.length > 0 && (
<Button
ml={2}
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debugNext'} w={'16px'} />}
variant={'primary'}
onClick={() => onNextNodeDebug()}
>
{t('common.Next Step')}
</Button>
)}
{workflowDebugData?.nextRunNodes && workflowDebugData?.nextRunNodes.length === 0 && (
<Button ml={2} size={'sm'} variant={'primary'} onClick={onStopNodeDebug}>
{t('core.workflow.debug.Done')}
</Button>
)}
</Flex>
{/* Show result */}
<Box maxH={'100%'} overflow={'auto'}>
{!debugResult.message && !response && (
<EmptyTip text={t('core.workflow.debug.Not result')} pt={2} pb={5} />
)}
{debugResult.message && (
<Box color={'red.600'} px={3} py={4}>
{debugResult.message}
</Box>
)}
{response && <ResponseBox response={[response]} showDetail hideTabs />}
</Box>
</Card>
)}
<ConfirmModal />
</>
) : null;
}, [
ConfirmModal,
debugResult,
nodeId,
onChangeNode,
onNextNodeDebug,
onStopNodeDebug,
openConfirm,
t,
workflowDebugData?.nextRunNodes
]);
return <>{RenderStatus}</>;
});

View File

@@ -1,205 +0,0 @@
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { Box, Flex } from '@chakra-ui/react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import NodeInputSelect from '@fastgpt/web/components/core/workflow/NodeInputSelect';
import MyIcon from '@fastgpt/web/components/common/Icon';
import dynamic from 'next/dynamic';
import { EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import ValueTypeLabel from '../ValueTypeLabel';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
const FieldEditModal = dynamic(() => import('../FieldEditModal'));
type Props = {
nodeId: string;
input: FlowNodeInputItemType;
};
const InputLabel = ({ nodeId, input }: Props) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const {
description,
toolDescription,
required,
label,
selectedTypeIndex,
renderTypeList,
valueType,
canEdit,
key
} = input;
const [editField, setEditField] = useState<EditNodeFieldType>();
const onChangeRenderType = useCallback(
(e: string) => {
const index = renderTypeList.findIndex((item) => item === e) || 0;
onChangeNode({
nodeId,
type: 'updateInput',
key: input.key,
value: {
...input,
selectedTypeIndex: index,
value: undefined
}
});
},
[input, nodeId, onChangeNode, renderTypeList]
);
const RenderLabel = useMemo(() => {
const renderType = renderTypeList?.[selectedTypeIndex || 0];
return (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Flex
alignItems={'center'}
position={'relative'}
fontWeight={'medium'}
color={'myGray.600'}
>
{required && (
<Box position={'absolute'} left={-2} top={-1} color={'red.600'}>
*
</Box>
)}
{t(label)}
{description && <QuestionTip ml={1} label={t(description)}></QuestionTip>}
</Flex>
{/* value type */}
{renderType === FlowNodeInputTypeEnum.reference && <ValueTypeLabel valueType={valueType} />}
{/* edit config */}
{canEdit && (
<>
{input.editField && Object.keys(input.editField).length > 0 && (
<MyIcon
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
ml={3}
color={'myGray.600'}
_hover={{ color: 'primary.500' }}
onClick={() =>
setEditField({
...input,
inputType: renderTypeList[0],
valueType: valueType,
key,
label,
description,
isToolInput: !!toolDescription
})
}
/>
)}
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.600'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onChangeNode({
nodeId,
type: 'delInput',
key: key
});
onChangeNode({
nodeId,
type: 'delOutput',
key: key
});
}}
/>
</>
)}
{/* input type select */}
{renderTypeList && renderTypeList.length > 1 && (
<Box ml={2}>
<NodeInputSelect
renderTypeList={renderTypeList}
renderTypeIndex={selectedTypeIndex}
onChange={onChangeRenderType}
/>
</Box>
)}
{!!editField?.key && (
<FieldEditModal
editField={input.editField}
keys={[editField.key]}
defaultField={editField}
onClose={() => setEditField(undefined)}
onSubmit={({ data, changeKey }) => {
if (!data.inputType || !data.key || !data.label || !editField.key) return;
const newInput: FlowNodeInputItemType = {
...input,
renderTypeList: [data.inputType],
valueType: data.valueType,
key: data.key,
required: data.required,
label: data.label,
description: data.description,
toolDescription: data.isToolInput ? data.description : undefined,
maxLength: data.maxLength,
value: data.defaultValue,
max: data.max,
min: data.min
};
if (changeKey) {
onChangeNode({
nodeId,
type: 'replaceInput',
key: editField.key,
value: newInput
});
} else {
onChangeNode({
nodeId,
type: 'updateInput',
key: newInput.key,
value: newInput
});
}
setEditField(undefined);
}}
/>
)}
</Flex>
);
}, [
canEdit,
description,
editField,
input,
key,
label,
nodeId,
onChangeNode,
onChangeRenderType,
renderTypeList,
required,
selectedTypeIndex,
t,
toolDescription,
valueType
]);
return RenderLabel;
};
export default React.memo(InputLabel);

View File

@@ -1,119 +0,0 @@
import React, { useMemo } from 'react';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { Box } from '@chakra-ui/react';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import dynamic from 'next/dynamic';
import InputLabel from './Label';
import type { RenderInputProps } from './type';
const RenderList: {
types: FlowNodeInputTypeEnum[];
Component: React.ComponentType<RenderInputProps>;
}[] = [
{
types: [FlowNodeInputTypeEnum.reference],
Component: dynamic(() => import('./templates/Reference'))
},
{
types: [FlowNodeInputTypeEnum.input],
Component: dynamic(() => import('./templates/TextInput'))
},
{
types: [FlowNodeInputTypeEnum.numberInput],
Component: dynamic(() => import('./templates/NumberInput'))
},
{
types: [FlowNodeInputTypeEnum.switch],
Component: dynamic(() => import('./templates/Switch'))
},
{
types: [FlowNodeInputTypeEnum.textarea],
Component: dynamic(() => import('./templates/Textarea'))
},
{
types: [FlowNodeInputTypeEnum.selectApp],
Component: dynamic(() => import('./templates/SelectApp'))
},
{
types: [FlowNodeInputTypeEnum.selectLLMModel],
Component: dynamic(() => import('./templates/SelectLLMModel'))
},
{
types: [FlowNodeInputTypeEnum.settingLLMModel],
Component: dynamic(() => import('./templates/SettingLLMModel'))
},
{
types: [FlowNodeInputTypeEnum.selectDataset],
Component: dynamic(() => import('./templates/SelectDataset'))
},
{
types: [FlowNodeInputTypeEnum.selectDatasetParamsModal],
Component: dynamic(() => import('./templates/SelectDatasetParams'))
},
{
types: [FlowNodeInputTypeEnum.addInputParam],
Component: dynamic(() => import('./templates/AddInputParam'))
},
{
types: [FlowNodeInputTypeEnum.JSONEditor],
Component: dynamic(() => import('./templates/JsonEditor'))
},
{
types: [FlowNodeInputTypeEnum.settingDatasetQuotePrompt],
Component: dynamic(() => import('./templates/SettingQuotePrompt'))
}
];
const hideLabelTypeList = [FlowNodeInputTypeEnum.addInputParam];
type Props = {
flowInputList: FlowNodeInputItemType[];
nodeId: string;
CustomComponent?: Record<string, (e: FlowNodeInputItemType) => React.ReactNode>;
mb?: number;
};
const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props) => {
const copyInputs = useMemo(() => JSON.stringify(flowInputList), [flowInputList]);
const filterInputs = useMemo(() => {
const parseSortInputs = JSON.parse(copyInputs) as FlowNodeInputItemType[];
return parseSortInputs.filter((input) => {
return true;
});
}, [copyInputs]);
const memoCustomComponent = useMemo(() => CustomComponent || {}, [CustomComponent]);
const Render = useMemo(() => {
return filterInputs.map((input) => {
const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0];
const RenderComponent = (() => {
if (renderType === FlowNodeInputTypeEnum.custom && memoCustomComponent[input.key]) {
return <>{memoCustomComponent[input.key]({ ...input })}</>;
}
const Component = RenderList.find((item) => item.types.includes(renderType))?.Component;
if (!Component) return null;
return <Component inputs={filterInputs} item={input} nodeId={nodeId} />;
})();
return renderType !== FlowNodeInputTypeEnum.hidden ? (
<Box key={input.key} _notLast={{ mb }} position={'relative'}>
{!!input.label && !hideLabelTypeList.includes(renderType) && (
<InputLabel nodeId={nodeId} input={input} />
)}
{!!RenderComponent && (
<Box mt={2} className={'nodrag'}>
{RenderComponent}
</Box>
)}
</Box>
) : null;
});
}, [filterInputs, mb, memoCustomComponent, nodeId]);
return <>{Render}</>;
};
export default React.memo(RenderInput);

View File

@@ -1,112 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Button, Flex } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next';
import { EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
import dynamic from 'next/dynamic';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import Reference from './Reference';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
const FieldEditModal = dynamic(() => import('../../FieldEditModal'));
const AddInputParam = (props: RenderInputProps) => {
const { item, inputs, nodeId } = props;
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const mode = useContextSelector(WorkflowContext, (ctx) => ctx.mode);
const inputValue = useMemo(() => (item.value || []) as FlowNodeInputItemType[], [item.value]);
const [editField, setEditField] = useState<EditNodeFieldType>();
const inputIndex = useMemo(
() => inputs?.findIndex((input) => input.key === item.key),
[inputs, item.key]
);
const onAddField = useCallback(
({ data }: { data: EditNodeFieldType }) => {
if (!data.key) return;
const newInput: FlowNodeInputItemType = {
key: data.key,
valueType: data.valueType,
label: data.label || '',
renderTypeList: [FlowNodeInputTypeEnum.reference],
required: data.required,
description: data.description,
canEdit: true,
editField: item.editField
};
onChangeNode({
nodeId,
type: 'addInput',
index: inputIndex ? inputIndex + 1 : 1,
value: newInput
});
setEditField(undefined);
},
[inputIndex, item, nodeId, onChangeNode]
);
const Render = useMemo(() => {
return (
<>
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Flex
alignItems={'center'}
position={'relative'}
fontWeight={'medium'}
color={'myGray.600'}
>
{t('core.workflow.Custom variable')}
{item.description && <QuestionTip ml={1} label={t(item.description)} />}
</Flex>
<Box flex={'1 0 0'} />
<Button
variant={'whiteBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
onClick={() => setEditField(item.dynamicParamDefaultValue ?? {})}
>
{t('common.Add New')}
</Button>
</Flex>
{mode === 'plugin' && (
<Box mt={1}>
<Reference {...props} />
</Box>
)}
{!!editField && (
<FieldEditModal
editField={item.editField}
defaultField={editField}
keys={inputValue.map((input) => input.key)}
onClose={() => setEditField(undefined)}
onSubmit={onAddField}
/>
)}
</>
);
}, [
editField,
inputValue,
item.description,
item.dynamicParamDefaultValue,
item.editField,
mode,
onAddField,
props,
t
]);
return Render;
};
export default React.memo(AddInputParam);

View File

@@ -1,79 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import type { RenderInputProps } from '../type';
import JSONEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useCreation } from 'ahooks';
import { useTranslation } from 'next-i18next';
import { AppContext } from '@/web/core/app/context/appContext';
const JsonEditor = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { appDetail } = useContextSelector(AppContext, (v) => v);
// get variable
const variables = useCreation(() => {
const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.canEdit)
.map((item) => ({
key: item.key,
label: item.label
}))
);
return [...globalVariables, ...moduleVariables];
}, [inputs, nodeList]);
const update = useCallback(
(value: string) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value
}
});
},
[item, nodeId, onChangeNode]
);
const value = useMemo(() => {
if (typeof item.value === 'string') {
return item.value;
}
return JSON.stringify(item.value, null, 2);
}, [item.value]);
const Render = useMemo(() => {
return (
<JSONEditor
bg={'white'}
borderRadius={'sm'}
placeholder={item.placeholder}
resize
value={value}
onChange={(e) => {
update(e);
}}
variables={variables}
/>
);
}, [item.placeholder, update, value, variables]);
return Render;
};
export default React.memo(JsonEditor);

View File

@@ -1,46 +0,0 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import {
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper
} from '@chakra-ui/react';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
const NumberInputRender = ({ item, nodeId }: RenderInputProps) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const Render = useMemo(() => {
return (
<NumberInput
defaultValue={item.value}
min={item.min}
max={item.max}
onChange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: Number(e)
}
});
}}
>
<NumberInputField bg={'white'} px={3} borderRadius={'sm'} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
}, [item, nodeId, onChangeNode]);
return Render;
};
export default React.memo(NumberInputRender);

View File

@@ -1,206 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { Flex, Box, ButtonProps } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { computedNodeInputReference } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import {
NodeOutputKeyEnum,
VARIABLE_NODE_ID,
WorkflowIOValueTypeEnum
} from '@fastgpt/global/core/workflow/constants';
import type { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import dynamic from 'next/dynamic';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppContext } from '@/web/core/app/context/appContext';
const MultipleRowSelect = dynamic(
() => import('@fastgpt/web/components/common/MySelect/MultipleRowSelect')
);
const Avatar = dynamic(() => import('@/components/Avatar'));
type SelectProps = {
value?: ReferenceValueProps;
placeholder?: string;
list: {
label: string | React.ReactNode;
value: string;
children: {
label: string;
value: string;
}[];
}[];
onSelect: (val: ReferenceValueProps) => void;
styles?: ButtonProps;
};
const Reference = ({ item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onSelect = useCallback(
(e: ReferenceValueProps) => {
const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
);
if (e[0] === workflowStartNode?.id && e[1] !== NodeOutputKeyEnum.userChatInput) {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: [VARIABLE_NODE_ID, e[1]]
}
});
} else {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}
},
[item, nodeId, nodeList, onChangeNode]
);
const { referenceList, formatValue } = useReference({
nodeId,
valueType: item.valueType,
value: item.value
});
return (
<ReferSelector
placeholder={t(item.referencePlaceholder || '选择引用变量')}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
);
};
export default React.memo(Reference);
export const useReference = ({
nodeId,
valueType = WorkflowIOValueTypeEnum.any,
value
}: {
nodeId: string;
valueType?: WorkflowIOValueTypeEnum;
value?: any;
}) => {
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const referenceList = useMemo(() => {
const sourceNodes = computedNodeInputReference({
nodeId,
nodes: nodeList,
edges: edges,
chatConfig: appDetail.chatConfig,
t
});
if (!sourceNodes) return [];
// 转换为 select 的数据结构
const list: SelectProps['list'] = sourceNodes
.map((node) => {
return {
label: (
<Flex alignItems={'center'}>
<Avatar mr={1} src={node.avatar} w={'14px'} borderRadius={'ms'} />
<Box>{t(node.name)}</Box>
</Flex>
),
value: node.nodeId,
children: node.outputs
.filter(
(output) =>
valueType === WorkflowIOValueTypeEnum.any ||
output.valueType === WorkflowIOValueTypeEnum.any ||
output.valueType === valueType
)
.filter((output) => output.id !== NodeOutputKeyEnum.addOutputParam)
.map((output) => {
return {
label: t(output.label || ''),
value: output.id
};
})
};
})
.filter((item) => item.children.length > 0);
return list;
}, [appDetail.chatConfig, edges, nodeId, nodeList, t, valueType]);
const formatValue = useMemo(() => {
if (
Array.isArray(value) &&
value.length === 2 &&
typeof value[0] === 'string' &&
typeof value[1] === 'string'
) {
return value as ReferenceValueProps;
}
return undefined;
}, [value]);
return {
referenceList,
formatValue
};
};
export const ReferSelector = ({ placeholder, value, list = [], onSelect }: SelectProps) => {
const selectItemLabel = useMemo(() => {
if (!value) {
return;
}
const firstColumn = list.find((item) => item.value === value[0]);
if (!firstColumn) {
return;
}
const secondColumn = firstColumn.children.find((item) => item.value === value[1]);
if (!secondColumn) {
return;
}
return [firstColumn, secondColumn];
}, [list, value]);
const Render = useMemo(() => {
return (
<MultipleRowSelect
label={
selectItemLabel ? (
<Flex alignItems={'center'}>
{selectItemLabel[0].label}
<MyIcon name={'common/rightArrowLight'} mx={1} w={'14px'}></MyIcon>
{selectItemLabel[1].label}
</Flex>
) : (
<Box>{placeholder}</Box>
)
}
value={value as any[]}
list={list}
onSelect={(e) => {
onSelect(e as ReferenceValueProps);
}}
/>
);
}, [list, onSelect, placeholder, selectItemLabel, value]);
return Render;
};

View File

@@ -1,34 +0,0 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { WorkflowContext } from '@/components/core/workflow/context';
import { useContextSelector } from 'use-context-selector';
const SelectRender = ({ item, nodeId }: RenderInputProps) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const Render = useMemo(() => {
return (
<MySelect
width={'100%'}
value={item.value}
list={item.list || []}
onchange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
);
}, [item, nodeId, onChangeNode]);
return Render;
};
export default React.memo(SelectRender);

View File

@@ -1,107 +0,0 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Button, useDisclosure } from '@chakra-ui/react';
import { SelectAppItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Avatar from '@/components/Avatar';
import SelectAppModal from '../../../../SelectAppModal';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getAppDetailById } from '@/web/core/app/api';
const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const filterAppIds = useContextSelector(WorkflowContext, (ctx) => ctx.filterAppIds);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const {
isOpen: isOpenSelectApp,
onOpen: onOpenSelectApp,
onClose: onCloseSelectApp
} = useDisclosure();
const value = item.value as SelectAppItemType | undefined;
const { data: appDetail, loading } = useRequest2(
() => {
if (value?.id) return getAppDetailById(value.id);
return Promise.resolve(null);
},
{
manual: false,
refreshDeps: [value?.id],
errorToast: 'Error',
onError() {
onChangeNode({
nodeId,
type: 'updateInput',
key: 'app',
value: {
...item,
value: undefined
}
});
}
}
);
const Render = useMemo(() => {
return (
<>
<Box onClick={onOpenSelectApp}>
{!value ? (
<Button variant={'whiteFlow'} w={'100%'}>
{t('core.module.Select app')}
</Button>
) : (
<Button
isLoading={loading}
w={'100%'}
justifyContent={loading ? 'center' : 'flex-start'}
variant={'whiteFlow'}
leftIcon={<Avatar src={appDetail?.avatar} w={6} />}
>
{appDetail?.name}
</Button>
)}
</Box>
{isOpenSelectApp && (
<SelectAppModal
value={item.value}
filterAppIds={filterAppIds}
onClose={onCloseSelectApp}
onSuccess={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: 'app',
value: {
...item,
value: e
}
});
}}
/>
)}
</>
);
}, [
appDetail?.avatar,
appDetail?.name,
filterAppIds,
isOpenSelectApp,
item,
loading,
nodeId,
onChangeNode,
onCloseSelectApp,
onOpenSelectApp,
t,
value
]);
return Render;
};
export default React.memo(SelectAppRender);

View File

@@ -1,130 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Button, Flex, Grid, useDisclosure, useTheme } from '@chakra-ui/react';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { SelectedDatasetType } from '@fastgpt/global/core/workflow/api';
import Avatar from '@/components/Avatar';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'next-i18next';
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const SelectDatasetRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const theme = useTheme();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [data, setData] = useState({
searchMode: DatasetSearchModeEnum.embedding,
limit: 5,
similarity: 0.5,
usingReRank: false
});
const { allDatasets, loadAllDatasets } = useDatasetStore();
const {
isOpen: isOpenDatasetSelect,
onOpen: onOpenDatasetSelect,
onClose: onCloseDatasetSelect
} = useDisclosure();
const selectedDatasets = useMemo(() => {
const value = item.value as SelectedDatasetType;
return allDatasets.filter((dataset) => value?.find((item) => item.datasetId === dataset._id));
}, [allDatasets, item.value]);
useQuery(['loadAllDatasets'], loadAllDatasets);
useEffect(() => {
inputs.forEach((input) => {
// @ts-ignore
if (data[input.key] !== undefined) {
setData((state) => ({
...state,
[input.key]: input.value
}));
}
});
}, [inputs]);
const Render = useMemo(() => {
return (
<>
<Grid
gridTemplateColumns={'repeat(2, minmax(0, 1fr))'}
gridGap={4}
minW={'350px'}
w={'100%'}
>
<Button
h={'36px'}
leftIcon={<MyIcon name={'common/selectLight'} w={'14px'} />}
onClick={onOpenDatasetSelect}
>
{t('common.Choose')}
</Button>
{selectedDatasets.map((item) => (
<Flex
key={item._id}
alignItems={'center'}
h={'36px'}
border={theme.borders.base}
px={2}
borderRadius={'md'}
>
<Avatar src={item.avatar} w={'24px'}></Avatar>
<Box
ml={3}
flex={'1 0 0'}
w={0}
className="textEllipsis"
fontWeight={'bold'}
fontSize={['md', 'lg']}
>
{item.name}
</Box>
</Flex>
))}
</Grid>
{isOpenDatasetSelect && (
<DatasetSelectModal
isOpen={isOpenDatasetSelect}
defaultSelectedDatasets={item.value}
onChange={(e) => {
onChangeNode({
nodeId,
key: item.key,
type: 'updateInput',
value: {
...item,
value: e
}
});
}}
onClose={onCloseDatasetSelect}
/>
)}
</>
);
}, [
isOpenDatasetSelect,
item,
nodeId,
onChangeNode,
onCloseDatasetSelect,
onOpenDatasetSelect,
selectedDatasets,
t,
theme.borders.base
]);
return Render;
};
export default React.memo(SelectDatasetRender);

View File

@@ -1,122 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import MyIcon from '@fastgpt/web/components/common/Icon';
import DatasetParamsModal, { DatasetParamsProps } from '@/components/core/app/DatasetParamsModal';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
const SelectDatasetParam = ({ inputs = [], nodeId }: RenderInputProps) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { t } = useTranslation();
const { llmModelList } = useSystemStore();
const [data, setData] = useState<DatasetParamsProps>({
searchMode: DatasetSearchModeEnum.embedding,
limit: 5,
similarity: 0.5,
usingReRank: false,
datasetSearchUsingExtensionQuery: true,
datasetSearchExtensionModel: llmModelList[0]?.model,
datasetSearchExtensionBg: ''
});
const tokenLimit = useMemo(() => {
let maxTokens = 3000;
nodeList.forEach((item) => {
if (item.flowNodeType === FlowNodeTypeEnum.chatNode) {
const model =
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken =
llmModelList.find((item) => item.model === model)?.quoteMaxToken || 3000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
return maxTokens;
}, [llmModelList, nodeList]);
const { isOpen, onOpen, onClose } = useDisclosure();
useEffect(() => {
inputs.forEach((input) => {
// @ts-ignore
if (data[input.key] !== undefined) {
setData((state) => ({
...state,
[input.key]: input.value
}));
}
});
}, [inputs]);
const Render = useMemo(() => {
return (
<>
{/* label */}
<Flex alignItems={'center'} mb={3} fontWeight={'medium'} color={'myGray.600'}>
{t('core.dataset.search.Params Setting')}
<MyIcon
name={'common/settingLight'}
ml={2}
w={'16px'}
cursor={'pointer'}
_hover={{
color: 'primary.600'
}}
onClick={onOpen}
/>
</Flex>
<SearchParamsTip
searchMode={data.searchMode}
similarity={data.similarity}
limit={data.limit}
usingReRank={data.usingReRank}
queryExtensionModel={data.datasetSearchExtensionModel}
/>
</>
);
}, [data, onOpen, t]);
return (
<>
{Render}
{isOpen && (
<DatasetParamsModal
{...data}
maxTokens={tokenLimit}
onClose={onClose}
onSuccess={(e) => {
setData(e);
for (let key in e) {
const item = inputs.find((input) => input.key === key);
if (!item) continue;
onChangeNode({
nodeId,
type: 'updateInput',
key,
value: {
...item,
//@ts-ignore
value: e[key]
}
});
}
}}
/>
)}
</>
);
};
export default React.memo(SelectDatasetParam);

View File

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

View File

@@ -1,59 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import type { RenderInputProps } from '../type';
import type { SettingAIDataType } from '@fastgpt/global/core/app/type.d';
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
const SelectAiModelRender = ({ item, inputs = [], nodeId }: RenderInputProps) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const onChangeModel = useCallback(
(e: SettingAIDataType) => {
for (const key in e) {
const input = inputs.find((input) => input.key === key);
input &&
onChangeNode({
nodeId,
type: 'updateInput',
key,
value: {
...input,
// @ts-ignore
value: e[key]
}
});
}
},
[inputs, nodeId, onChangeNode]
);
const llmModelData: SettingAIDataType = useMemo(
() => ({
model: inputs.find((input) => input.key === NodeInputKeyEnum.aiModel)?.value ?? '',
maxToken:
inputs.find((input) => input.key === NodeInputKeyEnum.aiChatMaxToken)?.value ?? 2048,
temperature:
inputs.find((input) => input.key === NodeInputKeyEnum.aiChatTemperature)?.value ?? 1,
isResponseAnswerText: inputs.find(
(input) => input.key === NodeInputKeyEnum.aiChatIsResponseText
)?.value
}),
[inputs]
);
const Render = useMemo(() => {
return (
<SettingLLMModel
llmModelType={item.llmModelType}
defaultData={llmModelData}
onChange={onChangeModel}
/>
);
}, [item.llmModelType, llmModelData, onChangeModel]);
return Render;
};
export default React.memo(SelectAiModelRender);

View File

@@ -1,284 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { Box, BoxProps, Button, Flex, ModalFooter, useDisclosure } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useForm } from 'react-hook-form';
import { PromptTemplateItem } from '@fastgpt/global/core/ai/type';
import { useTranslation } from 'next-i18next';
import { ModalBody } from '@chakra-ui/react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import {
Prompt_QuotePromptList,
Prompt_QuoteTemplateList
} from '@fastgpt/global/core/ai/prompt/AIChat';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import PromptTemplate from '@/components/PromptTemplate';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Reference from './Reference';
import ValueTypeLabel from '../../ValueTypeLabel';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useCreation } from 'ahooks';
import { AppContext } from '@/web/core/app/context/appContext';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const LabelStyles: BoxProps = {
fontSize: ['sm', 'md']
};
const selectTemplateBtn: BoxProps = {
color: 'primary.500',
cursor: 'pointer'
};
const SettingQuotePrompt = (props: RenderInputProps) => {
const { inputs = [], nodeId } = props;
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { watch, setValue, handleSubmit } = useForm({
defaultValues: {
quoteTemplate: inputs.find((input) => input.key === 'quoteTemplate')?.value || '',
quotePrompt: inputs.find((input) => input.key === 'quotePrompt')?.value || ''
}
});
const aiChatQuoteTemplate = watch('quoteTemplate');
const aiChatQuotePrompt = watch('quotePrompt');
const { appDetail } = useContextSelector(AppContext, (v) => v);
const variables = useCreation(() => {
const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
return globalVariables;
}, [nodeList, t]);
const [selectTemplateData, setSelectTemplateData] = useState<{
title: string;
templates: PromptTemplateItem[];
}>();
const quoteTemplateVariables = useMemo(
() => [
{
key: 'q',
label: 'q',
icon: 'core/app/simpleMode/variable'
},
{
key: 'a',
label: 'a',
icon: 'core/app/simpleMode/variable'
},
{
key: 'source',
label: t('core.dataset.search.Source name'),
icon: 'core/app/simpleMode/variable'
},
{
key: 'sourceId',
label: t('core.dataset.search.Source id'),
icon: 'core/app/simpleMode/variable'
},
{
key: 'index',
label: t('core.dataset.search.Quote index'),
icon: 'core/app/simpleMode/variable'
},
...variables
],
[t, variables]
);
const quotePromptVariables = useMemo(
() => [
{
key: 'quote',
label: t('core.app.Quote templates'),
icon: 'core/app/simpleMode/variable'
},
{
key: 'question',
label: t('core.module.input.label.user question'),
icon: 'core/app/simpleMode/variable'
},
...variables
],
[t, variables]
);
const onSubmit = useCallback(
(data: { quoteTemplate: string; quotePrompt: string }) => {
const quoteTemplateInput = inputs.find(
(input) => input.key === NodeInputKeyEnum.aiChatQuoteTemplate
);
const quotePromptInput = inputs.find(
(input) => input.key === NodeInputKeyEnum.aiChatQuotePrompt
);
if (quoteTemplateInput) {
onChangeNode({
nodeId,
type: 'updateInput',
key: quoteTemplateInput.key,
value: {
...quoteTemplateInput,
value: data.quoteTemplate
}
});
}
if (quotePromptInput) {
onChangeNode({
nodeId,
type: 'updateInput',
key: quotePromptInput.key,
value: {
...quotePromptInput,
value: data.quotePrompt
}
});
}
onClose();
},
[inputs, nodeId, onChangeNode, onClose]
);
const Render = useMemo(() => {
return (
<>
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Box position={'relative'} color={'myGray.600'} fontWeight={'medium'}>
{t('core.module.Dataset quote.label')}
</Box>
<ValueTypeLabel valueType={WorkflowIOValueTypeEnum.datasetQuote} />
<MyTooltip label={t('core.module.Setting quote prompt')}>
<MyIcon
ml={1}
name={'common/settingLight'}
w={'14px'}
cursor={'pointer'}
onClick={onOpen}
/>
</MyTooltip>
</Flex>
<Box mt={1}>
<Reference {...props} />
</Box>
<MyModal
isOpen={isOpen}
iconSrc={'modal/edit'}
title={t('core.module.Quote prompt setting')}
w={'600px'}
>
<ModalBody>
<Box>
<Flex {...LabelStyles} mb={1}>
<FormLabel>{t('core.app.Quote templates')}</FormLabel>
<QuestionTip
ml={1}
label={t('template.Quote Content Tip', {
default: Prompt_QuoteTemplateList[0].value
})}
></QuestionTip>
<Box flex={1} />
<Box
{...selectTemplateBtn}
fontSize={'sm'}
onClick={() =>
setSelectTemplateData({
title: t('core.app.Select quote template'),
templates: Prompt_QuoteTemplateList
})
}
>
{t('common.Select template')}
</Box>
</Flex>
<PromptEditor
variables={quoteTemplateVariables}
h={160}
title={t('core.app.Quote templates')}
placeholder={t('template.Quote Content Tip', {
default: Prompt_QuoteTemplateList[0].value
})}
value={aiChatQuoteTemplate}
onChange={(e) => {
setValue('quoteTemplate', e);
}}
/>
</Box>
<Box mt={4}>
<Flex {...LabelStyles} mb={1}>
<FormLabel>{t('core.app.Quote prompt')}</FormLabel>
<QuestionTip
ml={1}
label={t('template.Quote Prompt Tip', {
default: Prompt_QuotePromptList[0].value
})}
></QuestionTip>
</Flex>
<PromptEditor
variables={quotePromptVariables}
title={t('core.app.Quote prompt')}
h={280}
placeholder={t('template.Quote Prompt Tip', {
default: Prompt_QuotePromptList[0].value
})}
value={aiChatQuotePrompt}
onChange={(e) => {
setValue('quotePrompt', e);
}}
/>
</Box>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={2} onClick={onClose}>
{t('common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmit)}>{t('common.Confirm')}</Button>
</ModalFooter>
</MyModal>
{!!selectTemplateData && (
<PromptTemplate
title={selectTemplateData.title}
templates={selectTemplateData.templates}
onClose={() => setSelectTemplateData(undefined)}
onSuccess={(e) => {
const quoteVal = e.value;
const promptVal = Prompt_QuotePromptList.find(
(item) => item.title === e.title
)?.value;
setValue('quoteTemplate', quoteVal);
setValue('quotePrompt', promptVal);
}}
/>
)}
</>
);
}, [
aiChatQuotePrompt,
aiChatQuoteTemplate,
handleSubmit,
isOpen,
onClose,
onOpen,
onSubmit,
props,
quotePromptVariables,
quoteTemplateVariables,
selectTemplateData,
setValue,
t
]);
return Render;
};
export default React.memo(SettingQuotePrompt);

View File

@@ -1,42 +0,0 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { useTranslation } from 'next-i18next';
import { Box } from '@chakra-ui/react';
import MySlider from '@/components/Slider';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
const SliderRender = ({ item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const Render = useMemo(() => {
return (
<Box px={2}>
<MySlider
markList={item.markList}
width={'100%'}
min={item.min || 0}
max={item.max}
step={item.step || 1}
value={item.value}
onChange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}}
/>
</Box>
);
}, [item, nodeId, onChangeNode]);
return Render;
};
export default React.memo(SliderRender);

View File

@@ -1,33 +0,0 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { Switch } from '@chakra-ui/react';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
const SwitchRender = ({ item, nodeId }: RenderInputProps) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const Render = useMemo(() => {
return (
<Switch
size={'md'}
isChecked={item.value}
onChange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e.target.checked
}
});
}}
/>
);
}, [item, nodeId, onChangeNode]);
return Render;
};
export default React.memo(SwitchRender);

View File

@@ -1,36 +0,0 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { Input } from '@chakra-ui/react';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
const TextInput = ({ item, nodeId }: RenderInputProps) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const Render = useMemo(() => {
return (
<Input
placeholder={item.placeholder}
defaultValue={item.value}
bg={'white'}
px={3}
borderRadius={'sm'}
onBlur={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e.target.value
}
});
}}
/>
);
}, [item, nodeId, onChangeNode]);
return Render;
};
export default React.memo(TextInput);

View File

@@ -1,70 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { useTranslation } from 'next-i18next';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useCreation } from 'ahooks';
import { AppContext } from '@/web/core/app/context/appContext';
const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { appDetail } = useContextSelector(AppContext, (v) => v);
// get variable
const variables = useCreation(() => {
const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig,
t
});
const moduleVariables = formatEditorVariablePickerIcon(
inputs
.filter((input) => input.canEdit)
.map((item) => ({
key: item.key,
label: item.label
}))
);
return [...globalVariables, ...moduleVariables];
}, [nodeList, inputs, t]);
const onChange = useCallback(
(e: string) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
},
[item, nodeId, onChangeNode]
);
const Render = useMemo(() => {
return (
<PromptEditor
variables={variables}
title={t(item.label)}
maxLength={item.maxLength}
h={150}
placeholder={t(item.placeholder || '')}
value={item.value}
onChange={onChange}
isFlow={true}
/>
);
}, [item.label, item.maxLength, item.placeholder, item.value, onChange, t, variables]);
return Render;
};
export default React.memo(TextareaRender);

View File

@@ -1,7 +0,0 @@
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
export type RenderInputProps = {
inputs?: FlowNodeInputItemType[];
item: FlowNodeInputItemType;
nodeId: string;
};

View File

@@ -1,56 +0,0 @@
import { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import React, { useMemo } from 'react';
import { useTranslation } from 'next-i18next';
import { Box, Flex } from '@chakra-ui/react';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { SourceHandle } from '../Handle';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { Position } from 'reactflow';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import ValueTypeLabel from '../ValueTypeLabel';
const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutputItemType }) => {
const { t } = useTranslation();
const { label = '', description, valueType } = output;
const Render = useMemo(() => {
return (
<Box position={'relative'}>
<Flex
className="nodrag"
cursor={'default'}
alignItems={'center'}
fontWeight={'medium'}
color={'myGray.600'}
{...(output.type === FlowNodeOutputTypeEnum.source
? {
flexDirection: 'row-reverse'
}
: {})}
>
<Box
position={'relative'}
mr={1}
ml={output.type === FlowNodeOutputTypeEnum.source ? 1 : 0}
>
{t(label)}
</Box>
{description && <QuestionTip label={t(description)} />}
<ValueTypeLabel valueType={valueType} />
</Flex>
{output.type === FlowNodeOutputTypeEnum.source && (
<SourceHandle
nodeId={nodeId}
handleId={getHandleId(nodeId, 'source', output.key)}
translate={[26, 0]}
position={Position.Right}
/>
)}
</Box>
);
}, [output.type, output.key, t, label, description, valueType, nodeId]);
return Render;
};
export default React.memo(OutputLabel);

View File

@@ -1,197 +0,0 @@
import React, { useMemo, useState } from 'react';
import type { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { Box, Button, Flex } from '@chakra-ui/react';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import OutputLabel from './Label';
import { RenderOutputProps } from './type';
import { useTranslation } from 'next-i18next';
import { SmallAddIcon } from '@chakra-ui/icons';
import VariableTable from '../VariableTable';
import { EditNodeFieldType } from '@fastgpt/global/core/workflow/node/type';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
const RenderList: {
types: FlowNodeOutputTypeEnum[];
Component: React.ComponentType<RenderOutputProps>;
}[] = [];
const RenderOutput = ({
nodeId,
flowOutputList
}: {
nodeId: string;
flowOutputList: FlowNodeOutputItemType[];
}) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const outputString = useMemo(() => JSON.stringify(flowOutputList), [flowOutputList]);
const copyOutputs = useMemo(() => {
const parseOutputs = JSON.parse(outputString) as FlowNodeOutputItemType[];
return parseOutputs;
}, [outputString]);
const [createField, setCreateField] = useState<EditNodeFieldType>();
const [editField, setEditField] = useState<EditNodeFieldType>();
const RenderDynamicOutputs = useMemo(() => {
const dynamicOutputs = copyOutputs.filter(
(item) => item.type === FlowNodeOutputTypeEnum.dynamic
);
const addOutput = dynamicOutputs.find((item) => item.key === NodeOutputKeyEnum.addOutputParam);
const filterAddOutput = dynamicOutputs.filter(
(item) => item.key !== NodeOutputKeyEnum.addOutputParam
);
return dynamicOutputs.length === 0 || !addOutput ? null : (
<Box mb={5}>
<Flex
mb={2}
className="nodrag"
cursor={'default'}
alignItems={'center'}
position={'relative'}
>
<Box position={'relative'} fontWeight={'medium'}>
{t('core.workflow.Custom outputs')}
</Box>
<QuestionTip ml={1} label={addOutput.description} />
<Box flex={'1 0 0'} />
<Button
variant={'whitePrimary'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
onClick={() => {
setCreateField({});
}}
>
{t('common.Add New')}
</Button>
</Flex>
<VariableTable
fieldEditType={addOutput.editField}
keys={copyOutputs.map((output) => output.key)}
onCloseFieldEdit={() => {
setCreateField(undefined);
setEditField(undefined);
}}
variables={filterAddOutput.map((output) => ({
label: output.label || '-',
type: output.valueType ? t(FlowValueTypeMap[output.valueType]?.label) : '-',
key: output.key
}))}
createField={createField}
onCreate={({ data }) => {
if (!data.key) {
return;
}
const newOutput: FlowNodeOutputItemType = {
id: getNanoid(),
type: FlowNodeOutputTypeEnum.dynamic,
key: data.key,
valueType: data.valueType,
label: data.key
};
onChangeNode({
nodeId,
type: 'addOutput',
value: newOutput
});
setCreateField(undefined);
}}
editField={editField}
onStartEdit={(e) => {
const output = copyOutputs.find((output) => output.key === e);
if (!output) return;
setEditField({
valueType: output.valueType,
required: output.required,
key: output.key,
label: output.label,
description: output.description
});
}}
onEdit={({ data, changeKey }) => {
if (!data.key || !editField?.key) return;
const output = copyOutputs.find((output) => output.key === editField.key);
const newOutput: FlowNodeOutputItemType = {
...(output as FlowNodeOutputItemType),
valueType: data.valueType,
key: data.key,
label: data.label,
description: data.description
};
if (changeKey) {
onChangeNode({
nodeId,
type: 'replaceOutput',
key: editField.key,
value: newOutput
});
} else {
onChangeNode({
nodeId,
type: 'updateOutput',
key: newOutput.key,
value: newOutput
});
}
setEditField(undefined);
}}
onDelete={(key) => {
onChangeNode({
nodeId,
type: 'delOutput',
key
});
}}
/>
</Box>
);
}, [copyOutputs, createField, editField, nodeId, onChangeNode, t]);
const RenderCommonOutputs = useMemo(() => {
const renderOutputs = copyOutputs.filter(
(item) =>
item.type !== FlowNodeOutputTypeEnum.dynamic && item.type !== FlowNodeOutputTypeEnum.hidden
);
return (
<>
{renderOutputs.map((output) => {
return output.label ? (
<Box key={output.key} _notLast={{ mb: 5 }} position={'relative'}>
{output.required && (
<Box position={'absolute'} left={'-6px'} top={-1} color={'red.600'}>
*
</Box>
)}
<OutputLabel nodeId={nodeId} output={output} />
</Box>
) : null;
})}
</>
);
}, [copyOutputs, nodeId]);
return (
<>
{RenderDynamicOutputs}
{RenderCommonOutputs}
</>
);
};
export default React.memo(RenderOutput);

View File

@@ -1,7 +0,0 @@
import { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io.d';
export type RenderOutputProps = {
outputs?: FlowNodeOutputItemType[];
item: FlowNodeOutputItemType;
nodeId: string;
};

View File

@@ -1,138 +0,0 @@
import React, { useCallback, useRef } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import type { EditFieldModalProps } from './type';
import { useTranslation } from 'next-i18next';
import {
Box,
Button,
Flex,
Input,
ModalBody,
ModalFooter,
Switch,
Textarea
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { defaultEditFormData } from './constants';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import { fnValueTypeSelect } from '@/web/core/workflow/constants/dataType';
const EditFieldModal = ({
defaultValue = defaultEditFormData,
nodeId,
onClose
}: EditFieldModalProps) => {
const { t } = useTranslation();
const { toast } = useToast();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { register, setValue, handleSubmit, watch } = useForm<FlowNodeInputItemType>({
defaultValues: defaultValue
});
const valueType = watch('valueType');
const { mutate: onclickSubmit } = useRequest({
mutationFn: async (e: FlowNodeInputItemType) => {
const inputConfig: FlowNodeInputItemType = {
...e,
label: e.key
};
if (defaultValue.key) {
// edit
onChangeNode({
nodeId,
type: 'replaceInput',
key: defaultValue.key,
value: inputConfig
});
} else {
// create
onChangeNode({
nodeId,
type: 'addInput',
index: 1,
value: {
...e,
label: e.key
}
});
}
onClose();
}
});
const onclickSubmitError = useCallback(
(e: Object) => {
for (const item of Object.values(e)) {
if (item.message) {
toast({
status: 'warning',
title: item.message
});
break;
}
}
},
[toast]
);
return (
<MyModal isOpen iconSrc="modal/edit" title={'工具字段参数配置'} onClose={onClose}>
<ModalBody>
<Flex alignItems={'center'} mb={5}>
<Box flex={'0 0 80px'}>{t('common.Require Input')}</Box>
<Switch {...register('required')} />
</Flex>
<Flex alignItems={'center'} mb={5}>
<Box flex={'0 0 80px'}>{t('core.module.Data Type')}</Box>
<Box flex={'1 0 0'}>
<MySelect
list={fnValueTypeSelect}
value={valueType}
onchange={(e: any) => {
setValue('valueType', e);
}}
/>
</Box>
</Flex>
<Flex alignItems={'center'} mb={5}>
<Box flex={'0 0 80px'}>{t('core.module.Field Name')}</Box>
<Input
bg={'myGray.50'}
{...register('key', {
required: true,
pattern: {
value: /^[a-zA-Z]+[0-9]*$/,
message: '字段key必须是纯英文字母或数字并且不能以数字开头。'
}
})}
/>
</Flex>
<Box mb={5}>
<Box flex={'0 0 80px'}>{t('core.module.Field Description')}</Box>
<Textarea
bg={'myGray.50'}
rows={5}
{...register('toolDescription', {
required: true
})}
/>
</Box>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={2} onClick={onClose}>
{t('common.Close')}
</Button>
<Button onClick={handleSubmit((data) => onclickSubmit(data), onclickSubmitError)}>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(EditFieldModal);

View File

@@ -1,17 +0,0 @@
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
export const defaultEditFormData: FlowNodeInputItemType = {
valueType: WorkflowIOValueTypeEnum.string,
renderTypeList: [FlowNodeInputTypeEnum.reference],
key: '',
label: '',
toolDescription: '',
required: true,
canEdit: true,
editField: {
key: true,
description: true
}
};

View File

@@ -1,124 +0,0 @@
import React, { useMemo, useState } from 'react';
import type {
FlowNodeInputItemType,
FlowNodeOutputItemType
} from '@fastgpt/global/core/workflow/type/io.d';
import {
Box,
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Flex
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import dynamic from 'next/dynamic';
import { defaultEditFormData } from './constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
const EditFieldModal = dynamic(() => import('./EditFieldModal'));
const RenderToolInput = ({
nodeId,
inputs,
canEdit = false
}: {
nodeId: string;
inputs: FlowNodeInputItemType[];
canEdit?: boolean;
}) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [editField, setEditField] = useState<FlowNodeInputItemType>();
return (
<>
{canEdit && (
<Flex mb={2} alignItems={'center'}>
<Box flex={'1 0 0'} fontWeight={'medium'} color={'myGray.600'}>
{t('common.Field')}
</Box>
<Button
variant={'unstyled'}
leftIcon={<MyIcon name={'common/addLight'} w={'14px'} />}
size={'sm'}
px={3}
_hover={{
bg: 'myGray.150'
}}
onClick={() => setEditField(defaultEditFormData)}
>
{t('core.module.extract.Add field')}
</Button>
</Flex>
)}
<Box borderRadius={'md'} overflow={'hidden'} borderWidth={'1px'} borderBottom="none">
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th></Th>
<Th></Th>
<Th></Th>
{canEdit && <Th></Th>}
</Tr>
</Thead>
<Tbody>
{inputs.map((item, index) => (
<Tr
key={index}
position={'relative'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
>
<Td>{item.key}</Td>
<Td>{item.toolDescription}</Td>
<Td>{item.required ? '✔' : ''}</Td>
{canEdit && (
<Td whiteSpace={'nowrap'}>
<MyIcon
mr={3}
name={'common/settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => setEditField(item)}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
onChangeNode({
nodeId,
type: 'delInput',
key: item.key
});
}}
/>
</Td>
)}
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
{!!editField && (
<EditFieldModal
defaultValue={editField}
nodeId={nodeId}
onClose={() => setEditField(undefined)}
/>
)}
</>
);
};
export default React.memo(RenderToolInput);

View File

@@ -1,5 +0,0 @@
export type EditFieldModalProps = {
defaultValue?: EditFieldFormProps;
nodeId: string;
onClose: () => void;
};

View File

@@ -1,33 +0,0 @@
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import { Box } from '@chakra-ui/react';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import React from 'react';
const ValueTypeLabel = ({ valueType }: { valueType?: WorkflowIOValueTypeEnum }) => {
const valueTypeData = valueType ? FlowValueTypeMap[valueType] : undefined;
const label = valueTypeData?.label || '';
const description = valueTypeData?.description || '';
return !!label ? (
<MyTooltip label={description}>
<Box
bg={'myGray.100'}
color={'myGray.500'}
border={'base'}
borderRadius={'sm'}
ml={2}
px={1}
h={6}
display={'flex'}
alignItems={'center'}
fontSize={'11px'}
>
{label}
</Box>
</MyTooltip>
) : null;
};
export default React.memo(ValueTypeLabel);

View File

@@ -1,123 +0,0 @@
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, Table, Thead, Tbody, Tr, Th, Td, TableContainer, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import type {
EditInputFieldMapType,
EditNodeFieldType
} from '@fastgpt/global/core/workflow/node/type';
import dynamic from 'next/dynamic';
const FieldEditModal = dynamic(() => import('./FieldEditModal'));
const VariableTable = ({
fieldEditType,
variables = [],
keys,
createField,
onCreate,
editField,
onStartEdit,
onEdit,
onCloseFieldEdit,
onDelete
}: {
fieldEditType?: EditInputFieldMapType;
variables: { icon?: string; label: string; type: string; key: string }[];
keys: string[];
createField?: EditNodeFieldType;
onCreate?: (e: { data: EditNodeFieldType }) => void;
editField?: EditNodeFieldType;
onStartEdit: (key: string) => void;
onEdit?: (e: { data: EditNodeFieldType; changeKey: boolean }) => void;
onCloseFieldEdit: () => void;
onDelete?: (key: string) => void;
}) => {
const { t } = useTranslation();
const fileEditData = (createField || editField) as EditNodeFieldType | undefined;
return (
<>
<Box
bg={'white'}
borderRadius={'md'}
overflow={'hidden'}
borderWidth={'1px'}
borderBottom={'none'}
>
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th borderBottomLeftRadius={'none !important'}>
{t('core.module.variable.variable name')}
</Th>
<Th>{t('core.workflow.Value type')}</Th>
<Th borderBottomRightRadius={'none !important'}></Th>
</Tr>
</Thead>
<Tbody>
{variables.map((item) => (
<Tr key={item.key}>
<Td>
<Flex alignItems={'center'}>
{!!item.icon && <MyIcon name={item.icon as any} w={'14px'} mr={1} />}
{item.label || item.key}
</Flex>
</Td>
<Td>{item.type}</Td>
<Td>
<MyIcon
mr={3}
name={'common/settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => onStartEdit(item.key)}
/>
<MyIcon
className="delete"
name={'delete'}
w={'16px'}
color={'myGray.600'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onDelete?.(item.key);
}}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
{!!fileEditData && (
<FieldEditModal
editField={fieldEditType}
defaultField={fileEditData}
keys={keys}
onClose={onCloseFieldEdit}
onSubmit={(e) => {
if (!!createField && onCreate) {
onCreate(e);
} else if (!!editField && onEdit) {
onEdit(e);
}
}}
/>
)}
</>
);
};
export default React.memo(VariableTable);

View File

@@ -1,186 +0,0 @@
import React, { useCallback, useState } from 'react';
import { getPublishList, postRevertVersion } from '@/web/core/app/versionApi';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
import { useTranslation } from 'next-i18next';
import { useMemoizedFn } from 'ahooks';
import { Box, Button, Flex } from '@chakra-ui/react';
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { AppContext } from '@/web/core/app/context/appContext';
const PublishHistoriesSlider = () => {
const { t } = useTranslation();
const { openConfirm, ConfirmModal } = useConfirm({
content: t('core.workflow.publish.OnRevert version confirm')
});
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
const appId = useContextSelector(WorkflowContext, (e) => e.appId);
const setIsShowVersionHistories = useContextSelector(
WorkflowContext,
(e) => e.setIsShowVersionHistories
);
const initData = useContextSelector(WorkflowContext, (e) => e.initData);
const [selectedHistoryId, setSelectedHistoryId] = useState<string>();
const { list, ScrollList, isLoading } = useScrollPagination(getPublishList, {
itemHeight: 49,
overscan: 20,
pageSize: 30,
defaultParams: {
appId
}
});
const onClose = useMemoizedFn(() => {
setIsShowVersionHistories(false);
});
const onPreview = useCallback((data: AppVersionSchemaType) => {
setSelectedHistoryId(data._id);
initData({
nodes: data.nodes,
edges: data.edges
});
}, []);
const onCloseSlider = useCallback(
(data: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => {
setSelectedHistoryId(undefined);
initData(data);
onClose();
},
[appDetail]
);
const { mutate: onRevert, isLoading: isReverting } = useRequest({
mutationFn: async (data: AppVersionSchemaType) => {
if (!appId) return;
await postRevertVersion(appId, {
versionId: data._id,
editNodes: appDetail.modules, // old workflow
editEdges: appDetail.edges
});
setAppDetail((state) => ({
...state,
modules: data.nodes,
edges: data.edges
}));
onCloseSlider(data);
}
});
const showLoading = isLoading || isReverting;
return (
<>
<CustomRightDrawer
onClose={() =>
onCloseSlider({
nodes: appDetail.modules,
edges: appDetail.edges
})
}
iconSrc="core/workflow/versionHistories"
title={t('core.workflow.publish.histories')}
maxW={'300px'}
px={0}
showMask={false}
mt={'60px'}
overflow={'unset'}
>
<Button
mx={'20px'}
variant={'whitePrimary'}
mb={2}
isDisabled={!selectedHistoryId}
onClick={() => {
setSelectedHistoryId(undefined);
initData({
nodes: appDetail.modules,
edges: appDetail.edges
});
}}
>
{t('core.workflow.Current workflow')}
</Button>
<ScrollList isLoading={showLoading} flex={'1 0 0'} px={5}>
{list.map((data, index) => {
const item = data.data;
return (
<Flex
key={data.index}
alignItems={'center'}
py={3}
px={3}
borderRadius={'md'}
cursor={'pointer'}
fontWeight={500}
_hover={{
bg: 'primary.50'
}}
{...(selectedHistoryId === item._id && {
color: 'primary.600'
})}
onClick={() => onPreview(item)}
>
<Box
w={'12px'}
h={'12px'}
borderWidth={'2px'}
borderColor={'primary.600'}
borderRadius={'50%'}
position={'relative'}
{...(index !== list.length - 1 && {
_after: {
content: '""',
height: '40px',
width: '2px',
bgColor: 'myGray.250',
position: 'absolute',
top: '10px',
left: '3px'
}
})}
></Box>
<Box ml={3} flex={'1 0 0'}>
{formatTime2YMDHM(item.time)}
</Box>
{item._id === selectedHistoryId && (
<MyTooltip label={t('core.workflow.publish.OnRevert version')}>
<MyIcon
name={'core/workflow/revertVersion'}
w={'20px'}
color={'primary.600'}
onClick={(e) => {
e.stopPropagation();
openConfirm(() => onRevert(item))();
}}
/>
</MyTooltip>
)}
</Flex>
);
})}
</ScrollList>
</CustomRightDrawer>
<ConfirmModal />
</>
);
};
export default React.memo(PublishHistoriesSlider);

View File

@@ -1,23 +0,0 @@
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type';
import React from 'react';
import { DefaultEdgeOptions } from 'reactflow';
export const connectionLineStyle: React.CSSProperties = {
strokeWidth: 2,
stroke: '#487FFF'
};
export const defaultEdgeOptions: DefaultEdgeOptions = {
zIndex: 0
};
export const defaultRunningStatus: FlowNodeItemType['debugResult'] = {
status: 'running',
message: '',
showResult: false
};
export const defaultSkippedStatus: FlowNodeItemType['debugResult'] = {
status: 'skipped',
message: '',
showResult: false
};

View File

@@ -1,722 +0,0 @@
import { postWorkflowDebug } from '@/web/core/workflow/api';
import { storeEdgesRenderEdge, storeNode2FlowNode } from '@/web/core/workflow/utils';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import {
FlowNodeItemType,
FlowNodeTemplateType,
StoreNodeItemType
} from '@fastgpt/global/core/workflow/type';
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useMemoizedFn } from 'ahooks';
import React, {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import {
Edge,
EdgeChange,
Node,
NodeChange,
OnConnectStartParams,
useEdgesState,
useNodesState
} from 'reactflow';
import { createContext, useContextSelector } from 'use-context-selector';
import { defaultRunningStatus } from './constants';
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import { AppContext } from '@/web/core/app/context/appContext';
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
type WorkflowContextType = {
appId?: string;
mode: 'app' | 'plugin';
basicNodeTemplates: FlowNodeTemplateType[];
filterAppIds?: string[];
reactFlowWrapper: React.RefObject<HTMLDivElement> | null;
// nodes
nodes: Node<FlowNodeItemType, string | undefined>[];
nodeList: FlowNodeItemType[];
setNodes: Dispatch<SetStateAction<Node<FlowNodeItemType, string | undefined>[]>>;
onNodesChange: OnChange<NodeChange>;
hasToolNode: boolean;
hoverNodeId?: string;
setHoverNodeId: React.Dispatch<React.SetStateAction<string | undefined>>;
onUpdateNodeError: (node: string, isError: Boolean) => void;
onResetNode: (e: { id: string; node: FlowNodeTemplateType }) => void;
onChangeNode: (e: FlowNodeChangeProps) => void;
// edges
edges: Edge<any>[];
setEdges: Dispatch<SetStateAction<Edge<any>[]>>;
onEdgesChange: OnChange<EdgeChange>;
onDelEdge: (e: {
nodeId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}) => void;
hoverEdgeId?: string;
setHoverEdgeId: React.Dispatch<React.SetStateAction<string | undefined>>;
// connect
connectingEdge?: OnConnectStartParams;
setConnectingEdge: React.Dispatch<React.SetStateAction<OnConnectStartParams | undefined>>;
// common function
onFixView: () => void;
splitToolInputs: (
inputs: FlowNodeInputItemType[],
nodeId: string
) => {
isTool: boolean;
toolInputs: FlowNodeInputItemType[];
commonInputs: FlowNodeInputItemType[];
};
initData: (e: {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
chatConfig?: AppChatConfigType;
}) => Promise<void>;
// debug
workflowDebugData:
| {
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
nextRunNodes: RuntimeNodeItemType[];
}
| undefined;
onNextNodeDebug: () => Promise<void>;
onStartNodeDebug: ({
entryNodeId,
runtimeNodes,
runtimeEdges
}: {
entryNodeId: string;
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
}) => Promise<void>;
onStopNodeDebug: () => void;
// version history
isShowVersionHistories: boolean;
setIsShowVersionHistories: React.Dispatch<React.SetStateAction<boolean>>;
};
type ContextValueProps = Pick<
WorkflowContextType,
'mode' | 'basicNodeTemplates' | 'filterAppIds'
> & {
appId?: string;
pluginId?: string;
};
type DebugDataType = {
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
nextRunNodes: RuntimeNodeItemType[];
};
export const WorkflowContext = createContext<WorkflowContextType>({
mode: 'app',
setConnectingEdge: function (
value: React.SetStateAction<OnConnectStartParams | undefined>
): void {
throw new Error('Function not implemented.');
},
onFixView: function (): void {
throw new Error('Function not implemented.');
},
basicNodeTemplates: [],
reactFlowWrapper: null,
nodes: [],
nodeList: [],
setNodes: function (
value: React.SetStateAction<Node<FlowNodeItemType, string | undefined>[]>
): void {
throw new Error('Function not implemented.');
},
onNodesChange: function (changes: NodeChange[]): void {
throw new Error('Function not implemented.');
},
hasToolNode: false,
setHoverNodeId: function (value: React.SetStateAction<string | undefined>): void {
throw new Error('Function not implemented.');
},
onUpdateNodeError: function (node: string, isError: Boolean): void {
throw new Error('Function not implemented.');
},
edges: [],
setEdges: function (value: React.SetStateAction<Edge<any>[]>): void {
throw new Error('Function not implemented.');
},
onEdgesChange: function (changes: EdgeChange[]): void {
throw new Error('Function not implemented.');
},
onResetNode: function (e: { id: string; node: FlowNodeTemplateType }): void {
throw new Error('Function not implemented.');
},
onDelEdge: function (e: {
nodeId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}): void {
throw new Error('Function not implemented.');
},
splitToolInputs: function (
inputs: FlowNodeInputItemType[],
nodeId: string
): {
isTool: boolean;
toolInputs: FlowNodeInputItemType[];
commonInputs: FlowNodeInputItemType[];
} {
throw new Error('Function not implemented.');
},
initData: function (e: {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
}): Promise<void> {
throw new Error('Function not implemented.');
},
workflowDebugData: undefined,
onNextNodeDebug: function (): Promise<void> {
throw new Error('Function not implemented.');
},
onStartNodeDebug: function ({
entryNodeId,
runtimeNodes,
runtimeEdges
}: {
entryNodeId: string;
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
}): Promise<void> {
throw new Error('Function not implemented.');
},
onStopNodeDebug: function (): void {
throw new Error('Function not implemented.');
},
onChangeNode: function (e: FlowNodeChangeProps): void {
throw new Error('Function not implemented.');
},
isShowVersionHistories: false,
setIsShowVersionHistories: function (value: React.SetStateAction<boolean>): void {
throw new Error('Function not implemented.');
},
setHoverEdgeId: function (value: React.SetStateAction<string | undefined>): void {
throw new Error('Function not implemented.');
}
});
const WorkflowContextProvider = ({
children,
value
}: {
children: React.ReactNode;
value: ContextValueProps;
}) => {
const { appId, pluginId } = value;
const { toast } = useToast();
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const setAppDetail = useContextSelector(AppContext, (v) => v.setAppDetail);
/* edge */
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [hoverEdgeId, setHoverEdgeId] = useState<string>();
const onDelEdge = useCallback(
({
nodeId,
sourceHandle,
targetHandle
}: {
nodeId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}) => {
if (!sourceHandle && !targetHandle) return;
setEdges((state) =>
state.filter((edge) => {
if (edge.source === nodeId && edge.sourceHandle === sourceHandle) return false;
if (edge.target === nodeId && edge.targetHandle === targetHandle) return false;
return true;
})
);
},
[setEdges]
);
/* connect */
const [connectingEdge, setConnectingEdge] = useState<OnConnectStartParams>();
/* node */
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowNodeItemType>([]);
const [hoverNodeId, setHoverNodeId] = useState<string>();
const nodeListString = JSON.stringify(nodes.map((node) => node.data));
const nodeList = useMemo(
() => JSON.parse(nodeListString) as FlowNodeItemType[],
[nodeListString]
);
const hasToolNode = useMemo(() => {
return !!nodes.find((node) => node.data.flowNodeType === FlowNodeTypeEnum.tools);
}, [nodes]);
const onUpdateNodeError = useMemoizedFn((nodeId: string, isError: Boolean) => {
setNodes((nodes) => {
return nodes.map((item) => {
if (item.data?.nodeId === nodeId) {
item.selected = true;
//@ts-ignore
item.data.isError = isError;
}
return item;
});
});
});
// reset a node data. delete edge and replace it
const onResetNode = useMemoizedFn(({ id, node }: { id: string; node: FlowNodeTemplateType }) => {
setNodes((state) =>
state.map((item) => {
if (item.id === id) {
return {
...item,
data: {
...item.data,
...node,
inputs: node.inputs.map((input) => {
const value =
item.data.inputs.find((i) => i.key === input.key)?.value ?? input.value;
return {
...input,
value
};
})
}
};
}
return item;
})
);
});
const onChangeNode = useMemoizedFn((props: FlowNodeChangeProps) => {
const { nodeId, type } = props;
setNodes((nodes) =>
nodes.map((node) => {
if (node.id !== nodeId) return node;
const updateObj: Record<string, any> = {};
if (type === 'attr') {
if (props.key) {
updateObj[props.key] = props.value;
}
} else if (type === 'updateInput') {
updateObj.inputs = node.data.inputs.map((item) =>
item.key === props.key ? props.value : item
);
} else if (type === 'replaceInput') {
const oldInputIndex = node.data.inputs.findIndex((item) => item.key === props.key);
updateObj.inputs = node.data.inputs.filter((item) => item.key !== props.key);
setTimeout(() => {
onChangeNode({
nodeId,
type: 'addInput',
index: oldInputIndex,
value: props.value
});
});
} else if (type === 'addInput') {
const input = node.data.inputs.find((input) => input.key === props.value.key);
if (input) {
toast({
status: 'warning',
title: 'key 重复'
});
updateObj.inputs = node.data.inputs;
} else {
if (props.index !== undefined) {
const inputs = [...node.data.inputs];
inputs.splice(props.index, 0, props.value);
updateObj.inputs = inputs;
} else {
updateObj.inputs = node.data.inputs.concat(props.value);
}
}
} else if (type === 'delInput') {
updateObj.inputs = node.data.inputs.filter((item) => item.key !== props.key);
} else if (type === 'updateOutput') {
updateObj.outputs = node.data.outputs.map((item) =>
item.key === props.key ? props.value : item
);
} else if (type === 'replaceOutput') {
onDelEdge({ nodeId, sourceHandle: getHandleId(nodeId, 'source', props.key) });
const oldOutputIndex = node.data.outputs.findIndex((item) => item.key === props.key);
updateObj.outputs = node.data.outputs.filter((item) => item.key !== props.key);
console.log(props.value);
setTimeout(() => {
onChangeNode({
nodeId,
type: 'addOutput',
index: oldOutputIndex,
value: props.value
});
});
} else if (type === 'addOutput') {
const output = node.data.outputs.find((output) => output.key === props.value.key);
if (output) {
toast({
status: 'warning',
title: 'key 重复'
});
updateObj.outputs = node.data.outputs;
} else {
if (props.index !== undefined) {
const outputs = [...node.data.outputs];
outputs.splice(props.index, 0, props.value);
updateObj.outputs = outputs;
} else {
updateObj.outputs = node.data.outputs.concat(props.value);
}
}
} else if (type === 'delOutput') {
onDelEdge({ nodeId, sourceHandle: getHandleId(nodeId, 'source', props.key) });
updateObj.outputs = node.data.outputs.filter((item) => item.key !== props.key);
}
return {
...node,
data: {
...node.data,
...updateObj
}
};
})
);
});
/* function */
const onFixView = useMemoizedFn(() => {
const btn = document.querySelector('.custom-workflow-fix_view') as HTMLButtonElement;
setTimeout(() => {
btn && btn.click();
}, 100);
});
/* If the module is connected by a tool, the tool input and the normal input are separated */
const splitToolInputs = (inputs: FlowNodeInputItemType[], nodeId: string) => {
const isTool = !!edges.find(
(edge) => edge.targetHandle === NodeOutputKeyEnum.selectedTools && edge.target === nodeId
);
return {
isTool,
toolInputs: inputs.filter((item) => isTool && item.toolDescription),
commonInputs: inputs.filter((item) => {
if (!isTool) return true;
return !item.toolDescription;
})
};
};
const initData = useMemoizedFn(async (e: Parameters<WorkflowContextType['initData']>[0]) => {
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item })) || []);
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []);
const chatConfig = e.chatConfig;
if (chatConfig) {
setAppDetail((state) => ({
...state,
chatConfig
}));
}
});
/* debug */
const [workflowDebugData, setWorkflowDebugData] = useState<DebugDataType>();
const onNextNodeDebug = useCallback(
async (debugData = workflowDebugData) => {
if (!debugData) return;
// 1. Cancel node selected status and debugResult.showStatus
setNodes((state) =>
state.map((node) => ({
...node,
selected: false,
data: {
...node.data,
debugResult: node.data.debugResult
? {
...node.data.debugResult,
showResult: false,
isExpired: true
}
: undefined
}
}))
);
// 2. Set isEntry field and get entryNodes
const runtimeNodes = debugData.runtimeNodes.map((item) => ({
...item,
isEntry: debugData.nextRunNodes.some((node) => node.nodeId === item.nodeId)
}));
const entryNodes = runtimeNodes.filter((item) => item.isEntry);
const runtimeNodeStatus: Record<string, string> = entryNodes
.map((node) => {
const status = checkNodeRunStatus({
node,
runtimeEdges: debugData?.runtimeEdges || []
});
return {
nodeId: node.nodeId,
status
};
})
.reduce(
(acc, cur) => ({
...acc,
[cur.nodeId]: cur.status
}),
{}
);
// 3. Set entry node status to running
entryNodes.forEach((node) => {
if (runtimeNodeStatus[node.nodeId] !== 'wait') {
onChangeNode({
nodeId: node.nodeId,
type: 'attr',
key: 'debugResult',
value: defaultRunningStatus
});
}
});
try {
// 4. Run one step
const { finishedEdges, finishedNodes, nextStepRunNodes, flowResponses } =
await postWorkflowDebug({
nodes: runtimeNodes,
edges: debugData.runtimeEdges,
variables: {},
appId,
pluginId
});
// console.log({ finishedEdges, finishedNodes, nextStepRunNodes, flowResponses });
// 5. Store debug result
const newStoreDebugData = {
runtimeNodes: finishedNodes,
// edges need to save status
runtimeEdges: finishedEdges.map((edge) => {
const oldEdge = debugData.runtimeEdges.find(
(item) => item.source === edge.source && item.target === edge.target
);
const status =
oldEdge?.status && oldEdge.status !== RuntimeEdgeStatusEnum.waiting
? oldEdge.status
: edge.status;
return {
...edge,
status
};
}),
nextRunNodes: nextStepRunNodes
};
setWorkflowDebugData(newStoreDebugData);
// 6. selected entry node and Update entry node debug result
setNodes((state) =>
state.map((node) => {
const isEntryNode = entryNodes.some((item) => item.nodeId === node.data.nodeId);
if (!isEntryNode || runtimeNodeStatus[node.data.nodeId] === 'wait') return node;
const result = flowResponses.find((item) => item.nodeId === node.data.nodeId);
if (runtimeNodeStatus[node.data.nodeId] === 'skip') {
return {
...node,
selected: isEntryNode,
data: {
...node.data,
debugResult: {
status: 'skipped',
showResult: true,
isExpired: false
}
}
};
}
return {
...node,
selected: isEntryNode,
data: {
...node.data,
debugResult: {
status: 'success',
response: result,
showResult: true,
isExpired: false
}
}
};
})
);
// Check for an empty response
if (flowResponses.length === 0 && nextStepRunNodes.length > 0) {
onNextNodeDebug(newStoreDebugData);
}
} catch (error) {
entryNodes.forEach((node) => {
onChangeNode({
nodeId: node.nodeId,
type: 'attr',
key: 'debugResult',
value: {
status: 'failed',
message: getErrText(error, 'Debug failed'),
showResult: true
}
});
});
console.log(error);
}
},
[appId, onChangeNode, pluginId, setNodes, workflowDebugData]
);
const onStopNodeDebug = useCallback(() => {
setWorkflowDebugData(undefined);
setNodes((state) =>
state.map((node) => ({
...node,
selected: false,
data: {
...node.data,
debugResult: undefined
}
}))
);
}, [setNodes]);
const onStartNodeDebug = useCallback(
async ({
entryNodeId,
runtimeNodes,
runtimeEdges
}: {
entryNodeId: string;
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
}) => {
const data = {
runtimeNodes,
runtimeEdges,
nextRunNodes: runtimeNodes.filter((node) => node.nodeId === entryNodeId)
};
onStopNodeDebug();
setWorkflowDebugData(data);
onNextNodeDebug(data);
},
[onNextNodeDebug, onStopNodeDebug]
);
/* Version histories */
const [isShowVersionHistories, setIsShowVersionHistories] = useState(false);
/* event bus */
useEffect(() => {
eventBus.on(EventNameEnum.requestWorkflowStore, () => {
eventBus.emit(EventNameEnum.receiveWorkflowStore, {
nodes
});
});
return () => {
eventBus.off(EventNameEnum.requestWorkflowStore);
};
}, [nodes]);
return (
<WorkflowContext.Provider
value={{
appId,
reactFlowWrapper,
...value,
// node
nodes,
setNodes,
onNodesChange,
nodeList,
hasToolNode,
hoverNodeId,
setHoverNodeId,
onUpdateNodeError,
onResetNode,
onChangeNode,
// edge
edges,
setEdges,
hoverEdgeId,
setHoverEdgeId,
onEdgesChange,
connectingEdge,
setConnectingEdge,
onDelEdge,
// function
onFixView,
splitToolInputs,
initData,
// debug
workflowDebugData,
onNextNodeDebug,
onStartNodeDebug,
onStopNodeDebug,
// version history
isShowVersionHistories,
setIsShowVersionHistories
}}
>
{children}
</WorkflowContext.Provider>
);
};
export default WorkflowContextProvider;
type GetWorkflowStoreResponse = {
nodes: Node<FlowNodeItemType>[];
};
export const getWorkflowStore = () =>
new Promise<GetWorkflowStoreResponse>((resolve) => {
eventBus.on(EventNameEnum.receiveWorkflowStore, (data: GetWorkflowStoreResponse) => {
resolve(data);
eventBus.off(EventNameEnum.receiveWorkflowStore);
});
eventBus.emit(EventNameEnum.requestWorkflowStore);
});

View File

@@ -1,69 +0,0 @@
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { FlowNodeItemType, StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { type Node, type Edge } from 'reactflow';
export const uiWorkflow2StoreWorkflow = ({
nodes,
edges
}: {
nodes: Node<FlowNodeItemType, string | undefined>[];
edges: Edge<any>[];
}) => {
const formatNodes: StoreNodeItemType[] = nodes.map((item) => ({
nodeId: item.data.nodeId,
name: item.data.name,
intro: item.data.intro,
avatar: item.data.avatar,
flowNodeType: item.data.flowNodeType,
showStatus: item.data.showStatus,
position: item.position,
version: item.data.version,
inputs: item.data.inputs,
outputs: item.data.outputs,
pluginId: item.data.pluginId,
nodeVersion: item.data.nodeVersion
}));
// get all handle
const reactFlowViewport = document.querySelector('.react-flow__viewport');
// Gets the value of data-handleid on all elements below it whose data-handleid is not empty
const handleList =
reactFlowViewport?.querySelectorAll('[data-handleid]:not([data-handleid=""])') || [];
const handleIdList = Array.from(handleList).map(
(item) => item.getAttribute('data-handleid') || ''
);
const formatEdges: StoreEdgeItemType[] = edges
.map((item) => ({
source: item.source,
target: item.target,
sourceHandle: item.sourceHandle || '',
targetHandle: item.targetHandle || ''
}))
.filter((item) => item.sourceHandle && item.targetHandle)
.filter(
// Filter out edges that do not have both sourceHandle and targetHandle
(item) => handleIdList.includes(item.sourceHandle) && handleIdList.includes(item.targetHandle)
);
return {
nodes: formatNodes,
edges: formatEdges
};
};
export const filterExportModules = (modules: StoreNodeItemType[]) => {
modules.forEach((module) => {
// dataset - remove select dataset value
if (module.flowNodeType === FlowNodeTypeEnum.datasetSearchNode) {
module.inputs.forEach((item) => {
if (item.key === NodeInputKeyEnum.datasetSelectList) {
item.value = [];
}
});
}
});
return JSON.stringify(modules, null, 2);
};

View File

@@ -41,6 +41,7 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import MyBox from '@fastgpt/web/components/common/MyBox';
type EditProps = EditApiKeyProps & { _id?: string };
const defaultEditData: EditProps = {
@@ -84,8 +85,14 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
}, []);
return (
<Flex flexDirection={'column'} h={'100%'} position={'relative'}>
<Box display={['block', 'flex']} py={[0, 3]} px={5} alignItems={'center'}>
<MyBox
isLoading={isGetting || isDeleting}
display={'flex'}
flexDirection={'column'}
h={'100%'}
position={'relative'}
>
<Box display={['block', 'flex']} alignItems={'center'}>
<Box flex={1}>
<Flex alignItems={'flex-end'}>
<Box color={'myGray.900'} fontSize={'lg'}>
@@ -103,7 +110,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
</Link>
)}
</Flex>
<Box fontSize={'xs'} color={'myGray.600'}>
<Box fontSize={'mini'} color={'myGray.600'}>
{tips}
</Box>
</Box>
@@ -140,7 +147,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
</Button>
</Box>
</Box>
<TableContainer mt={2} position={'relative'} minH={'300px'}>
<TableContainer mt={3} position={'relative'} minH={'300px'}>
<Table>
<Thead>
<Tr>
@@ -225,7 +232,6 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
))}
</Tbody>
</Table>
<Loading loading={isGetting || isDeleting} fixed={false} />
</TableContainer>
{!!editData && (
@@ -279,7 +285,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
</Button>
</ModalFooter>
</MyModal>
</Flex>
</MyBox>
);
};

View File

@@ -40,7 +40,7 @@ const ConfigPerModal = ({
<ModalBody>
<HStack>
<Avatar src={avatar} w={'1.75rem'} />
<Box fontSize={'lg'}>{name}</Box>
<Box>{name}</Box>
</HStack>
<Box mt={6}>
<Box fontSize={'sm'}>{t('permission.Default permission')}</Box>

View File

@@ -3,7 +3,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useContextSelector } from 'use-context-selector';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import RowTabs from '../../../../common/Rowtabs';
import RowTabs from '@fastgpt/web/components/common/Tabs/RowTabs';
import { useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';