perf: workflow context split (#3083)

* perf: workflow context split

* perf: context
This commit is contained in:
Archer
2024-11-07 10:02:22 +08:00
committed by archer
parent 78ef74f902
commit f58dba2eda
34 changed files with 1193 additions and 997 deletions

View File

@@ -29,6 +29,11 @@ import { useDebounceEffect } from 'ahooks';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import SaveButton from '../Workflow/components/SaveButton'; import SaveButton from '../Workflow/components/SaveButton';
import PublishHistories from '../PublishHistoriesSlider'; import PublishHistories from '../PublishHistoriesSlider';
import {
WorkflowActionContext,
WorkflowInitContext
} from '../WorkflowComponents/context/workflowInitContext';
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
const Header = () => { const Header = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -44,20 +49,24 @@ const Header = () => {
onClose: onCloseBackConfirm onClose: onCloseBackConfirm
} = useDisclosure(); } = useDisclosure();
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
const { const {
flowData2StoreData, flowData2StoreData,
flowData2StoreDataAndCheck, flowData2StoreDataAndCheck,
setWorkflowTestData, setWorkflowTestData,
setShowHistoryModal,
showHistoryModal,
nodes,
edges,
past, past,
future, future,
setPast, setPast,
onSwitchTmpVersion, onSwitchTmpVersion,
onSwitchCloudVersion onSwitchCloudVersion
} = useContextSelector(WorkflowContext, (v) => v); } = useContextSelector(WorkflowContext, (v) => v);
const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal);
const setShowHistoryModal = useContextSelector(
WorkflowEventContext,
(v) => v.setShowHistoryModal
);
const { lastAppListRouteType } = useSystemStore(); const { lastAppListRouteType } = useSystemStore();
const [isPublished, setIsPublished] = useState(false); const [isPublished, setIsPublished] = useState(false);

View File

@@ -29,6 +29,11 @@ import { useDebounceEffect } from 'ahooks';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import SaveButton from './components/SaveButton'; import SaveButton from './components/SaveButton';
import PublishHistories from '../PublishHistoriesSlider'; import PublishHistories from '../PublishHistoriesSlider';
import {
WorkflowActionContext,
WorkflowInitContext
} from '../WorkflowComponents/context/workflowInitContext';
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
const Header = () => { const Header = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -48,20 +53,23 @@ const Header = () => {
onClose: onCloseBackConfirm onClose: onCloseBackConfirm
} = useDisclosure(); } = useDisclosure();
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
const { const {
flowData2StoreData, flowData2StoreData,
flowData2StoreDataAndCheck, flowData2StoreDataAndCheck,
setWorkflowTestData, setWorkflowTestData,
setShowHistoryModal,
showHistoryModal,
nodes,
edges,
past, past,
future, future,
setPast, setPast,
onSwitchTmpVersion, onSwitchTmpVersion,
onSwitchCloudVersion onSwitchCloudVersion
} = useContextSelector(WorkflowContext, (v) => v); } = useContextSelector(WorkflowContext, (v) => v);
const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal);
const setShowHistoryModal = useContextSelector(
WorkflowEventContext,
(v) => v.setShowHistoryModal
);
const { lastAppListRouteType } = useSystemStore(); const { lastAppListRouteType } = useSystemStore();

View File

@@ -14,7 +14,7 @@ import { cloneDeep } from 'lodash';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import Flow from '../WorkflowComponents/Flow'; import Flow from '../WorkflowComponents/Flow';
import { ReactFlowProvider } from 'reactflow'; import { ReactFlowCustomProvider } from '../WorkflowComponents/context/index';
const Logs = dynamic(() => import('../Logs/index')); const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish')); const PublishChannel = dynamic(() => import('../Publish'));
@@ -67,11 +67,9 @@ const WorkflowEdit = () => {
const Render = () => { const Render = () => {
return ( return (
<ReactFlowProvider> <ReactFlowCustomProvider templates={appSystemModuleTemplates}>
<WorkflowContextProvider basicNodeTemplates={appSystemModuleTemplates}>
<WorkflowEdit /> <WorkflowEdit />
</WorkflowContextProvider> </ReactFlowCustomProvider>
</ReactFlowProvider>
); );
}; };

View File

@@ -50,6 +50,7 @@ import { useUserStore } from '@/web/support/user/useUserStore';
import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart'; import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart';
import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd'; import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { WorkflowActionContext } from '../context/workflowInitContext';
type ModuleTemplateListProps = { type ModuleTemplateListProps = {
isOpen: boolean; isOpen: boolean;
@@ -79,10 +80,10 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
const [parentId, setParentId] = useState<ParentIdType>(''); const [parentId, setParentId] = useState<ParentIdType>('');
const [searchKey, setSearchKey] = useState(''); const [searchKey, setSearchKey] = useState('');
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const { basicNodeTemplates, hasToolNode, nodeList, appId } = useContextSelector( const basicNodeTemplates = useContextSelector(WorkflowContext, (v) => v.basicNodeTemplates);
WorkflowContext, const hasToolNode = useContextSelector(WorkflowContext, (v) => v.hasToolNode);
(v) => v const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
); const appId = useContextSelector(WorkflowContext, (v) => v.appId);
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, { const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
manual: !feConfigs.isPlus manual: !feConfigs.isPlus
@@ -217,7 +218,6 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
} }
); );
const Render = useMemo(() => {
return ( return (
<> <>
<Box <Box
@@ -368,23 +368,6 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
</MyBox> </MyBox>
</> </>
); );
}, [
isOpen,
onClose,
isLoading,
t,
templateType,
feConfigs.systemPluginCourseUrl,
searchKey,
parentId,
paths,
onUpdateParentId,
templates,
loadNodeTemplates,
router
]);
return Render;
}; };
export default React.memo(NodeTemplatesModal); export default React.memo(NodeTemplatesModal);
@@ -403,9 +386,11 @@ const RenderList = React.memo(function RenderList({
const isSystemPlugin = type === TemplateTypeEnum.systemPlugin; const isSystemPlugin = type === TemplateTypeEnum.systemPlugin;
const { screenToFlowPosition } = useReactFlow(); const { screenToFlowPosition } = useReactFlow();
const { toast } = useToast();
const { reactFlowWrapper, setNodes, nodeList } = useContextSelector(WorkflowContext, (v) => v);
const { computedNewNodeName } = useWorkflowUtils(); const { computedNewNodeName } = useWorkflowUtils();
const { toast } = useToast();
const setNodes = useContextSelector(WorkflowActionContext, (v) => v.setNodes);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const formatTemplates = useMemo<NodeTemplateListType>(() => { const formatTemplates = useMemo<NodeTemplateListType>(() => {
const copy: NodeTemplateListType = cloneDeep(workflowNodeTemplateList); const copy: NodeTemplateListType = cloneDeep(workflowNodeTemplateList);
@@ -426,8 +411,6 @@ const RenderList = React.memo(function RenderList({
template: NodeTemplateListItemType; template: NodeTemplateListItemType;
position: XYPosition; position: XYPosition;
}) => { }) => {
if (!reactFlowWrapper?.current) return;
// Load template node // Load template node
const templateNode = await (async () => { const templateNode = await (async () => {
try { try {
@@ -539,16 +522,7 @@ const RenderList = React.memo(function RenderList({
return newState; return newState;
}); });
}, },
[ [screenToFlowPosition, nodeList, computedNewNodeName, t, setNodes, setLoading, toast]
reactFlowWrapper,
screenToFlowPosition,
nodeList,
computedNewNodeName,
t,
setNodes,
setLoading,
toast
]
); );
const gridStyle = useMemo(() => { const gridStyle = useMemo(() => {
@@ -571,7 +545,6 @@ const RenderList = React.memo(function RenderList({
}; };
}, [type]); }, [type]);
const Render = useMemo(() => {
return templates.length === 0 ? ( return templates.length === 0 ? (
<EmptyTip text={t('app:module.No Modules')} /> <EmptyTip text={t('app:module.No Modules')} />
) : ( ) : (
@@ -686,18 +659,4 @@ const RenderList = React.memo(function RenderList({
</Box> </Box>
</Box> </Box>
); );
}, [
feConfigs.systemTitle,
formatTemplates,
gridStyle,
isPc,
isSystemPlugin,
onAddNode,
onClose,
setParentId,
t,
templates.length
]);
return Render;
}); });

View File

@@ -6,12 +6,15 @@ import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/w
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context'; import { WorkflowContext } from '../../context';
import { useThrottleEffect } from 'ahooks'; import { useThrottleEffect } from 'ahooks';
import { WorkflowActionContext, WorkflowInitContext } from '../../context/workflowInitContext';
import { WorkflowEventContext } from '../../context/workflowEventContext';
const ButtonEdge = (props: EdgeProps) => { const ButtonEdge = (props: EdgeProps) => {
const { nodes, nodeList, onEdgesChange, workflowDebugData, hoverEdgeId } = useContextSelector( const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
WorkflowContext, const onEdgesChange = useContextSelector(WorkflowActionContext, (v) => v.onEdgesChange);
(v) => v const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
); const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData);
const hoverEdgeId = useContextSelector(WorkflowEventContext, (v) => v.hoverEdgeId);
const { const {
id, id,

View File

@@ -7,30 +7,29 @@ import { CommentNode } from '@fastgpt/global/core/workflow/template/system/comme
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context'; import { WorkflowContext } from '../../context';
import { useReactFlow } from 'reactflow'; import { useReactFlow } from 'reactflow';
import { WorkflowActionContext } from '../../context/workflowInitContext';
import { WorkflowEventContext } from '../../context/workflowEventContext';
type ContextMenuProps = { const ContextMenu = () => {
top: number;
left: number;
};
const ContextMenu = ({ top, left }: ContextMenuProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const setNodes = useContextSelector(WorkflowContext, (ctx) => ctx.setNodes); const setNodes = useContextSelector(WorkflowActionContext, (v) => v.setNodes);
const setMenu = useContextSelector(WorkflowContext, (ctx) => ctx.setMenu); const menu = useContextSelector(WorkflowEventContext, (v) => v.menu);
const setMenu = useContextSelector(WorkflowEventContext, (ctx) => ctx.setMenu);
const { screenToFlowPosition } = useReactFlow(); const { screenToFlowPosition } = useReactFlow();
const newNode = nodeTemplate2FlowNode({ const newNode = nodeTemplate2FlowNode({
template: CommentNode, template: CommentNode,
position: screenToFlowPosition({ x: left, y: top }), position: screenToFlowPosition({ x: menu?.left ?? 0, y: menu?.top ?? 0 }),
t t
}); });
return ( return (
!!menu && (
<Box position="relative"> <Box position="relative">
<Box <Box
position="absolute" position="absolute"
top={`${top - 6}px`} top={`${menu.top - 6}px`}
left={`${left + 10}px`} left={`${menu.left + 10}px`}
width={0} width={0}
height={0} height={0}
borderLeft="6px solid transparent" borderLeft="6px solid transparent"
@@ -41,8 +40,8 @@ const ContextMenu = ({ top, left }: ContextMenuProps) => {
/> />
<Flex <Flex
position={'absolute'} position={'absolute'}
top={top} top={menu.top}
left={left} left={menu.left}
bg={'white'} bg={'white'}
w={'120px'} w={'120px'}
height={9} height={9}
@@ -77,6 +76,7 @@ const ContextMenu = ({ top, left }: ContextMenuProps) => {
</Box> </Box>
</Flex> </Flex>
</Box> </Box>
)
); );
}; };

View File

@@ -15,8 +15,9 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import styles from './index.module.scss'; import styles from './index.module.scss';
import { maxZoom, minZoom } from '../index'; import { maxZoom, minZoom } from '../../constants';
import { useKeyPress } from 'ahooks'; import { useKeyPress } from 'ahooks';
import { WorkflowEventContext } from '../../context/workflowEventContext';
const buttonStyle = { const buttonStyle = {
border: 'none', border: 'none',
@@ -27,16 +28,20 @@ const buttonStyle = {
const FlowController = React.memo(function FlowController() { const FlowController = React.memo(function FlowController() {
const { fitView, zoomIn, zoomOut } = useReactFlow(); const { fitView, zoomIn, zoomOut } = useReactFlow();
const { zoom } = useViewport(); const { zoom } = useViewport();
const { const undo = useContextSelector(WorkflowContext, (v) => v.undo);
undo, const redo = useContextSelector(WorkflowContext, (v) => v.redo);
redo, const canRedo = useContextSelector(WorkflowContext, (v) => v.canRedo);
canRedo, const canUndo = useContextSelector(WorkflowContext, (v) => v.canUndo);
canUndo, const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
workflowControlMode, const workflowControlMode = useContextSelector(
setWorkflowControlMode, WorkflowEventContext,
mouseInCanvas, (v) => v.workflowControlMode
nodeList );
} = useContextSelector(WorkflowContext, (v) => v); const setWorkflowControlMode = useContextSelector(
WorkflowEventContext,
(v) => v.setWorkflowControlMode
);
const mouseInCanvas = useContextSelector(WorkflowEventContext, (v) => v.mouseInCanvas);
const { t } = useTranslation(); const { t } = useTranslation();
const isMac = !window ? false : window.navigator.userAgent.toLocaleLowerCase().includes('mac'); const isMac = !window ? false : window.navigator.userAgent.toLocaleLowerCase().includes('mac');

View File

@@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { Box, StackProps, HStack } from '@chakra-ui/react'; import { Box, StackProps, HStack } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
const IOTitle = ({ text, ...props }: { text?: 'Input' | 'Output' | string } & StackProps) => { const IOTitle = ({ text, ...props }: { text?: 'Input' | 'Output' | string } & StackProps) => {
return ( return (

View File

@@ -1,7 +1,7 @@
import { storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils'; import { storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { useCallback, useState, useMemo, useEffect } from 'react'; import { useCallback, useState, useMemo } from 'react';
import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils'; import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
@@ -27,13 +27,14 @@ import {
} from '@fastgpt/global/core/workflow/constants'; } from '@fastgpt/global/core/workflow/constants';
import { checkInputIsReference } from '@fastgpt/global/core/workflow/utils'; import { checkInputIsReference } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../../context'; import { WorkflowContext } from '../../context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppContext } from '../../../context'; import { AppContext } from '../../../context';
import { VariableInputItem } from '@/components/core/chat/ChatContainer/ChatBox/components/VariableInput'; import { VariableInputItem } from '@/components/core/chat/ChatContainer/ChatBox/components/VariableInput';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import MyTextarea from '@/components/common/Textarea/MyTextarea'; import MyTextarea from '@/components/common/Textarea/MyTextarea';
import { WorkflowActionContext } from '../../context/workflowInitContext';
const MyRightDrawer = dynamic( const MyRightDrawer = dynamic(
() => import('@fastgpt/web/components/common/MyDrawer/MyRightDrawer') () => import('@fastgpt/web/components/common/MyDrawer/MyRightDrawer')
@@ -49,9 +50,10 @@ export const useDebug = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes); const setNodes = useContextSelector(WorkflowActionContext, (v) => v.setNodes);
const getNodes = useContextSelector(WorkflowActionContext, (v) => v.getNodes);
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError); const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const onStartNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStartNodeDebug); const onStartNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStartNodeDebug);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail); const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
@@ -76,7 +78,7 @@ export const useDebug = () => {
const [runtimeEdges, setRuntimeEdges] = useState<RuntimeEdgeItemType[]>(); const [runtimeEdges, setRuntimeEdges] = useState<RuntimeEdgeItemType[]>();
const flowData2StoreDataAndCheck = useCallback(async () => { const flowData2StoreDataAndCheck = useCallback(async () => {
const { nodes } = await getWorkflowStore(); const nodes = getNodes();
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges }); const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
if (!checkResults) { if (!checkResults) {

View File

@@ -5,14 +5,18 @@ import { useTranslation } from 'next-i18next';
import { Node, useKeyPress } from 'reactflow'; import { Node, useKeyPress } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../../context';
import { useWorkflowUtils } from './useUtils'; import { useWorkflowUtils } from './useUtils';
import { useKeyPress as useKeyPressEffect } from 'ahooks'; import { useKeyPress as useKeyPressEffect } from 'ahooks';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { WorkflowActionContext } from '../../context/workflowInitContext';
import { WorkflowEventContext } from '../../context/workflowEventContext';
export const useKeyboard = () => { export const useKeyboard = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { setNodes, mouseInCanvas } = useContextSelector(WorkflowContext, (v) => v); const getNodes = useContextSelector(WorkflowActionContext, (v) => v.getNodes);
const setNodes = useContextSelector(WorkflowActionContext, (v) => v.setNodes);
const mouseInCanvas = useContextSelector(WorkflowEventContext, (v) => v.mouseInCanvas);
const { copyData } = useCopyData(); const { copyData } = useCopyData();
const { computedNewNodeName } = useWorkflowUtils(); const { computedNewNodeName } = useWorkflowUtils();
@@ -33,14 +37,14 @@ export const useKeyboard = () => {
const onCopy = useCallback(async () => { const onCopy = useCallback(async () => {
if (hasInputtingElement()) return; if (hasInputtingElement()) return;
const { nodes } = await getWorkflowStore(); const nodes = getNodes();
const selectedNodes = nodes.filter( const selectedNodes = nodes.filter(
(node) => node.selected && !node.data?.isError && node.data?.unique !== true (node) => node.selected && !node.data?.isError && node.data?.unique !== true
); );
if (selectedNodes.length === 0) return; if (selectedNodes.length === 0) return;
copyData(JSON.stringify(selectedNodes), t('common:core.workflow.Copy node')); copyData(JSON.stringify(selectedNodes), t('common:core.workflow.Copy node'));
}, [copyData, hasInputtingElement, t]); }, [copyData, getNodes, hasInputtingElement, t]);
const onParse = useCallback(async () => { const onParse = useCallback(async () => {
if (hasInputtingElement()) return; if (hasInputtingElement()) return;

View File

@@ -25,12 +25,16 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context'; import { WorkflowContext } from '../../context';
import { THelperLine } from '@fastgpt/global/core/workflow/type'; import { THelperLine } from '@fastgpt/global/core/workflow/type';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useMemoizedFn } from 'ahooks'; import { useDebounceEffect, useMemoizedFn } from 'ahooks';
import { import {
Input_Template_Node_Height, Input_Template_Node_Height,
Input_Template_Node_Width Input_Template_Node_Width
} from '@fastgpt/global/core/workflow/template/input'; } from '@fastgpt/global/core/workflow/template/input';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { WorkflowActionContext, WorkflowInitContext } from '../../context/workflowInitContext';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { AppContext } from '../../../context';
import { WorkflowEventContext } from '../../context/workflowEventContext';
/* /*
Compute helper lines for snapping nodes to each other Compute helper lines for snapping nodes to each other
@@ -271,21 +275,22 @@ export const useWorkflow = () => {
const { toast } = useToast(); const { toast } = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
const { isDowningCtrl } = useKeyboard(); const appDetail = useContextSelector(AppContext, (e) => e.appDetail);
const {
setConnectingEdge, const nodes = useContextSelector(WorkflowInitContext, (state) => state.nodes);
edges, const onNodesChange = useContextSelector(WorkflowActionContext, (state) => state.onNodesChange);
nodes, const edges = useContextSelector(WorkflowActionContext, (state) => state.edges);
nodeList, const setEdges = useContextSelector(WorkflowActionContext, (v) => v.setEdges);
onNodesChange, const onEdgesChange = useContextSelector(WorkflowActionContext, (v) => v.onEdgesChange);
setEdges, const { setConnectingEdge, nodeList, onChangeNode, pushPastSnapshot } = useContextSelector(
onChangeNode, WorkflowContext,
onEdgesChange, (v) => v
setHoverEdgeId, );
setMenu const setHoverEdgeId = useContextSelector(WorkflowEventContext, (v) => v.setHoverEdgeId);
} = useContextSelector(WorkflowContext, (v) => v); const setMenu = useContextSelector(WorkflowEventContext, (v) => v.setMenu);
const { getIntersectingNodes } = useReactFlow(); const { getIntersectingNodes } = useReactFlow();
const { isDowningCtrl } = useKeyboard();
// Loop node size and position // Loop node size and position
const resetParentNodeSizeAndPosition = useMemoizedFn((rect: Rect, parentId: string) => { const resetParentNodeSizeAndPosition = useMemoizedFn((rect: Rect, parentId: string) => {
@@ -330,6 +335,7 @@ export const useWorkflow = () => {
const [helperLineVertical, setHelperLineVertical] = useState<THelperLine>(); const [helperLineVertical, setHelperLineVertical] = useState<THelperLine>();
const checkNodeHelpLine = useMemoizedFn((change: NodeChange, nodes: Node[]) => { const checkNodeHelpLine = useMemoizedFn((change: NodeChange, nodes: Node[]) => {
requestAnimationFrame(() => {
const positionChange = change.type === 'position' && change.dragging ? change : undefined; const positionChange = change.type === 'position' && change.dragging ? change : undefined;
if (positionChange?.position) { if (positionChange?.position) {
@@ -366,6 +372,7 @@ export const useWorkflow = () => {
setHelperLineVertical(undefined); setHelperLineVertical(undefined);
} }
}); });
});
// Check if a node is placed on top of a loop node // Check if a node is placed on top of a loop node
const checkNodeOverLoopNode = useMemoizedFn((node: Node) => { const checkNodeOverLoopNode = useMemoizedFn((node: Node) => {
@@ -642,6 +649,23 @@ export const useWorkflow = () => {
setMenu(null); setMenu(null);
}, [setMenu]); }, [setMenu]);
// Watch
// Auto save snapshot
useDebounceEffect(
() => {
if (nodes.length === 0 || !appDetail.chatConfig) return;
pushPastSnapshot({
pastNodes: nodes,
pastEdges: edges,
customTitle: formatTime2YMDHMS(new Date()),
chatConfig: appDetail.chatConfig
});
},
[nodes, edges, appDetail.chatConfig],
{ wait: 500 }
);
return { return {
handleNodesChange, handleNodesChange,
handleEdgeChange, handleEdgeChange,

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { useEffect, useRef } from 'react';
import ReactFlow, { NodeProps, ReactFlowProvider, SelectionMode } from 'reactflow'; import ReactFlow, { NodeProps, ReactFlowProvider, SelectionMode } from 'reactflow';
import { Box, IconButton, useDisclosure } from '@chakra-ui/react'; import { Box, IconButton, useDisclosure } from '@chakra-ui/react';
import { SmallCloseIcon } from '@chakra-ui/icons'; import { SmallCloseIcon } from '@chakra-ui/icons';
@@ -11,16 +11,15 @@ import NodeTemplatesModal from './NodeTemplatesModal';
import 'reactflow/dist/style.css'; import 'reactflow/dist/style.css';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d'; import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import { connectionLineStyle, defaultEdgeOptions } from '../constants'; import { connectionLineStyle, defaultEdgeOptions, maxZoom, minZoom } from '../constants';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context'; import { WorkflowContext } from '../context';
import { useWorkflow } from './hooks/useWorkflow'; import { useWorkflow } from './hooks/useWorkflow';
import HelperLines from './components/HelperLines'; import HelperLines from './components/HelperLines';
import FlowController from './components/FlowController'; import FlowController from './components/FlowController';
import ContextMenu from './components/ContextMenu'; import ContextMenu from './components/ContextMenu';
import { WorkflowActionContext, WorkflowInitContext } from '../context/workflowInitContext';
export const minZoom = 0.1; import { WorkflowEventContext } from '../context/workflowEventContext';
export const maxZoom = 1.5;
const NodeSimple = dynamic(() => import('./nodes/NodeSimple')); const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
const nodeTypes: Record<FlowNodeTypeEnum, any> = { const nodeTypes: Record<FlowNodeTypeEnum, any> = {
@@ -66,9 +65,12 @@ const edgeTypes = {
}; };
const Workflow = () => { const Workflow = () => {
const { nodes, edges, menu, reactFlowWrapper, workflowControlMode } = useContextSelector( const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
WorkflowContext, const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
(v) => v const reactFlowWrapper = useContextSelector(WorkflowEventContext, (v) => v.reactFlowWrapper);
const workflowControlMode = useContextSelector(
WorkflowEventContext,
(v) => v.workflowControlMode
); );
const { const {
@@ -125,7 +127,7 @@ const Workflow = () => {
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} /> <NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
</> </>
{menu && <ContextMenu {...menu} />} <ContextMenu />
<ReactFlow <ReactFlow
ref={reactFlowWrapper} ref={reactFlowWrapper}
fitView fitView

View File

@@ -49,6 +49,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getEditorVariables } from '../../../utils'; import { getEditorVariables } from '../../../utils';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor'; import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { WorkflowActionContext } from '../../../context/workflowInitContext';
const CurlImportModal = dynamic(() => import('./CurlImportModal')); const CurlImportModal = dynamic(() => import('./CurlImportModal'));
const defaultFormBody = { const defaultFormBody = {
@@ -80,9 +81,10 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const { appDetail } = useContextSelector(AppContext, (v) => v); const { appDetail } = useContextSelector(AppContext, (v) => v);
const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure(); const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure();
@@ -256,8 +258,9 @@ export function RenderHttpProps({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedTab, setSelectedTab] = useState(TabEnum.params); const [selectedTab, setSelectedTab] = useState(TabEnum.params);
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const { appDetail } = useContextSelector(AppContext, (v) => v); const { appDetail } = useContextSelector(AppContext, (v) => v);

View File

@@ -53,6 +53,7 @@ const NodePluginConfig = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
[chatConfig, setAppDetail] [chatConfig, setAppDetail]
); );
const Render = useMemo(() => {
return ( return (
<NodeCard <NodeCard
selected={selected} selected={selected}
@@ -71,6 +72,9 @@ const NodePluginConfig = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
</Container> </Container>
</NodeCard> </NodeCard>
); );
}, [componentsProps, data, selected]);
return Render;
}; };
export default React.memo(NodePluginConfig); export default React.memo(NodePluginConfig);
@@ -116,8 +120,10 @@ function Instruction({ chatConfig: { instruction }, setAppDetail }: ComponentPro
function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) { function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const pluginInputNode = nodes.find((item) => item.type === FlowNodeTypeEnum.pluginInput)!; const pluginInputNode = nodeList.find(
(item) => item.flowNodeType === FlowNodeTypeEnum.pluginInput
)!;
return ( return (
<> <>
@@ -137,7 +143,7 @@ function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: Co
// Dynamic add or delete userFilesInput // Dynamic add or delete userFilesInput
const canUploadFiles = e.canSelectFile || e.canSelectImg; const canUploadFiles = e.canSelectFile || e.canSelectImg;
const repeatKey = pluginInputNode?.data.outputs.find( const repeatKey = pluginInputNode?.outputs.find(
(item) => item.key === userFilesInput.key (item) => item.key === userFilesInput.key
); );
if (canUploadFiles) { if (canUploadFiles) {

View File

@@ -29,7 +29,8 @@ type ComponentProps = {
}; };
const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => { const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v); const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const setAppDetail = useContextSelector(AppContext, (v) => v.setAppDetail);
const chatConfig = useMemo<AppChatConfigType>(() => { const chatConfig = useMemo<AppChatConfigType>(() => {
return getAppChatConfig({ return getAppChatConfig({
@@ -47,6 +48,7 @@ const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
[chatConfig, setAppDetail] [chatConfig, setAppDetail]
); );
const Render = useMemo(() => {
return ( return (
<> <>
<NodeCard <NodeCard
@@ -86,6 +88,9 @@ const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
</NodeCard> </NodeCard>
</> </>
); );
}, [componentsProps, data, selected]);
return Render;
}; };
export default React.memo(NodeUserGuide); export default React.memo(NodeUserGuide);
@@ -218,8 +223,10 @@ function QuestionInputGuide({ chatConfig: { chatInputGuide }, setAppDetail }: Co
function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) { function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const workflowStartNode = nodes.find((item) => item.type === FlowNodeTypeEnum.workflowStart)!; const workflowStartNode = nodeList.find(
(item) => item.flowNodeType === FlowNodeTypeEnum.workflowStart
)!;
return ( return (
<FileSelect <FileSelect
@@ -235,7 +242,7 @@ function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: Co
// Dynamic add or delete userFilesInput // Dynamic add or delete userFilesInput
const canUploadFiles = e.canSelectFile || e.canSelectImg; const canUploadFiles = e.canSelectFile || e.canSelectImg;
const repeatKey = workflowStartNode?.data.outputs.find( const repeatKey = workflowStartNode?.outputs.find(
(item) => item.key === userFilesInput.key (item) => item.key === userFilesInput.key
); );
if (canUploadFiles) { if (canUploadFiles) {

View File

@@ -34,6 +34,7 @@ import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { useCreation, useMemoizedFn } from 'ahooks'; import { useCreation, useMemoizedFn } from 'ahooks';
import { getEditorVariables } from '../../utils'; import { getEditorVariables } from '../../utils';
import { isArray } from 'lodash'; import { isArray } from 'lodash';
import { WorkflowActionContext } from '../../context/workflowInitContext';
const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => { const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { inputs = [], nodeId } = data; const { inputs = [], nodeId } = data;
@@ -42,7 +43,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail); const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const edges = useContextSelector(WorkflowContext, (v) => v.edges); const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
const menuList = useRef([ const menuList = useRef([
{ {
@@ -263,6 +264,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
} }
); );
const Render = useMemo(() => {
return ( return (
<NodeCard selected={selected} maxW={'1000px'} {...data}> <NodeCard selected={selected} maxW={'1000px'} {...data}>
<Box px={4} pb={4}> <Box px={4} pb={4}>
@@ -301,6 +303,9 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
</Box> </Box>
</NodeCard> </NodeCard>
); );
}, [ValueRender, data, onUpdateList, selected, t, updateList]);
return Render;
}; };
export default React.memo(NodeVariableUpdate); export default React.memo(NodeVariableUpdate);

View File

@@ -24,8 +24,8 @@ import MyDivider from '@fastgpt/web/components/common/MyDivider';
const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => { const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { nodeId, outputs } = data; const { nodeId, outputs } = data;
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const customGlobalVariables = useCreation(() => { const customGlobalVariables = useCreation(() => {
const globalVariables = formatEditorVariablePickerIcon( const globalVariables = formatEditorVariablePickerIcon(
@@ -62,7 +62,7 @@ const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
})), })),
[t] [t]
); );
const Render = useMemo(() => {
return ( return (
<NodeCard <NodeCard
minW={'420px'} minW={'420px'}
@@ -90,6 +90,9 @@ const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
</Container> </Container>
</NodeCard> </NodeCard>
); );
}, [customGlobalVariables, data, nodeId, outputs, selected, systemVariables, t]);
return Render;
}; };
export default React.memo(NodeStart); export default React.memo(NodeStart);

View File

@@ -5,6 +5,7 @@ import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../../context'; import { WorkflowContext } from '../../../../context';
import { WorkflowActionContext } from '../../../../context/workflowInitContext';
export const ConnectionSourceHandle = ({ export const ConnectionSourceHandle = ({
nodeId, nodeId,
@@ -13,7 +14,8 @@ export const ConnectionSourceHandle = ({
nodeId: string; nodeId: string;
isFoldNode?: boolean; isFoldNode?: boolean;
}) => { }) => {
const { connectingEdge, nodeList, edges } = useContextSelector(WorkflowContext, (ctx) => ctx); const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
const { connectingEdge, nodeList } = useContextSelector(WorkflowContext, (ctx) => ctx);
const { showSourceHandle, RightHandle, LeftHandlee, TopHandlee, BottomHandlee } = useMemo(() => { const { showSourceHandle, RightHandle, LeftHandlee, TopHandlee, BottomHandlee } = useMemo(() => {
const node = nodeList.find((node) => node.nodeId === nodeId); const node = nodeList.find((node) => node.nodeId === nodeId);
@@ -135,7 +137,8 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
}: { }: {
nodeId: string; nodeId: string;
}) { }) {
const { connectingEdge, nodeList, edges } = useContextSelector(WorkflowContext, (ctx) => ctx); const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
const { connectingEdge, nodeList } = useContextSelector(WorkflowContext, (ctx) => ctx);
const { LeftHandle, rightHandle, topHandle, bottomHandle } = useMemo(() => { const { LeftHandle, rightHandle, topHandle, bottomHandle } = useMemo(() => {
const node = nodeList.find((node) => node.nodeId === nodeId); const node = nodeList.find((node) => node.nodeId === nodeId);

View File

@@ -6,6 +6,7 @@ import { Connection, Handle, Position } from 'reactflow';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context'; import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import { WorkflowActionContext } from '../../../../context/workflowInitContext';
const handleSize = '16px'; const handleSize = '16px';
@@ -16,7 +17,7 @@ type ToolHandleProps = BoxProps & {
export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => { export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge); const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const edges = useContextSelector(WorkflowContext, (v) => v.edges); const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
const handleId = NodeOutputKeyEnum.selectedTools; const handleId = NodeOutputKeyEnum.selectedTools;
@@ -64,7 +65,7 @@ export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
export const ToolSourceHandle = () => { export const ToolSourceHandle = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges); const setEdges = useContextSelector(WorkflowActionContext, (v) => v.setEdges);
/* onConnect edge, delete tool input and switch */ /* onConnect edge, delete tool input and switch */
const onConnect = useCallback( const onConnect = useCallback(

View File

@@ -5,6 +5,11 @@ import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../../context'; import { WorkflowContext } from '../../../../context';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import {
WorkflowActionContext,
WorkflowInitContext
} from '../../../../context/workflowInitContext';
import { WorkflowEventContext } from '../../../../context/workflowEventContext';
type Props = { type Props = {
nodeId: string; nodeId: string;
@@ -24,11 +29,10 @@ const MySourceHandle = React.memo(function MySourceHandle({
highlightStyle: Record<string, any>; highlightStyle: Record<string, any>;
connectedStyle: Record<string, any>; connectedStyle: Record<string, any>;
}) { }) {
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge); const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const edges = useContextSelector(WorkflowContext, (v) => v.edges); const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
const hoverNodeId = useContextSelector(WorkflowEventContext, (v) => v.hoverNodeId);
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 node = useMemo(() => nodes.find((node) => node.data.nodeId === nodeId), [nodes, nodeId]);
const connected = edges.some((edge) => edge.sourceHandle === handleId); const connected = edges.some((edge) => edge.sourceHandle === handleId);
@@ -142,7 +146,8 @@ const MyTargetHandle = React.memo(function MyTargetHandle({
highlightStyle: Record<string, any>; highlightStyle: Record<string, any>;
connectedStyle: Record<string, any>; connectedStyle: Record<string, any>;
}) { }) {
const { connectingEdge, edges } = useContextSelector(WorkflowContext, (ctx) => ctx); const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const connected = edges.some((edge) => edge.targetHandle === handleId); const connected = edges.some((edge) => edge.targetHandle === handleId);

View File

@@ -26,6 +26,8 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useWorkflowUtils } from '../../hooks/useUtils'; import { useWorkflowUtils } from '../../hooks/useUtils';
import { WholeResponseContent } from '@/components/core/chat/components/WholeResponseModal'; import { WholeResponseContent } from '@/components/core/chat/components/WholeResponseModal';
import { getDocPath } from '@/web/common/system/doc'; import { getDocPath } from '@/web/common/system/doc';
import { WorkflowActionContext } from '../../../context/workflowInitContext';
import { WorkflowEventContext } from '../../../context/workflowEventContext';
type Props = FlowNodeItemType & { type Props = FlowNodeItemType & {
children?: React.ReactNode | React.ReactNode[] | string; children?: React.ReactNode | React.ReactNode[] | string;
@@ -68,10 +70,10 @@ const NodeCard = (props: Props) => {
customStyle customStyle
} = props; } = props;
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const setHoverNodeId = useContextSelector(WorkflowContext, (v) => v.setHoverNodeId);
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError); const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode); const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode);
const setHoverNodeId = useContextSelector(WorkflowEventContext, (v) => v.setHoverNodeId);
// custom title edit // custom title edit
const { onOpenModal: onOpenCustomTitleModal, EditModal: EditTitleModal } = useEditTitle({ const { onOpenModal: onOpenCustomTitleModal, EditModal: EditTitleModal } = useEditTitle({
@@ -391,7 +393,8 @@ const MenuRender = React.memo(function MenuRender({
const { t } = useTranslation(); const { t } = useTranslation();
const { openDebugNode, DebugInputModal } = useDebug(); const { openDebugNode, DebugInputModal } = useDebug();
const { setNodes, setEdges, onNodesChange } = useContextSelector(WorkflowContext, (v) => v); const setNodes = useContextSelector(WorkflowActionContext, (v) => v.setNodes);
const setEdges = useContextSelector(WorkflowActionContext, (v) => v.setEdges);
const { computedNewNodeName } = useWorkflowUtils(); const { computedNewNodeName } = useWorkflowUtils();
const onCopyNode = useCallback( const onCopyNode = useCallback(

View File

@@ -42,8 +42,6 @@ const InputLabel = ({ nodeId, input }: Props) => {
}, },
[input, nodeId, onChangeNode, renderTypeList] [input, nodeId, onChangeNode, renderTypeList]
); );
const RenderLabel = useMemo(() => {
const renderType = renderTypeList?.[selectedTypeIndex || 0]; const renderType = renderTypeList?.[selectedTypeIndex || 0];
return ( return (
@@ -55,9 +53,9 @@ const InputLabel = ({ nodeId, input }: Props) => {
{description && <QuestionTip ml={1} label={t(description as any)}></QuestionTip>} {description && <QuestionTip ml={1} label={t(description as any)}></QuestionTip>}
</Flex> </Flex>
{/* value type */} {/* value type */}
{[FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.fileSelect].includes( {[FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.fileSelect].includes(renderType) && (
renderType <ValueTypeLabel valueType={valueType} valueDesc={valueDesc} />
) && <ValueTypeLabel valueType={valueType} valueDesc={valueDesc} />} )}
{/* input type select */} {/* input type select */}
{renderTypeList && renderTypeList.length > 1 && ( {renderTypeList && renderTypeList.length > 1 && (
@@ -79,21 +77,6 @@ const InputLabel = ({ nodeId, input }: Props) => {
)} )}
</Flex> </Flex>
); );
}, [
description,
input.renderTypeList,
input.selectedTypeIndex,
label,
onChangeRenderType,
renderTypeList,
required,
selectedTypeIndex,
t,
valueDesc,
valueType
]);
return RenderLabel;
}; };
export default React.memo(InputLabel); export default React.memo(InputLabel);

View File

@@ -85,30 +85,22 @@ type Props = {
const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props) => { const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props) => {
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const copyInputs = useMemo( const filterInputs = useMemo(() => {
() => return flowInputList.filter((input) => {
JSON.stringify(
flowInputList.filter((input) => {
if (input.isPro && !feConfigs?.isPlus) return false; if (input.isPro && !feConfigs?.isPlus) return false;
return true; return true;
}) });
), }, [feConfigs?.isPlus, flowInputList]);
[feConfigs?.isPlus, flowInputList]
);
const filterInputs = useMemo(() => {
return JSON.parse(copyInputs) as FlowNodeInputItemType[];
}, [copyInputs]);
const memoCustomComponent = useMemo(() => CustomComponent || {}, [CustomComponent]); return (
<>
const Render = useMemo(() => { {filterInputs.map((input) => {
return filterInputs.map((input) => {
const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0]; const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0];
const isDynamic = !!input.canEdit; const isDynamic = !!input.canEdit;
const RenderComponent = (() => { const RenderComponent = (() => {
if (renderType === FlowNodeInputTypeEnum.custom && memoCustomComponent[input.key]) { if (renderType === FlowNodeInputTypeEnum.custom && CustomComponent?.[input.key]) {
return <>{memoCustomComponent[input.key]({ ...input })}</>; return <>{CustomComponent?.[input.key]({ ...input })}</>;
} }
const Component = RenderList.find((item) => item.types.includes(renderType))?.Component; const Component = RenderList.find((item) => item.types.includes(renderType))?.Component;
@@ -129,10 +121,9 @@ const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props)
)} )}
</Box> </Box>
) : null; ) : null;
}); })}
}, [filterInputs, mb, memoCustomComponent, nodeId]); </>
);
return <>{Render}</>;
}; };
export default React.memo(RenderInput); export default React.memo(RenderInput);

View File

@@ -50,8 +50,7 @@ const JsonEditor = ({ inputs = [], item, nodeId }: RenderInputProps) => {
} }
return JSON.stringify(item.value, null, 2); return JSON.stringify(item.value, null, 2);
}, [item.value]); }, [item.value]);
console.log(12121);
const Render = useMemo(() => {
return ( return (
<JSONEditor <JSONEditor
className="nowheel" className="nowheel"
@@ -66,9 +65,6 @@ const JsonEditor = ({ inputs = [], item, nodeId }: RenderInputProps) => {
variables={variables} variables={variables}
/> />
); );
}, [item.placeholder, t, update, value, variables]);
return Render;
}; };
export default React.memo(JsonEditor); export default React.memo(JsonEditor);

View File

@@ -18,6 +18,7 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context'; import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppContext } from '@/pages/app/detail/components/context'; import { AppContext } from '@/pages/app/detail/components/context';
import { WorkflowActionContext } from '../../../../../context/workflowInitContext';
const MultipleRowSelect = dynamic(() => const MultipleRowSelect = dynamic(() =>
import('@fastgpt/web/components/common/MySelect/MultipleRowSelect').then( import('@fastgpt/web/components/common/MySelect/MultipleRowSelect').then(
@@ -59,8 +60,9 @@ export const useReference = ({
valueType?: WorkflowIOValueTypeEnum; valueType?: WorkflowIOValueTypeEnum;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v); const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const { nodeList, edges } = useContextSelector(WorkflowContext, (v) => v); const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
// 获取可选的变量列表 // 获取可选的变量列表
const referenceList = useMemo(() => { const referenceList = useMemo(() => {
@@ -319,7 +321,7 @@ const MultipleReferenceSelector = ({
popDirection={popDirection} popDirection={popDirection}
/> />
); );
}, [getSelectValue, list, onSelect, placeholder, popDirection, t, value]); }, [getSelectValue, list, onSelect, placeholder, popDirection, value]);
return ArraySelector; return ArraySelector;
}; };

View File

@@ -7,12 +7,14 @@ import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponent
import { useCreation } from 'ahooks'; import { useCreation } from 'ahooks';
import { AppContext } from '@/pages/app/detail/components/context'; import { AppContext } from '@/pages/app/detail/components/context';
import { getEditorVariables } from '../../../../../utils'; import { getEditorVariables } from '../../../../../utils';
import { WorkflowActionContext } from '../../../../../context/workflowInitContext';
const TextInputRender = ({ inputs = [], item, nodeId }: RenderInputProps) => { const TextInputRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { nodeList, edges, onChangeNode } = useContextSelector(WorkflowContext, (v) => v); const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
const { appDetail } = useContextSelector(AppContext, (v) => v); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
// get variable // get variable
const variables = useCreation(() => { const variables = useCreation(() => {

View File

@@ -7,11 +7,12 @@ import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponent
import { useCreation } from 'ahooks'; import { useCreation } from 'ahooks';
import { AppContext } from '@/pages/app/detail/components/context'; import { AppContext } from '@/pages/app/detail/components/context';
import { getEditorVariables } from '../../../../../utils'; import { getEditorVariables } from '../../../../../utils';
import { WorkflowActionContext } from '../../../../../context/workflowInitContext';
const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => { const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { appDetail } = useContextSelector(AppContext, (v) => v); const { appDetail } = useContextSelector(AppContext, (v) => v);

View File

@@ -13,7 +13,6 @@ const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutpu
const { t } = useTranslation(); const { t } = useTranslation();
const { label = '', description, valueType, valueDesc } = output; const { label = '', description, valueType, valueDesc } = output;
const Render = useMemo(() => {
return ( return (
<Box position={'relative'}> <Box position={'relative'}>
<Flex <Flex
@@ -48,9 +47,6 @@ const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutpu
)} )}
</Box> </Box>
); );
}, [output.type, output.key, t, label, description, valueType, valueDesc, nodeId]);
return Render;
}; };
export default React.memo(OutputLabel); export default React.memo(OutputLabel);

View File

@@ -2,6 +2,9 @@ import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import React from 'react'; import React from 'react';
import { DefaultEdgeOptions } from 'reactflow'; import { DefaultEdgeOptions } from 'reactflow';
export const minZoom = 0.1;
export const maxZoom = 1.5;
export const connectionLineStyle: React.CSSProperties = { export const connectionLineStyle: React.CSSProperties = {
strokeWidth: 2, strokeWidth: 2,
stroke: '#487FFF' stroke: '#487FFF'

View File

@@ -15,7 +15,7 @@ import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/wor
import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe'; import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { useDebounceEffect, useLocalStorageState, useMemoizedFn, useUpdateEffect } from 'ahooks'; import { useLocalStorageState, useMemoizedFn, useUpdateEffect } from 'ahooks';
import React, { import React, {
Dispatch, Dispatch,
SetStateAction, SetStateAction,
@@ -25,31 +25,40 @@ import React, {
useRef, useRef,
useState useState
} from 'react'; } from 'react';
import { import { Edge, Node, OnConnectStartParams, ReactFlowProvider, useReactFlow } from 'reactflow';
Edge,
EdgeChange,
Node,
NodeChange,
OnConnectStartParams,
useEdgesState,
useNodesState,
useReactFlow
} from 'reactflow';
import { createContext, useContextSelector } from 'use-context-selector'; import { createContext, useContextSelector } from 'use-context-selector';
import { defaultRunningStatus } from './constants'; import { defaultRunningStatus } from '../constants';
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils'; 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 { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { AppChatConfigType } from '@fastgpt/global/core/app/type'; import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import { AppContext } from '@/pages/app/detail/components/context'; import { AppContext } from '@/pages/app/detail/components/context';
import ChatTest from './Flow/ChatTest'; import ChatTest from '../Flow/ChatTest';
import { useDisclosure } from '@chakra-ui/react'; import { useDisclosure } from '@chakra-ui/react';
import { uiWorkflow2StoreWorkflow } from './utils'; import { uiWorkflow2StoreWorkflow } from '../utils';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { formatTime2YMDHMS, formatTime2YMDHMW } from '@fastgpt/global/common/string/time'; import { formatTime2YMDHMS, formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { SetState } from 'ahooks/lib/createUseStorageState';
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version'; import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
import WorkflowInitContextProvider, { WorkflowActionContext } from './workflowInitContext';
import WorkflowEventContextProvider from './workflowEventContext';
export const ReactFlowCustomProvider = ({
templates,
children
}: {
templates: FlowNodeTemplateType[];
children: React.ReactNode;
}) => {
return (
<ReactFlowProvider>
<WorkflowInitContextProvider>
<WorkflowContextProvider basicNodeTemplates={templates}>
<WorkflowEventContextProvider>{children}</WorkflowEventContextProvider>
</WorkflowContextProvider>
</WorkflowInitContextProvider>
</ReactFlowProvider>
);
};
type OnChange<ChangesType> = (changes: ChangesType[]) => void; type OnChange<ChangesType> = (changes: ChangesType[]) => void;
@@ -65,33 +74,22 @@ type WorkflowContextType = {
appId?: string; appId?: string;
basicNodeTemplates: FlowNodeTemplateType[]; basicNodeTemplates: FlowNodeTemplateType[];
filterAppIds?: string[]; filterAppIds?: string[];
reactFlowWrapper: React.RefObject<HTMLDivElement> | null;
mouseInCanvas: boolean;
// nodes // nodes
nodes: Node<FlowNodeItemType, string | undefined>[];
nodeList: FlowNodeItemType[]; nodeList: FlowNodeItemType[];
setNodes: Dispatch<SetStateAction<Node<FlowNodeItemType, string | undefined>[]>>;
onNodesChange: OnChange<NodeChange>;
hasToolNode: boolean; hasToolNode: boolean;
hoverNodeId?: string;
setHoverNodeId: React.Dispatch<React.SetStateAction<string | undefined>>;
onUpdateNodeError: (node: string, isError: Boolean) => void; onUpdateNodeError: (node: string, isError: Boolean) => void;
onResetNode: (e: { id: string; node: FlowNodeTemplateType }) => void; onResetNode: (e: { id: string; node: FlowNodeTemplateType }) => void;
onChangeNode: (e: FlowNodeChangeProps) => void; onChangeNode: (e: FlowNodeChangeProps) => void;
getNodeDynamicInputs: (nodeId: string) => FlowNodeInputItemType[]; getNodeDynamicInputs: (nodeId: string) => FlowNodeInputItemType[];
// edges // edges
edges: Edge<any>[];
setEdges: Dispatch<SetStateAction<Edge<any>[]>>;
onEdgesChange: OnChange<EdgeChange>;
onDelEdge: (e: { onDelEdge: (e: {
nodeId: string; nodeId: string;
sourceHandle?: string | undefined; sourceHandle?: string | undefined;
targetHandle?: string | undefined; targetHandle?: string | undefined;
}) => void; }) => void;
hoverEdgeId?: string;
setHoverEdgeId: React.Dispatch<React.SetStateAction<string | undefined>>;
onSwitchTmpVersion: (data: WorkflowSnapshotsType, customTitle: string) => boolean; onSwitchTmpVersion: (data: WorkflowSnapshotsType, customTitle: string) => boolean;
onSwitchCloudVersion: (appVersion: AppVersionSchemaType) => boolean; onSwitchCloudVersion: (appVersion: AppVersionSchemaType) => boolean;
@@ -102,6 +100,19 @@ type WorkflowContextType = {
undo: () => void; undo: () => void;
canRedo: boolean; canRedo: boolean;
canUndo: boolean; canUndo: boolean;
pushPastSnapshot: ({
pastNodes,
pastEdges,
customTitle,
chatConfig,
isSaved
}: {
pastNodes: Node[];
pastEdges: Edge[];
customTitle?: string;
chatConfig: AppChatConfigType;
isSaved?: boolean;
}) => boolean;
// connect // connect
connectingEdge?: OnConnectStartParams; connectingEdge?: OnConnectStartParams;
@@ -159,10 +170,6 @@ type WorkflowContextType = {
}) => Promise<void>; }) => Promise<void>;
onStopNodeDebug: () => void; onStopNodeDebug: () => void;
// version history
showHistoryModal: boolean;
setShowHistoryModal: React.Dispatch<React.SetStateAction<boolean>>;
// chat test // chat test
setWorkflowTestData: React.Dispatch< setWorkflowTestData: React.Dispatch<
React.SetStateAction< React.SetStateAction<
@@ -173,15 +180,6 @@ type WorkflowContextType = {
| undefined | undefined
> >
>; >;
//
workflowControlMode?: 'drag' | 'select';
setWorkflowControlMode: (value?: SetState<'drag' | 'select'> | undefined) => void;
menu: {
top: number;
left: number;
} | null;
setMenu: (value: React.SetStateAction<{ top: number; left: number } | null>) => void;
}; };
type DebugDataType = { type DebugDataType = {
@@ -198,32 +196,11 @@ export const WorkflowContext = createContext<WorkflowContextType>({
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
basicNodeTemplates: [], basicNodeTemplates: [],
reactFlowWrapper: null,
nodes: [],
nodeList: [], nodeList: [],
mouseInCanvas: false,
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, hasToolNode: false,
setHoverNodeId: function (value: React.SetStateAction<string | undefined>): void {
throw new Error('Function not implemented.');
},
onUpdateNodeError: function (node: string, isError: Boolean): void { onUpdateNodeError: function (node: string, isError: Boolean): void {
throw new Error('Function not implemented.'); 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 { onResetNode: function (e: { id: string; node: FlowNodeTemplateType }): void {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
@@ -272,9 +249,6 @@ export const WorkflowContext = createContext<WorkflowContextType>({
onChangeNode: function (e: FlowNodeChangeProps): void { onChangeNode: function (e: FlowNodeChangeProps): void {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
setHoverEdgeId: function (value: React.SetStateAction<string | undefined>): void {
throw new Error('Function not implemented.');
},
setWorkflowTestData: function ( setWorkflowTestData: function (
value: React.SetStateAction< value: React.SetStateAction<
{ nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] } | undefined { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] } | undefined
@@ -292,10 +266,6 @@ export const WorkflowContext = createContext<WorkflowContextType>({
| undefined { | undefined {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
showHistoryModal: false,
setShowHistoryModal: function (value: React.SetStateAction<boolean>): void {
throw new Error('Function not implemented.');
},
getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] { getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
@@ -312,18 +282,27 @@ export const WorkflowContext = createContext<WorkflowContextType>({
}, },
canRedo: false, canRedo: false,
canUndo: false, canUndo: false,
workflowControlMode: 'drag',
setWorkflowControlMode: function (value?: SetState<'drag' | 'select'> | undefined): void {
throw new Error('Function not implemented.');
},
onSwitchTmpVersion: function (data: WorkflowSnapshotsType, customTitle: string): boolean { onSwitchTmpVersion: function (data: WorkflowSnapshotsType, customTitle: string): boolean {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
onSwitchCloudVersion: function (appVersion: AppVersionSchemaType): boolean { onSwitchCloudVersion: function (appVersion: AppVersionSchemaType): boolean {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
menu: null,
setMenu: function (value: React.SetStateAction<{ top: number; left: number } | null>): void { pushPastSnapshot: function ({
pastNodes,
pastEdges,
customTitle,
chatConfig,
isSaved
}: {
pastNodes: Node[];
pastEdges: Edge[];
customTitle?: string;
chatConfig: AppChatConfigType;
isSaved?: boolean;
}): boolean {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
} }
}); });
@@ -337,39 +316,14 @@ const WorkflowContextProvider = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v); const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const setAppDetail = useContextSelector(AppContext, (v) => v.setAppDetail);
const appId = appDetail._id; const appId = appDetail._id;
const [workflowControlMode, setWorkflowControlMode] = useLocalStorageState<'drag' | 'select'>(
'workflow-control-mode',
{
defaultValue: 'drag',
listenStorageChange: true
}
);
// Mouse in canvas
const [mouseInCanvas, setMouseInCanvas] = useState(false);
useEffect(() => {
const handleMouseInCanvas = (e: MouseEvent) => {
setMouseInCanvas(true);
};
const handleMouseOutCanvas = (e: MouseEvent) => {
setMouseInCanvas(false);
};
reactFlowWrapper?.current?.addEventListener('mouseenter', handleMouseInCanvas);
reactFlowWrapper?.current?.addEventListener('mouseleave', handleMouseOutCanvas);
return () => {
reactFlowWrapper?.current?.removeEventListener('mouseenter', handleMouseInCanvas);
reactFlowWrapper?.current?.removeEventListener('mouseleave', handleMouseOutCanvas);
};
}, [reactFlowWrapper?.current]);
/* edge */ /* edge */
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const edges = useContextSelector(WorkflowActionContext, (state) => state.edges);
const [hoverEdgeId, setHoverEdgeId] = useState<string>(); const setEdges = useContextSelector(WorkflowActionContext, (state) => state.setEdges);
const onDelEdge = useCallback( const onDelEdge = useCallback(
({ ({
nodeId, nodeId,
@@ -397,32 +351,16 @@ const WorkflowContextProvider = ({
const [connectingEdge, setConnectingEdge] = useState<OnConnectStartParams>(); const [connectingEdge, setConnectingEdge] = useState<OnConnectStartParams>();
/* node */ /* node */
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowNodeItemType>([]); const setNodes = useContextSelector(WorkflowActionContext, (state) => state.setNodes);
const [hoverNodeId, setHoverNodeId] = useState<string>(); const getNodes = useContextSelector(WorkflowActionContext, (state) => state.getNodes);
const nodeListString = useContextSelector(WorkflowActionContext, (state) => state.nodeListString);
const nodeListString = JSON.stringify(nodes.map((node) => node.data)); console.log(121211111111);
const nodeList = useMemo( const nodeList = useMemo(
() => JSON.parse(nodeListString) as FlowNodeItemType[], () => JSON.parse(nodeListString) as FlowNodeItemType[],
[nodeListString] [nodeListString]
); );
// Elevate childNodes
useEffect(() => {
setNodes((nodes) =>
nodes.map((node) => (node.data.parentNodeId ? { ...node, zIndex: 1001 } : node))
);
}, [nodeList]);
// Elevate edges of childNodes
useEffect(() => {
setEdges((state) =>
state.map((item) =>
nodeList.some((node) => item.source === node.nodeId && node.parentNodeId)
? { ...item, zIndex: 1001 }
: item
)
);
}, [edges.length]);
const hasToolNode = useMemo(() => { const hasToolNode = useMemo(() => {
return !!nodeList.find((node) => node.flowNodeType === FlowNodeTypeEnum.tools); return !!nodeList.find((node) => node.flowNodeType === FlowNodeTypeEnum.tools);
}, [nodeList]); }, [nodeList]);
@@ -571,6 +509,7 @@ const WorkflowContextProvider = ({
/* ui flow to store data */ /* ui flow to store data */
const { fitView } = useReactFlow(); const { fitView } = useReactFlow();
const flowData2StoreDataAndCheck = useMemoizedFn((hideTip = false) => { const flowData2StoreDataAndCheck = useMemoizedFn((hideTip = false) => {
const nodes = getNodes();
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges }); const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
if (!checkResults) { if (!checkResults) {
@@ -593,6 +532,7 @@ const WorkflowContextProvider = ({
}); });
const flowData2StoreData = useMemoizedFn(() => { const flowData2StoreData = useMemoizedFn(() => {
const nodes = getNodes();
return uiWorkflow2StoreWorkflow({ nodes, edges }); return uiWorkflow2StoreWorkflow({ nodes, edges });
}); });
@@ -892,22 +832,6 @@ const WorkflowContextProvider = ({
}); });
}); });
// Auto save snapshot
useDebounceEffect(
() => {
if (nodes.length === 0 || !appDetail.chatConfig) return;
pushPastSnapshot({
pastNodes: nodes,
pastEdges: edges,
customTitle: formatTime2YMDHMS(new Date()),
chatConfig: appDetail.chatConfig
});
},
[nodes, edges, appDetail.chatConfig],
{ wait: 500 }
);
const undo = useMemoizedFn(() => { const undo = useMemoizedFn(() => {
if (past[1]) { if (past[1]) {
setFuture((future) => [past[0], ...future]); setFuture((future) => [past[0], ...future]);
@@ -978,51 +902,20 @@ const WorkflowContextProvider = ({
] ]
); );
/* Version histories */ const value = useMemo(
const [showHistoryModal, setShowHistoryModal] = useState(false); () => ({
/* event bus */
useEffect(() => {
eventBus.on(EventNameEnum.requestWorkflowStore, () => {
eventBus.emit(EventNameEnum.receiveWorkflowStore, {
nodes,
edges
});
});
return () => {
eventBus.off(EventNameEnum.requestWorkflowStore);
};
}, [edges, nodes]);
const [menu, setMenu] = useState<{ top: number; left: number } | null>(null);
const value = {
appId, appId,
reactFlowWrapper,
basicNodeTemplates, basicNodeTemplates,
workflowControlMode,
setWorkflowControlMode,
mouseInCanvas,
// node // node
nodes,
setNodes,
onNodesChange,
nodeList, nodeList,
hasToolNode, hasToolNode,
hoverNodeId,
setHoverNodeId,
onUpdateNodeError, onUpdateNodeError,
onResetNode, onResetNode,
onChangeNode, onChangeNode,
getNodeDynamicInputs, getNodeDynamicInputs,
// edge // edge
edges,
setEdges,
hoverEdgeId,
setHoverEdgeId,
onEdgesChange,
connectingEdge, connectingEdge,
setConnectingEdge, setConnectingEdge,
onDelEdge, onDelEdge,
@@ -1037,6 +930,7 @@ const WorkflowContextProvider = ({
canRedo: !!future.length, canRedo: !!future.length,
onSwitchTmpVersion, onSwitchTmpVersion,
onSwitchCloudVersion, onSwitchCloudVersion,
pushPastSnapshot,
// function // function
splitToolInputs, splitToolInputs,
@@ -1050,16 +944,38 @@ const WorkflowContextProvider = ({
onStartNodeDebug, onStartNodeDebug,
onStopNodeDebug, onStopNodeDebug,
// version history
showHistoryModal,
setShowHistoryModal,
// chat test // chat test
setWorkflowTestData, setWorkflowTestData
}),
menu, [
setMenu appId,
}; basicNodeTemplates,
connectingEdge,
flowData2StoreData,
flowData2StoreDataAndCheck,
future,
getNodeDynamicInputs,
hasToolNode,
initData,
nodeList,
onChangeNode,
onDelEdge,
onNextNodeDebug,
onResetNode,
onStartNodeDebug,
onStopNodeDebug,
onSwitchCloudVersion,
onSwitchTmpVersion,
onUpdateNodeError,
past,
pushPastSnapshot,
redo,
setPast,
splitToolInputs,
undo,
workflowDebugData
]
);
return ( return (
<WorkflowContext.Provider value={value}> <WorkflowContext.Provider value={value}>
@@ -1068,18 +984,4 @@ const WorkflowContextProvider = ({
</WorkflowContext.Provider> </WorkflowContext.Provider>
); );
}; };
export default React.memo(WorkflowContextProvider);
export default WorkflowContextProvider;
type GetWorkflowStoreResponse = {
nodes: Node<FlowNodeItemType>[];
edges: Edge<any>[];
};
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

@@ -0,0 +1,119 @@
import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { createContext } from 'use-context-selector';
import { useLocalStorageState } from 'ahooks';
import { SetState } from 'ahooks/lib/createUseStorageState';
type WorkflowEventContextType = {
mouseInCanvas: boolean;
reactFlowWrapper: React.RefObject<HTMLDivElement> | null;
hoverNodeId?: string;
setHoverNodeId: React.Dispatch<React.SetStateAction<string | undefined>>;
hoverEdgeId?: string;
setHoverEdgeId: React.Dispatch<React.SetStateAction<string | undefined>>;
workflowControlMode?: 'drag' | 'select';
setWorkflowControlMode: (value?: SetState<'drag' | 'select'> | undefined) => void;
menu: {
top: number;
left: number;
} | null;
setMenu: (value: React.SetStateAction<{ top: number; left: number } | null>) => void;
// version history
showHistoryModal: boolean;
setShowHistoryModal: React.Dispatch<React.SetStateAction<boolean>>;
};
export const WorkflowEventContext = createContext<WorkflowEventContextType>({
mouseInCanvas: false,
reactFlowWrapper: null,
setHoverNodeId: function (value: React.SetStateAction<string | undefined>): void {
throw new Error('Function not implemented.');
},
setHoverEdgeId: function (value: React.SetStateAction<string | undefined>): void {
throw new Error('Function not implemented.');
},
workflowControlMode: 'drag',
setWorkflowControlMode: function (value?: SetState<'drag' | 'select'> | undefined): void {
throw new Error('Function not implemented.');
},
menu: null,
setMenu: function (value: React.SetStateAction<{ top: number; left: number } | null>): void {
throw new Error('Function not implemented.');
},
showHistoryModal: false,
setShowHistoryModal: function (value: React.SetStateAction<boolean>): void {
throw new Error('Function not implemented.');
}
});
const WorkflowEventContextProvider = ({ children }: { children: ReactNode }) => {
// Watch mouse in canvas
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const [mouseInCanvas, setMouseInCanvas] = useState(false);
useEffect(() => {
const handleMouseInCanvas = (e: MouseEvent) => {
setMouseInCanvas(true);
};
const handleMouseOutCanvas = (e: MouseEvent) => {
setMouseInCanvas(false);
};
reactFlowWrapper?.current?.addEventListener('mouseenter', handleMouseInCanvas);
reactFlowWrapper?.current?.addEventListener('mouseleave', handleMouseOutCanvas);
return () => {
reactFlowWrapper?.current?.removeEventListener('mouseenter', handleMouseInCanvas);
reactFlowWrapper?.current?.removeEventListener('mouseleave', handleMouseOutCanvas);
};
}, [reactFlowWrapper?.current, setMouseInCanvas]);
// Watch hover node
const [hoverNodeId, setHoverNodeId] = useState<string>();
// Watch hover edge
const [hoverEdgeId, setHoverEdgeId] = useState<string>();
const [workflowControlMode, setWorkflowControlMode] = useLocalStorageState<'drag' | 'select'>(
'workflow-control-mode',
{
defaultValue: 'drag',
listenStorageChange: true
}
);
const [menu, setMenu] = useState<{ top: number; left: number } | null>(null);
/* Version histories */
const [showHistoryModal, setShowHistoryModal] = useState(false);
const contextValue = useMemo(
() => ({
mouseInCanvas,
reactFlowWrapper,
hoverNodeId,
setHoverNodeId,
hoverEdgeId,
setHoverEdgeId,
workflowControlMode,
setWorkflowControlMode,
menu,
setMenu,
showHistoryModal,
setShowHistoryModal
}),
[
mouseInCanvas,
hoverNodeId,
setHoverNodeId,
hoverEdgeId,
setHoverEdgeId,
workflowControlMode,
setWorkflowControlMode,
menu,
setMenu,
showHistoryModal,
setShowHistoryModal
]
);
return (
<WorkflowEventContext.Provider value={contextValue}>{children}</WorkflowEventContext.Provider>
);
};
export default WorkflowEventContextProvider;

View File

@@ -0,0 +1,138 @@
import { createContext } from 'use-context-selector';
import { postWorkflowDebug } from '@/web/core/workflow/api';
import {
checkWorkflowNodeAndConnection,
compareSnapshot,
storeEdgesRenderEdge,
storeNode2FlowNode
} from '@/web/core/workflow/utils';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { NodeOutputKeyEnum } 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, StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
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 { useDebounceEffect, useLocalStorageState, useMemoizedFn, useUpdateEffect } from 'ahooks';
import React, {
Dispatch,
SetStateAction,
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import {
Edge,
EdgeChange,
Node,
NodeChange,
OnConnectStartParams,
useEdgesState,
useNodesState,
useReactFlow
} from 'reactflow';
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
type WorkflowInitContextType = {
nodes: Node<FlowNodeItemType, string | undefined>[];
};
export const WorkflowInitContext = createContext<WorkflowInitContextType>({
nodes: []
});
type WorkflowActionContextType = {
setNodes: Dispatch<SetStateAction<Node<FlowNodeItemType, string | undefined>[]>>;
onNodesChange: OnChange<NodeChange>;
getNodes: () => Node<FlowNodeItemType, string | undefined>[];
nodeListString: string;
edges: Edge<any>[];
setEdges: Dispatch<SetStateAction<Edge<any>[]>>;
onEdgesChange: OnChange<EdgeChange>;
};
export const WorkflowActionContext = createContext<WorkflowActionContextType>({
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.');
},
getNodes: function (): Node<FlowNodeItemType, string | undefined>[] {
throw new Error('Function not implemented.');
},
nodeListString: JSON.stringify([]),
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.');
}
});
const WorkflowInitContextProvider = ({ children }: { children: ReactNode }) => {
// Nodes
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowNodeItemType>([]);
const getNodes = useMemoizedFn(() => nodes);
const nodeListString = JSON.stringify(nodes.map((node) => node.data));
const nodeList = useMemo(
() => JSON.parse(nodeListString) as FlowNodeItemType[],
[nodeListString]
);
// Edges
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// Elevate childNodes
useEffect(() => {
setNodes((nodes) =>
nodes.map((node) => (node.data.parentNodeId ? { ...node, zIndex: 1001 } : node))
);
}, [nodeList]);
// Elevate edges of childNodes
useEffect(() => {
setEdges((state) =>
state.map((item) =>
nodeList.some((node) => item.source === node.nodeId && node.parentNodeId)
? { ...item, zIndex: 1001 }
: item
)
);
}, [edges.length]);
const actionContextValue = useMemo(
() => ({
setNodes,
onNodesChange,
getNodes,
nodeListString,
edges,
setEdges,
onEdgesChange
}),
[setNodes, onNodesChange, getNodes, nodeListString, edges, setEdges, onEdgesChange]
);
return (
<WorkflowInitContext.Provider
value={{
nodes
}}
>
<WorkflowActionContext.Provider value={actionContextValue}>
{children}
</WorkflowActionContext.Provider>
</WorkflowInitContext.Provider>
);
};
export default WorkflowInitContextProvider;

View File

@@ -1,4 +1,4 @@
import { Dispatch, ReactNode, SetStateAction, useCallback, useState } from 'react'; import { Dispatch, ReactNode, SetStateAction, useCallback, useMemo, useState } from 'react';
import { createContext } from 'use-context-selector'; import { createContext } from 'use-context-selector';
import { defaultApp } from '@/web/core/app/constants'; import { defaultApp } from '@/web/core/app/constants';
import { delAppById, getAppDetailById, putAppById } from '@/web/core/app/api'; import { delAppById, getAppDetailById, putAppById } from '@/web/core/app/api';
@@ -186,7 +186,8 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => {
[appDetail.name, deleteApp, openConfirmDel, t] [appDetail.name, deleteApp, openConfirmDel, t]
); );
const contextValue: AppContextType = { const contextValue: AppContextType = useMemo(
() => ({
appId, appId,
currentTab, currentTab,
route2Tab, route2Tab,
@@ -201,7 +202,23 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => {
appLatestVersion, appLatestVersion,
reloadAppLatestVersion, reloadAppLatestVersion,
reloadApp reloadApp
}; }),
[
appDetail,
appId,
appLatestVersion,
currentTab,
loadingApp,
onDelApp,
onOpenInfoEdit,
onOpenTeamTagModal,
onSaveApp,
reloadApp,
reloadAppLatestVersion,
route2Tab,
updateAppDetail
]
);
return ( return (
<AppContext.Provider value={contextValue}> <AppContext.Provider value={contextValue}>

View File

@@ -1,9 +1,6 @@
export enum EventNameEnum { export enum EventNameEnum {
sendQuestion = 'sendQuestion', sendQuestion = 'sendQuestion',
editQuestion = 'editQuestion', editQuestion = 'editQuestion'
requestWorkflowStore = 'requestWorkflowStore',
receiveWorkflowStore = 'receiveWorkflowStore'
} }
export const eventBus = { export const eventBus = {