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
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,65 +403,149 @@ 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
});
436
532
isError .value = true ;
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 ;
0 commit comments