mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
feat(editor): Add AI Assistant support chat (#10656)
This commit is contained in:
parent
899b0a19ef
commit
3a8078068e
|
@ -31,7 +31,8 @@ describe('AI Assistant::enabled', () => {
|
||||||
aiAssistant.getters.askAssistantFloatingButton().click();
|
aiAssistant.getters.askAssistantFloatingButton().click();
|
||||||
aiAssistant.getters.askAssistantChat().should('be.visible');
|
aiAssistant.getters.askAssistantChat().should('be.visible');
|
||||||
aiAssistant.getters.placeholderMessage().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().should('be.visible');
|
||||||
aiAssistant.getters.closeChatButton().click();
|
aiAssistant.getters.closeChatButton().click();
|
||||||
aiAssistant.getters.askAssistantChat().should('not.be.visible');
|
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");
|
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', () => {
|
it('should show quick replies when node is executed after new suggestion', () => {
|
||||||
cy.intercept('POST', '/rest/ai-assistant/chat', (req) => {
|
cy.intercept('POST', '/rest/ai-assistant/chat', (req) => {
|
||||||
req.reply((res) => {
|
req.reply((res) => {
|
||||||
|
@ -281,4 +259,32 @@ describe('AI Assistant::enabled', () => {
|
||||||
aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
|
aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
|
||||||
aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended');
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"sessionId": "f9130bd7-c078-4862-a38a-369b27b0ff20-e96eb9f7-d581-4684-b6a9-fd3dfe9fe1fb-XCldJLlusGrEVku5I9cYT",
|
"sessionId": "1",
|
||||||
"messages": [
|
"messages": [
|
||||||
{
|
{
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"type": "agent-suggestion",
|
"type": "message",
|
||||||
"title": "Glad to Help",
|
"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!"
|
"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!"
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,13 +38,24 @@ export class AIAssistant extends BasePage {
|
||||||
};
|
};
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
enableAssistant(): void {
|
enableAssistant: () => {
|
||||||
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.enabledFor);
|
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.enabledFor);
|
||||||
cy.enableFeature(AI_ASSISTANT_FEATURE.name);
|
cy.enableFeature(AI_ASSISTANT_FEATURE.name);
|
||||||
},
|
},
|
||||||
disableAssistant(): void {
|
disableAssistant: () => {
|
||||||
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.disabledFor);
|
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.disabledFor);
|
||||||
cy.disableFeature(AI_ASSISTANT_FEATURE.name);
|
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');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -247,3 +247,23 @@ AssistantThinkingChat.args = {
|
||||||
},
|
},
|
||||||
loadingMessage: 'Thinking...',
|
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,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
|
@ -163,11 +163,16 @@ function growInput() {
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<span v-if="message.role === 'user'" v-html="renderMarkdown(message.content)"></span>
|
<span v-if="message.role === 'user'" v-html="renderMarkdown(message.content)"></span>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<span
|
<div
|
||||||
v-else
|
v-else
|
||||||
:class="$style.assistantText"
|
:class="$style.assistantText"
|
||||||
v-html="renderMarkdown(message.content)"
|
v-html="renderMarkdown(message.content)"
|
||||||
></span>
|
></div>
|
||||||
|
<div
|
||||||
|
v-if="message?.codeSnippet"
|
||||||
|
:class="$style['code-snippet']"
|
||||||
|
v-html="renderMarkdown(message.codeSnippet).trim()"
|
||||||
|
></div>
|
||||||
<BlinkingCursor
|
<BlinkingCursor
|
||||||
v-if="streaming && i === messages?.length - 1 && message.role === 'assistant'"
|
v-if="streaming && i === messages?.length - 1 && message.role === 'assistant'"
|
||||||
/>
|
/>
|
||||||
|
@ -243,20 +248,16 @@ function growInput() {
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{ t('assistantChat.placeholder.2') }}
|
{{ t('assistantChat.placeholder.2') }}
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{ t('assistantChat.placeholder.3') }}
|
|
||||||
<InlineAskAssistantButton size="small" :static="true" />
|
<InlineAskAssistantButton size="small" :static="true" />
|
||||||
{{ t('assistantChat.placeholder.4') }}
|
{{ t('assistantChat.placeholder.3') }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{ t('assistantChat.placeholder.5') }}
|
{{ t('assistantChat.placeholder.4') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="messages?.length"
|
|
||||||
:class="{ [$style.inputWrapper]: true, [$style.disabledInput]: sessionEnded }"
|
:class="{ [$style.inputWrapper]: true, [$style.disabledInput]: sessionEnded }"
|
||||||
data-test-id="chat-input-wrapper"
|
data-test-id="chat-input-wrapper"
|
||||||
>
|
>
|
||||||
|
@ -407,8 +408,29 @@ p {
|
||||||
|
|
||||||
.textMessage {
|
.textMessage {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
font-size: var(--font-size-2xs);
|
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 {
|
.block {
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<span
|
<div
|
||||||
class="assistantText"
|
class="assistantText"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
|
@ -144,9 +144,10 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
|
@ -449,6 +450,7 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
|
@ -842,13 +844,10 @@ exports[`AskAssistantChat > renders default placeholder chat correctly 1`] = `
|
||||||
class="info"
|
class="info"
|
||||||
>
|
>
|
||||||
<p>
|
<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>
|
||||||
<p>
|
<p>
|
||||||
While I'm still learning, I'm already equipped to help you debug any errors you might encounter.
|
For specific tasks, you’ll see the
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
If you run into an issue with a node, you'll see the
|
|
||||||
<button
|
<button
|
||||||
class="button"
|
class="button"
|
||||||
style="height: 18px;"
|
style="height: 18px;"
|
||||||
|
@ -901,15 +900,33 @@ exports[`AskAssistantChat > renders default placeholder chat correctly 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
button
|
button in the UI.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Clicking it will start a chat with me, and I'll do my best to assist you!
|
How can I help?
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -1046,7 +1063,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 -->
|
||||||
|
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<span
|
<div
|
||||||
class="assistantText"
|
class="assistantText"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
|
@ -1058,9 +1075,10 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
|
@ -1295,7 +1313,7 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<span
|
<div
|
||||||
class="assistantText"
|
class="assistantText"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
|
@ -1307,9 +1325,10 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -39,13 +39,10 @@ export default {
|
||||||
'assistantChat.you': 'You',
|
'assistantChat.you': 'You',
|
||||||
'assistantChat.quickRepliesTitle': 'Quick reply 👇',
|
'assistantChat.quickRepliesTitle': 'Quick reply 👇',
|
||||||
'assistantChat.placeholder.1': () =>
|
'assistantChat.placeholder.1': () =>
|
||||||
"I'm your Assistant, here to guide you through your journey with n8n.",
|
'I can answer most questions about building workflows in n8n.',
|
||||||
'assistantChat.placeholder.2':
|
'assistantChat.placeholder.2': 'For specific tasks, you’ll see the',
|
||||||
"While I'm still learning, I'm already equipped to help you debug any errors you might encounter.",
|
'assistantChat.placeholder.3': 'button in the UI.',
|
||||||
'assistantChat.placeholder.3': "If you run into an issue with a node, you'll see the",
|
'assistantChat.placeholder.4': 'How can I help?',
|
||||||
'assistantChat.placeholder.4': 'button',
|
|
||||||
'assistantChat.placeholder.5':
|
|
||||||
"Clicking it will start a chat with me, and I'll do my best to assist you!",
|
|
||||||
'assistantChat.inputPlaceholder': 'Enter your response...',
|
'assistantChat.inputPlaceholder': 'Enter your response...',
|
||||||
'inlineAskAssistantButton.asked': 'Asked',
|
'inlineAskAssistantButton.asked': 'Asked',
|
||||||
} as N8nLocale;
|
} as N8nLocale;
|
||||||
|
|
|
@ -3,6 +3,7 @@ export namespace ChatUI {
|
||||||
role: 'assistant' | 'user';
|
role: 'assistant' | 'user';
|
||||||
type: 'text';
|
type: 'text';
|
||||||
content: string;
|
content: string;
|
||||||
|
codeSnippet?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SummaryBlock {
|
export interface SummaryBlock {
|
||||||
|
|
|
@ -27,14 +27,16 @@ function onResizeDebounced(data: { direction: string; x: number; width: number }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onUserMessage(content: string, quickReplyType?: string, isFeedback = false) {
|
async function onUserMessage(content: string, quickReplyType?: string, isFeedback = false) {
|
||||||
await assistantStore.sendMessage({ text: content, quickReplyType });
|
// If there is no current session running, initialize the support chat session
|
||||||
const task = 'error';
|
if (!assistantStore.currentSessionId) {
|
||||||
const solutionCount =
|
await assistantStore.initSupportChat(content);
|
||||||
task === 'error'
|
} else {
|
||||||
? assistantStore.chatMessages.filter(
|
await assistantStore.sendMessage({ text: content, quickReplyType });
|
||||||
(msg) => msg.role === 'assistant' && !['text', 'event'].includes(msg.type),
|
}
|
||||||
).length
|
const task = assistantStore.isSupportChatSessionInProgress ? 'support' : 'error';
|
||||||
: null;
|
const solutionCount = assistantStore.chatMessages.filter(
|
||||||
|
(msg) => msg.role === 'assistant' && !['text', 'event'].includes(msg.type),
|
||||||
|
).length;
|
||||||
if (isFeedback) {
|
if (isFeedback) {
|
||||||
telemetry.track('User gave feedback', {
|
telemetry.track('User gave feedback', {
|
||||||
task,
|
task,
|
||||||
|
|
|
@ -143,7 +143,7 @@ defineExpose({
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.floatingNodes {
|
.floatingNodes {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
|
@ -79,9 +79,13 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
ENABLED_VIEWS.includes(route.name as VIEWS),
|
ENABLED_VIEWS.includes(route.name as VIEWS),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const assistantMessages = computed(() =>
|
||||||
|
chatMessages.value.filter((msg) => msg.role === 'assistant'),
|
||||||
|
);
|
||||||
|
const usersMessages = computed(() => chatMessages.value.filter((msg) => msg.role === 'user'));
|
||||||
|
|
||||||
const isSessionEnded = computed(() => {
|
const isSessionEnded = computed(() => {
|
||||||
const assistantMessages = chatMessages.value.filter((msg) => msg.role === 'assistant');
|
const lastAssistantMessage = assistantMessages.value[assistantMessages.value.length - 1];
|
||||||
const lastAssistantMessage = assistantMessages[assistantMessages.length - 1];
|
|
||||||
|
|
||||||
const sessionExplicitlyEnded =
|
const sessionExplicitlyEnded =
|
||||||
lastAssistantMessage?.type === 'event' && lastAssistantMessage?.eventName === 'end-session';
|
lastAssistantMessage?.type === 'event' && lastAssistantMessage?.eventName === 'end-session';
|
||||||
|
@ -106,6 +110,10 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
).length,
|
).length,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isSupportChatSessionInProgress = computed(() => {
|
||||||
|
return currentSessionId.value !== undefined && chatSessionError.value === undefined;
|
||||||
|
});
|
||||||
|
|
||||||
watch(route, () => {
|
watch(route, () => {
|
||||||
const activeWorkflowId = workflowsStore.workflowId;
|
const activeWorkflowId = workflowsStore.workflowId;
|
||||||
if (!currentSessionId.value || currentSessionWorkflowId.value === activeWorkflowId) {
|
if (!currentSessionId.value || currentSessionWorkflowId.value === activeWorkflowId) {
|
||||||
|
@ -137,17 +145,21 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
// Looks smoother if we wait for slide animation to finish before updating the grid width
|
// Looks smoother if we wait for slide animation to finish before updating the grid width
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uiStore.appGridWidth = window.innerWidth;
|
uiStore.appGridWidth = window.innerWidth;
|
||||||
|
// If session has ended, reset the chat
|
||||||
|
if (isSessionEnded.value) {
|
||||||
|
resetAssistantChat();
|
||||||
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addAssistantMessages(assistantMessages: ChatRequest.MessageResponse[], id: string) {
|
function addAssistantMessages(newMessages: ChatRequest.MessageResponse[], id: string) {
|
||||||
const read = chatWindowOpen.value;
|
const read = chatWindowOpen.value;
|
||||||
const messages = [...chatMessages.value].filter(
|
const messages = [...chatMessages.value].filter(
|
||||||
(msg) => !(msg.id === id && msg.role === 'assistant'),
|
(msg) => !(msg.id === id && msg.role === 'assistant'),
|
||||||
);
|
);
|
||||||
assistantThinkingMessage.value = undefined;
|
assistantThinkingMessage.value = undefined;
|
||||||
// TODO: simplify
|
// TODO: simplify
|
||||||
assistantMessages.forEach((msg) => {
|
newMessages.forEach((msg) => {
|
||||||
if (msg.type === 'message') {
|
if (msg.type === 'message') {
|
||||||
messages.push({
|
messages.push({
|
||||||
id,
|
id,
|
||||||
|
@ -155,6 +167,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: msg.text,
|
content: msg.text,
|
||||||
quickReplies: msg.quickReplies,
|
quickReplies: msg.quickReplies,
|
||||||
|
codeSnippet: msg.codeSnippet,
|
||||||
read,
|
read,
|
||||||
});
|
});
|
||||||
} else if (msg.type === 'code-diff') {
|
} else if (msg.type === 'code-diff') {
|
||||||
|
@ -262,10 +275,15 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
'Assistant session started',
|
'Assistant session started',
|
||||||
{
|
{
|
||||||
chat_session_id: currentSessionId.value,
|
chat_session_id: currentSessionId.value,
|
||||||
task: 'error',
|
task: isSupportChatSessionInProgress.value ? 'support' : 'error',
|
||||||
},
|
},
|
||||||
{ withPostHog: true },
|
{ withPostHog: true },
|
||||||
);
|
);
|
||||||
|
// Track first user message in support chat now that we have a session id
|
||||||
|
if (usersMessages.value.length === 1 && isSupportChatSessionInProgress.value) {
|
||||||
|
const firstUserMessage = usersMessages.value[0] as ChatUI.TextMessage;
|
||||||
|
trackUserMessage(firstUserMessage.content, false);
|
||||||
|
}
|
||||||
} else if (currentSessionId.value !== response.sessionId) {
|
} else if (currentSessionId.value !== response.sessionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -289,6 +307,33 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
}, 4000);
|
}, 4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function initSupportChat(userMessage: string) {
|
||||||
|
const id = getRandomId();
|
||||||
|
resetAssistantChat();
|
||||||
|
chatSessionError.value = undefined;
|
||||||
|
currentSessionActiveExecutionId.value = undefined;
|
||||||
|
currentSessionWorkflowId.value = workflowsStore.workflowId;
|
||||||
|
addUserMessage(userMessage, id);
|
||||||
|
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking'));
|
||||||
|
streaming.value = true;
|
||||||
|
chatWithAssistant(
|
||||||
|
rootStore.restApiContext,
|
||||||
|
{
|
||||||
|
payload: {
|
||||||
|
role: 'user',
|
||||||
|
type: 'init-support-chat',
|
||||||
|
user: {
|
||||||
|
firstName: usersStore.currentUser?.firstName ?? '',
|
||||||
|
},
|
||||||
|
question: userMessage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(msg) => onEachStreamingMessage(msg, id),
|
||||||
|
() => onDoneStreaming(id),
|
||||||
|
(e) => handleServiceError(e, id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function initErrorHelper(context: ChatRequest.ErrorContext) {
|
async function initErrorHelper(context: ChatRequest.ErrorContext) {
|
||||||
const id = getRandomId();
|
const id = getRandomId();
|
||||||
if (chatSessionError.value) {
|
if (chatSessionError.value) {
|
||||||
|
@ -433,18 +478,26 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
() => onDoneStreaming(id),
|
() => onDoneStreaming(id),
|
||||||
(e) => handleServiceError(e, id),
|
(e) => handleServiceError(e, id),
|
||||||
);
|
);
|
||||||
telemetry.track('User sent message in Assistant', {
|
trackUserMessage(chatMessage.text, !!chatMessage.quickReplyType);
|
||||||
message: chatMessage.text,
|
|
||||||
is_quick_reply: !!chatMessage.quickReplyType,
|
|
||||||
chat_session_id: currentSessionId.value,
|
|
||||||
message_number: chatMessages.value.filter((msg) => msg.role === 'user').length,
|
|
||||||
});
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
// in case of assert
|
// in case of assert
|
||||||
handleServiceError(e, id);
|
handleServiceError(e, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trackUserMessage(message: string, isQuickReply: boolean) {
|
||||||
|
if (!currentSessionId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
telemetry.track('User sent message in Assistant', {
|
||||||
|
message,
|
||||||
|
is_quick_reply: isQuickReply,
|
||||||
|
chat_session_id: currentSessionId.value,
|
||||||
|
message_number: usersMessages.value.length,
|
||||||
|
task: isSupportChatSessionInProgress.value ? 'support' : 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateParameters(nodeName: string, parameters: INodeParameters) {
|
function updateParameters(nodeName: string, parameters: INodeParameters) {
|
||||||
if (ndvStore.activeNodeName === nodeName) {
|
if (ndvStore.activeNodeName === nodeName) {
|
||||||
Object.keys(parameters).forEach((key) => {
|
Object.keys(parameters).forEach((key) => {
|
||||||
|
@ -584,6 +637,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
updateWindowWidth,
|
updateWindowWidth,
|
||||||
isNodeErrorActive,
|
isNodeErrorActive,
|
||||||
initErrorHelper,
|
initErrorHelper,
|
||||||
|
initSupportChat,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
applyCodeDiff,
|
applyCodeDiff,
|
||||||
undoCodeDiff,
|
undoCodeDiff,
|
||||||
|
@ -591,5 +645,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
chatWindowOpen,
|
chatWindowOpen,
|
||||||
addAssistantMessages,
|
addAssistantMessages,
|
||||||
assistantThinkingMessage,
|
assistantThinkingMessage,
|
||||||
|
chatSessionError,
|
||||||
|
isSupportChatSessionInProgress,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -32,6 +32,15 @@ export namespace ChatRequest {
|
||||||
authType?: { name: string; value: string };
|
authType?: { name: string; value: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InitSupportChat {
|
||||||
|
role: 'user';
|
||||||
|
type: 'init-support-chat';
|
||||||
|
user: {
|
||||||
|
firstName: string;
|
||||||
|
};
|
||||||
|
question: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type InteractionEventName = 'node-execution-succeeded' | 'node-execution-errored';
|
export type InteractionEventName = 'node-execution-succeeded' | 'node-execution-errored';
|
||||||
|
|
||||||
interface EventRequestPayload {
|
interface EventRequestPayload {
|
||||||
|
@ -50,7 +59,7 @@ export namespace ChatRequest {
|
||||||
|
|
||||||
export type RequestPayload =
|
export type RequestPayload =
|
||||||
| {
|
| {
|
||||||
payload: InitErrorHelper;
|
payload: InitErrorHelper | InitSupportChat;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
payload: EventRequestPayload | UserChatMessage;
|
payload: EventRequestPayload | UserChatMessage;
|
||||||
|
@ -77,6 +86,7 @@ export namespace ChatRequest {
|
||||||
type: 'message';
|
type: 'message';
|
||||||
text: string;
|
text: string;
|
||||||
step?: 'n8n_documentation' | 'n8n_forum';
|
step?: 'n8n_documentation' | 'n8n_forum';
|
||||||
|
codeSnippet?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AssistantSummaryMessage {
|
interface AssistantSummaryMessage {
|
||||||
|
|
Loading…
Reference in a new issue