fix(editor): Fix styling and typography in AI Assistant chat (#10895)

This commit is contained in:
Milorad FIlipović 2024-09-23 13:05:15 +02:00 committed by GitHub
parent 67fb6d6fdd
commit 57ff3cc27b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 284 additions and 59 deletions

View file

@ -37,7 +37,7 @@ export class AIAssistant extends BasePage {
cy.getByTestId('node-error-view-ask-assistant-button').find('button').first(),
credentialEditAssistantButton: () =>
cy.getByTestId('credentail-edit-ask-assistant-button').find('button').first(),
codeSnippet: () => cy.getByTestId('assistant-code-snippet'),
codeSnippet: () => cy.getByTestId('assistant-code-snippet-content'),
};
actions = {

View file

@ -268,3 +268,107 @@ WithCodeSnippet.args = {
},
]),
};
export const RichTextMessage = Template.bind({});
RichTextMessage.args = {
user: {
firstName: 'Kobi',
lastName: 'Dog',
},
messages: getMessages([
{
id: '29083188',
role: 'user',
type: 'text',
content: 'Hey',
read: true,
},
{
id: '29083188',
type: 'text',
role: 'assistant',
content: 'Hello Kobi! How can I assist you with n8n today?',
read: true,
},
{
id: '21514129',
role: 'user',
type: 'text',
content: 'Can you show me a message example with paragraphs, lists and links?',
read: true,
},
{
id: '21514129',
type: 'text',
role: 'assistant',
content:
"Sure: \n\nTo connect your Slack account to n8n, follow these steps:\n\n1. Open your [Slack API Apps](https://api.slack.com/apps) page.\n2. Select **Create New App > From scratch**.\n3. Enter an **App Name**.\n4. Select the **Workspace** where you'll be developing your app.\n5. Select **Create App**.\n6. In **Basic Information**, open the **App Credentials** section.\n7. Copy the **Client ID** and **Client Secret**. Paste these into the corresponding fields in n8n.\n8. In **Basic Information > Building Apps for Slack**, select **Add features and functionality**.\n9. Select **Permissions**.\n10. In the **Redirect URLs** section, select **Add New Redirect URL**.\n\nFor more details, you can refer to the [Slack API Quickstart](https://api.slack.com/quickstart) and the [Installing with OAuth](https://api.slack.com/authentication/oauth-v2) documentation.",
codeSnippet: '',
read: true,
},
{
id: '86572001',
role: 'user',
type: 'text',
content: 'Can you show me an example of a table?',
read: true,
},
{
id: '86572001',
type: 'text',
role: 'assistant',
content:
'Sure, here it is:\n\n| **Scope name** | **Notes** |\n| --- | --- |\n| `channels:read` | |\n| `channels:write` | Not available as a bot token scope |\n| `stars:read`| Not available as a bot token scope |\n| `stars:write` | Not available as a bot token scope |\n| `users.profile:write` | Not available as a bot token scope |\n| `users:read` | |',
read: true,
},
{
id: '86572001',
role: 'user',
type: 'text',
content: 'Thanks, can you send me another one with more columns?',
read: true,
},
{
id: '86572001',
type: 'text',
role: 'assistant',
content:
'Yup:\n\n| **Scope name** | **Notes** | **One More Column** |\n| --- | --- | --- |\n| `channels:read` | | Something else |\n| `channels:write` | Not available as a bot token scope | Something else |\n| `stars:read`| Not available as a bot token scope |\n| `stars:write` | Not available as a bot token scope |\n| `users.profile:write` | Not available as a bot token scope |\n| `users:read` | |',
read: true,
},
{
id: '2556642',
role: 'user',
type: 'text',
content: 'Great',
read: true,
},
{
id: '2556642',
type: 'text',
role: 'assistant',
content:
"I'm glad you found the information helpful! If you have any more questions about n8n or need further assistance, feel free to ask.",
read: true,
},
{
id: '2556642',
type: 'text',
role: 'assistant',
content: 'Did this answer solve your question?',
quickReplies: [
{
text: 'Yes, thanks',
type: 'all-good',
isFeedback: true,
},
{
text: 'No, I am still stuck',
type: 'still-stuck',
isFeedback: true,
},
],
read: true,
},
]),
};

View file

@ -18,12 +18,21 @@ const { t } = useI18n();
const md = new Markdown({
breaks: true,
}).use(markdownLink, {
});
md.use(markdownLink, {
attrs: {
target: '_blank',
rel: 'noopener',
},
});
// Wrap tables in div
md.renderer.rules.table_open = function () {
return '<div class="table-wrapper"><table>';
};
md.renderer.rules.table_close = function () {
return '</table></div>';
};
const MAX_CHAT_INPUT_HEIGHT = 100;
@ -65,6 +74,10 @@ const showPlaceholder = computed(() => {
return !props.messages?.length && !props.loadingMessage && !props.sessionId;
});
const isClipboardSupported = computed(() => {
return navigator.clipboard?.writeText;
});
function isEndOfSessionEvent(event?: ChatUI.AssistantMessage) {
return event?.type === 'event' && event?.eventName === 'end-session';
}
@ -97,6 +110,15 @@ function growInput() {
const scrollHeight = chatInput.value.scrollHeight;
chatInput.value.style.height = `${Math.min(scrollHeight, MAX_CHAT_INPUT_HEIGHT)}px`;
}
async function onCopyButtonClick(content: string, e: MouseEvent) {
const button = e.target as HTMLButtonElement;
await navigator.clipboard.writeText(content);
button.innerText = t('assistantChat.copied');
setTimeout(() => {
button.innerText = t('assistantChat.copy');
}, 2000);
}
</script>
<template>
@ -151,7 +173,10 @@ function growInput() {
/>
</div>
<div :class="$style.blockBody">
<span v-n8n-html="renderMarkdown(message.content)"></span>
<span
v-n8n-html="renderMarkdown(message.content)"
:class="$style['rendered-content']"
></span>
<BlinkingCursor
v-if="streaming && i === messages?.length - 1 && message.title && message.content"
/>
@ -162,18 +187,35 @@ function growInput() {
<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"
:class="[$style.assistantText, $style['rendered-content']]"
></div>
<div
v-if="message?.codeSnippet"
:class="$style['code-snippet']"
data-test-id="assistant-code-snippet"
v-n8n-html="renderMarkdown(message.codeSnippet).trim()"
></div>
>
<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'"
/>
@ -415,14 +457,37 @@ p {
word-break: break-word;
}
code[class^='language-'] {
display: block;
padding: var(--spacing-4xs);
}
.code-snippet {
position: relative;
border: var(--border-base);
background-color: var(--color-foreground-xlight);
border-radius: var(--border-radius-base);
padding: var(--spacing-2xs);
font-family: var(--font-family-monospace);
font-size: var(--font-size-3xs);
max-height: 218px; // 12 lines
overflow: auto;
margin: var(--spacing-4s) 0;
header {
display: flex;
justify-content: flex-end;
padding: var(--spacing-4xs);
border-bottom: var(--border-base);
button:active,
button:focus {
outline: none !important;
}
}
.snippet-content {
padding: var(--spacing-2xs);
}
pre {
white-space-collapse: collapse;
@ -491,17 +556,60 @@ p {
}
.assistantText {
display: inline;
display: inline-flex;
flex-direction: column;
}
.rendered-content {
p {
display: inline;
line-height: 1.7;
margin: 0;
margin: var(--spacing-4xs) 0;
}
h1,
h2,
h3 {
font-weight: var(--font-weight-bold);
font-size: var(--font-size-xs);
margin: var(--spacing-xs) 0 var(--spacing-4xs);
}
h4,
h5,
h6 {
font-weight: var(--font-weight-bold);
font-size: var(--font-size-2xs);
}
ul,
ol {
list-style-position: inside;
margin: var(--spacing-xs) 0 var(--spacing-xs) var(--spacing-xs);
margin: var(--spacing-4xs) 0 var(--spacing-4xs) var(--spacing-l);
ul,
ol {
margin-left: var(--spacing-xs);
margin-top: var(--spacing-4xs);
}
}
:global(.table-wrapper) {
overflow-x: auto;
}
table {
margin: var(--spacing-4xs) 0;
th {
white-space: nowrap;
min-width: 120px;
width: auto;
}
th,
td {
border: var(--border-base);
padding: var(--spacing-4xs);
}
}
}

View file

@ -130,7 +130,7 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
class="textMessage"
>
<div
class="assistantText"
class="assistantText rendered-content"
>
<p>
Hi Max! Here is my top solution to fix the error in your
@ -434,7 +434,9 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
<div
class="textMessage"
>
<span>
<span
class="rendered-content"
>
<p>
Give it to me
<strong>
@ -511,7 +513,9 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
<div
class="blockBody"
>
<span>
<span
class="rendered-content"
>
<p>
Solution steps:
</p>
@ -1055,7 +1059,7 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
class="textMessage"
>
<div
class="assistantText"
class="assistantText rendered-content"
>
<p>
Hi Max! Here is my top solution to fix the error in your
@ -1300,7 +1304,7 @@ exports[`AskAssistantChat > renders message with code snippet 1`] = `
class="textMessage"
>
<div
class="assistantText"
class="assistantText rendered-content"
>
<p>
Hi Max! Here is my top solution to fix the error in your
@ -1316,66 +1320,72 @@ exports[`AskAssistantChat > renders message with code snippet 1`] = `
class="code-snippet"
data-test-id="assistant-code-snippet"
>
<p>
node.on('input', function(msg) {
<br />
<!--v-if-->
<div
class="snippet-content rendered-content"
data-test-id="assistant-code-snippet-content"
>
<p>
node.on('input', function(msg) {
<br />
if (msg.seed) { dummyjson.seed = msg.seed; }
<br />
<br />
try {
<br />
<br />
var value = dummyjson.parse(node.template, {mockdata: msg});
<br />
<br />
if (node.syntax === 'json') {
<br />
<br />
try { value = JSON.parse(value); }
<br />
<br />
catch(e) { node.error(RED._('datagen.errors.json-error')); }
<br />
<br />
}
<br />
<br />
if (node.fieldType === 'msg') {
<br />
<br />
RED.util.setMessageProperty(msg,node.field,value);
<br />
<br />
}
<br />
<br />
else if (node.fieldType === 'flow') {
<br />
<br />
node.context().flow.set(node.field,value);
<br />
<br />
}
<br />
<br />
else if (node.fieldType === 'global') {
<br />
<br />
node.context().global.set(node.field,value);
<br />
<br />
}
<br />
<br />
node.send(msg);
<br />
<br />
}
<br />
<br />
catch(e) {
</p>
</p>
</div>
</div>
<!--v-if-->
</div>
@ -1538,7 +1548,7 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
class="textMessage"
>
<div
class="assistantText"
class="assistantText rendered-content"
>
<p>
Hi Max! Here is my top solution to fix the error in your

View file

@ -24,6 +24,7 @@ const configuredSanitize = (html: string) =>
input: ['type', 'id', 'checked'],
code: ['class'],
a: sanitize.defaults.allowedAttributes.a.concat(['data-*']),
div: ['class'],
},
});

View file

@ -44,5 +44,7 @@ export default {
'assistantChat.placeholder.3': 'button in the UI.',
'assistantChat.placeholder.4': 'How can I help?',
'assistantChat.inputPlaceholder': 'Enter your response...',
'assistantChat.copy': 'Copy',
'assistantChat.copied': 'Copied',
'inlineAskAssistantButton.asked': 'Asked',
} as N8nLocale;