mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -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,
|
readonly: false,
|
||||||
clearable: false,
|
clearable: false,
|
||||||
rows: 2,
|
rows: 2,
|
||||||
maxlength: Infinity,
|
maxlength: undefined,
|
||||||
title: '',
|
title: '',
|
||||||
name: () => uid('input'),
|
name: () => uid('input'),
|
||||||
autocomplete: 'off',
|
autocomplete: 'off',
|
||||||
|
@ -81,6 +81,7 @@ defineExpose({ focus, blur, select });
|
||||||
:clearable="clearable"
|
:clearable="clearable"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
:title="title"
|
:title="title"
|
||||||
|
:maxlength="maxlength"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<template v-if="$slots.prepend" #prepend>
|
<template v-if="$slots.prepend" #prepend>
|
||||||
|
|
|
@ -44,7 +44,7 @@ describe('ButtonParameter', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(useNDVStore).mockReturnValue({
|
vi.mocked(useNDVStore).mockReturnValue({
|
||||||
ndvInputData: [{}],
|
ndvInputData: [{}],
|
||||||
activeNode: { name: 'TestNode' },
|
activeNode: { name: 'TestNode', parameters: {} },
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
vi.mocked(useWorkflowsStore).mockReturnValue({
|
vi.mocked(useWorkflowsStore).mockReturnValue({
|
||||||
|
|
|
@ -1,21 +1,16 @@
|
||||||
<script setup lang="ts">
|
<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 type { INodeUi, IUpdateInformation } from '@/Interface';
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { N8nButton, N8nInput, N8nTooltip } from 'n8n-design-system/components';
|
import { N8nButton, N8nInput, N8nTooltip } from 'n8n-design-system/components';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { getSchemas, getParentNodes } from './utils';
|
import { getParentNodes, generateCodeForAiTransform } from './utils';
|
||||||
import { useRootStore } from '@/stores/root.store';
|
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { generateCodeForPrompt } from '@/api/ai';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
|
||||||
import { format } from 'prettier';
|
const AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT = 'codeGeneratedForPrompt';
|
||||||
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 emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
valueChanged: [value: IUpdateInformation];
|
valueChanged: [value: IUpdateInformation];
|
||||||
|
@ -27,8 +22,7 @@ const props = defineProps<{
|
||||||
path: string;
|
path: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const rootStore = useRootStore();
|
const { activeNode } = useNDVStore();
|
||||||
const settingsStore = useSettingsStore();
|
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
@ -53,6 +47,11 @@ const isSubmitEnabled = computed(() => {
|
||||||
|
|
||||||
return true;
|
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() {
|
function startLoading() {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
@ -69,7 +68,6 @@ function getPath(parameter: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
const { activeNode } = useNDVStore();
|
|
||||||
const { showMessage } = useToast();
|
const { showMessage } = useToast();
|
||||||
const action: string | NodePropertyAction | undefined =
|
const action: string | NodePropertyAction | undefined =
|
||||||
props.parameter.typeOptions?.buttonConfig?.action;
|
props.parameter.typeOptions?.buttonConfig?.action;
|
||||||
|
@ -93,46 +91,26 @@ async function onSubmit() {
|
||||||
startLoading();
|
startLoading();
|
||||||
|
|
||||||
try {
|
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) {
|
switch (type) {
|
||||||
case 'askAiCodeGeneration':
|
case 'askAiCodeGeneration':
|
||||||
let value;
|
const updateInformation = await generateCodeForAiTransform(
|
||||||
if (settingsStore.isAskAiEnabled) {
|
prompt.value,
|
||||||
const { restApiContext } = useRootStore();
|
getPath(target as string),
|
||||||
const { code } = await generateCodeForPrompt(restApiContext, payload);
|
);
|
||||||
value = code;
|
if (!updateInformation) return;
|
||||||
} 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,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
//updade code parameter
|
||||||
emit('valueChanged', updateInformation);
|
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', {
|
useTelemetry().trackAiTransform('generationFinished', {
|
||||||
prompt: prompt.value,
|
prompt: prompt.value,
|
||||||
code: formattedCode,
|
code: updateInformation.value,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
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(() => {
|
onMounted(() => {
|
||||||
parentNodes.value = getParentNodes();
|
parentNodes.value = getParentNodes();
|
||||||
});
|
});
|
||||||
|
@ -185,13 +173,18 @@ onMounted(() => {
|
||||||
>
|
>
|
||||||
</n8n-input-label>
|
</n8n-input-label>
|
||||||
<div :class="$style.inputContainer" :hidden="!hasInputField">
|
<div :class="$style.inputContainer" :hidden="!hasInputField">
|
||||||
<div :class="$style.meta">
|
<div :class="$style.meta" :style="useDarkBackdrop()">
|
||||||
<span
|
<span
|
||||||
v-if="inputFieldMaxLength"
|
v-if="inputFieldMaxLength"
|
||||||
v-show="prompt.length > 1"
|
v-show="prompt.length > 1"
|
||||||
:class="$style.counter"
|
:class="$style.counter"
|
||||||
v-text="`${prompt.length} / ${inputFieldMaxLength}`"
|
v-text="`${prompt.length} / ${inputFieldMaxLength}`"
|
||||||
/>
|
/>
|
||||||
|
<span
|
||||||
|
v-if="promptUpdated"
|
||||||
|
:class="$style['warning-text']"
|
||||||
|
v-text="'Instructions changed'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<N8nInput
|
<N8nInput
|
||||||
v-model="prompt"
|
v-model="prompt"
|
||||||
|
@ -255,9 +248,15 @@ onMounted(() => {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
position: absolute;
|
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);
|
left: var(--spacing-xs);
|
||||||
right: var(--spacing-xs);
|
right: var(--spacing-xs);
|
||||||
|
gap: 10px;
|
||||||
|
align-items: end;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
@ -267,10 +266,15 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
.counter {
|
.counter {
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.controls {
|
.controls {
|
||||||
padding: var(--spacing-2xs) 0;
|
padding: var(--spacing-2xs) 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
.warning-text {
|
||||||
|
color: var(--color-warning);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
import type { Schema } from '@/Interface';
|
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 { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { useDataSchema } from '@/composables/useDataSchema';
|
import { useDataSchema } from '@/composables/useDataSchema';
|
||||||
import { executionDataToJson } from '@/utils/nodeTypesUtils';
|
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() {
|
export function getParentNodes() {
|
||||||
const activeNode = useNDVStore().activeNode;
|
const activeNode = useNDVStore().activeNode;
|
||||||
|
@ -44,3 +51,41 @@ export function getSchemas() {
|
||||||
parentNodesSchemas,
|
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,
|
FORM_TRIGGER_NODE_TYPE,
|
||||||
CHAT_TRIGGER_NODE_TYPE,
|
CHAT_TRIGGER_NODE_TYPE,
|
||||||
} from '@/constants';
|
} 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 { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
@ -21,6 +26,8 @@ import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
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 NODE_TEST_STEP_POPUP_COUNT_KEY = 'N8N_NODE_TEST_STEP_POPUP_COUNT';
|
||||||
const MAX_POPUP_COUNT = 10;
|
const MAX_POPUP_COUNT = 10;
|
||||||
|
@ -47,6 +54,7 @@ const props = withDefaults(
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
stopExecution: [];
|
stopExecution: [];
|
||||||
execute: [];
|
execute: [];
|
||||||
|
valueChanged: [value: IUpdateInformation];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
@ -54,6 +62,7 @@ defineOptions({
|
||||||
});
|
});
|
||||||
|
|
||||||
const lastPopupCountUpdate = ref(0);
|
const lastPopupCountUpdate = ref(0);
|
||||||
|
const codeGenerationInProgress = ref(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { runWorkflow, runWorkflowResolvePending, stopCurrentExecution } = useRunWorkflow({ router });
|
const { runWorkflow, runWorkflowResolvePending, stopCurrentExecution } = useRunWorkflow({ router });
|
||||||
|
@ -76,7 +85,7 @@ const nodeType = computed((): INodeTypeDescription | null => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const isNodeRunning = computed(() => {
|
const isNodeRunning = computed(() => {
|
||||||
if (!uiStore.isActionActive['workflowRunning']) return false;
|
if (!uiStore.isActionActive['workflowRunning'] || codeGenerationInProgress.value) return false;
|
||||||
const triggeredNode = workflowsStore.executedNode;
|
const triggeredNode = workflowsStore.executedNode;
|
||||||
return (
|
return (
|
||||||
workflowsStore.isNodeExecuting(node.value?.name ?? '') || triggeredNode === node.value?.name
|
workflowsStore.isNodeExecuting(node.value?.name ?? '') || triggeredNode === node.value?.name
|
||||||
|
@ -142,6 +151,10 @@ const disabledHint = computed(() => {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (codeGenerationInProgress.value) {
|
||||||
|
return i18n.baseText('ndv.execute.generatingCode');
|
||||||
|
}
|
||||||
|
|
||||||
if (isTriggerNode.value && node?.value?.disabled) {
|
if (isTriggerNode.value && node?.value?.disabled) {
|
||||||
return i18n.baseText('ndv.execute.nodeIsDisabled');
|
return i18n.baseText('ndv.execute.nodeIsDisabled');
|
||||||
}
|
}
|
||||||
|
@ -163,6 +176,9 @@ const disabledHint = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const tooltipText = computed(() => {
|
const tooltipText = computed(() => {
|
||||||
|
if (shouldGenerateCode.value) {
|
||||||
|
return i18n.baseText('ndv.execute.generateCodeAndTestNode.description');
|
||||||
|
}
|
||||||
if (disabledHint.value) return disabledHint.value;
|
if (disabledHint.value) return disabledHint.value;
|
||||||
if (props.tooltip && !isLoading.value && testStepButtonPopupCount() < MAX_POPUP_COUNT) {
|
if (props.tooltip && !isLoading.value && testStepButtonPopupCount() < MAX_POPUP_COUNT) {
|
||||||
return props.tooltip;
|
return props.tooltip;
|
||||||
|
@ -199,9 +215,37 @@ const buttonLabel = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const isLoading = 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() {
|
async function stopWaitingForWebhook() {
|
||||||
try {
|
try {
|
||||||
await workflowsStore.removeTestWebhook(workflowsStore.workflowId);
|
await workflowsStore.removeTestWebhook(workflowsStore.workflowId);
|
||||||
|
@ -227,6 +271,51 @@ function onMouseOver() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onClick() {
|
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'))) {
|
if (isChatNode.value || (isChatChild.value && ndvStore.isNDVDataEmpty('input'))) {
|
||||||
ndvStore.setActiveNodeName(null);
|
ndvStore.setActiveNodeName(null);
|
||||||
nodeViewEventBus.emit('openChat');
|
nodeViewEventBus.emit('openChat');
|
||||||
|
@ -296,7 +385,7 @@ async function onClick() {
|
||||||
:label="buttonLabel"
|
:label="buttonLabel"
|
||||||
:type="type"
|
:type="type"
|
||||||
:size="size"
|
:size="size"
|
||||||
:icon="!isListeningForEvents && !hideIcon ? 'flask' : undefined"
|
:icon="buttonIcon"
|
||||||
:transparent-background="transparent"
|
:transparent-background="transparent"
|
||||||
:title="
|
:title="
|
||||||
!isTriggerNode && !tooltipText ? i18n.baseText('ndv.execute.testNode.description') : ''
|
!isTriggerNode && !tooltipText ? i18n.baseText('ndv.execute.testNode.description') : ''
|
||||||
|
|
|
@ -974,6 +974,7 @@ onBeforeUnmount(() => {
|
||||||
telemetry-source="parameters"
|
telemetry-source="parameters"
|
||||||
@execute="onNodeExecute"
|
@execute="onNodeExecute"
|
||||||
@stop-execution="onStopExecution"
|
@stop-execution="onStopExecution"
|
||||||
|
@value-changed="valueChanged"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
|
||||||
import { within } from '@testing-library/vue';
|
import { within } from '@testing-library/vue';
|
||||||
import { waitFor } from '@testing-library/vue';
|
import { waitFor } from '@testing-library/vue';
|
||||||
import ParameterOptions from './ParameterOptions.vue';
|
import ParameterOptions from './ParameterOptions.vue';
|
||||||
|
import { setActivePinia, createPinia } from 'pinia';
|
||||||
|
|
||||||
const DEFAULT_PARAMETER = {
|
const DEFAULT_PARAMETER = {
|
||||||
displayName: 'Fields to Set',
|
displayName: 'Fields to Set',
|
||||||
|
@ -12,6 +13,17 @@ const DEFAULT_PARAMETER = {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('ParameterOptions', () => {
|
describe('ParameterOptions', () => {
|
||||||
|
let pinia: ReturnType<typeof createPinia>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
pinia = createPinia();
|
||||||
|
setActivePinia(pinia);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('renders default options properly', () => {
|
it('renders default options properly', () => {
|
||||||
const { getByTestId } = renderComponent(ParameterOptions, {
|
const { getByTestId } = renderComponent(ParameterOptions, {
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import type { INodeProperties, NodeParameterValueType } from 'n8n-workflow';
|
||||||
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
||||||
import { isValueExpression } from '@/utils/nodeTypesUtils';
|
import { isValueExpression } from '@/utils/nodeTypesUtils';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { AI_TRANSFORM_NODE_TYPE } from '@/constants';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
parameter: INodeProperties;
|
parameter: INodeProperties;
|
||||||
|
@ -59,10 +61,19 @@ const shouldShowOptions = computed(() => {
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
const selectedView = computed(() => (isValueAnExpression.value ? 'expression' : 'fixed'));
|
const selectedView = computed(() => (isValueAnExpression.value ? 'expression' : 'fixed'));
|
||||||
|
const activeNode = computed(() => useNDVStore().activeNode);
|
||||||
const hasRemoteMethod = computed(
|
const hasRemoteMethod = computed(
|
||||||
() =>
|
() =>
|
||||||
!!props.parameter.typeOptions?.loadOptionsMethod || !!props.parameter.typeOptions?.loadOptions,
|
!!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(() => {
|
const actions = computed(() => {
|
||||||
if (Array.isArray(props.customActions) && props.customActions.length > 0) {
|
if (Array.isArray(props.customActions) && props.customActions.length > 0) {
|
||||||
return props.customActions;
|
return props.customActions;
|
||||||
|
@ -79,7 +90,7 @@ const actions = computed(() => {
|
||||||
|
|
||||||
const parameterActions = [
|
const parameterActions = [
|
||||||
{
|
{
|
||||||
label: i18n.baseText('parameterInput.resetValue'),
|
label: resetValueLabel.value,
|
||||||
value: 'resetValue',
|
value: 'resetValue',
|
||||||
disabled: isDefault.value,
|
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.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": "Test step",
|
||||||
"ndv.execute.testNode.description": "Runs the current node. Will also run previous nodes if they have not been run yet",
|
"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.executing": "Executing",
|
||||||
"ndv.execute.fetchEvent": "Fetch Test Event",
|
"ndv.execute.fetchEvent": "Fetch Test Event",
|
||||||
"ndv.execute.fixPrevious": "Fix previous node first",
|
"ndv.execute.fixPrevious": "Fix previous node first",
|
||||||
|
"ndv.execute.generatingCode": "Genereting code from instructions",
|
||||||
"ndv.execute.listenForTestEvent": "Listen for test event",
|
"ndv.execute.listenForTestEvent": "Listen for test event",
|
||||||
"ndv.execute.testChat": "Test chat",
|
"ndv.execute.testChat": "Test chat",
|
||||||
"ndv.execute.testStep": "Test step",
|
"ndv.execute.testStep": "Test step",
|
||||||
|
@ -1383,6 +1387,7 @@
|
||||||
"parameterInput.parameterHasIssues": "Parameter: \"{shortPath}\" has issues",
|
"parameterInput.parameterHasIssues": "Parameter: \"{shortPath}\" has issues",
|
||||||
"parameterInput.parameterHasIssuesAndExpression": "Parameter: \"{shortPath}\" has issues and an expression",
|
"parameterInput.parameterHasIssuesAndExpression": "Parameter: \"{shortPath}\" has issues and an expression",
|
||||||
"parameterInput.refreshList": "Refresh List",
|
"parameterInput.refreshList": "Refresh List",
|
||||||
|
"parameterInput.clearContents": "Clear Contents",
|
||||||
"parameterInput.resetValue": "Reset Value",
|
"parameterInput.resetValue": "Reset Value",
|
||||||
"parameterInput.select": "Select",
|
"parameterInput.select": "Select",
|
||||||
"parameterInput.selectDateAndTime": "Select date and time",
|
"parameterInput.selectDateAndTime": "Select date and time",
|
||||||
|
|
|
@ -5,6 +5,8 @@ import {
|
||||||
type INodeExecutionData,
|
type INodeExecutionData,
|
||||||
type INodeType,
|
type INodeType,
|
||||||
type INodeTypeDescription,
|
type INodeTypeDescription,
|
||||||
|
AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT,
|
||||||
|
AI_TRANSFORM_JS_CODE,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import set from 'lodash/set';
|
import set from 'lodash/set';
|
||||||
|
@ -46,36 +48,29 @@ export class AiTransform implements INodeType {
|
||||||
inputFieldMaxLength: 500,
|
inputFieldMaxLength: 500,
|
||||||
action: {
|
action: {
|
||||||
type: 'askAiCodeGeneration',
|
type: 'askAiCodeGeneration',
|
||||||
target: 'jsCode',
|
target: AI_TRANSFORM_JS_CODE,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Transformation Code',
|
displayName: 'Code Generated For Prompt',
|
||||||
name: 'jsCode',
|
name: AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT,
|
||||||
|
type: 'hidden',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Generated JavaScript',
|
||||||
|
name: AI_TRANSFORM_JS_CODE,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
typeOptions: {
|
typeOptions: {
|
||||||
editor: 'jsEditor',
|
editor: 'jsEditor',
|
||||||
editorIsReadOnly: true,
|
editorIsReadOnly: true,
|
||||||
},
|
},
|
||||||
default: '',
|
default: '',
|
||||||
description:
|
hint: 'Read-only. To edit this code, adjust the instructions or copy and paste it into a Code node.',
|
||||||
'Read-only. To edit this code, adjust the prompt or copy and paste it into a Code node.',
|
|
||||||
noDataExpression: true,
|
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 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