Skip to content

Commit 4741950

Browse files
committed
feat: add error handling for AI image generation and improve UI feedback in VisionTable and VisionAction components
1 parent ee9d960 commit 4741950

File tree

3 files changed

+174
-83
lines changed

3 files changed

+174
-83
lines changed

custom/VisionAction.vue

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
:carouselSaveImages="carouselSaveImages"
3737
:carouselImageIndex="carouselImageIndex"
3838
:regenerateImagesRefreshRate="props.meta.refreshRates?.regenerateImages"
39+
:isAiGenerationError="isAiGenerationError"
40+
:aiGenerationErrorMessage="aiGenerationErrorMessage"
41+
:isAiImageGenerationError="isAiImageGenerationError"
42+
:imageGenerationErrorMessage="imageGenerationErrorMessage"
43+
@regenerate-images="regenerateImages"
3944
/>
4045
</div>
4146
<div class="text-red-600 flex items-center w-full">
@@ -53,6 +58,7 @@ import VisionTable from './VisionTable.vue'
5358
import adminforth from '@/adminforth';
5459
import { useI18n } from 'vue-i18n';
5560
import { AdminUser, type AdminForthResourceCommon } from '@/types';
61+
import { run } from 'node:test';
5662
5763
const { t } = useI18n();
5864
const props = defineProps<{
@@ -94,6 +100,10 @@ const isGeneratingImages = ref(false);
94100
const isAnalizingFields = ref(false);
95101
const isAnalizingImages = ref(false);
96102
const isDialogOpen = ref(false);
103+
const isAiGenerationError = ref<boolean[]>([false]);
104+
const aiGenerationErrorMessage = ref<string[]>([]);
105+
const isAiImageGenerationError = ref<boolean[]>([false]);
106+
const imageGenerationErrorMessage = ref<string[]>([]);
97107
98108
const openDialog = async () => {
99109
isDialogOpen.value = true;
@@ -416,47 +426,59 @@ async function runAiAction({
416426
actionType,
417427
responseFlag,
418428
updateOnSuccess = true,
429+
recordsIds = props.checkboxes,
430+
disableRateLimitCheck = false,
419431
}: {
420432
endpoint: string;
421433
actionType: 'analyze' | 'analyze_no_images' | 'generate_images';
422434
responseFlag: Ref<boolean[]>;
423435
updateOnSuccess?: boolean;
436+
recordsIds?: any[];
437+
disableRateLimitCheck?: boolean;
424438
}) {
425439
let hasError = false;
426440
let errorMessage = '';
427441
const jobsIds: { jobId: any; recordId: any; }[] = [];
428-
responseFlag.value = props.checkboxes.map(() => false);
442+
// responseFlag.value = props.checkboxes.map(() => false);
443+
for (let i = 0; i < recordsIds.length; i++) {
444+
const index = props.checkboxes.findIndex(item => String(item) === String(recordsIds[i]));
445+
if (index !== -1) {
446+
responseFlag.value[index] = false;
447+
}
448+
}
429449
let isRateLimitExceeded = false;
430-
try {
431-
const rateLimitRes = await callAdminForthApi({
432-
path: `/plugin/${props.meta.pluginInstanceId}/update-rate-limits`,
433-
method: 'POST',
434-
body: {
435-
actionType: actionType,
436-
},
437-
});
438-
if (rateLimitRes?.error) {
439-
isRateLimitExceeded = true;
450+
if (!disableRateLimitCheck){
451+
try {
452+
const rateLimitRes = await callAdminForthApi({
453+
path: `/plugin/${props.meta.pluginInstanceId}/update-rate-limits`,
454+
method: 'POST',
455+
body: {
456+
actionType: actionType,
457+
},
458+
});
459+
if (rateLimitRes?.error) {
460+
isRateLimitExceeded = true;
461+
adminforth.alert({
462+
message: `Rate limit exceeded for "${actionType.replace('_', ' ')}" action. Please try again later.`,
463+
variant: 'danger',
464+
timeout: 'unlimited',
465+
});
466+
return;
467+
}
468+
} catch (e) {
440469
adminforth.alert({
441-
message: `Rate limit exceeded for "${actionType.replace('_', ' ')}" action. Please try again later.`,
442-
variant: 'danger',
443-
timeout: 'unlimited',
444-
});
445-
return;
470+
message: `Error checking rate limit for "${actionType.replace('_', ' ')}" action.`,
471+
variant: 'danger',
472+
timeout: 'unlimited',
473+
});
474+
isRateLimitExceeded = true;
446475
}
447-
} catch (e) {
448-
adminforth.alert({
449-
message: `Error checking rate limit for "${actionType.replace('_', ' ')}" action.`,
450-
variant: 'danger',
451-
timeout: 'unlimited',
452-
});
453-
isRateLimitExceeded = true;
476+
if (isRateLimitExceeded) {
477+
return;
478+
};
454479
}
455-
if (isRateLimitExceeded) {
456-
return;
457-
};
458480
//creating jobs
459-
const tasks = props.checkboxes.map(async (checkbox, i) => {
481+
const tasks = recordsIds.map(async (checkbox, i) => {
460482
try {
461483
const res = await callAdminForthApi({
462484
path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
@@ -549,13 +571,22 @@ async function runAiAction({
549571
}
550572
if (index !== -1) {
551573
jobsIds.splice(jobsIds.findIndex(j => j.jobId === jobId), 1);
574+
} else {
575+
jobsIds.splice(0, jobsIds.length);
552576
}
553577
isAtLeastOneInProgress = true;
554578
adminforth.alert({
555579
message: `Generation action "${actionType.replace('_', ' ')}" failed for record: ${recordId}. Error: ${jobResponse.job?.error || 'Unknown error'}`,
556580
variant: 'danger',
557581
timeout: 'unlimited',
558582
});
583+
if (actionType === 'generate_images') {
584+
isAiImageGenerationError.value[index] = true;
585+
imageGenerationErrorMessage.value[index] = jobResponse.job?.error || 'Unknown error';
586+
} else {
587+
isAiGenerationError.value[index] = true;
588+
aiGenerationErrorMessage.value[index] = jobResponse.job?.error || 'Unknown error';
589+
}
559590
}
560591
}
561592
if (!isAtLeastOneInProgress) {
@@ -668,4 +699,15 @@ async function uploadImage(imgBlob, id, fieldName) {
668699
}
669700
}
670701
702+
function regenerateImages(recordInfo: any) {
703+
isGeneratingImages.value = true;
704+
runAiAction({
705+
endpoint: 'initial_image_generate',
706+
actionType: 'generate_images',
707+
responseFlag: isAiResponseReceivedImage,
708+
recordsIds: [recordInfo.recordInfo],
709+
disableRateLimitCheck: true,
710+
});
711+
}
712+
671713
</script>

custom/VisionTable.vue

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,22 @@
1919
<!-- IMAGE CELL TEMPLATE -->
2020
<template #cell:images="{item}">
2121
<div class="flex flex-shrink-0 gap-2">
22-
<div v-for="image in item.images" :key="image">
22+
<div v-if="item.images.length" v-for="image in item.images" :key="image">
2323
<div class="mt-2 flex items-center justify-center gap-2">
24-
<img
24+
<img
25+
v-if="isValidUrl(image)"
2526
:src="image"
2627
class="w-20 h-20 object-cover rounded cursor-pointer border hover:border-blue-500 transition"
2728
@click="zoomImage(image)"
2829
/>
30+
<div v-else class="w-20 h-20">
31+
<p>Invalid source image</p>
32+
</div>
2933
</div>
3034
</div>
35+
<div class="flex items-center justify-center text-center w-20 h-20" v-else>
36+
<p>No images found</p>
37+
</div>
3138
<transition name="fade">
3239
<div
3340
v-if="zoomedImage"
@@ -85,11 +92,33 @@
8592
<div v-if="isAiResponseReceivedImage[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])]">
8693
<div v-if="isInColumnImage(n)">
8794
<div class="mt-2 flex items-center justify-center gap-2">
88-
<img
89-
:src="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
90-
class="w-20 h-20 object-cover rounded cursor-pointer border hover:border-blue-500 transition"
91-
@click="() => {openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = true}"
95+
<img v-if="isValidUrl(selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n])"
96+
:src="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
97+
class="w-20 h-20 object-cover rounded cursor-pointer border hover:border-blue-500 transition"
98+
@click="() => {openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = true}"
9299
/>
100+
<div v-else class="flex items-center justify-center text-center w-20 h-20">
101+
<Tooltip v-if="imageGenerationErrorMessage[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] === 'No source images found'">
102+
<p
103+
>
104+
Can't generate image.
105+
</p>
106+
<template #tooltip>
107+
{{ imageGenerationErrorMessage[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] }}
108+
</template>
109+
</Tooltip>
110+
<Tooltip v-else>
111+
<div>
112+
<IconRefreshOutline
113+
@click="() => {regenerateImages(item[primaryKey])}"
114+
class="w-20 h-20 hover:text-blue-500 cursor-pointer transition hover:scale-105"
115+
/>
116+
</div>
117+
<template #tooltip>
118+
{{ imageGenerationErrorMessage[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] + '. Click to retry.' }}
119+
</template>
120+
</Tooltip>
121+
</div>
93122
</div>
94123
<div>
95124
<GenerationCarousel
@@ -122,8 +151,9 @@
122151

123152
<script lang="ts" setup>
124153
import { ref } from 'vue'
125-
import { Select, Input, Textarea, Table, Checkbox, Skeleton, Toggle } from '@/afcl'
154+
import { Select, Input, Textarea, Table, Checkbox, Skeleton, Toggle, Tooltip } from '@/afcl'
126155
import GenerationCarousel from './ImageGenerationCarousel.vue'
156+
import { IconRefreshOutline } from '@iconify-prerendered/vue-flowbite';
127157
128158
const props = defineProps<{
129159
meta: any,
@@ -141,8 +171,12 @@ const props = defineProps<{
141171
carouselSaveImages: any[]
142172
carouselImageIndex: any[]
143173
regenerateImagesRefreshRate: number
174+
isAiGenerationError: boolean[],
175+
aiGenerationErrorMessage: string[],
176+
isAiImageGenerationError: boolean[],
177+
imageGenerationErrorMessage: string[]
144178
}>();
145-
const emit = defineEmits(['error']);
179+
const emit = defineEmits(['error', 'regenerateImages']);
146180
147181
148182
const zoomedImage = ref(null)
@@ -188,10 +222,27 @@ function handleError({ isError, errorMessage }) {
188222
});
189223
}
190224
225+
function regenerateImages(recordInfo: any) {
226+
emit('regenerateImages', {
227+
recordInfo
228+
});
229+
}
230+
191231
function updateActiveIndex(newIndex: number, id: any, fieldName: string) {
192232
props.carouselImageIndex[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === id)][fieldName] = newIndex;
193233
}
194234
235+
function isValidUrl(str: string): boolean {
236+
try {
237+
new URL(str);
238+
return true;
239+
} catch {
240+
return false;
241+
}
242+
}
243+
244+
245+
195246
</script>
196247

197248
<style scoped>

0 commit comments

Comments
 (0)