feat(editor): Implement AI Assistant chat UI (#9300)
This commit is contained in:
committed by
GitHub
parent
23b676d7cb
commit
491c6ec546
@@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user