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