From f58dba2eda5dd3f110b94ebecea3f60315c83021 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Thu, 7 Nov 2024 10:02:22 +0800 Subject: [PATCH] perf: workflow context split (#3083) * perf: workflow context split * perf: context --- .../app/detail/components/Plugin/Header.tsx | 17 +- .../app/detail/components/Workflow/Header.tsx | 16 +- .../app/detail/components/Workflow/index.tsx | 10 +- .../Flow/NodeTemplatesModal.tsx | 547 ++++++++---------- .../Flow/components/ButtonEdge.tsx | 11 +- .../Flow/components/ContextMenu.tsx | 120 ++-- .../Flow/components/FlowController.tsx | 27 +- .../Flow/components/IOTitle.tsx | 1 - .../Flow/hooks/useDebug.tsx | 12 +- .../Flow/hooks/useKeyboard.tsx | 12 +- .../Flow/hooks/useWorkflow.tsx | 112 ++-- .../WorkflowComponents/Flow/index.tsx | 20 +- .../Flow/nodes/NodeHttp/index.tsx | 7 +- .../nodes/NodePluginIO/NodePluginConfig.tsx | 48 +- .../Flow/nodes/NodeSystemConfig.tsx | 93 +-- .../Flow/nodes/NodeVariableUpdate.tsx | 81 +-- .../Flow/nodes/NodeWorkflowStart.tsx | 57 +- .../nodes/render/Handle/ConnectionHandle.tsx | 7 +- .../Flow/nodes/render/Handle/ToolHandle.tsx | 5 +- .../Flow/nodes/render/Handle/index.tsx | 15 +- .../Flow/nodes/render/NodeCard.tsx | 7 +- .../Flow/nodes/render/RenderInput/Label.tsx | 81 +-- .../Flow/nodes/render/RenderInput/index.tsx | 81 ++- .../RenderInput/templates/JsonEditor.tsx | 34 +- .../RenderInput/templates/Reference.tsx | 8 +- .../RenderInput/templates/TextInput.tsx | 8 +- .../render/RenderInput/templates/Textarea.tsx | 3 +- .../Flow/nodes/render/RenderOutput/Label.tsx | 70 ++- .../WorkflowComponents/constants.tsx | 3 + .../{context.tsx => context/index.tsx} | 364 +++++------- .../context/workflowEventContext.tsx | 119 ++++ .../context/workflowInitContext.tsx | 138 +++++ .../pages/app/detail/components/context.tsx | 51 +- projects/app/src/web/common/utils/eventbus.ts | 5 +- 34 files changed, 1193 insertions(+), 997 deletions(-) rename projects/app/src/pages/app/detail/components/WorkflowComponents/{context.tsx => context/index.tsx} (78%) create mode 100644 projects/app/src/pages/app/detail/components/WorkflowComponents/context/workflowEventContext.tsx create mode 100644 projects/app/src/pages/app/detail/components/WorkflowComponents/context/workflowInitContext.tsx diff --git a/projects/app/src/pages/app/detail/components/Plugin/Header.tsx b/projects/app/src/pages/app/detail/components/Plugin/Header.tsx index fe2dd9dc2..70db4ab7d 100644 --- a/projects/app/src/pages/app/detail/components/Plugin/Header.tsx +++ b/projects/app/src/pages/app/detail/components/Plugin/Header.tsx @@ -29,6 +29,11 @@ import { useDebounceEffect } from 'ahooks'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import SaveButton from '../Workflow/components/SaveButton'; import PublishHistories from '../PublishHistoriesSlider'; +import { + WorkflowActionContext, + WorkflowInitContext +} from '../WorkflowComponents/context/workflowInitContext'; +import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext'; const Header = () => { const { t } = useTranslation(); @@ -44,20 +49,24 @@ const Header = () => { onClose: onCloseBackConfirm } = useDisclosure(); + const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes); + const edges = useContextSelector(WorkflowActionContext, (v) => v.edges); const { flowData2StoreData, flowData2StoreDataAndCheck, setWorkflowTestData, - setShowHistoryModal, - showHistoryModal, - nodes, - edges, past, future, setPast, onSwitchTmpVersion, onSwitchCloudVersion } = useContextSelector(WorkflowContext, (v) => v); + const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal); + const setShowHistoryModal = useContextSelector( + WorkflowEventContext, + (v) => v.setShowHistoryModal + ); + const { lastAppListRouteType } = useSystemStore(); const [isPublished, setIsPublished] = useState(false); diff --git a/projects/app/src/pages/app/detail/components/Workflow/Header.tsx b/projects/app/src/pages/app/detail/components/Workflow/Header.tsx index 58f49e338..640ee0063 100644 --- a/projects/app/src/pages/app/detail/components/Workflow/Header.tsx +++ b/projects/app/src/pages/app/detail/components/Workflow/Header.tsx @@ -29,6 +29,11 @@ import { useDebounceEffect } from 'ahooks'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import SaveButton from './components/SaveButton'; import PublishHistories from '../PublishHistoriesSlider'; +import { + WorkflowActionContext, + WorkflowInitContext +} from '../WorkflowComponents/context/workflowInitContext'; +import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext'; const Header = () => { const { t } = useTranslation(); @@ -48,20 +53,23 @@ const Header = () => { onClose: onCloseBackConfirm } = useDisclosure(); + const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes); + const edges = useContextSelector(WorkflowActionContext, (v) => v.edges); const { flowData2StoreData, flowData2StoreDataAndCheck, setWorkflowTestData, - setShowHistoryModal, - showHistoryModal, - nodes, - edges, past, future, setPast, onSwitchTmpVersion, onSwitchCloudVersion } = useContextSelector(WorkflowContext, (v) => v); + const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal); + const setShowHistoryModal = useContextSelector( + WorkflowEventContext, + (v) => v.setShowHistoryModal + ); const { lastAppListRouteType } = useSystemStore(); diff --git a/projects/app/src/pages/app/detail/components/Workflow/index.tsx b/projects/app/src/pages/app/detail/components/Workflow/index.tsx index 5ebbe3507..07812123e 100644 --- a/projects/app/src/pages/app/detail/components/Workflow/index.tsx +++ b/projects/app/src/pages/app/detail/components/Workflow/index.tsx @@ -14,7 +14,7 @@ import { cloneDeep } from 'lodash'; import { useTranslation } from 'next-i18next'; import Flow from '../WorkflowComponents/Flow'; -import { ReactFlowProvider } from 'reactflow'; +import { ReactFlowCustomProvider } from '../WorkflowComponents/context/index'; const Logs = dynamic(() => import('../Logs/index')); const PublishChannel = dynamic(() => import('../Publish')); @@ -67,11 +67,9 @@ const WorkflowEdit = () => { const Render = () => { return ( - - - - - + + + ); }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx index 8eec02669..967ec14e6 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/NodeTemplatesModal.tsx @@ -50,6 +50,7 @@ import { useUserStore } from '@/web/support/user/useUserStore'; import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart'; import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd'; import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import { WorkflowActionContext } from '../context/workflowInitContext'; type ModuleTemplateListProps = { isOpen: boolean; @@ -79,10 +80,10 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => { const [parentId, setParentId] = useState(''); const [searchKey, setSearchKey] = useState(''); const { feConfigs } = useSystemStore(); - const { basicNodeTemplates, hasToolNode, nodeList, appId } = useContextSelector( - WorkflowContext, - (v) => v - ); + const basicNodeTemplates = useContextSelector(WorkflowContext, (v) => v.basicNodeTemplates); + const hasToolNode = useContextSelector(WorkflowContext, (v) => v.hasToolNode); + const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); + const appId = useContextSelector(WorkflowContext, (v) => v.appId); const { data: members = [] } = useRequest2(loadAndGetTeamMembers, { manual: !feConfigs.isPlus @@ -217,105 +218,120 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => { } ); - const Render = useMemo(() => { - return ( - <> - - - {/* Header */} - - {/* Tabs */} - - - { - loadNodeTemplates({ - type: e as TemplateTypeEnum, - parentId: '' - }); - }} - /> - - {/* close icon */} - } - borderColor={'myGray.300'} - variant={'grayBase'} - aria-label={''} - onClick={onClose} + return ( + <> + + + {/* Header */} + + {/* Tabs */} + + + { + loadNodeTemplates({ + type: e as TemplateTypeEnum, + parentId: '' + }); + }} /> - - {/* Search */} - {(templateType === TemplateTypeEnum.teamPlugin || - templateType === TemplateTypeEnum.systemPlugin) && ( - - - - - - setSearchKey(e.target.value)} - /> - - - {templateType === TemplateTypeEnum.teamPlugin && ( + + {/* close icon */} + } + borderColor={'myGray.300'} + variant={'grayBase'} + aria-label={''} + onClick={onClose} + /> + + {/* Search */} + {(templateType === TemplateTypeEnum.teamPlugin || + templateType === TemplateTypeEnum.systemPlugin) && ( + + + + + + setSearchKey(e.target.value)} + /> + + + {templateType === TemplateTypeEnum.teamPlugin && ( + router.push('/app/list')} + gap={1} + > + {t('common:create')} + + + )} + {templateType === TemplateTypeEnum.systemPlugin && + feConfigs.systemPluginCourseUrl && ( { color: 'primary.600' }} fontSize={'sm'} - onClick={() => router.push('/app/list')} + onClick={() => window.open(feConfigs.systemPluginCourseUrl)} gap={1} > - {t('common:create')} + {t('common:plugin.contribute')} )} - {templateType === TemplateTypeEnum.systemPlugin && - feConfigs.systemPluginCourseUrl && ( - window.open(feConfigs.systemPluginCourseUrl)} - gap={1} - > - {t('common:plugin.contribute')} - - - )} + + )} + {/* paths */} + {(templateType === TemplateTypeEnum.teamPlugin || + templateType === TemplateTypeEnum.systemPlugin) && + !searchKey && + parentId && ( + + )} - {/* paths */} - {(templateType === TemplateTypeEnum.teamPlugin || - templateType === TemplateTypeEnum.systemPlugin) && - !searchKey && - parentId && ( - - - - )} - - - - - ); - }, [ - isOpen, - onClose, - isLoading, - t, - templateType, - feConfigs.systemPluginCourseUrl, - searchKey, - parentId, - paths, - onUpdateParentId, - templates, - loadNodeTemplates, - router - ]); - - return Render; + + + + + ); }; export default React.memo(NodeTemplatesModal); @@ -403,9 +386,11 @@ const RenderList = React.memo(function RenderList({ const isSystemPlugin = type === TemplateTypeEnum.systemPlugin; const { screenToFlowPosition } = useReactFlow(); - const { toast } = useToast(); - const { reactFlowWrapper, setNodes, nodeList } = useContextSelector(WorkflowContext, (v) => v); const { computedNewNodeName } = useWorkflowUtils(); + const { toast } = useToast(); + + const setNodes = useContextSelector(WorkflowActionContext, (v) => v.setNodes); + const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const formatTemplates = useMemo(() => { const copy: NodeTemplateListType = cloneDeep(workflowNodeTemplateList); @@ -426,8 +411,6 @@ const RenderList = React.memo(function RenderList({ template: NodeTemplateListItemType; position: XYPosition; }) => { - if (!reactFlowWrapper?.current) return; - // Load template node const templateNode = await (async () => { try { @@ -539,16 +522,7 @@ const RenderList = React.memo(function RenderList({ return newState; }); }, - [ - reactFlowWrapper, - screenToFlowPosition, - nodeList, - computedNewNodeName, - t, - setNodes, - setLoading, - toast - ] + [screenToFlowPosition, nodeList, computedNewNodeName, t, setNodes, setLoading, toast] ); const gridStyle = useMemo(() => { @@ -571,133 +545,118 @@ const RenderList = React.memo(function RenderList({ }; }, [type]); - const Render = useMemo(() => { - return templates.length === 0 ? ( - - ) : ( - - - {formatTemplates.map((item, i) => ( - - {item.label && formatTemplates.length > 1 && ( - - - {t(item.label as any)} - - - )} + return templates.length === 0 ? ( + + ) : ( + + + {formatTemplates.map((item, i) => ( + + {item.label && formatTemplates.length > 1 && ( + + + {t(item.label as any)} + + + )} - - {item.list.map((template) => ( - - - - - {t(template.name as any)} - - - - {t(template.intro as any) || t('common:core.workflow.Not intro')} - - {isSystemPlugin && } - - } - > - { - if (e.clientX < sliderWidth) return; - onAddNode({ - template, - position: { x: e.clientX, y: e.clientY } - }); - }} - onClick={(e) => { - if (template.isFolder) { - return setParentId(template.id); - } - if (isPc) { - return onAddNode({ - template, - position: { x: sliderWidth * 1.5, y: 200 } - }); - } - onAddNode({ - template, - position: { x: e.clientX, y: e.clientY } - }); - onClose(); - }} - > - - - + + {item.list.map((template) => ( + + + + {t(template.name as any)} - {gridStyle.authorInName && template.author !== undefined && ( - - {`by ${template.author || feConfigs.systemTitle}`} - - )} + + + {t(template.intro as any) || t('common:core.workflow.Not intro')} - - {gridStyle.authorInRight && template.authorAvatar && template.author && ( - - - - {template.author} - - + {isSystemPlugin && } + + } + > + { + if (e.clientX < sliderWidth) return; + onAddNode({ + template, + position: { x: e.clientX, y: e.clientY } + }); + }} + onClick={(e) => { + if (template.isFolder) { + return setParentId(template.id); + } + if (isPc) { + return onAddNode({ + template, + position: { x: sliderWidth * 1.5, y: 200 } + }); + } + onAddNode({ + template, + position: { x: e.clientX, y: e.clientY } + }); + onClose(); + }} + > + + + + {t(template.name as any)} + + {gridStyle.authorInName && template.author !== undefined && ( + + {`by ${template.author || feConfigs.systemTitle}`} + )} - - - ))} - - - ))} - - - ); - }, [ - feConfigs.systemTitle, - formatTemplates, - gridStyle, - isPc, - isSystemPlugin, - onAddNode, - onClose, - setParentId, - t, - templates.length - ]); + - return Render; + {gridStyle.authorInRight && template.authorAvatar && template.author && ( + + + + {template.author} + + + )} + + + ))} + + + ))} + + + ); }); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ButtonEdge.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ButtonEdge.tsx index 5bc19587c..1d0985403 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ButtonEdge.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ButtonEdge.tsx @@ -6,12 +6,15 @@ import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/w import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../context'; import { useThrottleEffect } from 'ahooks'; +import { WorkflowActionContext, WorkflowInitContext } from '../../context/workflowInitContext'; +import { WorkflowEventContext } from '../../context/workflowEventContext'; const ButtonEdge = (props: EdgeProps) => { - const { nodes, nodeList, onEdgesChange, workflowDebugData, hoverEdgeId } = useContextSelector( - WorkflowContext, - (v) => v - ); + const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes); + const onEdgesChange = useContextSelector(WorkflowActionContext, (v) => v.onEdgesChange); + const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); + const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData); + const hoverEdgeId = useContextSelector(WorkflowEventContext, (v) => v.hoverEdgeId); const { id, diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ContextMenu.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ContextMenu.tsx index 12a352c6a..125c49918 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ContextMenu.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/ContextMenu.tsx @@ -7,76 +7,76 @@ import { CommentNode } from '@fastgpt/global/core/workflow/template/system/comme import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../context'; import { useReactFlow } from 'reactflow'; +import { WorkflowActionContext } from '../../context/workflowInitContext'; +import { WorkflowEventContext } from '../../context/workflowEventContext'; -type ContextMenuProps = { - top: number; - left: number; -}; - -const ContextMenu = ({ top, left }: ContextMenuProps) => { +const ContextMenu = () => { const { t } = useTranslation(); - const setNodes = useContextSelector(WorkflowContext, (ctx) => ctx.setNodes); - const setMenu = useContextSelector(WorkflowContext, (ctx) => ctx.setMenu); + const setNodes = useContextSelector(WorkflowActionContext, (v) => v.setNodes); + const menu = useContextSelector(WorkflowEventContext, (v) => v.menu); + const setMenu = useContextSelector(WorkflowEventContext, (ctx) => ctx.setMenu); const { screenToFlowPosition } = useReactFlow(); const newNode = nodeTemplate2FlowNode({ template: CommentNode, - position: screenToFlowPosition({ x: left, y: top }), + position: screenToFlowPosition({ x: menu?.left ?? 0, y: menu?.top ?? 0 }), t }); return ( - - - { - setMenu(null); - setNodes((state) => { - const newState = state - .map((node) => ({ - ...node, - selected: false - })) - // @ts-ignore - .concat(newNode); - return newState; - }); - }} - zIndex={1} - > - - - {t('workflow:context_menu.add_comment')} - - - + !!menu && ( + + + { + setMenu(null); + setNodes((state) => { + const newState = state + .map((node) => ({ + ...node, + selected: false + })) + // @ts-ignore + .concat(newNode); + return newState; + }); + }} + zIndex={1} + > + + + {t('workflow:context_menu.add_comment')} + + + + ) ); }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/FlowController.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/FlowController.tsx index 8c192a59a..9fac06035 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/FlowController.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/FlowController.tsx @@ -15,8 +15,9 @@ import MyIcon from '@fastgpt/web/components/common/Icon'; import { Box } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import styles from './index.module.scss'; -import { maxZoom, minZoom } from '../index'; +import { maxZoom, minZoom } from '../../constants'; import { useKeyPress } from 'ahooks'; +import { WorkflowEventContext } from '../../context/workflowEventContext'; const buttonStyle = { border: 'none', @@ -27,16 +28,20 @@ const buttonStyle = { const FlowController = React.memo(function FlowController() { const { fitView, zoomIn, zoomOut } = useReactFlow(); const { zoom } = useViewport(); - const { - undo, - redo, - canRedo, - canUndo, - workflowControlMode, - setWorkflowControlMode, - mouseInCanvas, - nodeList - } = useContextSelector(WorkflowContext, (v) => v); + const undo = useContextSelector(WorkflowContext, (v) => v.undo); + const redo = useContextSelector(WorkflowContext, (v) => v.redo); + const canRedo = useContextSelector(WorkflowContext, (v) => v.canRedo); + const canUndo = useContextSelector(WorkflowContext, (v) => v.canUndo); + const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); + const workflowControlMode = useContextSelector( + WorkflowEventContext, + (v) => v.workflowControlMode + ); + const setWorkflowControlMode = useContextSelector( + WorkflowEventContext, + (v) => v.setWorkflowControlMode + ); + const mouseInCanvas = useContextSelector(WorkflowEventContext, (v) => v.mouseInCanvas); const { t } = useTranslation(); const isMac = !window ? false : window.navigator.userAgent.toLocaleLowerCase().includes('mac'); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/IOTitle.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/IOTitle.tsx index 8601cd7a2..77c164f01 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/IOTitle.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/components/IOTitle.tsx @@ -1,6 +1,5 @@ import React from '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) => { return ( diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useDebug.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useDebug.tsx index 31496601b..64b8dd53b 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useDebug.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useDebug.tsx @@ -1,7 +1,7 @@ import { storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils'; import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; 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 { useTranslation } from 'next-i18next'; import { useToast } from '@fastgpt/web/hooks/useToast'; @@ -27,13 +27,14 @@ import { } 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 { WorkflowContext } from '../../context'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { AppContext } from '../../../context'; import { VariableInputItem } from '@/components/core/chat/ChatContainer/ChatBox/components/VariableInput'; import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; import MyTextarea from '@/components/common/Textarea/MyTextarea'; +import { WorkflowActionContext } from '../../context/workflowInitContext'; const MyRightDrawer = dynamic( () => import('@fastgpt/web/components/common/MyDrawer/MyRightDrawer') @@ -49,9 +50,10 @@ export const useDebug = () => { const { t } = useTranslation(); 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 edges = useContextSelector(WorkflowContext, (v) => v.edges); const onStartNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStartNodeDebug); const appDetail = useContextSelector(AppContext, (v) => v.appDetail); @@ -76,7 +78,7 @@ export const useDebug = () => { const [runtimeEdges, setRuntimeEdges] = useState(); const flowData2StoreDataAndCheck = useCallback(async () => { - const { nodes } = await getWorkflowStore(); + const nodes = getNodes(); const checkResults = checkWorkflowNodeAndConnection({ nodes, edges }); if (!checkResults) { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useKeyboard.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useKeyboard.tsx index 824814d4d..2a20c2d25 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useKeyboard.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useKeyboard.tsx @@ -5,14 +5,18 @@ import { useTranslation } from 'next-i18next'; import { Node, useKeyPress } from 'reactflow'; import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import { useContextSelector } from 'use-context-selector'; -import { WorkflowContext, getWorkflowStore } from '../../context'; import { useWorkflowUtils } from './useUtils'; import { useKeyPress as useKeyPressEffect } from 'ahooks'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { WorkflowActionContext } from '../../context/workflowInitContext'; +import { WorkflowEventContext } from '../../context/workflowEventContext'; export const useKeyboard = () => { 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 { computedNewNodeName } = useWorkflowUtils(); @@ -33,14 +37,14 @@ export const useKeyboard = () => { const onCopy = useCallback(async () => { if (hasInputtingElement()) return; - const { nodes } = await getWorkflowStore(); + const nodes = getNodes(); const selectedNodes = nodes.filter( (node) => node.selected && !node.data?.isError && node.data?.unique !== true ); if (selectedNodes.length === 0) return; copyData(JSON.stringify(selectedNodes), t('common:core.workflow.Copy node')); - }, [copyData, hasInputtingElement, t]); + }, [copyData, getNodes, hasInputtingElement, t]); const onParse = useCallback(async () => { if (hasInputtingElement()) return; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx index bd90b7f1b..f2383dda9 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx @@ -25,12 +25,16 @@ import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../context'; import { THelperLine } from '@fastgpt/global/core/workflow/type'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; -import { useMemoizedFn } from 'ahooks'; +import { useDebounceEffect, useMemoizedFn } from 'ahooks'; import { Input_Template_Node_Height, Input_Template_Node_Width } from '@fastgpt/global/core/workflow/template/input'; 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 @@ -271,21 +275,22 @@ export const useWorkflow = () => { const { toast } = useToast(); const { t } = useTranslation(); - const { isDowningCtrl } = useKeyboard(); - const { - setConnectingEdge, - edges, - nodes, - nodeList, - onNodesChange, - setEdges, - onChangeNode, - onEdgesChange, - setHoverEdgeId, - setMenu - } = useContextSelector(WorkflowContext, (v) => v); + const appDetail = useContextSelector(AppContext, (e) => e.appDetail); + + const nodes = useContextSelector(WorkflowInitContext, (state) => state.nodes); + const onNodesChange = useContextSelector(WorkflowActionContext, (state) => state.onNodesChange); + const edges = useContextSelector(WorkflowActionContext, (state) => state.edges); + const setEdges = useContextSelector(WorkflowActionContext, (v) => v.setEdges); + const onEdgesChange = useContextSelector(WorkflowActionContext, (v) => v.onEdgesChange); + const { setConnectingEdge, nodeList, onChangeNode, pushPastSnapshot } = useContextSelector( + WorkflowContext, + (v) => v + ); + const setHoverEdgeId = useContextSelector(WorkflowEventContext, (v) => v.setHoverEdgeId); + const setMenu = useContextSelector(WorkflowEventContext, (v) => v.setMenu); const { getIntersectingNodes } = useReactFlow(); + const { isDowningCtrl } = useKeyboard(); // Loop node size and position const resetParentNodeSizeAndPosition = useMemoizedFn((rect: Rect, parentId: string) => { @@ -330,41 +335,43 @@ export const useWorkflow = () => { const [helperLineVertical, setHelperLineVertical] = useState(); const checkNodeHelpLine = useMemoizedFn((change: NodeChange, nodes: Node[]) => { - const positionChange = change.type === 'position' && change.dragging ? change : undefined; + requestAnimationFrame(() => { + const positionChange = change.type === 'position' && change.dragging ? change : undefined; - if (positionChange?.position) { - // 只判断,3000px 内的 nodes,并按从近到远的顺序排序 - const filterNodes = nodes - .filter((node) => { - if (!positionChange.position) return false; + if (positionChange?.position) { + // 只判断,3000px 内的 nodes,并按从近到远的顺序排序 + const filterNodes = nodes + .filter((node) => { + if (!positionChange.position) return false; - return ( - Math.abs(node.position.x - positionChange.position.x) <= 3000 && - Math.abs(node.position.y - positionChange.position.y) <= 3000 - ); - }) - .sort((a, b) => { - if (!positionChange.position) return 0; - return ( - Math.abs(a.position.x - positionChange.position.x) + - Math.abs(a.position.y - positionChange.position.y) - - Math.abs(b.position.x - positionChange.position.x) - - Math.abs(b.position.y - positionChange.position.y) - ); - }) - .slice(0, 15); + return ( + Math.abs(node.position.x - positionChange.position.x) <= 3000 && + Math.abs(node.position.y - positionChange.position.y) <= 3000 + ); + }) + .sort((a, b) => { + if (!positionChange.position) return 0; + return ( + Math.abs(a.position.x - positionChange.position.x) + + Math.abs(a.position.y - positionChange.position.y) - + Math.abs(b.position.x - positionChange.position.x) - + Math.abs(b.position.y - positionChange.position.y) + ); + }) + .slice(0, 15); - const helperLines = computeHelperLines(positionChange, filterNodes); + const helperLines = computeHelperLines(positionChange, filterNodes); - positionChange.position.x = helperLines.snapPosition.x ?? positionChange.position.x; - positionChange.position.y = helperLines.snapPosition.y ?? positionChange.position.y; + positionChange.position.x = helperLines.snapPosition.x ?? positionChange.position.x; + positionChange.position.y = helperLines.snapPosition.y ?? positionChange.position.y; - setHelperLineHorizontal(helperLines.horizontal); - setHelperLineVertical(helperLines.vertical); - } else { - setHelperLineHorizontal(undefined); - setHelperLineVertical(undefined); - } + setHelperLineHorizontal(helperLines.horizontal); + setHelperLineVertical(helperLines.vertical); + } else { + setHelperLineHorizontal(undefined); + setHelperLineVertical(undefined); + } + }); }); // Check if a node is placed on top of a loop node @@ -642,6 +649,23 @@ export const useWorkflow = () => { setMenu(null); }, [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 { handleNodesChange, handleEdgeChange, diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx index c29c25473..200085f6b 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import ReactFlow, { NodeProps, ReactFlowProvider, SelectionMode } from 'reactflow'; import { Box, IconButton, useDisclosure } from '@chakra-ui/react'; import { SmallCloseIcon } from '@chakra-ui/icons'; @@ -11,16 +11,15 @@ import NodeTemplatesModal from './NodeTemplatesModal'; import 'reactflow/dist/style.css'; 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 { WorkflowContext } from '../context'; import { useWorkflow } from './hooks/useWorkflow'; import HelperLines from './components/HelperLines'; import FlowController from './components/FlowController'; import ContextMenu from './components/ContextMenu'; - -export const minZoom = 0.1; -export const maxZoom = 1.5; +import { WorkflowActionContext, WorkflowInitContext } from '../context/workflowInitContext'; +import { WorkflowEventContext } from '../context/workflowEventContext'; const NodeSimple = dynamic(() => import('./nodes/NodeSimple')); const nodeTypes: Record = { @@ -66,9 +65,12 @@ const edgeTypes = { }; const Workflow = () => { - const { nodes, edges, menu, reactFlowWrapper, workflowControlMode } = useContextSelector( - WorkflowContext, - (v) => v + const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes); + const edges = useContextSelector(WorkflowActionContext, (v) => v.edges); + const reactFlowWrapper = useContextSelector(WorkflowEventContext, (v) => v.reactFlowWrapper); + const workflowControlMode = useContextSelector( + WorkflowEventContext, + (v) => v.workflowControlMode ); const { @@ -125,7 +127,7 @@ const Workflow = () => { - {menu && } + import('./CurlImportModal')); const defaultFormBody = { @@ -80,9 +81,10 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({ }) { const { t } = useTranslation(); const { toast } = useToast(); + + const edges = useContextSelector(WorkflowActionContext, (v) => v.edges); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); - const edges = useContextSelector(WorkflowContext, (v) => v.edges); const { appDetail } = useContextSelector(AppContext, (v) => v); const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure(); @@ -256,8 +258,9 @@ export function RenderHttpProps({ }) { const { t } = useTranslation(); const [selectedTab, setSelectedTab] = useState(TabEnum.params); + + const edges = useContextSelector(WorkflowActionContext, (v) => v.edges); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); - const edges = useContextSelector(WorkflowContext, (v) => v.edges); const { appDetail } = useContextSelector(AppContext, (v) => v); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/NodePluginConfig.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/NodePluginConfig.tsx index 48fd924e2..ce0bd249e 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/NodePluginConfig.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/NodePluginConfig.tsx @@ -53,24 +53,28 @@ const NodePluginConfig = ({ data, selected }: NodeProps) => { [chatConfig, setAppDetail] ); - return ( - - - - - - - - - ); + const Render = useMemo(() => { + return ( + + + + + + + + + ); + }, [componentsProps, data, selected]); + + return Render; }; export default React.memo(NodePluginConfig); @@ -116,8 +120,10 @@ function Instruction({ chatConfig: { instruction }, setAppDetail }: ComponentPro function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) { const { t } = useTranslation(); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); - const nodes = useContextSelector(WorkflowContext, (v) => v.nodes); - const pluginInputNode = nodes.find((item) => item.type === FlowNodeTypeEnum.pluginInput)!; + const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); + const pluginInputNode = nodeList.find( + (item) => item.flowNodeType === FlowNodeTypeEnum.pluginInput + )!; return ( <> @@ -137,7 +143,7 @@ function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: Co // Dynamic add or delete userFilesInput const canUploadFiles = e.canSelectFile || e.canSelectImg; - const repeatKey = pluginInputNode?.data.outputs.find( + const repeatKey = pluginInputNode?.outputs.find( (item) => item.key === userFilesInput.key ); if (canUploadFiles) { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSystemConfig.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSystemConfig.tsx index c0d3b06dd..382520547 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSystemConfig.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeSystemConfig.tsx @@ -29,7 +29,8 @@ type ComponentProps = { }; const NodeUserGuide = ({ data, selected }: NodeProps) => { - const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v); + const appDetail = useContextSelector(AppContext, (v) => v.appDetail); + const setAppDetail = useContextSelector(AppContext, (v) => v.setAppDetail); const chatConfig = useMemo(() => { return getAppChatConfig({ @@ -47,45 +48,49 @@ const NodeUserGuide = ({ data, selected }: NodeProps) => { [chatConfig, setAppDetail] ); - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); + const Render = useMemo(() => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + }, [componentsProps, data, selected]); + + return Render; }; export default React.memo(NodeUserGuide); @@ -218,8 +223,10 @@ function QuestionInputGuide({ chatConfig: { chatInputGuide }, setAppDetail }: Co function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) { const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); - const nodes = useContextSelector(WorkflowContext, (v) => v.nodes); - const workflowStartNode = nodes.find((item) => item.type === FlowNodeTypeEnum.workflowStart)!; + const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); + const workflowStartNode = nodeList.find( + (item) => item.flowNodeType === FlowNodeTypeEnum.workflowStart + )!; return ( item.key === userFilesInput.key ); if (canUploadFiles) { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx index 1707c8298..71eb32f9e 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeVariableUpdate.tsx @@ -34,6 +34,7 @@ import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor'; import { useCreation, useMemoizedFn } from 'ahooks'; import { getEditorVariables } from '../../utils'; import { isArray } from 'lodash'; +import { WorkflowActionContext } from '../../context/workflowInitContext'; const NodeVariableUpdate = ({ data, selected }: NodeProps) => { const { inputs = [], nodeId } = data; @@ -42,7 +43,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps) => const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const appDetail = useContextSelector(AppContext, (v) => v.appDetail); - const edges = useContextSelector(WorkflowContext, (v) => v.edges); + const edges = useContextSelector(WorkflowActionContext, (v) => v.edges); const menuList = useRef([ { @@ -263,44 +264,48 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps) => } ); - return ( - - - <> - {updateList.map((updateItem, index) => ( - - ))} - - - - - - - ); + + + + + ); + }, [ValueRender, data, onUpdateList, selected, t, updateList]); + + return Render; }; export default React.memo(NodeVariableUpdate); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeWorkflowStart.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeWorkflowStart.tsx index dc3cedf4d..28dfda5ab 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeWorkflowStart.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeWorkflowStart.tsx @@ -24,8 +24,8 @@ import MyDivider from '@fastgpt/web/components/common/MyDivider'; const NodeStart = ({ data, selected }: NodeProps) => { const { t } = useTranslation(); const { nodeId, outputs } = data; + const appDetail = useContextSelector(AppContext, (v) => v.appDetail); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); - const { appDetail } = useContextSelector(AppContext, (v) => v); const customGlobalVariables = useCreation(() => { const globalVariables = formatEditorVariablePickerIcon( @@ -62,34 +62,37 @@ const NodeStart = ({ data, selected }: NodeProps) => { })), [t] ); + const Render = useMemo(() => { + return ( + + + + + + + + {customGlobalVariables.length > 0 && ( + <> + + + + )} - return ( - - - - - - - - {customGlobalVariables.length > 0 && ( - <> - - - - )} + + + + ); + }, [customGlobalVariables, data, nodeId, outputs, selected, systemVariables, t]); - - - - ); + return Render; }; export default React.memo(NodeStart); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/ConnectionHandle.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/ConnectionHandle.tsx index 29b96f3c6..39c3907b8 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/ConnectionHandle.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/ConnectionHandle.tsx @@ -5,6 +5,7 @@ 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 '../../../../context'; +import { WorkflowActionContext } from '../../../../context/workflowInitContext'; export const ConnectionSourceHandle = ({ nodeId, @@ -13,7 +14,8 @@ export const ConnectionSourceHandle = ({ nodeId: string; 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 node = nodeList.find((node) => node.nodeId === nodeId); @@ -135,7 +137,8 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle }: { 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 node = nodeList.find((node) => node.nodeId === nodeId); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/ToolHandle.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/ToolHandle.tsx index b5f0fe8b8..9eb2f76a9 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/ToolHandle.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/ToolHandle.tsx @@ -6,6 +6,7 @@ import { Connection, Handle, Position } from 'reactflow'; import { useCallback, useMemo } from 'react'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context'; +import { WorkflowActionContext } from '../../../../context/workflowInitContext'; const handleSize = '16px'; @@ -16,7 +17,7 @@ type ToolHandleProps = BoxProps & { export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => { const { t } = useTranslation(); 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; @@ -64,7 +65,7 @@ export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => { export const ToolSourceHandle = () => { const { t } = useTranslation(); - const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges); + const setEdges = useContextSelector(WorkflowActionContext, (v) => v.setEdges); /* onConnect edge, delete tool input and switch */ const onConnect = useCallback( diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/index.tsx index 106084e45..427f68c79 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/Handle/index.tsx @@ -5,6 +5,11 @@ import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../../../context'; import MyIcon from '@fastgpt/web/components/common/Icon'; +import { + WorkflowActionContext, + WorkflowInitContext +} from '../../../../context/workflowInitContext'; +import { WorkflowEventContext } from '../../../../context/workflowEventContext'; type Props = { nodeId: string; @@ -24,11 +29,10 @@ const MySourceHandle = React.memo(function MySourceHandle({ highlightStyle: Record; connectedStyle: Record; }) { + const edges = useContextSelector(WorkflowActionContext, (v) => v.edges); 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 nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes); + const hoverNodeId = useContextSelector(WorkflowEventContext, (v) => v.hoverNodeId); const node = useMemo(() => nodes.find((node) => node.data.nodeId === nodeId), [nodes, nodeId]); const connected = edges.some((edge) => edge.sourceHandle === handleId); @@ -142,7 +146,8 @@ const MyTargetHandle = React.memo(function MyTargetHandle({ highlightStyle: Record; connectedStyle: Record; }) { - 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); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx index 987d76a78..04e71ab57 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/NodeCard.tsx @@ -26,6 +26,8 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useWorkflowUtils } from '../../hooks/useUtils'; import { WholeResponseContent } from '@/components/core/chat/components/WholeResponseModal'; import { getDocPath } from '@/web/common/system/doc'; +import { WorkflowActionContext } from '../../../context/workflowInitContext'; +import { WorkflowEventContext } from '../../../context/workflowEventContext'; type Props = FlowNodeItemType & { children?: React.ReactNode | React.ReactNode[] | string; @@ -68,10 +70,10 @@ const NodeCard = (props: Props) => { customStyle } = 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 setHoverNodeId = useContextSelector(WorkflowEventContext, (v) => v.setHoverNodeId); // custom title edit const { onOpenModal: onOpenCustomTitleModal, EditModal: EditTitleModal } = useEditTitle({ @@ -391,7 +393,8 @@ const MenuRender = React.memo(function MenuRender({ const { t } = useTranslation(); 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 onCopyNode = useCallback( diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/Label.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/Label.tsx index 184dcb8ca..d8fb9eb49 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/Label.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/Label.tsx @@ -42,58 +42,41 @@ const InputLabel = ({ nodeId, input }: Props) => { }, [input, nodeId, onChangeNode, renderTypeList] ); + const renderType = renderTypeList?.[selectedTypeIndex || 0]; - const RenderLabel = useMemo(() => { - const renderType = renderTypeList?.[selectedTypeIndex || 0]; - - return ( - - - - {t(label as any)} - - {description && } - - {/* value type */} - {[FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.fileSelect].includes( - renderType - ) && } - - {/* input type select */} - {renderTypeList && renderTypeList.length > 1 && ( - - - - )} - - {/* Variable picker tip */} - {input.renderTypeList[input.selectedTypeIndex ?? 0] === FlowNodeInputTypeEnum.textarea && ( - <> - - - - )} + return ( + + + + {t(label as any)} + + {description && } - ); - }, [ - description, - input.renderTypeList, - input.selectedTypeIndex, - label, - onChangeRenderType, - renderTypeList, - required, - selectedTypeIndex, - t, - valueDesc, - valueType - ]); + {/* value type */} + {[FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.fileSelect].includes(renderType) && ( + + )} - return RenderLabel; + {/* input type select */} + {renderTypeList && renderTypeList.length > 1 && ( + + + + )} + + {/* Variable picker tip */} + {input.renderTypeList[input.selectedTypeIndex ?? 0] === FlowNodeInputTypeEnum.textarea && ( + <> + + + + )} + + ); }; export default React.memo(InputLabel); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/index.tsx index 2c95ae45c..bfb860016 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/index.tsx @@ -85,54 +85,45 @@ type Props = { const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props) => { const { feConfigs } = useSystemStore(); - const copyInputs = useMemo( - () => - JSON.stringify( - flowInputList.filter((input) => { - if (input.isPro && !feConfigs?.isPlus) return false; - return true; - }) - ), - [feConfigs?.isPlus, flowInputList] - ); const filterInputs = useMemo(() => { - return JSON.parse(copyInputs) as FlowNodeInputItemType[]; - }, [copyInputs]); - - const memoCustomComponent = useMemo(() => CustomComponent || {}, [CustomComponent]); - - const Render = useMemo(() => { - return filterInputs.map((input) => { - const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0]; - const isDynamic = !!input.canEdit; - - 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 ; - })(); - - return renderType !== FlowNodeInputTypeEnum.hidden && !isDynamic ? ( - - {!!input.label && !hideLabelTypeList.includes(renderType) && ( - - )} - {!!RenderComponent && ( - - {RenderComponent} - - )} - - ) : null; + return flowInputList.filter((input) => { + if (input.isPro && !feConfigs?.isPlus) return false; + return true; }); - }, [filterInputs, mb, memoCustomComponent, nodeId]); + }, [feConfigs?.isPlus, flowInputList]); - return <>{Render}; + return ( + <> + {filterInputs.map((input) => { + const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0]; + const isDynamic = !!input.canEdit; + + const RenderComponent = (() => { + if (renderType === FlowNodeInputTypeEnum.custom && CustomComponent?.[input.key]) { + return <>{CustomComponent?.[input.key]({ ...input })}; + } + + const Component = RenderList.find((item) => item.types.includes(renderType))?.Component; + + if (!Component) return null; + return ; + })(); + + return renderType !== FlowNodeInputTypeEnum.hidden && !isDynamic ? ( + + {!!input.label && !hideLabelTypeList.includes(renderType) && ( + + )} + {!!RenderComponent && ( + + {RenderComponent} + + )} + + ) : null; + })} + + ); }; export default React.memo(RenderInput); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/JsonEditor.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/JsonEditor.tsx index 21f4ac0f1..43e703fa4 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/JsonEditor.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/JsonEditor.tsx @@ -50,25 +50,21 @@ const JsonEditor = ({ inputs = [], item, nodeId }: RenderInputProps) => { } return JSON.stringify(item.value, null, 2); }, [item.value]); - - const Render = useMemo(() => { - return ( - { - update(e); - }} - variables={variables} - /> - ); - }, [item.placeholder, t, update, value, variables]); - - return Render; + console.log(12121); + return ( + { + update(e); + }} + variables={variables} + /> + ); }; export default React.memo(JsonEditor); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx index c07f2857a..f6e5389e6 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Reference.tsx @@ -18,6 +18,7 @@ import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { AppContext } from '@/pages/app/detail/components/context'; +import { WorkflowActionContext } from '../../../../../context/workflowInitContext'; const MultipleRowSelect = dynamic(() => import('@fastgpt/web/components/common/MySelect/MultipleRowSelect').then( @@ -59,8 +60,9 @@ export const useReference = ({ valueType?: WorkflowIOValueTypeEnum; }) => { const { t } = useTranslation(); - const { appDetail } = useContextSelector(AppContext, (v) => v); - const { nodeList, edges } = useContextSelector(WorkflowContext, (v) => v); + const appDetail = useContextSelector(AppContext, (v) => v.appDetail); + const edges = useContextSelector(WorkflowActionContext, (v) => v.edges); + const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); // 获取可选的变量列表 const referenceList = useMemo(() => { @@ -319,7 +321,7 @@ const MultipleReferenceSelector = ({ popDirection={popDirection} /> ); - }, [getSelectValue, list, onSelect, placeholder, popDirection, t, value]); + }, [getSelectValue, list, onSelect, placeholder, popDirection, value]); return ArraySelector; }; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/TextInput.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/TextInput.tsx index 2aeb00e58..b612512be 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/TextInput.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/TextInput.tsx @@ -7,12 +7,14 @@ import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponent import { useCreation } from 'ahooks'; import { AppContext } from '@/pages/app/detail/components/context'; import { getEditorVariables } from '../../../../../utils'; +import { WorkflowActionContext } from '../../../../../context/workflowInitContext'; const TextInputRender = ({ inputs = [], item, nodeId }: RenderInputProps) => { const { t } = useTranslation(); - const { nodeList, edges, onChangeNode } = useContextSelector(WorkflowContext, (v) => v); - - const { appDetail } = useContextSelector(AppContext, (v) => v); + const appDetail = useContextSelector(AppContext, (v) => v.appDetail); + const edges = useContextSelector(WorkflowActionContext, (v) => v.edges); + const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); + const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); // get variable const variables = useCreation(() => { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Textarea.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Textarea.tsx index 102f9908a..37dc6e481 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Textarea.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Textarea.tsx @@ -7,11 +7,12 @@ import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponent import { useCreation } from 'ahooks'; import { AppContext } from '@/pages/app/detail/components/context'; import { getEditorVariables } from '../../../../../utils'; +import { WorkflowActionContext } from '../../../../../context/workflowInitContext'; const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => { const { t } = useTranslation(); + const edges = useContextSelector(WorkflowActionContext, (v) => v.edges); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); - const edges = useContextSelector(WorkflowContext, (v) => v.edges); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const { appDetail } = useContextSelector(AppContext, (v) => v); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderOutput/Label.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderOutput/Label.tsx index d22421967..d2929bc73 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderOutput/Label.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderOutput/Label.tsx @@ -13,44 +13,40 @@ const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutpu const { t } = useTranslation(); const { label = '', description, valueType, valueDesc } = output; - const Render = useMemo(() => { - return ( - - + + - - {t(label as any)} - - {description && } - - - {output.type === FlowNodeOutputTypeEnum.source && ( - - )} - - ); - }, [output.type, output.key, t, label, description, valueType, valueDesc, nodeId]); - - return Render; + {t(label as any)} + + {description && } + + + {output.type === FlowNodeOutputTypeEnum.source && ( + + )} + + ); }; export default React.memo(OutputLabel); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/constants.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/constants.tsx index cbeedb840..47c9315a3 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/constants.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/constants.tsx @@ -2,6 +2,9 @@ import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import React from 'react'; import { DefaultEdgeOptions } from 'reactflow'; +export const minZoom = 0.1; +export const maxZoom = 1.5; + export const connectionLineStyle: React.CSSProperties = { strokeWidth: 2, stroke: '#487FFF' diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx similarity index 78% rename from projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx rename to projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx index 5f2998081..a6cf006f2 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/context.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx @@ -15,7 +15,7 @@ import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/wor 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 { useLocalStorageState, useMemoizedFn, useUpdateEffect } from 'ahooks'; import React, { Dispatch, SetStateAction, @@ -25,31 +25,40 @@ import React, { useRef, useState } from 'react'; -import { - Edge, - EdgeChange, - Node, - NodeChange, - OnConnectStartParams, - useEdgesState, - useNodesState, - useReactFlow -} from 'reactflow'; +import { Edge, Node, OnConnectStartParams, ReactFlowProvider, useReactFlow } from 'reactflow'; 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 { 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 '@/pages/app/detail/components/context'; -import ChatTest from './Flow/ChatTest'; +import ChatTest from '../Flow/ChatTest'; import { useDisclosure } from '@chakra-ui/react'; -import { uiWorkflow2StoreWorkflow } from './utils'; +import { uiWorkflow2StoreWorkflow } from '../utils'; import { useTranslation } from 'next-i18next'; import { formatTime2YMDHMS, formatTime2YMDHMW } from '@fastgpt/global/common/string/time'; import { cloneDeep } from 'lodash'; -import { SetState } from 'ahooks/lib/createUseStorageState'; 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 ( + + + + {children} + + + + ); +}; type OnChange = (changes: ChangesType[]) => void; @@ -65,33 +74,22 @@ type WorkflowContextType = { appId?: string; basicNodeTemplates: FlowNodeTemplateType[]; filterAppIds?: string[]; - reactFlowWrapper: React.RefObject | null; - mouseInCanvas: boolean; // nodes - nodes: Node[]; nodeList: FlowNodeItemType[]; - setNodes: Dispatch[]>>; - onNodesChange: OnChange; hasToolNode: boolean; - hoverNodeId?: string; - setHoverNodeId: React.Dispatch>; + onUpdateNodeError: (node: string, isError: Boolean) => void; onResetNode: (e: { id: string; node: FlowNodeTemplateType }) => void; onChangeNode: (e: FlowNodeChangeProps) => void; getNodeDynamicInputs: (nodeId: string) => FlowNodeInputItemType[]; // edges - edges: Edge[]; - setEdges: Dispatch[]>>; - onEdgesChange: OnChange; onDelEdge: (e: { nodeId: string; sourceHandle?: string | undefined; targetHandle?: string | undefined; }) => void; - hoverEdgeId?: string; - setHoverEdgeId: React.Dispatch>; onSwitchTmpVersion: (data: WorkflowSnapshotsType, customTitle: string) => boolean; onSwitchCloudVersion: (appVersion: AppVersionSchemaType) => boolean; @@ -102,6 +100,19 @@ type WorkflowContextType = { undo: () => void; canRedo: boolean; canUndo: boolean; + pushPastSnapshot: ({ + pastNodes, + pastEdges, + customTitle, + chatConfig, + isSaved + }: { + pastNodes: Node[]; + pastEdges: Edge[]; + customTitle?: string; + chatConfig: AppChatConfigType; + isSaved?: boolean; + }) => boolean; // connect connectingEdge?: OnConnectStartParams; @@ -159,10 +170,6 @@ type WorkflowContextType = { }) => Promise; onStopNodeDebug: () => void; - // version history - showHistoryModal: boolean; - setShowHistoryModal: React.Dispatch>; - // chat test setWorkflowTestData: React.Dispatch< React.SetStateAction< @@ -173,15 +180,6 @@ type WorkflowContextType = { | 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 = { @@ -198,32 +196,11 @@ export const WorkflowContext = createContext({ throw new Error('Function not implemented.'); }, basicNodeTemplates: [], - reactFlowWrapper: null, - nodes: [], nodeList: [], - mouseInCanvas: false, - setNodes: function ( - value: React.SetStateAction[]> - ): 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): 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[]>): 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.'); }, @@ -272,9 +249,6 @@ export const WorkflowContext = createContext({ onChangeNode: function (e: FlowNodeChangeProps): void { throw new Error('Function not implemented.'); }, - setHoverEdgeId: function (value: React.SetStateAction): void { - throw new Error('Function not implemented.'); - }, setWorkflowTestData: function ( value: React.SetStateAction< { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] } | undefined @@ -292,10 +266,6 @@ export const WorkflowContext = createContext({ | undefined { throw new Error('Function not implemented.'); }, - showHistoryModal: false, - setShowHistoryModal: function (value: React.SetStateAction): void { - throw new Error('Function not implemented.'); - }, getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] { throw new Error('Function not implemented.'); }, @@ -312,18 +282,27 @@ export const WorkflowContext = createContext({ }, canRedo: 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 { throw new Error('Function not implemented.'); }, onSwitchCloudVersion: function (appVersion: AppVersionSchemaType): boolean { 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.'); } }); @@ -337,39 +316,14 @@ const WorkflowContextProvider = ({ }) => { const { t } = useTranslation(); const { toast } = useToast(); - const reactFlowWrapper = useRef(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 [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 */ - const [edges, setEdges, onEdgesChange] = useEdgesState([]); - const [hoverEdgeId, setHoverEdgeId] = useState(); + const edges = useContextSelector(WorkflowActionContext, (state) => state.edges); + const setEdges = useContextSelector(WorkflowActionContext, (state) => state.setEdges); const onDelEdge = useCallback( ({ nodeId, @@ -397,32 +351,16 @@ const WorkflowContextProvider = ({ const [connectingEdge, setConnectingEdge] = useState(); /* node */ - const [nodes = [], setNodes, onNodesChange] = useNodesState([]); - const [hoverNodeId, setHoverNodeId] = useState(); + const setNodes = useContextSelector(WorkflowActionContext, (state) => state.setNodes); + 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( () => JSON.parse(nodeListString) as FlowNodeItemType[], [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(() => { return !!nodeList.find((node) => node.flowNodeType === FlowNodeTypeEnum.tools); }, [nodeList]); @@ -571,6 +509,7 @@ const WorkflowContextProvider = ({ /* ui flow to store data */ const { fitView } = useReactFlow(); const flowData2StoreDataAndCheck = useMemoizedFn((hideTip = false) => { + const nodes = getNodes(); const checkResults = checkWorkflowNodeAndConnection({ nodes, edges }); if (!checkResults) { @@ -593,6 +532,7 @@ const WorkflowContextProvider = ({ }); const flowData2StoreData = useMemoizedFn(() => { + const nodes = getNodes(); 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(() => { if (past[1]) { setFuture((future) => [past[0], ...future]); @@ -978,88 +902,80 @@ const WorkflowContextProvider = ({ ] ); - /* Version histories */ - const [showHistoryModal, setShowHistoryModal] = useState(false); + const value = useMemo( + () => ({ + appId, + basicNodeTemplates, - /* event bus */ - useEffect(() => { - eventBus.on(EventNameEnum.requestWorkflowStore, () => { - eventBus.emit(EventNameEnum.receiveWorkflowStore, { - nodes, - edges - }); - }); - return () => { - eventBus.off(EventNameEnum.requestWorkflowStore); - }; - }, [edges, nodes]); + // node + nodeList, + hasToolNode, + onUpdateNodeError, + onResetNode, + onChangeNode, + getNodeDynamicInputs, - const [menu, setMenu] = useState<{ top: number; left: number } | null>(null); + // edge + connectingEdge, + setConnectingEdge, + onDelEdge, - const value = { - appId, - reactFlowWrapper, - basicNodeTemplates, - workflowControlMode, - setWorkflowControlMode, - mouseInCanvas, + // snapshots + past, + setPast, + future, + undo, + redo, + canUndo: past.length > 1, + canRedo: !!future.length, + onSwitchTmpVersion, + onSwitchCloudVersion, + pushPastSnapshot, - // node - nodes, - setNodes, - onNodesChange, - nodeList, - hasToolNode, - hoverNodeId, - setHoverNodeId, - onUpdateNodeError, - onResetNode, - onChangeNode, - getNodeDynamicInputs, + // function + splitToolInputs, + initData, + flowData2StoreDataAndCheck, + flowData2StoreData, - // edge - edges, - setEdges, - hoverEdgeId, - setHoverEdgeId, - onEdgesChange, - connectingEdge, - setConnectingEdge, - onDelEdge, + // debug + workflowDebugData, + onNextNodeDebug, + onStartNodeDebug, + onStopNodeDebug, - // snapshots - past, - setPast, - future, - undo, - redo, - canUndo: past.length > 1, - canRedo: !!future.length, - onSwitchTmpVersion, - onSwitchCloudVersion, - - // function - splitToolInputs, - initData, - flowData2StoreDataAndCheck, - flowData2StoreData, - - // debug - workflowDebugData, - onNextNodeDebug, - onStartNodeDebug, - onStopNodeDebug, - - // version history - showHistoryModal, - setShowHistoryModal, - - // chat test - setWorkflowTestData, - - menu, - setMenu - }; + // chat test + setWorkflowTestData + }), + [ + 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 ( @@ -1068,18 +984,4 @@ const WorkflowContextProvider = ({ ); }; - -export default WorkflowContextProvider; - -type GetWorkflowStoreResponse = { - nodes: Node[]; - edges: Edge[]; -}; -export const getWorkflowStore = () => - new Promise((resolve) => { - eventBus.on(EventNameEnum.receiveWorkflowStore, (data: GetWorkflowStoreResponse) => { - resolve(data); - eventBus.off(EventNameEnum.receiveWorkflowStore); - }); - eventBus.emit(EventNameEnum.requestWorkflowStore); - }); +export default React.memo(WorkflowContextProvider); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/workflowEventContext.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/workflowEventContext.tsx new file mode 100644 index 000000000..8073b11be --- /dev/null +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/workflowEventContext.tsx @@ -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 | null; + hoverNodeId?: string; + setHoverNodeId: React.Dispatch>; + hoverEdgeId?: string; + setHoverEdgeId: React.Dispatch>; + 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>; +}; + +export const WorkflowEventContext = createContext({ + mouseInCanvas: false, + reactFlowWrapper: null, + setHoverNodeId: function (value: React.SetStateAction): void { + throw new Error('Function not implemented.'); + }, + setHoverEdgeId: function (value: React.SetStateAction): 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): void { + throw new Error('Function not implemented.'); + } +}); + +const WorkflowEventContextProvider = ({ children }: { children: ReactNode }) => { + // Watch mouse in canvas + const reactFlowWrapper = useRef(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(); + // Watch hover edge + const [hoverEdgeId, setHoverEdgeId] = useState(); + + 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 ( + {children} + ); +}; + +export default WorkflowEventContextProvider; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/workflowInitContext.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/workflowInitContext.tsx new file mode 100644 index 000000000..56019632d --- /dev/null +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/workflowInitContext.tsx @@ -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 = (changes: ChangesType[]) => void; + +type WorkflowInitContextType = { + nodes: Node[]; +}; +export const WorkflowInitContext = createContext({ + nodes: [] +}); + +type WorkflowActionContextType = { + setNodes: Dispatch[]>>; + onNodesChange: OnChange; + getNodes: () => Node[]; + nodeListString: string; + edges: Edge[]; + setEdges: Dispatch[]>>; + onEdgesChange: OnChange; +}; +export const WorkflowActionContext = createContext({ + setNodes: function ( + value: React.SetStateAction[]> + ): void { + throw new Error('Function not implemented.'); + }, + onNodesChange: function (changes: NodeChange[]): void { + throw new Error('Function not implemented.'); + }, + getNodes: function (): Node[] { + throw new Error('Function not implemented.'); + }, + nodeListString: JSON.stringify([]), + edges: [], + setEdges: function (value: React.SetStateAction[]>): 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([]); + 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 ( + + + {children} + + + ); +}; + +export default WorkflowInitContextProvider; diff --git a/projects/app/src/pages/app/detail/components/context.tsx b/projects/app/src/pages/app/detail/components/context.tsx index a25632fc3..40aeb7a94 100644 --- a/projects/app/src/pages/app/detail/components/context.tsx +++ b/projects/app/src/pages/app/detail/components/context.tsx @@ -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 { defaultApp } from '@/web/core/app/constants'; import { delAppById, getAppDetailById, putAppById } from '@/web/core/app/api'; @@ -186,22 +186,39 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => { [appDetail.name, deleteApp, openConfirmDel, t] ); - const contextValue: AppContextType = { - appId, - currentTab, - route2Tab, - appDetail, - setAppDetail, - loadingApp, - updateAppDetail, - onOpenInfoEdit, - onOpenTeamTagModal, - onDelApp, - onSaveApp, - appLatestVersion, - reloadAppLatestVersion, - reloadApp - }; + const contextValue: AppContextType = useMemo( + () => ({ + appId, + currentTab, + route2Tab, + appDetail, + setAppDetail, + loadingApp, + updateAppDetail, + onOpenInfoEdit, + onOpenTeamTagModal, + onDelApp, + onSaveApp, + appLatestVersion, + reloadAppLatestVersion, + reloadApp + }), + [ + appDetail, + appId, + appLatestVersion, + currentTab, + loadingApp, + onDelApp, + onOpenInfoEdit, + onOpenTeamTagModal, + onSaveApp, + reloadApp, + reloadAppLatestVersion, + route2Tab, + updateAppDetail + ] + ); return ( diff --git a/projects/app/src/web/common/utils/eventbus.ts b/projects/app/src/web/common/utils/eventbus.ts index 7bb7fe22f..8f21c04e8 100644 --- a/projects/app/src/web/common/utils/eventbus.ts +++ b/projects/app/src/web/common/utils/eventbus.ts @@ -1,9 +1,6 @@ export enum EventNameEnum { sendQuestion = 'sendQuestion', - editQuestion = 'editQuestion', - - requestWorkflowStore = 'requestWorkflowStore', - receiveWorkflowStore = 'receiveWorkflowStore' + editQuestion = 'editQuestion' } export const eventBus = {