@adminforth/bulk-ai-flow 1.8.1 → 1.9.1

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.
@@ -10,7 +10,7 @@
10
10
  header="Bulk AI Flow"
11
11
  class="!max-w-full w-full lg:w-[1600px] !lg:max-w-[1600px]"
12
12
  :buttons="[
13
- { label: checkedCount > 1 ? 'Save fields' : 'Save field', options: { disabled: isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords, loader: isLoading, class: 'w-fit sm:w-40' }, onclick: (dialog) => { saveData(); dialog.hide(); } },
13
+ { label: checkedCount > 1 ? 'Save fields' : 'Save field', options: { disabled: isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords || isGeneratingImages || isAnalizingFields || isAnalizingImages, loader: isLoading, class: 'w-fit sm:w-40' }, onclick: (dialog) => { saveData(); dialog.hide(); } },
14
14
  { label: 'Cancel', onclick: (dialog) => dialog.hide() },
15
15
  ]"
16
16
  >
@@ -33,6 +33,7 @@
33
33
  @error="handleTableError"
34
34
  :carouselSaveImages="carouselSaveImages"
35
35
  :carouselImageIndex="carouselImageIndex"
36
+ :regenerateImagesRefreshRate="props.meta.refreshRates?.regenerateImages"
36
37
  />
37
38
  </div>
38
39
  <div class="text-red-600 flex items-center w-full">
@@ -52,7 +53,6 @@ import { useI18n } from 'vue-i18n';
52
53
  import { AdminUser, type AdminForthResourceCommon } from '@/types';
53
54
 
54
55
  const { t } = useI18n();
55
-
56
56
  const props = defineProps<{
57
57
  checkboxes: any,
58
58
  meta: any,
@@ -88,9 +88,14 @@ const isCriticalError = ref(false);
88
88
  const isImageGenerationError = ref(false);
89
89
  const errorMessage = ref('');
90
90
  const checkedCount = ref(0);
91
+ const isGeneratingImages = ref(false);
92
+ const isAnalizingFields = ref(false);
93
+ const isAnalizingImages = ref(false);
94
+
91
95
 
92
96
  const openDialog = async () => {
93
97
  confirmDialog.value.open();
98
+ isFetchingRecords.value = true;
94
99
  await getRecords();
95
100
  if (props.meta.isAttachFiles) {
96
101
  await getImages();
@@ -101,42 +106,41 @@ const openDialog = async () => {
101
106
  tableColumnsIndexes.value = result.indexes;
102
107
  customFieldNames.value = tableHeaders.value.slice((props.meta.isAttachFiles) ? 3 : 2).map(h => h.fieldName);
103
108
  setSelected();
109
+ if (props.meta.isImageGeneration) {
110
+ fillCarouselSaveImages();
111
+ }
104
112
  for (let i = 0; i < selected.value?.length; i++) {
105
113
  openGenerationCarousel.value[i] = props.meta.outputImageFields?.reduce((acc,key) =>{
106
114
  acc[key] = false;
107
115
  return acc;
108
116
  },{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
109
117
  }
110
- isFetchingRecords.value = true;
111
- const tasks = [];
118
+ isFetchingRecords.value = false;
119
+
120
+ if (props.meta.isImageGeneration) {
121
+ isGeneratingImages.value = true;
122
+ runAiAction({
123
+ endpoint: 'initial_image_generate',
124
+ actionType: 'generate_images',
125
+ responseFlag: isAiResponseReceivedImage,
126
+ });
127
+ }
112
128
  if (props.meta.isFieldsForAnalizeFromImages) {
113
- tasks.push(runAiAction({
129
+ isAnalizingImages.value = true;
130
+ runAiAction({
114
131
  endpoint: 'analyze',
115
132
  actionType: 'analyze',
116
133
  responseFlag: isAiResponseReceivedAnalize,
117
- }));
134
+ });
118
135
  }
119
136
  if (props.meta.isFieldsForAnalizePlain) {
120
- tasks.push(runAiAction({
137
+ isAnalizingFields.value = true;
138
+ runAiAction({
121
139
  endpoint: 'analyze_no_images',
122
140
  actionType: 'analyze_no_images',
123
141
  responseFlag: isAiResponseReceivedAnalize,
124
- }));
125
- }
126
- if (props.meta.isImageGeneration) {
127
- tasks.push(runAiAction({
128
- endpoint: 'initial_image_generate',
129
- actionType: 'generate_images',
130
- responseFlag: isAiResponseReceivedImage,
131
- }));
132
- }
133
- await Promise.all(tasks);
134
-
135
- if (props.meta.isImageGeneration) {
136
- fillCarouselSaveImages();
142
+ });
137
143
  }
138
-
139
- isFetchingRecords.value = false;
140
144
  }
141
145
 
142
146
  watch(selected, (val) => {
@@ -149,10 +153,10 @@ function fillCarouselSaveImages() {
149
153
  const tempItem: any = {};
150
154
  const tempItemIndex: any = {};
151
155
  for (const [key, value] of Object.entries(item)) {
152
- if (props.meta.outputImageFields?.includes(key)) {
153
- tempItem[key] = [value];
154
- tempItemIndex[key] = 0;
155
- }
156
+ if (props.meta.outputImageFields?.includes(key)) {
157
+ tempItem[key] = [];
158
+ tempItemIndex[key] = 0;
159
+ }
156
160
  }
157
161
  carouselSaveImages.value.push(tempItem);
158
162
  carouselImageIndex.value.push(tempItemIndex);
@@ -398,38 +402,129 @@ async function runAiAction({
398
402
  responseFlag: Ref<boolean[]>;
399
403
  updateOnSuccess?: boolean;
400
404
  }) {
401
- let res: any;
402
- let error: any = null;
403
-
404
- try {
405
- responseFlag.value = props.checkboxes.map(() => false);
405
+ let hasError = false;
406
+ let errorMessage = '';
407
+ const jobsIds: { jobId: any; recordId: any; }[] = [];
408
+ responseFlag.value = props.checkboxes.map(() => false);
409
+
410
+ //creating jobs
411
+ const tasks = props.checkboxes.map(async (checkbox, i) => {
412
+ try {
413
+ const res = await callAdminForthApi({
414
+ path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
415
+ method: 'POST',
416
+ body: {
417
+ actionType: actionType,
418
+ recordId: checkbox,
419
+ },
420
+ });
406
421
 
407
- res = await callAdminForthApi({
408
- path: `/plugin/${props.meta.pluginInstanceId}/${endpoint}`,
409
- method: 'POST',
410
- body: {
411
- selectedIds: props.checkboxes,
412
- },
413
- });
422
+ if (res?.error) {
423
+ throw new Error(res.error);
424
+ }
425
+
426
+ if (!res) {
427
+ throw new Error(`${actionType} request returned empty response.`);
428
+ }
414
429
 
415
- if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
416
- responseFlag.value = props.checkboxes.map(() => true);
430
+ jobsIds.push({ jobId: res.jobId, recordId: checkbox });
431
+ } catch (e) {
432
+ console.error(`Error during ${actionType} for item ${i}:`, e);
433
+ hasError = true;
434
+ errorMessage = `Failed to ${actionType.replace('_', ' ')}. Please, try to re-run the action.`;
435
+ return { success: false, index: i, error: e };
417
436
  }
418
- } catch (e) {
419
- console.error(`Error during ${actionType}:`, e);
420
- error = `Failed to ${actionType.replace('_', ' ')}. Please, try to re-run the action.`;
421
- }
437
+ });
438
+ await Promise.all(tasks);
422
439
 
423
- if (res?.error) {
424
- error = res.error;
425
- }
426
- if (!res && !error) {
427
- error = `Error: ${actionType} request returned empty response.`;
440
+ //polling jobs
441
+ let isInProgress = true;
442
+ //if no jobs were created, skip polling
443
+ while (isInProgress) {
444
+ //check if at least one job is still in progress
445
+ let isAtLeastOneInProgress = false;
446
+ //checking status of each job
447
+ for (const { jobId, recordId } of jobsIds) {
448
+ //check job status
449
+ const jobResponse = await callAdminForthApi({
450
+ path: `/plugin/${props.meta.pluginInstanceId}/get-job-status`,
451
+ method: 'POST',
452
+ body: { jobId },
453
+ });
454
+ //check for errors
455
+ if (jobResponse?.error) {
456
+ console.error(`Error during ${actionType}:`, jobResponse.error);
457
+ break;
458
+ };
459
+ // extract job status
460
+ let jobStatus = jobResponse?.job?.status;
461
+ // check if job is still in progress. If in progress - skip to next job
462
+ if (jobStatus === 'in_progress') {
463
+ isAtLeastOneInProgress = true;
464
+ //if job is completed - update record data
465
+ } else if (jobStatus === 'completed') {
466
+ // finding index of the record in selected array
467
+ const index = selected.value.findIndex(item => String(item[primaryKey]) === String(recordId));
468
+ //if we are generating images - update carouselSaveImages with new image
469
+ if (actionType === 'generate_images') {
470
+ for (const [key, value] of Object.entries(carouselSaveImages.value[index])) {
471
+ if (props.meta.outputImageFields?.includes(key)) {
472
+ carouselSaveImages.value[index][key] = [jobResponse.job.result[key]];
473
+ }
474
+ }
475
+ }
476
+ //marking that we received response for this record
477
+ if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
478
+ responseFlag.value[index] = true;
479
+ }
480
+ //updating selected with new data from AI
481
+ const pk = selected.value[index]?.[primaryKey];
482
+ if (pk) {
483
+ selected.value[index] = {
484
+ ...selected.value[index],
485
+ ...jobResponse.job.result,
486
+ isChecked: true,
487
+ [primaryKey]: pk,
488
+ };
489
+ }
490
+ //removing job from jobsIds
491
+ if (index !== -1) {
492
+ jobsIds.splice(jobsIds.findIndex(j => j.jobId === jobId), 1);
493
+ }
494
+ // checking one more time if we have in progress jobs
495
+ isAtLeastOneInProgress = true;
496
+ // if job is failed - set error
497
+ } else if (jobStatus === 'failed') {
498
+ const index = selected.value.findIndex(item => String(item[primaryKey]) === String(recordId));
499
+ if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
500
+ responseFlag.value[index] = true;
501
+ }
502
+ adminforth.alert({
503
+ message: `Generation action "${actionType.replace('_', ' ')}" failed for record: ${recordId}. Error: ${jobResponse.job?.error || 'Unknown error'}`,
504
+ variant: 'danger',
505
+ timeout: 'unlimited',
506
+ });
507
+ }
508
+ }
509
+ if (!isAtLeastOneInProgress) {
510
+ isInProgress = false;
511
+ }
512
+ if (jobsIds.length > 0) {
513
+ if (actionType === 'generate_images') {
514
+ await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.generateImages));
515
+ } else if (actionType === 'analyze') {
516
+ await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.fillFieldsFromImages));
517
+ } else if (actionType === 'analyze_no_images') {
518
+ await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.fillPlainFields));
519
+ } else {
520
+ await new Promise(resolve => setTimeout(resolve, 2000));
521
+ }
522
+ }
428
523
  }
429
524
 
430
- if (error) {
525
+ if (hasError) {
431
526
  adminforth.alert({
432
- message: error,
527
+ message: errorMessage,
433
528
  variant: 'danger',
434
529
  timeout: 'unlimited',
435
530
  });
@@ -437,26 +532,19 @@ async function runAiAction({
437
532
  if (actionType === 'generate_images') {
438
533
  isImageGenerationError.value = true;
439
534
  }
440
- errorMessage.value = error;
535
+ this.errorMessage.value = errorMessage;
441
536
  return;
442
537
  }
443
538
 
444
- if (updateOnSuccess) {
445
- res.result.forEach((item: any, idx: number) => {
446
- const pk = selected.value[idx]?.[primaryKey];
447
- if (pk) {
448
- selected.value[idx] = {
449
- ...selected.value[idx],
450
- ...item,
451
- isChecked: true,
452
- [primaryKey]: pk,
453
- };
454
- }
455
- });
539
+ if (actionType === 'generate_images') {
540
+ isGeneratingImages.value = false;
541
+ } else if (actionType === 'analyze') {
542
+ isAnalizingImages.value = false;
543
+ } else if (actionType === 'analyze_no_images') {
544
+ isAnalizingFields.value = false;
456
545
  }
457
546
  }
458
547
 
459
-
460
548
  async function uploadImage(imgBlob, id, fieldName) {
461
549
  const file = new File([imgBlob], `generated_${fieldName}_${id}.${imgBlob.type.split('/').pop()}`, { type: imgBlob.type });
462
550
  const { name, size, type } = file;
@@ -10,7 +10,7 @@
10
10
  </template>
11
11
  <!-- CHECKBOX CELL TEMPLATE -->
12
12
  <template #cell:checkboxes="{ item }">
13
- <div class="flex items-center justify-center">
13
+ <div class="max-w-[100px] flex items-center justify-center">
14
14
  <Checkbox
15
15
  v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])].isChecked"
16
16
  />
@@ -99,6 +99,7 @@
99
99
  :meta="props.meta"
100
100
  :fieldName="n"
101
101
  :carouselImageIndex="carouselImageIndex[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
102
+ :regenerateImagesRefreshRate="regenerateImagesRefreshRate"
102
103
  @error="handleError"
103
104
  @close="openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false"
104
105
  @selectImage="updateSelectedImage"
@@ -139,6 +140,7 @@ const props = defineProps<{
139
140
  errorMessage: string
140
141
  carouselSaveImages: any[]
141
142
  carouselImageIndex: any[]
143
+ regenerateImagesRefreshRate: number
142
144
  }>();
143
145
  const emit = defineEmits(['error']);
144
146