mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -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(),
|
||||
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 = {
|
||||
|
|
|
@ -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({
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -24,6 +24,7 @@ const configuredSanitize = (html: string) =>
|
|||
input: ['type', 'id', 'checked'],
|
||||
code: ['class'],
|
||||
a: sanitize.defaults.allowedAttributes.a.concat(['data-*']),
|
||||
div: ['class'],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue