@adminforth/bulk-ai-flow 1.1.5 → 1.2.1

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.
@@ -13,7 +13,7 @@
13
13
  <template #cell:checkboxes="{ item }">
14
14
  <div class="flex items-center justify-center">
15
15
  <Checkbox
16
- v-model="selected[tableColumnsIndexes.findIndex(el => el.label === item.label)].isChecked"
16
+ v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])].isChecked"
17
17
  />
18
18
  </div>
19
19
  </template>
@@ -44,7 +44,7 @@
44
44
  </template>
45
45
  <!-- CUSTOM FIELD TEMPLATES -->
46
46
  <template v-for="n in customFieldNames" :key="n" #[`cell:${n}`]="{ item, column }">
47
- <div v-if="isAiResponseReceived[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])]">
47
+ <div v-if="isAiResponseReceivedAnalize[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] && !isInColumnImage(n)">
48
48
  <div v-if="isInColumnEnum(n)">
49
49
  <Select
50
50
  :options="convertColumnEnumToSelectOptions(props.meta.columnEnums, n)"
@@ -77,6 +77,35 @@
77
77
  />
78
78
  </div>
79
79
  </div>
80
+
81
+ <div v-else-if="isAiResponseReceivedImage[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])]">
82
+ <div v-if="isInColumnImage(n)">
83
+ <div class="mt-2 flex items-center justify-center gap-2">
84
+ <img
85
+ :src="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
86
+ class="w-20 h-20 object-cover rounded cursor-pointer border hover:border-blue-500 transition"
87
+ @click="() => {openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = true}"
88
+ />
89
+ </div>
90
+ <div>
91
+ <GenerationCarousel
92
+ v-if="openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
93
+ :images="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
94
+ :recordId="item[primaryKey]"
95
+ :meta="props.meta"
96
+ :fieldName="n"
97
+ @error="handleError"
98
+ @close="openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false"
99
+ @selectImage="updateSelectedImage"
100
+ />
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <div v-else-if="isInColumnImage(n)">
106
+ <Skeleton type="image" class="w-20 h-20" />
107
+ </div>
108
+
80
109
  <div v-else>
81
110
  <Skeleton class="w-full h-6" />
82
111
  </div>
@@ -89,6 +118,7 @@
89
118
  import { ref, nextTick, watch } from 'vue'
90
119
  import mediumZoom from 'medium-zoom'
91
120
  import { Select, Input, Textarea, Table, Checkbox, Skeleton, Toggle } from '@/afcl'
121
+ import GenerationCarousel from './imageGenerationCarousel.vue'
92
122
 
93
123
  const props = defineProps<{
94
124
  meta: any,
@@ -97,13 +127,20 @@ const props = defineProps<{
97
127
  customFieldNames: any,
98
128
  tableColumnsIndexes: any,
99
129
  selected: any,
100
- isAiResponseReceived: boolean[],
101
- primaryKey: any
130
+ isAiResponseReceivedAnalize: boolean[],
131
+ isAiResponseReceivedImage: boolean[],
132
+ primaryKey: any,
133
+ openGenerationCarousel: any
134
+ isError: boolean,
135
+ errorMessage: string
102
136
  }>();
137
+ const emit = defineEmits(['error']);
138
+
103
139
 
104
140
  const zoomedImage = ref(null)
105
141
  const zoomedImg = ref(null)
106
142
 
143
+
107
144
  function zoomImage(img) {
108
145
  zoomedImage.value = img
109
146
  }
@@ -131,6 +168,10 @@ function isInColumnEnum(key: string): boolean {
131
168
  return true;
132
169
  }
133
170
 
171
+ function isInColumnImage(key: string): boolean {
172
+ return props.meta.outputImageFields?.includes(key) || false;
173
+ }
174
+
134
175
  function convertColumnEnumToSelectOptions(columnEnumArray: any[], key: string) {
135
176
  const col = columnEnumArray.find(c => c.name === key);
136
177
  if (!col) return [];
@@ -140,4 +181,15 @@ function convertColumnEnumToSelectOptions(columnEnumArray: any[], key: string) {
140
181
  }));
141
182
  }
142
183
 
184
+ function updateSelectedImage(image: string, id: any, fieldName: string) {
185
+ props.selected[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === id)][fieldName] = image;
186
+ }
187
+
188
+ function handleError({ isError, errorMessage }) {
189
+ emit('error', {
190
+ isError,
191
+ errorMessage
192
+ });
193
+ }
194
+
143
195
  </script>
@@ -0,0 +1,462 @@
1
+
2
+ <template>
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 ">
6
+ <!-- Modal content -->
7
+ <div class="relative bg-white rounded-lg shadow-xl dark:bg-gray-700">
8
+ <!-- Modal header -->
9
+ <div class="flex items-center justify-between p-3 md:p-4 border-b rounded-t dark:border-gray-600">
10
+ <h3 class="text-xl font-semibold text-gray-900 dark:text-white">
11
+ {{ $t('Generate image with AI') }}
12
+ </h3>
13
+ <button type="button"
14
+ @click="emit('close')"
15
+ 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" >
16
+ <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
17
+ <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"/>
18
+ </svg>
19
+ <span class="sr-only">{{ $t('Close modal') }}</span>
20
+ </button>
21
+ </div>
22
+ <!-- Modal body -->
23
+ <div class="p-4 md:p-5 space-y-4">
24
+ <!-- PROMPT TEXTAREA -->
25
+ <!-- Textarea -->
26
+ <textarea
27
+ id="message"
28
+ rows="3"
29
+ class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
30
+ :placeholder="$t('Prompt which will be passed to AI network')"
31
+ v-model="prompt"
32
+ :title="$t('Prompt which will be passed to AI network')"
33
+ ></textarea>
34
+
35
+ <!-- Thumbnails -->
36
+ <div class="mt-2 flex flex-wrap gap-2">
37
+ <img
38
+ v-for="(img, idx) in attachmentFiles"
39
+ :key="idx"
40
+ :src="img"
41
+ class="w-20 h-20 object-cover rounded cursor-pointer border hover:border-blue-500 transition"
42
+ :alt="`Generated image ${idx + 1}`"
43
+ @click="zoomImage(img)"
44
+ />
45
+ </div>
46
+
47
+ <!-- Fullscreen Modal -->
48
+ <div
49
+ v-if="zoomedImage"
50
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-80"
51
+ @click.self="closeZoom"
52
+ >
53
+ <img
54
+ :src="zoomedImage"
55
+ ref="zoomedImg"
56
+ class="max-w-full max-h-full rounded-lg object-contain cursor-grab z-75"
57
+ />
58
+ </div>
59
+
60
+ <div class="flex flex-col items-center justify-center w-full relative">
61
+ <div
62
+ v-if="loading"
63
+ class=" absolute flex items-center justify-center w-full h-full z-40 bg-white/80 dark:bg-gray-900/80 rounded-lg"
64
+ >
65
+ <div role="status" class="absolute -translate-x-1/2 -translate-y-1/2 top-2/4 left-1/2">
66
+ <svg aria-hidden="true" class="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/><path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/></svg>
67
+ <span class="sr-only">{{ $t('Loading...') }}</span>
68
+ </div>
69
+ </div>
70
+
71
+ <div v-if="loadingTimer" class="absolute pt-12 flex items-center justify-center w-full h-full z-40 bg-white/80 dark:bg-gray-900/80 rounded-lg">
72
+ <div class="text-gray-800 dark:text-gray-100 text-lg font-semibold"
73
+ v-if="!historicalAverage"
74
+ >
75
+ {{ formatTime(loadingTimer) }} {{ $t('passed...') }}
76
+ </div>
77
+ <div class="w-64" v-else>
78
+ <ProgressBar
79
+ class="absolute max-w-full"
80
+ :currentValue="loadingTimer < historicalAverage ? loadingTimer : historicalAverage"
81
+ :minValue="0"
82
+ :maxValue="historicalAverage"
83
+ :showValues="false"
84
+ :progressFormatter="(value: number, percentage: number) => `${ formatTime(loadingTimer) } ( ~ ${ Math.floor( (
85
+ loadingTimer < historicalAverage ? loadingTimer : historicalAverage
86
+ ) / historicalAverage * 100) }% )`"
87
+ />
88
+ </div>
89
+ </div>
90
+
91
+ <div v-if="errorMessage" class="absolute flex items-center justify-center w-full h-full z-40 bg-white/80 dark:bg-gray-900/80 rounded-lg">
92
+ <div class="pt-20 text-red-500 dark:text-red-400 text-lg font-semibold">
93
+ {{ errorMessage }}
94
+ </div>
95
+ </div>
96
+
97
+
98
+ <div id="gallery" class="relative w-full" data-carousel="static">
99
+ <!-- Carousel wrapper -->
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="[
106
+ index === 0 ? 'block' : 'hidden',
107
+ 'duration-700 ease-in-out'
108
+ ]"
109
+ data-carousel-item
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"
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
+
124
+ </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
+ </div>
161
+ </div>
162
+ </div>
163
+ <!-- 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>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+
180
+
181
+
182
+ </template>
183
+
184
+ <script setup lang="ts">
185
+
186
+ import { ref, onMounted, nextTick, Ref, h, computed, watch, reactive } from 'vue'
187
+ import { Carousel } from 'flowbite';
188
+ import { callAdminForthApi } from '@/utils';
189
+ import { useI18n } from 'vue-i18n';
190
+ import adminforth from '@/adminforth';
191
+ import { ProgressBar } from '@/afcl';
192
+
193
+ const { t: $t } = useI18n();
194
+
195
+ const prompt = ref('');
196
+ const emit = defineEmits(['close', 'selectImage', 'error']);
197
+ const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage']);
198
+ const images = ref([]);
199
+ const loading = ref(false);
200
+ const attachmentFiles = ref<string[]>([])
201
+
202
+ function minifyField(field: string): string {
203
+ if (field.length > 100) {
204
+ return field.slice(0, 100) + '...';
205
+ }
206
+ return field;
207
+ }
208
+
209
+ const caurosel = ref(null);
210
+ onMounted(async () => {
211
+ images.value.push((props.images || []));
212
+ const temp = await getGenerationPrompt() || '';
213
+ prompt.value = temp[props.fieldName];
214
+ await nextTick();
215
+
216
+ const currentIndex = caurosel.value?.getActiveItem()?.position || 0;
217
+ caurosel.value = new Carousel(
218
+ document.getElementById('gallery'),
219
+ images.value.map((img, index) => {
220
+ return {
221
+ image: img,
222
+ el: document.getElementById('gallery').querySelector(`[data-carousel-item]:nth-child(${index + 1})`),
223
+ position: index,
224
+ };
225
+ }),
226
+ {
227
+ internal: 0,
228
+ defaultPosition: currentIndex,
229
+ },
230
+ {
231
+ override: true,
232
+ }
233
+ );
234
+
235
+ const context = {
236
+ field: props.meta.pathColumnLabel,
237
+ resource: props.meta.resourceLabel,
238
+ };
239
+ let template = '';
240
+ if (prompt.value) {
241
+ template = prompt.value;
242
+ } else {
243
+ template = 'Generate image for field {{field}} in {{resource}}. No text should be on image.';
244
+ }
245
+ // iterate over all variables in template and replace them with their values from props.record[field].
246
+ // if field is not present in props.record[field] then replace it with empty string and drop warning
247
+ const regex = /{{(.*?)}}/g;
248
+ const matches = template.match(regex);
249
+ if (matches) {
250
+ matches.forEach((match) => {
251
+ const field = match.replace(/{{|}}/g, '').trim();
252
+ if (field in context) {
253
+ return;
254
+ } else if (field in props.record) {
255
+ context[field] = minifyField(props.record[field]);
256
+ } else {
257
+ adminforth.alert({
258
+ message: $t('Field {{field}} defined in template but not found in record', { field }),
259
+ variant: 'warning',
260
+ timeout: 15,
261
+ });
262
+ }
263
+ });
264
+ }
265
+
266
+ prompt.value = template.replace(regex, (_, field) => {
267
+ return context[field.trim()] || '';
268
+ });
269
+
270
+ if (props.record[props.record[props.meta.recorPkFieldName]]) {
271
+ const recordId = props.record[props.meta.recorPkFieldName];
272
+ } else {
273
+ emit('error', {
274
+ isError: true,
275
+ errorMessage: 'Record ID not found, cannot generate images'
276
+ });
277
+ return;
278
+ }
279
+
280
+ });
281
+
282
+ async function slide(direction: number) {
283
+ if (!caurosel.value) return;
284
+ const curPos = caurosel.value.getActiveItem().position;
285
+ if (curPos === 0 && direction === -1) return;
286
+ if (curPos === images.value.length - 1 && direction === 1) {
287
+ await generateImages();
288
+ }
289
+ if (direction === 1) {
290
+ caurosel.value.next();
291
+ } else {
292
+ caurosel.value.prev();
293
+ }
294
+ }
295
+
296
+ async function confirmImage() {
297
+ loading.value = true;
298
+
299
+ const currentIndex = caurosel.value?.getActiveItem()?.position || 0;
300
+ const img = images.value[currentIndex];
301
+
302
+ emit('selectImage', img, props.recordId, props.fieldName);
303
+ emit('close');
304
+
305
+ loading.value = false;
306
+ }
307
+
308
+ const loadingTimer: Ref<number | null> = ref(null);
309
+
310
+
311
+ const errorMessage: Ref<string | null> = ref(null);
312
+
313
+ const historicalAverage: Ref<number | null> = ref(null);
314
+
315
+
316
+ function formatTime(seconds: number): string {
317
+ const minutes = Math.floor(seconds / 60);
318
+ return `${minutes % 60}m ${Math.floor(seconds % 60)}s`;
319
+ }
320
+
321
+
322
+ async function getHistoricalAverage() {
323
+ const resp = await callAdminForthApi({
324
+ path: `/plugin/${props.meta.pluginInstanceId}/averageDuration`,
325
+ method: 'GET',
326
+ });
327
+ historicalAverage.value = resp?.averageDuration || null;
328
+ }
329
+
330
+ async function getGenerationPrompt() {
331
+ try{
332
+ const resp = await callAdminForthApi({
333
+ path: `/plugin/${props.meta.pluginInstanceId}/get_generation_prompts`,
334
+ method: 'POST',
335
+ body: {
336
+ recordId: props.recordId,
337
+ },
338
+ });
339
+ if(!resp) {
340
+ emit('error', {
341
+ isError: true,
342
+ errorMessage: "Something went wrong. Check your internet connection and try again."
343
+ });
344
+ }
345
+ return resp?.generationOptions || null;
346
+ } catch (e) {
347
+ emit('error', {
348
+ isError: true,
349
+ errorMessage: e.message
350
+ });
351
+ }
352
+ }
353
+
354
+ async function generateImages() {
355
+ errorMessage.value = null;
356
+ loading.value = true;
357
+ loadingTimer.value = 0;
358
+ const start = Date.now();
359
+ const ticker = setInterval(() => {
360
+ const elapsed = (Date.now() - start) / 1000;
361
+ loadingTimer.value = elapsed;
362
+ }, 100);
363
+ const currentIndex = caurosel.value?.getActiveItem()?.position || 0;
364
+
365
+ await getHistoricalAverage();
366
+ let resp = null;
367
+ let error = null;
368
+ try {
369
+ resp = await callAdminForthApi({
370
+ path: `/plugin/${props.meta.pluginInstanceId}/regenerate_images`,
371
+ method: 'POST',
372
+ body: {
373
+ prompt: prompt.value,
374
+ recordId: props.recordId,
375
+ fieldName: props.fieldName,
376
+ },
377
+ });
378
+ } catch (e) {
379
+ console.error(e);
380
+ } finally {
381
+ clearInterval(ticker);
382
+ loadingTimer.value = null;
383
+ loading.value = false;
384
+ }
385
+ if (resp?.error) {
386
+ error = resp.error;
387
+ }
388
+ if (!resp) {
389
+ error = $t('Error generating images, something went wrong');
390
+ }
391
+
392
+ if (error) {
393
+ if (images.value.length === 0) {
394
+ errorMessage.value = error;
395
+ } else {
396
+ adminforth.alert({
397
+ message: error,
398
+ variant: 'danger',
399
+ timeout: 'unlimited',
400
+ });
401
+ emit('error', {
402
+ isError: true,
403
+ errorMessage: error
404
+ });
405
+ }
406
+ return;
407
+ }
408
+
409
+ images.value = [
410
+ ...images.value,
411
+ ...resp.images,
412
+ ];
413
+
414
+ await nextTick();
415
+
416
+
417
+ caurosel.value = new Carousel(
418
+ document.getElementById('gallery'),
419
+ images.value.map((img, index) => {
420
+ return {
421
+ image: img,
422
+ el: document.getElementById('gallery').querySelector(`[data-carousel-item]:nth-child(${index + 1})`),
423
+ position: index,
424
+ };
425
+ }),
426
+ {
427
+ internal: 0,
428
+ defaultPosition: currentIndex,
429
+ },
430
+ {
431
+ override: true,
432
+ }
433
+ );
434
+ await nextTick();
435
+
436
+ loading.value = false;
437
+ }
438
+
439
+ import mediumZoom from 'medium-zoom'
440
+
441
+ const zoomedImage = ref(null)
442
+ const zoomedImg = ref(null)
443
+
444
+ function zoomImage(img) {
445
+ zoomedImage.value = img
446
+ }
447
+
448
+ function closeZoom() {
449
+ zoomedImage.value = null
450
+ }
451
+
452
+ watch(zoomedImage, async (val) => {
453
+ await nextTick()
454
+ if (val && zoomedImg.value) {
455
+ mediumZoom(zoomedImg.value, {
456
+ margin: 24,
457
+ background: 'rgba(0, 0, 0, 0.9)',
458
+ scrollOffset: 150
459
+ }).show()
460
+ }
461
+ })
462
+ </script>