mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Fix styling and typography in AI Assistant chat (#10895)
This commit is contained in:
parent
67fb6d6fdd
commit
57ff3cc27b
|
@ -37,7 +37,7 @@ export class AIAssistant extends BasePage {
|
||||||
cy.getByTestId('node-error-view-ask-assistant-button').find('button').first(),
|
cy.getByTestId('node-error-view-ask-assistant-button').find('button').first(),
|
||||||
credentialEditAssistantButton: () =>
|
credentialEditAssistantButton: () =>
|
||||||
cy.getByTestId('credentail-edit-ask-assistant-button').find('button').first(),
|
cy.getByTestId('credentail-edit-ask-assistant-button').find('button').first(),
|
||||||
codeSnippet: () => cy.getByTestId('assistant-code-snippet'),
|
codeSnippet: () => cy.getByTestId('assistant-code-snippet-content'),
|
||||||
};
|
};
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
|
@ -18,12 +18,21 @@ const { t } = useI18n();
|
||||||
|
|
||||||
const md = new Markdown({
|
const md = new Markdown({
|
||||||
breaks: true,
|
breaks: true,
|
||||||
}).use(markdownLink, {
|
});
|
||||||
|
md.use(markdownLink, {
|
||||||
attrs: {
|
attrs: {
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
rel: 'noopener',
|
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;
|
const MAX_CHAT_INPUT_HEIGHT = 100;
|
||||||
|
|
||||||
|
@ -65,6 +74,10 @@ const showPlaceholder = computed(() => {
|
||||||
return !props.messages?.length && !props.loadingMessage && !props.sessionId;
|
return !props.messages?.length && !props.loadingMessage && !props.sessionId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isClipboardSupported = computed(() => {
|
||||||
|
return navigator.clipboard?.writeText;
|
||||||
|
});
|
||||||
|
|
||||||
function isEndOfSessionEvent(event?: ChatUI.AssistantMessage) {
|
function isEndOfSessionEvent(event?: ChatUI.AssistantMessage) {
|
||||||
return event?.type === 'event' && event?.eventName === 'end-session';
|
return event?.type === 'event' && event?.eventName === 'end-session';
|
||||||
}
|
}
|
||||||
|
@ -97,6 +110,15 @@ function growInput() {
|
||||||
const scrollHeight = chatInput.value.scrollHeight;
|
const scrollHeight = chatInput.value.scrollHeight;
|
||||||
chatInput.value.style.height = `${Math.min(scrollHeight, MAX_CHAT_INPUT_HEIGHT)}px`;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -151,7 +173,10 @@ function growInput() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.blockBody">
|
<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
|
<BlinkingCursor
|
||||||
v-if="streaming && i === messages?.length - 1 && message.title && message.content"
|
v-if="streaming && i === messages?.length - 1 && message.title && message.content"
|
||||||
/>
|
/>
|
||||||
|
@ -162,18 +187,35 @@ function growInput() {
|
||||||
<span
|
<span
|
||||||
v-if="message.role === 'user'"
|
v-if="message.role === 'user'"
|
||||||
v-n8n-html="renderMarkdown(message.content)"
|
v-n8n-html="renderMarkdown(message.content)"
|
||||||
|
:class="$style['rendered-content']"
|
||||||
></span>
|
></span>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
v-n8n-html="renderMarkdown(message.content)"
|
v-n8n-html="renderMarkdown(message.content)"
|
||||||
:class="$style.assistantText"
|
:class="[$style.assistantText, $style['rendered-content']]"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
v-if="message?.codeSnippet"
|
v-if="message?.codeSnippet"
|
||||||
:class="$style['code-snippet']"
|
:class="$style['code-snippet']"
|
||||||
data-test-id="assistant-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
|
<BlinkingCursor
|
||||||
v-if="streaming && i === messages?.length - 1 && message.role === 'assistant'"
|
v-if="streaming && i === messages?.length - 1 && message.role === 'assistant'"
|
||||||
/>
|
/>
|
||||||
|
@ -415,14 +457,37 @@ p {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code[class^='language-'] {
|
||||||
|
display: block;
|
||||||
|
padding: var(--spacing-4xs);
|
||||||
|
}
|
||||||
|
|
||||||
.code-snippet {
|
.code-snippet {
|
||||||
|
position: relative;
|
||||||
border: var(--border-base);
|
border: var(--border-base);
|
||||||
background-color: var(--color-foreground-xlight);
|
background-color: var(--color-foreground-xlight);
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
padding: var(--spacing-2xs);
|
|
||||||
font-family: var(--font-family-monospace);
|
font-family: var(--font-family-monospace);
|
||||||
|
font-size: var(--font-size-3xs);
|
||||||
max-height: 218px; // 12 lines
|
max-height: 218px; // 12 lines
|
||||||
overflow: auto;
|
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 {
|
pre {
|
||||||
white-space-collapse: collapse;
|
white-space-collapse: collapse;
|
||||||
|
@ -491,17 +556,60 @@ p {
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistantText {
|
.assistantText {
|
||||||
display: inline;
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-content {
|
||||||
p {
|
p {
|
||||||
display: inline;
|
margin: 0;
|
||||||
line-height: 1.7;
|
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,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
list-style-position: inside;
|
margin: var(--spacing-4xs) 0 var(--spacing-4xs) var(--spacing-l);
|
||||||
margin: var(--spacing-xs) 0 var(--spacing-xs) var(--spacing-xs);
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -130,7 +130,7 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
|
||||||
class="textMessage"
|
class="textMessage"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="assistantText"
|
class="assistantText rendered-content"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Hi Max! Here is my top solution to fix the error in your
|
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
|
<div
|
||||||
class="textMessage"
|
class="textMessage"
|
||||||
>
|
>
|
||||||
<span>
|
<span
|
||||||
|
class="rendered-content"
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
Give it to me
|
Give it to me
|
||||||
<strong>
|
<strong>
|
||||||
|
@ -511,7 +513,9 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
|
||||||
<div
|
<div
|
||||||
class="blockBody"
|
class="blockBody"
|
||||||
>
|
>
|
||||||
<span>
|
<span
|
||||||
|
class="rendered-content"
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
Solution steps:
|
Solution steps:
|
||||||
</p>
|
</p>
|
||||||
|
@ -1055,7 +1059,7 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
|
||||||
class="textMessage"
|
class="textMessage"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="assistantText"
|
class="assistantText rendered-content"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Hi Max! Here is my top solution to fix the error in your
|
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"
|
class="textMessage"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="assistantText"
|
class="assistantText rendered-content"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Hi Max! Here is my top solution to fix the error in your
|
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"
|
class="code-snippet"
|
||||||
data-test-id="assistant-code-snippet"
|
data-test-id="assistant-code-snippet"
|
||||||
>
|
>
|
||||||
<p>
|
<!--v-if-->
|
||||||
node.on('input', function(msg) {
|
<div
|
||||||
<br />
|
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; }
|
if (msg.seed) { dummyjson.seed = msg.seed; }
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
try {
|
try {
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
var value = dummyjson.parse(node.template, {mockdata: msg});
|
var value = dummyjson.parse(node.template, {mockdata: msg});
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
if (node.syntax === 'json') {
|
if (node.syntax === 'json') {
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
try { value = JSON.parse(value); }
|
try { value = JSON.parse(value); }
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
catch(e) { node.error(RED._('datagen.errors.json-error')); }
|
catch(e) { node.error(RED._('datagen.errors.json-error')); }
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
}
|
}
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
if (node.fieldType === 'msg') {
|
if (node.fieldType === 'msg') {
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
RED.util.setMessageProperty(msg,node.field,value);
|
RED.util.setMessageProperty(msg,node.field,value);
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
}
|
}
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
else if (node.fieldType === 'flow') {
|
else if (node.fieldType === 'flow') {
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
node.context().flow.set(node.field,value);
|
node.context().flow.set(node.field,value);
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
}
|
}
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
else if (node.fieldType === 'global') {
|
else if (node.fieldType === 'global') {
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
node.context().global.set(node.field,value);
|
node.context().global.set(node.field,value);
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
}
|
}
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
node.send(msg);
|
node.send(msg);
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
}
|
}
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
catch(e) {
|
catch(e) {
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
|
@ -1538,7 +1548,7 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
|
||||||
class="textMessage"
|
class="textMessage"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="assistantText"
|
class="assistantText rendered-content"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Hi Max! Here is my top solution to fix the error in your
|
Hi Max! Here is my top solution to fix the error in your
|
||||||
|
|
|
@ -24,6 +24,7 @@ const configuredSanitize = (html: string) =>
|
||||||
input: ['type', 'id', 'checked'],
|
input: ['type', 'id', 'checked'],
|
||||||
code: ['class'],
|
code: ['class'],
|
||||||
a: sanitize.defaults.allowedAttributes.a.concat(['data-*']),
|
a: sanitize.defaults.allowedAttributes.a.concat(['data-*']),
|
||||||
|
div: ['class'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -44,5 +44,7 @@ export default {
|
||||||
'assistantChat.placeholder.3': 'button in the UI.',
|
'assistantChat.placeholder.3': 'button in the UI.',
|
||||||
'assistantChat.placeholder.4': 'How can I help?',
|
'assistantChat.placeholder.4': 'How can I help?',
|
||||||
'assistantChat.inputPlaceholder': 'Enter your response...',
|
'assistantChat.inputPlaceholder': 'Enter your response...',
|
||||||
|
'assistantChat.copy': 'Copy',
|
||||||
|
'assistantChat.copied': 'Copied',
|
||||||
'inlineAskAssistantButton.asked': 'Asked',
|
'inlineAskAssistantButton.asked': 'Asked',
|
||||||
} as N8nLocale;
|
} as N8nLocale;
|
||||||
|
|
Loading…
Reference in a new issue