diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts b/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts index e9a6318f74..44d205b4ad 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts @@ -169,6 +169,7 @@ import IconLucideSettings from '~icons/lucide/settings'; import IconLucideShare from '~icons/lucide/share'; import IconLucideSlidersHorizontal from '~icons/lucide/sliders-horizontal'; import IconLucideSmile from '~icons/lucide/smile'; +import IconLucideSparkles from '~icons/lucide/sparkles'; import IconLucideSquare from '~icons/lucide/square'; import IconLucideSquareCheck from '~icons/lucide/square-check'; import IconLucideSquarePen from '~icons/lucide/square-pen'; @@ -583,6 +584,7 @@ export const updatedIconSet = { share: IconLucideShare, 'sliders-horizontal': IconLucideSlidersHorizontal, smile: IconLucideSmile, + sparkles: IconLucideSparkles, square: IconLucideSquare, 'square-check': IconLucideSquareCheck, 'square-pen': IconLucideSquarePen, diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index bce0c0f889..6fd4c74f65 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -2713,6 +2713,7 @@ "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.link": "Back to Personal", + "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.error.fetching": "Error fetching workflows", "workflows.shareModal.title": "Share '{name}'", diff --git a/packages/frontend/@n8n/stores/src/constants.ts b/packages/frontend/@n8n/stores/src/constants.ts index 3a7f5795dd..061d91afd8 100644 --- a/packages/frontend/@n8n/stores/src/constants.ts +++ b/packages/frontend/@n8n/stores/src/constants.ts @@ -34,5 +34,6 @@ export const STORES = { AI_TEMPLATES_STARTER_COLLECTION: 'aiTemplatesStarterCollection', PERSONALIZED_TEMPLATES: 'personalizedTemplates', EXPERIMENT_READY_TO_RUN_WORKFLOWS: 'readyToRunWorkflows', + EXPERIMENT_READY_TO_RUN_WORKFLOWS_V2: 'readyToRunWorkflowsV2', EXPERIMENT_TEMPLATE_RECO_V2: 'templateRecoV2', } as const; diff --git a/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.vue b/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.vue index d11bd4bcc9..a5d92c1a77 100644 --- a/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.vue +++ b/packages/frontend/editor-ui/src/components/Projects/ProjectHeader.vue @@ -21,6 +21,7 @@ import type { IUser } from 'n8n-workflow'; import { type IconOrEmoji, isIconOrEmoji } from '@n8n/design-system/components/N8nIconPicker/types'; import { useUIStore } from '@/stores/ui.store'; import { PROJECT_DATA_STORES } from '@/features/dataStore/constants'; +import ReadyToRunV2Button from '@/experiments/readyToRunWorkflowsV2/components/ReadyToRunV2Button.vue'; const route = useRoute(); const router = useRouter(); @@ -32,6 +33,10 @@ const uiStore = useUIStore(); const projectPages = useProjectPages(); +const props = defineProps<{ + hasActiveCallouts?: boolean; +}>(); + const emit = defineEmits<{ createFolder: []; }>(); @@ -341,18 +346,21 @@ const onSelect = (action: string) => { :disabled="!sourceControlStore.preferences.branchReadOnly" :content="i18n.baseText('readOnlyEnv.cantAdd.any')" > - - - +
+ + + + +
diff --git a/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionFinished.ts b/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionFinished.ts index 9d79832e7a..460f1cd107 100644 --- a/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionFinished.ts +++ b/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionFinished.ts @@ -10,6 +10,7 @@ import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants'; import { codeNodeEditorEventBus, globalLinkActionsEventBus } from '@/event-bus'; import { useAITemplatesStarterCollectionStore } from '@/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store'; import { useReadyToRunWorkflowsStore } from '@/experiments/readyToRunWorkflows/stores/readyToRunWorkflows.store'; +import { useReadyToRunWorkflowsV2Store } from '@/experiments/readyToRunWorkflowsV2/stores/readyToRunWorkflowsV2.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useSettingsStore } from '@/stores/settings.store'; import { useUIStore } from '@/stores/ui.store'; @@ -48,6 +49,7 @@ export async function executionFinished( const uiStore = useUIStore(); const aiTemplatesStarterCollectionStore = useAITemplatesStarterCollectionStore(); const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore(); + const readyToRunWorkflowsV2Store = useReadyToRunWorkflowsV2Store(); workflowsStore.lastAddedExecutingNode = null; @@ -76,6 +78,15 @@ export async function executionFinished( ); } else if (templateId.startsWith('37_onboarding_experiments_batch_aug11')) { 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)) { telemetry.track('User executed pre-built Agent', { template: templateId, diff --git a/packages/frontend/editor-ui/src/constants.ts b/packages/frontend/editor-ui/src/constants.ts index 2941c2d0c6..6a512970a1 100644 --- a/packages/frontend/editor-ui/src/constants.ts +++ b/packages/frontend/editor-ui/src/constants.ts @@ -810,6 +810,13 @@ export const TEMPLATE_RECO_V2 = { 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 = [ WORKFLOW_BUILDER_EXPERIMENT.name, EXTRA_TEMPLATE_LINKS_EXPERIMENT.name, @@ -818,6 +825,7 @@ export const EXPERIMENTS_TO_TRACK = [ BATCH_11AUG_EXPERIMENT.name, PRE_BUILT_AGENTS_EXPERIMENT.name, TEMPLATE_RECO_V2.name, + READY_TO_RUN_V2_EXPERIMENT.name, ]; export const MFA_FORM = { diff --git a/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/components/ReadyToRunV2Button.vue b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/components/ReadyToRunV2Button.vue new file mode 100644 index 0000000000..b59d71a6a5 --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/components/ReadyToRunV2Button.vue @@ -0,0 +1,74 @@ + + + diff --git a/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/components/SimplifiedEmptyLayout.vue b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/components/SimplifiedEmptyLayout.vue new file mode 100644 index 0000000000..fb97c1cf54 --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/components/SimplifiedEmptyLayout.vue @@ -0,0 +1,222 @@ + + + + + diff --git a/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/composables/useEmptyStateDetection.ts b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/composables/useEmptyStateDetection.ts new file mode 100644 index 0000000000..364c18d7ce --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/composables/useEmptyStateDetection.ts @@ -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, + }; +} diff --git a/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/stores/readyToRunWorkflowsV2.store.ts b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/stores/readyToRunWorkflowsV2.store.ts new file mode 100644 index 0000000000..c186d53c77 --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/stores/readyToRunWorkflowsV2.store.ts @@ -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, + }; + }, +); diff --git a/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/utils/workflowSamples.ts b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/utils/workflowSamples.ts new file mode 100644 index 0000000000..f2bfd84ae4 --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/utils/workflowSamples.ts @@ -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)), + ]; +}; diff --git a/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/workflows/ai-workflow-v2.ts b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/workflows/ai-workflow-v2.ts new file mode 100644 index 0000000000..837167ecf3 --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/workflows/ai-workflow-v2.ts @@ -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: {}, +}; diff --git a/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/workflows/ai-workflow.ts b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/workflows/ai-workflow.ts new file mode 100644 index 0000000000..244572cdbe --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/readyToRunWorkflowsV2/workflows/ai-workflow.ts @@ -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: {}, +}; diff --git a/packages/frontend/editor-ui/src/features/dataStore/DataStoreView.test.ts b/packages/frontend/editor-ui/src/features/dataStore/DataStoreView.test.ts index f4ff4bc4cc..3391bc386e 100644 --- a/packages/frontend/editor-ui/src/features/dataStore/DataStoreView.test.ts +++ b/packages/frontend/editor-ui/src/features/dataStore/DataStoreView.test.ts @@ -62,6 +62,7 @@ vi.mock('@/composables/useDocumentTitle', () => ({ const mockDebounce = { callDebounced: vi.fn((fn) => fn()), + debounce: vi.fn(), }; vi.mock('@/composables/useDebounce', () => ({ useDebounce: vi.fn(() => mockDebounce), diff --git a/packages/frontend/editor-ui/src/utils/templates/workflowSamples.ts b/packages/frontend/editor-ui/src/utils/templates/workflowSamples.ts index ec97cca210..bb0e2329f1 100644 --- a/packages/frontend/editor-ui/src/utils/templates/workflowSamples.ts +++ b/packages/frontend/editor-ui/src/utils/templates/workflowSamples.ts @@ -1,6 +1,7 @@ import { ApplicationError, type INodeTypeNameVersion } from 'n8n-workflow'; import type { WorkflowDataWithTemplateId } from '@/Interface'; import { isWorkflowDataWithTemplateId } from '@/utils/templates/typeGuards'; +import { getReadyToRunAIWorkflows } from '@/experiments/readyToRunWorkflowsV2/utils/workflowSamples'; /* eslint-disable import-x/extensions */ import easyAiStarterJson from '@/utils/templates/samples/easy_ai_starter.json'; @@ -192,6 +193,7 @@ export const getSampleWorkflowByTemplateId = ( const workflows = [ getEasyAiWorkflowJson(), getRagStarterWorkflowJson(), + ...getReadyToRunAIWorkflows(), ...getPrebuiltAgents().map((agent) => agent.template), ...getTutorialTemplates().map((tutorial) => tutorial.template), ]; diff --git a/packages/frontend/editor-ui/src/views/WorkflowsView.vue b/packages/frontend/editor-ui/src/views/WorkflowsView.vue index 35e41dda68..32c098b567 100644 --- a/packages/frontend/editor-ui/src/views/WorkflowsView.vue +++ b/packages/frontend/editor-ui/src/views/WorkflowsView.vue @@ -26,8 +26,10 @@ 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 { useReadyToRunWorkflowsV2Store } from '@/experiments/readyToRunWorkflowsV2/stores/readyToRunWorkflowsV2.store'; import TemplateRecommendationV2 from '@/experiments/templateRecoV2/components/TemplateRecommendationV2.vue'; 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 { useInsightsStore } from '@/features/insights/insights.store'; import type { @@ -124,6 +126,7 @@ const templatesStore = useTemplatesStore(); const aiStarterTemplatesStore = useAITemplatesStarterCollectionStore(); const personalizedTemplatesStore = usePersonalizedTemplatesStore(); const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore(); +const readyToRunWorkflowsV2Store = useReadyToRunWorkflowsV2Store(); const personalizedTemplatesV2Store = usePersonalizedTemplatesV2Store(); const documentTitle = useDocumentTitle(); @@ -437,6 +440,19 @@ const showPersonalizedTemplates = computed( () => !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 */ @@ -1713,7 +1729,10 @@ const onNameSubmit = async (name: string) => {