Skip to content

Commit 05f699c

Browse files
committed
feat: implement job-based architecture for generation
1 parent 00921e4 commit 05f699c

File tree

5 files changed

+428
-263
lines changed

5 files changed

+428
-263
lines changed

custom/ImageGenerationCarousel.vue

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ const { t: $t } = useI18n();
190190
191191
const prompt = ref('');
192192
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']);
194194
const images = ref([]);
195195
const loading = ref(false);
196196
const attachmentFiles = ref<string[]>([])
@@ -370,26 +370,24 @@ async function generateImages() {
370370
let error = null;
371371
try {
372372
resp = await callAdminForthApi({
373-
path: `/plugin/${props.meta.pluginInstanceId}/regenerate_images`,
373+
path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
374374
method: 'POST',
375375
body: {
376-
prompt: prompt.value,
376+
actionType: 'regenerate_images',
377377
recordId: props.recordId,
378-
fieldName: props.fieldName,
378+
prompt: prompt.value,
379+
fieldName: props.fieldName
379380
},
380381
});
381382
} catch (e) {
382383
console.error(e);
383-
} finally {
384-
clearInterval(ticker);
385-
loadingTimer.value = null;
386-
loading.value = false;
387384
}
385+
388386
if (resp?.error) {
389387
error = resp.error;
390388
}
391389
if (!resp) {
392-
error = $t('Error generating images, something went wrong');
390+
error = $t('Error creating image generation job');
393391
}
394392
395393
if (error) {
@@ -401,19 +399,50 @@ async function generateImages() {
401399
variant: 'danger',
402400
timeout: 'unlimited',
403401
});
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 },
407414
});
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');
408422
}
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+
});
409432
return;
410433
}
411434
435+
const respImages = jobResponse?.job?.result[props.fieldName] || [];
436+
412437
images.value = [
413438
...images.value,
414-
...resp.images,
439+
...respImages,
415440
];
416441
442+
clearInterval(ticker);
443+
loadingTimer.value = null;
444+
loading.value = false;
445+
417446
await nextTick();
418447
419448

custom/VisionAction.vue

Lines changed: 102 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
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
>
@@ -33,6 +33,7 @@
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);
8889
const isImageGenerationError = ref(false);
8990
const errorMessage = ref('');
9091
const checkedCount = ref(0);
92+
const isGeneratingImages = ref(false);
93+
const isAnalizingFields = ref(false);
94+
const isAnalizingImages = ref(false);
95+
9196
9297
const openDialog = async () => {
9398
confirmDialog.value.open();
99+
isFetchingRecords.value = true;
94100
await getRecords();
95101
if (props.meta.isAttachFiles) {
96102
await getImages();
@@ -110,42 +116,39 @@ const openDialog = async () => {
110116
return acc;
111117
},{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
112118
}
113-
isFetchingRecords.value = true;
119+
isFetchingRecords.value = false;
114120
115121
if (props.meta.isImageGeneration) {
122+
isGeneratingImages.value = true;
116123
runAiAction({
117124
endpoint: 'initial_image_generate',
118125
actionType: 'generate_images',
119126
responseFlag: isAiResponseReceivedImage,
120127
});
121128
}
122129
if (props.meta.isFieldsForAnalizeFromImages) {
130+
isAnalizingImages.value = true;
123131
runAiAction({
124132
endpoint: 'analyze',
125133
actionType: 'analyze',
126134
responseFlag: isAiResponseReceivedAnalize,
127135
});
128136
}
129137
if (props.meta.isFieldsForAnalizePlain) {
138+
isAnalizingFields.value = true;
130139
runAiAction({
131140
endpoint: 'analyze_no_images',
132141
actionType: 'analyze_no_images',
133142
responseFlag: isAiResponseReceivedAnalize,
134143
});
135144
}
136-
137-
isFetchingRecords.value = false;
138145
}
139146
140147
watch(selected, (val) => {
141-
console.log('Selected changed:', val);
148+
//console.log('Selected changed:', val);
142149
checkedCount.value = val.filter(item => item.isChecked === true).length;
143150
}, { deep: true });
144151
145-
watch(carouselSaveImages, (val) => {
146-
console.log('carouselSaveImages changed:', val);
147-
}, { deep: true });
148-
149152
function fillCarouselSaveImages() {
150153
for (const item of selected.value) {
151154
const tempItem: any = {};
@@ -400,19 +403,20 @@ async function runAiAction({
400403
responseFlag: Ref<boolean[]>;
401404
updateOnSuccess?: boolean;
402405
}) {
403-
const results: any[] = new Array(props.checkboxes.length);
404406
let hasError = false;
405407
let errorMessage = '';
406-
408+
const jobsIds: { jobId: any; recordId: any; }[] = [];
407409
responseFlag.value = props.checkboxes.map(() => false);
408410
411+
//creating jobs
409412
const tasks = props.checkboxes.map(async (checkbox, i) => {
410413
try {
411414
const res = await callAdminForthApi({
412-
path: `/plugin/${props.meta.pluginInstanceId}/${endpoint}`,
415+
path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
413416
method: 'POST',
414417
body: {
415-
selectedId: checkbox,
418+
actionType: actionType,
419+
recordId: checkbox,
416420
},
417421
});
418422
@@ -424,41 +428,96 @@ async function runAiAction({
424428
throw new Error(`${actionType} request returned empty response.`);
425429
}
426430
427-
results[i] = res;
428-
429-
if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
430-
responseFlag.value[i] = true;
431-
}
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 };
437+
}
438+
});
439+
await Promise.all(tasks);
432440
433-
if (res.result) {
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
434470
if (actionType === 'generate_images') {
435-
for (const [key, value] of Object.entries(carouselSaveImages.value[i])) {
471+
for (const [key, value] of Object.entries(carouselSaveImages.value[index])) {
436472
if (props.meta.outputImageFields?.includes(key)) {
437-
carouselSaveImages.value[i][key] = [res.result[key]];
473+
carouselSaveImages.value[index][key] = [jobResponse.job.result[key]];
438474
}
439475
}
440476
}
441-
442-
const pk = selected.value[i]?.[primaryKey];
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];
443483
if (pk) {
444-
selected.value[i] = {
445-
...selected.value[i],
446-
...res.result,
484+
selected.value[index] = {
485+
...selected.value[index],
486+
...jobResponse.job.result,
447487
isChecked: true,
448488
[primaryKey]: pk,
449489
};
450490
}
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+
adminforth.alert({
500+
message: `Generation action "${actionType.replace('_', ' ')}" failed for record: ${recordId}. Error: ${jobResponse.job?.error || 'Unknown error'}`,
501+
variant: 'danger',
502+
timeout: 'unlimited',
503+
});
451504
}
452-
return { success: true, index: i, data: res };
453-
} catch (e) {
454-
console.error(`Error during ${actionType} for item ${i}:`, e);
455-
hasError = true;
456-
errorMessage = `Failed to ${actionType.replace('_', ' ')}. Please, try to re-run the action.`;
457-
return { success: false, index: i, error: e };
458505
}
459-
});
460-
461-
await Promise.all(tasks);
506+
if (!isAtLeastOneInProgress) {
507+
isInProgress = false;
508+
}
509+
if (jobsIds.length > 0) {
510+
if (actionType === 'generate_images') {
511+
await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.generateImages));
512+
} else if (actionType === 'analyze') {
513+
await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.fillFieldsFromImages));
514+
} else if (actionType === 'analyze_no_images') {
515+
await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.fillPlainFields));
516+
} else {
517+
await new Promise(resolve => setTimeout(resolve, 2000));
518+
}
519+
}
520+
}
462521
463522
if (hasError) {
464523
adminforth.alert({
@@ -473,6 +532,14 @@ async function runAiAction({
473532
this.errorMessage.value = errorMessage;
474533
return;
475534
}
535+
536+
if (actionType === 'generate_images') {
537+
isGeneratingImages.value = false;
538+
} else if (actionType === 'analyze') {
539+
isAnalizingImages.value = false;
540+
} else if (actionType === 'analyze_no_images') {
541+
isAnalizingFields.value = false;
542+
}
476543
}
477544
478545
async function uploadImage(imgBlob, id, fieldName) {

custom/VisionTable.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
:meta="props.meta"
100100
:fieldName="n"
101101
:carouselImageIndex="carouselImageIndex[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
102+
:regenerateImagesRefreshRate="regenerateImagesRefreshRate"
102103
@error="handleError"
103104
@close="openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false"
104105
@selectImage="updateSelectedImage"
@@ -139,6 +140,7 @@ const props = defineProps<{
139140
errorMessage: string
140141
carouselSaveImages: any[]
141142
carouselImageIndex: any[]
143+
regenerateImagesRefreshRate: number
142144
}>();
143145
const emit = defineEmits(['error']);
144146

0 commit comments

Comments
 (0)