mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
feat(AI Transform Node): New node (#10405)
Co-authored-by: Giulio Andreini <g.andreini@gmail.com> Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com>
This commit is contained in:
parent
5ed2a77740
commit
4d222ac19d
|
@ -50,7 +50,9 @@ export class ManualChatTrigger implements INodeType {
|
|||
name: 'openChat',
|
||||
type: 'button',
|
||||
typeOptions: {
|
||||
action: 'openChat',
|
||||
buttonConfig: {
|
||||
action: 'openChat',
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { INodePropertyOptions } from 'n8n-workflow';
|
||||
import type { INodePropertyOptions, NodeParameterValueType } from 'n8n-workflow';
|
||||
|
||||
import { Post, RestController } from '@/decorators';
|
||||
import { getBase } from '@/WorkflowExecuteAdditionalData';
|
||||
|
@ -92,4 +92,28 @@ export class DynamicNodeParametersController {
|
|||
credentials,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/action-result')
|
||||
async getActionResult(
|
||||
req: DynamicNodeParametersRequest.ActionResult,
|
||||
): Promise<NodeParameterValueType> {
|
||||
const { currentNodeParameters, nodeTypeAndVersion, path, credentials, handler, payload } =
|
||||
req.body;
|
||||
|
||||
const additionalData = await getBase(req.user.id, currentNodeParameters);
|
||||
|
||||
if (handler) {
|
||||
return await this.service.getActionResult(
|
||||
handler,
|
||||
path,
|
||||
additionalData,
|
||||
nodeTypeAndVersion,
|
||||
currentNodeParameters,
|
||||
payload,
|
||||
credentials,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -420,6 +420,12 @@ export declare namespace DynamicNodeParametersRequest {
|
|||
type ResourceMapperFields = BaseRequest<{
|
||||
methodName: string;
|
||||
}>;
|
||||
|
||||
/** POST /dynamic-node-parameters/action-result */
|
||||
type ActionResult = BaseRequest<{
|
||||
handler: string;
|
||||
payload: IDataObject | string | undefined;
|
||||
}>;
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
|
|
|
@ -15,6 +15,8 @@ import type {
|
|||
INodeCredentials,
|
||||
INodeParameters,
|
||||
INodeTypeNameVersion,
|
||||
NodeParameterValueType,
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
import { Workflow, RoutingNode, ApplicationError } from 'n8n-workflow';
|
||||
import { NodeExecuteFunctions } from 'n8n-core';
|
||||
|
@ -156,6 +158,24 @@ export class DynamicNodeParametersService {
|
|||
return method.call(thisArgs);
|
||||
}
|
||||
|
||||
/** Returns the result of the action handler */
|
||||
async getActionResult(
|
||||
handler: string,
|
||||
path: string,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
nodeTypeAndVersion: INodeTypeNameVersion,
|
||||
currentNodeParameters: INodeParameters,
|
||||
payload: IDataObject | string | undefined,
|
||||
credentials?: INodeCredentials,
|
||||
): Promise<NodeParameterValueType> {
|
||||
const nodeType = this.getNodeType(nodeTypeAndVersion);
|
||||
const method = this.getMethod('actionHandler', handler, nodeType);
|
||||
const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials);
|
||||
const thisArgs = this.getThisArg(path, additionalData, workflow);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return method.call(thisArgs, payload);
|
||||
}
|
||||
|
||||
private getMethod(
|
||||
type: 'resourceMapping',
|
||||
methodName: string,
|
||||
|
@ -175,9 +195,14 @@ export class DynamicNodeParametersService {
|
|||
methodName: string,
|
||||
nodeType: INodeType,
|
||||
): (this: ILoadOptionsFunctions) => Promise<INodePropertyOptions[]>;
|
||||
private getMethod(
|
||||
type: 'actionHandler',
|
||||
methodName: string,
|
||||
nodeType: INodeType,
|
||||
): (this: ILoadOptionsFunctions, payload?: string) => Promise<NodeParameterValueType>;
|
||||
|
||||
private getMethod(
|
||||
type: 'resourceMapping' | 'listSearch' | 'loadOptions',
|
||||
type: 'resourceMapping' | 'listSearch' | 'loadOptions' | 'actionHandler',
|
||||
methodName: string,
|
||||
nodeType: INodeType,
|
||||
) {
|
||||
|
|
|
@ -1553,6 +1553,11 @@ export declare namespace DynamicNodeParameters {
|
|||
interface ResourceMapperFieldsRequest extends BaseRequest {
|
||||
methodName: string;
|
||||
}
|
||||
|
||||
interface ActionResultRequest extends BaseRequest {
|
||||
handler: string;
|
||||
payload: IDataObject | string | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export interface EnvironmentVariable {
|
||||
|
|
|
@ -5,6 +5,7 @@ import type {
|
|||
INodePropertyOptions,
|
||||
INodeTypeDescription,
|
||||
INodeTypeNameVersion,
|
||||
NodeParameterValueType,
|
||||
ResourceMapperFields,
|
||||
} from 'n8n-workflow';
|
||||
import axios from 'axios';
|
||||
|
@ -57,3 +58,15 @@ export async function getResourceMapperFields(
|
|||
sendData,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getNodeParameterActionResult(
|
||||
context: IRestApiContext,
|
||||
sendData: DynamicNodeParameters.ActionResultRequest,
|
||||
): Promise<NodeParameterValueType> {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'POST',
|
||||
'/dynamic-node-parameters/action-result',
|
||||
sendData,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,281 @@
|
|||
<script setup lang="ts">
|
||||
import { ApplicationError, type INodeProperties, type NodePropertyAction } from 'n8n-workflow';
|
||||
import type { INodeUi, IUpdateInformation } from '@/Interface';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { N8nButton, N8nInput, N8nTooltip } from 'n8n-design-system/components';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { getSchemas, getParentNodes } from './utils';
|
||||
import { ASK_AI_EXPERIMENT } from '@/constants';
|
||||
import { usePostHog } from '@/stores/posthog.store';
|
||||
import { useRootStore } from '@/stores/root.store';
|
||||
import { generateCodeForPrompt } from '@/api/ai';
|
||||
|
||||
import { format } from 'prettier';
|
||||
import jsParser from 'prettier/plugins/babel';
|
||||
import * as estree from 'prettier/plugins/estree';
|
||||
|
||||
const emit = defineEmits<{
|
||||
valueChanged: [value: IUpdateInformation];
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
parameter: INodeProperties;
|
||||
value: string;
|
||||
path: string;
|
||||
}>();
|
||||
|
||||
const posthog = usePostHog();
|
||||
const rootStore = useRootStore();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const prompt = ref(props.value);
|
||||
const parentNodes = ref<INodeUi[]>([]);
|
||||
|
||||
const hasExecutionData = computed(() => (useNDVStore().ndvInputData || []).length > 0);
|
||||
const hasInputField = computed(() => props.parameter.typeOptions?.buttonConfig?.hasInputField);
|
||||
const inputFieldMaxLength = computed(
|
||||
() => props.parameter.typeOptions?.buttonConfig?.inputFieldMaxLength,
|
||||
);
|
||||
const buttonLabel = computed(
|
||||
() => props.parameter.typeOptions?.buttonConfig?.label ?? props.parameter.displayName,
|
||||
);
|
||||
const isSubmitEnabled = computed(() => {
|
||||
if (!hasExecutionData.value) return false;
|
||||
if (!prompt.value) return false;
|
||||
|
||||
const maxlength = inputFieldMaxLength.value;
|
||||
if (maxlength && prompt.value.length > maxlength) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
function startLoading() {
|
||||
isLoading.value = true;
|
||||
}
|
||||
|
||||
function stopLoading() {
|
||||
setTimeout(() => {
|
||||
isLoading.value = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function getPath(parameter: string) {
|
||||
return (props.path ? `${props.path}.` : '') + parameter;
|
||||
}
|
||||
|
||||
function createPrompt(prompt: string) {
|
||||
return `
|
||||
Important! The original input must remain unchanged. If there is a risk of modifying the original input, create a copy of it before making any changes. Use appropriate methods to ensure that the properties of objects are not directly altered.
|
||||
|
||||
Always return an array
|
||||
|
||||
${prompt}
|
||||
`;
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
const { activeNode } = useNDVStore();
|
||||
const { showMessage } = useToast();
|
||||
const action: string | NodePropertyAction | undefined =
|
||||
props.parameter.typeOptions?.buttonConfig?.action;
|
||||
|
||||
if (!action || !activeNode) return;
|
||||
|
||||
if (typeof action === 'string') {
|
||||
switch (action) {
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
emit('valueChanged', {
|
||||
name: getPath(props.parameter.name),
|
||||
value: prompt.value,
|
||||
});
|
||||
|
||||
const { type, target } = action;
|
||||
|
||||
startLoading();
|
||||
|
||||
try {
|
||||
const schemas = getSchemas();
|
||||
const version = rootStore.versionCli;
|
||||
const model =
|
||||
usePostHog().getVariant(ASK_AI_EXPERIMENT.name) === ASK_AI_EXPERIMENT.gpt4
|
||||
? 'gpt-4'
|
||||
: 'gpt-3.5-turbo-16k';
|
||||
|
||||
const payload = {
|
||||
question: createPrompt(prompt.value),
|
||||
context: {
|
||||
schema: schemas.parentNodesSchemas,
|
||||
inputSchema: schemas.inputSchema!,
|
||||
ndvPushRef: useNDVStore().pushRef,
|
||||
pushRef: rootStore.pushRef,
|
||||
},
|
||||
model,
|
||||
n8nVersion: version,
|
||||
};
|
||||
switch (type) {
|
||||
case 'askAiCodeGeneration':
|
||||
let value;
|
||||
if (posthog.isAiEnabled()) {
|
||||
const { restApiContext } = useRootStore();
|
||||
const { code } = await generateCodeForPrompt(restApiContext, payload);
|
||||
value = code;
|
||||
} else {
|
||||
throw new ApplicationError('AI code generation is not enabled');
|
||||
}
|
||||
|
||||
if (value === undefined) return;
|
||||
|
||||
const formattedCode = await format(String(value), {
|
||||
parser: 'babel',
|
||||
plugins: [jsParser, estree],
|
||||
});
|
||||
|
||||
const updateInformation = {
|
||||
name: getPath(target as string),
|
||||
value: formattedCode,
|
||||
};
|
||||
|
||||
emit('valueChanged', updateInformation);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage({
|
||||
type: 'success',
|
||||
title: i18n.baseText('codeNodeEditor.askAi.generationCompleted'),
|
||||
});
|
||||
|
||||
stopLoading();
|
||||
} catch (error) {
|
||||
showMessage({
|
||||
type: 'error',
|
||||
title: i18n.baseText('codeNodeEditor.askAi.generationFailed'),
|
||||
message: error.message,
|
||||
});
|
||||
stopLoading();
|
||||
}
|
||||
}
|
||||
|
||||
function onPromptInput(inputValue: string) {
|
||||
prompt.value = inputValue;
|
||||
emit('valueChanged', {
|
||||
name: getPath(props.parameter.name),
|
||||
value: inputValue,
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
parentNodes.value = getParentNodes();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n8n-input-label
|
||||
v-if="hasInputField"
|
||||
:label="i18n.nodeText().inputLabelDisplayName(parameter, path)"
|
||||
:tooltip-text="i18n.nodeText().inputLabelDescription(parameter, path)"
|
||||
:bold="false"
|
||||
size="small"
|
||||
color="text-dark"
|
||||
>
|
||||
</n8n-input-label>
|
||||
<div :class="$style.inputContainer" :hidden="!hasInputField">
|
||||
<div :class="$style.meta">
|
||||
<span
|
||||
v-if="inputFieldMaxLength"
|
||||
v-show="prompt.length > 1"
|
||||
:class="$style.counter"
|
||||
v-text="`${prompt.length} / ${inputFieldMaxLength}`"
|
||||
/>
|
||||
</div>
|
||||
<N8nInput
|
||||
v-model="prompt"
|
||||
:class="$style.input"
|
||||
style="border: 1px solid var(--color-foreground-base)"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
:maxlength="inputFieldMaxLength"
|
||||
:placeholder="parameter.placeholder"
|
||||
@input="onPromptInput"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.controls">
|
||||
<N8nTooltip :disabled="isSubmitEnabled">
|
||||
<div>
|
||||
<N8nButton
|
||||
:disabled="!isSubmitEnabled"
|
||||
size="small"
|
||||
:loading="isLoading"
|
||||
type="secondary"
|
||||
@click="onSubmit"
|
||||
>
|
||||
{{ buttonLabel }}
|
||||
</N8nButton>
|
||||
</div>
|
||||
<template #content>
|
||||
<span
|
||||
v-if="!hasExecutionData"
|
||||
v-text="i18n.baseText('codeNodeEditor.askAi.noInputData')"
|
||||
/>
|
||||
<span
|
||||
v-else-if="prompt.length === 0"
|
||||
v-text="i18n.baseText('codeNodeEditor.askAi.noPrompt')"
|
||||
/>
|
||||
</template>
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.input * {
|
||||
border: 0 !important;
|
||||
}
|
||||
.input textarea {
|
||||
font-size: var(--font-size-2xs);
|
||||
padding-bottom: var(--spacing-2xl);
|
||||
font-family: var(--font-family);
|
||||
resize: none;
|
||||
}
|
||||
.intro {
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-2xs);
|
||||
color: var(--color-text-dark);
|
||||
padding: var(--spacing-2xs) 0 0;
|
||||
}
|
||||
.inputContainer {
|
||||
position: relative;
|
||||
}
|
||||
.meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
bottom: var(--spacing-2xs);
|
||||
left: var(--spacing-xs);
|
||||
right: var(--spacing-xs);
|
||||
z-index: 1;
|
||||
|
||||
* {
|
||||
font-size: var(--font-size-2xs);
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
.counter {
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
.controls {
|
||||
padding: var(--spacing-2xs) 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
46
packages/editor-ui/src/components/ButtonParameter/utils.ts
Normal file
46
packages/editor-ui/src/components/ButtonParameter/utils.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import type { Schema } from '@/Interface';
|
||||
import type { INodeExecutionData } from 'n8n-workflow';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useDataSchema } from '@/composables/useDataSchema';
|
||||
import { executionDataToJson } from '@/utils/nodeTypesUtils';
|
||||
|
||||
export function getParentNodes() {
|
||||
const activeNode = useNDVStore().activeNode;
|
||||
const { getCurrentWorkflow, getNodeByName } = useWorkflowsStore();
|
||||
const workflow = getCurrentWorkflow();
|
||||
|
||||
if (!activeNode || !workflow) return [];
|
||||
|
||||
return workflow
|
||||
.getParentNodesByDepth(activeNode?.name)
|
||||
.filter(({ name }, i, nodes) => {
|
||||
return name !== activeNode.name && nodes.findIndex((node) => node.name === name) === i;
|
||||
})
|
||||
.map((n) => getNodeByName(n.name))
|
||||
.filter((n) => n !== null);
|
||||
}
|
||||
|
||||
export function getSchemas() {
|
||||
const parentNodes = getParentNodes();
|
||||
const parentNodesNames = parentNodes.map((node) => node?.name);
|
||||
const { getSchemaForExecutionData, getInputDataWithPinned } = useDataSchema();
|
||||
const parentNodesSchemas: Array<{ nodeName: string; schema: Schema }> = parentNodes
|
||||
.map((node) => {
|
||||
const inputData: INodeExecutionData[] = getInputDataWithPinned(node);
|
||||
|
||||
return {
|
||||
nodeName: node?.name || '',
|
||||
schema: getSchemaForExecutionData(executionDataToJson(inputData), true),
|
||||
};
|
||||
})
|
||||
.filter((node) => node.schema?.value.length > 0);
|
||||
|
||||
const inputSchema = parentNodesSchemas.shift();
|
||||
|
||||
return {
|
||||
parentNodesNames,
|
||||
inputSchema,
|
||||
parentNodesSchemas,
|
||||
};
|
||||
}
|
|
@ -60,13 +60,12 @@ import jsParser from 'prettier/plugins/babel';
|
|||
import * as estree from 'prettier/plugins/estree';
|
||||
import { type Ref, computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { ASK_AI_EXPERIMENT, CODE_NODE_TYPE } from '@/constants';
|
||||
import { CODE_NODE_TYPE } from '@/constants';
|
||||
import { codeNodeEditorEventBus } from '@/event-bus';
|
||||
import { useRootStore } from '@/stores/root.store';
|
||||
import { usePostHog } from '@/stores/posthog.store';
|
||||
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import AskAI from './AskAI/AskAI.vue';
|
||||
import { readOnlyEditorExtensions, writableEditorExtensions } from './baseExtensions';
|
||||
import { useCompleter } from './completer';
|
||||
|
@ -114,7 +113,6 @@ const { autocompletionExtension } = useCompleter(() => props.mode, editor);
|
|||
const { createLinter } = useLinter(() => props.mode, editor);
|
||||
|
||||
const rootStore = useRootStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const posthog = usePostHog();
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
|
@ -194,13 +192,7 @@ onBeforeUnmount(() => {
|
|||
});
|
||||
|
||||
const aiEnabled = computed(() => {
|
||||
const isAiExperimentEnabled = [ASK_AI_EXPERIMENT.gpt3, ASK_AI_EXPERIMENT.gpt4].includes(
|
||||
(posthog.getVariant(ASK_AI_EXPERIMENT.name) ?? '') as string,
|
||||
);
|
||||
|
||||
return (
|
||||
isAiExperimentEnabled && settingsStore.settings.ai.enabled && props.language === 'javaScript'
|
||||
);
|
||||
return posthog.isAiEnabled() && props.language === 'javaScript';
|
||||
});
|
||||
|
||||
const placeholder = computed(() => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div :class="$style.editor">
|
||||
<div :class="$style.editor" :style="isReadOnly ? 'opacity: 0.7' : ''">
|
||||
<div ref="jsEditorRef" class="ph-no-capture js-editor"></div>
|
||||
<slot name="suffix" />
|
||||
</div>
|
||||
|
@ -21,7 +21,7 @@ import {
|
|||
keymap,
|
||||
lineNumbers,
|
||||
} from '@codemirror/view';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
autocompleteKeyMap,
|
||||
|
@ -45,11 +45,37 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
onMounted(() => {
|
||||
createEditor();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue: string) => {
|
||||
const editorValue = editor.value?.state?.doc.toString();
|
||||
|
||||
// If model value changes from outside the component
|
||||
if (
|
||||
editorValue !== undefined &&
|
||||
editorValue.length !== newValue.length &&
|
||||
editorValue !== newValue
|
||||
) {
|
||||
destroyEditor();
|
||||
createEditor();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function createEditor() {
|
||||
const state = EditorState.create({ doc: props.modelValue, extensions: extensions.value });
|
||||
const parent = jsEditorRef.value;
|
||||
|
||||
editor.value = new EditorView({ parent, state });
|
||||
editorState.value = editor.value.state;
|
||||
});
|
||||
}
|
||||
|
||||
function destroyEditor() {
|
||||
editor.value?.destroy();
|
||||
}
|
||||
|
||||
const jsEditorRef = ref<HTMLDivElement>();
|
||||
const editor = ref<EditorView | null>(null);
|
||||
|
|
|
@ -9,6 +9,7 @@ import type {
|
|||
import {
|
||||
AI_CATEGORY_AGENTS,
|
||||
AI_SUBCATEGORY,
|
||||
AI_TRANSFORM_NODE_TYPE,
|
||||
CORE_NODES_CATEGORY,
|
||||
DEFAULT_SUBCATEGORY,
|
||||
} from '@/constants';
|
||||
|
@ -19,6 +20,8 @@ import type { NodeViewItemSection } from './viewsData';
|
|||
import { i18n } from '@/plugins/i18n';
|
||||
import { sortBy } from 'lodash-es';
|
||||
|
||||
import { usePostHog } from '@/stores/posthog.store';
|
||||
|
||||
export function transformNodeType(
|
||||
node: SimplifiedNodeType,
|
||||
subcategory?: string,
|
||||
|
@ -74,6 +77,11 @@ export function sortNodeCreateElements(nodes: INodeCreateElement[]) {
|
|||
}
|
||||
|
||||
export function searchNodes(searchFilter: string, items: INodeCreateElement[]) {
|
||||
const aiEnabled = usePostHog().isAiEnabled();
|
||||
if (!aiEnabled) {
|
||||
items = items.filter((item) => item.key !== AI_TRANSFORM_NODE_TYPE);
|
||||
}
|
||||
|
||||
// In order to support the old search we need to remove the 'trigger' part
|
||||
const trimmedFilter = searchFilter.toLowerCase().replace('trigger', '').trimEnd();
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
AI_TRANSFORM_NODE_TYPE,
|
||||
CORE_NODES_CATEGORY,
|
||||
WEBHOOK_NODE_TYPE,
|
||||
OTHER_TRIGGER_NODES_SUBCATEGORY,
|
||||
|
@ -63,6 +64,7 @@ import { NodeConnectionType } from 'n8n-workflow';
|
|||
import { useTemplatesStore } from '@/stores/templates.store';
|
||||
import type { BaseTextKey } from '@/plugins/i18n';
|
||||
import { camelCase } from 'lodash-es';
|
||||
import { usePostHog } from '@/stores/posthog.store';
|
||||
|
||||
export interface NodeViewItemSection {
|
||||
key: string;
|
||||
|
@ -429,6 +431,13 @@ export function TriggerView() {
|
|||
export function RegularView(nodes: SimplifiedNodeType[]) {
|
||||
const i18n = useI18n();
|
||||
|
||||
const popularItemsSubcategory = [SET_NODE_TYPE, CODE_NODE_TYPE, DATETIME_NODE_TYPE];
|
||||
const aiEnabled = usePostHog().isAiEnabled();
|
||||
|
||||
if (aiEnabled) {
|
||||
popularItemsSubcategory.push(AI_TRANSFORM_NODE_TYPE);
|
||||
}
|
||||
|
||||
const view: NodeView = {
|
||||
value: REGULAR_NODE_CREATOR_VIEW,
|
||||
title: i18n.baseText('nodeCreator.triggerHelperPanel.whatHappensNext'),
|
||||
|
@ -453,7 +462,7 @@ export function RegularView(nodes: SimplifiedNodeType[]) {
|
|||
{
|
||||
key: 'popular',
|
||||
title: i18n.baseText('nodeCreator.sectionNames.popular'),
|
||||
items: [SET_NODE_TYPE, CODE_NODE_TYPE, DATETIME_NODE_TYPE],
|
||||
items: popularItemsSubcategory,
|
||||
},
|
||||
{
|
||||
key: 'addOrRemove',
|
||||
|
|
|
@ -136,13 +136,14 @@
|
|||
:model-value="modelValueString"
|
||||
:default-value="parameter.default"
|
||||
:language="editorLanguage"
|
||||
:is-read-only="isReadOnly"
|
||||
:is-read-only="isReadOnly || editorIsReadOnly"
|
||||
:rows="editorRows"
|
||||
:ai-button-enabled="settingsStore.isCloudDeployment"
|
||||
@update:model-value="valueChangedDebounced"
|
||||
>
|
||||
<template #suffix>
|
||||
<n8n-icon
|
||||
v-if="!editorIsReadOnly"
|
||||
data-test-id="code-editor-fullscreen-button"
|
||||
icon="external-link-alt"
|
||||
size="xsmall"
|
||||
|
@ -200,12 +201,13 @@
|
|||
v-else-if="editorType === 'jsEditor'"
|
||||
:key="'js-' + codeEditDialogVisible.toString()"
|
||||
:model-value="modelValueString"
|
||||
:is-read-only="isReadOnly"
|
||||
:is-read-only="isReadOnly || editorIsReadOnly"
|
||||
:rows="editorRows"
|
||||
@update:model-value="valueChangedDebounced"
|
||||
>
|
||||
<template #suffix>
|
||||
<n8n-icon
|
||||
v-if="!editorIsReadOnly"
|
||||
data-test-id="code-editor-fullscreen-button"
|
||||
icon="external-link-alt"
|
||||
size="xsmall"
|
||||
|
@ -861,6 +863,9 @@ const getIssues = computed<string[]>(() => {
|
|||
const editorType = computed<EditorType | 'json' | 'code'>(() => {
|
||||
return getArgument<EditorType>('editor');
|
||||
});
|
||||
const editorIsReadOnly = computed<boolean>(() => {
|
||||
return getArgument<boolean>('editorIsReadOnly') ?? false;
|
||||
});
|
||||
|
||||
const editorLanguage = computed<CodeNodeEditorLanguage>(() => {
|
||||
if (editorType.value === 'json' || props.parameter.type === 'json')
|
||||
|
|
|
@ -35,14 +35,15 @@
|
|||
@action="onNoticeAction"
|
||||
/>
|
||||
|
||||
<n8n-button
|
||||
v-else-if="parameter.type === 'button'"
|
||||
class="parameter-item"
|
||||
block
|
||||
@click="onButtonAction(parameter)"
|
||||
>
|
||||
{{ $locale.nodeText().inputLabelDisplayName(parameter, path) }}
|
||||
</n8n-button>
|
||||
<div v-else-if="parameter.type === 'button'" class="parameter-item">
|
||||
<ButtonParameter
|
||||
:parameter="parameter"
|
||||
:path="path"
|
||||
:value="getParameterValue(parameter.name)"
|
||||
:is-read-only="isReadOnly"
|
||||
@value-changed="valueChanged"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="['collection', 'fixedCollection'].includes(parameter.type)"
|
||||
|
@ -177,6 +178,7 @@ import AssignmentCollection from '@/components/AssignmentCollection/AssignmentCo
|
|||
import FilterConditions from '@/components/FilterConditions/FilterConditions.vue';
|
||||
import ImportCurlParameter from '@/components/ImportCurlParameter.vue';
|
||||
import MultipleParameter from '@/components/MultipleParameter.vue';
|
||||
import ButtonParameter from '@/components/ButtonParameter/ButtonParameter.vue';
|
||||
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
|
@ -483,14 +485,6 @@ function onNoticeAction(action: string) {
|
|||
* Handles default node button parameter type actions
|
||||
* @param parameter
|
||||
*/
|
||||
function onButtonAction(parameter: INodeProperties) {
|
||||
const action: string | undefined = parameter.typeOptions?.action;
|
||||
|
||||
switch (action) {
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldHideAuthRelatedParameter(parameter: INodeProperties): boolean {
|
||||
// TODO: For now, hide all fields that are used in authentication fields displayOptions
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import ButtonParameter from '@/components/ButtonParameter/ButtonParameter.vue';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { usePostHog } from '@/stores/posthog.store';
|
||||
import { useRootStore } from '@/stores/root.store';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
vi.mock('@/stores/ndv.store');
|
||||
vi.mock('@/stores/workflows.store');
|
||||
vi.mock('@/stores/posthog.store');
|
||||
vi.mock('@/stores/root.store');
|
||||
vi.mock('@/api/ai');
|
||||
vi.mock('@/composables/useI18n');
|
||||
vi.mock('@/composables/useToast');
|
||||
|
||||
describe('ButtonParameter', () => {
|
||||
const defaultProps = {
|
||||
parameter: {
|
||||
name: 'testParam',
|
||||
displayName: 'Test Parameter',
|
||||
type: 'string',
|
||||
default: '',
|
||||
typeOptions: {
|
||||
buttonConfig: {
|
||||
label: 'Generate',
|
||||
action: {
|
||||
type: 'askAiCodeGeneration',
|
||||
target: 'targetParam',
|
||||
},
|
||||
hasInputField: true,
|
||||
},
|
||||
},
|
||||
} as INodeProperties,
|
||||
value: '',
|
||||
path: 'testPath',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useNDVStore).mockReturnValue({
|
||||
ndvInputData: [{}],
|
||||
activeNode: { name: 'TestNode' },
|
||||
} as any);
|
||||
|
||||
vi.mocked(useWorkflowsStore).mockReturnValue({
|
||||
getCurrentWorkflow: vi.fn().mockReturnValue({
|
||||
getParentNodesByDepth: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
getNodeByName: vi.fn().mockReturnValue({}),
|
||||
} as any);
|
||||
|
||||
vi.mocked(usePostHog).mockReturnValue({
|
||||
isAiEnabled: vi.fn().mockReturnValue(true),
|
||||
getVariant: vi.fn().mockReturnValue('gpt-3.5-turbo-16k'),
|
||||
} as any);
|
||||
|
||||
vi.mocked(useRootStore).mockReturnValue({
|
||||
versionCli: '1.0.0',
|
||||
pushRef: 'testPushRef',
|
||||
} as any);
|
||||
|
||||
vi.mocked(useI18n).mockReturnValue({
|
||||
baseText: vi.fn().mockReturnValue('Mocked Text'),
|
||||
nodeText: () => ({
|
||||
inputLabelDisplayName: vi.fn().mockReturnValue('Mocked Display Name'),
|
||||
inputLabelDescription: vi.fn().mockReturnValue('Mocked Description'),
|
||||
}),
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToast).mockReturnValue({
|
||||
showMessage: vi.fn(),
|
||||
} as any);
|
||||
});
|
||||
|
||||
const mountComponent = (props = defaultProps) => {
|
||||
return mount(ButtonParameter, {
|
||||
props,
|
||||
global: {
|
||||
plugins: [createTestingPinia()],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it('renders correctly', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.find('textarea').exists()).toBe(true);
|
||||
expect(wrapper.find('button').text()).toBe('Generate');
|
||||
});
|
||||
|
||||
it('emits valueChanged event on input', async () => {
|
||||
const wrapper = mountComponent();
|
||||
const input = wrapper.find('textarea');
|
||||
await input.setValue('Test prompt');
|
||||
expect(wrapper.emitted('valueChanged')).toBeTruthy();
|
||||
expect(wrapper.emitted('valueChanged')![0][0]).toEqual({
|
||||
name: 'testPath.testParam',
|
||||
value: 'Test prompt',
|
||||
});
|
||||
});
|
||||
|
||||
it('disables submit button when there is no execution data', async () => {
|
||||
vi.mocked(useNDVStore).mockReturnValue({
|
||||
ndvInputData: [],
|
||||
} as any);
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.find('button').attributes('disabled')).toBeDefined();
|
||||
});
|
||||
|
||||
it('disables submit button when prompt is empty', async () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.find('button').attributes('disabled')).toBeDefined();
|
||||
});
|
||||
|
||||
it('enables submit button when there is execution data and prompt', async () => {
|
||||
const wrapper = mountComponent();
|
||||
await wrapper.find('textarea').setValue('Test prompt');
|
||||
expect(wrapper.find('button').attributes('disabled')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('calls onSubmit when button is clicked', async () => {
|
||||
const wrapper = mountComponent();
|
||||
await wrapper.find('textarea').setValue('Test prompt');
|
||||
|
||||
const submitButton = wrapper.find('button');
|
||||
expect(submitButton.attributes('disabled')).toBeUndefined();
|
||||
|
||||
await submitButton.trigger('click');
|
||||
|
||||
expect(useToast().showMessage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -191,6 +191,7 @@ export const CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE =
|
|||
'@n8n/n8n-nodes-langchain.chainSummarization';
|
||||
export const SIMULATE_NODE_TYPE = 'n8n-nodes-base.simulate';
|
||||
export const SIMULATE_TRIGGER_NODE_TYPE = 'n8n-nodes-base.simulateTrigger';
|
||||
export const AI_TRANSFORM_NODE_TYPE = 'n8n-nodes-base.aiTransform';
|
||||
|
||||
export const CREDENTIAL_ONLY_NODE_PREFIX = 'n8n-creds-base';
|
||||
export const CREDENTIAL_ONLY_HTTP_NODE_VERSION = 4.1;
|
||||
|
@ -210,7 +211,11 @@ export const NON_ACTIVATABLE_TRIGGER_NODE_TYPES = [
|
|||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
];
|
||||
|
||||
export const NODES_USING_CODE_NODE_EDITOR = [CODE_NODE_TYPE, AI_CODE_NODE_TYPE];
|
||||
export const NODES_USING_CODE_NODE_EDITOR = [
|
||||
CODE_NODE_TYPE,
|
||||
AI_CODE_NODE_TYPE,
|
||||
AI_TRANSFORM_NODE_TYPE,
|
||||
];
|
||||
|
||||
export const PIN_DATA_NODE_TYPES_DENYLIST = [SPLIT_IN_BATCHES_NODE_TYPE, STICKY_NODE_TYPE];
|
||||
|
||||
|
|
|
@ -303,6 +303,12 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
|||
}
|
||||
};
|
||||
|
||||
const getNodeParameterActionResult = async (
|
||||
sendData: DynamicNodeParameters.ActionResultRequest,
|
||||
) => {
|
||||
return await nodeTypesApi.getNodeParameterActionResult(rootStore.restApiContext, sendData);
|
||||
};
|
||||
|
||||
// #endregion
|
||||
|
||||
return {
|
||||
|
@ -321,6 +327,7 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
|||
visibleNodeTypesByInputConnectionTypeNames,
|
||||
isConfigurableNode,
|
||||
getResourceMapperFields,
|
||||
getNodeParameterActionResult,
|
||||
getResourceLocatorResults,
|
||||
getNodeParameterOptions,
|
||||
getNodesInformation,
|
||||
|
|
|
@ -6,7 +6,11 @@ import { useUsersStore } from '@/stores/users.store';
|
|||
import { useRootStore } from '@/stores/root.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import type { FeatureFlags, IDataObject } from 'n8n-workflow';
|
||||
import { EXPERIMENTS_TO_TRACK, LOCAL_STORAGE_EXPERIMENT_OVERRIDES } from '@/constants';
|
||||
import {
|
||||
ASK_AI_EXPERIMENT,
|
||||
EXPERIMENTS_TO_TRACK,
|
||||
LOCAL_STORAGE_EXPERIMENT_OVERRIDES,
|
||||
} from '@/constants';
|
||||
import { useTelemetryStore } from './telemetry.store';
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
|
||||
|
@ -38,6 +42,14 @@ export const usePostHog = defineStore('posthog', () => {
|
|||
return overrides.value[experiment] ?? featureFlags.value?.[experiment];
|
||||
};
|
||||
|
||||
const isAiEnabled = () => {
|
||||
const isAiExperimentEnabled = [ASK_AI_EXPERIMENT.gpt3, ASK_AI_EXPERIMENT.gpt4].includes(
|
||||
(getVariant(ASK_AI_EXPERIMENT.name) ?? '') as string,
|
||||
);
|
||||
|
||||
return isAiExperimentEnabled && settingsStore.settings.ai.enabled;
|
||||
};
|
||||
|
||||
const isVariantEnabled = (experiment: string, variant: string) => {
|
||||
return getVariant(experiment) === variant;
|
||||
};
|
||||
|
@ -183,6 +195,7 @@ export const usePostHog = defineStore('posthog', () => {
|
|||
|
||||
return {
|
||||
init,
|
||||
isAiEnabled,
|
||||
isFeatureEnabled,
|
||||
isVariantEnabled,
|
||||
getVariant,
|
||||
|
|
18
packages/nodes-base/nodes/AiTransform/AiTransform.node.json
Normal file
18
packages/nodes-base/nodes/AiTransform/AiTransform.node.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"node": "n8n-nodes-base.aiTransform",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"details": "Modify data by writing a prompt",
|
||||
"categories": ["Development", "Core Nodes"],
|
||||
"resources": {
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.aitransform/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"alias": ["code", "Javascript", "JS", "Script", "Custom Code", "Function", "AI", "LLM"],
|
||||
"subcategories": {
|
||||
"Core Nodes": ["Data Transformation"]
|
||||
}
|
||||
}
|
148
packages/nodes-base/nodes/AiTransform/AiTransform.node.ts
Normal file
148
packages/nodes-base/nodes/AiTransform/AiTransform.node.ts
Normal file
|
@ -0,0 +1,148 @@
|
|||
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
|
||||
import {
|
||||
NodeOperationError,
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import set from 'lodash/set';
|
||||
|
||||
import { JavaScriptSandbox } from '../Code/JavaScriptSandbox';
|
||||
import { getSandboxContext } from '../Code/Sandbox';
|
||||
import { standardizeOutput } from '../Code/utils';
|
||||
|
||||
const { CODE_ENABLE_STDOUT } = process.env;
|
||||
|
||||
export class AiTransform implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'AI Transform',
|
||||
name: 'aiTransform',
|
||||
icon: 'file:aitransform.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Modify data based on instructions written in plain english',
|
||||
defaults: {
|
||||
name: 'AI Transform',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
parameterPane: 'wide',
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Instructions',
|
||||
name: 'generate',
|
||||
type: 'button',
|
||||
default: '',
|
||||
description:
|
||||
"Provide instructions on how you want to transform the data, then click 'Generate code'. Use dot notation to refer to nested fields (e.g. address.street).",
|
||||
placeholder:
|
||||
"Example: Merge 'firstname' and 'lastname' into a field 'details.name' and sort by 'email'",
|
||||
typeOptions: {
|
||||
buttonConfig: {
|
||||
label: 'Generate code',
|
||||
hasInputField: true,
|
||||
inputFieldMaxLength: 500,
|
||||
action: {
|
||||
type: 'askAiCodeGeneration',
|
||||
target: 'jsCode',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Transformation Code',
|
||||
name: 'jsCode',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
editor: 'jsEditor',
|
||||
editorIsReadOnly: true,
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Read-only. To edit this code, adjust the prompt or copy and paste it into a Code node.',
|
||||
noDataExpression: true,
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
"Click on 'Test step' to run the transformation code. Further executions will use the generated code (and not invoke AI again).",
|
||||
name: 'hint',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
jsCode: [{ _cnd: { exists: true } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions) {
|
||||
const workflowMode = this.getMode();
|
||||
|
||||
const node = this.getNode();
|
||||
|
||||
const codeParameterName = 'jsCode';
|
||||
|
||||
const getSandbox = (index = 0) => {
|
||||
let code = '';
|
||||
try {
|
||||
code = this.getNodeParameter(codeParameterName, index) as string;
|
||||
|
||||
if (!code) {
|
||||
const instructions = this.getNodeParameter('generate', index) as string;
|
||||
if (!instructions) {
|
||||
throw new NodeOperationError(node, 'Missing instructions to generate code', {
|
||||
description:
|
||||
"Enter your prompt in the 'Instructions' parameter and click 'Generate code'",
|
||||
});
|
||||
}
|
||||
throw new NodeOperationError(node, 'Missing code for data transformation', {
|
||||
description: "Click the 'Generate code' button to create the code",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof NodeOperationError) throw error;
|
||||
|
||||
throw new NodeOperationError(node, error);
|
||||
}
|
||||
|
||||
const context = getSandboxContext.call(this, index);
|
||||
|
||||
context.items = context.$input.all();
|
||||
|
||||
const Sandbox = JavaScriptSandbox;
|
||||
const sandbox = new Sandbox(context, code, index, this.helpers);
|
||||
sandbox.on(
|
||||
'output',
|
||||
workflowMode === 'manual'
|
||||
? this.sendMessageToUI
|
||||
: CODE_ENABLE_STDOUT === 'true'
|
||||
? (...args) =>
|
||||
console.log(`[Workflow "${this.getWorkflow().id}"][Node "${node.name}"]`, ...args)
|
||||
: () => {},
|
||||
);
|
||||
return sandbox;
|
||||
};
|
||||
|
||||
const sandbox = getSandbox();
|
||||
let items: INodeExecutionData[];
|
||||
try {
|
||||
items = (await sandbox.runCodeAllItems()) as INodeExecutionData[];
|
||||
} catch (error) {
|
||||
if (!this.continueOnFail(error)) {
|
||||
set(error, 'node', node);
|
||||
throw error;
|
||||
}
|
||||
items = [{ json: { error: error.message } }];
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
standardizeOutput(item.json);
|
||||
}
|
||||
|
||||
return [items];
|
||||
}
|
||||
}
|
10
packages/nodes-base/nodes/AiTransform/aitransform.svg
Normal file
10
packages/nodes-base/nodes/AiTransform/aitransform.svg
Normal file
|
@ -0,0 +1,10 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.4956 19.5195L34.8 18.006C31.7965 17.015 30.0369 14.5846 29.2111 11.5306L27.1447 1.46518C27.0908 1.2663 26.9593 1 26.5548 1C26.2144 1 26.0188 1.2663 25.9649 1.46518L23.8986 11.534C23.0693 14.588 21.3131 17.0184 18.3097 18.0094L13.6141 19.5229C12.95 19.7386 12.9399 20.6757 13.6006 20.9016L18.3299 22.5297C21.3232 23.5241 23.0693 25.9512 23.8986 28.9917L25.9683 38.9458C26.0222 39.1447 26.1335 39.502 26.5582 39.502C26.9977 39.502 27.0906 39.1585 27.1449 38.9577L27.1481 38.9458L29.2178 28.9917C30.047 25.9478 31.7931 23.5208 34.7865 22.5297L39.5158 20.9016C40.1697 20.6724 40.1596 19.7353 39.4956 19.5195ZM12.6028 28.277C9.82036 27.3584 9.54358 26.324 9.07959 24.5899L9.06681 24.5421L7.89038 20.4162C7.81959 20.1499 7.12519 20.1499 7.05103 20.4162L6.25214 24.2286C5.77684 25.9848 4.76558 27.3804 3.0397 27.9501L0.289074 29.1433C-0.0918337 29.2681 -0.0985755 29.8074 0.282332 29.9355L3.05318 30.9164C4.77233 31.4861 5.77684 32.8816 6.25551 34.6311L7.0544 38.2784C7.12856 38.5447 7.81959 38.5447 7.89038 38.2784L8.82748 34.648C9.30277 32.8884 10.0309 31.4895 12.3669 30.9164L14.9692 29.9355C15.3501 29.804 15.3467 29.2647 14.9625 29.14L12.6028 28.277ZM13.3255 2.03231C13.4169 1.65456 13.954 1.64852 14.0473 2.02833L14.0475 2.02889L15.0714 6.25591C15.2111 6.74054 15.5906 7.11618 16.0783 7.2487L18.6822 7.95026C19.0346 8.04918 19.0457 8.54279 18.7009 8.65979L18.7004 8.65996L15.97 9.57684C15.531 9.72415 15.1905 10.0756 15.0538 10.5179L14.0473 14.4327L14.0471 14.4337C13.9524 14.8153 13.4207 14.7996 13.3291 14.4333L12.3559 10.5303C12.2222 10.0878 11.8845 9.73333 11.4489 9.58314L8.72013 8.63869C8.37299 8.51471 8.40082 8.02141 8.75078 7.92932L8.75135 7.92917L11.3368 7.25837C11.8372 7.12876 12.2283 6.74382 12.3616 6.24505L13.3255 2.03231Z" fill="url(#paint0_linear_1373_20357)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1373_20357" x1="0" y1="1" x2="47.0035" y2="13.8158" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5B60E8"/>
|
||||
<stop offset="0.5" stop-color="#AA7BEC"/>
|
||||
<stop offset="1" stop-color="#EC7B8E"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -390,6 +390,7 @@
|
|||
"dist/nodes/AgileCrm/AgileCrm.node.js",
|
||||
"dist/nodes/Airtable/Airtable.node.js",
|
||||
"dist/nodes/Airtable/AirtableTrigger.node.js",
|
||||
"dist/nodes/AiTransform/AiTransform.node.js",
|
||||
"dist/nodes/Amqp/Amqp.node.js",
|
||||
"dist/nodes/Amqp/AmqpTrigger.node.js",
|
||||
"dist/nodes/ApiTemplateIo/ApiTemplateIo.node.js",
|
||||
|
|
|
@ -37,6 +37,7 @@ export const CODE_NODE_TYPE = 'n8n-nodes-base.code';
|
|||
export const FUNCTION_NODE_TYPE = 'n8n-nodes-base.function';
|
||||
export const FUNCTION_ITEM_NODE_TYPE = 'n8n-nodes-base.functionItem';
|
||||
export const MERGE_NODE_TYPE = 'n8n-nodes-base.merge';
|
||||
export const AI_TRANSFORM_NODE_TYPE = 'n8n-nodes-base.aiTransform';
|
||||
|
||||
export const STARTING_NODE_TYPES = [
|
||||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
|
@ -45,7 +46,12 @@ export const STARTING_NODE_TYPES = [
|
|||
START_NODE_TYPE,
|
||||
];
|
||||
|
||||
export const SCRIPTING_NODE_TYPES = [FUNCTION_NODE_TYPE, FUNCTION_ITEM_NODE_TYPE, CODE_NODE_TYPE];
|
||||
export const SCRIPTING_NODE_TYPES = [
|
||||
FUNCTION_NODE_TYPE,
|
||||
FUNCTION_ITEM_NODE_TYPE,
|
||||
CODE_NODE_TYPE,
|
||||
AI_TRANSFORM_NODE_TYPE,
|
||||
];
|
||||
|
||||
/**
|
||||
* Nodes whose parameter values may refer to other nodes without expressions.
|
||||
|
@ -55,6 +61,7 @@ export const NODES_WITH_RENAMABLE_CONTENT = new Set([
|
|||
CODE_NODE_TYPE,
|
||||
FUNCTION_NODE_TYPE,
|
||||
FUNCTION_ITEM_NODE_TYPE,
|
||||
AI_TRANSFORM_NODE_TYPE,
|
||||
]);
|
||||
|
||||
//@n8n/n8n-nodes-langchain
|
||||
|
|
|
@ -1229,12 +1229,25 @@ export interface ILoadOptions {
|
|||
};
|
||||
}
|
||||
|
||||
export type NodePropertyAction = {
|
||||
type: 'askAiCodeGeneration';
|
||||
handler?: string;
|
||||
target?: string;
|
||||
};
|
||||
|
||||
export interface INodePropertyTypeOptions {
|
||||
action?: string; // Supported by: button
|
||||
// Supported by: button
|
||||
buttonConfig?: {
|
||||
action?: string | NodePropertyAction;
|
||||
label?: string; // otherwise "displayName" is used
|
||||
hasInputField?: boolean;
|
||||
inputFieldMaxLength?: number; // Supported if hasInputField is true
|
||||
};
|
||||
containerClass?: string; // Supported by: notice
|
||||
alwaysOpenEditWindow?: boolean; // Supported by: json
|
||||
codeAutocomplete?: CodeAutocompleteTypes; // Supported by: string
|
||||
editor?: EditorType; // Supported by: string
|
||||
editorIsReadOnly?: boolean; // Supported by: string
|
||||
sqlDialect?: SQLDialect; // Supported by: sqlEditor
|
||||
loadOptionsDependsOn?: string[]; // Supported by: options
|
||||
loadOptionsMethod?: string; // Supported by: options
|
||||
|
@ -1526,6 +1539,12 @@ export interface INodeType {
|
|||
resourceMapping?: {
|
||||
[functionName: string]: (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
||||
};
|
||||
actionHandler?: {
|
||||
[functionName: string]: (
|
||||
this: ILoadOptionsFunctions,
|
||||
payload: IDataObject | string | undefined,
|
||||
) => Promise<NodeParameterValueType>;
|
||||
};
|
||||
};
|
||||
webhookMethods?: {
|
||||
[name in IWebhookDescription['name']]?: {
|
||||
|
|
Loading…
Reference in a new issue