mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-21 11:49:59 +00:00
feat(editor): Move AI Assistant button to canvas action buttons (#16879)
This commit is contained in:
@@ -35,8 +35,8 @@ describe('AI Assistant::enabled', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders placeholder UI', () => {
|
it('renders placeholder UI', () => {
|
||||||
aiAssistant.getters.askAssistantFloatingButton().should('be.visible');
|
aiAssistant.getters.askAssistantCanvasActionButton().should('be.visible');
|
||||||
aiAssistant.getters.askAssistantFloatingButton().click();
|
aiAssistant.getters.askAssistantCanvasActionButton().click();
|
||||||
aiAssistant.getters.askAssistantChat().should('be.visible');
|
aiAssistant.getters.askAssistantChat().should('be.visible');
|
||||||
aiAssistant.getters.placeholderMessage().should('be.visible');
|
aiAssistant.getters.placeholderMessage().should('be.visible');
|
||||||
aiAssistant.getters.chatInput().should('be.visible');
|
aiAssistant.getters.chatInput().should('be.visible');
|
||||||
@@ -47,7 +47,7 @@ describe('AI Assistant::enabled', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should resize assistant chat up', () => {
|
it('should resize assistant chat up', () => {
|
||||||
aiAssistant.getters.askAssistantFloatingButton().click();
|
aiAssistant.getters.askAssistantCanvasActionButton().click();
|
||||||
aiAssistant.getters.askAssistantSidebarResizer().should('be.visible');
|
aiAssistant.getters.askAssistantSidebarResizer().should('be.visible');
|
||||||
aiAssistant.getters.askAssistantChat().should('be.visible');
|
aiAssistant.getters.askAssistantChat().should('be.visible');
|
||||||
aiAssistant.getters.askAssistantChat().then((element) => {
|
aiAssistant.getters.askAssistantChat().then((element) => {
|
||||||
@@ -64,7 +64,7 @@ describe('AI Assistant::enabled', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should resize assistant chat down', () => {
|
it('should resize assistant chat down', () => {
|
||||||
aiAssistant.getters.askAssistantFloatingButton().click();
|
aiAssistant.getters.askAssistantCanvasActionButton().click();
|
||||||
aiAssistant.getters.askAssistantSidebarResizer().should('be.visible');
|
aiAssistant.getters.askAssistantSidebarResizer().should('be.visible');
|
||||||
aiAssistant.getters.askAssistantChat().should('be.visible');
|
aiAssistant.getters.askAssistantChat().should('be.visible');
|
||||||
aiAssistant.getters.askAssistantChat().then((element) => {
|
aiAssistant.getters.askAssistantChat().then((element) => {
|
||||||
@@ -300,11 +300,11 @@ describe('AI Assistant::enabled', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
aiAssistant.actions.openChat();
|
aiAssistant.actions.openChatFromCanvas();
|
||||||
aiAssistant.actions.sendMessage('Hello');
|
aiAssistant.actions.sendMessage('Hello');
|
||||||
cy.wait('@chatRequest');
|
cy.wait('@chatRequest');
|
||||||
aiAssistant.actions.closeChat();
|
aiAssistant.actions.closeChat();
|
||||||
aiAssistant.actions.openChat();
|
aiAssistant.actions.openChatFromCanvas();
|
||||||
// After closing and reopening the chat, all messages should be still there
|
// After closing and reopening the chat, all messages should be still there
|
||||||
aiAssistant.getters.chatMessagesAll().should('have.length', 2);
|
aiAssistant.getters.chatMessagesAll().should('have.length', 2);
|
||||||
// End the session
|
// End the session
|
||||||
@@ -313,7 +313,7 @@ describe('AI Assistant::enabled', () => {
|
|||||||
aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
|
aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
|
||||||
aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended');
|
aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended');
|
||||||
aiAssistant.actions.closeChat();
|
aiAssistant.actions.closeChat();
|
||||||
aiAssistant.actions.openChat();
|
aiAssistant.actions.openChatFromCanvas();
|
||||||
// Now, session should be reset
|
// Now, session should be reset
|
||||||
aiAssistant.getters.placeholderMessage().should('be.visible');
|
aiAssistant.getters.placeholderMessage().should('be.visible');
|
||||||
});
|
});
|
||||||
@@ -324,7 +324,7 @@ describe('AI Assistant::enabled', () => {
|
|||||||
fixture: 'aiAssistant/responses/simple_message_response.json',
|
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
aiAssistant.actions.openChat();
|
aiAssistant.actions.openChatFromCanvas();
|
||||||
aiAssistant.actions.sendMessage('Hello');
|
aiAssistant.actions.sendMessage('Hello');
|
||||||
wf.actions.openNode(SCHEDULE_TRIGGER_NODE_NAME);
|
wf.actions.openNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
ndv.getters.nodeExecuteButton().click();
|
ndv.getters.nodeExecuteButton().click();
|
||||||
@@ -339,7 +339,7 @@ describe('AI Assistant::enabled', () => {
|
|||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
|
|
||||||
wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
aiAssistant.actions.openChat();
|
aiAssistant.actions.openChatFromCanvas();
|
||||||
nodeCreatorFeature.actions.openNodeCreator();
|
nodeCreatorFeature.actions.openNodeCreator();
|
||||||
aiAssistant.getters.chatInput().type('Hello{Enter}');
|
aiAssistant.getters.chatInput().type('Hello{Enter}');
|
||||||
|
|
||||||
@@ -488,8 +488,8 @@ describe('General help', () => {
|
|||||||
fixture: 'aiAssistant/responses/code_snippet_response.json',
|
fixture: 'aiAssistant/responses/code_snippet_response.json',
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
|
|
||||||
aiAssistant.getters.askAssistantFloatingButton().should('be.visible');
|
aiAssistant.getters.askAssistantCanvasActionButton().should('be.visible');
|
||||||
aiAssistant.getters.askAssistantFloatingButton().click();
|
aiAssistant.getters.askAssistantCanvasActionButton().click();
|
||||||
aiAssistant.getters.askAssistantChat().should('be.visible');
|
aiAssistant.getters.askAssistantChat().should('be.visible');
|
||||||
aiAssistant.getters.placeholderMessage().should('be.visible');
|
aiAssistant.getters.placeholderMessage().should('be.visible');
|
||||||
aiAssistant.getters.chatInput().should('be.visible');
|
aiAssistant.getters.chatInput().should('be.visible');
|
||||||
@@ -537,7 +537,7 @@ describe('General help', () => {
|
|||||||
fixture: 'aiAssistant/responses/simple_message_response.json',
|
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
|
|
||||||
aiAssistant.getters.askAssistantFloatingButton().click();
|
aiAssistant.getters.askAssistantCanvasActionButton().click();
|
||||||
aiAssistant.actions.sendMessage('What is wrong with this workflow?');
|
aiAssistant.actions.sendMessage('What is wrong with this workflow?');
|
||||||
|
|
||||||
cy.wait('@chatRequest').then((interception) => {
|
cy.wait('@chatRequest').then((interception) => {
|
||||||
@@ -557,7 +557,7 @@ describe('General help', () => {
|
|||||||
fixture: 'aiAssistant/responses/simple_message_response.json',
|
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
|
|
||||||
aiAssistant.getters.askAssistantFloatingButton().click();
|
aiAssistant.getters.askAssistantCanvasActionButton().click();
|
||||||
wf.getters.zoomToFitButton().click();
|
wf.getters.zoomToFitButton().click();
|
||||||
|
|
||||||
aiAssistant.actions.sendMessage('What is wrong with this workflow?');
|
aiAssistant.actions.sendMessage('What is wrong with this workflow?');
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export class AIAssistant extends BasePage {
|
|||||||
|
|
||||||
getters = {
|
getters = {
|
||||||
askAssistantFloatingButton: () => cy.getByTestId('ask-assistant-floating-button'),
|
askAssistantFloatingButton: () => cy.getByTestId('ask-assistant-floating-button'),
|
||||||
|
askAssistantCanvasActionButton: () => cy.getByTestId('ask-assistant-canvas-action-button'),
|
||||||
askAssistantSidebar: () => cy.getByTestId('ask-assistant-sidebar'),
|
askAssistantSidebar: () => cy.getByTestId('ask-assistant-sidebar'),
|
||||||
askAssistantSidebarResizer: () =>
|
askAssistantSidebarResizer: () =>
|
||||||
this.getters.askAssistantSidebar().find('[class^=_resizer][data-dir=left]').first(),
|
this.getters.askAssistantSidebar().find('[class^=_resizer][data-dir=left]').first(),
|
||||||
@@ -53,7 +54,11 @@ export class AIAssistant extends BasePage {
|
|||||||
this.getters.closeChatButton().click();
|
this.getters.closeChatButton().click();
|
||||||
this.getters.askAssistantChat().should('not.be.visible');
|
this.getters.askAssistantChat().should('not.be.visible');
|
||||||
},
|
},
|
||||||
openChat: () => {
|
openChatFromCanvas: () => {
|
||||||
|
this.getters.askAssistantCanvasActionButton().click();
|
||||||
|
this.getters.askAssistantChat().should('be.visible');
|
||||||
|
},
|
||||||
|
openChatFromNdv: () => {
|
||||||
this.getters.askAssistantFloatingButton().click();
|
this.getters.askAssistantFloatingButton().click();
|
||||||
this.getters.askAssistantChat().should('be.visible');
|
this.getters.askAssistantChat().should('be.visible');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { ref } from 'vue';
|
|||||||
import { useI18n } from '../../composables/useI18n';
|
import { useI18n } from '../../composables/useI18n';
|
||||||
import AssistantIcon from '../AskAssistantIcon/AssistantIcon.vue';
|
import AssistantIcon from '../AskAssistantIcon/AssistantIcon.vue';
|
||||||
import AssistantText from '../AskAssistantText/AssistantText.vue';
|
import AssistantText from '../AskAssistantText/AssistantText.vue';
|
||||||
import BetaTag from '../BetaTag/BetaTag.vue';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -48,9 +47,6 @@ function onMouseLeave() {
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<BetaTag />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -92,7 +88,6 @@ function onMouseLeave() {
|
|||||||
|
|
||||||
.text {
|
.text {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -1px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
|
|||||||
@@ -21,13 +21,6 @@ exports[`AskAssistantButton > renders button with unread messages correctly 1`]
|
|||||||
Ask Assistant
|
Ask Assistant
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="beta"
|
|
||||||
>
|
|
||||||
beta
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,13 +76,6 @@ exports[`AskAssistantButton > renders default button correctly 1`] = `
|
|||||||
Ask Assistant
|
Ask Assistant
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="beta"
|
|
||||||
>
|
|
||||||
beta
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import type { ChatUI } from '../../types/assistant';
|
|||||||
import AssistantIcon from '../AskAssistantIcon/AssistantIcon.vue';
|
import AssistantIcon from '../AskAssistantIcon/AssistantIcon.vue';
|
||||||
import AssistantLoadingMessage from '../AskAssistantLoadingMessage/AssistantLoadingMessage.vue';
|
import AssistantLoadingMessage from '../AskAssistantLoadingMessage/AssistantLoadingMessage.vue';
|
||||||
import AssistantText from '../AskAssistantText/AssistantText.vue';
|
import AssistantText from '../AskAssistantText/AssistantText.vue';
|
||||||
import BetaTag from '../BetaTag/BetaTag.vue';
|
|
||||||
import InlineAskAssistantButton from '../InlineAskAssistantButton/InlineAskAssistantButton.vue';
|
import InlineAskAssistantButton from '../InlineAskAssistantButton/InlineAskAssistantButton.vue';
|
||||||
import N8nButton from '../N8nButton';
|
import N8nButton from '../N8nButton';
|
||||||
import N8nIcon from '../N8nIcon';
|
import N8nIcon from '../N8nIcon';
|
||||||
@@ -123,7 +122,6 @@ function onSubmitFeedback(feedback: string) {
|
|||||||
<AssistantIcon size="large" />
|
<AssistantIcon size="large" />
|
||||||
<AssistantText size="large" :text="title" />
|
<AssistantText size="large" :text="title" />
|
||||||
</div>
|
</div>
|
||||||
<BetaTag />
|
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.back" data-test-id="close-chat-button" @click="onClose">
|
<div :class="$style.back" data-test-id="close-chat-button" @click="onClose">
|
||||||
|
|||||||
@@ -54,11 +54,6 @@ exports[`AskAssistantChat > does not render retry button if no error is present
|
|||||||
AI Assistant
|
AI Assistant
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="beta"
|
|
||||||
>
|
|
||||||
beta
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -247,11 +242,6 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
|
|||||||
AI Assistant
|
AI Assistant
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="beta"
|
|
||||||
>
|
|
||||||
beta
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1064,11 +1054,6 @@ exports[`AskAssistantChat > renders default placeholder chat correctly 1`] = `
|
|||||||
AI Assistant
|
AI Assistant
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="beta"
|
|
||||||
>
|
|
||||||
beta
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1250,11 +1235,6 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
|
|||||||
AI Assistant
|
AI Assistant
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="beta"
|
|
||||||
>
|
|
||||||
beta
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1520,11 +1500,6 @@ exports[`AskAssistantChat > renders error message correctly with retry button 1`
|
|||||||
AI Assistant
|
AI Assistant
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="beta"
|
|
||||||
>
|
|
||||||
beta
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1725,11 +1700,6 @@ exports[`AskAssistantChat > renders message with code snippet 1`] = `
|
|||||||
AI Assistant
|
AI Assistant
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="beta"
|
|
||||||
>
|
|
||||||
beta
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1988,11 +1958,6 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
|
|||||||
AI Assistant
|
AI Assistant
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="beta"
|
|
||||||
>
|
|
||||||
beta
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -195,6 +195,7 @@
|
|||||||
"aiAssistant.prompts.currentView.credentialsList": "The user is currently looking at the list of credentials.",
|
"aiAssistant.prompts.currentView.credentialsList": "The user is currently looking at the list of credentials.",
|
||||||
"aiAssistant.prompts.currentView.executionsView": "The user is currently looking at the list of executions for the currently open workflow.",
|
"aiAssistant.prompts.currentView.executionsView": "The user is currently looking at the list of executions for the currently open workflow.",
|
||||||
"aiAssistant.prompts.currentView.workflowEditor": "The user is currently looking at the current workflow in n8n editor, without any specific node selected.",
|
"aiAssistant.prompts.currentView.workflowEditor": "The user is currently looking at the current workflow in n8n editor, without any specific node selected.",
|
||||||
|
"aiAssistant.tooltip": "Ask Assistant",
|
||||||
"banners.confirmEmail.message.1": "To secure your account and prevent future access issues, please confirm your",
|
"banners.confirmEmail.message.1": "To secure your account and prevent future access issues, please confirm your",
|
||||||
"banners.confirmEmail.message.2": "email address.",
|
"banners.confirmEmail.message.2": "email address.",
|
||||||
"banners.confirmEmail.button": "Confirm email",
|
"banners.confirmEmail.button": "Confirm email",
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ useHistoryHelper(route);
|
|||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const defaultLocale = computed(() => rootStore.defaultLocale);
|
const defaultLocale = computed(() => rootStore.defaultLocale);
|
||||||
const isDemoMode = computed(() => route.name === VIEWS.DEMO);
|
const isDemoMode = computed(() => route.name === VIEWS.DEMO);
|
||||||
const showAssistantButton = computed(() => assistantStore.canShowAssistantButtonsOnCanvas);
|
const showAssistantFloatingButton = computed(
|
||||||
|
() =>
|
||||||
|
assistantStore.canShowAssistantButtonsOnCanvas && !assistantStore.hideAssistantFloatingButton,
|
||||||
|
);
|
||||||
const hasContentFooter = ref(false);
|
const hasContentFooter = ref(false);
|
||||||
const appGrid = ref<Element | null>(null);
|
const appGrid = ref<Element | null>(null);
|
||||||
|
|
||||||
@@ -129,7 +132,7 @@ watch(
|
|||||||
<Modals />
|
<Modals />
|
||||||
</div>
|
</div>
|
||||||
<Telemetry />
|
<Telemetry />
|
||||||
<AskAssistantFloatingButton v-if="showAssistantButton" />
|
<AskAssistantFloatingButton v-if="showAssistantFloatingButton" />
|
||||||
</div>
|
</div>
|
||||||
<AssistantsHub />
|
<AssistantsHub />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ const onClick = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="assistantStore.canShowAssistantButtonsOnCanvas && !assistantStore.isAssistantOpen"
|
v-if="
|
||||||
|
assistantStore.canShowAssistantButtonsOnCanvas &&
|
||||||
|
!assistantStore.isAssistantOpen &&
|
||||||
|
!assistantStore.hideAssistantFloatingButton
|
||||||
|
"
|
||||||
:class="$style.container"
|
:class="$style.container"
|
||||||
data-test-id="ask-assistant-floating-button"
|
data-test-id="ask-assistant-floating-button"
|
||||||
:style="{ '--canvas-panel-height-offset': `${logsStore.height}px` }"
|
:style="{ '--canvas-panel-height-offset': `${logsStore.height}px` }"
|
||||||
@@ -64,16 +68,9 @@ const onClick = () => {
|
|||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.container {
|
.container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: calc(var(--canvas-panel-height-offset, 0px) + var(--spacing-s));
|
bottom: var(--spacing-2xl);
|
||||||
right: var(--spacing-s);
|
right: var(--spacing-s);
|
||||||
z-index: var(--z-index-ask-assistant-floating-button);
|
z-index: var(--z-index-ask-assistant-floating-button);
|
||||||
|
|
||||||
/* Prevent overlap with 'Execute Workflow' / 'Open Chat' buttons on small screens */
|
|
||||||
@include mixins.breakpoint('sm-only') {
|
|
||||||
bottom: calc(
|
|
||||||
var(--canvas-panel-height-offset, 0px) + var(--spacing-s) + var(--spacing-xs) + 42px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ import type {
|
|||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import { useActions } from './NodeCreator/composables/useActions';
|
import { useActions } from './NodeCreator/composables/useActions';
|
||||||
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
||||||
|
import AssistantIcon from '@n8n/design-system/components/AskAssistantIcon/AssistantIcon.vue';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import { useAssistantStore } from '@/stores/assistant.store';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
nodeViewScale: number;
|
nodeViewScale: number;
|
||||||
@@ -44,6 +46,7 @@ const uiStore = useUIStore();
|
|||||||
const focusPanelStore = useFocusPanelStore();
|
const focusPanelStore = useFocusPanelStore();
|
||||||
const posthogStore = usePostHog();
|
const posthogStore = usePostHog();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const assistantStore = useAssistantStore();
|
||||||
|
|
||||||
const { getAddedNodesAndConnections } = useActions();
|
const { getAddedNodesAndConnections } = useActions();
|
||||||
|
|
||||||
@@ -82,6 +85,17 @@ function nodeTypeSelected(value: NodeTypeSelectedPayload[]) {
|
|||||||
emit('addNodes', getAddedNodesAndConnections(value));
|
emit('addNodes', getAddedNodesAndConnections(value));
|
||||||
closeNodeCreator(true);
|
closeNodeCreator(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onAskAssistantButtonClick() {
|
||||||
|
if (!assistantStore.chatWindowOpen)
|
||||||
|
assistantStore.trackUserOpenedAssistant({
|
||||||
|
source: 'canvas',
|
||||||
|
task: 'placeholder',
|
||||||
|
has_existing_session: !assistantStore.isSessionEnded,
|
||||||
|
});
|
||||||
|
|
||||||
|
assistantStore.toggleChatOpen();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -125,6 +139,24 @@ function nodeTypeSelected(value: NodeTypeSelectedPayload[]) {
|
|||||||
@click="focusPanelStore.toggleFocusPanel"
|
@click="focusPanelStore.toggleFocusPanel"
|
||||||
/>
|
/>
|
||||||
</KeyboardShortcutTooltip>
|
</KeyboardShortcutTooltip>
|
||||||
|
<n8n-tooltip placement="left">
|
||||||
|
<template #content> {{ i18n.baseText('aiAssistant.tooltip') }}</template>
|
||||||
|
<n8n-button
|
||||||
|
v-if="assistantStore.canShowAssistantButtonsOnCanvas"
|
||||||
|
type="tertiary"
|
||||||
|
size="large"
|
||||||
|
square
|
||||||
|
:class="$style.icon"
|
||||||
|
data-test-id="ask-assistant-canvas-action-button"
|
||||||
|
@click="onAskAssistantButtonClick"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<div>
|
||||||
|
<AssistantIcon size="large" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n8n-button>
|
||||||
|
</n8n-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<LazyNodeCreator
|
<LazyNodeCreator
|
||||||
@@ -146,4 +178,14 @@ function nodeTypeSelected(value: NodeTypeSelectedPayload[]) {
|
|||||||
padding: var(--spacing-s);
|
padding: var(--spacing-s);
|
||||||
pointer-events: all !important;
|
pointer-events: all !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
MIN_CHAT_WIDTH,
|
MIN_CHAT_WIDTH,
|
||||||
useAssistantStore,
|
useAssistantStore,
|
||||||
} from '@/stores/assistant.store';
|
} from '@/stores/assistant.store';
|
||||||
|
import { useWorkflowsStore } from './workflows.store';
|
||||||
import type { ChatRequest } from '@/types/assistant.types';
|
import type { ChatRequest } from '@/types/assistant.types';
|
||||||
import { usePostHog } from './posthog.store';
|
import { usePostHog } from './posthog.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
@@ -43,11 +44,12 @@ const setAssistantEnabled = (enabled: boolean) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let currentRouteName = ENABLED_VIEWS[0];
|
let currentRouteName = ENABLED_VIEWS[0];
|
||||||
|
let currentRouteParams = {};
|
||||||
vi.mock('vue-router', () => ({
|
vi.mock('vue-router', () => ({
|
||||||
useRoute: vi.fn(() =>
|
useRoute: vi.fn(() =>
|
||||||
reactive({
|
reactive({
|
||||||
path: '/',
|
path: '/',
|
||||||
params: {},
|
params: currentRouteParams,
|
||||||
name: currentRouteName,
|
name: currentRouteName,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -58,6 +60,7 @@ vi.mock('vue-router', () => ({
|
|||||||
describe('AI Assistant store', () => {
|
describe('AI Assistant store', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
currentRouteParams = {};
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
settingsStore = useSettingsStore();
|
settingsStore = useSettingsStore();
|
||||||
settingsStore.setSettings(
|
settingsStore.setSettings(
|
||||||
@@ -305,7 +308,7 @@ describe('AI Assistant store', () => {
|
|||||||
|
|
||||||
[VIEWS.PROJECTS_CREDENTIALS, VIEWS.TEMPLATE_SETUP, VIEWS.CREDENTIALS].forEach((view) => {
|
[VIEWS.PROJECTS_CREDENTIALS, VIEWS.TEMPLATE_SETUP, VIEWS.CREDENTIALS].forEach((view) => {
|
||||||
it(`should show assistant if on ${view} page`, () => {
|
it(`should show assistant if on ${view} page`, () => {
|
||||||
currentRouteName = VIEWS.PROJECTS_CREDENTIALS;
|
currentRouteName = view;
|
||||||
const assistantStore = useAssistantStore();
|
const assistantStore = useAssistantStore();
|
||||||
|
|
||||||
setAssistantEnabled(true);
|
setAssistantEnabled(true);
|
||||||
@@ -315,6 +318,48 @@ describe('AI Assistant store', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
{ view: VIEWS.WORKFLOW, nodeId: 'nodeId' },
|
||||||
|
{ view: VIEWS.NEW_WORKFLOW },
|
||||||
|
{ view: VIEWS.EXECUTION_DEBUG },
|
||||||
|
].forEach(({ view, nodeId }) => {
|
||||||
|
it(`should show ai assistant floating button if on ${view} page`, () => {
|
||||||
|
currentRouteName = view;
|
||||||
|
currentRouteParams = nodeId ? { nodeId } : {};
|
||||||
|
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
workflowsStore.activeNode = () => ({
|
||||||
|
id: 'test-node',
|
||||||
|
name: 'Test Node',
|
||||||
|
type: 'test',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [0, 0],
|
||||||
|
parameters: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const assistantStore = useAssistantStore();
|
||||||
|
|
||||||
|
setAssistantEnabled(true);
|
||||||
|
expect(assistantStore.canShowAssistantButtonsOnCanvas).toBe(true);
|
||||||
|
expect(assistantStore.hideAssistantFloatingButton).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[{ view: VIEWS.WORKFLOW }, { view: VIEWS.NEW_WORKFLOW }].forEach(({ view }) => {
|
||||||
|
it(`should hide ai assistant floating button if on canvas of ${view} page`, () => {
|
||||||
|
currentRouteName = view;
|
||||||
|
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
workflowsStore.activeNode = () => null;
|
||||||
|
|
||||||
|
const assistantStore = useAssistantStore();
|
||||||
|
|
||||||
|
setAssistantEnabled(true);
|
||||||
|
expect(assistantStore.canShowAssistantButtonsOnCanvas).toBe(true);
|
||||||
|
expect(assistantStore.hideAssistantFloatingButton).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should initialize assistant chat session on node error', async () => {
|
it('should initialize assistant chat session on node error', async () => {
|
||||||
const context: ChatRequest.ErrorContext = {
|
const context: ChatRequest.ErrorContext = {
|
||||||
error: {
|
error: {
|
||||||
|
|||||||
@@ -113,6 +113,11 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
|||||||
const canShowAssistantButtonsOnCanvas = computed(
|
const canShowAssistantButtonsOnCanvas = computed(
|
||||||
() => isAssistantEnabled.value && EDITABLE_CANVAS_VIEWS.includes(route.name as VIEWS),
|
() => isAssistantEnabled.value && EDITABLE_CANVAS_VIEWS.includes(route.name as VIEWS),
|
||||||
);
|
);
|
||||||
|
const hideAssistantFloatingButton = computed(
|
||||||
|
() =>
|
||||||
|
(route.name === VIEWS.WORKFLOW || route.name === VIEWS.NEW_WORKFLOW) &&
|
||||||
|
!workflowsStore.activeNode(),
|
||||||
|
);
|
||||||
|
|
||||||
const unreadCount = computed(
|
const unreadCount = computed(
|
||||||
() =>
|
() =>
|
||||||
@@ -161,6 +166,14 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
|||||||
}, ASK_AI_SLIDE_OUT_DURATION_MS + 50);
|
}, ASK_AI_SLIDE_OUT_DURATION_MS + 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleChatOpen() {
|
||||||
|
if (chatWindowOpen.value) {
|
||||||
|
closeChat();
|
||||||
|
} else {
|
||||||
|
openChat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addAssistantMessages(newMessages: ChatRequest.MessageResponse[], id: string) {
|
function addAssistantMessages(newMessages: ChatRequest.MessageResponse[], id: string) {
|
||||||
const read = chatWindowOpen.value;
|
const read = chatWindowOpen.value;
|
||||||
const messages = [...chatMessages.value].filter(
|
const messages = [...chatMessages.value].filter(
|
||||||
@@ -814,6 +827,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
|||||||
return {
|
return {
|
||||||
isAssistantEnabled,
|
isAssistantEnabled,
|
||||||
canShowAssistantButtonsOnCanvas,
|
canShowAssistantButtonsOnCanvas,
|
||||||
|
hideAssistantFloatingButton,
|
||||||
chatWidth,
|
chatWidth,
|
||||||
chatMessages,
|
chatMessages,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
@@ -827,6 +841,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
|||||||
trackUserOpenedAssistant,
|
trackUserOpenedAssistant,
|
||||||
closeChat,
|
closeChat,
|
||||||
openChat,
|
openChat,
|
||||||
|
toggleChatOpen,
|
||||||
updateWindowWidth,
|
updateWindowWidth,
|
||||||
isNodeErrorActive,
|
isNodeErrorActive,
|
||||||
initErrorHelper,
|
initErrorHelper,
|
||||||
|
|||||||
Reference in New Issue
Block a user