feat: Decrease AI Builder wordiness (no-changelog) (#18920)

This commit is contained in:
Mutasem Aldmour
2025-09-01 11:36:12 +02:00
committed by GitHub
parent bf198f8263
commit a9d59ed84a
9 changed files with 350 additions and 82 deletions

View File

@@ -1,9 +1,8 @@
import { render } from '@testing-library/vue';
import { vi } from 'vitest';
import { n8nHtml } from '@n8n/design-system/directives';
import AskAssistantChat from './AskAssistantChat.vue';
import { n8nHtml } from '../../directives';
import type { Props as MessageWrapperProps } from './messages/MessageWrapper.vue';
import type { ChatUI } from '../../types/assistant';
@@ -388,6 +387,70 @@ describe('AskAssistantChat', () => {
});
});
it('should collapse tool messages with same toolName with hidden messages in between', () => {
const messages: Array<ChatUI.AssistantMessage & { id: string }> = [
createToolMessage({
id: '1',
status: 'running',
displayTitle: 'Searching...',
updates: [{ type: 'progress', data: { status: 'Initializing search' } }],
}),
{
id: '2',
role: 'assistant',
type: 'agent-suggestion',
title: 'Agent Suggestion',
content: 'This is a suggestion from the agent',
suggestionId: 'test',
quickReplies: [
{ type: 'accept', text: 'Accept suggestion' },
{ type: 'reject', text: 'Reject suggestion' },
],
read: true,
},
createToolMessage({
id: '2',
status: 'running',
displayTitle: 'Still searching...',
customDisplayTitle: 'Custom Search Title',
updates: [{ type: 'progress', data: { status: 'Processing results' } }],
}),
{
id: 'test',
role: 'assistant',
type: 'workflow-updated',
codeSnippet: '',
},
createToolMessage({
id: '3',
status: 'completed',
displayTitle: 'Search Complete',
updates: [{ type: 'output', data: { result: 'Found 10 items' } }],
}),
];
renderWithMessages(messages);
expectMessageWrapperCalledTimes(1);
const props = getMessageWrapperProps();
expectToolMessage(props, {
id: '3',
role: 'assistant',
type: 'tool',
toolName: 'search',
status: 'running',
displayTitle: 'Still searching...',
customDisplayTitle: 'Custom Search Title',
updates: [
{ type: 'progress', data: { status: 'Initializing search' } },
{ type: 'progress', data: { status: 'Processing results' } },
{ type: 'output', data: { result: 'Found 10 items' } },
],
read: true,
});
});
it('should not collapse tool messages with different toolNames', () => {
const messages = [
createToolMessage({
@@ -738,4 +801,184 @@ describe('AskAssistantChat', () => {
);
});
});
describe('Quick Replies', () => {
const renderWithQuickReplies = (
messages: ChatUI.AssistantMessage[],
streaming = false,
loadingMessage?: string,
) => {
return render(AskAssistantChat, {
global: {
directives: { n8nHtml },
stubs: {
...Object.fromEntries(stubs.map((stub) => [stub, true])),
'n8n-button': { template: '<button><slot></button' },
},
},
props: {
user: { firstName: 'Kobi', lastName: 'Dog' },
messages,
streaming,
loadingMessage,
},
});
};
it('should render quick replies for code-diff message', () => {
const messages: ChatUI.AssistantMessage[] = [
{
id: '1',
role: 'assistant',
type: 'text',
content: 'Here is a solution',
read: true,
},
{
id: '2',
role: 'assistant',
type: 'code-diff',
description: 'Code solution',
codeDiff: 'diff content',
suggestionId: 'test',
quickReplies: [
{ type: 'new-suggestion', text: 'Give me another solution' },
{ type: 'resolved', text: 'All good' },
],
read: true,
},
];
const wrapper = renderWithQuickReplies(messages);
// Quick replies should be rendered (2 buttons found)
expect(wrapper.queryAllByTestId('quick-replies')).toHaveLength(2);
// Quick reply title should be visible
expect(wrapper.container.textContent).toContain('Quick reply');
expect(wrapper.container).toHaveTextContent('Give me another solution');
expect(wrapper.container).toHaveTextContent('All good');
});
it('should render quick replies for agent-suggestion messages', () => {
const messages: ChatUI.AssistantMessage[] = [
{
id: '1',
role: 'assistant',
type: 'text',
content: 'Here is a solution',
read: true,
},
{
id: '2',
role: 'assistant',
type: 'agent-suggestion',
title: 'Agent Suggestion',
content: 'This is a suggestion from the agent',
suggestionId: 'test',
quickReplies: [
{ type: 'accept', text: 'Accept suggestion' },
{ type: 'reject', text: 'Reject suggestion' },
],
read: true,
},
];
const wrapper = renderWithQuickReplies(messages);
// Quick replies should still be rendered even though agent-suggestion is filtered out
expect(wrapper.queryAllByTestId('quick-replies')).toHaveLength(2);
// Quick reply title should be visible
expect(wrapper.container.textContent).toContain('Quick reply');
expect(wrapper.container).toHaveTextContent('Accept suggestion');
expect(wrapper.container).toHaveTextContent('Reject suggestion');
});
it('should not render quick replies when streaming', () => {
const messages: ChatUI.AssistantMessage[] = [
{
id: '1',
role: 'assistant',
type: 'code-diff',
description: 'Code solution',
codeDiff: 'diff content',
suggestionId: 'test',
quickReplies: [{ type: 'new-suggestion', text: 'Give me another solution' }],
read: true,
},
];
const wrapper = renderWithQuickReplies(messages, true);
expect(wrapper.queryAllByTestId('quick-replies')).toHaveLength(0);
expect(wrapper.container.textContent).not.toContain('Quick reply');
expect(wrapper.container.textContent).not.toContain('Give me another solution');
});
it('should not render quick replies for non-last messages', () => {
const messages: ChatUI.AssistantMessage[] = [
{
id: '1',
role: 'assistant',
type: 'code-diff',
description: 'Code solution',
codeDiff: 'diff content',
suggestionId: 'test',
quickReplies: [{ type: 'new-suggestion', text: 'Give me another solution' }],
read: true,
},
{
id: '2',
role: 'assistant',
type: 'text',
content: 'Follow up message',
read: true,
},
];
const wrapper = renderWithQuickReplies(messages);
// Quick replies should not be rendered since the message with quick replies is not last
expect(wrapper.queryAllByTestId('quick-replies')).toHaveLength(0);
expect(wrapper.container.textContent).not.toContain('Quick reply');
expect(wrapper.container.textContent).not.toContain('Give me another solution');
});
it('should not render quick replies when last message has no quickReplies', () => {
const messages: ChatUI.AssistantMessage[] = [
{
id: '1',
role: 'assistant',
type: 'text',
content: 'Simple text message',
read: true,
},
];
const wrapper = renderWithQuickReplies(messages);
expect(wrapper.queryAllByTestId('quick-replies')).toHaveLength(0);
expect(wrapper.container.textContent).not.toContain('Quick reply');
});
it('should not render quick replies when last message has empty quickReplies array', () => {
const messages: ChatUI.AssistantMessage[] = [
{
id: '1',
role: 'assistant',
type: 'code-diff',
description: 'Code solution',
codeDiff: 'diff content',
suggestionId: 'test',
quickReplies: [],
read: true,
},
];
const wrapper = renderWithQuickReplies(messages);
expect(wrapper.queryAllByTestId('quick-replies')).toHaveLength(0);
expect(wrapper.container.textContent).not.toContain('Quick reply');
});
});
});

View File

@@ -11,6 +11,7 @@ import AssistantText from '../AskAssistantText/AssistantText.vue';
import InlineAskAssistantButton from '../InlineAskAssistantButton/InlineAskAssistantButton.vue';
import N8nButton from '../N8nButton';
import N8nIcon from '../N8nIcon';
import { getSupportedMessageComponent } from './messages/helpers';
const { t } = useI18n();
@@ -63,6 +64,11 @@ function normalizeMessages(messages: ChatUI.AssistantMessage[]): ChatUI.Assistan
}));
}
// filter out these messages so that tool collapsing works correctly
function filterOutHiddenMessages(messages: ChatUI.AssistantMessage[]): ChatUI.AssistantMessage[] {
return messages.filter((message) => Boolean(getSupportedMessageComponent(message.type)));
}
function collapseToolMessages(messages: ChatUI.AssistantMessage[]): ChatUI.AssistantMessage[] {
const result: ChatUI.AssistantMessage[] = [];
let i = 0;
@@ -136,7 +142,17 @@ function collapseToolMessages(messages: ChatUI.AssistantMessage[]): ChatUI.Assis
// Ensure all messages have required id and read properties, and collapse tool messages
const normalizedMessages = computed(() => {
const normalized = normalizeMessages(props.messages);
return collapseToolMessages(normalized);
return collapseToolMessages(filterOutHiddenMessages(normalized));
});
// Get quickReplies from the last message in the original messages (before filtering)
const lastMessageQuickReplies = computed(() => {
if (!props.messages?.length || props.streaming) return [];
const lastMessage = props.messages[props.messages.length - 1];
return 'quickReplies' in lastMessage && lastMessage.quickReplies?.length
? lastMessage.quickReplies
: [];
});
const textInputValue = ref<string>('');
@@ -252,18 +268,17 @@ watch(
/>
<div
v-if="
!streaming &&
'quickReplies' in message &&
message.quickReplies?.length &&
i === normalizedMessages.length - 1
"
v-if="lastMessageQuickReplies.length && i === normalizedMessages.length - 1"
:class="$style.quickReplies"
>
<div :class="$style.quickRepliesTitle">
{{ t('assistantChat.quickRepliesTitle') }}
</div>
<div v-for="opt in message.quickReplies" :key="opt.type" data-test-id="quick-replies">
<div
v-for="opt in lastMessageQuickReplies"
:key="opt.type"
data-test-id="quick-replies"
>
<N8nButton
v-if="opt.text"
type="secondary"

View File

@@ -3,12 +3,7 @@
<script setup lang="ts">
import { computed, type Component } from 'vue';
import BlockMessage from './BlockMessage.vue';
import CodeDiffMessage from './CodeDiffMessage.vue';
import ErrorMessage from './ErrorMessage.vue';
import EventMessage from './EventMessage.vue';
import TextMessage from './TextMessage.vue';
import ToolMessage from './ToolMessage.vue';
import { getSupportedMessageComponent } from './helpers';
import type { ChatUI, RatingFeedback } from '../../../types/assistant';
export interface Props {
@@ -31,25 +26,7 @@ const emit = defineEmits<{
}>();
const messageComponent = computed<Component | null>(() => {
switch (props.message.type) {
case 'text':
return TextMessage;
case 'block':
return BlockMessage;
case 'code-diff':
return CodeDiffMessage;
case 'error':
return ErrorMessage;
case 'event':
return EventMessage;
case 'tool':
return ToolMessage;
case 'agent-suggestion':
case 'workflow-updated':
return null;
default:
return null;
}
return getSupportedMessageComponent(props.message.type);
});
</script>

View File

@@ -0,0 +1,30 @@
import type { ChatUI } from '@n8n/design-system/types';
import BlockMessage from './BlockMessage.vue';
import CodeDiffMessage from './CodeDiffMessage.vue';
import ErrorMessage from './ErrorMessage.vue';
import EventMessage from './EventMessage.vue';
import TextMessage from './TextMessage.vue';
import ToolMessage from './ToolMessage.vue';
export function getSupportedMessageComponent(type: ChatUI.AssistantMessage['type']) {
switch (type) {
case 'text':
return TextMessage;
case 'block':
return BlockMessage;
case 'code-diff':
return CodeDiffMessage;
case 'error':
return ErrorMessage;
case 'event':
return EventMessage;
case 'tool':
return ToolMessage;
case 'agent-suggestion':
case 'workflow-updated':
return null;
default:
return null;
}
}