Skip to content

Commit abdf9b9

Browse files
committed
feat: enhance AI generation dialog with internationalization and new options
1 parent dbcb8fe commit abdf9b9

2 files changed

Lines changed: 92 additions & 26 deletions

File tree

custom/VisionAction.vue

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,73 @@
11
<template>
22
<div class="flex items-end justify-start gap-2 cursor-pointer">
33
<div class="flex items-center justify-center text-white bg-gradient-to-r h-[18px] from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 font-medium rounded-md text-sm px-1 text-center">
4-
AI
4+
{{t('AI')}}
55
</div>
66
<p class="text-justify max-h-[18px] truncate max-w-[60vw] md:max-w-none">{{ props.meta.actionName }}</p>
77
</div>
88
<Dialog
99
ref="confirmDialog"
10-
header="Bulk AI Flow"
10+
header="Bulk AI Generation"
1111
class="[scrollbar-gutter:stable] !max-w-full w-fit h-fit"
1212
:class="popupMode === 'generation' ? 'lg:w-[1600px] !lg:max-w-[1600px]'
1313
: popupMode === 'settings' ? 'lg:w-[1000px] !lg:max-w-[1000px]'
1414
: 'lg:w-[500px] !lg:max-w-[500px]'"
1515
:beforeCloseFunction="closeDialog"
1616
:closable="false"
1717
:askForCloseConfirmation="popupMode === 'generation' ? true : false"
18-
closeConfirmationText="Are you sure you want to close without saving?"
18+
:closeConfirmationText="t('Are you sure you want to close without saving?')"
1919
:buttons="popupMode === 'generation' ? [
2020
{
21-
label: checkedCount > 1 ? 'Save fields' : 'Save field',
21+
label: checkedCount > 1 ? t('Save fields') : t('Save field'),
2222
options: {
2323
disabled: isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords || isGeneratingImages || isAnalizingFields || isAnalizingImages,
2424
loader: isLoading, class: 'w-fit'
2525
},
2626
onclick: async (dialog) => { await saveData(); dialog.hide(); }
2727
},
2828
{
29-
label: 'Cancel',
29+
label: t('Cancel'),
3030
options: {
3131
class: 'bg-white hover:!bg-gray-100 !text-gray-900 hover:!text-gray-800 dark:!bg-gray-800 dark:!text-gray-100 dark:hover:!bg-gray-700 !border-gray-200'
3232
},
3333
onclick: (dialog) => confirmDialog.tryToHideModal()
3434
},
3535
] : popupMode === 'settings' ? [
3636
{
37-
label: 'Save settings',
37+
label: t('Save settings'),
3838
options: {
3939
class: 'w-fit'
4040
},
4141
onclick: (dialog) => { saveSettings(); }
4242
},
4343
] :
4444
[
45+
// {
46+
// label: t('Edit prompts'),
47+
// options: {
48+
// class: 'w-fit ml-auto'
49+
// },
50+
// onclick: (dialog) => { clickSettingsButton(); }
51+
// },
4552
{
46-
label: 'Edit prompts',
53+
label: t('Cancel'),
4754
options: {
48-
class: 'w-fit ml-auto'
55+
class: 'w-2/5 bg-white hover:!bg-gray-100 !text-gray-900 hover:!text-gray-800 dark:!bg-gray-800 dark:!text-gray-100 dark:hover:!bg-gray-700 !border-gray-200'
4956
},
50-
onclick: (dialog) => { clickSettingsButton(); }
57+
onclick: (dialog) => confirmDialog.tryToHideModal()
5158
},
59+
{
60+
label: t('Start generation'),
61+
options: {
62+
class: 'w-3/5 px-5 py-2.5 bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 rounded-md text-white border-none'
63+
},
64+
onclick: (dialog) => { runAiActions(); }
65+
}
5266
]"
5367
:click-to-close-outside="false"
5468
>
55-
<div class="[scrollbar-gutter:stable] bulk-vision-table flex flex-col items-center max-w-[1560px] md:max-h-[75vh] gap-3 md:gap-4 w-full h-full overflow-y-auto">
56-
<div v-if="records && props.checkboxes.length && popupMode === 'generation'" class="w-full overflow-x-auto">
69+
<div class="bulk-vision-table flex flex-col items-center max-w-[1560px] md:max-h-[75vh] gap-3 md:gap-4 w-full h-full overflow-y-auto">
70+
<div v-if="records && props.checkboxes.length && popupMode === 'generation'" class="[scrollbar-gutter:stable] w-full overflow-x-auto">
5771
<VisionTable
5872
:checkbox="props.checkboxes"
5973
:records="records"
@@ -110,7 +124,7 @@
110124
}}</p>
111125
<div class="grid grid-cols-2 gap-4">
112126
<div v-for="(prompt, promptKey) in promptsCategory" :key="promptKey">
113-
{{ formatLabel(promptKey) }} prompt:
127+
{{ formatLabel(promptKey) }} {{ t('prompt') }}:
114128
<Textarea
115129
v-model="generationPrompts[key][promptKey]"
116130
class="w-full h-64 p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
@@ -120,10 +134,27 @@
120134
</div>
121135
</div>
122136
</div>
123-
<div v-else class="flex flex-col gap-2">
124-
<Button @click="runAiActions" class="px-5 py-2.5 my-20 bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 rounded-md text-white border-none">
125-
Start generation
126-
</Button>
137+
<div v-else class="flex flex-col gap-2 mt-2 w-full h-full">
138+
<div class="flex items-center justify-between mb-2 w-full">
139+
<div class="flex items-center justify-center gap-2">
140+
<IconShieldSolid class="w-6 h-6 text-lightPrimary dark:text-darkPrimary" />
141+
<p class="sm:text-base text-sm">{{ t('Do not overwrite existing values') }}</p>
142+
<Tooltip>
143+
<IconInfoCircleSolid class="w-5 h-5 me-2 text-lightPrimary dark:text-darkPrimary"/>
144+
<template #tooltip>
145+
<p class="max-w-64">{{ t('When enabled, the AI will skip generating content for fields that already have data. This helps to preserve existing information and avoid overwriting valuable content.') }}</p>
146+
</template>
147+
</Tooltip>
148+
</div>
149+
<Toggle
150+
v-model="skipFilledFieldsForGeneration"
151+
/>
152+
</div>
153+
<div :class="skipFilledFieldsForGeneration === false ? 'opacity-100' : 'opacity-0'" class="flex items-center text-yellow-800 bg-yellow-100 p-2 rounded-md border border-yellow-300">
154+
<IconExclamationTriangle class="w-6 h-6 me-2"/>
155+
<p class="sm:text-base text-sm">{{ t('Warning: Existing values will be overwritten.') }}</p>
156+
</div>
157+
<p class="w-fit flex justify-start text-lightPrimary dark:text-lightPrimary hover:underline cursor-pointer" @click="clickSettingsButton()">{{ t('Configure prompts') }}</p>
127158
</div>
128159
</div>
129160
</Dialog>
@@ -132,12 +163,15 @@
132163
<script lang="ts" setup>
133164
import { callAdminForthApi } from '@/utils';
134165
import { Ref, ref, watch } from 'vue'
135-
import { Dialog, Button, Textarea } from '@/afcl';
166+
import { Dialog, Button, Textarea, Toggle, Tooltip } from '@/afcl';
136167
import VisionTable from './VisionTable.vue'
137168
import adminforth from '@/adminforth';
138169
import { useI18n } from 'vue-i18n';
139170
import { AdminUser, type AdminForthResourceCommon } from '@/types/Common';
140171
import { useCoreStore } from '@/stores/core';
172+
import { IconShieldSolid, IconInfoCircleSolid } from '@iconify-prerendered/vue-flowbite';
173+
import { IconExclamationTriangle } from '@iconify-prerendered/vue-humbleicons';
174+
141175
142176
const coreStore = useCoreStore();
143177
@@ -205,6 +239,7 @@ const generationPrompts = ref<any>({});
205239
const isDataSaved = ref(false);
206240
207241
const regeneratingFieldsStatus = ref<Record<string, Record<string, boolean>>>({});
242+
const skipFilledFieldsForGeneration = ref<boolean>(true);
208243
209244
const openDialog = async () => {
210245
window.addEventListener('beforeunload', beforeUnloadHandler);
@@ -629,6 +664,7 @@ async function runAiAction({
629664
actionType: actionType,
630665
recordId: checkbox,
631666
...(customPrompt !== undefined ? { customPrompt: JSON.stringify(customPrompt) } : {}),
667+
filterFilledFields: skipFilledFieldsForGeneration.value,
632668
},
633669
silentError: true,
634670
});

index.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
5959
return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.generateImages, record, v => String(customPrompt ? v : v.prompt));
6060
}
6161

62+
private removeFromPromptFilledFields(compiledOutputFields: Record<string, string>, record: Record<string, any>): Record<string, string> {
63+
const newCompiledOutputFields: Record<string, string> = {};
64+
for (const [key, value] of Object.entries(record)) {
65+
if (compiledOutputFields[key]) {
66+
if (value !== null && value !== undefined && value !== '') {
67+
continue;
68+
}
69+
newCompiledOutputFields[key] = compiledOutputFields[key];
70+
}
71+
}
72+
return newCompiledOutputFields;
73+
}
74+
6275
private async checkRateLimit(field: string, fieldNameRateLimit: string | undefined, headers: Record<string, string | string[] | undefined>): Promise<void | { error?: string; }> {
6376
if (fieldNameRateLimit) {
6477
// rate limit
@@ -94,7 +107,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
94107
return prompt;
95108
}
96109

97-
private async analyze_image(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>, customPrompt? : string) {
110+
private async analyze_image(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>, customPrompt? : string, filterFilledFields: boolean = true) {
98111
const selectedId = recordId;
99112
let isError = false;
100113
// Fetch the record using the provided ID
@@ -125,8 +138,15 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
125138
}
126139
//create prompt for OpenAI
127140
const compiledOutputFields = await this.compileOutputFieldsTemplates(record, customPrompt);
128-
const prompt = this.getPromptForImageAnalysis(compiledOutputFields);
141+
const filteredCompiledOutputFields = filterFilledFields ? this.removeFromPromptFilledFields(compiledOutputFields, record) : compiledOutputFields;
142+
143+
if (Object.keys(filteredCompiledOutputFields).length === 0) {
144+
jobs.set(jobId, { status: 'completed', result: {} });
145+
return { ok: true };
146+
}
129147

148+
const prompt = this.getPromptForImageAnalysis(filteredCompiledOutputFields);
149+
130150
//send prompt to OpenAI and get response
131151
let chatResponse;
132152
try {
@@ -168,7 +188,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
168188

169189
}
170190

171-
private async analyzeNoImages(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>, customPrompt? : string) {
191+
private async analyzeNoImages(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>, customPrompt? : string, filterFilledFields: boolean = true) {
172192
const selectedId = recordId;
173193
let isError = false;
174194
if (STUB_MODE) {
@@ -181,7 +201,14 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
181201
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, selectedId)] );
182202

183203
const compiledOutputFields = await this.compileOutputFieldsTemplatesNoImage(record, customPrompt);
184-
const prompt = this.getPromptForPlainFields(compiledOutputFields);
204+
205+
const filteredCompiledOutputFields = filterFilledFields ? this.removeFromPromptFilledFields(compiledOutputFields, record) : compiledOutputFields;
206+
207+
if (Object.keys(filteredCompiledOutputFields).length === 0) {
208+
jobs.set(jobId, { status: 'completed', result: {} });
209+
return { ok: true };
210+
}
211+
const prompt = this.getPromptForPlainFields(filteredCompiledOutputFields);
185212
//send prompt to OpenAI and get response
186213
const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
187214
let resp: any;
@@ -212,7 +239,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
212239
}
213240
}
214241

215-
private async initialImageGenerate(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>, customPrompt? : string) {
242+
private async initialImageGenerate(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>, customPrompt? : string, filterFilledFields: boolean = true) {
216243
const selectedId = recordId;
217244
let isError = false;
218245
const start = +new Date();
@@ -232,6 +259,9 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
232259
}
233260
}
234261
const fieldTasks = Object.keys(this.options?.generateImages || {}).map(async (key) => {
262+
if ( record[key] && filterFilledFields ) {
263+
return { key, images: [] };
264+
}
235265
const prompt = (await this.compileGenerationFieldTemplates(record, customPrompt))[key];
236266
let images;
237267
if (this.options.attachFiles && attachmentFiles.length === 0) {
@@ -837,7 +867,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
837867
method: 'POST',
838868
path: `/plugin/${this.pluginInstanceId}/create-job`,
839869
handler: async ({ body, adminUser, headers }) => {
840-
const { actionType, recordId, customPrompt } = body;
870+
const { actionType, recordId, customPrompt, filterFilledFields } = body;
841871
const jobId = randomUUID();
842872
jobs.set(jobId, { status: "in_progress" });
843873
if (!actionType) {
@@ -850,13 +880,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
850880
} else {
851881
switch(actionType) {
852882
case 'generate_images':
853-
this.initialImageGenerate(jobId, recordId, adminUser, headers, customPrompt);
883+
this.initialImageGenerate(jobId, recordId, adminUser, headers, customPrompt, filterFilledFields);
854884
break;
855885
case 'analyze_no_images':
856-
this.analyzeNoImages(jobId, recordId, adminUser, headers, customPrompt);
886+
this.analyzeNoImages(jobId, recordId, adminUser, headers, customPrompt, filterFilledFields);
857887
break;
858888
case 'analyze':
859-
this.analyze_image(jobId, recordId, adminUser, headers, customPrompt);
889+
this.analyze_image(jobId, recordId, adminUser, headers, customPrompt, filterFilledFields);
860890
break;
861891
case 'regenerate_images':
862892
if (!body.prompt || !body.fieldName) {

0 commit comments

Comments
 (0)