diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.vue b/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.vue
index 97505a0b40..832b5a1d52 100644
--- a/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.vue
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.vue
@@ -1,91 +1,36 @@
-
-
- {{ locale.baseText('chat.window.title') }}
-
+
+
+
{{ locale.baseText('chat.window.session.title') }}
@@ -174,9 +175,9 @@ function copySessionId() {
icon="times"
@click="emit('close')"
/>
-
-
-
+
+
+
-
+
+defineProps<{ title: string }>();
+
+defineSlots<{ actions: {} }>();
+
+const emit = defineEmits<{ click: [] }>();
+
+
+
+
+
+
+
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatState.ts b/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatState.ts
new file mode 100644
index 0000000000..e4329d3adc
--- /dev/null
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatState.ts
@@ -0,0 +1,229 @@
+import type { RunWorkflowChatPayload } from '@/components/CanvasChat/composables/useChatMessaging';
+import { useChatMessaging } from '@/components/CanvasChat/composables/useChatMessaging';
+import { useChatTrigger } from '@/components/CanvasChat/composables/useChatTrigger';
+import { useI18n } from '@/composables/useI18n';
+import { useNodeHelpers } from '@/composables/useNodeHelpers';
+import { useRunWorkflow } from '@/composables/useRunWorkflow';
+import { VIEWS } from '@/constants';
+import { type INodeUi } from '@/Interface';
+import { useCanvasStore } from '@/stores/canvas.store';
+import { useNodeTypesStore } from '@/stores/nodeTypes.store';
+import { useWorkflowsStore } from '@/stores/workflows.store';
+import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants';
+import { chatEventBus } from '@n8n/chat/event-buses';
+import type { Chat, ChatMessage, ChatOptions } from '@n8n/chat/types';
+import { type INode } from 'n8n-workflow';
+import { v4 as uuid } from 'uuid';
+import type { Ref } from 'vue';
+import { computed, provide, ref, watch } from 'vue';
+import { useRouter } from 'vue-router';
+
+interface ChatState {
+ currentSessionId: Ref;
+ messages: Ref;
+ chatTriggerNode: Ref;
+ connectedNode: Ref;
+ sendMessage: (message: string, files?: File[]) => Promise;
+ refreshSession: () => void;
+ displayExecution: (executionId: string) => void;
+}
+
+export function useChatState(isDisabled: Ref, onWindowResize: () => void): ChatState {
+ const workflowsStore = useWorkflowsStore();
+ const nodeTypesStore = useNodeTypesStore();
+ const canvasStore = useCanvasStore();
+ const router = useRouter();
+ const nodeHelpers = useNodeHelpers();
+ const { runWorkflow } = useRunWorkflow({ router });
+
+ const messages = ref([]);
+ const currentSessionId = ref(uuid().replace(/-/g, ''));
+
+ const canvasNodes = computed(() => workflowsStore.allNodes);
+ const allConnections = computed(() => workflowsStore.allConnections);
+ const isChatOpen = computed(() => {
+ const result = workflowsStore.isChatPanelOpen;
+ return result;
+ });
+ const workflow = computed(() => workflowsStore.getCurrentWorkflow());
+
+ // Initialize features with injected dependencies
+ const {
+ chatTriggerNode,
+ connectedNode,
+ allowFileUploads,
+ allowedFilesMimeTypes,
+ setChatTriggerNode,
+ setConnectedNode,
+ } = useChatTrigger({
+ workflow,
+ canvasNodes,
+ getNodeByName: workflowsStore.getNodeByName,
+ getNodeType: nodeTypesStore.getNodeType,
+ });
+
+ const { sendMessage, getChatMessages, isLoading } = useChatMessaging({
+ chatTrigger: chatTriggerNode,
+ connectedNode,
+ messages,
+ sessionId: currentSessionId,
+ workflow,
+ executionResultData: computed(() => workflowsStore.getWorkflowExecution?.data?.resultData),
+ getWorkflowResultDataByNodeName: workflowsStore.getWorkflowResultDataByNodeName,
+ onRunChatWorkflow,
+ });
+
+ // Extracted pure functions for better testability
+ function createChatConfig(params: {
+ messages: Chat['messages'];
+ sendMessage: Chat['sendMessage'];
+ currentSessionId: Chat['currentSessionId'];
+ isLoading: Ref;
+ isDisabled: Ref;
+ allowFileUploads: Ref;
+ locale: ReturnType;
+ }): { chatConfig: Chat; chatOptions: ChatOptions } {
+ const chatConfig: Chat = {
+ messages: params.messages,
+ sendMessage: params.sendMessage,
+ initialMessages: ref([]),
+ currentSessionId: params.currentSessionId,
+ waitingForResponse: params.isLoading,
+ };
+
+ const chatOptions: ChatOptions = {
+ i18n: {
+ en: {
+ title: '',
+ footer: '',
+ subtitle: '',
+ inputPlaceholder: params.locale.baseText('chat.window.chat.placeholder'),
+ getStarted: '',
+ closeButtonTooltip: '',
+ },
+ },
+ webhookUrl: '',
+ mode: 'window',
+ showWindowCloseButton: true,
+ disabled: params.isDisabled,
+ allowFileUploads: params.allowFileUploads,
+ allowedFilesMimeTypes,
+ };
+
+ return { chatConfig, chatOptions };
+ }
+
+ // Initialize chat config
+ const { chatConfig, chatOptions } = createChatConfig({
+ messages,
+ sendMessage,
+ currentSessionId,
+ isLoading,
+ isDisabled,
+ allowFileUploads,
+ locale: useI18n(),
+ });
+
+ // Provide chat context
+ provide(ChatSymbol, chatConfig);
+ provide(ChatOptionsSymbol, chatOptions);
+
+ // Watchers
+ watch(
+ () => isChatOpen.value,
+ (isOpen) => {
+ if (isOpen) {
+ setChatTriggerNode();
+ setConnectedNode();
+
+ if (messages.value.length === 0) {
+ messages.value = getChatMessages();
+ }
+
+ setTimeout(() => {
+ onWindowResize();
+ chatEventBus.emit('focusInput');
+ }, 0);
+ }
+ },
+ { immediate: true },
+ );
+
+ watch(
+ () => allConnections.value,
+ () => {
+ if (canvasStore.isLoading) return;
+ setTimeout(() => {
+ if (!chatTriggerNode.value) {
+ setChatTriggerNode();
+ }
+ setConnectedNode();
+ }, 0);
+ },
+ { deep: true },
+ );
+
+ // This function creates a promise that resolves when the workflow execution completes
+ // It's used to handle the loading state while waiting for the workflow to finish
+ async function createExecutionPromise() {
+ return await new Promise((resolve) => {
+ const resolveIfFinished = (isRunning: boolean) => {
+ if (!isRunning) {
+ unwatch();
+ resolve();
+ }
+ };
+
+ // Watch for changes in the workflow execution status
+ const unwatch = watch(() => workflowsStore.isWorkflowRunning, resolveIfFinished);
+ resolveIfFinished(workflowsStore.isWorkflowRunning);
+ });
+ }
+
+ async function onRunChatWorkflow(payload: RunWorkflowChatPayload) {
+ const runWorkflowOptions: Parameters[0] = {
+ triggerNode: payload.triggerNode,
+ nodeData: payload.nodeData,
+ source: payload.source,
+ };
+
+ if (workflowsStore.chatPartialExecutionDestinationNode) {
+ runWorkflowOptions.destinationNode = workflowsStore.chatPartialExecutionDestinationNode;
+ workflowsStore.chatPartialExecutionDestinationNode = null;
+ }
+
+ const response = await runWorkflow(runWorkflowOptions);
+
+ if (response) {
+ await createExecutionPromise();
+ workflowsStore.appendChatMessage(payload.message);
+ return response;
+ }
+ return;
+ }
+
+ function refreshSession() {
+ workflowsStore.setWorkflowExecutionData(null);
+ nodeHelpers.updateNodesExecutionIssues();
+ messages.value = [];
+ currentSessionId.value = uuid().replace(/-/g, '');
+ }
+
+ function displayExecution(executionId: string) {
+ const route = router.resolve({
+ name: VIEWS.EXECUTION_PREVIEW,
+ params: { name: workflow.value.id, executionId },
+ });
+ window.open(route.href, '_blank');
+ }
+
+ return {
+ currentSessionId,
+ messages,
+ chatTriggerNode,
+ connectedNode,
+ sendMessage,
+ refreshSession,
+ displayExecution,
+ };
+}
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/v2/CanvasBottomPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/v2/CanvasBottomPanel.vue
new file mode 100644
index 0000000000..470de78777
--- /dev/null
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/v2/CanvasBottomPanel.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/v2/components/LogsPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/v2/components/LogsPanel.vue
new file mode 100644
index 0000000000..7c419afa4e
--- /dev/null
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/v2/components/LogsPanel.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
diff --git a/packages/frontend/editor-ui/src/router.ts b/packages/frontend/editor-ui/src/router.ts
index 1c7db22579..c67b81486c 100644
--- a/packages/frontend/editor-ui/src/router.ts
+++ b/packages/frontend/editor-ui/src/router.ts
@@ -26,7 +26,8 @@ const ErrorView = async () => await import('./views/ErrorView.vue');
const ForgotMyPasswordView = async () => await import('./views/ForgotMyPasswordView.vue');
const MainHeader = async () => await import('@/components/MainHeader/MainHeader.vue');
const MainSidebar = async () => await import('@/components/MainSidebar.vue');
-const CanvasChat = async () => await import('@/components/CanvasChat/CanvasChat.vue');
+const CanvasBottomPanel = async () =>
+ await import('@/components/CanvasChat/v2/CanvasBottomPanel.vue');
const NodeView = async () => await import('@/views/NodeView.vue');
const WorkflowExecutionsView = async () => await import('@/views/WorkflowExecutionsView.vue');
const WorkflowExecutionsLandingPage = async () =>
@@ -358,7 +359,7 @@ export const routes: RouteRecordRaw[] = [
default: NodeView,
header: MainHeader,
sidebar: MainSidebar,
- footer: CanvasChat,
+ footer: CanvasBottomPanel,
},
meta: {
nodeView: true,
@@ -391,7 +392,7 @@ export const routes: RouteRecordRaw[] = [
default: NodeView,
header: MainHeader,
sidebar: MainSidebar,
- footer: CanvasChat,
+ footer: CanvasBottomPanel,
},
meta: {
nodeView: true,