@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.
package/build.log CHANGED
@@ -11,5 +11,5 @@ custom/package-lock.json
11
11
  custom/package.json
12
12
  custom/tsconfig.json
13
13
 
14
- sent 179,998 bytes received 134 bytes 360,264.00 bytes/sec
15
- total size is 179,465 speedup is 1.00
14
+ sent 184,983 bytes received 134 bytes 370,234.00 bytes/sec
15
+ total size is 184,450 speedup is 1.00
@@ -190,7 +190,7 @@ const { t: $t } = useI18n();
190
190
 
191
191
  const prompt = ref('');
192
192
  const emit = defineEmits(['close', 'selectImage', 'error', 'updateCarouselIndex']);
193
- const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex']);
193
+ const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex', 'regenerateImagesRefreshRate']);
194
194
  const images = ref([]);
195
195
  const loading = ref(false);
196
196
  const attachmentFiles = ref<string[]>([])
@@ -370,26 +370,24 @@ async function generateImages() {
370
370
  let error = null;
371
371
  try {
372
372
  resp = await callAdminForthApi({
373
- path: `/plugin/${props.meta.pluginInstanceId}/regenerate_images`,
373
+ path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
374
374
  method: 'POST',
375
375
  body: {
376
- prompt: prompt.value,
376
+ actionType: 'regenerate_images',
377
377
  recordId: props.recordId,
378
- fieldName: props.fieldName,
378
+ prompt: prompt.value,
379
+ fieldName: props.fieldName
379
380
  },
380
381
  });
381
382
  } catch (e) {
382
383
  console.error(e);
383
- } finally {
384
- clearInterval(ticker);
385
- loadingTimer.value = null;
386
- loading.value = false;
387
384
  }
385
+
388
386
  if (resp?.error) {
389
387
  error = resp.error;
390
388
  }
391
389
  if (!resp) {
392
- error = $t('Error generating images, something went wrong');
390
+ error = $t('Error creating image generation job');
393
391
  }
394
392
 
395
393
  if (error) {
@@ -401,19 +399,50 @@ async function generateImages() {
401
399
  variant: 'danger',
402
400
  timeout: 'unlimited',
403
401
  });
404
- emit('error', {
405
- isError: true,
406
- errorMessage: "Error re-generating images"
402
+ }
403
+ return;
404
+ }
405
+
406
+ const jobId = resp.jobId;
407
+ let jobStatus = null;
408
+ let jobResponse = null;
409
+ while (jobStatus !== 'completed' && jobStatus !== 'failed') {
410
+ jobResponse = await callAdminForthApi({
411
+ path: `/plugin/${props.meta.pluginInstanceId}/get-job-status`,
412
+ method: 'POST',
413
+ body: { jobId },
407
414
  });
415
+ if (jobResponse?.error) {
416
+ error = jobResponse.error;
417
+ break;
418
+ };
419
+ jobStatus = jobResponse?.job?.status;
420
+ if (jobStatus === 'failed') {
421
+ error = jobResponse?.job?.error || $t('Image generation job failed');
408
422
  }
423
+ await new Promise((resolve) => setTimeout(resolve, props.regenerateImagesRefreshRate));
424
+ }
425
+
426
+ if (error) {
427
+ adminforth.alert({
428
+ message: error,
429
+ variant: 'danger',
430
+ timeout: 'unlimited',
431
+ });
409
432
  return;
410
433
  }
411
434
 
435
+ const respImages = jobResponse?.job?.result[props.fieldName] || [];
436
+
412
437
  images.value = [
413
438
  ...images.value,
414
- ...resp.images,
439
+ ...respImages,
415
440
  ];
416
441
 
442
+ clearInterval(ticker);
443
+ loadingTimer.value = null;
444
+ loading.value = false;
445
+
417
446
  await nextTick();
418
447
 
419
448
 
@@ -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
 
@@ -190,7 +190,7 @@ const { t: $t } = useI18n();
190
190
 
191
191
  const prompt = ref('');
192
192
  const emit = defineEmits(['close', 'selectImage', 'error', 'updateCarouselIndex']);
193
- const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex']);
193
+ const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex', 'regenerateImagesRefreshRate']);
194
194
  const images = ref([]);
195
195
  const loading = ref(false);
196
196
  const attachmentFiles = ref<string[]>([])
@@ -370,26 +370,24 @@ async function generateImages() {
370
370
  let error = null;
371
371
  try {
372
372
  resp = await callAdminForthApi({
373
- path: `/plugin/${props.meta.pluginInstanceId}/regenerate_images`,
373
+ path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
374
374
  method: 'POST',
375
375
  body: {
376
- prompt: prompt.value,
376
+ actionType: 'regenerate_images',
377
377
  recordId: props.recordId,
378
- fieldName: props.fieldName,
378
+ prompt: prompt.value,
379
+ fieldName: props.fieldName
379
380
  },
380
381
  });
381
382
  } catch (e) {
382
383
  console.error(e);
383
- } finally {
384
- clearInterval(ticker);
385
- loadingTimer.value = null;
386
- loading.value = false;
387
384
  }
385
+
388
386
  if (resp?.error) {
389
387
  error = resp.error;
390
388
  }
391
389
  if (!resp) {
392
- error = $t('Error generating images, something went wrong');
390
+ error = $t('Error creating image generation job');
393
391
  }
394
392
 
395
393
  if (error) {
@@ -401,19 +399,50 @@ async function generateImages() {
401
399
  variant: 'danger',
402
400
  timeout: 'unlimited',
403
401
  });
404
- emit('error', {
405
- isError: true,
406
- errorMessage: "Error re-generating images"
402
+ }
403
+ return;
404
+ }
405
+
406
+ const jobId = resp.jobId;
407
+ let jobStatus = null;
408
+ let jobResponse = null;
409
+ while (jobStatus !== 'completed' && jobStatus !== 'failed') {
410
+ jobResponse = await callAdminForthApi({
411
+ path: `/plugin/${props.meta.pluginInstanceId}/get-job-status`,
412
+ method: 'POST',
413
+ body: { jobId },
407
414
  });
415
+ if (jobResponse?.error) {
416
+ error = jobResponse.error;
417
+ break;
418
+ };
419
+ jobStatus = jobResponse?.job?.status;
420
+ if (jobStatus === 'failed') {
421
+ error = jobResponse?.job?.error || $t('Image generation job failed');
408
422
  }
423
+ await new Promise((resolve) => setTimeout(resolve, props.regenerateImagesRefreshRate));
424
+ }
425
+
426
+ if (error) {
427
+ adminforth.alert({
428
+ message: error,
429
+ variant: 'danger',
430
+ timeout: 'unlimited',
431
+ });
409
432
  return;
410
433
  }
411
434
 
435
+ const respImages = jobResponse?.job?.result[props.fieldName] || [];
436
+
412
437
  images.value = [
413
438
  ...images.value,
414
- ...resp.images,
439
+ ...respImages,
415
440
  ];
416
441
 
442
+ clearInterval(ticker);
443
+ loadingTimer.value = null;
444
+ loading.value = false;
445
+
417
446
  await nextTick();
418
447
 
419
448