diff --git a/packages/global/core/workflow/type/node.d.ts b/packages/global/core/workflow/type/node.d.ts index a5f121418..239408f12 100644 --- a/packages/global/core/workflow/type/node.d.ts +++ b/packages/global/core/workflow/type/node.d.ts @@ -104,6 +104,7 @@ export type FlowNodeItemType = FlowNodeTemplateType & { response?: ChatHistoryItemResType; isExpired?: boolean; }; + isFolded?: boolean; }; // store node type diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index abcc7400d..0696b6b7f 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -123,6 +123,7 @@ export const iconPaths = { 'core/chat/chatLight': () => import('./icons/core/chat/chatLight.svg'), 'core/chat/chatModelTag': () => import('./icons/core/chat/chatModelTag.svg'), 'core/chat/chevronDown': () => import('./icons/core/chat/chevronDown.svg'), + 'core/chat/chevronRight': () => import('./icons/core/chat/chevronRight.svg'), 'core/chat/chevronSelector': () => import('./icons/core/chat/chevronSelector.svg'), 'core/chat/chevronUp': () => import('./icons/core/chat/chevronUp.svg'), 'core/chat/export/pdf': () => import('./icons/core/chat/export/pdf.svg'), diff --git a/packages/web/components/common/Icon/icons/core/chat/chevronRight.svg b/packages/web/components/common/Icon/icons/core/chat/chevronRight.svg new file mode 100644 index 000000000..b6724706e --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/chat/chevronRight.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file 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 0f0e956f0..bf4032dbd 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 @@ -1,6 +1,6 @@ import React, { useCallback, useMemo } from 'react'; import { BezierEdge, getBezierPath, EdgeLabelRenderer, EdgeProps } from 'reactflow'; -import { Flex } from '@chakra-ui/react'; +import { Box, Flex } from '@chakra-ui/react'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/workflow/constants'; import { useContextSelector } from 'use-context-selector'; @@ -27,6 +27,16 @@ const ButtonEdge = (props: EdgeProps) => { targetHandleId, style } = props; + + const parentNode = useMemo(() => { + for (const node of nodeList) { + if ((node.nodeId === source || node.nodeId === target) && node.parentNodeId) { + return nodeList.find((parent) => parent.nodeId === node.parentNodeId); + } + } + return undefined; + }, [nodeList, source, target]); + const defaultZIndex = useMemo( () => (nodeList.find((node) => node.nodeId === source && node.parentNodeId) ? 1001 : 0), [nodeList, source] @@ -127,55 +137,59 @@ const ButtonEdge = (props: EdgeProps) => { })(); return ( - onDelConnect(id)} - > - - - {!isToolEdge && ( + ); }, [ + parentNode?.isFolded, isHover, highlightEdge, labelX, labelY, isToolEdge, + defaultZIndex, edgeColor, targetPosition, newTargetX, @@ -214,7 +228,8 @@ const ButtonEdge = (props: EdgeProps) => { targetY={newTargetY} style={{ ...edgeStyle, - stroke: edgeColor + stroke: edgeColor, + display: parentNode?.isFolded ? 'none' : 'block' }} /> ); @@ -227,7 +242,8 @@ const ButtonEdge = (props: EdgeProps) => { source, target, style, - highlightEdge + highlightEdge, + parentNode?.isFolded ]); return ( 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 3685d99a7..d46f9dd3f 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 @@ -1,5 +1,13 @@ -import React, { useMemo } from 'react'; -import { Background, ControlButton, MiniMap, Panel, useReactFlow, useViewport } from 'reactflow'; +import React, { useCallback, useMemo } from 'react'; +import { + Background, + ControlButton, + MiniMap, + MiniMapNodeProps, + Panel, + useReactFlow, + useViewport +} from 'reactflow'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../context'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; @@ -26,7 +34,8 @@ const FlowController = React.memo(function FlowController() { canUndo, workflowControlMode, setWorkflowControlMode, - mouseInCanvas + mouseInCanvas, + nodeList } = useContextSelector(WorkflowContext, (v) => v); const { t } = useTranslation(); @@ -52,6 +61,20 @@ const FlowController = React.memo(function FlowController() { zoomOut(); }); + const MiniMapNode = useCallback( + ({ x, y, width, height, color, id }: MiniMapNodeProps) => { + const node = nodeList.find((node) => node.nodeId === id); + const parentNode = nodeList.find((n) => n.nodeId === node?.parentNodeId); + + if (parentNode?.isFolded) { + return null; + } + + return ; + }, + [nodeList] + ); + const Render = useMemo(() => { return ( <> @@ -64,6 +87,7 @@ const FlowController = React.memo(function FlowController() { boxShadow: '0px 0px 1px rgba(19, 51, 107, 0.10), 0px 4px 10px rgba(19, 51, 107, 0.10)' }} pannable + nodeComponent={MiniMapNode} /> ); }, [ + MiniMapNode, workflowControlMode, - isMac, t, + isMac, undo, canUndo, redo, 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 cc77eea98..69818409c 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 @@ -13,7 +13,8 @@ import { getNodesBounds, Rect, NodeRemoveChange, - NodeSelectionChange + NodeSelectionChange, + Position } from 'reactflow'; import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import 'reactflow/dist/style.css'; @@ -30,6 +31,8 @@ import { Input_Template_Node_Width } from '@fastgpt/global/core/workflow/template/input'; import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; +import { getHandleId } from '@fastgpt/global/core/workflow/utils'; +import { IfElseResultEnum } from '@fastgpt/global/core/workflow/template/system/ifElse/constant'; /* Compute helper lines for snapping nodes to each other @@ -367,8 +370,8 @@ export const useWorkflow = () => { const checkNodeOverLoopNode = useMemoizedFn((node: Node) => { if (!node) return; - // 获取所有与当前节点相交的节点 - const intersections = getIntersectingNodes(node); + // 获取所有与当前节点相交的节点,不包含折叠的节点 + const intersections = getIntersectingNodes(node).filter((node) => !node.data.isFolded); // 获取所有与当前节点相交的节点中,类型为 loop 的节点 const parentNode = intersections.find((item) => item.type === FlowNodeTypeEnum.loop); @@ -544,9 +547,19 @@ export const useWorkflow = () => { /* connect */ const onConnectStart = useCallback( (event: any, params: OnConnectStartParams) => { + if (!params.nodeId) return; + const sourceNode = nodes.find((node) => node.id === params.nodeId); + if (sourceNode?.data.isFolded) { + return onChangeNode({ + nodeId: params.nodeId, + type: 'attr', + key: 'isFolded', + value: false + }); + } setConnectingEdge(params); }, - [setConnectingEdge] + [nodes, setConnectingEdge, onChangeNode] ); const onConnectEnd = useCallback(() => { setConnectingEdge(undefined); @@ -581,7 +594,7 @@ export const useWorkflow = () => { connect }); }, - [onConnect, t, toast] + [onConnect, t, toast, nodes] ); /* edge */ diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx index 17503589e..3c70c8a68 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx @@ -22,7 +22,7 @@ import { WorkflowContext } from '../../../context'; const NodeLoop = ({ data, selected }: NodeProps) => { const { t } = useTranslation(); - const { nodeId, inputs, outputs } = data; + const { nodeId, inputs, outputs, isFolded } = data; const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v); const { nodeWidth, nodeHeight } = useMemo(() => { @@ -53,14 +53,14 @@ const NodeLoop = ({ data, selected }: NodeProps) => { return ( diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx index 4d39ccf8f..4c4d4f564 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx @@ -94,17 +94,15 @@ const NodeLoopStart = ({ data, selected }: NodeProps) => { - + {!loopItemInputType ? ( - + ) : ( diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeUserSelect.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeUserSelect.tsx index 750d9211a..7004cdf92 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeUserSelect.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodeUserSelect.tsx @@ -56,11 +56,6 @@ const NodeUserSelect = ({ data, selected }: NodeProps) => { value: options.filter((input) => input.key !== item.key) } }); - onChangeNode({ - nodeId, - type: 'delOutput', - key: item.key - }); }} /> 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 f98720bf7..0b53e7f63 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 @@ -6,7 +6,13 @@ import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../../../context'; -export const ConnectionSourceHandle = ({ nodeId }: { nodeId: string }) => { +export const ConnectionSourceHandle = ({ + nodeId, + isFoldNode +}: { + nodeId: string; + isFoldNode?: boolean; +}) => { const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const edges = useContextSelector(WorkflowContext, (v) => v.edges); @@ -26,8 +32,7 @@ export const ConnectionSourceHandle = ({ nodeId }: { nodeId: string }) => { const rightTargetConnected = edges.some( (edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Right) ); - - if (!node || !node?.sourceHandle?.right || rightTargetConnected) return null; + if (!isFoldNode && (!node || !node?.sourceHandle?.right || rightTargetConnected)) return null; return ( { TopHandlee, BottomHandlee }; - }, [connectingEdge, edges, nodeId, nodeList]); + }, [connectingEdge, edges, nodeId, nodeList, isFoldNode]); return showSourceHandle ? ( <> 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 f70a8820d..32cb584d8 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 @@ -32,8 +32,8 @@ const MySourceHandle = React.memo(function MySourceHandle({ const node = useMemo(() => nodes.find((node) => node.data.nodeId === nodeId), [nodes, nodeId]); const connected = edges.some((edge) => edge.sourceHandle === handleId); + const nodeFolded = node?.data.isFolded && edges.some((edge) => edge.source === nodeId); const nodeIsHover = hoverNodeId === nodeId; - const active = useMemo( () => nodeIsHover || node?.selected || connectingEdge?.handleId === handleId, [nodeIsHover, node?.selected, connectingEdge, handleId] @@ -73,7 +73,7 @@ const MySourceHandle = React.memo(function MySourceHandle({ }; } - if (connected) { + if (connected || nodeFolded) { return { styles: { ...connectedStyle, @@ -89,7 +89,7 @@ const MySourceHandle = React.memo(function MySourceHandle({ styles: undefined, showAddIcon: false }; - }, [active, connected, highlightStyle, translateStr, transform, connectedStyle]); + }, [active, connected, nodeFolded, highlightStyle, translateStr, transform, connectedStyle]); const RenderHandle = useMemo(() => { return ( 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 27557c001..a96e08a9c 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 @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { Box, Button, Card, Flex, Image } from '@chakra-ui/react'; import MyIcon from '@fastgpt/web/components/common/Icon'; import Avatar from '@fastgpt/web/components/common/Avatar'; @@ -61,7 +61,8 @@ const NodeCard = (props: Props) => { menuForbid, isTool = false, isError = false, - debugResult + debugResult, + isFolded } = props; const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); @@ -81,7 +82,12 @@ const NodeCard = (props: Props) => { [isTool, nodeList] ); - const node = nodeList.find((node) => node.nodeId === nodeId); + const { node, parentNode } = useMemo(() => { + const node = nodeList.find((node) => node.nodeId === nodeId); + const parentNode = nodeList.find((n) => n.nodeId === node?.parentNodeId); + return { node, parentNode }; + }, [nodeList, nodeId]); + const { openConfirm: onOpenConfirmSync, ConfirmModal: ConfirmSyncModal } = useConfirm({ content: t('app:module.Confirm Sync') }); @@ -155,6 +161,31 @@ const NodeCard = (props: Props) => { {/* avatar and name */} + { + onChangeNode({ + nodeId, + type: 'attr', + key: 'isFolded', + value: !isFolded + }); + }} + > + {!isFolded ? ( + + ) : ( + + )} + {t(name as any)} @@ -250,19 +281,21 @@ const NodeCard = (props: Props) => { ConfirmSyncModal, onOpenCustomTitleModal, onChangeNode, - toast + toast, + isFolded ]); const RenderHandle = useMemo(() => { return ( <> - + ); - }, [nodeId]); + }, [nodeId, isFolded]); return (