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:
कारतोफ्फेलस्क्रिप्ट™
2023-10-02 17:33:43 +02:00
committed by GitHub
parent 04dfcd73be
commit 00a4b8b0c6
93 changed files with 6209 additions and 728 deletions

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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', {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,
};
}

View File

@@ -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,

View File

@@ -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]) {

View File

@@ -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;
}