1010 header =" Bulk AI Flow"
1111 class =" !max-w-full w-full lg:w-[1600px] !lg:max-w-[1600px]"
1212 :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(); } },
1414 { label: 'Cancel', onclick: (dialog) => dialog.hide() },
1515 ]"
1616 >
3333 @error =" handleTableError"
3434 :carouselSaveImages =" carouselSaveImages"
3535 :carouselImageIndex =" carouselImageIndex"
36+ :regenerateImagesRefreshRate =" props.meta.refreshRates?.regenerateImages"
3637 />
3738 </div >
3839 <div class =" text-red-600 flex items-center w-full" >
@@ -88,9 +89,14 @@ const isCriticalError = ref(false);
8889const isImageGenerationError = ref (false );
8990const errorMessage = ref (' ' );
9091const checkedCount = ref (0 );
92+ const isGeneratingImages = ref (false );
93+ const isAnalizingFields = ref (false );
94+ const isAnalizingImages = ref (false );
95+
9196
9297const openDialog = async () => {
9398 confirmDialog .value .open ();
99+ isFetchingRecords .value = true ;
94100 await getRecords ();
95101 if (props .meta .isAttachFiles ) {
96102 await getImages ();
@@ -101,42 +107,41 @@ const openDialog = async () => {
101107 tableColumnsIndexes .value = result .indexes ;
102108 customFieldNames .value = tableHeaders .value .slice ((props .meta .isAttachFiles ) ? 3 : 2 ).map (h => h .fieldName );
103109 setSelected ();
110+ if (props .meta .isImageGeneration ) {
111+ fillCarouselSaveImages ();
112+ }
104113 for (let i = 0 ; i < selected .value ?.length ; i ++ ) {
105114 openGenerationCarousel .value [i ] = props .meta .outputImageFields ?.reduce ((acc ,key ) => {
106115 acc [key ] = false ;
107116 return acc ;
108117 },{[primaryKey ]: records .value [i ][primaryKey ]} as Record <string , boolean >);
109118 }
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+ }
112129 if (props .meta .isFieldsForAnalizeFromImages ) {
113- tasks .push (runAiAction ({
130+ isAnalizingImages .value = true ;
131+ runAiAction ({
114132 endpoint: ' analyze' ,
115133 actionType: ' analyze' ,
116134 responseFlag: isAiResponseReceivedAnalize ,
117- })) ;
135+ });
118136 }
119137 if (props .meta .isFieldsForAnalizePlain ) {
120- tasks .push (runAiAction ({
138+ isAnalizingFields .value = true ;
139+ runAiAction ({
121140 endpoint: ' analyze_no_images' ,
122141 actionType: ' analyze_no_images' ,
123142 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+ });
137144 }
138-
139- isFetchingRecords .value = false ;
140145}
141146
142147watch (selected , (val ) => {
@@ -149,10 +154,10 @@ function fillCarouselSaveImages() {
149154 const tempItem: any = {};
150155 const tempItemIndex: any = {};
151156 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+ }
156161 }
157162 carouselSaveImages .value .push (tempItem );
158163 carouselImageIndex .value .push (tempItemIndex );
@@ -398,65 +403,149 @@ async function runAiAction({
398403 responseFlag: Ref <boolean []>;
399404 updateOnSuccess? : boolean ;
400405}) {
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+ });
406422
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+ }
414430
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 };
417437 }
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 );
422440
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+ }
428524 }
429525
430- if (error ) {
526+ if (hasError ) {
431527 adminforth .alert ({
432- message: error ,
528+ message: errorMessage ,
433529 variant: ' danger' ,
434530 timeout: ' unlimited' ,
435531 });
436532 isError .value = true ;
437533 if (actionType === ' generate_images' ) {
438534 isImageGenerationError .value = true ;
439535 }
440- errorMessage .value = error ;
536+ this . errorMessage .value = errorMessage ;
441537 return ;
442538 }
443539
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 ;
456546 }
457547}
458548
459-
460549async function uploadImage(imgBlob , id , fieldName ) {
461550 const file = new File ([imgBlob ], ` generated_${fieldName }_${id }.${imgBlob .type .split (' /' ).pop ()} ` , { type: imgBlob .type });
462551 const { name, size, type } = file ;
0 commit comments