fixes
This commit is contained in:
306
src/components/GenerationPreviewModal.vue
Normal file
306
src/components/GenerationPreviewModal.vue
Normal file
@@ -0,0 +1,306 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Button from 'primevue/button'
|
||||
import Tag from 'primevue/tag'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
previewImages: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
initialIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
apiUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'reusePrompt', 'reuseAsset', 'useResultAsAsset'])
|
||||
|
||||
const previewIndex = ref(0)
|
||||
const previewImage = computed(() => props.previewImages[previewIndex.value] || null)
|
||||
|
||||
// Reset index when modal opens
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
previewIndex.value = props.initialIndex
|
||||
window.addEventListener('keydown', handlePreviewKeydown)
|
||||
} else {
|
||||
window.removeEventListener('keydown', handlePreviewKeydown)
|
||||
}
|
||||
})
|
||||
|
||||
const close = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const navigatePreview = (direction) => {
|
||||
const count = props.previewImages.length
|
||||
if (count <= 1) return
|
||||
let newIndex = previewIndex.value + direction
|
||||
if (newIndex < 0) newIndex = count - 1
|
||||
if (newIndex >= count) newIndex = 0
|
||||
previewIndex.value = newIndex
|
||||
}
|
||||
|
||||
const handlePreviewKeydown = (e) => {
|
||||
if (!props.visible) return
|
||||
if (e.key === 'ArrowLeft') { navigatePreview(-1); e.preventDefault() }
|
||||
if (e.key === 'ArrowRight') { navigatePreview(1); e.preventDefault() }
|
||||
if (e.key === 'Escape') { close(); e.preventDefault() }
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handlePreviewKeydown)
|
||||
})
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const copyToClipboard = (text) => {
|
||||
if (!text) return
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
// Parent could show a toast if needed, or we just log
|
||||
console.log('Copied to clipboard')
|
||||
})
|
||||
}
|
||||
|
||||
// Actions
|
||||
const onReusePrompt = () => {
|
||||
if (previewImage.value?.gen) emit('reusePrompt', previewImage.value.gen)
|
||||
}
|
||||
|
||||
const onReuseAsset = () => {
|
||||
if (previewImage.value?.gen) emit('reuseAsset', previewImage.value.gen)
|
||||
}
|
||||
|
||||
const onUseResultAsAsset = () => {
|
||||
if (previewImage.value?.gen) emit('useResultAsAsset', previewImage.value.gen)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :visible="visible" @update:visible="val => emit('update:visible', val)" modal dismissableMask
|
||||
:style="{ width: '95vw', maxWidth: '1200px' }"
|
||||
class="preview-dialog"
|
||||
:pt="{
|
||||
root: { class: '!bg-transparent !border-none !shadow-none' },
|
||||
header: { class: '!hidden' },
|
||||
content: { class: '!p-0 !bg-transparent' }
|
||||
}">
|
||||
|
||||
<div v-if="previewImage" class="flex flex-col lg:flex-row h-[90vh] w-full overflow-hidden relative bg-slate-900/95 backdrop-blur-xl border border-white/10 shadow-2xl rounded-3xl" @click.stop>
|
||||
|
||||
<!-- Left: Image Viewer -->
|
||||
<div class="flex-1 bg-black/40 flex items-center justify-center relative min-h-[40vh] lg:min-h-0 border-r border-white/5">
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<Button v-if="previewImages.length > 1" icon="pi pi-chevron-left" @click.stop="navigatePreview(-1)"
|
||||
rounded text
|
||||
class="!absolute left-4 top-1/2 -translate-y-1/2 z-20 !text-white !bg-black/50 hover:!bg-black/70 !w-12 !h-12 !rounded-full !border !border-white/10 backdrop-blur-md transition-all hover:scale-110" />
|
||||
|
||||
<Button v-if="previewImages.length > 1" icon="pi pi-chevron-right" @click.stop="navigatePreview(1)"
|
||||
rounded text
|
||||
class="!absolute right-4 top-1/2 -translate-y-1/2 z-20 !text-white !bg-black/50 hover:!bg-black/70 !w-12 !h-12 !rounded-full !border !border-white/10 backdrop-blur-md transition-all hover:scale-110" />
|
||||
|
||||
<!-- Main Image -->
|
||||
<img :src="previewImage.url"
|
||||
class="max-w-full max-h-[85vh] object-contain shadow-2xl transition-transform duration-300"
|
||||
draggable="false" />
|
||||
|
||||
<!-- Image Index Badge -->
|
||||
<div v-if="previewImages.length > 1"
|
||||
class="absolute bottom-4 left-1/2 -translate-x-1/2 z-20 bg-black/60 backdrop-blur-md text-white text-xs font-mono px-3 py-1.5 rounded-full border border-white/10">
|
||||
{{ previewIndex + 1 }} / {{ previewImages.length }}
|
||||
</div>
|
||||
|
||||
<!-- Close Button (Mobile Only) -->
|
||||
<Button icon="pi pi-times" @click="close" rounded text
|
||||
class="absolute top-4 right-4 z-30 lg:hidden !text-white !bg-black/50 !w-10 !h-10" />
|
||||
</div>
|
||||
|
||||
<!-- Right: Generation Info & Actions -->
|
||||
<div class="w-full lg:w-96 flex flex-col bg-slate-900/50 backdrop-blur-md overflow-hidden">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="p-5 border-b border-white/10 flex justify-between items-center">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-white m-0">Generation Details</h3>
|
||||
<p class="text-[10px] text-slate-500 font-mono uppercase tracking-widest mt-0.5">
|
||||
{{ previewImage?.gen?.id || previewImage?.gen?._id || 'Unknown ID' }}
|
||||
</p>
|
||||
</div>
|
||||
<Button icon="pi pi-times" @click="close" rounded text
|
||||
class="!hidden lg:!flex !text-slate-500 hover:!text-white !w-8 !h-8" />
|
||||
</div>
|
||||
|
||||
<!-- Scrollable Content -->
|
||||
<div class="flex-1 overflow-y-auto p-5 custom-scrollbar flex flex-col gap-6">
|
||||
|
||||
<!-- Actions Section -->
|
||||
<div v-if="previewImage?.gen" class="flex flex-col gap-2">
|
||||
<label class="text-[10px] font-bold text-slate-500 uppercase tracking-widest px-1">Actions</label>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<Button label="Reuse Prompt" icon="pi pi-refresh" @click="onReusePrompt"
|
||||
class="!bg-violet-600/10 !border-violet-500/30 !text-violet-400 hover:!bg-violet-600/20 !text-sm !justify-start" />
|
||||
|
||||
<Button label="Reuse References" icon="pi pi-clone" @click="onReuseAsset"
|
||||
class="!bg-blue-600/10 !border-blue-500/30 !text-blue-400 hover:!bg-blue-600/20 !text-sm !justify-start" />
|
||||
|
||||
<Button label="Reference Result" icon="pi pi-image" @click="onUseResultAsAsset"
|
||||
class="!bg-emerald-600/10 !border-emerald-500/30 !text-emerald-400 hover:!bg-emerald-600/20 !text-sm !justify-start" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Section -->
|
||||
<div v-if="previewImage?.gen?.prompt" class="flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center px-1">
|
||||
<label class="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Prompt</label>
|
||||
<Button icon="pi pi-copy" @click="copyToClipboard(previewImage.gen.prompt)"
|
||||
text class="!p-0 !w-6 !h-6 !text-slate-500 hover:!text-white" v-tooltip.top="'Copy Prompt'" />
|
||||
</div>
|
||||
<div class="bg-black/30 p-3 rounded-xl border border-white/5 text-sm text-slate-300 leading-relaxed max-h-40 overflow-y-auto custom-scrollbar">
|
||||
{{ previewImage.gen.prompt }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Technical Data -->
|
||||
<div v-if="previewImage?.gen" class="flex flex-col gap-2">
|
||||
<label class="text-[10px] font-bold text-slate-500 uppercase tracking-widest px-1">Technical Data</label>
|
||||
<div class="flex flex-col gap-3 bg-black/20 p-4 rounded-xl border border-white/5">
|
||||
<!-- Grid for main params -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-[10px] text-slate-500 uppercase">Quality</span>
|
||||
<span class="text-xs text-slate-200 font-semibold">{{ previewImage.gen.quality || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-[10px] text-slate-500 uppercase">Aspect Ratio</span>
|
||||
<span class="text-xs text-slate-200 font-semibold">{{ previewImage.gen.aspect_ratio || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-[10px] text-slate-500 uppercase">Execution Time</span>
|
||||
<span class="text-xs text-slate-200 font-semibold">{{ previewImage.gen.execution_time_seconds?.toFixed(2) || '0' }}s (API: {{ previewImage.gen.api_execution_time_seconds?.toFixed(2) || '0' }}s)</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-[10px] text-slate-500 uppercase">Cost</span>
|
||||
<span class="text-xs text-emerald-400 font-bold">${{ previewImage.gen.cost?.toFixed(4) || '0.00' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-white/5 w-full"></div>
|
||||
|
||||
<!-- Tokens Section -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-[10px] text-slate-500 uppercase">Token Usage</span>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="flex flex-col bg-white/5 p-2 rounded-lg items-center">
|
||||
<span class="text-[8px] text-slate-500 uppercase">Input</span>
|
||||
<span class="text-[10px] text-slate-200 font-mono">{{ previewImage.gen.input_token_usage || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col bg-white/5 p-2 rounded-lg items-center">
|
||||
<span class="text-[8px] text-slate-500 uppercase">Output</span>
|
||||
<span class="text-[10px] text-slate-200 font-mono">{{ previewImage.gen.output_token_usage || 0 }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col bg-violet-600/20 p-2 rounded-lg items-center border border-violet-500/20">
|
||||
<span class="text-[8px] text-violet-400 uppercase">Total</span>
|
||||
<span class="text-[10px] text-violet-300 font-bold font-mono">{{ previewImage.gen.token_usage || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-white/5 w-full"></div>
|
||||
|
||||
<!-- Technical Prompt -->
|
||||
<div v-if="previewImage?.gen?.tech_prompt" class="flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-[10px] text-slate-500 uppercase">Technical Prompt</span>
|
||||
<Button icon="pi pi-copy" @click="copyToClipboard(previewImage.gen.tech_prompt)"
|
||||
text class="!p-0 !w-4 !h-4 !text-slate-500 hover:!text-white" />
|
||||
</div>
|
||||
<div class="bg-black/40 p-2 rounded-lg border border-white/5 text-[10px] font-mono text-slate-400 leading-relaxed max-h-24 overflow-y-auto custom-scrollbar whitespace-pre-wrap">
|
||||
{{ previewImage.gen.tech_prompt }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-white/5 w-full"></div>
|
||||
|
||||
<!-- Metadata -->
|
||||
<div class="flex flex-col gap-1.5 text-[10px]">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-slate-500 uppercase">Created:</span>
|
||||
<span class="text-slate-300">{{ formatDate(previewImage.gen.created_at) }}</span>
|
||||
</div>
|
||||
<div v-if="previewImage.gen.generation_group_id" class="flex justify-between">
|
||||
<span class="text-slate-500 uppercase">Group ID:</span>
|
||||
<span class="text-slate-300 font-mono">{{ previewImage.gen.generation_group_id }}</span>
|
||||
</div>
|
||||
<div v-if="previewImage.gen.idea_id" class="flex justify-between">
|
||||
<span class="text-slate-500 uppercase">Idea ID:</span>
|
||||
<span class="text-slate-300 font-mono">{{ previewImage.gen.idea_id }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-slate-500 uppercase">Status:</span>
|
||||
<Tag :value="previewImage.gen.status"
|
||||
:severity="previewImage.gen.status === 'done' ? 'success' : (previewImage.gen.status === 'failed' ? 'danger' : 'info')"
|
||||
class="!text-[8px] !px-1.5 !py-0 w-fit h-4" />
|
||||
</div>
|
||||
<div v-if="previewImage.gen.failed_reason" class="flex flex-col gap-1 mt-1 p-2 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400">
|
||||
<span class="uppercase font-bold">Error:</span>
|
||||
<span class="italic">{{ previewImage.gen.failed_reason }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- References Used -->
|
||||
<div v-if="previewImage?.gen?.assets_list?.length > 0" class="flex flex-col gap-2">
|
||||
<label class="text-[10px] font-bold text-slate-500 uppercase tracking-widest px-1">References Used</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div v-for="assetId in previewImage.gen.assets_list" :key="assetId"
|
||||
class="w-10 h-10 rounded-lg overflow-hidden border border-white/10 shadow-lg">
|
||||
<img :src="apiUrl + '/assets/' + assetId + '?thumbnail=true'"
|
||||
class="w-full h-full object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
@@ -519,10 +519,16 @@ const qualityOptions = ref([{
|
||||
}])
|
||||
const aspectRatio = ref({ key: "NINESIXTEEN", value: "9:16" })
|
||||
const aspectRatioOptions = ref([
|
||||
{ key: "NINESIXTEEN", value: "9:16" },
|
||||
{ key: "FOURTHREE", value: "4:3" },
|
||||
{ key: "ONEONE", value: "1:1" },
|
||||
{ key: "TWOTHREE", value: "2:3" },
|
||||
{ key: "THREETWO", value: "3:2" },
|
||||
{ key: "THREEFOUR", value: "3:4" },
|
||||
{ key: "SIXTEENNINE", value: "16:9" }
|
||||
{ key: "FOURTHREE", value: "4:3" },
|
||||
{ key: "FOURFIVE", value: "4:5" },
|
||||
{ key: "FIVEFOUR", value: "5:4" },
|
||||
{ key: "NINESIXTEEN", value: "9:16" },
|
||||
{ key: "SIXTEENNINE", value: "16:9" },
|
||||
{ key: "TWENTYONENINE", value: "21:9" }
|
||||
])
|
||||
|
||||
const assetsFirst = ref(0)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { dataService } from '../services/dataService'
|
||||
import { aiService } from '../services/aiService'
|
||||
import { postService } from '../services/postService'
|
||||
import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {dataService} from '../services/dataService'
|
||||
import {aiService} from '../services/aiService'
|
||||
import {postService} from '../services/postService'
|
||||
import Button from 'primevue/button'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import InputText from 'primevue/inputtext'
|
||||
@@ -11,15 +11,13 @@ import Dialog from 'primevue/dialog'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
import DatePicker from 'primevue/datepicker'
|
||||
import MultiSelect from 'primevue/multiselect'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import ProgressBar from 'primevue/progressbar'
|
||||
import Message from 'primevue/message'
|
||||
import Tag from 'primevue/tag'
|
||||
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import { useAlbumStore } from '../stores/albums'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import {useAlbumStore} from '../stores/albums'
|
||||
import {useToast} from 'primevue/usetoast'
|
||||
import Toast from 'primevue/toast'
|
||||
import GenerationPreviewModal from '../components/GenerationPreviewModal.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const API_URL = import.meta.env.VITE_API_URL
|
||||
@@ -211,10 +209,16 @@ const qualityOptions = ref([
|
||||
{ key: 'FOURK', value: '4K' }
|
||||
])
|
||||
const aspectRatioOptions = ref([
|
||||
{ key: "NINESIXTEEN", value: "9:16" },
|
||||
{ key: "FOURTHREE", value: "4:3" },
|
||||
{ key: "ONEONE", value: "1:1" },
|
||||
{ key: "TWOTHREE", value: "2:3" },
|
||||
{ key: "THREETWO", value: "3:2" },
|
||||
{ key: "THREEFOUR", value: "3:4" },
|
||||
{ key: "SIXTEENNINE", value: "16:9" }
|
||||
{ key: "FOURTHREE", value: "4:3" },
|
||||
{ key: "FOURFIVE", value: "4:5" },
|
||||
{ key: "FIVEFOUR", value: "5:4" },
|
||||
{ key: "NINESIXTEEN", value: "9:16" },
|
||||
{ key: "SIXTEENNINE", value: "16:9" },
|
||||
{ key: "TWENTYONENINE", value: "21:9" }
|
||||
])
|
||||
|
||||
// --- Persistence ---
|
||||
@@ -589,7 +593,8 @@ const allPreviewImages = computed(() => {
|
||||
images.push({
|
||||
url: API_URL + '/assets/' + assetId,
|
||||
genId: gen.id,
|
||||
prompt: gen.prompt
|
||||
prompt: gen.prompt,
|
||||
gen: gen
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -598,48 +603,11 @@ const allPreviewImages = computed(() => {
|
||||
})
|
||||
|
||||
const openImagePreview = (url) => {
|
||||
// Find index of this image in the flat list
|
||||
const idx = allPreviewImages.value.findIndex(img => img.url === url)
|
||||
previewIndex.value = idx >= 0 ? idx : 0
|
||||
previewImage.value = allPreviewImages.value[previewIndex.value] || { url }
|
||||
isImagePreviewVisible.value = true
|
||||
}
|
||||
|
||||
const navigatePreview = (direction) => {
|
||||
const images = allPreviewImages.value
|
||||
if (images.length === 0) return
|
||||
let newIndex = previewIndex.value + direction
|
||||
if (newIndex < 0) newIndex = images.length - 1
|
||||
if (newIndex >= images.length) newIndex = 0
|
||||
previewIndex.value = newIndex
|
||||
previewImage.value = images[newIndex]
|
||||
}
|
||||
|
||||
const onPreviewKeydown = (e) => {
|
||||
if (!isImagePreviewVisible.value) return
|
||||
if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault()
|
||||
navigatePreview(-1)
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
e.preventDefault()
|
||||
navigatePreview(1)
|
||||
} else if (e.key === 'Escape') {
|
||||
isImagePreviewVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(isImagePreviewVisible, (visible) => {
|
||||
if (visible) {
|
||||
window.addEventListener('keydown', onPreviewKeydown)
|
||||
} else {
|
||||
window.removeEventListener('keydown', onPreviewKeydown)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', onPreviewKeydown)
|
||||
})
|
||||
|
||||
const reusePrompt = (gen) => {
|
||||
if (gen.prompt) {
|
||||
prompt.value = gen.prompt
|
||||
@@ -1392,44 +1360,15 @@ const confirmAddToAlbum = async () => {
|
||||
|
||||
|
||||
|
||||
<Dialog v-model:visible="isImagePreviewVisible" modal dismissableMask
|
||||
:style="{ width: '95vw', maxWidth: '1100px', background: 'transparent', boxShadow: 'none' }"
|
||||
:pt="{ root: { class: '!bg-transparent !border-none !shadow-none' }, header: { class: '!hidden' }, content: { class: '!bg-transparent !p-0' } }">
|
||||
<div class="relative flex items-center justify-center" @click.self="isImagePreviewVisible = false">
|
||||
|
||||
<!-- Previous Button -->
|
||||
<Button v-if="allPreviewImages.length > 1" icon="pi pi-chevron-left" @click.stop="navigatePreview(-1)"
|
||||
rounded text
|
||||
class="!absolute left-2 top-1/2 -translate-y-1/2 z-20 !text-white !bg-black/50 hover:!bg-black/70 !w-12 !h-12 !rounded-full !border !border-white/20 backdrop-blur-sm transition-all hover:!scale-110" />
|
||||
|
||||
<!-- Image -->
|
||||
<img v-if="previewImage" :src="previewImage.url"
|
||||
class="max-w-full max-h-[85vh] object-contain rounded-xl shadow-2xl select-none"
|
||||
draggable="false" />
|
||||
|
||||
<!-- Next Button -->
|
||||
<Button v-if="allPreviewImages.length > 1" icon="pi pi-chevron-right" @click.stop="navigatePreview(1)"
|
||||
rounded text
|
||||
class="!absolute right-2 top-1/2 -translate-y-1/2 z-20 !text-white !bg-black/50 hover:!bg-black/70 !w-12 !h-12 !rounded-full !border !border-white/20 backdrop-blur-sm transition-all hover:!scale-110" />
|
||||
|
||||
<!-- Close Button -->
|
||||
<Button icon="pi pi-times" @click="isImagePreviewVisible = false" rounded text
|
||||
class="!absolute -top-4 -right-4 z-20 !text-white !bg-black/50 hover:!bg-black/70 !w-10 !h-10" />
|
||||
|
||||
<!-- Counter -->
|
||||
<div v-if="allPreviewImages.length > 1"
|
||||
class="absolute bottom-4 left-1/2 -translate-x-1/2 z-20 bg-black/60 backdrop-blur-sm text-white text-sm font-mono px-4 py-1.5 rounded-full border border-white/10">
|
||||
{{ previewIndex + 1 }} / {{ allPreviewImages.length }}
|
||||
</div>
|
||||
|
||||
<!-- Prompt (click to copy) -->
|
||||
<div v-if="previewImage?.prompt"
|
||||
class="absolute bottom-14 left-1/2 -translate-x-1/2 z-20 bg-black/60 backdrop-blur-sm text-white/80 text-xs px-4 py-2 rounded-xl border border-white/10 max-w-md text-center line-clamp-2 cursor-pointer hover:bg-black/80 hover:border-white/20 transition-all"
|
||||
v-tooltip.top="'Click to copy'" @click.stop="navigator.clipboard.writeText(previewImage.prompt)">
|
||||
{{ previewImage.prompt }}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
<GenerationPreviewModal
|
||||
v-model:visible="isImagePreviewVisible"
|
||||
:preview-images="allPreviewImages"
|
||||
:initial-index="previewIndex"
|
||||
:api-url="API_URL"
|
||||
@reuse-prompt="reusePrompt"
|
||||
@reuse-asset="reuseAsset"
|
||||
@use-result-as-asset="useResultAsAsset"
|
||||
/>
|
||||
|
||||
<Dialog v-model:visible="isAssetPickerVisible" modal header="Select Assets"
|
||||
:style="{ width: '80vw', maxWidth: '900px' }"
|
||||
|
||||
@@ -26,6 +26,8 @@ import FileUpload from 'primevue/fileupload'
|
||||
import ConfirmDialog from 'primevue/confirmdialog'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import DatePicker from 'primevue/datepicker'
|
||||
import Tag from 'primevue/tag'
|
||||
import GenerationPreviewModal from '../components/GenerationPreviewModal.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -188,10 +190,16 @@ const qualityOptions = ref([
|
||||
{ key: 'FOURK', value: '4K' }
|
||||
])
|
||||
const aspectRatioOptions = ref([
|
||||
{ key: "NINESIXTEEN", value: "9:16" },
|
||||
{ key: "FOURTHREE", value: "4:3" },
|
||||
{ key: "ONEONE", value: "1:1" },
|
||||
{ key: "TWOTHREE", value: "2:3" },
|
||||
{ key: "THREETWO", value: "3:2" },
|
||||
{ key: "THREEFOUR", value: "3:4" },
|
||||
{ key: "SIXTEENNINE", value: "16:9" }
|
||||
{ key: "FOURTHREE", value: "4:3" },
|
||||
{ key: "FOURFIVE", value: "4:5" },
|
||||
{ key: "FIVEFOUR", value: "5:4" },
|
||||
{ key: "NINESIXTEEN", value: "9:16" },
|
||||
{ key: "SIXTEENNINE", value: "16:9" },
|
||||
{ key: "TWENTYONENINE", value: "21:9" }
|
||||
])
|
||||
|
||||
// Removed duplicate characters ref
|
||||
@@ -594,29 +602,13 @@ const useResultAsAsset = (gen) => {
|
||||
const isImagePreviewVisible = ref(false)
|
||||
const previewImages = ref([])
|
||||
const previewIndex = ref(0)
|
||||
|
||||
const openImagePreview = (imageList, startIdx = 0) => {
|
||||
// imageList should now be [{url, gen}]
|
||||
previewImages.value = imageList
|
||||
previewIndex.value = startIdx
|
||||
isImagePreviewVisible.value = true
|
||||
}
|
||||
const prevPreview = () => {
|
||||
if (previewIndex.value > 0) previewIndex.value--
|
||||
}
|
||||
const nextPreview = () => {
|
||||
if (previewIndex.value < previewImages.value.length - 1) previewIndex.value++
|
||||
}
|
||||
|
||||
// Global keyboard nav for preview modal (Safari doesn't focus Dialog)
|
||||
const handlePreviewKeydown = (e) => {
|
||||
if (e.key === 'ArrowLeft') { prevPreview(); e.preventDefault() }
|
||||
if (e.key === 'ArrowRight') { nextPreview(); e.preventDefault() }
|
||||
if (e.key === 'Escape') { isImagePreviewVisible.value = false; e.preventDefault() }
|
||||
}
|
||||
watch(isImagePreviewVisible, (visible) => {
|
||||
if (visible) window.addEventListener('keydown', handlePreviewKeydown)
|
||||
else window.removeEventListener('keydown', handlePreviewKeydown)
|
||||
})
|
||||
onUnmounted(() => window.removeEventListener('keydown', handlePreviewKeydown))
|
||||
|
||||
// --- Computeds ---
|
||||
const groupedGenerations = computed(() => {
|
||||
@@ -1021,7 +1013,7 @@ watch(viewMode, (v) => {
|
||||
class="relative group/img cursor-pointer aspect-[4/3]">
|
||||
<img :src="API_URL + '/assets/' + res + '?thumbnail=true'"
|
||||
class="w-full h-full object-cover"
|
||||
@click="openImagePreview(gen.result_list.map(r => API_URL + '/assets/' + r), resIdx)" />
|
||||
@click="openImagePreview(gen.result_list.map(r => ({ url: API_URL + '/assets/' + r, gen: gen })), resIdx)" />
|
||||
<!-- Per-image hover overlay -->
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover/img:opacity-100 transition-opacity duration-200 pointer-events-none">
|
||||
@@ -1100,7 +1092,7 @@ watch(viewMode, (v) => {
|
||||
<div v-for="(img, idx) in allGalleryImages" :key="img.assetId"
|
||||
class="aspect-[2/3] relative rounded-xl overflow-hidden group bg-slate-800 border-2 transition-all cursor-pointer"
|
||||
:class="isSelectMode && selectedAssetIds.has(img.assetId) ? 'border-violet-500 ring-2 ring-violet-500/30' : 'border-white/5 hover:border-violet-500/50'"
|
||||
@click="isSelectMode ? toggleImageSelection(img.assetId) : openImagePreview(allGalleryImages.map(i => i.url), idx)">
|
||||
@click="isSelectMode ? toggleImageSelection(img.assetId) : openImagePreview(allGalleryImages, idx)">
|
||||
|
||||
<img :src="img.thumbnailUrl" class="w-full h-full object-cover" />
|
||||
|
||||
@@ -1422,45 +1414,15 @@ watch(viewMode, (v) => {
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- Preview Modal with Slider -->
|
||||
<Dialog v-model:visible="isImagePreviewVisible" modal dismissableMask
|
||||
:style="{ width: '90vw', maxWidth: '1200px', background: 'transparent', boxShadow: 'none' }"
|
||||
:pt="{ root: { class: '!bg-transparent !border-none !shadow-none' }, header: { class: '!hidden' }, content: { class: '!bg-transparent !p-0' } }"
|
||||
@keydown.left.prevent="prevPreview" @keydown.right.prevent="nextPreview">
|
||||
<div class="relative flex items-center justify-center h-[90vh]" @click="isImagePreviewVisible = false">
|
||||
<!-- Main image -->
|
||||
<img v-if="previewImages.length > 0" :src="previewImages[previewIndex]"
|
||||
class="max-w-full max-h-full object-contain rounded-xl shadow-2xl" @click.stop />
|
||||
|
||||
<!-- Prev arrow -->
|
||||
<button v-if="previewImages.length > 1 && previewIndex > 0" @click.stop="prevPreview"
|
||||
class="absolute left-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-black/60 hover:bg-black/80 backdrop-blur-sm flex items-center justify-center text-white transition-all shadow-lg z-20">
|
||||
<i class="pi pi-chevron-left" style="font-size: 16px"></i>
|
||||
</button>
|
||||
|
||||
<!-- Next arrow -->
|
||||
<button v-if="previewImages.length > 1 && previewIndex < previewImages.length - 1"
|
||||
@click.stop="nextPreview"
|
||||
class="absolute right-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-black/60 hover:bg-black/80 backdrop-blur-sm flex items-center justify-center text-white transition-all shadow-lg z-20">
|
||||
<i class="pi pi-chevron-right" style="font-size: 16px"></i>
|
||||
</button>
|
||||
|
||||
<!-- Counter badge -->
|
||||
<div v-if="previewImages.length > 1"
|
||||
class="absolute top-4 right-4 bg-black/60 backdrop-blur-sm text-white text-sm font-bold px-3 py-1 rounded-full z-20">
|
||||
{{ previewIndex + 1 }} / {{ previewImages.length }}
|
||||
</div>
|
||||
|
||||
<!-- Dot indicators -->
|
||||
<div v-if="previewImages.length > 1"
|
||||
class="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2 z-20">
|
||||
<button v-for="(img, idx) in previewImages" :key="'prev-dot-' + idx"
|
||||
@click.stop="previewIndex = idx" class="w-2.5 h-2.5 rounded-full transition-all"
|
||||
:class="previewIndex === idx ? 'bg-white scale-125' : 'bg-white/40 hover:bg-white/60'">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
<GenerationPreviewModal
|
||||
v-model:visible="isImagePreviewVisible"
|
||||
:preview-images="previewImages"
|
||||
:initial-index="previewIndex"
|
||||
:api-url="dataService.API_URL || API_URL"
|
||||
@reuse-prompt="reusePrompt"
|
||||
@reuse-asset="reuseAssets"
|
||||
@use-result-as-asset="useResultAsAsset"
|
||||
/>
|
||||
|
||||
<!-- Add to Content Plan Dialog -->
|
||||
<Dialog v-model:visible="showAddToPlanDialog" header="Add to content plan" modal :style="{ width: '420px' }"
|
||||
|
||||
@@ -107,10 +107,16 @@ const qualityOptions = ref([
|
||||
{ key: 'FOURK', value: '4K' }
|
||||
])
|
||||
const aspectRatioOptions = ref([
|
||||
{ key: "NINESIXTEEN", value: "9:16" },
|
||||
{ key: "FOURTHREE", value: "4:3" },
|
||||
{ key: "ONEONE", value: "1:1" },
|
||||
{ key: "TWOTHREE", value: "2:3" },
|
||||
{ key: "THREETWO", value: "3:2" },
|
||||
{ key: "THREEFOUR", value: "3:4" },
|
||||
{ key: "SIXTEENNINE", value: "16:9" }
|
||||
{ key: "FOURTHREE", value: "4:3" },
|
||||
{ key: "FOURFIVE", value: "4:5" },
|
||||
{ key: "FIVEFOUR", value: "5:4" },
|
||||
{ key: "NINESIXTEEN", value: "9:16" },
|
||||
{ key: "SIXTEENNINE", value: "16:9" },
|
||||
{ key: "TWENTYONENINE", value: "21:9" }
|
||||
])
|
||||
|
||||
// Restore settings
|
||||
|
||||
@@ -11,6 +11,8 @@ import Checkbox from 'primevue/checkbox'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Paginator from 'primevue/paginator'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Tag from 'primevue/tag'
|
||||
import GenerationPreviewModal from '../components/GenerationPreviewModal.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const API_URL = import.meta.env.VITE_API_URL
|
||||
@@ -64,10 +66,16 @@ const qualityOptions = ref([
|
||||
|
||||
const aspectRatio = ref({ key: "NINESIXTEEN", value: "9:16" })
|
||||
const aspectRatioOptions = ref([
|
||||
{ key: "NINESIXTEEN", value: "9:16" },
|
||||
{ key: "FOURTHREE", value: "4:3" },
|
||||
{ key: "ONEONE", value: "1:1" },
|
||||
{ key: "TWOTHREE", value: "2:3" },
|
||||
{ key: "THREETWO", value: "3:2" },
|
||||
{ key: "THREEFOUR", value: "3:4" },
|
||||
{ key: "SIXTEENNINE", value: "16:9" }
|
||||
{ key: "FOURTHREE", value: "4:3" },
|
||||
{ key: "FOURFIVE", value: "4:5" },
|
||||
{ key: "FIVEFOUR", value: "5:4" },
|
||||
{ key: "NINESIXTEEN", value: "9:16" },
|
||||
{ key: "SIXTEENNINE", value: "16:9" },
|
||||
{ key: "TWENTYONENINE", value: "21:9" }
|
||||
])
|
||||
|
||||
// --- Data Loading ---
|
||||
@@ -293,23 +301,16 @@ const restoreGeneration = async (gen) => {
|
||||
if (foundAspect) aspectRatio.value = foundAspect
|
||||
|
||||
if (gen.status === 'done' && gen.result_list && gen.result_list.length > 0) {
|
||||
// We need to fetch details or just display the image
|
||||
// history list usually has the main image preview
|
||||
// Keep original gen object for preview and details
|
||||
generatedResult.value = {
|
||||
...gen,
|
||||
type: 'assets',
|
||||
// Mocking asset object structure from history usage in DetailView
|
||||
assets: gen.result_list.map(id => ({
|
||||
id,
|
||||
url: `/assets/${id}`, // This might need adjustment based on how API serves files
|
||||
// Ideally history API should return full asset objects or URLs.
|
||||
// If not, we rely on the implementation in CharacterDetailView:
|
||||
// :src="API_URL + '/assets/' + gen.result_list[0]"
|
||||
// So let's construct it similarly
|
||||
url: `/assets/${id}`,
|
||||
})),
|
||||
tech_prompt: gen.tech_prompt,
|
||||
execution_time: gen.execution_time_seconds,
|
||||
api_execution_time: gen.api_execution_time_seconds,
|
||||
token_usage: gen.token_usage
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,16 +338,11 @@ const handleImprovePrompt = async () => {
|
||||
const isImagePreviewVisible = ref(false)
|
||||
const previewImage = ref(null)
|
||||
|
||||
const openImagePreview = (url, name = 'Image Preview', createdAt = null) => {
|
||||
previewImage.value = { url, name, createdAt }
|
||||
const openImagePreview = (url, name = 'Image Preview', createdAt = null, gen = null) => {
|
||||
previewImage.value = { url, name, createdAt, gen }
|
||||
isImagePreviewVisible.value = true
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
return new Date(dateString).toLocaleString()
|
||||
}
|
||||
|
||||
const undoImprovePrompt = () => {
|
||||
if (previousPrompt.value) {
|
||||
const temp = prompt.value
|
||||
@@ -419,10 +415,6 @@ const useResultAsReference = (gen) => {
|
||||
|
||||
// --- Utils ---
|
||||
|
||||
const copyToClipboard = () => {
|
||||
// Implement if needed for prompt copying
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- Lifecycle ---
|
||||
@@ -604,7 +596,7 @@ onMounted(() => {
|
||||
v-if="generatedResult.type === 'assets' && generatedResult.assets && generatedResult.assets.length > 0">
|
||||
<!-- Displaying the first asset as main preview -->
|
||||
<img :src="API_URL + '/assets/' + generatedResult.assets[0].id"
|
||||
@click="openImagePreview(API_URL + '/assets/' + generatedResult.assets[0].id, 'Generated Result', new Date().toISOString())"
|
||||
@click="openImagePreview(API_URL + '/assets/' + generatedResult.assets[0].id, 'Generated Result', new Date().toISOString(), generatedResult)"
|
||||
class="w-full h-full object-contain cursor-pointer hover:scale-[1.01] transition-transform duration-300" />
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -757,18 +749,15 @@ onMounted(() => {
|
||||
</Dialog>
|
||||
|
||||
<!-- Image Preview Modal -->
|
||||
<Dialog v-model:visible="isImagePreviewVisible" modal dismissableMask
|
||||
:header="previewImage?.name || 'Image Preview'" :style="{ width: '90vw', maxWidth: '800px' }"
|
||||
class="glass-panel rounded-2xl">
|
||||
<div v-if="previewImage" class="flex flex-col items-center">
|
||||
<img :src="previewImage.url" :alt="previewImage.name"
|
||||
class="max-w-full max-h-[70vh] rounded-xl object-contain shadow-2xl" />
|
||||
<div class="mt-6 text-center">
|
||||
<h2 class="text-2xl font-bold mb-2">{{ previewImage.name }}</h2>
|
||||
<p v-if="previewImage.createdAt" class="text-slate-400">{{ formatDate(previewImage.createdAt) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
<GenerationPreviewModal
|
||||
v-model:visible="isImagePreviewVisible"
|
||||
:preview-images="[previewImage].filter(i => !!i)"
|
||||
:initial-index="0"
|
||||
:api-url="API_URL"
|
||||
@reuse-prompt="reusePrompt"
|
||||
@reuse-asset="reuseAsset"
|
||||
@use-result-as-asset="useResultAsReference"
|
||||
/>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user