feat: AI workflow builder front-end (no-changelog) (#14820)

Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
This commit is contained in:
oleg
2025-04-28 15:38:32 +02:00
committed by GitHub
parent dbffcdc2ff
commit 97055d5714
56 changed files with 3857 additions and 1067 deletions

View File

@@ -1 +1,26 @@
@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap');
/* Storybook-specific font paths */
@font-face {
font-family: InterVariable;
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('../src/css/fonts/InterVariable.woff2') format('woff2');
}
@font-face {
font-family: InterVariable;
font-style: italic;
font-weight: 100 900;
font-display: swap;
src: url('../src/css/fonts/InterVariable-Italic.woff2') format('woff2');
}
@font-face {
font-family: CommitMono;
font-style: italic;
font-weight: 100 900;
font-display: swap;
src: url('../src/css/fonts/CommitMonoVariable.woff2') format('woff2');
}
@import '../src/css/_tokens.scss';

View File

@@ -10,7 +10,7 @@ const { t } = useI18n();
const hovering = ref(false);
const props = defineProps<{ unreadCount?: number }>();
const props = defineProps<{ unreadCount?: number; type?: 'assistant' | 'builder' }>();
const emit = defineEmits<{
click: [e: MouseEvent];
@@ -40,7 +40,13 @@ function onMouseLeave() {
<AssistantIcon v-else size="large" :theme="hovering ? 'blank' : 'default'" />
<div v-show="hovering" :class="$style.text">
<div>
<AssistantText :text="t('askAssistantButton.askAssistant')" />
<AssistantText
:text="
type === 'builder'
? t('assistantChat.builder.name')
: t('askAssistantButton.askAssistant')
"
/>
</div>
<div>
<BetaTag />

View File

@@ -25,6 +25,24 @@ const Template: StoryFn = (args, { argTypes }) => ({
methods,
});
const TemplateWithInputPlaceholder: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes),
components: {
AskAssistantChat,
},
template: `
<div style="width:275px; height:500px">
<ask-assistant-chat v-bind="args" >
<template #inputPlaceholder>
<button>Click me</button>
</template>
</ask-assistant-chat>
</div>
`,
methods,
});
export const DefaultPlaceholderChat = Template.bind({});
DefaultPlaceholderChat.args = {
user: {
@@ -33,6 +51,14 @@ DefaultPlaceholderChat.args = {
},
};
export const InputPlaceholderChat = TemplateWithInputPlaceholder.bind({});
DefaultPlaceholderChat.args = {
user: {
firstName: 'Max',
lastName: 'Test',
},
};
export const Chat = Template.bind({});
Chat.args = {
user: {
@@ -78,7 +104,7 @@ Chat.args = {
id: '2',
type: 'block',
role: 'assistant',
title: 'Credential doesnt have correct permissions to send a message',
title: "Credential doesn't have correct permissions to send a message",
content:
'Solution steps:\n1. Lorem ipsum dolor sit amet, consectetur **adipiscing** elit. Proin id nulla placerat, tristique ex at, euismod dui.\n2. Copy this into somewhere\n3. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin id nulla placerat, tristique ex at, euismod dui.\n4. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin id nulla placerat, tristique ex at, euismod dui. \n Testing more code \n - Unordered item 1 \n - Unordered item 2',
read: false,
@@ -117,7 +143,7 @@ JustSummary.args = {
id: '123',
role: 'assistant',
type: 'block',
title: 'Credential doesnt have correct permissions to send a message',
title: "Credential doesn't have correct permissions to send a message",
content:
'Solution steps:\n1. Lorem ipsum dolor sit amet, consectetur **adipiscing** elit. Proin id nulla placerat, tristique ex at, euismod dui.\n2. Copy this into somewhere\n3. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin id nulla placerat, tristique ex at, euismod dui.\n4. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin id nulla placerat, tristique ex at, euismod dui. \n Testing more code \n - Unordered item 1 \n - Unordered item 2',
read: false,
@@ -136,7 +162,7 @@ SummaryTitleStreaming.args = {
id: '123',
role: 'assistant',
type: 'block',
title: 'Credential doesnt have',
title: "Credential doesn't have",
content: '',
read: false,
},
@@ -155,7 +181,7 @@ SummaryContentStreaming.args = {
id: '123',
role: 'assistant',
type: 'block',
title: 'Credential doesnt have correct permissions to send a message',
title: "Credential doesn't have correct permissions to send a message",
content: 'Solution steps:\n1. Lorem ipsum dolor sit amet, consectetur',
read: false,
},
@@ -372,3 +398,94 @@ RichTextMessage.args = {
},
]),
};
export const WorkflowStepsChat = Template.bind({});
WorkflowStepsChat.args = {
user: {
firstName: 'Max',
lastName: 'Test',
},
messages: getMessages([
{
id: '123',
type: 'workflow-step',
role: 'assistant',
steps: [
'Create a new HTTP Trigger node',
'Add a Transform node to process the data',
'Connect to your database using PostgreSQL node',
'Send confirmation email with SendGrid node',
],
read: false,
},
]),
};
export const WorkflowNodesChat = Template.bind({});
WorkflowNodesChat.args = {
user: {
firstName: 'Max',
lastName: 'Test',
},
messages: getMessages([
{
id: '124',
type: 'workflow-node',
role: 'assistant',
nodes: ['HTTP Trigger', 'Transform', 'PostgreSQL', 'SendGrid'],
read: false,
},
]),
};
export const ComposedNodesChat = Template.bind({});
ComposedNodesChat.args = {
user: {
firstName: 'Max',
lastName: 'Test',
},
messages: getMessages([
{
id: '125',
type: 'workflow-composed',
role: 'assistant',
nodes: [
{
name: 'HTTP Trigger',
type: 'n8n-nodes-base.httpTrigger',
parameters: {
path: '/webhook',
authentication: 'none',
},
position: [100, 100],
},
{
name: 'Transform',
type: 'n8n-nodes-base.set',
parameters: {
values: { field: 'value' },
},
position: [300, 100],
},
],
read: false,
},
]),
};
export const RateWorkflowMessage = Template.bind({});
RateWorkflowMessage.args = {
user: {
firstName: 'Max',
lastName: 'Test',
},
messages: getMessages([
{
id: '126',
type: 'rate-workflow',
role: 'assistant',
content: 'Is this workflow helpful?',
read: false,
},
]),
};

View File

@@ -1,39 +1,26 @@
<script setup lang="ts">
import Markdown from 'markdown-it';
import markdownLink from 'markdown-it-link-attributes';
import { computed, ref } from 'vue';
import BlockMessage from './messages/BlockMessage.vue';
import CodeDiffMessage from './messages/CodeDiffMessage.vue';
import ErrorMessage from './messages/ErrorMessage.vue';
import EventMessage from './messages/EventMessage.vue';
import TextMessage from './messages/TextMessage.vue';
import ComposedNodesMessage from './messages/workflow/ComposedNodesMessage.vue';
import RateWorkflowMessage from './messages/workflow/RateWorkflowMessage.vue';
import WorkflowGeneratedMessage from './messages/workflow/WorkflowGeneratedMessage.vue';
import WorkflowNodesMessage from './messages/workflow/WorkflowNodesMessage.vue';
import WorkflowStepsMessage from './messages/workflow/WorkflowStepsMessage.vue';
import { useI18n } from '../../composables/useI18n';
import type { ChatUI } from '../../types/assistant';
import AssistantAvatar from '../AskAssistantAvatar/AssistantAvatar.vue';
import AssistantIcon from '../AskAssistantIcon/AssistantIcon.vue';
import AssistantLoadingMessage from '../AskAssistantLoadingMessage/AssistantLoadingMessage.vue';
import AssistantText from '../AskAssistantText/AssistantText.vue';
import BetaTag from '../BetaTag/BetaTag.vue';
import BlinkingCursor from '../BlinkingCursor/BlinkingCursor.vue';
import CodeDiff from '../CodeDiff/CodeDiff.vue';
import InlineAskAssistantButton from '../InlineAskAssistantButton/InlineAskAssistantButton.vue';
const { t } = useI18n();
const md = new Markdown({
breaks: true,
});
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;
interface Props {
@@ -45,6 +32,8 @@ interface Props {
streaming?: boolean;
loadingMessage?: string;
sessionId?: string;
title?: string;
placeholder?: string;
}
const emit = defineEmits<{
@@ -52,11 +41,23 @@ const emit = defineEmits<{
message: [string, string?, boolean?];
codeReplace: [number];
codeUndo: [number];
thumbsUp: [];
thumbsDown: [];
submitFeedback: [string];
}>();
const onClose = () => emit('close');
const props = defineProps<Props>();
const props = withDefaults(defineProps<Props>(), {
title: () => useI18n().t('assistantChat.aiAssistantLabel'),
user: () => ({
firstName: '',
lastName: '',
}),
messages: () => [],
loadingMessage: undefined,
sessionId: undefined,
});
const textInputValue = ref<string>('');
@@ -74,10 +75,6 @@ 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';
}
@@ -95,15 +92,6 @@ function onSendMessage() {
}
}
function renderMarkdown(content: string) {
try {
return md.render(content);
} catch (e) {
console.error(`Error parsing markdown content ${content}`);
return `<p>${t('assistantChat.errorParsingMarkdown')}</p>`;
}
}
function growInput() {
if (!chatInput.value) return;
chatInput.value.style.height = 'auto';
@@ -111,13 +99,16 @@ function growInput() {
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);
function onThumbsUp() {
emit('thumbsUp');
}
function onThumbsDown() {
emit('thumbsDown');
}
function onSubmitFeedback(feedback: string) {
emit('submitFeedback', feedback);
}
</script>
@@ -127,9 +118,10 @@ async function onCopyButtonClick(content: string, e: MouseEvent) {
<div :class="$style.chatTitle">
<div :class="$style.headerText">
<AssistantIcon size="large" />
<AssistantText size="large" :text="t('assistantChat.aiAssistantLabel')" />
<AssistantText size="large" :text="title" />
</div>
<BetaTag />
<slot name="header" />
</div>
<div :class="$style.back" data-test-id="close-chat-button" @click="onClose">
<n8n-icon icon="arrow-right" color="text-base" />
@@ -138,140 +130,91 @@ async function onCopyButtonClick(content: string, e: MouseEvent) {
<div :class="$style.body">
<div v-if="messages?.length || loadingMessage" :class="$style.messages">
<div v-if="messages?.length">
<div
<data
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="
!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
"
/>
</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-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>
<div
<TextMessage
v-if="message.type === 'text'"
:message="message"
:is-first-of-role="i === 0 || message.role !== messages[i - 1].role"
:user="user"
:streaming="streaming"
:is-last-message="i === messages.length - 1"
/>
<BlockMessage
v-else-if="message.type === 'block'"
:message="message"
:is-first-of-role="i === 0 || message.role !== messages[i - 1].role"
:user="user"
:streaming="streaming"
:is-last-message="i === messages.length - 1"
/>
<ErrorMessage
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?.()"
>
{{ 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>
:message="message"
:is-first-of-role="i === 0 || message.role !== messages[i - 1].role"
:user="user"
/>
<EventMessage
v-else-if="message.type === 'event'"
:message="message"
:is-first-of-role="i === 0 || message.role !== messages[i - 1].role"
:user="user"
/>
<CodeDiffMessage
v-else-if="message.type === 'code-diff'"
:message="message"
:is-first-of-role="i === 0 || message.role !== messages[i - 1].role"
:user="user"
:streaming="streaming"
:is-last-message="i === messages.length - 1"
@code-replace="() => emit('codeReplace', i)"
@code-undo="() => emit('codeUndo', i)"
/>
<WorkflowStepsMessage
v-else-if="message.type === 'workflow-step'"
:message="message"
:is-first-of-role="i === 0 || message.role !== messages[i - 1].role"
:user="user"
/>
<WorkflowNodesMessage
v-else-if="message.type === 'workflow-node'"
:message="message"
:is-first-of-role="i === 0 || message.role !== messages[i - 1].role"
:user="user"
/>
<ComposedNodesMessage
v-else-if="message.type === 'workflow-composed'"
:message="message"
:is-first-of-role="i === 0 || message.role !== messages[i - 1].role"
:user="user"
/>
<WorkflowGeneratedMessage
v-else-if="message.type === 'workflow-generated'"
:message="message"
:is-first-of-role="i === 0 || message.role !== messages[i - 1].role"
:user="user"
/>
<RateWorkflowMessage
v-else-if="message.type === 'rate-workflow'"
:message="message"
:is-first-of-role="i === 0 || message.role !== messages[i - 1].role"
:user="user"
@thumbs-up="onThumbsUp"
@thumbs-down="onThumbsDown"
@submit-feedback="onSubmitFeedback"
/>
<div
v-if="
!streaming &&
'quickReplies' in message &&
message.quickReplies?.length &&
i === messages?.length - 1
i === messages.length - 1
"
:class="$style.quickReplies"
>
@@ -289,7 +232,7 @@ async function onCopyButtonClick(content: string, e: MouseEvent) {
</n8n-button>
</div>
</div>
</div>
</data>
</div>
<div
v-if="loadingMessage"
@@ -303,48 +246,59 @@ async function onCopyButtonClick(content: string, e: MouseEvent) {
:class="$style.placeholder"
data-test-id="placeholder-message"
>
<div :class="$style.greeting">Hi {{ user?.firstName }} 👋</div>
<div :class="$style.info">
<p>
{{ t('assistantChat.placeholder.1') }}
</p>
<p>
{{ t('assistantChat.placeholder.2') }}
<InlineAskAssistantButton size="small" :static="true" />
{{ t('assistantChat.placeholder.3') }}
</p>
<p>
{{ t('assistantChat.placeholder.4') }}
</p>
<div v-if="$slots.placeholder" :class="$style.info">
<slot name="placeholder" />
</div>
<template v-else>
<div :class="$style.greeting">Hi {{ user?.firstName }} 👋</div>
<div :class="$style.info">
<p>
{{ t('assistantChat.placeholder.1') }}
</p>
<p>
{{ t('assistantChat.placeholder.2') }}
<InlineAskAssistantButton size="small" :static="true" />
{{ t('assistantChat.placeholder.3') }}
</p>
<p>
{{ t('assistantChat.placeholder.4') }}
</p>
</div>
</template>
</div>
</div>
<div
:class="{ [$style.inputWrapper]: true, [$style.disabledInput]: sessionEnded }"
data-test-id="chat-input-wrapper"
>
<textarea
ref="chatInput"
v-model="textInputValue"
class="ignore-key-press-node-creator ignore-key-press-canvas"
:disabled="sessionEnded"
:placeholder="t('assistantChat.inputPlaceholder')"
rows="1"
wrap="hard"
data-test-id="chat-input"
@keydown.enter.exact.prevent="onSendMessage"
@input.prevent="growInput"
@keydown.stop
/>
<n8n-icon-button
:class="{ [$style.sendButton]: true }"
icon="paper-plane"
type="text"
size="large"
data-test-id="send-message-button"
:disabled="sendDisabled"
@click="onSendMessage"
/>
<div v-if="$slots.inputPlaceholder" :class="$style.inputPlaceholder">
<slot name="inputPlaceholder" />
</div>
<template v-else>
<textarea
ref="chatInput"
v-model="textInputValue"
class="ignore-key-press-node-creator ignore-key-press-canvas"
:class="{ [$style.disabled]: sessionEnded || streaming }"
:disabled="sessionEnded || streaming"
:placeholder="placeholder ?? t('assistantChat.inputPlaceholder')"
rows="1"
wrap="hard"
data-test-id="chat-input"
@keydown.enter.exact.prevent="onSendMessage"
@input.prevent="growInput"
@keydown.stop
/>
<n8n-icon-button
:class="{ [$style.sendButton]: true }"
icon="paper-plane"
type="text"
size="large"
data-test-id="send-message-button"
:disabled="sendDisabled"
@click="onSendMessage"
/>
</template>
</div>
</div>
</template>
@@ -357,11 +311,6 @@ async function onCopyButtonClick(content: string, e: MouseEvent) {
grid-template-rows: auto 1fr auto;
}
p {
line-height: var(--font-line-height-xloose);
margin: var(--spacing-2xs) 0;
}
.header {
height: 65px; // same as header height in editor
padding: 0 var(--spacing-l);
@@ -420,23 +369,6 @@ p {
}
}
.roleName {
display: flex;
align-items: center;
margin-bottom: var(--spacing-3xs);
font-weight: var(--font-weight-bold);
font-size: var(--font-size-2xs);
> * {
margin-right: var(--spacing-3xs);
}
}
.userSection {
margin-top: var(--spacing-l);
}
.chatTitle {
display: flex;
gap: var(--spacing-3xs);
@@ -477,78 +409,6 @@ p {
color: var(--color-text-base);
}
.textMessage {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
font-size: var(--font-size-2xs);
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);
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;
}
code {
background-color: transparent;
font-size: var(--font-size-3xs);
}
}
.block {
font-size: var(--font-size-2xs);
background-color: var(--color-foreground-xlight);
border: var(--border-base);
border-radius: var(--border-radius-base);
word-break: break-word;
li {
margin-left: var(--spacing-xs);
}
}
.blockTitle {
border-bottom: var(--border-base);
padding: var(--spacing-2xs);
font-weight: var(--font-weight-bold);
}
.blockBody {
padding: var(--spacing-xs);
}
.inputWrapper {
display: flex;
background-color: var(--color-foreground-xlight);
@@ -577,91 +437,6 @@ code[class^='language-'] {
}
}
.error {
color: var(--color-danger);
display: flex;
flex-direction: column;
align-items: start;
}
.retryButton {
margin-top: var(--spacing-3xs);
}
.assistantText {
display: inline-flex;
flex-direction: column;
}
.rendered-content {
p {
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 {
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);
}
}
}
.endOfSessionText {
margin-top: var(--spacing-l);
padding-top: var(--spacing-3xs);
border-top: var(--border-base);
color: var(--color-text-base);
> button,
> span {
margin-right: var(--spacing-3xs);
}
button {
display: inline-flex;
}
}
.disabledInput {
cursor: not-allowed;
@@ -669,4 +444,13 @@ code[class^='language-'] {
cursor: not-allowed;
}
}
textarea.disabled {
background-color: var(--color-foreground-base);
cursor: not-allowed;
}
.inputPlaceholder {
width: 100%;
}
</style>

View File

@@ -0,0 +1,65 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from '../../../composables/useI18n';
import type { ChatUI } from '../../../types/assistant';
import AssistantAvatar from '../../AskAssistantAvatar/AssistantAvatar.vue';
interface Props {
message: ChatUI.AssistantMessage;
isFirstOfRole: boolean;
user?: {
firstName: string;
lastName: string;
};
}
const props = defineProps<Props>();
const { t } = useI18n();
const isUserMessage = computed(() => props.message.role === 'user');
</script>
<template>
<div :class="$style.message">
<div
v-if="isFirstOfRole"
:class="{ [$style.roleName]: true, [$style.userSection]: !isUserMessage }"
>
<template v-if="isUserMessage">
<n8n-avatar :first-name="user?.firstName" :last-name="user?.lastName" size="xsmall" />
<span>{{ t('assistantChat.you') }}</span>
</template>
<template v-else>
<AssistantAvatar />
<span>{{ t('assistantChat.aiAssistantName') }}</span>
</template>
</div>
<slot></slot>
</div>
</template>
<style lang="scss" module>
.message {
margin-bottom: var(--spacing-xs);
font-size: var(--font-size-2xs);
line-height: var(--font-line-height-xloose);
}
.roleName {
display: flex;
align-items: center;
margin-bottom: var(--spacing-3xs);
height: var(--spacing-xl);
font-weight: var(--font-weight-bold);
font-size: var(--font-size-2xs);
> * {
margin-right: var(--spacing-3xs);
}
}
.userSection {
margin-top: var(--spacing-m);
}
</style>

View File

@@ -0,0 +1,115 @@
<script setup lang="ts">
import BaseMessage from './BaseMessage.vue';
import { useMarkdown } from './useMarkdown';
import type { ChatUI } from '../../../types/assistant';
import BlinkingCursor from '../../BlinkingCursor/BlinkingCursor.vue';
interface Props {
message: ChatUI.SummaryBlock & { id: string; read: boolean; quickReplies?: ChatUI.QuickReply[] };
isFirstOfRole: boolean;
user?: {
firstName: string;
lastName: string;
};
streaming?: boolean;
isLastMessage?: boolean;
}
defineProps<Props>();
const { renderMarkdown } = useMarkdown();
</script>
<template>
<BaseMessage :message="message" :is-first-of-role="isFirstOfRole" :user="user">
<div :class="$style.block">
<div :class="$style.blockTitle">
{{ message.title }}
<BlinkingCursor v-if="streaming && isLastMessage && !message.content" />
</div>
<div :class="$style.blockBody">
<span
v-n8n-html="renderMarkdown(message.content)"
:class="$style['rendered-content']"
></span>
<BlinkingCursor v-if="streaming && isLastMessage && message.title && message.content" />
</div>
</div>
</BaseMessage>
</template>
<style lang="scss" module>
.block {
font-size: var(--font-size-2xs);
background-color: var(--color-foreground-xlight);
border: var(--border-base);
border-radius: var(--border-radius-base);
word-break: break-word;
li {
margin-left: var(--spacing-xs);
}
}
.blockTitle {
border-bottom: var(--border-base);
padding: var(--spacing-2xs);
font-weight: var(--font-weight-bold);
}
.blockBody {
padding: var(--spacing-xs);
}
.rendered-content {
p {
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 {
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);
}
}
}
</style>

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import BaseMessage from './BaseMessage.vue';
import type { ChatUI } from '../../../types/assistant';
import CodeDiff from '../../CodeDiff/CodeDiff.vue';
interface Props {
message: {
role: 'assistant';
type: 'code-diff';
description?: string;
codeDiff?: string;
replacing?: boolean;
replaced?: boolean;
error?: boolean;
suggestionId: string;
id: string;
read: boolean;
quickReplies?: ChatUI.QuickReply[];
};
isFirstOfRole: boolean;
user?: {
firstName: string;
lastName: string;
};
streaming?: boolean;
isLastMessage?: boolean;
}
defineProps<Props>();
const emit = defineEmits<{
codeReplace: [];
codeUndo: [];
}>();
</script>
<template>
<BaseMessage :message="message" :is-first-of-role="isFirstOfRole" :user="user">
<CodeDiff
:title="message.description"
:content="message.codeDiff"
:replacing="message.replacing"
:replaced="message.replaced"
:error="message.error"
:streaming="streaming && isLastMessage"
@replace="emit('codeReplace')"
@undo="emit('codeUndo')"
/>
</BaseMessage>
</template>

View File

@@ -0,0 +1,66 @@
<script setup lang="ts">
import BaseMessage from './BaseMessage.vue';
import { useI18n } from '../../../composables/useI18n';
import type { ChatUI } from '../../../types/assistant';
interface Props {
message: ChatUI.ErrorMessage & { id: string; read: boolean };
isFirstOfRole: boolean;
user?: {
firstName: string;
lastName: string;
};
}
defineProps<Props>();
const { t } = useI18n();
</script>
<template>
<BaseMessage :message="message" :is-first-of-role="isFirstOfRole" :user="user">
<div :class="$style.error" data-test-id="chat-message-system">
<p :class="$style.errorText">
<n8n-icon icon="exclamation-triangle" size="small" :class="$style.errorIcon" />
{{ message.content }}
</p>
<n8n-button
v-if="message.retry"
type="secondary"
size="mini"
:class="$style.retryButton"
data-test-id="error-retry-button"
@click="() => message.retry?.()"
>
{{ t('generic.retry') }}
</n8n-button>
</div>
</BaseMessage>
</template>
<style lang="scss" module>
.error {
display: flex;
align-items: center;
gap: var(--spacing-2xs);
padding: var(--spacing-2xs) var(--spacing-xs);
border: 1px solid var(--color-foreground-base);
border-radius: var(--border-radius-base);
background-color: var(--color-background-xlight);
}
.errorIcon {
margin-right: var(--spacing-5xs);
color: var(--color-danger);
}
.errorText {
color: var(--color-danger);
font-weight: var(--font-weight-regular);
line-height: var(--font-line-height-tight);
word-break: break-word;
}
.retryButton {
margin-top: var(--spacing-3xs);
}
</style>

View File

@@ -0,0 +1,72 @@
<script setup lang="ts">
import BaseMessage from './BaseMessage.vue';
import { useI18n } from '../../../composables/useI18n';
import InlineAskAssistantButton from '../../InlineAskAssistantButton/InlineAskAssistantButton.vue';
type EventName = 'end-session' | 'session-timeout' | 'session-error';
interface Props {
message: {
role: 'assistant';
type: 'event';
eventName: EventName;
id: string;
read: boolean;
};
isFirstOfRole: boolean;
user?: {
firstName: string;
lastName: string;
};
}
defineProps<Props>();
const { t } = useI18n();
const eventMessages: Record<EventName, { part1: string; part2: string }> = {
'end-session': {
part1: 'assistantChat.sessionEndMessage.1',
part2: 'assistantChat.sessionEndMessage.2',
},
'session-timeout': {
part1: 'assistantChat.sessionTimeoutMessage.1',
part2: 'assistantChat.sessionTimeoutMessage.2',
},
'session-error': {
part1: 'assistantChat.sessionErrorMessage.1',
part2: 'assistantChat.sessionErrorMessage.2',
},
} as const;
</script>
<template>
<BaseMessage :message="message" :is-first-of-role="isFirstOfRole" :user="user">
<div :class="$style.eventText" data-test-id="chat-message-system">
<span>
{{ t(eventMessages[message.eventName]?.part1 || 'assistantChat.unknownEvent') }}
</span>
<InlineAskAssistantButton size="small" :static="true" />
<span>
{{ t(eventMessages[message.eventName]?.part2 || '') }}
</span>
</div>
</BaseMessage>
</template>
<style lang="scss" module>
.eventText {
margin-top: var(--spacing-l);
padding-top: var(--spacing-3xs);
border-top: var(--border-base);
color: var(--color-text-base);
> button,
> span {
margin-right: var(--spacing-3xs);
}
button {
display: inline-flex;
}
}
</style>

View File

@@ -0,0 +1,182 @@
<script setup lang="ts">
import { computed } from 'vue';
import BaseMessage from './BaseMessage.vue';
import { useMarkdown } from './useMarkdown';
import { useI18n } from '../../../composables/useI18n';
import type { ChatUI } from '../../../types/assistant';
import BlinkingCursor from '../../BlinkingCursor/BlinkingCursor.vue';
interface Props {
message: ChatUI.TextMessage & { id: string; read: boolean; quickReplies?: ChatUI.QuickReply[] };
isFirstOfRole: boolean;
user?: {
firstName: string;
lastName: string;
};
streaming?: boolean;
isLastMessage?: boolean;
}
defineProps<Props>();
const { renderMarkdown } = useMarkdown();
const { t } = useI18n();
const isClipboardSupported = computed(() => {
return navigator.clipboard?.writeText;
});
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>
<BaseMessage :message="message" :is-first-of-role="isFirstOfRole" :user="user">
<div :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-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 && isLastMessage && message.role === 'assistant'" />
</div>
</BaseMessage>
</template>
<style lang="scss" module>
.textMessage {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
font-size: var(--font-size-2xs);
word-break: break-word;
}
.code-snippet {
position: relative;
border: var(--border-base);
background-color: var(--color-foreground-xlight);
border-radius: var(--border-radius-base);
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;
}
code {
background-color: transparent;
font-size: var(--font-size-3xs);
}
}
.assistantText {
display: inline-flex;
flex-direction: column;
}
.rendered-content {
p {
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 {
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);
}
}
}
</style>

View File

@@ -0,0 +1,32 @@
import Markdown from 'markdown-it';
import markdownLink from 'markdown-it-link-attributes';
import { useI18n } from '../../../composables/useI18n';
export function useMarkdown() {
const { t } = useI18n();
const md = new Markdown({
breaks: true,
});
md.use(markdownLink, {
attrs: {
target: '_blank',
rel: 'noopener',
},
});
function renderMarkdown(content: string) {
try {
return md.render(content);
} catch (e) {
console.error(`Error parsing markdown content ${content}`);
return `<p>${t('assistantChat.errorParsingMarkdown')}</p>`;
}
}
return {
renderMarkdown,
};
}

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import AssistantLoadingMessage from '@n8n/design-system/components/AskAssistantLoadingMessage/AssistantLoadingMessage.vue';
import type { ChatUI } from '../../../../types/assistant';
import BaseMessage from '../BaseMessage.vue';
interface Props {
message: ChatUI.AssistantMessage;
isFirstOfRole: boolean;
user?: {
firstName: string;
lastName: string;
};
nextStep?: string;
}
defineProps<Props>();
</script>
<template>
<BaseMessage :message="message" :is-first-of-role="isFirstOfRole" :user="user">
<div :class="$style.workflowMessage">
<div :class="$style.message">
<slot name="icon"></slot>
<div :class="$style.content">
<div v-if="$slots.title" :class="$style.title">
<slot name="title"></slot>
</div>
<div :class="$style.details">
<slot></slot>
</div>
</div>
</div>
<AssistantLoadingMessage v-if="nextStep" :message="nextStep" :class="$style.nextStep" />
</div>
</BaseMessage>
</template>
<style lang="scss" module>
.workflowMessage {
display: flex;
flex-direction: column;
}
.message {
display: flex;
gap: var(--spacing-s);
padding: 0 var(--spacing-2xs) var(--spacing-2xs) 0;
background-color: var(--color-background-light);
border-radius: var(--border-radius-base);
}
.nextStep {
flex: 1;
width: 100%;
}
.content {
flex: 1;
}
.title {
font-weight: var(--font-weight-medium);
margin-bottom: var(--spacing-3xs);
}
.details {
color: var(--color-text-base);
font-size: var(--font-size-2xs);
}
</style>

View File

@@ -0,0 +1,54 @@
<script setup lang="ts">
import { useI18n } from '@n8n/design-system/composables/useI18n';
import BaseWorkflowMessage from './BaseWorkflowMessage.vue';
import type { ChatUI } from '../../../../types/assistant';
interface Props {
message: ChatUI.WorkflowComposedMessage & { id: string; read: boolean };
isFirstOfRole: boolean;
user?: {
firstName: string;
lastName: string;
};
}
const { t } = useI18n();
defineProps<Props>();
</script>
<template>
<BaseWorkflowMessage
:message="message"
:is-first-of-role="isFirstOfRole"
:user="user"
:next-step="t('assistantChat.builder.generatingFinalWorkflow')"
>
<template #title>{{ t('assistantChat.builder.configuredNodes') }}</template>
<ol :class="$style.nodesList">
<li v-for="node in message.nodes" :key="node.name" :class="$style.node">
<div :class="$style.nodeName">{{ node.name }}</div>
</li>
</ol>
</BaseWorkflowMessage>
</template>
<style lang="scss" module>
.nodesList {
display: flex;
flex-direction: column;
gap: var(--spacing-3xs);
list-style-position: outside;
margin: 0;
padding: 0 0 0 var(--spacing-s);
li {
color: var(--color-text-base);
line-height: var(--font-line-height-loose);
}
}
.nodeType {
font-size: var(--font-size-3xs);
color: var(--color-text-light);
}
</style>

View File

@@ -0,0 +1,122 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from '@n8n/design-system/composables/useI18n';
import BaseWorkflowMessage from './BaseWorkflowMessage.vue';
import type { ChatUI } from '../../../../types/assistant';
interface Props {
message: ChatUI.RateWorkflowMessage & { id: string; read: boolean };
isFirstOfRole: boolean;
user?: {
firstName: string;
lastName: string;
};
}
const { t } = useI18n();
const emit = defineEmits<{
thumbsUp: [];
thumbsDown: [];
submitFeedback: [string];
}>();
defineProps<Props>();
const feedback = ref('');
const showFeedback = ref(false);
const showSuccess = ref(false);
function onRateButton(rating: 'thumbsUp' | 'thumbsDown') {
showFeedback.value = true;
if (rating === 'thumbsUp') {
emit('thumbsUp');
} else {
emit('thumbsDown');
}
}
function onSubmitFeedback() {
emit('submitFeedback', feedback.value);
showFeedback.value = false;
showSuccess.value = true;
}
</script>
<template>
<BaseWorkflowMessage :message="message" :is-first-of-role="isFirstOfRole" :user="user">
<div :class="$style.content">
<p v-if="!showSuccess">{{ message.content }}</p>
<div v-if="!showFeedback && !showSuccess" :class="$style.buttons">
<n8n-button
type="secondary"
size="small"
:label="t('assistantChat.builder.thumbsUp')"
data-test-id="message-thumbs-up-button"
icon="thumbs-up"
@click="onRateButton('thumbsUp')"
/>
<n8n-button
type="secondary"
size="small"
data-test-id="message-thumbs-down-button"
:label="t('assistantChat.builder.thumbsDown')"
icon="thumbs-down"
@click="onRateButton('thumbsDown')"
/>
</div>
<div v-if="showFeedback" :class="$style.feedbackTextArea">
<n8n-input
v-model="feedback"
:class="$style.feedbackInput"
type="textarea"
:placeholder="t('assistantChat.builder.feedbackPlaceholder')"
data-test-id="message-feedback-input"
:read-only="false"
resize="none"
:rows="5"
/>
<div :class="$style.feedbackTextArea__footer">
<n8n-button
native-type="submit"
type="secondary"
size="small"
data-test-id="message-submit-feedback-button"
@click="onSubmitFeedback"
>
{{ t('assistantChat.builder.submit') }}
</n8n-button>
</div>
</div>
<p v-if="showSuccess" :class="$style.success">{{ t('assistantChat.builder.success') }}</p>
</div>
</BaseWorkflowMessage>
</template>
<style lang="scss" module>
.content {
display: flex;
flex-direction: column;
gap: var(--spacing-2xs);
}
.buttons {
display: flex;
gap: var(--spacing-2xs);
}
.feedbackTextArea {
display: flex;
flex-direction: column;
gap: var(--spacing-2xs);
:global(.el-textarea__inner) {
resize: none;
font-family: var(--font-family);
font-size: var(--font-size-2xs);
}
}
.feedbackTextArea__footer {
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { useI18n } from '@n8n/design-system/composables/useI18n';
import BaseWorkflowMessage from './BaseWorkflowMessage.vue';
import type { ChatUI } from '../../../../types/assistant';
interface Props {
message: ChatUI.WorkflowGeneratedMessage & { id: string; read: boolean };
isFirstOfRole: boolean;
user?: {
firstName: string;
lastName: string;
};
}
defineProps<Props>();
const { t } = useI18n();
</script>
<template>
<BaseWorkflowMessage :message="message" :is-first-of-role="isFirstOfRole" :user="user">
<p>{{ t('assistantChat.builder.workflowGenerated1') }}</p>
<p>{{ t('assistantChat.builder.workflowGenerated2') }}</p>
</BaseWorkflowMessage>
</template>
<style lang="scss" module>
.code {
white-space: pre-wrap;
}
</style>

View File

@@ -0,0 +1,48 @@
<script setup lang="ts">
import { useI18n } from '@n8n/design-system/composables/useI18n';
import BaseWorkflowMessage from './BaseWorkflowMessage.vue';
import type { ChatUI } from '../../../../types/assistant';
interface Props {
message: ChatUI.GeneratedNodesMessage & { id: string; read: boolean };
isFirstOfRole: boolean;
user?: {
firstName: string;
lastName: string;
};
}
const { t } = useI18n();
defineProps<Props>();
</script>
<template>
<BaseWorkflowMessage
:message="message"
:is-first-of-role="isFirstOfRole"
:user="user"
:next-step="t('assistantChat.builder.configuringNodes')"
>
<template #title>{{ t('assistantChat.builder.selectedNodes') }}</template>
<ol :class="$style.nodesList">
<li v-for="node in message.nodes" :key="node">{{ node }}</li>
</ol>
</BaseWorkflowMessage>
</template>
<style lang="scss" module>
.nodesList {
display: flex;
flex-direction: column;
gap: var(--spacing-3xs);
list-style-position: outside;
margin: 0;
padding-left: var(--spacing-s);
li {
color: var(--color-text-base);
line-height: var(--font-line-height-loose);
}
}
</style>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import { useI18n } from '@n8n/design-system/composables/useI18n';
import BaseWorkflowMessage from './BaseWorkflowMessage.vue';
import type { ChatUI } from '../../../../types/assistant';
interface Props {
message: ChatUI.WorkflowStepMessage & { id: string; read: boolean };
isFirstOfRole: boolean;
user?: {
firstName: string;
lastName: string;
};
}
defineProps<Props>();
const { t } = useI18n();
</script>
<template>
<BaseWorkflowMessage
:message="message"
:is-first-of-role="isFirstOfRole"
:user="user"
:next-step="t('assistantChat.builder.selectingNodes')"
>
<template #title>{{ t('assistantChat.builder.generatedNodes') }}</template>
<ol :class="$style.stepsList">
<li v-for="step in message.steps" :key="step">{{ step }}</li>
</ol>
</BaseWorkflowMessage>
</template>
<style lang="scss" module>
.stepsList {
display: flex;
flex-direction: column;
gap: var(--spacing-3xs);
list-style-position: outside;
margin: 0;
padding: 0 0 0 var(--spacing-s);
li {
color: var(--color-text-base);
line-height: var(--font-line-height-loose);
}
}
</style>

View File

@@ -28,12 +28,13 @@ withDefaults(
<style module lang="scss">
.container {
display: flex;
align-items: center;
gap: var(--spacing-3xs);
user-select: none;
}
.avatar {
height: var(--spacing-s);
height: var(--spacing-m);
animation: pulse 1.5s infinite;
position: relative;
}
@@ -43,12 +44,15 @@ withDefaults(
position: relative;
overflow: hidden;
line-height: 1.4rem;
height: var(--spacing-xl);
align-items: center;
}
.message {
margin: 0;
padding: 0;
font-weight: var(--font-weight-bold);
font-size: var(--font-size-2xs);
color: var(--color-text-base);
text-align: left;
}

View File

@@ -31,6 +31,20 @@ export default {
'codeDiff.undo': 'Undo',
'betaTag.beta': 'beta',
'askAssistantButton.askAssistant': 'Ask Assistant',
'assistantChat.builder.name': 'AI Builder',
'assistantChat.builder.generatingFinalWorkflow': 'Generating final workflow...',
'assistantChat.builder.configuredNodes': 'Configured nodes',
'assistantChat.builder.thumbsUp': 'Helpful',
'assistantChat.builder.thumbsDown': 'Not helpful',
'assistantChat.builder.feedbackPlaceholder': 'Tell us about your experience',
'assistantChat.builder.success': 'Thank you for your feedback!',
'assistantChat.builder.submit': 'Submit feedback',
'assistantChat.builder.workflowGenerated1': 'Your workflow was created successfully!',
'assistantChat.builder.workflowGenerated2': 'Fix any missing credentials before testing it.',
'assistantChat.builder.configuringNodes': 'Configuring nodes...',
'assistantChat.builder.selectedNodes': 'Selected workflow nodes',
'assistantChat.builder.selectingNodes': 'Selecting nodes...',
'assistantChat.builder.generatedNodes': 'Generated workflow nodes',
'assistantChat.errorParsingMarkdown': 'Error parsing markdown content',
'assistantChat.aiAssistantLabel': 'AI Assistant',
'assistantChat.aiAssistantName': 'Assistant',

View File

@@ -13,7 +13,7 @@ export namespace ChatUI {
content: string;
}
interface CodeDiffMessage {
export interface CodeDiffMessage {
role: 'assistant';
type: 'code-diff';
description?: string;
@@ -24,12 +24,41 @@ export namespace ChatUI {
suggestionId: string;
}
interface EndSessionMessage {
export interface EndSessionMessage {
role: 'assistant';
type: 'event';
eventName: 'end-session';
}
export interface SessionTimeoutMessage {
role: 'assistant';
type: 'event';
eventName: 'session-timeout';
}
export interface SessionErrorMessage {
role: 'assistant';
type: 'event';
eventName: 'session-error';
}
export interface GeneratedNodesMessage {
role: 'assistant';
type: 'workflow-node';
nodes: string[];
}
export interface ComposedNodesMessage {
role: 'assistant';
type: 'workflow-composed';
nodes: Array<{
parameters: Record<string, unknown>;
type: string;
name: string;
position: [number, number];
}>;
}
export interface QuickReply {
type: string;
text: string;
@@ -51,6 +80,39 @@ export namespace ChatUI {
suggestionId: string;
}
export interface WorkflowStepMessage {
role: 'assistant';
type: 'workflow-step';
steps: string[];
}
export interface WorkflowNodeMessage {
role: 'assistant';
type: 'workflow-node';
nodes: string[];
}
export interface WorkflowComposedMessage {
role: 'assistant';
type: 'workflow-composed';
nodes: Array<{
parameters: Record<string, unknown>;
type: string;
name: string;
position: [number, number];
}>;
}
export interface WorkflowGeneratedMessage {
role: 'assistant';
type: 'workflow-generated';
codeSnippet: string;
}
export interface RateWorkflowMessage {
role: 'assistant';
type: 'rate-workflow';
content: string;
}
type MessagesWithReplies = (
| TextMessage
| CodeDiffMessage
@@ -64,7 +126,14 @@ export namespace ChatUI {
| MessagesWithReplies
| ErrorMessage
| EndSessionMessage
| SessionTimeoutMessage
| SessionErrorMessage
| AgentSuggestionMessage
| WorkflowStepMessage
| WorkflowNodeMessage
| WorkflowComposedMessage
| WorkflowGeneratedMessage
| RateWorkflowMessage
) & {
id: string;
read: boolean;