1212 :class =" popupMode === ' generation' ? ' lg:w-auto !lg:max-w-[1600px]'
1313 : popupMode === ' settings' ? ' lg:w-[1000px] !lg:max-w-[1000px]'
1414 : ' lg:w-[500px] !lg:max-w-[500px]' "
15- :beforeCloseFunction =" closeDialog "
15+ :beforeCloseFunction =" handleBeforeClose "
1616 :closable =" false "
17- :askForCloseConfirmation =" popupMode === ' generation' ? true : false "
18- :closeConfirmationText =" t (' Are you sure you want to close without saving?' )"
1917 :buttons =" popupMode === ' generation' ? generationModeButtons : popupMode === ' settings' ? [
2018 {
2119 label: t (' Save settings' ),
@@ -185,6 +183,7 @@ import { useFiltersStore } from '@/stores/filters';
185183
186184const coreStore = useCoreStore ();
187185const filtersStore = useFiltersStore ();
186+ const showCloseConfirmModal = ref (false );
188187
189188const { t } = useI18n ();
190189const props = defineProps <{
@@ -197,6 +196,7 @@ const props = defineProps<{
197196}>();
198197
199198type RecordStatus = ' pending' | ' processing' | ' completed' | ' failed' ;
199+ type GenerationAction = ' analyze' | ' analyze_no_images' | ' generate_images' ;
200200
201201type RecordState = {
202202 id: string | number ;
@@ -256,6 +256,11 @@ const startedRecordCount = ref(0);
256256const isCheckingRateLimits = ref (false );
257257let startGate = Promise .resolve ();
258258const tableRef = ref <any >(null );
259+ const generationFailureGroups = new Map < string , {
260+ actionType : GenerationAction;
261+ error : string;
262+ recordIds : Set<string>;
263+ }>();
259264const processedCount = computed (() => {
260265 recordsVersion .value ;
261266 return Array .from (recordsById .values ()).filter (record => record .status === ' completed' || record .status === ' failed' ).length ;
@@ -313,7 +318,7 @@ const generationModeButtons = computed(() => {
313318 options: {
314319 class: ' bg-white hover:!bg-gray-100 !text-gray-900 hover:!text-gray-800 dark:!bg-gray-800 dark:!text-gray-100 dark:hover:!bg-gray-700 !border-gray-200 dark:!border-gray-600'
315320 },
316- onclick : (dialog ) => confirmDialog . value . tryToHideModal ()
321+ onclick : async (dialog ) => { await handleBeforeClose ( dialog ); }
317322 },
318323 ]
319324
@@ -330,6 +335,28 @@ const generationModeButtons = computed(() => {
330335 return arrayToReturn ;
331336});
332337
338+ const handleBeforeClose = async (dialog ? : any ) => {
339+ if (popupMode .value === ' generation' ) {
340+ const confirmed = await adminforth .confirm ({
341+ title: t (' Close without saving?' ),
342+ message: t (' Are you sure you want to close without saving?' ),
343+ yes: t (' Yes' ),
344+ no: t (' Cancel' ),
345+ });
346+
347+ if (confirmed ) {
348+ closeDialog ();
349+
350+ if (confirmDialog .value && typeof confirmDialog .value .hide === ' function' ) {
351+ confirmDialog .value .hide ();
352+ } else if (dialog && typeof dialog .hide === ' function' ) {
353+ dialog .hide ();
354+ }
355+ return true ;
356+ }
357+ return false ;
358+ }
359+ }
333360
334361const isSavingCurrent = ref (false );
335362function checkIfDialogOpen() {
@@ -398,12 +425,14 @@ async function runAiActions() {
398425 isActiveGeneration .value = true ;
399426 completedRecordIds .value = new Set ();
400427 startedRecordCount .value = 0 ;
428+ generationFailureGroups .clear ();
401429 await nextTick ();
402430 tableRef .value ?.refresh ();
403431 const limit = pLimit (props .meta .concurrencyLimit || 10 );
404432 const tasks = recordIds .value
405433 .map (id => limit (() => processOneRecord (String (id ))));
406434 await Promise .all (tasks );
435+ showGenerationFailureSummary ();
407436 isActiveGeneration .value = false ;
408437}
409438
@@ -431,6 +460,47 @@ function resetGlobalState() {
431460 recordIds .value = [];
432461 recordsById .clear ();
433462 uncheckedRecordIds .clear ();
463+ generationFailureGroups .clear ();
464+ }
465+
466+ function getActionLabel(actionType : GenerationAction ) {
467+ return actionType .replace (' _' , ' ' );
468+ }
469+
470+ function getGenerationFailureGroupKey(actionType : GenerationAction , error : string ) {
471+ const normalizedError = error
472+ .replace (/ Please retry in [\d. ] + s\. ? / g , ' Please retry later.' )
473+ .replace (/ \b \d + \. \d + s\b / g , ' <duration>' )
474+ .replace (/ \b [0-9a-f ] {8} -[0-9a-f ] {4} -[0-9a-f ] {4} -[0-9a-f ] {4} -[0-9a-f ] {12} \b / gi , ' <uuid>' );
475+ return ` ${actionType }:${normalizedError } ` ;
476+ }
477+
478+ function registerGenerationFailure(record : RecordState , actionType : GenerationAction , error : string ) {
479+ const key = getGenerationFailureGroupKey (actionType , error );
480+ let group = generationFailureGroups .get (key );
481+ if (! group ) {
482+ group = {
483+ actionType ,
484+ error ,
485+ recordIds: new Set (),
486+ };
487+ generationFailureGroups .set (key , group );
488+ }
489+ group .recordIds .add (String (record .id ));
490+ }
491+
492+ function showGenerationFailureSummary() {
493+ for (const group of generationFailureGroups .values ()) {
494+ const failedCount = group .recordIds .size ;
495+ const firstRecordId = Array .from (group .recordIds )[0 ];
496+ adminforth .alert ({
497+ message: t (
498+ ` Generation action "${getActionLabel (group .actionType )}" failed for ${failedCount } record(s). First failed record: ${firstRecordId }. Error: ${group .error } ` ,
499+ ),
500+ variant: ' danger' ,
501+ timeout: ' unlimited' ,
502+ });
503+ }
434504}
435505
436506function getOrCreateRecord(recordId : string | number ): RecordState {
@@ -622,7 +692,7 @@ async function processOneRecord(recordId: string) {
622692 }
623693 }
624694
625- const actions: Array < ' generate_images ' | ' analyze ' | ' analyze_no_images ' > = [];
695+ const actions: GenerationAction [] = [];
626696 if (props .meta .isImageGeneration ) {
627697 actions .push (' generate_images' );
628698 }
@@ -645,7 +715,7 @@ async function processOneRecord(recordId: string) {
645715
646716async function checkRateLimits() {
647717 isCheckingRateLimits .value = true ;
648- const actionsToCheck: Array < ' generate_images ' | ' analyze ' | ' analyze_no_images ' > = [];
718+ const actionsToCheck: GenerationAction [] = [];
649719 if (props .meta .isImageGeneration ) {
650720 actionsToCheck .push (' generate_images' );
651721 }
@@ -664,15 +734,15 @@ async function checkRateLimits() {
664734 });
665735 if (rateLimitRes ?.error || rateLimitRes ?.ok === false ) {
666736 adminforth .alert ({
667- message: t (` Rate limit exceeded for "${actionType . replace ( ' _ ' , ' ' )}" action. Please try again later. ` ),
737+ message: t (` Rate limit exceeded for "${getActionLabel ( actionType )}" action. Please try again later. ` ),
668738 variant: ' danger' ,
669739 timeout: ' unlimited' ,
670740 });
671741 return false ;
672742 }
673743 } catch (e ) {
674744 adminforth .alert ({
675- message: t (` Error checking rate limit for "${actionType . replace ( ' _ ' , ' ' )}" action. ` ),
745+ message: t (` Error checking rate limit for "${getActionLabel ( actionType )}" action. ` ),
676746 variant: ' danger' ,
677747 timeout: ' unlimited' ,
678748 });
@@ -684,7 +754,7 @@ async function checkRateLimits() {
684754 return true ;
685755}
686756
687- async function runActionForRecord(record : RecordState , actionType : ' analyze ' | ' analyze_no_images ' | ' generate_images ' ) {
757+ async function runActionForRecord(record : RecordState , actionType : GenerationAction ) {
688758 if (! checkIfDialogOpen ()) {
689759 return ;
690760 }
@@ -722,6 +792,7 @@ async function runActionForRecord(record: RecordState, actionType: 'analyze' | '
722792 });
723793 } catch (e ) {
724794 record .aiStatus [responseFlag ] = true ;
795+ registerGenerationFailure (record , actionType , e instanceof Error ? e .message : String (e ));
725796 throw e ;
726797 }
727798
@@ -731,11 +802,7 @@ async function runActionForRecord(record: RecordState, actionType: 'analyze' | '
731802
732803 if (createJobResponse ?.error || ! createJobResponse ?.jobId ) {
733804 record .aiStatus [responseFlag ] = true ;
734- adminforth .alert ({
735- message: t (` Failed to ${actionType .replace (' _' , ' ' )}. Please, try to re-run the action. ` ),
736- variant: ' danger' ,
737- timeout: ' unlimited' ,
738- });
805+ registerGenerationFailure (record , actionType , createJobResponse ?.error || ` Failed to ${getActionLabel (actionType )}. Please, try to re-run the action. ` );
739806 throw new Error (createJobResponse ?.error || ' Failed to create job' );
740807 }
741808
@@ -745,7 +812,7 @@ async function runActionForRecord(record: RecordState, actionType: 'analyze' | '
745812async function pollJob(
746813 record : RecordState ,
747814 jobId : string ,
748- actionType : ' analyze ' | ' analyze_no_images ' | ' generate_images ' ,
815+ actionType : GenerationAction ,
749816 responseFlag : keyof RecordState [' aiStatus' ]
750817) {
751818 let isInProgress = true ;
@@ -762,6 +829,7 @@ async function pollJob(
762829 }
763830 if (jobResponse ?.error ) {
764831 record .aiStatus [responseFlag ] = true ;
832+ registerGenerationFailure (record , actionType , jobResponse .error );
765833 throw new Error (jobResponse .error );
766834 }
767835 const jobStatus = jobResponse ?.job ?.status ;
@@ -783,7 +851,7 @@ async function pollJob(
783851 }
784852}
785853
786- function applyJobResult(record : RecordState , job : any , actionType : ' analyze ' | ' analyze_no_images ' | ' generate_images ' ) {
854+ function applyJobResult(record : RecordState , job : any , actionType : GenerationAction ) {
787855 if (actionType === ' generate_images' ) {
788856 for (const fieldName of props .meta .outputImageFields || []) {
789857 const resultValue = job ?.result ?.[fieldName ];
@@ -805,12 +873,8 @@ function applyJobResult(record: RecordState, job: any, actionType: 'analyze' | '
805873 touchRecords ();
806874}
807875
808- function applyJobFailure(record : RecordState , job : any , actionType : ' analyze' | ' analyze_no_images' | ' generate_images' ) {
809- adminforth .alert ({
810- message: t (` Generation action "${actionType .replace (' _' , ' ' )}" failed for record: ${record .id }. Error: ${job ?.error || ' Unknown error' } ` ),
811- variant: ' danger' ,
812- timeout: ' unlimited' ,
813- });
876+ function applyJobFailure(record : RecordState , job : any , actionType : GenerationAction ) {
877+ registerGenerationFailure (record , actionType , job ?.error || ' Unknown error' );
814878 if (actionType === ' generate_images' ) {
815879 record .imageGenerationFailed = true ;
816880 record .imageGenerationErrorMessage = job ?.error || ' Unknown error' ;
@@ -826,7 +890,7 @@ function applyJobFailure(record: RecordState, job: any, actionType: 'analyze' |
826890 touchRecords ();
827891}
828892
829- async function waitForRefresh(actionType : ' analyze ' | ' analyze_no_images ' | ' generate_images ' ) {
893+ async function waitForRefresh(actionType : GenerationAction ) {
830894 if (actionType === ' generate_images' ) {
831895 await new Promise (resolve => setTimeout (resolve , props .meta .refreshRates ?.generateImages ));
832896 } else if (actionType === ' analyze' ) {
@@ -1532,4 +1596,4 @@ async function saveCurrentGenerated() {
15321596 props .updateList ();
15331597}
15341598
1535- </script >
1599+ </script >
0 commit comments