@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.
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,984 bytes received 134 bytes 370,236.00 bytes/sec
15
+ total size is 184,451 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">
@@ -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
 
@@ -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