mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 11:22:15 +00:00
feat: Add Ask assistant behind feature flag (#9995)
Co-authored-by: Ricardo Espinoza <ricardo@n8n.io> Co-authored-by: Milorad Filipovic <milorad@n8n.io>
This commit is contained in:
@@ -49,6 +49,7 @@
|
||||
"markdown-it-emoji": "^2.0.2",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"parse-diff": "^0.11.1",
|
||||
"sanitize-html": "2.12.1",
|
||||
"vue": "catalog:frontend",
|
||||
"vue-boring-avatars": "^1.3.0",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import AssistantAvatar from './AssistantAvatar.vue';
|
||||
import type { StoryFn } from '@storybook/vue3';
|
||||
|
||||
export default {
|
||||
title: 'Assistant/AssistantAvatar',
|
||||
component: AssistantAvatar,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const Template: StoryFn = (args, { argTypes }) => ({
|
||||
setup: () => ({ args }),
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
AssistantAvatar,
|
||||
},
|
||||
template: '<AssistantAvatar v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {};
|
||||
|
||||
export const Mini = Template.bind({});
|
||||
Mini.args = {
|
||||
size: 'mini',
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import AssistantIcon from '../AskAssistantIcon/AssistantIcon.vue';
|
||||
|
||||
withDefaults(defineProps<{ size: 'small' | 'mini' }>(), {
|
||||
size: 'small',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[$style.container, $style[size]]">
|
||||
<AssistantIcon :size="size" theme="blank" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
background: var(--color-assistant-highlight-gradient);
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.small {
|
||||
height: var(--spacing-m);
|
||||
width: var(--spacing-m);
|
||||
}
|
||||
|
||||
.mini {
|
||||
height: var(--spacing-s);
|
||||
width: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
import AskAssistantButton from './AskAssistantButton.vue';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { StoryFn } from '@storybook/vue3';
|
||||
|
||||
export default {
|
||||
title: 'Assistant/AskAssistantButton',
|
||||
component: AskAssistantButton,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const methods = {
|
||||
onClick: action('click'),
|
||||
};
|
||||
|
||||
const Template: StoryFn = (args, { argTypes }) => ({
|
||||
setup: () => ({ args }),
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
AskAssistantButton,
|
||||
},
|
||||
template:
|
||||
'<div style="display: flex; height: 50px; width: 300px; align-items: center; justify-content: center"><AskAssistantButton v-bind="args" @click="onClick" /></div>',
|
||||
methods,
|
||||
});
|
||||
|
||||
export const Button = Template.bind({});
|
||||
|
||||
export const Notifications = Template.bind({});
|
||||
Notifications.args = {
|
||||
unreadCount: 1,
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import AssistantIcon from '../AskAssistantIcon/AssistantIcon.vue';
|
||||
import AssistantText from '../AskAssistantText/AssistantText.vue';
|
||||
import BetaTag from '../BetaTag/BetaTag.vue';
|
||||
import { useI18n } from '../../composables/useI18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const hovering = ref(false);
|
||||
|
||||
const props = defineProps<{ unreadCount?: number }>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [e: MouseEvent];
|
||||
}>();
|
||||
|
||||
const onClick = (e: MouseEvent) => emit('click', e);
|
||||
|
||||
function onMouseEnter() {
|
||||
hovering.value = true;
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
hovering.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:class="$style.button"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
@click="onClick"
|
||||
>
|
||||
<div v-if="props.unreadCount" :class="$style.num">
|
||||
{{ props.unreadCount }}
|
||||
</div>
|
||||
<AssistantIcon v-else size="large" :theme="hovering ? 'blank' : 'default'" />
|
||||
<div v-show="hovering" :class="$style.text">
|
||||
<div>
|
||||
<AssistantText :text="t('askAssistantButton.askAssistant')" />
|
||||
</div>
|
||||
<div>
|
||||
<BetaTag />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.button {
|
||||
border: var(--border-base);
|
||||
background: var(--color-foreground-xlight);
|
||||
border-radius: var(--border-radius-base);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 42px;
|
||||
width: 42px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
background: var(--color-assistant-highlight-reverse);
|
||||
|
||||
> div {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.num {
|
||||
color: var(--prim-color-white);
|
||||
background: var(--color-assistant-highlight-reverse);
|
||||
border-radius: 50%;
|
||||
width: var(--spacing-s);
|
||||
height: var(--spacing-s);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-3xs);
|
||||
}
|
||||
|
||||
.text {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: end;
|
||||
width: 100px;
|
||||
right: 48px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,240 @@
|
||||
import AskAssistantChat from './AskAssistantChat.vue';
|
||||
import type { StoryFn } from '@storybook/vue3';
|
||||
import type { ChatUI } from '../../types/assistant';
|
||||
|
||||
export default {
|
||||
title: 'Assistant/AskAssistantChat',
|
||||
component: AskAssistantChat,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
function getMessages(messages: ChatUI.AssistantMessage[]): ChatUI.AssistantMessage[] {
|
||||
return messages;
|
||||
}
|
||||
|
||||
const methods = {};
|
||||
|
||||
const Template: StoryFn = (args, { argTypes }) => ({
|
||||
setup: () => ({ args }),
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
AskAssistantChat,
|
||||
},
|
||||
template: '<div style="width:275px; height:100%"><ask-assistant-chat v-bind="args" /></div>',
|
||||
methods,
|
||||
});
|
||||
|
||||
export const DefaultPlaceholderChat = Template.bind({});
|
||||
DefaultPlaceholderChat.args = {
|
||||
user: {
|
||||
firstName: 'Max',
|
||||
lastName: 'Test',
|
||||
},
|
||||
};
|
||||
|
||||
export const Chat = Template.bind({});
|
||||
Chat.args = {
|
||||
user: {
|
||||
firstName: 'Max',
|
||||
lastName: 'Test',
|
||||
},
|
||||
messages: getMessages([
|
||||
{
|
||||
id: '1',
|
||||
type: 'text',
|
||||
role: 'assistant',
|
||||
content: 'Hi Max! Here is my top solution to fix the error in your **Transform data** node👇',
|
||||
read: false,
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'code-diff',
|
||||
role: 'assistant',
|
||||
description: 'Short solution description here that can spill over to two lines',
|
||||
codeDiff:
|
||||
'@@ -1,7 +1,6 @@\n-The Way that can be told of is not the eternal Way;\n-The name that can be named is not the eternal name.\nThe Nameless is the origin of Heaven and Earth;\n-The Named is the mother of all things.\n+The named is the mother of all things.\n+\nTherefore let there always be non-being,\nso we may see their subtlety,\nAnd let there always be being,\n@@ -9,3 +8,6 @@\n The two are the same,\n But after they are produced,\n they have different names.\n+They both may be called deep and profound.\n+Deeper and more profound,\n+The door of all subtleties!',
|
||||
suggestionId: 'test',
|
||||
quickReplies: [
|
||||
{
|
||||
type: 'new-suggestion',
|
||||
text: 'Give me another solution',
|
||||
},
|
||||
{
|
||||
type: 'resolved',
|
||||
text: 'All good',
|
||||
},
|
||||
],
|
||||
read: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'text',
|
||||
role: 'user',
|
||||
content: 'Give it to me **ignore this markdown**',
|
||||
read: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'block',
|
||||
role: 'assistant',
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'code-diff',
|
||||
role: 'assistant',
|
||||
description: 'Short solution with min height',
|
||||
codeDiff:
|
||||
'@@ -1,7 +1,6 @@\n-The Way that can be told of is not the eternal Way;\n-The name that can be named is not the eternal name.\n+The door of all subtleties!',
|
||||
quickReplies: [
|
||||
{
|
||||
type: 'new-suggestion',
|
||||
text: 'Give me another solution',
|
||||
},
|
||||
{
|
||||
type: 'resolved',
|
||||
text: 'All good',
|
||||
},
|
||||
],
|
||||
suggestionId: 'test',
|
||||
read: false,
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
export const JustSummary = Template.bind({});
|
||||
JustSummary.args = {
|
||||
user: {
|
||||
firstName: 'Max',
|
||||
lastName: 'Test',
|
||||
},
|
||||
messages: getMessages([
|
||||
{
|
||||
id: '123',
|
||||
role: 'assistant',
|
||||
type: 'block',
|
||||
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,
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
export const SummaryTitleStreaming = Template.bind({});
|
||||
SummaryTitleStreaming.args = {
|
||||
user: {
|
||||
firstName: 'Max',
|
||||
lastName: 'Test',
|
||||
},
|
||||
messages: getMessages([
|
||||
{
|
||||
id: '123',
|
||||
role: 'assistant',
|
||||
type: 'block',
|
||||
title: 'Credential doesn’t have',
|
||||
content: '',
|
||||
read: false,
|
||||
},
|
||||
]),
|
||||
streaming: true,
|
||||
};
|
||||
|
||||
export const SummaryContentStreaming = Template.bind({});
|
||||
SummaryContentStreaming.args = {
|
||||
user: {
|
||||
firstName: 'Max',
|
||||
lastName: 'Test',
|
||||
},
|
||||
messages: getMessages([
|
||||
{
|
||||
id: '123',
|
||||
role: 'assistant',
|
||||
type: 'block',
|
||||
title: 'Credential doesn’t have correct permissions to send a message',
|
||||
content: 'Solution steps:\n1. Lorem ipsum dolor sit amet, consectetur',
|
||||
read: false,
|
||||
},
|
||||
]),
|
||||
streaming: true,
|
||||
};
|
||||
|
||||
export const ErrorChat = Template.bind({});
|
||||
ErrorChat.args = {
|
||||
user: {
|
||||
firstName: 'Max',
|
||||
lastName: 'Test',
|
||||
},
|
||||
messages: getMessages([
|
||||
{
|
||||
id: '123',
|
||||
role: 'assistant',
|
||||
type: 'error',
|
||||
content: 'There was an error reaching the service',
|
||||
read: false,
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
export const EmptyStreamingChat = Template.bind({});
|
||||
EmptyStreamingChat.args = {
|
||||
user: {
|
||||
firstName: 'Max',
|
||||
lastName: 'Test',
|
||||
},
|
||||
messages: getMessages([
|
||||
{
|
||||
id: '123',
|
||||
type: 'text',
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
read: false,
|
||||
},
|
||||
]),
|
||||
streaming: true,
|
||||
};
|
||||
|
||||
export const StreamingChat = Template.bind({});
|
||||
StreamingChat.args = {
|
||||
user: {
|
||||
firstName: 'Max',
|
||||
lastName: 'Test',
|
||||
},
|
||||
messages: getMessages([
|
||||
{
|
||||
id: '123',
|
||||
type: 'text',
|
||||
role: 'assistant',
|
||||
content: 'I am thinking through this problem',
|
||||
read: false,
|
||||
},
|
||||
]),
|
||||
streaming: true,
|
||||
};
|
||||
|
||||
export const EndOfSessionChat = Template.bind({});
|
||||
EndOfSessionChat.args = {
|
||||
user: {
|
||||
firstName: 'Max',
|
||||
lastName: 'Test',
|
||||
},
|
||||
messages: getMessages([
|
||||
{
|
||||
id: '123',
|
||||
type: 'text',
|
||||
role: 'assistant',
|
||||
content: "Great, glad I could help! I'm here whenever you need more help.",
|
||||
read: false,
|
||||
},
|
||||
{
|
||||
id: '123',
|
||||
role: 'assistant',
|
||||
type: 'event',
|
||||
eventName: 'end-session',
|
||||
read: false,
|
||||
},
|
||||
]),
|
||||
};
|
||||
@@ -0,0 +1,471 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import AssistantIcon from '../AskAssistantIcon/AssistantIcon.vue';
|
||||
import AssistantText from '../AskAssistantText/AssistantText.vue';
|
||||
import AssistantAvatar from '../AskAssistantAvatar/AssistantAvatar.vue';
|
||||
import CodeDiff from '../CodeDiff/CodeDiff.vue';
|
||||
import type { ChatUI } from '../../types/assistant';
|
||||
import BlinkingCursor from '../BlinkingCursor/BlinkingCursor.vue';
|
||||
|
||||
import Markdown from 'markdown-it';
|
||||
import InlineAskAssistantButton from '../InlineAskAssistantButton/InlineAskAssistantButton.vue';
|
||||
import BetaTag from '../BetaTag/BetaTag.vue';
|
||||
import { useI18n } from '../../composables/useI18n';
|
||||
import markdownLink from 'markdown-it-link-attributes';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const md = new Markdown({
|
||||
breaks: true,
|
||||
}).use(markdownLink, {
|
||||
attrs: {
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
},
|
||||
});
|
||||
|
||||
const MAX_CHAT_INPUT_HEIGHT = 100;
|
||||
|
||||
interface Props {
|
||||
user?: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
messages?: ChatUI.AssistantMessage[];
|
||||
streaming?: boolean;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
message: [string, string | undefined];
|
||||
codeReplace: [number];
|
||||
codeUndo: [number];
|
||||
}>();
|
||||
|
||||
const onClose = () => emit('close');
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const textInputValue = ref<string>('');
|
||||
|
||||
const chatInput = ref<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const sessionEnded = computed(() => {
|
||||
return isEndOfSessionEvent(props.messages?.[props.messages.length - 1]);
|
||||
});
|
||||
|
||||
const sendDisabled = computed(() => {
|
||||
return !textInputValue.value || props.streaming || sessionEnded.value;
|
||||
});
|
||||
|
||||
function isEndOfSessionEvent(event?: ChatUI.AssistantMessage) {
|
||||
return event?.type === 'event' && event?.eventName === 'end-session';
|
||||
}
|
||||
|
||||
function onQuickReply(opt: ChatUI.QuickReply) {
|
||||
emit('message', opt.text, opt.type);
|
||||
}
|
||||
|
||||
function onSendMessage() {
|
||||
if (sendDisabled.value) return;
|
||||
emit('message', textInputValue.value, undefined);
|
||||
textInputValue.value = '';
|
||||
if (chatInput.value) {
|
||||
chatInput.value.style.height = 'auto';
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
const scrollHeight = chatInput.value.scrollHeight;
|
||||
chatInput.value.style.height = `${Math.min(scrollHeight, MAX_CHAT_INPUT_HEIGHT)}px`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<div :class="$style.header">
|
||||
<div :class="$style.chatTitle">
|
||||
<div :class="$style.headerText">
|
||||
<AssistantIcon size="large" />
|
||||
<AssistantText size="large" :text="t('assistantChat.aiAssistantLabel')" />
|
||||
</div>
|
||||
<BetaTag />
|
||||
</div>
|
||||
<div :class="$style.back" @click="onClose">
|
||||
<n8n-icon icon="arrow-right" color="text-base" />
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.body">
|
||||
<div v-if="messages?.length" :class="$style.messages">
|
||||
<div v-for="(message, i) in messages" :key="i" :class="$style.message">
|
||||
<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">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-html="renderMarkdown(message.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">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-if="message.role === 'user'" v-html="renderMarkdown(message.content)"></span>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span
|
||||
v-else
|
||||
:class="$style.assistantText"
|
||||
v-html="renderMarkdown(message.content)"
|
||||
></span>
|
||||
<BlinkingCursor
|
||||
v-if="streaming && i === messages?.length - 1 && message.role === 'assistant'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="message.type === 'error'" :class="$style.error">
|
||||
<span>⚠️ {{ message.content }}</span>
|
||||
</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">
|
||||
<span>
|
||||
{{ t('assistantChat.sessionEndMessage.1') }}
|
||||
</span>
|
||||
<InlineAskAssistantButton size="small" :static="true" />
|
||||
<span>
|
||||
{{ t('assistantChat.sessionEndMessage.2') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
!streaming &&
|
||||
'quickReplies' in message &&
|
||||
message.quickReplies?.length &&
|
||||
i === messages?.length - 1
|
||||
"
|
||||
:class="$style.quickReplies"
|
||||
>
|
||||
<div :class="$style.quickRepliesTitle">{{ t('assistantChat.quickRepliesTitle') }}</div>
|
||||
<div v-for="opt in message.quickReplies" :key="opt.type">
|
||||
<n8n-button
|
||||
v-if="opt.text"
|
||||
type="secondary"
|
||||
size="mini"
|
||||
@click="() => onQuickReply(opt)"
|
||||
>
|
||||
{{ opt.text }}
|
||||
</n8n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else :class="$style.placeholder">
|
||||
<div :class="$style.greeting">Hi {{ user?.firstName }} 👋</div>
|
||||
<div :class="$style.info">
|
||||
<p>
|
||||
{{
|
||||
t('assistantChat.placeholder.1', [
|
||||
`${user?.firstName}`,
|
||||
t('assistantChat.aiAssistantName'),
|
||||
])
|
||||
}}
|
||||
</p>
|
||||
<p>
|
||||
{{ t('assistantChat.placeholder.2') }}
|
||||
<InlineAskAssistantButton size="small" :static="true" />
|
||||
{{ t('assistantChat.placeholder.3') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ t('assistantChat.placeholder.4') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="messages?.length"
|
||||
:class="{ [$style.inputWrapper]: true, [$style.disabledInput]: sessionEnded }"
|
||||
>
|
||||
<textarea
|
||||
ref="chatInput"
|
||||
v-model="textInputValue"
|
||||
:disabled="sessionEnded"
|
||||
:placeholder="t('assistantChat.inputPlaceholder')"
|
||||
rows="1"
|
||||
wrap="hard"
|
||||
@keydown.enter.exact.prevent="onSendMessage"
|
||||
@input.prevent="growInput"
|
||||
@keydown.stop
|
||||
/>
|
||||
<n8n-icon-button
|
||||
:class="{ [$style.sendButton]: true }"
|
||||
icon="paper-plane"
|
||||
type="text"
|
||||
size="large"
|
||||
:disabled="sendDisabled"
|
||||
@click="onSendMessage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
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);
|
||||
background-color: var(--color-background-xlight);
|
||||
border: var(--border-base);
|
||||
border-top: 0;
|
||||
display: flex;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
> div:first-of-type {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
background-color: var(--color-background-light);
|
||||
border: var(--border-base);
|
||||
border-top: 0;
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
padding-bottom: 250px; // make scrollable at the end
|
||||
position: relative;
|
||||
|
||||
pre {
|
||||
text-wrap: stable;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
padding: var(--spacing-s);
|
||||
}
|
||||
|
||||
.messages {
|
||||
padding: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
.headerText {
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.greeting {
|
||||
color: var(--color-text-dark);
|
||||
font-size: var(--font-size-m);
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: var(--font-size-s);
|
||||
color: var(--color-text-base);
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
.back:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quickReplies {
|
||||
margin-top: var(--spacing-s);
|
||||
> * {
|
||||
margin-bottom: var(--spacing-3xs);
|
||||
}
|
||||
}
|
||||
|
||||
.quickRepliesTitle {
|
||||
font-size: var(--font-size-3xs);
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
|
||||
.textMessage {
|
||||
font-size: var(--font-size-2xs);
|
||||
}
|
||||
|
||||
.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 {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
bottom: 0;
|
||||
background-color: var(--color-foreground-xlight);
|
||||
border: var(--border-base);
|
||||
width: 100%;
|
||||
padding-top: 1px;
|
||||
|
||||
textarea {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
font-size: var(--spacing-xs);
|
||||
padding: var(--spacing-xs);
|
||||
outline: none;
|
||||
color: var(--color-text-dark);
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.sendButton {
|
||||
color: var(--color-text-base) !important;
|
||||
|
||||
&[disabled] {
|
||||
color: var(--color-text-light) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.assistantText {
|
||||
display: inline;
|
||||
|
||||
p {
|
||||
display: inline;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
list-style-position: inside;
|
||||
margin: var(--spacing-xs) 0 var(--spacing-xs) var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
* {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
import AssistantIcon from './AssistantIcon.vue';
|
||||
import type { StoryFn } from '@storybook/vue3';
|
||||
|
||||
export default {
|
||||
title: 'Assistant/AssistantIcon',
|
||||
component: AssistantIcon,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const Template: StoryFn = (args, { argTypes }) => ({
|
||||
setup: () => ({ args }),
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
AssistantIcon,
|
||||
},
|
||||
template: '<div style="background: lightgray;"><AssistantIcon v-bind="args" /></div>',
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
theme: 'default',
|
||||
};
|
||||
|
||||
export const Blank = Template.bind({
|
||||
template: '<div style="background=black;"><AssistantIcon v-bind="args" /></div>',
|
||||
});
|
||||
Blank.args = {
|
||||
theme: 'blank',
|
||||
};
|
||||
|
||||
export const Mini = Template.bind({});
|
||||
Mini.args = {
|
||||
size: 'mini',
|
||||
};
|
||||
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
size: 'small',
|
||||
};
|
||||
|
||||
export const Medium = Template.bind({});
|
||||
Medium.args = {
|
||||
size: 'medium',
|
||||
};
|
||||
|
||||
export const Large = Template.bind({});
|
||||
Large.args = {
|
||||
size: 'large',
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
size: 'mini' | 'small' | 'medium' | 'large';
|
||||
theme: 'default' | 'blank' | 'disabled';
|
||||
}>(),
|
||||
{
|
||||
size: 'medium',
|
||||
theme: 'default',
|
||||
},
|
||||
);
|
||||
|
||||
const sizes = {
|
||||
mini: 8,
|
||||
small: 10,
|
||||
medium: 12,
|
||||
large: 18,
|
||||
};
|
||||
|
||||
const svgFill = computed(() => {
|
||||
if (props.theme === 'blank') {
|
||||
return 'white';
|
||||
} else if (props.theme === 'disabled') {
|
||||
return 'var(--color-text-light)';
|
||||
}
|
||||
return 'url(#paint0_linear_173_12825)';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:width="sizes[size]"
|
||||
:height="sizes[size]"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19.9658 14.0171C19.9679 14.3549 19.8654 14.6851 19.6722 14.9622C19.479 15.2393 19.2046 15.4497 18.8869 15.5645L13.5109 17.5451L11.5303 22.9211C11.4137 23.2376 11.2028 23.5107 10.9261 23.7037C10.6494 23.8966 10.3202 24 9.9829 24C9.64559 24 9.3164 23.8966 9.0397 23.7037C8.76301 23.5107 8.55212 23.2376 8.43549 22.9211L6.45487 17.5451L1.07888 15.5645C0.762384 15.4479 0.489262 15.237 0.296347 14.9603C0.103431 14.6836 0 14.3544 0 14.0171C0 13.6798 0.103431 13.3506 0.296347 13.0739C0.489262 12.7972 0.762384 12.5863 1.07888 12.4697L6.45487 10.4891L8.43549 5.11309C8.55212 4.79659 8.76301 4.52347 9.0397 4.33055C9.3164 4.13764 9.64559 4.0342 9.9829 4.0342C10.3202 4.0342 10.6494 4.13764 10.9261 4.33055C11.2028 4.52347 11.4137 4.79659 11.5303 5.11309L13.5109 10.4891L18.8869 12.4697C19.2046 12.5845 19.479 12.7949 19.6722 13.072C19.8654 13.3491 19.9679 13.6793 19.9658 14.0171ZM14.1056 4.12268H15.7546V5.77175C15.7546 5.99043 15.8415 6.20015 15.9961 6.35478C16.1508 6.50941 16.3605 6.59628 16.5792 6.59628C16.7979 6.59628 17.0076 6.50941 17.1622 6.35478C17.3168 6.20015 17.4037 5.99043 17.4037 5.77175V4.12268H19.0528C19.2715 4.12268 19.4812 4.03581 19.6358 3.88118C19.7905 3.72655 19.8773 3.51682 19.8773 3.29814C19.8773 3.07946 19.7905 2.86974 19.6358 2.71511C19.4812 2.56048 19.2715 2.47361 19.0528 2.47361H17.4037V0.824535C17.4037 0.605855 17.3168 0.396131 17.1622 0.241501C17.0076 0.0868704 16.7979 0 16.5792 0C16.3605 0 16.1508 0.0868704 15.9961 0.241501C15.8415 0.396131 15.7546 0.605855 15.7546 0.824535V2.47361H14.1056C13.8869 2.47361 13.6772 2.56048 13.5225 2.71511C13.3679 2.86974 13.281 3.07946 13.281 3.29814C13.281 3.51682 13.3679 3.72655 13.5225 3.88118C13.6772 4.03581 13.8869 4.12268 14.1056 4.12268ZM23.1755 7.42082H22.3509V6.59628C22.3509 6.3776 22.2641 6.16788 22.1094 6.01325C21.9548 5.85862 21.7451 5.77175 21.5264 5.77175C21.3077 5.77175 21.098 5.85862 20.9434 6.01325C20.7887 6.16788 20.7019 6.3776 20.7019 6.59628V7.42082H19.8773C19.6586 7.42082 19.4489 7.50769 19.2943 7.66232C19.1397 7.81695 19.0528 8.02667 19.0528 8.24535C19.0528 8.46404 19.1397 8.67376 19.2943 8.82839C19.4489 8.98302 19.6586 9.06989 19.8773 9.06989H20.7019V9.89443C20.7019 10.1131 20.7887 10.3228 20.9434 10.4775C21.098 10.6321 21.3077 10.719 21.5264 10.719C21.7451 10.719 21.9548 10.6321 22.1094 10.4775C22.2641 10.3228 22.3509 10.1131 22.3509 9.89443V9.06989H23.1755C23.3941 9.06989 23.6039 8.98302 23.7585 8.82839C23.9131 8.67376 24 8.46404 24 8.24535C24 8.02667 23.9131 7.81695 23.7585 7.66232C23.6039 7.50769 23.3941 7.42082 23.1755 7.42082Z"
|
||||
:fill="svgFill"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_173_12825"
|
||||
x1="-3.67094e-07"
|
||||
y1="-0.000120994"
|
||||
x2="28.8315"
|
||||
y2="9.82667"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--color-assistant-highlight-1)" />
|
||||
<stop offset="0.495" stop-color="var(--color-assistant-highlight-2)" />
|
||||
<stop offset="1" stop-color="var(--color-assistant-highlight-3)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
import AssistantText from './AssistantText.vue';
|
||||
import type { StoryFn } from '@storybook/vue3';
|
||||
|
||||
export default {
|
||||
title: 'Assistant/AssistantText',
|
||||
component: AssistantText,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const Template: StoryFn = (args, { argTypes }) => ({
|
||||
setup: () => ({ args }),
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
AssistantText,
|
||||
},
|
||||
template: '<AssistantText v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
text: 'Ask me something!!!',
|
||||
};
|
||||
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
text: 'Ask me something!!!',
|
||||
size: 'small',
|
||||
};
|
||||
|
||||
export const Medium = Template.bind({});
|
||||
Medium.args = {
|
||||
text: 'Ask me something!!!',
|
||||
size: 'medium',
|
||||
};
|
||||
|
||||
export const Large = Template.bind({});
|
||||
Large.args = {
|
||||
text: 'Ask me something!!!',
|
||||
size: 'large',
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{ text: string; size: 'small' | 'medium' | 'large' | 'xlarge' }>(), {
|
||||
text: '',
|
||||
size: 'medium',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="[$style.text, $style[size]]">{{ text }}</span>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.text {
|
||||
background: var(--color-assistant-highlight-gradient);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 9px;
|
||||
line-height: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.medium {
|
||||
font-size: var(--spacing-xs);
|
||||
line-height: var(--spacing-s);
|
||||
}
|
||||
|
||||
.large {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.xlarge {
|
||||
font-size: var(--spacing-s);
|
||||
line-height: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
import BetaTag from './BetaTag.vue';
|
||||
import type { StoryFn } from '@storybook/vue3';
|
||||
|
||||
export default {
|
||||
title: 'Assistant/BetaTag',
|
||||
component: BetaTag,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const Template: StoryFn = (args, { argTypes }) => ({
|
||||
setup: () => ({ args }),
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
BetaTag,
|
||||
},
|
||||
template: '<BetaTag v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Beta = Template.bind({});
|
||||
22
packages/design-system/src/components/BetaTag/BetaTag.vue
Normal file
22
packages/design-system/src/components/BetaTag/BetaTag.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from '../../composables/useI18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.beta">{{ t('betaTag.beta') }}</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.beta {
|
||||
display: inline-block;
|
||||
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--font-size-3xs);
|
||||
font-weight: var(--font-weight-bold);
|
||||
background-color: var(--color-secondary-tint-3);
|
||||
padding: var(--spacing-5xs) var(--spacing-4xs);
|
||||
border-radius: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
import BlinkingCursor from './BlinkingCursor.vue';
|
||||
import type { StoryFn } from '@storybook/vue3';
|
||||
|
||||
export default {
|
||||
title: 'Assistant/BlinkingCursor',
|
||||
component: BlinkingCursor,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const Template: StoryFn = (args, { argTypes }) => ({
|
||||
setup: () => ({ args }),
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
BlinkingCursor,
|
||||
},
|
||||
template: '<blinking-cursor v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Cursor = Template.bind({});
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<span class="blinking-cursor"></span>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.blinking-cursor {
|
||||
display: inline-block;
|
||||
height: var(--font-size-m);
|
||||
width: var(--spacing-3xs);
|
||||
border-radius: var(--border-radius-small);
|
||||
margin-left: var(--spacing-4xs);
|
||||
|
||||
animation: 1s blink step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
from,
|
||||
to {
|
||||
background-color: transparent;
|
||||
}
|
||||
50% {
|
||||
background-color: var(--color-foreground-xdark);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
import CodeDiff from './CodeDiff.vue';
|
||||
import type { StoryFn } from '@storybook/vue3';
|
||||
|
||||
export default {
|
||||
title: 'Assistant/CodeDiff',
|
||||
component: CodeDiff,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const methods = {};
|
||||
|
||||
const Template: StoryFn = (args, { argTypes }) => ({
|
||||
setup: () => ({ args }),
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
CodeDiff,
|
||||
},
|
||||
template: '<div style="width:300px; height:100%"><code-diff v-bind="args" /></div>',
|
||||
methods,
|
||||
});
|
||||
|
||||
export const Example = Template.bind({});
|
||||
Example.args = {
|
||||
title: 'Lao Tzu example unified diff',
|
||||
content:
|
||||
'@@ -1,7 +1,6 @@\n-The Way that can be told of is not the eternal Way;\n-The name that can be named is not the eternal name.\nThe Nameless is the origin of Heaven and Earth;\n-The Named is the mother of all things.\n+The named is the mother of all things.\n+\nTherefore let there always be non-being,\nso we may see their subtlety,\nAnd let there always be being,\n@@ -9,3 +8,6 @@\n The two are the same,\n But after they are produced,\n they have different names.\n+They both may be called deep and profound.\n+Deeper and more profound,\n+The door of all subtleties!',
|
||||
};
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.args = {};
|
||||
|
||||
export const Code = Template.bind({});
|
||||
Code.args = {
|
||||
title: "Fix reference to the node and remove unsupported 'require' statement.",
|
||||
content:
|
||||
"--- original.js\n+++ modified.js\n@@ -1,2 +1,2 @@\n-const SIGNING_SECRET = $input.first().json.slack_secret_signature;\n-const item = $('Webhook to call for Slack command').first();\n+const SIGNING_SECRET = items[0].json.slack_secret_signature;\n+const item = items[0];\n@@ -7,8 +7,6 @@\n}\n\n-const crypto = require('crypto');\n-\n const { binary: { data } } = item;\n\n if (\n@@ -22,7 +20,7 @@\n const rawBody = Buffer.from(data.data, 'base64').toString()\n \n // compute the ",
|
||||
streaming: true,
|
||||
};
|
||||
|
||||
export const StreamingTitleEmpty = Template.bind({});
|
||||
StreamingTitleEmpty.args = {
|
||||
streaming: true,
|
||||
};
|
||||
|
||||
export const StreamingTitle = Template.bind({});
|
||||
StreamingTitle.args = {
|
||||
streaming: true,
|
||||
title: 'Hello world',
|
||||
};
|
||||
|
||||
export const StreamingContentWithOneLine = Template.bind({});
|
||||
StreamingContentWithOneLine.args = {
|
||||
streaming: true,
|
||||
title: 'Hello world',
|
||||
content: '@@ -1,7 +1,6 @@\n-The Way that can be told of is not th',
|
||||
};
|
||||
|
||||
export const StreamingContentWithMultipleLines = Template.bind({});
|
||||
StreamingContentWithMultipleLines.args = {
|
||||
streaming: true,
|
||||
title: 'Hello world',
|
||||
content:
|
||||
'@@ -1,7 +1,6 @@\n-The Way that can be told of is not the eternal Way;\n-The name that can b',
|
||||
};
|
||||
|
||||
export const StreamingWithManyManyLines = Template.bind({});
|
||||
StreamingWithManyManyLines.args = {
|
||||
title: 'Lao Tzu example unified diff',
|
||||
content:
|
||||
'@@ -1,7 +1,6 @@\n-The Way that can be told of is not the eternal Way;\n-The name that can be named is not the eternal name.\nThe Nameless is the origin of Heaven and Earth;\n-The Named is the mother of all things.\n+The named is the mother of all things.\n+\nTherefore let there always be non-being,\nso we may see their subtlety,\nAnd let there always be being,\n@@ -9,3 +8,6 @@\n The two are the same,\n But after they are produced,\n they have different names.\n+They both may be called deep and profound.\n+Deeper and more profound,\n+The door of all subtleties!',
|
||||
streaming: true,
|
||||
};
|
||||
|
||||
export const Replaced = Template.bind({});
|
||||
Replaced.args = {
|
||||
title: 'Lao Tzu example unified diff',
|
||||
content:
|
||||
'@@ -1,7 +1,6 @@\n-The Way that can be told of is not the eternal Way;\n-The name that can be named is not the eternal name.\nThe Nameless is the origin of Heaven and Earth;\n-The Named is the mother of all things.\n+The named is the mother of all things.\n+\nTherefore let there always be non-being,\nso we may see their subtlety,\nAnd let there always be being,\n@@ -9,3 +8,6 @@\n The two are the same,\n But after they are produced,\n they have different names.\n+They both may be called deep and profound.\n+Deeper and more profound,\n+The door of all subtleties!',
|
||||
replaced: true,
|
||||
};
|
||||
|
||||
export const Replacing = Template.bind({});
|
||||
Replacing.args = {
|
||||
title: 'Lao Tzu example unified diff',
|
||||
content:
|
||||
'@@ -1,7 +1,6 @@\n-The Way that can be told of is not the eternal Way;\n-The name that can be named is not the eternal name.\nThe Nameless is the origin of Heaven and Earth;\n-The Named is the mother of all things.\n+The named is the mother of all things.\n+\nTherefore let there always be non-being,\nso we may see their subtlety,\nAnd let there always be being,\n@@ -9,3 +8,6 @@\n The two are the same,\n But after they are produced,\n they have different names.\n+They both may be called deep and profound.\n+Deeper and more profound,\n+The door of all subtleties!',
|
||||
replacing: true,
|
||||
};
|
||||
|
||||
export const Error = Template.bind({});
|
||||
Error.args = {
|
||||
title: 'Lao Tzu example unified diff',
|
||||
content:
|
||||
'@@ -1,7 +1,6 @@\n-The Way that can be told of is not the eternal Way;\n-The name that can be named is not the eternal name.\nThe Nameless is the origin of Heaven and Earth;\n-The Named is the mother of all things.\n+The named is the mother of all things.\n+\nTherefore let there always be non-being,\nso we may see their subtlety,\nAnd let there always be being,\n@@ -9,3 +8,6 @@\n The two are the same,\n But after they are produced,\n they have different names.\n+They both may be called deep and profound.\n+Deeper and more profound,\n+The door of all subtleties!',
|
||||
error: true,
|
||||
};
|
||||
207
packages/design-system/src/components/CodeDiff/CodeDiff.vue
Normal file
207
packages/design-system/src/components/CodeDiff/CodeDiff.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<script setup lang="ts">
|
||||
import parseDiff from 'parse-diff';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'n8n-design-system/composables/useI18n';
|
||||
|
||||
const MIN_LINES = 4;
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
content: string;
|
||||
replacing: boolean;
|
||||
replaced: boolean;
|
||||
error: boolean;
|
||||
streaming: boolean;
|
||||
}
|
||||
|
||||
type Line =
|
||||
| parseDiff.Change
|
||||
| {
|
||||
type: 'filler' | 'seperator';
|
||||
content: string;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '',
|
||||
content: '',
|
||||
replacing: false,
|
||||
replaced: false,
|
||||
error: false,
|
||||
streaming: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
replace: [];
|
||||
undo: [];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const diffs = computed(() => {
|
||||
const parsed = parseDiff(props.content);
|
||||
|
||||
const file = parsed[0] ?? { chunks: [] };
|
||||
|
||||
const lines: Line[] = file.chunks.reduce((accu: Line[], chunk, i) => {
|
||||
const changes: Line[] = chunk.changes.map((change) => {
|
||||
let content = change.content;
|
||||
if (change.type === 'add' && content.startsWith('+')) {
|
||||
content = content.replace('+', '');
|
||||
} else if (change.type === 'del' && content.startsWith('-')) {
|
||||
content = content.replace('-', '');
|
||||
}
|
||||
|
||||
return {
|
||||
...change,
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
if (i !== file.chunks.length - 1) {
|
||||
changes.push({
|
||||
type: 'seperator',
|
||||
content: '...',
|
||||
});
|
||||
}
|
||||
return [...accu, ...changes];
|
||||
}, []);
|
||||
|
||||
const len = lines.length;
|
||||
// why programmatic and not min height? to ensure numbers border goes all the way down.
|
||||
if (len <= MIN_LINES) {
|
||||
for (let i = 0; i < MIN_LINES - len; i++) {
|
||||
lines.push({
|
||||
type: 'filler',
|
||||
content: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<div :class="$style.title">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div :class="$style.diffSection">
|
||||
<div v-for="(diff, i) in diffs" :key="i" :class="$style.diff">
|
||||
<div :class="$style.lineNumber">
|
||||
<!-- ln1 is line number in original text -->
|
||||
<!-- ln2 is line number in updated text -->
|
||||
{{ diff.type === 'normal' ? diff.ln2 : diff.type === 'add' ? diff.ln : '' }}
|
||||
</div>
|
||||
<div :class="[$style[diff.type], $style.diffContent]">
|
||||
<span v-if="diff.type === 'add'"> + </span>
|
||||
<span v-else-if="diff.type === 'del'"> - </span>
|
||||
<span v-else> </span>
|
||||
<span>
|
||||
{{ diff.content }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.actions">
|
||||
<div v-if="error">
|
||||
<n8n-icon icon="exclamation-triangle" color="danger" class="mr-5xs" />
|
||||
<span :class="$style.infoText">{{ t('codeDiff.couldNotReplace') }}</span>
|
||||
</div>
|
||||
<div v-else-if="replaced">
|
||||
<n8n-button type="secondary" size="mini" icon="undo" @click="() => emit('undo')">{{
|
||||
t('codeDiff.undo')
|
||||
}}</n8n-button>
|
||||
<n8n-icon icon="check" color="success" class="ml-xs" />
|
||||
<span :class="$style.infoText">{{ t('codeDiff.codeReplaced') }}</span>
|
||||
</div>
|
||||
<n8n-button
|
||||
v-else
|
||||
:type="replacing ? 'secondary' : 'primary'"
|
||||
size="mini"
|
||||
icon="refresh"
|
||||
:disabled="!content || streaming"
|
||||
:loading="replacing"
|
||||
@click="() => emit('replace')"
|
||||
>{{ replacing ? t('codeDiff.replacing') : t('codeDiff.replaceMyCode') }}</n8n-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
border: var(--border-base);
|
||||
background-color: var(--color-foreground-xlight);
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: var(--spacing-2xs);
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-2xs);
|
||||
// ensure consistent spacing even if title is empty
|
||||
min-height: 32.5px;
|
||||
line-height: normal;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.lineNumber {
|
||||
font-size: var(--font-size-3xs);
|
||||
min-width: 18px;
|
||||
max-width: 18px;
|
||||
text-align: center;
|
||||
border-right: var(--border-base);
|
||||
}
|
||||
|
||||
.diffSection {
|
||||
overflow: scroll;
|
||||
border-top: var(--border-base);
|
||||
border-bottom: var(--border-base);
|
||||
max-height: 218px; // 12 lines
|
||||
background-color: var(--color-background-base);
|
||||
font-family: var(--font-family-monospace);
|
||||
}
|
||||
|
||||
.diff {
|
||||
display: flex;
|
||||
font-size: var(--font-size-3xs);
|
||||
line-height: 18px; /* 100% */
|
||||
height: 18px;
|
||||
max-height: 18px;
|
||||
}
|
||||
|
||||
.diffContent {
|
||||
width: auto;
|
||||
text-wrap: nowrap;
|
||||
display: flex;
|
||||
|
||||
> span {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.add {
|
||||
color: var(--color-success);
|
||||
background-color: var(--color-success-tint-2);
|
||||
}
|
||||
|
||||
.del {
|
||||
color: var(--color-danger);
|
||||
background-color: var(--color-danger-tint-2);
|
||||
}
|
||||
|
||||
.normal {
|
||||
background-color: var(--color-foreground-xlight);
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.infoText {
|
||||
color: var(--color-text-light);
|
||||
font-size: var(--font-size-xs);
|
||||
margin-left: var(--spacing-4xs);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
import InlineAskAssistantButton from './InlineAskAssistantButton.vue';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { StoryFn } from '@storybook/vue3';
|
||||
|
||||
export default {
|
||||
title: 'Assistant/InlineAskAssistantButton',
|
||||
component: InlineAskAssistantButton,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const methods = {
|
||||
onClick: action('click'),
|
||||
};
|
||||
|
||||
const Template: StoryFn = (args, { argTypes }) => ({
|
||||
setup: () => ({ args }),
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
InlineAskAssistantButton,
|
||||
},
|
||||
template: '<InlineAskAssistantButton v-bind="args" @click="onClick" />',
|
||||
methods,
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
export const AskedButton = Template.bind({});
|
||||
AskedButton.args = {
|
||||
asked: true,
|
||||
};
|
||||
|
||||
export const Small = Template.bind({});
|
||||
Small.args = { size: 'small' };
|
||||
|
||||
export const Static = Template.bind({});
|
||||
Static.args = { static: true };
|
||||
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import AssistantIcon from '../AskAssistantIcon/AssistantIcon.vue';
|
||||
import AssistantText from '../AskAssistantText/AssistantText.vue';
|
||||
import { useI18n } from '../../composables/useI18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface Props {
|
||||
size: 'small' | 'medium';
|
||||
static: boolean;
|
||||
asked: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'medium',
|
||||
static: false,
|
||||
asked: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [];
|
||||
}>();
|
||||
|
||||
const sizes = {
|
||||
medium: {
|
||||
padding: '0px 12px',
|
||||
height: '28px',
|
||||
},
|
||||
small: {
|
||||
padding: '0px 6px',
|
||||
height: '18px',
|
||||
},
|
||||
};
|
||||
|
||||
const hoverable = computed(() => !props.static && !props.asked);
|
||||
|
||||
const onClick = () => {
|
||||
if (hoverable.value) {
|
||||
emit('click');
|
||||
}
|
||||
};
|
||||
// todo hoverable class not clean below
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:class="{ [$style.button]: true, [$style.hoverable]: hoverable, [$style.asked]: asked }"
|
||||
:style="{ height: sizes[size].height }"
|
||||
:disabled="asked"
|
||||
:tabindex="static ? '-1' : ''"
|
||||
@click="onClick"
|
||||
>
|
||||
<div>
|
||||
<div :style="{ padding: sizes[size].padding }">
|
||||
<AssistantIcon :size="size" :class="$style.icon" :theme="asked ? 'disabled' : 'default'" />
|
||||
<span v-if="asked">{{ t('inlineAskAssistantButton.asked') }}</span>
|
||||
<AssistantText v-else :size="size" :text="t('askAssistantButton.askAssistant')" />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.button {
|
||||
border-radius: var(--border-radius-base);
|
||||
position: relative;
|
||||
border: 0;
|
||||
padding: 1px;
|
||||
|
||||
background: var(--color-assistant-highlight-gradient);
|
||||
|
||||
> div {
|
||||
background-color: var(--color-askAssistant-button-background);
|
||||
border-radius: inherit;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
> div {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hoverable {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: var(--color-assistant-highlight-reverse);
|
||||
|
||||
> div {
|
||||
background: var(--color-askAssistant-button-background-hover);
|
||||
}
|
||||
|
||||
> div > div {
|
||||
background: var(--color-assistant-inner-highlight-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--color-assistant-highlight-gradient);
|
||||
|
||||
> div {
|
||||
background: var(--color-askAssistant-button-background-active);
|
||||
}
|
||||
|
||||
> div > div {
|
||||
background: var(--color-assistant-inner-highlight-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.asked {
|
||||
cursor: not-allowed;
|
||||
background: var(--color-foreground-base);
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: var(--spacing-3xs);
|
||||
}
|
||||
</style>
|
||||
@@ -7,8 +7,10 @@
|
||||
variant="marble"
|
||||
:colors="getColors(colors)"
|
||||
/>
|
||||
<span v-else :class="[$style.empty, $style[size]]" />
|
||||
<span v-if="name" :class="$style.initials">{{ initials }}</span>
|
||||
<div v-else :class="[$style.empty, $style[size]]"></div>
|
||||
<span v-if="firstName || lastName" :class="[$style.initials, $style[`text-${size}`]]">
|
||||
{{ initials }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -20,7 +22,7 @@ import { getInitials } from '../../utils/labelUtil';
|
||||
interface AvatarProps {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
size?: string;
|
||||
size?: 'xsmall' | 'small' | 'medium' | 'large';
|
||||
colors?: string[];
|
||||
}
|
||||
|
||||
@@ -47,6 +49,7 @@ const getColors = (colors: string[]): string[] => {
|
||||
};
|
||||
|
||||
const sizes: { [size: string]: number } = {
|
||||
xsmall: 20,
|
||||
small: 28,
|
||||
large: 48,
|
||||
medium: 40,
|
||||
@@ -78,6 +81,15 @@ const getSize = (size: string): number => sizes[size];
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.text-xsmall {
|
||||
font-size: 6px;
|
||||
}
|
||||
|
||||
.xsmall {
|
||||
height: var(--spacing-m);
|
||||
width: var(--spacing-m);
|
||||
}
|
||||
|
||||
.small {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
}"
|
||||
>
|
||||
<span v-if="loading || icon" :class="$style.icon">
|
||||
<N8nSpinner v-if="loading" :size="size" />
|
||||
<N8nIcon v-else-if="icon" :icon="icon" :size="size" />
|
||||
<N8nSpinner v-if="loading" :size="iconSize" />
|
||||
<N8nIcon v-else-if="icon" :icon="icon" :size="iconSize" />
|
||||
</span>
|
||||
<span v-if="label || $slots.default">
|
||||
<slot>{{ label }}</slot>
|
||||
@@ -56,6 +56,8 @@ const ariaBusy = computed(() => (props.loading ? 'true' : undefined));
|
||||
const ariaDisabled = computed(() => (props.disabled ? 'true' : undefined));
|
||||
const isDisabled = computed(() => props.disabled || props.loading);
|
||||
|
||||
const iconSize = computed(() => (props.size === 'mini' ? 'xsmall' : props.size));
|
||||
|
||||
const classes = computed(() => {
|
||||
return (
|
||||
`button ${$style.button} ${$style[props.type]}` +
|
||||
|
||||
@@ -17,13 +17,14 @@ import N8nIcon from '../N8nIcon';
|
||||
const TYPE = ['dots', 'ring'] as const;
|
||||
|
||||
interface SpinnerProps {
|
||||
size?: Exclude<TextSize, 'xsmall' | 'mini' | 'xlarge'>;
|
||||
size?: Exclude<TextSize, 'mini' | 'xlarge'>;
|
||||
type?: (typeof TYPE)[number];
|
||||
}
|
||||
|
||||
defineOptions({ name: 'N8nSpinner' });
|
||||
withDefaults(defineProps<SpinnerProps>(), {
|
||||
type: 'dots',
|
||||
size: 'medium',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -465,6 +465,8 @@
|
||||
var(--prim-color-alt-k-s),
|
||||
var(--prim-color-alt-k-l)
|
||||
);
|
||||
|
||||
--prim-color-white: hsl(0, 0%, 100%);
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@@ -33,6 +33,29 @@
|
||||
|
||||
// Secondary tokens
|
||||
|
||||
// AI Assistant
|
||||
--color-assistant-highlight-1: #8c90f2;
|
||||
--color-assistant-highlight-2: #a977f0;
|
||||
--color-assistant-highlight-3: #f0778b;
|
||||
--color-askAssistant-button-background: #2E2E2E;
|
||||
--color-askAssistant-button-background-hover: #383839;
|
||||
--color-askAssistant-button-background-active: #414244;
|
||||
--color-assistant-inner-highlight-hover: var(--color-askAssistant-button-background-hover);
|
||||
--color-assistant-inner-highlight-active: var(--color-askAssistant-button-background-active);
|
||||
|
||||
--color-assistant-highlight-gradient: linear-gradient(
|
||||
105deg,
|
||||
var(--color-assistant-highlight-1) 0%,
|
||||
var(--color-assistant-highlight-2) 50%,
|
||||
var(--color-assistant-highlight-3) 100%
|
||||
);
|
||||
--color-assistant-highlight-reverse: linear-gradient(
|
||||
105deg,
|
||||
var(--color-assistant-highlight-3) 0%,
|
||||
var(--color-assistant-highlight-2) 50%,
|
||||
var(--color-assistant-highlight-1) 100%
|
||||
);
|
||||
|
||||
// LangChain
|
||||
--color-lm-chat-messages-background: var(--prim-gray-820);
|
||||
--color-lm-chat-bot-background: var(--prim-gray-740);
|
||||
@@ -196,7 +219,7 @@
|
||||
--color-notice-font: var(--prim-gray-0);
|
||||
|
||||
// Callout
|
||||
--color-callout-info-border: var(--prim-gray-420);
|
||||
--color-callout-info-border: var(--prim-gray-670);
|
||||
--color-callout-info-background: var(--prim-gray-740);
|
||||
--color-callout-info-font: var(--prim-gray-0);
|
||||
--color-callout-success-border: var(--color-success);
|
||||
|
||||
@@ -110,6 +110,39 @@
|
||||
--color-sticky-background-7: var(--prim-gray-10);
|
||||
--color-sticky-border-7: var(--prim-gray-120);
|
||||
|
||||
// AI Assistant
|
||||
--color-askAssistant-button-background: var(--color-background-xlight);
|
||||
--color-askAssistant-button-background-hover: var(--color-background-xlight);
|
||||
--color-askAssistant-button-background-active: var(--color-background-xlight);
|
||||
|
||||
--color-assistant-highlight-1: #5b60e8;
|
||||
--color-assistant-highlight-2: #aa7bec;
|
||||
--color-assistant-highlight-3: #ec7b8e;
|
||||
--color-assistant-highlight-gradient: linear-gradient(
|
||||
105deg,
|
||||
var(--color-assistant-highlight-1) 0%,
|
||||
var(--color-assistant-highlight-2) 50%,
|
||||
var(--color-assistant-highlight-3) 100%
|
||||
);
|
||||
--color-assistant-highlight-reverse: linear-gradient(
|
||||
105deg,
|
||||
var(--color-assistant-highlight-3) 0%,
|
||||
var(--color-assistant-highlight-2) 50%,
|
||||
var(--color-assistant-highlight-1) 100%
|
||||
);
|
||||
--color-assistant-inner-highlight-hover: linear-gradient(
|
||||
108.82deg,
|
||||
rgba(236, 123, 142, 0.12) 0%,
|
||||
rgba(170, 123, 236, 0.12) 50.5%,
|
||||
rgba(91, 96, 232, 0.12) 100%
|
||||
);
|
||||
--color-assistant-inner-highlight-active: linear-gradient(
|
||||
108.82deg,
|
||||
rgba(236, 123, 142, 0.25) 0%,
|
||||
rgba(170, 123, 236, 0.25) 50.5%,
|
||||
rgba(91, 96, 232, 0.25) 100%
|
||||
);
|
||||
|
||||
// NodeIcon
|
||||
--color-node-icon-gray: var(--prim-gray-420);
|
||||
--color-node-icon-black: var(--prim-gray-780);
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
&.content-toast {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@include mixins.e(group) {
|
||||
margin-left: var.$notification-group-margin-left;
|
||||
margin-right: var.$notification-group-margin-right;
|
||||
|
||||
@@ -23,4 +23,27 @@ export default {
|
||||
'You can style with <a href="https://docs.n8n.io/workflows/sticky-notes/" target="_blank">Markdown</a>',
|
||||
'tags.showMore': (count: number) => `+${count} more`,
|
||||
'datatable.pageSize': 'Page size',
|
||||
'codeDiff.couldNotReplace': 'Could not replace code',
|
||||
'codeDiff.codeReplaced': 'Code replaced',
|
||||
'codeDiff.replaceMyCode': 'Replace my code',
|
||||
'codeDiff.replacing': 'Replacing...',
|
||||
'codeDiff.undo': 'Undo',
|
||||
'betaTag.beta': 'beta',
|
||||
'askAssistantButton.askAssistant': 'Ask Assistant',
|
||||
'assistantChat.errorParsingMarkdown': 'Error parsing markdown content',
|
||||
'assistantChat.aiAssistantLabel': 'AI Assistant',
|
||||
'assistantChat.aiAssistantName': 'Ava',
|
||||
'assistantChat.sessionEndMessage.1':
|
||||
'This Assistant session has ended. To start a new session with the Assistant, click an',
|
||||
'assistantChat.sessionEndMessage.2': 'button in n8n',
|
||||
'assistantChat.you': 'You',
|
||||
'assistantChat.quickRepliesTitle': 'Quick reply 👇',
|
||||
'assistantChat.placeholder.1': (options: string[]) =>
|
||||
`Hi ${options[0][0] || 'there'}, I'm ${options[0][1]} and I'm here to assist you with building workflows.`,
|
||||
'assistantChat.placeholder.2':
|
||||
"Whenever you encounter a task that I can help with, you'll see the",
|
||||
'assistantChat.placeholder.3': 'button.',
|
||||
'assistantChat.placeholder.4': 'Clicking it starts a chat session with me.',
|
||||
'assistantChat.inputPlaceholder': 'Enter your response...',
|
||||
'inlineAskAssistantButton.asked': 'Asked',
|
||||
} as N8nLocale;
|
||||
|
||||
69
packages/design-system/src/types/assistant.ts
Normal file
69
packages/design-system/src/types/assistant.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export namespace ChatUI {
|
||||
export interface TextMessage {
|
||||
role: 'assistant' | 'user';
|
||||
type: 'text';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface SummaryBlock {
|
||||
role: 'assistant';
|
||||
type: 'block';
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface CodeDiffMessage {
|
||||
role: 'assistant';
|
||||
type: 'code-diff';
|
||||
description?: string;
|
||||
codeDiff?: string;
|
||||
replacing?: boolean;
|
||||
replaced?: boolean;
|
||||
error?: boolean;
|
||||
suggestionId: string;
|
||||
}
|
||||
|
||||
interface EndSessionMessage {
|
||||
role: 'assistant';
|
||||
type: 'event';
|
||||
eventName: 'end-session';
|
||||
}
|
||||
|
||||
export interface QuickReply {
|
||||
type: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ErrorMessage {
|
||||
role: 'assistant';
|
||||
type: 'error';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface AgentSuggestionMessage {
|
||||
role: 'assistant';
|
||||
type: 'agent-suggestion';
|
||||
title: string;
|
||||
content: string;
|
||||
suggestionId: string;
|
||||
}
|
||||
|
||||
type MessagesWithReplies = (
|
||||
| TextMessage
|
||||
| CodeDiffMessage
|
||||
| SummaryBlock
|
||||
| AgentSuggestionMessage
|
||||
) & {
|
||||
quickReplies?: QuickReply[];
|
||||
};
|
||||
|
||||
export type AssistantMessage = (
|
||||
| MessagesWithReplies
|
||||
| ErrorMessage
|
||||
| EndSessionMessage
|
||||
| AgentSuggestionMessage
|
||||
) & {
|
||||
id: string;
|
||||
read: boolean;
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export type ButtonElement = (typeof BUTTON_ELEMENT)[number];
|
||||
const BUTTON_TYPE = ['primary', 'secondary', 'tertiary', 'success', 'warning', 'danger'] as const;
|
||||
export type ButtonType = (typeof BUTTON_TYPE)[number];
|
||||
|
||||
const BUTTON_SIZE = ['small', 'medium', 'large'] as const;
|
||||
const BUTTON_SIZE = ['mini', 'small', 'medium', 'large'] as const;
|
||||
export type ButtonSize = (typeof BUTTON_SIZE)[number];
|
||||
|
||||
const BUTTON_NATIVE_TYPE = ['submit', 'reset', 'button'] as const;
|
||||
|
||||
Reference in New Issue
Block a user