Compare commits
4 Commits
7d7cd25040
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a737c38c53 | |||
| 2ed2ee2937 | |||
| 4f9807cfe7 | |||
| f89548b363 |
@@ -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 isTemporarilyUnblurred = ref(false)
|
||||||
|
|
||||||
const isBlurred = computed(() => {
|
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 = () => {
|
const toggleBlur = () => {
|
||||||
@@ -46,14 +46,6 @@ const handleImageClick = (e) => {
|
|||||||
emit('toggle-select', props.generation.result_list[0])
|
emit('toggle-select', props.generation.result_list[0])
|
||||||
} else {
|
} else {
|
||||||
if (isBlurred.value) {
|
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])
|
emit('open-preview', props.apiUrl + '/assets/' + props.generation.result_list[0])
|
||||||
} else {
|
} else {
|
||||||
emit('open-preview', props.apiUrl + '/assets/' + props.generation.result_list[0])
|
emit('open-preview', props.apiUrl + '/assets/' + props.generation.result_list[0])
|
||||||
@@ -138,6 +130,10 @@ const handleOverlayClick = () => {
|
|||||||
<Button icon="pi pi-pencil"
|
<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"
|
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)" />
|
@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"
|
<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"
|
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)" />
|
@click.stop="emit('delete', generation)" />
|
||||||
|
|||||||
@@ -231,6 +231,18 @@ const onUseResultAsAsset = () => {
|
|||||||
<div class="flex flex-col gap-3 bg-black/20 p-4 rounded-xl border border-white/5">
|
<div class="flex flex-col gap-3 bg-black/20 p-4 rounded-xl border border-white/5">
|
||||||
<!-- Grid for main params -->
|
<!-- Grid for main params -->
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<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">
|
<div class="flex flex-col gap-0.5">
|
||||||
<span class="text-[10px] text-slate-500 uppercase">Quality</span>
|
<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>
|
<span class="text-xs text-slate-200 font-semibold">{{ previewImage.gen.quality || 'N/A' }}</span>
|
||||||
|
|||||||
@@ -69,6 +69,14 @@ export const aiService = {
|
|||||||
return response.data
|
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)
|
// Get usage statistics (runs, tokens, cost)
|
||||||
async getUsageReport(breakdown = null, projectId = null) {
|
async getUsageReport(breakdown = null, projectId = null) {
|
||||||
const params = {}
|
const params = {}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<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 {useRoute, useRouter} from 'vue-router'
|
||||||
import {dataService} from '../services/dataService'
|
import {dataService} from '../services/dataService'
|
||||||
import {aiService} from '../services/aiService'
|
import {aiService} from '../services/aiService'
|
||||||
@@ -7,7 +7,6 @@ import Button from 'primevue/button'
|
|||||||
import Skeleton from 'primevue/skeleton'
|
import Skeleton from 'primevue/skeleton'
|
||||||
import Tag from 'primevue/tag'
|
import Tag from 'primevue/tag'
|
||||||
import Dialog from 'primevue/dialog'
|
import Dialog from 'primevue/dialog'
|
||||||
import Textarea from 'primevue/textarea'
|
|
||||||
import Checkbox from 'primevue/checkbox'
|
import Checkbox from 'primevue/checkbox'
|
||||||
import ProgressBar from 'primevue/progressbar'
|
import ProgressBar from 'primevue/progressbar'
|
||||||
import Message from 'primevue/message'
|
import Message from 'primevue/message'
|
||||||
@@ -19,8 +18,6 @@ import Tab from 'primevue/tab'
|
|||||||
import TabPanels from 'primevue/tabpanels'
|
import TabPanels from 'primevue/tabpanels'
|
||||||
import TabPanel from 'primevue/tabpanel'
|
import TabPanel from 'primevue/tabpanel'
|
||||||
import Paginator from 'primevue/paginator'
|
import Paginator from 'primevue/paginator'
|
||||||
import MultiSelect from 'primevue/multiselect'
|
|
||||||
import Dropdown from 'primevue/dropdown'
|
|
||||||
|
|
||||||
import GenerationPreviewModal from '../components/GenerationPreviewModal.vue'
|
import GenerationPreviewModal from '../components/GenerationPreviewModal.vue'
|
||||||
|
|
||||||
@@ -519,17 +516,7 @@ const prompt = ref('')
|
|||||||
const isGenerating = ref(false)
|
const isGenerating = ref(false)
|
||||||
const generationStatus = ref('')
|
const generationStatus = ref('')
|
||||||
const generationProgress = ref(0)
|
const generationProgress = ref(0)
|
||||||
const sendToTelegram = ref(false)
|
|
||||||
const useProfileImage = ref(true)
|
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 generationSuccess = ref(false)
|
||||||
const generationError = ref(null)
|
const generationError = ref(null)
|
||||||
const generatedResult = ref(null)
|
const generatedResult = ref(null)
|
||||||
@@ -542,6 +529,12 @@ const previousPrompt = ref('')
|
|||||||
const isUploading = ref(false)
|
const isUploading = ref(false)
|
||||||
const fileInput = ref(null)
|
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 selectedAssets = ref([])
|
||||||
const toggleAssetSelection = (asset) => {
|
const toggleAssetSelection = (asset) => {
|
||||||
const index = selectedAssets.value.findIndex(a => a.id === asset.id)
|
const index = selectedAssets.value.findIndex(a => a.id === asset.id)
|
||||||
@@ -561,9 +554,6 @@ const quality = ref({
|
|||||||
value: '2K'
|
value: '2K'
|
||||||
})
|
})
|
||||||
const qualityOptions = ref([{
|
const qualityOptions = ref([{
|
||||||
key: 'ONEK',
|
|
||||||
value: '1K'
|
|
||||||
}, {
|
|
||||||
key: 'TWOK',
|
key: 'TWOK',
|
||||||
value: '2K'
|
value: '2K'
|
||||||
}, {
|
}, {
|
||||||
@@ -732,6 +722,10 @@ const restoreGeneration = async (gen) => {
|
|||||||
// 1. Set prompt
|
// 1. Set prompt
|
||||||
prompt.value = gen.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
|
// 2. Set Quality
|
||||||
const foundQuality = qualityOptions.value.find(opt => opt.key === gen.quality)
|
const foundQuality = qualityOptions.value.find(opt => opt.key === gen.quality)
|
||||||
if (foundQuality) quality.value = foundQuality
|
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 = () => {
|
const triggerFileUpload = () => {
|
||||||
if (fileInput.value) fileInput.value.click()
|
if (fileInput.value) fileInput.value.click()
|
||||||
}
|
}
|
||||||
@@ -878,25 +895,14 @@ const handleGenerate = async () => {
|
|||||||
generatedResult.value = null
|
generatedResult.value = null
|
||||||
|
|
||||||
try {
|
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 = {
|
const payload = {
|
||||||
|
model: model.value.key,
|
||||||
linked_character_id: character.value?.id,
|
linked_character_id: character.value?.id,
|
||||||
environment_id: selectedEnvironment.value?.id || selectedEnvironment.value?._id || null,
|
environment_id: selectedEnvironment.value?.id || selectedEnvironment.value?._id || null,
|
||||||
aspect_ratio: aspectRatio.value.key,
|
aspect_ratio: aspectRatio.value.key,
|
||||||
quality: quality.value.key,
|
quality: quality.value.key,
|
||||||
prompt: prompt.value,
|
prompt: prompt.value,
|
||||||
assets_list: selectedAssets.value.map(a => a.id),
|
assets_list: selectedAssets.value.map(a => a.id),
|
||||||
telegram_id: sendToTelegram.value ? telegramId.value : null,
|
|
||||||
use_profile_image: useProfileImage.value,
|
use_profile_image: useProfileImage.value,
|
||||||
count: generationCount.value
|
count: generationCount.value
|
||||||
}
|
}
|
||||||
@@ -1027,6 +1033,21 @@ const handleGenerate = async () => {
|
|||||||
<h2 class="text-sm font-bold m-0">Settings</h2>
|
<h2 class="text-sm font-bold m-0">Settings</h2>
|
||||||
</div>
|
</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">
|
<div class="flex flex-col gap-1.5">
|
||||||
<label
|
<label
|
||||||
class="text-slate-400 text-[9px] font-semibold uppercase tracking-wider">Quality</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">
|
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"
|
<div v-for="option in qualityOptions" :key="option.key"
|
||||||
@click="quality = option"
|
@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="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/9 text-white rounded-lg' : ''">
|
:class="quality.key === option.key ? 'bg-white/10 text-white rounded-lg' : ''">
|
||||||
<span class="text-white w-full text-center">{{ option.value }}</span>
|
<span class="text-white w-full text-center text-[8px]">{{ option.value }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1.5">
|
<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">
|
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"
|
<div v-for="option in aspectRatioOptions" :key="option.key"
|
||||||
@click="aspectRatio = option"
|
@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="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/9 text-white rounded-lg' : ''">
|
:class="aspectRatio.key === option.key ? 'bg-white/10 text-white rounded-lg' : ''">
|
||||||
<span class="text-white w-full text-center">{{ option.value }}</span>
|
<span class="text-white w-full text-center text-[8px]">{{ option.value }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1.5">
|
<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-1.5 mt-auto pt-1.5 border-t border-white/5">
|
||||||
<div class="flex flex-col gap-2 mb-2">
|
<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">
|
<div class="flex items-center gap-2 mt-1">
|
||||||
<Checkbox v-model="useProfileImage" :binary="true"
|
<Checkbox v-model="useProfileImage" :binary="true"
|
||||||
inputId="profile-img-check" />
|
inputId="profile-img-check" />
|
||||||
@@ -1381,6 +1389,7 @@ const handleGenerate = async () => {
|
|||||||
<span class="capitalize"
|
<span class="capitalize"
|
||||||
:class="gen.status === 'done' ? 'text-green-500' : (gen.status === 'failed' ? 'text-red-500' : 'text-amber-500')">{{
|
:class="gen.status === 'done' ? 'text-green-500' : (gen.status === 'failed' ? 'text-red-500' : 'text-amber-500')">{{
|
||||||
gen.status }}</span>
|
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"
|
<i v-if="gen.failed_reason"
|
||||||
v-tooltip.right="gen.failed_reason"
|
v-tooltip.right="gen.failed_reason"
|
||||||
class="pi pi-exclamation-circle text-red-500"
|
class="pi pi-exclamation-circle text-red-500"
|
||||||
@@ -1417,6 +1426,11 @@ const handleGenerate = async () => {
|
|||||||
:disabled="gen.status !== 'done' || gen.result_list.length == 0"
|
:disabled="gen.status !== 'done' || gen.result_list.length == 0"
|
||||||
@click.stop="useResultAsReference(gen)"
|
@click.stop="useResultAsReference(gen)"
|
||||||
v-tooltip.bottom="'Use result as reference'" />
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -151,12 +151,14 @@ const assetPickerTab = ref('all') // 'all', 'uploaded', 'generated'
|
|||||||
const modalAssets = ref([])
|
const modalAssets = ref([])
|
||||||
const isModalLoading = ref(false)
|
const isModalLoading = ref(false)
|
||||||
const tempSelectedAssets = ref([])
|
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 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 generationCount = ref(1)
|
||||||
const sendToTelegram = ref(false)
|
|
||||||
const telegramId = ref('')
|
|
||||||
const isTelegramIdSaved = ref(false)
|
|
||||||
const useProfileImage = ref(true)
|
const useProfileImage = ref(true)
|
||||||
const useEnvironment = ref(false)
|
const useEnvironment = ref(false)
|
||||||
const isImprovingPrompt = ref(false)
|
const isImprovingPrompt = ref(false)
|
||||||
@@ -164,7 +166,7 @@ const previousPrompt = ref('')
|
|||||||
let _savedEnvironmentId = null
|
let _savedEnvironmentId = null
|
||||||
|
|
||||||
// NSFW Toggle
|
// NSFW Toggle
|
||||||
const showNsfwGlobal = ref(localStorage.getItem('show_nsfw_global') === 'true')
|
const showNsfwGlobal = ref(false)
|
||||||
|
|
||||||
watch(showNsfwGlobal, (val) => {
|
watch(showNsfwGlobal, (val) => {
|
||||||
localStorage.setItem('show_nsfw_global', val)
|
localStorage.setItem('show_nsfw_global', val)
|
||||||
@@ -214,22 +216,9 @@ const onlyLiked = ref(false)
|
|||||||
|
|
||||||
// Options
|
// Options
|
||||||
const qualityOptions = ref([
|
const qualityOptions = ref([
|
||||||
{ key: 'ONEK', value: '1K' },
|
|
||||||
{ key: 'TWOK', value: '2K' },
|
{ key: 'TWOK', value: '2K' },
|
||||||
{ key: 'FOURK', value: '4K' }
|
{ 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 ---
|
// --- Persistence ---
|
||||||
const STORAGE_KEY = 'flexible_gen_settings'
|
const STORAGE_KEY = 'flexible_gen_settings'
|
||||||
@@ -237,24 +226,17 @@ const STORAGE_KEY = 'flexible_gen_settings'
|
|||||||
const saveSettings = () => {
|
const saveSettings = () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
prompt: prompt.value,
|
prompt: prompt.value,
|
||||||
|
model: model.value,
|
||||||
selectedCharacterId: selectedCharacter.value?.id || selectedCharacter.value?._id,
|
selectedCharacterId: selectedCharacter.value?.id || selectedCharacter.value?._id,
|
||||||
selectedEnvironmentId: selectedEnvironment.value?.id || selectedEnvironment.value?._id,
|
selectedEnvironmentId: selectedEnvironment.value?.id || selectedEnvironment.value?._id,
|
||||||
selectedAssetIds: selectedAssets.value.map(a => a.id),
|
selectedAssetIds: selectedAssets.value.map(a => a.id),
|
||||||
quality: quality.value,
|
quality: quality.value,
|
||||||
aspectRatio: aspectRatio.value,
|
aspectRatio: aspectRatio.value,
|
||||||
sendToTelegram: sendToTelegram.value,
|
|
||||||
telegramId: telegramId.value,
|
|
||||||
useProfileImage: useProfileImage.value,
|
useProfileImage: useProfileImage.value,
|
||||||
useEnvironment: useEnvironment.value,
|
useEnvironment: useEnvironment.value,
|
||||||
generationCount: generationCount.value
|
generationCount: generationCount.value
|
||||||
}
|
}
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings))
|
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 = () => {
|
const restoreSettings = () => {
|
||||||
@@ -266,11 +248,16 @@ const restoreSettings = () => {
|
|||||||
|
|
||||||
// We need characters and assets loaded to fully restore objects
|
// We need characters and assets loaded to fully restore objects
|
||||||
// For now, we'll store IDs and restore in loadData
|
// 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.quality) quality.value = settings.quality
|
||||||
if (settings.aspectRatio) aspectRatio.value = settings.aspectRatio
|
if (settings.aspectRatio) {
|
||||||
sendToTelegram.value = settings.sendToTelegram || false
|
// Handle legacy object format if present
|
||||||
telegramId.value = settings.telegramId || localStorage.getItem('telegram_id') || ''
|
if (typeof settings.aspectRatio === 'object' && settings.aspectRatio.key) {
|
||||||
if (telegramId.value) isTelegramIdSaved.value = true
|
aspectRatio.value = settings.aspectRatio.key
|
||||||
|
} else {
|
||||||
|
aspectRatio.value = settings.aspectRatio
|
||||||
|
}
|
||||||
|
}
|
||||||
if (settings.useProfileImage !== undefined) useProfileImage.value = settings.useProfileImage
|
if (settings.useProfileImage !== undefined) useProfileImage.value = settings.useProfileImage
|
||||||
if (settings.useEnvironment !== undefined) useEnvironment.value = settings.useEnvironment
|
if (settings.useEnvironment !== undefined) useEnvironment.value = settings.useEnvironment
|
||||||
if (settings.generationCount) generationCount.value = Math.min(settings.generationCount, 4)
|
if (settings.generationCount) generationCount.value = Math.min(settings.generationCount, 4)
|
||||||
@@ -285,7 +272,7 @@ const restoreSettings = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Watchers for auto-save
|
// 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()
|
saveSettings()
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
@@ -417,11 +404,6 @@ const refreshHistory = async () => {
|
|||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!prompt.value.trim()) return
|
if (!prompt.value.trim()) return
|
||||||
|
|
||||||
if (sendToTelegram.value && !telegramId.value) {
|
|
||||||
alert("Please enter your Telegram ID")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isSubmitting.value = true
|
isSubmitting.value = true
|
||||||
|
|
||||||
// Close settings to show gallery/progress (optional preference)
|
// Close settings to show gallery/progress (optional preference)
|
||||||
@@ -429,13 +411,13 @@ const handleGenerate = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
aspect_ratio: aspectRatio.value.key,
|
model: model.value.key,
|
||||||
|
aspect_ratio: aspectRatio.value, // Now a string
|
||||||
quality: quality.value.key,
|
quality: quality.value.key,
|
||||||
prompt: prompt.value,
|
prompt: prompt.value,
|
||||||
assets_list: selectedAssets.value.map(a => a.id),
|
assets_list: selectedAssets.value.map(a => a.id),
|
||||||
linked_character_id: selectedCharacter.value?.id || selectedCharacter.value?._id || null,
|
linked_character_id: selectedCharacter.value?.id || selectedCharacter.value?._id || null,
|
||||||
environment_id: (selectedCharacter.value && useEnvironment.value) ? (selectedEnvironment.value?.id || selectedEnvironment.value?._id || null) : 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,
|
use_profile_image: selectedCharacter.value ? useProfileImage.value : false,
|
||||||
count: generationCount.value
|
count: generationCount.value
|
||||||
}
|
}
|
||||||
@@ -560,6 +542,10 @@ const loadMoreHistory = async () => {
|
|||||||
|
|
||||||
// --- Initial Load ---
|
// --- Initial Load ---
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// Reset NSFW on page load
|
||||||
|
showNsfwGlobal.value = false
|
||||||
|
localStorage.removeItem('show_nsfw_global')
|
||||||
|
|
||||||
loadData().then(() => {
|
loadData().then(() => {
|
||||||
// slight delay to allow DOM render
|
// slight delay to allow DOM render
|
||||||
setTimeout(setupInfiniteScroll, 500)
|
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 ---
|
// --- Asset Picker Logic ---
|
||||||
|
|
||||||
const loadModalAssets = async () => {
|
const loadModalAssets = async () => {
|
||||||
@@ -893,7 +900,7 @@ const confirmAddToAlbum = async () => {
|
|||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<img v-if="slotProps.option.avatar_image" :src="API_URL + slotProps.option.avatar_image"
|
<img v-if="slotProps.option.avatar_image" :src="API_URL + slotProps.option.avatar_image"
|
||||||
class="w-5 h-5 rounded-full object-cover" />
|
class="w-5 h-5 rounded-full object-cover" />
|
||||||
<span class="">{{ slotProps.option.name }}</span>
|
<span class="text-sm">{{ slotProps.option.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@@ -902,6 +909,13 @@ const confirmAddToAlbum = async () => {
|
|||||||
class="!w-7 !h-7 !p-0"
|
class="!w-7 !h-7 !p-0"
|
||||||
:class="onlyLiked ? '!text-pink-500 !bg-pink-500/10' : '!text-slate-400 hover:!bg-white/10'"
|
: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'" />
|
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
|
<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" />
|
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"
|
<Button :icon="isSelectMode ? 'pi pi-times' : 'pi pi-check-square'" @click="toggleSelectMode"
|
||||||
@@ -949,6 +963,7 @@ const confirmAddToAlbum = async () => {
|
|||||||
@reuse-asset="reuseAsset"
|
@reuse-asset="reuseAsset"
|
||||||
@use-result="useResultAsAsset"
|
@use-result="useResultAsAsset"
|
||||||
@toggle-overlay="toggleMobileOverlay"
|
@toggle-overlay="toggleMobileOverlay"
|
||||||
|
@mark-nsfw="markNsfw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -973,6 +988,7 @@ const confirmAddToAlbum = async () => {
|
|||||||
@reuse-asset="reuseAsset"
|
@reuse-asset="reuseAsset"
|
||||||
@use-result="useResultAsAsset"
|
@use-result="useResultAsAsset"
|
||||||
@toggle-overlay="toggleMobileOverlay"
|
@toggle-overlay="toggleMobileOverlay"
|
||||||
|
@mark-nsfw="markNsfw"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -1151,19 +1167,41 @@ const confirmAddToAlbum = async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full lg:w-80 flex flex-col gap-4">
|
<div class="w-full lg:w-80 flex flex-col gap-4">
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-1">
|
||||||
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider">Quality</label>
|
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Model</label>
|
||||||
<Dropdown v-model="quality" :options="qualityOptions" optionLabel="value"
|
<div class="flex bg-slate-800 p-1 rounded-xl border border-white/10 h-[34px]">
|
||||||
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl"
|
<div v-for="option in modelOptions" :key="option.key" @click="model = option"
|
||||||
: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="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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1181,26 +1219,6 @@ const confirmAddToAlbum = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="mt-auto">
|
||||||
<Button :label="isSubmitting ? 'Starting...' : 'Generate'"
|
<Button :label="isSubmitting ? 'Starting...' : 'Generate'"
|
||||||
:icon="isSubmitting ? 'pi pi-spin pi-spinner' : 'pi pi-sparkles'"
|
:icon="isSubmitting ? 'pi pi-spin pi-spinner' : 'pi pi-sparkles'"
|
||||||
|
|||||||
@@ -76,7 +76,12 @@ const saveName = async () => {
|
|||||||
|
|
||||||
const prompt = ref('')
|
const prompt = ref('')
|
||||||
const negativePrompt = 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)
|
// Character & Assets (declared early for settings persistence)
|
||||||
const characters = ref([])
|
const characters = ref([])
|
||||||
@@ -89,10 +94,8 @@ const showAssetPicker = ref(false) // Deprecated, using isAssetPickerVisible
|
|||||||
// --- Load saved settings from localStorage ---
|
// --- Load saved settings from localStorage ---
|
||||||
const SETTINGS_KEY = 'idea-gen-settings'
|
const SETTINGS_KEY = 'idea-gen-settings'
|
||||||
const quality = ref({ key: 'TWOK', value: '2K' })
|
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 imageCount = ref(1)
|
||||||
const sendToTelegram = ref(false)
|
|
||||||
const telegramId = ref('')
|
|
||||||
const useProfileImage = ref(true)
|
const useProfileImage = ref(true)
|
||||||
const useEnvironment = ref(false)
|
const useEnvironment = ref(false)
|
||||||
const isImprovingPrompt = ref(false)
|
const isImprovingPrompt = ref(false)
|
||||||
@@ -135,12 +138,11 @@ watch(selectedCharacter, (newChar) => {
|
|||||||
const saveSettings = () => {
|
const saveSettings = () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
prompt: prompt.value,
|
prompt: prompt.value,
|
||||||
|
model: model.value,
|
||||||
quality: quality.value,
|
quality: quality.value,
|
||||||
aspectRatio: aspectRatio.value,
|
aspectRatio: aspectRatio.value,
|
||||||
imageCount: imageCount.value,
|
imageCount: imageCount.value,
|
||||||
selectedModel: selectedModel.value,
|
selectedModel: selectedModel.value,
|
||||||
sendToTelegram: sendToTelegram.value,
|
|
||||||
telegramId: telegramId.value,
|
|
||||||
useProfileImage: useProfileImage.value,
|
useProfileImage: useProfileImage.value,
|
||||||
useEnvironment: useEnvironment.value,
|
useEnvironment: useEnvironment.value,
|
||||||
selectedCharacterId: selectedCharacter.value?.id || selectedCharacter.value?._id || null,
|
selectedCharacterId: selectedCharacter.value?.id || selectedCharacter.value?._id || null,
|
||||||
@@ -148,9 +150,6 @@ const saveSettings = () => {
|
|||||||
selectedAssetIds: selectedAssets.value.map(a => a.id),
|
selectedAssetIds: selectedAssets.value.map(a => a.id),
|
||||||
}
|
}
|
||||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings))
|
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings))
|
||||||
if (telegramId.value) {
|
|
||||||
localStorage.setItem('telegram_id', telegramId.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const restoreSettings = () => {
|
const restoreSettings = () => {
|
||||||
@@ -158,13 +157,19 @@ const restoreSettings = () => {
|
|||||||
if (!stored) return
|
if (!stored) return
|
||||||
try {
|
try {
|
||||||
const s = JSON.parse(stored)
|
const s = JSON.parse(stored)
|
||||||
|
if (s.model) model.value = s.model
|
||||||
if (s.prompt) prompt.value = s.prompt
|
if (s.prompt) prompt.value = s.prompt
|
||||||
if (s.quality) quality.value = s.quality
|
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.imageCount) imageCount.value = Math.min(s.imageCount, 4)
|
||||||
if (s.selectedModel) selectedModel.value = s.selectedModel
|
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.useProfileImage !== undefined) useProfileImage.value = s.useProfileImage
|
||||||
if (s.useEnvironment !== undefined) useEnvironment.value = s.useEnvironment
|
if (s.useEnvironment !== undefined) useEnvironment.value = s.useEnvironment
|
||||||
_savedCharacterId = s.selectedCharacterId || null
|
_savedCharacterId = s.selectedCharacterId || null
|
||||||
@@ -182,7 +187,7 @@ const restoreSettings = () => {
|
|||||||
}
|
}
|
||||||
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 viewMode = ref('feed') // 'feed' or 'gallery'
|
||||||
const onlyLiked = ref(false)
|
const onlyLiked = ref(false)
|
||||||
@@ -196,22 +201,9 @@ watch(isSettingsVisible, (val) => {
|
|||||||
const API_URL = import.meta.env.VITE_API_URL
|
const API_URL = import.meta.env.VITE_API_URL
|
||||||
|
|
||||||
const qualityOptions = ref([
|
const qualityOptions = ref([
|
||||||
{ key: 'ONEK', value: '1K' },
|
|
||||||
{ key: 'TWOK', value: '2K' },
|
{ key: 'TWOK', value: '2K' },
|
||||||
{ key: 'FOURK', value: '4K' }
|
{ 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
|
// Removed duplicate characters ref
|
||||||
const loadingGenerations = ref(false) // Added this ref based on usage in fetchGenerations
|
const loadingGenerations = ref(false) // Added this ref based on usage in fetchGenerations
|
||||||
@@ -311,13 +303,13 @@ const handleGenerate = async () => {
|
|||||||
try {
|
try {
|
||||||
// Construct Payload
|
// Construct Payload
|
||||||
const payload = {
|
const payload = {
|
||||||
|
model: model.value.key,
|
||||||
prompt: prompt.value,
|
prompt: prompt.value,
|
||||||
aspect_ratio: aspectRatio.value.key,
|
aspect_ratio: aspectRatio.value, // Now a string
|
||||||
quality: quality.value.key,
|
quality: quality.value.key,
|
||||||
assets_list: selectedAssets.value.map(a => a.id),
|
assets_list: selectedAssets.value.map(a => a.id),
|
||||||
linked_character_id: selectedCharacter.value?.id || selectedCharacter.value?._id || null,
|
linked_character_id: selectedCharacter.value?.id || selectedCharacter.value?._id || null,
|
||||||
environment_id: (selectedCharacter.value && useEnvironment.value) ? (selectedEnvironment.value?.id || selectedEnvironment.value?._id || null) : 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,
|
use_profile_image: selectedCharacter.value ? useProfileImage.value : false,
|
||||||
count: imageCount.value,
|
count: imageCount.value,
|
||||||
idea_id: currentIdea.value.id
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -1082,6 +1094,11 @@ watch(viewMode, (v) => {
|
|||||||
text rounded size="small"
|
text rounded size="small"
|
||||||
class="!w-7 !h-7 !text-slate-400 hover:!text-violet-400"
|
class="!w-7 !h-7 !text-slate-400 hover:!text-violet-400"
|
||||||
v-tooltip.top="'Reuse Assets'" @click="reuseAssets(gen)" />
|
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"
|
<Button icon="pi pi-trash" text rounded size="small"
|
||||||
class="!w-7 !h-7 !text-slate-400 hover:!text-red-400"
|
class="!w-7 !h-7 !text-slate-400 hover:!text-red-400"
|
||||||
v-tooltip.top="'Delete'" @click="deleteGeneration(gen)" />
|
v-tooltip.top="'Delete'" @click="deleteGeneration(gen)" />
|
||||||
@@ -1221,6 +1238,11 @@ watch(viewMode, (v) => {
|
|||||||
@click.stop="setAsReference(img.assetId)" />
|
@click.stop="setAsReference(img.assetId)" />
|
||||||
<Button icon="pi pi-refresh" rounded text size="small"
|
<Button icon="pi pi-refresh" rounded text size="small"
|
||||||
class="!text-white hover:!bg-white/20" @click.stop="reusePrompt(img.gen)" />
|
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"
|
<Button icon="pi pi-trash" rounded text size="small"
|
||||||
class="!text-red-400 hover:!bg-red-500/20"
|
class="!text-red-400 hover:!bg-red-500/20"
|
||||||
@click.stop="deleteAssetFromGeneration(img.gen, img.assetId)" />
|
@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="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="[
|
:class="[
|
||||||
(selectedEnvironment?.id === (env.id || env._id) || selectedEnvironment?._id === (env.id || env._id))
|
(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'
|
: 'border-white/5 hover:border-white/20'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@@ -1401,20 +1423,40 @@ watch(viewMode, (v) => {
|
|||||||
</div> </div>
|
</div> </div>
|
||||||
|
|
||||||
<div class="w-full lg:w-72 flex flex-col gap-2">
|
<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">
|
<div class="flex flex-col gap-1">
|
||||||
<label
|
<label
|
||||||
class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Quality</label>
|
class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Quality</label>
|
||||||
<Dropdown v-model="quality" :options="qualityOptions" optionLabel="value"
|
<Dropdown v-model="quality" :options="qualityOptions" optionLabel="value"
|
||||||
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl"
|
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl !h-[34px]"
|
||||||
: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' } }" />
|
: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>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<label
|
<label
|
||||||
class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Ratio</label>
|
class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Format</label>
|
||||||
<Dropdown v-model="aspectRatio" :options="aspectRatioOptions" optionLabel="value"
|
<div class="flex bg-slate-800 rounded-xl border border-white/10 overflow-hidden h-[34px]">
|
||||||
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl"
|
<button @click="aspectRatio = 'THREEFOUR'"
|
||||||
: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="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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1432,18 +1474,6 @@ watch(viewMode, (v) => {
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- NSFW Toggle -->
|
||||||
<div class="flex flex-col gap-1 bg-slate-800/50 p-2 rounded-lg border border-white/5">
|
<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">
|
<div class="flex items-center justify-between">
|
||||||
|
|||||||
@@ -187,12 +187,15 @@ const startIdeaFromInspiration = (inspiration) => {
|
|||||||
|
|
||||||
// --- Generation Settings ---
|
// --- Generation Settings ---
|
||||||
const prompt = ref('')
|
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 selectedModel = ref('flux-schnell')
|
||||||
const quality = ref({ key: 'TWOK', value: '2K' })
|
const quality = ref({ key: 'TWOK', value: '2K' })
|
||||||
const aspectRatio = ref({ key: 'NINESIXTEEN', value: '9:16' })
|
const aspectRatio = ref({ key: 'NINESIXTEEN', value: '9:16' })
|
||||||
const imageCount = ref(1)
|
const imageCount = ref(1)
|
||||||
const sendToTelegram = ref(false)
|
|
||||||
const telegramId = ref('')
|
|
||||||
const useProfileImage = ref(true)
|
const useProfileImage = ref(true)
|
||||||
const useEnvironment = ref(false)
|
const useEnvironment = ref(false)
|
||||||
const isSubmittingGen = ref(false)
|
const isSubmittingGen = ref(false)
|
||||||
@@ -232,7 +235,6 @@ watch(selectedCharacter, (newChar) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const qualityOptions = ref([
|
const qualityOptions = ref([
|
||||||
{ key: 'ONEK', value: '1K' },
|
|
||||||
{ key: 'TWOK', value: '2K' },
|
{ key: 'TWOK', value: '2K' },
|
||||||
{ key: 'FOURK', value: '4K' }
|
{ key: 'FOURK', value: '4K' }
|
||||||
])
|
])
|
||||||
@@ -256,13 +258,12 @@ const restoreSettings = () => {
|
|||||||
if (!stored) return
|
if (!stored) return
|
||||||
try {
|
try {
|
||||||
const s = JSON.parse(stored)
|
const s = JSON.parse(stored)
|
||||||
|
if (s.model) model.value = s.model
|
||||||
if (s.prompt) prompt.value = s.prompt
|
if (s.prompt) prompt.value = s.prompt
|
||||||
if (s.quality) quality.value = s.quality
|
if (s.quality) quality.value = s.quality
|
||||||
if (s.aspectRatio) aspectRatio.value = s.aspectRatio
|
if (s.aspectRatio) aspectRatio.value = s.aspectRatio
|
||||||
if (s.imageCount) imageCount.value = Math.min(s.imageCount, 4)
|
if (s.imageCount) imageCount.value = Math.min(s.imageCount, 4)
|
||||||
if (s.selectedModel) selectedModel.value = s.selectedModel
|
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.useProfileImage !== undefined) useProfileImage.value = s.useProfileImage
|
||||||
if (s.useEnvironment !== undefined) useEnvironment.value = s.useEnvironment
|
if (s.useEnvironment !== undefined) useEnvironment.value = s.useEnvironment
|
||||||
_savedCharacterId = s.selectedCharacterId || null
|
_savedCharacterId = s.selectedCharacterId || null
|
||||||
@@ -283,12 +284,11 @@ const restoreSettings = () => {
|
|||||||
const saveSettings = () => {
|
const saveSettings = () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
prompt: prompt.value,
|
prompt: prompt.value,
|
||||||
|
model: model.value,
|
||||||
quality: quality.value,
|
quality: quality.value,
|
||||||
aspectRatio: aspectRatio.value,
|
aspectRatio: aspectRatio.value,
|
||||||
imageCount: imageCount.value,
|
imageCount: imageCount.value,
|
||||||
selectedModel: selectedModel.value,
|
selectedModel: selectedModel.value,
|
||||||
sendToTelegram: sendToTelegram.value,
|
|
||||||
telegramId: telegramId.value,
|
|
||||||
useProfileImage: useProfileImage.value,
|
useProfileImage: useProfileImage.value,
|
||||||
useEnvironment: useEnvironment.value,
|
useEnvironment: useEnvironment.value,
|
||||||
selectedCharacterId: selectedCharacter.value?.id || null,
|
selectedCharacterId: selectedCharacter.value?.id || null,
|
||||||
@@ -296,10 +296,9 @@ const saveSettings = () => {
|
|||||||
selectedAssetIds: selectedAssets.value.map(a => a.id),
|
selectedAssetIds: selectedAssets.value.map(a => a.id),
|
||||||
}
|
}
|
||||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings))
|
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 () => {
|
onMounted(async () => {
|
||||||
restoreSettings()
|
restoreSettings()
|
||||||
@@ -834,18 +833,37 @@ const handleAssetPickerUpload = async (event) => {
|
|||||||
|
|
||||||
<!-- RIGHT COLUMN: Settings & Button -->
|
<!-- RIGHT COLUMN: Settings & Button -->
|
||||||
<div class="w-full lg:w-72 flex flex-col gap-2">
|
<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">
|
<div class="flex flex-col gap-1">
|
||||||
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Quality</label>
|
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Quality</label>
|
||||||
<Dropdown v-model="quality" :options="qualityOptions" optionLabel="value"
|
<div class="flex bg-slate-800 p-1 rounded-xl border border-white/10 h-[34px]">
|
||||||
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl"
|
<div v-for="option in qualityOptions" :key="option.key" @click="quality = option"
|
||||||
: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="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>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Ratio</label>
|
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Ratio</label>
|
||||||
<Dropdown v-model="aspectRatio" :options="aspectRatioOptions" optionLabel="value"
|
<Dropdown v-model="aspectRatio" :options="aspectRatioOptions" optionLabel="value"
|
||||||
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl"
|
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl !h-[34px]"
|
||||||
: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' } }" />
|
: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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -863,19 +881,6 @@ const handleAssetPickerUpload = async (event) => {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="mt-auto">
|
||||||
<Button :label="isSubmittingGen ? 'Starting...' : 'Generate New Session'"
|
<Button :label="isSubmittingGen ? 'Starting...' : 'Generate New Session'"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Dialog from 'primevue/dialog'
|
|||||||
import Paginator from 'primevue/paginator'
|
import Paginator from 'primevue/paginator'
|
||||||
import InputText from 'primevue/inputtext'
|
import InputText from 'primevue/inputtext'
|
||||||
import Tag from 'primevue/tag'
|
import Tag from 'primevue/tag'
|
||||||
|
import Dropdown from 'primevue/dropdown'
|
||||||
import GenerationPreviewModal from '../components/GenerationPreviewModal.vue'
|
import GenerationPreviewModal from '../components/GenerationPreviewModal.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -44,22 +45,17 @@ const assetsTotalRecords = ref(0)
|
|||||||
const assetsRows = ref(12)
|
const assetsRows = ref(12)
|
||||||
const assetsFirst = ref(0)
|
const assetsFirst = ref(0)
|
||||||
const activeAssetFilter = ref('all')
|
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 isUploading = ref(false)
|
||||||
const fileInput = ref(null)
|
const fileInput = ref(null)
|
||||||
|
|
||||||
const saveTelegramId = () => {
|
const model = ref({ key: 'gemini-3-pro-image-preview', value: 'Pro' })
|
||||||
if (telegramId.value) {
|
const modelOptions = ref([
|
||||||
localStorage.setItem('telegram_id', telegramId.value)
|
{ key: 'gemini-3.1-flash-image-preview', value: '2' },
|
||||||
isTelegramIdSaved.value = true
|
{ key: 'gemini-3-pro-image-preview', value: 'Pro' }
|
||||||
}
|
])
|
||||||
}
|
|
||||||
|
|
||||||
const quality = ref({ key: 'TWOK', value: '2K' })
|
const quality = ref({ key: 'TWOK', value: '2K' })
|
||||||
const qualityOptions = ref([
|
const qualityOptions = ref([
|
||||||
{ key: 'ONEK', value: '1K' },
|
|
||||||
{ key: 'TWOK', value: '2K' },
|
{ key: 'TWOK', value: '2K' },
|
||||||
{ key: 'FOURK', value: '4K' }
|
{ key: 'FOURK', value: '4K' }
|
||||||
])
|
])
|
||||||
@@ -186,18 +182,6 @@ const onFileSelected = async (event) => {
|
|||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!prompt.value.trim()) return
|
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
|
isGenerating.value = true
|
||||||
generationSuccess.value = false
|
generationSuccess.value = false
|
||||||
generationError.value = null
|
generationError.value = null
|
||||||
@@ -207,12 +191,12 @@ const handleGenerate = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
model: model.value.key,
|
||||||
aspect_ratio: aspectRatio.value.key,
|
aspect_ratio: aspectRatio.value.key,
|
||||||
quality: quality.value.key,
|
quality: quality.value.key,
|
||||||
prompt: prompt.value,
|
prompt: prompt.value,
|
||||||
assets_list: selectedAssets.value.map(a => a.id),
|
assets_list: selectedAssets.value.map(a => a.id),
|
||||||
linked_character_id: null, // Explicitly null for global generation
|
linked_character_id: null, // Explicitly null for global generation
|
||||||
telegram_id: sendToTelegram.value ? telegramId.value : null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await aiService.runGeneration(payload)
|
const response = await aiService.runGeneration(payload)
|
||||||
@@ -294,6 +278,9 @@ const pollStatus = async (id) => {
|
|||||||
const restoreGeneration = async (gen) => {
|
const restoreGeneration = async (gen) => {
|
||||||
prompt.value = gen.prompt
|
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)
|
const foundQuality = qualityOptions.value.find(opt => opt.key === gen.quality)
|
||||||
if (foundQuality) quality.value = foundQuality
|
if (foundQuality) quality.value = foundQuality
|
||||||
|
|
||||||
@@ -461,13 +448,23 @@ onMounted(() => {
|
|||||||
<!-- Settings Card -->
|
<!-- Settings Card -->
|
||||||
<div class="glass-panel p-6 rounded-2xl border border-white/5 bg-white/5 flex flex-col gap-6">
|
<div class="glass-panel p-6 rounded-2xl border border-white/5 bg-white/5 flex flex-col gap-6">
|
||||||
|
|
||||||
<!-- Quality & Aspect Ratio -->
|
<!-- Settings Row: Model, Quality & Aspect Ratio -->
|
||||||
<div class="grid grid-cols-2 gap-6">
|
<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">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider">Quality</label>
|
<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"
|
<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'">
|
:class="quality.key === option.key ? 'bg-white/10 text-white shadow-sm' : 'text-slate-500 hover:text-slate-300'">
|
||||||
{{ option.value }}
|
{{ option.value }}
|
||||||
</div>
|
</div>
|
||||||
@@ -476,14 +473,14 @@ onMounted(() => {
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider">Aspect
|
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider">Aspect
|
||||||
Ratio</label>
|
Ratio</label>
|
||||||
<div class="flex bg-slate-900/50 p-1 rounded-lg border border-white/10">
|
<Dropdown v-model="aspectRatio" :options="aspectRatioOptions" optionLabel="value"
|
||||||
<div v-for="option in aspectRatioOptions" :key="option.key"
|
class="w-full !bg-slate-900/50 !border-white/10 !text-white !rounded-lg !h-[34px]"
|
||||||
@click="aspectRatio = option"
|
:pt="{
|
||||||
class="flex-1 text-center py-1.5 cursor-pointer rounded-md text-xs font-medium transition-all"
|
input: { class: '!text-white !text-[10px] !py-1 !px-2 !font-bold' },
|
||||||
:class="aspectRatio.key === option.key ? 'bg-white/10 text-white shadow-sm' : 'text-slate-500 hover:text-slate-300'">
|
trigger: { class: '!text-slate-400 !w-6' },
|
||||||
{{ option.value }}
|
panel: { class: '!bg-slate-900 !border-white/10' },
|
||||||
</div>
|
item: { class: '!text-slate-300 hover:!bg-white/10 hover:!text-white !text-[10px] !py-1' }
|
||||||
</div>
|
}" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -551,21 +548,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
<!-- Generate Button -->
|
<!-- Generate Button -->
|
||||||
<div class="mt-auto">
|
<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'"
|
<Button :label="isGenerating ? 'Generating...' : 'Generate Image'"
|
||||||
:icon="isGenerating ? 'pi pi-spin pi-spinner' : 'pi pi-magic'" :loading="isGenerating"
|
:icon="isGenerating ? 'pi pi-spin pi-spinner' : 'pi pi-magic'" :loading="isGenerating"
|
||||||
@click="handleGenerate"
|
@click="handleGenerate"
|
||||||
|
|||||||
Reference in New Issue
Block a user