diff --git a/cypress/e2e/45-ai-assistant.cy.ts b/cypress/e2e/45-ai-assistant.cy.ts index 6be8a8db60..f80b4637d0 100644 --- a/cypress/e2e/45-ai-assistant.cy.ts +++ b/cypress/e2e/45-ai-assistant.cy.ts @@ -31,7 +31,8 @@ describe('AI Assistant::enabled', () => { aiAssistant.getters.askAssistantFloatingButton().click(); aiAssistant.getters.askAssistantChat().should('be.visible'); aiAssistant.getters.placeholderMessage().should('be.visible'); - aiAssistant.getters.chatInputWrapper().should('not.exist'); + aiAssistant.getters.chatInput().should('be.visible'); + aiAssistant.getters.sendMessageButton().should('be.disabled'); aiAssistant.getters.closeChatButton().should('be.visible'); aiAssistant.getters.closeChatButton().click(); aiAssistant.getters.askAssistantChat().should('not.be.visible'); @@ -137,29 +138,6 @@ describe('AI Assistant::enabled', () => { aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it"); }); - it('should send message to assistant when node is executed only once', () => { - const TOTAL_REQUEST_COUNT = 1; - cy.intercept('POST', '/rest/ai-assistant/chat', { - statusCode: 200, - fixture: 'aiAssistant/simple_message_response.json', - }).as('chatRequest'); - cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); - wf.actions.openNode('Edit Fields'); - ndv.getters.nodeExecuteButton().click(); - aiAssistant.getters.nodeErrorViewAssistantButton().click(); - cy.wait('@chatRequest'); - aiAssistant.getters.chatMessagesAssistant().should('have.length', 1); - cy.get('@chatRequest.all').then((interceptions) => { - expect(interceptions).to.have.length(TOTAL_REQUEST_COUNT); - }); - // Executing the same node should not send a new message if users haven't responded to quick replies - ndv.getters.nodeExecuteButton().click(); - cy.get('@chatRequest.all').then((interceptions) => { - expect(interceptions).to.have.length(TOTAL_REQUEST_COUNT); - }); - aiAssistant.getters.chatMessagesAssistant().should('have.length', 2); - }); - it('should show quick replies when node is executed after new suggestion', () => { cy.intercept('POST', '/rest/ai-assistant/chat', (req) => { req.reply((res) => { @@ -281,4 +259,32 @@ describe('AI Assistant::enabled', () => { aiAssistant.getters.chatMessagesSystem().should('have.length', 1); aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended'); }); + + it('should reset session after it ended and sidebar is closed', () => { + cy.intercept('POST', '/rest/ai-assistant/chat', (req) => { + req.reply((res) => { + if (['init-support-chat'].includes(req.body.payload.type)) { + res.send({ statusCode: 200, fixture: 'aiAssistant/simple_message_response.json' }); + } else { + res.send({ statusCode: 200, fixture: 'aiAssistant/end_session_response.json' }); + } + }); + }).as('chatRequest'); + aiAssistant.actions.openChat(); + aiAssistant.actions.sendMessage('Hello'); + cy.wait('@chatRequest'); + aiAssistant.actions.closeChat(); + aiAssistant.actions.openChat(); + // After closing and reopening the chat, all messages should be still there + aiAssistant.getters.chatMessagesAll().should('have.length', 2); + // End the session + aiAssistant.actions.sendMessage('Thanks, bye'); + cy.wait('@chatRequest'); + aiAssistant.getters.chatMessagesSystem().should('have.length', 1); + aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended'); + aiAssistant.actions.closeChat(); + aiAssistant.actions.openChat(); + // Now, session should be reset + aiAssistant.getters.placeholderMessage().should('be.visible'); + }); }); diff --git a/cypress/fixtures/aiAssistant/end_session_response.json b/cypress/fixtures/aiAssistant/end_session_response.json index c53574d93a..9478c3adb3 100644 --- a/cypress/fixtures/aiAssistant/end_session_response.json +++ b/cypress/fixtures/aiAssistant/end_session_response.json @@ -1,9 +1,9 @@ { - "sessionId": "f9130bd7-c078-4862-a38a-369b27b0ff20-e96eb9f7-d581-4684-b6a9-fd3dfe9fe1fb-XCldJLlusGrEVku5I9cYT", + "sessionId": "1", "messages": [ { "role": "assistant", - "type": "agent-suggestion", + "type": "message", "title": "Glad to Help", "text": "I'm glad I could help. If you have any more questions or need further assistance with your n8n workflows, feel free to ask!" }, diff --git a/cypress/pages/features/ai-assistant.ts b/cypress/pages/features/ai-assistant.ts index dbd8491923..fe4e4d6435 100644 --- a/cypress/pages/features/ai-assistant.ts +++ b/cypress/pages/features/ai-assistant.ts @@ -38,13 +38,24 @@ export class AIAssistant extends BasePage { }; actions = { - enableAssistant(): void { + enableAssistant: () => { overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.enabledFor); cy.enableFeature(AI_ASSISTANT_FEATURE.name); }, - disableAssistant(): void { + disableAssistant: () => { overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.disabledFor); cy.disableFeature(AI_ASSISTANT_FEATURE.name); }, + sendMessage: (message: string) => { + this.getters.chatInput().type(message).type('{enter}'); + }, + closeChat: () => { + this.getters.closeChatButton().click(); + this.getters.askAssistantChat().should('not.be.visible'); + }, + openChat: () => { + this.getters.askAssistantFloatingButton().click(); + this.getters.askAssistantChat().should('be.visible'); + }, }; } diff --git a/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts b/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts index 93c90b5089..45da527485 100644 --- a/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts +++ b/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts @@ -247,3 +247,23 @@ AssistantThinkingChat.args = { }, loadingMessage: 'Thinking...', }; + +export const WithCodeSnippet = Template.bind({}); +WithCodeSnippet.args = { + user: { + firstName: 'Max', + lastName: 'Test', + }, + messages: getMessages([ + { + id: '58575953', + type: 'text', + role: 'assistant', + content: + 'To filter every other item in the Code node, you can use the following JavaScript code snippet. This code will iterate through the incoming items and only pass through every other item.', + codeSnippet: + "node.on('input', function(msg) {\n if (msg.seed) { dummyjson.seed = msg.seed; }\n try {\n var value = dummyjson.parse(node.template, {mockdata: msg});\n if (node.syntax === 'json') {\n try { value = JSON.parse(value); }\n catch(e) { node.error(RED._('datagen.errors.json-error')); }\n }\n if (node.fieldType === 'msg') {\n RED.util.setMessageProperty(msg,node.field,value);\n }\n else if (node.fieldType === 'flow') {\n node.context().flow.set(node.field,value);\n }\n else if (node.fieldType === 'global') {\n node.context().global.set(node.field,value);\n }\n node.send(msg);\n }\n catch(e) {", + read: true, + }, + ]), +}; diff --git a/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.vue b/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.vue index 1a1c50891f..1fb5d42c63 100644 --- a/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.vue +++ b/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.vue @@ -163,11 +163,16 @@ function growInput() { - + > +
{{ t('assistantChat.placeholder.2') }} -
-
- {{ t('assistantChat.placeholder.3') }}
- {{ t('assistantChat.placeholder.5') }} + {{ t('assistantChat.placeholder.4') }}
@@ -144,9 +144,10 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
- +- I'm your Assistant, here to guide you through your journey with n8n. + I can answer most questions about building workflows in n8n.
- While I'm still learning, I'm already equipped to help you debug any errors you might encounter. -
-- If you run into an issue with a node, you'll see the + For specific tasks, you’ll see the - button + button in the UI.
- Clicking it will start a chat with me, and I'll do my best to assist you! + How can I help?
- +@@ -1058,9 +1075,10 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
- + + @@ -1295,7 +1313,7 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = ` -@@ -1307,9 +1325,10 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
- + + diff --git a/packages/design-system/src/locale/lang/en.ts b/packages/design-system/src/locale/lang/en.ts index a9357c4ce9..45caa4071e 100644 --- a/packages/design-system/src/locale/lang/en.ts +++ b/packages/design-system/src/locale/lang/en.ts @@ -39,13 +39,10 @@ export default { 'assistantChat.you': 'You', 'assistantChat.quickRepliesTitle': 'Quick reply 👇', 'assistantChat.placeholder.1': () => - "I'm your Assistant, here to guide you through your journey with n8n.", - 'assistantChat.placeholder.2': - "While I'm still learning, I'm already equipped to help you debug any errors you might encounter.", - 'assistantChat.placeholder.3': "If you run into an issue with a node, you'll see the", - 'assistantChat.placeholder.4': 'button', - 'assistantChat.placeholder.5': - "Clicking it will start a chat with me, and I'll do my best to assist you!", + 'I can answer most questions about building workflows in n8n.', + 'assistantChat.placeholder.2': 'For specific tasks, you’ll see the', + 'assistantChat.placeholder.3': 'button in the UI.', + 'assistantChat.placeholder.4': 'How can I help?', 'assistantChat.inputPlaceholder': 'Enter your response...', 'inlineAskAssistantButton.asked': 'Asked', } as N8nLocale; diff --git a/packages/design-system/src/types/assistant.ts b/packages/design-system/src/types/assistant.ts index b1cab7a748..70da9f2ccc 100644 --- a/packages/design-system/src/types/assistant.ts +++ b/packages/design-system/src/types/assistant.ts @@ -3,6 +3,7 @@ export namespace ChatUI { role: 'assistant' | 'user'; type: 'text'; content: string; + codeSnippet?: string; } export interface SummaryBlock { diff --git a/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue b/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue index 9ae4b29a5f..732c09b903 100644 --- a/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue +++ b/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue @@ -27,14 +27,16 @@ function onResizeDebounced(data: { direction: string; x: number; width: number } } async function onUserMessage(content: string, quickReplyType?: string, isFeedback = false) { - await assistantStore.sendMessage({ text: content, quickReplyType }); - const task = 'error'; - const solutionCount = - task === 'error' - ? assistantStore.chatMessages.filter( - (msg) => msg.role === 'assistant' && !['text', 'event'].includes(msg.type), - ).length - : null; + // If there is no current session running, initialize the support chat session + if (!assistantStore.currentSessionId) { + await assistantStore.initSupportChat(content); + } else { + await assistantStore.sendMessage({ text: content, quickReplyType }); + } + const task = assistantStore.isSupportChatSessionInProgress ? 'support' : 'error'; + const solutionCount = assistantStore.chatMessages.filter( + (msg) => msg.role === 'assistant' && !['text', 'event'].includes(msg.type), + ).length; if (isFeedback) { telemetry.track('User gave feedback', { task, diff --git a/packages/editor-ui/src/components/NDVFloatingNodes.vue b/packages/editor-ui/src/components/NDVFloatingNodes.vue index b26c8988d3..303696934c 100644 --- a/packages/editor-ui/src/components/NDVFloatingNodes.vue +++ b/packages/editor-ui/src/components/NDVFloatingNodes.vue @@ -143,7 +143,7 @@ defineExpose({