@adminforth/bulk-ai-flow 1.15.10 → 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 CHANGED
@@ -13,5 +13,5 @@ custom/package-lock.json
13
13
  custom/package.json
14
14
  custom/tsconfig.json
15
15
 
16
- sent 81,130 bytes received 172 bytes 162,604.00 bytes/sec
17
- total size is 80,498 speedup is 0.99
16
+ sent 88,118 bytes received 172 bytes 176,580.00 bytes/sec
17
+ total size is 87,473 speedup is 0.99
@@ -13,7 +13,7 @@
13
13
  : popupMode === 'settings' ? 'lg:w-[1000px] !lg:max-w-[1000px]'
14
14
  : 'lg:w-[500px] !lg:max-w-[500px]'"
15
15
  :beforeCloseFunction="closeDialog"
16
- :closable="popupMode === 'generation' ? false : true"
16
+ :closable="false"
17
17
  :askForCloseConfirmation="popupMode === 'generation' ? true : false"
18
18
  closeConfirmationText="Are you sure you want to close without saving?"
19
19
  :buttons="popupMode === 'generation' ? [
@@ -65,7 +65,8 @@
65
65
  :tableColumnsIndexes="tableColumnsIndexes"
66
66
  :selected="selected"
67
67
  :oldData="oldData"
68
- :isAiResponseReceivedAnalize="isAiResponseReceivedAnalize"
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 isAiResponseReceivedAnalize = ref([]);
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
- popupMode.value = 'generation';
232
- if (props.meta.isImageGeneration) {
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: isAiResponseReceivedAnalize,
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: isAiResponseReceivedAnalize,
274
+ responseFlag: isAiResponseReceivedAnalizeNoImage,
254
275
  });
255
276
  }
256
277
  }
257
278
 
258
279
  const closeDialog = () => {
259
- isAiResponseReceivedAnalize.value = [];
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
- isAiGenerationError.value[index] = true;
696
- aiGenerationErrorMessage.value[index] = jobResponse.job?.error || 'Unknown error';
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="isAiResponseReceivedAnalize[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] && !isInColumnImage(n)" @mouseenter="(() => { hovers[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = true})" @mouseleave="(() => { hovers[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false})">
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-start justify-end min-h-[90px]">
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="!isAiResponseReceivedAnalize[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] && !isInColumnImage(n)">
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
- isAiResponseReceivedAnalize: boolean[],
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>
@@ -13,7 +13,7 @@
13
13
  : popupMode === 'settings' ? 'lg:w-[1000px] !lg:max-w-[1000px]'
14
14
  : 'lg:w-[500px] !lg:max-w-[500px]'"
15
15
  :beforeCloseFunction="closeDialog"
16
- :closable="popupMode === 'generation' ? false : true"
16
+ :closable="false"
17
17
  :askForCloseConfirmation="popupMode === 'generation' ? true : false"
18
18
  closeConfirmationText="Are you sure you want to close without saving?"
19
19
  :buttons="popupMode === 'generation' ? [
@@ -65,7 +65,8 @@
65
65
  :tableColumnsIndexes="tableColumnsIndexes"
66
66
  :selected="selected"
67
67
  :oldData="oldData"
68
- :isAiResponseReceivedAnalize="isAiResponseReceivedAnalize"
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 isAiResponseReceivedAnalize = ref([]);
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
- popupMode.value = 'generation';
232
- if (props.meta.isImageGeneration) {
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: isAiResponseReceivedAnalize,
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: isAiResponseReceivedAnalize,
274
+ responseFlag: isAiResponseReceivedAnalizeNoImage,
254
275
  });
255
276
  }
256
277
  }
257
278
 
258
279
  const closeDialog = () => {
259
- isAiResponseReceivedAnalize.value = [];
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
- isAiGenerationError.value[index] = true;
696
- aiGenerationErrorMessage.value[index] = jobResponse.job?.error || 'Unknown error';
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="isAiResponseReceivedAnalize[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] && !isInColumnImage(n)" @mouseenter="(() => { hovers[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = true})" @mouseleave="(() => { hovers[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false})">
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-start justify-end min-h-[90px]">
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="!isAiResponseReceivedAnalize[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] && !isInColumnImage(n)">
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
- isAiResponseReceivedAnalize: boolean[],
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 = `Analyze the following image(s) and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
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
- return {};
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 = `Generate the values of fields in object by using next prompts (key is field name, value is 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
- outputPlainFields: this.options.fillPlainFields,
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 = `Analyze the following image(s) and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
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
- return {};
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 = `Generate the values of fields in object by using next prompts (key is field name, value is 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
- outputPlainFields: this.options.fillPlainFields,
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.15.10",
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.34",
30
+ "adminforth": "^2.13.0-next.40",
31
31
  "handlebars": "^4.7.8"
32
32
  },
33
33
  "release": {