mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 21:07:28 -08:00
feat(AI Transform Node): UX improvements (#11280)
Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com>
This commit is contained in:
parent
cb28b5cd60
commit
8a484077af
|
@ -32,7 +32,7 @@ const props = withDefaults(defineProps<InputProps>(), {
|
|||
readonly: false,
|
||||
clearable: false,
|
||||
rows: 2,
|
||||
maxlength: Infinity,
|
||||
maxlength: undefined,
|
||||
title: '',
|
||||
name: () => uid('input'),
|
||||
autocomplete: 'off',
|
||||
|
@ -81,6 +81,7 @@ defineExpose({ focus, blur, select });
|
|||
:clearable="clearable"
|
||||
:rows="rows"
|
||||
:title="title"
|
||||
:maxlength="maxlength"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template v-if="$slots.prepend" #prepend>
|
||||
|
|
|
@ -44,7 +44,7 @@ describe('ButtonParameter', () => {
|
|||
beforeEach(() => {
|
||||
vi.mocked(useNDVStore).mockReturnValue({
|
||||
ndvInputData: [{}],
|
||||
activeNode: { name: 'TestNode' },
|
||||
activeNode: { name: 'TestNode', parameters: {} },
|
||||
} as any);
|
||||
|
||||
vi.mocked(useWorkflowsStore).mockReturnValue({
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import { ApplicationError, type INodeProperties, type NodePropertyAction } from 'n8n-workflow';
|
||||
import { 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 { useRootStore } from '@/stores/root.store';
|
||||
import { getParentNodes, generateCodeForAiTransform } from './utils';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { generateCodeForPrompt } from '@/api/ai';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
|
||||
import { format } from 'prettier';
|
||||
import jsParser from 'prettier/plugins/babel';
|
||||
import * as estree from 'prettier/plugins/estree';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import type { AskAiRequest } from '@/types/assistant.types';
|
||||
const AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT = 'codeGeneratedForPrompt';
|
||||
|
||||
const emit = defineEmits<{
|
||||
valueChanged: [value: IUpdateInformation];
|
||||
|
@ -27,8 +22,7 @@ const props = defineProps<{
|
|||
path: string;
|
||||
}>();
|
||||
|
||||
const rootStore = useRootStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { activeNode } = useNDVStore();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
|
@ -53,6 +47,11 @@ const isSubmitEnabled = computed(() => {
|
|||
|
||||
return true;
|
||||
});
|
||||
const promptUpdated = computed(() => {
|
||||
const lastPrompt = activeNode?.parameters[AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT] as string;
|
||||
if (!lastPrompt) return false;
|
||||
return lastPrompt.trim() !== prompt.value.trim();
|
||||
});
|
||||
|
||||
function startLoading() {
|
||||
isLoading.value = true;
|
||||
|
@ -69,7 +68,6 @@ function getPath(parameter: string) {
|
|||
}
|
||||
|
||||
async function onSubmit() {
|
||||
const { activeNode } = useNDVStore();
|
||||
const { showMessage } = useToast();
|
||||
const action: string | NodePropertyAction | undefined =
|
||||
props.parameter.typeOptions?.buttonConfig?.action;
|
||||
|
@ -93,46 +91,26 @@ async function onSubmit() {
|
|||
startLoading();
|
||||
|
||||
try {
|
||||
const schemas = getSchemas();
|
||||
|
||||
const payload: AskAiRequest.RequestPayload = {
|
||||
question: prompt.value,
|
||||
context: {
|
||||
schema: schemas.parentNodesSchemas,
|
||||
inputSchema: schemas.inputSchema!,
|
||||
ndvPushRef: useNDVStore().pushRef,
|
||||
pushRef: rootStore.pushRef,
|
||||
},
|
||||
forNode: 'transform',
|
||||
};
|
||||
switch (type) {
|
||||
case 'askAiCodeGeneration':
|
||||
let value;
|
||||
if (settingsStore.isAskAiEnabled) {
|
||||
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,
|
||||
};
|
||||
const updateInformation = await generateCodeForAiTransform(
|
||||
prompt.value,
|
||||
getPath(target as string),
|
||||
);
|
||||
if (!updateInformation) return;
|
||||
|
||||
//updade code parameter
|
||||
emit('valueChanged', updateInformation);
|
||||
|
||||
//update code generated for prompt parameter
|
||||
emit('valueChanged', {
|
||||
name: getPath(AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT),
|
||||
value: prompt.value,
|
||||
});
|
||||
|
||||
useTelemetry().trackAiTransform('generationFinished', {
|
||||
prompt: prompt.value,
|
||||
code: formattedCode,
|
||||
code: updateInformation.value,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
@ -168,6 +146,16 @@ function onPromptInput(inputValue: string) {
|
|||
});
|
||||
}
|
||||
|
||||
function useDarkBackdrop(): string {
|
||||
const theme = useUIStore().appliedTheme;
|
||||
|
||||
if (theme === 'light') {
|
||||
return 'background-color: var(--color-background-xlight);';
|
||||
} else {
|
||||
return 'background-color: var(--color-background-light);';
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
parentNodes.value = getParentNodes();
|
||||
});
|
||||
|
@ -185,13 +173,18 @@ onMounted(() => {
|
|||
>
|
||||
</n8n-input-label>
|
||||
<div :class="$style.inputContainer" :hidden="!hasInputField">
|
||||
<div :class="$style.meta">
|
||||
<div :class="$style.meta" :style="useDarkBackdrop()">
|
||||
<span
|
||||
v-if="inputFieldMaxLength"
|
||||
v-show="prompt.length > 1"
|
||||
:class="$style.counter"
|
||||
v-text="`${prompt.length} / ${inputFieldMaxLength}`"
|
||||
/>
|
||||
<span
|
||||
v-if="promptUpdated"
|
||||
:class="$style['warning-text']"
|
||||
v-text="'Instructions changed'"
|
||||
/>
|
||||
</div>
|
||||
<N8nInput
|
||||
v-model="prompt"
|
||||
|
@ -255,9 +248,15 @@ onMounted(() => {
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
bottom: var(--spacing-2xs);
|
||||
padding-bottom: var(--spacing-2xs);
|
||||
padding-top: var(--spacing-2xs);
|
||||
margin: 1px;
|
||||
margin-right: var(--spacing-s);
|
||||
bottom: 0;
|
||||
left: var(--spacing-xs);
|
||||
right: var(--spacing-xs);
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
z-index: 1;
|
||||
|
||||
* {
|
||||
|
@ -267,10 +266,15 @@ onMounted(() => {
|
|||
}
|
||||
.counter {
|
||||
color: var(--color-text-light);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.controls {
|
||||
padding: var(--spacing-2xs) 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.warning-text {
|
||||
color: var(--color-warning);
|
||||
line-height: 1.2;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
import type { Schema } from '@/Interface';
|
||||
import type { INodeExecutionData } from 'n8n-workflow';
|
||||
import { ApplicationError, 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';
|
||||
import { generateCodeForPrompt } from '../../api/ai';
|
||||
import { useRootStore } from '../../stores/root.store';
|
||||
import { type AskAiRequest } from '../../types/assistant.types';
|
||||
import { useSettingsStore } from '../../stores/settings.store';
|
||||
import { format } from 'prettier';
|
||||
import jsParser from 'prettier/plugins/babel';
|
||||
import * as estree from 'prettier/plugins/estree';
|
||||
|
||||
export function getParentNodes() {
|
||||
const activeNode = useNDVStore().activeNode;
|
||||
|
@ -44,3 +51,41 @@ export function getSchemas() {
|
|||
parentNodesSchemas,
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateCodeForAiTransform(prompt: string, path: string) {
|
||||
const schemas = getSchemas();
|
||||
|
||||
const payload: AskAiRequest.RequestPayload = {
|
||||
question: prompt,
|
||||
context: {
|
||||
schema: schemas.parentNodesSchemas,
|
||||
inputSchema: schemas.inputSchema!,
|
||||
ndvPushRef: useNDVStore().pushRef,
|
||||
pushRef: useRootStore().pushRef,
|
||||
},
|
||||
forNode: 'transform',
|
||||
};
|
||||
|
||||
let value;
|
||||
if (useSettingsStore().isAskAiEnabled) {
|
||||
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: path,
|
||||
value: formattedCode,
|
||||
};
|
||||
|
||||
return updateInformation;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,12 @@ import {
|
|||
FORM_TRIGGER_NODE_TYPE,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
import {
|
||||
AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT,
|
||||
AI_TRANSFORM_JS_CODE,
|
||||
AI_TRANSFORM_NODE_TYPE,
|
||||
type INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
|
@ -21,6 +26,8 @@ import { useUIStore } from '@/stores/ui.store';
|
|||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { type IUpdateInformation } from '../Interface';
|
||||
import { generateCodeForAiTransform } from '@/components/ButtonParameter/utils';
|
||||
|
||||
const NODE_TEST_STEP_POPUP_COUNT_KEY = 'N8N_NODE_TEST_STEP_POPUP_COUNT';
|
||||
const MAX_POPUP_COUNT = 10;
|
||||
|
@ -47,6 +54,7 @@ const props = withDefaults(
|
|||
const emit = defineEmits<{
|
||||
stopExecution: [];
|
||||
execute: [];
|
||||
valueChanged: [value: IUpdateInformation];
|
||||
}>();
|
||||
|
||||
defineOptions({
|
||||
|
@ -54,6 +62,7 @@ defineOptions({
|
|||
});
|
||||
|
||||
const lastPopupCountUpdate = ref(0);
|
||||
const codeGenerationInProgress = ref(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { runWorkflow, runWorkflowResolvePending, stopCurrentExecution } = useRunWorkflow({ router });
|
||||
|
@ -76,7 +85,7 @@ const nodeType = computed((): INodeTypeDescription | null => {
|
|||
});
|
||||
|
||||
const isNodeRunning = computed(() => {
|
||||
if (!uiStore.isActionActive['workflowRunning']) return false;
|
||||
if (!uiStore.isActionActive['workflowRunning'] || codeGenerationInProgress.value) return false;
|
||||
const triggeredNode = workflowsStore.executedNode;
|
||||
return (
|
||||
workflowsStore.isNodeExecuting(node.value?.name ?? '') || triggeredNode === node.value?.name
|
||||
|
@ -142,6 +151,10 @@ const disabledHint = computed(() => {
|
|||
return '';
|
||||
}
|
||||
|
||||
if (codeGenerationInProgress.value) {
|
||||
return i18n.baseText('ndv.execute.generatingCode');
|
||||
}
|
||||
|
||||
if (isTriggerNode.value && node?.value?.disabled) {
|
||||
return i18n.baseText('ndv.execute.nodeIsDisabled');
|
||||
}
|
||||
|
@ -163,6 +176,9 @@ const disabledHint = computed(() => {
|
|||
});
|
||||
|
||||
const tooltipText = computed(() => {
|
||||
if (shouldGenerateCode.value) {
|
||||
return i18n.baseText('ndv.execute.generateCodeAndTestNode.description');
|
||||
}
|
||||
if (disabledHint.value) return disabledHint.value;
|
||||
if (props.tooltip && !isLoading.value && testStepButtonPopupCount() < MAX_POPUP_COUNT) {
|
||||
return props.tooltip;
|
||||
|
@ -199,9 +215,37 @@ const buttonLabel = computed(() => {
|
|||
});
|
||||
|
||||
const isLoading = computed(
|
||||
() => isNodeRunning.value && !isListeningForEvents.value && !isListeningForWorkflowEvents.value,
|
||||
() =>
|
||||
codeGenerationInProgress.value ||
|
||||
(isNodeRunning.value && !isListeningForEvents.value && !isListeningForWorkflowEvents.value),
|
||||
);
|
||||
|
||||
const buttonIcon = computed(() => {
|
||||
if (shouldGenerateCode.value) return 'terminal';
|
||||
if (!isListeningForEvents.value && !props.hideIcon) return 'flask';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const shouldGenerateCode = computed(() => {
|
||||
if (node.value?.type !== AI_TRANSFORM_NODE_TYPE) {
|
||||
return false;
|
||||
}
|
||||
if (!node.value?.parameters?.instructions) {
|
||||
return false;
|
||||
}
|
||||
if (!node.value?.parameters?.jsCode) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
node.value?.parameters[AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT] &&
|
||||
(node.value?.parameters?.instructions as string).trim() !==
|
||||
(node.value?.parameters?.[AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT] as string).trim()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
async function stopWaitingForWebhook() {
|
||||
try {
|
||||
await workflowsStore.removeTestWebhook(workflowsStore.workflowId);
|
||||
|
@ -227,6 +271,51 @@ function onMouseOver() {
|
|||
}
|
||||
|
||||
async function onClick() {
|
||||
if (shouldGenerateCode.value) {
|
||||
// Generate code if user hasn't clicked 'Generate Code' button
|
||||
// and update parameters
|
||||
codeGenerationInProgress.value = true;
|
||||
try {
|
||||
toast.showMessage({
|
||||
title: i18n.baseText('ndv.execute.generateCode.title'),
|
||||
message: i18n.baseText('ndv.execute.generateCode.message', {
|
||||
interpolate: { nodeName: node.value?.name as string },
|
||||
}),
|
||||
type: 'success',
|
||||
});
|
||||
const prompt = node.value?.parameters?.instructions as string;
|
||||
const updateInformation = await generateCodeForAiTransform(
|
||||
prompt,
|
||||
`parameters.${AI_TRANSFORM_JS_CODE}`,
|
||||
);
|
||||
if (!updateInformation) return;
|
||||
|
||||
emit('valueChanged', updateInformation);
|
||||
|
||||
emit('valueChanged', {
|
||||
name: `parameters.${AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT}`,
|
||||
value: prompt,
|
||||
});
|
||||
|
||||
useTelemetry().trackAiTransform('generationFinished', {
|
||||
prompt,
|
||||
code: updateInformation.value,
|
||||
});
|
||||
} catch (error) {
|
||||
useTelemetry().trackAiTransform('generationFinished', {
|
||||
prompt,
|
||||
code: '',
|
||||
hasError: true,
|
||||
});
|
||||
toast.showMessage({
|
||||
type: 'error',
|
||||
title: i18n.baseText('codeNodeEditor.askAi.generationFailed'),
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
codeGenerationInProgress.value = false;
|
||||
}
|
||||
|
||||
if (isChatNode.value || (isChatChild.value && ndvStore.isNDVDataEmpty('input'))) {
|
||||
ndvStore.setActiveNodeName(null);
|
||||
nodeViewEventBus.emit('openChat');
|
||||
|
@ -296,7 +385,7 @@ async function onClick() {
|
|||
:label="buttonLabel"
|
||||
:type="type"
|
||||
:size="size"
|
||||
:icon="!isListeningForEvents && !hideIcon ? 'flask' : undefined"
|
||||
:icon="buttonIcon"
|
||||
:transparent-background="transparent"
|
||||
:title="
|
||||
!isTriggerNode && !tooltipText ? i18n.baseText('ndv.execute.testNode.description') : ''
|
||||
|
|
|
@ -974,6 +974,7 @@ onBeforeUnmount(() => {
|
|||
telemetry-source="parameters"
|
||||
@execute="onNodeExecute"
|
||||
@stop-execution="onStopExecution"
|
||||
@value-changed="valueChanged"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
|
|||
import { within } from '@testing-library/vue';
|
||||
import { waitFor } from '@testing-library/vue';
|
||||
import ParameterOptions from './ParameterOptions.vue';
|
||||
import { setActivePinia, createPinia } from 'pinia';
|
||||
|
||||
const DEFAULT_PARAMETER = {
|
||||
displayName: 'Fields to Set',
|
||||
|
@ -12,6 +13,17 @@ const DEFAULT_PARAMETER = {
|
|||
};
|
||||
|
||||
describe('ParameterOptions', () => {
|
||||
let pinia: ReturnType<typeof createPinia>;
|
||||
|
||||
beforeEach(async () => {
|
||||
pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders default options properly', () => {
|
||||
const { getByTestId } = renderComponent(ParameterOptions, {
|
||||
props: {
|
||||
|
|
|
@ -4,6 +4,8 @@ import type { INodeProperties, NodeParameterValueType } from 'n8n-workflow';
|
|||
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
||||
import { isValueExpression } from '@/utils/nodeTypesUtils';
|
||||
import { computed } from 'vue';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { AI_TRANSFORM_NODE_TYPE } from '@/constants';
|
||||
|
||||
interface Props {
|
||||
parameter: INodeProperties;
|
||||
|
@ -59,10 +61,19 @@ const shouldShowOptions = computed(() => {
|
|||
return false;
|
||||
});
|
||||
const selectedView = computed(() => (isValueAnExpression.value ? 'expression' : 'fixed'));
|
||||
const activeNode = computed(() => useNDVStore().activeNode);
|
||||
const hasRemoteMethod = computed(
|
||||
() =>
|
||||
!!props.parameter.typeOptions?.loadOptionsMethod || !!props.parameter.typeOptions?.loadOptions,
|
||||
);
|
||||
const resetValueLabel = computed(() => {
|
||||
if (activeNode.value && [AI_TRANSFORM_NODE_TYPE].includes(activeNode.value.type)) {
|
||||
return i18n.baseText('parameterInput.clearContents');
|
||||
}
|
||||
|
||||
return i18n.baseText('parameterInput.resetValue');
|
||||
});
|
||||
|
||||
const actions = computed(() => {
|
||||
if (Array.isArray(props.customActions) && props.customActions.length > 0) {
|
||||
return props.customActions;
|
||||
|
@ -79,7 +90,7 @@ const actions = computed(() => {
|
|||
|
||||
const parameterActions = [
|
||||
{
|
||||
label: i18n.baseText('parameterInput.resetValue'),
|
||||
label: resetValueLabel.value,
|
||||
value: 'resetValue',
|
||||
disabled: isDefault.value,
|
||||
},
|
||||
|
|
|
@ -913,9 +913,13 @@
|
|||
"ndv.backToCanvas.waitingForTriggerWarning": "Waiting for a Trigger node to execute. Close this view to see the Workflow Canvas.",
|
||||
"ndv.execute.testNode": "Test step",
|
||||
"ndv.execute.testNode.description": "Runs the current node. Will also run previous nodes if they have not been run yet",
|
||||
"ndv.execute.generateCodeAndTestNode.description": "Generates code and then runs the current node",
|
||||
"ndv.execute.generateCode.message": "The instructions in '{nodeName}' have changed",
|
||||
"ndv.execute.generateCode.title": "Generating transformation code",
|
||||
"ndv.execute.executing": "Executing",
|
||||
"ndv.execute.fetchEvent": "Fetch Test Event",
|
||||
"ndv.execute.fixPrevious": "Fix previous node first",
|
||||
"ndv.execute.generatingCode": "Genereting code from instructions",
|
||||
"ndv.execute.listenForTestEvent": "Listen for test event",
|
||||
"ndv.execute.testChat": "Test chat",
|
||||
"ndv.execute.testStep": "Test step",
|
||||
|
@ -1383,6 +1387,7 @@
|
|||
"parameterInput.parameterHasIssues": "Parameter: \"{shortPath}\" has issues",
|
||||
"parameterInput.parameterHasIssuesAndExpression": "Parameter: \"{shortPath}\" has issues and an expression",
|
||||
"parameterInput.refreshList": "Refresh List",
|
||||
"parameterInput.clearContents": "Clear Contents",
|
||||
"parameterInput.resetValue": "Reset Value",
|
||||
"parameterInput.select": "Select",
|
||||
"parameterInput.selectDateAndTime": "Select date and time",
|
||||
|
|
|
@ -5,6 +5,8 @@ import {
|
|||
type INodeExecutionData,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT,
|
||||
AI_TRANSFORM_JS_CODE,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import set from 'lodash/set';
|
||||
|
@ -46,36 +48,29 @@ export class AiTransform implements INodeType {
|
|||
inputFieldMaxLength: 500,
|
||||
action: {
|
||||
type: 'askAiCodeGeneration',
|
||||
target: 'jsCode',
|
||||
target: AI_TRANSFORM_JS_CODE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Transformation Code',
|
||||
name: 'jsCode',
|
||||
displayName: 'Code Generated For Prompt',
|
||||
name: AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT,
|
||||
type: 'hidden',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Generated JavaScript',
|
||||
name: AI_TRANSFORM_JS_CODE,
|
||||
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.',
|
||||
hint: 'Read-only. To edit this code, adjust the instructions 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 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -117,3 +117,5 @@ export const SINGLE_EXECUTION_NODES: { [key: string]: { [key: string]: NodeParam
|
|||
};
|
||||
|
||||
export const SEND_AND_WAIT_OPERATION = 'sendAndWait';
|
||||
export const AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT = 'codeGeneratedForPrompt';
|
||||
export const AI_TRANSFORM_JS_CODE = 'jsCode';
|
||||
|
|
Loading…
Reference in a new issue