n8n/packages/editor-ui/src/components/ButtonParameter/ButtonParameter.vue
Michael Kret 8a484077af
feat(AI Transform Node): UX improvements (#11280)
Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com>
2024-11-05 18:47:52 +00:00

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>