From 6429c2644de8352cf0214309c04fcd7d3650ab21 Mon Sep 17 00:00:00 2001 From: Romeo Balta <7095569+romeobalta@users.noreply.github.com> Date: Mon, 11 Aug 2025 11:47:14 +0100 Subject: [PATCH] feat(editor): Implement template recommendation v2 experiment (no-changelog) (#18196) --- .../frontend/@n8n/i18n/src/locales/en.json | 8 + .../frontend/@n8n/stores/src/constants.ts | 1 + .../editor-ui/src/components/Modals.vue | 8 + packages/frontend/editor-ui/src/constants.ts | 7 + .../components/NodeRecommendationCard.vue | 66 +++++ .../components/NodeRecommendationModal.vue | 251 ++++++++++++++++++ .../components/TemplateCard.vue | 118 ++++++++ .../components/TemplateRecommendationV2.vue | 36 +++ .../templateRecoV2/components/YoutubeCard.vue | 44 +++ .../templateRecoV2/nodes/predefinedData.ts | 156 +++++++++++ .../stores/templateRecoV2.store.ts | 101 +++++++ .../frontend/editor-ui/src/stores/ui.store.ts | 7 + .../editor-ui/src/views/WorkflowsView.vue | 18 +- 13 files changed, 820 insertions(+), 1 deletion(-) create mode 100644 packages/frontend/editor-ui/src/experiments/templateRecoV2/components/NodeRecommendationCard.vue create mode 100644 packages/frontend/editor-ui/src/experiments/templateRecoV2/components/NodeRecommendationModal.vue create mode 100644 packages/frontend/editor-ui/src/experiments/templateRecoV2/components/TemplateCard.vue create mode 100644 packages/frontend/editor-ui/src/experiments/templateRecoV2/components/TemplateRecommendationV2.vue create mode 100644 packages/frontend/editor-ui/src/experiments/templateRecoV2/components/YoutubeCard.vue create mode 100644 packages/frontend/editor-ui/src/experiments/templateRecoV2/nodes/predefinedData.ts create mode 100644 packages/frontend/editor-ui/src/experiments/templateRecoV2/stores/templateRecoV2.store.ts diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index bdd80def48..6721e95471 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -2662,6 +2662,14 @@ "workflows.item.readonly": "Read only", "workflows.item.archived": "Archived", "workflows.itemSuggestion.try": "Try template", + "workflows.templateRecoV2.starterTemplates": "Starter templates", + "workflows.templateRecoV2.seeMoreStarterTemplates": "See more starter templates", + "workflows.templateRecoV2.popularTemplates": "Popular templates", + "workflows.templateRecoV2.seeMorePopularTemplates": "See more popular templates", + "workflows.templateRecoV2.tutorials": "Tutorials", + "workflows.templateRecoV2.loadingTemplates": "Loading templates...", + "workflows.templateRecoV2.useTemplate": "Use template", + "workflows.templateRecoV2.exploreTemplates": "Or explore templates to get inspired and learn fast:", "workflows.search.placeholder": "Search", "workflows.filters": "Filters", "workflows.filters.tags": "Tags", diff --git a/packages/frontend/@n8n/stores/src/constants.ts b/packages/frontend/@n8n/stores/src/constants.ts index da27d9360e..3a7f5795dd 100644 --- a/packages/frontend/@n8n/stores/src/constants.ts +++ b/packages/frontend/@n8n/stores/src/constants.ts @@ -34,4 +34,5 @@ export const STORES = { AI_TEMPLATES_STARTER_COLLECTION: 'aiTemplatesStarterCollection', PERSONALIZED_TEMPLATES: 'personalizedTemplates', EXPERIMENT_READY_TO_RUN_WORKFLOWS: 'readyToRunWorkflows', + EXPERIMENT_TEMPLATE_RECO_V2: 'templateRecoV2', } as const; diff --git a/packages/frontend/editor-ui/src/components/Modals.vue b/packages/frontend/editor-ui/src/components/Modals.vue index cd2d0f17c2..625bd73c1b 100644 --- a/packages/frontend/editor-ui/src/components/Modals.vue +++ b/packages/frontend/editor-ui/src/components/Modals.vue @@ -41,6 +41,7 @@ import { WORKFLOW_HISTORY_VERSION_RESTORE, WORKFLOW_SETTINGS_MODAL_KEY, WORKFLOW_SHARE_MODAL_KEY, + EXPERIMENT_TEMPLATE_RECO_V2_KEY, } from '@/constants'; import AboutModal from '@/components/AboutModal.vue'; @@ -83,6 +84,7 @@ import WorkflowDiffModal from '@/features/workflow-diff/WorkflowDiffModal.vue'; import type { EventBus } from '@n8n/utils/event-bus'; import PromptMfaCodeModal from './PromptMfaCodeModal/PromptMfaCodeModal.vue'; import DynamicModalLoader from './DynamicModalLoader.vue'; +import NodeRecommendationModal from '@/experiments/templateRecoV2/components/NodeRecommendationModal.vue'; + + + + diff --git a/packages/frontend/editor-ui/src/constants.ts b/packages/frontend/editor-ui/src/constants.ts index 4351d2472e..c78d947837 100644 --- a/packages/frontend/editor-ui/src/constants.ts +++ b/packages/frontend/editor-ui/src/constants.ts @@ -86,6 +86,7 @@ export const FROM_AI_PARAMETERS_MODAL_KEY = 'fromAiParameters'; export const WORKFLOW_EXTRACTION_NAME_MODAL_KEY = 'workflowExtractionName'; export const WHATS_NEW_MODAL_KEY = 'whatsNew'; export const WORKFLOW_DIFF_MODAL_KEY = 'workflowDiff'; +export const EXPERIMENT_TEMPLATE_RECO_V2_KEY = 'templateRecoV2'; export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = { UNINSTALL: 'uninstall', @@ -787,6 +788,12 @@ export const PRE_BUILT_AGENTS_EXPERIMENT = { variant: 'variant', }; +export const TEMPLATE_RECO_V2 = { + name: '039_template_onboarding_v2', + control: 'control', + variant: 'variant', +}; + export const EXPERIMENTS_TO_TRACK = [ WORKFLOW_BUILDER_EXPERIMENT.name, EXTRA_TEMPLATE_LINKS_EXPERIMENT.name, diff --git a/packages/frontend/editor-ui/src/experiments/templateRecoV2/components/NodeRecommendationCard.vue b/packages/frontend/editor-ui/src/experiments/templateRecoV2/components/NodeRecommendationCard.vue new file mode 100644 index 0000000000..5c565ef9fb --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/templateRecoV2/components/NodeRecommendationCard.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/packages/frontend/editor-ui/src/experiments/templateRecoV2/components/NodeRecommendationModal.vue b/packages/frontend/editor-ui/src/experiments/templateRecoV2/components/NodeRecommendationModal.vue new file mode 100644 index 0000000000..d975c2cb41 --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/templateRecoV2/components/NodeRecommendationModal.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/packages/frontend/editor-ui/src/experiments/templateRecoV2/components/TemplateCard.vue b/packages/frontend/editor-ui/src/experiments/templateRecoV2/components/TemplateCard.vue new file mode 100644 index 0000000000..31f582249b --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/templateRecoV2/components/TemplateCard.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/packages/frontend/editor-ui/src/experiments/templateRecoV2/components/TemplateRecommendationV2.vue b/packages/frontend/editor-ui/src/experiments/templateRecoV2/components/TemplateRecommendationV2.vue new file mode 100644 index 0000000000..f87fcb954c --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/templateRecoV2/components/TemplateRecommendationV2.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/packages/frontend/editor-ui/src/experiments/templateRecoV2/components/YoutubeCard.vue b/packages/frontend/editor-ui/src/experiments/templateRecoV2/components/YoutubeCard.vue new file mode 100644 index 0000000000..5fcab9af13 --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/templateRecoV2/components/YoutubeCard.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/packages/frontend/editor-ui/src/experiments/templateRecoV2/nodes/predefinedData.ts b/packages/frontend/editor-ui/src/experiments/templateRecoV2/nodes/predefinedData.ts new file mode 100644 index 0000000000..ec0bdb60e9 --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/templateRecoV2/nodes/predefinedData.ts @@ -0,0 +1,156 @@ +export interface PredefinedNodeData { + starter: number[]; + popular: number[]; + youtube: Array<{ + id: string; + title: string; + description: string; + }>; +} + +export const NODE_DATA: Record = { + '@n8n/n8n-nodes-langchain.agent': { + starter: [6270, 5462, 3100], + popular: [2465, 2326, 2006], + youtube: [ + { + id: '4cQWJViybAQ', + title: 'n8n Quick Start Tutorial: Build Your First Workflow [2025]', + description: + 'In this tutorial, @theflowgrammer walks you through the conceptual foundations you need to know to build powerful n8n workflows from scratch.', + }, + { + id: '77Z07QnLlB8', + title: 'Building AI Agents: Prompt Engineering for Beginners [Part 3]', + description: + 'In Part 3 of our Building AI Agents series, we focus on the essentials of prompt engineering—specifically for single-task agents in n8n.', + }, + ], + }, + '@n8n/n8n-nodes-langchain.openAi': { + starter: [3100, 2722, 5462], + popular: [2462, 2783, 2187], + youtube: [ + { + id: '4cQWJViybAQ', + title: 'n8n Quick Start Tutorial: Build Your First Workflow [2025]', + description: + 'In this tutorial, @theflowgrammer walks you through the conceptual foundations you need to know to build powerful n8n workflows from scratch.', + }, + { + id: '77Z07QnLlB8', + title: 'Building AI Agents: Prompt Engineering for Beginners [Part 3]', + description: + 'In Part 3 of our Building AI Agents series, we focus on the essentials of prompt engineering—specifically for single-task agents in n8n.', + }, + ], + }, + 'n8n-nodes-base.googleSheets': { + starter: [2581, 5462, 1751], + popular: [5690, 2819, 6468], + youtube: [ + { + id: '4cQWJViybAQ', + title: 'n8n Quick Start Tutorial: Build Your First Workflow [2025]', + description: + 'In this tutorial, @theflowgrammer walks you through the conceptual foundations you need to know to build powerful n8n workflows from scratch.', + }, + { + id: 'IJdt_Ds-gmc', + title: 'OpenAI and Google Sheets integration: Automated workflows (+ 2 Free Templates)', + description: + 'In this video, we connect OpenAI and Google Sheets into two powerful workflows.', + }, + ], + }, + 'n8n-nodes-base.gmail': { + starter: [1953, 6277, 2722], + popular: [3686, 3123, 3905], + youtube: [ + { + id: '4cQWJViybAQ', + title: 'n8n Quick Start Tutorial: Build Your First Workflow [2025]', + description: + 'In this tutorial, @theflowgrammer walks you through the conceptual foundations you need to know to build powerful n8n workflows from scratch.', + }, + { + id: 'UnSKuFJPtyk', + title: 'Build Your First AI Agent for Free with No Code (n8n + Google Gemini 2.5 Pro)', + description: + 'Learn how to build your own AI-powered email assistant with zero coding using n8n and Google Gemini. This step-by-step tutorial shows how to create an agent that can read, draft, and send emails on your behalf — all automatically.', + }, + ], + }, + 'n8n-nodes-base.httpRequest': { + starter: [1748, 3858, 5171], + popular: [2035, 4110, 3100], + youtube: [ + { + id: '4cQWJViybAQ', + title: 'n8n Quick Start Tutorial: Build Your First Workflow [2025]', + description: + 'In this tutorial, @theflowgrammer walks you through the conceptual foundations you need to know to build powerful n8n workflows from scratch.', + }, + { + id: 'tKwvqgVEBOU', + title: 'n8n HTTP Request Node Made Simple: 10x Your Automations in 10 Minutes', + description: + "The n8n HTTP Request node is the most powerful tool you're probably not using. Most n8n users stick to pre-built integrations because the HTTP Request node looks intimidating, but mastering n8n HTTP requests will literally 10x what you can automate.", + }, + ], + }, + '@n8n/n8n-nodes-langchain.googleGemini': { + starter: [6270, 4365, 3905], + popular: [5993, 2753, 2466], + youtube: [ + { + id: '4cQWJViybAQ', + title: 'n8n Quick Start Tutorial: Build Your First Workflow [2025]', + description: + 'In this tutorial, @theflowgrammer walks you through the conceptual foundations you need to know to build powerful n8n workflows from scratch.', + }, + { + id: 'UnSKuFJPtyk', + title: 'Build Your First AI Agent for Free with No Code (n8n + Google Gemini 2.5 Pro)', + description: + 'Learn how to build your own AI-powered email assistant with zero coding using n8n and Google Gemini. This step-by-step tutorial shows how to create an agent that can read, draft, and send emails on your behalf — all automatically.', + }, + ], + }, + 'n8n-nodes-base.googleDrive': { + starter: [6611, 1960, 2782], + popular: [2753, 4767, 3135], + youtube: [ + { + id: '4cQWJViybAQ', + title: 'n8n Quick Start Tutorial: Build Your First Workflow [2025]', + description: + 'In this tutorial, @theflowgrammer walks you through the conceptual foundations you need to know to build powerful n8n workflows from scratch.', + }, + { + id: 'vqZTpKGh_jU', + title: 'I Automated My Entire Google Drive With n8n – It Organizes Itself', + description: + 'In this video, learn how to use n8n to automatically organize your Google Drive files From organizing files to streamlining tasks, discover smart ways to boost productivity in just minutes!', + }, + ], + }, + 'n8n-nodes-base.telegram': { + starter: [2462, 2114, 4365], + popular: [3654, 2783, 3686], + youtube: [ + { + id: '4cQWJViybAQ', + title: 'n8n Quick Start Tutorial: Build Your First Workflow [2025]', + description: + 'In this tutorial, @theflowgrammer walks you through the conceptual foundations you need to know to build powerful n8n workflows from scratch.', + }, + { + id: 'ODdRXozldPw', + title: 'How to build a Telegram AI bot with n8n – Step-by-step tutorial', + description: + "In this video, we’ll guide you through the workflow that integrates with Telegram to create an AI-powered chatbot. It uses OpenAI's Chat Model and Dall-E 3 to understand and respond to user messages, correct errors, and generate and send images based on user queries.", + }, + ], + }, +}; diff --git a/packages/frontend/editor-ui/src/experiments/templateRecoV2/stores/templateRecoV2.store.ts b/packages/frontend/editor-ui/src/experiments/templateRecoV2/stores/templateRecoV2.store.ts new file mode 100644 index 0000000000..18559a2f3e --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/templateRecoV2/stores/templateRecoV2.store.ts @@ -0,0 +1,101 @@ +import { useTelemetry } from '@/composables/useTelemetry'; +import { TEMPLATE_RECO_V2, VIEWS } from '@/constants'; +import { useCloudPlanStore } from '@/stores/cloudPlan.store'; +import { usePostHog } from '@/stores/posthog.store'; +import { useTemplatesStore } from '@/stores/templates.store'; +import { STORES } from '@n8n/stores'; +import { defineStore } from 'pinia'; +import { computed } from 'vue'; +import { NODE_DATA, type PredefinedNodeData } from '../nodes/predefinedData'; + +const PREDEFINED_NODES = Object.keys(NODE_DATA); + +export const usePersonalizedTemplatesV2Store = defineStore( + STORES.EXPERIMENT_TEMPLATE_RECO_V2, + () => { + const telemetry = useTelemetry(); + const posthogStore = usePostHog(); + const cloudPlanStore = useCloudPlanStore(); + const templatesStore = useTemplatesStore(); + + const isFeatureEnabled = () => { + return ( + posthogStore.getVariant(TEMPLATE_RECO_V2.name) === TEMPLATE_RECO_V2.variant && + cloudPlanStore.userIsTrialing + ); + }; + + function getNodeData(nodeId: string): PredefinedNodeData { + if (nodeId in NODE_DATA) { + return NODE_DATA[nodeId]; + } + + return { + starter: [], + popular: [], + youtube: [], + }; + } + + async function getTemplateData(templateId: number) { + return await templatesStore.fetchTemplateById(templateId.toString()); + } + + function getTemplateRoute(id: number) { + return { name: VIEWS.TEMPLATE, params: { id } }; + } + + const nodes = computed(() => { + const selectedApps = cloudPlanStore.selectedApps; + + if (!selectedApps?.length) { + return []; + } + + return PREDEFINED_NODES.filter((nodeName) => selectedApps.includes(nodeName)).slice(0, 3); + }); + + function trackMinicardClick(tool: string) { + telemetry.track('User clicked on node minicard', { + tool, + }); + } + + function trackModalTabView(tool: string) { + telemetry.track('User visited template recommendation modal tab', { + tool, + }); + } + + function trackTemplateTileClick(templateId: number) { + telemetry.track('User clicked on template recommendation tile', { + templateId, + }); + } + + function trackVideoClick(name: string) { + telemetry.track('User clicked on template recommendation video', { + name, + }); + } + + function trackSeeMoreClick(type: 'starter' | 'popular') { + telemetry.track('User clicked on template recommendation see more', { + type, + }); + } + + return { + isFeatureEnabled, + getNodeData, + getTemplateData, + nodes, + getTemplateRoute, + trackMinicardClick, + trackModalTabView, + trackTemplateTileClick, + trackVideoClick, + trackSeeMoreClick, + }; + }, +); diff --git a/packages/frontend/editor-ui/src/stores/ui.store.ts b/packages/frontend/editor-ui/src/stores/ui.store.ts index 2ec2efd853..0f74210935 100644 --- a/packages/frontend/editor-ui/src/stores/ui.store.ts +++ b/packages/frontend/editor-ui/src/stores/ui.store.ts @@ -44,6 +44,7 @@ import { LOCAL_STORAGE_THEME, WHATS_NEW_MODAL_KEY, WORKFLOW_DIFF_MODAL_KEY, + EXPERIMENT_TEMPLATE_RECO_V2_KEY, } from '@/constants'; import { STORES } from '@n8n/stores'; import type { @@ -219,6 +220,12 @@ export const useUIStore = defineStore(STORES.UI, () => { articleId: undefined, }, }, + [EXPERIMENT_TEMPLATE_RECO_V2_KEY]: { + open: false, + data: { + nodeName: '', + }, + }, }); const modalStack = ref([]); diff --git a/packages/frontend/editor-ui/src/views/WorkflowsView.vue b/packages/frontend/editor-ui/src/views/WorkflowsView.vue index 372b173bf7..104f1190c5 100644 --- a/packages/frontend/editor-ui/src/views/WorkflowsView.vue +++ b/packages/frontend/editor-ui/src/views/WorkflowsView.vue @@ -25,6 +25,8 @@ import SuggestedWorkflowCard from '@/experiments/personalizedTemplates/component import SuggestedWorkflows from '@/experiments/personalizedTemplates/components/SuggestedWorkflows.vue'; import { usePersonalizedTemplatesStore } from '@/experiments/personalizedTemplates/stores/personalizedTemplates.store'; import { useReadyToRunWorkflowsStore } from '@/experiments/readyToRunWorkflows/stores/readyToRunWorkflows.store'; +import TemplateRecommendationV2 from '@/experiments/templateRecoV2/components/TemplateRecommendationV2.vue'; +import { usePersonalizedTemplatesV2Store } from '@/experiments/templateRecoV2/stores/templateRecoV2.store'; import InsightsSummary from '@/features/insights/components/InsightsSummary.vue'; import { useInsightsStore } from '@/features/insights/insights.store'; import type { @@ -120,6 +122,7 @@ const templatesStore = useTemplatesStore(); const aiStarterTemplatesStore = useAITemplatesStarterCollectionStore(); const personalizedTemplatesStore = usePersonalizedTemplatesStore(); const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore(); +const personalizedTemplatesV2Store = usePersonalizedTemplatesV2Store(); const documentTitle = useDocumentTitle(); const { callDebounced } = useDebounce(); @@ -476,6 +479,18 @@ const onFolderDeleted = async (payload: { }); }; +const showInsights = computed(() => { + return ( + projectPages.isOverviewSubPage && + insightsStore.isSummaryEnabled && + (!personalizedTemplatesV2Store.isFeatureEnabled() || workflowListResources.value.length > 0) + ); +}); + +const showTemplateRecommendationV2 = computed(() => { + return personalizedTemplatesV2Store.isFeatureEnabled() && !loading.value; +}); + /** * LIFE-CYCLE HOOKS */ @@ -1699,7 +1714,7 @@ const onNameSubmit = async (name: string) => {