fix(editor): Fix AI assistant loading message layout (#11819)

This commit is contained in:
Milorad FIlipović 2024-11-22 08:05:24 +01:00 committed by GitHub
parent ececdb09e4
commit 89b4807243
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 1217 additions and 1193 deletions

View file

@ -21,7 +21,7 @@ const Template: StoryFn = (args, { argTypes }) => ({
components: {
AskAssistantChat,
},
template: '<div style="width:275px; height:100%"><ask-assistant-chat v-bind="args" /></div>',
template: '<div style="width:275px; height:500px"><ask-assistant-chat v-bind="args" /></div>',
methods,
});

View file

@ -136,158 +136,167 @@ async function onCopyButtonClick(content: string, e: MouseEvent) {
</div>
</div>
<div :class="$style.body">
<div v-if="messages?.length" :class="$style.messages">
<div
v-for="(message, i) in messages"
:key="i"
:class="$style.message"
:data-test-id="
message.role === 'assistant' ? 'chat-message-assistant' : 'chat-message-user'
"
>
<div v-if="messages?.length || loadingMessage" :class="$style.messages">
<div v-if="messages?.length">
<div
v-if="
!isEndOfSessionEvent(message) && (i === 0 || message.role !== messages[i - 1].role)
v-for="(message, i) in messages"
:key="i"
:class="$style.message"
:data-test-id="
message.role === 'assistant' ? 'chat-message-assistant' : 'chat-message-user'
"
:class="{ [$style.roleName]: true, [$style.userSection]: i > 0 }"
>
<AssistantAvatar v-if="message.role === 'assistant'" />
<n8n-avatar
v-else
:first-name="user?.firstName"
:last-name="user?.lastName"
size="xsmall"
/>
<div
v-if="
!isEndOfSessionEvent(message) && (i === 0 || message.role !== messages[i - 1].role)
"
:class="{ [$style.roleName]: true, [$style.userSection]: i > 0 }"
>
<AssistantAvatar v-if="message.role === 'assistant'" />
<n8n-avatar
v-else
:first-name="user?.firstName"
:last-name="user?.lastName"
size="xsmall"
/>
<span v-if="message.role === 'assistant'">{{
t('assistantChat.aiAssistantName')
}}</span>
<span v-else>{{ t('assistantChat.you') }}</span>
</div>
<div v-if="message.type === 'block'">
<div :class="$style.block">
<div :class="$style.blockTitle">
{{ message.title }}
<BlinkingCursor
v-if="streaming && i === messages?.length - 1 && !message.content"
/>
</div>
<div :class="$style.blockBody">
<span
v-n8n-html="renderMarkdown(message.content)"
:class="$style['rendered-content']"
></span>
<BlinkingCursor
v-if="streaming && i === messages?.length - 1 && message.title && message.content"
/>
<span v-if="message.role === 'assistant'">{{
t('assistantChat.aiAssistantName')
}}</span>
<span v-else>{{ t('assistantChat.you') }}</span>
</div>
<div v-if="message.type === 'block'">
<div :class="$style.block">
<div :class="$style.blockTitle">
{{ message.title }}
<BlinkingCursor
v-if="streaming && i === messages?.length - 1 && !message.content"
/>
</div>
<div :class="$style.blockBody">
<span
v-n8n-html="renderMarkdown(message.content)"
:class="$style['rendered-content']"
></span>
<BlinkingCursor
v-if="
streaming && i === messages?.length - 1 && message.title && message.content
"
/>
</div>
</div>
</div>
</div>
<div v-else-if="message.type === 'text'" :class="$style.textMessage">
<span
v-if="message.role === 'user'"
v-n8n-html="renderMarkdown(message.content)"
:class="$style['rendered-content']"
></span>
<div
v-else
v-n8n-html="renderMarkdown(message.content)"
:class="[$style.assistantText, $style['rendered-content']]"
></div>
<div
v-if="message?.codeSnippet"
:class="$style['code-snippet']"
data-test-id="assistant-code-snippet"
>
<header v-if="isClipboardSupported">
<n8n-button
type="tertiary"
text="true"
size="mini"
data-test-id="assistant-copy-snippet-button"
@click="onCopyButtonClick(message.codeSnippet, $event)"
>
{{ t('assistantChat.copy') }}
</n8n-button>
</header>
<div v-else-if="message.type === 'text'" :class="$style.textMessage">
<span
v-if="message.role === 'user'"
v-n8n-html="renderMarkdown(message.content)"
:class="$style['rendered-content']"
></span>
<div
v-n8n-html="renderMarkdown(message.codeSnippet).trim()"
data-test-id="assistant-code-snippet-content"
:class="[$style['snippet-content'], $style['rendered-content']]"
v-else
v-n8n-html="renderMarkdown(message.content)"
:class="[$style.assistantText, $style['rendered-content']]"
></div>
<div
v-if="message?.codeSnippet"
:class="$style['code-snippet']"
data-test-id="assistant-code-snippet"
>
<header v-if="isClipboardSupported">
<n8n-button
type="tertiary"
text="true"
size="mini"
data-test-id="assistant-copy-snippet-button"
@click="onCopyButtonClick(message.codeSnippet, $event)"
>
{{ t('assistantChat.copy') }}
</n8n-button>
</header>
<div
v-n8n-html="renderMarkdown(message.codeSnippet).trim()"
data-test-id="assistant-code-snippet-content"
:class="[$style['snippet-content'], $style['rendered-content']]"
></div>
</div>
<BlinkingCursor
v-if="streaming && i === messages?.length - 1 && message.role === 'assistant'"
/>
</div>
<BlinkingCursor
v-if="streaming && i === messages?.length - 1 && message.role === 'assistant'"
/>
</div>
<div
v-else-if="message.type === 'error'"
:class="$style.error"
data-test-id="chat-message-system"
>
<span> {{ message.content }}</span>
<n8n-button
v-if="message.retry"
type="secondary"
size="mini"
:class="$style.retryButton"
data-test-id="error-retry-button"
@click="() => message.retry?.()"
<div
v-else-if="message.type === 'error'"
:class="$style.error"
data-test-id="chat-message-system"
>
{{ t('generic.retry') }}
</n8n-button>
</div>
<div v-else-if="message.type === 'code-diff'">
<CodeDiff
:title="message.description"
:content="message.codeDiff"
:replacing="message.replacing"
:replaced="message.replaced"
:error="message.error"
:streaming="streaming && i === messages?.length - 1"
@replace="() => emit('codeReplace', i)"
@undo="() => emit('codeUndo', i)"
/>
</div>
<div
v-else-if="isEndOfSessionEvent(message)"
:class="$style.endOfSessionText"
data-test-id="chat-message-system"
>
<span>
{{ t('assistantChat.sessionEndMessage.1') }}
</span>
<InlineAskAssistantButton size="small" :static="true" />
<span>
{{ t('assistantChat.sessionEndMessage.2') }}
</span>
</div>
<div
v-if="
!streaming &&
'quickReplies' in message &&
message.quickReplies?.length &&
i === messages?.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">
<span> {{ message.content }}</span>
<n8n-button
v-if="opt.text"
v-if="message.retry"
type="secondary"
size="mini"
@click="() => onQuickReply(opt)"
:class="$style.retryButton"
data-test-id="error-retry-button"
@click="() => message.retry?.()"
>
{{ opt.text }}
{{ t('generic.retry') }}
</n8n-button>
</div>
<div v-else-if="message.type === 'code-diff'">
<CodeDiff
:title="message.description"
:content="message.codeDiff"
:replacing="message.replacing"
:replaced="message.replaced"
:error="message.error"
:streaming="streaming && i === messages?.length - 1"
@replace="() => emit('codeReplace', i)"
@undo="() => emit('codeUndo', i)"
/>
</div>
<div
v-else-if="isEndOfSessionEvent(message)"
:class="$style.endOfSessionText"
data-test-id="chat-message-system"
>
<span>
{{ t('assistantChat.sessionEndMessage.1') }}
</span>
<InlineAskAssistantButton size="small" :static="true" />
<span>
{{ t('assistantChat.sessionEndMessage.2') }}
</span>
</div>
<div
v-if="
!streaming &&
'quickReplies' in message &&
message.quickReplies?.length &&
i === messages?.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">
<n8n-button
v-if="opt.text"
type="secondary"
size="mini"
@click="() => onQuickReply(opt)"
>
{{ opt.text }}
</n8n-button>
</div>
</div>
</div>
</div>
</div>
<div v-if="loadingMessage" :class="$style.messages">
<AssistantLoadingMessage :message="loadingMessage" />
<div
v-if="loadingMessage"
:class="{ [$style.message]: true, [$style.loading]: messages?.length }"
>
<AssistantLoadingMessage :message="loadingMessage" />
</div>
</div>
<div
v-else-if="showPlaceholder"
@ -405,6 +414,10 @@ p {
margin-bottom: var(--spacing-xs);
font-size: var(--font-size-2xs);
line-height: var(--font-line-height-xloose);
&.loading {
margin-top: var(--spacing-m);
}
}
.roleName {

View file

@ -29,11 +29,11 @@ withDefaults(
.container {
display: flex;
gap: var(--spacing-3xs);
align-items: flex-start;
user-select: none;
}
.avatar {
height: var(--spacing-s);
animation: pulse 1.5s infinite;
position: relative;
}