mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
feat(editor): Implement AI Assistant chat UI (#9300)
This commit is contained in:
parent
23b676d7cb
commit
491c6ec546
|
@ -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);
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 || '<Empty response>';
|
||||
return (message.value as ChatMessageText).text || '<Empty response>';
|
||||
});
|
||||
|
||||
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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -21,6 +21,7 @@ export const defaultOptions: ChatOptions = {
|
|||
footer: '',
|
||||
getStarted: 'New Conversation',
|
||||
inputPlaceholder: 'Type your question..',
|
||||
closeButtonTooltip: 'Close chat',
|
||||
},
|
||||
},
|
||||
theme: {},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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. We’d love to invite you to be one of the first users to get their hands on the real feature once it’s 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",
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
185
pnpm-lock.yaml
185
pnpm-lock.yaml
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue