Skip to content

Commit b5d4bd1

Browse files
authored
Merge pull request #3 from devforth/AdminForth/811
Admin forth/811
2 parents c450d80 + 8f127b2 commit b5d4bd1

File tree

7 files changed

+391
-3782
lines changed

7 files changed

+391
-3782
lines changed

custom/ImageGenerationCarousel.vue

Lines changed: 35 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
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 -->
@@ -20,7 +20,7 @@
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
@@ -47,7 +47,7 @@
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
@@ -98,79 +98,30 @@
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';
185136
import { useI18n } from 'vue-i18n';
186137
import adminforth from '@/adminforth';
187138
import { ProgressBar } from '@/afcl';
139+
import Swiper from './Swiper.vue';
188140
189141
const { t: $t } = useI18n();
142+
const sliderRef = ref(null)
190143
191144
const prompt = ref('');
192145
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']);
194147
const images = ref([]);
195148
const loading = ref(false);
196149
const 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);
206151
onMounted(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
293174
async 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;

custom/Swiper.vue

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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>

0 commit comments

Comments
 (0)