This commit is contained in:
xds
2026-02-19 21:25:48 +03:00
parent 6de5ded2fa
commit 741857de92
5 changed files with 807 additions and 157 deletions

View File

@@ -76,6 +76,8 @@ const selectedModel = ref('flux-schnell')
// Character & Assets (declared early for settings persistence)
const characters = ref([])
const selectedCharacter = ref(null)
const environments = ref([])
const selectedEnvironment = ref(null)
const selectedAssets = ref([])
const showAssetPicker = ref(false) // Deprecated, using isAssetPickerVisible
@@ -90,6 +92,31 @@ const useProfileImage = ref(true)
const isImprovingPrompt = ref(false)
const previousPrompt = ref('')
let _savedCharacterId = null
let _savedEnvironmentId = null
const loadEnvironments = async (charId) => {
if (!charId) {
environments.value = []
selectedEnvironment.value = null
return
}
try {
const response = await dataService.getEnvironments(charId)
environments.value = Array.isArray(response) ? response : (response.environments || [])
if (_savedEnvironmentId) {
selectedEnvironment.value = environments.value.find(e => (e.id === _savedEnvironmentId || e._id === _savedEnvironmentId)) || null
_savedEnvironmentId = null
}
} catch (e) {
console.error('Failed to load environments', e)
environments.value = []
}
}
watch(selectedCharacter, (newChar) => {
loadEnvironments(newChar?.id || newChar?._id)
})
// --- Persist settings to localStorage on change ---
const saveSettings = () => {
@@ -102,7 +129,8 @@ const saveSettings = () => {
sendToTelegram: sendToTelegram.value,
telegramId: telegramId.value,
useProfileImage: useProfileImage.value,
selectedCharacterId: selectedCharacter.value?.id || null,
selectedCharacterId: selectedCharacter.value?.id || selectedCharacter.value?._id || null,
selectedEnvironmentId: selectedEnvironment.value?.id || selectedEnvironment.value?._id || null,
selectedAssetIds: selectedAssets.value.map(a => a.id),
}
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings))
@@ -125,6 +153,7 @@ const restoreSettings = () => {
telegramId.value = s.telegramId || localStorage.getItem('telegram_id') || ''
if (s.useProfileImage !== undefined) useProfileImage.value = s.useProfileImage
_savedCharacterId = s.selectedCharacterId || null
_savedEnvironmentId = s.selectedEnvironmentId || null
if (s.selectedAssetIds && s.selectedAssetIds.length > 0) {
selectedAssets.value = s.selectedAssetIds.map(id => ({
id,
@@ -138,7 +167,7 @@ const restoreSettings = () => {
}
restoreSettings()
watch([prompt, quality, aspectRatio, imageCount, selectedModel, sendToTelegram, telegramId, useProfileImage, selectedCharacter, selectedAssets], saveSettings, { deep: true })
watch([prompt, quality, aspectRatio, imageCount, selectedModel, sendToTelegram, telegramId, useProfileImage, selectedCharacter, selectedEnvironment, selectedAssets], saveSettings, { deep: true })
const viewMode = ref('feed') // 'feed' or 'gallery'
const isSubmitting = ref(false)
@@ -203,7 +232,7 @@ const loadCharacters = async () => {
characters.value = await dataService.getCharacters()
// Restore saved character selection after characters are loaded
if (_savedCharacterId && characters.value.length > 0) {
const found = characters.value.find(c => c.id === _savedCharacterId)
const found = characters.value.find(c => (c.id === _savedCharacterId || c._id === _savedCharacterId))
if (found) selectedCharacter.value = found
_savedCharacterId = null
}
@@ -259,7 +288,8 @@ const handleGenerate = async () => {
aspect_ratio: aspectRatio.value.key,
quality: quality.value.key,
assets_list: selectedAssets.value.map(a => a.id),
linked_character_id: selectedCharacter.value?.id || null,
linked_character_id: selectedCharacter.value?.id || selectedCharacter.value?._id || null,
environment_id: selectedEnvironment.value?.id || selectedEnvironment.value?._id || null,
telegram_id: sendToTelegram.value ? telegramId.value : null,
use_profile_image: selectedCharacter.value ? useProfileImage.value : false,
count: imageCount.value,
@@ -1145,75 +1175,115 @@ watch(viewMode, (v) => {
class="w-full !h-28 bg-slate-800 !text-sm border-white/10 text-white rounded-lg p-2 focus:border-violet-500 focus:ring-1 focus:ring-violet-500/50 transition-all resize-none shadow-inner" />
</div>
<div class="flex flex-col md:flex-row gap-2">
<div class="flex-1 flex flex-col gap-1">
<label
class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Character</label>
<Dropdown v-model="selectedCharacter" :options="characters" optionLabel="name"
placeholder="Select Character" filter showClear
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl" :pt="{
root: { class: '!bg-slate-800' },
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' }
}">
<template #value="slotProps">
<div v-if="slotProps.value" class="flex items-center gap-2">
<img v-if="slotProps.value.avatar_image"
:src="API_URL + slotProps.value.avatar_image"
class="w-6 h-6 rounded-full object-cover" />
<span>{{ slotProps.value.name }}</span>
</div>
<span v-else>{{ slotProps.placeholder }}</span>
</template>
<template #option="slotProps">
<div class="flex items-center gap-2">
<img v-if="slotProps.option.avatar_image"
:src="API_URL + slotProps.option.avatar_image"
class="w-8 h-8 rounded-full object-cover" />
<span>{{ slotProps.option.name }}</span>
</div>
</template>
</Dropdown>
<div v-if="selectedCharacter"
class="flex items-center gap-2 mt-2 px-1 animate-in fade-in slide-in-from-top-1">
<Checkbox v-model="useProfileImage" :binary="true" inputId="idea-use-profile-img"
class="!border-white/20" :pt="{
box: ({ props, state }) => ({
class: ['!bg-slate-800 !border-white/20', { '!bg-violet-600 !border-violet-600': props.modelValue }]
})
}" />
<label for="idea-use-profile-img"
class="text-xs text-slate-300 cursor-pointer select-none">Use
Character
Photo</label>
</div>
</div>
<div class="flex-1 flex flex-col gap-1">
<label
class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Assets</label>
<div @click="openAssetPicker"
class="w-full bg-slate-800 border border-white/10 rounded-lg p-2 min-h-[38px] cursor-pointer hover:bg-slate-700/50 transition-colors flex flex-wrap gap-1.5">
<span v-if="selectedAssets.length === 0"
class="text-slate-400 text-sm py-0.5">Select
Assets</span>
<div v-for="asset in selectedAssets" :key="asset.id"
class="px-2 py-1 bg-violet-600/30 border border-violet-500/30 text-violet-200 text-xs rounded-md flex items-center gap-2 animate-in fade-in zoom-in duration-200"
@click.stop>
<img v-if="asset.url" :src="API_URL + asset.url + '?thumbnail=true'"
class="w-4 h-4 rounded object-cover" />
<span class="truncate max-w-[100px]">{{ asset.name || 'Asset ' + (asset.id ?
asset.id.substring(0, 4) : '') }}</span>
<i class="pi pi-times cursor-pointer hover:text-white"
@click.stop="removeAsset(asset)"></i>
</div>
</div>
</div>
</div>
</div>
<div class="flex flex-col md:flex-row gap-2">
<div class="flex-1 flex flex-col gap-1">
<label
class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Character</label>
<Dropdown v-model="selectedCharacter" :options="characters" optionLabel="name"
placeholder="Select Character" filter showClear
class="w-full !bg-slate-800 !border-white/10 !text-white !rounded-xl" :pt="{
root: { class: '!bg-slate-800' },
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' }
}">
<template #value="slotProps">
<div v-if="slotProps.value" class="flex items-center gap-2">
<img v-if="slotProps.value.avatar_image"
:src="API_URL + slotProps.value.avatar_image"
class="w-6 h-6 rounded-full object-cover" />
<span>{{ slotProps.value.name }}</span>
</div>
<span v-else>{{ slotProps.placeholder }}</span>
</template>
<template #option="slotProps">
<div class="flex items-center gap-2">
<img v-if="slotProps.option.avatar_image"
:src="API_URL + slotProps.option.avatar_image"
class="w-8 h-8 rounded-full object-cover" />
<span>{{ slotProps.option.name }}</span>
</div>
</template>
</Dropdown>
<div v-if="selectedCharacter"
class="flex items-center gap-2 mt-2 px-1 animate-in fade-in slide-in-from-top-1">
<Checkbox v-model="useProfileImage" :binary="true" inputId="idea-use-profile-img"
class="!border-white/20" :pt="{
box: ({ props, state }) => ({
class: ['!bg-slate-800 !border-white/20', { '!bg-violet-600 !border-violet-600': props.modelValue }]
})
}" />
<label for="idea-use-profile-img"
class="text-xs text-slate-300 cursor-pointer select-none">Use
Character
Photo</label>
</div>
</div>
<div class="flex-1 flex flex-col gap-1">
<label
class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Assets</label>
<div @click="openAssetPicker"
class="w-full bg-slate-800 border border-white/10 rounded-lg p-2 min-h-[38px] cursor-pointer hover:bg-slate-700/50 transition-colors flex flex-wrap gap-1.5">
<span v-if="selectedAssets.length === 0"
class="text-slate-400 text-sm py-0.5">Select
Assets</span>
<div v-for="asset in selectedAssets" :key="asset.id"
class="px-2 py-1 bg-violet-600/30 border border-violet-500/30 text-violet-200 text-xs rounded-md flex items-center gap-2 animate-in fade-in zoom-in duration-200"
@click.stop>
<img v-if="asset.url" :src="API_URL + asset.url + '?thumbnail=true'"
class="w-4 h-4 rounded object-cover" />
<span class="truncate max-w-[100px]">{{ asset.name || 'Asset ' + (asset.id ?
asset.id.substring(0, 4) : '') }}</span>
<i class="pi pi-times cursor-pointer hover:text-white"
@click.stop="removeAsset(asset)"></i>
</div>
</div>
</div>
</div>
<!-- Environment Row (Below) -->
<div v-if="selectedCharacter" class="flex-1 flex flex-col gap-1 animate-in fade-in slide-in-from-top-1">
<div class="flex justify-between items-center">
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Environment</label>
<Button v-if="selectedEnvironment" icon="pi pi-times" @click="selectedEnvironment = null" text size="small"
class="!p-0 !h-4 !w-4 !text-[8px] text-slate-500 hover:text-white" />
</div>
<div v-if="environments.length > 0" class="flex gap-2 overflow-x-auto pb-1 custom-scrollbar no-scrollbar">
<div v-for="env in environments" :key="env.id || env._id"
@click="selectedEnvironment = env"
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.1)]'
: 'border-white/5 hover:border-white/20'
]"
>
<div class="w-6 h-6 rounded overflow-hidden flex-shrink-0 bg-slate-900 flex items-center justify-center border border-white/5">
<img v-if="env.asset_ids?.length > 0"
:src="API_URL + '/assets/' + env.asset_ids[0] + '?thumbnail=true'"
class="w-full h-full object-cover"
/>
<i v-else class="pi pi-map-marker text-[10px]"
:class="(selectedEnvironment?.id === (env.id || env._id) || selectedEnvironment?._id === (env.id || env._id)) ? 'text-violet-400' : 'text-slate-500'"
></i>
</div>
<span class="text-[10px] whitespace-nowrap pr-1"
:class="(selectedEnvironment?.id === (env.id || env._id) || selectedEnvironment?._id === (env.id || env._id)) ? 'text-violet-300 font-bold' : 'text-slate-400 group-hover:text-slate-200'"
>
{{ env.name }}
</span>
<i v-if="(selectedEnvironment?.id === (env.id || env._id) || selectedEnvironment?._id === (env.id || env._id))"
class="pi pi-check text-violet-400 text-[8px]"></i>
</div>
</div>
<div v-else class="py-2 px-3 bg-slate-800/50 border border-white/5 rounded-xl text-center">
<p class="text-[9px] text-slate-600 uppercase m-0">No environments</p>
</div>
</div> </div>
<div class="w-full lg:w-72 flex flex-col gap-2">
<div class="grid grid-cols-2 gap-2">