diff --git a/cypress/e2e/45-ai-assistant.cy.ts b/cypress/e2e/45-ai-assistant.cy.ts index 37d38f739f..6be8a8db60 100644 --- a/cypress/e2e/45-ai-assistant.cy.ts +++ b/cypress/e2e/45-ai-assistant.cy.ts @@ -130,14 +130,15 @@ describe('AI Assistant::enabled', () => { ndv.getters.nodeExecuteButton().click(); aiAssistant.getters.nodeErrorViewAssistantButton().click(); cy.wait('@chatRequest'); - aiAssistant.getters.quickReplies().should('have.length', 2); - aiAssistant.getters.quickReplies().eq(0).click(); + aiAssistant.getters.quickReplyButtons().should('have.length', 2); + aiAssistant.getters.quickReplyButtons().eq(0).click(); cy.wait('@chatRequest'); aiAssistant.getters.chatMessagesUser().should('have.length', 1); aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it"); }); - it('should send message to assistant when node is executed', () => { + 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', @@ -148,10 +149,46 @@ describe('AI Assistant::enabled', () => { aiAssistant.getters.nodeErrorViewAssistantButton().click(); cy.wait('@chatRequest'); aiAssistant.getters.chatMessagesAssistant().should('have.length', 1); - // Executing the same node should sende a new message to the assistant automatically + 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) => { + if (['init-error-helper', 'message'].includes(req.body.payload.type)) { + res.send({ statusCode: 200, fixture: 'aiAssistant/simple_message_response.json' }); + } else if (req.body.payload.type === 'event') { + res.send({ statusCode: 200, fixture: 'aiAssistant/node_execution_error_response.json' }); + } else { + res.send({ statusCode: 500 }); + } + }); + }).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); ndv.getters.nodeExecuteButton().click(); cy.wait('@chatRequest'); - aiAssistant.getters.chatMessagesAssistant().should('have.length', 2); + // Respond 'Yes' to the quick reply (request new suggestion) + aiAssistant.getters.quickReplies().contains('Yes').click(); + cy.wait('@chatRequest'); + // No quick replies at this point + aiAssistant.getters.quickReplies().should('not.exist'); + ndv.getters.nodeExecuteButton().click(); + // But after executing the node again, quick replies should be shown + aiAssistant.getters.chatMessagesAssistant().should('have.length', 4); + aiAssistant.getters.quickReplies().should('have.length', 2); }); it('should warn before starting a new session', () => { diff --git a/cypress/fixtures/aiAssistant/node_execution_error_response.json b/cypress/fixtures/aiAssistant/node_execution_error_response.json new file mode 100644 index 0000000000..5fabc17034 --- /dev/null +++ b/cypress/fixtures/aiAssistant/node_execution_error_response.json @@ -0,0 +1,20 @@ +{ + "sessionId": "1", + "messages": [ + { + "role": "assistant", + "type": "message", + "text": "It seems like my suggestion did not work. Do you want me to come up with a different suggestion? You can also provide more context via the chat.", + "quickReplies": [ + { + "text": "Yes", + "type": "new-suggestion" + }, + { + "text": "No, I don't think you can help", + "type": "event:end-session" + } + ] + } + ] +} diff --git a/cypress/pages/features/ai-assistant.ts b/cypress/pages/features/ai-assistant.ts index abca07fbbe..dbd8491923 100644 --- a/cypress/pages/features/ai-assistant.ts +++ b/cypress/pages/features/ai-assistant.ts @@ -26,7 +26,8 @@ export class AIAssistant extends BasePage { chatMessagesAssistant: () => cy.getByTestId('chat-message-assistant'), chatMessagesUser: () => cy.getByTestId('chat-message-user'), chatMessagesSystem: () => cy.getByTestId('chat-message-system'), - quickReplies: () => cy.getByTestId('quick-replies').find('button'), + quickReplies: () => cy.getByTestId('quick-replies'), + quickReplyButtons: () => this.getters.quickReplies().find('button'), newAssistantSessionModal: () => cy.getByTestId('new-assistant-session-modal'), codeDiffs: () => cy.getByTestId('code-diff-suggestion'), applyCodeDiffButtons: () => cy.getByTestId('replace-code-button'), diff --git a/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts b/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts index ec44622844..93c90b5089 100644 --- a/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts +++ b/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts @@ -238,3 +238,12 @@ EndOfSessionChat.args = { }, ]), }; + +export const AssistantThinkingChat = Template.bind({}); +AssistantThinkingChat.args = { + user: { + firstName: 'Max', + lastName: 'Test', + }, + loadingMessage: 'Thinking...', +}; diff --git a/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.vue b/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.vue index a1ae2e8e88..1a1c50891f 100644 --- a/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.vue +++ b/packages/design-system/src/components/AskAssistantChat/AskAssistantChat.vue @@ -3,6 +3,7 @@ import { computed, ref } from 'vue'; import AssistantIcon from '../AskAssistantIcon/AssistantIcon.vue'; import AssistantText from '../AskAssistantText/AssistantText.vue'; import AssistantAvatar from '../AskAssistantAvatar/AssistantAvatar.vue'; +import AssistantLoadingMessage from '../AskAssistantLoadingMessage/AssistantLoadingMessage.vue'; import CodeDiff from '../CodeDiff/CodeDiff.vue'; import type { ChatUI } from '../../types/assistant'; import BlinkingCursor from '../BlinkingCursor/BlinkingCursor.vue'; @@ -33,11 +34,13 @@ interface Props { }; messages?: ChatUI.AssistantMessage[]; streaming?: boolean; + loadingMessage?: string; + sessionId?: string; } const emit = defineEmits<{ close: []; - message: [string, string | undefined]; + message: [string, string?, boolean?]; codeReplace: [number]; codeUndo: [number]; }>(); @@ -58,17 +61,21 @@ const sendDisabled = computed(() => { return !textInputValue.value || props.streaming || sessionEnded.value; }); +const showPlaceholder = computed(() => { + return !props.messages?.length && !props.loadingMessage && !props.sessionId; +}); + function isEndOfSessionEvent(event?: ChatUI.AssistantMessage) { return event?.type === 'event' && event?.eventName === 'end-session'; } function onQuickReply(opt: ChatUI.QuickReply) { - emit('message', opt.text, opt.type); + emit('message', opt.text, opt.type, opt.isFeedback); } function onSendMessage() { if (sendDisabled.value) return; - emit('message', textInputValue.value, undefined); + emit('message', textInputValue.value); textInputValue.value = ''; if (chatInput.value) { chatInput.value.style.height = 'auto'; @@ -221,26 +228,30 @@ function growInput() { - -
+
+ +
+
Hi {{ user?.firstName }} 👋

- {{ - t('assistantChat.placeholder.1', [ - `${user?.firstName}`, - t('assistantChat.aiAssistantName'), - ]) - }} + {{ t('assistantChat.placeholder.1') }}

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

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

+

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

@@ -325,6 +336,10 @@ p { .messages { padding: var(--spacing-xs); + + & + & { + padding-top: 0; + } } .message { @@ -391,6 +406,8 @@ p { } .textMessage { + display: flex; + align-items: center; font-size: var(--font-size-2xs); } 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 826732bdb8..fec00dc9a5 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 @@ -730,6 +730,7 @@ Testing more code +
renders default placeholder chat correctly 1`] = `
+
renders default placeholder chat correctly 1`] = ` class="info" >

- Hi Kobi, I'm Assistant and I'm here to assist you with building workflows. + I'm your Assistant, here to guide you through your journey with n8n.

- Whenever you encounter a task that I can help with, you'll see the + 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

- button. + button

- Clicking it starts a chat session with me. + Clicking it will start a chat with me, and I'll do my best to assist you!

@@ -1131,6 +1136,7 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = ` +
renders streaming chat correctly 1`] = `
+
({ + setup: () => ({ args }), + props: Object.keys(argTypes), + components: { + AssistantLoadingMessage, + }, + template: `
`, +}); + +export const Default = Template.bind({}); +Default.args = { + message: 'Searching n8n documentation for the best possible answer...', +}; + +export const NarrowContainer = Template.bind({}); +NarrowContainer.args = { + ...Default.args, + templateWidth: '200px', +}; diff --git a/packages/design-system/src/components/AskAssistantLoadingMessage/AssistantLoadingMessage.vue b/packages/design-system/src/components/AskAssistantLoadingMessage/AssistantLoadingMessage.vue new file mode 100644 index 0000000000..2aa045565e --- /dev/null +++ b/packages/design-system/src/components/AskAssistantLoadingMessage/AssistantLoadingMessage.vue @@ -0,0 +1,122 @@ + + + + + + + diff --git a/packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.stories.ts b/packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.stories.ts new file mode 100644 index 0000000000..870127f0d3 --- /dev/null +++ b/packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.stories.ts @@ -0,0 +1,30 @@ +import DemoComponent from './DemoComponent.vue'; +import type { StoryFn } from '@storybook/vue3'; + +export default { + title: 'Assistant/AskAssistantLoadingMessageTransitions', + component: DemoComponent, + argTypes: {}, +}; + +const Template: StoryFn = (args, { argTypes }) => ({ + setup: () => ({ args }), + props: Object.keys(argTypes), + components: { + DemoComponent, + }, + template: '', +}); + +export const Default = Template.bind({}); +Default.args = {}; + +export const Horizontal = Template.bind({}); +Horizontal.args = { + animationType: 'slide-horizontal', +}; + +export const Fade = Template.bind({}); +Fade.args = { + animationType: 'fade', +}; diff --git a/packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.vue b/packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.vue new file mode 100644 index 0000000000..deaf903af5 --- /dev/null +++ b/packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/packages/design-system/src/components/AskAssistantLoadingMessage/__tests__/AskAssistantLoadingMessage.spec.ts b/packages/design-system/src/components/AskAssistantLoadingMessage/__tests__/AskAssistantLoadingMessage.spec.ts new file mode 100644 index 0000000000..86f9b8ae51 --- /dev/null +++ b/packages/design-system/src/components/AskAssistantLoadingMessage/__tests__/AskAssistantLoadingMessage.spec.ts @@ -0,0 +1,13 @@ +import { render } from '@testing-library/vue'; +import AssistantLoadingMessage from '../AssistantLoadingMessage.vue'; + +describe('AssistantLoadingMessage', () => { + it('renders loading message correctly', () => { + const { container } = render(AssistantLoadingMessage, { + props: { + loadingMessage: 'Thinking...', + }, + }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/design-system/src/components/AskAssistantLoadingMessage/__tests__/__snapshots__/AskAssistantLoadingMessage.spec.ts.snap b/packages/design-system/src/components/AskAssistantLoadingMessage/__tests__/__snapshots__/AskAssistantLoadingMessage.spec.ts.snap new file mode 100644 index 0000000000..32d8db1bba --- /dev/null +++ b/packages/design-system/src/components/AskAssistantLoadingMessage/__tests__/__snapshots__/AskAssistantLoadingMessage.spec.ts.snap @@ -0,0 +1,71 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`AssistantLoadingMessage > renders loading message correctly 1`] = ` +
+
+
+
+ + + + + + + + + + +
+
+
+ + + +
+
+
+`; diff --git a/packages/design-system/src/locale/lang/en.ts b/packages/design-system/src/locale/lang/en.ts index 449a617f91..a9357c4ce9 100644 --- a/packages/design-system/src/locale/lang/en.ts +++ b/packages/design-system/src/locale/lang/en.ts @@ -38,12 +38,14 @@ export default { 'assistantChat.sessionEndMessage.2': 'button in n8n', 'assistantChat.you': 'You', 'assistantChat.quickRepliesTitle': 'Quick reply 👇', - 'assistantChat.placeholder.1': (options: string[]) => - `Hi ${options[0][0] || 'there'}, I'm ${options[0][1]} and I'm here to assist you with building workflows.`, + 'assistantChat.placeholder.1': () => + "I'm your Assistant, here to guide you through your journey with n8n.", 'assistantChat.placeholder.2': - "Whenever you encounter a task that I can help with, you'll see the", - 'assistantChat.placeholder.3': 'button.', - 'assistantChat.placeholder.4': 'Clicking it starts a chat session with me.', + "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!", '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 40e1340d46..b1cab7a748 100644 --- a/packages/design-system/src/types/assistant.ts +++ b/packages/design-system/src/types/assistant.ts @@ -32,6 +32,7 @@ export namespace ChatUI { export interface QuickReply { type: string; text: string; + isFeedback?: boolean; } export interface ErrorMessage { diff --git a/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue b/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue index ab4e62efa3..9ae4b29a5f 100644 --- a/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue +++ b/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue @@ -16,6 +16,8 @@ const user = computed(() => ({ lastName: usersStore.currentUser?.lastName ?? '', })); +const loadingMessage = computed(() => assistantStore.assistantThinkingMessage); + function onResize(data: { direction: string; x: number; width: number }) { assistantStore.updateWindowWidth(data.width); } @@ -24,7 +26,7 @@ function onResizeDebounced(data: { direction: string; x: number; width: number } void useDebounce().callDebounced(onResize, { debounceTime: 10, trailing: true }, data); } -async function onUserMessage(content: string, quickReplyType?: string) { +async function onUserMessage(content: string, quickReplyType?: string, isFeedback = false) { await assistantStore.sendMessage({ text: content, quickReplyType }); const task = 'error'; const solutionCount = @@ -33,9 +35,10 @@ async function onUserMessage(content: string, quickReplyType?: string) { (msg) => msg.role === 'assistant' && !['text', 'event'].includes(msg.type), ).length : null; - if (quickReplyType === 'all-good' || quickReplyType === 'still-stuck') { + if (isFeedback) { telemetry.track('User gave feedback', { task, + chat_session_id: assistantStore.currentSessionId, is_quick_reply: !!quickReplyType, is_positive: quickReplyType === 'all-good', solution_count: solutionCount, @@ -83,6 +86,8 @@ function onClose() { :user="user" :messages="assistantStore.chatMessages" :streaming="assistantStore.streaming" + :loading-message="loadingMessage" + :session-id="assistantStore.currentSessionId" @close="onClose" @message="onUserMessage" @code-replace="onCodeReplace" diff --git a/packages/editor-ui/src/composables/useToast.ts b/packages/editor-ui/src/composables/useToast.ts index 609683b7e1..ea0a367de3 100644 --- a/packages/editor-ui/src/composables/useToast.ts +++ b/packages/editor-ui/src/composables/useToast.ts @@ -22,7 +22,7 @@ const messageDefaults: Partial> = { position: 'bottom-right', zIndex: 1900, // above NDV and below the modals offset: 64, - appendTo: '#node-view-root', + appendTo: '#app-grid', customClass: 'content-toast', }; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index e1c12c350d..5afaf7f2a3 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -147,6 +147,8 @@ "aiAssistant.serviceError.message": "Unable to connect to n8n's AI service", "aiAssistant.codeUpdated.message.title": "Assistant modified workflow", "aiAssistant.codeUpdated.message.body": "Open the {nodeName} node to see the changes", + "aiAssistant.thinkingSteps.analyzingError": "Analyzing the error...", + "aiAssistant.thinkingSteps.thinking": "Thinking...", "banners.confirmEmail.message.1": "To secure your account and prevent future access issues, please confirm your", "banners.confirmEmail.message.2": "email address.", "banners.confirmEmail.button": "Confirm email", diff --git a/packages/editor-ui/src/stores/__tests__/assistant.store.test.ts b/packages/editor-ui/src/stores/__tests__/assistant.store.test.ts index 279f9e0b01..ffca695066 100644 --- a/packages/editor-ui/src/stores/__tests__/assistant.store.test.ts +++ b/packages/editor-ui/src/stores/__tests__/assistant.store.test.ts @@ -311,7 +311,6 @@ describe('AI Assistant store', () => { }; const assistantStore = useAssistantStore(); await assistantStore.initErrorHelper(context); - expect(assistantStore.chatMessages.length).toBe(2); expect(apiSpy).toHaveBeenCalled(); }); }); diff --git a/packages/editor-ui/src/stores/assistant.store.ts b/packages/editor-ui/src/stores/assistant.store.ts index bad9e8aee2..05299ad6c0 100644 --- a/packages/editor-ui/src/stores/assistant.store.ts +++ b/packages/editor-ui/src/stores/assistant.store.ts @@ -31,7 +31,7 @@ import { useUIStore } from './ui.store'; export const MAX_CHAT_WIDTH = 425; export const MIN_CHAT_WIDTH = 250; -export const DEFAULT_CHAT_WIDTH = 325; +export const DEFAULT_CHAT_WIDTH = 330; export const ENABLED_VIEWS = [...EDITABLE_CANVAS_VIEWS, VIEWS.EXECUTION_PREVIEW]; const READABLE_TYPES = ['code-diff', 'text', 'block']; @@ -63,6 +63,10 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { const currentSessionActiveExecutionId = ref(); const currentSessionWorkflowId = ref(); const lastUnread = ref(); + const nodeExecutionStatus = ref<'not_executed' | 'success' | 'error'>('not_executed'); + // This is used to show a message when the assistant is performing intermediate steps + // We use streaming for assistants that support it, and this for agents + const assistantThinkingMessage = ref(); const isExperimentEnabled = computed( () => getVariant(AI_ASSISTANT_EXPERIMENT.name) === AI_ASSISTANT_EXPERIMENT.variant, @@ -117,6 +121,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { lastUnread.value = undefined; currentSessionActiveExecutionId.value = undefined; suggestions.value = {}; + nodeExecutionStatus.value = 'not_executed'; } // As assistant sidebar opens and closes, use window width to calculate the container width @@ -140,6 +145,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { const messages = [...chatMessages.value].filter( (msg) => !(msg.id === id && msg.role === 'assistant'), ); + assistantThinkingMessage.value = undefined; // TODO: simplify assistantMessages.forEach((msg) => { if (msg.type === 'message') { @@ -190,6 +196,8 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { quickReplies: msg.quickReplies, read, }); + } else if (msg.type === 'intermediate-step') { + assistantThinkingMessage.value = msg.text; } }); chatMessages.value = messages; @@ -226,14 +234,8 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { }); } - function addEmptyAssistantMessage(id: string) { - chatMessages.value.push({ - id, - role: 'assistant', - type: 'text', - content: '', - read: false, - }); + function addLoadingAssistantMessage(message: string) { + assistantThinkingMessage.value = message; } function addUserMessage(content: string, id: string) { @@ -249,6 +251,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { function handleServiceError(e: unknown, id: string) { assert(e instanceof Error); stopStreaming(); + assistantThinkingMessage.value = undefined; addAssistantError(`${locale.baseText('aiAssistant.serviceError.message')}: (${e.message})`, id); } @@ -316,7 +319,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { const availableAuthOptions = getNodeAuthOptions(nodeType); authType = availableAuthOptions.find((option) => option.value === credentialInUse); } - addEmptyAssistantMessage(id); + addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.analyzingError')); openChat(); streaming.value = true; @@ -351,7 +354,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { assert(currentSessionId.value); const id = getRandomId(); - addEmptyAssistantMessage(id); + addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking')); streaming.value = true; chatWithAssistant( rootStore.restApiContext, @@ -369,21 +372,30 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { (e) => handleServiceError(e, id), ); } - async function onNodeExecution(pushEvent: IPushDataNodeExecuteAfter) { if (!chatSessionError.value || pushEvent.nodeName !== chatSessionError.value.node.name) { return; } - if (pushEvent.data.error) { + if (pushEvent.data.error && nodeExecutionStatus.value !== 'error') { await sendEvent('node-execution-errored', pushEvent.data.error); - } else if (pushEvent.data.executionStatus === 'success') { + nodeExecutionStatus.value = 'error'; + telemetry.track('User executed node after assistant suggestion', { + task: 'error', + chat_session_id: currentSessionId.value, + success: false, + }); + } else if ( + pushEvent.data.executionStatus === 'success' && + nodeExecutionStatus.value !== 'success' + ) { await sendEvent('node-execution-succeeded'); + nodeExecutionStatus.value = 'success'; + telemetry.track('User executed node after assistant suggestion', { + task: 'error', + chat_session_id: currentSessionId.value, + success: true, + }); } - telemetry.track('User executed node after assistant suggestion', { - task: 'error', - chat_session_id: currentSessionId.value, - success: pushEvent.data.executionStatus === 'success', - }); } async function sendMessage( @@ -396,10 +408,16 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { const id = getRandomId(); try { addUserMessage(chatMessage.text, id); - addEmptyAssistantMessage(id); + addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking')); streaming.value = true; assert(currentSessionId.value); + if ( + chatMessage.quickReplyType === 'new-suggestion' && + nodeExecutionStatus.value !== 'not_executed' + ) { + nodeExecutionStatus.value = 'not_executed'; + } chatWithAssistant( rootStore.restApiContext, { @@ -415,6 +433,12 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { () => onDoneStreaming(id), (e) => handleServiceError(e, id), ); + telemetry.track('User sent message in Assistant', { + 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) { // in case of assert handleServiceError(e, id); @@ -566,5 +590,6 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { resetAssistantChat, chatWindowOpen, addAssistantMessages, + assistantThinkingMessage, }; }); diff --git a/packages/editor-ui/src/types/assistant.types.ts b/packages/editor-ui/src/types/assistant.types.ts index 86f3576afe..6cbfa61ed0 100644 --- a/packages/editor-ui/src/types/assistant.types.ts +++ b/packages/editor-ui/src/types/assistant.types.ts @@ -76,6 +76,7 @@ export namespace ChatRequest { role: 'assistant'; type: 'message'; text: string; + step?: 'n8n_documentation' | 'n8n_forum'; } interface AssistantSummaryMessage { @@ -98,8 +99,21 @@ export namespace ChatRequest { text: string; } + interface AgentThinkingStep { + role: 'assistant'; + type: 'intermediate-step'; + text: string; + step: string; + } + export type MessageResponse = - | ((AssistantChatMessage | CodeDiffMessage | AssistantSummaryMessage | AgentChatMessage) & { + | (( + | AssistantChatMessage + | CodeDiffMessage + | AssistantSummaryMessage + | AgentChatMessage + | AgentThinkingStep + ) & { quickReplies?: QuickReplyOption[]; }) | EndSessionMessage;