mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-12 15:44:06 -08:00
Merge branch 'master' of https://github.com/n8n-io/n8n into node-1669-switch-node-adding-routing-rule-reconnects-fallback-route
This commit is contained in:
commit
97b7a5bd16
|
@ -90,6 +90,14 @@ function createRunDataWithError(inputMessage: string) {
|
|||
routine: 'InitPostgres',
|
||||
} as unknown as Error,
|
||||
} as ExecutionError,
|
||||
metadata: {
|
||||
subRun: [
|
||||
{
|
||||
node: 'Postgres Chat Memory',
|
||||
runIndex: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||
executionStatus: 'error',
|
||||
|
@ -124,14 +132,6 @@ function createRunDataWithError(inputMessage: string) {
|
|||
description: 'Internal error',
|
||||
message: 'Internal error',
|
||||
} as unknown as ExecutionError,
|
||||
metadata: {
|
||||
subRun: [
|
||||
{
|
||||
node: 'Postgres Chat Memory',
|
||||
runIndex: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -278,6 +278,9 @@ describe('Langchain Integration', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
||||
},
|
||||
inputOverride: {
|
||||
ai_languageModel: [
|
||||
[
|
||||
|
@ -316,9 +319,6 @@ describe('Langchain Integration', () => {
|
|||
jsonData: {
|
||||
main: { output: 'Hi there! How can I assist you today?' },
|
||||
},
|
||||
metadata: {
|
||||
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
lastNodeExecuted: AGENT_NODE_NAME,
|
||||
|
|
|
@ -100,14 +100,15 @@ const isTriggerNode = computed(() => {
|
|||
});
|
||||
|
||||
const hasAiMetadata = computed(() => {
|
||||
if (isNodeRunning.value || !workflowRunData.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.value) {
|
||||
const resultData = workflowsStore.getWorkflowResultDataByNodeName(node.value.name);
|
||||
const connectedSubNodes = props.workflow.getParentNodes(node.value.name, 'ALL_NON_MAIN');
|
||||
const resultData = connectedSubNodes.map(workflowsStore.getWorkflowResultDataByNodeName);
|
||||
|
||||
if (!resultData || !Array.isArray(resultData) || resultData.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!resultData[resultData.length - 1].metadata;
|
||||
return resultData && Array.isArray(resultData) && resultData.length > 0;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
@ -295,6 +296,7 @@ const activatePane = () => {
|
|||
:block-u-i="blockUI"
|
||||
:is-production-execution-preview="isProductionExecutionPreview"
|
||||
:is-pane-active="isPaneActive"
|
||||
:hide-pagination="outputMode === 'logs'"
|
||||
pane-type="output"
|
||||
:data-output-type="outputMode"
|
||||
@activate-pane="activatePane"
|
||||
|
@ -368,7 +370,7 @@ const activatePane = () => {
|
|||
</template>
|
||||
|
||||
<template v-if="outputMode === 'logs' && node" #content>
|
||||
<RunDataAi :node="node" :run-index="runIndex" />
|
||||
<RunDataAi :node="node" :run-index="runIndex" :workflow="workflow" />
|
||||
</template>
|
||||
|
||||
<template #recovered-artificial-output-data>
|
||||
|
|
|
@ -162,6 +162,10 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hidePagination: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const ndvStore = useNDVStore();
|
||||
|
@ -1743,6 +1747,7 @@ export default defineComponent({
|
|||
</div>
|
||||
<div
|
||||
v-if="
|
||||
hidePagination === false &&
|
||||
hasNodeRun &&
|
||||
!hasRunError &&
|
||||
displayMode !== 'binary' &&
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import type { ITaskSubRunMetadata, ITaskDataConnections } from 'n8n-workflow';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import type { ITaskDataConnections, NodeConnectionType, Workflow, ITaskData } from 'n8n-workflow';
|
||||
import type { IAiData, IAiDataContent, INodeUi } from '@/Interface';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
|
@ -28,29 +27,21 @@ export interface Props {
|
|||
runIndex?: number;
|
||||
hideTitle?: boolean;
|
||||
slim?: boolean;
|
||||
workflow: Workflow;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), { runIndex: 0 });
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const selectedRun: Ref<IAiData[]> = ref([]);
|
||||
|
||||
function isTreeNodeSelected(node: TreeNode) {
|
||||
return selectedRun.value.some((run) => run.node === node.node && run.runIndex === node.runIndex);
|
||||
}
|
||||
|
||||
function getReferencedData(
|
||||
reference: ITaskSubRunMetadata,
|
||||
taskData: ITaskData,
|
||||
withInput: boolean,
|
||||
withOutput: boolean,
|
||||
): IAiDataContent[] {
|
||||
const resultData = workflowsStore.getWorkflowResultDataByNodeName(reference.node);
|
||||
|
||||
if (!resultData?.[reference.runIndex]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const taskData = resultData[reference.runIndex];
|
||||
|
||||
if (!taskData) {
|
||||
return [];
|
||||
}
|
||||
|
@ -98,18 +89,18 @@ function onItemClick(data: TreeNode) {
|
|||
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedNodeRun = workflowsStore.getWorkflowResultDataByNodeName(data.node)?.[
|
||||
data.runIndex
|
||||
];
|
||||
if (!selectedNodeRun) {
|
||||
return;
|
||||
}
|
||||
selectedRun.value = [
|
||||
{
|
||||
node: data.node,
|
||||
runIndex: data.runIndex,
|
||||
data: getReferencedData(
|
||||
{
|
||||
node: data.node,
|
||||
runIndex: data.runIndex,
|
||||
},
|
||||
true,
|
||||
true,
|
||||
),
|
||||
data: getReferencedData(selectedNodeRun, true, true),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -145,21 +136,20 @@ const createNode = (
|
|||
});
|
||||
|
||||
function getTreeNodeData(nodeName: string, currentDepth: number): TreeNode[] {
|
||||
const { connectionsByDestinationNode } = workflowsStore.getCurrentWorkflow();
|
||||
const connections = connectionsByDestinationNode[nodeName];
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
const connections = props.workflow.connectionsByDestinationNode[nodeName];
|
||||
const resultData = aiData.value?.filter((data) => data.node === nodeName) ?? [];
|
||||
|
||||
if (!connections) {
|
||||
return resultData.map((d) => createNode(nodeName, currentDepth, d));
|
||||
}
|
||||
|
||||
const nonMainConnectionsKeys = Object.keys(connections).filter(
|
||||
(key) => key !== NodeConnectionType.Main,
|
||||
);
|
||||
const children = nonMainConnectionsKeys.flatMap((key) =>
|
||||
connections[key][0].flatMap((node) => getTreeNodeData(node.node, currentDepth + 1)),
|
||||
);
|
||||
// Get the first level of children
|
||||
const connectedSubNodes = props.workflow.getParentNodes(nodeName, 'ALL_NON_MAIN', 1);
|
||||
|
||||
const children = connectedSubNodes
|
||||
// Only include sub-nodes which have data
|
||||
.filter((name) => aiData.value?.find((data) => data.node === name))
|
||||
.flatMap((name) => getTreeNodeData(name, currentDepth + 1));
|
||||
|
||||
children.sort((a, b) => a.startTime - b.startTime);
|
||||
|
||||
|
@ -170,35 +160,49 @@ function getTreeNodeData(nodeName: string, currentDepth: number): TreeNode[] {
|
|||
return [createNode(nodeName, currentDepth, undefined, children)];
|
||||
}
|
||||
|
||||
const aiData = computed<AIResult[] | undefined>(() => {
|
||||
const resultData = workflowsStore.getWorkflowResultDataByNodeName(props.node.name);
|
||||
const aiData = computed<AIResult[]>(() => {
|
||||
const result: AIResult[] = [];
|
||||
const connectedSubNodes = props.workflow.getParentNodes(props.node.name, 'ALL_NON_MAIN');
|
||||
const rootNodeResult = workflowsStore.getWorkflowResultDataByNodeName(props.node.name);
|
||||
const rootNodeStartTime = rootNodeResult?.[0]?.startTime ?? 0;
|
||||
const rootNodeEndTime = rootNodeStartTime + (rootNodeResult?.[0]?.executionTime ?? 0);
|
||||
|
||||
if (!resultData || !Array.isArray(resultData)) {
|
||||
return;
|
||||
}
|
||||
connectedSubNodes.forEach((nodeName) => {
|
||||
const nodeRunData = workflowsStore.getWorkflowResultDataByNodeName(nodeName) ?? [];
|
||||
|
||||
const subRun = resultData[props.runIndex].metadata?.subRun;
|
||||
if (!Array.isArray(subRun)) {
|
||||
return;
|
||||
}
|
||||
// Extend the subRun with the data and sort by adding execution time + startTime and comparing them
|
||||
const subRunWithData = subRun.flatMap((run) =>
|
||||
getReferencedData(run, false, true).map((data) => ({ ...run, data })),
|
||||
);
|
||||
nodeRunData.forEach((run, runIndex) => {
|
||||
const referenceData = {
|
||||
data: getReferencedData(run, false, true)[0],
|
||||
node: nodeName,
|
||||
runIndex,
|
||||
};
|
||||
|
||||
subRunWithData.sort((a, b) => {
|
||||
const aTime = a.data?.metadata?.startTime || 0;
|
||||
const bTime = b.data?.metadata?.startTime || 0;
|
||||
result.push(referenceData);
|
||||
});
|
||||
});
|
||||
|
||||
// Sort the data by start time
|
||||
result.sort((a, b) => {
|
||||
const aTime = a.data?.metadata?.startTime ?? 0;
|
||||
const bTime = b.data?.metadata?.startTime ?? 0;
|
||||
return aTime - bTime;
|
||||
});
|
||||
|
||||
return subRunWithData;
|
||||
// Only show data that is within the root node's execution time
|
||||
// This is because sub-node could be connected to multiple root nodes
|
||||
const currentNodeResult = result.filter((r) => {
|
||||
const startTime = r.data?.metadata?.startTime ?? 0;
|
||||
|
||||
return startTime >= rootNodeStartTime && startTime <= rootNodeEndTime;
|
||||
});
|
||||
|
||||
return currentNodeResult;
|
||||
});
|
||||
|
||||
const executionTree = computed<TreeNode[]>(() => {
|
||||
const rootNode = props.node;
|
||||
|
||||
const tree = getTreeNodeData(rootNode.name, 0);
|
||||
const tree = getTreeNodeData(rootNode.name, 1);
|
||||
return tree || [];
|
||||
});
|
||||
|
||||
|
@ -206,7 +210,7 @@ watch(() => props.runIndex, selectFirst, { immediate: true });
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="aiData" :class="$style.container">
|
||||
<div v-if="aiData.length > 0" :class="$style.container">
|
||||
<div :class="{ [$style.tree]: true, [$style.slim]: slim }">
|
||||
<ElTree
|
||||
:data="executionTree"
|
||||
|
|
|
@ -22,7 +22,6 @@ import { useUsersStore } from '@/stores/users.store';
|
|||
import MessagesList from '@n8n/chat/components/MessagesList.vue';
|
||||
import type { ArrowKeyDownPayload } from '@n8n/chat/components/Input.vue';
|
||||
import ChatInput from '@n8n/chat/components/Input.vue';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||
import type { Chat, ChatMessage, ChatMessageText, ChatOptions } from '@n8n/chat/types';
|
||||
|
@ -78,7 +77,6 @@ interface MemoryOutput {
|
|||
}
|
||||
|
||||
const router = useRouter();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
@ -145,9 +143,9 @@ const messageVars = {
|
|||
'--chat--color-typing': 'var(--color-text-dark)',
|
||||
};
|
||||
|
||||
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
|
||||
function getTriggerNode() {
|
||||
const workflow = workflowHelpers.getCurrentWorkflow();
|
||||
const triggerNode = workflow.queryNodes((nodeType: INodeType) =>
|
||||
const triggerNode = workflow.value.queryNodes((nodeType: INodeType) =>
|
||||
[CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(nodeType.description.name),
|
||||
);
|
||||
|
||||
|
@ -164,19 +162,19 @@ function setNode() {
|
|||
return;
|
||||
}
|
||||
|
||||
const workflow = workflowHelpers.getCurrentWorkflow();
|
||||
const childNodes = workflow.getChildNodes(triggerNode.name);
|
||||
const childNodes = workflow.value.getChildNodes(triggerNode.name);
|
||||
|
||||
for (const childNode of childNodes) {
|
||||
// Look for the first connected node with metadata
|
||||
// TODO: Allow later users to change that in the UI
|
||||
const resultData = workflowsStore.getWorkflowResultDataByNodeName(childNode);
|
||||
const connectedSubNodes = workflow.value.getParentNodes(childNode, 'ALL_NON_MAIN');
|
||||
const resultData = connectedSubNodes.map(workflowsStore.getWorkflowResultDataByNodeName);
|
||||
|
||||
if (!resultData && !Array.isArray(resultData)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (resultData[resultData.length - 1].metadata) {
|
||||
if (resultData.some((data) => data?.[0].metadata)) {
|
||||
node.value = workflowsStore.getNodeByName(childNode);
|
||||
break;
|
||||
}
|
||||
|
@ -190,7 +188,6 @@ function setConnectedNode() {
|
|||
showError(new Error('Chat Trigger Node could not be found!'), 'Trigger Node not found');
|
||||
return;
|
||||
}
|
||||
const workflow = workflowHelpers.getCurrentWorkflow();
|
||||
|
||||
const chatNode = workflowsStore.getNodes().find((storeNode: INodeUi): boolean => {
|
||||
if (storeNode.type === CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE) return false;
|
||||
|
@ -202,10 +199,10 @@ function setConnectedNode() {
|
|||
|
||||
let isCustomChainOrAgent = false;
|
||||
if (nodeType.name === AI_CODE_NODE_TYPE) {
|
||||
const inputs = NodeHelpers.getNodeInputs(workflow, storeNode, nodeType);
|
||||
const inputs = NodeHelpers.getNodeInputs(workflow.value, storeNode, nodeType);
|
||||
const inputTypes = NodeHelpers.getConnectionTypes(inputs);
|
||||
|
||||
const outputs = NodeHelpers.getNodeOutputs(workflow, storeNode, nodeType);
|
||||
const outputs = NodeHelpers.getNodeOutputs(workflow.value, storeNode, nodeType);
|
||||
const outputTypes = NodeHelpers.getConnectionTypes(outputs);
|
||||
|
||||
if (
|
||||
|
@ -219,7 +216,7 @@ function setConnectedNode() {
|
|||
|
||||
if (!isAgent && !isChain && !isCustomChainOrAgent) return false;
|
||||
|
||||
const parentNodes = workflow.getParentNodes(storeNode.name);
|
||||
const parentNodes = workflow.value.getParentNodes(storeNode.name);
|
||||
const isChatChild = parentNodes.some((parentNodeName) => parentNodeName === triggerNode.name);
|
||||
|
||||
return Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
|
||||
|
@ -431,10 +428,9 @@ async function sendMessage(message: string, files?: File[]) {
|
|||
}
|
||||
|
||||
function displayExecution(executionId: string) {
|
||||
const workflow = workflowHelpers.getCurrentWorkflow();
|
||||
const route = router.resolve({
|
||||
name: VIEWS.EXECUTION_PREVIEW,
|
||||
params: { name: workflow.id, executionId },
|
||||
params: { name: workflow.value.id, executionId },
|
||||
});
|
||||
window.open(route.href, '_blank');
|
||||
}
|
||||
|
@ -452,9 +448,10 @@ function reuseMessage(message: ChatMessageText) {
|
|||
function getChatMessages(): ChatMessageText[] {
|
||||
if (!connectedNode.value) return [];
|
||||
|
||||
const workflow = workflowHelpers.getCurrentWorkflow();
|
||||
const connectedMemoryInputs =
|
||||
workflow.connectionsByDestinationNode[connectedNode.value.name][NodeConnectionType.AiMemory];
|
||||
workflow.value.connectionsByDestinationNode[connectedNode.value.name][
|
||||
NodeConnectionType.AiMemory
|
||||
];
|
||||
if (!connectedMemoryInputs) return [];
|
||||
|
||||
const memoryConnection = (connectedMemoryInputs ?? []).find((i) => i.length > 0)?.[0];
|
||||
|
@ -573,7 +570,13 @@ onMounted(() => {
|
|||
locale.baseText('chat.window.logs')
|
||||
}}</n8n-text>
|
||||
<div :class="$style.logs">
|
||||
<LazyRunDataAi :key="messages.length" :node="node" hide-title slim />
|
||||
<LazyRunDataAi
|
||||
:key="messages.length"
|
||||
:node="node"
|
||||
hide-title
|
||||
slim
|
||||
:workflow="workflow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue