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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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--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--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--color: var(--chat--color-dark);
--chat--message--bot--border: none;
--chat--message--user--background: var(--chat--color-secondary);
--chat--message--user--color: var(--chat--color-white);
--chat--message--user--border: none;
--chat--message--pre--background: rgba(0, 0, 0, 0.05);
--chat--toggle--background: var(--chat--color-primary);

View file

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

View file

@ -1,5 +1,7 @@
<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 GetStarted from '@n8n/chat/components/GetStarted.vue';
import GetStartedFooter from '@n8n/chat/components/GetStartedFooter.vue';
@ -14,7 +16,12 @@ const chatStore = useChat();
const { messages, currentSessionId } = chatStore;
const { options } = useOptions();
const showCloseButton = computed(() => options.mode === 'window' && options.showWindowCloseButton);
async function getStarted() {
if (!chatStore.startNewSession) {
return;
}
void chatStore.startNewSession();
void nextTick(() => {
chatEventBus.emit('scrollToBottom');
@ -22,12 +29,19 @@ async function getStarted() {
}
async function initialize() {
if (!chatStore.loadPreviousSession) {
return;
}
await chatStore.loadPreviousSession();
void nextTick(() => {
chatEventBus.emit('scrollToBottom');
});
}
function closeChat() {
chatEventBus.emit('close');
}
onMounted(async () => {
await initialize();
if (!options.showWelcomeScreen && !currentSessionId.value) {
@ -39,8 +53,20 @@ onMounted(async () => {
<template>
<Layout class="chat-wrapper">
<template #header>
<h1>{{ t('title') }}</h1>
<p>{{ t('subtitle') }}</p>
<div class="chat-heading">
<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>
<GetStarted v-if="!currentSessionId && options.showWelcomeScreen" @click:button="getStarted" />
<MessagesList v-else :messages="messages" />
@ -50,3 +76,22 @@ onMounted(async () => {
</template>
</Layout>
</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">
// eslint-disable-next-line import/no-unresolved
import IconSend from 'virtual:icons/mdi/send';
import { computed, ref } from 'vue';
import { useI18n, useChat } from '@n8n/chat/composables';
import { computed, onMounted, ref } from 'vue';
import { useI18n, useChat, useOptions } from '@n8n/chat/composables';
import { chatEventBus } from '@n8n/chat/event-buses';
const { options } = useOptions();
const chatStore = useChat();
const { waitingForResponse } = chatStore;
const { t } = useI18n();
const chatTextArea = ref<HTMLTextAreaElement | null>(null);
const input = ref('');
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) {
@ -38,8 +51,10 @@ async function onSubmitKeydown(event: KeyboardEvent) {
<template>
<div class="chat-input">
<textarea
ref="chatTextArea"
v-model="input"
rows="1"
:disabled="isInputDisabled"
:placeholder="t('inputPlaceholder')"
@keydown.enter="onSubmitKeydown"
/>
@ -55,10 +70,11 @@ async function onSubmitKeydown(event: KeyboardEvent) {
justify-content: center;
align-items: center;
width: 100%;
background: white;
textarea {
font-family: inherit;
font-size: inherit;
font-size: var(--chat--input--font-size, inherit);
width: 100%;
border: 0;
padding: var(--chat--spacing);
@ -71,7 +87,7 @@ async function onSubmitKeydown(event: KeyboardEvent) {
width: var(--chat--textarea--height);
background: white;
cursor: pointer;
color: var(--chat--color-secondary);
color: var(--chat--input--send--button--color, var(--chat--color-secondary));
border: 0;
font-size: 24px;
display: inline-flex;

View file

@ -58,9 +58,26 @@ onBeforeUnmount(() => {
);
.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));
background: var(--chat--header--background, var(--chat--color-dark));
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 {

View file

@ -6,7 +6,8 @@ import VueMarkdown from 'vue-markdown-render';
import hljs from 'highlight.js/lib/core';
import markdownLink from 'markdown-it-link-attributes';
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({
message: {
@ -16,15 +17,17 @@ const props = defineProps({
});
const { message } = toRefs(props);
const { options } = useOptions();
const messageText = computed(() => {
return message.value.text || '&lt;Empty response&gt;';
return (message.value as ChatMessageText).text || '&lt;Empty response&gt;';
});
const classes = computed(() => {
return {
'chat-message-from-user': message.value.sender === 'user',
'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
},
};
const messageComponents = options.messageComponents ?? {};
</script>
<template>
<div class="chat-message" :class="classes">
<slot>
<template v-if="message.type === 'component' && messageComponents[message.key]">
<component :is="messageComponents[message.key]" v-bind="message.arguments" />
</template>
<VueMarkdown
v-else
class="chat-message-markdown"
:source="messageText"
:options="markdownOptions"
@ -66,21 +75,40 @@ const markdownOptions = {
.chat-message {
display: block;
max-width: 80%;
font-size: var(--chat--message--font-size, 1rem);
padding: var(--chat--message--padding, var(--chat--spacing));
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 {
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 {
background-color: var(--chat--message--bot--background);
&:not(.chat-message-transparent) {
background-color: var(--chat--message--bot--background);
border: var(--chat--message--bot--border, none);
}
color: var(--chat--message--bot--color);
border-bottom-left-radius: 0;
}
&.chat-message-from-user {
background-color: var(--chat--message--user--background);
&:not(.chat-message-transparent) {
background-color: var(--chat--message--user--background);
border: var(--chat--message--user--border, none);
}
color: var(--chat--message--user--color);
margin-left: auto;
border-bottom-right-radius: 0;

View file

@ -1,3 +1,4 @@
import { isRef } from 'vue';
import { useOptions } from '@n8n/chat/composables/useOptions';
export function useI18n() {
@ -5,7 +6,11 @@ export function useI18n() {
const language = options?.defaultLanguage ?? 'en';
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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -55,6 +55,7 @@ import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers';
import type { Component } from 'vue';
import type { Scope } from '@n8n/permissions';
import type { NotificationOptions as ElementNotificationOptions } from 'element-plus';
import type { Connection } from '@jsplumb/core';
export * from 'n8n-design-system/types';
@ -1894,3 +1895,17 @@ export type SuggestedTemplatesWorkflowPreview = {
preview: IWorkflowData;
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>
.zoomMenu {
position: absolute;
width: 210px;
bottom: var(--spacing-l);
left: var(--spacing-l);
line-height: 25px;

View file

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

View file

@ -171,10 +171,34 @@
var(--node-type-background-l)
);
--chat--spacing: var(--spacing-s);
// Using native css variable enables us to use this value in JS
--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 {

View file

@ -88,6 +88,22 @@
"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.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.currentPassword": "Current 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.",
"noTagsView.readyToOrganizeYourWorkflows": "Ready to organize your workflows?",
"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.activateDeactivateNode": "Activate/Deactivate Node",
"node.changeColor": "Change color",

View file

@ -3,14 +3,53 @@ import * as aiApi from '@/api/ai';
import type { DebugErrorPayload, GenerateCurlPayload } from '@/api/ai';
import { useRootStore } from '@/stores/n8nRoot.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', () => {
const rootStore = useRootStore();
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 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) {
return await aiApi.debugError(rootStore.getRestApiContext, payload);
@ -20,5 +59,16 @@ export const useAIStore = defineStore('ai', () => {
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 => {
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);
left: var(--trigger-placeholder-left-position);
// 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;
&:hover .button svg path {

View file

@ -40,7 +40,7 @@
:show-tooltip="!containsTrigger && showTriggerMissingTooltip"
:position="canvasStore.canvasAddButtonPosition"
data-test-id="canvas-add-button"
@click="showTriggerCreator(NODE_CREATOR_OPEN_SOURCES.TRIGGER_PLACEHOLDER_BUTTON)"
@click="onCanvasAddButtonCLick"
@hook:mounted="canvasStore.setRecenteredCanvasAddButtonPosition"
/>
<Node
@ -119,6 +119,9 @@
<Suspense>
<ContextMenu @action="onContextMenuAction" />
</Suspense>
<Suspense>
<NextStepPopup v-show="isNextStepPopupVisible" @option-selected="onNextStepSelected" />
</Suspense>
<div v-if="!isReadOnlyRoute && !readOnlyEnv" class="workflow-execute-wrapper">
<span
v-if="!isManualChatOnly"
@ -246,6 +249,7 @@ import {
DRAG_EVENT_DATA_KEY,
UPDATE_WEBHOOK_ID_NODE_TYPES,
TIME,
AI_ASSISTANT_LOCAL_STORAGE_KEY,
} from '@/constants';
import useGlobalLinkActions from '@/composables/useGlobalLinkActions';
@ -266,6 +270,7 @@ import Node from '@/components/Node.vue';
import Sticky from '@/components/Sticky.vue';
import CanvasAddButton from './CanvasAddButton.vue';
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
import NextStepPopup from '@/components/AIAssistantChat/NextStepPopup.vue';
import { v4 as uuid } from 'uuid';
import type {
IConnection,
@ -294,6 +299,7 @@ import {
TelemetryHelpers,
} from 'n8n-workflow';
import type {
NewConnectionInfo,
ICredentialsResponse,
IExecutionResponse,
IWorkflowDb,
@ -311,6 +317,7 @@ import type {
NodeCreatorOpenSource,
AddedNodesAndConnections,
ToggleNodeCreatorOptions,
AIAssistantConnectionInfo,
} from '@/Interface';
import { type Route, type RawLocation, useRouter } from 'vue-router';
@ -381,6 +388,9 @@ import { useCanvasPanning } from '@/composables/useCanvasPanning';
import { tryToParseNumber } from '@/utils/typesUtils';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useRunWorkflow } from '@/composables/useRunWorkflow';
import { useAIStore } from '@/stores/ai.store';
import { useStorage } from '@/composables/useStorage';
import { isJSPlumbEndpointElement } from '@/utils/typeGuards';
interface AddNodeOptions {
position?: XYPosition;
@ -411,6 +421,7 @@ export default defineComponent({
CanvasControls,
ContextMenu,
SetupWorkflowCredentialsButton,
NextStepPopup,
},
async beforeRouteLeave(to, from, next) {
if (
@ -606,6 +617,7 @@ export default defineComponent({
usePushConnectionStore,
useSourceControlStore,
useExecutionsStore,
useAIStore,
),
nativelyNumberSuffixedDefaults(): string[] {
return this.nodeTypesStore.nativelyNumberSuffixedDefaults;
@ -758,6 +770,16 @@ export default defineComponent({
isReadOnlyRoute() {
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() {
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) {
if (this.createNodeActive) return;
this.nodeCreatorStore.setSelectedView(TRIGGER_NODE_CREATOR_VIEW);
@ -1449,6 +1498,7 @@ export default defineComponent({
// Save the location of the mouse click
this.lastClickPosition = this.getMousePositionWithinNodeView(e);
if (e instanceof MouseEvent && e.button === 1) {
this.aiStore.closeNextStepPopup();
this.moveCanvasKeyPressed = true;
}
@ -1475,6 +1525,7 @@ export default defineComponent({
},
async keyDown(e: KeyboardEvent) {
this.contextMenu.close();
this.aiStore.closeNextStepPopup();
const ctrlModifier = this.deviceSupport.isCtrlKeyPressed(e) && !e.shiftKey && !e.altKey;
const shiftModifier = e.shiftKey && !e.altKey && !this.deviceSupport.isCtrlKeyPressed(e);
@ -2825,15 +2876,7 @@ export default defineComponent({
return filter;
},
insertNodeAfterSelected(info: {
sourceId: string;
index: number;
eventSource: NodeCreatorOpenSource;
connection?: Connection;
nodeCreatorView?: string;
outputType?: NodeConnectionType;
endpointUuid?: string;
}) {
insertNodeAfterSelected(info: NewConnectionInfo) {
const type = info.outputType ?? NodeConnectionType.Main;
// Get the node and set it as active that new nodes
// which get created get automatically connected
@ -2907,13 +2950,58 @@ export default defineComponent({
}
return;
}
this.insertNodeAfterSelected({
sourceId: connection.parameters.nodeId,
index: connection.parameters.index,
eventSource: NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_DROP,
connection,
outputType: connection.parameters.type,
// 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({
sourceId: connection.parameters.nodeId,
index: connection.parameters.index,
eventSource: NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_DROP,
connection,
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) {
console.error(e);
@ -3429,13 +3517,30 @@ export default defineComponent({
.forEach((endpoint) => setTimeout(() => endpoint.instance.revalidate(endpoint.element), 0));
},
onPlusEndpointClick(endpoint: Endpoint) {
if (endpoint?.__meta) {
if (this.shouldShowNextStepDialog) {
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({
sourceId: endpoint.__meta.nodeId,
index: endpoint.__meta.index,
eventSource: NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT,
outputType: endpoint.scope as ConnectionTypes,
endpointUuid: endpoint.uuid,
stepName: endpoint.__meta.nodeName,
});
}
},

View file

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