@adminforth/bulk-ai-flow 1.15.11 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.log +2 -2
- package/custom/VisionAction.vue +103 -14
- package/custom/VisionTable.vue +85 -7
- package/dist/custom/VisionAction.vue +103 -14
- package/dist/custom/VisionTable.vue +85 -7
- package/dist/index.js +120 -10
- package/index.ts +120 -10
- package/package.json +2 -2
package/build.log
CHANGED
|
@@ -13,5 +13,5 @@ custom/package-lock.json
|
|
|
13
13
|
custom/package.json
|
|
14
14
|
custom/tsconfig.json
|
|
15
15
|
|
|
16
|
-
sent
|
|
17
|
-
total size is
|
|
16
|
+
sent 88,118 bytes received 172 bytes 176,580.00 bytes/sec
|
|
17
|
+
total size is 87,473 speedup is 0.99
|
package/custom/VisionAction.vue
CHANGED
|
@@ -65,7 +65,8 @@
|
|
|
65
65
|
:tableColumnsIndexes="tableColumnsIndexes"
|
|
66
66
|
:selected="selected"
|
|
67
67
|
:oldData="oldData"
|
|
68
|
-
:
|
|
68
|
+
:isAiResponseReceivedAnalizeImage="isAiResponseReceivedAnalizeImage"
|
|
69
|
+
:isAiResponseReceivedAnalizeNoImage="isAiResponseReceivedAnalizeNoImage"
|
|
69
70
|
:isAiResponseReceivedImage="isAiResponseReceivedImage"
|
|
70
71
|
:primaryKey="primaryKey"
|
|
71
72
|
:openGenerationCarousel="openGenerationCarousel"
|
|
@@ -81,6 +82,15 @@
|
|
|
81
82
|
@regenerate-images="regenerateImages"
|
|
82
83
|
:isImageHasPreviewUrl="isImageHasPreviewUrl"
|
|
83
84
|
:imageGenerationPrompts="generationPrompts.generateImages"
|
|
85
|
+
:isImageToTextGenerationError="isImageToTextGenerationError"
|
|
86
|
+
:imageToTextErrorMessages="imageToTextErrorMessages"
|
|
87
|
+
:isTextToTextGenerationError="isTextToTextGenerationError"
|
|
88
|
+
:textToTextErrorMessages="textToTextErrorMessages"
|
|
89
|
+
:outputImageFields="props.meta.outputImageFields"
|
|
90
|
+
:outputFieldsForAnalizeFromImages="props.meta.outputFieldsForAnalizeFromImages"
|
|
91
|
+
:outputPlainFields="props.meta.outputPlainFields"
|
|
92
|
+
:regeneratingFieldsStatus="regeneratingFieldsStatus"
|
|
93
|
+
@regenerate-cell="regenerateCell"
|
|
84
94
|
/>
|
|
85
95
|
<div class="text-red-600 flex items-center w-full">
|
|
86
96
|
<p v-if="isError === true">{{ errorMessage }}</p>
|
|
@@ -159,7 +169,8 @@ const selected = ref<any[]>([]);
|
|
|
159
169
|
const oldData = ref<any[]>([]);
|
|
160
170
|
const carouselSaveImages = ref<any[]>([]);
|
|
161
171
|
const carouselImageIndex = ref<any[]>([]);
|
|
162
|
-
const
|
|
172
|
+
const isAiResponseReceivedAnalizeImage = ref([]);
|
|
173
|
+
const isAiResponseReceivedAnalizeNoImage = ref([]);
|
|
163
174
|
const isAiResponseReceivedImage = ref([]);
|
|
164
175
|
const primaryKey = props.meta.primaryKey;
|
|
165
176
|
const openGenerationCarousel = ref([]);
|
|
@@ -178,11 +189,20 @@ const isDialogOpen = ref(false);
|
|
|
178
189
|
const isAiGenerationError = ref<boolean[]>([false]);
|
|
179
190
|
const aiGenerationErrorMessage = ref<string[]>([]);
|
|
180
191
|
const isAiImageGenerationError = ref<boolean[]>([false]);
|
|
192
|
+
|
|
193
|
+
const isImageToTextGenerationError = ref<boolean[]>([false]);
|
|
194
|
+
const imageToTextErrorMessages = ref<string[]>([]);
|
|
195
|
+
|
|
196
|
+
const isTextToTextGenerationError = ref<boolean[]>([false]);
|
|
197
|
+
const textToTextErrorMessages = ref<string[]>([]);
|
|
198
|
+
|
|
181
199
|
const imageGenerationErrorMessage = ref<string[]>([]);
|
|
182
200
|
const isImageHasPreviewUrl = ref<Record<string, boolean>>({});
|
|
183
201
|
const popupMode = ref<'generation' | 'confirmation' | 'settings'>('confirmation');
|
|
184
202
|
const generationPrompts = ref<any>({});
|
|
185
203
|
|
|
204
|
+
const regeneratingFieldsStatus = ref<Record<string, Record<string, boolean>>>({});
|
|
205
|
+
|
|
186
206
|
const openDialog = async () => {
|
|
187
207
|
if (props.meta.askConfirmationBeforeGenerating) {
|
|
188
208
|
popupMode.value = 'confirmation';
|
|
@@ -228,8 +248,9 @@ const openDialog = async () => {
|
|
|
228
248
|
|
|
229
249
|
|
|
230
250
|
function runAiActions() {
|
|
231
|
-
|
|
232
|
-
|
|
251
|
+
popupMode.value = 'generation';
|
|
252
|
+
|
|
253
|
+
if (props.meta.isImageGeneration) {
|
|
233
254
|
isGeneratingImages.value = true;
|
|
234
255
|
runAiAction({
|
|
235
256
|
endpoint: 'initial_image_generate',
|
|
@@ -242,7 +263,7 @@ function runAiActions() {
|
|
|
242
263
|
runAiAction({
|
|
243
264
|
endpoint: 'analyze',
|
|
244
265
|
actionType: 'analyze',
|
|
245
|
-
responseFlag:
|
|
266
|
+
responseFlag: isAiResponseReceivedAnalizeImage,
|
|
246
267
|
});
|
|
247
268
|
}
|
|
248
269
|
if (props.meta.isFieldsForAnalizePlain) {
|
|
@@ -250,13 +271,14 @@ function runAiActions() {
|
|
|
250
271
|
runAiAction({
|
|
251
272
|
endpoint: 'analyze_no_images',
|
|
252
273
|
actionType: 'analyze_no_images',
|
|
253
|
-
responseFlag:
|
|
274
|
+
responseFlag: isAiResponseReceivedAnalizeNoImage,
|
|
254
275
|
});
|
|
255
276
|
}
|
|
256
277
|
}
|
|
257
278
|
|
|
258
279
|
const closeDialog = () => {
|
|
259
|
-
|
|
280
|
+
isAiResponseReceivedAnalizeImage.value = [];
|
|
281
|
+
isAiResponseReceivedAnalizeNoImage.value = [];
|
|
260
282
|
isAiResponseReceivedImage.value = [];
|
|
261
283
|
|
|
262
284
|
records.value = [];
|
|
@@ -652,9 +674,9 @@ async function runAiAction({
|
|
|
652
674
|
}
|
|
653
675
|
}
|
|
654
676
|
//marking that we received response for this record
|
|
655
|
-
if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
|
|
677
|
+
//if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
|
|
656
678
|
responseFlag.value[index] = true;
|
|
657
|
-
}
|
|
679
|
+
//}
|
|
658
680
|
//updating selected with new data from AI
|
|
659
681
|
const pk = selected.value[index]?.[primaryKey];
|
|
660
682
|
if (pk) {
|
|
@@ -674,9 +696,9 @@ async function runAiAction({
|
|
|
674
696
|
// if job is failed - set error
|
|
675
697
|
} else if (jobStatus === 'failed') {
|
|
676
698
|
const index = selected.value.findIndex(item => String(item[primaryKey]) === String(recordId));
|
|
677
|
-
if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
|
|
699
|
+
//if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
|
|
678
700
|
responseFlag.value[index] = true;
|
|
679
|
-
}
|
|
701
|
+
//}
|
|
680
702
|
if (index !== -1) {
|
|
681
703
|
jobsIds.splice(jobsIds.findIndex(j => j.jobId === jobId), 1);
|
|
682
704
|
} else {
|
|
@@ -691,9 +713,12 @@ async function runAiAction({
|
|
|
691
713
|
if (actionType === 'generate_images') {
|
|
692
714
|
isAiImageGenerationError.value[index] = true;
|
|
693
715
|
imageGenerationErrorMessage.value[index] = jobResponse.job?.error || 'Unknown error';
|
|
694
|
-
} else {
|
|
695
|
-
|
|
696
|
-
|
|
716
|
+
} else if (actionType === 'analyze') {
|
|
717
|
+
isImageToTextGenerationError.value[index] = true;
|
|
718
|
+
imageToTextErrorMessages.value[index] = jobResponse.job?.error || 'Unknown error';
|
|
719
|
+
} else if (actionType === 'analyze_no_images') {
|
|
720
|
+
isTextToTextGenerationError.value[index] = true;
|
|
721
|
+
textToTextErrorMessages.value[index] = jobResponse.job?.error || 'Unknown error';
|
|
697
722
|
}
|
|
698
723
|
}
|
|
699
724
|
}
|
|
@@ -918,4 +943,68 @@ function checkAndAddNewFieldsToPrompts(savedPrompts, defaultPrompts) {
|
|
|
918
943
|
return savedPrompts;
|
|
919
944
|
}
|
|
920
945
|
|
|
946
|
+
async function regenerateCell(recordInfo: any) {
|
|
947
|
+
console.log('Regenerating cell for record:', recordInfo.recordId, 'field:', recordInfo.fieldName);
|
|
948
|
+
if (!regeneratingFieldsStatus.value[recordInfo.recordId]) {
|
|
949
|
+
regeneratingFieldsStatus.value[recordInfo.recordId] = {};
|
|
950
|
+
}
|
|
951
|
+
regeneratingFieldsStatus.value[recordInfo.recordId][recordInfo.fieldName] = true;
|
|
952
|
+
const actionType = props.meta.outputFieldsForAnalizeFromImages?.includes(recordInfo.fieldName)
|
|
953
|
+
? 'analyze'
|
|
954
|
+
: props.meta.outputPlainFields?.includes(recordInfo.fieldName)
|
|
955
|
+
? 'analyze_no_images'
|
|
956
|
+
: null;
|
|
957
|
+
if (!actionType) {
|
|
958
|
+
console.error(`Field ${recordInfo.fieldName} is not configured for analysis.`);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
let generationPromptsForField = {};
|
|
963
|
+
if (actionType === 'analyze') {
|
|
964
|
+
generationPromptsForField = generationPrompts.value.imageFieldsPrompts || {};
|
|
965
|
+
} else if (actionType === 'analyze_no_images') {
|
|
966
|
+
generationPromptsForField = generationPrompts.value.plainFieldsPrompts || {};
|
|
967
|
+
}
|
|
968
|
+
console.log('Using generation prompts for field regeneration:', generationPromptsForField);
|
|
969
|
+
|
|
970
|
+
let res;
|
|
971
|
+
try {
|
|
972
|
+
res = await callAdminForthApi({
|
|
973
|
+
path: `/plugin/${props.meta.pluginInstanceId}/regenerate-cell`,
|
|
974
|
+
method: 'POST',
|
|
975
|
+
body: {
|
|
976
|
+
fieldToRegenerate: recordInfo.fieldName,
|
|
977
|
+
recordId: recordInfo.recordId,
|
|
978
|
+
actionType: actionType,
|
|
979
|
+
prompt: generationPromptsForField[recordInfo.fieldName] || null,
|
|
980
|
+
},
|
|
981
|
+
});
|
|
982
|
+
} catch (e) {
|
|
983
|
+
console.error(`Error during cell regeneration for record ${recordInfo.recordId}, field ${recordInfo.fieldName}:`, e);
|
|
984
|
+
}
|
|
985
|
+
if ( res.ok === false) {
|
|
986
|
+
adminforth.alert({
|
|
987
|
+
message: res.error,
|
|
988
|
+
variant: 'danger',
|
|
989
|
+
});
|
|
990
|
+
isError.value = true;
|
|
991
|
+
errorMessage.value = t(`Failed to regenerate field. You are not allowed to regenerate.`);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
console.log('Regeneration response:', res);
|
|
995
|
+
const index = selected.value.findIndex(item => String(item[primaryKey]) === String(recordInfo.recordId));
|
|
996
|
+
console.log('Found index in selected array:', index);
|
|
997
|
+
|
|
998
|
+
const pk = selected.value[index]?.[primaryKey];
|
|
999
|
+
console.log('Primary key for the record:', pk);
|
|
1000
|
+
if (pk) {
|
|
1001
|
+
selected.value[index] = {
|
|
1002
|
+
...selected.value[index],
|
|
1003
|
+
...res.result,
|
|
1004
|
+
isChecked: true,
|
|
1005
|
+
[primaryKey]: pk,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
regeneratingFieldsStatus.value[recordInfo.recordId][recordInfo.fieldName] = false;
|
|
1009
|
+
}
|
|
921
1010
|
</script>
|
package/custom/VisionTable.vue
CHANGED
|
@@ -54,7 +54,21 @@
|
|
|
54
54
|
</template>
|
|
55
55
|
<!-- CUSTOM FIELD TEMPLATES -->
|
|
56
56
|
<template v-for="n in customFieldNames" :key="n" #[`cell:${n}`]="{ item, column }">
|
|
57
|
-
<div v-if="
|
|
57
|
+
<div v-if="(isAnalyzing(item, n) && !(props.regeneratingFieldsStatus[item[props.primaryKey]] && props.regeneratingFieldsStatus[item[props.primaryKey]][n])) && !isInColumnImage(n)" @mouseenter="(() => { hovers[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = true})" @mouseleave="(() => { hovers[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false})">
|
|
58
|
+
<div class="flex gap-1 justify-end">
|
|
59
|
+
<Tooltip v-if="checkForError(item, n)">
|
|
60
|
+
<IconExclamationCircleSolid class="my-2 w-5 h-5 text-red-500" />
|
|
61
|
+
<template #tooltip>
|
|
62
|
+
{{ checkForError(item, n) }}
|
|
63
|
+
</template>
|
|
64
|
+
</Tooltip>
|
|
65
|
+
<Tooltip>
|
|
66
|
+
<IconRefreshOutline class="my-2 w-5 h-5 hover:text-blue-500" :class="{ 'opacity-50 cursor-not-allowed hover': shouldDisableRegenerateFieldIcon(item, n) }" @click="regerenerateFieldIconClick(item, n)"/>
|
|
67
|
+
<template #tooltip>
|
|
68
|
+
{{ shouldDisableRegenerateFieldIcon(item, n) ? $t("Can't analyze image without source image") : $t('Regenerate') }}
|
|
69
|
+
</template>
|
|
70
|
+
</Tooltip>
|
|
71
|
+
</div>
|
|
58
72
|
<div v-if="isInColumnEnum(n)" class="flex flex-col items-start justify-end min-h-[90px]">
|
|
59
73
|
<Select
|
|
60
74
|
class="min-w-[150px]"
|
|
@@ -89,7 +103,7 @@
|
|
|
89
103
|
</template>
|
|
90
104
|
</Tooltip>
|
|
91
105
|
</div>
|
|
92
|
-
<div v-else-if="typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'boolean'" class="flex flex-col items-
|
|
106
|
+
<div v-else-if="typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'boolean'" class="flex flex-col items-center justify-end min-h-[90px]">
|
|
93
107
|
<Toggle
|
|
94
108
|
class="p-2"
|
|
95
109
|
v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
|
|
@@ -195,7 +209,7 @@
|
|
|
195
209
|
<Skeleton type="image" class="w-20 h-20" />
|
|
196
210
|
</div>
|
|
197
211
|
|
|
198
|
-
<div v-if="!
|
|
212
|
+
<div v-if="(!isAnalyzing(item, n) || (props.regeneratingFieldsStatus[item[props.primaryKey]] && props.regeneratingFieldsStatus[item[props.primaryKey]][n])) && !isInColumnImage(n)">
|
|
199
213
|
<Skeleton class="w-full h-6" />
|
|
200
214
|
</div>
|
|
201
215
|
</template>
|
|
@@ -203,11 +217,11 @@
|
|
|
203
217
|
</template>
|
|
204
218
|
|
|
205
219
|
<script lang="ts" setup>
|
|
206
|
-
import { ref, watch } from 'vue'
|
|
220
|
+
import { ref, watch, onMounted } from 'vue'
|
|
207
221
|
import { Select, Input, Textarea, Table, Checkbox, Skeleton, Toggle, Tooltip } from '@/afcl'
|
|
208
222
|
import GenerationCarousel from './ImageGenerationCarousel.vue'
|
|
209
223
|
import ImageCompare from './ImageCompare.vue';
|
|
210
|
-
import { IconRefreshOutline } from '@iconify-prerendered/vue-flowbite';
|
|
224
|
+
import { IconRefreshOutline, IconExclamationCircleSolid } from '@iconify-prerendered/vue-flowbite';
|
|
211
225
|
|
|
212
226
|
const props = defineProps<{
|
|
213
227
|
meta: any,
|
|
@@ -216,7 +230,8 @@ const props = defineProps<{
|
|
|
216
230
|
customFieldNames: any,
|
|
217
231
|
tableColumnsIndexes: any,
|
|
218
232
|
selected: any,
|
|
219
|
-
|
|
233
|
+
isAiResponseReceivedAnalizeImage: boolean[],
|
|
234
|
+
isAiResponseReceivedAnalizeNoImage: boolean[],
|
|
220
235
|
isAiResponseReceivedImage: boolean[],
|
|
221
236
|
primaryKey: any,
|
|
222
237
|
openGenerationCarousel: any,
|
|
@@ -233,8 +248,16 @@ const props = defineProps<{
|
|
|
233
248
|
oldData: any[],
|
|
234
249
|
isImageHasPreviewUrl: Record<string, boolean>
|
|
235
250
|
imageGenerationPrompts: Record<string, any>
|
|
251
|
+
isImageToTextGenerationError: boolean[],
|
|
252
|
+
imageToTextErrorMessages: string[],
|
|
253
|
+
isTextToTextGenerationError: boolean[],
|
|
254
|
+
textToTextErrorMessages: string[],
|
|
255
|
+
outputImageFields: string[],
|
|
256
|
+
outputFieldsForAnalizeFromImages: string[],
|
|
257
|
+
outputPlainFields: string[],
|
|
258
|
+
regeneratingFieldsStatus: Record<string, Record<string, boolean>>
|
|
236
259
|
}>();
|
|
237
|
-
const emit = defineEmits(['error', 'regenerateImages']);
|
|
260
|
+
const emit = defineEmits(['error', 'regenerateImages', 'regenerateCell']);
|
|
238
261
|
|
|
239
262
|
|
|
240
263
|
const zoomedImage = ref(null);
|
|
@@ -305,6 +328,61 @@ function isValidUrl(str: string): boolean {
|
|
|
305
328
|
}
|
|
306
329
|
}
|
|
307
330
|
|
|
331
|
+
function regerenerateFieldIconClick(item, name) {
|
|
332
|
+
if (shouldDisableRegenerateFieldIcon(item, name)) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
emit('regenerateCell', {
|
|
336
|
+
recordId: item[props.primaryKey],
|
|
337
|
+
fieldName: name
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
function shouldDisableRegenerateFieldIcon(item, name) {
|
|
342
|
+
if (props.outputFieldsForAnalizeFromImages.findIndex( el => el === name) !== -1 &&
|
|
343
|
+
props.imageToTextErrorMessages[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === item[props.primaryKey])] === 'No source images found') {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function checkForError(item, name) {
|
|
350
|
+
if (props.outputFieldsForAnalizeFromImages.findIndex( el => el === name) !== -1) {
|
|
351
|
+
const errorMessage = props.imageToTextErrorMessages[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === item[props.primaryKey])];
|
|
352
|
+
if (errorMessage) {
|
|
353
|
+
return errorMessage;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (props.outputPlainFields.findIndex( el => el === name) !== -1) {
|
|
357
|
+
const errorMessage = props.textToTextErrorMessages[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === item[props.primaryKey])];
|
|
358
|
+
if (errorMessage) {
|
|
359
|
+
return errorMessage;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function isAnalyzing(item, name) {
|
|
366
|
+
if (props.outputFieldsForAnalizeFromImages.findIndex( el => el === name) !== -1) {
|
|
367
|
+
return isImagesAnalyzing(item);
|
|
368
|
+
}
|
|
369
|
+
if (props.outputPlainFields.findIndex( el => el === name) !== -1) {
|
|
370
|
+
return isNoImageAnalyzing(item);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function isImagesAnalyzing(item) {
|
|
377
|
+
const isImagesAnalyzing = props.isAiResponseReceivedAnalizeImage[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === item[props.primaryKey])]
|
|
378
|
+
return isImagesAnalyzing;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function isNoImageAnalyzing(item) {
|
|
382
|
+
const isNoImageAnalyzing = props.isAiResponseReceivedAnalizeNoImage[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === item[props.primaryKey])]
|
|
383
|
+
return isNoImageAnalyzing;
|
|
384
|
+
}
|
|
385
|
+
|
|
308
386
|
|
|
309
387
|
|
|
310
388
|
</script>
|
|
@@ -65,7 +65,8 @@
|
|
|
65
65
|
:tableColumnsIndexes="tableColumnsIndexes"
|
|
66
66
|
:selected="selected"
|
|
67
67
|
:oldData="oldData"
|
|
68
|
-
:
|
|
68
|
+
:isAiResponseReceivedAnalizeImage="isAiResponseReceivedAnalizeImage"
|
|
69
|
+
:isAiResponseReceivedAnalizeNoImage="isAiResponseReceivedAnalizeNoImage"
|
|
69
70
|
:isAiResponseReceivedImage="isAiResponseReceivedImage"
|
|
70
71
|
:primaryKey="primaryKey"
|
|
71
72
|
:openGenerationCarousel="openGenerationCarousel"
|
|
@@ -81,6 +82,15 @@
|
|
|
81
82
|
@regenerate-images="regenerateImages"
|
|
82
83
|
:isImageHasPreviewUrl="isImageHasPreviewUrl"
|
|
83
84
|
:imageGenerationPrompts="generationPrompts.generateImages"
|
|
85
|
+
:isImageToTextGenerationError="isImageToTextGenerationError"
|
|
86
|
+
:imageToTextErrorMessages="imageToTextErrorMessages"
|
|
87
|
+
:isTextToTextGenerationError="isTextToTextGenerationError"
|
|
88
|
+
:textToTextErrorMessages="textToTextErrorMessages"
|
|
89
|
+
:outputImageFields="props.meta.outputImageFields"
|
|
90
|
+
:outputFieldsForAnalizeFromImages="props.meta.outputFieldsForAnalizeFromImages"
|
|
91
|
+
:outputPlainFields="props.meta.outputPlainFields"
|
|
92
|
+
:regeneratingFieldsStatus="regeneratingFieldsStatus"
|
|
93
|
+
@regenerate-cell="regenerateCell"
|
|
84
94
|
/>
|
|
85
95
|
<div class="text-red-600 flex items-center w-full">
|
|
86
96
|
<p v-if="isError === true">{{ errorMessage }}</p>
|
|
@@ -159,7 +169,8 @@ const selected = ref<any[]>([]);
|
|
|
159
169
|
const oldData = ref<any[]>([]);
|
|
160
170
|
const carouselSaveImages = ref<any[]>([]);
|
|
161
171
|
const carouselImageIndex = ref<any[]>([]);
|
|
162
|
-
const
|
|
172
|
+
const isAiResponseReceivedAnalizeImage = ref([]);
|
|
173
|
+
const isAiResponseReceivedAnalizeNoImage = ref([]);
|
|
163
174
|
const isAiResponseReceivedImage = ref([]);
|
|
164
175
|
const primaryKey = props.meta.primaryKey;
|
|
165
176
|
const openGenerationCarousel = ref([]);
|
|
@@ -178,11 +189,20 @@ const isDialogOpen = ref(false);
|
|
|
178
189
|
const isAiGenerationError = ref<boolean[]>([false]);
|
|
179
190
|
const aiGenerationErrorMessage = ref<string[]>([]);
|
|
180
191
|
const isAiImageGenerationError = ref<boolean[]>([false]);
|
|
192
|
+
|
|
193
|
+
const isImageToTextGenerationError = ref<boolean[]>([false]);
|
|
194
|
+
const imageToTextErrorMessages = ref<string[]>([]);
|
|
195
|
+
|
|
196
|
+
const isTextToTextGenerationError = ref<boolean[]>([false]);
|
|
197
|
+
const textToTextErrorMessages = ref<string[]>([]);
|
|
198
|
+
|
|
181
199
|
const imageGenerationErrorMessage = ref<string[]>([]);
|
|
182
200
|
const isImageHasPreviewUrl = ref<Record<string, boolean>>({});
|
|
183
201
|
const popupMode = ref<'generation' | 'confirmation' | 'settings'>('confirmation');
|
|
184
202
|
const generationPrompts = ref<any>({});
|
|
185
203
|
|
|
204
|
+
const regeneratingFieldsStatus = ref<Record<string, Record<string, boolean>>>({});
|
|
205
|
+
|
|
186
206
|
const openDialog = async () => {
|
|
187
207
|
if (props.meta.askConfirmationBeforeGenerating) {
|
|
188
208
|
popupMode.value = 'confirmation';
|
|
@@ -228,8 +248,9 @@ const openDialog = async () => {
|
|
|
228
248
|
|
|
229
249
|
|
|
230
250
|
function runAiActions() {
|
|
231
|
-
|
|
232
|
-
|
|
251
|
+
popupMode.value = 'generation';
|
|
252
|
+
|
|
253
|
+
if (props.meta.isImageGeneration) {
|
|
233
254
|
isGeneratingImages.value = true;
|
|
234
255
|
runAiAction({
|
|
235
256
|
endpoint: 'initial_image_generate',
|
|
@@ -242,7 +263,7 @@ function runAiActions() {
|
|
|
242
263
|
runAiAction({
|
|
243
264
|
endpoint: 'analyze',
|
|
244
265
|
actionType: 'analyze',
|
|
245
|
-
responseFlag:
|
|
266
|
+
responseFlag: isAiResponseReceivedAnalizeImage,
|
|
246
267
|
});
|
|
247
268
|
}
|
|
248
269
|
if (props.meta.isFieldsForAnalizePlain) {
|
|
@@ -250,13 +271,14 @@ function runAiActions() {
|
|
|
250
271
|
runAiAction({
|
|
251
272
|
endpoint: 'analyze_no_images',
|
|
252
273
|
actionType: 'analyze_no_images',
|
|
253
|
-
responseFlag:
|
|
274
|
+
responseFlag: isAiResponseReceivedAnalizeNoImage,
|
|
254
275
|
});
|
|
255
276
|
}
|
|
256
277
|
}
|
|
257
278
|
|
|
258
279
|
const closeDialog = () => {
|
|
259
|
-
|
|
280
|
+
isAiResponseReceivedAnalizeImage.value = [];
|
|
281
|
+
isAiResponseReceivedAnalizeNoImage.value = [];
|
|
260
282
|
isAiResponseReceivedImage.value = [];
|
|
261
283
|
|
|
262
284
|
records.value = [];
|
|
@@ -652,9 +674,9 @@ async function runAiAction({
|
|
|
652
674
|
}
|
|
653
675
|
}
|
|
654
676
|
//marking that we received response for this record
|
|
655
|
-
if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
|
|
677
|
+
//if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
|
|
656
678
|
responseFlag.value[index] = true;
|
|
657
|
-
}
|
|
679
|
+
//}
|
|
658
680
|
//updating selected with new data from AI
|
|
659
681
|
const pk = selected.value[index]?.[primaryKey];
|
|
660
682
|
if (pk) {
|
|
@@ -674,9 +696,9 @@ async function runAiAction({
|
|
|
674
696
|
// if job is failed - set error
|
|
675
697
|
} else if (jobStatus === 'failed') {
|
|
676
698
|
const index = selected.value.findIndex(item => String(item[primaryKey]) === String(recordId));
|
|
677
|
-
if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
|
|
699
|
+
//if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
|
|
678
700
|
responseFlag.value[index] = true;
|
|
679
|
-
}
|
|
701
|
+
//}
|
|
680
702
|
if (index !== -1) {
|
|
681
703
|
jobsIds.splice(jobsIds.findIndex(j => j.jobId === jobId), 1);
|
|
682
704
|
} else {
|
|
@@ -691,9 +713,12 @@ async function runAiAction({
|
|
|
691
713
|
if (actionType === 'generate_images') {
|
|
692
714
|
isAiImageGenerationError.value[index] = true;
|
|
693
715
|
imageGenerationErrorMessage.value[index] = jobResponse.job?.error || 'Unknown error';
|
|
694
|
-
} else {
|
|
695
|
-
|
|
696
|
-
|
|
716
|
+
} else if (actionType === 'analyze') {
|
|
717
|
+
isImageToTextGenerationError.value[index] = true;
|
|
718
|
+
imageToTextErrorMessages.value[index] = jobResponse.job?.error || 'Unknown error';
|
|
719
|
+
} else if (actionType === 'analyze_no_images') {
|
|
720
|
+
isTextToTextGenerationError.value[index] = true;
|
|
721
|
+
textToTextErrorMessages.value[index] = jobResponse.job?.error || 'Unknown error';
|
|
697
722
|
}
|
|
698
723
|
}
|
|
699
724
|
}
|
|
@@ -918,4 +943,68 @@ function checkAndAddNewFieldsToPrompts(savedPrompts, defaultPrompts) {
|
|
|
918
943
|
return savedPrompts;
|
|
919
944
|
}
|
|
920
945
|
|
|
946
|
+
async function regenerateCell(recordInfo: any) {
|
|
947
|
+
console.log('Regenerating cell for record:', recordInfo.recordId, 'field:', recordInfo.fieldName);
|
|
948
|
+
if (!regeneratingFieldsStatus.value[recordInfo.recordId]) {
|
|
949
|
+
regeneratingFieldsStatus.value[recordInfo.recordId] = {};
|
|
950
|
+
}
|
|
951
|
+
regeneratingFieldsStatus.value[recordInfo.recordId][recordInfo.fieldName] = true;
|
|
952
|
+
const actionType = props.meta.outputFieldsForAnalizeFromImages?.includes(recordInfo.fieldName)
|
|
953
|
+
? 'analyze'
|
|
954
|
+
: props.meta.outputPlainFields?.includes(recordInfo.fieldName)
|
|
955
|
+
? 'analyze_no_images'
|
|
956
|
+
: null;
|
|
957
|
+
if (!actionType) {
|
|
958
|
+
console.error(`Field ${recordInfo.fieldName} is not configured for analysis.`);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
let generationPromptsForField = {};
|
|
963
|
+
if (actionType === 'analyze') {
|
|
964
|
+
generationPromptsForField = generationPrompts.value.imageFieldsPrompts || {};
|
|
965
|
+
} else if (actionType === 'analyze_no_images') {
|
|
966
|
+
generationPromptsForField = generationPrompts.value.plainFieldsPrompts || {};
|
|
967
|
+
}
|
|
968
|
+
console.log('Using generation prompts for field regeneration:', generationPromptsForField);
|
|
969
|
+
|
|
970
|
+
let res;
|
|
971
|
+
try {
|
|
972
|
+
res = await callAdminForthApi({
|
|
973
|
+
path: `/plugin/${props.meta.pluginInstanceId}/regenerate-cell`,
|
|
974
|
+
method: 'POST',
|
|
975
|
+
body: {
|
|
976
|
+
fieldToRegenerate: recordInfo.fieldName,
|
|
977
|
+
recordId: recordInfo.recordId,
|
|
978
|
+
actionType: actionType,
|
|
979
|
+
prompt: generationPromptsForField[recordInfo.fieldName] || null,
|
|
980
|
+
},
|
|
981
|
+
});
|
|
982
|
+
} catch (e) {
|
|
983
|
+
console.error(`Error during cell regeneration for record ${recordInfo.recordId}, field ${recordInfo.fieldName}:`, e);
|
|
984
|
+
}
|
|
985
|
+
if ( res.ok === false) {
|
|
986
|
+
adminforth.alert({
|
|
987
|
+
message: res.error,
|
|
988
|
+
variant: 'danger',
|
|
989
|
+
});
|
|
990
|
+
isError.value = true;
|
|
991
|
+
errorMessage.value = t(`Failed to regenerate field. You are not allowed to regenerate.`);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
console.log('Regeneration response:', res);
|
|
995
|
+
const index = selected.value.findIndex(item => String(item[primaryKey]) === String(recordInfo.recordId));
|
|
996
|
+
console.log('Found index in selected array:', index);
|
|
997
|
+
|
|
998
|
+
const pk = selected.value[index]?.[primaryKey];
|
|
999
|
+
console.log('Primary key for the record:', pk);
|
|
1000
|
+
if (pk) {
|
|
1001
|
+
selected.value[index] = {
|
|
1002
|
+
...selected.value[index],
|
|
1003
|
+
...res.result,
|
|
1004
|
+
isChecked: true,
|
|
1005
|
+
[primaryKey]: pk,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
regeneratingFieldsStatus.value[recordInfo.recordId][recordInfo.fieldName] = false;
|
|
1009
|
+
}
|
|
921
1010
|
</script>
|
|
@@ -54,7 +54,21 @@
|
|
|
54
54
|
</template>
|
|
55
55
|
<!-- CUSTOM FIELD TEMPLATES -->
|
|
56
56
|
<template v-for="n in customFieldNames" :key="n" #[`cell:${n}`]="{ item, column }">
|
|
57
|
-
<div v-if="
|
|
57
|
+
<div v-if="(isAnalyzing(item, n) && !(props.regeneratingFieldsStatus[item[props.primaryKey]] && props.regeneratingFieldsStatus[item[props.primaryKey]][n])) && !isInColumnImage(n)" @mouseenter="(() => { hovers[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = true})" @mouseleave="(() => { hovers[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false})">
|
|
58
|
+
<div class="flex gap-1 justify-end">
|
|
59
|
+
<Tooltip v-if="checkForError(item, n)">
|
|
60
|
+
<IconExclamationCircleSolid class="my-2 w-5 h-5 text-red-500" />
|
|
61
|
+
<template #tooltip>
|
|
62
|
+
{{ checkForError(item, n) }}
|
|
63
|
+
</template>
|
|
64
|
+
</Tooltip>
|
|
65
|
+
<Tooltip>
|
|
66
|
+
<IconRefreshOutline class="my-2 w-5 h-5 hover:text-blue-500" :class="{ 'opacity-50 cursor-not-allowed hover': shouldDisableRegenerateFieldIcon(item, n) }" @click="regerenerateFieldIconClick(item, n)"/>
|
|
67
|
+
<template #tooltip>
|
|
68
|
+
{{ shouldDisableRegenerateFieldIcon(item, n) ? $t("Can't analyze image without source image") : $t('Regenerate') }}
|
|
69
|
+
</template>
|
|
70
|
+
</Tooltip>
|
|
71
|
+
</div>
|
|
58
72
|
<div v-if="isInColumnEnum(n)" class="flex flex-col items-start justify-end min-h-[90px]">
|
|
59
73
|
<Select
|
|
60
74
|
class="min-w-[150px]"
|
|
@@ -89,7 +103,7 @@
|
|
|
89
103
|
</template>
|
|
90
104
|
</Tooltip>
|
|
91
105
|
</div>
|
|
92
|
-
<div v-else-if="typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'boolean'" class="flex flex-col items-
|
|
106
|
+
<div v-else-if="typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'boolean'" class="flex flex-col items-center justify-end min-h-[90px]">
|
|
93
107
|
<Toggle
|
|
94
108
|
class="p-2"
|
|
95
109
|
v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
|
|
@@ -195,7 +209,7 @@
|
|
|
195
209
|
<Skeleton type="image" class="w-20 h-20" />
|
|
196
210
|
</div>
|
|
197
211
|
|
|
198
|
-
<div v-if="!
|
|
212
|
+
<div v-if="(!isAnalyzing(item, n) || (props.regeneratingFieldsStatus[item[props.primaryKey]] && props.regeneratingFieldsStatus[item[props.primaryKey]][n])) && !isInColumnImage(n)">
|
|
199
213
|
<Skeleton class="w-full h-6" />
|
|
200
214
|
</div>
|
|
201
215
|
</template>
|
|
@@ -203,11 +217,11 @@
|
|
|
203
217
|
</template>
|
|
204
218
|
|
|
205
219
|
<script lang="ts" setup>
|
|
206
|
-
import { ref, watch } from 'vue'
|
|
220
|
+
import { ref, watch, onMounted } from 'vue'
|
|
207
221
|
import { Select, Input, Textarea, Table, Checkbox, Skeleton, Toggle, Tooltip } from '@/afcl'
|
|
208
222
|
import GenerationCarousel from './ImageGenerationCarousel.vue'
|
|
209
223
|
import ImageCompare from './ImageCompare.vue';
|
|
210
|
-
import { IconRefreshOutline } from '@iconify-prerendered/vue-flowbite';
|
|
224
|
+
import { IconRefreshOutline, IconExclamationCircleSolid } from '@iconify-prerendered/vue-flowbite';
|
|
211
225
|
|
|
212
226
|
const props = defineProps<{
|
|
213
227
|
meta: any,
|
|
@@ -216,7 +230,8 @@ const props = defineProps<{
|
|
|
216
230
|
customFieldNames: any,
|
|
217
231
|
tableColumnsIndexes: any,
|
|
218
232
|
selected: any,
|
|
219
|
-
|
|
233
|
+
isAiResponseReceivedAnalizeImage: boolean[],
|
|
234
|
+
isAiResponseReceivedAnalizeNoImage: boolean[],
|
|
220
235
|
isAiResponseReceivedImage: boolean[],
|
|
221
236
|
primaryKey: any,
|
|
222
237
|
openGenerationCarousel: any,
|
|
@@ -233,8 +248,16 @@ const props = defineProps<{
|
|
|
233
248
|
oldData: any[],
|
|
234
249
|
isImageHasPreviewUrl: Record<string, boolean>
|
|
235
250
|
imageGenerationPrompts: Record<string, any>
|
|
251
|
+
isImageToTextGenerationError: boolean[],
|
|
252
|
+
imageToTextErrorMessages: string[],
|
|
253
|
+
isTextToTextGenerationError: boolean[],
|
|
254
|
+
textToTextErrorMessages: string[],
|
|
255
|
+
outputImageFields: string[],
|
|
256
|
+
outputFieldsForAnalizeFromImages: string[],
|
|
257
|
+
outputPlainFields: string[],
|
|
258
|
+
regeneratingFieldsStatus: Record<string, Record<string, boolean>>
|
|
236
259
|
}>();
|
|
237
|
-
const emit = defineEmits(['error', 'regenerateImages']);
|
|
260
|
+
const emit = defineEmits(['error', 'regenerateImages', 'regenerateCell']);
|
|
238
261
|
|
|
239
262
|
|
|
240
263
|
const zoomedImage = ref(null);
|
|
@@ -305,6 +328,61 @@ function isValidUrl(str: string): boolean {
|
|
|
305
328
|
}
|
|
306
329
|
}
|
|
307
330
|
|
|
331
|
+
function regerenerateFieldIconClick(item, name) {
|
|
332
|
+
if (shouldDisableRegenerateFieldIcon(item, name)) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
emit('regenerateCell', {
|
|
336
|
+
recordId: item[props.primaryKey],
|
|
337
|
+
fieldName: name
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
function shouldDisableRegenerateFieldIcon(item, name) {
|
|
342
|
+
if (props.outputFieldsForAnalizeFromImages.findIndex( el => el === name) !== -1 &&
|
|
343
|
+
props.imageToTextErrorMessages[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === item[props.primaryKey])] === 'No source images found') {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function checkForError(item, name) {
|
|
350
|
+
if (props.outputFieldsForAnalizeFromImages.findIndex( el => el === name) !== -1) {
|
|
351
|
+
const errorMessage = props.imageToTextErrorMessages[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === item[props.primaryKey])];
|
|
352
|
+
if (errorMessage) {
|
|
353
|
+
return errorMessage;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (props.outputPlainFields.findIndex( el => el === name) !== -1) {
|
|
357
|
+
const errorMessage = props.textToTextErrorMessages[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === item[props.primaryKey])];
|
|
358
|
+
if (errorMessage) {
|
|
359
|
+
return errorMessage;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function isAnalyzing(item, name) {
|
|
366
|
+
if (props.outputFieldsForAnalizeFromImages.findIndex( el => el === name) !== -1) {
|
|
367
|
+
return isImagesAnalyzing(item);
|
|
368
|
+
}
|
|
369
|
+
if (props.outputPlainFields.findIndex( el => el === name) !== -1) {
|
|
370
|
+
return isNoImageAnalyzing(item);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function isImagesAnalyzing(item) {
|
|
377
|
+
const isImagesAnalyzing = props.isAiResponseReceivedAnalizeImage[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === item[props.primaryKey])]
|
|
378
|
+
return isImagesAnalyzing;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function isNoImageAnalyzing(item) {
|
|
382
|
+
const isNoImageAnalyzing = props.isAiResponseReceivedAnalizeNoImage[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === item[props.primaryKey])]
|
|
383
|
+
return isNoImageAnalyzing;
|
|
384
|
+
}
|
|
385
|
+
|
|
308
386
|
|
|
309
387
|
|
|
310
388
|
</script>
|
package/dist/index.js
CHANGED
|
@@ -77,6 +77,20 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
77
77
|
}
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
+
getPromptForImageAnalysis(compiledOutputFields) {
|
|
81
|
+
const prompt = `Analyze the following image(s) and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
|
|
82
|
+
Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
|
|
83
|
+
Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names. If it's number field - return only number.
|
|
84
|
+
Image URLs:`;
|
|
85
|
+
return prompt;
|
|
86
|
+
}
|
|
87
|
+
getPromptForPlainFields(compiledOutputFields) {
|
|
88
|
+
const prompt = `Generate the values of fields in object by using next prompts (key is field name, value is prompt):
|
|
89
|
+
${JSON.stringify(compiledOutputFields)} In output object use the same field names (keys) as in input.
|
|
90
|
+
Return a single valid passable JSON object in format like: {"meta_title": "generated_value"}.
|
|
91
|
+
Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.`;
|
|
92
|
+
return prompt;
|
|
93
|
+
}
|
|
80
94
|
analyze_image(jobId, recordId, adminUser, headers, customPrompt) {
|
|
81
95
|
return __awaiter(this, void 0, void 0, function* () {
|
|
82
96
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
@@ -113,10 +127,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
113
127
|
}
|
|
114
128
|
//create prompt for OpenAI
|
|
115
129
|
const compiledOutputFields = yield this.compileOutputFieldsTemplates(record, customPrompt);
|
|
116
|
-
const prompt =
|
|
117
|
-
Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
|
|
118
|
-
Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names. If it's number field - return only number.
|
|
119
|
-
Image URLs:`;
|
|
130
|
+
const prompt = this.getPromptForImageAnalysis(compiledOutputFields);
|
|
120
131
|
//send prompt to OpenAI and get response
|
|
121
132
|
let chatResponse;
|
|
122
133
|
try {
|
|
@@ -165,16 +176,14 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
165
176
|
if (STUB_MODE) {
|
|
166
177
|
yield new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 20000) + 1000));
|
|
167
178
|
jobs.set(jobId, { status: 'completed', result: {} });
|
|
168
|
-
|
|
179
|
+
jobs.set(jobId, { status: 'failed', error: `ERROR: test error` });
|
|
180
|
+
return { ok: false, error: 'test error' };
|
|
169
181
|
}
|
|
170
182
|
else {
|
|
171
183
|
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
172
184
|
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, selectedId)]);
|
|
173
185
|
const compiledOutputFields = yield this.compileOutputFieldsTemplatesNoImage(record, customPrompt);
|
|
174
|
-
const prompt =
|
|
175
|
-
${JSON.stringify(compiledOutputFields)} In output object use the same field names (keys) as in input.
|
|
176
|
-
Return a single valid passable JSON object in format like: {"meta_title": "generated_value"}.
|
|
177
|
-
Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.`;
|
|
186
|
+
const prompt = this.getPromptForPlainFields(compiledOutputFields);
|
|
178
187
|
//send prompt to OpenAI and get response
|
|
179
188
|
const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
|
|
180
189
|
let resp;
|
|
@@ -407,6 +416,18 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
407
416
|
outputImageFields.push(key);
|
|
408
417
|
}
|
|
409
418
|
}
|
|
419
|
+
const outputPlainFields = [];
|
|
420
|
+
if (this.options.fillPlainFields) {
|
|
421
|
+
for (const [key, value] of Object.entries(this.options.fillPlainFields)) {
|
|
422
|
+
outputPlainFields.push(key);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const outputFieldsForAnalizeFromImages = [];
|
|
426
|
+
if (this.options.fillFieldsFromImages) {
|
|
427
|
+
for (const [key, value] of Object.entries(this.options.fillFieldsFromImages)) {
|
|
428
|
+
outputFieldsForAnalizeFromImages.push(key);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
410
431
|
const outputImagesPluginInstanceIds = {};
|
|
411
432
|
//check if Upload plugin is installed on all attachment fields
|
|
412
433
|
if (this.options.generateImages) {
|
|
@@ -435,7 +456,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
435
456
|
actionName: this.options.actionName,
|
|
436
457
|
columnEnums: columnEnums,
|
|
437
458
|
outputImageFields: outputImageFields,
|
|
438
|
-
|
|
459
|
+
outputFieldsForAnalizeFromImages: outputFieldsForAnalizeFromImages,
|
|
460
|
+
outputPlainFields: outputPlainFields,
|
|
439
461
|
primaryKey: primaryKeyColumn.name,
|
|
440
462
|
outputImagesPluginInstanceIds: outputImagesPluginInstanceIds,
|
|
441
463
|
isFieldsForAnalizeFromImages: this.options.fillFieldsFromImages ? Object.keys(this.options.fillFieldsFromImages).length > 0 : false,
|
|
@@ -814,5 +836,93 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
814
836
|
}
|
|
815
837
|
})
|
|
816
838
|
});
|
|
839
|
+
server.endpoint({
|
|
840
|
+
method: 'POST',
|
|
841
|
+
path: `/plugin/${this.pluginInstanceId}/regenerate-cell`,
|
|
842
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers }) {
|
|
843
|
+
var _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
844
|
+
const recordId = body.recordId;
|
|
845
|
+
const fieldToRegenerate = body.fieldToRegenerate;
|
|
846
|
+
let prompt = body.prompt;
|
|
847
|
+
const actionType = body.actionType;
|
|
848
|
+
console.log('Regenerate cell called with:', { recordId, fieldToRegenerate, actionType, prompt });
|
|
849
|
+
if (!fieldToRegenerate || !recordId || !actionType) {
|
|
850
|
+
return { ok: false, error: "Missing parameters" };
|
|
851
|
+
}
|
|
852
|
+
if (!prompt) {
|
|
853
|
+
if (actionType === 'analyze') {
|
|
854
|
+
prompt = this.options.fillFieldsFromImages ? this.options.fillFieldsFromImages[fieldToRegenerate] : null;
|
|
855
|
+
}
|
|
856
|
+
else if (actionType === 'analyze_no_images') {
|
|
857
|
+
prompt = this.options.fillPlainFields ? this.options.fillPlainFields[fieldToRegenerate] : null;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
861
|
+
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, recordId)]);
|
|
862
|
+
let promptToPass = JSON.stringify({ [fieldToRegenerate]: prompt });
|
|
863
|
+
if (STUB_MODE) {
|
|
864
|
+
yield new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 20000) + 1000));
|
|
865
|
+
return { ok: true, result: { [fieldToRegenerate]: "stub value" } };
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
if (actionType === 'analyze') {
|
|
869
|
+
const compiledPropmt = yield this.compileOutputFieldsTemplates(record, promptToPass);
|
|
870
|
+
const finalPrompt = this.getPromptForImageAnalysis(compiledPropmt);
|
|
871
|
+
const attachmentFiles = yield this.options.attachFiles({ record: record });
|
|
872
|
+
if (attachmentFiles.length === 0) {
|
|
873
|
+
return { ok: false, error: "No source images found" };
|
|
874
|
+
}
|
|
875
|
+
let visionAdapterResponse;
|
|
876
|
+
try {
|
|
877
|
+
visionAdapterResponse = yield this.options.visionAdapter.generate({ prompt: finalPrompt, inputFileUrls: attachmentFiles });
|
|
878
|
+
}
|
|
879
|
+
catch (e) {
|
|
880
|
+
return { ok: false, error: 'AI provider refused to analyze images' };
|
|
881
|
+
}
|
|
882
|
+
const resp = visionAdapterResponse.response;
|
|
883
|
+
const topLevelError = visionAdapterResponse.error;
|
|
884
|
+
if (topLevelError || (resp === null || resp === void 0 ? void 0 : resp.error)) {
|
|
885
|
+
return { ok: false, error: `ERROR: ${JSON.stringify(topLevelError.message || (resp === null || resp === void 0 ? void 0 : resp.error.message))}` };
|
|
886
|
+
}
|
|
887
|
+
const textOutput = (_g = (_f = (_e = (_d = (_c = (_b = resp === null || resp === void 0 ? void 0 : resp.output) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.content) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.text) !== null && _f !== void 0 ? _f : resp === null || resp === void 0 ? void 0 : resp.output_text) !== null && _g !== void 0 ? _g : (_k = (_j = (_h = resp === null || resp === void 0 ? void 0 : resp.choices) === null || _h === void 0 ? void 0 : _h[0]) === null || _j === void 0 ? void 0 : _j.message) === null || _k === void 0 ? void 0 : _k.content;
|
|
888
|
+
if (!textOutput || typeof textOutput !== 'string') {
|
|
889
|
+
return { ok: false, error: 'AI response is not valid text' };
|
|
890
|
+
}
|
|
891
|
+
let resData;
|
|
892
|
+
try {
|
|
893
|
+
resData = JSON.parse(textOutput);
|
|
894
|
+
}
|
|
895
|
+
catch (e) {
|
|
896
|
+
return { ok: false, error: 'AI response is not valid JSON. Probably attached invalid image URL' };
|
|
897
|
+
}
|
|
898
|
+
return { ok: true, result: resData };
|
|
899
|
+
}
|
|
900
|
+
else if (actionType === 'analyze_no_images') {
|
|
901
|
+
const compiledPropmt = yield this.compileOutputFieldsTemplatesNoImage(record, promptToPass);
|
|
902
|
+
const finalPrompt = this.getPromptForPlainFields(compiledPropmt);
|
|
903
|
+
const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
|
|
904
|
+
let resp;
|
|
905
|
+
try {
|
|
906
|
+
const { content: chatResponse, error: topLevelError } = yield this.options.textCompleteAdapter.complete(finalPrompt, [], numberOfTokens);
|
|
907
|
+
if (topLevelError) {
|
|
908
|
+
return { ok: false, error: `ERROR: ${JSON.stringify(topLevelError)}` };
|
|
909
|
+
}
|
|
910
|
+
resp = chatResponse;
|
|
911
|
+
}
|
|
912
|
+
catch (e) {
|
|
913
|
+
return { ok: false, error: 'AI provider refused to analyze plain fields' };
|
|
914
|
+
}
|
|
915
|
+
let resData;
|
|
916
|
+
try {
|
|
917
|
+
resData = JSON.parse(resp);
|
|
918
|
+
}
|
|
919
|
+
catch (e) {
|
|
920
|
+
return { ok: false, error: 'AI response is not valid JSON' };
|
|
921
|
+
}
|
|
922
|
+
return { ok: true, result: resData };
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
})
|
|
926
|
+
});
|
|
817
927
|
}
|
|
818
928
|
}
|
package/index.ts
CHANGED
|
@@ -76,6 +76,22 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
private getPromptForImageAnalysis(compiledOutputFields: Record<string, string>) {
|
|
80
|
+
const prompt = `Analyze the following image(s) and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
|
|
81
|
+
Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
|
|
82
|
+
Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names. If it's number field - return only number.
|
|
83
|
+
Image URLs:`;
|
|
84
|
+
return prompt;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private getPromptForPlainFields(compiledOutputFields: Record<string, string>){
|
|
88
|
+
const prompt = `Generate the values of fields in object by using next prompts (key is field name, value is prompt):
|
|
89
|
+
${JSON.stringify(compiledOutputFields)} In output object use the same field names (keys) as in input.
|
|
90
|
+
Return a single valid passable JSON object in format like: {"meta_title": "generated_value"}.
|
|
91
|
+
Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.`;
|
|
92
|
+
return prompt;
|
|
93
|
+
}
|
|
94
|
+
|
|
79
95
|
private async analyze_image(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>, customPrompt? : string) {
|
|
80
96
|
const selectedId = recordId;
|
|
81
97
|
let isError = false;
|
|
@@ -107,10 +123,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
107
123
|
}
|
|
108
124
|
//create prompt for OpenAI
|
|
109
125
|
const compiledOutputFields = await this.compileOutputFieldsTemplates(record, customPrompt);
|
|
110
|
-
const prompt =
|
|
111
|
-
Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
|
|
112
|
-
Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names. If it's number field - return only number.
|
|
113
|
-
Image URLs:`;
|
|
126
|
+
const prompt = this.getPromptForImageAnalysis(compiledOutputFields);
|
|
114
127
|
|
|
115
128
|
//send prompt to OpenAI and get response
|
|
116
129
|
let chatResponse;
|
|
@@ -159,16 +172,14 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
159
172
|
if (STUB_MODE) {
|
|
160
173
|
await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 20000) + 1000));
|
|
161
174
|
jobs.set(jobId, { status: 'completed', result: {} });
|
|
162
|
-
|
|
175
|
+
jobs.set(jobId, { status: 'failed', error: `ERROR: test error` });
|
|
176
|
+
return { ok: false, error: 'test error' };
|
|
163
177
|
} else {
|
|
164
178
|
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
165
179
|
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, selectedId)] );
|
|
166
180
|
|
|
167
181
|
const compiledOutputFields = await this.compileOutputFieldsTemplatesNoImage(record, customPrompt);
|
|
168
|
-
const prompt =
|
|
169
|
-
${JSON.stringify(compiledOutputFields)} In output object use the same field names (keys) as in input.
|
|
170
|
-
Return a single valid passable JSON object in format like: {"meta_title": "generated_value"}.
|
|
171
|
-
Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.`;
|
|
182
|
+
const prompt = this.getPromptForPlainFields(compiledOutputFields);
|
|
172
183
|
//send prompt to OpenAI and get response
|
|
173
184
|
const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
|
|
174
185
|
let resp: any;
|
|
@@ -393,6 +404,19 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
393
404
|
outputImageFields.push(key);
|
|
394
405
|
}
|
|
395
406
|
}
|
|
407
|
+
const outputPlainFields = [];
|
|
408
|
+
if (this.options.fillPlainFields) {
|
|
409
|
+
for (const [key, value] of Object.entries(this.options.fillPlainFields)) {
|
|
410
|
+
outputPlainFields.push(key);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const outputFieldsForAnalizeFromImages = [];
|
|
414
|
+
if (this.options.fillFieldsFromImages) {
|
|
415
|
+
for (const [key, value] of Object.entries(this.options.fillFieldsFromImages)) {
|
|
416
|
+
outputFieldsForAnalizeFromImages.push(key);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
396
420
|
const outputImagesPluginInstanceIds = {};
|
|
397
421
|
//check if Upload plugin is installed on all attachment fields
|
|
398
422
|
if (this.options.generateImages) {
|
|
@@ -430,7 +454,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
430
454
|
actionName: this.options.actionName,
|
|
431
455
|
columnEnums: columnEnums,
|
|
432
456
|
outputImageFields: outputImageFields,
|
|
433
|
-
|
|
457
|
+
outputFieldsForAnalizeFromImages: outputFieldsForAnalizeFromImages,
|
|
458
|
+
outputPlainFields: outputPlainFields,
|
|
434
459
|
primaryKey: primaryKeyColumn.name,
|
|
435
460
|
outputImagesPluginInstanceIds: outputImagesPluginInstanceIds,
|
|
436
461
|
isFieldsForAnalizeFromImages: this.options.fillFieldsFromImages ? Object.keys(this.options.fillFieldsFromImages).length > 0 : false,
|
|
@@ -836,6 +861,91 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
836
861
|
}
|
|
837
862
|
});
|
|
838
863
|
|
|
864
|
+
server.endpoint({
|
|
865
|
+
method: 'POST',
|
|
866
|
+
path: `/plugin/${this.pluginInstanceId}/regenerate-cell`,
|
|
867
|
+
handler: async ({ body, adminUser, headers }) => {
|
|
868
|
+
const recordId = body.recordId;
|
|
869
|
+
const fieldToRegenerate = body.fieldToRegenerate;
|
|
870
|
+
let prompt = body.prompt;
|
|
871
|
+
const actionType = body.actionType;
|
|
872
|
+
console.log('Regenerate cell called with:', { recordId, fieldToRegenerate, actionType, prompt });
|
|
873
|
+
if (!fieldToRegenerate || !recordId || !actionType ) {
|
|
874
|
+
return { ok: false, error: "Missing parameters" };
|
|
875
|
+
}
|
|
876
|
+
if ( !prompt ) {
|
|
877
|
+
if (actionType === 'analyze') {
|
|
878
|
+
prompt = this.options.fillFieldsFromImages ? (this.options.fillFieldsFromImages as any)[fieldToRegenerate] : null;
|
|
879
|
+
} else if (actionType === 'analyze_no_images') {
|
|
880
|
+
prompt = this.options.fillPlainFields ? (this.options.fillPlainFields as any)[fieldToRegenerate] : null;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
884
|
+
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, recordId)] );
|
|
839
885
|
|
|
886
|
+
let promptToPass = JSON.stringify({[fieldToRegenerate]: prompt});
|
|
887
|
+
if (STUB_MODE) {
|
|
888
|
+
await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 20000) + 1000));
|
|
889
|
+
return { ok: true, result: {[fieldToRegenerate]: "stub value"} };
|
|
890
|
+
} else {
|
|
891
|
+
if ( actionType === 'analyze') {
|
|
892
|
+
const compiledPropmt = await this.compileOutputFieldsTemplates(record, promptToPass);
|
|
893
|
+
const finalPrompt = this.getPromptForImageAnalysis(compiledPropmt);
|
|
894
|
+
const attachmentFiles = await this.options.attachFiles({ record: record });
|
|
895
|
+
if (attachmentFiles.length === 0) {
|
|
896
|
+
return { ok: false, error: "No source images found" };
|
|
897
|
+
}
|
|
898
|
+
let visionAdapterResponse;
|
|
899
|
+
try {
|
|
900
|
+
visionAdapterResponse = await this.options.visionAdapter.generate({ prompt: finalPrompt, inputFileUrls: attachmentFiles });
|
|
901
|
+
} catch (e) {
|
|
902
|
+
return { ok: false, error: 'AI provider refused to analyze images' };
|
|
903
|
+
}
|
|
904
|
+
const resp: any = (visionAdapterResponse as any).response;
|
|
905
|
+
const topLevelError = (visionAdapterResponse as any).error;
|
|
906
|
+
if (topLevelError || resp?.error) {
|
|
907
|
+
return { ok: false, error: `ERROR: ${JSON.stringify(topLevelError.message || resp?.error.message)}` };
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const textOutput = resp?.output?.[0]?.content?.[0]?.text ?? resp?.output_text ?? resp?.choices?.[0]?.message?.content;
|
|
911
|
+
if (!textOutput || typeof textOutput !== 'string') {
|
|
912
|
+
return { ok: false, error: 'AI response is not valid text' };
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
let resData;
|
|
916
|
+
try {
|
|
917
|
+
resData = JSON.parse(textOutput);
|
|
918
|
+
} catch (e) {
|
|
919
|
+
return { ok: false, error: 'AI response is not valid JSON. Probably attached invalid image URL' };
|
|
920
|
+
}
|
|
921
|
+
return { ok: true, result: resData };
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
} else if ( actionType === 'analyze_no_images') {
|
|
926
|
+
const compiledPropmt = await this.compileOutputFieldsTemplatesNoImage(record, promptToPass);
|
|
927
|
+
const finalPrompt = this.getPromptForPlainFields(compiledPropmt);
|
|
928
|
+
const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
|
|
929
|
+
let resp;
|
|
930
|
+
try {
|
|
931
|
+
const { content: chatResponse, error: topLevelError } = await this.options.textCompleteAdapter.complete(finalPrompt, [], numberOfTokens);
|
|
932
|
+
if (topLevelError) {
|
|
933
|
+
return { ok: false, error: `ERROR: ${JSON.stringify(topLevelError)}` };
|
|
934
|
+
}
|
|
935
|
+
resp = chatResponse;
|
|
936
|
+
} catch (e) {
|
|
937
|
+
return { ok: false, error: 'AI provider refused to analyze plain fields' };
|
|
938
|
+
}
|
|
939
|
+
let resData;
|
|
940
|
+
try {
|
|
941
|
+
resData = JSON.parse(resp);
|
|
942
|
+
} catch (e) {
|
|
943
|
+
return { ok: false, error: 'AI response is not valid JSON' };
|
|
944
|
+
}
|
|
945
|
+
return { ok: true, result: resData };
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
});
|
|
840
950
|
}
|
|
841
951
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/bulk-ai-flow",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@types/handlebars": "^4.0.40",
|
|
30
|
-
"adminforth": "^2.13.0-next.
|
|
30
|
+
"adminforth": "^2.13.0-next.40",
|
|
31
31
|
"handlebars": "^4.7.8"
|
|
32
32
|
},
|
|
33
33
|
"release": {
|