Skip to content

Commit df9e1a4

Browse files
authored
Merge pull request #2 from devforth/JOB-architecture
Job architecture
2 parents 076ad08 + d9e223c commit df9e1a4

File tree

5 files changed

+522
-285
lines changed

5 files changed

+522
-285
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: 153 additions & 64 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();
@@ -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
142147
watch(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-
460549
async 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

Comments
 (0)