diff --git a/packages/frontend/editor-ui/src/components/RunDataAi/RunDataAi.vue b/packages/frontend/editor-ui/src/components/RunDataAi/RunDataAi.vue index 1c93087b51..5c4ed99e44 100644 --- a/packages/frontend/editor-ui/src/components/RunDataAi/RunDataAi.vue +++ b/packages/frontend/editor-ui/src/components/RunDataAi/RunDataAi.vue @@ -31,11 +31,20 @@ const selectedRun: Ref = ref([]); const i18n = useI18n(); const aiData = computed(() => - createAiData(props.node.name, props.workflow, workflowsStore.getWorkflowResultDataByNodeName), + createAiData( + props.node.name, + props.workflow.connectionsBySourceNode, + workflowsStore.getWorkflowResultDataByNodeName, + ), ); const executionTree = computed(() => - getTreeNodeData(props.node.name, props.workflow, aiData.value, props.runIndex), + getTreeNodeData( + props.node.name, + props.workflow.connectionsBySourceNode, + aiData.value, + props.runIndex, + ), ); function isTreeNodeSelected(node: TreeNode) { diff --git a/packages/frontend/editor-ui/src/components/RunDataAi/utils.test.ts b/packages/frontend/editor-ui/src/components/RunDataAi/utils.test.ts index 3f2f95e42d..74b28a6223 100644 --- a/packages/frontend/editor-ui/src/components/RunDataAi/utils.test.ts +++ b/packages/frontend/editor-ui/src/components/RunDataAi/utils.test.ts @@ -89,8 +89,12 @@ describe(getTreeNodeData, () => { expect( getTreeNodeData( 'A', - workflow, - createAiData('A', workflow, (name) => taskDataByNodeName[name] ?? null), + workflow.connectionsBySourceNode, + createAiData( + 'A', + workflow.connectionsBySourceNode, + (name) => taskDataByNodeName[name] ?? null, + ), 0, ), ).toEqual([ @@ -215,10 +219,14 @@ describe(getTreeNodeData, () => { }), ], }; - const aiData = createAiData('A', workflow, (name) => taskDataByNodeName[name] ?? null); + const aiData = createAiData( + 'A', + workflow.connectionsBySourceNode, + (name) => taskDataByNodeName[name] ?? null, + ); - const treeRun0 = getTreeNodeData('A', workflow, aiData, 0); - const treeRun1 = getTreeNodeData('A', workflow, aiData, 1); + const treeRun0 = getTreeNodeData('A', workflow.connectionsBySourceNode, aiData, 0); + const treeRun1 = getTreeNodeData('A', workflow.connectionsBySourceNode, aiData, 1); expect(treeRun0).toHaveLength(1); expect(treeRun0[0].node).toBe('A'); @@ -293,13 +301,23 @@ describe(getTreeNodeData, () => { const aiData = [sharedSubNodeData1, sharedSubNodeData2]; // Test for RootNode1 - should only show SharedSubNode with source RootNode1 - const rootNode1Tree = getTreeNodeData('RootNode1', workflowWithSharedSubNode, aiData, 0); + const rootNode1Tree = getTreeNodeData( + 'RootNode1', + workflowWithSharedSubNode.connectionsBySourceNode, + aiData, + 0, + ); expect(rootNode1Tree[0].children.length).toBe(1); expect(rootNode1Tree[0].children[0].node).toBe('SharedSubNode'); expect(rootNode1Tree[0].children[0].runIndex).toBe(0); // Test for RootNode2 - should only show SharedSubNode with source RootNode2 - const rootNode2Tree = getTreeNodeData('RootNode2', workflowWithSharedSubNode, aiData, 0); + const rootNode2Tree = getTreeNodeData( + 'RootNode2', + workflowWithSharedSubNode.connectionsBySourceNode, + aiData, + 0, + ); expect(rootNode2Tree[0].children.length).toBe(1); expect(rootNode2Tree[0].children[0].node).toBe('SharedSubNode'); expect(rootNode2Tree[0].children[0].runIndex).toBe(1); @@ -352,13 +370,13 @@ describe(getTreeNodeData, () => { const aiData = [subNodeData1, subNodeData2]; // Test for run #1 of RootNode - should only show SubNode with source run index 0 - const rootNode1Tree = getTreeNodeData('RootNode', workflow, aiData, 0); + const rootNode1Tree = getTreeNodeData('RootNode', workflow.connectionsBySourceNode, aiData, 0); expect(rootNode1Tree[0].children.length).toBe(1); expect(rootNode1Tree[0].children[0].node).toBe('SubNode'); expect(rootNode1Tree[0].children[0].runIndex).toBe(0); // Test for run #2 of RootNode - should only show SubNode with source run index 1 - const rootNode2Tree = getTreeNodeData('RootNode', workflow, aiData, 1); + const rootNode2Tree = getTreeNodeData('RootNode', workflow.connectionsBySourceNode, aiData, 1); expect(rootNode2Tree[0].children.length).toBe(1); expect(rootNode2Tree[0].children[0].node).toBe('SubNode'); expect(rootNode2Tree[0].children[0].runIndex).toBe(1); @@ -395,7 +413,7 @@ describe(getTreeNodeData, () => { const aiData = [subNodeData]; // Test for RootNode - should still show SubNode even without source info - const rootNodeTree = getTreeNodeData('RootNode', workflow, aiData, 0); + const rootNodeTree = getTreeNodeData('RootNode', workflow.connectionsBySourceNode, aiData, 0); expect(rootNodeTree[0].children.length).toBe(1); expect(rootNodeTree[0].children[0].node).toBe('SubNode'); expect(rootNodeTree[0].children[0].runIndex).toBe(0); @@ -432,7 +450,7 @@ describe(getTreeNodeData, () => { const aiData = [subNodeData]; // Test for RootNode - should still show SubNode even with empty source array - const rootNodeTree = getTreeNodeData('RootNode', workflow, aiData, 0); + const rootNodeTree = getTreeNodeData('RootNode', workflow.connectionsBySourceNode, aiData, 0); expect(rootNodeTree[0].children.length).toBe(1); expect(rootNodeTree[0].children[0].node).toBe('SubNode'); expect(rootNodeTree[0].children[0].runIndex).toBe(0); @@ -468,7 +486,7 @@ describe(getTreeNodeData, () => { // Create test AI data array const aiData = [subNodeData]; - const rootNodeTree = getTreeNodeData('RootNode', workflow, aiData, 0); + const rootNodeTree = getTreeNodeData('RootNode', workflow.connectionsBySourceNode, aiData, 0); expect(rootNodeTree[0].children.length).toBe(1); expect(rootNodeTree[0].children[0].node).toBe('SubNode'); @@ -564,7 +582,7 @@ describe(getTreeNodeData, () => { const aiData = [sharedSubNodeData1, sharedSubNodeData2, deepSubNodeData1, deepSubNodeData2]; // Test filtering for RootNode1 - const rootNode1Tree = getTreeNodeData('RootNode1', workflow, aiData, 0); + const rootNode1Tree = getTreeNodeData('RootNode1', workflow.connectionsBySourceNode, aiData, 0); expect(rootNode1Tree[0].children.length).toBe(1); expect(rootNode1Tree[0].children[0].node).toBe('SharedSubNode'); expect(rootNode1Tree[0].children[0].runIndex).toBe(0); @@ -573,7 +591,7 @@ describe(getTreeNodeData, () => { expect(rootNode1Tree[0].children[0].children[0].runIndex).toBe(0); // Test filtering for RootNode2 - const rootNode2Tree = getTreeNodeData('RootNode2', workflow, aiData, 0); + const rootNode2Tree = getTreeNodeData('RootNode2', workflow.connectionsBySourceNode, aiData, 0); expect(rootNode2Tree[0].children.length).toBe(1); expect(rootNode2Tree[0].children[0].node).toBe('SharedSubNode'); expect(rootNode2Tree[0].children[0].runIndex).toBe(1); diff --git a/packages/frontend/editor-ui/src/components/RunDataAi/utils.ts b/packages/frontend/editor-ui/src/components/RunDataAi/utils.ts index 45e8c81982..587ab5e16a 100644 --- a/packages/frontend/editor-ui/src/components/RunDataAi/utils.ts +++ b/packages/frontend/editor-ui/src/components/RunDataAi/utils.ts @@ -1,16 +1,17 @@ import { type LlmTokenUsageData, type IAiDataContent } from '@/Interface'; import { addTokenUsageData, emptyTokenUsageData } from '@/utils/aiUtils'; -import { - type INodeExecutionData, - type ITaskData, - type ITaskDataConnections, - type NodeConnectionType, - type Workflow, +import type { + IConnections, + INodeExecutionData, + ITaskData, + ITaskDataConnections, + NodeConnectionType, } from 'n8n-workflow'; import { splitTextBySearch } from '@/utils/stringUtils'; import { escapeHtml } from 'xss'; import type MarkdownIt from 'markdown-it'; import { unescapeAll } from 'markdown-it/lib/common/utils'; +import * as workflowUtils from 'n8n-workflow/common'; export interface AIResult { node: string; @@ -51,26 +52,28 @@ function createNode( export function getTreeNodeData( nodeName: string, - workflow: Workflow, + connectionsBySourceNode: IConnections, aiData: AIResult[] | undefined, runIndex: number, ): TreeNode[] { - return getTreeNodeDataRec(undefined, nodeName, 0, workflow, aiData, runIndex); + return getTreeNodeDataRec(undefined, nodeName, 0, connectionsBySourceNode, aiData, runIndex); } function getTreeNodeDataRec( parent: TreeNode | undefined, nodeName: string, currentDepth: number, - workflow: Workflow, + connectionsBySourceNode: IConnections, aiData: AIResult[] | undefined, runIndex: number, ): TreeNode[] { - const connections = workflow.connectionsByDestinationNode[nodeName]; + const connectionsByDestinationNode = + workflowUtils.mapConnectionsByDestination(connectionsBySourceNode); + const nodeConnections = connectionsByDestinationNode[nodeName]; const resultData = aiData?.filter((data) => data.node === nodeName && runIndex === data.runIndex) ?? []; - if (!connections) { + if (!nodeConnections) { return resultData.map((d) => createNode(parent, nodeName, currentDepth, d.runIndex, d)); } @@ -88,14 +91,26 @@ function getTreeNodeDataRec( }); // Get the first level of children - const connectedSubNodes = workflow.getParentNodes(nodeName, 'ALL_NON_MAIN', 1); + const connectedSubNodes = workflowUtils.getParentNodes( + connectionsByDestinationNode, + nodeName, + 'ALL_NON_MAIN', + 1, + ); const treeNode = createNode(parent, nodeName, currentDepth, runIndex); // Only include sub-nodes which have data const children = (filteredAiData ?? []).flatMap((data) => connectedSubNodes.includes(data.node) - ? getTreeNodeDataRec(treeNode, data.node, currentDepth + 1, workflow, aiData, data.runIndex) + ? getTreeNodeDataRec( + treeNode, + data.node, + currentDepth + 1, + connectionsBySourceNode, + aiData, + data.runIndex, + ) : [], ); @@ -112,11 +127,13 @@ function getTreeNodeDataRec( export function createAiData( nodeName: string, - workflow: Workflow, + connectionsBySourceNode: IConnections, getWorkflowResultDataByNodeName: (nodeName: string) => ITaskData[] | null, ): AIResult[] { - return workflow - .getParentNodes(nodeName, 'ALL_NON_MAIN') + const connectionsByDestinationNode = + workflowUtils.mapConnectionsByDestination(connectionsBySourceNode); + return workflowUtils + .getParentNodes(connectionsByDestinationNode, nodeName, 'ALL_NON_MAIN') .flatMap((node) => (getWorkflowResultDataByNodeName(node) ?? []).map((task, index) => ({ node, task, index })), )