From 3a8078068e5c0b01dfd34ff838fe1b30d604abc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Thu, 5 Sep 2024 10:54:35 +0200 Subject: [PATCH] feat(editor): Add AI Assistant support chat (#10656) --- cypress/e2e/45-ai-assistant.cy.ts | 54 +++++++------ .../aiAssistant/end_session_response.json | 4 +- cypress/pages/features/ai-assistant.ts | 15 +++- .../AskAssistantChat.stories.ts | 20 +++++ .../AskAssistantChat/AskAssistantChat.vue | 40 +++++++--- .../AskAssistantChat.spec.ts.snap | 47 +++++++---- packages/design-system/src/locale/lang/en.ts | 11 +-- packages/design-system/src/types/assistant.ts | 1 + .../AskAssistant/AskAssistantChat.vue | 18 +++-- .../src/components/NDVFloatingNodes.vue | 2 +- .../editor-ui/src/stores/assistant.store.ts | 78 ++++++++++++++++--- .../editor-ui/src/types/assistant.types.ts | 12 ++- 12 files changed, 223 insertions(+), 79 deletions(-) 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() { - + > +
@@ -243,20 +248,16 @@ function growInput() {

{{ t('assistantChat.placeholder.2') }} -

-

- {{ t('assistantChat.placeholder.3') }} - {{ t('assistantChat.placeholder.4') }} + {{ t('assistantChat.placeholder.3') }}

- {{ t('assistantChat.placeholder.5') }} + {{ t('assistantChat.placeholder.4') }}

@@ -407,8 +408,29 @@ p { .textMessage { display: flex; - align-items: center; + flex-direction: column; + gap: var(--spacing-xs); font-size: var(--font-size-2xs); + word-break: break-word; +} + +.code-snippet { + border: var(--border-base); + background-color: var(--color-foreground-xlight); + border-radius: var(--border-radius-base); + padding: var(--spacing-2xs); + font-family: var(--font-family-monospace); + max-height: 218px; // 12 lines + overflow: auto; + + pre { + white-space-collapse: collapse; + } + + code { + background-color: transparent; + font-size: var(--font-size-3xs); + } } .block { diff --git a/packages/design-system/src/components/AskAssistantChat/__tests__/__snapshots__/AskAssistantChat.spec.ts.snap b/packages/design-system/src/components/AskAssistantChat/__tests__/__snapshots__/AskAssistantChat.spec.ts.snap index fec00dc9a..c7aa8da5c 100644 --- a/packages/design-system/src/components/AskAssistantChat/__tests__/__snapshots__/AskAssistantChat.spec.ts.snap +++ b/packages/design-system/src/components/AskAssistantChat/__tests__/__snapshots__/AskAssistantChat.spec.ts.snap @@ -132,7 +132,7 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = ` -

@@ -144,9 +144,10 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `

-
+
+ @@ -449,6 +450,7 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = ` + @@ -842,13 +844,10 @@ exports[`AskAssistantChat > renders default placeholder chat correctly 1`] = ` class="info" >

- 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?

- +
+