@adminforth/bulk-ai-flow 1.8.0 → 1.9.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.
@@ -1,8 +1,8 @@
1
1
 
2
2
  <template>
3
3
  <!-- Main modal -->
4
- <div tabindex="-1" class="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 bottom-0 z-10 flex justify-center items-center w-full md:inset-0 h-full max-h-full bg-black/50 dark:bg-gray-900 dark:bg-opacity-50">
5
- <div class="relative p-4 w-10/12 max-w-full max-h-full ">
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] ">
6
6
  <!-- Modal content -->
7
7
  <div class="relative bg-white rounded-lg shadow-xl dark:bg-gray-700">
8
8
  <!-- Modal header -->
@@ -95,20 +95,20 @@
95
95
  </div>
96
96
 
97
97
 
98
- <div id="gallery" class="relative w-full" data-carousel="static">
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
101
  <!-- Item 1 -->
102
102
  <div
103
103
  v-for="(img, index) in images"
104
104
  :key="index"
105
+ class="flex items-center justify-center w-full h-full"
105
106
  :class="[
106
- index === 0 ? 'block' : 'hidden',
107
- 'duration-700 ease-in-out'
107
+ index === 0 ? 'block' : 'hidden'
108
108
  ]"
109
109
  data-carousel-item
110
110
  >
111
- <img :src="img" class="absolute block max-w-full max-h-full -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 object-cover"
111
+ <img :src="img" class="max-w-full max-h-full object-contain"
112
112
  :alt="`Generated image ${index + 1}`"
113
113
  />
114
114
  </div>
@@ -190,7 +190,7 @@ const { t: $t } = useI18n();
190
190
 
191
191
  const prompt = ref('');
192
192
  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']);
194
194
  const images = ref([]);
195
195
  const loading = ref(false);
196
196
  const attachmentFiles = ref<string[]>([])
@@ -370,26 +370,24 @@ async function generateImages() {
370
370
  let error = null;
371
371
  try {
372
372
  resp = await callAdminForthApi({
373
- path: `/plugin/${props.meta.pluginInstanceId}/regenerate_images`,
373
+ path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
374
374
  method: 'POST',
375
375
  body: {
376
- prompt: prompt.value,
376
+ actionType: 'regenerate_images',
377
377
  recordId: props.recordId,
378
- fieldName: props.fieldName,
378
+ prompt: prompt.value,
379
+ fieldName: props.fieldName
379
380
  },
380
381
  });
381
382
  } catch (e) {
382
383
  console.error(e);
383
- } finally {
384
- clearInterval(ticker);
385
- loadingTimer.value = null;
386
- loading.value = false;
387
384
  }
385
+
388
386
  if (resp?.error) {
389
387
  error = resp.error;
390
388
  }
391
389
  if (!resp) {
392
- error = $t('Error generating images, something went wrong');
390
+ error = $t('Error creating image generation job');
393
391
  }
394
392
 
395
393
  if (error) {
@@ -401,19 +399,50 @@ async function generateImages() {
401
399
  variant: 'danger',
402
400
  timeout: 'unlimited',
403
401
  });
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 },
407
414
  });
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');
408
422
  }
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
+ });
409
432
  return;
410
433
  }
411
434
 
435
+ const respImages = jobResponse?.job?.result[props.fieldName] || [];
436
+
412
437
  images.value = [
413
438
  ...images.value,
414
- ...resp.images,
439
+ ...respImages,
415
440
  ];
416
441
 
442
+ clearInterval(ticker);
443
+ loadingTimer.value = null;
444
+ loading.value = false;
445
+
417
446
  await nextTick();
418
447
 
419
448
 
@@ -5,61 +5,39 @@
5
5
  </div>
6
6
  <p class="text-justify max-h-[18px] truncate max-w-[60vw] md:max-w-none">{{ props.meta.actionName }}</p>
7
7
  </div>
8
- <Dialog ref="confirmDialog">
9
- <div
10
- class="fixed inset-0 z-20 flex items-center justify-center bg-black/40"
11
- >
12
- <div
13
- class="bulk-vision-dialog flex items-center justify-center relative w-[100vw] h-[100vh] max-h-[100vh] md:w-auto md:max-w-[95vw] md:min-w-[640px] md:h-auto md:max-h-[90vh] bg-white dark:bg-gray-900 rounded-none md:rounded-md shadow-2xl overflow-hidden"
14
- >
15
- <div class="bulk-vision-table flex flex-col items-center justify-evenly md:max-h-[90vh] gap-3 md:gap-4 w-full h-full p-4 md:p-6 overflow-y-auto overflow-x-auto">
16
- <button type="button"
17
- @click="closeDialog"
18
- class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" >
19
- <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
20
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
21
- </svg>
22
- </button>
23
-
24
- <div v-if="records && props.checkboxes.length" class="w-full overflow-x-auto">
25
- <VisionTable
26
- :checkbox="props.checkboxes"
27
- :records="records"
28
- :meta="props.meta"
29
- :images="images"
30
- :tableHeaders="tableHeaders"
31
- :tableColumns="tableColumns"
32
- :customFieldNames="customFieldNames"
33
- :tableColumnsIndexes="tableColumnsIndexes"
34
- :selected="selected"
35
- :isAiResponseReceivedAnalize="isAiResponseReceivedAnalize"
36
- :isAiResponseReceivedImage="isAiResponseReceivedImage"
37
- :primaryKey="primaryKey"
38
- :openGenerationCarousel="openGenerationCarousel"
39
- @error="handleTableError"
40
- :carouselSaveImages="carouselSaveImages"
41
- :carouselImageIndex="carouselImageIndex"
42
- />
43
- </div>
44
- <div class="flex w-full flex-col md:flex-row items-stretch md:items-end justify-end gap-3 md:gap-4">
45
- <div class="h-full text-red-600 font-semibold flex items-center justify-center md:mb-2">
46
- <p v-if="isError === true">{{ errorMessage }}</p>
47
- </div>
48
- <button type="button" class="w-full md:w-auto 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"
49
- @click="closeDialog"
50
- >
51
- {{'Cancel'}}
52
- </button>
53
- <Button
54
- class="w-full md:w-64"
55
- @click="saveData"
56
- :disabled="isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords"
57
- :loader="isLoading"
58
- >
59
- {{ checkedCount > 1 ? 'Save fields' : 'Save field' }}
60
- </Button>
61
- </div>
62
- </div>
8
+ <Dialog
9
+ ref="confirmDialog"
10
+ header="Bulk AI Flow"
11
+ class="!max-w-full w-full lg:w-[1600px] !lg:max-w-[1600px]"
12
+ :buttons="[
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(); } },
14
+ { label: 'Cancel', onclick: (dialog) => dialog.hide() },
15
+ ]"
16
+ >
17
+ <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">
18
+ <div v-if="records && props.checkboxes.length" class="w-full overflow-x-auto">
19
+ <VisionTable
20
+ :checkbox="props.checkboxes"
21
+ :records="records"
22
+ :meta="props.meta"
23
+ :images="images"
24
+ :tableHeaders="tableHeaders"
25
+ :tableColumns="tableColumns"
26
+ :customFieldNames="customFieldNames"
27
+ :tableColumnsIndexes="tableColumnsIndexes"
28
+ :selected="selected"
29
+ :isAiResponseReceivedAnalize="isAiResponseReceivedAnalize"
30
+ :isAiResponseReceivedImage="isAiResponseReceivedImage"
31
+ :primaryKey="primaryKey"
32
+ :openGenerationCarousel="openGenerationCarousel"
33
+ @error="handleTableError"
34
+ :carouselSaveImages="carouselSaveImages"
35
+ :carouselImageIndex="carouselImageIndex"
36
+ :regenerateImagesRefreshRate="props.meta.refreshRates?.regenerateImages"
37
+ />
38
+ </div>
39
+ <div class="text-red-600 flex items-center w-full">
40
+ <p v-if="isError === true">{{ errorMessage }}</p>
63
41
  </div>
64
42
  </div>
65
43
  </Dialog>
@@ -111,9 +89,14 @@ const isCriticalError = ref(false);
111
89
  const isImageGenerationError = ref(false);
112
90
  const errorMessage = ref('');
113
91
  const checkedCount = ref(0);
92
+ const isGeneratingImages = ref(false);
93
+ const isAnalizingFields = ref(false);
94
+ const isAnalizingImages = ref(false);
95
+
114
96
 
115
97
  const openDialog = async () => {
116
98
  confirmDialog.value.open();
99
+ isFetchingRecords.value = true;
117
100
  await getRecords();
118
101
  if (props.meta.isAttachFiles) {
119
102
  await getImages();
@@ -124,42 +107,41 @@ const openDialog = async () => {
124
107
  tableColumnsIndexes.value = result.indexes;
125
108
  customFieldNames.value = tableHeaders.value.slice((props.meta.isAttachFiles) ? 3 : 2).map(h => h.fieldName);
126
109
  setSelected();
110
+ if (props.meta.isImageGeneration) {
111
+ fillCarouselSaveImages();
112
+ }
127
113
  for (let i = 0; i < selected.value?.length; i++) {
128
114
  openGenerationCarousel.value[i] = props.meta.outputImageFields?.reduce((acc,key) =>{
129
115
  acc[key] = false;
130
116
  return acc;
131
117
  },{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
132
118
  }
133
- isFetchingRecords.value = true;
134
- 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
+ }
135
129
  if (props.meta.isFieldsForAnalizeFromImages) {
136
- tasks.push(runAiAction({
130
+ isAnalizingImages.value = true;
131
+ runAiAction({
137
132
  endpoint: 'analyze',
138
133
  actionType: 'analyze',
139
134
  responseFlag: isAiResponseReceivedAnalize,
140
- }));
135
+ });
141
136
  }
142
137
  if (props.meta.isFieldsForAnalizePlain) {
143
- tasks.push(runAiAction({
138
+ isAnalizingFields.value = true;
139
+ runAiAction({
144
140
  endpoint: 'analyze_no_images',
145
141
  actionType: 'analyze_no_images',
146
142
  responseFlag: isAiResponseReceivedAnalize,
147
- }));
148
- }
149
- if (props.meta.isImageGeneration) {
150
- tasks.push(runAiAction({
151
- endpoint: 'initial_image_generate',
152
- actionType: 'generate_images',
153
- responseFlag: isAiResponseReceivedImage,
154
- }));
155
- }
156
- await Promise.all(tasks);
157
-
158
- if (props.meta.isImageGeneration) {
159
- fillCarouselSaveImages();
143
+ });
160
144
  }
161
-
162
- isFetchingRecords.value = false;
163
145
  }
164
146
 
165
147
  watch(selected, (val) => {
@@ -167,39 +149,22 @@ watch(selected, (val) => {
167
149
  checkedCount.value = val.filter(item => item.isChecked === true).length;
168
150
  }, { deep: true });
169
151
 
170
- const closeDialog = () => {
171
- confirmDialog.value.close();
172
- isAiResponseReceivedAnalize.value = [];
173
- isAiResponseReceivedImage.value = [];
174
-
175
- records.value = [];
176
- images.value = [];
177
- selected.value = [];
178
- tableColumns.value = [];
179
- tableColumnsIndexes.value = [];
180
- isError.value = false;
181
- isCriticalError.value = false;
182
- isImageGenerationError.value = false;
183
- errorMessage.value = '';
184
- carouselSaveImages.value = [];
185
- carouselImageIndex.value = [];
186
- }
187
-
188
152
  function fillCarouselSaveImages() {
189
153
  for (const item of selected.value) {
190
154
  const tempItem: any = {};
191
155
  const tempItemIndex: any = {};
192
156
  for (const [key, value] of Object.entries(item)) {
193
- if (props.meta.outputImageFields?.includes(key)) {
194
- tempItem[key] = [value];
195
- tempItemIndex[key] = 0;
196
- }
157
+ if (props.meta.outputImageFields?.includes(key)) {
158
+ tempItem[key] = [];
159
+ tempItemIndex[key] = 0;
160
+ }
197
161
  }
198
162
  carouselSaveImages.value.push(tempItem);
199
163
  carouselImageIndex.value.push(tempItemIndex);
200
164
  }
201
165
  }
202
166
 
167
+
203
168
  function formatLabel(str) {
204
169
  return str
205
170
  .split('_')
@@ -438,38 +403,129 @@ async function runAiAction({
438
403
  responseFlag: Ref<boolean[]>;
439
404
  updateOnSuccess?: boolean;
440
405
  }) {
441
- let res: any;
442
- let error: any = null;
443
-
444
- try {
445
- 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
+ });
446
422
 
447
- res = await callAdminForthApi({
448
- path: `/plugin/${props.meta.pluginInstanceId}/${endpoint}`,
449
- method: 'POST',
450
- body: {
451
- selectedIds: props.checkboxes,
452
- },
453
- });
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
+ }
454
430
 
455
- if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
456
- 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 };
457
437
  }
458
- } catch (e) {
459
- console.error(`Error during ${actionType}:`, e);
460
- error = `Failed to ${actionType.replace('_', ' ')}. Please, try to re-run the action.`;
461
- }
438
+ });
439
+ await Promise.all(tasks);
462
440
 
463
- if (res?.error) {
464
- error = res.error;
465
- }
466
- if (!res && !error) {
467
- 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
+ }
468
524
  }
469
525
 
470
- if (error) {
526
+ if (hasError) {
471
527
  adminforth.alert({
472
- message: error,
528
+ message: errorMessage,
473
529
  variant: 'danger',
474
530
  timeout: 'unlimited',
475
531
  });
@@ -477,26 +533,19 @@ async function runAiAction({
477
533
  if (actionType === 'generate_images') {
478
534
  isImageGenerationError.value = true;
479
535
  }
480
- errorMessage.value = error;
536
+ this.errorMessage.value = errorMessage;
481
537
  return;
482
538
  }
483
539
 
484
- if (updateOnSuccess) {
485
- res.result.forEach((item: any, idx: number) => {
486
- const pk = selected.value[idx]?.[primaryKey];
487
- if (pk) {
488
- selected.value[idx] = {
489
- ...selected.value[idx],
490
- ...item,
491
- isChecked: true,
492
- [primaryKey]: pk,
493
- };
494
- }
495
- });
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;
496
546
  }
497
547
  }
498
548
 
499
-
500
549
  async function uploadImage(imgBlob, id, fieldName) {
501
550
  const file = new File([imgBlob], `generated_${fieldName}_${id}.${imgBlob.type.split('/').pop()}`, { type: imgBlob.type });
502
551
  const { name, size, type } = file;
@@ -1,9 +1,8 @@
1
1
  <template>
2
- <div>
3
2
  <Table
4
3
  :columns="tableHeaders"
5
4
  :data="tableColumns"
6
- :pageSize="8"
5
+ :pageSize="6"
7
6
  >
8
7
  <!-- HEADER TEMPLATE -->
9
8
  <template #header:checkboxes="{ item }">
@@ -11,7 +10,7 @@
11
10
  </template>
12
11
  <!-- CHECKBOX CELL TEMPLATE -->
13
12
  <template #cell:checkboxes="{ item }">
14
- <div class="flex items-center justify-center">
13
+ <div class="max-w-[100px] flex items-center justify-center">
15
14
  <Checkbox
16
15
  v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])].isChecked"
17
16
  />
@@ -28,18 +27,22 @@
28
27
  @click="zoomImage(image)"
29
28
  />
30
29
  </div>
30
+ </div>
31
+ <transition name="fade">
31
32
  <div
32
33
  v-if="zoomedImage"
33
34
  class="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
34
35
  @click.self="closeZoom"
35
36
  >
36
- <img
37
- :src="zoomedImage"
38
- ref="zoomedImg"
39
- class="max-w-full max-h-full rounded-lg object-contain cursor-grab z-75"
40
- />
37
+ <transition name="zoom">
38
+ <img
39
+ v-if="zoomedImage"
40
+ :src="zoomedImage"
41
+ class="max-w-full max-h-full rounded-lg object-contain cursor-grab z-75"
42
+ />
43
+ </transition>
41
44
  </div>
42
- </div>
45
+ </transition>
43
46
  </div>
44
47
  </template>
45
48
  <!-- CUSTOM FIELD TEMPLATES -->
@@ -96,6 +99,7 @@
96
99
  :meta="props.meta"
97
100
  :fieldName="n"
98
101
  :carouselImageIndex="carouselImageIndex[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
102
+ :regenerateImagesRefreshRate="regenerateImagesRefreshRate"
99
103
  @error="handleError"
100
104
  @close="openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false"
101
105
  @selectImage="updateSelectedImage"
@@ -114,12 +118,10 @@
114
118
  </div>
115
119
  </template>
116
120
  </Table>
117
- </div>
118
121
  </template>
119
122
 
120
123
  <script lang="ts" setup>
121
- import { ref, nextTick, watch } from 'vue'
122
- import mediumZoom from 'medium-zoom'
124
+ import { ref } from 'vue'
123
125
  import { Select, Input, Textarea, Table, Checkbox, Skeleton, Toggle } from '@/afcl'
124
126
  import GenerationCarousel from './ImageGenerationCarousel.vue'
125
127
 
@@ -138,12 +140,12 @@ const props = defineProps<{
138
140
  errorMessage: string
139
141
  carouselSaveImages: any[]
140
142
  carouselImageIndex: any[]
143
+ regenerateImagesRefreshRate: number
141
144
  }>();
142
145
  const emit = defineEmits(['error']);
143
146
 
144
147
 
145
148
  const zoomedImage = ref(null)
146
- const zoomedImg = ref(null)
147
149
 
148
150
 
149
151
  function zoomImage(img) {
@@ -154,17 +156,6 @@ function closeZoom() {
154
156
  zoomedImage.value = null
155
157
  }
156
158
 
157
- watch(zoomedImage, async (val) => {
158
- await nextTick()
159
- if (val && zoomedImg.value) {
160
- mediumZoom(zoomedImg.value, {
161
- margin: 24,
162
- background: 'rgba(0, 0, 0, 0.9)',
163
- scrollOffset: 150
164
- }).show()
165
- }
166
- })
167
-
168
159
  function isInColumnEnum(key: string): boolean {
169
160
  const colEnum = props.meta.columnEnums?.find(c => c.name === key);
170
161
  if (!colEnum) {
@@ -201,4 +192,21 @@ function updateActiveIndex(newIndex: number, id: any, fieldName: string) {
201
192
  props.carouselImageIndex[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === id)][fieldName] = newIndex;
202
193
  }
203
194
 
204
- </script>
195
+ </script>
196
+
197
+ <style scoped>
198
+ .fade-enter-active, .fade-leave-active {
199
+ transition: opacity 0.2s ease;
200
+ }
201
+ .fade-enter-from, .fade-leave-to {
202
+ opacity: 0;
203
+ }
204
+
205
+ .zoom-enter-active, .zoom-leave-active {
206
+ transition: transform 0.2s ease, opacity 0.2s ease;
207
+ }
208
+ .zoom-enter-from, .zoom-leave-to {
209
+ transform: scale(0.95);
210
+ opacity: 0;
211
+ }
212
+ </style>