import type { ComputedRef, MaybeRef } from 'vue'; import { ref, computed, unref } from 'vue'; import { CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE, NodeConnectionType, NodeHelpers, } from 'n8n-workflow'; import type { INodeTypeDescription, Workflow, INode, INodeParameters } from 'n8n-workflow'; import { AI_CATEGORY_AGENTS, AI_CATEGORY_CHAINS, AI_CODE_NODE_TYPE, AI_SUBCATEGORY, CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE, } from '@/constants'; import type { INodeUi } from '@/Interface'; export interface ChatTriggerDependencies { getNodeByName: (name: string) => INodeUi | null; getNodeType: (type: string, version: number) => INodeTypeDescription | null; canvasNodes: MaybeRef; workflow: ComputedRef; } export function useChatTrigger({ getNodeByName, getNodeType, canvasNodes, workflow, }: ChatTriggerDependencies) { const chatTriggerName = ref(null); const connectedNode = ref(null); const chatTriggerNode = computed(() => chatTriggerName.value ? getNodeByName(chatTriggerName.value) : null, ); const allowFileUploads = computed(() => { return ( (chatTriggerNode.value?.parameters?.options as INodeParameters)?.allowFileUploads === true ); }); const allowedFilesMimeTypes = computed(() => { return ( ( chatTriggerNode.value?.parameters?.options as INodeParameters )?.allowedFilesMimeTypes?.toString() ?? '' ); }); /** Gets the chat trigger node from the workflow */ function setChatTriggerNode() { const triggerNode = unref(canvasNodes).find((node) => [CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(node.type), ); if (!triggerNode) { return; } chatTriggerName.value = triggerNode.name; } /** Sets the connected node after finding the trigger */ function setConnectedNode() { const triggerNode = chatTriggerNode.value; if (!triggerNode) { return; } const chatChildren = workflow.value.getChildNodes(triggerNode.name); const chatRootNode = chatChildren .reverse() .map((nodeName: string) => getNodeByName(nodeName)) .filter((n): n is INodeUi => n !== null) // Reverse the nodes to match the last node logs first .reverse() .find((storeNode: INodeUi): boolean => { // Skip summarization nodes if (storeNode.type === CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE) return false; const nodeType = getNodeType(storeNode.type, storeNode.typeVersion); if (!nodeType) return false; // Check if node is an AI agent or chain based on its metadata const isAgent = nodeType.codex?.subcategories?.[AI_SUBCATEGORY]?.includes(AI_CATEGORY_AGENTS); const isChain = nodeType.codex?.subcategories?.[AI_SUBCATEGORY]?.includes(AI_CATEGORY_CHAINS); // Handle custom AI Langchain Code nodes that could act as chains or agents let isCustomChainOrAgent = false; if (nodeType.name === AI_CODE_NODE_TYPE) { // Get node connection types for inputs and outputs const inputs = NodeHelpers.getNodeInputs(workflow.value, storeNode, nodeType); const inputTypes = NodeHelpers.getConnectionTypes(inputs); const outputs = NodeHelpers.getNodeOutputs(workflow.value, storeNode, nodeType); const outputTypes = NodeHelpers.getConnectionTypes(outputs); // Validate if node has required AI connection types if ( inputTypes.includes(NodeConnectionType.AiLanguageModel) && inputTypes.includes(NodeConnectionType.Main) && outputTypes.includes(NodeConnectionType.Main) ) { isCustomChainOrAgent = true; } } // Skip if node is not an AI component if (!isAgent && !isChain && !isCustomChainOrAgent) return false; // Check if this node is connected to the trigger node const parentNodes = workflow.value.getParentNodes(storeNode.name); const isChatChild = parentNodes.some( (parentNodeName) => parentNodeName === triggerNode.name, ); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const result = Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent)); return result; }); connectedNode.value = chatRootNode ?? null; } return { allowFileUploads, allowedFilesMimeTypes, chatTriggerNode, connectedNode: computed(() => connectedNode.value), setChatTriggerNode, setConnectedNode, }; }