Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 35 additions & 168 deletions custom/ImageGenerationCarousel.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@

<template>
<!-- Main modal -->
<div tabindex="-1" class="fixed inset-0 z-10 flex justify-center items-center dark:bg-gray-900/50 overflow-y-auto">
<div class="relative p-4 w-full max-w-[1600px] max-h-[90vh] ">
<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">
<div class="relative p-4 w-full max-w-[1600px]">
<!-- Modal content -->
<div class="relative bg-white rounded-lg shadow-xl dark:bg-gray-700">
<!-- Modal header -->
Expand All @@ -20,7 +20,7 @@
</button>
</div>
<!-- Modal body -->
<div class="p-4 md:p-5 space-y-4">
<div class="p-4 md:p-5">
<!-- PROMPT TEXTAREA -->
<!-- Textarea -->
<textarea
Expand All @@ -47,7 +47,7 @@
<!-- Fullscreen Modal -->
<div
v-if="zoomedImage"
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-80"
class="w-full h-full fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-80"
@click.self="closeZoom"
>
<img
Expand Down Expand Up @@ -98,79 +98,30 @@
<div id="gallery" class="relative w-full min-w-0" data-carousel="static">
<!-- Carousel wrapper -->
<div class="relative h-56 overflow-hidden rounded-lg md:h-[calc(100vh-400px)]">
<!-- Item 1 -->
<div
v-for="(img, index) in images"
:key="index"
class="flex items-center justify-center w-full h-full"
:class="[
index === 0 ? 'block' : 'hidden'
]"
data-carousel-item
>
<img :src="img" class="max-w-full max-h-full object-contain"
:alt="`Generated image ${index + 1}`"
/>
</div>

<div v-if="images.length === 0" class="flex items-center justify-center w-full h-full">

<button @click="generateImages" type="button" 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
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 ms-2">{{ $t('Generate images') }}</button>

</div>

<Swiper
ref="sliderRef"
:images="images"
/>
</div>
<!-- Slider controls -->
<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"
@click="slide(-1)"
:disabled="images.length === 0"
>
<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 ">
<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"
:class="{
'text-gray-800 dark:text-gray-200': images.length > 0,
'text-gray-200 dark:text-gray-800': images.length === 0
}"
>
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4"/>
</svg>
<span class="sr-only">{{ $t('Previous') }}</span>
</span>
</button>
<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 "
:disabled="images.length === 0"
@click="slide(1)"
>
<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 ">
<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"
:class="{
'text-gray-800 dark:text-gray-200': images.length > 0,
'text-gray-200 dark:text-gray-800': images.length === 0
}"
>
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/>
</svg>
<span class="sr-only">{{ $t('Next') }}</span>
</span>
</button>


</div>
</div>
</div>
<!-- Modal footer -->
<div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
<button type="button" @click="confirmImage"
:disabled="loading || images.length === 0"
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
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
disabled:opacity-50 disabled:cursor-not-allowed"
>{{ $t('Use image') }}</button>
<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"
@click="emit('close')"
>{{ $t('Cancel') }}</button>
<div class="flex justify-between p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600 gap-3">
<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"
@click="generateImages"
>{{ $t('Regenerate') }}</button>
<div class="flex gap-3">
<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"
@click="emit('close')"
>{{ $t('Cancel') }}</button>
<button type="button" @click="confirmImage"
:disabled="loading || images.length === 0"
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
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
disabled:opacity-50 disabled:cursor-not-allowed"
>{{ $t('Use image') }}</button>
</div>
</div>
</div>
</div>
Expand All @@ -185,115 +136,45 @@ import { callAdminForthApi } from '@/utils';
import { useI18n } from 'vue-i18n';
import adminforth from '@/adminforth';
import { ProgressBar } from '@/afcl';
import Swiper from './Swiper.vue';

const { t: $t } = useI18n();
const sliderRef = ref(null)

const prompt = ref('');
const emit = defineEmits(['close', 'selectImage', 'error', 'updateCarouselIndex']);
const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex', 'regenerateImagesRefreshRate']);
const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex', 'regenerateImagesRefreshRate','sourceImage']);
const images = ref([]);
const loading = ref(false);
const attachmentFiles = ref<string[]>([])

function minifyField(field: string): string {
if (field.length > 100) {
return field.slice(0, 100) + '...';
}
return field;
}

const caurosel = ref(null);
onMounted(async () => {
for (const img of props.images || []) {
images.value.push(img);
}
const temp = await getGenerationPrompt() || '';
attachmentFiles.value = props.sourceImage || [];
prompt.value = temp[props.fieldName];
await nextTick();

const currentIndex = props.carouselImageIndex || 0;
caurosel.value = new Carousel(
document.getElementById('gallery'),
images.value.map((img, index) => {
return {
image: img,
el: document.getElementById('gallery').querySelector(`[data-carousel-item]:nth-child(${index + 1})`),
position: index,
};
}),
{
internal: 0,
defaultPosition: currentIndex,
},
{
override: true,
}
);
sliderRef.value?.slideTo(currentIndex);


const context = {
field: props.meta.pathColumnLabel,
resource: props.meta.resourceLabel,
};
let template = '';
if (prompt.value) {
template = prompt.value;
} else {
template = 'Generate image for field {{field}} in {{resource}}. No text should be on image.';
}
// iterate over all variables in template and replace them with their values from props.record[field].
// if field is not present in props.record[field] then replace it with empty string and drop warning
const regex = /{{(.*?)}}/g;
const matches = template.match(regex);
if (matches) {
matches.forEach((match) => {
const field = match.replace(/{{|}}/g, '').trim();
if (field in context) {
return;
} else if (field in props.record) {
context[field] = minifyField(props.record[field]);
} else {
adminforth.alert({
message: $t('Field {{field}} defined in template but not found in record', { field }),
variant: 'warning',
timeout: 15,
});
}
});
}

prompt.value = template.replace(regex, (_, field) => {
return context[field.trim()] || '';
});

const recordId = props.record[props.meta.recorPkFieldName];
if (!recordId) {
emit('error', {
isError: true,
errorMessage: 'Record ID not found, cannot generate images'
});
return;
}

prompt.value = template;
});

async function slide(direction: number) {
if (!caurosel.value) return;
const curPos = caurosel.value.getActiveItem().position;
if (curPos === 0 && direction === -1) return;
if (curPos === images.value.length - 1 && direction === 1) {
await generateImages();
}
if (direction === 1) {
caurosel.value.next();
} else {
caurosel.value.prev();
}
}

async function confirmImage() {
loading.value = true;

const currentIndex = caurosel.value?.getActiveItem()?.position || 0;
const currentIndex = sliderRef.value?.getActiveIndex() || 0;
const img = images.value[currentIndex];

props.images.splice(0, props.images.length);
Expand Down Expand Up @@ -363,7 +244,6 @@ async function generateImages() {
const elapsed = (Date.now() - start) / 1000;
loadingTimer.value = elapsed;
}, 100);
const currentIndex = caurosel.value?.getActiveItem()?.position || 0;

await getHistoricalAverage();
let resp = null;
Expand Down Expand Up @@ -429,6 +309,9 @@ async function generateImages() {
variant: 'danger',
timeout: 'unlimited',
});
clearInterval(ticker);
loadingTimer.value = null;
loading.value = false;
return;
}

Expand All @@ -445,24 +328,8 @@ async function generateImages() {

await nextTick();

sliderRef.value?.slideTo(images.value.length-1);

caurosel.value = new Carousel(
document.getElementById('gallery'),
images.value.map((img, index) => {
return {
image: img,
el: document.getElementById('gallery').querySelector(`[data-carousel-item]:nth-child(${index + 1})`),
position: index,
};
}),
{
internal: 0,
defaultPosition: currentIndex,
},
{
override: true,
}
);
await nextTick();

loading.value = false;
Expand Down
76 changes: 76 additions & 0 deletions custom/Swiper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<template>
<swiper-container class="flex items-center justify-center w-full h-full">
<swiper-slide v-for="(image, index) in images" :key="index">
<img :src="image" class="object-contain w-full h-full" />
</swiper-slide>
</swiper-container>
</template>

<script setup lang="ts">
import { onMounted } from 'vue'
import { register } from 'swiper/element/bundle'
import { SwiperOptions } from 'swiper/types';

const props = defineProps<{images: string[]}>()
let swiperEl: any;

function getActiveIndex() {
if (swiperEl && swiperEl.swiper) {
return swiperEl.swiper.activeIndex;
}
return 0;
}

function slideTo(index) {

if (!swiperEl || !swiperEl.swiper) {
setTimeout(() => slideTo(index), 50);
return;
}

if (index >= 0 && index < props.images.length) {
swiperEl.swiper.update();
setTimeout(() => {
swiperEl.swiper.slideTo(index, 300);
}, 10);
}
}

defineExpose({
getActiveIndex,
slideTo
})

register()
onMounted(() => {
swiperEl = document.querySelector('swiper-container')

const swiperParams: SwiperOptions = {
slidesPerView: 1,
navigation: true,
pagination: {
type: 'fraction',
},
allowTouchMove: true,
}

Object.assign(swiperEl, swiperParams)
swiperEl.initialize()
})
</script>

<style>
.swiper {
width: 100%;
height: 100%;
}

.swiper-slide {
text-align: center;
font-size: 18px;
background: #444;
display: flex;
justify-content: center;
align-items: center;
}
</style>
Loading