feat(editor): Implement AI Assistant chat UI (#9300)

This commit is contained in:
Milorad FIlipović
2024-05-07 15:43:19 +02:00
committed by GitHub
parent 23b676d7cb
commit 491c6ec546
28 changed files with 948 additions and 193 deletions

View File

@@ -210,12 +210,31 @@ The Chat window is entirely customizable using CSS variables.
--chat--window--width: 400px; --chat--window--width: 400px;
--chat--window--height: 600px; --chat--window--height: 600px;
--chat--header-height: auto;
--chat--header--padding: var(--chat--spacing);
--chat--header--background: var(--chat--color-dark);
--chat--header--color: var(--chat--color-light);
--chat--header--border-top: none;
--chat--header--border-bottom: none;
--chat--header--border-bottom: none;
--chat--header--border-bottom: none;
--chat--heading--font-size: 2em;
--chat--header--color: var(--chat--color-light);
--chat--subtitle--font-size: inherit;
--chat--subtitle--line-height: 1.8;
--chat--textarea--height: 50px; --chat--textarea--height: 50px;
--chat--message--font-size: 1rem;
--chat--message--padding: var(--chat--spacing);
--chat--message--border-radius: var(--chat--border-radius);
--chat--message-line-height: 1.8;
--chat--message--bot--background: var(--chat--color-white); --chat--message--bot--background: var(--chat--color-white);
--chat--message--bot--color: var(--chat--color-dark); --chat--message--bot--color: var(--chat--color-dark);
--chat--message--bot--border: none;
--chat--message--user--background: var(--chat--color-secondary); --chat--message--user--background: var(--chat--color-secondary);
--chat--message--user--color: var(--chat--color-white); --chat--message--user--color: var(--chat--color-white);
--chat--message--user--border: none;
--chat--message--pre--background: rgba(0, 0, 0, 0.05); --chat--message--pre--background: rgba(0, 0, 0, 0.05);
--chat--toggle--background: var(--chat--color-primary); --chat--toggle--background: var(--chat--color-primary);

View File

@@ -42,7 +42,7 @@
"highlight.js": "^11.8.0", "highlight.js": "^11.8.0",
"markdown-it-link-attributes": "^4.0.1", "markdown-it-link-attributes": "^4.0.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"vue": "^3.3.4", "vue": "^3.4.21",
"vue-markdown-render": "^2.1.1" "vue-markdown-render": "^2.1.1"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { nextTick, onMounted } from 'vue'; // eslint-disable-next-line import/no-unresolved
import Close from 'virtual:icons/mdi/close';
import { computed, nextTick, onMounted } from 'vue';
import Layout from '@n8n/chat/components/Layout.vue'; import Layout from '@n8n/chat/components/Layout.vue';
import GetStarted from '@n8n/chat/components/GetStarted.vue'; import GetStarted from '@n8n/chat/components/GetStarted.vue';
import GetStartedFooter from '@n8n/chat/components/GetStartedFooter.vue'; import GetStartedFooter from '@n8n/chat/components/GetStartedFooter.vue';
@@ -14,7 +16,12 @@ const chatStore = useChat();
const { messages, currentSessionId } = chatStore; const { messages, currentSessionId } = chatStore;
const { options } = useOptions(); const { options } = useOptions();
const showCloseButton = computed(() => options.mode === 'window' && options.showWindowCloseButton);
async function getStarted() { async function getStarted() {
if (!chatStore.startNewSession) {
return;
}
void chatStore.startNewSession(); void chatStore.startNewSession();
void nextTick(() => { void nextTick(() => {
chatEventBus.emit('scrollToBottom'); chatEventBus.emit('scrollToBottom');
@@ -22,12 +29,19 @@ async function getStarted() {
} }
async function initialize() { async function initialize() {
if (!chatStore.loadPreviousSession) {
return;
}
await chatStore.loadPreviousSession(); await chatStore.loadPreviousSession();
void nextTick(() => { void nextTick(() => {
chatEventBus.emit('scrollToBottom'); chatEventBus.emit('scrollToBottom');
}); });
} }
function closeChat() {
chatEventBus.emit('close');
}
onMounted(async () => { onMounted(async () => {
await initialize(); await initialize();
if (!options.showWelcomeScreen && !currentSessionId.value) { if (!options.showWelcomeScreen && !currentSessionId.value) {
@@ -39,8 +53,20 @@ onMounted(async () => {
<template> <template>
<Layout class="chat-wrapper"> <Layout class="chat-wrapper">
<template #header> <template #header>
<h1>{{ t('title') }}</h1> <div class="chat-heading">
<p>{{ t('subtitle') }}</p> <h1>
{{ t('title') }}
</h1>
<button
v-if="showCloseButton"
class="chat-close-button"
:title="t('closeButtonTooltip')"
@click="closeChat"
>
<Close height="18" width="18" />
</button>
</div>
<p v-if="t('subtitle')">{{ t('subtitle') }}</p>
</template> </template>
<GetStarted v-if="!currentSessionId && options.showWelcomeScreen" @click:button="getStarted" /> <GetStarted v-if="!currentSessionId && options.showWelcomeScreen" @click:button="getStarted" />
<MessagesList v-else :messages="messages" /> <MessagesList v-else :messages="messages" />
@@ -50,3 +76,22 @@ onMounted(async () => {
</template> </template>
</Layout> </Layout>
</template> </template>
<style lang="scss">
.chat-heading {
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-close-button {
display: flex;
border: none;
background: none;
cursor: pointer;
&:hover {
color: var(--chat--close--button--color-hover, var(--chat--color-primary));
}
}
</style>

View File

@@ -1,17 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import IconSend from 'virtual:icons/mdi/send'; import IconSend from 'virtual:icons/mdi/send';
import { computed, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useI18n, useChat } from '@n8n/chat/composables'; import { useI18n, useChat, useOptions } from '@n8n/chat/composables';
import { chatEventBus } from '@n8n/chat/event-buses';
const { options } = useOptions();
const chatStore = useChat(); const chatStore = useChat();
const { waitingForResponse } = chatStore; const { waitingForResponse } = chatStore;
const { t } = useI18n(); const { t } = useI18n();
const chatTextArea = ref<HTMLTextAreaElement | null>(null);
const input = ref(''); const input = ref('');
const isSubmitDisabled = computed(() => { const isSubmitDisabled = computed(() => {
return input.value === '' || waitingForResponse.value; return input.value === '' || waitingForResponse.value || options.disabled?.value === true;
});
const isInputDisabled = computed(() => options.disabled?.value === true);
onMounted(() => {
chatEventBus.on('focusInput', () => {
if (chatTextArea.value) {
chatTextArea.value.focus();
}
});
}); });
async function onSubmit(event: MouseEvent | KeyboardEvent) { async function onSubmit(event: MouseEvent | KeyboardEvent) {
@@ -38,8 +51,10 @@ async function onSubmitKeydown(event: KeyboardEvent) {
<template> <template>
<div class="chat-input"> <div class="chat-input">
<textarea <textarea
ref="chatTextArea"
v-model="input" v-model="input"
rows="1" rows="1"
:disabled="isInputDisabled"
:placeholder="t('inputPlaceholder')" :placeholder="t('inputPlaceholder')"
@keydown.enter="onSubmitKeydown" @keydown.enter="onSubmitKeydown"
/> />
@@ -55,10 +70,11 @@ async function onSubmitKeydown(event: KeyboardEvent) {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 100%; width: 100%;
background: white;
textarea { textarea {
font-family: inherit; font-family: inherit;
font-size: inherit; font-size: var(--chat--input--font-size, inherit);
width: 100%; width: 100%;
border: 0; border: 0;
padding: var(--chat--spacing); padding: var(--chat--spacing);
@@ -71,7 +87,7 @@ async function onSubmitKeydown(event: KeyboardEvent) {
width: var(--chat--textarea--height); width: var(--chat--textarea--height);
background: white; background: white;
cursor: pointer; cursor: pointer;
color: var(--chat--color-secondary); color: var(--chat--input--send--button--color, var(--chat--color-secondary));
border: 0; border: 0;
font-size: 24px; font-size: 24px;
display: inline-flex; display: inline-flex;

View File

@@ -58,9 +58,26 @@ onBeforeUnmount(() => {
); );
.chat-header { .chat-header {
display: flex;
flex-direction: column;
justify-content: center;
gap: 1em;
height: var(--chat--header-height, auto);
padding: var(--chat--header--padding, var(--chat--spacing)); padding: var(--chat--header--padding, var(--chat--spacing));
background: var(--chat--header--background, var(--chat--color-dark)); background: var(--chat--header--background, var(--chat--color-dark));
color: var(--chat--header--color, var(--chat--color-light)); color: var(--chat--header--color, var(--chat--color-light));
border-top: var(--chat--header--border-top, none);
border-bottom: var(--chat--header--border-bottom, none);
border-left: var(--chat--header--border-left, none);
border-right: var(--chat--header--border-right, none);
h1 {
font-size: var(--chat--heading--font-size);
color: var(--chat--header--color, var(--chat--color-light));
}
p {
font-size: var(--chat--subtitle--font-size, inherit);
line-height: var(--chat--subtitle--line-height, 1.8);
}
} }
.chat-body { .chat-body {

View File

@@ -6,7 +6,8 @@ import VueMarkdown from 'vue-markdown-render';
import hljs from 'highlight.js/lib/core'; import hljs from 'highlight.js/lib/core';
import markdownLink from 'markdown-it-link-attributes'; import markdownLink from 'markdown-it-link-attributes';
import type MarkdownIt from 'markdown-it'; import type MarkdownIt from 'markdown-it';
import type { ChatMessage } from '@n8n/chat/types'; import type { ChatMessage, ChatMessageText } from '@n8n/chat/types';
import { useOptions } from '@n8n/chat/composables';
const props = defineProps({ const props = defineProps({
message: { message: {
@@ -16,15 +17,17 @@ const props = defineProps({
}); });
const { message } = toRefs(props); const { message } = toRefs(props);
const { options } = useOptions();
const messageText = computed(() => { const messageText = computed(() => {
return message.value.text || '&lt;Empty response&gt;'; return (message.value as ChatMessageText).text || '&lt;Empty response&gt;';
}); });
const classes = computed(() => { const classes = computed(() => {
return { return {
'chat-message-from-user': message.value.sender === 'user', 'chat-message-from-user': message.value.sender === 'user',
'chat-message-from-bot': message.value.sender === 'bot', 'chat-message-from-bot': message.value.sender === 'bot',
'chat-message-transparent': message.value.transparent === true,
}; };
}); });
@@ -48,11 +51,17 @@ const markdownOptions = {
return ''; // use external default escaping return ''; // use external default escaping
}, },
}; };
const messageComponents = options.messageComponents ?? {};
</script> </script>
<template> <template>
<div class="chat-message" :class="classes"> <div class="chat-message" :class="classes">
<slot> <slot>
<template v-if="message.type === 'component' && messageComponents[message.key]">
<component :is="messageComponents[message.key]" v-bind="message.arguments" />
</template>
<VueMarkdown <VueMarkdown
v-else
class="chat-message-markdown" class="chat-message-markdown"
:source="messageText" :source="messageText"
:options="markdownOptions" :options="markdownOptions"
@@ -66,21 +75,40 @@ const markdownOptions = {
.chat-message { .chat-message {
display: block; display: block;
max-width: 80%; max-width: 80%;
font-size: var(--chat--message--font-size, 1rem);
padding: var(--chat--message--padding, var(--chat--spacing)); padding: var(--chat--message--padding, var(--chat--spacing));
border-radius: var(--chat--message--border-radius, var(--chat--border-radius)); border-radius: var(--chat--message--border-radius, var(--chat--border-radius));
p {
line-height: var(--chat--message-line-height, 1.8);
word-wrap: break-word;
}
// Default message gap is half of the spacing
+ .chat-message { + .chat-message {
margin-top: var(--chat--message--margin-bottom, calc(var(--chat--spacing) * 0.5)); margin-top: var(--chat--message--margin-bottom, calc(var(--chat--spacing) * 0.5));
} }
// Spacing between messages from different senders is double the individual message gap
&.chat-message-from-user + &.chat-message-from-bot,
&.chat-message-from-bot + &.chat-message-from-user {
margin-top: var(--chat--spacing);
}
&.chat-message-from-bot { &.chat-message-from-bot {
&:not(.chat-message-transparent) {
background-color: var(--chat--message--bot--background); background-color: var(--chat--message--bot--background);
border: var(--chat--message--bot--border, none);
}
color: var(--chat--message--bot--color); color: var(--chat--message--bot--color);
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }
&.chat-message-from-user { &.chat-message-from-user {
&:not(.chat-message-transparent) {
background-color: var(--chat--message--user--background); background-color: var(--chat--message--user--background);
border: var(--chat--message--user--border, none);
}
color: var(--chat--message--user--color); color: var(--chat--message--user--color);
margin-left: auto; margin-left: auto;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;

View File

@@ -1,3 +1,4 @@
import { isRef } from 'vue';
import { useOptions } from '@n8n/chat/composables/useOptions'; import { useOptions } from '@n8n/chat/composables/useOptions';
export function useI18n() { export function useI18n() {
@@ -5,7 +6,11 @@ export function useI18n() {
const language = options?.defaultLanguage ?? 'en'; const language = options?.defaultLanguage ?? 'en';
function t(key: string): string { function t(key: string): string {
return options?.i18n?.[language]?.[key] ?? key; const val = options?.i18n?.[language]?.[key];
if (isRef(val)) {
return val.value as string;
}
return val ?? key;
} }
function te(key: string): boolean { function te(key: string): boolean {

View File

@@ -21,6 +21,7 @@ export const defaultOptions: ChatOptions = {
footer: '', footer: '',
getStarted: 'New Conversation', getStarted: 'New Conversation',
inputPlaceholder: 'Type your question..', inputPlaceholder: 'Type your question..',
closeButtonTooltip: 'Close chat',
}, },
}, },
theme: {}, theme: {},

View File

@@ -33,4 +33,6 @@
--chat--toggle--active--background: var(--chat--color-primary-shade-100); --chat--toggle--active--background: var(--chat--color-primary-shade-100);
--chat--toggle--color: var(--chat--color-white); --chat--toggle--color: var(--chat--color-white);
--chat--toggle--size: 64px; --chat--toggle--size: 64px;
--chat--heading--font-size: 2em;
} }

View File

@@ -6,7 +6,7 @@ export interface Chat {
messages: Ref<ChatMessage[]>; messages: Ref<ChatMessage[]>;
currentSessionId: Ref<string | null>; currentSessionId: Ref<string | null>;
waitingForResponse: Ref<boolean>; waitingForResponse: Ref<boolean>;
loadPreviousSession: () => Promise<string | undefined>; loadPreviousSession?: () => Promise<string | undefined>;
startNewSession: () => Promise<void>; startNewSession?: () => Promise<void>;
sendMessage: (text: string) => Promise<void>; sendMessage: (text: string) => Promise<void>;
} }

View File

@@ -1,6 +1,19 @@
export interface ChatMessage { export type ChatMessage<T = Record<string, unknown>> = ChatMessageComponent<T> | ChatMessageText;
id: string;
export interface ChatMessageComponent<T = Record<string, unknown>> extends ChatMessageBase {
type: 'component';
key: string;
arguments: T;
}
export interface ChatMessageText extends ChatMessageBase {
type?: 'text';
text: string; text: string;
}
interface ChatMessageBase {
id: string;
createdAt: string; createdAt: string;
transparent?: boolean;
sender: 'user' | 'bot'; sender: 'user' | 'bot';
} }

View File

@@ -1,3 +1,4 @@
import type { Component, Ref } from 'vue';
export interface ChatOptions { export interface ChatOptions {
webhookUrl: string; webhookUrl: string;
webhookConfig?: { webhookConfig?: {
@@ -6,6 +7,7 @@ export interface ChatOptions {
}; };
target?: string | Element; target?: string | Element;
mode?: 'window' | 'fullscreen'; mode?: 'window' | 'fullscreen';
showWindowCloseButton?: boolean;
showWelcomeScreen?: boolean; showWelcomeScreen?: boolean;
loadPreviousSession?: boolean; loadPreviousSession?: boolean;
chatInputKey?: string; chatInputKey?: string;
@@ -21,8 +23,11 @@ export interface ChatOptions {
footer: string; footer: string;
getStarted: string; getStarted: string;
inputPlaceholder: string; inputPlaceholder: string;
closeButtonTooltip: string;
[message: string]: string; [message: string]: string;
} }
>; >;
theme?: {}; theme?: {};
messageComponents?: Record<string, Component>;
disabled?: Ref<boolean>;
} }

View File

@@ -26,6 +26,9 @@
<component :is="Component" v-else /> <component :is="Component" v-else />
</router-view> </router-view>
</div> </div>
<div id="chat" :class="{ [$style.chat]: true, [$style.open]: aiStore.assistantChatOpen }">
<AIAssistantChat v-if="aiStore.assistantChatOpen" />
</div>
<Modals /> <Modals />
<Telemetry /> <Telemetry />
</div> </div>
@@ -59,6 +62,8 @@ import { useUsersStore } from '@/stores/users.store';
import { useHistoryHelper } from '@/composables/useHistoryHelper'; import { useHistoryHelper } from '@/composables/useHistoryHelper';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { initializeAuthenticatedFeatures } from '@/init'; import { initializeAuthenticatedFeatures } from '@/init';
import { useAIStore } from './stores/ai.store';
import AIAssistantChat from './components/AIAssistantChat/AIAssistantChat.vue';
export default defineComponent({ export default defineComponent({
name: 'App', name: 'App',
@@ -67,6 +72,7 @@ export default defineComponent({
LoadingView, LoadingView,
Telemetry, Telemetry,
Modals, Modals,
AIAssistantChat,
}, },
mixins: [userHelpers], mixins: [userHelpers],
setup() { setup() {
@@ -88,6 +94,7 @@ export default defineComponent({
useSourceControlStore, useSourceControlStore,
useCloudPlanStore, useCloudPlanStore,
useUsageStore, useUsageStore,
useAIStore,
), ),
defaultLocale(): string { defaultLocale(): string {
return this.rootStore.defaultLocale; return this.rootStore.defaultLocale;
@@ -140,10 +147,10 @@ export default defineComponent({
.container { .container {
display: grid; display: grid;
grid-template-areas: grid-template-areas:
'banners banners' 'banners banners banners'
'sidebar header' 'sidebar header chat'
'sidebar content'; 'sidebar content chat';
grid-auto-columns: fit-content($sidebar-expanded-width) 1fr; grid-auto-columns: fit-content($sidebar-expanded-width) 1fr fit-content($chat-width);
grid-template-rows: auto fit-content($header-height) 1fr; grid-template-rows: auto fit-content($header-height) 1fr;
height: 100vh; height: 100vh;
} }
@@ -177,4 +184,15 @@ export default defineComponent({
height: 100%; height: 100%;
z-index: 999; z-index: 999;
} }
.chat {
grid-area: chat;
z-index: 999;
height: 100%;
width: 0;
transition: all 0.2s ease-in-out;
&.open {
width: $chat-width;
}
}
</style> </style>

View File

@@ -55,6 +55,7 @@ import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers';
import type { Component } from 'vue'; import type { Component } from 'vue';
import type { Scope } from '@n8n/permissions'; import type { Scope } from '@n8n/permissions';
import type { NotificationOptions as ElementNotificationOptions } from 'element-plus'; import type { NotificationOptions as ElementNotificationOptions } from 'element-plus';
import type { Connection } from '@jsplumb/core';
export * from 'n8n-design-system/types'; export * from 'n8n-design-system/types';
@@ -1894,3 +1895,17 @@ export type SuggestedTemplatesWorkflowPreview = {
preview: IWorkflowData; preview: IWorkflowData;
nodes: Array<Pick<ITemplatesNode, 'id' | 'displayName' | 'icon' | 'defaults' | 'iconData'>>; nodes: Array<Pick<ITemplatesNode, 'id' | 'displayName' | 'icon' | 'defaults' | 'iconData'>>;
}; };
export type NewConnectionInfo = {
sourceId: string;
index: number;
eventSource: NodeCreatorOpenSource;
connection?: Connection;
nodeCreatorView?: string;
outputType?: NodeConnectionType;
endpointUuid?: string;
};
export type AIAssistantConnectionInfo = NewConnectionInfo & {
stepName: string;
};

View File

@@ -0,0 +1,227 @@
<script setup lang="ts">
import { useI18n } from '@/composables/useI18n';
import { useUsersStore } from '@/stores/users.store';
import ChatComponent from '@n8n/chat/components/Chat.vue';
import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants';
import type { Chat, ChatMessage, ChatOptions } from '@n8n/chat/types';
import type { Ref } from 'vue';
import { computed, provide, ref } from 'vue';
import QuickReplies from './QuickReplies.vue';
import { DateTime } from 'luxon';
import { useAIStore } from '@/stores/ai.store';
import { chatEventBus } from '@n8n/chat/event-buses';
import { onMounted } from 'vue';
import {
AI_ASSISTANT_EXPERIMENT_URLS,
AI_ASSISTANT_LOCAL_STORAGE_KEY,
MODAL_CONFIRM,
} from '@/constants';
import { useStorage } from '@/composables/useStorage';
import { useMessage } from '@/composables/useMessage';
import { useTelemetry } from '@/composables/useTelemetry';
import { onBeforeUnmount } from 'vue';
const locale = useI18n();
const telemetry = useTelemetry();
const { confirm } = useMessage();
const usersStore = useUsersStore();
const aiStore = useAIStore();
const messages: Ref<ChatMessage[]> = ref([]);
const waitingForResponse = ref(false);
const currentSessionId = ref<string>(String(Date.now()));
const disableChat = ref(false);
const userName = computed(() => usersStore.currentUser?.firstName ?? 'there');
const latestConnectionInfo = computed(() => aiStore.latestConnectionInfo);
const chatTitle = locale.baseText('aiAssistantChat.title');
const nowMilliseconds = () => String(DateTime.now().toMillis());
const nowIsoString = () => new Date().toISOString();
const thanksResponses: ChatMessage[] = [
{
id: nowMilliseconds(),
sender: 'bot',
text: locale.baseText('aiAssistantChat.response.message1'),
createdAt: nowIsoString(),
},
{
id: nowMilliseconds(),
sender: 'bot',
text: locale.baseText('aiAssistantChat.response.message2'),
createdAt: nowIsoString(),
},
{
id: nowMilliseconds(),
sender: 'bot',
text: '🙏',
createdAt: new Date().toISOString(),
},
{
id: nowMilliseconds(),
type: 'component',
key: 'QuickReplies',
sender: 'user',
createdAt: nowIsoString(),
transparent: true,
arguments: {
suggestions: [
{ label: locale.baseText('aiAssistantChat.response.quickReply.close'), key: 'close' },
{
label: locale.baseText('aiAssistantChat.response.quickReply.giveFeedback'),
key: 'give_feedback',
},
{
label: locale.baseText('aiAssistantChat.response.quickReply.signUp'),
key: 'sign_up',
},
],
onReplySelected: ({ key }: { key: string; label: string }) => {
switch (key) {
case 'give_feedback':
window.open(AI_ASSISTANT_EXPERIMENT_URLS.FEEDBACK_FORM, '_blank');
break;
case 'sign_up':
window.open(AI_ASSISTANT_EXPERIMENT_URLS.SIGN_UP, '_blank');
break;
}
aiStore.assistantChatOpen = false;
},
},
},
];
const initialMessageText = computed(() => {
if (latestConnectionInfo.value) {
return locale.baseText('aiAssistantChat.initialMessage.nextStep', {
interpolate: { currentAction: latestConnectionInfo.value.stepName },
});
}
return locale.baseText('aiAssistantChat.initialMessage.firstStep');
});
const initialMessages: Ref<ChatMessage[]> = ref([
{
id: '1',
type: 'text',
sender: 'bot',
createdAt: new Date().toISOString(),
text: `${locale.baseText('aiAssistantChat.greeting', { interpolate: { username: userName.value ?? 'there' } })} ${initialMessageText.value}`,
},
]);
const sendMessage = async (message: string) => {
disableChat.value = true;
waitingForResponse.value = true;
messages.value.push({
id: String(messages.value.length + 1),
sender: 'user',
text: message,
createdAt: new Date().toISOString(),
});
trackUserMessage(message);
thanksResponses.forEach((response, index) => {
// Push each response with a delay of 1500ms
setTimeout(
() => {
messages.value.push(response);
chatEventBus.emit('scrollToBottom');
if (index === thanksResponses.length - 1) {
waitingForResponse.value = false;
// Once last message is sent, disable the experiment
useStorage(AI_ASSISTANT_LOCAL_STORAGE_KEY).value = 'true';
}
},
1500 * (index + 1),
);
});
chatEventBus.emit('scrollToBottom');
};
const trackUserMessage = (message: string) => {
telemetry.track('User responded in AI chat', {
prompt: message,
chatMode: 'nextStepAssistant',
initialMessage: initialMessageText.value,
});
};
const chatOptions: ChatOptions = {
i18n: {
en: {
title: chatTitle,
footer: '',
subtitle: '',
inputPlaceholder: locale.baseText('aiAssistantChat.chatPlaceholder'),
getStarted: locale.baseText('aiAssistantChat.getStarted'),
closeButtonTooltip: locale.baseText('aiAssistantChat.closeButtonTooltip'),
},
},
webhookUrl: 'https://webhook.url',
mode: 'window',
showWindowCloseButton: true,
messageComponents: {
QuickReplies,
},
disabled: disableChat,
};
const chatConfig: Chat = {
messages,
sendMessage,
initialMessages,
currentSessionId,
waitingForResponse,
};
provide(ChatSymbol, chatConfig);
provide(ChatOptionsSymbol, chatOptions);
onMounted(() => {
chatEventBus.emit('focusInput');
chatEventBus.on('close', onBeforeClose);
});
onBeforeUnmount(() => {
chatEventBus.off('close', onBeforeClose);
});
async function onBeforeClose() {
const confirmModal = await confirm(locale.baseText('aiAssistantChat.closeChatConfirmation'), {
confirmButtonText: locale.baseText('aiAssistantChat.closeChatConfirmation.confirm'),
cancelButtonText: locale.baseText('aiAssistantChat.closeChatConfirmation.cancel'),
});
if (confirmModal === MODAL_CONFIRM) {
aiStore.assistantChatOpen = false;
}
}
</script>
<template>
<div :class="[$style.container, 'ignore-key-press']">
<ChatComponent />
</div>
</template>
<style module lang="scss">
.container {
height: 100%;
background-color: var(--color-background-light);
filter: drop-shadow(0px 8px 24px #41424412);
border-left: 1px solid var(--color-foreground-dark);
overflow: hidden;
}
.header {
font-size: var(--font-size-l);
background-color: #fff;
padding: var(--spacing-xs);
}
.content {
padding: var(--spacing-xs);
}
</style>

View File

@@ -0,0 +1,148 @@
<script setup lang="ts">
import { useAIStore } from '@/stores/ai.store';
import { useI18n } from '@/composables/useI18n';
import { computed } from 'vue';
import { useTelemetry } from '@/composables/useTelemetry';
const aiStore = useAIStore();
const locale = useI18n();
const telemetry = useTelemetry();
const emit = defineEmits<{ (event: 'optionSelected', option: string): void }>();
const aiAssistantChatOpen = computed(() => aiStore.assistantChatOpen);
const title = computed(() => {
return aiStore.nextStepPopupConfig.title;
});
const options = computed(() => [
{
label: locale.baseText('nextStepPopup.option.choose'),
icon: '',
key: 'choose',
disabled: false,
},
{
label: locale.baseText('nextStepPopup.option.generate'),
icon: '✨',
key: 'generate',
disabled: aiAssistantChatOpen.value,
},
]);
const position = computed(() => {
return [aiStore.nextStepPopupConfig.position[0], aiStore.nextStepPopupConfig.position[1]];
});
const style = computed(() => {
return {
left: `${position.value[0]}px`,
top: `${position.value[1]}px`,
};
});
const close = () => {
aiStore.closeNextStepPopup();
};
const onOptionSelected = (option: string) => {
if (option === 'choose') {
emit('optionSelected', option);
} else if (option === 'generate') {
telemetry.track('User clicked generate AI button', {}, { withPostHog: true });
aiStore.assistantChatOpen = true;
}
close();
};
</script>
<template>
<div v-on-click-outside="close" :class="$style.container" :style="style">
<div :class="$style.title">{{ title }}</div>
<ul :class="$style.options">
<li
v-for="option in options"
:key="option.key"
:class="{ [$style.option]: true, [$style.disabled]: option.disabled }"
@click="onOptionSelected(option.key)"
>
<div :class="$style.icon">
{{ option.icon }}
</div>
<div :class="$style.label">
{{ option.label }}
</div>
</li>
</ul>
</div>
</template>
<style module lang="scss">
.container {
position: fixed;
display: flex;
flex-direction: column;
min-width: 190px;
font-size: var(--font-size-2xs);
background: var(--color-background-xlight);
filter: drop-shadow(0px 6px 16px #441c170f);
border: var(--border-width-base) var(--border-style-base) var(--color-foreground-light);
border-radius: var(--border-radius-base);
// Arrow border is created as the outer triange
&:before {
content: '';
position: relative;
left: -11px;
top: calc(50% - 8px);
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-right: 10px solid var(--color-foreground-light);
position: absolute;
}
// Arrow background is created as the inner triangle
&:after {
content: '';
position: relative;
left: -10px;
top: calc(50% - 8px);
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-right: 10px solid var(--color-background-xlight);
position: absolute;
}
}
.title {
padding: var(--spacing-xs);
color: var(--color-text-base);
font-weight: var(--font-weight-bold);
}
.options {
list-style: none;
display: flex;
flex-direction: column;
padding-bottom: var(--spacing-2xs);
}
.option {
display: flex;
padding: var(--spacing-3xs) var(--spacing-xs);
gap: var(--spacing-xs);
cursor: pointer;
color: var(--color-text-dark);
&:hover {
background: var(--color-background-base);
font-weight: var(--font-weight-bold);
}
&.disabled {
pointer-events: none;
color: var(--color-text-light);
}
}
</style>

View File

@@ -0,0 +1,66 @@
<script setup lang="ts">
import { useI18n } from '@/composables/useI18n';
import Button from 'n8n-design-system/components/N8nButton/Button.vue';
type QuickReply = {
label: string;
key: string;
};
const locale = useI18n();
const emit = defineEmits<{
(event: 'replySelected', value: QuickReply): void;
}>();
defineProps<{
suggestions: QuickReply[];
}>();
function onButtonClick(action: QuickReply) {
emit('replySelected', action);
}
</script>
<template>
<div :class="$style.container">
<p :class="$style.hint">{{ locale.baseText('aiAssistantChat.quickReply.title') }}</p>
<div :class="$style.suggestions">
<Button
v-for="action in suggestions"
:key="action.key"
:class="$style.replyButton"
outline
type="secondary"
@click="onButtonClick(action)"
>
{{ action.label }}
</Button>
</div>
</div>
</template>
<style module lang="scss">
.container {
display: flex;
flex-direction: column;
gap: var(--spacing-2xs);
width: auto;
justify-content: flex-end;
align-items: flex-end;
}
.suggestions {
display: flex;
flex-direction: column;
width: fit-content;
gap: var(--spacing-4xs);
}
.hint {
color: var(--color-text-base);
font-size: var(--font-size-2xs);
}
.replyButton {
display: flex;
background: var(--chat--color-white);
}
</style>

View File

@@ -94,7 +94,6 @@ onBeforeUnmount(() => {
<style lang="scss" module> <style lang="scss" module>
.zoomMenu { .zoomMenu {
position: absolute; position: absolute;
width: 210px;
bottom: var(--spacing-l); bottom: var(--spacing-l);
left: var(--spacing-l); left: var(--spacing-l);
line-height: 25px; line-height: 25px;

View File

@@ -1,11 +1,16 @@
<template> <template>
<div> <div>
<aside :class="{ [$style.nodeCreatorScrim]: true, [$style.active]: showScrim }" /> <aside
:class="{
[$style.nodeCreatorScrim]: true,
[$style.active]: showScrim,
}"
/>
<SlideTransition> <SlideTransition>
<div <div
v-if="active" v-if="active"
ref="nodeCreator" ref="nodeCreator"
:class="$style.nodeCreator" :class="{ [$style.nodeCreator]: true, [$style.chatOpened]: chatSidebarOpen }"
:style="nodeCreatorInlineStyle" :style="nodeCreatorInlineStyle"
data-test-id="node-creator" data-test-id="node-creator"
@dragover="onDragOver" @dragover="onDragOver"
@@ -32,6 +37,7 @@ import { useActionsGenerator } from './composables/useActionsGeneration';
import NodesListPanel from './Panel/NodesListPanel.vue'; import NodesListPanel from './Panel/NodesListPanel.vue';
import { useCredentialsStore } from '@/stores/credentials.store'; import { useCredentialsStore } from '@/stores/credentials.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useAIStore } from '@/stores/ai.store';
import { DRAG_EVENT_DATA_KEY } from '@/constants'; import { DRAG_EVENT_DATA_KEY } from '@/constants';
export interface Props { export interface Props {
@@ -47,6 +53,7 @@ const emit = defineEmits<{
(event: 'nodeTypeSelected', value: string[]): void; (event: 'nodeTypeSelected', value: string[]): void;
}>(); }>();
const uiStore = useUIStore(); const uiStore = useUIStore();
const aiStore = useAIStore();
const { setShowScrim, setActions, setMergeNodes } = useNodeCreatorStore(); const { setShowScrim, setActions, setMergeNodes } = useNodeCreatorStore();
const { generateMergedNodesAndActions } = useActionsGenerator(); const { generateMergedNodesAndActions } = useActionsGenerator();
@@ -60,6 +67,8 @@ const showScrim = computed(() => useNodeCreatorStore().showScrim);
const viewStacksLength = computed(() => useViewStacks().viewStacks.length); const viewStacksLength = computed(() => useViewStacks().viewStacks.length);
const chatSidebarOpen = computed(() => aiStore.assistantChatOpen);
const nodeCreatorInlineStyle = computed(() => { const nodeCreatorInlineStyle = computed(() => {
return { top: `${uiStore.bannersHeight + uiStore.headerHeight}px` }; return { top: `${uiStore.bannersHeight + uiStore.headerHeight}px` };
}); });
@@ -168,6 +177,10 @@ onBeforeUnmount(() => {
z-index: 200; z-index: 200;
width: $node-creator-width; width: $node-creator-width;
color: $node-creator-text-color; color: $node-creator-text-color;
&.chatOpened {
right: $chat-width;
}
} }
.nodeCreatorScrim { .nodeCreatorScrim {

View File

@@ -643,7 +643,17 @@ export const ASK_AI_EXPERIMENT = {
export const TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT = '017_template_credential_setup_v2'; export const TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT = '017_template_credential_setup_v2';
export const EXPERIMENTS_TO_TRACK = [ASK_AI_EXPERIMENT.name, TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT]; export const AI_ASSISTANT_EXPERIMENT = {
name: '19_ai_assistant_experiment',
control: 'control',
variant: 'variant',
};
export const EXPERIMENTS_TO_TRACK = [
ASK_AI_EXPERIMENT.name,
TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT,
AI_ASSISTANT_EXPERIMENT.name,
];
export const MFA_AUTHENTICATION_REQUIRED_ERROR_CODE = 998; export const MFA_AUTHENTICATION_REQUIRED_ERROR_CODE = 998;
@@ -795,3 +805,10 @@ export const INSECURE_CONNECTION_WARNING = `
</ul> </ul>
</div> </div>
</body>`; </body>`;
export const AI_ASSISTANT_EXPERIMENT_URLS = {
FEEDBACK_FORM: 'https://chat.arro.co/to4639rATEMV',
SIGN_UP: 'https://adore.app.n8n.cloud/form/4704cce3-4cef-4dc8-b67f-8a510c5d561a',
};
export const AI_ASSISTANT_LOCAL_STORAGE_KEY = 'N8N_AI_ASSISTANT_EXPERIMENT';

View File

@@ -36,6 +36,7 @@ $warning-tooltip-color: var(--color-danger);
// sass variable is used for scss files // sass variable is used for scss files
$header-height: calc(var(--header-height) * 1px); $header-height: calc(var(--header-height) * 1px);
$chat-width: calc(var(--chat-width) * 1px);
// sidebar // sidebar
$sidebar-width: 65px; $sidebar-width: 65px;

View File

@@ -171,10 +171,34 @@
var(--node-type-background-l) var(--node-type-background-l)
); );
--chat--spacing: var(--spacing-s);
// Using native css variable enables us to use this value in JS // Using native css variable enables us to use this value in JS
--header-height: 65; --header-height: 65;
--chat-width: 350;
// n8n-chat customizations
--chat--spacing: var(--spacing-2xs);
--chat--header-height: calc(var(--header-height) * 1px);
--chat--header--padding: 0 var(--spacing-xs);
--chat--heading--font-size: var(--font-size-m);
--chat--subtitle--font-size: var(--font-size-s);
--chat--subtitle--line-height: var(--font-line-height-base);
--chat--header--background: var(--color-background-xlight);
--chat--header--color: var(--color-text-dark);
--chat--header--border-bottom: var(--border-base);
--chat--close--button--color-hover: var(--color-primary);
// Message styles
--chat--message--padding: var(--spacing-3xs);
--chat--message--font-size: 14px;
--chat--message-line-height: 1.5;
--chat--message--bot--border: 1px solid var(--color-foreground-light);
--chat--message--bot--color: var(--color-text-dark);
--chat--message--user--color: var(--color-text-dark);
--chat--message--user--background: var(--color-success-tint-1);
--chat--message--user--border: 1px solid var(--color-success-light-2);
// Chat input
--chat--input--font-size: var(--font-size-s);
--chat--input--send--button--color: var(--color-success);
} }
.clickable { .clickable {

View File

@@ -88,6 +88,22 @@
"activationModal.yourTriggersWillNowFire": "Your triggers will now fire production executions automatically.", "activationModal.yourTriggersWillNowFire": "Your triggers will now fire production executions automatically.",
"activationModal.yourWorkflowWillNowListenForEvents": "Your workflow will now listen for events from {serviceName} and trigger executions.", "activationModal.yourWorkflowWillNowListenForEvents": "Your workflow will now listen for events from {serviceName} and trigger executions.",
"activationModal.yourWorkflowWillNowRegularlyCheck": "Your workflow will now regularly check {serviceName} for events and trigger executions for them.", "activationModal.yourWorkflowWillNowRegularlyCheck": "Your workflow will now regularly check {serviceName} for events and trigger executions for them.",
"aiAssistantChat.title": "✨ Generate workflow step with AI",
"aiAssistantChat.greeting": "Hi {username}!",
"aiAssistantChat.chatPlaceholder": "Enter your response...",
"aiAssistantChat.getStarted": "Get started",
"aiAssistantChat.initialMessage.firstStep": "What should the first step in your workflow do?",
"aiAssistantChat.initialMessage.nextStep": "Can you describe the next step after the __{currentAction}__ action in your workflow?",
"aiAssistantChat.response.message1": "Thanks for trying our new __Generate Workflow Step__ feature. Currently, the feature is not ready yet. We're gathering real-world prompts like yours to ensure we're creating a high-quality, valuable feature.",
"aiAssistantChat.response.message2": "We understand this may be disappointing, but we believe it's crucial to developing the best possible feature for you and others. Wed love to invite you to be one of the first users to get their hands on the real feature once its ready.",
"aiAssistantChat.response.quickReply.close": "Close chat thread",
"aiAssistantChat.response.quickReply.signUp": "Sign up for early access",
"aiAssistantChat.response.quickReply.giveFeedback": "Give feedback to product team",
"aiAssistantChat.closeButtonTooltip": "Close chat",
"aiAssistantChat.closeChatConfirmation": "Are you sure you want to end this chat session?",
"aiAssistantChat.closeChatConfirmation.confirm": "Yes, close it",
"aiAssistantChat.closeChatConfirmation.cancel": "No, stay",
"aiAssistantChat.quickReply.title": "Quick reply 👇",
"auth.changePassword": "Change password", "auth.changePassword": "Change password",
"auth.changePassword.currentPassword": "Current password", "auth.changePassword.currentPassword": "Current password",
"auth.changePassword.error": "Problem changing the password", "auth.changePassword.error": "Problem changing the password",
@@ -873,6 +889,10 @@
"ndv.httpRequest.credentialOnly.docsNotice": "Use the <a target=\"_blank\" href=\"{docsUrl}\">{nodeName} docs</a> to construct your request. We'll take care of the authentication part if you add a {nodeName} credential below.", "ndv.httpRequest.credentialOnly.docsNotice": "Use the <a target=\"_blank\" href=\"{docsUrl}\">{nodeName} docs</a> to construct your request. We'll take care of the authentication part if you add a {nodeName} credential below.",
"noTagsView.readyToOrganizeYourWorkflows": "Ready to organize your workflows?", "noTagsView.readyToOrganizeYourWorkflows": "Ready to organize your workflows?",
"noTagsView.withWorkflowTagsYouReFree": "With workflow tags, you're free to create the perfect tagging system for your flows", "noTagsView.withWorkflowTagsYouReFree": "With workflow tags, you're free to create the perfect tagging system for your flows",
"nextStepPopup.title.firstStep": "What triggers this workflow?",
"nextStepPopup.title.nextStep": "What happens next?",
"nextStepPopup.option.choose": "Choose from list...",
"nextStepPopup.option.generate": "Generate step with AI...",
"node.thisIsATriggerNode": "This is a Trigger node. <a target=\"_blank\" href=\"https://docs.n8n.io/workflows/components/nodes/\">Learn more</a>", "node.thisIsATriggerNode": "This is a Trigger node. <a target=\"_blank\" href=\"https://docs.n8n.io/workflows/components/nodes/\">Learn more</a>",
"node.activateDeactivateNode": "Activate/Deactivate Node", "node.activateDeactivateNode": "Activate/Deactivate Node",
"node.changeColor": "Change color", "node.changeColor": "Change color",

View File

@@ -3,14 +3,53 @@ import * as aiApi from '@/api/ai';
import type { DebugErrorPayload, GenerateCurlPayload } from '@/api/ai'; import type { DebugErrorPayload, GenerateCurlPayload } from '@/api/ai';
import { useRootStore } from '@/stores/n8nRoot.store'; import { useRootStore } from '@/stores/n8nRoot.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { computed } from 'vue'; import { computed, reactive, ref } from 'vue';
import type { Ref } from 'vue';
import type { AIAssistantConnectionInfo, XYPosition } from '@/Interface';
import { usePostHog } from './posthog.store';
import { AI_ASSISTANT_EXPERIMENT } from '@/constants';
const CURRENT_POPUP_HEIGHT = 94;
/**
* Calculates the position for the next step popup based on the specified element
* so they are aligned vertically.
*/
const getPopupCenterPosition = (relativeElement: HTMLElement) => {
const bounds = relativeElement.getBoundingClientRect();
const rectMiddle = bounds.top + bounds.height / 2;
const x = bounds.left + bounds.width + 22;
const y = rectMiddle - CURRENT_POPUP_HEIGHT / 2;
return [x, y] as XYPosition;
};
export const useAIStore = defineStore('ai', () => { export const useAIStore = defineStore('ai', () => {
const rootStore = useRootStore(); const rootStore = useRootStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const posthogStore = usePostHog();
const assistantChatOpen = ref(false);
const nextStepPopupConfig = reactive({
open: false,
title: '',
position: [0, 0] as XYPosition,
});
const latestConnectionInfo: Ref<AIAssistantConnectionInfo | null> = ref(null);
const isErrorDebuggingEnabled = computed(() => settingsStore.settings.ai.features.errorDebugging); const isErrorDebuggingEnabled = computed(() => settingsStore.settings.ai.features.errorDebugging);
const isGenerateCurlEnabled = computed(() => settingsStore.settings.ai.features.generateCurl); const isGenerateCurlEnabled = computed(() => settingsStore.settings.ai.features.generateCurl);
const isAssistantExperimentEnabled = computed(
() => posthogStore.getVariant(AI_ASSISTANT_EXPERIMENT.name) === AI_ASSISTANT_EXPERIMENT.variant,
);
function openNextStepPopup(title: string, relativeElement: HTMLElement) {
nextStepPopupConfig.open = true;
nextStepPopupConfig.title = title;
nextStepPopupConfig.position = getPopupCenterPosition(relativeElement);
}
function closeNextStepPopup() {
nextStepPopupConfig.open = false;
}
async function debugError(payload: DebugErrorPayload) { async function debugError(payload: DebugErrorPayload) {
return await aiApi.debugError(rootStore.getRestApiContext, payload); return await aiApi.debugError(rootStore.getRestApiContext, payload);
@@ -20,5 +59,16 @@ export const useAIStore = defineStore('ai', () => {
return await aiApi.generateCurl(rootStore.getRestApiContext, payload); return await aiApi.generateCurl(rootStore.getRestApiContext, payload);
} }
return { isErrorDebuggingEnabled, isGenerateCurlEnabled, debugError, generateCurl }; return {
isErrorDebuggingEnabled,
debugError,
assistantChatOpen,
nextStepPopupConfig,
openNextStepPopup,
closeNextStepPopup,
latestConnectionInfo,
generateCurl,
isGenerateCurlEnabled,
isAssistantExperimentEnabled,
};
}); });

View File

@@ -39,3 +39,7 @@ export const isCredentialModalState = (value: unknown): value is NewCredentialsM
export const isResourceMapperValue = (value: unknown): value is string | number | boolean => { export const isResourceMapperValue = (value: unknown): value is string | number | boolean => {
return ['string', 'number', 'boolean'].includes(typeof value); return ['string', 'number', 'boolean'].includes(typeof value);
}; };
export const isJSPlumbEndpointElement = (element: Node): element is HTMLElement => {
return 'jtk' in element && 'endpoint' in (element.jtk as object);
};

View File

@@ -54,7 +54,7 @@ const containerCssVars = computed(() => ({
top: var(--trigger-placeholder-top-position); top: var(--trigger-placeholder-top-position);
left: var(--trigger-placeholder-left-position); left: var(--trigger-placeholder-left-position);
// We have to increase z-index to make sure it's higher than selecting box in NodeView // We have to increase z-index to make sure it's higher than selecting box in NodeView
// otherwise the clics wouldn't register // otherwise the clicks wouldn't register
z-index: 101; z-index: 101;
&:hover .button svg path { &:hover .button svg path {

View File

@@ -40,7 +40,7 @@
:show-tooltip="!containsTrigger && showTriggerMissingTooltip" :show-tooltip="!containsTrigger && showTriggerMissingTooltip"
:position="canvasStore.canvasAddButtonPosition" :position="canvasStore.canvasAddButtonPosition"
data-test-id="canvas-add-button" data-test-id="canvas-add-button"
@click="showTriggerCreator(NODE_CREATOR_OPEN_SOURCES.TRIGGER_PLACEHOLDER_BUTTON)" @click="onCanvasAddButtonCLick"
@hook:mounted="canvasStore.setRecenteredCanvasAddButtonPosition" @hook:mounted="canvasStore.setRecenteredCanvasAddButtonPosition"
/> />
<Node <Node
@@ -119,6 +119,9 @@
<Suspense> <Suspense>
<ContextMenu @action="onContextMenuAction" /> <ContextMenu @action="onContextMenuAction" />
</Suspense> </Suspense>
<Suspense>
<NextStepPopup v-show="isNextStepPopupVisible" @option-selected="onNextStepSelected" />
</Suspense>
<div v-if="!isReadOnlyRoute && !readOnlyEnv" class="workflow-execute-wrapper"> <div v-if="!isReadOnlyRoute && !readOnlyEnv" class="workflow-execute-wrapper">
<span <span
v-if="!isManualChatOnly" v-if="!isManualChatOnly"
@@ -246,6 +249,7 @@ import {
DRAG_EVENT_DATA_KEY, DRAG_EVENT_DATA_KEY,
UPDATE_WEBHOOK_ID_NODE_TYPES, UPDATE_WEBHOOK_ID_NODE_TYPES,
TIME, TIME,
AI_ASSISTANT_LOCAL_STORAGE_KEY,
} from '@/constants'; } from '@/constants';
import useGlobalLinkActions from '@/composables/useGlobalLinkActions'; import useGlobalLinkActions from '@/composables/useGlobalLinkActions';
@@ -266,6 +270,7 @@ import Node from '@/components/Node.vue';
import Sticky from '@/components/Sticky.vue'; import Sticky from '@/components/Sticky.vue';
import CanvasAddButton from './CanvasAddButton.vue'; import CanvasAddButton from './CanvasAddButton.vue';
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue'; import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
import NextStepPopup from '@/components/AIAssistantChat/NextStepPopup.vue';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import type { import type {
IConnection, IConnection,
@@ -294,6 +299,7 @@ import {
TelemetryHelpers, TelemetryHelpers,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { import type {
NewConnectionInfo,
ICredentialsResponse, ICredentialsResponse,
IExecutionResponse, IExecutionResponse,
IWorkflowDb, IWorkflowDb,
@@ -311,6 +317,7 @@ import type {
NodeCreatorOpenSource, NodeCreatorOpenSource,
AddedNodesAndConnections, AddedNodesAndConnections,
ToggleNodeCreatorOptions, ToggleNodeCreatorOptions,
AIAssistantConnectionInfo,
} from '@/Interface'; } from '@/Interface';
import { type Route, type RawLocation, useRouter } from 'vue-router'; import { type Route, type RawLocation, useRouter } from 'vue-router';
@@ -381,6 +388,9 @@ import { useCanvasPanning } from '@/composables/useCanvasPanning';
import { tryToParseNumber } from '@/utils/typesUtils'; import { tryToParseNumber } from '@/utils/typesUtils';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useRunWorkflow } from '@/composables/useRunWorkflow'; import { useRunWorkflow } from '@/composables/useRunWorkflow';
import { useAIStore } from '@/stores/ai.store';
import { useStorage } from '@/composables/useStorage';
import { isJSPlumbEndpointElement } from '@/utils/typeGuards';
interface AddNodeOptions { interface AddNodeOptions {
position?: XYPosition; position?: XYPosition;
@@ -411,6 +421,7 @@ export default defineComponent({
CanvasControls, CanvasControls,
ContextMenu, ContextMenu,
SetupWorkflowCredentialsButton, SetupWorkflowCredentialsButton,
NextStepPopup,
}, },
async beforeRouteLeave(to, from, next) { async beforeRouteLeave(to, from, next) {
if ( if (
@@ -606,6 +617,7 @@ export default defineComponent({
usePushConnectionStore, usePushConnectionStore,
useSourceControlStore, useSourceControlStore,
useExecutionsStore, useExecutionsStore,
useAIStore,
), ),
nativelyNumberSuffixedDefaults(): string[] { nativelyNumberSuffixedDefaults(): string[] {
return this.nodeTypesStore.nativelyNumberSuffixedDefaults; return this.nodeTypesStore.nativelyNumberSuffixedDefaults;
@@ -758,6 +770,16 @@ export default defineComponent({
isReadOnlyRoute() { isReadOnlyRoute() {
return this.$route?.meta?.readOnlyCanvas === true; return this.$route?.meta?.readOnlyCanvas === true;
}, },
isNextStepPopupVisible(): boolean {
return this.aiStore.nextStepPopupConfig.open;
},
shouldShowNextStepDialog(): boolean {
const userHasSeenAIAssistantExperiment =
useStorage(AI_ASSISTANT_LOCAL_STORAGE_KEY).value === 'true';
const experimentEnabled = this.aiStore.isAssistantExperimentEnabled;
const isCloudDeployment = this.settingsStore.isCloudDeployment;
return isCloudDeployment && experimentEnabled && !userHasSeenAIAssistantExperiment;
},
}, },
data() { data() {
return { return {
@@ -1204,6 +1226,33 @@ export default defineComponent({
} }
} }
}, },
async onCanvasAddButtonCLick(event: PointerEvent) {
if (event) {
if (this.shouldShowNextStepDialog) {
const newNodeButton = (event.target as HTMLElement).closest('button');
if (newNodeButton) {
this.aiStore.latestConnectionInfo = null;
this.aiStore.openNextStepPopup(
this.$locale.baseText('nextStepPopup.title.firstStep'),
newNodeButton,
);
}
return;
}
this.showTriggerCreator(NODE_CREATOR_OPEN_SOURCES.TRIGGER_PLACEHOLDER_BUTTON);
return;
}
},
onNextStepSelected(action: string) {
if (action === 'choose') {
const lastConnectionInfo = this.aiStore.latestConnectionInfo as NewConnectionInfo;
if (lastConnectionInfo === null) {
this.showTriggerCreator(NODE_CREATOR_OPEN_SOURCES.TRIGGER_PLACEHOLDER_BUTTON);
} else {
this.insertNodeAfterSelected(lastConnectionInfo);
}
}
},
showTriggerCreator(source: NodeCreatorOpenSource) { showTriggerCreator(source: NodeCreatorOpenSource) {
if (this.createNodeActive) return; if (this.createNodeActive) return;
this.nodeCreatorStore.setSelectedView(TRIGGER_NODE_CREATOR_VIEW); this.nodeCreatorStore.setSelectedView(TRIGGER_NODE_CREATOR_VIEW);
@@ -1449,6 +1498,7 @@ export default defineComponent({
// Save the location of the mouse click // Save the location of the mouse click
this.lastClickPosition = this.getMousePositionWithinNodeView(e); this.lastClickPosition = this.getMousePositionWithinNodeView(e);
if (e instanceof MouseEvent && e.button === 1) { if (e instanceof MouseEvent && e.button === 1) {
this.aiStore.closeNextStepPopup();
this.moveCanvasKeyPressed = true; this.moveCanvasKeyPressed = true;
} }
@@ -1475,6 +1525,7 @@ export default defineComponent({
}, },
async keyDown(e: KeyboardEvent) { async keyDown(e: KeyboardEvent) {
this.contextMenu.close(); this.contextMenu.close();
this.aiStore.closeNextStepPopup();
const ctrlModifier = this.deviceSupport.isCtrlKeyPressed(e) && !e.shiftKey && !e.altKey; const ctrlModifier = this.deviceSupport.isCtrlKeyPressed(e) && !e.shiftKey && !e.altKey;
const shiftModifier = e.shiftKey && !e.altKey && !this.deviceSupport.isCtrlKeyPressed(e); const shiftModifier = e.shiftKey && !e.altKey && !this.deviceSupport.isCtrlKeyPressed(e);
@@ -2825,15 +2876,7 @@ export default defineComponent({
return filter; return filter;
}, },
insertNodeAfterSelected(info: { insertNodeAfterSelected(info: NewConnectionInfo) {
sourceId: string;
index: number;
eventSource: NodeCreatorOpenSource;
connection?: Connection;
nodeCreatorView?: string;
outputType?: NodeConnectionType;
endpointUuid?: string;
}) {
const type = info.outputType ?? NodeConnectionType.Main; const type = info.outputType ?? NodeConnectionType.Main;
// Get the node and set it as active that new nodes // Get the node and set it as active that new nodes
// which get created get automatically connected // which get created get automatically connected
@@ -2907,7 +2950,12 @@ export default defineComponent({
} }
return; return;
} }
// When connection is aborted, we want to show the 'Next step' popup
const endpointId = `${connection.parameters.nodeId}-output${connection.parameters.index}`;
const endpoint = connection.instance.getEndpoint(endpointId);
// First, show node creator if endpoint is not a plus endpoint
// or if the AI Assistant experiment doesn't need to be shown to user
if (!endpoint?.endpoint?.canvas || !this.shouldShowNextStepDialog) {
this.insertNodeAfterSelected({ this.insertNodeAfterSelected({
sourceId: connection.parameters.nodeId, sourceId: connection.parameters.nodeId,
index: connection.parameters.index, index: connection.parameters.index,
@@ -2915,6 +2963,46 @@ export default defineComponent({
connection, connection,
outputType: connection.parameters.type, outputType: connection.parameters.type,
}); });
return;
}
// Else render the popup
const endpointElement: HTMLElement = endpoint.endpoint.canvas;
// Use observer to trigger the popup once the endpoint is rendered back again
// after connection drag is aborted (so we can get it's position and dimensions)
const observer = new MutationObserver((mutations) => {
// Find the mutation in which the current endpoint becomes visible again
const endpointMutation = mutations.find((mutation) => {
const target = mutation.target;
return (
isJSPlumbEndpointElement(target) &&
target.jtk.endpoint.uuid === endpoint.uuid &&
target.style.display === 'block'
);
});
if (endpointMutation) {
// When found, display the popup
const newConnectionInfo: AIAssistantConnectionInfo = {
sourceId: connection.parameters.nodeId,
index: connection.parameters.index,
eventSource: NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_DROP,
outputType: connection.parameters.type,
endpointUuid: endpoint.uuid,
stepName: endpoint.__meta.nodeName,
};
this.aiStore.latestConnectionInfo = newConnectionInfo;
this.aiStore.openNextStepPopup(
this.$locale.baseText('nextStepPopup.title.nextStep'),
endpointElement,
);
observer.disconnect();
return;
}
});
observer.observe(this.$refs.nodeViewRef as HTMLElement, {
attributes: true,
attributeFilter: ['style'],
subtree: true,
});
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
@@ -3429,13 +3517,30 @@ export default defineComponent({
.forEach((endpoint) => setTimeout(() => endpoint.instance.revalidate(endpoint.element), 0)); .forEach((endpoint) => setTimeout(() => endpoint.instance.revalidate(endpoint.element), 0));
}, },
onPlusEndpointClick(endpoint: Endpoint) { onPlusEndpointClick(endpoint: Endpoint) {
if (this.shouldShowNextStepDialog) {
if (endpoint?.__meta) { if (endpoint?.__meta) {
this.aiStore.latestConnectionInfo = {
sourceId: endpoint.__meta.nodeId,
index: endpoint.__meta.index,
eventSource: NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT,
outputType: endpoint.scope as NodeConnectionType,
endpointUuid: endpoint.uuid,
stepName: endpoint.__meta.nodeName,
};
const endpointElement = endpoint.endpoint.canvas;
this.aiStore.openNextStepPopup(
this.$locale.baseText('nextStepPopup.title.nextStep'),
endpointElement,
);
}
} else {
this.insertNodeAfterSelected({ this.insertNodeAfterSelected({
sourceId: endpoint.__meta.nodeId, sourceId: endpoint.__meta.nodeId,
index: endpoint.__meta.index, index: endpoint.__meta.index,
eventSource: NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT, eventSource: NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT,
outputType: endpoint.scope as ConnectionTypes, outputType: endpoint.scope as ConnectionTypes,
endpointUuid: endpoint.uuid, endpointUuid: endpoint.uuid,
stepName: endpoint.__meta.nodeName,
}); });
} }
}, },

185
pnpm-lock.yaml generated
View File

@@ -152,11 +152,11 @@ importers:
specifier: ^8.3.2 specifier: ^8.3.2
version: 8.3.2 version: 8.3.2
vue: vue:
specifier: ^3.3.4 specifier: ^3.4.21
version: 3.3.4 version: 3.4.21(typescript@5.4.2)
vue-markdown-render: vue-markdown-render:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1(vue@3.3.4) version: 2.1.1(vue@3.4.21)
devDependencies: devDependencies:
'@iconify-json/mdi': '@iconify-json/mdi':
specifier: ^1.1.54 specifier: ^1.1.54
@@ -3062,7 +3062,7 @@ packages:
resolution: {integrity: sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==} resolution: {integrity: sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.23.6 '@babel/types': 7.24.0
'@jridgewell/gen-mapping': 0.3.2 '@jridgewell/gen-mapping': 0.3.2
'@jridgewell/trace-mapping': 0.3.18 '@jridgewell/trace-mapping': 0.3.18
jsesc: 2.5.2 jsesc: 2.5.2
@@ -3071,7 +3071,7 @@ packages:
resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.23.6 '@babel/types': 7.24.0
'@jridgewell/gen-mapping': 0.3.2 '@jridgewell/gen-mapping': 0.3.2
'@jridgewell/trace-mapping': 0.3.18 '@jridgewell/trace-mapping': 0.3.18
jsesc: 2.5.2 jsesc: 2.5.2
@@ -3081,14 +3081,14 @@ packages:
resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.23.6 '@babel/types': 7.24.0
dev: true dev: true
/@babel/helper-builder-binary-assignment-operator-visitor@7.22.15: /@babel/helper-builder-binary-assignment-operator-visitor@7.22.15:
resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==} resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.23.6 '@babel/types': 7.24.0
dev: true dev: true
/@babel/helper-compilation-targets@7.22.9(@babel/core@7.22.9): /@babel/helper-compilation-targets@7.22.9(@babel/core@7.22.9):
@@ -3219,7 +3219,7 @@ packages:
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/template': 7.22.5 '@babel/template': 7.22.5
'@babel/types': 7.23.6 '@babel/types': 7.24.0
/@babel/helper-function-name@7.23.0: /@babel/helper-function-name@7.23.0:
resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==}
@@ -3233,20 +3233,20 @@ packages:
resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.23.6 '@babel/types': 7.24.0
/@babel/helper-member-expression-to-functions@7.22.5: /@babel/helper-member-expression-to-functions@7.22.5:
resolution: {integrity: sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==} resolution: {integrity: sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.23.6 '@babel/types': 7.24.0
dev: true dev: true
/@babel/helper-member-expression-to-functions@7.23.0: /@babel/helper-member-expression-to-functions@7.23.0:
resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==} resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.23.6 '@babel/types': 7.24.0
dev: true dev: true
/@babel/helper-module-imports@7.22.15: /@babel/helper-module-imports@7.22.15:
@@ -3260,7 +3260,7 @@ packages:
resolution: {integrity: sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==} resolution: {integrity: sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.23.6 '@babel/types': 7.24.0
/@babel/helper-module-transforms@7.22.9(@babel/core@7.22.9): /@babel/helper-module-transforms@7.22.9(@babel/core@7.22.9):
resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==} resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==}
@@ -3293,7 +3293,7 @@ packages:
resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.23.6 '@babel/types': 7.24.0
dev: true dev: true
/@babel/helper-plugin-utils@7.22.5: /@babel/helper-plugin-utils@7.22.5:
@@ -3345,20 +3345,20 @@ packages:
resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.23.6 '@babel/types': 7.24.0
/@babel/helper-skip-transparent-expression-wrappers@7.22.5: /@babel/helper-skip-transparent-expression-wrappers@7.22.5:
resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.23.6 '@babel/types': 7.24.0
dev: true dev: true
/@babel/helper-split-export-declaration@7.22.6: /@babel/helper-split-export-declaration@7.22.6:
resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/types': 7.23.6 '@babel/types': 7.24.0
/@babel/helper-string-parser@7.23.4: /@babel/helper-string-parser@7.23.4:
resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==}
@@ -3383,7 +3383,7 @@ packages:
dependencies: dependencies:
'@babel/helper-function-name': 7.22.5 '@babel/helper-function-name': 7.22.5
'@babel/template': 7.24.0 '@babel/template': 7.24.0
'@babel/types': 7.23.6 '@babel/types': 7.24.0
dev: true dev: true
/@babel/helpers@7.22.6: /@babel/helpers@7.22.6:
@@ -3392,7 +3392,7 @@ packages:
dependencies: dependencies:
'@babel/template': 7.22.5 '@babel/template': 7.22.5
'@babel/traverse': 7.22.8 '@babel/traverse': 7.22.8
'@babel/types': 7.23.6 '@babel/types': 7.24.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -3423,13 +3423,6 @@ packages:
chalk: 2.4.2 chalk: 2.4.2
js-tokens: 4.0.0 js-tokens: 4.0.0
/@babel/parser@7.23.6:
resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==}
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
'@babel/types': 7.23.6
/@babel/parser@7.24.0: /@babel/parser@7.24.0:
resolution: {integrity: sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==} resolution: {integrity: sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
@@ -4481,7 +4474,7 @@ packages:
dependencies: dependencies:
'@babel/core': 7.24.0 '@babel/core': 7.24.0
'@babel/helper-plugin-utils': 7.24.0 '@babel/helper-plugin-utils': 7.24.0
'@babel/types': 7.23.6 '@babel/types': 7.24.0
esutils: 2.0.3 esutils: 2.0.3
dev: true dev: true
@@ -4539,8 +4532,8 @@ packages:
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
'@babel/code-frame': 7.22.5 '@babel/code-frame': 7.22.5
'@babel/parser': 7.23.6 '@babel/parser': 7.24.0
'@babel/types': 7.23.6 '@babel/types': 7.24.0
/@babel/template@7.24.0: /@babel/template@7.24.0:
resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==}
@@ -4562,7 +4555,7 @@ packages:
'@babel/helper-hoist-variables': 7.22.5 '@babel/helper-hoist-variables': 7.22.5
'@babel/helper-split-export-declaration': 7.22.6 '@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.24.0 '@babel/parser': 7.24.0
'@babel/types': 7.23.6 '@babel/types': 7.24.0
debug: 4.3.4(supports-color@8.1.1) debug: 4.3.4(supports-color@8.1.1)
globals: 11.12.0 globals: 11.12.0
transitivePeerDependencies: transitivePeerDependencies:
@@ -4601,7 +4594,6 @@ packages:
'@babel/helper-string-parser': 7.23.4 '@babel/helper-string-parser': 7.23.4
'@babel/helper-validator-identifier': 7.22.20 '@babel/helper-validator-identifier': 7.22.20
to-fast-properties: 2.0.0 to-fast-properties: 2.0.0
dev: true
/@bcoe/v8-coverage@0.2.3: /@bcoe/v8-coverage@0.2.3:
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
@@ -8838,7 +8830,7 @@ packages:
dependencies: dependencies:
'@babel/core': 7.24.0 '@babel/core': 7.24.0
'@babel/preset-env': 7.24.0(@babel/core@7.24.0) '@babel/preset-env': 7.24.0(@babel/core@7.24.0)
'@babel/types': 7.23.6 '@babel/types': 7.24.0
'@storybook/csf': 0.1.2 '@storybook/csf': 0.1.2
'@storybook/csf-tools': 8.0.0 '@storybook/csf-tools': 8.0.0
'@storybook/node-logger': 8.0.0 '@storybook/node-logger': 8.0.0
@@ -9476,8 +9468,8 @@ packages:
/@types/babel__core@7.20.0: /@types/babel__core@7.20.0:
resolution: {integrity: sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==} resolution: {integrity: sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==}
dependencies: dependencies:
'@babel/parser': 7.23.6 '@babel/parser': 7.24.0
'@babel/types': 7.23.6 '@babel/types': 7.24.0
'@types/babel__generator': 7.6.4 '@types/babel__generator': 7.6.4
'@types/babel__template': 7.4.1 '@types/babel__template': 7.4.1
'@types/babel__traverse': 7.18.2 '@types/babel__traverse': 7.18.2
@@ -9485,18 +9477,18 @@ packages:
/@types/babel__generator@7.6.4: /@types/babel__generator@7.6.4:
resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==}
dependencies: dependencies:
'@babel/types': 7.23.6 '@babel/types': 7.24.0
/@types/babel__template@7.4.1: /@types/babel__template@7.4.1:
resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==}
dependencies: dependencies:
'@babel/parser': 7.23.6 '@babel/parser': 7.24.0
'@babel/types': 7.23.6 '@babel/types': 7.24.0
/@types/babel__traverse@7.18.2: /@types/babel__traverse@7.18.2:
resolution: {integrity: sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==} resolution: {integrity: sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==}
dependencies: dependencies:
'@babel/types': 7.23.6 '@babel/types': 7.24.0
/@types/basic-auth@1.1.3: /@types/basic-auth@1.1.3:
resolution: {integrity: sha512-W3rv6J0IGlxqgE2eQ2pTb0gBjaGtejQpJ6uaCjz3UQ65+TFTPC5/lAE+POfx1YLdjtxvejJzsIAfd3MxWiVmfg==} resolution: {integrity: sha512-W3rv6J0IGlxqgE2eQ2pTb0gBjaGtejQpJ6uaCjz3UQ65+TFTPC5/lAE+POfx1YLdjtxvejJzsIAfd3MxWiVmfg==}
@@ -10556,15 +10548,6 @@ packages:
path-browserify: 1.0.1 path-browserify: 1.0.1
dev: true dev: true
/@vue/compiler-core@3.3.4:
resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==}
dependencies:
'@babel/parser': 7.23.6
'@vue/shared': 3.3.4
estree-walker: 2.0.2
source-map-js: 1.0.2
dev: false
/@vue/compiler-core@3.4.21: /@vue/compiler-core@3.4.21:
resolution: {integrity: sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==} resolution: {integrity: sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==}
dependencies: dependencies:
@@ -10574,34 +10557,12 @@ packages:
estree-walker: 2.0.2 estree-walker: 2.0.2
source-map-js: 1.0.2 source-map-js: 1.0.2
/@vue/compiler-dom@3.3.4:
resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==}
dependencies:
'@vue/compiler-core': 3.3.4
'@vue/shared': 3.3.4
dev: false
/@vue/compiler-dom@3.4.21: /@vue/compiler-dom@3.4.21:
resolution: {integrity: sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==} resolution: {integrity: sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==}
dependencies: dependencies:
'@vue/compiler-core': 3.4.21 '@vue/compiler-core': 3.4.21
'@vue/shared': 3.4.21 '@vue/shared': 3.4.21
/@vue/compiler-sfc@3.3.4:
resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==}
dependencies:
'@babel/parser': 7.23.6
'@vue/compiler-core': 3.3.4
'@vue/compiler-dom': 3.3.4
'@vue/compiler-ssr': 3.3.4
'@vue/reactivity-transform': 3.3.4
'@vue/shared': 3.3.4
estree-walker: 2.0.2
magic-string: 0.30.5
postcss: 8.4.32
source-map-js: 1.0.2
dev: false
/@vue/compiler-sfc@3.4.21: /@vue/compiler-sfc@3.4.21:
resolution: {integrity: sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==} resolution: {integrity: sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==}
dependencies: dependencies:
@@ -10615,13 +10576,6 @@ packages:
postcss: 8.4.35 postcss: 8.4.35
source-map-js: 1.0.2 source-map-js: 1.0.2
/@vue/compiler-ssr@3.3.4:
resolution: {integrity: sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==}
dependencies:
'@vue/compiler-dom': 3.3.4
'@vue/shared': 3.3.4
dev: false
/@vue/compiler-ssr@3.4.21: /@vue/compiler-ssr@3.4.21:
resolution: {integrity: sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==} resolution: {integrity: sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==}
dependencies: dependencies:
@@ -10708,48 +10662,17 @@ packages:
vue-template-compiler: 2.7.14 vue-template-compiler: 2.7.14
dev: true dev: true
/@vue/reactivity-transform@3.3.4:
resolution: {integrity: sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==}
dependencies:
'@babel/parser': 7.23.6
'@vue/compiler-core': 3.3.4
'@vue/shared': 3.3.4
estree-walker: 2.0.2
magic-string: 0.30.8
dev: false
/@vue/reactivity@3.3.4:
resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==}
dependencies:
'@vue/shared': 3.3.4
dev: false
/@vue/reactivity@3.4.21: /@vue/reactivity@3.4.21:
resolution: {integrity: sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==} resolution: {integrity: sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==}
dependencies: dependencies:
'@vue/shared': 3.4.21 '@vue/shared': 3.4.21
/@vue/runtime-core@3.3.4:
resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==}
dependencies:
'@vue/reactivity': 3.3.4
'@vue/shared': 3.3.4
dev: false
/@vue/runtime-core@3.4.21: /@vue/runtime-core@3.4.21:
resolution: {integrity: sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==} resolution: {integrity: sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==}
dependencies: dependencies:
'@vue/reactivity': 3.4.21 '@vue/reactivity': 3.4.21
'@vue/shared': 3.4.21 '@vue/shared': 3.4.21
/@vue/runtime-dom@3.3.4:
resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==}
dependencies:
'@vue/runtime-core': 3.3.4
'@vue/shared': 3.3.4
csstype: 3.1.1
dev: false
/@vue/runtime-dom@3.4.21: /@vue/runtime-dom@3.4.21:
resolution: {integrity: sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==} resolution: {integrity: sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==}
dependencies: dependencies:
@@ -10757,16 +10680,6 @@ packages:
'@vue/shared': 3.4.21 '@vue/shared': 3.4.21
csstype: 3.1.3 csstype: 3.1.3
/@vue/server-renderer@3.3.4(vue@3.3.4):
resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==}
peerDependencies:
vue: 3.3.4
dependencies:
'@vue/compiler-ssr': 3.3.4
'@vue/shared': 3.3.4
vue: 3.3.4
dev: false
/@vue/server-renderer@3.4.21(vue@3.4.21): /@vue/server-renderer@3.4.21(vue@3.4.21):
resolution: {integrity: sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==} resolution: {integrity: sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==}
peerDependencies: peerDependencies:
@@ -10776,10 +10689,6 @@ packages:
'@vue/shared': 3.4.21 '@vue/shared': 3.4.21
vue: 3.4.21(typescript@5.4.2) vue: 3.4.21(typescript@5.4.2)
/@vue/shared@3.3.4:
resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==}
dev: false
/@vue/shared@3.4.21: /@vue/shared@3.4.21:
resolution: {integrity: sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==} resolution: {integrity: sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==}
@@ -11642,7 +11551,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies: dependencies:
'@babel/template': 7.22.5 '@babel/template': 7.22.5
'@babel/types': 7.23.6 '@babel/types': 7.24.0
'@types/babel__core': 7.20.0 '@types/babel__core': 7.20.0
'@types/babel__traverse': 7.18.2 '@types/babel__traverse': 7.18.2
@@ -11715,7 +11624,7 @@ packages:
resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
dependencies: dependencies:
'@babel/types': 7.23.6 '@babel/types': 7.24.0
dev: true dev: true
/balanced-match@1.0.2: /balanced-match@1.0.2:
@@ -12734,7 +12643,7 @@ packages:
resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==}
dependencies: dependencies:
'@babel/parser': 7.24.0 '@babel/parser': 7.24.0
'@babel/types': 7.23.6 '@babel/types': 7.24.0
dev: true dev: true
/content-disposition@0.5.4: /content-disposition@0.5.4:
@@ -13061,10 +12970,6 @@ packages:
rrweb-cssom: 0.6.0 rrweb-cssom: 0.6.0
dev: true dev: true
/csstype@3.1.1:
resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
dev: false
/csstype@3.1.2: /csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
dev: true dev: true
@@ -16479,7 +16384,7 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dependencies: dependencies:
'@babel/core': 7.22.9 '@babel/core': 7.22.9
'@babel/parser': 7.23.6 '@babel/parser': 7.24.0
'@istanbuljs/schema': 0.1.3 '@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.0 istanbul-lib-coverage: 3.2.0
semver: 7.6.0 semver: 7.6.0
@@ -16936,7 +16841,7 @@ packages:
'@babel/generator': 7.22.9 '@babel/generator': 7.22.9
'@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.22.9) '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.22.9)
'@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.22.9) '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.22.9)
'@babel/types': 7.23.6 '@babel/types': 7.24.0
'@jest/expect-utils': 29.6.2 '@jest/expect-utils': 29.6.2
'@jest/transform': 29.6.2 '@jest/transform': 29.6.2
'@jest/types': 29.6.1 '@jest/types': 29.6.1
@@ -18431,6 +18336,7 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
dev: true
/magic-string@0.30.8: /magic-string@0.30.8:
resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
@@ -18441,8 +18347,8 @@ packages:
/magicast@0.3.3: /magicast@0.3.3:
resolution: {integrity: sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==} resolution: {integrity: sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==}
dependencies: dependencies:
'@babel/parser': 7.23.6 '@babel/parser': 7.24.0
'@babel/types': 7.23.6 '@babel/types': 7.24.0
source-map-js: 1.0.2 source-map-js: 1.0.2
dev: true dev: true
@@ -20641,6 +20547,7 @@ packages:
nanoid: 3.3.7 nanoid: 3.3.7
picocolors: 1.0.0 picocolors: 1.0.0
source-map-js: 1.0.2 source-map-js: 1.0.2
dev: true
/postcss@8.4.35: /postcss@8.4.35:
resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==} resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==}
@@ -24452,13 +24359,13 @@ packages:
- typescript - typescript
dev: false dev: false
/vue-markdown-render@2.1.1(vue@3.3.4): /vue-markdown-render@2.1.1(vue@3.4.21):
resolution: {integrity: sha512-szuJVbCwgIpVsggd8IHGB2lLo8BH8Ojd+wakaOTASNxdYcccKxoMcvDqUsLoGwgKDY8yJf0U/+ppffEYxsEHkA==} resolution: {integrity: sha512-szuJVbCwgIpVsggd8IHGB2lLo8BH8Ojd+wakaOTASNxdYcccKxoMcvDqUsLoGwgKDY8yJf0U/+ppffEYxsEHkA==}
peerDependencies: peerDependencies:
vue: ^3.3.4 vue: ^3.3.4
dependencies: dependencies:
markdown-it: 12.3.2 markdown-it: 12.3.2
vue: 3.3.4 vue: 3.4.21(typescript@5.4.2)
dev: false dev: false
/vue-router@4.2.2(vue@3.4.21): /vue-router@4.2.2(vue@3.4.21):
@@ -24505,16 +24412,6 @@ packages:
resolution: {integrity: sha512-uXTclRzn7de1mgiDIZ8N4J/wnWl1vBPLTWr60fqoLXu7ifhDKpl83Q2m9qA20KfEiAy+L4X/xXGc5ptGmdPh4A==} resolution: {integrity: sha512-uXTclRzn7de1mgiDIZ8N4J/wnWl1vBPLTWr60fqoLXu7ifhDKpl83Q2m9qA20KfEiAy+L4X/xXGc5ptGmdPh4A==}
dev: false dev: false
/vue@3.3.4:
resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==}
dependencies:
'@vue/compiler-dom': 3.3.4
'@vue/compiler-sfc': 3.3.4
'@vue/runtime-dom': 3.3.4
'@vue/server-renderer': 3.3.4(vue@3.3.4)
'@vue/shared': 3.3.4
dev: false
/vue@3.4.21(typescript@5.4.2): /vue@3.4.21(typescript@5.4.2):
resolution: {integrity: sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==} resolution: {integrity: sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==}
peerDependencies: peerDependencies:
@@ -24824,7 +24721,7 @@ packages:
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
dependencies: dependencies:
'@babel/parser': 7.24.0 '@babel/parser': 7.24.0
'@babel/types': 7.23.6 '@babel/types': 7.24.0
assert-never: 1.2.1 assert-never: 1.2.1
babel-walk: 3.0.0-canary-5 babel-walk: 3.0.0-canary-5
dev: true dev: true