mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-01 17:27:14 +02:00
281 lines
6.6 KiB
Vue
281 lines
6.6 KiB
Vue
<script setup lang="ts">
|
|
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 { getParentNodes, generateCodeForAiTransform } from './utils';
|
|
import { useTelemetry } from '@/composables/useTelemetry';
|
|
import { useUIStore } from '@/stores/ui.store';
|
|
|
|
const AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT = 'codeGeneratedForPrompt';
|
|
|
|
const emit = defineEmits<{
|
|
valueChanged: [value: IUpdateInformation];
|
|
}>();
|
|
|
|
const props = defineProps<{
|
|
parameter: INodeProperties;
|
|
value: string;
|
|
path: string;
|
|
}>();
|
|
|
|
const { activeNode } = useNDVStore();
|
|
|
|
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;
|
|
});
|
|
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;
|
|
}
|
|
|
|
function stopLoading() {
|
|
setTimeout(() => {
|
|
isLoading.value = false;
|
|
}, 200);
|
|
}
|
|
|
|
function getPath(parameter: string) {
|
|
return (props.path ? `${props.path}.` : '') + parameter;
|
|
}
|
|
|
|
async function onSubmit() {
|
|
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 {
|
|
switch (type) {
|
|
case 'askAiCodeGeneration':
|
|
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: updateInformation.value,
|
|
});
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
showMessage({
|
|
type: 'success',
|
|
title: i18n.baseText('codeNodeEditor.askAi.generationCompleted'),
|
|
});
|
|
|
|
stopLoading();
|
|
} catch (error) {
|
|
useTelemetry().trackAiTransform('generationFinished', {
|
|
prompt: prompt.value,
|
|
code: '',
|
|
hasError: true,
|
|
});
|
|
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,
|
|
});
|
|
}
|
|
|
|
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();
|
|
});
|
|
</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" :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"
|
|
: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;
|
|
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;
|
|
|
|
* {
|
|
font-size: var(--font-size-2xs);
|
|
line-height: 1;
|
|
}
|
|
}
|
|
.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>
|