mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(editor): Implement Ready to Run Workflows V2 experiment (no-changelog) (#19468)
This commit is contained in:
@@ -169,6 +169,7 @@ import IconLucideSettings from '~icons/lucide/settings';
|
|||||||
import IconLucideShare from '~icons/lucide/share';
|
import IconLucideShare from '~icons/lucide/share';
|
||||||
import IconLucideSlidersHorizontal from '~icons/lucide/sliders-horizontal';
|
import IconLucideSlidersHorizontal from '~icons/lucide/sliders-horizontal';
|
||||||
import IconLucideSmile from '~icons/lucide/smile';
|
import IconLucideSmile from '~icons/lucide/smile';
|
||||||
|
import IconLucideSparkles from '~icons/lucide/sparkles';
|
||||||
import IconLucideSquare from '~icons/lucide/square';
|
import IconLucideSquare from '~icons/lucide/square';
|
||||||
import IconLucideSquareCheck from '~icons/lucide/square-check';
|
import IconLucideSquareCheck from '~icons/lucide/square-check';
|
||||||
import IconLucideSquarePen from '~icons/lucide/square-pen';
|
import IconLucideSquarePen from '~icons/lucide/square-pen';
|
||||||
@@ -583,6 +584,7 @@ export const updatedIconSet = {
|
|||||||
share: IconLucideShare,
|
share: IconLucideShare,
|
||||||
'sliders-horizontal': IconLucideSlidersHorizontal,
|
'sliders-horizontal': IconLucideSlidersHorizontal,
|
||||||
smile: IconLucideSmile,
|
smile: IconLucideSmile,
|
||||||
|
sparkles: IconLucideSparkles,
|
||||||
square: IconLucideSquare,
|
square: IconLucideSquare,
|
||||||
'square-check': IconLucideSquareCheck,
|
'square-check': IconLucideSquareCheck,
|
||||||
'square-pen': IconLucideSquarePen,
|
'square-pen': IconLucideSquarePen,
|
||||||
|
|||||||
@@ -2713,6 +2713,7 @@
|
|||||||
"workflows.empty.preBuiltAgents": "Try a pre-built agent",
|
"workflows.empty.preBuiltAgents": "Try a pre-built agent",
|
||||||
"workflows.empty.shared-with-me": "No {resource} has been shared with you",
|
"workflows.empty.shared-with-me": "No {resource} has been shared with you",
|
||||||
"workflows.empty.shared-with-me.link": "<a href=\"#\">Back to Personal</a>",
|
"workflows.empty.shared-with-me.link": "<a href=\"#\">Back to Personal</a>",
|
||||||
|
"workflows.empty.readyToRunV2": "Try an AI workflow",
|
||||||
"workflows.list.easyAI": "Test the power of AI in n8n with this simple AI Agent Workflow",
|
"workflows.list.easyAI": "Test the power of AI in n8n with this simple AI Agent Workflow",
|
||||||
"workflows.list.error.fetching": "Error fetching workflows",
|
"workflows.list.error.fetching": "Error fetching workflows",
|
||||||
"workflows.shareModal.title": "Share '{name}'",
|
"workflows.shareModal.title": "Share '{name}'",
|
||||||
|
|||||||
@@ -34,5 +34,6 @@ export const STORES = {
|
|||||||
AI_TEMPLATES_STARTER_COLLECTION: 'aiTemplatesStarterCollection',
|
AI_TEMPLATES_STARTER_COLLECTION: 'aiTemplatesStarterCollection',
|
||||||
PERSONALIZED_TEMPLATES: 'personalizedTemplates',
|
PERSONALIZED_TEMPLATES: 'personalizedTemplates',
|
||||||
EXPERIMENT_READY_TO_RUN_WORKFLOWS: 'readyToRunWorkflows',
|
EXPERIMENT_READY_TO_RUN_WORKFLOWS: 'readyToRunWorkflows',
|
||||||
|
EXPERIMENT_READY_TO_RUN_WORKFLOWS_V2: 'readyToRunWorkflowsV2',
|
||||||
EXPERIMENT_TEMPLATE_RECO_V2: 'templateRecoV2',
|
EXPERIMENT_TEMPLATE_RECO_V2: 'templateRecoV2',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type { IUser } from 'n8n-workflow';
|
|||||||
import { type IconOrEmoji, isIconOrEmoji } from '@n8n/design-system/components/N8nIconPicker/types';
|
import { type IconOrEmoji, isIconOrEmoji } from '@n8n/design-system/components/N8nIconPicker/types';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { PROJECT_DATA_STORES } from '@/features/dataStore/constants';
|
import { PROJECT_DATA_STORES } from '@/features/dataStore/constants';
|
||||||
|
import ReadyToRunV2Button from '@/experiments/readyToRunWorkflowsV2/components/ReadyToRunV2Button.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -32,6 +33,10 @@ const uiStore = useUIStore();
|
|||||||
|
|
||||||
const projectPages = useProjectPages();
|
const projectPages = useProjectPages();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
hasActiveCallouts?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
createFolder: [];
|
createFolder: [];
|
||||||
}>();
|
}>();
|
||||||
@@ -341,18 +346,21 @@ const onSelect = (action: string) => {
|
|||||||
:disabled="!sourceControlStore.preferences.branchReadOnly"
|
:disabled="!sourceControlStore.preferences.branchReadOnly"
|
||||||
:content="i18n.baseText('readOnlyEnv.cantAdd.any')"
|
:content="i18n.baseText('readOnlyEnv.cantAdd.any')"
|
||||||
>
|
>
|
||||||
<ProjectCreateResource
|
<div style="display: flex; gap: var(--spacing-xs); align-items: center">
|
||||||
data-test-id="add-resource-buttons"
|
<ReadyToRunV2Button :has-active-callouts="props.hasActiveCallouts" />
|
||||||
:actions="menu"
|
<ProjectCreateResource
|
||||||
:disabled="sourceControlStore.preferences.branchReadOnly"
|
data-test-id="add-resource-buttons"
|
||||||
@action="onSelect"
|
:actions="menu"
|
||||||
>
|
:disabled="sourceControlStore.preferences.branchReadOnly"
|
||||||
<N8nButton
|
@action="onSelect"
|
||||||
data-test-id="add-resource-workflow"
|
>
|
||||||
v-bind="createWorkflowButton"
|
<N8nButton
|
||||||
@click="onSelect(ACTION_TYPES.WORKFLOW)"
|
data-test-id="add-resource-workflow"
|
||||||
/>
|
v-bind="createWorkflowButton"
|
||||||
</ProjectCreateResource>
|
@click="onSelect(ACTION_TYPES.WORKFLOW)"
|
||||||
|
/>
|
||||||
|
</ProjectCreateResource>
|
||||||
|
</div>
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
|
|||||||
import { codeNodeEditorEventBus, globalLinkActionsEventBus } from '@/event-bus';
|
import { codeNodeEditorEventBus, globalLinkActionsEventBus } from '@/event-bus';
|
||||||
import { useAITemplatesStarterCollectionStore } from '@/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store';
|
import { useAITemplatesStarterCollectionStore } from '@/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store';
|
||||||
import { useReadyToRunWorkflowsStore } from '@/experiments/readyToRunWorkflows/stores/readyToRunWorkflows.store';
|
import { useReadyToRunWorkflowsStore } from '@/experiments/readyToRunWorkflows/stores/readyToRunWorkflows.store';
|
||||||
|
import { useReadyToRunWorkflowsV2Store } from '@/experiments/readyToRunWorkflowsV2/stores/readyToRunWorkflowsV2.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
@@ -48,6 +49,7 @@ export async function executionFinished(
|
|||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const aiTemplatesStarterCollectionStore = useAITemplatesStarterCollectionStore();
|
const aiTemplatesStarterCollectionStore = useAITemplatesStarterCollectionStore();
|
||||||
const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore();
|
const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore();
|
||||||
|
const readyToRunWorkflowsV2Store = useReadyToRunWorkflowsV2Store();
|
||||||
|
|
||||||
workflowsStore.lastAddedExecutingNode = null;
|
workflowsStore.lastAddedExecutingNode = null;
|
||||||
|
|
||||||
@@ -76,6 +78,15 @@ export async function executionFinished(
|
|||||||
);
|
);
|
||||||
} else if (templateId.startsWith('37_onboarding_experiments_batch_aug11')) {
|
} else if (templateId.startsWith('37_onboarding_experiments_batch_aug11')) {
|
||||||
readyToRunWorkflowsStore.trackExecuteWorkflow(templateId.split('-').pop() ?? '', data.status);
|
readyToRunWorkflowsStore.trackExecuteWorkflow(templateId.split('-').pop() ?? '', data.status);
|
||||||
|
} else if (
|
||||||
|
templateId === 'ready-to-run-ai-workflow-v1' ||
|
||||||
|
templateId === 'ready-to-run-ai-workflow-v2'
|
||||||
|
) {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
readyToRunWorkflowsV2Store.trackExecuteAiWorkflowSuccess();
|
||||||
|
} else {
|
||||||
|
readyToRunWorkflowsV2Store.trackExecuteAiWorkflow(data.status);
|
||||||
|
}
|
||||||
} else if (isPrebuiltAgentTemplateId(templateId)) {
|
} else if (isPrebuiltAgentTemplateId(templateId)) {
|
||||||
telemetry.track('User executed pre-built Agent', {
|
telemetry.track('User executed pre-built Agent', {
|
||||||
template: templateId,
|
template: templateId,
|
||||||
|
|||||||
@@ -810,6 +810,13 @@ export const TEMPLATE_RECO_V2 = {
|
|||||||
variant: 'variant',
|
variant: 'variant',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const READY_TO_RUN_V2_EXPERIMENT = {
|
||||||
|
name: '042_ready-to-run-worfklow_v2',
|
||||||
|
control: 'control',
|
||||||
|
variant1: 'variant-1-singlebox',
|
||||||
|
variant2: 'variant-2-twoboxes',
|
||||||
|
};
|
||||||
|
|
||||||
export const EXPERIMENTS_TO_TRACK = [
|
export const EXPERIMENTS_TO_TRACK = [
|
||||||
WORKFLOW_BUILDER_EXPERIMENT.name,
|
WORKFLOW_BUILDER_EXPERIMENT.name,
|
||||||
EXTRA_TEMPLATE_LINKS_EXPERIMENT.name,
|
EXTRA_TEMPLATE_LINKS_EXPERIMENT.name,
|
||||||
@@ -818,6 +825,7 @@ export const EXPERIMENTS_TO_TRACK = [
|
|||||||
BATCH_11AUG_EXPERIMENT.name,
|
BATCH_11AUG_EXPERIMENT.name,
|
||||||
PRE_BUILT_AGENTS_EXPERIMENT.name,
|
PRE_BUILT_AGENTS_EXPERIMENT.name,
|
||||||
TEMPLATE_RECO_V2.name,
|
TEMPLATE_RECO_V2.name,
|
||||||
|
READY_TO_RUN_V2_EXPERIMENT.name,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const MFA_FORM = {
|
export const MFA_FORM = {
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { N8nButton } from '@n8n/design-system';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import { getResourcePermissions } from '@n8n/permissions';
|
||||||
|
import { useProjectPages } from '@/composables/useProjectPages';
|
||||||
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
|
import { useFoldersStore } from '@/stores/folders.store';
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import { useReadyToRunWorkflowsV2Store } from '../stores/readyToRunWorkflowsV2.store';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
hasActiveCallouts?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const i18n = useI18n();
|
||||||
|
const toast = useToast();
|
||||||
|
const projectPages = useProjectPages();
|
||||||
|
const projectsStore = useProjectsStore();
|
||||||
|
const sourceControlStore = useSourceControlStore();
|
||||||
|
const foldersStore = useFoldersStore();
|
||||||
|
const readyToRunWorkflowsV2Store = useReadyToRunWorkflowsV2Store();
|
||||||
|
|
||||||
|
const projectPermissions = computed(() => {
|
||||||
|
return getResourcePermissions(
|
||||||
|
projectsStore.currentProject?.scopes ?? projectsStore.personalProject?.scopes,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const showButton = computed(() => {
|
||||||
|
return (
|
||||||
|
readyToRunWorkflowsV2Store.getButtonVisibility(
|
||||||
|
foldersStore.totalWorkflowCount > 0, // Has workflows
|
||||||
|
projectPermissions.value.workflow.create,
|
||||||
|
sourceControlStore.preferences.branchReadOnly,
|
||||||
|
) && !props.hasActiveCallouts // Hide when callouts are shown
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClick = async () => {
|
||||||
|
const projectId = projectPages.isOverviewSubPage
|
||||||
|
? projectsStore.personalProject?.id
|
||||||
|
: (route.params.projectId as string);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await readyToRunWorkflowsV2Store.claimCreditsAndOpenWorkflow(
|
||||||
|
'button',
|
||||||
|
route.params.folderId as string,
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast.showError(error, i18n.baseText('generic.error'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<N8nButton
|
||||||
|
v-if="showButton"
|
||||||
|
data-test-id="ready-to-run-v2-button"
|
||||||
|
type="secondary"
|
||||||
|
icon="sparkles"
|
||||||
|
:loading="readyToRunWorkflowsV2Store.claimingCredits"
|
||||||
|
:disabled="
|
||||||
|
sourceControlStore.preferences.branchReadOnly || readyToRunWorkflowsV2Store.claimingCredits
|
||||||
|
"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
{{ i18n.baseText('workflows.empty.readyToRunV2') }}
|
||||||
|
</N8nButton>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { N8nCard, N8nHeading, N8nText, N8nIcon } from '@n8n/design-system';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
|
import { getResourcePermissions } from '@n8n/permissions';
|
||||||
|
import { useProjectPages } from '@/composables/useProjectPages';
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import { useReadyToRunWorkflowsV2Store } from '../stores/readyToRunWorkflowsV2.store';
|
||||||
|
import type { IUser } from 'n8n-workflow';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const i18n = useI18n();
|
||||||
|
const toast = useToast();
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
const projectsStore = useProjectsStore();
|
||||||
|
const sourceControlStore = useSourceControlStore();
|
||||||
|
const projectPages = useProjectPages();
|
||||||
|
const readyToRunWorkflowsV2Store = useReadyToRunWorkflowsV2Store();
|
||||||
|
|
||||||
|
const currentUser = computed(() => usersStore.currentUser ?? ({} as IUser));
|
||||||
|
const personalProject = computed(() => projectsStore.personalProject);
|
||||||
|
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
|
||||||
|
|
||||||
|
const projectPermissions = computed(() => {
|
||||||
|
return getResourcePermissions(
|
||||||
|
projectsStore.currentProject?.scopes ?? personalProject.value?.scopes,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const emptyListDescription = computed(() => {
|
||||||
|
if (readOnlyEnv.value) {
|
||||||
|
return i18n.baseText('workflows.empty.description.readOnlyEnv');
|
||||||
|
} else if (!projectPermissions.value.workflow.create) {
|
||||||
|
return i18n.baseText('workflows.empty.description.noPermission');
|
||||||
|
} else {
|
||||||
|
return i18n.baseText('workflows.empty.description');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const showReadyToRunV2Card = computed(() => {
|
||||||
|
return readyToRunWorkflowsV2Store.getCardVisibility(
|
||||||
|
projectPermissions.value.workflow.create,
|
||||||
|
readOnlyEnv.value,
|
||||||
|
false, // loading is false in simplified layout
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleReadyToRunV2Click = async () => {
|
||||||
|
const projectId = projectPages.isOverviewSubPage
|
||||||
|
? personalProject.value?.id
|
||||||
|
: (route.params.projectId as string);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await readyToRunWorkflowsV2Store.claimCreditsAndOpenWorkflow(
|
||||||
|
'card',
|
||||||
|
route.params.folderId as string,
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast.showError(error, i18n.baseText('generic.error'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addWorkflow = () => {
|
||||||
|
emit('click:add');
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'click:add': [];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="$style.simplifiedLayout">
|
||||||
|
<div :class="$style.content">
|
||||||
|
<div :class="$style.welcome">
|
||||||
|
<N8nHeading tag="h1" size="2xlarge" :class="$style.welcomeTitle">
|
||||||
|
{{
|
||||||
|
currentUser.firstName
|
||||||
|
? i18n.baseText('workflows.empty.heading', {
|
||||||
|
interpolate: { name: currentUser.firstName },
|
||||||
|
})
|
||||||
|
: i18n.baseText('workflows.empty.heading.userNotSetup')
|
||||||
|
}}
|
||||||
|
</N8nHeading>
|
||||||
|
<N8nText size="large" color="text-base" :class="$style.welcomeDescription">
|
||||||
|
{{ emptyListDescription }}
|
||||||
|
</N8nText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!readOnlyEnv && projectPermissions.workflow.create"
|
||||||
|
:class="$style.actionsContainer"
|
||||||
|
>
|
||||||
|
<N8nCard
|
||||||
|
v-if="showReadyToRunV2Card"
|
||||||
|
:class="$style.actionCard"
|
||||||
|
hoverable
|
||||||
|
data-test-id="ready-to-run-v2-card"
|
||||||
|
@click="handleReadyToRunV2Click"
|
||||||
|
>
|
||||||
|
<div :class="$style.cardContent">
|
||||||
|
<N8nIcon
|
||||||
|
:class="$style.cardIcon"
|
||||||
|
icon="sparkles"
|
||||||
|
color="foreground-dark"
|
||||||
|
:stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
<N8nText size="large" class="mt-xs">
|
||||||
|
{{ i18n.baseText('workflows.empty.readyToRunV2') }}
|
||||||
|
</N8nText>
|
||||||
|
</div>
|
||||||
|
</N8nCard>
|
||||||
|
|
||||||
|
<N8nCard
|
||||||
|
:class="$style.actionCard"
|
||||||
|
hoverable
|
||||||
|
data-test-id="new-workflow-card"
|
||||||
|
@click="addWorkflow"
|
||||||
|
>
|
||||||
|
<div :class="$style.cardContent">
|
||||||
|
<N8nIcon
|
||||||
|
:class="$style.cardIcon"
|
||||||
|
icon="file"
|
||||||
|
color="foreground-dark"
|
||||||
|
:stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
<N8nText size="large" class="mt-xs">
|
||||||
|
{{ i18n.baseText('workflows.empty.startFromScratch') }}
|
||||||
|
</N8nText>
|
||||||
|
</div>
|
||||||
|
</N8nCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.simplifiedLayout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: fixed;
|
||||||
|
top: var(--spacing-l);
|
||||||
|
left: var(--spacing-l);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 600px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome {
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeTitle {
|
||||||
|
margin-bottom: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeDescription {
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionsContainer {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionCard {
|
||||||
|
width: 192px;
|
||||||
|
height: 230px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition:
|
||||||
|
transform 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.cardIcon svg {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardIcon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import type { RouteLocationNormalized } from 'vue-router';
|
||||||
|
import { useFoldersStore } from '@/stores/folders.store';
|
||||||
|
import { useProjectPages } from '@/composables/useProjectPages';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the instance is truly empty and should show the simplified layout
|
||||||
|
*/
|
||||||
|
export function useEmptyStateDetection() {
|
||||||
|
const foldersStore = useFoldersStore();
|
||||||
|
const projectPages = useProjectPages();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current state qualifies as "truly empty"
|
||||||
|
* - No workflows exist in the instance
|
||||||
|
* - User is on the main workflows view (not in a specific folder)
|
||||||
|
* - User is on overview page or personal project workflows
|
||||||
|
* - No search filters are applied
|
||||||
|
* - Not currently refreshing data
|
||||||
|
*/
|
||||||
|
const isTrulyEmpty = (currentRoute: RouteLocationNormalized = route) => {
|
||||||
|
const hasNoWorkflows = foldersStore.totalWorkflowCount === 0;
|
||||||
|
const isNotInSpecificFolder = !currentRoute.params?.folderId;
|
||||||
|
const isMainWorkflowsPage = projectPages.isOverviewSubPage || !projectPages.isSharedSubPage;
|
||||||
|
|
||||||
|
// Check for any search or filter parameters that would indicate filtering is active
|
||||||
|
const hasSearchQuery = !!currentRoute.query?.search;
|
||||||
|
const hasFilters = !!(
|
||||||
|
currentRoute.query?.status ||
|
||||||
|
currentRoute.query?.tags ||
|
||||||
|
currentRoute.query?.showArchived ||
|
||||||
|
currentRoute.query?.homeProject
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
hasNoWorkflows &&
|
||||||
|
isNotInSpecificFolder &&
|
||||||
|
isMainWorkflowsPage &&
|
||||||
|
!hasSearchQuery &&
|
||||||
|
!hasFilters
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if we're in a state where the simplified layout should be shown
|
||||||
|
* This matches the logic from ResourcesListLayout's showEmptyState computed property
|
||||||
|
*/
|
||||||
|
const shouldShowSimplifiedLayout = (
|
||||||
|
currentRoute: RouteLocationNormalized,
|
||||||
|
isFeatureEnabled: boolean,
|
||||||
|
loading: boolean,
|
||||||
|
) => {
|
||||||
|
// Don't show simplified layout if loading or feature is disabled
|
||||||
|
if (loading || !isFeatureEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isTrulyEmpty(currentRoute);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isTrulyEmpty,
|
||||||
|
shouldShowSimplifiedLayout,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import { READY_TO_RUN_V2_EXPERIMENT, VIEWS } from '@/constants';
|
||||||
|
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||||
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import { STORES } from '@n8n/stores';
|
||||||
|
import { useLocalStorage } from '@vueuse/core';
|
||||||
|
import { OPEN_AI_API_CREDENTIAL_TYPE } from 'n8n-workflow';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useRouter, type RouteLocationNormalized } from 'vue-router';
|
||||||
|
import { READY_TO_RUN_WORKFLOW_V1 } from '../workflows/ai-workflow';
|
||||||
|
import { READY_TO_RUN_WORKFLOW_V2 } from '../workflows/ai-workflow-v2';
|
||||||
|
import { useEmptyStateDetection } from '../composables/useEmptyStateDetection';
|
||||||
|
|
||||||
|
const LOCAL_STORAGE_CREDENTIAL_KEY = 'N8N_READY_TO_RUN_V2_OPENAI_CREDENTIAL_ID';
|
||||||
|
|
||||||
|
export const useReadyToRunWorkflowsV2Store = defineStore(
|
||||||
|
STORES.EXPERIMENT_READY_TO_RUN_WORKFLOWS_V2,
|
||||||
|
() => {
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
const i18n = useI18n();
|
||||||
|
const toast = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
const credentialsStore = useCredentialsStore();
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const posthogStore = usePostHog();
|
||||||
|
const cloudPlanStore = useCloudPlanStore();
|
||||||
|
|
||||||
|
const isFeatureEnabled = computed(() => {
|
||||||
|
const variant = posthogStore.getVariant(READY_TO_RUN_V2_EXPERIMENT.name);
|
||||||
|
return (
|
||||||
|
(variant === READY_TO_RUN_V2_EXPERIMENT.variant1 ||
|
||||||
|
variant === READY_TO_RUN_V2_EXPERIMENT.variant2) &&
|
||||||
|
cloudPlanStore.userIsTrialing
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const claimedCredentialIdRef = useLocalStorage(LOCAL_STORAGE_CREDENTIAL_KEY, '');
|
||||||
|
|
||||||
|
const claimingCredits = ref(false);
|
||||||
|
|
||||||
|
const userHasOpenAiCredentialAlready = computed(
|
||||||
|
() =>
|
||||||
|
!!credentialsStore.allCredentials.filter(
|
||||||
|
(credential) => credential.type === OPEN_AI_API_CREDENTIAL_TYPE,
|
||||||
|
).length,
|
||||||
|
);
|
||||||
|
|
||||||
|
const userHasClaimedAiCreditsAlready = computed(
|
||||||
|
() => !!usersStore.currentUser?.settings?.userClaimedAiCredits,
|
||||||
|
);
|
||||||
|
|
||||||
|
const userCanClaimOpenAiCredits = computed(() => {
|
||||||
|
return (
|
||||||
|
settingsStore.isAiCreditsEnabled &&
|
||||||
|
!userHasOpenAiCredentialAlready.value &&
|
||||||
|
!userHasClaimedAiCreditsAlready.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCurrentVariant = () => {
|
||||||
|
return posthogStore.getVariant(READY_TO_RUN_V2_EXPERIMENT.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackExecuteAiWorkflow = (status: string) => {
|
||||||
|
const variant = getCurrentVariant();
|
||||||
|
telemetry.track('User executed ready to run AI workflow', {
|
||||||
|
status,
|
||||||
|
variant,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackExecuteAiWorkflowSuccess = () => {
|
||||||
|
const variant = getCurrentVariant();
|
||||||
|
telemetry.track('User executed ready to run AI workflow successfully', {
|
||||||
|
variant,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const claimFreeAiCredits = async (projectId?: string) => {
|
||||||
|
claimingCredits.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const credential = await credentialsStore.claimFreeAiCredits(projectId);
|
||||||
|
|
||||||
|
if (usersStore?.currentUser?.settings) {
|
||||||
|
usersStore.currentUser.settings.userClaimedAiCredits = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
claimedCredentialIdRef.value = credential.id;
|
||||||
|
|
||||||
|
telemetry.track('User claimed OpenAI credits');
|
||||||
|
return credential;
|
||||||
|
} catch (e) {
|
||||||
|
toast.showError(
|
||||||
|
e,
|
||||||
|
i18n.baseText('freeAi.credits.showError.claim.title'),
|
||||||
|
i18n.baseText('freeAi.credits.showError.claim.message'),
|
||||||
|
);
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
claimingCredits.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAiWorkflow = async (source: 'card' | 'button', parentFolderId?: string) => {
|
||||||
|
const variant = getCurrentVariant();
|
||||||
|
telemetry.track('User opened ready to run AI workflow', {
|
||||||
|
source,
|
||||||
|
variant,
|
||||||
|
});
|
||||||
|
|
||||||
|
const workflow =
|
||||||
|
variant === READY_TO_RUN_V2_EXPERIMENT.variant2
|
||||||
|
? READY_TO_RUN_WORKFLOW_V2
|
||||||
|
: READY_TO_RUN_WORKFLOW_V1;
|
||||||
|
|
||||||
|
await router.push({
|
||||||
|
name: VIEWS.TEMPLATE_IMPORT,
|
||||||
|
params: { id: workflow.meta?.templateId },
|
||||||
|
query: { fromJson: 'true', parentFolderId },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const claimCreditsAndOpenWorkflow = async (
|
||||||
|
source: 'card' | 'button',
|
||||||
|
parentFolderId?: string,
|
||||||
|
projectId?: string,
|
||||||
|
) => {
|
||||||
|
await claimFreeAiCredits(projectId);
|
||||||
|
await openAiWorkflow(source, parentFolderId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCardVisibility = (
|
||||||
|
canCreate: boolean | undefined,
|
||||||
|
readOnlyEnv: boolean,
|
||||||
|
loading: boolean,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
!loading &&
|
||||||
|
isFeatureEnabled.value &&
|
||||||
|
userCanClaimOpenAiCredits.value &&
|
||||||
|
!readOnlyEnv &&
|
||||||
|
canCreate
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getButtonVisibility = (
|
||||||
|
hasWorkflows: boolean,
|
||||||
|
canCreate: boolean | undefined,
|
||||||
|
readOnlyEnv: boolean,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
isFeatureEnabled.value &&
|
||||||
|
userCanClaimOpenAiCredits.value &&
|
||||||
|
!readOnlyEnv &&
|
||||||
|
canCreate &&
|
||||||
|
hasWorkflows
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { shouldShowSimplifiedLayout } = useEmptyStateDetection();
|
||||||
|
|
||||||
|
const getSimplifiedLayoutVisibility = (route: RouteLocationNormalized, loading: boolean) => {
|
||||||
|
return shouldShowSimplifiedLayout(route, isFeatureEnabled.value, loading);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFeatureEnabled,
|
||||||
|
claimingCredits,
|
||||||
|
userCanClaimOpenAiCredits,
|
||||||
|
claimFreeAiCredits,
|
||||||
|
openAiWorkflow,
|
||||||
|
claimCreditsAndOpenWorkflow,
|
||||||
|
getCardVisibility,
|
||||||
|
getButtonVisibility,
|
||||||
|
getSimplifiedLayoutVisibility,
|
||||||
|
trackExecuteAiWorkflow,
|
||||||
|
trackExecuteAiWorkflowSuccess,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { ApplicationError, deepCopy, OPEN_AI_API_CREDENTIAL_TYPE } from 'n8n-workflow';
|
||||||
|
import type { WorkflowDataWithTemplateId } from '@/Interface';
|
||||||
|
import { isWorkflowDataWithTemplateId } from '@/utils/templates/typeGuards';
|
||||||
|
import { READY_TO_RUN_WORKFLOW_V1 } from '../workflows/ai-workflow';
|
||||||
|
import { READY_TO_RUN_WORKFLOW_V2 } from '../workflows/ai-workflow-v2';
|
||||||
|
|
||||||
|
const getWorkflowJson = (json: unknown): WorkflowDataWithTemplateId => {
|
||||||
|
if (!isWorkflowDataWithTemplateId(json)) {
|
||||||
|
throw new ApplicationError('Invalid workflow template JSON structure');
|
||||||
|
}
|
||||||
|
|
||||||
|
return json;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects OpenAI credentials into workflow template if available in localStorage
|
||||||
|
*/
|
||||||
|
const injectOpenAiCredentialIntoWorkflow = (
|
||||||
|
workflow: WorkflowDataWithTemplateId,
|
||||||
|
): WorkflowDataWithTemplateId => {
|
||||||
|
const credentialId = localStorage.getItem('N8N_READY_TO_RUN_V2_OPENAI_CREDENTIAL_ID');
|
||||||
|
|
||||||
|
if (!credentialId) {
|
||||||
|
return workflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clonedWorkflow = deepCopy(workflow);
|
||||||
|
|
||||||
|
if (clonedWorkflow.nodes) {
|
||||||
|
const openAiNode = clonedWorkflow.nodes.find((node) => node.name === 'OpenAI Model');
|
||||||
|
if (openAiNode) {
|
||||||
|
openAiNode.credentials ??= {};
|
||||||
|
openAiNode.credentials[OPEN_AI_API_CREDENTIAL_TYPE] = {
|
||||||
|
id: credentialId,
|
||||||
|
name: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clonedWorkflow;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getReadyToRunAIWorkflows = (): WorkflowDataWithTemplateId[] => {
|
||||||
|
return [
|
||||||
|
injectOpenAiCredentialIntoWorkflow(getWorkflowJson(READY_TO_RUN_WORKFLOW_V1)),
|
||||||
|
injectOpenAiCredentialIntoWorkflow(getWorkflowJson(READY_TO_RUN_WORKFLOW_V2)),
|
||||||
|
];
|
||||||
|
};
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import type { WorkflowDataCreate } from '@n8n/rest-api-client';
|
||||||
|
|
||||||
|
export const READY_TO_RUN_WORKFLOW_V2: WorkflowDataCreate = {
|
||||||
|
name: 'AI Agent workflow',
|
||||||
|
meta: { templateId: 'ready-to-run-ai-workflow-v2' },
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
url: 'https://www.theverge.com/rss/index.xml',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.rssFeedReadTool',
|
||||||
|
typeVersion: 1.2,
|
||||||
|
position: [-16, 768],
|
||||||
|
id: '303e9b4e-cc4e-4d8a-8ede-7550f070d212',
|
||||||
|
name: 'Get Tech News',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
toolDescription: 'Reads the news',
|
||||||
|
url: '=https://feeds.bbci.co.uk/news/world/rss.xml',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.rssFeedReadTool',
|
||||||
|
typeVersion: 1.2,
|
||||||
|
position: [112, 768],
|
||||||
|
id: '4090a753-f131-40b1-87c3-cf74d5a7e325',
|
||||||
|
name: 'Get World News',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
rule: {
|
||||||
|
interval: [
|
||||||
|
{
|
||||||
|
triggerAtHour: 7,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.scheduleTrigger',
|
||||||
|
typeVersion: 1.2,
|
||||||
|
position: [-560, 752],
|
||||||
|
id: '651543b5-0213-433f-8760-57d62b8d6d64',
|
||||||
|
name: 'Run every day at 7AM',
|
||||||
|
notesInFlow: true,
|
||||||
|
notes: 'Double-click to open',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
assignments: {
|
||||||
|
assignments: [
|
||||||
|
{
|
||||||
|
id: '85b5c530-2c13-4424-ab83-05979bc879a5',
|
||||||
|
name: 'output',
|
||||||
|
value: '={{ $json.output }}',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.set',
|
||||||
|
typeVersion: 3.4,
|
||||||
|
position: [160, 544],
|
||||||
|
id: '99f7bb9e-f8c0-43ca-a9a8-a76634ac9611',
|
||||||
|
name: 'Output',
|
||||||
|
notesInFlow: true,
|
||||||
|
notes: 'Double-click to open',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {},
|
||||||
|
type: 'n8n-nodes-base.manualTrigger',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [-560, 544],
|
||||||
|
id: 'a0390291-6794-4673-9a6a-5c3d3a5d9e4b',
|
||||||
|
name: 'Click ‘Execute workflow’ to run',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
content: '## ⚡ Start here:',
|
||||||
|
height: 224,
|
||||||
|
width: 224,
|
||||||
|
color: 7,
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [-624, 480],
|
||||||
|
id: 'fac5929f-e065-4474-96b1-7bcc06834238',
|
||||||
|
name: 'Sticky Note',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
model: {
|
||||||
|
__rl: true,
|
||||||
|
mode: 'list',
|
||||||
|
value: 'gpt-4.1-mini',
|
||||||
|
},
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||||
|
typeVersion: 1.2,
|
||||||
|
position: [-272, 768],
|
||||||
|
id: 'b16482e8-0d48-4426-aa93-c3fee11dd3cd',
|
||||||
|
name: 'OpenAI Model',
|
||||||
|
notesInFlow: true,
|
||||||
|
credentials: {},
|
||||||
|
notes: 'Double-click to open',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
content: '@[youtube](cMyOkQ4N-5M)',
|
||||||
|
height: 512,
|
||||||
|
width: 902,
|
||||||
|
color: 7,
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [-352, -96],
|
||||||
|
id: 'ec65e69e-77fa-4912-a4af-49e0a248e2c8',
|
||||||
|
name: 'Sticky Note3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
promptType: 'define',
|
||||||
|
text: '=Summarize world news and tech news from the last 24 hours. \nSkip your comments. \nThe titles should be "World news:" and "Tech news:" \nToday is {{ $today }}',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: '@n8n/n8n-nodes-langchain.agent',
|
||||||
|
typeVersion: 2.2,
|
||||||
|
position: [-272, 544],
|
||||||
|
id: '084d56aa-d157-4964-9073-b36d9d9589c5',
|
||||||
|
name: 'AI Summary Agent',
|
||||||
|
notesInFlow: true,
|
||||||
|
notes: 'Double-click to open',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
content: '### Double click here to see the results:',
|
||||||
|
height: 240,
|
||||||
|
width: 192,
|
||||||
|
color: 7,
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [112, 464],
|
||||||
|
id: 'a4b7a69a-0db8-4b9b-a81d-fd83378043a3',
|
||||||
|
name: 'Sticky Note1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
content:
|
||||||
|
'### 📰 Daily AI Summary\n\n\nThis workflow gets the latest news and asks AI to summarize it for you.\n\n⭐ Bonus: Send the summary via email by connecting your Gmail account\n\n▶ Watch the video to get started ',
|
||||||
|
height: 272,
|
||||||
|
width: 224,
|
||||||
|
color: 5,
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [-624, 32],
|
||||||
|
id: '74d80857-5e63-47a8-8e86-8ecd10fd5f9e',
|
||||||
|
name: 'Sticky Note2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
subject: 'Your news daily summary',
|
||||||
|
emailType: 'text',
|
||||||
|
message: '={{ $json.output }}',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.gmail',
|
||||||
|
typeVersion: 2.1,
|
||||||
|
position: [432, 544],
|
||||||
|
id: '45625d0d-bf26-4379-9eed-7bbc8e5d87a5',
|
||||||
|
name: 'Send summary by email',
|
||||||
|
webhookId: '093b04f1-5e78-4926-9863-1b100d6f2ead',
|
||||||
|
notesInFlow: true,
|
||||||
|
credentials: {},
|
||||||
|
notes: 'Double-click to open',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
'Get Tech News': {
|
||||||
|
ai_tool: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'AI Summary Agent',
|
||||||
|
type: 'ai_tool',
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Get World News': {
|
||||||
|
ai_tool: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'AI Summary Agent',
|
||||||
|
type: 'ai_tool',
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Run every day at 7AM': {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'AI Summary Agent',
|
||||||
|
type: 'main',
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Click ‘Execute workflow’ to run': {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'AI Summary Agent',
|
||||||
|
type: 'main',
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'OpenAI Model': {
|
||||||
|
ai_languageModel: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'AI Summary Agent',
|
||||||
|
type: 'ai_languageModel',
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'AI Summary Agent': {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'Output',
|
||||||
|
type: 'main',
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Output: {
|
||||||
|
main: [[]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pinData: {},
|
||||||
|
};
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
import type { WorkflowDataCreate } from '@n8n/rest-api-client';
|
||||||
|
|
||||||
|
export const READY_TO_RUN_WORKFLOW_V1: WorkflowDataCreate = {
|
||||||
|
name: 'AI Agent workflow',
|
||||||
|
meta: { templateId: 'ready-to-run-ai-workflow-v1' },
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
url: 'https://www.theverge.com/rss/index.xml',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.rssFeedReadTool',
|
||||||
|
typeVersion: 1.2,
|
||||||
|
position: [-16, 768],
|
||||||
|
id: '303e9b4e-cc4e-4d8a-8ede-7550f070d212',
|
||||||
|
name: 'Get Tech News',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
toolDescription: 'Reads the news',
|
||||||
|
url: '=https://feeds.bbci.co.uk/news/world/rss.xml',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.rssFeedReadTool',
|
||||||
|
typeVersion: 1.2,
|
||||||
|
position: [112, 768],
|
||||||
|
id: '4090a753-f131-40b1-87c3-cf74d5a7e325',
|
||||||
|
name: 'Get World News',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
rule: {
|
||||||
|
interval: [
|
||||||
|
{
|
||||||
|
triggerAtHour: 7,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.scheduleTrigger',
|
||||||
|
typeVersion: 1.2,
|
||||||
|
position: [-560, 752],
|
||||||
|
id: '651543b5-0213-433f-8760-57d62b8d6d64',
|
||||||
|
name: 'Run every day at 7AM',
|
||||||
|
notesInFlow: true,
|
||||||
|
notes: 'Double-click to open',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
assignments: {
|
||||||
|
assignments: [
|
||||||
|
{
|
||||||
|
id: '85b5c530-2c13-4424-ab83-05979bc879a5',
|
||||||
|
name: 'output',
|
||||||
|
value: '={{ $json.output }}',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.set',
|
||||||
|
typeVersion: 3.4,
|
||||||
|
position: [160, 544],
|
||||||
|
id: '99f7bb9e-f8c0-43ca-a9a8-a76634ac9611',
|
||||||
|
name: 'Output',
|
||||||
|
notesInFlow: true,
|
||||||
|
notes: 'Double-click to open',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {},
|
||||||
|
type: 'n8n-nodes-base.manualTrigger',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [-560, 544],
|
||||||
|
id: 'a0390291-6794-4673-9a6a-5c3d3a5d9e4b',
|
||||||
|
name: 'Click ‘Execute workflow’ to run',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
content: '## ⚡ Start here:',
|
||||||
|
height: 240,
|
||||||
|
width: 224,
|
||||||
|
color: 7,
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [-624, 464],
|
||||||
|
id: 'fac5929f-e065-4474-96b1-7bcc06834238',
|
||||||
|
name: 'Sticky Note',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
model: {
|
||||||
|
__rl: true,
|
||||||
|
mode: 'list',
|
||||||
|
value: 'gpt-4.1-mini',
|
||||||
|
},
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||||
|
typeVersion: 1.2,
|
||||||
|
position: [-272, 768],
|
||||||
|
id: 'b16482e8-0d48-4426-aa93-c3fee11dd3cd',
|
||||||
|
name: 'OpenAI Model',
|
||||||
|
notesInFlow: true,
|
||||||
|
credentials: {},
|
||||||
|
notes: 'Double-click to open',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
promptType: 'define',
|
||||||
|
text: '=Summarize world news and tech news from the last 24 hours. \nSkip your comments. \nThe titles should be "World news:" and "Tech news:" \nToday is {{ $today }}',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: '@n8n/n8n-nodes-langchain.agent',
|
||||||
|
typeVersion: 2.2,
|
||||||
|
position: [-272, 544],
|
||||||
|
id: '084d56aa-d157-4964-9073-b36d9d9589c5',
|
||||||
|
name: 'AI Summary Agent',
|
||||||
|
notesInFlow: true,
|
||||||
|
notes: 'Double-click to open',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
content: '### Double click here to see the results:',
|
||||||
|
height: 240,
|
||||||
|
width: 192,
|
||||||
|
color: 7,
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [112, 464],
|
||||||
|
id: 'a4b7a69a-0db8-4b9b-a81d-fd83378043a3',
|
||||||
|
name: 'Sticky Note1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
content:
|
||||||
|
'### 📰 Daily AI Summary\n\n\nThis workflow gets the latest news and asks AI to summarize it for you.\n\n⭐ Bonus: Send the summary via email by connecting your Gmail account\n\n\n\n@[youtube](cMyOkQ4N-5M)',
|
||||||
|
height: 432,
|
||||||
|
width: 384,
|
||||||
|
color: 5,
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [-1152, 464],
|
||||||
|
id: '74d80857-5e63-47a8-8e86-8ecd10fd5f9e',
|
||||||
|
name: 'Sticky Note2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
subject: 'Your news daily summary',
|
||||||
|
emailType: 'text',
|
||||||
|
message: '={{ $json.output }}',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.gmail',
|
||||||
|
typeVersion: 2.1,
|
||||||
|
position: [432, 544],
|
||||||
|
id: '45625d0d-bf26-4379-9eed-7bbc8e5d87a5',
|
||||||
|
name: 'Send summary by email',
|
||||||
|
webhookId: '093b04f1-5e78-4926-9863-1b100d6f2ead',
|
||||||
|
notesInFlow: true,
|
||||||
|
credentials: {},
|
||||||
|
notes: 'Double-click to open',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
'Get Tech News': {
|
||||||
|
ai_tool: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'AI Summary Agent',
|
||||||
|
type: 'ai_tool',
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Get World News': {
|
||||||
|
ai_tool: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'AI Summary Agent',
|
||||||
|
type: 'ai_tool',
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Run every day at 7AM': {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'AI Summary Agent',
|
||||||
|
type: 'main',
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Click ‘Execute workflow’ to run': {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'AI Summary Agent',
|
||||||
|
type: 'main',
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'OpenAI Model': {
|
||||||
|
ai_languageModel: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'AI Summary Agent',
|
||||||
|
type: 'ai_languageModel',
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'AI Summary Agent': {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'Output',
|
||||||
|
type: 'main',
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Output: {
|
||||||
|
main: [[]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pinData: {},
|
||||||
|
};
|
||||||
@@ -62,6 +62,7 @@ vi.mock('@/composables/useDocumentTitle', () => ({
|
|||||||
|
|
||||||
const mockDebounce = {
|
const mockDebounce = {
|
||||||
callDebounced: vi.fn((fn) => fn()),
|
callDebounced: vi.fn((fn) => fn()),
|
||||||
|
debounce: vi.fn(),
|
||||||
};
|
};
|
||||||
vi.mock('@/composables/useDebounce', () => ({
|
vi.mock('@/composables/useDebounce', () => ({
|
||||||
useDebounce: vi.fn(() => mockDebounce),
|
useDebounce: vi.fn(() => mockDebounce),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ApplicationError, type INodeTypeNameVersion } from 'n8n-workflow';
|
import { ApplicationError, type INodeTypeNameVersion } from 'n8n-workflow';
|
||||||
import type { WorkflowDataWithTemplateId } from '@/Interface';
|
import type { WorkflowDataWithTemplateId } from '@/Interface';
|
||||||
import { isWorkflowDataWithTemplateId } from '@/utils/templates/typeGuards';
|
import { isWorkflowDataWithTemplateId } from '@/utils/templates/typeGuards';
|
||||||
|
import { getReadyToRunAIWorkflows } from '@/experiments/readyToRunWorkflowsV2/utils/workflowSamples';
|
||||||
|
|
||||||
/* eslint-disable import-x/extensions */
|
/* eslint-disable import-x/extensions */
|
||||||
import easyAiStarterJson from '@/utils/templates/samples/easy_ai_starter.json';
|
import easyAiStarterJson from '@/utils/templates/samples/easy_ai_starter.json';
|
||||||
@@ -192,6 +193,7 @@ export const getSampleWorkflowByTemplateId = (
|
|||||||
const workflows = [
|
const workflows = [
|
||||||
getEasyAiWorkflowJson(),
|
getEasyAiWorkflowJson(),
|
||||||
getRagStarterWorkflowJson(),
|
getRagStarterWorkflowJson(),
|
||||||
|
...getReadyToRunAIWorkflows(),
|
||||||
...getPrebuiltAgents().map((agent) => agent.template),
|
...getPrebuiltAgents().map((agent) => agent.template),
|
||||||
...getTutorialTemplates().map((tutorial) => tutorial.template),
|
...getTutorialTemplates().map((tutorial) => tutorial.template),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ import SuggestedWorkflowCard from '@/experiments/personalizedTemplates/component
|
|||||||
import SuggestedWorkflows from '@/experiments/personalizedTemplates/components/SuggestedWorkflows.vue';
|
import SuggestedWorkflows from '@/experiments/personalizedTemplates/components/SuggestedWorkflows.vue';
|
||||||
import { usePersonalizedTemplatesStore } from '@/experiments/personalizedTemplates/stores/personalizedTemplates.store';
|
import { usePersonalizedTemplatesStore } from '@/experiments/personalizedTemplates/stores/personalizedTemplates.store';
|
||||||
import { useReadyToRunWorkflowsStore } from '@/experiments/readyToRunWorkflows/stores/readyToRunWorkflows.store';
|
import { useReadyToRunWorkflowsStore } from '@/experiments/readyToRunWorkflows/stores/readyToRunWorkflows.store';
|
||||||
|
import { useReadyToRunWorkflowsV2Store } from '@/experiments/readyToRunWorkflowsV2/stores/readyToRunWorkflowsV2.store';
|
||||||
import TemplateRecommendationV2 from '@/experiments/templateRecoV2/components/TemplateRecommendationV2.vue';
|
import TemplateRecommendationV2 from '@/experiments/templateRecoV2/components/TemplateRecommendationV2.vue';
|
||||||
import { usePersonalizedTemplatesV2Store } from '@/experiments/templateRecoV2/stores/templateRecoV2.store';
|
import { usePersonalizedTemplatesV2Store } from '@/experiments/templateRecoV2/stores/templateRecoV2.store';
|
||||||
|
import SimplifiedEmptyLayout from '@/experiments/readyToRunWorkflowsV2/components/SimplifiedEmptyLayout.vue';
|
||||||
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
import InsightsSummary from '@/features/insights/components/InsightsSummary.vue';
|
||||||
import { useInsightsStore } from '@/features/insights/insights.store';
|
import { useInsightsStore } from '@/features/insights/insights.store';
|
||||||
import type {
|
import type {
|
||||||
@@ -124,6 +126,7 @@ const templatesStore = useTemplatesStore();
|
|||||||
const aiStarterTemplatesStore = useAITemplatesStarterCollectionStore();
|
const aiStarterTemplatesStore = useAITemplatesStarterCollectionStore();
|
||||||
const personalizedTemplatesStore = usePersonalizedTemplatesStore();
|
const personalizedTemplatesStore = usePersonalizedTemplatesStore();
|
||||||
const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore();
|
const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore();
|
||||||
|
const readyToRunWorkflowsV2Store = useReadyToRunWorkflowsV2Store();
|
||||||
const personalizedTemplatesV2Store = usePersonalizedTemplatesV2Store();
|
const personalizedTemplatesV2Store = usePersonalizedTemplatesV2Store();
|
||||||
|
|
||||||
const documentTitle = useDocumentTitle();
|
const documentTitle = useDocumentTitle();
|
||||||
@@ -437,6 +440,19 @@ const showPersonalizedTemplates = computed(
|
|||||||
() => !loading.value && personalizedTemplatesStore.isFeatureEnabled(),
|
() => !loading.value && personalizedTemplatesStore.isFeatureEnabled(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const shouldUseSimplifiedLayout = computed(() => {
|
||||||
|
return readyToRunWorkflowsV2Store.getSimplifiedLayoutVisibility(route, loading.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasActiveCallouts = computed(() => {
|
||||||
|
return (
|
||||||
|
showPrebuiltAgentsCallout.value ||
|
||||||
|
showAIStarterCollectionCallout.value ||
|
||||||
|
showPersonalizedTemplates.value ||
|
||||||
|
showReadyToRunWorkflowsCallout.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WATCHERS, STORE SUBSCRIPTIONS AND EVENT BUS HANDLERS
|
* WATCHERS, STORE SUBSCRIPTIONS AND EVENT BUS HANDLERS
|
||||||
*/
|
*/
|
||||||
@@ -1713,7 +1729,10 @@ const onNameSubmit = async (name: string) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<SimplifiedEmptyLayout v-if="shouldUseSimplifiedLayout" @click:add="addWorkflow" />
|
||||||
|
|
||||||
<ResourcesListLayout
|
<ResourcesListLayout
|
||||||
|
v-else
|
||||||
v-model:filters="filters"
|
v-model:filters="filters"
|
||||||
resource-key="workflows"
|
resource-key="workflows"
|
||||||
type="list-paginated"
|
type="list-paginated"
|
||||||
@@ -1735,7 +1754,10 @@ const onNameSubmit = async (name: string) => {
|
|||||||
@mouseleave="folderHelpers.resetDropTarget"
|
@mouseleave="folderHelpers.resetDropTarget"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<ProjectHeader @create-folder="createFolderInCurrent">
|
<ProjectHeader
|
||||||
|
:has-active-callouts="hasActiveCallouts"
|
||||||
|
@create-folder="createFolderInCurrent"
|
||||||
|
>
|
||||||
<InsightsSummary
|
<InsightsSummary
|
||||||
v-if="showInsights"
|
v-if="showInsights"
|
||||||
:loading="insightsStore.weeklySummary.isLoading"
|
:loading="insightsStore.weeklySummary.isLoading"
|
||||||
|
|||||||
Reference in New Issue
Block a user