mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(editor): Improve feedback buttons behavior (#18247)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,277 @@
|
|||||||
|
import { render, fireEvent, waitFor } from '@testing-library/vue';
|
||||||
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
|
import MessageRating from './MessageRating.vue';
|
||||||
|
|
||||||
|
const stubs = ['n8n-button', 'n8n-icon-button', 'n8n-input'];
|
||||||
|
|
||||||
|
// Mock i18n to return keys instead of translated text
|
||||||
|
vi.mock('@n8n/design-system/composables/useI18n', () => ({
|
||||||
|
useI18n: () => ({
|
||||||
|
t: (key: string) => key,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia());
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MessageRating', () => {
|
||||||
|
it('should render correctly with default props', () => {
|
||||||
|
const wrapper = render(MessageRating, {
|
||||||
|
global: { stubs },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper.container.querySelector('[data-test-id="message-thumbs-up-button"]'),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
wrapper.container.querySelector('[data-test-id="message-thumbs-down-button"]'),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(wrapper.html()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('rating interactions', () => {
|
||||||
|
it('should emit feedback when thumbs up is clicked', async () => {
|
||||||
|
const wrapper = render(MessageRating, {
|
||||||
|
global: { stubs },
|
||||||
|
});
|
||||||
|
|
||||||
|
const upButton = wrapper.container.querySelector('[data-test-id="message-thumbs-up-button"]');
|
||||||
|
await fireEvent.click(upButton!);
|
||||||
|
|
||||||
|
expect(wrapper.emitted()).toHaveProperty('feedback');
|
||||||
|
expect(wrapper.emitted().feedback[0]).toEqual([{ rating: 'up' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit feedback when thumbs down is clicked', async () => {
|
||||||
|
const wrapper = render(MessageRating, {
|
||||||
|
global: { stubs },
|
||||||
|
});
|
||||||
|
|
||||||
|
const downButton = wrapper.container.querySelector(
|
||||||
|
'[data-test-id="message-thumbs-down-button"]',
|
||||||
|
);
|
||||||
|
await fireEvent.click(downButton!);
|
||||||
|
|
||||||
|
expect(wrapper.emitted()).toHaveProperty('feedback');
|
||||||
|
expect(wrapper.emitted().feedback[0]).toEqual([{ rating: 'down' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide rating buttons and show success after thumbs up', async () => {
|
||||||
|
const wrapper = render(MessageRating, {
|
||||||
|
global: { stubs },
|
||||||
|
});
|
||||||
|
|
||||||
|
const upButton = wrapper.container.querySelector('[data-test-id="message-thumbs-up-button"]');
|
||||||
|
await fireEvent.click(upButton!);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper.container.querySelector('[data-test-id="message-thumbs-up-button"]'),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
wrapper.container.querySelector('[data-test-id="message-thumbs-down-button"]'),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(wrapper.getByText('assistantChat.builder.success')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide rating buttons and show feedback form after thumbs down when showFeedback is true', async () => {
|
||||||
|
const wrapper = render(MessageRating, {
|
||||||
|
props: { showFeedback: true },
|
||||||
|
global: { stubs },
|
||||||
|
});
|
||||||
|
|
||||||
|
const downButton = wrapper.container.querySelector(
|
||||||
|
'[data-test-id="message-thumbs-down-button"]',
|
||||||
|
);
|
||||||
|
await fireEvent.click(downButton!);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper.container.querySelector('[data-test-id="message-thumbs-up-button"]'),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
wrapper.container.querySelector('[data-test-id="message-thumbs-down-button"]'),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
wrapper.container.querySelector('[data-test-id="message-feedback-input"]'),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
wrapper.container.querySelector('[data-test-id="message-submit-feedback-button"]'),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show success immediately after thumbs down when showFeedback is false', async () => {
|
||||||
|
const wrapper = render(MessageRating, {
|
||||||
|
props: { showFeedback: false },
|
||||||
|
global: { stubs },
|
||||||
|
});
|
||||||
|
|
||||||
|
const downButton = wrapper.container.querySelector(
|
||||||
|
'[data-test-id="message-thumbs-down-button"]',
|
||||||
|
);
|
||||||
|
await fireEvent.click(downButton!);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper.container.querySelector('[data-test-id="message-feedback-input"]'),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(wrapper.getByText('assistantChat.builder.success')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('feedback form interactions', () => {
|
||||||
|
it('should submit feedback and show success', async () => {
|
||||||
|
const wrapper = render(MessageRating, {
|
||||||
|
props: { showFeedback: true },
|
||||||
|
global: { stubs: ['n8n-button', 'n8n-icon-button'] }, // Don't stub n8n-input
|
||||||
|
});
|
||||||
|
|
||||||
|
const downButton = wrapper.container.querySelector(
|
||||||
|
'[data-test-id="message-thumbs-down-button"]',
|
||||||
|
);
|
||||||
|
await fireEvent.click(downButton!);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// Find the actual textarea element within the N8nInput component
|
||||||
|
const textarea = wrapper.container.querySelector(
|
||||||
|
'textarea[data-test-id="message-feedback-input"]',
|
||||||
|
);
|
||||||
|
await fireEvent.update(textarea!, 'This is my feedback about the response');
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const submitButton = wrapper.container.querySelector(
|
||||||
|
'[data-test-id="message-submit-feedback-button"]',
|
||||||
|
);
|
||||||
|
await fireEvent.click(submitButton!);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.emitted().feedback).toHaveLength(2);
|
||||||
|
expect(wrapper.emitted().feedback[1]).toEqual([
|
||||||
|
{ feedback: 'This is my feedback about the response' },
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
wrapper.container.querySelector('[data-test-id="message-feedback-input"]'),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(wrapper.getByText('assistantChat.builder.success')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cancel feedback and return to rating buttons', async () => {
|
||||||
|
const wrapper = render(MessageRating, {
|
||||||
|
props: { showFeedback: true },
|
||||||
|
global: { stubs },
|
||||||
|
});
|
||||||
|
|
||||||
|
const downButton = wrapper.container.querySelector(
|
||||||
|
'[data-test-id="message-thumbs-down-button"]',
|
||||||
|
);
|
||||||
|
await fireEvent.click(downButton!);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const cancelButton = wrapper.container.querySelector(
|
||||||
|
'n8n-button-stub[label="generic.cancel"]',
|
||||||
|
);
|
||||||
|
await fireEvent.click(cancelButton!);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper.container.querySelector('[data-test-id="message-feedback-input"]'),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
wrapper.container.querySelector('[data-test-id="message-thumbs-up-button"]'),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
wrapper.container.querySelector('[data-test-id="message-thumbs-down-button"]'),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should focus feedback input after thumbs down in regular mode', async () => {
|
||||||
|
const wrapper = render(MessageRating, {
|
||||||
|
props: { showFeedback: true, style: 'regular' },
|
||||||
|
global: { stubs },
|
||||||
|
});
|
||||||
|
|
||||||
|
const downButton = wrapper.container.querySelector(
|
||||||
|
'[data-test-id="message-thumbs-down-button"]',
|
||||||
|
);
|
||||||
|
await fireEvent.click(downButton!);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const feedbackInput = wrapper.container.querySelector(
|
||||||
|
'[data-test-id="message-feedback-input"]',
|
||||||
|
);
|
||||||
|
expect(feedbackInput).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear feedback text when cancelling', async () => {
|
||||||
|
const wrapper = render(MessageRating, {
|
||||||
|
props: { showFeedback: true },
|
||||||
|
global: { stubs },
|
||||||
|
});
|
||||||
|
|
||||||
|
const downButton = wrapper.container.querySelector(
|
||||||
|
'[data-test-id="message-thumbs-down-button"]',
|
||||||
|
);
|
||||||
|
await fireEvent.click(downButton!);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const cancelButton = wrapper.container.querySelector(
|
||||||
|
'n8n-button-stub[label="generic.cancel"]',
|
||||||
|
);
|
||||||
|
await fireEvent.click(cancelButton!);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const downButtonAgain = wrapper.container.querySelector(
|
||||||
|
'[data-test-id="message-thumbs-down-button"]',
|
||||||
|
);
|
||||||
|
await fireEvent.click(downButtonAgain!);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const feedbackInputAfter = wrapper.container.querySelector(
|
||||||
|
'[data-test-id="message-feedback-input"]',
|
||||||
|
);
|
||||||
|
expect(feedbackInputAfter?.getAttribute('modelvalue')).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('textarea rows based on style', () => {
|
||||||
|
it('should have 5 rows for regular style', async () => {
|
||||||
|
const wrapper = render(MessageRating, {
|
||||||
|
props: { showFeedback: true, style: 'regular' },
|
||||||
|
global: { stubs },
|
||||||
|
});
|
||||||
|
|
||||||
|
const downButton = wrapper.container.querySelector(
|
||||||
|
'[data-test-id="message-thumbs-down-button"]',
|
||||||
|
);
|
||||||
|
await fireEvent.click(downButton!);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const feedbackInput = wrapper.container.querySelector(
|
||||||
|
'[data-test-id="message-feedback-input"]',
|
||||||
|
);
|
||||||
|
expect(feedbackInput?.getAttribute('rows')).toBe('5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have 3 rows for minimal style', async () => {
|
||||||
|
const wrapper = render(MessageRating, {
|
||||||
|
props: { showFeedback: true, style: 'minimal' },
|
||||||
|
global: { stubs },
|
||||||
|
});
|
||||||
|
|
||||||
|
const downButton = wrapper.container.querySelector(
|
||||||
|
'[data-test-id="message-thumbs-down-button"]',
|
||||||
|
);
|
||||||
|
await fireEvent.click(downButton!);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const feedbackInput = wrapper.container.querySelector(
|
||||||
|
'[data-test-id="message-feedback-input"]',
|
||||||
|
);
|
||||||
|
expect(feedbackInput?.getAttribute('rows')).toBe('3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -25,6 +25,7 @@ const emit = defineEmits<{
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const showRatingButtons = ref(true);
|
const showRatingButtons = ref(true);
|
||||||
const showFeedbackArea = ref(false);
|
const showFeedbackArea = ref(false);
|
||||||
|
const feedbackInput = ref<HTMLInputElement | null>(null);
|
||||||
const showSuccess = ref(false);
|
const showSuccess = ref(false);
|
||||||
const selectedRating = ref<'up' | 'down' | null>(null);
|
const selectedRating = ref<'up' | 'down' | null>(null);
|
||||||
const feedback = ref('');
|
const feedback = ref('');
|
||||||
@@ -34,8 +35,13 @@ function onRateButton(rating: 'up' | 'down') {
|
|||||||
showRatingButtons.value = false;
|
showRatingButtons.value = false;
|
||||||
|
|
||||||
emit('feedback', { rating });
|
emit('feedback', { rating });
|
||||||
if (props.showFeedback) {
|
if (props.showFeedback && rating === 'down') {
|
||||||
showFeedbackArea.value = true;
|
showFeedbackArea.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (feedbackInput.value) {
|
||||||
|
feedbackInput.value.focus();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
} else {
|
} else {
|
||||||
showSuccess.value = true;
|
showSuccess.value = true;
|
||||||
}
|
}
|
||||||
@@ -100,6 +106,7 @@ function onCancelFeedback() {
|
|||||||
|
|
||||||
<div v-if="showFeedbackArea" :class="$style.feedbackContainer">
|
<div v-if="showFeedbackArea" :class="$style.feedbackContainer">
|
||||||
<N8nInput
|
<N8nInput
|
||||||
|
ref="feedbackInput"
|
||||||
v-model="feedback"
|
v-model="feedback"
|
||||||
:class="$style.feedbackInput"
|
:class="$style.feedbackInput"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`MessageRating > should render correctly with default props 1`] = `
|
||||||
|
"<div class="rating regular">
|
||||||
|
<div class="buttons">
|
||||||
|
<n8n-button-stub icon="thumbs-up" block="false" element="button" label="assistantChat.builder.thumbsUp" square="false" active="false" disabled="false" loading="false" outline="false" size="small" text="false" type="secondary" data-test-id="message-thumbs-up-button"></n8n-button-stub>
|
||||||
|
<n8n-button-stub icon="thumbs-down" block="false" element="button" label="assistantChat.builder.thumbsDown" square="false" active="false" disabled="false" loading="false" outline="false" size="small" text="false" type="secondary" data-test-id="message-thumbs-down-button"></n8n-button-stub>
|
||||||
|
</div>
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
|
</div>"
|
||||||
|
`;
|
||||||
@@ -40,7 +40,7 @@ export default {
|
|||||||
'assistantChat.builder.configuredNodes': 'Configured nodes',
|
'assistantChat.builder.configuredNodes': 'Configured nodes',
|
||||||
'assistantChat.builder.thumbsUp': 'Helpful',
|
'assistantChat.builder.thumbsUp': 'Helpful',
|
||||||
'assistantChat.builder.thumbsDown': 'Not helpful',
|
'assistantChat.builder.thumbsDown': 'Not helpful',
|
||||||
'assistantChat.builder.feedbackPlaceholder': 'Tell us about your experience',
|
'assistantChat.builder.feedbackPlaceholder': 'What went wrong?',
|
||||||
'assistantChat.builder.success': 'Thank you for your feedback!',
|
'assistantChat.builder.success': 'Thank you for your feedback!',
|
||||||
'assistantChat.builder.submit': 'Submit feedback',
|
'assistantChat.builder.submit': 'Submit feedback',
|
||||||
'assistantChat.builder.workflowGenerated1': 'Your workflow was created successfully!',
|
'assistantChat.builder.workflowGenerated1': 'Your workflow was created successfully!',
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { useBuilderMessages } from '@/composables/useBuilderMessages';
|
import { useBuilderMessages } from '@/composables/useBuilderMessages';
|
||||||
import type { ChatUI } from '@n8n/design-system/types/assistant';
|
import type { ChatUI } from '@n8n/design-system/types/assistant';
|
||||||
import type { ChatRequest } from '@/types/assistant.types';
|
import type { ChatRequest } from '@/types/assistant.types';
|
||||||
|
|
||||||
|
// Mock useI18n to return the keys instead of translations
|
||||||
|
vi.mock('@n8n/i18n', () => ({
|
||||||
|
useI18n: () => ({
|
||||||
|
baseText: (key: string) => key,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('useBuilderMessages', () => {
|
describe('useBuilderMessages', () => {
|
||||||
let builderMessages: ReturnType<typeof useBuilderMessages>;
|
let builderMessages: ReturnType<typeof useBuilderMessages>;
|
||||||
|
|
||||||
@@ -282,7 +289,7 @@ describe('useBuilderMessages', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(result.messages).toHaveLength(1);
|
expect(result.messages).toHaveLength(1);
|
||||||
expect(result.thinkingMessage).toBe('Running tools...');
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.runningTools');
|
||||||
expect(result.shouldClearThinking).toBe(false);
|
expect(result.shouldClearThinking).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -309,7 +316,7 @@ describe('useBuilderMessages', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(result.messages).toHaveLength(1);
|
expect(result.messages).toHaveLength(1);
|
||||||
expect(result.thinkingMessage).toBe('Processing results...');
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.processingResults');
|
||||||
expect(result.shouldClearThinking).toBe(false);
|
expect(result.shouldClearThinking).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -382,8 +389,8 @@ describe('useBuilderMessages', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(result.messages).toHaveLength(2);
|
expect(result.messages).toHaveLength(2);
|
||||||
// Should show "Running tools..." for the new running tool, not "Processing results..."
|
// Should show "aiAssistant.thinkingSteps.runningTools" for the new running tool, not "aiAssistant.thinkingSteps.processingResults"
|
||||||
expect(result.thinkingMessage).toBe('Running tools...');
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.runningTools');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show processing message when second tool completes', () => {
|
it('should show processing message when second tool completes', () => {
|
||||||
@@ -419,7 +426,7 @@ describe('useBuilderMessages', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(result.messages).toHaveLength(2);
|
expect(result.messages).toHaveLength(2);
|
||||||
expect(result.thinkingMessage).toBe('Processing results...');
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.processingResults');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should keep showing running tools message when parallel tools complete one by one', () => {
|
it('should keep showing running tools message when parallel tools complete one by one', () => {
|
||||||
@@ -466,8 +473,8 @@ describe('useBuilderMessages', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(result.messages).toHaveLength(2);
|
expect(result.messages).toHaveLength(2);
|
||||||
// Should still show "Running tools..." because call-456 is still running
|
// Should still show "aiAssistant.thinkingSteps.runningTools" because call-456 is still running
|
||||||
expect(result.thinkingMessage).toBe('Running tools...');
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.runningTools');
|
||||||
|
|
||||||
// Verify first tool is now completed
|
// Verify first tool is now completed
|
||||||
const firstTool = result.messages.find(
|
const firstTool = result.messages.find(
|
||||||
@@ -526,8 +533,8 @@ describe('useBuilderMessages', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(result.messages).toHaveLength(2);
|
expect(result.messages).toHaveLength(2);
|
||||||
// Should now show "Processing results..." because all tools are completed
|
// Should now show "aiAssistant.thinkingSteps.processingResults" because all tools are completed
|
||||||
expect(result.thinkingMessage).toBe('Processing results...');
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.processingResults');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should keep processing message when workflow-updated arrives after tools complete', () => {
|
it('should keep processing message when workflow-updated arrives after tools complete', () => {
|
||||||
@@ -561,8 +568,8 @@ describe('useBuilderMessages', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(result.messages).toHaveLength(2);
|
expect(result.messages).toHaveLength(2);
|
||||||
// Should still show "Processing results..." because workflow-updated is not a text response
|
// Should still show "aiAssistant.thinkingSteps.processingResults" because workflow-updated is not a text response
|
||||||
expect(result.thinkingMessage).toBe('Processing results...');
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.processingResults');
|
||||||
// Should NOT clear thinking for workflow updates
|
// Should NOT clear thinking for workflow updates
|
||||||
expect(result.shouldClearThinking).toBe(false);
|
expect(result.shouldClearThinking).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -1256,7 +1263,7 @@ describe('useBuilderMessages', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let result = builderMessages.processAssistantMessages(currentMessages, batch1, 'batch-1');
|
let result = builderMessages.processAssistantMessages(currentMessages, batch1, 'batch-1');
|
||||||
expect(result.thinkingMessage).toBe('Running tools...');
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.runningTools');
|
||||||
currentMessages = result.messages;
|
currentMessages = result.messages;
|
||||||
|
|
||||||
// Second batch: tool completes
|
// Second batch: tool completes
|
||||||
@@ -1272,7 +1279,7 @@ describe('useBuilderMessages', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
result = builderMessages.processAssistantMessages(currentMessages, batch2, 'batch-2');
|
result = builderMessages.processAssistantMessages(currentMessages, batch2, 'batch-2');
|
||||||
expect(result.thinkingMessage).toBe('Processing results...');
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.processingResults');
|
||||||
currentMessages = result.messages;
|
currentMessages = result.messages;
|
||||||
|
|
||||||
// Third batch: workflow updated
|
// Third batch: workflow updated
|
||||||
@@ -1285,7 +1292,7 @@ describe('useBuilderMessages', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
result = builderMessages.processAssistantMessages(currentMessages, batch3, 'batch-3');
|
result = builderMessages.processAssistantMessages(currentMessages, batch3, 'batch-3');
|
||||||
expect(result.thinkingMessage).toBe('Processing results...');
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.processingResults');
|
||||||
currentMessages = result.messages;
|
currentMessages = result.messages;
|
||||||
|
|
||||||
// Fourth batch: final text response
|
// Fourth batch: final text response
|
||||||
@@ -1308,4 +1315,460 @@ describe('useBuilderMessages', () => {
|
|||||||
expect(result.messages.find((m) => m.type === 'text')).toBeTruthy();
|
expect(result.messages.find((m) => m.type === 'text')).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('clearRatingLogic', () => {
|
||||||
|
it('should remove showRating and ratingStyle properties from text messages', () => {
|
||||||
|
const messages: ChatUI.AssistantMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Hello there!',
|
||||||
|
showRating: true,
|
||||||
|
ratingStyle: 'regular',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-2',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'How can I help?',
|
||||||
|
showRating: false,
|
||||||
|
ratingStyle: 'minimal',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = builderMessages.clearRatingLogic(messages);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0]).toMatchObject({
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Hello there!',
|
||||||
|
read: false,
|
||||||
|
});
|
||||||
|
expect(result[0]).not.toHaveProperty('showRating');
|
||||||
|
expect(result[0]).not.toHaveProperty('ratingStyle');
|
||||||
|
|
||||||
|
expect(result[1]).toMatchObject({
|
||||||
|
id: 'msg-2',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'How can I help?',
|
||||||
|
read: false,
|
||||||
|
});
|
||||||
|
expect(result[1]).not.toHaveProperty('showRating');
|
||||||
|
expect(result[1]).not.toHaveProperty('ratingStyle');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should leave non-text messages unchanged', () => {
|
||||||
|
const messages: ChatUI.AssistantMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'tool-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'tool',
|
||||||
|
toolName: 'add_nodes',
|
||||||
|
toolCallId: 'call-1',
|
||||||
|
status: 'completed',
|
||||||
|
updates: [],
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'workflow-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'workflow-updated',
|
||||||
|
codeSnippet: '{}',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = builderMessages.clearRatingLogic(messages);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0]).toEqual(messages[0]);
|
||||||
|
expect(result[1]).toEqual(messages[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle text messages without rating properties', () => {
|
||||||
|
const messages: ChatUI.AssistantMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'No rating here',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = builderMessages.clearRatingLogic(messages);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]).toEqual(messages[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty message array', () => {
|
||||||
|
const result = builderMessages.clearRatingLogic([]);
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed message types with some having rating properties', () => {
|
||||||
|
const messages: ChatUI.AssistantMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'With rating',
|
||||||
|
showRating: true,
|
||||||
|
ratingStyle: 'regular',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tool-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'tool',
|
||||||
|
toolName: 'test_tool',
|
||||||
|
toolCallId: 'call-1',
|
||||||
|
status: 'completed',
|
||||||
|
updates: [],
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-2',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Without rating',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = builderMessages.clearRatingLogic(messages);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result[0]).not.toHaveProperty('showRating');
|
||||||
|
expect(result[0]).not.toHaveProperty('ratingStyle');
|
||||||
|
expect(result[1]).toEqual(messages[1]); // tool message unchanged
|
||||||
|
expect(result[2]).toEqual(messages[2]); // text without rating unchanged
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('applyRatingLogic', () => {
|
||||||
|
it('should apply rating to the last assistant text message after workflow-updated when no tools are running', () => {
|
||||||
|
const messages: ChatUI.AssistantMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Starting process...',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'workflow-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'workflow-updated',
|
||||||
|
codeSnippet: '{"nodes": [], "connections": {}}',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-2',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Process completed!',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = builderMessages.applyRatingLogic(messages);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result[0].showRating).toBeUndefined();
|
||||||
|
expect(result[1].showRating).toBeUndefined();
|
||||||
|
expect(result[1].type).toBe('workflow-updated');
|
||||||
|
expect(result[2]).toMatchObject({
|
||||||
|
id: 'msg-2',
|
||||||
|
content: 'Process completed!',
|
||||||
|
showRating: true,
|
||||||
|
ratingStyle: 'regular',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not apply rating when tools are still running', () => {
|
||||||
|
const messages: ChatUI.AssistantMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'workflow-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'workflow-updated',
|
||||||
|
codeSnippet: '{}',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tool-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'tool',
|
||||||
|
toolName: 'add_nodes',
|
||||||
|
toolCallId: 'call-1',
|
||||||
|
status: 'running',
|
||||||
|
updates: [],
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Working on it...',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = builderMessages.applyRatingLogic(messages);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
result.forEach((message) => {
|
||||||
|
expect(message.showRating).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not apply rating when still thinking (tools completed but no text response)', () => {
|
||||||
|
const messages: ChatUI.AssistantMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'workflow-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'workflow-updated',
|
||||||
|
codeSnippet: '{}',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tool-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'tool',
|
||||||
|
toolName: 'add_nodes',
|
||||||
|
toolCallId: 'call-1',
|
||||||
|
status: 'completed',
|
||||||
|
updates: [],
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = builderMessages.applyRatingLogic(messages);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
result.forEach((message) => {
|
||||||
|
expect(message.showRating).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not apply rating when no workflow-updated message exists', () => {
|
||||||
|
const messages: ChatUI.AssistantMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Hello there!',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-2',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'How can I help?',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = builderMessages.applyRatingLogic(messages);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0].showRating).toBeUndefined();
|
||||||
|
expect(result[1].showRating).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove existing ratings when tools are running', () => {
|
||||||
|
const messages: ChatUI.AssistantMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Previous message',
|
||||||
|
showRating: true,
|
||||||
|
ratingStyle: 'regular',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tool-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'tool',
|
||||||
|
toolName: 'add_nodes',
|
||||||
|
toolCallId: 'call-1',
|
||||||
|
status: 'running',
|
||||||
|
updates: [],
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = builderMessages.applyRatingLogic(messages);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0]).not.toHaveProperty('showRating');
|
||||||
|
expect(result[0]).not.toHaveProperty('ratingStyle');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove ratings from non-target messages when applying rating to target message', () => {
|
||||||
|
const messages: ChatUI.AssistantMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Earlier message',
|
||||||
|
showRating: true,
|
||||||
|
ratingStyle: 'minimal',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'workflow-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'workflow-updated',
|
||||||
|
codeSnippet: '{}',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-2',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Target message',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = builderMessages.applyRatingLogic(messages);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result[0]).not.toHaveProperty('showRating');
|
||||||
|
expect(result[0]).not.toHaveProperty('ratingStyle');
|
||||||
|
expect(result[2]).toMatchObject({
|
||||||
|
showRating: true,
|
||||||
|
ratingStyle: 'regular',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple workflow-updated messages and apply rating after the last one', () => {
|
||||||
|
const messages: ChatUI.AssistantMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'workflow-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'workflow-updated',
|
||||||
|
codeSnippet: '{"nodes": []}',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'First update done',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'workflow-2',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'workflow-updated',
|
||||||
|
codeSnippet: '{"nodes": [{"name": "HTTP"}]}',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-2',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Final update complete',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = builderMessages.applyRatingLogic(messages);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(4);
|
||||||
|
expect(result[1].showRating).toBeUndefined(); // First text message
|
||||||
|
expect(result[3]).toMatchObject({
|
||||||
|
content: 'Final update complete',
|
||||||
|
showRating: true,
|
||||||
|
ratingStyle: 'regular',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle user messages mixed with assistant messages', () => {
|
||||||
|
const messages: ChatUI.AssistantMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'user-1',
|
||||||
|
role: 'user',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Create a workflow',
|
||||||
|
read: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'workflow-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'workflow-updated',
|
||||||
|
codeSnippet: '{}',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Workflow created!',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = builderMessages.applyRatingLogic(messages);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result[0].showRating).toBeUndefined(); // User message
|
||||||
|
expect(result[2]).toMatchObject({
|
||||||
|
content: 'Workflow created!',
|
||||||
|
showRating: true,
|
||||||
|
ratingStyle: 'regular',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty message array', () => {
|
||||||
|
const result = builderMessages.applyRatingLogic([]);
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply rating only to assistant text messages, not user text messages', () => {
|
||||||
|
const messages: ChatUI.AssistantMessage[] = [
|
||||||
|
{
|
||||||
|
id: 'workflow-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'workflow-updated',
|
||||||
|
codeSnippet: '{}',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'user-1',
|
||||||
|
role: 'user',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Thanks!',
|
||||||
|
read: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'You are welcome!',
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = builderMessages.applyRatingLogic(messages);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result[1].showRating).toBeUndefined(); // User message should not have rating
|
||||||
|
expect(result[2]).toMatchObject({
|
||||||
|
content: 'You are welcome!',
|
||||||
|
showRating: true,
|
||||||
|
ratingStyle: 'regular',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,32 +13,33 @@ export function useBuilderMessages() {
|
|||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply rating logic to messages - only show rating on the last AI text message after workflow-updated
|
* Clear rating from all messages
|
||||||
* when no tools are running
|
|
||||||
*/
|
*/
|
||||||
function applyRatingLogic(messages: ChatUI.AssistantMessage[]): ChatUI.AssistantMessage[] {
|
function clearRatingLogic(messages: ChatUI.AssistantMessage[]): ChatUI.AssistantMessage[] {
|
||||||
// Check if any tools are still running
|
|
||||||
const hasRunningTools = messages.some(
|
|
||||||
(m) => m.type === 'tool' && (m as ChatUI.ToolMessage).status === 'running',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Don't apply rating if tools are still running
|
|
||||||
if (hasRunningTools) {
|
|
||||||
// Remove any existing ratings
|
|
||||||
return messages.map((message) => {
|
return messages.map((message) => {
|
||||||
if (message.type === 'text' && 'showRating' in message) {
|
if (message.type === 'text' && 'showRating' in message) {
|
||||||
// Pick all properties except showRating and ratingStyle
|
// Pick all properties except showRating and ratingStyle
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { showRating, ratingStyle, ...cleanMessage } = message as ChatUI.TextMessage & {
|
const { showRating, ratingStyle, ...cleanMessage } = message;
|
||||||
showRating?: boolean;
|
|
||||||
ratingStyle?: string;
|
|
||||||
};
|
|
||||||
return cleanMessage;
|
return cleanMessage;
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply rating logic to messages - only show rating on the last AI text message after workflow-updated
|
||||||
|
* when no tools are running
|
||||||
|
*/
|
||||||
|
function applyRatingLogic(messages: ChatUI.AssistantMessage[]): ChatUI.AssistantMessage[] {
|
||||||
|
const { hasAnyRunningTools, isStillThinking } = getThinkingState(messages);
|
||||||
|
|
||||||
|
// Don't apply rating if tools are still running
|
||||||
|
if (hasAnyRunningTools || isStillThinking) {
|
||||||
|
// Remove any existing ratings
|
||||||
|
return clearRatingLogic(messages);
|
||||||
|
}
|
||||||
|
|
||||||
// Find the index of the last workflow-updated message
|
// Find the index of the last workflow-updated message
|
||||||
let lastWorkflowUpdateIndex = -1;
|
let lastWorkflowUpdateIndex = -1;
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
@@ -82,10 +83,7 @@ export function useBuilderMessages() {
|
|||||||
// Remove any existing rating from other messages
|
// Remove any existing rating from other messages
|
||||||
if (message.type === 'text' && 'showRating' in message) {
|
if (message.type === 'text' && 'showRating' in message) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { showRating, ratingStyle, ...cleanMessage } = message as ChatUI.TextMessage & {
|
const { showRating, ratingStyle, ...cleanMessage } = message;
|
||||||
showRating?: boolean;
|
|
||||||
ratingStyle?: string;
|
|
||||||
};
|
|
||||||
return cleanMessage;
|
return cleanMessage;
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
@@ -109,14 +107,14 @@ export function useBuilderMessages() {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
content: msg.text,
|
content: msg.text,
|
||||||
read: false,
|
read: false,
|
||||||
} as ChatUI.AssistantMessage);
|
} satisfies ChatUI.AssistantMessage);
|
||||||
shouldClearThinking = true;
|
shouldClearThinking = true;
|
||||||
} else if (isWorkflowUpdatedMessage(msg)) {
|
} else if (isWorkflowUpdatedMessage(msg)) {
|
||||||
messages.push({
|
messages.push({
|
||||||
...msg,
|
...msg,
|
||||||
id: messageId,
|
id: messageId,
|
||||||
read: false,
|
read: false,
|
||||||
} as ChatUI.AssistantMessage);
|
} satisfies ChatUI.AssistantMessage);
|
||||||
// Don't clear thinking for workflow updates - they're just state changes
|
// Don't clear thinking for workflow updates - they're just state changes
|
||||||
} else if (isToolMessage(msg)) {
|
} else if (isToolMessage(msg)) {
|
||||||
processToolMessage(messages, msg, messageId);
|
processToolMessage(messages, msg, messageId);
|
||||||
@@ -149,9 +147,7 @@ export function useBuilderMessages() {
|
|||||||
|
|
||||||
// Check if we already have this tool message
|
// Check if we already have this tool message
|
||||||
const existingIndex = msg.toolCallId
|
const existingIndex = msg.toolCallId
|
||||||
? messages.findIndex(
|
? messages.findIndex((m) => m.type === 'tool' && m.toolCallId === msg.toolCallId)
|
||||||
(m) => m.type === 'tool' && (m as ChatUI.ToolMessage).toolCallId === msg.toolCallId,
|
|
||||||
)
|
|
||||||
: -1;
|
: -1;
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
if (existingIndex !== -1) {
|
||||||
@@ -180,21 +176,35 @@ export function useBuilderMessages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine the thinking message based on tool states
|
* If any tools are running, then it's still running tools and not done thinking
|
||||||
|
* If all tools are done and no text response yet, then it's still thinking
|
||||||
|
* Otherwise, it's done
|
||||||
|
*
|
||||||
|
* @param messages
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
function determineThinkingMessage(messages: ChatUI.AssistantMessage[]): string | undefined {
|
function getThinkingState(messages: ChatUI.AssistantMessage[]): {
|
||||||
// Check ALL messages to determine state
|
hasAnyRunningTools: boolean;
|
||||||
|
isStillThinking: boolean;
|
||||||
|
} {
|
||||||
const allToolMessages = messages.filter(
|
const allToolMessages = messages.filter(
|
||||||
(msg): msg is ChatUI.ToolMessage => msg.type === 'tool',
|
(msg): msg is ChatUI.ToolMessage => msg.type === 'tool',
|
||||||
);
|
);
|
||||||
const hasAnyRunningTools = allToolMessages.some((msg) => msg.status === 'running');
|
const hasAnyRunningTools = allToolMessages.some((msg) => msg.status === 'running');
|
||||||
|
if (hasAnyRunningTools) {
|
||||||
|
return {
|
||||||
|
hasAnyRunningTools: true,
|
||||||
|
isStillThinking: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const hasCompletedTools = allToolMessages.some((msg) => msg.status === 'completed');
|
const hasCompletedTools = allToolMessages.some((msg) => msg.status === 'completed');
|
||||||
|
|
||||||
// Find the last completed tool message
|
// Find the last completed tool message
|
||||||
let lastCompletedToolIndex = -1;
|
let lastCompletedToolIndex = -1;
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
const msg = messages[i];
|
const msg = messages[i];
|
||||||
if (msg.type === 'tool' && (msg as ChatUI.ToolMessage).status === 'completed') {
|
if (msg.type === 'tool' && msg.status === 'completed') {
|
||||||
lastCompletedToolIndex = i;
|
lastCompletedToolIndex = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -213,12 +223,21 @@ export function useBuilderMessages() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// - If any tools are running, show "Running tools..."
|
return {
|
||||||
// - If all tools are done and no text response yet, show "Processing results..."
|
hasAnyRunningTools: false,
|
||||||
// - Otherwise, clear the thinking message
|
isStillThinking: hasCompletedTools && !hasTextAfterTools,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the thinking message based on tool states
|
||||||
|
*/
|
||||||
|
function determineThinkingMessage(messages: ChatUI.AssistantMessage[]): string | undefined {
|
||||||
|
const { hasAnyRunningTools, isStillThinking } = getThinkingState(messages);
|
||||||
|
|
||||||
if (hasAnyRunningTools) {
|
if (hasAnyRunningTools) {
|
||||||
return locale.baseText('aiAssistant.thinkingSteps.runningTools');
|
return locale.baseText('aiAssistant.thinkingSteps.runningTools');
|
||||||
} else if (hasCompletedTools && !hasTextAfterTools) {
|
} else if (isStillThinking) {
|
||||||
return locale.baseText('aiAssistant.thinkingSteps.processingResults');
|
return locale.baseText('aiAssistant.thinkingSteps.processingResults');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +278,7 @@ export function useBuilderMessages() {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
content,
|
content,
|
||||||
read: true,
|
read: true,
|
||||||
} as ChatUI.AssistantMessage;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAssistantMessage(content: string, id: string): ChatUI.AssistantMessage {
|
function createAssistantMessage(content: string, id: string): ChatUI.AssistantMessage {
|
||||||
@@ -269,7 +288,7 @@ export function useBuilderMessages() {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
content,
|
content,
|
||||||
read: true,
|
read: true,
|
||||||
} as ChatUI.AssistantMessage;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createErrorMessage(
|
function createErrorMessage(
|
||||||
@@ -284,7 +303,7 @@ export function useBuilderMessages() {
|
|||||||
content,
|
content,
|
||||||
retry,
|
retry,
|
||||||
read: false,
|
read: false,
|
||||||
} as ChatUI.AssistantMessage;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearMessages(): ChatUI.AssistantMessage[] {
|
function clearMessages(): ChatUI.AssistantMessage[] {
|
||||||
@@ -310,7 +329,7 @@ export function useBuilderMessages() {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
content: message.text,
|
content: message.text,
|
||||||
read: false,
|
read: false,
|
||||||
} as ChatUI.AssistantMessage;
|
} satisfies ChatUI.AssistantMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isWorkflowUpdatedMessage(message)) {
|
if (isWorkflowUpdatedMessage(message)) {
|
||||||
@@ -318,7 +337,7 @@ export function useBuilderMessages() {
|
|||||||
...message,
|
...message,
|
||||||
id,
|
id,
|
||||||
read: false,
|
read: false,
|
||||||
} as ChatUI.AssistantMessage;
|
} satisfies ChatUI.AssistantMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isToolMessage(message)) {
|
if (isToolMessage(message)) {
|
||||||
@@ -331,7 +350,7 @@ export function useBuilderMessages() {
|
|||||||
status: message.status,
|
status: message.status,
|
||||||
updates: message.updates || [],
|
updates: message.updates || [],
|
||||||
read: false,
|
read: false,
|
||||||
} as ChatUI.AssistantMessage;
|
} satisfies ChatUI.AssistantMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle event messages
|
// Handle event messages
|
||||||
@@ -340,7 +359,7 @@ export function useBuilderMessages() {
|
|||||||
...message,
|
...message,
|
||||||
id,
|
id,
|
||||||
read: false,
|
read: false,
|
||||||
} as ChatUI.AssistantMessage;
|
} satisfies ChatUI.AssistantMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default fallback
|
// Default fallback
|
||||||
@@ -350,7 +369,7 @@ export function useBuilderMessages() {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
content: locale.baseText('aiAssistant.thinkingSteps.thinking'),
|
content: locale.baseText('aiAssistant.thinkingSteps.thinking'),
|
||||||
read: false,
|
read: false,
|
||||||
} as ChatUI.AssistantMessage;
|
} satisfies ChatUI.AssistantMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -361,5 +380,7 @@ export function useBuilderMessages() {
|
|||||||
clearMessages,
|
clearMessages,
|
||||||
addMessages,
|
addMessages,
|
||||||
mapAssistantMessageToUI,
|
mapAssistantMessageToUI,
|
||||||
|
applyRatingLogic,
|
||||||
|
clearRatingLogic,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ import type { Telemetry } from '@/plugins/telemetry';
|
|||||||
import type { ChatUI } from '@n8n/design-system/types/assistant';
|
import type { ChatUI } from '@n8n/design-system/types/assistant';
|
||||||
import { DEFAULT_CHAT_WIDTH, MAX_CHAT_WIDTH, MIN_CHAT_WIDTH } from './assistant.store';
|
import { DEFAULT_CHAT_WIDTH, MAX_CHAT_WIDTH, MIN_CHAT_WIDTH } from './assistant.store';
|
||||||
|
|
||||||
|
// Mock useI18n to return the keys instead of translations
|
||||||
|
vi.mock('@n8n/i18n', () => ({
|
||||||
|
useI18n: () => ({
|
||||||
|
baseText: (key: string) => key,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||||
let posthogStore: ReturnType<typeof usePostHog>;
|
let posthogStore: ReturnType<typeof usePostHog>;
|
||||||
|
|
||||||
@@ -195,8 +202,8 @@ describe('AI Builder store', () => {
|
|||||||
|
|
||||||
builderStore.sendChatMessage({ text: 'Add nodes and connect them' });
|
builderStore.sendChatMessage({ text: 'Add nodes and connect them' });
|
||||||
|
|
||||||
// Initially shows "Thinking..." from prepareForStreaming
|
// Initially shows "aiAssistant.thinkingSteps.thinking" from prepareForStreaming
|
||||||
expect(builderStore.assistantThinkingMessage).toBe('Thinking...');
|
expect(builderStore.assistantThinkingMessage).toBe('aiAssistant.thinkingSteps.thinking');
|
||||||
|
|
||||||
// First tool starts
|
// First tool starts
|
||||||
onMessageCallback({
|
onMessageCallback({
|
||||||
@@ -212,8 +219,8 @@ describe('AI Builder store', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should show "Running tools..."
|
// Should show "aiAssistant.thinkingSteps.runningTools"
|
||||||
expect(builderStore.assistantThinkingMessage).toBe('Running tools...');
|
expect(builderStore.assistantThinkingMessage).toBe('aiAssistant.thinkingSteps.runningTools');
|
||||||
|
|
||||||
// Second tool starts (different toolCallId)
|
// Second tool starts (different toolCallId)
|
||||||
onMessageCallback({
|
onMessageCallback({
|
||||||
@@ -229,8 +236,8 @@ describe('AI Builder store', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Still showing "Running tools..." with multiple tools
|
// Still showing "aiAssistant.thinkingSteps.runningTools" with multiple tools
|
||||||
expect(builderStore.assistantThinkingMessage).toBe('Running tools...');
|
expect(builderStore.assistantThinkingMessage).toBe('aiAssistant.thinkingSteps.runningTools');
|
||||||
|
|
||||||
// First tool completes
|
// First tool completes
|
||||||
onMessageCallback({
|
onMessageCallback({
|
||||||
@@ -246,8 +253,8 @@ describe('AI Builder store', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Still "Running tools..." because second tool is still running
|
// Still "aiAssistant.thinkingSteps.runningTools" because second tool is still running
|
||||||
expect(builderStore.assistantThinkingMessage).toBe('Running tools...');
|
expect(builderStore.assistantThinkingMessage).toBe('aiAssistant.thinkingSteps.runningTools');
|
||||||
|
|
||||||
// Second tool completes
|
// Second tool completes
|
||||||
onMessageCallback({
|
onMessageCallback({
|
||||||
@@ -263,15 +270,19 @@ describe('AI Builder store', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Now should show "Processing results..." because all tools completed
|
// Now should show "aiAssistant.thinkingSteps.processingResults" because all tools completed
|
||||||
expect(builderStore.assistantThinkingMessage).toBe('Processing results...');
|
expect(builderStore.assistantThinkingMessage).toBe(
|
||||||
|
'aiAssistant.thinkingSteps.processingResults',
|
||||||
|
);
|
||||||
|
|
||||||
// Call onDone to stop streaming
|
// Call onDone to stop streaming
|
||||||
onDoneCallback();
|
onDoneCallback();
|
||||||
|
|
||||||
// Message should persist after streaming ends
|
// Message should persist after streaming ends
|
||||||
expect(builderStore.streaming).toBe(false);
|
expect(builderStore.streaming).toBe(false);
|
||||||
expect(builderStore.assistantThinkingMessage).toBe('Processing results...');
|
expect(builderStore.assistantThinkingMessage).toBe(
|
||||||
|
'aiAssistant.thinkingSteps.processingResults',
|
||||||
|
);
|
||||||
|
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
@@ -311,14 +322,18 @@ describe('AI Builder store', () => {
|
|||||||
|
|
||||||
builderStore.sendChatMessage({ text: 'Add a node' });
|
builderStore.sendChatMessage({ text: 'Add a node' });
|
||||||
|
|
||||||
// Should show "Processing results..." when tool completes
|
// Should show "aiAssistant.thinkingSteps.processingResults" when tool completes
|
||||||
await vi.waitFor(() =>
|
await vi.waitFor(() =>
|
||||||
expect(builderStore.assistantThinkingMessage).toBe('Processing results...'),
|
expect(builderStore.assistantThinkingMessage).toBe(
|
||||||
|
'aiAssistant.thinkingSteps.processingResults',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Should still show "Processing results..." after workflow-updated
|
// Should still show "aiAssistant.thinkingSteps.processingResults" after workflow-updated
|
||||||
await vi.waitFor(() => expect(builderStore.chatMessages).toHaveLength(3)); // user + tool + workflow
|
await vi.waitFor(() => expect(builderStore.chatMessages).toHaveLength(3)); // user + tool + workflow
|
||||||
expect(builderStore.assistantThinkingMessage).toBe('Processing results...');
|
expect(builderStore.assistantThinkingMessage).toBe(
|
||||||
|
'aiAssistant.thinkingSteps.processingResults',
|
||||||
|
);
|
||||||
|
|
||||||
// Verify streaming has ended
|
// Verify streaming has ended
|
||||||
expect(builderStore.streaming).toBe(false);
|
expect(builderStore.streaming).toBe(false);
|
||||||
@@ -709,4 +724,58 @@ describe('AI Builder store', () => {
|
|||||||
expect((assistantMessages[0] as ChatUI.TextMessage).content).toBe('[Task aborted]');
|
expect((assistantMessages[0] as ChatUI.TextMessage).content).toBe('[Task aborted]');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Rating logic integration', () => {
|
||||||
|
it('should clear ratings from existing messages when preparing for streaming', () => {
|
||||||
|
const builderStore = useBuilderStore();
|
||||||
|
|
||||||
|
// Setup initial messages with ratings
|
||||||
|
builderStore.chatMessages = [
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Previous message',
|
||||||
|
showRating: true,
|
||||||
|
ratingStyle: 'regular',
|
||||||
|
read: false,
|
||||||
|
} satisfies ChatUI.AssistantMessage,
|
||||||
|
{
|
||||||
|
id: 'msg-2',
|
||||||
|
role: 'assistant',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Another message',
|
||||||
|
showRating: true,
|
||||||
|
ratingStyle: 'minimal',
|
||||||
|
read: false,
|
||||||
|
} satisfies ChatUI.AssistantMessage,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock API to prevent actual network calls
|
||||||
|
apiSpy.mockImplementationOnce(() => {});
|
||||||
|
|
||||||
|
// Send new message which calls prepareForStreaming
|
||||||
|
builderStore.sendChatMessage({ text: 'New message' });
|
||||||
|
|
||||||
|
// Verify that existing messages no longer have rating properties
|
||||||
|
expect(builderStore.chatMessages).toHaveLength(3); // 2 existing + 1 new user message
|
||||||
|
|
||||||
|
const firstMessage = builderStore.chatMessages[0] as ChatUI.TextMessage;
|
||||||
|
expect(firstMessage).not.toHaveProperty('showRating');
|
||||||
|
expect(firstMessage).not.toHaveProperty('ratingStyle');
|
||||||
|
expect(firstMessage.content).toBe('Previous message');
|
||||||
|
|
||||||
|
const secondMessage = builderStore.chatMessages[1] as ChatUI.TextMessage;
|
||||||
|
expect(secondMessage).not.toHaveProperty('showRating');
|
||||||
|
expect(secondMessage).not.toHaveProperty('ratingStyle');
|
||||||
|
expect(secondMessage.content).toBe('Another message');
|
||||||
|
|
||||||
|
// New user message should not have rating properties
|
||||||
|
const userMessage = builderStore.chatMessages[2] as ChatUI.TextMessage;
|
||||||
|
expect(userMessage.role).toBe('user');
|
||||||
|
expect(userMessage.content).toBe('New message');
|
||||||
|
expect(userMessage).not.toHaveProperty('showRating');
|
||||||
|
expect(userMessage).not.toHaveProperty('ratingStyle');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
|
|||||||
createErrorMessage,
|
createErrorMessage,
|
||||||
clearMessages,
|
clearMessages,
|
||||||
mapAssistantMessageToUI,
|
mapAssistantMessageToUI,
|
||||||
|
clearRatingLogic,
|
||||||
} = useBuilderMessages();
|
} = useBuilderMessages();
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
@@ -203,7 +204,7 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
|
|||||||
*/
|
*/
|
||||||
function prepareForStreaming(userMessage: string, messageId: string) {
|
function prepareForStreaming(userMessage: string, messageId: string) {
|
||||||
const userMsg = createUserMessage(userMessage, messageId);
|
const userMsg = createUserMessage(userMessage, messageId);
|
||||||
chatMessages.value = [...chatMessages.value, userMsg];
|
chatMessages.value = clearRatingLogic([...chatMessages.value, userMsg]);
|
||||||
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking'));
|
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking'));
|
||||||
streaming.value = true;
|
streaming.value = true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user