@adminforth/bulk-ai-flow 1.10.3 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build.log CHANGED
@@ -5,11 +5,12 @@
5
5
  sending incremental file list
6
6
  custom/
7
7
  custom/ImageGenerationCarousel.vue
8
+ custom/Swiper.vue
8
9
  custom/VisionAction.vue
9
10
  custom/VisionTable.vue
10
11
  custom/package-lock.json
11
12
  custom/package.json
12
13
  custom/tsconfig.json
13
14
 
14
- sent 186,651 bytes received 134 bytes 373,570.00 bytes/sec
15
- total size is 186,118 speedup is 1.00
15
+ sent 62,830 bytes received 153 bytes 125,966.00 bytes/sec
16
+ total size is 62,264 speedup is 0.99
@@ -1,8 +1,8 @@
1
1
 
2
2
  <template>
3
3
  <!-- Main modal -->
4
- <div tabindex="-1" class="fixed inset-0 z-10 flex justify-center items-center dark:bg-gray-900/50 overflow-y-auto">
5
- <div class="relative p-4 w-full max-w-[1600px] max-h-[90vh] ">
4
+ <div tabindex="-1" class="[scrollbar-gutter:stable] fixed inset-0 z-10 flex justify-center items-center bg-gray-800/50 dark:bg-gray-900/50 overflow-y-auto">
5
+ <div class="relative p-4 w-full max-w-[1600px]">
6
6
  <!-- Modal content -->
7
7
  <div class="relative bg-white rounded-lg shadow-xl dark:bg-gray-700">
8
8
  <!-- Modal header -->
@@ -20,7 +20,7 @@
20
20
  </button>
21
21
  </div>
22
22
  <!-- Modal body -->
23
- <div class="p-4 md:p-5 space-y-4">
23
+ <div class="p-4 md:p-5">
24
24
  <!-- PROMPT TEXTAREA -->
25
25
  <!-- Textarea -->
26
26
  <textarea
@@ -47,7 +47,7 @@
47
47
  <!-- Fullscreen Modal -->
48
48
  <div
49
49
  v-if="zoomedImage"
50
- class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-80"
50
+ class="w-full h-full fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-80"
51
51
  @click.self="closeZoom"
52
52
  >
53
53
  <img
@@ -98,79 +98,30 @@
98
98
  <div id="gallery" class="relative w-full min-w-0" data-carousel="static">
99
99
  <!-- Carousel wrapper -->
100
100
  <div class="relative h-56 overflow-hidden rounded-lg md:h-[calc(100vh-400px)]">
101
- <!-- Item 1 -->
102
- <div
103
- v-for="(img, index) in images"
104
- :key="index"
105
- class="flex items-center justify-center w-full h-full"
106
- :class="[
107
- index === 0 ? 'block' : 'hidden'
108
- ]"
109
- data-carousel-item
110
- >
111
- <img :src="img" class="max-w-full max-h-full object-contain"
112
- :alt="`Generated image ${index + 1}`"
113
- />
114
- </div>
115
-
116
- <div v-if="images.length === 0" class="flex items-center justify-center w-full h-full">
117
-
118
- <button @click="generateImages" type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4
119
- focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center
120
- dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 ms-2">{{ $t('Generate images') }}</button>
121
-
122
- </div>
123
-
101
+ <Swiper
102
+ ref="sliderRef"
103
+ :images="images"
104
+ />
124
105
  </div>
125
- <!-- Slider controls -->
126
- <button type="button" class="absolute top-0 start-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-none"
127
- @click="slide(-1)"
128
- :disabled="images.length === 0"
129
- >
130
- <span class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-gray-800/30 group-hover:bg-white/50 dark:group-hover:bg-gray-800/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-none ">
131
- <svg class="w-4 h-4 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"
132
- :class="{
133
- 'text-gray-800 dark:text-gray-200': images.length > 0,
134
- 'text-gray-200 dark:text-gray-800': images.length === 0
135
- }"
136
- >
137
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4"/>
138
- </svg>
139
- <span class="sr-only">{{ $t('Previous') }}</span>
140
- </span>
141
- </button>
142
- <button type="button" class="absolute top-0 end-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-none "
143
- :disabled="images.length === 0"
144
- @click="slide(1)"
145
- >
146
- <span class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-gray-800/30 group-hover:bg-white/50 dark:group-hover:bg-gray-800/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-none ">
147
- <svg class="w-4 h-4 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"
148
- :class="{
149
- 'text-gray-800 dark:text-gray-200': images.length > 0,
150
- 'text-gray-200 dark:text-gray-800': images.length === 0
151
- }"
152
- >
153
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/>
154
- </svg>
155
- <span class="sr-only">{{ $t('Next') }}</span>
156
- </span>
157
- </button>
158
-
159
-
160
106
  </div>
161
107
  </div>
162
108
  </div>
163
109
  <!-- Modal footer -->
164
- <div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
165
- <button type="button" @click="confirmImage"
166
- :disabled="loading || images.length === 0"
167
- class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center
168
- dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
169
- disabled:opacity-50 disabled:cursor-not-allowed"
170
- >{{ $t('Use image') }}</button>
171
- <button type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
172
- @click="emit('close')"
173
- >{{ $t('Cancel') }}</button>
110
+ <div class="flex justify-between p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600 gap-3">
111
+ <button type="button" class="px-5 py-2.5 bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 rounded-md text-white"
112
+ @click="generateImages"
113
+ >{{ $t('Regenerate') }}</button>
114
+ <div class="flex gap-3">
115
+ <button type="button" class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
116
+ @click="emit('close')"
117
+ >{{ $t('Cancel') }}</button>
118
+ <button type="button" @click="confirmImage"
119
+ :disabled="loading || images.length === 0"
120
+ class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center
121
+ dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
122
+ disabled:opacity-50 disabled:cursor-not-allowed"
123
+ >{{ $t('Use image') }}</button>
124
+ </div>
174
125
  </div>
175
126
  </div>
176
127
  </div>
@@ -185,115 +136,45 @@ import { callAdminForthApi } from '@/utils';
185
136
  import { useI18n } from 'vue-i18n';
186
137
  import adminforth from '@/adminforth';
187
138
  import { ProgressBar } from '@/afcl';
139
+ import Swiper from './Swiper.vue';
188
140
 
189
141
  const { t: $t } = useI18n();
142
+ const sliderRef = ref(null)
190
143
 
191
144
  const prompt = ref('');
192
145
  const emit = defineEmits(['close', 'selectImage', 'error', 'updateCarouselIndex']);
193
- const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex', 'regenerateImagesRefreshRate']);
146
+ const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex', 'regenerateImagesRefreshRate','sourceImage']);
194
147
  const images = ref([]);
195
148
  const loading = ref(false);
196
149
  const attachmentFiles = ref<string[]>([])
197
150
 
198
- function minifyField(field: string): string {
199
- if (field.length > 100) {
200
- return field.slice(0, 100) + '...';
201
- }
202
- return field;
203
- }
204
-
205
- const caurosel = ref(null);
206
151
  onMounted(async () => {
207
152
  for (const img of props.images || []) {
208
153
  images.value.push(img);
209
154
  }
210
155
  const temp = await getGenerationPrompt() || '';
156
+ attachmentFiles.value = props.sourceImage || [];
211
157
  prompt.value = temp[props.fieldName];
212
158
  await nextTick();
213
159
 
214
160
  const currentIndex = props.carouselImageIndex || 0;
215
- caurosel.value = new Carousel(
216
- document.getElementById('gallery'),
217
- images.value.map((img, index) => {
218
- return {
219
- image: img,
220
- el: document.getElementById('gallery').querySelector(`[data-carousel-item]:nth-child(${index + 1})`),
221
- position: index,
222
- };
223
- }),
224
- {
225
- internal: 0,
226
- defaultPosition: currentIndex,
227
- },
228
- {
229
- override: true,
230
- }
231
- );
161
+ sliderRef.value?.slideTo(currentIndex);
162
+
232
163
 
233
- const context = {
234
- field: props.meta.pathColumnLabel,
235
- resource: props.meta.resourceLabel,
236
- };
237
164
  let template = '';
238
165
  if (prompt.value) {
239
166
  template = prompt.value;
240
167
  } else {
241
168
  template = 'Generate image for field {{field}} in {{resource}}. No text should be on image.';
242
169
  }
243
- // iterate over all variables in template and replace them with their values from props.record[field].
244
- // if field is not present in props.record[field] then replace it with empty string and drop warning
245
- const regex = /{{(.*?)}}/g;
246
- const matches = template.match(regex);
247
- if (matches) {
248
- matches.forEach((match) => {
249
- const field = match.replace(/{{|}}/g, '').trim();
250
- if (field in context) {
251
- return;
252
- } else if (field in props.record) {
253
- context[field] = minifyField(props.record[field]);
254
- } else {
255
- adminforth.alert({
256
- message: $t('Field {{field}} defined in template but not found in record', { field }),
257
- variant: 'warning',
258
- timeout: 15,
259
- });
260
- }
261
- });
262
- }
263
-
264
- prompt.value = template.replace(regex, (_, field) => {
265
- return context[field.trim()] || '';
266
- });
267
-
268
- const recordId = props.record[props.meta.recorPkFieldName];
269
- if (!recordId) {
270
- emit('error', {
271
- isError: true,
272
- errorMessage: 'Record ID not found, cannot generate images'
273
- });
274
- return;
275
- }
276
-
170
+ prompt.value = template;
277
171
  });
278
172
 
279
- async function slide(direction: number) {
280
- if (!caurosel.value) return;
281
- const curPos = caurosel.value.getActiveItem().position;
282
- if (curPos === 0 && direction === -1) return;
283
- if (curPos === images.value.length - 1 && direction === 1) {
284
- await generateImages();
285
- }
286
- if (direction === 1) {
287
- caurosel.value.next();
288
- } else {
289
- caurosel.value.prev();
290
- }
291
- }
292
173
 
293
174
  async function confirmImage() {
294
175
  loading.value = true;
295
176
 
296
- const currentIndex = caurosel.value?.getActiveItem()?.position || 0;
177
+ const currentIndex = sliderRef.value?.getActiveIndex() || 0;
297
178
  const img = images.value[currentIndex];
298
179
 
299
180
  props.images.splice(0, props.images.length);
@@ -363,7 +244,6 @@ async function generateImages() {
363
244
  const elapsed = (Date.now() - start) / 1000;
364
245
  loadingTimer.value = elapsed;
365
246
  }, 100);
366
- const currentIndex = caurosel.value?.getActiveItem()?.position || 0;
367
247
 
368
248
  await getHistoricalAverage();
369
249
  let resp = null;
@@ -429,6 +309,9 @@ async function generateImages() {
429
309
  variant: 'danger',
430
310
  timeout: 'unlimited',
431
311
  });
312
+ clearInterval(ticker);
313
+ loadingTimer.value = null;
314
+ loading.value = false;
432
315
  return;
433
316
  }
434
317
 
@@ -445,24 +328,8 @@ async function generateImages() {
445
328
 
446
329
  await nextTick();
447
330
 
331
+ sliderRef.value?.slideTo(images.value.length-1);
448
332
 
449
- caurosel.value = new Carousel(
450
- document.getElementById('gallery'),
451
- images.value.map((img, index) => {
452
- return {
453
- image: img,
454
- el: document.getElementById('gallery').querySelector(`[data-carousel-item]:nth-child(${index + 1})`),
455
- position: index,
456
- };
457
- }),
458
- {
459
- internal: 0,
460
- defaultPosition: currentIndex,
461
- },
462
- {
463
- override: true,
464
- }
465
- );
466
333
  await nextTick();
467
334
 
468
335
  loading.value = false;
@@ -0,0 +1,76 @@
1
+ <template>
2
+ <swiper-container class="flex items-center justify-center w-full h-full">
3
+ <swiper-slide v-for="(image, index) in images" :key="index">
4
+ <img :src="image" class="object-contain w-full h-full" />
5
+ </swiper-slide>
6
+ </swiper-container>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import { onMounted } from 'vue'
11
+ import { register } from 'swiper/element/bundle'
12
+ import { SwiperOptions } from 'swiper/types';
13
+
14
+ const props = defineProps<{images: string[]}>()
15
+ let swiperEl: any;
16
+
17
+ function getActiveIndex() {
18
+ if (swiperEl && swiperEl.swiper) {
19
+ return swiperEl.swiper.activeIndex;
20
+ }
21
+ return 0;
22
+ }
23
+
24
+ function slideTo(index) {
25
+
26
+ if (!swiperEl || !swiperEl.swiper) {
27
+ setTimeout(() => slideTo(index), 50);
28
+ return;
29
+ }
30
+
31
+ if (index >= 0 && index < props.images.length) {
32
+ swiperEl.swiper.update();
33
+ setTimeout(() => {
34
+ swiperEl.swiper.slideTo(index, 300);
35
+ }, 10);
36
+ }
37
+ }
38
+
39
+ defineExpose({
40
+ getActiveIndex,
41
+ slideTo
42
+ })
43
+
44
+ register()
45
+ onMounted(() => {
46
+ swiperEl = document.querySelector('swiper-container')
47
+
48
+ const swiperParams: SwiperOptions = {
49
+ slidesPerView: 1,
50
+ navigation: true,
51
+ pagination: {
52
+ type: 'fraction',
53
+ },
54
+ allowTouchMove: true,
55
+ }
56
+
57
+ Object.assign(swiperEl, swiperParams)
58
+ swiperEl.initialize()
59
+ })
60
+ </script>
61
+
62
+ <style>
63
+ .swiper {
64
+ width: 100%;
65
+ height: 100%;
66
+ }
67
+
68
+ .swiper-slide {
69
+ text-align: center;
70
+ font-size: 18px;
71
+ background: #444;
72
+ display: flex;
73
+ justify-content: center;
74
+ align-items: center;
75
+ }
76
+ </style>
@@ -8,14 +8,15 @@
8
8
  <Dialog
9
9
  ref="confirmDialog"
10
10
  header="Bulk AI Flow"
11
- class="!max-w-full w-full lg:w-[1600px] !lg:max-w-[1600px]"
11
+ class="[scrollbar-gutter:stable] !max-w-full w-full lg:w-[1600px] !lg:max-w-[1600px]"
12
12
  :beforeCloseFunction="closeDialog"
13
13
  :buttons="[
14
14
  { label: checkedCount > 1 ? 'Save fields' : 'Save field', options: { disabled: isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords || isGeneratingImages || isAnalizingFields || isAnalizingImages, loader: isLoading, class: 'w-fit' }, onclick: async (dialog) => { await saveData(); dialog.hide(); } },
15
15
  { label: 'Cancel', options: {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'}, onclick: (dialog) => dialog.hide() },
16
16
  ]"
17
+ :click-to-close-outside="false"
17
18
  >
18
- <div class="bulk-vision-table flex flex-col items-center max-w-[1560px] md:max-h-[90vh] gap-3 md:gap-4 w-full h-full overflow-y-auto">
19
+ <div class="[scrollbar-gutter:stable] bulk-vision-table flex flex-col items-center max-w-[1560px] md:max-h-[90vh] gap-3 md:gap-4 w-full h-full overflow-y-auto">
19
20
  <div v-if="records && props.checkboxes.length" class="w-full overflow-x-auto">
20
21
  <VisionTable
21
22
  :checkbox="props.checkboxes"
@@ -35,6 +36,11 @@
35
36
  :carouselSaveImages="carouselSaveImages"
36
37
  :carouselImageIndex="carouselImageIndex"
37
38
  :regenerateImagesRefreshRate="props.meta.refreshRates?.regenerateImages"
39
+ :isAiGenerationError="isAiGenerationError"
40
+ :aiGenerationErrorMessage="aiGenerationErrorMessage"
41
+ :isAiImageGenerationError="isAiImageGenerationError"
42
+ :imageGenerationErrorMessage="imageGenerationErrorMessage"
43
+ @regenerate-images="regenerateImages"
38
44
  />
39
45
  </div>
40
46
  <div class="text-red-600 flex items-center w-full">
@@ -52,6 +58,7 @@ import VisionTable from './VisionTable.vue'
52
58
  import adminforth from '@/adminforth';
53
59
  import { useI18n } from 'vue-i18n';
54
60
  import { AdminUser, type AdminForthResourceCommon } from '@/types';
61
+ import { run } from 'node:test';
55
62
 
56
63
  const { t } = useI18n();
57
64
  const props = defineProps<{
@@ -93,6 +100,10 @@ const isGeneratingImages = ref(false);
93
100
  const isAnalizingFields = ref(false);
94
101
  const isAnalizingImages = ref(false);
95
102
  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[]>([]);
96
107
 
97
108
  const openDialog = async () => {
98
109
  isDialogOpen.value = true;
@@ -415,47 +426,59 @@ async function runAiAction({
415
426
  actionType,
416
427
  responseFlag,
417
428
  updateOnSuccess = true,
429
+ recordsIds = props.checkboxes,
430
+ disableRateLimitCheck = false,
418
431
  }: {
419
432
  endpoint: string;
420
433
  actionType: 'analyze' | 'analyze_no_images' | 'generate_images';
421
434
  responseFlag: Ref<boolean[]>;
422
435
  updateOnSuccess?: boolean;
436
+ recordsIds?: any[];
437
+ disableRateLimitCheck?: boolean;
423
438
  }) {
424
439
  let hasError = false;
425
440
  let errorMessage = '';
426
441
  const jobsIds: { jobId: any; recordId: any; }[] = [];
427
- 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
+ }
428
449
  let isRateLimitExceeded = false;
429
- try {
430
- const rateLimitRes = await callAdminForthApi({
431
- path: `/plugin/${props.meta.pluginInstanceId}/update-rate-limits`,
432
- method: 'POST',
433
- body: {
434
- actionType: actionType,
435
- },
436
- });
437
- if (rateLimitRes?.error) {
438
- 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) {
439
469
  adminforth.alert({
440
- message: `Rate limit exceeded for "${actionType.replace('_', ' ')}" action. Please try again later.`,
441
- variant: 'danger',
442
- timeout: 'unlimited',
443
- });
444
- return;
470
+ message: `Error checking rate limit for "${actionType.replace('_', ' ')}" action.`,
471
+ variant: 'danger',
472
+ timeout: 'unlimited',
473
+ });
474
+ isRateLimitExceeded = true;
445
475
  }
446
- } catch (e) {
447
- adminforth.alert({
448
- message: `Error checking rate limit for "${actionType.replace('_', ' ')}" action.`,
449
- variant: 'danger',
450
- timeout: 'unlimited',
451
- });
452
- isRateLimitExceeded = true;
476
+ if (isRateLimitExceeded) {
477
+ return;
478
+ };
453
479
  }
454
- if (isRateLimitExceeded) {
455
- return;
456
- };
457
480
  //creating jobs
458
- const tasks = props.checkboxes.map(async (checkbox, i) => {
481
+ const tasks = recordsIds.map(async (checkbox, i) => {
459
482
  try {
460
483
  const res = await callAdminForthApi({
461
484
  path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
@@ -548,6 +571,8 @@ async function runAiAction({
548
571
  }
549
572
  if (index !== -1) {
550
573
  jobsIds.splice(jobsIds.findIndex(j => j.jobId === jobId), 1);
574
+ } else {
575
+ jobsIds.splice(0, jobsIds.length);
551
576
  }
552
577
  isAtLeastOneInProgress = true;
553
578
  adminforth.alert({
@@ -555,6 +580,13 @@ async function runAiAction({
555
580
  variant: 'danger',
556
581
  timeout: 'unlimited',
557
582
  });
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
+ }
558
590
  }
559
591
  }
560
592
  if (!isAtLeastOneInProgress) {
@@ -667,4 +699,15 @@ async function uploadImage(imgBlob, id, fieldName) {
667
699
  }
668
700
  }
669
701
 
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
+
670
713
  </script>