From 12e1b82328cb018878608d4412af4987006939eb Mon Sep 17 00:00:00 2001 From: Minamiyama Date: Mon, 7 Jul 2025 18:28:35 +0800 Subject: [PATCH 1/5] feat(workflow): add node relations panel to display dependencies Introduce a new relations tab in workflow panel to visualize node dependencies and dependents. Includes: - New RelationType enum for dependency types - Internationalized UI text for multiple languages - Components for displaying relations with arrows and empty states - Logic to determine and display dependency relationships --- .../_base/components/workflow-panel/index.tsx | 26 ++++ .../workflow-panel/relations/container.tsx | 35 +++++ .../workflow-panel/relations/empty.tsx | 27 ++++ .../workflow-panel/relations/index.tsx | 121 ++++++++++++++++++ .../workflow-panel/relations/item.tsx | 59 +++++++++ .../workflow-panel/relations/line.tsx | 92 +++++++++++++ .../workflow-panel/relations/types.ts | 4 + .../_base/components/workflow-panel/tab.tsx | 2 + web/i18n/en-US/workflow.ts | 9 ++ web/i18n/ja-JP/workflow.ts | 9 ++ web/i18n/zh-Hans/workflow.ts | 9 ++ web/i18n/zh-Hant/workflow.ts | 13 +- 12 files changed, 404 insertions(+), 2 deletions(-) create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/container.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/empty.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/item.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/line.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/types.ts diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 164369e64c..e1b7364b07 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -59,6 +59,8 @@ import { useLogs } from '@/app/components/workflow/run/hooks' import PanelWrap from '../before-run-form/panel-wrap' import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel' import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' +import Relations from './relations' +import { RelationType } from './relations/types' type BasePanelProps = { children: ReactNode @@ -420,6 +422,30 @@ const BasePanel: FC = ({ {...passedLogParams} /> )} + + {tabType === TabType.relations && ( + <> +
+
+ {t('workflow.debug.relations.dependencies').toLocaleUpperCase()} +
+
+ {t('workflow.debug.relations.dependenciesDescription')} +
+ +
+ +
+
+ {t('workflow.debug.relations.dependents').toLocaleUpperCase()} +
+
+ {t('workflow.debug.relations.dependentsDescription')} +
+ +
+ + )} ) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/container.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/container.tsx new file mode 100644 index 0000000000..52802c45e3 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/container.tsx @@ -0,0 +1,35 @@ +import Empty from './empty' +import Item from './item' +import type { Node } from '@/app/components/workflow/types' +import cn from '@/utils/classnames' +import { RelationType } from './types' +import { useTranslation } from 'react-i18next' +type ContainerProps = { + nextNode?: Node + relationType: RelationType +} + +const Container = ({ + nextNode, + relationType, +}: ContainerProps) => { + const { t } = useTranslation() + return ( +
+ {nextNode && ( + + )} + {!nextNode && ( + + )} +
+ ) +} + +export default Container diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/empty.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/empty.tsx new file mode 100644 index 0000000000..c0ab918c57 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/empty.tsx @@ -0,0 +1,27 @@ +import { memo } from 'react' + +type EmptyProps = { + display: string +} + +const Empty = ({ + display = '', +}: EmptyProps) => { + return ( + <> +
+
+ {display} +
+
+ + ) +} + +export default memo(Empty) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx new file mode 100644 index 0000000000..656c88f4dd --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx @@ -0,0 +1,121 @@ +import { memo } from 'react' +import { isEqual } from 'lodash-es' +import { + getOutgoers, + useStore, +} from 'reactflow' +import { useToolIcon } from '@/app/components/workflow/hooks' +import BlockIcon from '@/app/components/workflow/block-icon' +import type { + Node, +} from '@/app/components/workflow/types' +import Line from './line' +import Container from './container' +import { getNodeUsedVars } from '../../variable/utils' +import { RelationType } from './types' + +type RelationsProps = { + selectedNode: Node + relationType: RelationType +} +const Relations = ({ + selectedNode, + relationType, +}: RelationsProps) => { + const data = selectedNode.data + const toolIcon = useToolIcon(data) + const edges = useStore(s => s.edges.map(edge => ({ + id: edge.id, + source: edge.source, + sourceHandle: edge.sourceHandle, + target: edge.target, + targetHandle: edge.targetHandle, + })), isEqual) + const nodes = useStore(s => s.getNodes().map(node => ({ + id: node.id, + data: node.data, + })), isEqual) + const workflowNodes = useStore(s => s.getNodes()) + + const list: Node[] = [] + + if (relationType === RelationType.dependencies) { + const usedVars = getNodeUsedVars(selectedNode) + const dependencyNodes: Node[] = [] + usedVars.forEach((valueSelector) => { + const node = workflowNodes.find(node => node.id === valueSelector?.[0]) + if (node) { + if (!dependencyNodes.includes(node)) + dependencyNodes.push(node) + } + }) + list.push(...dependencyNodes) + } + else { + const outgoers = getOutgoers(selectedNode as Node, nodes as Node[], edges) + for (let currIdx = 0; currIdx < outgoers.length; currIdx++) { + const node = outgoers[currIdx] + const outgoersForNode = getOutgoers(node, nodes as Node[], edges) + outgoersForNode.forEach((item) => { + const existed = outgoers.some(v => v.id === item.id) + if (!existed) + outgoers.push(item) + }) + } + + const dependentNodes: Node[] = [] + outgoers.forEach((node) => { + const usedVars = getNodeUsedVars(node) + const used = usedVars.some(v => v?.[0] === selectedNode.id) + if (used) { + const existed = dependentNodes.some(v => v.id === node.id) + if (!existed) + dependentNodes.push(node) + } + }) + list.push(...dependentNodes) + } + + const getThisNode = () => { + return ( +
+ +
+ ) + } + + const getLinkedNodes = () => { + return ( +
+ {list.length > 0 ? ( + list.map((item, index) => { + return ( + + ) + }) + ) : ( + + )} +
+ ) + } + + return ( +
+ {getThisNode()} + + {getLinkedNodes()} +
+ ) +} + +export default memo(Relations) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/item.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/item.tsx new file mode 100644 index 0000000000..253d6480e9 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/item.tsx @@ -0,0 +1,59 @@ +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import type { + CommonNodeType, +} from '@/app/components/workflow/types' +import BlockIcon from '@/app/components/workflow/block-icon' +import { + useNodesInteractions, + useNodesReadOnly, + useToolIcon, +} from '@/app/components/workflow/hooks' +import Button from '@/app/components/base/button' + +type ItemProps = { + nodeId: string + data: CommonNodeType +} +const Item = ({ + nodeId, + data, +}: ItemProps) => { + const { t } = useTranslation() + const { nodesReadOnly } = useNodesReadOnly() + const { handleNodeSelect } = useNodesInteractions() + const toolIcon = useToolIcon(data) + + return ( +
+ +
+ {data.title} +
+ { + !nodesReadOnly && ( + <> + + + ) + } +
+ ) +} + +export default memo(Item) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/line.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/line.tsx new file mode 100644 index 0000000000..1f2f5ff274 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/line.tsx @@ -0,0 +1,92 @@ +import { memo } from 'react' +import { RelationType } from './types' + +type LineProps = { + rowCount: number + relationType: RelationType +} +const Line = ({ + rowCount = 1, + relationType, +}: LineProps) => { + const list: number[] = [] + const listH = 40 + const ySpacing = 8 + + const svgHeight = listH * rowCount + ySpacing * (rowCount - 1) + + const lineWidth = 48 + const arrowStrokeWidth = 1 + const arrowLineLength = 6 + + return ( + + { + Array.from({ length: rowCount }).map((_, index) => { + const space = index * listH + index * ySpacing + 16 + return ( + + { + index === 0 && ( + <> + + + + + ) + } + { + index > 0 && ( + <> + + {relationType === RelationType.dependents && ( + + )} + + ) + } + + + ) + }) + } + + ) +} + +export default memo(Line) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/types.ts b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/types.ts new file mode 100644 index 0000000000..274de73ca5 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/types.ts @@ -0,0 +1,4 @@ +export enum RelationType { + dependencies, + dependents, +} diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx index 09d7ed266d..913ecdc758 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next' export enum TabType { settings = 'settings', lastRun = 'lastRun', + relations = 'relations', } type Props = { @@ -24,6 +25,7 @@ const Tab: FC = ({ items={[ { id: TabType.settings, name: t('workflow.debug.settingsTab').toLocaleUpperCase() }, { id: TabType.lastRun, name: t('workflow.debug.lastRunTab').toLocaleUpperCase() }, + { id: TabType.relations, name: t('workflow.debug.relationsTab').toLocaleUpperCase() }, ]} itemClassName='ml-0' value={value} diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index df0bb904fd..980d6e0de3 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -919,6 +919,7 @@ const translation = { debug: { settingsTab: 'Settings', lastRunTab: 'Last Run', + relationsTab: 'Relations', noData: { description: 'The results of the last run will be displayed here', runThisNode: 'Run this node', @@ -944,6 +945,14 @@ const translation = { chatNode: 'Conversation', systemNode: 'System', }, + relations: { + dependencies: 'Dependencies', + dependents: 'Dependents', + dependenciesDescription: 'Nodes that this node relies on', + dependentsDescription: 'Nodes that rely on this node', + noDependencies: 'No dependencies', + noDependents: 'No dependents', + }, }, } diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index 3c959669bf..bceec4e860 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -947,6 +947,15 @@ const translation = { }, settingsTab: '設定', lastRunTab: '最後の実行', + relationsTab: '関係', + relations: { + dependencies: '依存元', + dependents: '依存先', + dependenciesDescription: 'このノードが依存している他のノード', + dependentsDescription: 'このノードに依存している他のノード', + noDependencies: '依存元なし', + noDependents: '依存先なし', + }, }, } diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index aa9dcae261..96d56ae462 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -920,6 +920,7 @@ const translation = { debug: { settingsTab: '设置', lastRunTab: '上次运行', + relationsTab: '关系', noData: { description: '上次运行的结果将显示在这里', runThisNode: '运行此节点', @@ -945,6 +946,14 @@ const translation = { chatNode: '会话变量', systemNode: '系统变量', }, + relations: { + dependencies: '依赖', + dependents: '被依赖', + dependenciesDescription: '本节点依赖的其他节点', + dependentsDescription: '依赖于本节点的其他节点', + noDependencies: '无依赖', + noDependents: '无被依赖', + }, }, } diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index 8262e60351..417bc85dfe 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -921,6 +921,9 @@ const translation = { defaultName: '未命名版本', }, debug: { + settingsTab: '設定', + lastRunTab: '最後一次運行', + relationsTab: '關係', noData: { runThisNode: '運行此節點', description: '上次運行的結果將顯示在這裡', @@ -946,8 +949,14 @@ const translation = { emptyTip: '在畫布上逐步執行節點或逐步運行節點後,您可以在變數檢視中查看節點變數的當前值。', resetConversationVar: '將對話變數重置為默認值', }, - settingsTab: '設定', - lastRunTab: '最後一次運行', + relations: { + dependencies: '依賴', + dependents: '被依賴', + dependenciesDescription: '此節點所依賴的其他節點', + dependentsDescription: '依賴此節點的其他節點', + noDependencies: '無依賴', + noDependents: '無被依賴', + }, }, } From b1248792738f07d2ea2f185e0e2beb53fa7546f8 Mon Sep 17 00:00:00 2001 From: Mminamiyama Date: Sat, 12 Jul 2025 12:10:04 +0800 Subject: [PATCH 2/5] fix(workflow-panel): pass relationType prop to all Container instances Ensure consistent behavior by passing relationType prop to Container components in both conditional rendering cases --- .../nodes/_base/components/workflow-panel/relations/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx index 656c88f4dd..1edf8b6b0d 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx @@ -96,11 +96,12 @@ const Relations = ({ ) }) ) : ( - + )} ) From 4c27386ba018d4484bf3a989d8bf5726866c6166 Mon Sep 17 00:00:00 2001 From: Minamiyama Date: Mon, 7 Jul 2025 18:28:35 +0800 Subject: [PATCH 3/5] feat(workflow): add node relations panel to display dependencies Introduce a new relations tab in workflow panel to visualize node dependencies and dependents. Includes: - New RelationType enum for dependency types - Internationalized UI text for multiple languages - Components for displaying relations with arrows and empty states - Logic to determine and display dependency relationships --- .../_base/components/workflow-panel/index.tsx | 26 ++++ .../workflow-panel/relations/container.tsx | 35 +++++ .../workflow-panel/relations/empty.tsx | 27 ++++ .../workflow-panel/relations/index.tsx | 121 ++++++++++++++++++ .../workflow-panel/relations/item.tsx | 59 +++++++++ .../workflow-panel/relations/line.tsx | 92 +++++++++++++ .../workflow-panel/relations/types.ts | 4 + .../_base/components/workflow-panel/tab.tsx | 2 + web/i18n/en-US/workflow.ts | 9 ++ web/i18n/ja-JP/workflow.ts | 9 ++ web/i18n/zh-Hans/workflow.ts | 9 ++ web/i18n/zh-Hant/workflow.ts | 13 +- 12 files changed, 404 insertions(+), 2 deletions(-) create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/container.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/empty.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/item.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/line.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/relations/types.ts diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index b2a09e2f10..a0e812a98a 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -59,6 +59,8 @@ import { useLogs } from '@/app/components/workflow/run/hooks' import PanelWrap from '../before-run-form/panel-wrap' import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel' import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' +import Relations from './relations' +import { RelationType } from './relations/types' type BasePanelProps = { children: ReactNode @@ -426,6 +428,30 @@ const BasePanel: FC = ({ {...passedLogParams} /> )} + + {tabType === TabType.relations && ( + <> +
+
+ {t('workflow.debug.relations.dependencies').toLocaleUpperCase()} +
+
+ {t('workflow.debug.relations.dependenciesDescription')} +
+ +
+ +
+
+ {t('workflow.debug.relations.dependents').toLocaleUpperCase()} +
+
+ {t('workflow.debug.relations.dependentsDescription')} +
+ +
+ + )} ) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/container.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/container.tsx new file mode 100644 index 0000000000..52802c45e3 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/container.tsx @@ -0,0 +1,35 @@ +import Empty from './empty' +import Item from './item' +import type { Node } from '@/app/components/workflow/types' +import cn from '@/utils/classnames' +import { RelationType } from './types' +import { useTranslation } from 'react-i18next' +type ContainerProps = { + nextNode?: Node + relationType: RelationType +} + +const Container = ({ + nextNode, + relationType, +}: ContainerProps) => { + const { t } = useTranslation() + return ( +
+ {nextNode && ( + + )} + {!nextNode && ( + + )} +
+ ) +} + +export default Container diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/empty.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/empty.tsx new file mode 100644 index 0000000000..c0ab918c57 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/empty.tsx @@ -0,0 +1,27 @@ +import { memo } from 'react' + +type EmptyProps = { + display: string +} + +const Empty = ({ + display = '', +}: EmptyProps) => { + return ( + <> +
+
+ {display} +
+
+ + ) +} + +export default memo(Empty) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx new file mode 100644 index 0000000000..656c88f4dd --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx @@ -0,0 +1,121 @@ +import { memo } from 'react' +import { isEqual } from 'lodash-es' +import { + getOutgoers, + useStore, +} from 'reactflow' +import { useToolIcon } from '@/app/components/workflow/hooks' +import BlockIcon from '@/app/components/workflow/block-icon' +import type { + Node, +} from '@/app/components/workflow/types' +import Line from './line' +import Container from './container' +import { getNodeUsedVars } from '../../variable/utils' +import { RelationType } from './types' + +type RelationsProps = { + selectedNode: Node + relationType: RelationType +} +const Relations = ({ + selectedNode, + relationType, +}: RelationsProps) => { + const data = selectedNode.data + const toolIcon = useToolIcon(data) + const edges = useStore(s => s.edges.map(edge => ({ + id: edge.id, + source: edge.source, + sourceHandle: edge.sourceHandle, + target: edge.target, + targetHandle: edge.targetHandle, + })), isEqual) + const nodes = useStore(s => s.getNodes().map(node => ({ + id: node.id, + data: node.data, + })), isEqual) + const workflowNodes = useStore(s => s.getNodes()) + + const list: Node[] = [] + + if (relationType === RelationType.dependencies) { + const usedVars = getNodeUsedVars(selectedNode) + const dependencyNodes: Node[] = [] + usedVars.forEach((valueSelector) => { + const node = workflowNodes.find(node => node.id === valueSelector?.[0]) + if (node) { + if (!dependencyNodes.includes(node)) + dependencyNodes.push(node) + } + }) + list.push(...dependencyNodes) + } + else { + const outgoers = getOutgoers(selectedNode as Node, nodes as Node[], edges) + for (let currIdx = 0; currIdx < outgoers.length; currIdx++) { + const node = outgoers[currIdx] + const outgoersForNode = getOutgoers(node, nodes as Node[], edges) + outgoersForNode.forEach((item) => { + const existed = outgoers.some(v => v.id === item.id) + if (!existed) + outgoers.push(item) + }) + } + + const dependentNodes: Node[] = [] + outgoers.forEach((node) => { + const usedVars = getNodeUsedVars(node) + const used = usedVars.some(v => v?.[0] === selectedNode.id) + if (used) { + const existed = dependentNodes.some(v => v.id === node.id) + if (!existed) + dependentNodes.push(node) + } + }) + list.push(...dependentNodes) + } + + const getThisNode = () => { + return ( +
+ +
+ ) + } + + const getLinkedNodes = () => { + return ( +
+ {list.length > 0 ? ( + list.map((item, index) => { + return ( + + ) + }) + ) : ( + + )} +
+ ) + } + + return ( +
+ {getThisNode()} + + {getLinkedNodes()} +
+ ) +} + +export default memo(Relations) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/item.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/item.tsx new file mode 100644 index 0000000000..253d6480e9 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/item.tsx @@ -0,0 +1,59 @@ +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import type { + CommonNodeType, +} from '@/app/components/workflow/types' +import BlockIcon from '@/app/components/workflow/block-icon' +import { + useNodesInteractions, + useNodesReadOnly, + useToolIcon, +} from '@/app/components/workflow/hooks' +import Button from '@/app/components/base/button' + +type ItemProps = { + nodeId: string + data: CommonNodeType +} +const Item = ({ + nodeId, + data, +}: ItemProps) => { + const { t } = useTranslation() + const { nodesReadOnly } = useNodesReadOnly() + const { handleNodeSelect } = useNodesInteractions() + const toolIcon = useToolIcon(data) + + return ( +
+ +
+ {data.title} +
+ { + !nodesReadOnly && ( + <> + + + ) + } +
+ ) +} + +export default memo(Item) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/line.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/line.tsx new file mode 100644 index 0000000000..1f2f5ff274 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/line.tsx @@ -0,0 +1,92 @@ +import { memo } from 'react' +import { RelationType } from './types' + +type LineProps = { + rowCount: number + relationType: RelationType +} +const Line = ({ + rowCount = 1, + relationType, +}: LineProps) => { + const list: number[] = [] + const listH = 40 + const ySpacing = 8 + + const svgHeight = listH * rowCount + ySpacing * (rowCount - 1) + + const lineWidth = 48 + const arrowStrokeWidth = 1 + const arrowLineLength = 6 + + return ( + + { + Array.from({ length: rowCount }).map((_, index) => { + const space = index * listH + index * ySpacing + 16 + return ( + + { + index === 0 && ( + <> + + + + + ) + } + { + index > 0 && ( + <> + + {relationType === RelationType.dependents && ( + + )} + + ) + } + + + ) + }) + } + + ) +} + +export default memo(Line) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/types.ts b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/types.ts new file mode 100644 index 0000000000..274de73ca5 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/types.ts @@ -0,0 +1,4 @@ +export enum RelationType { + dependencies, + dependents, +} diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx index 09d7ed266d..913ecdc758 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next' export enum TabType { settings = 'settings', lastRun = 'lastRun', + relations = 'relations', } type Props = { @@ -24,6 +25,7 @@ const Tab: FC = ({ items={[ { id: TabType.settings, name: t('workflow.debug.settingsTab').toLocaleUpperCase() }, { id: TabType.lastRun, name: t('workflow.debug.lastRunTab').toLocaleUpperCase() }, + { id: TabType.relations, name: t('workflow.debug.relationsTab').toLocaleUpperCase() }, ]} itemClassName='ml-0' value={value} diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 763739ba32..1117e01120 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -941,6 +941,7 @@ const translation = { debug: { settingsTab: 'Settings', lastRunTab: 'Last Run', + relationsTab: 'Relations', noData: { description: 'The results of the last run will be displayed here', runThisNode: 'Run this node', @@ -966,6 +967,14 @@ const translation = { chatNode: 'Conversation', systemNode: 'System', }, + relations: { + dependencies: 'Dependencies', + dependents: 'Dependents', + dependenciesDescription: 'Nodes that this node relies on', + dependentsDescription: 'Nodes that rely on this node', + noDependencies: 'No dependencies', + noDependents: 'No dependents', + }, }, } diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index 04702194f8..bbe56fdf30 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -958,6 +958,15 @@ const translation = { }, settingsTab: '設定', lastRunTab: '最後の実行', + relationsTab: '関係', + relations: { + dependencies: '依存元', + dependents: '依存先', + dependenciesDescription: 'このノードが依存している他のノード', + dependentsDescription: 'このノードに依存している他のノード', + noDependencies: '依存元なし', + noDependents: '依存先なし', + }, }, } diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 6bd202d58f..c745dcd908 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -941,6 +941,7 @@ const translation = { debug: { settingsTab: '设置', lastRunTab: '上次运行', + relationsTab: '关系', noData: { description: '上次运行的结果将显示在这里', runThisNode: '运行此节点', @@ -966,6 +967,14 @@ const translation = { chatNode: '会话变量', systemNode: '系统变量', }, + relations: { + dependencies: '依赖', + dependents: '被依赖', + dependenciesDescription: '本节点依赖的其他节点', + dependentsDescription: '依赖于本节点的其他节点', + noDependencies: '无依赖', + noDependents: '无被依赖', + }, }, } diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index 1d29d2f5ab..956fecc53a 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -932,6 +932,9 @@ const translation = { defaultName: '未命名版本', }, debug: { + settingsTab: '設定', + lastRunTab: '最後一次運行', + relationsTab: '關係', noData: { runThisNode: '運行此節點', description: '上次運行的結果將顯示在這裡', @@ -957,8 +960,14 @@ const translation = { emptyTip: '在畫布上逐步執行節點或逐步運行節點後,您可以在變數檢視中查看節點變數的當前值。', resetConversationVar: '將對話變數重置為默認值', }, - settingsTab: '設定', - lastRunTab: '最後一次運行', + relations: { + dependencies: '依賴', + dependents: '被依賴', + dependenciesDescription: '此節點所依賴的其他節點', + dependentsDescription: '依賴此節點的其他節點', + noDependencies: '無依賴', + noDependents: '無被依賴', + }, }, } From 0de8e0e275e4fed7a90d578b07712482f93f23fc Mon Sep 17 00:00:00 2001 From: Mminamiyama Date: Sat, 12 Jul 2025 12:10:04 +0800 Subject: [PATCH 4/5] fix(workflow-panel): pass relationType prop to all Container instances Ensure consistent behavior by passing relationType prop to Container components in both conditional rendering cases --- .../nodes/_base/components/workflow-panel/relations/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx index 656c88f4dd..1edf8b6b0d 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/relations/index.tsx @@ -96,11 +96,12 @@ const Relations = ({ ) }) ) : ( - + )} ) From a293b78d8054d3c88b87bee41a8dc7b4e3a87115 Mon Sep 17 00:00:00 2001 From: Mminamiyama Date: Fri, 18 Jul 2025 14:21:01 +0800 Subject: [PATCH 5/5] feat(workflow): add node dimming and temporary edges for dependency visualization - Implement dimming of non-related nodes when Shift key is pressed to highlight dependencies - Add temporary dashed edges to visualize connections between selected node and its dependencies - Introduce _dimmed and _isTemp flags in node and edge types to control visual states - Add keyboard shortcuts (Shift key) to trigger dimming and undimming functionality --- web/app/components/workflow/custom-edge.tsx | 3 +- .../workflow/hooks/use-nodes-interactions.ts | 138 +++++++++++++++++- .../workflow/hooks/use-shortcuts.ts | 38 +++++ .../_base/components/variable/var-list.tsx | 2 +- .../components/workflow/nodes/_base/node.tsx | 1 + web/app/components/workflow/types.ts | 4 +- 6 files changed, 182 insertions(+), 4 deletions(-) diff --git a/web/app/components/workflow/custom-edge.tsx b/web/app/components/workflow/custom-edge.tsx index 4467b0adb5..4874fc700b 100644 --- a/web/app/components/workflow/custom-edge.tsx +++ b/web/app/components/workflow/custom-edge.tsx @@ -134,7 +134,8 @@ const CustomEdge = ({ style={{ stroke, strokeWidth: 2, - opacity: data._waitingRun ? 0.7 : 1, + opacity: data._dimmed ? 0.3 : (data._waitingRun ? 0.7 : 1), + strokeDasharray: data._isTemp ? '8 8' : undefined, }} /> diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index b598951adb..ef9070995b 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -1,5 +1,5 @@ import type { MouseEvent } from 'react' -import { useCallback, useRef } from 'react' +import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import produce from 'immer' import type { @@ -61,6 +61,7 @@ import { } from './use-workflow' import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' import useInspectVarsCrud from './use-inspect-vars-crud' +import { getNodeUsedVars } from '../nodes/_base/components/variable/utils' export const useNodesInteractions = () => { const { t } = useTranslation() @@ -1530,6 +1531,139 @@ export const useNodesInteractions = () => { setNodes(nodes) }, [redo, store, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly]) + const [isDimming, setIsDimming] = useState(false) + /** 让除 nodeId 以外的节点都加上 opacity-30 */ + const dimOtherNodes = useCallback(() => { + if (isDimming) + return + const { getNodes, setNodes, edges, setEdges } = store.getState() + const nodes = getNodes() + + const selectedNode = nodes.find(n => n.data.selected) + if (!selectedNode) + return + + setIsDimming(true) + + // const workflowNodes = useStore(s => s.getNodes()) + const workflowNodes = nodes + + const usedVars = getNodeUsedVars(selectedNode) + const dependencyNodes: Node[] = [] + usedVars.forEach((valueSelector) => { + const node = workflowNodes.find(node => node.id === valueSelector?.[0]) + if (node) { + if (!dependencyNodes.includes(node)) + dependencyNodes.push(node) + } + }) + + const outgoers = getOutgoers(selectedNode as Node, nodes as Node[], edges) + for (let currIdx = 0; currIdx < outgoers.length; currIdx++) { + const node = outgoers[currIdx] + const outgoersForNode = getOutgoers(node, nodes as Node[], edges) + outgoersForNode.forEach((item) => { + const existed = outgoers.some(v => v.id === item.id) + if (!existed) + outgoers.push(item) + }) + } + + const dependentNodes: Node[] = [] + outgoers.forEach((node) => { + const usedVars = getNodeUsedVars(node) + const used = usedVars.some(v => v?.[0] === selectedNode.id) + if (used) { + const existed = dependentNodes.some(v => v.id === node.id) + if (!existed) + dependentNodes.push(node) + } + }) + + const dimNodes = [...dependencyNodes, ...dependentNodes, selectedNode] + + const newNodes = produce(nodes, (draft) => { + console.log('==========selecteds: ') + draft.forEach((n) => { + const dimNode = dimNodes.find(v => v.id === n.id) + if (!dimNode) + n.data._dimmed = true + }) + console.log('====================end of selecteds') + }) + + setNodes(newNodes) + + /* == ② 生成临时连线 == */ + const tempEdges: Edge[] = [] + + dependencyNodes.forEach((n) => { + tempEdges.push({ + id: `tmp_${n.id}-source-${selectedNode.id}-target`, + type: CUSTOM_EDGE, // 复用自定义 Edge,也可以写一个 DIM_EDGE + source: n.id, // 依赖 → 选中 + sourceHandle: 'source_tmp', + target: selectedNode.id, + targetHandle: 'target_tmp', + animated: true, + data: { + sourceType: n.data.type, + targetType: selectedNode.data.type, + _isTemp: true, + _connectedNodeIsHovering: true, + }, + }) + }) + dependentNodes.forEach((n) => { + tempEdges.push({ + id: `tmp_${selectedNode.id}-source-${n.id}-target`, + type: CUSTOM_EDGE, // 复用自定义 Edge,也可以写一个 DIM_EDGE + source: selectedNode.id, // 依赖 → 选中 + sourceHandle: 'source_tmp', + target: n.id, + targetHandle: 'target_tmp', + animated: true, + data: { + sourceType: selectedNode.data.type, + targetType: n.data.type, + _isTemp: true, + _connectedNodeIsHovering: true, + }, + }) + }) + + const newEdges = produce(edges, (draft) => { + draft.forEach((e) => { + e.data._dimmed = true + }) + draft.push(...tempEdges) + }) + setEdges(newEdges) + }, [isDimming, store]) + + /** 把所有节点恢复为不透明 */ + const undimAllNodes = useCallback(() => { + const { getNodes, setNodes, edges, setEdges } = store.getState() + const nodes = getNodes() + setIsDimming(false) + + const newNodes = produce(nodes, (draft) => { + draft.forEach((n) => { + n.data._dimmed = false + // handleNodeLeave(null as unknown as MouseEvent, n) + }) + }) + + setNodes(newNodes) + + const newEdges = produce(edges.filter(e => !e.data._isTemp), (draft) => { + draft.forEach((e) => { + e.data._dimmed = false + }) + }) + setEdges(newEdges) + }, [store]) + return { handleNodeDragStart, handleNodeDrag, @@ -1554,5 +1688,7 @@ export const useNodesInteractions = () => { handleNodeDisconnect, handleHistoryBack, handleHistoryForward, + dimOtherNodes, + undimAllNodes, } } diff --git a/web/app/components/workflow/hooks/use-shortcuts.ts b/web/app/components/workflow/hooks/use-shortcuts.ts index 118ec94058..f91dcf6b2c 100644 --- a/web/app/components/workflow/hooks/use-shortcuts.ts +++ b/web/app/components/workflow/hooks/use-shortcuts.ts @@ -25,6 +25,8 @@ export const useShortcuts = (): void => { handleNodesDelete, handleHistoryBack, handleHistoryForward, + dimOtherNodes, + undimAllNodes, } = useNodesInteractions() const { handleStartWorkflowRun } = useWorkflowStartRun() const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore() @@ -211,4 +213,40 @@ export const useShortcuts = (): void => { exactMatch: true, useCapture: true, }) + + // Shift ↓(按下) + useKeyPress( + 'shift', + (e) => { + console.log('Shift down', e) + if (shouldHandleShortcut(e)) { + // handleSelectedNodeHighlight() + dimOtherNodes() + } + }, + { + exactMatch: true, + useCapture: true, + events: ['keydown'], + }, + ) + + // Shift ↑ + useKeyPress( + (e) => { + return e.key === 'Shift' + }, + (e) => { + if (shouldHandleShortcut(e)) { + console.log('Shift up 2: ', e) + // handleSelectedNodeUnhighlight() + undimAllNodes() + } + }, + { + exactMatch: true, + useCapture: true, + events: ['keyup'], + }, + ) } diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx index b1a8d52a05..2ff8661598 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx @@ -76,7 +76,7 @@ const VarList: FC = ({ }) onChange(newList) } - }, [list, onVarNameChange, onChange]) + }, [list, onVarNameChange, onChange, t]) const handleVarReferenceChange = useCallback((index: number) => { return (value: ValueSelector | string, varKindType: VarKindType, varInfo?: Var) => { diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index b969702dd7..058ef836bc 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -142,6 +142,7 @@ const BaseNode: FC = ({ showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent', !showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight', data._waitingRun && 'opacity-70', + data._dimmed && 'opacity-30', )} ref={nodeRef} style={{ diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 5f36956798..f5bb4dd14d 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -94,6 +94,7 @@ export type CommonNodeType = { retry_config?: WorkflowRetryConfig default_value?: DefaultValueForm[] credential_id?: string + _dimmed?: boolean } & T & Partial> export type CommonEdgeType = { @@ -109,7 +110,8 @@ export type CommonEdgeType = { isInLoop?: boolean loop_id?: string sourceType: BlockEnum - targetType: BlockEnum + targetType: BlockEnum, + _isTemp?: boolean, } export type Node = ReactFlowNode>