mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-19 11:01:15 +00:00
feat(editor): Move AI Assistant button to canvas action buttons (#16879)
This commit is contained in:
@@ -38,7 +38,10 @@ useHistoryHelper(route);
|
||||
const loading = ref(true);
|
||||
const defaultLocale = computed(() => rootStore.defaultLocale);
|
||||
const isDemoMode = computed(() => route.name === VIEWS.DEMO);
|
||||
const showAssistantButton = computed(() => assistantStore.canShowAssistantButtonsOnCanvas);
|
||||
const showAssistantFloatingButton = computed(
|
||||
() =>
|
||||
assistantStore.canShowAssistantButtonsOnCanvas && !assistantStore.hideAssistantFloatingButton,
|
||||
);
|
||||
const hasContentFooter = ref(false);
|
||||
const appGrid = ref<Element | null>(null);
|
||||
|
||||
@@ -129,7 +132,7 @@ watch(
|
||||
<Modals />
|
||||
</div>
|
||||
<Telemetry />
|
||||
<AskAssistantFloatingButton v-if="showAssistantButton" />
|
||||
<AskAssistantFloatingButton v-if="showAssistantFloatingButton" />
|
||||
</div>
|
||||
<AssistantsHub />
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,11 @@ const onClick = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="assistantStore.canShowAssistantButtonsOnCanvas && !assistantStore.isAssistantOpen"
|
||||
v-if="
|
||||
assistantStore.canShowAssistantButtonsOnCanvas &&
|
||||
!assistantStore.isAssistantOpen &&
|
||||
!assistantStore.hideAssistantFloatingButton
|
||||
"
|
||||
:class="$style.container"
|
||||
data-test-id="ask-assistant-floating-button"
|
||||
:style="{ '--canvas-panel-height-offset': `${logsStore.height}px` }"
|
||||
@@ -64,16 +68,9 @@ const onClick = () => {
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
position: absolute;
|
||||
bottom: calc(var(--canvas-panel-height-offset, 0px) + var(--spacing-s));
|
||||
bottom: var(--spacing-2xl);
|
||||
right: var(--spacing-s);
|
||||
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 {
|
||||
|
||||
@@ -19,7 +19,9 @@ import type {
|
||||
} from '@/Interface';
|
||||
import { useActions } from './NodeCreator/composables/useActions';
|
||||
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
||||
import AssistantIcon from '@n8n/design-system/components/AskAssistantIcon/AssistantIcon.vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useAssistantStore } from '@/stores/assistant.store';
|
||||
|
||||
type Props = {
|
||||
nodeViewScale: number;
|
||||
@@ -44,6 +46,7 @@ const uiStore = useUIStore();
|
||||
const focusPanelStore = useFocusPanelStore();
|
||||
const posthogStore = usePostHog();
|
||||
const i18n = useI18n();
|
||||
const assistantStore = useAssistantStore();
|
||||
|
||||
const { getAddedNodesAndConnections } = useActions();
|
||||
|
||||
@@ -82,6 +85,17 @@ function nodeTypeSelected(value: NodeTypeSelectedPayload[]) {
|
||||
emit('addNodes', getAddedNodesAndConnections(value));
|
||||
closeNodeCreator(true);
|
||||
}
|
||||
|
||||
function onAskAssistantButtonClick() {
|
||||
if (!assistantStore.chatWindowOpen)
|
||||
assistantStore.trackUserOpenedAssistant({
|
||||
source: 'canvas',
|
||||
task: 'placeholder',
|
||||
has_existing_session: !assistantStore.isSessionEnded,
|
||||
});
|
||||
|
||||
assistantStore.toggleChatOpen();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -125,6 +139,24 @@ function nodeTypeSelected(value: NodeTypeSelectedPayload[]) {
|
||||
@click="focusPanelStore.toggleFocusPanel"
|
||||
/>
|
||||
</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>
|
||||
<Suspense>
|
||||
<LazyNodeCreator
|
||||
@@ -146,4 +178,14 @@ function nodeTypeSelected(value: NodeTypeSelectedPayload[]) {
|
||||
padding: var(--spacing-s);
|
||||
pointer-events: all !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
MIN_CHAT_WIDTH,
|
||||
useAssistantStore,
|
||||
} from '@/stores/assistant.store';
|
||||
import { useWorkflowsStore } from './workflows.store';
|
||||
import type { ChatRequest } from '@/types/assistant.types';
|
||||
import { usePostHog } from './posthog.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
@@ -43,11 +44,12 @@ const setAssistantEnabled = (enabled: boolean) => {
|
||||
};
|
||||
|
||||
let currentRouteName = ENABLED_VIEWS[0];
|
||||
let currentRouteParams = {};
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: vi.fn(() =>
|
||||
reactive({
|
||||
path: '/',
|
||||
params: {},
|
||||
params: currentRouteParams,
|
||||
name: currentRouteName,
|
||||
}),
|
||||
),
|
||||
@@ -58,6 +60,7 @@ vi.mock('vue-router', () => ({
|
||||
describe('AI Assistant store', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
currentRouteParams = {};
|
||||
setActivePinia(createPinia());
|
||||
settingsStore = useSettingsStore();
|
||||
settingsStore.setSettings(
|
||||
@@ -305,7 +308,7 @@ describe('AI Assistant store', () => {
|
||||
|
||||
[VIEWS.PROJECTS_CREDENTIALS, VIEWS.TEMPLATE_SETUP, VIEWS.CREDENTIALS].forEach((view) => {
|
||||
it(`should show assistant if on ${view} page`, () => {
|
||||
currentRouteName = VIEWS.PROJECTS_CREDENTIALS;
|
||||
currentRouteName = view;
|
||||
const assistantStore = useAssistantStore();
|
||||
|
||||
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 () => {
|
||||
const context: ChatRequest.ErrorContext = {
|
||||
error: {
|
||||
|
||||
@@ -113,6 +113,11 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||
const canShowAssistantButtonsOnCanvas = computed(
|
||||
() => 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(
|
||||
() =>
|
||||
@@ -161,6 +166,14 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||
}, ASK_AI_SLIDE_OUT_DURATION_MS + 50);
|
||||
}
|
||||
|
||||
function toggleChatOpen() {
|
||||
if (chatWindowOpen.value) {
|
||||
closeChat();
|
||||
} else {
|
||||
openChat();
|
||||
}
|
||||
}
|
||||
|
||||
function addAssistantMessages(newMessages: ChatRequest.MessageResponse[], id: string) {
|
||||
const read = chatWindowOpen.value;
|
||||
const messages = [...chatMessages.value].filter(
|
||||
@@ -814,6 +827,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||
return {
|
||||
isAssistantEnabled,
|
||||
canShowAssistantButtonsOnCanvas,
|
||||
hideAssistantFloatingButton,
|
||||
chatWidth,
|
||||
chatMessages,
|
||||
unreadCount,
|
||||
@@ -827,6 +841,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||
trackUserOpenedAssistant,
|
||||
closeChat,
|
||||
openChat,
|
||||
toggleChatOpen,
|
||||
updateWindowWidth,
|
||||
isNodeErrorActive,
|
||||
initErrorHelper,
|
||||
|
||||
Reference in New Issue
Block a user