`;
+exports[`AskAssistantChat > renders error message correctly with retry button 1`] = `
+
Promise;
}
export interface AgentSuggestionMessage {
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 760176321e..a4685e9e8d 100644
--- a/packages/editor-ui/src/stores/__tests__/assistant.store.test.ts
+++ b/packages/editor-ui/src/stores/__tests__/assistant.store.test.ts
@@ -18,6 +18,7 @@ import { reactive } from 'vue';
import * as chatAPI from '@/api/ai';
import * as telemetryModule from '@/composables/useTelemetry';
import type { Telemetry } from '@/plugins/telemetry';
+import type { ChatUI } from 'n8n-design-system/types/assistant';
let settingsStore: ReturnType;
let posthogStore: ReturnType;
@@ -417,4 +418,74 @@ describe('AI Assistant store', () => {
workflow_id: '__EMPTY__',
});
});
+
+ it('should call the function again if retry is called after handleServiceError', async () => {
+ const mockFn = vi.fn();
+
+ const assistantStore = useAssistantStore();
+
+ assistantStore.handleServiceError(new Error('test error'), '125', mockFn);
+ expect(assistantStore.chatMessages.length).toBe(1);
+ const message = assistantStore.chatMessages[0];
+ expect(message.type).toBe('error');
+
+ const errorMessage = message as ChatUI.ErrorMessage;
+ expect(errorMessage.retry).toBeDefined();
+
+ // This simulates the button click from the UI
+ await errorMessage.retry?.();
+
+ expect(mockFn).toHaveBeenCalled();
+ });
+
+ it('should properly clear messages on retry in a chat session', async () => {
+ const assistantStore = useAssistantStore();
+ const mockSessionId = 'mockSessionId';
+
+ const message: ChatRequest.MessageResponse = {
+ type: 'message',
+ role: 'assistant',
+ text: 'Hello!',
+ quickReplies: [
+ { text: 'Yes', type: 'text' },
+ { text: 'No', type: 'text' },
+ ],
+ };
+
+ apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
+ onMessage({ messages: [message], sessionId: mockSessionId });
+ onDone();
+ });
+ apiSpy.mockImplementationOnce((_ctx, _payload, _onMessage, _onDone, onError) =>
+ onError(new Error('test error')),
+ );
+ apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
+ onMessage({ messages: [message], sessionId: mockSessionId });
+ onDone();
+ });
+
+ await assistantStore.initSupportChat('hello');
+
+ expect(assistantStore.chatMessages.length).toBe(2);
+ expect(assistantStore.chatMessages[0].type).toBe('text');
+ expect(assistantStore.chatMessages[1].type).toBe('text');
+
+ await assistantStore.sendMessage({ text: 'test' });
+
+ expect(assistantStore.chatMessages.length).toBe(4);
+ expect(assistantStore.chatMessages[0].type).toBe('text');
+ expect(assistantStore.chatMessages[1].type).toBe('text');
+ expect(assistantStore.chatMessages[2].type).toBe('text');
+ expect(assistantStore.chatMessages[3].type).toBe('error');
+
+ expect(assistantStore.chatMessages[3]).toHaveProperty('retry');
+ // This simulates the functionality triggered from the consumer (e.g. UI Button)
+ await (assistantStore.chatMessages[3] as ChatUI.ErrorMessage).retry?.();
+
+ expect(assistantStore.chatMessages.length).toBe(4);
+ expect(assistantStore.chatMessages[0].type).toBe('text');
+ expect(assistantStore.chatMessages[1].type).toBe('text');
+ expect(assistantStore.chatMessages[2].type).toBe('text');
+ expect(assistantStore.chatMessages[3].type).toBe('text');
+ });
});
diff --git a/packages/editor-ui/src/stores/assistant.store.ts b/packages/editor-ui/src/stores/assistant.store.ts
index c3f8c77e00..02178c0784 100644
--- a/packages/editor-ui/src/stores/assistant.store.ts
+++ b/packages/editor-ui/src/stores/assistant.store.ts
@@ -251,13 +251,14 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
streaming.value = false;
}
- function addAssistantError(content: string, id: string) {
+ function addAssistantError(content: string, id: string, retry?: () => Promise) {
chatMessages.value.push({
id,
role: 'assistant',
type: 'error',
content,
read: true,
+ retry,
});
}
@@ -275,11 +276,15 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
});
}
- function handleServiceError(e: unknown, id: string) {
+ function handleServiceError(e: unknown, id: string, retry?: () => Promise) {
assert(e instanceof Error);
stopStreaming();
assistantThinkingMessage.value = undefined;
- addAssistantError(`${locale.baseText('aiAssistant.serviceError.message')}: (${e.message})`, id);
+ addAssistantError(
+ `${locale.baseText('aiAssistant.serviceError.message')}: (${e.message})`,
+ id,
+ retry,
+ );
}
function onEachStreamingMessage(response: ChatRequest.ResponsePayload, id: string) {
@@ -447,7 +452,8 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
},
(msg) => onEachStreamingMessage(msg, id),
() => onDoneStreaming(id),
- (e) => handleServiceError(e, id),
+ (e) =>
+ handleServiceError(e, id, async () => await initSupportChat(userMessage, credentialType)),
);
}
@@ -498,7 +504,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
},
(msg) => onEachStreamingMessage(msg, id),
() => onDoneStreaming(id),
- (e) => handleServiceError(e, id),
+ (e) => handleServiceError(e, id, async () => await initErrorHelper(context)),
);
}
@@ -527,7 +533,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
},
(msg) => onEachStreamingMessage(msg, id),
() => onDoneStreaming(id),
- (e) => handleServiceError(e, id),
+ (e) => handleServiceError(e, id, async () => await sendEvent(eventName, error)),
);
}
async function onNodeExecution(pushEvent: PushPayload<'nodeExecuteAfter'>) {
@@ -564,6 +570,12 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
}
const id = getRandomId();
+
+ const retry = async () => {
+ chatMessages.value = chatMessages.value.filter((msg) => msg.id !== id);
+ await sendMessage(chatMessage);
+ };
+
try {
addUserMessage(chatMessage.text, id);
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking'));
@@ -594,12 +606,12 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
},
(msg) => onEachStreamingMessage(msg, id),
() => onDoneStreaming(id),
- (e) => handleServiceError(e, id),
+ (e) => handleServiceError(e, id, retry),
);
trackUserMessage(chatMessage.text, !!chatMessage.quickReplyType);
} catch (e: unknown) {
// in case of assert
- handleServiceError(e, id);
+ handleServiceError(e, id, retry);
}
}
@@ -824,5 +836,6 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
chatSessionTask,
initCredHelp,
isCredTypeActive,
+ handleServiceError,
};
});