mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(editor): Implement template recommendation v2 experiment (no-changelog) (#18196)
This commit is contained in:
@@ -2662,6 +2662,14 @@
|
|||||||
"workflows.item.readonly": "Read only",
|
"workflows.item.readonly": "Read only",
|
||||||
"workflows.item.archived": "Archived",
|
"workflows.item.archived": "Archived",
|
||||||
"workflows.itemSuggestion.try": "Try template",
|
"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.search.placeholder": "Search",
|
||||||
"workflows.filters": "Filters",
|
"workflows.filters": "Filters",
|
||||||
"workflows.filters.tags": "Tags",
|
"workflows.filters.tags": "Tags",
|
||||||
|
|||||||
@@ -34,4 +34,5 @@ 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_TEMPLATE_RECO_V2: 'templateRecoV2',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
WORKFLOW_HISTORY_VERSION_RESTORE,
|
WORKFLOW_HISTORY_VERSION_RESTORE,
|
||||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||||
WORKFLOW_SHARE_MODAL_KEY,
|
WORKFLOW_SHARE_MODAL_KEY,
|
||||||
|
EXPERIMENT_TEMPLATE_RECO_V2_KEY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
|
||||||
import AboutModal from '@/components/AboutModal.vue';
|
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 type { EventBus } from '@n8n/utils/event-bus';
|
||||||
import PromptMfaCodeModal from './PromptMfaCodeModal/PromptMfaCodeModal.vue';
|
import PromptMfaCodeModal from './PromptMfaCodeModal/PromptMfaCodeModal.vue';
|
||||||
import DynamicModalLoader from './DynamicModalLoader.vue';
|
import DynamicModalLoader from './DynamicModalLoader.vue';
|
||||||
|
import NodeRecommendationModal from '@/experiments/templateRecoV2/components/NodeRecommendationModal.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -342,6 +344,12 @@ import DynamicModalLoader from './DynamicModalLoader.vue';
|
|||||||
</template>
|
</template>
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
|
||||||
|
<ModalRoot :name="EXPERIMENT_TEMPLATE_RECO_V2_KEY">
|
||||||
|
<template #default="{ modalName, data }">
|
||||||
|
<NodeRecommendationModal :modal-name="modalName" :data="data" />
|
||||||
|
</template>
|
||||||
|
</ModalRoot>
|
||||||
|
|
||||||
<!-- Dynamic modals from modules -->
|
<!-- Dynamic modals from modules -->
|
||||||
<DynamicModalLoader />
|
<DynamicModalLoader />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export const FROM_AI_PARAMETERS_MODAL_KEY = 'fromAiParameters';
|
|||||||
export const WORKFLOW_EXTRACTION_NAME_MODAL_KEY = 'workflowExtractionName';
|
export const WORKFLOW_EXTRACTION_NAME_MODAL_KEY = 'workflowExtractionName';
|
||||||
export const WHATS_NEW_MODAL_KEY = 'whatsNew';
|
export const WHATS_NEW_MODAL_KEY = 'whatsNew';
|
||||||
export const WORKFLOW_DIFF_MODAL_KEY = 'workflowDiff';
|
export const WORKFLOW_DIFF_MODAL_KEY = 'workflowDiff';
|
||||||
|
export const EXPERIMENT_TEMPLATE_RECO_V2_KEY = 'templateRecoV2';
|
||||||
|
|
||||||
export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = {
|
export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = {
|
||||||
UNINSTALL: 'uninstall',
|
UNINSTALL: 'uninstall',
|
||||||
@@ -787,6 +788,12 @@ export const PRE_BUILT_AGENTS_EXPERIMENT = {
|
|||||||
variant: 'variant',
|
variant: 'variant',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TEMPLATE_RECO_V2 = {
|
||||||
|
name: '039_template_onboarding_v2',
|
||||||
|
control: 'control',
|
||||||
|
variant: 'variant',
|
||||||
|
};
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { EXPERIMENT_TEMPLATE_RECO_V2_KEY } from '@/constants';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { computed, onMounted } from 'vue';
|
||||||
|
import { usePersonalizedTemplatesV2Store } from '../stores/templateRecoV2.store';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
nodeName: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const uiStore = useUIStore();
|
||||||
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
const { trackMinicardClick } = usePersonalizedTemplatesV2Store();
|
||||||
|
|
||||||
|
const nodeType = computed(() => nodeTypesStore.getNodeType(props.nodeName));
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
trackMinicardClick(nodeType.value?.displayName ?? props.nodeName);
|
||||||
|
uiStore.openModalWithData({
|
||||||
|
name: EXPERIMENT_TEMPLATE_RECO_V2_KEY,
|
||||||
|
data: { nodeName: props.nodeName },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nodeTypesStore.loadNodeTypesIfNotLoaded();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<N8nCard :class="$style.nodeCard" hoverable @click="openModal">
|
||||||
|
<div :class="$style.emptyStateCardContent">
|
||||||
|
<NodeIcon :node-type="nodeType" :class="$style.nodeIcon" :stroke-width="1.5" />
|
||||||
|
<N8nText size="xsmall" class="mt-xs pl-2xs pr-2xs" :bold="true">
|
||||||
|
{{ nodeType?.displayName }}
|
||||||
|
</N8nText>
|
||||||
|
</div>
|
||||||
|
</N8nCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.nodeCard {
|
||||||
|
width: 100px;
|
||||||
|
height: 80px;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodeIcon {
|
||||||
|
font-size: var(--font-size-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyStateCardContent {
|
||||||
|
flex: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { EXPERIMENT_TEMPLATE_RECO_V2_KEY, TEMPLATES_URLS } from '@/constants';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import type { ITemplatesWorkflowFull } from '@n8n/rest-api-client';
|
||||||
|
import { computed, ref, watchEffect } from 'vue';
|
||||||
|
import { usePersonalizedTemplatesV2Store } from '../stores/templateRecoV2.store';
|
||||||
|
import TemplateCard from './TemplateCard.vue';
|
||||||
|
import YoutubeCard from './YoutubeCard.vue';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modalName: string;
|
||||||
|
data: {
|
||||||
|
nodeName?: string;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const uiStore = useUIStore();
|
||||||
|
const {
|
||||||
|
nodes: userSelectedNodes,
|
||||||
|
getNodeData,
|
||||||
|
getTemplateData,
|
||||||
|
trackModalTabView,
|
||||||
|
trackSeeMoreClick,
|
||||||
|
} = usePersonalizedTemplatesV2Store();
|
||||||
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
const locale = useI18n();
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
uiStore.closeModal(EXPERIMENT_TEMPLATE_RECO_V2_KEY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedNode = ref<string>(props.data.nodeName ?? userSelectedNodes[0] ?? '');
|
||||||
|
|
||||||
|
const starterTemplates = ref<ITemplatesWorkflowFull[]>([]);
|
||||||
|
const popularTemplates = ref<ITemplatesWorkflowFull[]>([]);
|
||||||
|
const isLoadingTemplates = ref(false);
|
||||||
|
|
||||||
|
const nodes = computed(() =>
|
||||||
|
userSelectedNodes.map((nodeName) => {
|
||||||
|
const nodeType = nodeTypesStore.getNodeType(nodeName);
|
||||||
|
return {
|
||||||
|
value: nodeName,
|
||||||
|
label: nodeType?.displayName ?? '',
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodeTypes = computed(
|
||||||
|
() =>
|
||||||
|
new Map(userSelectedNodes.map((nodeName) => [nodeName, nodeTypesStore.getNodeType(nodeName)])),
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentNodeData = computed(() => {
|
||||||
|
return getNodeData(selectedNode.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const youtubeVideos = computed(() => {
|
||||||
|
return currentNodeData.value.youtube || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const starterLink = computed(() => {
|
||||||
|
const name = nodeTypes.value.get(selectedNode.value)?.displayName ?? '';
|
||||||
|
const encodedName = encodeURIComponent(name);
|
||||||
|
return `${TEMPLATES_URLS.BASE_WEBSITE_URL}?integrations=${encodedName}&q=Simple`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const popularLink = computed(() => {
|
||||||
|
const name = nodeTypes.value.get(selectedNode.value)?.displayName ?? '';
|
||||||
|
const encodedName = encodeURIComponent(name);
|
||||||
|
return `${TEMPLATES_URLS.BASE_WEBSITE_URL}?integrations=${encodedName}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSelectedNodeChange(val: string) {
|
||||||
|
selectedNode.value = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(async () => {
|
||||||
|
if (!selectedNode.value) return;
|
||||||
|
|
||||||
|
const nodeDisplayName = nodes.value.find((n) => n.value === selectedNode.value)?.label;
|
||||||
|
if (nodeDisplayName) {
|
||||||
|
trackModalTabView(nodeDisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingTemplates.value = true;
|
||||||
|
try {
|
||||||
|
const nodeData = getNodeData(selectedNode.value);
|
||||||
|
const starterPromises = nodeData.starter?.map(async (id) => await getTemplateData(id)) || [];
|
||||||
|
const popularPromises = nodeData.popular?.map(async (id) => await getTemplateData(id)) || [];
|
||||||
|
|
||||||
|
const [starterResults, popularResults] = await Promise.all([
|
||||||
|
Promise.allSettled(starterPromises),
|
||||||
|
Promise.allSettled(popularPromises),
|
||||||
|
]);
|
||||||
|
|
||||||
|
starterTemplates.value = starterResults
|
||||||
|
.filter(
|
||||||
|
(result): result is PromiseFulfilledResult<ITemplatesWorkflowFull> =>
|
||||||
|
result.status === 'fulfilled' && result.value !== null,
|
||||||
|
)
|
||||||
|
.map((result) => result.value);
|
||||||
|
|
||||||
|
popularTemplates.value = popularResults
|
||||||
|
.filter(
|
||||||
|
(result): result is PromiseFulfilledResult<ITemplatesWorkflowFull> =>
|
||||||
|
result.status === 'fulfilled' && result.value !== null,
|
||||||
|
)
|
||||||
|
.map((result) => result.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading templates:', error);
|
||||||
|
} finally {
|
||||||
|
isLoadingTemplates.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
:name="EXPERIMENT_TEMPLATE_RECO_V2_KEY"
|
||||||
|
min-width="min(800px, 90vw)"
|
||||||
|
max-height="90vh"
|
||||||
|
@close="closeModal"
|
||||||
|
@canceled="closeModal"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div :class="$style.header">
|
||||||
|
<N8nRadioButtons
|
||||||
|
v-model="selectedNode"
|
||||||
|
:options="nodes"
|
||||||
|
size="medium"
|
||||||
|
@update:model-value="onSelectedNodeChange"
|
||||||
|
>
|
||||||
|
<template #option="option">
|
||||||
|
<div :class="$style.tab">
|
||||||
|
<NodeIcon
|
||||||
|
:size="18"
|
||||||
|
:class="$style.nodeIcon"
|
||||||
|
:stroke-width="1.5"
|
||||||
|
:node-type="nodeTypes.get(option.value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</N8nRadioButtons>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div :class="[$style.title, 'mb-m']">
|
||||||
|
<N8nText tag="h2" size="large" :bold="true">{{
|
||||||
|
locale.baseText('workflows.templateRecoV2.starterTemplates')
|
||||||
|
}}</N8nText>
|
||||||
|
<N8nLink :href="starterLink" @click="trackSeeMoreClick('starter')">
|
||||||
|
{{ locale.baseText('workflows.templateRecoV2.seeMoreStarterTemplates') }}
|
||||||
|
</N8nLink>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.suggestions">
|
||||||
|
<div v-if="isLoadingTemplates" :class="$style.loading">
|
||||||
|
<N8nSpinner size="small" />
|
||||||
|
<N8nText size="small">{{
|
||||||
|
locale.baseText('workflows.templateRecoV2.loadingTemplates')
|
||||||
|
}}</N8nText>
|
||||||
|
</div>
|
||||||
|
<TemplateCard
|
||||||
|
v-for="template in starterTemplates"
|
||||||
|
v-else
|
||||||
|
:key="template.id"
|
||||||
|
:template="template"
|
||||||
|
:current-node-name="selectedNode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div :class="[$style.title, 'mb-m mt-m']">
|
||||||
|
<N8nText tag="h2" size="large" :bold="true">{{
|
||||||
|
locale.baseText('workflows.templateRecoV2.popularTemplates')
|
||||||
|
}}</N8nText>
|
||||||
|
<N8nLink :href="popularLink" @click="trackSeeMoreClick('popular')">
|
||||||
|
{{ locale.baseText('workflows.templateRecoV2.seeMorePopularTemplates') }}</N8nLink
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.suggestions">
|
||||||
|
<div v-if="isLoadingTemplates" :class="$style.loading">
|
||||||
|
<N8nSpinner size="small" />
|
||||||
|
<N8nText size="small">{{
|
||||||
|
locale.baseText('workflows.templateRecoV2.loadingTemplates')
|
||||||
|
}}</N8nText>
|
||||||
|
</div>
|
||||||
|
<TemplateCard
|
||||||
|
v-for="template in popularTemplates"
|
||||||
|
v-else
|
||||||
|
:key="template.id"
|
||||||
|
:template="template"
|
||||||
|
:current-node-name="selectedNode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<N8nText tag="h2" size="large" :bold="true" class="mb-m mt-m">{{
|
||||||
|
locale.baseText('workflows.templateRecoV2.tutorials')
|
||||||
|
}}</N8nText>
|
||||||
|
<div :class="$style.videos">
|
||||||
|
<YoutubeCard
|
||||||
|
v-for="video in youtubeVideos"
|
||||||
|
:key="video.id"
|
||||||
|
:video-id="video.id"
|
||||||
|
:title="video.title"
|
||||||
|
:description="video.description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.header {
|
||||||
|
border-bottom: 1px solid var(--border-color-base);
|
||||||
|
padding-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: var(--spacing-3xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
overflow-x: auto;
|
||||||
|
min-height: 182px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videos {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
color: var(--color-text-light);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { type ITemplatesWorkflow } from '@n8n/rest-api-client';
|
||||||
|
import { usePersonalizedTemplatesV2Store } from '../stores/templateRecoV2.store';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { EXPERIMENT_TEMPLATE_RECO_V2_KEY } from '@/constants';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
template: ITemplatesWorkflow;
|
||||||
|
currentNodeName?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
const { getTemplateRoute, trackTemplateTileClick } = usePersonalizedTemplatesV2Store();
|
||||||
|
const router = useRouter();
|
||||||
|
const uiStore = useUIStore();
|
||||||
|
const locale = useI18n();
|
||||||
|
|
||||||
|
// Display the current node name and one other random node from the template
|
||||||
|
const templateNodes = computed(() => {
|
||||||
|
if (!props.template?.nodes) return [];
|
||||||
|
|
||||||
|
const uniqueNodeTypes = new Set(props.template.nodes.map((node) => node.name));
|
||||||
|
const nodeTypesArray = Array.from(uniqueNodeTypes);
|
||||||
|
|
||||||
|
if (props.currentNodeName && uniqueNodeTypes.has(props.currentNodeName)) {
|
||||||
|
const otherNodes = nodeTypesArray.filter((nodeType) => nodeType !== props.currentNodeName);
|
||||||
|
const nodesToShow = [props.currentNodeName];
|
||||||
|
|
||||||
|
if (otherNodes.length > 0) {
|
||||||
|
nodesToShow.push(otherNodes[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodesToShow.map((nodeType) => nodeTypesStore.getNodeType(nodeType)).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeTypesArray
|
||||||
|
.slice(0, 2)
|
||||||
|
.map((nodeType) => nodeTypesStore.getNodeType(nodeType))
|
||||||
|
.filter(Boolean);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleUseTemplate = async () => {
|
||||||
|
trackTemplateTileClick(props.template.id);
|
||||||
|
await router.push(getTemplateRoute(props.template.id));
|
||||||
|
uiStore.closeModal(EXPERIMENT_TEMPLATE_RECO_V2_KEY);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<N8nCard :class="$style.suggestion">
|
||||||
|
<div>
|
||||||
|
<div v-if="templateNodes.length > 0" :class="[$style.nodes, 'mb-s']">
|
||||||
|
<div v-for="nodeType in templateNodes" :key="nodeType!.name" :class="$style.nodeIcon">
|
||||||
|
<NodeIcon :size="18" :stroke-width="1.5" :node-type="nodeType" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<N8nText size="medium" :bold="true">
|
||||||
|
{{ template.name }}
|
||||||
|
</N8nText>
|
||||||
|
</div>
|
||||||
|
<div :class="[$style.actions, 'mt-m']">
|
||||||
|
<N8nButton
|
||||||
|
:label="locale.baseText('workflows.templateRecoV2.useTemplate')"
|
||||||
|
type="secondary"
|
||||||
|
size="mini"
|
||||||
|
@click="handleUseTemplate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</N8nCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.nodes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodeIcon {
|
||||||
|
padding: var(--spacing-2xs);
|
||||||
|
background-color: var(--color-dialog-background);
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: end;
|
||||||
|
margin-right: var(--spacing-3xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 100%;
|
||||||
|
margin-right: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { usePersonalizedTemplatesV2Store } from '../stores/templateRecoV2.store';
|
||||||
|
import NodeRecommendationCard from './NodeRecommendationCard.vue';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
|
||||||
|
const templateRecoV2Store = usePersonalizedTemplatesV2Store();
|
||||||
|
const locale = useI18n();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="templateRecoV2Store.nodes.length"
|
||||||
|
class="text-center mt-3xl"
|
||||||
|
data-test-id="list-empty-state"
|
||||||
|
>
|
||||||
|
<N8nHeading tag="h2" size="medium" class="mb-2xs" color="text-light">
|
||||||
|
{{ locale.baseText('workflows.templateRecoV2.exploreTemplates') }}
|
||||||
|
</N8nHeading>
|
||||||
|
<div :class="$style.nodeCardsContainer">
|
||||||
|
<NodeRecommendationCard
|
||||||
|
v-for="node in templateRecoV2Store.nodes"
|
||||||
|
:key="node"
|
||||||
|
:node-name="node"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.nodeCardsContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { usePersonalizedTemplatesV2Store } from '../stores/templateRecoV2.store';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
videoId: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { trackVideoClick } = usePersonalizedTemplatesV2Store();
|
||||||
|
|
||||||
|
const openYouTubeVideo = () => {
|
||||||
|
trackVideoClick(props.title);
|
||||||
|
window.open(`https://www.youtube.com/watch?v=${props.videoId}`, '_blank');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<N8nCard hoverable @click="openYouTubeVideo">
|
||||||
|
<div :class="$style.tutorial">
|
||||||
|
<img
|
||||||
|
:src="`https://img.youtube.com/vi/${props.videoId}/hq720.jpg`"
|
||||||
|
width="250px"
|
||||||
|
:class="$style.video"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<N8nText tag="h2" size="large" :bold="true">{{ props.title }}</N8nText>
|
||||||
|
<N8nText tag="h3" size="small" class="mt-2xs">{{ props.description }}</N8nText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</N8nCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.tutorial {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video {
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
export interface PredefinedNodeData {
|
||||||
|
starter: number[];
|
||||||
|
popular: number[];
|
||||||
|
youtube: Array<{
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NODE_DATA: Record<string, PredefinedNodeData> = {
|
||||||
|
'@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.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -44,6 +44,7 @@ import {
|
|||||||
LOCAL_STORAGE_THEME,
|
LOCAL_STORAGE_THEME,
|
||||||
WHATS_NEW_MODAL_KEY,
|
WHATS_NEW_MODAL_KEY,
|
||||||
WORKFLOW_DIFF_MODAL_KEY,
|
WORKFLOW_DIFF_MODAL_KEY,
|
||||||
|
EXPERIMENT_TEMPLATE_RECO_V2_KEY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { STORES } from '@n8n/stores';
|
import { STORES } from '@n8n/stores';
|
||||||
import type {
|
import type {
|
||||||
@@ -219,6 +220,12 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||||||
articleId: undefined,
|
articleId: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[EXPERIMENT_TEMPLATE_RECO_V2_KEY]: {
|
||||||
|
open: false,
|
||||||
|
data: {
|
||||||
|
nodeName: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const modalStack = ref<string[]>([]);
|
const modalStack = ref<string[]>([]);
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ 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 TemplateRecommendationV2 from '@/experiments/templateRecoV2/components/TemplateRecommendationV2.vue';
|
||||||
|
import { usePersonalizedTemplatesV2Store } from '@/experiments/templateRecoV2/stores/templateRecoV2.store';
|
||||||
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 {
|
||||||
@@ -120,6 +122,7 @@ const templatesStore = useTemplatesStore();
|
|||||||
const aiStarterTemplatesStore = useAITemplatesStarterCollectionStore();
|
const aiStarterTemplatesStore = useAITemplatesStarterCollectionStore();
|
||||||
const personalizedTemplatesStore = usePersonalizedTemplatesStore();
|
const personalizedTemplatesStore = usePersonalizedTemplatesStore();
|
||||||
const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore();
|
const readyToRunWorkflowsStore = useReadyToRunWorkflowsStore();
|
||||||
|
const personalizedTemplatesV2Store = usePersonalizedTemplatesV2Store();
|
||||||
|
|
||||||
const documentTitle = useDocumentTitle();
|
const documentTitle = useDocumentTitle();
|
||||||
const { callDebounced } = useDebounce();
|
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
|
* LIFE-CYCLE HOOKS
|
||||||
*/
|
*/
|
||||||
@@ -1699,7 +1714,7 @@ const onNameSubmit = async (name: string) => {
|
|||||||
<template #header>
|
<template #header>
|
||||||
<ProjectHeader @create-folder="createFolderInCurrent">
|
<ProjectHeader @create-folder="createFolderInCurrent">
|
||||||
<InsightsSummary
|
<InsightsSummary
|
||||||
v-if="projectPages.isOverviewSubPage && insightsStore.isSummaryEnabled"
|
v-if="showInsights"
|
||||||
:loading="insightsStore.weeklySummary.isLoading"
|
:loading="insightsStore.weeklySummary.isLoading"
|
||||||
:summary="insightsStore.weeklySummary.state"
|
:summary="insightsStore.weeklySummary.state"
|
||||||
time-range="week"
|
time-range="week"
|
||||||
@@ -2089,6 +2104,7 @@ const onNameSubmit = async (name: string) => {
|
|||||||
</div>
|
</div>
|
||||||
</N8nCard>
|
</N8nCard>
|
||||||
</div>
|
</div>
|
||||||
|
<TemplateRecommendationV2 v-if="showTemplateRecommendationV2" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #filters="{ setKeyValue }">
|
<template #filters="{ setKeyValue }">
|
||||||
|
|||||||
Reference in New Issue
Block a user