Skip to content

Commit 9af0e61

Browse files
committed
2 parents 3fd20e7 + dfb2923 commit 9af0e61

4 files changed

Lines changed: 298 additions & 47 deletions

File tree

custom/VisionAction.vue

Lines changed: 88 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@
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
186184
const coreStore = useCoreStore();
187185
const filtersStore = useFiltersStore();
186+
const showCloseConfirmModal = ref(false);
188187
189188
const { t } = useI18n();
190189
const props = defineProps<{
@@ -197,6 +196,7 @@ const props = defineProps<{
197196
}>();
198197
199198
type RecordStatus = 'pending' | 'processing' | 'completed' | 'failed';
199+
type GenerationAction = 'analyze' | 'analyze_no_images' | 'generate_images';
200200
201201
type RecordState = {
202202
id: string | number;
@@ -256,6 +256,11 @@ const startedRecordCount = ref(0);
256256
const isCheckingRateLimits = ref(false);
257257
let startGate = Promise.resolve();
258258
const tableRef = ref<any>(null);
259+
const generationFailureGroups = new Map<string, {
260+
actionType: GenerationAction;
261+
error: string;
262+
recordIds: Set<string>;
263+
}>();
259264
const 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
334361
const isSavingCurrent = ref(false);
335362
function 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
436506
function 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
646716
async 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' | '
745812
async 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>

index.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,16 +1017,37 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
10171017
server.endpoint({
10181018
method: 'POST',
10191019
path: `/plugin/${this.pluginInstanceId}/get-job-status`,
1020-
handler: async ({ body, adminUser, headers }) => {
1020+
handler: async ({ body, adminUser, headers, response }) => {
10211021
const jobId = body.jobId;
10221022
if (!jobId) {
1023-
return { error: "Can't find job id" };
1023+
response.setStatus(400);
1024+
1025+
return {
1026+
ok: false,
1027+
error: "Can't find job id",
1028+
};
10241029
}
10251030
const job = jobs.get(jobId);
10261031
if (!job) {
1027-
return { error: "Job not found" };
1032+
response.setStatus(404);
1033+
1034+
return {
1035+
ok: false,
1036+
error: "Job not found",
1037+
};
10281038
}
1029-
return { ok: true, job };
1039+
if (job.status === 'failed') {
1040+
response.setStatus(500);
1041+
1042+
return {
1043+
ok: false,
1044+
error: job.error
1045+
};
1046+
}
1047+
return {
1048+
ok: true,
1049+
job,
1050+
};
10301051
}
10311052
});
10321053

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"description": "Bulk AI workflow plugin for AdminForth for batch field generation and image processing",
2626
"devDependencies": {
2727
"@types/node": "latest",
28-
"adminforth": "^2.50.0",
28+
"adminforth": "^2.70.0",
2929
"semantic-release": "^24.2.1",
3030
"semantic-release-slack-bot": "^4.0.2",
3131
"typescript": "^5.7.3"

0 commit comments

Comments
 (0)