diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index be9d30edd2..a6c5ad7b2d 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -76,6 +76,7 @@ "generic.service": "the service", "generic.star": "Star", "generic.tryNow": "Try now", + "generic.startNow": "Start now", "generic.dismiss": "Dismiss", "generic.unsavedWork.confirmMessage.headline": "Save changes before leaving?", "generic.unsavedWork.confirmMessage.message": "If you don't save, you will lose your changes.", @@ -2679,6 +2680,10 @@ "workflows.create.folder.toast.title": "Workflow successfully created in \"{projectName}\", within \"{folderName}\"", "workflows.create.project.toast.text": "All members from {projectName} will have access to this workflow.", "workflows.deactivated": "Workflow deactivated", + "workflows.ai.starter.collection.callout": "Learn how to build AI Agents in n8n", + "workflows.ai.starter.collection.card": "Learn how to build AI Agents", + "workflows.ai.starter.collection.folder.name": "🎁 n8n basics: Learn how to build Agents in n8n", + "workflows.ai.starter.collection.error": "Error loading AI Agent starter collection. Please try again later.", "workflowSelectorParameterInput.createNewSubworkflow.name": "My Sub-Workflow", "importCurlModal.title": "Import cURL command", "importCurlModal.input.label": "cURL Command", diff --git a/packages/frontend/@n8n/stores/src/constants.ts b/packages/frontend/@n8n/stores/src/constants.ts index 15fc0862ed..a662294c62 100644 --- a/packages/frontend/@n8n/stores/src/constants.ts +++ b/packages/frontend/@n8n/stores/src/constants.ts @@ -31,4 +31,5 @@ export const STORES = { FOLDERS: 'folders', MODULES: 'modules', FOCUS_PANEL: 'focusPanel', + AI_TEMPLATES_STARTER_COLLECTION: 'aiTemplatesStarterCollection', } as const; 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 754540c7ce..dc4e383257 100644 --- a/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionFinished.ts +++ b/packages/frontend/editor-ui/src/composables/usePushConnection/handlers/executionFinished.ts @@ -27,6 +27,7 @@ import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useRunWorkflow } from '@/composables/useRunWorkflow'; import { useWorkflowSaving } from '@/composables/useWorkflowSaving'; +import { useAITemplatesStarterCollectionStore } from '@/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store'; export type SimplifiedExecution = Pick< IExecutionResponse, @@ -42,6 +43,7 @@ export async function executionFinished( ) { const workflowsStore = useWorkflowsStore(); const uiStore = useUIStore(); + const aiTemplatesStarterCollectionStore = useAITemplatesStarterCollectionStore(); workflowsStore.lastAddedExecutingNode = null; @@ -63,6 +65,12 @@ export async function executionFinished( status: data.status, }); } + if (workflow.meta.templateId.startsWith('035_template_onboarding')) { + aiTemplatesStarterCollectionStore.trackUserExecutedWorkflow( + workflow.meta.templateId.split('-').pop() ?? '', + data.status, + ); + } } uiStore.setProcessingExecutionResults(true); diff --git a/packages/frontend/editor-ui/src/constants.ts b/packages/frontend/editor-ui/src/constants.ts index 954378ef36..6f40fe9640 100644 --- a/packages/frontend/editor-ui/src/constants.ts +++ b/packages/frontend/editor-ui/src/constants.ts @@ -761,10 +761,18 @@ export const FOCUS_PANEL_EXPERIMENT = { variant: 'variant', }; +export const TEMPLATE_ONBOARDING_EXPERIMENT = { + name: '035_template_onboarding', + control: 'control', + variantStarterPack: 'variant-starter-pack', + variantSuggestedTemplates: 'variant-suggested-templates', +}; + export const EXPERIMENTS_TO_TRACK = [ WORKFLOW_BUILDER_EXPERIMENT.name, RAG_STARTER_WORKFLOW_EXPERIMENT.name, EXTRA_TEMPLATE_LINKS_EXPERIMENT.name, + TEMPLATE_ONBOARDING_EXPERIMENT.name, ]; export const MFA_FORM = { diff --git a/packages/frontend/editor-ui/src/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store.ts b/packages/frontend/editor-ui/src/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store.ts new file mode 100644 index 0000000000..242cd57f6a --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store.ts @@ -0,0 +1,100 @@ +import { useFoldersStore } from '@/stores/folders.store'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import type { WorkflowDataCreate } from '@n8n/rest-api-client'; +import { STORES } from '@n8n/stores'; +import { defineStore } from 'pinia'; +import { computed } from 'vue'; +import { AGENT_WITH_MEMORY } from '../workflows/1_agent_with_memory'; +import { AGENT_WITH_TOOLS } from '../workflows/2_agent_with_tools'; +import { AGENT_WITH_KNOWLEDGE } from '../workflows/3_agent_with_knowledge'; +import { useTelemetry } from '@/composables/useTelemetry'; +import { usePostHog } from '@/stores/posthog.store'; +import { TEMPLATE_ONBOARDING_EXPERIMENT } from '@/constants'; +import { useLocalStorage } from '@vueuse/core'; +import { useI18n } from '@n8n/i18n'; + +const LOCAL_STORAGE_SETTING_KEY = 'N8N_AI_TEMPLATES_STARTER_COLLECTION_CALL_OUT_DISMISSED'; + +export const useAITemplatesStarterCollectionStore = defineStore( + STORES.AI_TEMPLATES_STARTER_COLLECTION, + () => { + const telemetry = useTelemetry(); + const i18n = useI18n(); + + const foldersStore = useFoldersStore(); + const workflowsStore = useWorkflowsStore(); + const posthogStore = usePostHog(); + + const calloutDismissedRef = useLocalStorage(LOCAL_STORAGE_SETTING_KEY, false); + const calloutDismissed = computed(() => calloutDismissedRef.value); + + const isFeatureEnabled = computed(() => { + return ( + posthogStore.getVariant(TEMPLATE_ONBOARDING_EXPERIMENT.name) === + TEMPLATE_ONBOARDING_EXPERIMENT.variantStarterPack + ); + }); + + const dismissCallout = () => { + calloutDismissedRef.value = true; + }; + + const createStarterWorkflows = async (projectId: string, parentFolderId?: string) => { + const collectionFolder = await foldersStore.createFolder( + i18n.baseText('workflows.ai.starter.collection.folder.name'), + projectId, + parentFolderId, + ); + const agentWitheMemory: WorkflowDataCreate = { + ...AGENT_WITH_MEMORY, + parentFolderId: collectionFolder.id, + }; + const agentWithTools: WorkflowDataCreate = { + ...AGENT_WITH_TOOLS, + parentFolderId: collectionFolder.id, + }; + const agentWithKnowledge: WorkflowDataCreate = { + ...AGENT_WITH_KNOWLEDGE, + parentFolderId: collectionFolder.id, + }; + await workflowsStore.createNewWorkflow(agentWithKnowledge); + await workflowsStore.createNewWorkflow(agentWithTools); + await workflowsStore.createNewWorkflow(agentWitheMemory); + dismissCallout(); + return collectionFolder; + }; + + const trackUserCreatedStarterCollection = (source: 'card' | 'callout') => { + telemetry.track('User created AI templates starter collection', { + source, + }); + }; + + const trackUserDismissedCallout = () => { + telemetry.track('User dismissed AI templates starter collection callout'); + }; + + const trackUserOpenedWorkflow = (template: string) => { + telemetry.track('User opened AI template workflow', { + template, + }); + }; + + const trackUserExecutedWorkflow = (template: string, status: string) => { + telemetry.track('User executed AI template', { + template, + status, + }); + }; + return { + isFeatureEnabled, + calloutDismissed, + dismissCallout, + createStarterWorkflows, + trackUserCreatedStarterCollection, + trackUserDismissedCallout, + trackUserOpenedWorkflow, + trackUserExecutedWorkflow, + }; + }, +); diff --git a/packages/frontend/editor-ui/src/experiments/aiTemplatesStarterCollection/workflows/1_agent_with_memory.ts b/packages/frontend/editor-ui/src/experiments/aiTemplatesStarterCollection/workflows/1_agent_with_memory.ts new file mode 100644 index 0000000000..c82b612f59 --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/aiTemplatesStarterCollection/workflows/1_agent_with_memory.ts @@ -0,0 +1,106 @@ +import type { WorkflowDataCreate } from '@n8n/rest-api-client'; + +export const AGENT_WITH_MEMORY: WorkflowDataCreate = { + meta: { + templateId: '035_template_onboarding-agent_with_memory', + }, + name: '1. Agent with memory', + nodes: [ + { + id: '536fa635-c0c8-4fd1-84ef-d19585be64cf', + name: 'When chat message received', + webhookId: '9baee3d2-b9cb-4333-b4d5-3b07db8da9b2', + type: '@n8n/n8n-nodes-langchain.chatTrigger', + typeVersion: 1.1, + position: [0, 0], + parameters: { + options: {}, + }, + }, + { + id: '1393095b-ec84-4514-b481-432609d1a5c5', + name: 'AI Agent', + type: '@n8n/n8n-nodes-langchain.agent', + typeVersion: 2.1, + position: [208, 0], + parameters: { + options: { + systemMessage: '=You are a helpful assistant\n\nTodays date: {{ $now }}', + }, + }, + }, + { + id: '658eb127-5a9d-4404-9270-72ef1da36ea3', + name: 'Sticky Note', + type: 'n8n-nodes-base.stickyNote', + typeVersion: 1, + position: [-464, -32], + parameters: { + content: + '### Readme\nChat with an AI agent that remembers your previous messages during a conversation.\n\n**Quick Start**\n1. Open the **Model** node to claim your free API credits and connect to OpenAI. \n2. Click the **Open chat** button to start talking to the agent. Provide it with some information to remember, like your name.\n3. Ask a follow-up question about the information you shared: "What\'s my name?"\n\n---\n\n**Learn More**\n- [AI Agent node documentation](https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.agent/)\n- [Simple Memory node documentation](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.memorybufferwindow/)', + height: 396, + width: 392, + color: 4, + }, + }, + { + id: '186173c3-d043-4485-b939-9fca3d1e906f', + name: 'Model', + type: '@n8n/n8n-nodes-langchain.lmChatOpenAi', + typeVersion: 1.2, + position: [160, 240], + parameters: { + model: { + __rl: true, + mode: 'list', + value: 'gpt-4.1-mini', + }, + options: {}, + }, + }, + { + id: 'dadaaa19-8378-4e23-9573-f9d3ff52c884', + name: 'Memory', + type: '@n8n/n8n-nodes-langchain.memoryBufferWindow', + typeVersion: 1.3, + position: [352, 240], + parameters: {}, + }, + ], + connections: { + 'When chat message received': { + main: [ + [ + { + node: 'AI Agent', + type: 'main', + index: 0, + }, + ], + ], + }, + Model: { + ai_languageModel: [ + [ + { + node: 'AI Agent', + type: 'ai_languageModel', + index: 0, + }, + ], + ], + }, + Memory: { + ai_memory: [ + [ + { + node: 'AI Agent', + type: 'ai_memory', + index: 0, + }, + ], + ], + }, + }, + // parentFolderId: newFolder.id, +}; diff --git a/packages/frontend/editor-ui/src/experiments/aiTemplatesStarterCollection/workflows/2_agent_with_tools.ts b/packages/frontend/editor-ui/src/experiments/aiTemplatesStarterCollection/workflows/2_agent_with_tools.ts new file mode 100644 index 0000000000..932f17d162 --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/aiTemplatesStarterCollection/workflows/2_agent_with_tools.ts @@ -0,0 +1,371 @@ +import type { WorkflowDataCreate } from '@n8n/rest-api-client'; + +export const AGENT_WITH_TOOLS: WorkflowDataCreate = { + meta: { + templateId: '035_template_onboarding-agent_with_tools', + }, + name: '2. Agent with tools', + nodes: [ + { + parameters: { + options: {}, + }, + type: '@n8n/n8n-nodes-langchain.chatTrigger', + typeVersion: 1.1, + position: [-48, -16], + id: 'f6c9fe3c-cbde-4514-9fcf-9d618526965c', + name: 'When chat message received', + webhookId: '1bf95244-fbc3-4210-9420-f34a45c4f5f5', + }, + { + parameters: { + options: {}, + }, + type: '@n8n/n8n-nodes-langchain.agent', + typeVersion: 2.1, + position: [208, -16], + id: '5db1043f-de79-425a-a66b-8288c3aaa7df', + name: 'AI Agent', + }, + { + parameters: {}, + type: '@n8n/n8n-nodes-langchain.memoryBufferWindow', + typeVersion: 1.3, + position: [208, 288], + id: '29f1ba2f-7511-4771-958a-be6463a64d83', + name: 'Simple Memory', + }, + { + parameters: { + operation: 'update', + documentId: { + __rl: true, + value: '1vbFb2dhys1VafAmX-hRtiyrEDgNKj_xaAA6ZmH09EL8', + mode: 'list', + cachedResultName: 'Demo', + cachedResultUrl: + 'https://docs.google.com/spreadsheets/d/1vbFb2dhys1VafAmX-hRtiyrEDgNKj_xaAA6ZmH09EL8/edit?usp=drivesdk', + }, + sheetName: { + __rl: true, + value: 'gid=0', + mode: 'list', + cachedResultName: 'Sheet1', + cachedResultUrl: + 'https://docs.google.com/spreadsheets/d/1vbFb2dhys1VafAmX-hRtiyrEDgNKj_xaAA6ZmH09EL8/edit#gid=0', + }, + columns: { + mappingMode: 'defineBelow', + value: { + ID: "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('ID__using_to_match_', ``, 'string') }}", + Name: "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Name', ``, 'string') }}", + }, + matchingColumns: ['ID'], + schema: [ + { + id: 'ID', + displayName: 'ID', + required: false, + defaultMatch: false, + display: true, + type: 'string', + canBeUsedToMatch: true, + removed: false, + }, + { + id: 'Name', + displayName: 'Name', + required: false, + defaultMatch: false, + display: true, + type: 'string', + canBeUsedToMatch: true, + removed: false, + }, + { + id: 'row_number', + displayName: 'row_number', + required: false, + defaultMatch: false, + display: true, + type: 'number', + canBeUsedToMatch: true, + readOnly: true, + removed: true, + }, + ], + attemptToConvertTypes: false, + convertFieldsToString: false, + }, + options: {}, + }, + type: 'n8n-nodes-base.googleSheetsTool', + typeVersion: 4.6, + position: [656, 640], + id: '24d82ca2-b666-415b-9020-f88cf3e095e6', + name: 'Update', + credentials: {}, + }, + { + parameters: { + operation: 'append', + documentId: { + __rl: true, + value: '1vbFb2dhys1VafAmX-hRtiyrEDgNKj_xaAA6ZmH09EL8', + mode: 'list', + cachedResultName: 'Demo', + cachedResultUrl: + 'https://docs.google.com/spreadsheets/d/1vbFb2dhys1VafAmX-hRtiyrEDgNKj_xaAA6ZmH09EL8/edit?usp=drivesdk', + }, + sheetName: { + __rl: true, + value: 'gid=0', + mode: 'list', + cachedResultName: 'Sheet1', + cachedResultUrl: + 'https://docs.google.com/spreadsheets/d/1vbFb2dhys1VafAmX-hRtiyrEDgNKj_xaAA6ZmH09EL8/edit#gid=0', + }, + columns: { + mappingMode: 'defineBelow', + value: { + // eslint-disable-next-line n8n-local-rules/no-interpolation-in-regular-string + ID: "={{ `${Math.random()}`.replace('0.', '') }}", + Name: "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Name', ``, 'string') }}", + }, + matchingColumns: ['ID'], + schema: [ + { + id: 'ID', + displayName: 'ID', + required: false, + defaultMatch: false, + display: true, + type: 'string', + canBeUsedToMatch: true, + removed: false, + }, + { + id: 'Name', + displayName: 'Name', + required: false, + defaultMatch: false, + display: true, + type: 'string', + canBeUsedToMatch: true, + removed: false, + }, + { + id: 'row_number', + displayName: 'row_number', + required: false, + defaultMatch: false, + display: true, + type: 'number', + canBeUsedToMatch: true, + readOnly: true, + removed: true, + }, + ], + attemptToConvertTypes: false, + convertFieldsToString: false, + }, + options: {}, + }, + type: 'n8n-nodes-base.googleSheetsTool', + typeVersion: 4.6, + position: [464, 640], + id: 'a0554e4a-8a7a-480c-a9e6-5f9746252cdb', + name: 'Create', + credentials: {}, + }, + { + parameters: { + documentId: { + __rl: true, + value: '1vbFb2dhys1VafAmX-hRtiyrEDgNKj_xaAA6ZmH09EL8', + mode: 'list', + cachedResultName: 'Demo', + cachedResultUrl: + 'https://docs.google.com/spreadsheets/d/1vbFb2dhys1VafAmX-hRtiyrEDgNKj_xaAA6ZmH09EL8/edit?usp=drivesdk', + }, + sheetName: { + __rl: true, + value: 'gid=0', + mode: 'list', + cachedResultName: 'Sheet1', + cachedResultUrl: + 'https://docs.google.com/spreadsheets/d/1vbFb2dhys1VafAmX-hRtiyrEDgNKj_xaAA6ZmH09EL8/edit#gid=0', + }, + options: {}, + }, + type: 'n8n-nodes-base.googleSheetsTool', + typeVersion: 4.6, + position: [480, 288], + id: 'ef476f0d-bdfe-4f41-8690-fb270ed82469', + name: 'Read', + credentials: {}, + }, + { + parameters: { + operation: 'delete', + documentId: { + __rl: true, + value: '1vbFb2dhys1VafAmX-hRtiyrEDgNKj_xaAA6ZmH09EL8', + mode: 'list', + cachedResultName: 'Demo', + cachedResultUrl: + 'https://docs.google.com/spreadsheets/d/1vbFb2dhys1VafAmX-hRtiyrEDgNKj_xaAA6ZmH09EL8/edit?usp=drivesdk', + }, + sheetName: { + __rl: true, + value: 'gid=0', + mode: 'list', + cachedResultName: 'Sheet1', + cachedResultUrl: + 'https://docs.google.com/spreadsheets/d/1vbFb2dhys1VafAmX-hRtiyrEDgNKj_xaAA6ZmH09EL8/edit#gid=0', + }, + startIndex: + "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Start_Row_Number', ``, 'number') }}", + numberToDelete: + "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Number_of_Rows_to_Delete', ``, 'number') }}", + }, + type: 'n8n-nodes-base.googleSheetsTool', + typeVersion: 4.6, + position: [864, 640], + id: 'd0cca35f-9e74-4935-92e2-9b5e37f1c7f4', + name: 'Delete', + credentials: {}, + }, + { + parameters: { + content: + "### Readme\nThis agent uses tools to interact with a simple spreadsheet of orders.\n\n**Quick Start**\n1. Copy this [spreadsheet](https://docs.google.com/spreadsheets/d/1vbFb2dhys1VafAmX-hRtiyrEDgNKj_xaAA6ZmH09EL8/edit?usp=sharing) into your Google Drive.\n2. Open the **Read** tool and connect your Google account by creating a credential and selecting the spreadsheet.\n3. Ask the Agent to calculate the total in the amount column, you should see it use the **Read** tool followed by the **Calculator**.\n4. Try some other questions and see how the agent responds.\n5. Use what you've learned to connect the other tools in the **Next steps** section.\n\n---\n\n**Learn More**\n- [Google sheet tool documentation](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.googlesheets/?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=n8n-nodes-base.googleSheetsTool)\n- [Calculator tool documentation](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolcalculator/)", + height: 460, + width: 440, + color: 4, + }, + type: 'n8n-nodes-base.stickyNote', + position: [-576, -32], + typeVersion: 1, + id: 'e57c1fe3-3ef9-447c-81e3-bdf8717706b9', + name: 'Sticky Note', + }, + { + parameters: {}, + type: '@n8n/n8n-nodes-langchain.toolCalculator', + typeVersion: 1, + position: [624, 288], + id: '49030d8b-0818-455b-a472-356b620566c4', + name: 'Calculator', + }, + { + parameters: { + model: { + __rl: true, + mode: 'list', + value: 'gpt-4.1-mini', + }, + options: {}, + }, + type: '@n8n/n8n-nodes-langchain.lmChatOpenAi', + typeVersion: 1.2, + position: [48, 288], + id: '67c78b12-b088-41b4-aeb4-70a7f056c9a7', + name: 'Model', + credentials: {}, + }, + { + parameters: { + content: '## 🛠️ Tools', + height: 224, + width: 368, + color: 7, + }, + type: 'n8n-nodes-base.stickyNote', + position: [400, 224], + typeVersion: 1, + id: '14c03c2d-e0ea-4958-94c1-3598e5c273d9', + name: 'Sticky Note1', + }, + { + parameters: { + content: + '## 🛠️ Next steps\n\nConnect these tools to perform create, read and update actions on your order list.', + height: 320, + width: 592, + color: 7, + }, + type: 'n8n-nodes-base.stickyNote', + position: [400, 496], + typeVersion: 1, + id: 'a683cb43-e740-497e-bb01-edebfe39a832', + name: 'Sticky Note2', + }, + ], + connections: { + 'When chat message received': { + main: [ + [ + { + node: 'AI Agent', + type: 'main', + index: 0, + }, + ], + ], + }, + 'Simple Memory': { + ai_memory: [ + [ + { + node: 'AI Agent', + type: 'ai_memory', + index: 0, + }, + ], + ], + }, + Update: { + ai_tool: [[]], + }, + Create: { + ai_tool: [[]], + }, + Read: { + ai_tool: [ + [ + { + node: 'AI Agent', + type: 'ai_tool', + index: 0, + }, + ], + ], + }, + Delete: { + ai_tool: [[]], + }, + Calculator: { + ai_tool: [ + [ + { + node: 'AI Agent', + type: 'ai_tool', + index: 0, + }, + ], + ], + }, + Model: { + ai_languageModel: [ + [ + { + node: 'AI Agent', + type: 'ai_languageModel', + index: 0, + }, + ], + ], + }, + }, +}; diff --git a/packages/frontend/editor-ui/src/experiments/aiTemplatesStarterCollection/workflows/3_agent_with_knowledge.ts b/packages/frontend/editor-ui/src/experiments/aiTemplatesStarterCollection/workflows/3_agent_with_knowledge.ts new file mode 100644 index 0000000000..ba4c48b97a --- /dev/null +++ b/packages/frontend/editor-ui/src/experiments/aiTemplatesStarterCollection/workflows/3_agent_with_knowledge.ts @@ -0,0 +1,250 @@ +import type { WorkflowDataCreate } from '@n8n/rest-api-client'; + +export const AGENT_WITH_KNOWLEDGE: WorkflowDataCreate = { + meta: { + templateId: '035_template_onboarding-agent_with_knowledge', + }, + name: '3. Agent with knowledge', + nodes: [ + { + parameters: { + formTitle: 'Upload your data to test RAG', + formFields: { + values: [ + { + fieldLabel: 'Upload your file(s)', + fieldType: 'file', + acceptFileTypes: '.pdf, .csv', + requiredField: true, + }, + ], + }, + options: {}, + }, + type: 'n8n-nodes-base.formTrigger', + typeVersion: 2.2, + position: [-368, -304], + id: 'e45eaedc-7ed9-4fd6-b79b-82a155734bce', + name: 'Upload your file here', + webhookId: '82848bc4-5ea2-4e5a-8bb6-3c09b94a8c5d', + }, + { + parameters: { + options: {}, + }, + type: '@n8n/n8n-nodes-langchain.embeddingsOpenAi', + typeVersion: 1.2, + position: [288, 176], + id: 'f9f1745a-9c6d-42cf-8f8b-19a1c4c91b77', + name: 'Embeddings OpenAI', + }, + { + parameters: { + dataType: 'binary', + options: {}, + }, + type: '@n8n/n8n-nodes-langchain.documentDefaultDataLoader', + typeVersion: 1.1, + position: [80, -144], + id: '56da285f-9ad9-4cb4-a7d4-fb0f035e9071', + name: 'Default Data Loader', + }, + { + parameters: { + content: + '### Readme\nLoad your data into a vector database with the 📚 **Load Data** flow, and then use your data as chat context with the 🐕 **Retriever** flow.\n\n**Quick start**\n1. Click on the `Execute Workflow` button to run the 📚 **Load Data** flow.\n2. Click on `Open Chat` button to run the 🐕 **Retriever** flow. Then ask a question about content from your document(s)\n\n\nFor more info, check [our docs on RAG in n8n](https://docs.n8n.io/advanced-ai/rag-in-n8n/).', + height: 300, + width: 440, + color: 4, + }, + type: 'n8n-nodes-base.stickyNote', + position: [-896, -368], + typeVersion: 1, + id: '72cb28c7-572b-41ce-ba4c-2dd0dc80acb4', + name: 'Sticky Note', + }, + { + parameters: { + content: '### 📚 Load Data Flow', + height: 460, + width: 700, + color: 7, + }, + type: 'n8n-nodes-base.stickyNote', + position: [-416, -368], + typeVersion: 1, + id: '075a4895-8b5f-487b-b7ff-8766aa1d1450', + name: 'Sticky Note1', + }, + { + parameters: { + mode: 'insert', + memoryKey: { + __rl: true, + value: 'vector_store_key', + mode: 'list', + cachedResultName: 'vector_store_key', + }, + }, + type: '@n8n/n8n-nodes-langchain.vectorStoreInMemory', + typeVersion: 1.2, + position: [-176, -304], + id: 'd24732cb-b6b4-4eb6-ad47-1f290f0da13d', + name: 'Insert Data to Store', + }, + { + parameters: { + mode: 'retrieve-as-tool', + toolName: 'knowledge_base', + toolDescription: 'Use this knowledge base to answer questions from the user', + memoryKey: { + __rl: true, + mode: 'list', + value: 'vector_store_key', + }, + }, + type: '@n8n/n8n-nodes-langchain.vectorStoreInMemory', + typeVersion: 1.2, + position: [704, -96], + id: '8915e9b6-2a6c-472c-b043-78c1d77888ae', + name: 'Query Data Tool', + }, + { + parameters: { + options: {}, + }, + type: '@n8n/n8n-nodes-langchain.agent', + typeVersion: 2, + position: [704, -320], + id: 'a2baaa1d-beee-40a2-8b5f-757b2b1aaca9', + name: 'AI Agent', + }, + { + parameters: { + options: {}, + }, + type: '@n8n/n8n-nodes-langchain.chatTrigger', + typeVersion: 1.1, + position: [480, -320], + id: 'c0a831dd-362c-4352-9409-bfe8c2e6f5ab', + name: 'When chat message received', + webhookId: '4091fa09-fb9a-4039-9411-7104d213f601', + }, + { + parameters: { + model: { + __rl: true, + mode: 'list', + value: 'gpt-4o-mini', + }, + options: {}, + }, + type: '@n8n/n8n-nodes-langchain.lmChatOpenAi', + typeVersion: 1.2, + position: [480, -96], + id: '2ba103b5-7783-49a7-9693-a156383ca697', + name: 'OpenAI Chat Model', + }, + { + parameters: { + content: '### 🐕 2. Retriever Flow', + height: 460, + width: 680, + color: 7, + }, + type: 'n8n-nodes-base.stickyNote', + position: [368, -368], + typeVersion: 1, + id: '729f9492-3811-43d8-8230-d9ad78d910f7', + name: 'Sticky Note2', + }, + { + parameters: { + content: + '### Embeddings\n\nThe Insert and Retrieve operation use the same embedding node.\n\nThis is to ensure that they are using the **exact same embeddings and settings**.\n\nDifferent embeddings might not work at all, or have unintended consequences.\n', + height: 240, + width: 320, + color: 4, + }, + type: 'n8n-nodes-base.stickyNote', + position: [432, 144], + typeVersion: 1, + id: '20a6b946-5588-4df2-b1f6-952fc82d166c', + name: 'Sticky Note3', + }, + ], + connections: { + 'Upload your file here': { + main: [ + [ + { + node: 'Insert Data to Store', + type: 'main', + index: 0, + }, + ], + ], + }, + 'Embeddings OpenAI': { + ai_embedding: [ + [ + { + node: 'Insert Data to Store', + type: 'ai_embedding', + index: 0, + }, + { + node: 'Query Data Tool', + type: 'ai_embedding', + index: 0, + }, + ], + ], + }, + 'Default Data Loader': { + ai_document: [ + [ + { + node: 'Insert Data to Store', + type: 'ai_document', + index: 0, + }, + ], + ], + }, + 'Query Data Tool': { + ai_tool: [ + [ + { + node: 'AI Agent', + type: 'ai_tool', + index: 0, + }, + ], + ], + }, + 'When chat message received': { + main: [ + [ + { + node: 'AI Agent', + type: 'main', + index: 0, + }, + ], + ], + }, + 'OpenAI Chat Model': { + ai_languageModel: [ + [ + { + node: 'AI Agent', + type: 'ai_languageModel', + index: 0, + }, + ], + ], + }, + }, + pinData: {}, +}; diff --git a/packages/frontend/editor-ui/src/views/NodeView.vue b/packages/frontend/editor-ui/src/views/NodeView.vue index 58fd90369c..6ea21871a0 100644 --- a/packages/frontend/editor-ui/src/views/NodeView.vue +++ b/packages/frontend/editor-ui/src/views/NodeView.vue @@ -137,6 +137,7 @@ import { useLogsStore } from '@/stores/logs.store'; import { canvasEventBus } from '@/event-bus/canvas'; import CanvasChatButton from '@/components/canvas/elements/buttons/CanvasChatButton.vue'; import { useFocusPanelStore } from '@/stores/focusPanel.store'; +import { useAITemplatesStarterCollectionStore } from '@/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store'; defineOptions({ name: 'NodeView', @@ -197,6 +198,7 @@ const foldersStore = useFoldersStore(); const posthogStore = usePostHog(); const agentRequestStore = useAgentRequestStore(); const logsStore = useLogsStore(); +const aiTemplatesStarterCollectionStore = useAITemplatesStarterCollectionStore(); const { addBeforeUnloadEventBindings, removeBeforeUnloadEventBindings } = useBeforeUnload({ route, @@ -500,6 +502,12 @@ async function initializeWorkspaceForExistingWorkflow(id: string) { trackOpenWorkflowFromOnboardingTemplate(); } + if (workflowData.meta?.templateId?.startsWith('035_template_onboarding')) { + aiTemplatesStarterCollectionStore.trackUserOpenedWorkflow( + workflowData.meta.templateId.split('-').pop() ?? '', + ); + } + await projectsStore.setProjectNavActiveIdByWorkflowHomeProject(workflowData.homeProject); } catch (error) { if (error.httpStatusCode === 404) { diff --git a/packages/frontend/editor-ui/src/views/WorkflowsView.vue b/packages/frontend/editor-ui/src/views/WorkflowsView.vue index 40d11e522d..1413f62844 100644 --- a/packages/frontend/editor-ui/src/views/WorkflowsView.vue +++ b/packages/frontend/editor-ui/src/views/WorkflowsView.vue @@ -69,6 +69,7 @@ import debounce from 'lodash/debounce'; import { type IUser, PROJECT_ROOT } from 'n8n-workflow'; import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'; import { type LocationQueryRaw, useRoute, useRouter } from 'vue-router'; +import { useAITemplatesStarterCollectionStore } from '@/experiments/aiTemplatesStarterCollection/stores/aiTemplatesStarterCollection.store'; const SEARCH_DEBOUNCE_TIME = 300; const FILTERS_DEBOUNCE_TIME = 100; @@ -112,6 +113,7 @@ const foldersStore = useFoldersStore(); const usageStore = useUsageStore(); const insightsStore = useInsightsStore(); const templatesStore = useTemplatesStore(); +const aiStarterTemplatesStore = useAITemplatesStarterCollectionStore(); const documentTitle = useDocumentTitle(); const { callDebounced } = useDebounce(); @@ -369,6 +371,19 @@ const showRegisteredCommunityCTA = computed( () => isSelfHostedDeployment.value && !foldersEnabled.value && canUserRegisterCommunityPlus.value, ); +const showAIStarterCollectionCallout = computed(() => { + return ( + !loading.value && + aiStarterTemplatesStore.isFeatureEnabled && + !aiStarterTemplatesStore.calloutDismissed && + !readOnlyEnv.value && + // We want to show the callout only if the user has permissions to create folders and workflows + // but also on the overview page + (projectPages.isOverviewSubPage || + (hasPermissionToCreateFolders.value && hasPermissionToCreateWorkflows.value)) + ); +}); + /** * WATCHERS, STORE SUBSCRIPTIONS AND EVENT BUS HANDLERS */ @@ -778,6 +793,45 @@ function isValidProjectId(projectId: string) { return projectsStore.availableProjects.some((project) => project.id === projectId); } +const createAIStarterWorkflows = async (source: 'card' | 'callout') => { + try { + const projectId = projectPages.isOverviewSubPage + ? personalProject.value?.id + : (route.params.projectId as string); + if (typeof projectId !== 'string') { + toast.showError(new Error(), i18n.baseText('workflows.ai.starter.collection.error')); + return; + } + const newFolder = await aiStarterTemplatesStore.createStarterWorkflows( + projectId, + currentFolderId.value ?? undefined, + ); + // If we are on the overview page, navigate to the new folder + if (projectPages.isOverviewSubPage) { + await router.push({ + name: VIEWS.PROJECTS_FOLDERS, + params: { projectId, folderId: newFolder.id }, + }); + } else { + // If we are in a specific folder, just add the new folder to the list + workflowsAndFolders.value.unshift({ + id: newFolder.id, + name: newFolder.name, + resource: 'folder', + createdAt: newFolder.createdAt, + updatedAt: newFolder.updatedAt, + subFolderCount: 0, + workflowCount: 3, + parentFolder: newFolder.parentFolder, + }); + } + aiStarterTemplatesStore.trackUserCreatedStarterCollection(source); + } catch (error) { + toast.showError(error, i18n.baseText('workflows.ai.starter.collection.error')); + return; + } +}; + const openAIWorkflow = async (source: string) => { dismissEasyAICallout(); telemetry.track('User clicked test AI workflow', { @@ -793,6 +847,11 @@ const openAIWorkflow = async (source: string) => { }); }; +const dismissStarterCollectionCallout = () => { + aiStarterTemplatesStore.dismissCallout(); + aiStarterTemplatesStore.trackUserDismissedCallout(); +}; + const dismissEasyAICallout = () => { easyAICalloutVisible.value = false; }; @@ -1610,7 +1669,34 @@ const onNameSubmit = async (name: string) => {