diff --git a/cypress/e2e/45-ai-assistant.cy.ts b/cypress/e2e/45-ai-assistant.cy.ts index 6be8a8db6..f80b4637d 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 c53574d93..9478c3adb 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 dbd849192..fe4e4d643 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 93c90b508..45da52748 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 1a1c50891..1fb5d42c6 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 a9357c4ce..45caa4071 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 b1cab7a74..70da9f2cc 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 9ae4b29a5..732c09b90 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 b26c8988d..303696934 100644 --- a/packages/editor-ui/src/components/NDVFloatingNodes.vue +++ b/packages/editor-ui/src/components/NDVFloatingNodes.vue @@ -143,7 +143,7 @@ defineExpose({