feat(core): Add support for building LLM applications (#7235)
This extracts all core and editor changes from #7246 and #7137, so that we can get these changes merged first. ADO-1120 [DB Tests](https://github.com/n8n-io/n8n/actions/runs/6379749011) [E2E Tests](https://github.com/n8n-io/n8n/actions/runs/6379751480) [Workflow Tests](https://github.com/n8n-io/n8n/actions/runs/6379752828) --------- Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com> Co-authored-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Alex Grozav <alex@grozav.com> Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
committed by
GitHub
parent
04dfcd73be
commit
00a4b8b0c6
@@ -12,7 +12,8 @@
|
||||
:data-test-id="dataTestId"
|
||||
>
|
||||
<template #icon>
|
||||
<node-icon :nodeType="nodeType" />
|
||||
<div v-if="isSubNode" :class="$style.subNodeBackground"></div>
|
||||
<node-icon :class="$style.nodeIcon" :nodeType="nodeType" />
|
||||
</template>
|
||||
|
||||
<template #tooltip v-if="isCommunityNode">
|
||||
@@ -50,6 +51,7 @@ import NodeIcon from '@/components/NodeIcon.vue';
|
||||
|
||||
import { useActions } from '../composables/useActions';
|
||||
import { useI18n, useTelemetry } from '@/composables';
|
||||
import { NodeHelpers, NodeConnectionType } from 'n8n-workflow';
|
||||
|
||||
export interface Props {
|
||||
nodeType: SimplifiedNodeType;
|
||||
@@ -75,7 +77,7 @@ const description = computed<string>(() => {
|
||||
return i18n.headerText({
|
||||
key: `headers.${shortNodeType.value}.description`,
|
||||
fallback: props.nodeType.description,
|
||||
}) as string;
|
||||
});
|
||||
});
|
||||
const showActionArrow = computed(() => hasActions.value);
|
||||
const dataTestId = computed(() =>
|
||||
@@ -109,9 +111,20 @@ const displayName = computed<any>(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const isSubNode = computed<boolean>(() => {
|
||||
if (!props.nodeType.outputs || typeof props.nodeType.outputs === 'string') {
|
||||
return false;
|
||||
}
|
||||
const outputTypes = NodeHelpers.getConnectionTypes(props.nodeType.outputs);
|
||||
return outputTypes
|
||||
? outputTypes.filter((output) => output !== NodeConnectionType.Main).length > 0
|
||||
: false;
|
||||
});
|
||||
|
||||
const isTrigger = computed<boolean>(() => {
|
||||
return props.nodeType.group.includes('trigger') && !hasActions.value;
|
||||
});
|
||||
|
||||
function onDragStart(event: DragEvent): void {
|
||||
/**
|
||||
* Workaround for firefox, that doesn't attach the pageX and pageY coordinates to "ondrag" event.
|
||||
@@ -170,6 +183,19 @@ function onCommunityNodeTooltipClick(event: MouseEvent) {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nodeIcon {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.subNodeBackground {
|
||||
background-color: var(--node-type-supplemental-background);
|
||||
border-radius: 50%;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
transform: translate(-7px, -7px);
|
||||
width: 40px;
|
||||
z-index: 1;
|
||||
}
|
||||
.communityNodeIcon {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,13 @@
|
||||
:showActionArrow="true"
|
||||
>
|
||||
<template #icon>
|
||||
<n8n-node-icon type="icon" :name="item.icon" :circle="false" :showTooltip="false" />
|
||||
<n8n-node-icon
|
||||
type="icon"
|
||||
:name="item.icon"
|
||||
:circle="false"
|
||||
:showTooltip="false"
|
||||
v-bind="item.iconProps"
|
||||
/>
|
||||
</template>
|
||||
</n8n-node-creator-node>
|
||||
</template>
|
||||
|
||||
@@ -2,13 +2,20 @@
|
||||
import { camelCase } from 'lodash-es';
|
||||
import { computed } from 'vue';
|
||||
import type { INodeCreateElement, NodeFilterType } from '@/Interface';
|
||||
import { TRIGGER_NODE_CREATOR_VIEW, HTTP_REQUEST_NODE_TYPE, WEBHOOK_NODE_TYPE } from '@/constants';
|
||||
import {
|
||||
TRIGGER_NODE_CREATOR_VIEW,
|
||||
HTTP_REQUEST_NODE_TYPE,
|
||||
WEBHOOK_NODE_TYPE,
|
||||
REGULAR_NODE_CREATOR_VIEW,
|
||||
AI_NODE_CREATOR_VIEW,
|
||||
AI_OTHERS_NODE_CREATOR_VIEW,
|
||||
} from '@/constants';
|
||||
|
||||
import type { BaseTextKey } from '@/plugins/i18n';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||
|
||||
import { TriggerView, RegularView } from '../viewsData';
|
||||
import { TriggerView, RegularView, AIView, AINodesView } from '../viewsData';
|
||||
import { transformNodeType } from '../utils';
|
||||
import { useViewStacks } from '../composables/useViewStacks';
|
||||
import { useActions } from '../composables/useActions';
|
||||
@@ -48,14 +55,22 @@ function selectNodeType(nodeTypes: string[]) {
|
||||
|
||||
function onSelected(item: INodeCreateElement) {
|
||||
if (item.type === 'subcategory') {
|
||||
const title = i18n.baseText(
|
||||
`nodeCreator.subcategoryNames.${camelCase(item.properties.title)}` as BaseTextKey,
|
||||
);
|
||||
const subcategoryKey = camelCase(item.properties.title);
|
||||
const title = i18n.baseText(`nodeCreator.subcategoryNames.${subcategoryKey}` as BaseTextKey);
|
||||
|
||||
pushViewStack({
|
||||
subcategory: item.key,
|
||||
title,
|
||||
mode: 'nodes',
|
||||
...(item.properties.icon
|
||||
? {
|
||||
nodeIcon: {
|
||||
icon: item.properties.icon,
|
||||
iconType: 'icon',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(item.properties.panelClass ? { panelClass: item.properties.panelClass } : {}),
|
||||
rootView: activeViewStack.value.rootView,
|
||||
forceIncludeNodes: item.properties.forceIncludeNodes,
|
||||
baseFilter: baseSubcategoriesFilter,
|
||||
@@ -99,11 +114,26 @@ function onSelected(item: INodeCreateElement) {
|
||||
}
|
||||
|
||||
if (item.type === 'view') {
|
||||
const view = item.key === TRIGGER_NODE_CREATOR_VIEW ? TriggerView() : RegularView();
|
||||
const views = {
|
||||
[TRIGGER_NODE_CREATOR_VIEW]: TriggerView,
|
||||
[REGULAR_NODE_CREATOR_VIEW]: RegularView,
|
||||
[AI_NODE_CREATOR_VIEW]: AIView,
|
||||
[AI_OTHERS_NODE_CREATOR_VIEW]: AINodesView,
|
||||
};
|
||||
|
||||
const itemKey = item.key as keyof typeof views;
|
||||
const matchedView = views[itemKey];
|
||||
|
||||
if (!matchedView) {
|
||||
console.warn(`No view found for ${itemKey}`);
|
||||
return;
|
||||
}
|
||||
const view = matchedView(mergedNodes);
|
||||
|
||||
pushViewStack({
|
||||
title: view.title,
|
||||
subtitle: view?.subtitle ?? '',
|
||||
info: view?.info ?? '',
|
||||
items: view.items as INodeCreateElement[],
|
||||
hasSearch: true,
|
||||
rootView: view.value as NodeFilterType,
|
||||
@@ -162,7 +192,7 @@ function onKeySelect(activeItemId: string) {
|
||||
const item = mergedItems.find((i) => i.uuid === activeItemId);
|
||||
if (!item) return;
|
||||
|
||||
onSelected(item as INodeCreateElement);
|
||||
onSelected(item);
|
||||
}
|
||||
|
||||
registerKeyHook('MainViewArrowRight', {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import type { INodeCreateElement } from '@/Interface';
|
||||
import { TRIGGER_NODE_CREATOR_VIEW } from '@/constants';
|
||||
import {
|
||||
AI_OTHERS_NODE_CREATOR_VIEW,
|
||||
AI_NODE_CREATOR_VIEW,
|
||||
REGULAR_NODE_CREATOR_VIEW,
|
||||
TRIGGER_NODE_CREATOR_VIEW,
|
||||
} from '@/constants';
|
||||
|
||||
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||
|
||||
import { TriggerView, RegularView } from '../viewsData';
|
||||
import { TriggerView, RegularView, AIView, AINodesView } from '../viewsData';
|
||||
import { useViewStacks } from '../composables/useViewStacks';
|
||||
import { useKeyboardNavigation } from '../composables/useKeyboardNavigation';
|
||||
import SearchBar from './SearchBar.vue';
|
||||
@@ -59,12 +64,27 @@ onUnmounted(() => {
|
||||
watch(
|
||||
() => nodeCreatorView.value,
|
||||
(selectedView) => {
|
||||
const view = selectedView === TRIGGER_NODE_CREATOR_VIEW ? TriggerView() : RegularView();
|
||||
const views = {
|
||||
[TRIGGER_NODE_CREATOR_VIEW]: TriggerView,
|
||||
[REGULAR_NODE_CREATOR_VIEW]: RegularView,
|
||||
[AI_NODE_CREATOR_VIEW]: AIView,
|
||||
[AI_OTHERS_NODE_CREATOR_VIEW]: AINodesView,
|
||||
};
|
||||
|
||||
const itemKey = selectedView;
|
||||
const matchedView = views[itemKey];
|
||||
|
||||
if (!matchedView) {
|
||||
console.warn(`No view found for ${itemKey}`);
|
||||
return;
|
||||
}
|
||||
const view = matchedView(mergedNodes);
|
||||
|
||||
pushViewStack({
|
||||
title: view.title,
|
||||
subtitle: view?.subtitle ?? '',
|
||||
items: view.items as INodeCreateElement[],
|
||||
info: view.info,
|
||||
hasSearch: true,
|
||||
mode: 'nodes',
|
||||
rootView: selectedView,
|
||||
@@ -86,13 +106,25 @@ function onBackButton() {
|
||||
:name="`panel-slide-${activeViewStack.transitionDirection}`"
|
||||
@afterLeave="onTransitionEnd"
|
||||
>
|
||||
<aside :class="$style.nodesListPanel" @keydown.capture.stop :key="`${activeViewStack.uuid}`">
|
||||
<aside
|
||||
:class="[$style.nodesListPanel, activeViewStack.panelClass]"
|
||||
@keydown.capture.stop
|
||||
:key="`${activeViewStack.uuid}`"
|
||||
>
|
||||
<header
|
||||
:class="{ [$style.header]: true, [$style.hasBg]: !activeViewStack.subtitle }"
|
||||
:class="{
|
||||
[$style.header]: true,
|
||||
[$style.hasBg]: !activeViewStack.subtitle,
|
||||
'nodes-list-panel-header': true,
|
||||
}"
|
||||
data-test-id="nodes-list-header"
|
||||
>
|
||||
<div :class="$style.top">
|
||||
<button :class="$style.backButton" @click="onBackButton" v-if="viewStacks.length > 1">
|
||||
<button
|
||||
:class="$style.backButton"
|
||||
@click="onBackButton"
|
||||
v-if="viewStacks.length > 1 && !activeViewStack.preventBack"
|
||||
>
|
||||
<font-awesome-icon :class="$style.backButtonIcon" icon="arrow-left" size="2x" />
|
||||
</button>
|
||||
<n8n-node-icon
|
||||
@@ -104,7 +136,7 @@ function onBackButton() {
|
||||
:color="activeViewStack.nodeIcon.color"
|
||||
:circle="false"
|
||||
:showTooltip="false"
|
||||
:size="16"
|
||||
:size="20"
|
||||
/>
|
||||
<p :class="$style.title" v-text="activeViewStack.title" v-if="activeViewStack.title" />
|
||||
</div>
|
||||
@@ -126,6 +158,12 @@ function onBackButton() {
|
||||
@update:modelValue="onSearch"
|
||||
/>
|
||||
<div :class="$style.renderedItems">
|
||||
<n8n-notice
|
||||
v-if="activeViewStack.info && !activeViewStack.search"
|
||||
:class="$style.info"
|
||||
:content="activeViewStack.info"
|
||||
theme="info"
|
||||
/>
|
||||
<!-- Actions mode -->
|
||||
<ActionsRenderer v-if="isActionsMode && activeViewStack.subcategory" v-bind="$attrs" />
|
||||
|
||||
@@ -160,6 +198,9 @@ function onBackButton() {
|
||||
// for the slide-out panel effect
|
||||
z-index: 1;
|
||||
}
|
||||
.info {
|
||||
margin: var(--spacing-2xs) var(--spacing-s);
|
||||
}
|
||||
.backButton {
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -173,7 +214,7 @@ function onBackButton() {
|
||||
padding: 0;
|
||||
}
|
||||
.nodeIcon {
|
||||
--node-icon-size: 16px;
|
||||
--node-icon-size: 20px;
|
||||
margin-right: var(--spacing-s);
|
||||
}
|
||||
.renderedItems {
|
||||
@@ -254,3 +295,13 @@ function onBackButton() {
|
||||
margin-left: calc(var(--spacing-xl) + var(--spacing-4xs));
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@each $node-type in $supplemental-node-types {
|
||||
.nodes-list-panel-#{$node-type} .nodes-list-panel-header {
|
||||
.n8n-node-icon svg {
|
||||
color: var(--node-type-#{$node-type}-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -36,7 +36,7 @@ const activeItemId = computed(() => useKeyboardNavigation()?.activeItemId);
|
||||
// Lazy render large items lists to prevent the browser from freezing
|
||||
// when loading many items.
|
||||
function renderItems() {
|
||||
if (props.elements.length <= LAZY_LOAD_THRESHOLD || props.lazyRender === false) {
|
||||
if (props.elements.length <= LAZY_LOAD_THRESHOLD || !props.lazyRender) {
|
||||
renderedItems.value = props.elements;
|
||||
return;
|
||||
}
|
||||
@@ -197,19 +197,21 @@ watch(
|
||||
}
|
||||
}
|
||||
.view {
|
||||
margin-top: var(--spacing-s);
|
||||
padding-top: var(--spacing-xs);
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: var(--spacing-s);
|
||||
right: var(--spacing-s);
|
||||
top: 0;
|
||||
margin: auto;
|
||||
bottom: 0;
|
||||
border-top: 1px solid var(--color-foreground-base);
|
||||
&:last-child {
|
||||
margin-top: var(--spacing-s);
|
||||
padding-top: var(--spacing-xs);
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: var(--spacing-s);
|
||||
right: var(--spacing-s);
|
||||
top: 0;
|
||||
margin: auto;
|
||||
bottom: 0;
|
||||
border-top: 1px solid var(--color-foreground-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -56,6 +56,7 @@ function getNodeTypeBase(nodeTypeDescription: INodeTypeDescription, label?: stri
|
||||
categories: [category],
|
||||
},
|
||||
iconUrl: nodeTypeDescription.iconUrl,
|
||||
outputs: nodeTypeDescription.outputs,
|
||||
icon: nodeTypeDescription.icon,
|
||||
defaults: nodeTypeDescription.defaults,
|
||||
};
|
||||
@@ -225,7 +226,7 @@ export function useActionsGenerator() {
|
||||
}
|
||||
|
||||
function getSimplifiedNodeType(node: INodeTypeDescription): SimplifiedNodeType {
|
||||
const { displayName, defaults, description, name, group, icon, iconUrl, codex } = node;
|
||||
const { displayName, defaults, description, name, group, icon, iconUrl, outputs, codex } = node;
|
||||
|
||||
return {
|
||||
displayName,
|
||||
@@ -235,6 +236,7 @@ export function useActionsGenerator() {
|
||||
group,
|
||||
icon,
|
||||
iconUrl,
|
||||
outputs,
|
||||
codex,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { INodeCreateElement, NodeFilterType, SimplifiedNodeType } from '@/Interface';
|
||||
import { DEFAULT_SUBCATEGORY, TRIGGER_NODE_CREATOR_VIEW } from '@/constants';
|
||||
import type {
|
||||
NodeConnectionType,
|
||||
INodeCreateElement,
|
||||
NodeFilterType,
|
||||
SimplifiedNodeType,
|
||||
} from '@/Interface';
|
||||
import {
|
||||
AI_OTHERS_NODE_CREATOR_VIEW,
|
||||
DEFAULT_SUBCATEGORY,
|
||||
TRIGGER_NODE_CREATOR_VIEW,
|
||||
} from '@/constants';
|
||||
|
||||
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||
|
||||
@@ -13,6 +22,11 @@ import {
|
||||
sortNodeCreateElements,
|
||||
searchNodes,
|
||||
} from '../utils';
|
||||
import { useI18n } from '@/composables';
|
||||
|
||||
import type { INodeInputFilter } from 'n8n-workflow';
|
||||
import { useNodeTypesStore } from '@/stores';
|
||||
import { AINodesView, type NodeViewItem } from '@/components/Node/NodeCreator/viewsData';
|
||||
|
||||
interface ViewStack {
|
||||
uuid?: string;
|
||||
@@ -20,6 +34,7 @@ interface ViewStack {
|
||||
subtitle?: string;
|
||||
search?: string;
|
||||
subcategory?: string;
|
||||
info?: string;
|
||||
nodeIcon?: {
|
||||
iconType?: string;
|
||||
icon?: string;
|
||||
@@ -30,6 +45,7 @@ interface ViewStack {
|
||||
activeIndex?: number;
|
||||
transitionDirection?: 'in' | 'out';
|
||||
hasSearch?: boolean;
|
||||
preventBack?: boolean;
|
||||
items?: INodeCreateElement[];
|
||||
baselineItems?: INodeCreateElement[];
|
||||
searchItems?: SimplifiedNodeType[];
|
||||
@@ -37,6 +53,7 @@ interface ViewStack {
|
||||
mode?: 'actions' | 'nodes';
|
||||
baseFilter?: (item: INodeCreateElement) => boolean;
|
||||
itemsMapper?: (item: INodeCreateElement) => INodeCreateElement;
|
||||
panelClass?: string;
|
||||
}
|
||||
|
||||
export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
|
||||
@@ -78,7 +95,7 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
|
||||
|
||||
const searchBaseItems = computed<INodeCreateElement[]>(() => {
|
||||
const stack = viewStacks.value[viewStacks.value.length - 1];
|
||||
if (!stack || !stack.searchItems) return [];
|
||||
if (!stack?.searchItems) return [];
|
||||
|
||||
return stack.searchItems.map((item) => transformNodeType(item, stack.subcategory));
|
||||
});
|
||||
@@ -86,7 +103,7 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
|
||||
// Generate a delta between the global search results(all nodes) and the stack search results
|
||||
const globalSearchItemsDiff = computed<INodeCreateElement[]>(() => {
|
||||
const stack = viewStacks.value[viewStacks.value.length - 1];
|
||||
if (!stack || !stack.search) return [];
|
||||
if (!stack?.search) return [];
|
||||
|
||||
const allNodes = nodeCreatorStore.mergedNodes.map((item) => transformNodeType(item));
|
||||
const globalSearchResult = extendItemsWithUUID(searchNodes(stack.search || '', allNodes));
|
||||
@@ -96,13 +113,75 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
async function gotoCompatibleConnectionView(
|
||||
connectionType: NodeConnectionType,
|
||||
isOutput?: boolean,
|
||||
filter?: INodeInputFilter,
|
||||
) {
|
||||
const i18n = useI18n();
|
||||
|
||||
let nodesByConnectionType: { [key: string]: string[] };
|
||||
let relatedAIView: NodeViewItem | { properties: { title: string; icon: string } } | undefined;
|
||||
|
||||
if (isOutput === true) {
|
||||
nodesByConnectionType = useNodeTypesStore().visibleNodeTypesByInputConnectionTypeNames;
|
||||
relatedAIView = {
|
||||
properties: {
|
||||
title: i18n.baseText('nodeCreator.aiPanel.aiNodes'),
|
||||
icon: 'robot',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
nodesByConnectionType = useNodeTypesStore().visibleNodeTypesByOutputConnectionTypeNames;
|
||||
|
||||
relatedAIView = AINodesView([]).items.find(
|
||||
(item) => item.properties.connectionType === connectionType,
|
||||
);
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
pushViewStack({
|
||||
title: relatedAIView?.properties.title,
|
||||
rootView: AI_OTHERS_NODE_CREATOR_VIEW,
|
||||
mode: 'nodes',
|
||||
items: nodeCreatorStore.allNodeCreatorNodes,
|
||||
nodeIcon: {
|
||||
iconType: 'icon',
|
||||
icon: relatedAIView?.properties.icon,
|
||||
color: relatedAIView?.properties.iconProps?.color,
|
||||
},
|
||||
panelClass: relatedAIView?.properties.panelClass,
|
||||
baseFilter: (i: INodeCreateElement) => {
|
||||
const displayNode = nodesByConnectionType[connectionType].includes(i.key);
|
||||
|
||||
// TODO: Filtering works currently fine for displaying compatible node when dropping
|
||||
// input connections. However, it does not work for output connections.
|
||||
// For that reason does it currently display nodes that are maybe not compatible
|
||||
// but then errors once it got selected by the user.
|
||||
if (displayNode && filter?.nodes?.length) {
|
||||
return filter.nodes.includes(i.key);
|
||||
}
|
||||
|
||||
return displayNode;
|
||||
},
|
||||
itemsMapper(item) {
|
||||
return {
|
||||
...item,
|
||||
subcategory: connectionType,
|
||||
};
|
||||
},
|
||||
preventBack: true,
|
||||
});
|
||||
}
|
||||
|
||||
function setStackBaselineItems() {
|
||||
const stack = viewStacks.value[viewStacks.value.length - 1];
|
||||
if (!stack || !activeViewStack.value.uuid) return;
|
||||
|
||||
const subcategorizedItems = subcategorizeItems(nodeCreatorStore.mergedNodes);
|
||||
let stackItems =
|
||||
stack?.items ?? subcategorizedItems[stack?.subcategory ?? DEFAULT_SUBCATEGORY] ?? [];
|
||||
stack?.items ??
|
||||
subcategorizeItems(nodeCreatorStore.mergedNodes)[stack?.subcategory ?? DEFAULT_SUBCATEGORY] ??
|
||||
[];
|
||||
|
||||
// Ensure that the nodes specified in `stack.forceIncludeNodes` are always included,
|
||||
// regardless of whether the subcategory is matched
|
||||
@@ -183,6 +262,7 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
|
||||
activeViewStack,
|
||||
activeViewStackMode,
|
||||
globalSearchItemsDiff,
|
||||
gotoCompatibleConnectionView,
|
||||
resetViewStacks,
|
||||
updateCurrentViewStack,
|
||||
pushViewStack,
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
SimplifiedNodeType,
|
||||
INodeCreateElement,
|
||||
} from '@/Interface';
|
||||
import { CORE_NODES_CATEGORY, DEFAULT_SUBCATEGORY } from '@/constants';
|
||||
import { AI_SUBCATEGORY, CORE_NODES_CATEGORY, DEFAULT_SUBCATEGORY } from '@/constants';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { sublimeSearch } from '@/utils';
|
||||
|
||||
@@ -31,12 +31,16 @@ export function transformNodeType(
|
||||
}
|
||||
|
||||
export function subcategorizeItems(items: SimplifiedNodeType[]) {
|
||||
const WHITE_LISTED_SUBCATEGORIES = [CORE_NODES_CATEGORY, AI_SUBCATEGORY];
|
||||
return items.reduce((acc: SubcategorizedNodeTypes, item) => {
|
||||
// Only Core Nodes subcategories are valid, others are uncategorized
|
||||
const isCoreNodesCategory = item.codex?.categories?.includes(CORE_NODES_CATEGORY);
|
||||
const subcategories = isCoreNodesCategory
|
||||
? item?.codex?.subcategories?.[CORE_NODES_CATEGORY] ?? []
|
||||
: [DEFAULT_SUBCATEGORY];
|
||||
// Only some subcategories are allowed
|
||||
let subcategories: string[] = [DEFAULT_SUBCATEGORY];
|
||||
|
||||
WHITE_LISTED_SUBCATEGORIES.forEach((category) => {
|
||||
if (item.codex?.categories?.includes(category)) {
|
||||
subcategories = item.codex?.subcategories?.[category] ?? [];
|
||||
}
|
||||
});
|
||||
|
||||
subcategories.forEach((subcategory: string) => {
|
||||
if (!acc[subcategory]) {
|
||||
|
||||
@@ -13,13 +13,216 @@ import {
|
||||
TRIGGER_NODE_CREATOR_VIEW,
|
||||
EMAIL_IMAP_NODE_TYPE,
|
||||
DEFAULT_SUBCATEGORY,
|
||||
AI_NODE_CREATOR_VIEW,
|
||||
AI_CATEGORY_AGENTS,
|
||||
AI_CATEGORY_CHAINS,
|
||||
AI_CATEGORY_DOCUMENT_LOADERS,
|
||||
AI_CATEGORY_LANGUAGE_MODELS,
|
||||
AI_CATEGORY_MEMORY,
|
||||
AI_CATEGORY_OUTPUTPARSER,
|
||||
AI_CATEGORY_RETRIEVERS,
|
||||
AI_CATEGORY_TEXT_SPLITTERS,
|
||||
AI_CATEGORY_TOOLS,
|
||||
AI_CATEGORY_VECTOR_STORES,
|
||||
AI_SUBCATEGORY,
|
||||
AI_CATEGORY_EMBEDDING,
|
||||
AI_OTHERS_NODE_CREATOR_VIEW,
|
||||
AI_UNCATEGORIZED_CATEGORY,
|
||||
} from '@/constants';
|
||||
import { useI18n } from '@/composables';
|
||||
import { useNodeTypesStore } from '@/stores';
|
||||
import type { SimplifiedNodeType } from '@/Interface';
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
|
||||
export function TriggerView() {
|
||||
export interface NodeViewItem {
|
||||
key: string;
|
||||
type: string;
|
||||
properties: {
|
||||
name?: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
iconProps?: {
|
||||
color?: string;
|
||||
};
|
||||
connectionType?: NodeConnectionType;
|
||||
panelClass?: string;
|
||||
group?: string[];
|
||||
description?: string;
|
||||
forceIncludeNodes?: string[];
|
||||
};
|
||||
category?: string | string[];
|
||||
}
|
||||
|
||||
interface NodeView {
|
||||
value: string;
|
||||
title: string;
|
||||
info?: string;
|
||||
subtitle?: string;
|
||||
items: NodeViewItem[];
|
||||
}
|
||||
|
||||
function getAiNodesBySubcategory(nodes: INodeTypeDescription[], subcategory: string) {
|
||||
return nodes
|
||||
.filter((node) => node.codex?.subcategories?.[AI_SUBCATEGORY]?.includes(subcategory))
|
||||
.map((node) => ({
|
||||
key: node.name,
|
||||
type: 'node',
|
||||
properties: {
|
||||
group: [],
|
||||
name: node.name,
|
||||
displayName: node.displayName,
|
||||
title: node.displayName,
|
||||
description: node.description,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
icon: node.icon!,
|
||||
},
|
||||
}))
|
||||
.sort((a, b) => a.properties.displayName.localeCompare(b.properties.displayName));
|
||||
}
|
||||
|
||||
export function AIView(_nodes: SimplifiedNodeType[]): NodeView {
|
||||
const i18n = useI18n();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
const chainNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_CHAINS);
|
||||
const agentNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_AGENTS);
|
||||
|
||||
return {
|
||||
value: AI_NODE_CREATOR_VIEW,
|
||||
title: i18n.baseText('nodeCreator.aiPanel.aiNodes'),
|
||||
subtitle: i18n.baseText('nodeCreator.aiPanel.selectAiNode'),
|
||||
info: i18n.baseText('nodeCreator.aiPanel.infoBox'),
|
||||
items: [
|
||||
...chainNodes,
|
||||
...agentNodes,
|
||||
{
|
||||
key: AI_OTHERS_NODE_CREATOR_VIEW,
|
||||
type: 'view',
|
||||
properties: {
|
||||
title: i18n.baseText('nodeCreator.aiPanel.aiOtherNodes'),
|
||||
icon: 'robot',
|
||||
description: i18n.baseText('nodeCreator.aiPanel.aiOtherNodesDescription'),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
export function AINodesView(_nodes: SimplifiedNodeType[]): NodeView {
|
||||
const i18n = useI18n();
|
||||
|
||||
function getAISubcategoryProperties(nodeConnectionType: NodeConnectionType) {
|
||||
return {
|
||||
connectionType: nodeConnectionType,
|
||||
iconProps: {
|
||||
color: `var(--node-type-${nodeConnectionType}-color)`,
|
||||
},
|
||||
panelClass: `nodes-list-panel-${nodeConnectionType}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: AI_OTHERS_NODE_CREATOR_VIEW,
|
||||
title: i18n.baseText('nodeCreator.aiPanel.aiOtherNodes'),
|
||||
subtitle: i18n.baseText('nodeCreator.aiPanel.selectAiNode'),
|
||||
items: [
|
||||
{
|
||||
key: AI_CATEGORY_DOCUMENT_LOADERS,
|
||||
type: 'subcategory',
|
||||
properties: {
|
||||
title: AI_CATEGORY_DOCUMENT_LOADERS,
|
||||
icon: 'file-import',
|
||||
...getAISubcategoryProperties(NodeConnectionType.AiDocument),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AI_CATEGORY_LANGUAGE_MODELS,
|
||||
type: 'subcategory',
|
||||
properties: {
|
||||
title: AI_CATEGORY_LANGUAGE_MODELS,
|
||||
icon: 'language',
|
||||
...getAISubcategoryProperties(NodeConnectionType.AiLanguageModel),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AI_CATEGORY_MEMORY,
|
||||
type: 'subcategory',
|
||||
properties: {
|
||||
title: AI_CATEGORY_MEMORY,
|
||||
icon: 'brain',
|
||||
...getAISubcategoryProperties(NodeConnectionType.AiMemory),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AI_CATEGORY_OUTPUTPARSER,
|
||||
type: 'subcategory',
|
||||
properties: {
|
||||
title: AI_CATEGORY_OUTPUTPARSER,
|
||||
icon: 'list',
|
||||
...getAISubcategoryProperties(NodeConnectionType.AiOutputParser),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AI_CATEGORY_RETRIEVERS,
|
||||
type: 'subcategory',
|
||||
properties: {
|
||||
title: AI_CATEGORY_RETRIEVERS,
|
||||
icon: 'search',
|
||||
...getAISubcategoryProperties(NodeConnectionType.AiRetriever),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AI_CATEGORY_TEXT_SPLITTERS,
|
||||
type: 'subcategory',
|
||||
properties: {
|
||||
title: AI_CATEGORY_TEXT_SPLITTERS,
|
||||
icon: 'grip-lines-vertical',
|
||||
...getAISubcategoryProperties(NodeConnectionType.AiTextSplitter),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AI_CATEGORY_TOOLS,
|
||||
type: 'subcategory',
|
||||
properties: {
|
||||
title: AI_CATEGORY_TOOLS,
|
||||
icon: 'tools',
|
||||
...getAISubcategoryProperties(NodeConnectionType.AiTool),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AI_CATEGORY_EMBEDDING,
|
||||
type: 'subcategory',
|
||||
properties: {
|
||||
title: AI_CATEGORY_EMBEDDING,
|
||||
icon: 'vector-square',
|
||||
...getAISubcategoryProperties(NodeConnectionType.AiEmbedding),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AI_CATEGORY_VECTOR_STORES,
|
||||
type: 'subcategory',
|
||||
properties: {
|
||||
title: AI_CATEGORY_VECTOR_STORES,
|
||||
icon: 'project-diagram',
|
||||
...getAISubcategoryProperties(NodeConnectionType.AiVectorStore),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AI_UNCATEGORIZED_CATEGORY,
|
||||
type: 'subcategory',
|
||||
properties: {
|
||||
title: AI_UNCATEGORIZED_CATEGORY,
|
||||
icon: 'code',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function TriggerView(nodes: SimplifiedNodeType[]) {
|
||||
const i18n = useI18n();
|
||||
|
||||
const view: NodeView = {
|
||||
value: TRIGGER_NODE_CREATOR_VIEW,
|
||||
title: i18n.baseText('nodeCreator.triggerHelperPanel.selectATrigger'),
|
||||
subtitle: i18n.baseText('nodeCreator.triggerHelperPanel.selectATriggerDescription'),
|
||||
@@ -96,12 +299,26 @@ export function TriggerView() {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const hasAINodes = (nodes ?? []).some((node) => node.codex?.categories?.includes(AI_SUBCATEGORY));
|
||||
if (hasAINodes)
|
||||
view.items.push({
|
||||
key: AI_NODE_CREATOR_VIEW,
|
||||
type: 'view',
|
||||
properties: {
|
||||
title: i18n.baseText('nodeCreator.aiPanel.langchainAiNodes'),
|
||||
icon: 'robot',
|
||||
description: i18n.baseText('nodeCreator.aiPanel.nodesForAi'),
|
||||
},
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
export function RegularView() {
|
||||
export function RegularView(nodes: SimplifiedNodeType[]) {
|
||||
const i18n = useI18n();
|
||||
|
||||
return {
|
||||
const view: NodeView = {
|
||||
value: REGULAR_NODE_CREATOR_VIEW,
|
||||
title: i18n.baseText('nodeCreator.triggerHelperPanel.whatHappensNext'),
|
||||
items: [
|
||||
@@ -149,15 +366,30 @@ export function RegularView() {
|
||||
icon: 'file-alt',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: TRIGGER_NODE_CREATOR_VIEW,
|
||||
type: 'view',
|
||||
properties: {
|
||||
title: i18n.baseText('nodeCreator.triggerHelperPanel.addAnotherTrigger'),
|
||||
icon: 'bolt',
|
||||
description: i18n.baseText('nodeCreator.triggerHelperPanel.addAnotherTriggerDescription'),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const hasAINodes = (nodes ?? []).some((node) => node.codex?.categories?.includes(AI_SUBCATEGORY));
|
||||
if (hasAINodes)
|
||||
view.items.push({
|
||||
key: AI_NODE_CREATOR_VIEW,
|
||||
type: 'view',
|
||||
properties: {
|
||||
title: i18n.baseText('nodeCreator.aiPanel.langchainAiNodes'),
|
||||
icon: 'robot',
|
||||
description: i18n.baseText('nodeCreator.aiPanel.nodesForAi'),
|
||||
},
|
||||
});
|
||||
|
||||
view.items.push({
|
||||
key: TRIGGER_NODE_CREATOR_VIEW,
|
||||
type: 'view',
|
||||
properties: {
|
||||
title: i18n.baseText('nodeCreator.triggerHelperPanel.addAnotherTrigger'),
|
||||
icon: 'bolt',
|
||||
description: i18n.baseText('nodeCreator.triggerHelperPanel.addAnotherTriggerDescription'),
|
||||
},
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user