11
22<template >
33 <!-- 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]" >
66 <!-- Modal content -->
77 <div class =" relative bg-white rounded-lg shadow-xl dark:bg-gray-700" >
88 <!-- Modal header -->
2020 </button >
2121 </div >
2222 <!-- Modal body -->
23- <div class =" p-4 md:p-5 space-y-4 " >
23+ <div class =" p-4 md:p-5" >
2424 <!-- PROMPT TEXTAREA -->
2525 <!-- Textarea -->
2626 <textarea
4747 <!-- Fullscreen Modal -->
4848 <div
4949 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"
5151 @click.self =" closeZoom"
5252 >
5353 <img
9898 <div id =" gallery" class =" relative w-full min-w-0" data-carousel =" static" >
9999 <!-- Carousel wrapper -->
100100 <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+ />
124105 </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-
160106 </div >
161107 </div >
162108 </div >
163109 <!-- 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 >
174125 </div >
175126 </div >
176127 </div >
@@ -185,115 +136,45 @@ import { callAdminForthApi } from '@/utils';
185136import { useI18n } from ' vue-i18n' ;
186137import adminforth from ' @/adminforth' ;
187138import { ProgressBar } from ' @/afcl' ;
139+ import Swiper from ' ./Swiper.vue' ;
188140
189141const { t : $t } = useI18n ();
142+ const sliderRef = ref (null )
190143
191144const prompt = ref (' ' );
192145const 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 ' ]);
194147const images = ref ([]);
195148const loading = ref (false );
196149const attachmentFiles = ref <string []>([])
197150
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 );
206151onMounted (async () => {
207152 for (const img of props .images || []) {
208153 images .value .push (img );
209154 }
210155 const temp = await getGenerationPrompt () || ' ' ;
156+ attachmentFiles .value = props .sourceImage || [];
211157 prompt .value = temp [props .fieldName ];
212158 await nextTick ();
213159
214160 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+
232163
233- const context = {
234- field: props .meta .pathColumnLabel ,
235- resource: props .meta .resourceLabel ,
236- };
237164 let template = ' ' ;
238165 if (prompt .value ) {
239166 template = prompt .value ;
240167 } else {
241168 template = ' Generate image for field {{field}} in {{resource}}. No text should be on image.' ;
242169 }
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 ;
277171});
278172
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- }
292173
293174async function confirmImage() {
294175 loading .value = true ;
295176
296- const currentIndex = caurosel .value ?.getActiveItem ()?. position || 0 ;
177+ const currentIndex = sliderRef .value ?.getActiveIndex () || 0 ;
297178 const img = images .value [currentIndex ];
298179
299180 props .images .splice (0 , props .images .length );
@@ -363,7 +244,6 @@ async function generateImages() {
363244 const elapsed = (Date .now () - start ) / 1000 ;
364245 loadingTimer .value = elapsed ;
365246 }, 100 );
366- const currentIndex = caurosel .value ?.getActiveItem ()?.position || 0 ;
367247
368248 await getHistoricalAverage ();
369249 let resp = null ;
@@ -429,6 +309,9 @@ async function generateImages() {
429309 variant: ' danger' ,
430310 timeout: ' unlimited' ,
431311 });
312+ clearInterval (ticker );
313+ loadingTimer .value = null ;
314+ loading .value = false ;
432315 return ;
433316 }
434317
@@ -445,24 +328,8 @@ async function generateImages() {
445328
446329 await nextTick ();
447330
331+ sliderRef .value ?.slideTo (images .value .length - 1 );
448332
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- );
466333 await nextTick ();
467334
468335 loading .value = false ;
0 commit comments