mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 03:12:15 +00:00
feat: AI workflow builder front-end (no-changelog) (#14820)
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 doesn’t 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 doesn’t 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 doesn’t have',
|
||||
title: "Credential doesn't have",
|
||||
content: '',
|
||||
read: false,
|
||||
},
|
||||
@@ -155,7 +181,7 @@ SummaryContentStreaming.args = {
|
||||
id: '123',
|
||||
role: 'assistant',
|
||||
type: 'block',
|
||||
title: 'Credential doesn’t 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,
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user