{
{
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%;
diff --git a/packages/editor-ui/src/components/CanvasChat/components/ChatLogsPanel.vue b/packages/editor-ui/src/components/CanvasChat/components/ChatLogsPanel.vue
index 1e2a89dd91..da5c9ed4c7 100644
--- a/packages/editor-ui/src/components/CanvasChat/components/ChatLogsPanel.vue
+++ b/packages/editor-ui/src/components/CanvasChat/components/ChatLogsPanel.vue
@@ -1,22 +1,19 @@
diff --git a/packages/editor-ui/src/components/CanvasChat/components/ChatMessagesPanel.vue b/packages/editor-ui/src/components/CanvasChat/components/ChatMessagesPanel.vue
index cf7604ccbc..cff796a202 100644
--- a/packages/editor-ui/src/components/CanvasChat/components/ChatMessagesPanel.vue
+++ b/packages/editor-ui/src/components/CanvasChat/components/ChatMessagesPanel.vue
@@ -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)"
/>
{{ label }}
-
+
diff --git a/packages/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts b/packages/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts
index 5d12596a6a..cb7ff0655d 100644
--- a/packages/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts
+++ b/packages/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts
@@ -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;
+export type RunWorkflowChatPayload = {
+ triggerNode: string;
+ nodeData: ITaskData;
+ source: string;
+ message: string;
+};
+export interface ChatMessagingDependencies {
chatTrigger: Ref;
+ connectedNode: Ref;
messages: Ref;
sessionId: Ref;
-}) {
- 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;
+ isLoading: ComputedRef;
+ executionResultData: ComputedRef;
+ getWorkflowResultDataByNodeName: (nodeName: string) => ITaskData[] | null;
+ onRunChatWorkflow: (
+ payload: RunWorkflowChatPayload,
+ ) => Promise;
+}
+
+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 {
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)
diff --git a/packages/editor-ui/src/components/CanvasChat/composables/useChatTrigger.ts b/packages/editor-ui/src/components/CanvasChat/composables/useChatTrigger.ts
index 753229fd7c..41d67cd61f 100644
--- a/packages/editor-ui/src/components/CanvasChat/composables/useChatTrigger.ts
+++ b/packages/editor-ui/src/components/CanvasChat/composables/useChatTrigger.ts
@@ -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 }) {
+export interface ChatTriggerDependencies {
+ getNodeByName: (name: string) => INodeUi | null;
+ getNodeType: (type: string, version: number) => INodeTypeDescription | null;
+ workflow: ComputedRef;
+}
+
+export function useChatTrigger({ getNodeByName, getNodeType, workflow }: ChatTriggerDependencies) {
const chatTriggerName = ref(null);
const connectedNode = ref(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
- [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 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 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;
diff --git a/packages/editor-ui/src/components/CanvasChat/composables/useResize.ts b/packages/editor-ui/src/components/CanvasChat/composables/useResize.ts
index 5338376338..cdbfcb82c2 100644
--- a/packages/editor-ui/src/components/CanvasChat/composables/useResize.ts
+++ b/packages/editor-ui/src/components/CanvasChat/composables/useResize.ts
@@ -132,7 +132,6 @@ export function useResize(container: Ref) {
onWindowResize,
onResizeDebounced,
onResizeChatDebounced,
- // onResizeChat,
panelToContainerRatio,
};
}
diff --git a/packages/editor-ui/src/components/WorkflowLMChatModal.test.ts b/packages/editor-ui/src/components/WorkflowLMChatModal.test.ts
deleted file mode 100644
index 8fa20d0965..0000000000
--- a/packages/editor-ui/src/components/WorkflowLMChatModal.test.ts
+++ /dev/null
@@ -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({
- 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;
-
- 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!');
- });
-});
diff --git a/packages/editor-ui/src/composables/useRunWorkflow.ts b/packages/editor-ui/src/composables/useRunWorkflow.ts
index 230a95a803..a944a93348 100644
--- a/packages/editor-ui/src/composables/useRunWorkflow.ts
+++ b/packages/editor-ui/src/composables/useRunWorkflow.ts
@@ -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 {
if (!rootStore.pushConnectionActive) {