mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(editor): Add RAG starter template callouts experiment (#16282)
Co-authored-by: Charlie Kolb <charlie@n8n.io>
This commit is contained in:
31
cypress/composables/nodeCreator.ts
Normal file
31
cypress/composables/nodeCreator.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Getters
|
||||||
|
export const nodeCreatorPlusButton = () => cy.getByTestId('node-creator-plus-button');
|
||||||
|
export const canvasAddButton = () => cy.getByTestId('canvas-add-button');
|
||||||
|
export const searchBar = () => cy.getByTestId('search-bar');
|
||||||
|
export const getCategoryItem = (label: string) => cy.get(`[data-keyboard-nav-id="${label}"]`);
|
||||||
|
export const getCreatorItem = (label: string) =>
|
||||||
|
getCreatorItems().contains(label).parents('[data-test-id="item-iterator-item"]');
|
||||||
|
export const getNthCreatorItem = (n: number) => getCreatorItems().eq(n);
|
||||||
|
export const nodeCreator = () => cy.getByTestId('node-creator');
|
||||||
|
export const nodeCreatorTabs = () => cy.getByTestId('node-creator-type-selector');
|
||||||
|
export const selectedTab = () => nodeCreatorTabs().find('.is-active');
|
||||||
|
export const categorizedItems = () => cy.getByTestId('categorized-items');
|
||||||
|
export const getCreatorItems = () => cy.getByTestId('item-iterator-item');
|
||||||
|
export const categoryItem = () => cy.getByTestId('node-creator-category-item');
|
||||||
|
export const communityNodeTooltip = () => cy.getByTestId('node-item-community-tooltip');
|
||||||
|
export const noResults = () => cy.getByTestId('node-creator-no-results');
|
||||||
|
export const nodeItemName = () => cy.getByTestId('node-creator-item-name');
|
||||||
|
export const nodeItemDescription = () => cy.getByTestId('node-creator-item-description');
|
||||||
|
export const activeSubcategory = () => cy.getByTestId('nodes-list-header');
|
||||||
|
export const expandedCategories = () =>
|
||||||
|
getCreatorItems().find('>div').filter('.active').invoke('text');
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
export const openNodeCreator = () => {
|
||||||
|
nodeCreatorPlusButton().click();
|
||||||
|
nodeCreator().should('be.visible');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectNode = (displayName: string) => {
|
||||||
|
getCreatorItem(displayName).click();
|
||||||
|
};
|
||||||
72
cypress/e2e/52-rag-callout-experiment.cy.ts
Normal file
72
cypress/e2e/52-rag-callout-experiment.cy.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { overrideFeatureFlag } from '../composables/featureFlags';
|
||||||
|
import { openNodeCreator, searchBar } from '../composables/nodeCreator';
|
||||||
|
import { addNodeToCanvas, navigateToNewWorkflowPage } from '../composables/workflow';
|
||||||
|
|
||||||
|
describe('RAG callout experiment', () => {
|
||||||
|
describe('NDV callout', () => {
|
||||||
|
it('should not show callout if experiment is control', () => {
|
||||||
|
overrideFeatureFlag('033_rag_template', 'control');
|
||||||
|
|
||||||
|
navigateToNewWorkflowPage();
|
||||||
|
|
||||||
|
addNodeToCanvas('Zep Vector Store', true, true, 'Add documents to vector store');
|
||||||
|
|
||||||
|
cy.contains('Tip: Get a feel for vector stores in n8n with our').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should callout is variant and open on click', () => {
|
||||||
|
cy.intercept('workflows/templates/rag-starter-template?fromJson=true');
|
||||||
|
overrideFeatureFlag('033_rag_template', 'variant');
|
||||||
|
|
||||||
|
navigateToNewWorkflowPage();
|
||||||
|
|
||||||
|
addNodeToCanvas('Zep Vector Store', true, true, 'Add documents to vector store');
|
||||||
|
|
||||||
|
cy.contains('Tip: Get a feel for vector stores in n8n with our').should('exist');
|
||||||
|
|
||||||
|
let openedUrl = '';
|
||||||
|
cy.window().then((win) => {
|
||||||
|
cy.stub(win, 'open').callsFake((url) => {
|
||||||
|
openedUrl = url;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cy.contains('RAG starter template').click();
|
||||||
|
cy.then(() => cy.visit(openedUrl));
|
||||||
|
|
||||||
|
cy.url().should('include', '/workflows/templates/rag-starter-template?fromJson=true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('search callout', () => {
|
||||||
|
it('should not show callout if experiment is control', () => {
|
||||||
|
overrideFeatureFlag('033_rag_template', 'control');
|
||||||
|
|
||||||
|
navigateToNewWorkflowPage();
|
||||||
|
|
||||||
|
openNodeCreator();
|
||||||
|
searchBar().type('rag');
|
||||||
|
|
||||||
|
cy.contains('RAG starter template').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should should callout is variant and open on click', () => {
|
||||||
|
cy.intercept('workflows/templates/rag-starter-template?fromJson=true');
|
||||||
|
overrideFeatureFlag('033_rag_template', 'variant');
|
||||||
|
|
||||||
|
navigateToNewWorkflowPage();
|
||||||
|
|
||||||
|
openNodeCreator();
|
||||||
|
searchBar().type('rag');
|
||||||
|
|
||||||
|
let openedUrl = '';
|
||||||
|
cy.window().then((win) => {
|
||||||
|
cy.stub(win, 'open').callsFake((url) => {
|
||||||
|
openedUrl = url;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cy.contains('RAG starter template').should('exist').click();
|
||||||
|
cy.then(() => cy.visit(openedUrl));
|
||||||
|
|
||||||
|
cy.url().should('include', '/workflows/templates/rag-starter-template?fromJson=true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,4 +5,5 @@ export class SettingsUpdateRequestDto extends Z.class({
|
|||||||
userActivated: z.boolean().optional(),
|
userActivated: z.boolean().optional(),
|
||||||
allowSSOManualLogin: z.boolean().optional(),
|
allowSSOManualLogin: z.boolean().optional(),
|
||||||
easyAIWorkflowOnboarded: z.boolean().optional(),
|
easyAIWorkflowOnboarded: z.boolean().optional(),
|
||||||
|
dismissedCallouts: z.record(z.string(), z.boolean()).optional(),
|
||||||
}) {}
|
}) {}
|
||||||
|
|||||||
@@ -303,8 +303,8 @@ export class AgentV1 implements INodeType {
|
|||||||
{
|
{
|
||||||
displayName:
|
displayName:
|
||||||
'Tip: Get a feel for agents with our quick <a href="https://docs.n8n.io/advanced-ai/intro-tutorial/" target="_blank">tutorial</a> or see an <a href="/templates/1954" target="_blank">example</a> of how this node works',
|
'Tip: Get a feel for agents with our quick <a href="https://docs.n8n.io/advanced-ai/intro-tutorial/" target="_blank">tutorial</a> or see an <a href="/templates/1954" target="_blank">example</a> of how this node works',
|
||||||
name: 'notice_tip',
|
name: 'aiAgentStarterCallout',
|
||||||
type: 'notice',
|
type: 'callout',
|
||||||
default: '',
|
default: '',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
|
|||||||
@@ -119,9 +119,9 @@ export class AgentV2 implements INodeType {
|
|||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
displayName:
|
displayName:
|
||||||
'Tip: Get a feel for agents with our quick <a href="https://docs.n8n.io/advanced-ai/intro-tutorial/" target="_blank">tutorial</a> or see an <a href="/templates/1954" target="_blank">example</a> of how this node works',
|
'Tip: Get a feel for agents with our quick <a href="https://docs.n8n.io/advanced-ai/intro-tutorial/" target="_blank">tutorial</a> or see an <a href="/workflows/templates/1954" target="_blank">example</a> of how this node works',
|
||||||
name: 'notice_tip',
|
name: 'aiAgentStarterCallout',
|
||||||
type: 'notice',
|
type: 'callout',
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
promptTypeOptions,
|
promptTypeOptions,
|
||||||
|
|||||||
@@ -78,6 +78,18 @@ exports[`createVectorStoreNode retrieve mode supplies vector store as data 1`] =
|
|||||||
})($parameter)
|
})($parameter)
|
||||||
}}",
|
}}",
|
||||||
"properties": [
|
"properties": [
|
||||||
|
{
|
||||||
|
"default": "",
|
||||||
|
"displayName": "Tip: Get a feel for vector stores in n8n with our",
|
||||||
|
"name": "ragStarterCallout",
|
||||||
|
"type": "callout",
|
||||||
|
"typeOptions": {
|
||||||
|
"calloutAction": {
|
||||||
|
"label": "RAG starter template",
|
||||||
|
"type": "openRagStarterTemplate",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"default": "retrieve",
|
"default": "retrieve",
|
||||||
"displayName": "Operation Mode",
|
"displayName": "Operation Mode",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
SupplyData,
|
SupplyData,
|
||||||
ISupplyDataFunctions,
|
ISupplyDataFunctions,
|
||||||
INodeType,
|
INodeType,
|
||||||
|
INodeProperties,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { getConnectionHintNoticeField } from '@utils/sharedFields';
|
import { getConnectionHintNoticeField } from '@utils/sharedFields';
|
||||||
@@ -26,7 +27,18 @@ import type { NodeOperationMode, VectorStoreNodeConstructorArgs } from './types'
|
|||||||
// Import utility functions
|
// Import utility functions
|
||||||
import { transformDescriptionForOperationMode, getOperationModeOptions } from './utils';
|
import { transformDescriptionForOperationMode, getOperationModeOptions } from './utils';
|
||||||
|
|
||||||
// Import operation handlers
|
const ragStarterCallout: INodeProperties = {
|
||||||
|
displayName: 'Tip: Get a feel for vector stores in n8n with our',
|
||||||
|
name: 'ragStarterCallout',
|
||||||
|
type: 'callout',
|
||||||
|
typeOptions: {
|
||||||
|
calloutAction: {
|
||||||
|
label: 'RAG starter template',
|
||||||
|
type: 'openRagStarterTemplate',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a vector store node with the given configuration
|
* Creates a vector store node with the given configuration
|
||||||
@@ -105,6 +117,7 @@ export const createVectorStoreNode = <T extends VectorStore = VectorStore>(
|
|||||||
})($parameter)
|
})($parameter)
|
||||||
}}`,
|
}}`,
|
||||||
properties: [
|
properties: [
|
||||||
|
ragStarterCallout,
|
||||||
{
|
{
|
||||||
displayName: 'Operation Mode',
|
displayName: 'Operation Mode',
|
||||||
name: 'mode',
|
name: 'mode',
|
||||||
|
|||||||
@@ -82,15 +82,26 @@ const getIconSize = computed<IconSize>(() => {
|
|||||||
padding: var(--spacing-xs);
|
padding: var(--spacing-xs);
|
||||||
border: var(--border-width-base) var(--border-style-base);
|
border: var(--border-width-base) var(--border-style-base);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: var(--font-line-height-loose);
|
line-height: var(--font-line-height-xloose);
|
||||||
border-color: var(--color-callout-info-border);
|
border-color: var(--color-callout-info-border);
|
||||||
background-color: var(--color-callout-info-background);
|
background-color: var(--color-callout-info-background);
|
||||||
color: var(--color-callout-info-font);
|
color: var(--color-callout-info-font);
|
||||||
|
|
||||||
&.slim {
|
&.slim {
|
||||||
line-height: var(--font-line-height-loose);
|
line-height: var(--font-line-height-xloose);
|
||||||
padding: var(--spacing-3xs) var(--spacing-2xs);
|
padding: var(--spacing-3xs) var(--spacing-2xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-secondary-link);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
text-decoration-line: underline;
|
||||||
|
text-decoration-style: solid;
|
||||||
|
text-decoration-skip-ink: none;
|
||||||
|
text-decoration-thickness: auto;
|
||||||
|
text-underline-offset: auto;
|
||||||
|
text-underline-position: from-font;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.round {
|
.round {
|
||||||
@@ -151,7 +162,7 @@ const getIconSize = computed<IconSize>(() => {
|
|||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
margin-right: var(--spacing-2xs);
|
margin-right: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary {
|
.secondary {
|
||||||
|
|||||||
@@ -1253,6 +1253,8 @@
|
|||||||
"nodeCreator.noResults.wantUsToMakeItFaster": "Want us to make it faster?",
|
"nodeCreator.noResults.wantUsToMakeItFaster": "Want us to make it faster?",
|
||||||
"nodeCreator.noResults.weDidntMakeThatYet": "We didn't make that... yet",
|
"nodeCreator.noResults.weDidntMakeThatYet": "We didn't make that... yet",
|
||||||
"nodeCreator.noResults.webhook": "Webhook",
|
"nodeCreator.noResults.webhook": "Webhook",
|
||||||
|
"nodeCreator.ragStarterTemplate.openTemplateItem.title": "RAG starter template",
|
||||||
|
"nodeCreator.ragStarterTemplate.openTemplateItem.description": "Get a feel for vector stores in n8n",
|
||||||
"nodeCreator.searchBar.searchNodes": "Search nodes...",
|
"nodeCreator.searchBar.searchNodes": "Search nodes...",
|
||||||
"nodeCreator.subcategoryDescriptions.appTriggerNodes": "Runs the flow when something happens in an app like Telegram, Notion or Airtable",
|
"nodeCreator.subcategoryDescriptions.appTriggerNodes": "Runs the flow when something happens in an app like Telegram, Notion or Airtable",
|
||||||
"nodeCreator.subcategoryDescriptions.appRegularNodes": "Do something in an app or service like Google Sheets, Telegram or Notion",
|
"nodeCreator.subcategoryDescriptions.appRegularNodes": "Do something in an app or service like Google Sheets, Telegram or Notion",
|
||||||
@@ -1620,6 +1622,9 @@
|
|||||||
"parameterInputList.parameterOptions": "Parameter Options",
|
"parameterInputList.parameterOptions": "Parameter Options",
|
||||||
"parameterInputList.loadingFields": "Loading fields...",
|
"parameterInputList.loadingFields": "Loading fields...",
|
||||||
"parameterInputList.loadingError": "Error loading fields. Refresh you page and try again.",
|
"parameterInputList.loadingError": "Error loading fields. Refresh you page and try again.",
|
||||||
|
"parameterInputList.callout.dismiss.confirm.text": "Do you want to permanently hide this?",
|
||||||
|
"parameterInputList.callout.dismiss.confirm.confirmButtonText": "Confirm",
|
||||||
|
"parameterInputList.callout.dismiss.confirm.cancelButtonText": "Cancel",
|
||||||
"parameterOverride.overridePanelText": "Defined automatically by the <b>model</b>",
|
"parameterOverride.overridePanelText": "Defined automatically by the <b>model</b>",
|
||||||
"parameterOverride.applyOverrideButtonTooltip": "Let the model define this parameter",
|
"parameterOverride.applyOverrideButtonTooltip": "Let the model define this parameter",
|
||||||
"parameterOverride.descriptionTooltip": "Explain to the LLM how it should generate this value, a good, specific description would allow LLMs to produce expected results much more often",
|
"parameterOverride.descriptionTooltip": "Explain to the LLM how it should generate this value, a good, specific description would allow LLMs to produce expected results much more often",
|
||||||
|
|||||||
@@ -797,6 +797,15 @@ export interface LinkItemProps {
|
|||||||
icon: string;
|
icon: string;
|
||||||
tag?: NodeCreatorTag;
|
tag?: NodeCreatorTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OpenTemplateItemProps {
|
||||||
|
key: 'rag-starter-template';
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
tag?: NodeCreatorTag;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ActionTypeDescription extends SimplifiedNodeType {
|
export interface ActionTypeDescription extends SimplifiedNodeType {
|
||||||
displayOptions?: IDisplayOptions;
|
displayOptions?: IDisplayOptions;
|
||||||
values?: IDataObject;
|
values?: IDataObject;
|
||||||
@@ -859,6 +868,11 @@ export interface LinkCreateElement extends CreateElementBase {
|
|||||||
properties: LinkItemProps;
|
properties: LinkItemProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OpenTemplateElement extends CreateElementBase {
|
||||||
|
type: 'openTemplate';
|
||||||
|
properties: OpenTemplateItemProps;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ActionCreateElement extends CreateElementBase {
|
export interface ActionCreateElement extends CreateElementBase {
|
||||||
type: 'action';
|
type: 'action';
|
||||||
subcategory: string;
|
subcategory: string;
|
||||||
@@ -873,7 +887,8 @@ export type INodeCreateElement =
|
|||||||
| ViewCreateElement
|
| ViewCreateElement
|
||||||
| LabelCreateElement
|
| LabelCreateElement
|
||||||
| ActionCreateElement
|
| ActionCreateElement
|
||||||
| LinkCreateElement;
|
| LinkCreateElement
|
||||||
|
| OpenTemplateElement;
|
||||||
|
|
||||||
export type NodeTypeSelectedPayload = {
|
export type NodeTypeSelectedPayload = {
|
||||||
type: string;
|
type: string;
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { OpenTemplateItemProps } from '@/Interface';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
openTemplate: OpenTemplateItemProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n8n-node-creator-node
|
||||||
|
:class="$style.creatorOpenTemplate"
|
||||||
|
:title="openTemplate.title"
|
||||||
|
:is-trigger="false"
|
||||||
|
:description="openTemplate.description"
|
||||||
|
:tag="openTemplate.tag"
|
||||||
|
:show-action-arrow="true"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n8n-node-icon type="icon" :name="openTemplate.icon" :circle="false" :show-tooltip="false" />
|
||||||
|
</template>
|
||||||
|
</n8n-node-creator-node>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.creatorOpenTemplate {
|
||||||
|
--action-arrow-color: var(--color-text-light);
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
filterAndSearchNodes,
|
filterAndSearchNodes,
|
||||||
prepareCommunityNodeDetailsViewStack,
|
prepareCommunityNodeDetailsViewStack,
|
||||||
transformNodeType,
|
transformNodeType,
|
||||||
|
getRootSearchCallouts,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { useViewStacks } from '../composables/useViewStacks';
|
import { useViewStacks } from '../composables/useViewStacks';
|
||||||
import { useKeyboardNavigation } from '../composables/useKeyboardNavigation';
|
import { useKeyboardNavigation } from '../composables/useKeyboardNavigation';
|
||||||
@@ -42,6 +43,7 @@ import { SEND_AND_WAIT_OPERATION, type INodeParameters } from 'n8n-workflow';
|
|||||||
|
|
||||||
import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
|
import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { useCalloutHelpers } from '@/composables/useCalloutHelpers';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
rootView: 'trigger' | 'action';
|
rootView: 'trigger' | 'action';
|
||||||
@@ -53,6 +55,8 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const calloutHelpers = useCalloutHelpers();
|
||||||
|
|
||||||
const { mergedNodes, actions, onSubcategorySelected } = useNodeCreatorStore();
|
const { mergedNodes, actions, onSubcategorySelected } = useNodeCreatorStore();
|
||||||
const { pushViewStack, popViewStack, isAiSubcategoryView } = useViewStacks();
|
const { pushViewStack, popViewStack, isAiSubcategoryView } = useViewStacks();
|
||||||
const { setAddedNodeActionParameters, nodeCreateElementToNodeTypeSelectedPayload } = useActions();
|
const { setAddedNodeActionParameters, nodeCreateElementToNodeTypeSelectedPayload } = useActions();
|
||||||
@@ -76,7 +80,10 @@ const moreFromCommunity = computed(() => {
|
|||||||
const isSearchResultEmpty = computed(() => {
|
const isSearchResultEmpty = computed(() => {
|
||||||
return (
|
return (
|
||||||
(activeViewStack.value.items || []).length === 0 &&
|
(activeViewStack.value.items || []).length === 0 &&
|
||||||
globalSearchItemsDiff.value.length + moreFromCommunity.value.length === 0
|
globalCallouts.value.length +
|
||||||
|
globalSearchItemsDiff.value.length +
|
||||||
|
moreFromCommunity.value.length ===
|
||||||
|
0
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -216,6 +223,12 @@ function onSelected(item: INodeCreateElement) {
|
|||||||
if (item.type === 'link') {
|
if (item.type === 'link') {
|
||||||
window.open(item.properties.url, '_blank');
|
window.open(item.properties.url, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.type === 'openTemplate') {
|
||||||
|
if (item.properties.key === 'rag-starter-template') {
|
||||||
|
void calloutHelpers.openRagStarterTemplate();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function subcategoriesMapper(item: INodeCreateElement) {
|
function subcategoriesMapper(item: INodeCreateElement) {
|
||||||
@@ -254,6 +267,12 @@ function baseSubcategoriesFilter(item: INodeCreateElement): boolean {
|
|||||||
return hasActions || !hasTriggerGroup;
|
return hasActions || !hasTriggerGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const globalCallouts = computed<INodeCreateElement[]>(() =>
|
||||||
|
getRootSearchCallouts(activeViewStack.value.search ?? '', {
|
||||||
|
isRagStarterCalloutVisible: calloutHelpers.isRagStarterCalloutVisible.value,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
function arrowLeft() {
|
function arrowLeft() {
|
||||||
popViewStack();
|
popViewStack();
|
||||||
}
|
}
|
||||||
@@ -286,6 +305,9 @@ registerKeyHook('MainViewArrowLeft', {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
|
<!-- Global Callouts-->
|
||||||
|
<ItemsRenderer :elements="globalCallouts" :class="$style.items" @selected="onSelected" />
|
||||||
|
|
||||||
<!-- Main Node Items -->
|
<!-- Main Node Items -->
|
||||||
<ItemsRenderer
|
<ItemsRenderer
|
||||||
v-memo="[activeViewStack.search]"
|
v-memo="[activeViewStack.search]"
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ import { REGULAR_NODE_CREATOR_VIEW } from '@/constants';
|
|||||||
import type { NodeFilterType } from '@/Interface';
|
import type { NodeFilterType } from '@/Interface';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
|
||||||
|
vi.mock('vue-router', () => ({
|
||||||
|
useRoute: vi.fn(() => ({ query: {}, params: {} })),
|
||||||
|
useRouter: vi.fn(),
|
||||||
|
RouterLink: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
function getWrapperComponent(setup: () => void) {
|
function getWrapperComponent(setup: () => void) {
|
||||||
const wrapperComponent = defineComponent({
|
const wrapperComponent = defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import CommunityNodeItem from '../ItemTypes/CommunityNodeItem.vue';
|
|||||||
import CategorizedItemsRenderer from './CategorizedItemsRenderer.vue';
|
import CategorizedItemsRenderer from './CategorizedItemsRenderer.vue';
|
||||||
|
|
||||||
import { useViewStacks } from '../composables/useViewStacks';
|
import { useViewStacks } from '../composables/useViewStacks';
|
||||||
|
import OpenTemplateItem from '../ItemTypes/OpenTemplateItem.vue';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
elements?: INodeCreateElement[];
|
elements?: INodeCreateElement[];
|
||||||
@@ -206,6 +207,12 @@ watch(
|
|||||||
:link="item.properties"
|
:link="item.properties"
|
||||||
:class="$style.linkItem"
|
:class="$style.linkItem"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<OpenTemplateItem
|
||||||
|
v-else-if="item.type === 'openTemplate'"
|
||||||
|
:open-template="item.properties"
|
||||||
|
:class="$style.linkItem"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<n8n-loading v-else :loading="true" :rows="1" variant="p" :class="$style.itemSkeleton" />
|
<n8n-loading v-else :loading="true" :rows="1" variant="p" :class="$style.itemSkeleton" />
|
||||||
|
|||||||
@@ -305,3 +305,26 @@ export function prepareCommunityNodeDetailsViewStack(
|
|||||||
communityNodeDetails,
|
communityNodeDetails,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRootSearchCallouts(search: string, { isRagStarterCalloutVisible = false }) {
|
||||||
|
const results: INodeCreateElement[] = [];
|
||||||
|
|
||||||
|
const ragKeywords = ['rag', 'vec', 'know'];
|
||||||
|
if (isRagStarterCalloutVisible && ragKeywords.some((x) => search.toLowerCase().startsWith(x))) {
|
||||||
|
results.push({
|
||||||
|
key: 'rag-starter-template',
|
||||||
|
type: 'openTemplate',
|
||||||
|
properties: {
|
||||||
|
key: 'rag-starter-template',
|
||||||
|
title: i18n.baseText('nodeCreator.ragStarterTemplate.openTemplateItem.title'),
|
||||||
|
icon: 'database',
|
||||||
|
description: i18n.baseText('nodeCreator.ragStarterTemplate.openTemplateItem.description'),
|
||||||
|
tag: {
|
||||||
|
type: 'info',
|
||||||
|
text: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerTag'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,32 @@ export const TEST_PARAMETERS: INodeProperties[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName:
|
||||||
|
'Note: This is a notice with <a href="notice.n8n.io" target="_blank">notice link</a>',
|
||||||
|
name: 'testNotice',
|
||||||
|
type: 'notice',
|
||||||
|
default: '',
|
||||||
|
displayOptions: {
|
||||||
|
show: { '@version': [{ _cnd: { gte: 1.1 } }] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName:
|
||||||
|
'Tip: This is a callout with <a href="callout.n8n.io" target="_blank">callout link</a>',
|
||||||
|
name: 'testCallout',
|
||||||
|
type: 'callout',
|
||||||
|
default: '',
|
||||||
|
displayOptions: {
|
||||||
|
show: { '@version': [{ _cnd: { gte: 1.1 } }] },
|
||||||
|
},
|
||||||
|
typeOptions: {
|
||||||
|
calloutAction: {
|
||||||
|
label: 'and action!',
|
||||||
|
type: 'openRagStarterTemplate',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const FIXED_COLLECTION_PARAMETERS: INodeProperties[] = TEST_PARAMETERS.filter(
|
export const FIXED_COLLECTION_PARAMETERS: INodeProperties[] = TEST_PARAMETERS.filter(
|
||||||
|
|||||||
@@ -83,9 +83,10 @@ describe('ParameterInputList', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Should render labels for all parameters
|
// Should render labels for all parameters
|
||||||
TEST_PARAMETERS.forEach((parameter) => {
|
FIXED_COLLECTION_PARAMETERS.forEach((parameter) => {
|
||||||
expect(getByText(parameter.displayName)).toBeInTheDocument();
|
expect(getByText(parameter.displayName)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should render input placeholders for all fixed collection parameters
|
// Should render input placeholders for all fixed collection parameters
|
||||||
expect(getAllByTestId('suspense-stub')).toHaveLength(FIXED_COLLECTION_PARAMETERS.length);
|
expect(getAllByTestId('suspense-stub')).toHaveLength(FIXED_COLLECTION_PARAMETERS.length);
|
||||||
});
|
});
|
||||||
@@ -100,7 +101,7 @@ describe('ParameterInputList', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Should render labels for all parameters
|
// Should render labels for all parameters
|
||||||
TEST_PARAMETERS.forEach((parameter) => {
|
FIXED_COLLECTION_PARAMETERS.forEach((parameter) => {
|
||||||
expect(getByText(parameter.displayName)).toBeInTheDocument();
|
expect(getByText(parameter.displayName)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
// Should render error message for fixed collection parameter
|
// Should render error message for fixed collection parameter
|
||||||
@@ -110,6 +111,35 @@ describe('ParameterInputList', () => {
|
|||||||
expect(getByText(TEST_ISSUE)).toBeInTheDocument();
|
expect(getByText(TEST_ISSUE)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders notice correctly', () => {
|
||||||
|
ndvStore.activeNode = TEST_NODE_NO_ISSUES;
|
||||||
|
const { getByText } = renderComponent({
|
||||||
|
props: {
|
||||||
|
parameters: TEST_PARAMETERS,
|
||||||
|
nodeValues: TEST_NODE_VALUES,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getByText('Note: This is a notice with')).toBeInTheDocument();
|
||||||
|
expect(getByText('notice link')).toBeInTheDocument();
|
||||||
|
expect(getByText('notice link').getAttribute('href')).toEqual('notice.n8n.io');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders callout correctly', () => {
|
||||||
|
ndvStore.activeNode = TEST_NODE_NO_ISSUES;
|
||||||
|
const { getByTestId, getByText } = renderComponent({
|
||||||
|
props: {
|
||||||
|
parameters: TEST_PARAMETERS,
|
||||||
|
nodeValues: TEST_NODE_VALUES,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByText('Tip: This is a callout with')).toBeInTheDocument();
|
||||||
|
expect(getByText('callout link')).toBeInTheDocument();
|
||||||
|
expect(getByText('callout link').getAttribute('href')).toEqual('callout.n8n.io');
|
||||||
|
expect(getByText('and action!')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('callout-dismiss-icon')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
describe('updateFormParameters', () => {
|
describe('updateFormParameters', () => {
|
||||||
const workflowHelpersMock: MockInstance = vi.spyOn(workflowHelpers, 'useWorkflowHelpers');
|
const workflowHelpersMock: MockInstance = vi.spyOn(workflowHelpers, 'useWorkflowHelpers');
|
||||||
const formParameters = [
|
const formParameters = [
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {
|
import type {
|
||||||
|
CalloutActionType,
|
||||||
INodeParameters,
|
INodeParameters,
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
NodeParameterValue,
|
NodeParameterValue,
|
||||||
@@ -20,10 +21,12 @@ import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
|
|||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
|
import { useMessage } from '@/composables/useMessage';
|
||||||
import {
|
import {
|
||||||
FORM_NODE_TYPE,
|
FORM_NODE_TYPE,
|
||||||
FORM_TRIGGER_NODE_TYPE,
|
FORM_TRIGGER_NODE_TYPE,
|
||||||
KEEP_AUTH_IN_NDV_FOR_NODES,
|
KEEP_AUTH_IN_NDV_FOR_NODES,
|
||||||
|
MODAL_CONFIRM,
|
||||||
WAIT_NODE_TYPE,
|
WAIT_NODE_TYPE,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
@@ -38,8 +41,17 @@ import { captureException } from '@sentry/vue';
|
|||||||
import { computedWithControl } from '@vueuse/core';
|
import { computedWithControl } from '@vueuse/core';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import set from 'lodash/set';
|
import set from 'lodash/set';
|
||||||
import { N8nIcon, N8nIconButton, N8nInputLabel, N8nNotice, N8nText } from '@n8n/design-system';
|
import {
|
||||||
|
N8nCallout,
|
||||||
|
N8nIcon,
|
||||||
|
N8nIconButton,
|
||||||
|
N8nInputLabel,
|
||||||
|
N8nLink,
|
||||||
|
N8nNotice,
|
||||||
|
N8nText,
|
||||||
|
} from '@n8n/design-system';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useCalloutHelpers } from '@/composables/useCalloutHelpers';
|
||||||
|
|
||||||
const LazyFixedCollectionParameter = defineAsyncComponent(
|
const LazyFixedCollectionParameter = defineAsyncComponent(
|
||||||
async () => await import('./FixedCollectionParameter.vue'),
|
async () => await import('./FixedCollectionParameter.vue'),
|
||||||
@@ -72,10 +84,13 @@ const emit = defineEmits<{
|
|||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
|
|
||||||
|
const message = useMessage();
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
const asyncLoadingError = ref(false);
|
const asyncLoadingError = ref(false);
|
||||||
const workflowHelpers = useWorkflowHelpers();
|
const workflowHelpers = useWorkflowHelpers();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const { dismissCallout, isCalloutDismissed, openRagStarterTemplate, isRagStarterCalloutVisible } =
|
||||||
|
useCalloutHelpers();
|
||||||
|
|
||||||
const { activeNode } = storeToRefs(ndvStore);
|
const { activeNode } = storeToRefs(ndvStore);
|
||||||
|
|
||||||
@@ -525,6 +540,47 @@ function getParameterValue<T extends NodeParameterValueType = NodeParameterValue
|
|||||||
): T {
|
): T {
|
||||||
return nodeHelpers.getParameterValue(props.nodeValues, name, props.path) as T;
|
return nodeHelpers.getParameterValue(props.nodeValues, name, props.path) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRagStarterCallout(parameter: INodeProperties): boolean {
|
||||||
|
return parameter.type === 'callout' && parameter.name === 'ragStarterCallout';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCalloutVisible(parameter: INodeProperties): boolean {
|
||||||
|
if (isCalloutDismissed(parameter.name)) return false;
|
||||||
|
|
||||||
|
if (isRagStarterCallout(parameter)) {
|
||||||
|
return isRagStarterCalloutVisible.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onCalloutAction(action: CalloutActionType) {
|
||||||
|
if (action === 'openRagStarterTemplate') {
|
||||||
|
await openRagStarterTemplate(activeNode.value?.type ?? 'no active node');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCalloutDismiss = async (parameter: INodeProperties) => {
|
||||||
|
const dismissConfirmed = await message.confirm(
|
||||||
|
i18n.baseText('parameterInputList.callout.dismiss.confirm.text'),
|
||||||
|
{
|
||||||
|
showClose: true,
|
||||||
|
confirmButtonText: i18n.baseText(
|
||||||
|
'parameterInputList.callout.dismiss.confirm.confirmButtonText',
|
||||||
|
),
|
||||||
|
cancelButtonText: i18n.baseText(
|
||||||
|
'parameterInputList.callout.dismiss.confirm.cancelButtonText',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dismissConfirmed !== MODAL_CONFIRM) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await dismissCallout(parameter.name);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -564,6 +620,46 @@ function getParameterValue<T extends NodeParameterValueType = NodeParameterValue
|
|||||||
@action="onNoticeAction"
|
@action="onNoticeAction"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<template v-else-if="parameter.type === 'callout'">
|
||||||
|
<N8nCallout
|
||||||
|
v-if="isCalloutVisible(parameter)"
|
||||||
|
:class="['parameter-item', parameter.typeOptions?.containerClass ?? '']"
|
||||||
|
theme="secondary"
|
||||||
|
>
|
||||||
|
<N8nText size="small">
|
||||||
|
<N8nText
|
||||||
|
size="small"
|
||||||
|
v-n8n-html="i18n.nodeText(activeNode?.type).inputLabelDisplayName(parameter, path)"
|
||||||
|
/>
|
||||||
|
<template v-if="parameter.typeOptions?.calloutAction">
|
||||||
|
{{ ' ' }}
|
||||||
|
<N8nLink
|
||||||
|
v-if="parameter.typeOptions?.calloutAction"
|
||||||
|
theme="secondary"
|
||||||
|
size="small"
|
||||||
|
:bold="true"
|
||||||
|
:underline="true"
|
||||||
|
@click="onCalloutAction(parameter.typeOptions.calloutAction.type)"
|
||||||
|
>
|
||||||
|
{{ parameter.typeOptions.calloutAction.label }}
|
||||||
|
</N8nLink>
|
||||||
|
</template>
|
||||||
|
</N8nText>
|
||||||
|
|
||||||
|
<template #trailingContent>
|
||||||
|
<N8nIcon
|
||||||
|
icon="times"
|
||||||
|
title="Dismiss"
|
||||||
|
size="medium"
|
||||||
|
type="secondary"
|
||||||
|
class="callout-dismiss"
|
||||||
|
data-test-id="callout-dismiss-icon"
|
||||||
|
@click="onCalloutDismiss(parameter)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</N8nCallout>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div v-else-if="parameter.type === 'button'" class="parameter-item">
|
<div v-else-if="parameter.type === 'button'" class="parameter-item">
|
||||||
<ButtonParameter
|
<ButtonParameter
|
||||||
:parameter="parameter"
|
:parameter="parameter"
|
||||||
@@ -766,5 +862,14 @@ function getParameterValue<T extends NodeParameterValueType = NodeParameterValue
|
|||||||
display: block;
|
display: block;
|
||||||
padding: var(--spacing-3xs) 0;
|
padding: var(--spacing-3xs) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.callout-dismiss {
|
||||||
|
margin-left: var(--spacing-xs);
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.callout-dismiss:hover {
|
||||||
|
color: var(--color-icon-hover);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { useCalloutHelpers } from '@/composables/useCalloutHelpers';
|
||||||
|
import { updateCurrentUserSettings } from '@/api/users';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
resolve: vi.fn(),
|
||||||
|
track: vi.fn(),
|
||||||
|
useRoute: vi.fn(() => ({ query: {}, params: {} })),
|
||||||
|
getVariant: vi.fn(() => 'default'),
|
||||||
|
isCalloutDismissed: vi.fn(() => false),
|
||||||
|
setCalloutDismissed: vi.fn(),
|
||||||
|
restApiContext: vi.fn(() => ({})),
|
||||||
|
getWorkflowById: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('vue-router', () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
resolve: mocks.resolve,
|
||||||
|
}),
|
||||||
|
useRoute: mocks.useRoute,
|
||||||
|
RouterLink: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/composables/useTelemetry', () => ({
|
||||||
|
useTelemetry: () => ({ track: mocks.track }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/stores/posthog.store', () => ({
|
||||||
|
usePostHog: () => ({
|
||||||
|
getVariant: mocks.getVariant,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/stores/users.store', () => ({
|
||||||
|
useUsersStore: () => ({
|
||||||
|
isCalloutDismissed: mocks.isCalloutDismissed,
|
||||||
|
setCalloutDismissed: mocks.setCalloutDismissed,
|
||||||
|
currentUser: { settings: { dismissedCallouts: {} } },
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/stores/workflows.store', () => ({
|
||||||
|
useWorkflowsStore: () => ({
|
||||||
|
getCurrentWorkflow: vi.fn(() => ({
|
||||||
|
id: '1',
|
||||||
|
})),
|
||||||
|
getWorkflowById: mocks.getWorkflowById,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@n8n/stores/useRootStore', () => ({
|
||||||
|
useRootStore: () => ({
|
||||||
|
restApiContext: mocks.restApiContext,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/api/users', () => ({
|
||||||
|
updateCurrentUserSettings: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useCalloutHelpers()', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('openRagStarterTemplate()', () => {
|
||||||
|
it('opens the RAG starter template successfully', async () => {
|
||||||
|
vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||||
|
mocks.resolve.mockReturnValue({ href: 'n8n.io' });
|
||||||
|
|
||||||
|
const { openRagStarterTemplate } = useCalloutHelpers();
|
||||||
|
const nodeType = 'testNode';
|
||||||
|
|
||||||
|
await openRagStarterTemplate('testNode');
|
||||||
|
|
||||||
|
expect(window.open).toHaveBeenCalledWith('n8n.io', '_blank');
|
||||||
|
expect(mocks.track).toHaveBeenCalledWith('User clicked on RAG callout', {
|
||||||
|
node_type: nodeType,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isRagStarterWorkflowExperimentEnabled', () => {
|
||||||
|
it('should be false if the RAG starter workflow experiment is not enabled', () => {
|
||||||
|
const { isRagStarterWorkflowExperimentEnabled } = useCalloutHelpers();
|
||||||
|
expect(isRagStarterWorkflowExperimentEnabled.value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be true if the RAG starter workflow experiment is enabled', () => {
|
||||||
|
mocks.getVariant.mockReturnValueOnce('variant');
|
||||||
|
|
||||||
|
const { isRagStarterWorkflowExperimentEnabled } = useCalloutHelpers();
|
||||||
|
expect(isRagStarterWorkflowExperimentEnabled.value).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isRagStarterCalloutVisible', () => {
|
||||||
|
it('should be false if the feature flag is disabled', () => {
|
||||||
|
const { isRagStarterCalloutVisible } = useCalloutHelpers();
|
||||||
|
expect(isRagStarterCalloutVisible.value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be true if the feature flag is enabled and not on the RAG starter template', () => {
|
||||||
|
mocks.getVariant.mockReturnValueOnce('variant');
|
||||||
|
|
||||||
|
const { isRagStarterCalloutVisible } = useCalloutHelpers();
|
||||||
|
expect(isRagStarterCalloutVisible.value).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be false if the feature flag is enabled and currently on unsaved RAG starter template', () => {
|
||||||
|
mocks.getVariant.mockReturnValueOnce('variant');
|
||||||
|
mocks.useRoute.mockReturnValueOnce({
|
||||||
|
query: { templateId: 'rag-starter-template' },
|
||||||
|
params: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isRagStarterCalloutVisible } = useCalloutHelpers();
|
||||||
|
expect(isRagStarterCalloutVisible.value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be false if the feature flag is enabled and currently on saved RAG starter template', () => {
|
||||||
|
mocks.getVariant.mockReturnValueOnce('variant');
|
||||||
|
mocks.getWorkflowById.mockReturnValueOnce({
|
||||||
|
meta: { templateId: 'rag-starter-template' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isRagStarterCalloutVisible } = useCalloutHelpers();
|
||||||
|
expect(isRagStarterCalloutVisible.value).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isCalloutDismissed()', () => {
|
||||||
|
it('should return false if callout is not dismissed', async () => {
|
||||||
|
const { isCalloutDismissed } = useCalloutHelpers();
|
||||||
|
const result = isCalloutDismissed('testNode');
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if callout is dismissed', async () => {
|
||||||
|
mocks.isCalloutDismissed.mockReturnValueOnce(true);
|
||||||
|
|
||||||
|
const { isCalloutDismissed } = useCalloutHelpers();
|
||||||
|
const result = isCalloutDismissed('testNode');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dismissCallout()', () => {
|
||||||
|
it('should dismiss the callout and update user settings', async () => {
|
||||||
|
const { dismissCallout } = useCalloutHelpers();
|
||||||
|
|
||||||
|
await dismissCallout('testCallout');
|
||||||
|
expect(mocks.setCalloutDismissed).toHaveBeenCalledWith('testCallout');
|
||||||
|
|
||||||
|
expect(updateCurrentUserSettings).toHaveBeenCalledWith(mocks.restApiContext, {
|
||||||
|
dismissedCallouts: {
|
||||||
|
testCallout: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||||
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import { RAG_STARTER_WORKFLOW_EXPERIMENT, VIEWS } from '@/constants';
|
||||||
|
import { getRagStarterWorkflowJson } from '@/utils/easyAiWorkflowUtils';
|
||||||
|
import { updateCurrentUserSettings } from '@/api/users';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
|
||||||
|
export function useCalloutHelpers() {
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
const posthogStore = usePostHog();
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
|
||||||
|
const openRagStarterTemplate = async (nodeType?: string) => {
|
||||||
|
telemetry.track('User clicked on RAG callout', {
|
||||||
|
node_type: nodeType ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const template = getRagStarterWorkflowJson();
|
||||||
|
|
||||||
|
const { href } = router.resolve({
|
||||||
|
name: VIEWS.TEMPLATE_IMPORT,
|
||||||
|
params: { id: template.meta.templateId },
|
||||||
|
query: { fromJson: 'true', parentFolderId: route.params.folderId },
|
||||||
|
});
|
||||||
|
|
||||||
|
window.open(href, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
const isRagStarterWorkflowExperimentEnabled = computed(() => {
|
||||||
|
return (
|
||||||
|
posthogStore.getVariant(RAG_STARTER_WORKFLOW_EXPERIMENT.name) ===
|
||||||
|
RAG_STARTER_WORKFLOW_EXPERIMENT.variant
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isRagStarterCalloutVisible = computed(() => {
|
||||||
|
const template = getRagStarterWorkflowJson();
|
||||||
|
|
||||||
|
const routeTemplateId = route.query.templateId;
|
||||||
|
const currentWorkflow = workflowsStore.getCurrentWorkflow();
|
||||||
|
const workflow = workflowsStore.getWorkflowById(currentWorkflow.id);
|
||||||
|
|
||||||
|
// Hide the RAG starter callout if we're currently on the RAG starter template
|
||||||
|
if ((routeTemplateId ?? workflow?.meta?.templateId) === template.meta.templateId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isRagStarterWorkflowExperimentEnabled.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isCalloutDismissed = (callout: string) => {
|
||||||
|
return usersStore.isCalloutDismissed(callout);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dismissCallout = async (callout: string) => {
|
||||||
|
usersStore.setCalloutDismissed(callout);
|
||||||
|
|
||||||
|
await updateCurrentUserSettings(rootStore.restApiContext, {
|
||||||
|
dismissedCallouts: {
|
||||||
|
...usersStore.currentUser?.settings?.dismissedCallouts,
|
||||||
|
[callout]: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
openRagStarterTemplate,
|
||||||
|
isRagStarterWorkflowExperimentEnabled,
|
||||||
|
isRagStarterCalloutVisible,
|
||||||
|
isCalloutDismissed,
|
||||||
|
dismissCallout,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -738,10 +738,17 @@ export const WORKFLOW_BUILDER_EXPERIMENT = {
|
|||||||
variant: 'variant',
|
variant: 'variant',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const RAG_STARTER_WORKFLOW_EXPERIMENT = {
|
||||||
|
name: '033_rag_template',
|
||||||
|
control: 'control',
|
||||||
|
variant: 'variant',
|
||||||
|
};
|
||||||
|
|
||||||
export const EXPERIMENTS_TO_TRACK = [
|
export const EXPERIMENTS_TO_TRACK = [
|
||||||
EASY_AI_WORKFLOW_EXPERIMENT.name,
|
EASY_AI_WORKFLOW_EXPERIMENT.name,
|
||||||
AI_CREDITS_EXPERIMENT.name,
|
AI_CREDITS_EXPERIMENT.name,
|
||||||
WORKFLOW_BUILDER_EXPERIMENT.name,
|
WORKFLOW_BUILDER_EXPERIMENT.name,
|
||||||
|
RAG_STARTER_WORKFLOW_EXPERIMENT.name,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const WORKFLOW_EVALUATION_EXPERIMENT = '032_evaluation_mvp';
|
export const WORKFLOW_EVALUATION_EXPERIMENT = '032_evaluation_mvp';
|
||||||
|
|||||||
@@ -90,4 +90,71 @@ describe('users.store', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isCalloutDismissed', () => {
|
||||||
|
it('should return true if callout is dismissed', () => {
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
|
||||||
|
usersStore.usersById['1'] = {
|
||||||
|
...mockUser,
|
||||||
|
isDefaultUser: false,
|
||||||
|
isPendingUser: false,
|
||||||
|
mfaEnabled: false,
|
||||||
|
settings: {
|
||||||
|
dismissedCallouts: {
|
||||||
|
testCallout: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
usersStore.currentUserId = '1';
|
||||||
|
|
||||||
|
const isDismissed = usersStore.isCalloutDismissed('testCallout');
|
||||||
|
expect(isDismissed).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setCalloutDismissed', () => {
|
||||||
|
it('should set callout as dismissed in user settings', () => {
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
|
||||||
|
usersStore.usersById['1'] = {
|
||||||
|
...mockUser,
|
||||||
|
isDefaultUser: false,
|
||||||
|
isPendingUser: false,
|
||||||
|
mfaEnabled: false,
|
||||||
|
settings: {},
|
||||||
|
};
|
||||||
|
usersStore.currentUserId = '1';
|
||||||
|
|
||||||
|
usersStore.setCalloutDismissed('testCallout');
|
||||||
|
|
||||||
|
expect(usersStore.usersById['1'].settings?.dismissedCallouts).toEqual({
|
||||||
|
testCallout: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not lose existing dismissed callouts', () => {
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
|
||||||
|
usersStore.usersById['1'] = {
|
||||||
|
...mockUser,
|
||||||
|
isDefaultUser: false,
|
||||||
|
isPendingUser: false,
|
||||||
|
mfaEnabled: false,
|
||||||
|
settings: {
|
||||||
|
dismissedCallouts: {
|
||||||
|
previousCallout: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
usersStore.currentUserId = '1';
|
||||||
|
|
||||||
|
usersStore.setCalloutDismissed('testCallout');
|
||||||
|
|
||||||
|
expect(usersStore.usersById['1'].settings?.dismissedCallouts).toEqual({
|
||||||
|
previousCallout: true,
|
||||||
|
testCallout: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -91,6 +91,19 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isCalloutDismissed = (callout: string) =>
|
||||||
|
Boolean(currentUser.value?.settings?.dismissedCallouts?.[callout]);
|
||||||
|
|
||||||
|
const setCalloutDismissed = (callout: string) => {
|
||||||
|
if (currentUser.value?.settings) {
|
||||||
|
if (!currentUser.value?.settings?.dismissedCallouts) {
|
||||||
|
currentUser.value.settings.dismissedCallouts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUser.value.settings.dismissedCallouts[callout] = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const personalizedNodeTypes = computed(() => {
|
const personalizedNodeTypes = computed(() => {
|
||||||
const user = currentUser.value;
|
const user = currentUser.value;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -437,6 +450,8 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
|
|||||||
updateGlobalRole,
|
updateGlobalRole,
|
||||||
reset,
|
reset,
|
||||||
setEasyAIWorkflowOnboardingDone,
|
setEasyAIWorkflowOnboardingDone,
|
||||||
|
isCalloutDismissed,
|
||||||
|
setCalloutDismissed,
|
||||||
submitContactEmail,
|
submitContactEmail,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,13 +3,6 @@ import { NodeConnectionTypes } from 'n8n-workflow';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a workflow JSON object for an AI Agent in n8n.
|
* Generates a workflow JSON object for an AI Agent in n8n.
|
||||||
*
|
|
||||||
* @param {Object} params - The parameters for generating the workflow JSON.
|
|
||||||
* @param {boolean} params.isInstanceInAiFreeCreditsExperiment - Indicates if the instance is part of the AI free credits experiment.
|
|
||||||
* @param {number} params.withOpenAiFreeCredits - The number of free OpenAI calls available.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This function can be deleted once the free AI credits experiment is removed.
|
|
||||||
*/
|
*/
|
||||||
export const getEasyAiWorkflowJson = (): WorkflowDataWithTemplateId => {
|
export const getEasyAiWorkflowJson = (): WorkflowDataWithTemplateId => {
|
||||||
return {
|
return {
|
||||||
@@ -94,3 +87,252 @@ export const getEasyAiWorkflowJson = (): WorkflowDataWithTemplateId => {
|
|||||||
pinData: {},
|
pinData: {},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getRagStarterWorkflowJson = (): WorkflowDataWithTemplateId => {
|
||||||
|
return {
|
||||||
|
name: 'Demo: RAG in n8n',
|
||||||
|
meta: {
|
||||||
|
templateId: 'rag-starter-template',
|
||||||
|
},
|
||||||
|
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: [-120, 0],
|
||||||
|
id: 'f7a656ec-83fc-4ed2-a089-57a9def662b7',
|
||||||
|
name: 'Upload your file here',
|
||||||
|
webhookId: '82848bc4-5ea2-4e5a-8bb6-3c09b94a8c5d',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: '@n8n/n8n-nodes-langchain.embeddingsOpenAi',
|
||||||
|
typeVersion: 1.2,
|
||||||
|
position: [520, 480],
|
||||||
|
id: '6ea78663-cf2f-4f2d-8e68-43047c2afd87',
|
||||||
|
name: 'Embeddings OpenAI',
|
||||||
|
credentials: {
|
||||||
|
openAiApi: {
|
||||||
|
id: '14',
|
||||||
|
name: 'OpenAi account',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
dataType: 'binary',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: '@n8n/n8n-nodes-langchain.documentDefaultDataLoader',
|
||||||
|
typeVersion: 1.1,
|
||||||
|
position: [320, 160],
|
||||||
|
id: '94aecac0-03f9-4915-932b-d14a2576607b',
|
||||||
|
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',
|
||||||
|
height: 300,
|
||||||
|
width: 440,
|
||||||
|
color: 4,
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
position: [-660, -60],
|
||||||
|
typeVersion: 1,
|
||||||
|
id: '0d07742b-0b36-4c2e-990c-266cbe6e2d4d',
|
||||||
|
name: 'Sticky Note',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
content: '### 📚 Load Data Flow',
|
||||||
|
height: 460,
|
||||||
|
width: 700,
|
||||||
|
color: 7,
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
position: [-180, -60],
|
||||||
|
typeVersion: 1,
|
||||||
|
id: 'd19d04f3-5231-4e47-bed7-9f24a4a8f582',
|
||||||
|
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: [60, 0],
|
||||||
|
id: 'bf50a11f-ca6a-4e04-a6d2-42fee272b260',
|
||||||
|
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: [940, 200],
|
||||||
|
id: '09c0db62-5413-440e-8c13-fb6bb66d9b6a',
|
||||||
|
name: 'Query Data Tool',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: '@n8n/n8n-nodes-langchain.agent',
|
||||||
|
typeVersion: 2,
|
||||||
|
position: [940, -20],
|
||||||
|
id: '579aed76-9644-42d1-ac13-7369059ff1c2',
|
||||||
|
name: 'AI Agent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
type: '@n8n/n8n-nodes-langchain.chatTrigger',
|
||||||
|
typeVersion: 1.1,
|
||||||
|
position: [720, -20],
|
||||||
|
id: '9c30de61-935a-471f-ae88-ec5f67beeefc',
|
||||||
|
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: [720, 200],
|
||||||
|
id: 'b5aa8942-9cd5-4c2f-bd77-7a0ceb921bac',
|
||||||
|
name: 'OpenAI Chat Model',
|
||||||
|
credentials: {
|
||||||
|
openAiApi: {
|
||||||
|
id: '14',
|
||||||
|
name: 'OpenAi account',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
content: '### 🐕 2. Retriever Flow',
|
||||||
|
height: 460,
|
||||||
|
width: 680,
|
||||||
|
color: 7,
|
||||||
|
},
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
position: [600, -60],
|
||||||
|
typeVersion: 1,
|
||||||
|
id: '28bc73a1-e64a-47bf-ac1c-ffe644894ea5',
|
||||||
|
name: 'Sticky Note2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
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: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ import { getResourcePermissions } from '@/permissions';
|
|||||||
import NodeViewUnfinishedWorkflowMessage from '@/components/NodeViewUnfinishedWorkflowMessage.vue';
|
import NodeViewUnfinishedWorkflowMessage from '@/components/NodeViewUnfinishedWorkflowMessage.vue';
|
||||||
import { createCanvasConnectionHandleString } from '@/utils/canvasUtils';
|
import { createCanvasConnectionHandleString } from '@/utils/canvasUtils';
|
||||||
import { isValidNodeConnectionType } from '@/utils/typeGuards';
|
import { isValidNodeConnectionType } from '@/utils/typeGuards';
|
||||||
import { getEasyAiWorkflowJson } from '@/utils/easyAiWorkflowUtils';
|
import { getEasyAiWorkflowJson, getRagStarterWorkflowJson } from '@/utils/easyAiWorkflowUtils';
|
||||||
import type { CanvasLayoutEvent } from '@/composables/useCanvasLayout';
|
import type { CanvasLayoutEvent } from '@/composables/useCanvasLayout';
|
||||||
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
|
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
|
||||||
import { useBuilderStore } from '@/stores/builder.store';
|
import { useBuilderStore } from '@/stores/builder.store';
|
||||||
@@ -374,7 +374,22 @@ async function initializeRoute(force = false) {
|
|||||||
|
|
||||||
if (loadWorkflowFromJSON) {
|
if (loadWorkflowFromJSON) {
|
||||||
const easyAiWorkflowJson = getEasyAiWorkflowJson();
|
const easyAiWorkflowJson = getEasyAiWorkflowJson();
|
||||||
await openTemplateFromWorkflowJSON(easyAiWorkflowJson);
|
const ragStarterWorkflowJson = getRagStarterWorkflowJson();
|
||||||
|
|
||||||
|
switch (templateId) {
|
||||||
|
case easyAiWorkflowJson.meta.templateId:
|
||||||
|
await openTemplateFromWorkflowJSON(easyAiWorkflowJson);
|
||||||
|
break;
|
||||||
|
case ragStarterWorkflowJson.meta.templateId:
|
||||||
|
await openTemplateFromWorkflowJSON(ragStarterWorkflowJson);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
toast.showError(
|
||||||
|
new Error(i18n.baseText('nodeView.couldntLoadWorkflow.invalidWorkflowObject')),
|
||||||
|
i18n.baseText('nodeView.couldntImportWorkflow'),
|
||||||
|
);
|
||||||
|
await router.replace({ name: VIEWS.NEW_WORKFLOW });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await openWorkflowTemplate(templateId.toString());
|
await openWorkflowTemplate(templateId.toString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1251,6 +1251,7 @@ export type NodePropertyTypes =
|
|||||||
| 'fixedCollection'
|
| 'fixedCollection'
|
||||||
| 'hidden'
|
| 'hidden'
|
||||||
| 'json'
|
| 'json'
|
||||||
|
| 'callout'
|
||||||
| 'notice'
|
| 'notice'
|
||||||
| 'multiOptions'
|
| 'multiOptions'
|
||||||
| 'number'
|
| 'number'
|
||||||
@@ -1294,6 +1295,12 @@ export type NodePropertyAction = {
|
|||||||
target?: string;
|
target?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CalloutActionType = 'openRagStarterTemplate';
|
||||||
|
export interface CalloutAction {
|
||||||
|
type: CalloutActionType;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface INodePropertyTypeOptions {
|
export interface INodePropertyTypeOptions {
|
||||||
// Supported by: button
|
// Supported by: button
|
||||||
buttonConfig?: {
|
buttonConfig?: {
|
||||||
@@ -1326,6 +1333,7 @@ export interface INodePropertyTypeOptions {
|
|||||||
assignment?: AssignmentTypeOptions;
|
assignment?: AssignmentTypeOptions;
|
||||||
minRequiredFields?: number; // Supported by: fixedCollection
|
minRequiredFields?: number; // Supported by: fixedCollection
|
||||||
maxAllowedFields?: number; // Supported by: fixedCollection
|
maxAllowedFields?: number; // Supported by: fixedCollection
|
||||||
|
calloutAction?: CalloutAction; // Supported by: callout
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2835,6 +2843,7 @@ export interface IUserSettings {
|
|||||||
npsSurvey?: NpsSurveyState;
|
npsSurvey?: NpsSurveyState;
|
||||||
easyAIWorkflowOnboarded?: boolean;
|
easyAIWorkflowOnboarded?: boolean;
|
||||||
userClaimedAiCredits?: boolean;
|
userClaimedAiCredits?: boolean;
|
||||||
|
dismissedCallouts?: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProcessedDataConfig {
|
export interface IProcessedDataConfig {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"notice_tip": "",
|
"aiAgentStarterCallout": "",
|
||||||
"agent": "toolsAgent",
|
"agent": "toolsAgent",
|
||||||
"promptType": "define",
|
"promptType": "define",
|
||||||
"text": "=Add this user to my Users sheet:\n{{ $json.toJsonString() }}",
|
"text": "=Add this user to my Users sheet:\n{{ $json.toJsonString() }}",
|
||||||
|
|||||||
Reference in New Issue
Block a user