Compare commits

4 Commits

Author SHA1 Message Date
xds
a737c38c53 models 2026-02-27 20:36:59 +03:00
xds
2ed2ee2937 likes 2026-02-27 13:51:14 +03:00
xds
4f9807cfe7 likes 2026-02-26 12:30:58 +03:00
xds
f89548b363 likes 2026-02-26 11:35:01 +03:00
8 changed files with 336 additions and 271 deletions

View File

@@ -29,12 +29,12 @@ const props = defineProps({
}
})
const emit = defineEmits(['toggle-select', 'open-preview', 'toggle-like', 'delete', 'reuse-prompt', 'reuse-asset', 'use-result', 'toggle-overlay'])
const emit = defineEmits(['toggle-select', 'open-preview', 'toggle-like', 'delete', 'reuse-prompt', 'reuse-asset', 'use-result', 'toggle-overlay', 'mark-nsfw'])
const isTemporarilyUnblurred = ref(false)
const isBlurred = computed(() => {
return props.generation.nsfw && !props.showNsfwGlobal && !isTemporarilyUnblurred.value
return (props.generation.is_nsfw || props.generation.nsfw) && !props.showNsfwGlobal && !isTemporarilyUnblurred.value
})
const toggleBlur = () => {
@@ -46,14 +46,6 @@ const handleImageClick = (e) => {
emit('toggle-select', props.generation.result_list[0])
} else {
if (isBlurred.value) {
// If blurred, click might just unblur or do nothing?
// Let's let the button handle unblur, and click opens preview if unblurred?
// Or maybe click unblurs? Let's stick to button for unblur to be explicit.
// But if user clicks image, maybe show preview anyway?
// Usually blurred images shouldn't be previewed full size unless unblurred.
// Let's allow preview, but maybe preview also needs to handle blur?
// For now, let's just open preview. The preview modal might need its own blur logic or just show it.
// Let's assume preview shows it.
emit('open-preview', props.apiUrl + '/assets/' + props.generation.result_list[0])
} else {
emit('open-preview', props.apiUrl + '/assets/' + props.generation.result_list[0])
@@ -138,6 +130,10 @@ const handleOverlayClick = () => {
<Button icon="pi pi-pencil"
class="!w-6 !h-6 !rounded-full !bg-white/20 !border-none !text-white text-[10px] hover:!bg-violet-500"
@click.stop="emit('use-result', generation)" />
<Button :icon="(generation.is_nsfw || generation.nsfw) ? 'pi pi-eye' : 'pi pi-eye-slash'"
class="!w-6 !h-6 !rounded-full !bg-white/20 !border-none !text-white text-[10px] hover:!bg-red-500 hover:!text-white"
@click.stop="emit('mark-nsfw', generation)"
v-tooltip.bottom="(generation.is_nsfw || generation.nsfw) ? 'Unmark NSFW' : 'Mark NSFW'" />
<Button icon="pi pi-trash"
class="!w-6 !h-6 !rounded-full !bg-red-500/20 !border-none !text-red-400 text-[10px] hover:!bg-red-500 hover:!text-white"
@click.stop="emit('delete', generation)" />

View File

@@ -231,6 +231,18 @@ const onUseResultAsAsset = () => {
<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">Model</span>
<span class="text-xs text-violet-400 font-bold truncate" :title="previewImage.gen.model">{{ previewImage.gen.model || 'N/A' }}</span>
</div>
<div v-if="previewImage.gen.seed !== undefined && previewImage.gen.seed !== null" class="flex flex-col gap-0.5">
<span class="text-[10px] text-slate-500 uppercase">Seed</span>
<div class="flex items-center gap-1">
<span class="text-xs text-slate-200 font-mono">{{ previewImage.gen.seed }}</span>
<Button icon="pi pi-copy" @click="copyToClipboard(previewImage.gen.seed.toString())"
text class="!p-0 !w-3 !h-3 !text-slate-500 hover:!text-white" />
</div>
</div>
<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>

View File

@@ -69,6 +69,14 @@ export const aiService = {
return response.data
},
// Mark generation as NSFW
async markGenerationNsfw(generationId, isNsfw = true) {
const response = await api.post(`/generations/${generationId}/nsfw`, {
is_nsfw: isNsfw
})
return response.data
},
// Get usage statistics (runs, tokens, cost)
async getUsageReport(breakdown = null, projectId = null) {
const params = {}

View File

@@ -1,5 +1,5 @@
<script setup>
import {computed, onMounted, ref, watch, nextTick} from 'vue'
import {computed, nextTick, onMounted, ref, watch} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import {dataService} from '../services/dataService'
import {aiService} from '../services/aiService'
@@ -7,7 +7,6 @@ import Button from 'primevue/button'
import Skeleton from 'primevue/skeleton'
import Tag from 'primevue/tag'
import Dialog from 'primevue/dialog'
import Textarea from 'primevue/textarea'
import Checkbox from 'primevue/checkbox'
import ProgressBar from 'primevue/progressbar'
import Message from 'primevue/message'
@@ -19,8 +18,6 @@ import Tab from 'primevue/tab'
import TabPanels from 'primevue/tabpanels'
import TabPanel from 'primevue/tabpanel'
import Paginator from 'primevue/paginator'
import MultiSelect from 'primevue/multiselect'
import Dropdown from 'primevue/dropdown'
import GenerationPreviewModal from '../components/GenerationPreviewModal.vue'
@@ -519,17 +516,7 @@ const prompt = ref('')
const isGenerating = ref(false)
const generationStatus = ref('')
const generationProgress = ref(0)
const sendToTelegram = ref(false)
const useProfileImage = ref(true)
const telegramId = ref(localStorage.getItem('telegram_id') || '')
const isTelegramIdSaved = ref(!!localStorage.getItem('telegram_id'))
const saveTelegramId = () => {
if (telegramId.value) {
localStorage.setItem('telegram_id', telegramId.value)
isTelegramIdSaved.value = true
}
}
const generationSuccess = ref(false)
const generationError = ref(null)
const generatedResult = ref(null)
@@ -542,6 +529,12 @@ const previousPrompt = ref('')
const isUploading = ref(false)
const fileInput = ref(null)
const model = ref({ key: 'gemini-3-pro-image-preview', value: 'Pro' })
const modelOptions = ref([
{ key: 'gemini-3.1-flash-image-preview', value: '2' },
{ key: 'gemini-3-pro-image-preview', value: 'Pro' }
])
const selectedAssets = ref([])
const toggleAssetSelection = (asset) => {
const index = selectedAssets.value.findIndex(a => a.id === asset.id)
@@ -561,9 +554,6 @@ const quality = ref({
value: '2K'
})
const qualityOptions = ref([{
key: 'ONEK',
value: '1K'
}, {
key: 'TWOK',
value: '2K'
}, {
@@ -732,6 +722,10 @@ const restoreGeneration = async (gen) => {
// 1. Set prompt
prompt.value = gen.prompt
// 1.1 Set Model
const foundModel = modelOptions.value.find(opt => opt.key === gen.model)
if (foundModel) model.value = foundModel
// 2. Set Quality
const foundQuality = qualityOptions.value.find(opt => opt.key === gen.quality)
if (foundQuality) quality.value = foundQuality
@@ -847,6 +841,29 @@ const useResultAsReference = (gen) => {
}
}
const markNsfw = async (gen) => {
// Determine new state (toggle)
const currentNsfw = gen.is_nsfw || gen.nsfw || false
const newNsfw = !currentNsfw
try {
await aiService.markGenerationNsfw(gen.id, newNsfw)
// Update local state
gen.is_nsfw = newNsfw
// Also update legacy property if present to keep UI consistent
if (gen.nsfw !== undefined) gen.nsfw = newNsfw
// If this is the currently displayed result, update it too
if (generatedResult.value && generatedResult.value.assets && generatedResult.value.assets.some(a => gen.result_list.includes(a.id))) {
generatedResult.value.is_nsfw = newNsfw
if (generatedResult.value.nsfw !== undefined) generatedResult.value.nsfw = newNsfw
}
} catch (e) {
console.error('Failed to toggle NSFW', e)
}
}
const triggerFileUpload = () => {
if (fileInput.value) fileInput.value.click()
}
@@ -878,25 +895,14 @@ const handleGenerate = async () => {
generatedResult.value = null
try {
if (sendToTelegram.value && !telegramId.value) {
alert("Please enter your Telegram ID")
isGenerating.value = false
return
}
if (telegramId.value && telegramId.value !== localStorage.getItem('telegram_id')) {
localStorage.setItem('telegram_id', telegramId.value)
isTelegramIdSaved.value = true
}
const payload = {
model: model.value.key,
linked_character_id: character.value?.id,
environment_id: selectedEnvironment.value?.id || selectedEnvironment.value?._id || null,
aspect_ratio: aspectRatio.value.key,
quality: quality.value.key,
prompt: prompt.value,
assets_list: selectedAssets.value.map(a => a.id),
telegram_id: sendToTelegram.value ? telegramId.value : null,
use_profile_image: useProfileImage.value,
count: generationCount.value
}
@@ -1027,6 +1033,21 @@ const handleGenerate = async () => {
<h2 class="text-sm font-bold m-0">Settings</h2>
</div>
<div class="grid grid-cols-3 gap-2">
<div class="flex flex-col gap-1.5">
<label
class="text-slate-400 text-[9px] font-semibold uppercase tracking-wider">Model</label>
<div
class="!flex !w-full !justify-between items-center justify-center !bg-slate-900/50 !p-1 gap-1 !rounded-lg !border !border-white/10">
<div v-for="option in modelOptions" :key="option.key"
@click="model = option"
class="w-full items-center justify-center justify-items-center !text-center hover:bg-white/5 hover:text-white p-1 hover:rounded-lg cursor-pointer"
:class="model.key === option.key ? 'bg-white/10 text-white rounded-lg shadow-sm' : 'text-slate-500'">
<span class="text-white w-full text-center text-[8px]">{{ option.value }}</span>
</div>
</div>
</div>
<div class="flex flex-col gap-1.5">
<label
class="text-slate-400 text-[9px] font-semibold uppercase tracking-wider">Quality</label>
@@ -1034,12 +1055,11 @@ const handleGenerate = async () => {
class="!flex !w-full !justify-between items-center justify-center !bg-slate-900/50 !p-1 gap-1 !rounded-lg !border !border-white/10">
<div v-for="option in qualityOptions" :key="option.key"
@click="quality = option"
class="w-full items-center justify-center justify-items-center !text-center hover:bg-white/5 hover:text-white p-1 hover:rounded-lg"
:class="quality.key === option.key ? 'bg-white/9 text-white rounded-lg' : ''">
<span class="text-white w-full text-center">{{ option.value }}</span>
class="w-full items-center justify-center justify-items-center !text-center hover:bg-white/5 hover:text-white p-1 hover:rounded-lg cursor-pointer"
:class="quality.key === option.key ? 'bg-white/10 text-white rounded-lg' : ''">
<span class="text-white w-full text-center text-[8px]">{{ option.value }}</span>
</div>
</div>
</div>
<div class="flex flex-col gap-1.5">
@@ -1049,12 +1069,12 @@ const handleGenerate = async () => {
class="!flex !w-full !justify-between items-center justify-center !bg-slate-900/50 !p-1 gap-1 !rounded-lg !border !border-white/10">
<div v-for="option in aspectRatioOptions" :key="option.key"
@click="aspectRatio = option"
class="w-full items-center justify-center justify-items-center !text-center hover:bg-white/5 hover:text-white p-1 hover:rounded-lg"
:class="aspectRatio.key === option.key ? 'bg-white/9 text-white rounded-lg' : ''">
<span class="text-white w-full text-center">{{ option.value }}</span>
class="w-full items-center justify-center justify-items-center !text-center hover:bg-white/5 hover:text-white p-1 hover:rounded-lg cursor-pointer"
:class="aspectRatio.key === option.key ? 'bg-white/10 text-white rounded-lg' : ''">
<span class="text-white w-full text-center text-[8px]">{{ option.value }}</span>
</div>
</div>
</div>
</div>
<div class="flex flex-col gap-1.5">
@@ -1142,18 +1162,6 @@ const handleGenerate = async () => {
<div class="flex flex-col gap-1.5 mt-auto pt-1.5 border-t border-white/5">
<div class="flex flex-col gap-2 mb-2">
<div class="flex items-center gap-2">
<Checkbox v-model="sendToTelegram" :binary="true"
inputId="tg-check-char" />
<label for="tg-check-char"
class="text-[10px] text-slate-400 cursor-pointer select-none">Send
result to Telegram</label>
</div>
<div v-if="sendToTelegram && !isTelegramIdSaved"
class="animate-in fade-in slide-in-from-top-1 duration-200">
<InputText v-model="telegramId" placeholder="Enter Telegram ID"
class="w-full !text-[16px] !py-1" @blur="saveTelegramId" />
</div>
<div class="flex items-center gap-2 mt-1">
<Checkbox v-model="useProfileImage" :binary="true"
inputId="profile-img-check" />
@@ -1381,6 +1389,7 @@ const handleGenerate = async () => {
<span class="capitalize"
:class="gen.status === 'done' ? 'text-green-500' : (gen.status === 'failed' ? 'text-red-500' : 'text-amber-500')">{{
gen.status }}</span>
<Tag v-if="gen.is_nsfw || gen.nsfw" value="NSFW" severity="danger" class="!text-[8px] !py-0 !px-1" />
<i v-if="gen.failed_reason"
v-tooltip.right="gen.failed_reason"
class="pi pi-exclamation-circle text-red-500"
@@ -1417,6 +1426,11 @@ const handleGenerate = async () => {
:disabled="gen.status !== 'done' || gen.result_list.length == 0"
@click.stop="useResultAsReference(gen)"
v-tooltip.bottom="'Use result as reference'" />
<Button :icon="(gen.is_nsfw || gen.nsfw) ? 'pi pi-eye' : 'pi pi-eye-slash'"
label="NSFW" size="small" text
class="!text-[10px] !py-0.5 !px-2 text-slate-400 hover:bg-white/5 flex-1"
@click.stop="markNsfw(gen)"
v-tooltip.bottom="(gen.is_nsfw || gen.nsfw) ? 'Unmark NSFW' : 'Mark NSFW'" />
</div>
</div>
</div>

View File

@@ -151,12 +151,14 @@ const assetPickerTab = ref('all') // 'all', 'uploaded', 'generated'
const modalAssets = ref([])
const isModalLoading = ref(false)
const tempSelectedAssets = ref([])
const model = ref({ key: 'gemini-3-pro-image-preview', value: 'Pro' })
const modelOptions = ref([
{ key: 'gemini-3.1-flash-image-preview', value: '2' },
{ key: 'gemini-3-pro-image-preview', value: 'Pro' }
])
const quality = ref({ key: 'TWOK', value: '2K' })
const aspectRatio = ref({ key: "NINESIXTEEN", value: "9:16" })
const aspectRatio = ref('NINESIXTEEN') // Default to Video (9:16)
const generationCount = ref(1)
const sendToTelegram = ref(false)
const telegramId = ref('')
const isTelegramIdSaved = ref(false)
const useProfileImage = ref(true)
const useEnvironment = ref(false)
const isImprovingPrompt = ref(false)
@@ -164,7 +166,7 @@ const previousPrompt = ref('')
let _savedEnvironmentId = null
// NSFW Toggle
const showNsfwGlobal = ref(localStorage.getItem('show_nsfw_global') === 'true')
const showNsfwGlobal = ref(false)
watch(showNsfwGlobal, (val) => {
localStorage.setItem('show_nsfw_global', val)
@@ -214,22 +216,9 @@ const onlyLiked = ref(false)
// Options
const qualityOptions = ref([
{ key: 'ONEK', value: '1K' },
{ key: 'TWOK', value: '2K' },
{ key: 'FOURK', value: '4K' }
])
const aspectRatioOptions = ref([
{ key: "ONEONE", value: "1:1" },
{ key: "TWOTHREE", value: "2:3" },
{ key: "THREETWO", value: "3:2" },
{ key: "THREEFOUR", value: "3:4" },
{ 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 ---
const STORAGE_KEY = 'flexible_gen_settings'
@@ -237,24 +226,17 @@ const STORAGE_KEY = 'flexible_gen_settings'
const saveSettings = () => {
const settings = {
prompt: prompt.value,
model: model.value,
selectedCharacterId: selectedCharacter.value?.id || selectedCharacter.value?._id,
selectedEnvironmentId: selectedEnvironment.value?.id || selectedEnvironment.value?._id,
selectedAssetIds: selectedAssets.value.map(a => a.id),
quality: quality.value,
aspectRatio: aspectRatio.value,
sendToTelegram: sendToTelegram.value,
telegramId: telegramId.value,
useProfileImage: useProfileImage.value,
useEnvironment: useEnvironment.value,
generationCount: generationCount.value
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings))
// Also save Telegram ID separately as it's used elsewhere
if (telegramId.value) {
localStorage.setItem('telegram_id', telegramId.value)
isTelegramIdSaved.value = true
}
}
const restoreSettings = () => {
@@ -266,11 +248,16 @@ const restoreSettings = () => {
// We need characters and assets loaded to fully restore objects
// For now, we'll store IDs and restore in loadData
if (settings.model) model.value = settings.model
if (settings.quality) quality.value = settings.quality
if (settings.aspectRatio) aspectRatio.value = settings.aspectRatio
sendToTelegram.value = settings.sendToTelegram || false
telegramId.value = settings.telegramId || localStorage.getItem('telegram_id') || ''
if (telegramId.value) isTelegramIdSaved.value = true
if (settings.aspectRatio) {
// Handle legacy object format if present
if (typeof settings.aspectRatio === 'object' && settings.aspectRatio.key) {
aspectRatio.value = settings.aspectRatio.key
} else {
aspectRatio.value = settings.aspectRatio
}
}
if (settings.useProfileImage !== undefined) useProfileImage.value = settings.useProfileImage
if (settings.useEnvironment !== undefined) useEnvironment.value = settings.useEnvironment
if (settings.generationCount) generationCount.value = Math.min(settings.generationCount, 4)
@@ -285,7 +272,7 @@ const restoreSettings = () => {
}
// Watchers for auto-save
watch([prompt, selectedCharacter, selectedEnvironment, selectedAssets, quality, aspectRatio, sendToTelegram, telegramId, useProfileImage, useEnvironment, generationCount], () => {
watch([prompt, selectedCharacter, selectedEnvironment, selectedAssets, quality, aspectRatio, useProfileImage, useEnvironment, generationCount], () => {
saveSettings()
}, { deep: true })
@@ -417,11 +404,6 @@ const refreshHistory = async () => {
const handleGenerate = async () => {
if (!prompt.value.trim()) return
if (sendToTelegram.value && !telegramId.value) {
alert("Please enter your Telegram ID")
return
}
isSubmitting.value = true
// Close settings to show gallery/progress (optional preference)
@@ -429,13 +411,13 @@ const handleGenerate = async () => {
try {
const payload = {
aspect_ratio: aspectRatio.value.key,
model: model.value.key,
aspect_ratio: aspectRatio.value, // Now a string
quality: quality.value.key,
prompt: prompt.value,
assets_list: selectedAssets.value.map(a => a.id),
linked_character_id: selectedCharacter.value?.id || selectedCharacter.value?._id || null,
environment_id: (selectedCharacter.value && useEnvironment.value) ? (selectedEnvironment.value?.id || selectedEnvironment.value?._id || null) : null,
telegram_id: sendToTelegram.value ? telegramId.value : null,
use_profile_image: selectedCharacter.value ? useProfileImage.value : false,
count: generationCount.value
}
@@ -560,6 +542,10 @@ const loadMoreHistory = async () => {
// --- Initial Load ---
onMounted(() => {
// Reset NSFW on page load
showNsfwGlobal.value = false
localStorage.removeItem('show_nsfw_global')
loadData().then(() => {
// slight delay to allow DOM render
setTimeout(setupInfiniteScroll, 500)
@@ -753,6 +739,27 @@ const toggleMobileOverlay = (id) => {
}
}
const markNsfw = async (gen) => {
// if (!confirm('Are you sure you want to mark this generation as NSFW?')) return
const currentNsfw = gen.is_nsfw || gen.nsfw || false
const newNsfw = !currentNsfw
try {
await aiService.markGenerationNsfw(gen.id, newNsfw)
gen.is_nsfw = newNsfw
if (gen.nsfw !== undefined) gen.nsfw = newNsfw
if (gen.isGroup && gen.children) {
gen.children.forEach(c => {
c.is_nsfw = newNsfw
if (c.nsfw !== undefined) c.nsfw = newNsfw
})
}
} catch (e) {
console.error('Failed to toggle NSFW', e)
}
}
// --- Asset Picker Logic ---
const loadModalAssets = async () => {
@@ -893,7 +900,7 @@ const confirmAddToAlbum = async () => {
<div class="flex items-center gap-1.5">
<img v-if="slotProps.option.avatar_image" :src="API_URL + slotProps.option.avatar_image"
class="w-5 h-5 rounded-full object-cover" />
<span class="">{{ slotProps.option.name }}</span>
<span class="text-sm">{{ slotProps.option.name }}</span>
</div>
</template>
</Dropdown>
@@ -902,6 +909,13 @@ const confirmAddToAlbum = async () => {
class="!w-7 !h-7 !p-0"
:class="onlyLiked ? '!text-pink-500 !bg-pink-500/10' : '!text-slate-400 hover:!bg-white/10'"
v-tooltip.bottom="onlyLiked ? 'Show all' : 'Show liked only'" />
<Button :icon="showNsfwGlobal ? 'pi pi-eye' : 'pi pi-eye-slash'"
@click="showNsfwGlobal = !showNsfwGlobal" rounded text
class="!w-7 !h-7 !p-0"
:class="showNsfwGlobal ? '!text-red-400 !bg-red-500/10' : '!text-slate-400 hover:!bg-white/10'"
v-tooltip.bottom="showNsfwGlobal ? 'Hide NSFW' : 'Show NSFW'" />
<Button icon="pi pi-refresh" @click="refreshHistory" rounded text
class="!text-slate-400 hover:!bg-white/10 !w-7 !h-7 !p-0 md:hidden" />
<Button :icon="isSelectMode ? 'pi pi-times' : 'pi pi-check-square'" @click="toggleSelectMode"
@@ -949,6 +963,7 @@ const confirmAddToAlbum = async () => {
@reuse-asset="reuseAsset"
@use-result="useResultAsAsset"
@toggle-overlay="toggleMobileOverlay"
@mark-nsfw="markNsfw"
/>
</div>
</div>
@@ -973,6 +988,7 @@ const confirmAddToAlbum = async () => {
@reuse-asset="reuseAsset"
@use-result="useResultAsAsset"
@toggle-overlay="toggleMobileOverlay"
@mark-nsfw="markNsfw"
/>
</template>
</div>
@@ -1151,19 +1167,41 @@ const confirmAddToAlbum = async () => {
</div>
<div class="w-full lg:w-80 flex flex-col gap-4">
<div class="grid grid-cols-2 gap-4">
<div class="flex flex-col gap-2">
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider">Quality</label>
<Dropdown v-model="quality" :options="qualityOptions" optionLabel="value"
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl"
:pt="{ input: { class: '!text-white' }, trigger: { class: '!text-slate-400' }, panel: { class: '!bg-slate-800 !border-white/10' }, item: { class: '!text-slate-300 hover:!bg-white/10 hover:!text-white' } }" />
<div class="grid grid-cols-3 gap-2">
<div class="flex flex-col gap-1">
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Model</label>
<div class="flex bg-slate-800 p-1 rounded-xl border border-white/10 h-[34px]">
<div v-for="option in modelOptions" :key="option.key" @click="model = option"
class="flex-1 flex items-center justify-center cursor-pointer rounded-md text-[10px] font-bold transition-all"
:class="model.key === option.key ? 'bg-white/10 text-white shadow-sm' : 'text-slate-500 hover:text-slate-300'">
{{ option.value }}
</div>
</div>
</div>
<div class="flex flex-col gap-1">
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Quality</label>
<div class="flex bg-slate-800 p-1 rounded-xl border border-white/10 h-[34px]">
<div v-for="option in qualityOptions" :key="option.key" @click="quality = option"
class="flex-1 flex items-center justify-center cursor-pointer rounded-md text-[10px] font-bold transition-all"
:class="quality.key === option.key ? 'bg-white/10 text-white shadow-sm' : 'text-slate-500 hover:text-slate-300'">
{{ option.value }}
</div>
</div>
</div>
<div class="flex flex-col gap-1">
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Format</label>
<div class="flex bg-slate-800 rounded-xl border border-white/10 h-[34px] p-1">
<button @click="aspectRatio = 'THREEFOUR'"
class="flex-1 text-[10px] font-bold transition-all flex items-center justify-center rounded-md"
:class="aspectRatio === 'THREEFOUR' ? 'bg-white/10 text-white shadow-sm' : 'text-slate-500 hover:text-white hover:bg-white/5'">
<i class="pi pi-image"></i>
</button>
<button @click="aspectRatio = 'NINESIXTEEN'"
class="flex-1 text-[10px] font-bold transition-all flex items-center justify-center rounded-md"
:class="aspectRatio === 'NINESIXTEEN' ? 'bg-white/10 text-white shadow-sm' : 'text-slate-500 hover:text-white hover:bg-white/5'">
<i class="pi pi-video"></i>
</button>
</div>
<div class="flex flex-col gap-2">
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider">Aspect
Ratio</label>
<Dropdown v-model="aspectRatio" :options="aspectRatioOptions" optionLabel="value"
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl"
:pt="{ input: { class: '!text-white' }, trigger: { class: '!text-slate-400' }, panel: { class: '!bg-slate-800 !border-white/10' }, item: { class: '!text-slate-300 hover:!bg-white/10 hover:!text-white' } }" />
</div>
</div>
@@ -1181,26 +1219,6 @@ const confirmAddToAlbum = async () => {
</div>
</div>
<div class="flex flex-col gap-2 bg-slate-800/50 p-3 rounded-xl border border-white/5">
<div class="flex items-center gap-2">
<Checkbox v-model="sendToTelegram" :binary="true" inputId="tg-check" />
<label for="tg-check" class="text-xs text-slate-300 cursor-pointer">Send to
Telegram</label>
</div>
<div v-if="sendToTelegram" class="animate-in fade-in slide-in-from-top-1">
<InputText v-model="telegramId" placeholder="Telegram ID"
class="w-full !text-[16px] !bg-slate-900 !border-white/10 !text-white !py-1.5" />
</div>
</div>
<!-- NSFW Toggle -->
<div class="flex flex-col gap-2 bg-slate-800/50 p-3 rounded-xl border border-white/5">
<div class="flex items-center justify-between">
<label class="text-xs text-slate-300 cursor-pointer">Show NSFW</label>
<InputSwitch v-model="showNsfwGlobal" />
</div>
</div>
<div class="mt-auto">
<Button :label="isSubmitting ? 'Starting...' : 'Generate'"
:icon="isSubmitting ? 'pi pi-spin pi-spinner' : 'pi pi-sparkles'"

View File

@@ -76,7 +76,12 @@ const saveName = async () => {
const prompt = ref('')
const negativePrompt = ref('')
const selectedModel = ref('flux-schnell')
const model = ref({ key: 'gemini-3-pro-image-preview', value: 'Pro' })
const modelOptions = ref([
{ key: 'gemini-3.1-flash-image-preview', value: '2' },
{ key: 'gemini-3-pro-image-preview', value: 'Pro' }
])
const selectedModel = ref('flux-schnell') // Keep legacy if needed elsewhere, but we will use 'model' for generation
// Character & Assets (declared early for settings persistence)
const characters = ref([])
@@ -89,10 +94,8 @@ const showAssetPicker = ref(false) // Deprecated, using isAssetPickerVisible
// --- Load saved settings from localStorage ---
const SETTINGS_KEY = 'idea-gen-settings'
const quality = ref({ key: 'TWOK', value: '2K' })
const aspectRatio = ref({ key: 'NINESIXTEEN', value: '9:16' })
const aspectRatio = ref('NINESIXTEEN') // Default to Video (9:16)
const imageCount = ref(1)
const sendToTelegram = ref(false)
const telegramId = ref('')
const useProfileImage = ref(true)
const useEnvironment = ref(false)
const isImprovingPrompt = ref(false)
@@ -135,12 +138,11 @@ watch(selectedCharacter, (newChar) => {
const saveSettings = () => {
const settings = {
prompt: prompt.value,
model: model.value,
quality: quality.value,
aspectRatio: aspectRatio.value,
imageCount: imageCount.value,
selectedModel: selectedModel.value,
sendToTelegram: sendToTelegram.value,
telegramId: telegramId.value,
useProfileImage: useProfileImage.value,
useEnvironment: useEnvironment.value,
selectedCharacterId: selectedCharacter.value?.id || selectedCharacter.value?._id || null,
@@ -148,9 +150,6 @@ const saveSettings = () => {
selectedAssetIds: selectedAssets.value.map(a => a.id),
}
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings))
if (telegramId.value) {
localStorage.setItem('telegram_id', telegramId.value)
}
}
const restoreSettings = () => {
@@ -158,13 +157,19 @@ const restoreSettings = () => {
if (!stored) return
try {
const s = JSON.parse(stored)
if (s.model) model.value = s.model
if (s.prompt) prompt.value = s.prompt
if (s.quality) quality.value = s.quality
if (s.aspectRatio) aspectRatio.value = s.aspectRatio
if (s.aspectRatio) {
// Handle legacy object format if present
if (typeof s.aspectRatio === 'object' && s.aspectRatio.key) {
aspectRatio.value = s.aspectRatio.key
} else {
aspectRatio.value = s.aspectRatio
}
}
if (s.imageCount) imageCount.value = Math.min(s.imageCount, 4)
if (s.selectedModel) selectedModel.value = s.selectedModel
sendToTelegram.value = s.sendToTelegram || false
telegramId.value = s.telegramId || localStorage.getItem('telegram_id') || ''
if (s.useProfileImage !== undefined) useProfileImage.value = s.useProfileImage
if (s.useEnvironment !== undefined) useEnvironment.value = s.useEnvironment
_savedCharacterId = s.selectedCharacterId || null
@@ -182,7 +187,7 @@ const restoreSettings = () => {
}
restoreSettings()
watch([prompt, quality, aspectRatio, imageCount, selectedModel, sendToTelegram, telegramId, useProfileImage, useEnvironment, selectedCharacter, selectedEnvironment, selectedAssets], saveSettings, { deep: true })
watch([prompt, quality, aspectRatio, imageCount, model, useProfileImage, useEnvironment, selectedCharacter, selectedEnvironment, selectedAssets], saveSettings, { deep: true })
const viewMode = ref('feed') // 'feed' or 'gallery'
const onlyLiked = ref(false)
@@ -196,22 +201,9 @@ watch(isSettingsVisible, (val) => {
const API_URL = import.meta.env.VITE_API_URL
const qualityOptions = ref([
{ key: 'ONEK', value: '1K' },
{ key: 'TWOK', value: '2K' },
{ key: 'FOURK', value: '4K' }
])
const aspectRatioOptions = ref([
{ key: "ONEONE", value: "1:1" },
{ key: "TWOTHREE", value: "2:3" },
{ key: "THREETWO", value: "3:2" },
{ key: "THREEFOUR", value: "3:4" },
{ 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
const loadingGenerations = ref(false) // Added this ref based on usage in fetchGenerations
@@ -311,13 +303,13 @@ const handleGenerate = async () => {
try {
// Construct Payload
const payload = {
model: model.value.key,
prompt: prompt.value,
aspect_ratio: aspectRatio.value.key,
aspect_ratio: aspectRatio.value, // Now a string
quality: quality.value.key,
assets_list: selectedAssets.value.map(a => a.id),
linked_character_id: selectedCharacter.value?.id || selectedCharacter.value?._id || null,
environment_id: (selectedCharacter.value && useEnvironment.value) ? (selectedEnvironment.value?.id || selectedEnvironment.value?._id || null) : null,
telegram_id: sendToTelegram.value ? telegramId.value : null,
use_profile_image: selectedCharacter.value ? useProfileImage.value : false,
count: imageCount.value,
idea_id: currentIdea.value.id
@@ -963,6 +955,26 @@ watch(viewMode, (v) => {
}
})
const markNsfw = async (gen) => {
// if (!confirm('Are you sure you want to mark this generation as NSFW?')) return
const currentNsfw = gen.is_nsfw || gen.nsfw || false
const newNsfw = !currentNsfw
try {
await aiService.markGenerationNsfw(gen.id, newNsfw)
gen.is_nsfw = newNsfw
if (gen.nsfw !== undefined) gen.nsfw = newNsfw
if (gen.isGroup && gen.children) {
gen.children.forEach(c => {
c.is_nsfw = newNsfw
if (c.nsfw !== undefined) c.nsfw = newNsfw
})
}
} catch (e) {
console.error('Failed to toggle NSFW', e)
}
}
</script>
<template>
@@ -1082,6 +1094,11 @@ watch(viewMode, (v) => {
text rounded size="small"
class="!w-7 !h-7 !text-slate-400 hover:!text-violet-400"
v-tooltip.top="'Reuse Assets'" @click="reuseAssets(gen)" />
<Button :icon="(gen.is_nsfw || gen.nsfw) ? 'pi pi-eye' : 'pi pi-eye-slash'"
text rounded size="small"
class="!w-7 !h-7 !text-slate-400 hover:!text-red-400"
v-tooltip.top="(gen.is_nsfw || gen.nsfw) ? 'Unmark NSFW' : 'Mark NSFW'"
@click="markNsfw(gen)" />
<Button icon="pi pi-trash" text rounded size="small"
class="!w-7 !h-7 !text-slate-400 hover:!text-red-400"
v-tooltip.top="'Delete'" @click="deleteGeneration(gen)" />
@@ -1221,6 +1238,11 @@ watch(viewMode, (v) => {
@click.stop="setAsReference(img.assetId)" />
<Button icon="pi pi-refresh" rounded text size="small"
class="!text-white hover:!bg-white/20" @click.stop="reusePrompt(img.gen)" />
<Button :icon="(img.gen.is_nsfw || img.gen.nsfw) ? 'pi pi-eye' : 'pi pi-eye-slash'"
rounded text size="small"
class="!text-white hover:!bg-red-500/20 hover:!text-red-400"
v-tooltip.top="(img.gen.is_nsfw || img.gen.nsfw) ? 'Unmark NSFW' : 'Mark NSFW'"
@click.stop="markNsfw(img.gen)" />
<Button icon="pi pi-trash" rounded text size="small"
class="!text-red-400 hover:!bg-red-500/20"
@click.stop="deleteAssetFromGeneration(img.gen, img.assetId)" />
@@ -1373,7 +1395,7 @@ watch(viewMode, (v) => {
class="flex-shrink-0 flex items-center gap-2 px-2 py-1.5 rounded-lg border-2 transition-all cursor-pointer group bg-slate-800/40"
:class="[
(selectedEnvironment?.id === (env.id || env._id) || selectedEnvironment?._id === (env.id || env._id))
? 'border-violet-500 bg-violet-500/10 shadow-[0_0_15px_rgba(124,58,237,0.15)]'
? 'border-violet-500 bg-violet-500/10 shadow-[0_0_15px_rgba(124,58,237,0.1)]'
: 'border-white/5 hover:border-white/20'
]"
>
@@ -1401,20 +1423,40 @@ watch(viewMode, (v) => {
</div> </div>
<div class="w-full lg:w-72 flex flex-col gap-2">
<div class="grid grid-cols-2 gap-2">
<div class="grid grid-cols-3 gap-2">
<div class="flex flex-col gap-1">
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Model</label>
<div class="flex bg-slate-800 p-1 rounded-xl border border-white/10">
<div v-for="option in modelOptions" :key="option.key" @click="model = option"
class="flex-1 text-center py-1 text-[10px] font-medium transition-all cursor-pointer rounded-md"
:class="model.key === option.key ? 'bg-white/10 text-white shadow-sm' : 'text-slate-500 hover:text-slate-300'">
{{ option.value }}
</div>
</div>
</div>
<div class="flex flex-col gap-1">
<label
class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Quality</label>
<Dropdown v-model="quality" :options="qualityOptions" optionLabel="value"
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl"
:pt="{ input: { class: '!text-white' }, trigger: { class: '!text-slate-400' }, panel: { class: '!bg-slate-800 !border-white/10' }, item: { class: '!text-slate-300 hover:!bg-white/10 hover:!text-white' } }" />
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl !h-[34px]"
:pt="{ input: { class: '!text-white !text-[10px] !py-1 !px-2' }, trigger: { class: '!text-slate-400 !w-6' }, panel: { class: '!bg-slate-800 !border-white/10' }, item: { class: '!text-slate-300 hover:!bg-white/10 hover:!text-white !text-[10px] !py-1' } }" />
</div>
<div class="flex flex-col gap-1">
<label
class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Ratio</label>
<Dropdown v-model="aspectRatio" :options="aspectRatioOptions" optionLabel="value"
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl"
:pt="{ input: { class: '!text-white' }, trigger: { class: '!text-slate-400' }, panel: { class: '!bg-slate-800 !border-white/10' }, item: { class: '!text-slate-300 hover:!bg-white/10 hover:!text-white' } }" />
class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Format</label>
<div class="flex bg-slate-800 rounded-xl border border-white/10 overflow-hidden h-[34px]">
<button @click="aspectRatio = 'THREEFOUR'"
class="flex-1 text-[10px] font-bold transition-all flex items-center justify-center"
:class="aspectRatio === 'THREEFOUR' ? 'bg-violet-600 text-white' : 'text-slate-400 hover:text-white hover:bg-white/5'">
<i class="pi pi-image"></i>
</button>
<div class="w-px bg-white/10"></div>
<button @click="aspectRatio = 'NINESIXTEEN'"
class="flex-1 text-[10px] font-bold transition-all flex items-center justify-center"
:class="aspectRatio === 'NINESIXTEEN' ? 'bg-violet-600 text-white' : 'text-slate-400 hover:text-white hover:bg-white/5'">
<i class="pi pi-video"></i>
</button>
</div>
</div>
</div>
@@ -1432,18 +1474,6 @@ watch(viewMode, (v) => {
</div>
</div>
<div class="flex flex-col gap-1 bg-slate-800/50 p-2 rounded-lg border border-white/5">
<div class="flex items-center gap-2">
<Checkbox v-model="sendToTelegram" :binary="true" inputId="idea-tg-check" />
<label for="idea-tg-check" class="text-xs text-slate-300 cursor-pointer">Send to
Telegram</label>
</div>
<div v-if="sendToTelegram" class="animate-in fade-in slide-in-from-top-1">
<InputText v-model="telegramId" placeholder="Telegram ID"
class="w-full !text-[16px] !bg-slate-900 !border-white/10 !text-white !py-1.5" />
</div>
</div>
<!-- NSFW Toggle -->
<div class="flex flex-col gap-1 bg-slate-800/50 p-2 rounded-lg border border-white/5">
<div class="flex items-center justify-between">

View File

@@ -187,12 +187,15 @@ const startIdeaFromInspiration = (inspiration) => {
// --- Generation Settings ---
const prompt = ref('')
const model = ref({ key: 'gemini-3-pro-image-preview', value: 'Pro' })
const modelOptions = ref([
{ key: 'gemini-3.1-flash-image-preview', value: '2' },
{ key: 'gemini-3-pro-image-preview', value: 'Pro' }
])
const selectedModel = ref('flux-schnell')
const quality = ref({ key: 'TWOK', value: '2K' })
const aspectRatio = ref({ key: 'NINESIXTEEN', value: '9:16' })
const imageCount = ref(1)
const sendToTelegram = ref(false)
const telegramId = ref('')
const useProfileImage = ref(true)
const useEnvironment = ref(false)
const isSubmittingGen = ref(false)
@@ -232,7 +235,6 @@ watch(selectedCharacter, (newChar) => {
})
const qualityOptions = ref([
{ key: 'ONEK', value: '1K' },
{ key: 'TWOK', value: '2K' },
{ key: 'FOURK', value: '4K' }
])
@@ -256,13 +258,12 @@ const restoreSettings = () => {
if (!stored) return
try {
const s = JSON.parse(stored)
if (s.model) model.value = s.model
if (s.prompt) prompt.value = s.prompt
if (s.quality) quality.value = s.quality
if (s.aspectRatio) aspectRatio.value = s.aspectRatio
if (s.imageCount) imageCount.value = Math.min(s.imageCount, 4)
if (s.selectedModel) selectedModel.value = s.selectedModel
sendToTelegram.value = s.sendToTelegram || false
telegramId.value = s.telegramId || localStorage.getItem('telegram_id') || ''
if (s.useProfileImage !== undefined) useProfileImage.value = s.useProfileImage
if (s.useEnvironment !== undefined) useEnvironment.value = s.useEnvironment
_savedCharacterId = s.selectedCharacterId || null
@@ -283,12 +284,11 @@ const restoreSettings = () => {
const saveSettings = () => {
const settings = {
prompt: prompt.value,
model: model.value,
quality: quality.value,
aspectRatio: aspectRatio.value,
imageCount: imageCount.value,
selectedModel: selectedModel.value,
sendToTelegram: sendToTelegram.value,
telegramId: telegramId.value,
useProfileImage: useProfileImage.value,
useEnvironment: useEnvironment.value,
selectedCharacterId: selectedCharacter.value?.id || null,
@@ -296,10 +296,9 @@ const saveSettings = () => {
selectedAssetIds: selectedAssets.value.map(a => a.id),
}
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings))
if (telegramId.value) localStorage.setItem('telegram_id', telegramId.value)
}
watch([prompt, quality, aspectRatio, imageCount, selectedModel, sendToTelegram, telegramId, useProfileImage, useEnvironment, selectedCharacter, selectedEnvironment, selectedAssets], saveSettings, { deep: true })
watch([prompt, quality, aspectRatio, imageCount, model, useProfileImage, useEnvironment, selectedCharacter, selectedEnvironment, selectedAssets], saveSettings, { deep: true })
onMounted(async () => {
restoreSettings()
@@ -834,18 +833,37 @@ const handleAssetPickerUpload = async (event) => {
<!-- RIGHT COLUMN: Settings & Button -->
<div class="w-full lg:w-72 flex flex-col gap-2">
<div class="grid grid-cols-2 gap-2">
<div class="grid grid-cols-3 gap-2">
<div class="flex flex-col gap-1">
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Model</label>
<div class="flex bg-slate-800 p-1 rounded-xl border border-white/10">
<div v-for="option in modelOptions" :key="option.key" @click="model = option"
class="flex-1 text-center py-1 text-[10px] font-medium transition-all cursor-pointer rounded-md"
:class="model.key === option.key ? 'bg-white/10 text-white shadow-sm' : 'text-slate-500 hover:text-slate-300'">
{{ option.value }}
</div>
</div>
</div>
<div class="flex flex-col gap-1">
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Quality</label>
<Dropdown v-model="quality" :options="qualityOptions" optionLabel="value"
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl"
:pt="{ input: { class: '!text-white' }, trigger: { class: '!text-slate-400' }, panel: { class: '!bg-slate-800 !border-white/10' }, item: { class: '!text-slate-300 hover:!bg-white/10 hover:!text-white' } }" />
<div class="flex bg-slate-800 p-1 rounded-xl border border-white/10 h-[34px]">
<div v-for="option in qualityOptions" :key="option.key" @click="quality = option"
class="flex-1 flex items-center justify-center cursor-pointer rounded-md text-[10px] font-bold transition-all"
:class="quality.key === option.key ? 'bg-white/10 text-white shadow-sm' : 'text-slate-500 hover:text-slate-300'">
{{ option.value }}
</div>
</div>
</div>
<div class="flex flex-col gap-1">
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Ratio</label>
<Dropdown v-model="aspectRatio" :options="aspectRatioOptions" optionLabel="value"
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl"
:pt="{ input: { class: '!text-white' }, trigger: { class: '!text-slate-400' }, panel: { class: '!bg-slate-800 !border-white/10' }, item: { class: '!text-slate-300 hover:!bg-white/10 hover:!text-white' } }" />
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl !h-[34px]"
:pt="{
input: { class: '!text-white !text-[10px] !py-1 !px-2 !font-bold' },
trigger: { class: '!text-slate-400 !w-6' },
panel: { class: '!bg-slate-800 !border-white/10' },
item: { class: '!text-slate-300 hover:!bg-white/10 hover:!text-white !text-[10px] !py-1' }
}" />
</div>
</div>
@@ -863,19 +881,6 @@ const handleAssetPickerUpload = async (event) => {
</div>
</div>
<!-- Telegram (Copied from Detail View) -->
<div class="flex flex-col gap-1 bg-slate-800/50 p-2 rounded-lg border border-white/5">
<div class="flex items-center gap-2">
<Checkbox v-model="sendToTelegram" :binary="true" inputId="idea-tg-check" />
<label for="idea-tg-check" class="text-xs text-slate-300 cursor-pointer">Send to
Telegram</label>
</div>
<div v-if="sendToTelegram" class="animate-in fade-in slide-in-from-top-1">
<InputText v-model="telegramId" placeholder="Telegram ID"
class="w-full !text-[16px] !bg-slate-900 !border-white/10 !text-white !py-1.5" />
</div>
</div>
<div class="mt-auto">
<Button :label="isSubmittingGen ? 'Starting...' : 'Generate New Session'"

View File

@@ -12,6 +12,7 @@ import Dialog from 'primevue/dialog'
import Paginator from 'primevue/paginator'
import InputText from 'primevue/inputtext'
import Tag from 'primevue/tag'
import Dropdown from 'primevue/dropdown'
import GenerationPreviewModal from '../components/GenerationPreviewModal.vue'
const router = useRouter()
@@ -44,22 +45,17 @@ const assetsTotalRecords = ref(0)
const assetsRows = ref(12)
const assetsFirst = ref(0)
const activeAssetFilter = ref('all')
const sendToTelegram = ref(false)
const telegramId = ref(localStorage.getItem('telegram_id') || '')
const isTelegramIdSaved = ref(!!localStorage.getItem('telegram_id'))
const isUploading = ref(false)
const fileInput = ref(null)
const saveTelegramId = () => {
if (telegramId.value) {
localStorage.setItem('telegram_id', telegramId.value)
isTelegramIdSaved.value = true
}
}
const model = ref({ key: 'gemini-3-pro-image-preview', value: 'Pro' })
const modelOptions = ref([
{ key: 'gemini-3.1-flash-image-preview', value: '2' },
{ key: 'gemini-3-pro-image-preview', value: 'Pro' }
])
const quality = ref({ key: 'TWOK', value: '2K' })
const qualityOptions = ref([
{ key: 'ONEK', value: '1K' },
{ key: 'TWOK', value: '2K' },
{ key: 'FOURK', value: '4K' }
])
@@ -186,18 +182,6 @@ const onFileSelected = async (event) => {
const handleGenerate = async () => {
if (!prompt.value.trim()) return
// Validation for Telegram
if (sendToTelegram.value && !telegramId.value) {
alert("Please enter your Telegram ID")
return
}
// Save ID if provided
if (telegramId.value && telegramId.value !== localStorage.getItem('telegram_id')) {
localStorage.setItem('telegram_id', telegramId.value)
isTelegramIdSaved.value = true
}
isGenerating.value = true
generationSuccess.value = false
generationError.value = null
@@ -207,12 +191,12 @@ const handleGenerate = async () => {
try {
const payload = {
model: model.value.key,
aspect_ratio: aspectRatio.value.key,
quality: quality.value.key,
prompt: prompt.value,
assets_list: selectedAssets.value.map(a => a.id),
linked_character_id: null, // Explicitly null for global generation
telegram_id: sendToTelegram.value ? telegramId.value : null
}
const response = await aiService.runGeneration(payload)
@@ -294,6 +278,9 @@ const pollStatus = async (id) => {
const restoreGeneration = async (gen) => {
prompt.value = gen.prompt
const foundModel = modelOptions.value.find(opt => opt.key === gen.model)
if (foundModel) model.value = foundModel
const foundQuality = qualityOptions.value.find(opt => opt.key === gen.quality)
if (foundQuality) quality.value = foundQuality
@@ -461,13 +448,23 @@ onMounted(() => {
<!-- Settings Card -->
<div class="glass-panel p-6 rounded-2xl border border-white/5 bg-white/5 flex flex-col gap-6">
<!-- Quality & Aspect Ratio -->
<div class="grid grid-cols-2 gap-6">
<!-- Settings Row: Model, Quality & Aspect Ratio -->
<div class="grid grid-cols-3 gap-4">
<div class="flex flex-col gap-2">
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider">Model</label>
<div class="flex bg-slate-900/50 p-1 rounded-lg border border-white/10 h-[34px]">
<div v-for="option in modelOptions" :key="option.key" @click="model = option"
class="flex-1 flex items-center justify-center cursor-pointer rounded-md text-[10px] font-bold transition-all"
:class="model.key === option.key ? 'bg-white/10 text-white shadow-sm' : 'text-slate-500 hover:text-slate-300'">
{{ option.value }}
</div>
</div>
</div>
<div class="flex flex-col gap-2">
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider">Quality</label>
<div class="flex bg-slate-900/50 p-1 rounded-lg border border-white/10">
<div class="flex bg-slate-900/50 p-1 rounded-lg border border-white/10 h-[34px]">
<div v-for="option in qualityOptions" :key="option.key" @click="quality = option"
class="flex-1 text-center py-1.5 cursor-pointer rounded-md text-xs font-medium transition-all"
class="flex-1 flex items-center justify-center cursor-pointer rounded-md text-[10px] font-bold transition-all"
:class="quality.key === option.key ? 'bg-white/10 text-white shadow-sm' : 'text-slate-500 hover:text-slate-300'">
{{ option.value }}
</div>
@@ -476,14 +473,14 @@ onMounted(() => {
<div class="flex flex-col gap-2">
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider">Aspect
Ratio</label>
<div class="flex bg-slate-900/50 p-1 rounded-lg border border-white/10">
<div v-for="option in aspectRatioOptions" :key="option.key"
@click="aspectRatio = option"
class="flex-1 text-center py-1.5 cursor-pointer rounded-md text-xs font-medium transition-all"
:class="aspectRatio.key === option.key ? 'bg-white/10 text-white shadow-sm' : 'text-slate-500 hover:text-slate-300'">
{{ option.value }}
</div>
</div>
<Dropdown v-model="aspectRatio" :options="aspectRatioOptions" optionLabel="value"
class="w-full !bg-slate-900/50 !border-white/10 !text-white !rounded-lg !h-[34px]"
:pt="{
input: { class: '!text-white !text-[10px] !py-1 !px-2 !font-bold' },
trigger: { class: '!text-slate-400 !w-6' },
panel: { class: '!bg-slate-900 !border-white/10' },
item: { class: '!text-slate-300 hover:!bg-white/10 hover:!text-white !text-[10px] !py-1' }
}" />
</div>
</div>
@@ -551,21 +548,6 @@ onMounted(() => {
<!-- Generate Button -->
<div class="mt-auto">
<div class="flex flex-col gap-2 mb-3">
<div class="flex items-center gap-2">
<Checkbox v-model="sendToTelegram" :binary="true" inputId="tg-check-gen" />
<label for="tg-check-gen"
class="text-xs text-slate-400 cursor-pointer select-none">Send result to
Telegram</label>
</div>
<div v-if="sendToTelegram && !isTelegramIdSaved"
class="animate-in fade-in slide-in-from-top-1 duration-200">
<InputText v-model="telegramId" placeholder="Enter Telegram ID"
class="w-full !text-[16px] !py-1.5" @blur="saveTelegramId" />
<small class="text-[10px] text-slate-500 block mt-0.5">ID will be saved for future
use</small>
</div>
</div>
<Button :label="isGenerating ? 'Generating...' : 'Generate Image'"
:icon="isGenerating ? 'pi pi-spin pi-spinner' : 'pi pi-magic'" :loading="isGenerating"
@click="handleGenerate"