Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(editor): Add AI Assistant support chat #10656

Merged
merged 13 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 30 additions & 24 deletions cypress/e2e/45-ai-assistant.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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');
});
});
4 changes: 2 additions & 2 deletions cypress/fixtures/aiAssistant/end_session_response.json
Original file line number Diff line number Diff line change
@@ -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!"
},
Expand Down
15 changes: 13 additions & 2 deletions cypress/pages/features/ai-assistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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:
'```javascript\nconst filteredItems = items.filter((item, index) => index % 2 === 0);\nreturn filteredItems;\n```',
read: true,
},
]),
};
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,16 @@ function growInput() {
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-if="message.role === 'user'" v-html="renderMarkdown(message.content)"></span>
<!-- eslint-disable-next-line vue/no-v-html -->
<span
<div
v-else
:class="$style.assistantText"
v-html="renderMarkdown(message.content)"
></span>
></div>
<div
v-if="message?.codeSnippet"
:class="$style['code-snippet']"
v-html="renderMarkdown(message.codeSnippet).trim()"
></div>
<BlinkingCursor
v-if="streaming && i === messages?.length - 1 && message.role === 'assistant'"
/>
Expand Down Expand Up @@ -243,10 +248,10 @@ function growInput() {
</p>
<p>
{{ t('assistantChat.placeholder.2') }}
<InlineAskAssistantButton size="small" :static="true" />
{{ t('assistantChat.placeholder.3') }}
</p>
<p>
{{ t('assistantChat.placeholder.3') }}
<InlineAskAssistantButton size="small" :static="true" />
{{ t('assistantChat.placeholder.4') }}
</p>
<p>
Expand All @@ -256,7 +261,6 @@ function growInput() {
</div>
</div>
<div
v-if="messages?.length"
:class="{ [$style.inputWrapper]: true, [$style.disabledInput]: sessionEnded }"
data-test-id="chat-input-wrapper"
>
Expand Down Expand Up @@ -407,8 +411,28 @@ 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);
max-height: 218px; // 12 lines
font-family: var(--font-family-monospace);

pre {
white-space-collapse: collapse;
}

code {
background-color: transparent;
font-size: var(--font-size-3xs);
}
}

.block {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
<!-- eslint-disable-next-line vue/no-v-html -->

<!-- eslint-disable-next-line vue/no-v-html -->
<span
<div
class="assistantText"
>
<p>
Expand All @@ -144,9 +144,10 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
</p>


</span>
</div>

<!--v-if-->
<!--v-if-->
</div>
<!--v-if-->
</div>
Expand Down Expand Up @@ -449,6 +450,7 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `

</span>
<!--v-if-->
<!--v-if-->
</div>
<!--v-if-->
</div>
Expand Down Expand Up @@ -842,13 +844,10 @@ exports[`AskAssistantChat > renders default placeholder chat correctly 1`] = `
class="info"
>
<p>
I'm your Assistant, here to guide you through your journey with n8n.
I can answer most questions about building workflows in n8n.
</p>
<p>
While I'm still learning, I'm already equipped to help you debug any errors you might encounter.
</p>
<p>
If you run into an issue with a node, you'll see the
For specific tasks, you’ll see the
<button
class="button"
style="height: 18px;"
Expand Down Expand Up @@ -901,15 +900,36 @@ exports[`AskAssistantChat > renders default placeholder chat correctly 1`] = `
</div>
</div>
</button>
button
button in the UI.
</p>
<p>
Clicking it starts a context-aware session, often leading to better results with less effort for you.
</p>
<p>
Clicking it will start a chat with me, and I'll do my best to assist you!
How can I help?
</p>
</div>
</div>
</div>
<!--v-if-->
<div
class="inputWrapper"
data-test-id="chat-input-wrapper"
>
<textarea
data-test-id="chat-input"
placeholder="Enter your response..."
rows="1"
wrap="hard"
/>
<n8n-icon-button
class="sendButton"
data-test-id="send-message-button"
disabled="true"
icon="paper-plane"
size="large"
type="text"
/>
</div>
</div>
</div>
`;
Expand Down Expand Up @@ -1046,7 +1066,7 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
<!-- eslint-disable-next-line vue/no-v-html -->

<!-- eslint-disable-next-line vue/no-v-html -->
<span
<div
class="assistantText"
>
<p>
Expand All @@ -1058,9 +1078,10 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
</p>


</span>
</div>

<!--v-if-->
<!--v-if-->
</div>
<!--v-if-->
</div>
Expand Down Expand Up @@ -1295,7 +1316,7 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
<!-- eslint-disable-next-line vue/no-v-html -->

<!-- eslint-disable-next-line vue/no-v-html -->
<span
<div
class="assistantText"
>
<p>
Expand All @@ -1307,9 +1328,10 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
</p>


</span>
</div>

<!--v-if-->
<!--v-if-->
</div>
<!--v-if-->
</div>
Expand Down
13 changes: 6 additions & 7 deletions packages/design-system/src/locale/lang/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,12 @@ 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':
'Clicking it starts a context-aware session, often leading to better results with less effort for you.',
'assistantChat.placeholder.5': 'How can I help?',
'assistantChat.inputPlaceholder': 'Enter your response...',
'inlineAskAssistantButton.asked': 'Asked',
} as N8nLocale;
1 change: 1 addition & 0 deletions packages/design-system/src/types/assistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export namespace ChatUI {
role: 'assistant' | 'user';
type: 'text';
content: string;
codeSnippet?: string;
}

export interface SummaryBlock {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's not related to this PR. But the backend should be returning this. We discussed this in planning but dropped in handover and final implementation.
I guess it's fine since we are likely to drop this feature any way, so no need to change anything.

(msg) => msg.role === 'assistant' && !['text', 'event'].includes(msg.type),
).length;
if (isFeedback) {
telemetry.track('User gave feedback', {
task,
Expand Down
Loading
Loading