mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Remove global state dependencies from composables
This commit is contained in:
parent
078f1b0045
commit
32b4eb523b
|
@ -1,44 +1,121 @@
|
|||
<script setup lang="ts">
|
||||
import { provide, watch, computed, ref, watchEffect } from 'vue';
|
||||
import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||
import { useCanvasStore } from '@/stores/canvas.store';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
// Constants & Symbols
|
||||
import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants';
|
||||
import { VIEWS } from '@/constants';
|
||||
|
||||
// Components
|
||||
import ChatMessagesPanel from './components/ChatMessagesPanel.vue';
|
||||
import ChatLogsPanel from './components/ChatLogsPanel.vue';
|
||||
|
||||
// Composables
|
||||
import { useChatTrigger } from './composables/useChatTrigger';
|
||||
import { useChatMessaging } from './composables/useChatMessaging';
|
||||
import { useResize } from './composables/useResize';
|
||||
import type { Chat, ChatMessage, ChatOptions } from '@n8n/chat/types';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||
|
||||
// Event Bus
|
||||
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||
|
||||
// Stores
|
||||
import { useCanvasStore } from '@/stores/canvas.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
|
||||
const router = useRouter();
|
||||
const uiStore = useUIStore();
|
||||
const locale = useI18n();
|
||||
const { getCurrentWorkflow } = useWorkflowHelpers({ router });
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const canvasStore = useCanvasStore();
|
||||
// Types
|
||||
import type { Chat, ChatMessage, ChatOptions } from '@n8n/chat/types';
|
||||
import type { RunWorkflowChatPayload } from './composables/useChatMessaging';
|
||||
|
||||
const messages = ref<ChatMessage[]>([]);
|
||||
const currentSessionId = ref<string>(uuid().replace(/-/g, ''));
|
||||
const isDisabled = ref(false);
|
||||
const container = ref<HTMLElement>();
|
||||
// Initialize stores and composables
|
||||
const setupStores = () => {
|
||||
const router = useRouter();
|
||||
const uiStore = useUIStore();
|
||||
const locale = useI18n();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const canvasStore = useCanvasStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
return {
|
||||
router,
|
||||
uiStore,
|
||||
locale,
|
||||
nodeHelpers,
|
||||
workflowsStore,
|
||||
canvasStore,
|
||||
nodeTypesStore,
|
||||
};
|
||||
};
|
||||
|
||||
// Component state
|
||||
const setupState = () => {
|
||||
const messages = ref<ChatMessage[]>([]);
|
||||
const currentSessionId = ref<string>(uuid().replace(/-/g, ''));
|
||||
const isDisabled = ref(false);
|
||||
const container = ref<HTMLElement>();
|
||||
|
||||
return {
|
||||
messages,
|
||||
currentSessionId,
|
||||
isDisabled,
|
||||
container,
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize component
|
||||
const { router, uiStore, locale, nodeHelpers, workflowsStore, canvasStore, nodeTypesStore } =
|
||||
setupStores();
|
||||
|
||||
const { messages, currentSessionId, isDisabled, container } = setupState();
|
||||
|
||||
// Computed properties
|
||||
const setupComputed = () => {
|
||||
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
|
||||
const isLoading = computed(() => uiStore.isActionActive.workflowRunning);
|
||||
const allConnections = computed(() => workflowsStore.allConnections);
|
||||
const isChatOpen = computed(() => workflowsStore.isChatPanelOpen);
|
||||
const isLogsOpen = computed(() => workflowsStore.isLogsPanelOpen);
|
||||
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
|
||||
|
||||
return {
|
||||
workflow,
|
||||
isLoading,
|
||||
allConnections,
|
||||
isChatOpen,
|
||||
isLogsOpen,
|
||||
previousChatMessages,
|
||||
};
|
||||
};
|
||||
|
||||
const { workflow, isLoading, allConnections, isChatOpen, isLogsOpen, previousChatMessages } =
|
||||
setupComputed();
|
||||
|
||||
// Initialize features
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
|
||||
const { chatTriggerNode, connectedNode, allowFileUploads, setChatTriggerNode, setConnectedNode } =
|
||||
useChatTrigger({ router });
|
||||
useChatTrigger({
|
||||
workflow,
|
||||
getNodeByName: workflowsStore.getNodeByName,
|
||||
getNodeType: nodeTypesStore.getNodeType,
|
||||
});
|
||||
|
||||
const { sendMessage, getChatMessages } = useChatMessaging({
|
||||
chatTrigger: chatTriggerNode,
|
||||
connectedNode,
|
||||
messages,
|
||||
router,
|
||||
sessionId: currentSessionId,
|
||||
workflow,
|
||||
isLoading,
|
||||
executionResultData: computed(() => workflowsStore.getWorkflowExecution?.data?.resultData),
|
||||
getWorkflowResultDataByNodeName: workflowsStore.getWorkflowResultDataByNodeName,
|
||||
onRunChatWorkflow,
|
||||
});
|
||||
|
||||
const {
|
||||
|
@ -51,69 +128,86 @@ const {
|
|||
onWindowResize,
|
||||
} = useResize(container);
|
||||
|
||||
const isLoading = computed(() => uiStore.isActionActive.workflowRunning);
|
||||
const allConnections = computed(() => workflowsStore.allConnections);
|
||||
const isChatOpen = computed(() => workflowsStore.isChatPanelOpen);
|
||||
const isLogsOpen = computed(() => workflowsStore.isLogsPanelOpen);
|
||||
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
|
||||
// Chat configuration
|
||||
const setupChatConfig = (): { chatConfig: Chat; chatOptions: ChatOptions } => {
|
||||
const chatConfig: Chat = {
|
||||
messages,
|
||||
sendMessage,
|
||||
initialMessages: ref([]),
|
||||
currentSessionId,
|
||||
waitingForResponse: isLoading,
|
||||
};
|
||||
|
||||
const chatConfig: Chat = {
|
||||
messages,
|
||||
sendMessage,
|
||||
initialMessages: ref([]),
|
||||
currentSessionId,
|
||||
waitingForResponse: isLoading,
|
||||
};
|
||||
|
||||
const chatOptions: ChatOptions = {
|
||||
i18n: {
|
||||
en: {
|
||||
title: '',
|
||||
footer: '',
|
||||
subtitle: '',
|
||||
inputPlaceholder: locale.baseText('chat.window.chat.placeholder'),
|
||||
getStarted: '',
|
||||
closeButtonTooltip: '',
|
||||
const chatOptions: ChatOptions = {
|
||||
i18n: {
|
||||
en: {
|
||||
title: '',
|
||||
footer: '',
|
||||
subtitle: '',
|
||||
inputPlaceholder: locale.baseText('chat.window.chat.placeholder'),
|
||||
getStarted: '',
|
||||
closeButtonTooltip: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
webhookUrl: '',
|
||||
mode: 'window',
|
||||
showWindowCloseButton: true,
|
||||
disabled: isDisabled,
|
||||
allowFileUploads,
|
||||
allowedFilesMimeTypes: '',
|
||||
webhookUrl: '',
|
||||
mode: 'window',
|
||||
showWindowCloseButton: true,
|
||||
disabled: isDisabled,
|
||||
allowFileUploads,
|
||||
allowedFilesMimeTypes: '',
|
||||
};
|
||||
|
||||
return { chatConfig, chatOptions };
|
||||
};
|
||||
|
||||
function displayExecution(executionId: string) {
|
||||
const workflow = getCurrentWorkflow();
|
||||
const { chatConfig, chatOptions } = setupChatConfig();
|
||||
|
||||
// Methods
|
||||
const displayExecution = (executionId: string) => {
|
||||
const route = router.resolve({
|
||||
name: VIEWS.EXECUTION_PREVIEW,
|
||||
params: { name: workflow.id, executionId },
|
||||
params: { name: workflow.value.id, executionId },
|
||||
});
|
||||
window.open(route.href, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
function refreshSession() {
|
||||
const refreshSession = () => {
|
||||
workflowsStore.setWorkflowExecutionData(null);
|
||||
nodeHelpers.updateNodesExecutionIssues();
|
||||
messages.value = [];
|
||||
currentSessionId.value = uuid().replace(/-/g, '');
|
||||
}
|
||||
};
|
||||
|
||||
function closeLogs() {
|
||||
const closeLogs = () => {
|
||||
workflowsStore.setPanelOpen('logs', false);
|
||||
};
|
||||
|
||||
async function onRunChatWorkflow(payload: RunWorkflowChatPayload) {
|
||||
const response = await runWorkflow({
|
||||
triggerNode: payload.triggerNode,
|
||||
nodeData: payload.nodeData,
|
||||
source: payload.source,
|
||||
});
|
||||
|
||||
workflowsStore.appendChatMessage(payload.message);
|
||||
return response;
|
||||
}
|
||||
|
||||
// Provide chat context
|
||||
provide(ChatSymbol, chatConfig);
|
||||
provide(ChatOptionsSymbol, chatOptions);
|
||||
|
||||
// Watchers
|
||||
watch(
|
||||
() => isChatOpen.value,
|
||||
(isOpen) => {
|
||||
if (isOpen) {
|
||||
setChatTriggerNode();
|
||||
setConnectedNode();
|
||||
messages.value = getChatMessages();
|
||||
|
||||
if (messages.value.length === 0) {
|
||||
messages.value = getChatMessages();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
onWindowResize();
|
||||
|
@ -138,18 +232,17 @@ watch(
|
|||
);
|
||||
|
||||
watchEffect(() => {
|
||||
canvasStore.setPanelHeight(isChatOpen.value || isLogsOpen.value ? height.value : 40);
|
||||
canvasStore.setPanelHeight(isChatOpen.value || isLogsOpen.value ? height.value : 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n8n-resize-wrapper
|
||||
v-show="chatTriggerNode"
|
||||
v-if="chatTriggerNode"
|
||||
:is-resizing-enabled="isChatOpen || isLogsOpen"
|
||||
:supported-directions="['top']"
|
||||
:class="[$style.resizeWrapper, !isChatOpen && !isLogsOpen && $style.empty]"
|
||||
:height="height"
|
||||
data-test-id="ask-assistant-sidebar"
|
||||
:style="rootStyles"
|
||||
@resize="onResizeDebounced"
|
||||
>
|
||||
|
@ -164,6 +257,7 @@ watchEffect(() => {
|
|||
>
|
||||
<div :class="$style.inner">
|
||||
<ChatMessagesPanel
|
||||
data-test-id="canvas-chat"
|
||||
:messages="messages"
|
||||
:session-id="currentSessionId"
|
||||
:past-chat-messages="previousChatMessages"
|
||||
|
@ -176,6 +270,8 @@ watchEffect(() => {
|
|||
<div v-if="isLogsOpen && connectedNode" :class="$style.logs">
|
||||
<ChatLogsPanel
|
||||
:key="messages.length"
|
||||
:workflow="workflow"
|
||||
data-test-id="canvas-chat-logs"
|
||||
:node="connectedNode"
|
||||
:slim="logsWidth < 700"
|
||||
@close="closeLogs"
|
||||
|
@ -192,7 +288,6 @@ watchEffect(() => {
|
|||
min-height: 4rem;
|
||||
max-height: 90vh;
|
||||
flex-basis: content;
|
||||
z-index: 300;
|
||||
border-top: 1px solid var(--color-foreground-base);
|
||||
|
||||
&.empty {
|
||||
|
@ -201,6 +296,7 @@ watchEffect(() => {
|
|||
flex-basis: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
@ -208,12 +304,14 @@ watchEffect(() => {
|
|||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chatResizer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: 1px solid var(--color-foreground-base);
|
||||
width: 100%;
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import type { INode } from 'n8n-workflow';
|
||||
import { computed } from 'vue';
|
||||
import type { INode, Workflow } from 'n8n-workflow';
|
||||
import RunDataAi from '@/components/RunDataAi/RunDataAi.vue';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
}>();
|
||||
|
||||
const locale = useI18n();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
|
||||
defineProps<{
|
||||
node: INode | null;
|
||||
slim?: boolean;
|
||||
workflow: Workflow;
|
||||
}>();
|
||||
|
||||
const locale = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -159,7 +159,7 @@ function copySessionId() {
|
|||
icon="redo"
|
||||
:label="locale.baseText('chat.window.chat.chatMessageOptions.repostMessage')"
|
||||
placement="left"
|
||||
@click="repostMessage(message)"
|
||||
@click.once="repostMessage(message)"
|
||||
/>
|
||||
|
||||
<MessageOptionAction
|
||||
|
|
|
@ -23,7 +23,7 @@ defineProps({
|
|||
<template #content>
|
||||
{{ label }}
|
||||
</template>
|
||||
<n8n-icon :class="$style.icon" :icon="icon" size="xsmall" @click="$emit('click')" />
|
||||
<n8n-icon :class="$style.icon" :icon="icon" size="xsmall" @click="$attrs.onClick" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import type { ComputedRef, Ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { ChatMessage, ChatMessageText } from '@n8n/chat/types';
|
||||
import { NodeConnectionType, CHAT_TRIGGER_NODE_TYPE } from 'n8n-workflow';
|
||||
|
@ -10,41 +10,53 @@ import type {
|
|||
IDataObject,
|
||||
IBinaryData,
|
||||
BinaryFileType,
|
||||
Workflow,
|
||||
IRunExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import { usePinnedData } from '@/composables/usePinnedData';
|
||||
import { get, isEmpty, last } from 'lodash-es';
|
||||
import { MANUAL_CHAT_TRIGGER_NODE_TYPE, MODAL_CONFIRM } from '@/constants';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import type { useRouter } from 'vue-router';
|
||||
import type { MemoryOutput } from '../types/chat';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { IExecutionPushResponse, INodeUi } from '@/Interface';
|
||||
|
||||
export function useChatMessaging({
|
||||
router,
|
||||
chatTrigger,
|
||||
messages,
|
||||
sessionId,
|
||||
}: {
|
||||
router: ReturnType<typeof useRouter>;
|
||||
export type RunWorkflowChatPayload = {
|
||||
triggerNode: string;
|
||||
nodeData: ITaskData;
|
||||
source: string;
|
||||
message: string;
|
||||
};
|
||||
export interface ChatMessagingDependencies {
|
||||
chatTrigger: Ref<INodeUi | null>;
|
||||
connectedNode: Ref<INodeUi | null>;
|
||||
messages: Ref<ChatMessage[]>;
|
||||
sessionId: Ref<string>;
|
||||
}) {
|
||||
const previousMessageIndex = ref(0);
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const { showError } = useToast();
|
||||
const uiStore = useUIStore();
|
||||
const locale = useI18n();
|
||||
workflow: ComputedRef<Workflow>;
|
||||
isLoading: ComputedRef<boolean>;
|
||||
executionResultData: ComputedRef<IRunExecutionData['resultData'] | undefined>;
|
||||
getWorkflowResultDataByNodeName: (nodeName: string) => ITaskData[] | null;
|
||||
onRunChatWorkflow: (
|
||||
payload: RunWorkflowChatPayload,
|
||||
) => Promise<IExecutionPushResponse | undefined>;
|
||||
}
|
||||
|
||||
export function useChatMessaging({
|
||||
chatTrigger,
|
||||
connectedNode,
|
||||
messages,
|
||||
sessionId,
|
||||
workflow,
|
||||
isLoading,
|
||||
executionResultData,
|
||||
getWorkflowResultDataByNodeName,
|
||||
onRunChatWorkflow,
|
||||
}: ChatMessagingDependencies) {
|
||||
const locale = useI18n();
|
||||
const { showError } = useToast();
|
||||
const previousMessageIndex = ref(0);
|
||||
|
||||
const isLoading = computed(() => uiStore.isActionActive.workflowRunning);
|
||||
/** Converts a file to binary data */
|
||||
async function convertFileToBinaryData(file: File): Promise<IBinaryData> {
|
||||
const reader = new FileReader();
|
||||
|
@ -136,13 +148,19 @@ export function useChatMessaging({
|
|||
source: [null],
|
||||
};
|
||||
|
||||
const response = await runWorkflow({
|
||||
const response = await onRunChatWorkflow({
|
||||
triggerNode: triggerNode.name,
|
||||
nodeData,
|
||||
source: 'RunData.ManualChatMessage',
|
||||
message,
|
||||
});
|
||||
// const response = await runWorkflow({
|
||||
// triggerNode: triggerNode.name,
|
||||
// nodeData,
|
||||
// source: 'RunData.ManualChatMessage',
|
||||
// });
|
||||
|
||||
workflowsStore.appendChatMessage(message);
|
||||
// workflowsStore.appendChatMessage(message);
|
||||
if (!response?.executionId) {
|
||||
showError(
|
||||
new Error('It was not possible to start workflow!'),
|
||||
|
@ -160,14 +178,12 @@ export function useChatMessaging({
|
|||
if (!isLoading.value) {
|
||||
clearInterval(waitInterval);
|
||||
|
||||
const lastNodeExecuted =
|
||||
workflowsStore.getWorkflowExecution?.data?.resultData.lastNodeExecuted;
|
||||
const lastNodeExecuted = executionResultData.value?.lastNodeExecuted;
|
||||
|
||||
if (!lastNodeExecuted) return;
|
||||
|
||||
const nodeResponseDataArray =
|
||||
get(workflowsStore.getWorkflowExecution?.data?.resultData.runData, lastNodeExecuted) ??
|
||||
[];
|
||||
get(executionResultData.value.runData, lastNodeExecuted) ?? [];
|
||||
|
||||
const nodeResponseData = nodeResponseDataArray[nodeResponseDataArray.length - 1];
|
||||
|
||||
|
@ -251,18 +267,20 @@ export function useChatMessaging({
|
|||
}
|
||||
|
||||
function getChatMessages(): ChatMessageText[] {
|
||||
if (!chatTrigger.value) return [];
|
||||
console.log('Getting chat messages', connectedNode.value);
|
||||
if (!connectedNode.value) return [];
|
||||
|
||||
const workflow = workflowHelpers.getCurrentWorkflow();
|
||||
const connectedMemoryInputs =
|
||||
workflow.connectionsByDestinationNode[chatTrigger.value.name]?.[NodeConnectionType.AiMemory];
|
||||
workflow.value.connectionsByDestinationNode[connectedNode.value.name]?.[
|
||||
NodeConnectionType.AiMemory
|
||||
];
|
||||
if (!connectedMemoryInputs) return [];
|
||||
|
||||
const memoryConnection = (connectedMemoryInputs ?? []).find((i) => i.length > 0)?.[0];
|
||||
|
||||
if (!memoryConnection) return [];
|
||||
|
||||
const nodeResultData = workflowsStore.getWorkflowResultDataByNodeName(memoryConnection.node);
|
||||
const nodeResultData = getWorkflowResultDataByNodeName(memoryConnection.node);
|
||||
|
||||
const memoryOutputData = (nodeResultData ?? [])
|
||||
.map((data) => get(data, ['data', NodeConnectionType.AiMemory, 0, 0, 'json']) as MemoryOutput)
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import type { ComputedRef } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import {
|
||||
CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE,
|
||||
NodeConnectionType,
|
||||
NodeHelpers,
|
||||
type INode,
|
||||
type INodeParameters,
|
||||
type INodeType,
|
||||
} from 'n8n-workflow';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import type {
|
||||
INodeTypeDescription,
|
||||
Workflow,
|
||||
INode,
|
||||
INodeParameters,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
AI_CATEGORY_AGENTS,
|
||||
AI_CATEGORY_CHAINS,
|
||||
|
@ -17,19 +21,19 @@ import {
|
|||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { useRouter } from 'vue-router';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
|
||||
export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter> }) {
|
||||
export interface ChatTriggerDependencies {
|
||||
getNodeByName: (name: string) => INodeUi | null;
|
||||
getNodeType: (type: string, version: number) => INodeTypeDescription | null;
|
||||
workflow: ComputedRef<Workflow>;
|
||||
}
|
||||
|
||||
export function useChatTrigger({ getNodeByName, getNodeType, workflow }: ChatTriggerDependencies) {
|
||||
const chatTriggerName = ref<string | null>(null);
|
||||
const connectedNode = ref<INode | null>(null);
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
|
||||
const chatTriggerNode = computed(() =>
|
||||
chatTriggerName.value ? workflowsStore.getNodeByName(chatTriggerName.value) : null,
|
||||
chatTriggerName.value ? getNodeByName(chatTriggerName.value) : null,
|
||||
);
|
||||
|
||||
const allowFileUploads = computed(() => {
|
||||
|
@ -48,11 +52,9 @@ export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter
|
|||
|
||||
/** Gets the chat trigger node from the workflow */
|
||||
function setChatTriggerNode() {
|
||||
const triggerNode = workflowHelpers
|
||||
.getCurrentWorkflow()
|
||||
.queryNodes((nodeType: INodeType) =>
|
||||
[CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(nodeType.description.name),
|
||||
);
|
||||
const triggerNode = workflow.value.queryNodes((nodeType: INodeType) =>
|
||||
[CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(nodeType.description.name),
|
||||
);
|
||||
|
||||
if (!triggerNode.length) {
|
||||
return;
|
||||
|
@ -62,25 +64,24 @@ export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter
|
|||
|
||||
/** Sets the connected node after finding the trigger */
|
||||
function setConnectedNode() {
|
||||
const workflow = workflowHelpers.getCurrentWorkflow();
|
||||
const triggerNode = chatTriggerNode.value;
|
||||
|
||||
if (!triggerNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chatChildren = workflow.getChildNodes(triggerNode.name);
|
||||
const chatChildren = workflow.value.getChildNodes(triggerNode.name);
|
||||
|
||||
const chatRootNode = chatChildren
|
||||
.reverse()
|
||||
.map((nodeName: string) => workflowsStore.getNodeByName(nodeName))
|
||||
.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 = nodeTypesStore.getNodeType(storeNode.type, storeNode.typeVersion);
|
||||
const nodeType = getNodeType(storeNode.type, storeNode.typeVersion);
|
||||
|
||||
if (!nodeType) return false;
|
||||
|
||||
|
@ -94,10 +95,10 @@ export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter
|
|||
let isCustomChainOrAgent = false;
|
||||
if (nodeType.name === AI_CODE_NODE_TYPE) {
|
||||
// Get node connection types for inputs and outputs
|
||||
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);
|
||||
|
||||
// Validate if node has required AI connection types
|
||||
|
@ -114,14 +115,14 @@ export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter
|
|||
if (!isAgent && !isChain && !isCustomChainOrAgent) return false;
|
||||
|
||||
// Check if this node is connected to the trigger node
|
||||
const parentNodes = workflow.getParentNodes(storeNode.name);
|
||||
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 resut = Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
|
||||
return resut;
|
||||
const result = Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
|
||||
return result;
|
||||
});
|
||||
|
||||
connectedNode.value = chatRootNode ?? null;
|
||||
|
|
|
@ -132,7 +132,6 @@ export function useResize(container: Ref<HTMLElement | undefined>) {
|
|||
onWindowResize,
|
||||
onResizeDebounced,
|
||||
onResizeChatDebounced,
|
||||
// onResizeChat,
|
||||
panelToContainerRatio,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,127 +0,0 @@
|
|||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { fireEvent, waitFor } from '@testing-library/vue';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import type { IConnections, INode } from 'n8n-workflow';
|
||||
|
||||
import WorkflowLMChatModal from '@/components/WorkflowLMChat/WorkflowLMChat.vue';
|
||||
import { WORKFLOW_LM_CHAT_MODAL_KEY } from '@/constants';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { setupServer } from '@/__tests__/server';
|
||||
import { defaultNodeDescriptions, mockNodes } from '@/__tests__/mocks';
|
||||
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
|
||||
|
||||
const connections: IConnections = {
|
||||
'Chat Trigger': {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Agent',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowLMChatModal, {
|
||||
props: {
|
||||
teleported: false,
|
||||
appendToBody: false,
|
||||
},
|
||||
});
|
||||
|
||||
async function createPiniaWithAINodes(options = { withConnections: true, withAgentNode: true }) {
|
||||
const { withConnections, withAgentNode } = options;
|
||||
|
||||
const chatTriggerNode = mockNodes[4];
|
||||
const agentNode = mockNodes[5];
|
||||
const nodes: INode[] = [chatTriggerNode];
|
||||
if (withAgentNode) nodes.push(agentNode);
|
||||
const workflow = mock<IWorkflowDb>({
|
||||
nodes,
|
||||
...(withConnections ? { connections } : {}),
|
||||
});
|
||||
|
||||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
|
||||
workflowsStore.workflow = workflow;
|
||||
|
||||
await useSettingsStore().getSettings();
|
||||
await useUsersStore().loginWithCookie();
|
||||
uiStore.openModal(WORKFLOW_LM_CHAT_MODAL_KEY);
|
||||
|
||||
return pinia;
|
||||
}
|
||||
|
||||
describe('WorkflowLMChatModal', () => {
|
||||
let server: ReturnType<typeof setupServer>;
|
||||
|
||||
beforeAll(() => {
|
||||
server = setupServer();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
createAppModals();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupAppModals();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.shutdown();
|
||||
});
|
||||
|
||||
it('should render correctly', async () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia: await createPiniaWithAINodes(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(getByTestId('lmChat-modal')).toBeInTheDocument());
|
||||
|
||||
expect(getByTestId('workflow-lm-chat-dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should send and display chat message', async () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia: await createPiniaWithAINodes({
|
||||
withConnections: true,
|
||||
withAgentNode: true,
|
||||
}),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(getByTestId('lmChat-modal')).toBeInTheDocument());
|
||||
|
||||
const chatDialog = getByTestId('workflow-lm-chat-dialog');
|
||||
const chatInputsContainer = getByTestId('lm-chat-inputs');
|
||||
const chatSendButton = chatInputsContainer.querySelector('.chat-input-send-button');
|
||||
const chatInput = chatInputsContainer.querySelector('textarea');
|
||||
|
||||
if (chatInput && chatSendButton) {
|
||||
await fireEvent.update(chatInput, 'Hello!');
|
||||
await fireEvent.click(chatSendButton);
|
||||
}
|
||||
|
||||
await waitFor(() =>
|
||||
expect(chatDialog.querySelectorAll('.chat-message-from-user')).toHaveLength(1),
|
||||
);
|
||||
|
||||
expect(chatDialog.querySelector('.chat-message-from-user')).toHaveTextContent('Hello!');
|
||||
});
|
||||
});
|
|
@ -37,7 +37,6 @@ import { get } from 'lodash-es';
|
|||
import { useExecutionsStore } from '@/stores/executions.store';
|
||||
import type { PushPayload } from '@n8n/api-types';
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
import { useCanvasStore } from '@/stores/canvas.store';
|
||||
|
||||
const FORM_RELOAD = 'n8n_redirect_to_next_form_test_page';
|
||||
|
||||
|
@ -51,7 +50,6 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
|
|||
const uiStore = useUIStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const executionsStore = useExecutionsStore();
|
||||
const canvasStore = useCanvasStore();
|
||||
// Starts to execute a workflow on server
|
||||
async function runWorkflowApi(runData: IStartRunData): Promise<IExecutionPushResponse> {
|
||||
if (!rootStore.pushConnectionActive) {
|
||||
|
|
Loading…
Reference in a new issue