refactor(core): Remove Ask AI HTTP request feature (no-changelog) (#9931)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2024-07-04 12:09:45 +02:00
committed by GitHub
parent cef177455e
commit 86018aa6e0
41 changed files with 30 additions and 5198 deletions

View File

@@ -26,9 +26,6 @@
<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>
@@ -60,8 +57,6 @@ import { useUsageStore } from '@/stores/usage.store';
import { useUsersStore } from '@/stores/users.store';
import { useHistoryHelper } from '@/composables/useHistoryHelper';
import { useRoute } from 'vue-router';
import { useAIStore } from './stores/ai.store';
import AIAssistantChat from './components/AIAssistantChat/AIAssistantChat.vue';
export default defineComponent({
name: 'App',
@@ -70,7 +65,6 @@ export default defineComponent({
LoadingView,
Telemetry,
Modals,
AIAssistantChat,
},
setup() {
return {
@@ -91,7 +85,6 @@ export default defineComponent({
useSourceControlStore,
useCloudPlanStore,
useUsageStore,
useAIStore,
),
defaultLocale(): string {
return this.rootStore.defaultLocale;
@@ -134,10 +127,10 @@ export default defineComponent({
.container {
display: grid;
grid-template-areas:
'banners banners banners'
'sidebar header chat'
'sidebar content chat';
grid-auto-columns: fit-content($sidebar-expanded-width) 1fr fit-content($chat-width);
'banners banners'
'sidebar header'
'sidebar content';
grid-auto-columns: fit-content($sidebar-expanded-width) 1fr;
grid-template-rows: auto fit-content($header-height) 1fr;
height: 100vh;
}
@@ -171,15 +164,4 @@ export default defineComponent({
height: 100%;
z-index: 999;
}
.chat {
grid-area: chat;
z-index: 999;
height: 100%;
width: 0;
transition: all 0.2s ease-in-out;
&.open {
width: $chat-width;
}
}
</style>

View File

@@ -1856,10 +1856,6 @@ export type NewConnectionInfo = {
endpointUuid?: string;
};
export type AIAssistantConnectionInfo = NewConnectionInfo & {
stepName?: string;
};
export type EnterpriseEditionFeatureKey =
| 'AdvancedExecutionFilters'
| 'Sharing'

View File

@@ -114,10 +114,6 @@ export const defaultSettings: IN8nUISettings = {
},
ai: {
enabled: false,
provider: '',
features: {
generateCurl: false,
},
},
workflowHistory: {
pruneTime: 0,

View File

@@ -2,16 +2,6 @@ import type { IRestApiContext, Schema } from '@/Interface';
import { makeRestApiRequest } from '@/utils/apiUtils';
import type { IDataObject } from 'n8n-workflow';
export interface GenerateCurlPayload {
service: string;
request: string;
}
export interface GenerateCurlResponse {
curl: string;
metadata: object;
}
export async function generateCodeForPrompt(
ctx: IRestApiContext,
{
@@ -38,15 +28,3 @@ export async function generateCodeForPrompt(
n8nVersion,
} as IDataObject);
}
export const generateCurl = async (
context: IRestApiContext,
payload: GenerateCurlPayload,
): Promise<GenerateCurlResponse> => {
return await makeRestApiRequest(
context,
'POST',
'/ai/generate-curl',
payload as unknown as IDataObject,
);
};

View File

@@ -1,225 +0,0 @@
<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, onMounted, onBeforeUnmount } 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 {
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';
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?.stepName) {
return locale.baseText('aiAssistantChat.initialMessage.nextStep', {
interpolate: { currentAction: latestConnectionInfo.value.stepName },
});
}
return locale.baseText('aiAssistantChat.initialMessage.firstStep');
});
const initialMessages: Ref<ChatMessage[]> = ref([
{
id: '1',
type: 'text',
sender: 'bot',
createdAt: new Date().toISOString(),
text: `${locale.baseText('aiAssistantChat.greeting', { interpolate: { username: userName.value ?? 'there' } })} ${initialMessageText.value}`,
},
]);
const sendMessage = async (message: string) => {
disableChat.value = true;
waitingForResponse.value = true;
messages.value.push({
id: String(messages.value.length + 1),
sender: 'user',
text: message,
createdAt: new Date().toISOString(),
});
trackUserMessage(message);
thanksResponses.forEach((response, index) => {
// Push each response with a delay of 1500ms
setTimeout(
() => {
messages.value.push(response);
chatEventBus.emit('scrollToBottom');
if (index === thanksResponses.length - 1) {
waitingForResponse.value = false;
// Once last message is sent, disable the experiment
useStorage(AI_ASSISTANT_LOCAL_STORAGE_KEY).value = 'true';
}
},
1500 * (index + 1),
);
});
chatEventBus.emit('scrollToBottom');
};
const trackUserMessage = (message: string) => {
telemetry.track('User responded in AI chat', {
prompt: message,
chatMode: 'nextStepAssistant',
initialMessage: initialMessageText.value,
});
};
const chatOptions: ChatOptions = {
i18n: {
en: {
title: chatTitle,
footer: '',
subtitle: '',
inputPlaceholder: locale.baseText('aiAssistantChat.chatPlaceholder'),
getStarted: locale.baseText('aiAssistantChat.getStarted'),
closeButtonTooltip: locale.baseText('aiAssistantChat.closeButtonTooltip'),
},
},
webhookUrl: 'https://webhook.url',
mode: 'window',
showWindowCloseButton: true,
messageComponents: {
QuickReplies,
},
disabled: disableChat,
};
const chatConfig: Chat = {
messages,
sendMessage,
initialMessages,
currentSessionId,
waitingForResponse,
};
provide(ChatSymbol, chatConfig);
provide(ChatOptionsSymbol, chatOptions);
onMounted(() => {
chatEventBus.emit('focusInput');
chatEventBus.on('close', onBeforeClose);
});
onBeforeUnmount(() => {
chatEventBus.off('close', onBeforeClose);
});
async function onBeforeClose() {
const confirmModal = await confirm(locale.baseText('aiAssistantChat.closeChatConfirmation'), {
confirmButtonText: locale.baseText('aiAssistantChat.closeChatConfirmation.confirm'),
cancelButtonText: locale.baseText('aiAssistantChat.closeChatConfirmation.cancel'),
});
if (confirmModal === MODAL_CONFIRM) {
aiStore.assistantChatOpen = false;
}
}
</script>
<template>
<div :class="[$style.container, 'ignore-key-press']">
<ChatComponent />
</div>
</template>
<style module lang="scss">
.container {
height: 100%;
background-color: var(--color-background-light);
filter: drop-shadow(0px 8px 24px #41424412);
border-left: 1px solid var(--color-foreground-dark);
overflow: hidden;
}
.header {
font-size: var(--font-size-l);
background-color: #fff;
padding: var(--spacing-xs);
}
.content {
padding: var(--spacing-xs);
}
</style>

View File

@@ -1,148 +0,0 @@
<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<{ optionSelected: [option: string] }>();
const aiAssistantChatOpen = computed(() => aiStore.assistantChatOpen);
const title = computed(() => {
return aiStore.nextStepPopupConfig.title;
});
const options = computed(() => [
{
label: locale.baseText('nextStepPopup.option.choose'),
icon: '',
key: 'choose',
disabled: false,
},
{
label: locale.baseText('nextStepPopup.option.generate'),
icon: '✨',
key: 'generate',
disabled: aiAssistantChatOpen.value,
},
]);
const position = computed(() => {
return [aiStore.nextStepPopupConfig.position[0], aiStore.nextStepPopupConfig.position[1]];
});
const style = computed(() => {
return {
left: `${position.value[0]}px`,
top: `${position.value[1]}px`,
};
});
const close = () => {
aiStore.closeNextStepPopup();
};
const onOptionSelected = (option: string) => {
if (option === 'choose') {
emit('optionSelected', option);
} else if (option === 'generate') {
telemetry.track('User clicked generate AI button', {}, { withPostHog: true });
aiStore.assistantChatOpen = true;
}
close();
};
</script>
<template>
<div v-on-click-outside="close" :class="$style.container" :style="style">
<div :class="$style.title">{{ title }}</div>
<ul :class="$style.options">
<li
v-for="option in options"
:key="option.key"
:class="{ [$style.option]: true, [$style.disabled]: option.disabled }"
@click="onOptionSelected(option.key)"
>
<div :class="$style.icon">
{{ option.icon }}
</div>
<div :class="$style.label">
{{ option.label }}
</div>
</li>
</ul>
</div>
</template>
<style module lang="scss">
.container {
position: fixed;
display: flex;
flex-direction: column;
min-width: 190px;
font-size: var(--font-size-2xs);
background: var(--color-background-xlight);
filter: drop-shadow(0px 6px 16px #441c170f);
border: var(--border-width-base) var(--border-style-base) var(--color-foreground-light);
border-radius: var(--border-radius-base);
// Arrow border is created as the outer triange
&:before {
content: '';
position: relative;
left: -11px;
top: calc(50% - 8px);
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-right: 10px solid var(--color-foreground-light);
position: absolute;
}
// Arrow background is created as the inner triangle
&:after {
content: '';
position: relative;
left: -10px;
top: calc(50% - 8px);
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-right: 10px solid var(--color-background-xlight);
position: absolute;
}
}
.title {
padding: var(--spacing-xs);
color: var(--color-text-base);
font-weight: var(--font-weight-bold);
}
.options {
list-style: none;
display: flex;
flex-direction: column;
padding-bottom: var(--spacing-2xs);
}
.option {
display: flex;
padding: var(--spacing-3xs) var(--spacing-xs);
gap: var(--spacing-xs);
cursor: pointer;
color: var(--color-text-dark);
&:hover {
background: var(--color-background-base);
font-weight: var(--font-weight-bold);
}
&.disabled {
pointer-events: none;
color: var(--color-text-light);
}
}
</style>

View File

@@ -1,66 +0,0 @@
<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<{
replySelected: [value: QuickReply];
}>();
defineProps<{
suggestions: QuickReply[];
}>();
function onButtonClick(action: QuickReply) {
emit('replySelected', action);
}
</script>
<template>
<div :class="$style.container">
<p :class="$style.hint">{{ locale.baseText('aiAssistantChat.quickReply.title') }}</p>
<div :class="$style.suggestions">
<Button
v-for="action in suggestions"
:key="action.key"
:class="$style.replyButton"
outline
type="secondary"
@click="onButtonClick(action)"
>
{{ action.label }}
</Button>
</div>
</div>
</template>
<style module lang="scss">
.container {
display: flex;
flex-direction: column;
gap: var(--spacing-2xs);
width: auto;
justify-content: flex-end;
align-items: flex-end;
}
.suggestions {
display: flex;
flex-direction: column;
width: fit-content;
gap: var(--spacing-4xs);
}
.hint {
color: var(--color-text-base);
font-size: var(--font-size-2xs);
}
.replyButton {
display: flex;
background: var(--chat--color-white);
}
</style>

View File

@@ -1,216 +0,0 @@
<template>
<Modal
width="700px"
:title="i18n.baseText('generateCurlModal.title')"
:event-bus="modalBus"
:name="GENERATE_CURL_MODAL_KEY"
:center="true"
>
<template #content>
<div :class="$style.container">
<N8nFormInputs
:inputs="formInputs"
:event-bus="formBus"
column-view
@update="onUpdate"
@submit="onSubmit"
/>
</div>
</template>
<template #footer>
<div :class="$style.modalFooter">
<N8nNotice
:class="$style.notice"
:content="i18n.baseText('generateCurlModal.notice.content')"
/>
<div>
<N8nButton
float="right"
:loading="loading"
:label="i18n.baseText('generateCurlModal.button.label')"
@click="onGenerate"
/>
</div>
</div>
</template>
</Modal>
</template>
<script lang="ts" setup>
import Modal from '@/components/Modal.vue';
import { GENERATE_CURL_MODAL_KEY } from '@/constants';
import { ref } from 'vue';
import { createEventBus } from 'n8n-design-system/utils';
import { useTelemetry } from '@/composables/useTelemetry';
import { useI18n } from '@/composables/useI18n';
import { useAIStore } from '@/stores/ai.store';
import type { IFormInput } from 'n8n-design-system';
import { useToast } from '@/composables/useToast';
import { useImportCurlCommand } from '@/composables/useImportCurlCommand';
import { useUIStore } from '@/stores/ui.store';
import { useNDVStore } from '@/stores/ndv.store';
const telemetry = useTelemetry();
const i18n = useI18n();
const toast = useToast();
const uiStore = useUIStore();
const aiStore = useAIStore();
const ndvStore = useNDVStore();
const modalBus = createEventBus();
const formBus = createEventBus();
const initialServiceValue = uiStore.modalsById[GENERATE_CURL_MODAL_KEY].data?.service as string;
const initialRequestValue = uiStore.modalsById[GENERATE_CURL_MODAL_KEY].data?.request as string;
const formInputs: IFormInput[] = [
{
name: 'service',
initialValue: initialServiceValue,
properties: {
label: i18n.baseText('generateCurlModal.service.label'),
placeholder: i18n.baseText('generateCurlModal.service.placeholder'),
type: 'text',
required: true,
capitalize: true,
},
},
{
name: 'request',
initialValue: initialRequestValue,
properties: {
label: i18n.baseText('generateCurlModal.request.label'),
placeholder: i18n.baseText('generateCurlModal.request.placeholder'),
type: 'text',
required: true,
capitalize: true,
},
},
];
const formValues = ref<{ service: string; request: string }>({
service: initialServiceValue ?? '',
request: initialRequestValue ?? '',
});
const loading = ref(false);
const { importCurlCommand } = useImportCurlCommand({
onImportSuccess,
onImportFailure,
onAfterImport,
i18n: {
invalidCurCommand: {
title: 'generateCurlModal.invalidCurlCommand.title',
message: 'generateCurlModal.invalidCurlCommand.message',
},
},
});
function closeDialog(): void {
modalBus.emit('close');
}
function onImportSuccess() {
sendImportCurlTelemetry();
toast.showMessage({
title: i18n.baseText('generateCurlModal.success.title'),
message: i18n.baseText('generateCurlModal.success.message'),
type: 'success',
});
closeDialog();
}
function onImportFailure(data: { invalidProtocol: boolean; protocol?: string }) {
sendImportCurlTelemetry({ valid: false, ...data });
}
function onAfterImport() {
uiStore.setModalData({
name: GENERATE_CURL_MODAL_KEY,
data: {
service: formValues.value.service,
request: formValues.value.request,
},
});
}
function sendImportCurlTelemetry(
data: { valid: boolean; invalidProtocol: boolean; protocol?: string } = {
valid: true,
invalidProtocol: false,
protocol: '',
},
): void {
const service = formValues.value.service;
const request = formValues.value.request;
telemetry.track(
'User generated curl command using AI',
{
request,
request_service_name: service,
valid_curl_response: data.valid,
api_docs_returned: false,
invalidProtocol: data.invalidProtocol,
protocol: data.protocol,
node_type: ndvStore.activeNode?.type,
node_name: ndvStore.activeNode?.name,
},
{ withPostHog: true },
);
}
async function onUpdate(field: { name: string; value: string }) {
formValues.value = {
...formValues.value,
[field.name]: field.value,
};
}
async function onGenerate() {
formBus.emit('submit');
}
async function onSubmit() {
const service = formValues.value.service;
const request = formValues.value.request;
try {
loading.value = true;
const data = await aiStore.generateCurl({
service,
request,
});
await importCurlCommand(data.curl);
} catch (error) {
toast.showError(error, i18n.baseText('error'));
} finally {
loading.value = false;
}
}
</script>
<style module lang="scss">
.modalFooter {
justify-content: space-between;
display: flex;
flex-direction: row;
}
.notice {
margin: 0;
}
.container > * {
margin-bottom: var(--spacing-s);
&:last-child {
margin-bottom: 0;
}
}
</style>

View File

@@ -1,22 +1,16 @@
<script lang="ts" setup>
import { GENERATE_CURL_MODAL_KEY, IMPORT_CURL_MODAL_KEY } from '@/constants';
import { IMPORT_CURL_MODAL_KEY } from '@/constants';
import { useUIStore } from '@/stores/ui.store';
import { useAIStore } from '@/stores/ai.store';
defineProps<{
isReadOnly: boolean;
}>();
const uiStore = useUIStore();
const aiStore = useAIStore();
function onImportCurlClicked() {
uiStore.openModal(IMPORT_CURL_MODAL_KEY);
}
function onGenerateCurlClicked() {
uiStore.openModal(GENERATE_CURL_MODAL_KEY);
}
</script>
<template>
@@ -28,16 +22,6 @@ function onGenerateCurlClicked() {
size="mini"
@click="onImportCurlClicked"
/>
<n8n-button
v-if="aiStore.isGenerateCurlEnabled"
class="mr-2xs"
type="secondary"
:label="$locale.baseText('generateCurlParameter.label')"
:disabled="isReadOnly"
size="mini"
@click="onGenerateCurlClicked"
/>
</div>
</template>

View File

@@ -29,7 +29,6 @@ import {
MFA_SETUP_MODAL_KEY,
WORKFLOW_HISTORY_VERSION_RESTORE,
SETUP_CREDENTIALS_MODAL_KEY,
GENERATE_CURL_MODAL_KEY,
PROJECT_MOVE_RESOURCE_MODAL,
PROJECT_MOVE_RESOURCE_CONFIRM_MODAL,
} from '@/constants';
@@ -55,7 +54,6 @@ import WorkflowSettings from '@/components/WorkflowSettings.vue';
import DeleteUserModal from '@/components/DeleteUserModal.vue';
import ActivationModal from '@/components/ActivationModal.vue';
import ImportCurlModal from '@/components/ImportCurlModal.vue';
import GenerateCurlModal from '@/components/GenerateCurlModal.vue';
import MfaSetupModal from '@/components/MfaSetupModal.vue';
import WorkflowShareModal from '@/components/WorkflowShareModal.ee.vue';
import EventDestinationSettingsModal from '@/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue';
@@ -166,10 +164,6 @@ import ProjectMoveResourceConfirmModal from '@/components/Projects/ProjectMoveRe
<ImportCurlModal />
</ModalRoot>
<ModalRoot :name="GENERATE_CURL_MODAL_KEY">
<GenerateCurlModal />
</ModalRoot>
<ModalRoot :name="COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY">
<template #default="{ modalName, activeId, mode }">
<CommunityPackageManageConfirmModal

View File

@@ -10,7 +10,7 @@
<div
v-if="active"
ref="nodeCreator"
:class="{ [$style.nodeCreator]: true, [$style.chatOpened]: chatSidebarOpen }"
:class="{ [$style.nodeCreator]: true }"
:style="nodeCreatorInlineStyle"
data-test-id="node-creator"
@dragover="onDragOver"
@@ -37,7 +37,6 @@ 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 {
@@ -53,7 +52,6 @@ const emit = defineEmits<{
nodeTypeSelected: [value: string[]];
}>();
const uiStore = useUIStore();
const aiStore = useAIStore();
const { setShowScrim, setActions, setMergeNodes } = useNodeCreatorStore();
const { generateMergedNodesAndActions } = useActionsGenerator();
@@ -67,8 +65,6 @@ 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` };
});
@@ -177,10 +173,6 @@ onBeforeUnmount(() => {
z-index: 200;
width: $node-creator-width;
color: $node-creator-text-color;
&.chatOpened {
right: $chat-width;
}
}
.nodeCreatorScrim {

View File

@@ -58,7 +58,6 @@ export const ONBOARDING_CALL_SIGNUP_MODAL_KEY = 'onboardingCallSignup';
export const COMMUNITY_PACKAGE_INSTALL_MODAL_KEY = 'communityPackageInstall';
export const COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY = 'communityPackageManageConfirm';
export const IMPORT_CURL_MODAL_KEY = 'importCurl';
export const GENERATE_CURL_MODAL_KEY = 'generateCurl';
export const LOG_STREAM_MODAL_KEY = 'settingsLogStream';
export const SOURCE_CONTROL_PUSH_MODAL_KEY = 'sourceControlPush';
export const SOURCE_CONTROL_PULL_MODAL_KEY = 'sourceControlPull';
@@ -671,12 +670,6 @@ export const ASK_AI_EXPERIMENT = {
export const TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT = '017_template_credential_setup_v2';
export const AI_ASSISTANT_EXPERIMENT = {
name: '19_ai_assistant_experiment',
control: 'control',
variant: 'variant',
};
export const CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT = {
name: '20_canvas_auto_add_manual_trigger',
control: 'control',
@@ -686,7 +679,6 @@ export const CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT = {
export const EXPERIMENTS_TO_TRACK = [
ASK_AI_EXPERIMENT.name,
TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT,
AI_ASSISTANT_EXPERIMENT.name,
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name,
];
@@ -848,13 +840,6 @@ export const INSECURE_CONNECTION_WARNING = `
</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';
/**
* Injection Keys
*/

View File

@@ -36,7 +36,6 @@ $warning-tooltip-color: var(--color-danger);
// sass variable is used for scss files
$header-height: calc(var(--header-height) * 1px);
$chat-width: calc(var(--chat-width) * 1px);
// sidebar
$sidebar-width: 65px;

View File

@@ -4,34 +4,6 @@
:root {
// Using native css variable enables us to use this value in JS
--header-height: 65;
--chat-width: 350;
// n8n-chat customizations
--chat--spacing: var(--spacing-2xs);
--chat--header-height: calc(var(--header-height) * 1px);
--chat--header--padding: 0 var(--spacing-xs);
--chat--heading--font-size: var(--font-size-m);
--chat--subtitle--font-size: var(--font-size-s);
--chat--subtitle--line-height: var(--font-line-height-base);
--chat--header--background: var(--color-background-xlight);
--chat--header--color: var(--color-text-dark);
--chat--header--border-bottom: var(--border-base);
--chat--close--button--color-hover: var(--color-primary);
// Message styles
--chat--message--padding: var(--spacing-3xs);
--chat--message--font-size: 14px;
--chat--message-line-height: 1.5;
--chat--message--bot--border: 1px solid var(--color-foreground-light);
--chat--message--bot--color: var(--color-text-dark);
--chat--message--user--color: var(--color-text-dark);
--chat--message--user--background: var(--color-success-tint-1);
--chat--message--user--border: 1px solid var(--color-success-light-2);
// Chat input
--chat--input--font-size: var(--font-size-s);
--chat--input--send--button--color: var(--color-success);
}
.clickable {

View File

@@ -96,22 +96,6 @@
"activationModal.yourTriggersWillNowFire": "Your triggers will now fire production executions automatically.",
"activationModal.yourWorkflowWillNowListenForEvents": "Your workflow will now listen for events from {serviceName} and trigger executions.",
"activationModal.yourWorkflowWillNowRegularlyCheck": "Your workflow will now regularly check {serviceName} for events and trigger executions for them.",
"aiAssistantChat.title": "✨ Generate workflow step with AI",
"aiAssistantChat.greeting": "Hi {username}!",
"aiAssistantChat.chatPlaceholder": "Enter your response...",
"aiAssistantChat.getStarted": "Get started",
"aiAssistantChat.initialMessage.firstStep": "What should the first step in your workflow do?",
"aiAssistantChat.initialMessage.nextStep": "Can you describe the next step after the __{currentAction}__ action in your workflow?",
"aiAssistantChat.response.message1": "Thanks for trying our new __Generate Workflow Step__ feature. Currently, the feature is not ready yet. We're gathering real-world prompts like yours to ensure we're creating a high-quality, valuable feature.",
"aiAssistantChat.response.message2": "We understand this may be disappointing, but we believe it's crucial to developing the best possible feature for you and others. Wed love to invite you to be one of the first users to get their hands on the real feature once its ready.",
"aiAssistantChat.response.quickReply.close": "Close chat thread",
"aiAssistantChat.response.quickReply.signUp": "Sign up for early access",
"aiAssistantChat.response.quickReply.giveFeedback": "Give feedback to product team",
"aiAssistantChat.closeButtonTooltip": "Close chat",
"aiAssistantChat.closeChatConfirmation": "Are you sure you want to end this chat session?",
"aiAssistantChat.closeChatConfirmation.confirm": "Yes, close it",
"aiAssistantChat.closeChatConfirmation.cancel": "No, stay",
"aiAssistantChat.quickReply.title": "Quick reply 👇",
"auth.changePassword": "Change password",
"auth.changePassword.currentPassword": "Current password",
"auth.changePassword.error": "Problem changing the password",
@@ -966,10 +950,6 @@
"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",
@@ -2265,18 +2245,6 @@
"importCurlParameter.showError.invalidProtocol1.title": "Use the {node} node",
"importCurlParameter.showError.invalidProtocol2.title": "Invalid Protocol",
"importCurlParameter.showError.invalidProtocol.message": "The HTTP node doesnt support {protocol} requests",
"generateCurlParameter.label": "Ask AI ✨",
"generateCurlModal.title": "Generate HTTP Request",
"generateCurlModal.notice.content": "This will overwrite any changes you have already made to the current node",
"generateCurlModal.button.label": "Generate",
"generateCurlModal.service.label": "Service",
"generateCurlModal.service.placeholder": "Enter the name of the service",
"generateCurlModal.request.label": "Request",
"generateCurlModal.request.placeholder": "Describe the request you want to make",
"generateCurlModal.invalidCurlCommand.title": "Generation failed",
"generateCurlModal.invalidCurlCommand.message": "The AI couldn't process your request",
"generateCurlModal.success.title": "HTTP Request filled out",
"generateCurlModal.success.message": "Please check carefully as AI content can be inaccurate",
"variables.heading": "Variables",
"variables.add": "Add variable",
"variables.add.unavailable": "Upgrade plan to keep using variables",

View File

@@ -1,67 +0,0 @@
import { defineStore } from 'pinia';
import * as aiApi from '@/api/ai';
import type { GenerateCurlPayload } from '@/api/ai';
import { useRootStore } from '@/stores/root.store';
import { useSettingsStore } from '@/stores/settings.store';
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 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 generateCurl(payload: GenerateCurlPayload) {
return await aiApi.generateCurl(rootStore.restApiContext, payload);
}
return {
assistantChatOpen,
nextStepPopupConfig,
openNextStepPopup,
closeNextStepPopup,
latestConnectionInfo,
generateCurl,
isGenerateCurlEnabled,
isAssistantExperimentEnabled,
};
});

View File

@@ -12,7 +12,7 @@ import type {
SimplifiedNodeType,
ActionsRecord,
ToggleNodeCreatorOptions,
AIAssistantConnectionInfo,
NewConnectionInfo,
} from '@/Interface';
import { computed, ref } from 'vue';
@@ -172,7 +172,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
});
}
function openNodeCreatorForConnectingNode(info: AIAssistantConnectionInfo) {
function openNodeCreatorForConnectingNode(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

View File

@@ -34,7 +34,6 @@ import {
N8N_PRICING_PAGE_URL,
WORKFLOW_HISTORY_VERSION_RESTORE,
SETUP_CREDENTIALS_MODAL_KEY,
GENERATE_CURL_MODAL_KEY,
PROJECT_MOVE_RESOURCE_MODAL,
PROJECT_MOVE_RESOURCE_CONFIRM_MODAL,
} from '@/constants';
@@ -141,13 +140,6 @@ export const useUIStore = defineStore(STORES.UI, () => {
curlCommand: '',
},
},
[GENERATE_CURL_MODAL_KEY]: {
open: false,
data: {
service: '',
request: '',
},
},
[LOG_STREAM_MODAL_KEY]: {
open: false,
data: undefined,

View File

@@ -120,9 +120,6 @@
<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,7 +243,6 @@ import {
AI_NODE_CREATOR_VIEW,
DRAG_EVENT_DATA_KEY,
UPDATE_WEBHOOK_ID_NODE_TYPES,
AI_ASSISTANT_LOCAL_STORAGE_KEY,
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT,
} from '@/constants';
@@ -268,7 +264,6 @@ 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,
@@ -314,7 +309,6 @@ import type {
AddedNodesAndConnections,
ToggleNodeCreatorOptions,
IPushDataExecutionFinished,
AIAssistantConnectionInfo,
NodeFilterType,
} from '@/Interface';
@@ -389,8 +383,6 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useRunWorkflow } from '@/composables/useRunWorkflow';
import { useProjectsStore } from '@/stores/projects.store';
import type { ProjectSharingData } from '@/types/projects.types';
import { useAIStore } from '@/stores/ai.store';
import { useStorage } from '@/composables/useStorage';
import { isJSPlumbEndpointElement, isJSPlumbConnection } from '@/utils/typeGuards';
import { usePostHog } from '@/stores/posthog.store';
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
@@ -424,7 +416,6 @@ export default defineComponent({
CanvasControls,
ContextMenu,
SetupWorkflowCredentialsButton,
NextStepPopup,
},
async beforeRouteLeave(to, from, next) {
if (
@@ -587,7 +578,6 @@ export default defineComponent({
useSourceControlStore,
useExecutionsStore,
useProjectsStore,
useAIStore,
useNpsSurveyStore,
),
nativelyNumberSuffixedDefaults(): string[] {
@@ -752,16 +742,6 @@ 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;
},
isProductionExecutionPreview(): boolean {
return this.nodeHelpers.isProductionExecutionPreview.value;
},
@@ -1247,32 +1227,8 @@ 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);
}
}
async onCanvasAddButtonCLick() {
this.showTriggerCreator(NODE_CREATOR_OPEN_SOURCES.TRIGGER_PLACEHOLDER_BUTTON);
},
showTriggerCreator(source: NodeCreatorOpenSource) {
if (this.createNodeActive) return;
@@ -1508,7 +1464,6 @@ 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;
}
@@ -1535,7 +1490,6 @@ 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);
@@ -2904,7 +2858,7 @@ export default defineComponent({
return filter;
},
insertNodeAfterSelected(info: AIAssistantConnectionInfo) {
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
@@ -2982,59 +2936,12 @@ export default defineComponent({
}
return;
}
// 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,
this.insertNodeAfterSelected({
sourceId: connection.parameters.nodeId,
index: connection.parameters.index,
eventSource: NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_DROP,
connection,
outputType: connection.parameters.type,
});
} catch (e) {
console.error(e);
@@ -3562,32 +3469,13 @@ export default defineComponent({
.forEach((endpoint) => setTimeout(() => endpoint.instance.revalidate(endpoint.element), 0));
},
onPlusEndpointClick(endpoint: Endpoint) {
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: getEndpointScope(endpoint.scope),
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: getEndpointScope(endpoint.scope),
endpointUuid: endpoint.uuid,
stepName: endpoint.__meta.nodeName,
});
}
this.insertNodeAfterSelected({
sourceId: endpoint.__meta.nodeId,
index: endpoint.__meta.index,
eventSource: NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT,
outputType: getEndpointScope(endpoint.scope),
endpointUuid: endpoint.uuid,
});
},
onAddInputEndpointClick(endpoint: Endpoint) {
if (endpoint?.__meta) {