+ env
This commit is contained in:
@@ -35,12 +35,14 @@ export const dataService = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
getAssetsByCharacterId: async (charId, limit, offset) => {
|
||||
const response = await api.get(`/characters/${charId}/assets`, { params: { limit, offset } })
|
||||
getAssetsByCharacterId: async (charId, limit, offset, type) => {
|
||||
const params = { limit, offset }
|
||||
if (type && type !== 'all') params.type = type
|
||||
const response = await api.get(`/characters/${charId}/assets`, { params })
|
||||
return response.data
|
||||
},
|
||||
|
||||
uploadAsset: async (file, linkedCharId) => {
|
||||
uploadAsset: async (file, linkedCharId, onProgress) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
if (linkedCharId) formData.append('linked_char_id', linkedCharId)
|
||||
@@ -48,6 +50,12 @@ export const dataService = {
|
||||
const response = await api.post('/assets/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (onProgress && progressEvent.total) {
|
||||
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||
onProgress(percentCompleted)
|
||||
}
|
||||
}
|
||||
})
|
||||
return response.data
|
||||
@@ -63,6 +71,28 @@ export const dataService = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Environments
|
||||
getEnvironments: async (characterId) => {
|
||||
if (!characterId) return []
|
||||
const response = await api.get(`/environments/character/${characterId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
createEnvironment: async (envData) => {
|
||||
const response = await api.post('/environments/', envData)
|
||||
return response.data
|
||||
},
|
||||
|
||||
updateEnvironment: async (id, envData) => {
|
||||
const response = await api.put(`/environments/${id}`, envData)
|
||||
return response.data
|
||||
},
|
||||
|
||||
deleteEnvironment: async (id) => {
|
||||
const response = await api.delete(`/environments/${id}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
generatePromptFromImage: async (files, prompt) => {
|
||||
const formData = new FormData()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import {computed, onMounted, ref} from 'vue'
|
||||
import {computed, onMounted, ref, watch, nextTick} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {dataService} from '../services/dataService'
|
||||
import {aiService} from '../services/aiService'
|
||||
@@ -19,11 +19,14 @@ 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'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const character = ref(null)
|
||||
const characterAssets = ref([])
|
||||
const environments = ref([])
|
||||
const assetsTotalRecords = ref(0)
|
||||
const historyGenerations = ref([])
|
||||
const historyTotal = ref(0)
|
||||
@@ -32,6 +35,197 @@ const historyFirst = ref(0)
|
||||
const loading = ref(true)
|
||||
const API_URL = import.meta.env.VITE_API_URL
|
||||
|
||||
const selectedEnvironment = ref(null)
|
||||
const isEnvModalVisible = ref(false)
|
||||
const isEnvAssetPickerVisible = ref(false)
|
||||
const isDeletingEnv = ref(false)
|
||||
const envForm = ref({
|
||||
name: '',
|
||||
asset_ids: []
|
||||
})
|
||||
const editingEnvId = ref(null)
|
||||
|
||||
// --- Env Asset Picker State ---
|
||||
const envModalAssets = ref([])
|
||||
const envAssetPickerTab = ref('all') // 'all', 'uploaded', 'generated'
|
||||
const envModalFirst = ref(0)
|
||||
const envModalRows = ref(20)
|
||||
const envModalTotal = ref(0)
|
||||
const isEnvModalLoading = ref(false)
|
||||
const envAssetScrollContainer = ref(null)
|
||||
const envAssetPickerFileInput = ref(null)
|
||||
const envUploadProgress = ref(0)
|
||||
const isEnvUploading = ref(false)
|
||||
let envAssetObserver = null
|
||||
|
||||
const envSelectedAssets = computed(() => {
|
||||
// We check against all known assets or just the ones in picker
|
||||
// Since picker is for the character, we can look into characterAssets too
|
||||
return [...characterAssets.value, ...envModalAssets.value]
|
||||
.filter((a, index, self) => self.findIndex(t => t.id === a.id) === index) // Unique
|
||||
.filter(a => envForm.value.asset_ids.includes(a.id))
|
||||
})
|
||||
|
||||
const loadEnvModalAssets = async (isNewTab = false) => {
|
||||
if (isEnvModalLoading.value) return
|
||||
isEnvModalLoading.value = true
|
||||
|
||||
if (isNewTab) {
|
||||
envModalFirst.value = 0
|
||||
envModalAssets.value = []
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await dataService.getAssetsByCharacterId(
|
||||
route.params.id,
|
||||
envModalRows.value,
|
||||
envModalFirst.value,
|
||||
envAssetPickerTab.value
|
||||
)
|
||||
|
||||
if (response && response.assets) {
|
||||
envModalAssets.value = [...envModalAssets.value, ...response.assets]
|
||||
envModalTotal.value = response.total_count || 0
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load env modal assets', e)
|
||||
} finally {
|
||||
isEnvModalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnvAssetInfiniteScroll = (entries) => {
|
||||
if (entries[0].isIntersecting && !isEnvModalLoading.value && envModalAssets.value.length < envModalTotal.value) {
|
||||
envModalFirst.value += envModalRows.value
|
||||
loadEnvModalAssets()
|
||||
}
|
||||
}
|
||||
|
||||
watch(isEnvAssetPickerVisible, (visible) => {
|
||||
if (visible) {
|
||||
loadEnvModalAssets(true)
|
||||
nextTick(() => {
|
||||
if (envAssetObserver) envAssetObserver.disconnect()
|
||||
|
||||
envAssetObserver = new IntersectionObserver(handleEnvAssetInfiniteScroll, {
|
||||
root: envAssetScrollContainer.value,
|
||||
rootMargin: '100px',
|
||||
threshold: 0.1
|
||||
})
|
||||
|
||||
if (envAssetScrollSentinel.value) {
|
||||
envAssetObserver.observe(envAssetScrollSentinel.value)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (envAssetObserver) envAssetObserver.disconnect()
|
||||
}
|
||||
})
|
||||
|
||||
watch(envAssetPickerTab, () => {
|
||||
loadEnvModalAssets(true)
|
||||
})
|
||||
|
||||
const toggleEnvAssetSelection = (id) => {
|
||||
const idx = envForm.value.asset_ids.indexOf(id)
|
||||
if (idx > -1) envForm.value.asset_ids.splice(idx, 1)
|
||||
else envForm.value.asset_ids.push(id)
|
||||
}
|
||||
|
||||
const triggerEnvAssetUpload = () => {
|
||||
if (envAssetPickerFileInput.value) envAssetPickerFileInput.value.click()
|
||||
}
|
||||
|
||||
const handleEnvAssetUpload = async (event) => {
|
||||
const file = event.target.files[0]
|
||||
if (!file) return
|
||||
|
||||
isEnvUploading.value = true
|
||||
envUploadProgress.value = 0
|
||||
try {
|
||||
const response = await dataService.uploadAsset(file, route.params.id, (progress) => {
|
||||
envUploadProgress.value = progress
|
||||
})
|
||||
// Switch to uploaded tab to see the new asset
|
||||
envAssetPickerTab.value = 'uploaded'
|
||||
await loadEnvModalAssets(true)
|
||||
|
||||
// Auto-select the newly uploaded asset
|
||||
if (response && response.id) {
|
||||
if (!envForm.value.asset_ids.includes(response.id)) {
|
||||
envForm.value.asset_ids.push(response.id)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to upload asset in environment modal', e)
|
||||
} finally {
|
||||
isEnvUploading.value = false
|
||||
envUploadProgress.value = 0
|
||||
if (event.target) event.target.value = '' // Clear input
|
||||
}
|
||||
}
|
||||
|
||||
const removeEnvAsset = (id) => {
|
||||
const idx = envForm.value.asset_ids.indexOf(id)
|
||||
if (idx > -1) envForm.value.asset_ids.splice(idx, 1)
|
||||
}
|
||||
|
||||
const loadEnvironments = async () => {
|
||||
try {
|
||||
const response = await dataService.getEnvironments(route.params.id)
|
||||
environments.value = Array.isArray(response) ? response : (response.environments || [])
|
||||
} catch (e) {
|
||||
console.error('Failed to load environments', e)
|
||||
}
|
||||
}
|
||||
|
||||
const openEnvModal = (env = null) => {
|
||||
if (env) {
|
||||
editingEnvId.value = env.id || env._id
|
||||
envForm.value = {
|
||||
name: env.name,
|
||||
asset_ids: env.asset_ids || []
|
||||
}
|
||||
} else {
|
||||
editingEnvId.value = null
|
||||
envForm.value = {
|
||||
name: '',
|
||||
asset_ids: []
|
||||
}
|
||||
}
|
||||
isEnvModalVisible.value = true
|
||||
}
|
||||
|
||||
const saveEnvironment = async () => {
|
||||
try {
|
||||
const payload = {
|
||||
...envForm.value,
|
||||
character_id: route.params.id
|
||||
}
|
||||
if (editingEnvId.value) {
|
||||
await dataService.updateEnvironment(editingEnvId.value, payload)
|
||||
} else {
|
||||
await dataService.createEnvironment(payload)
|
||||
}
|
||||
isEnvModalVisible.value = false
|
||||
editingEnvId.value = null
|
||||
await loadEnvironments()
|
||||
} catch (e) {
|
||||
console.error('Failed to save environment', e)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteEnvironment = async (id) => {
|
||||
if (!confirm('Are you sure you want to delete this environment?')) return
|
||||
try {
|
||||
await dataService.deleteEnvironment(id)
|
||||
if (selectedEnvironment.value?.id === id || selectedEnvironment.value?._id === id) selectedEnvironment.value = null
|
||||
await loadEnvironments()
|
||||
} catch (e) {
|
||||
console.error('Failed to delete environment', e)
|
||||
}
|
||||
}
|
||||
|
||||
const selectedAsset = ref(null)
|
||||
const isModalVisible = ref(false)
|
||||
const activeTab = ref("0")
|
||||
@@ -130,10 +324,11 @@ const loadData = async () => {
|
||||
loading.value = true
|
||||
const charId = route.params.id
|
||||
try {
|
||||
const [char, assetsResponse, historyResponse] = await Promise.all([
|
||||
const [char, assetsResponse, historyResponse, envsResponse] = await Promise.all([
|
||||
dataService.getCharacterById(charId),
|
||||
dataService.getAssetsByCharacterId(charId, assetsRows.value, assetsFirst.value),
|
||||
aiService.getGenerations(historyRows.value, historyFirst.value, charId)
|
||||
aiService.getGenerations(historyRows.value, historyFirst.value, charId),
|
||||
dataService.getEnvironments(charId)
|
||||
])
|
||||
character.value = char
|
||||
|
||||
@@ -145,6 +340,8 @@ const loadData = async () => {
|
||||
assetsTotalRecords.value = characterAssets.value.length
|
||||
}
|
||||
|
||||
environments.value = Array.isArray(envsResponse) ? envsResponse : (envsResponse.environments || [])
|
||||
|
||||
if (historyResponse && historyResponse.generations) {
|
||||
historyGenerations.value = historyResponse.generations
|
||||
historyTotal.value = historyResponse.total_count || 0
|
||||
@@ -590,6 +787,7 @@ const handleGenerate = async () => {
|
||||
|
||||
const payload = {
|
||||
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,
|
||||
@@ -702,6 +900,12 @@ const handleGenerate = async () => {
|
||||
<span>Assets ({{ assetsTotalRecords }})</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="envs">
|
||||
<div class="!flex !flex-row !gap-1">
|
||||
<i class="pi pi-map-marker text-[10px]" />
|
||||
<span>Environments ({{ environments.length }})</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="2" class="hidden">
|
||||
<div class="!flex !flex-row !gap-1">
|
||||
<i class="pi pi-history text-[10px]" />
|
||||
@@ -762,32 +966,6 @@ const handleGenerate = async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label
|
||||
class="text-slate-400 text-[9px] font-semibold uppercase tracking-wider">Description</label>
|
||||
|
||||
<div class="relative w-full">
|
||||
<Textarea v-model="prompt" rows="3" autoResize placeholder="Describe..."
|
||||
class="w-full bg-slate-900 border-white/10 text-white rounded-lg p-2 focus:border-violet-500 transition-all text-xs pr-16" />
|
||||
|
||||
<div class="absolute top-1.5 right-1.5 flex gap-1">
|
||||
<Button v-if="previousPrompt" icon="pi pi-undo"
|
||||
class="!p-1 !w-6 !h-6 !text-[10px] bg-slate-800 hover:bg-slate-700 border-white/10 text-slate-300"
|
||||
@click="undoImprovePrompt" v-tooltip.top="'Rollback'" />
|
||||
|
||||
<Button icon="pi pi-clipboard"
|
||||
class="!p-1 !w-6 !h-6 !text-[10px] bg-slate-800 hover:bg-slate-700 border-white/10 text-slate-400"
|
||||
@click="pastePrompt" v-tooltip.top="'Paste'" />
|
||||
|
||||
<Button icon="pi pi-sparkles" :loading="isImprovingPrompt"
|
||||
:disabled="prompt.length <= 10"
|
||||
class="!p-1 !w-6 !h-6 !text-[10px] bg-violet-600/20 hover:bg-violet-600/30 border-violet-500/30 text-violet-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="handleImprovePrompt"
|
||||
v-tooltip.top="prompt.length <= 10 ? 'Enter at least 10 characters' : 'Improve prompt'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assets Selection -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex justify-between items-center">
|
||||
@@ -816,6 +994,46 @@ const handleGenerate = async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Environment Selection -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex justify-between items-center">
|
||||
<label class="text-slate-400 text-[9px] font-semibold 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" v-tooltip.top="'Clear'" />
|
||||
</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-900/30"
|
||||
: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-800 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 text-center border border-dashed border-white/5 rounded-lg">
|
||||
<p class="text-[8px] text-slate-600 uppercase m-0">No environments</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Removed Ref Assets section if characterAssets is empty, but it's already conditional -->
|
||||
|
||||
<div class="flex flex-col gap-1.5 mt-auto pt-1.5 border-t border-white/5">
|
||||
@@ -1217,6 +1435,57 @@ const handleGenerate = async () => {
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value="envs">
|
||||
<div class="glass-panel p-8 rounded-3xl border border-white/5 bg-white/5">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-2xl font-bold m-0">Environments ({{ environments.length }})</h2>
|
||||
<Button label="Create Environment" icon="pi pi-plus" @click="openEnvModal()"
|
||||
class="!py-2 !px-4 !text-sm font-bold bg-violet-600 hover:bg-violet-700 border-none text-white rounded-xl transition-all shadow-lg shadow-violet-500/20" />
|
||||
</div>
|
||||
|
||||
<div v-if="environments.length === 0"
|
||||
class="text-center py-16 text-slate-400 bg-white/[0.02] rounded-2xl">
|
||||
<i class="pi pi-map-marker text-4xl mb-4 opacity-20"></i>
|
||||
<p>No environments defined for this character.</p>
|
||||
<p class="text-xs opacity-60">Environments allow grouping assets into spaces like "Bedroom" or "Kitchen".</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div v-for="env in environments" :key="env.id || env._id"
|
||||
class="glass-panel rounded-2xl overflow-hidden border border-white/5 hover:border-violet-500/30 transition-all duration-300 group bg-white/[0.02]">
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-white mb-1">{{ env.name }}</h3>
|
||||
<p class="text-xs text-slate-400">{{ env.asset_ids?.length || 0 }} assets linked</p>
|
||||
</div>
|
||||
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button icon="pi pi-pencil" text rounded size="small"
|
||||
class="!text-slate-400 hover:!text-violet-400 hover:!bg-violet-500/10"
|
||||
@click="openEnvModal(env)" />
|
||||
<Button icon="pi pi-trash" text rounded size="small"
|
||||
class="!text-slate-400 hover:!text-red-400 hover:!bg-red-500/10"
|
||||
@click="deleteEnvironment(env.id || env._id)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div v-for="assetId in env.asset_ids?.slice(0, 4)" :key="assetId"
|
||||
class="w-10 h-10 rounded-lg overflow-hidden border border-white/10 bg-black/20">
|
||||
<img :src="API_URL + '/assets/' + assetId + '?thumbnail=true'"
|
||||
class="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div v-if="env.asset_ids?.length > 4"
|
||||
class="w-10 h-10 rounded-lg border border-dashed border-white/10 flex items-center justify-center text-[10px] text-slate-500">
|
||||
+{{ env.asset_ids.length - 4 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
@@ -1286,6 +1555,120 @@ const handleGenerate = async () => {
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
<!-- Environment Modal -->
|
||||
<Dialog v-model:visible="isEnvModalVisible" modal :header="editingEnvId ? 'Edit Environment' : 'Create Environment'"
|
||||
:style="{ width: '500px' }" class="glass-panel rounded-2xl">
|
||||
<div class="flex flex-col gap-6 p-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs font-bold text-slate-400 uppercase">Environment Name</label>
|
||||
<InputText v-model="envForm.name" placeholder="e.g. Bedroom, Living Room..." class="w-full" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<label class="text-xs font-bold text-slate-400 uppercase">Linked Assets ({{ envForm.asset_ids.length }})</label>
|
||||
<Button label="Add Asset" icon="pi pi-plus" size="small" text
|
||||
class="!text-[10px] !py-0.5 !px-1.5 text-violet-400 hover:bg-violet-500/10"
|
||||
@click="isEnvAssetPickerVisible = true" />
|
||||
</div>
|
||||
|
||||
<div v-if="envSelectedAssets.length > 0" class="flex flex-wrap gap-2 p-3 bg-slate-900/50 rounded-xl border border-white/5">
|
||||
<div v-for="asset in envSelectedAssets" :key="asset.id"
|
||||
class="relative w-12 h-12 rounded overflow-hidden border border-violet-500/50 group">
|
||||
<img :src="API_URL + asset.url + '?thumbnail=true'"
|
||||
class="w-full h-full object-cover" />
|
||||
<div @click="removeEnvAsset(asset.id)"
|
||||
class="absolute inset-0 bg-black/60 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer">
|
||||
<i class="pi pi-times text-white text-[10px]"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else
|
||||
class="text-center py-6 text-xs text-slate-500 border border-dashed border-white/10 rounded-xl">
|
||||
No assets selected for this environment
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 mt-4">
|
||||
<Button label="Cancel" @click="isEnvModalVisible = false" text class="text-slate-400" />
|
||||
<Button :label="editingEnvId ? 'Update' : 'Create'" @click="saveEnvironment"
|
||||
class="bg-violet-600 hover:bg-violet-700 border-none px-6" />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<!-- Environment Asset Selection Modal (Character-specific) -->
|
||||
<Dialog v-model:visible="isEnvAssetPickerVisible" modal header="Select Character Assets for Environment"
|
||||
:style="{ width: '80vw', maxWidth: '800px' }" class="glass-panel rounded-2xl">
|
||||
<div class="flex flex-col h-[70vh]">
|
||||
<!-- Tabs & Upload -->
|
||||
<div class="flex border-b border-white/5 mb-4 px-2 items-center">
|
||||
<div class="flex flex-1">
|
||||
<button v-for="tab in ['all', 'uploaded', 'generated']" :key="tab" @click="envAssetPickerTab = tab"
|
||||
class="px-4 py-3 text-xs font-medium border-b-2 transition-colors capitalize"
|
||||
:class="envAssetPickerTab === tab ? 'border-violet-500 text-violet-400' : 'border-transparent text-slate-400 hover:text-slate-200'">
|
||||
{{ tab }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="file" ref="envAssetPickerFileInput" @change="handleEnvAssetUpload" class="hidden"
|
||||
accept="image/*" />
|
||||
<Button :label="isEnvModalLoading && envAssetPickerTab === 'uploaded' ? 'Uploading...' : 'Upload'"
|
||||
icon="pi pi-upload" size="small" text
|
||||
class="!text-[10px] !py-1 !px-2 text-violet-400 hover:bg-violet-500/10"
|
||||
@click="triggerEnvAssetUpload" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Progress -->
|
||||
<div v-if="isEnvUploading" class="px-2 mb-4 animate-in fade-in slide-in-from-top-1">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<span class="text-[10px] text-violet-400 font-bold uppercase tracking-wider">Uploading Asset...</span>
|
||||
<span class="text-[10px] text-slate-500 font-mono">{{ envUploadProgress }}%</span>
|
||||
</div>
|
||||
<ProgressBar :value="envUploadProgress" style="height: 4px; width: 100%"
|
||||
:showValue="false" class="rounded-full overflow-hidden !bg-slate-800" :pt="{
|
||||
value: { class: '!bg-gradient-to-r !from-violet-600 !to-cyan-500 !transition-all !duration-300' }
|
||||
}" />
|
||||
</div>
|
||||
|
||||
<div ref="envAssetScrollContainer" class="flex-1 overflow-y-auto p-1 text-slate-100 custom-scrollbar">
|
||||
<div v-if="envModalAssets.length > 0" class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
<div v-for="asset in envModalAssets" :key="asset.id" @click="toggleEnvAssetSelection(asset.id)"
|
||||
class="aspect-square rounded-xl overflow-hidden cursor-pointer relative border transition-all"
|
||||
:class="envForm.asset_ids.includes(asset.id) ? 'border-violet-500 ring-2 ring-violet-500/20' : 'border-white/10 hover:border-white/30'">
|
||||
<img :src="API_URL + asset.url + '?thumbnail=true'"
|
||||
class="w-full h-full object-cover" />
|
||||
<div v-if="envForm.asset_ids.includes(asset.id)"
|
||||
class="absolute inset-0 bg-violet-600/30 flex items-center justify-center">
|
||||
<div class="bg-violet-600 rounded-full p-1 shadow-lg">
|
||||
<i class="pi pi-check text-white text-xs font-bold"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 right-0 p-1 bg-black/60 backdrop-blur-sm">
|
||||
<p class="text-[9px] text-white truncate">{{ asset.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isEnvModalLoading" class="flex flex-col items-center justify-center py-20 text-slate-500 gap-3">
|
||||
<i class="pi pi-images text-4xl opacity-20"></i>
|
||||
<p>No assets found for this character.</p>
|
||||
</div>
|
||||
|
||||
<!-- Infinite Scroll Sentinel -->
|
||||
<div ref="envAssetScrollSentinel" class="w-full h-12 flex items-center justify-center mt-4">
|
||||
<i v-if="isEnvModalLoading" class="pi pi-spin pi-spinner text-violet-500 text-xl"></i>
|
||||
<span v-else-if="envModalAssets.length > 0 && envModalAssets.length >= envModalTotal" class="text-[10px] text-slate-600 italic">
|
||||
All assets loaded
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-4 border-t border-white/10 flex justify-between items-center text-slate-100">
|
||||
<span class="text-sm text-slate-400">{{ envForm.asset_ids.length }} selected</span>
|
||||
<Button label="Done" @click="isEnvAssetPickerVisible = false" class="!px-6" />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -138,6 +138,8 @@ const confirmAddToPlan = async () => {
|
||||
// --- State ---
|
||||
const prompt = ref('')
|
||||
const selectedCharacter = ref(null)
|
||||
const environments = ref([])
|
||||
const selectedEnvironment = ref(null)
|
||||
const selectedAssets = ref([])
|
||||
// Album Picker State
|
||||
const isAlbumPickerVisible = ref(false)
|
||||
@@ -156,8 +158,34 @@ const sendToTelegram = ref(false)
|
||||
const telegramId = ref('')
|
||||
const isTelegramIdSaved = ref(false)
|
||||
const useProfileImage = ref(true)
|
||||
const useEnvironment = ref(false)
|
||||
const isImprovingPrompt = ref(false)
|
||||
const previousPrompt = ref('')
|
||||
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)
|
||||
})
|
||||
|
||||
const characters = ref([])
|
||||
const allAssets = ref([])
|
||||
@@ -195,13 +223,15 @@ const STORAGE_KEY = 'flexible_gen_settings'
|
||||
const saveSettings = () => {
|
||||
const settings = {
|
||||
prompt: prompt.value,
|
||||
selectedCharacterId: selectedCharacter.value?.id,
|
||||
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))
|
||||
@@ -228,7 +258,9 @@ const restoreSettings = () => {
|
||||
telegramId.value = settings.telegramId || localStorage.getItem('telegram_id') || ''
|
||||
if (telegramId.value) isTelegramIdSaved.value = true
|
||||
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)
|
||||
if (settings.selectedEnvironmentId) _savedEnvironmentId = settings.selectedEnvironmentId
|
||||
|
||||
return settings // Return to use in loadData
|
||||
} catch (e) {
|
||||
@@ -239,7 +271,7 @@ const restoreSettings = () => {
|
||||
}
|
||||
|
||||
// Watchers for auto-save
|
||||
watch([prompt, selectedCharacter, selectedAssets, quality, aspectRatio, sendToTelegram, telegramId, useProfileImage, generationCount], () => {
|
||||
watch([prompt, selectedCharacter, selectedEnvironment, selectedAssets, quality, aspectRatio, sendToTelegram, telegramId, useProfileImage, useEnvironment, generationCount], () => {
|
||||
saveSettings()
|
||||
}, { deep: true })
|
||||
|
||||
@@ -258,7 +290,7 @@ const loadData = async () => {
|
||||
const [charsRes, assetsRes, historyRes] = await Promise.all([
|
||||
dataService.getCharacters(), // Assuming this exists and returns list
|
||||
dataService.getAssets(100, 0, 'all'), // Load a batch of assets
|
||||
aiService.getGenerations(historyRows.value, historyFirst.value, filterCharacter.value?.id)
|
||||
aiService.getGenerations(historyRows.value, historyFirst.value, filterCharacter.value?.id || filterCharacter.value?._id)
|
||||
])
|
||||
|
||||
// Characters
|
||||
@@ -310,7 +342,7 @@ const loadData = async () => {
|
||||
const savedSettings = restoreSettings()
|
||||
if (savedSettings) {
|
||||
if (savedSettings.selectedCharacterId) {
|
||||
selectedCharacter.value = characters.value.find(c => c.id === savedSettings.selectedCharacterId) || null
|
||||
selectedCharacter.value = characters.value.find(c => (c.id === savedSettings.selectedCharacterId || c._id === savedSettings.selectedCharacterId)) || null
|
||||
}
|
||||
if (savedSettings.selectedAssetIds && savedSettings.selectedAssetIds.length > 0) {
|
||||
// Determine which assets to select.
|
||||
@@ -328,7 +360,7 @@ const loadData = async () => {
|
||||
|
||||
const refreshHistory = async () => {
|
||||
try {
|
||||
const response = await aiService.getGenerations(historyRows.value, 0, filterCharacter.value?.id)
|
||||
const response = await aiService.getGenerations(historyRows.value, 0, filterCharacter.value?.id || filterCharacter.value?._id)
|
||||
if (response && response.generations) {
|
||||
// Update existing items and add new ones at the top
|
||||
const newGenerations = []
|
||||
@@ -380,7 +412,8 @@ const handleGenerate = async () => {
|
||||
quality: quality.value.key,
|
||||
prompt: prompt.value,
|
||||
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: generationCount.value
|
||||
@@ -1170,6 +1203,7 @@ const confirmAddToAlbum = async () => {
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Character & Assets Row -->
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
<label class="text-xs font-bold text-slate-400 uppercase tracking-wider">Character
|
||||
@@ -1203,7 +1237,8 @@ const confirmAddToAlbum = async () => {
|
||||
</Dropdown>
|
||||
|
||||
<div v-if="selectedCharacter"
|
||||
class="flex items-center gap-2 mt-2 px-1 animate-in fade-in slide-in-from-top-1">
|
||||
class="flex flex-wrap items-center gap-x-4 gap-y-2 mt-2 px-1 animate-in fade-in slide-in-from-top-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox v-model="useProfileImage" :binary="true" inputId="use-profile-img"
|
||||
class="!border-white/20" :pt="{
|
||||
box: ({ props, state }) => ({
|
||||
@@ -1211,9 +1246,19 @@ const confirmAddToAlbum = async () => {
|
||||
})
|
||||
}" />
|
||||
<label for="use-profile-img"
|
||||
class="text-xs text-slate-300 cursor-pointer select-none">Use
|
||||
Character
|
||||
Photo</label>
|
||||
class="text-xs text-slate-300 cursor-pointer select-none">Use Photo</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox v-model="useEnvironment" :binary="true" inputId="use-env"
|
||||
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="use-env"
|
||||
class="text-xs text-slate-300 cursor-pointer select-none">Use Environment</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1238,6 +1283,47 @@ const confirmAddToAlbum = async () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Environment Row (Below) -->
|
||||
<div v-if="selectedCharacter && useEnvironment" class="flex flex-col gap-2 animate-in fade-in slide-in-from-top-1 mt-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<label class="text-xs 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-5 !w-5 !text-[10px] text-slate-500 hover:text-white" />
|
||||
</div>
|
||||
|
||||
<div v-if="environments.length > 0" class="flex gap-2 overflow-x-auto pb-2 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-3 px-3 py-2 rounded-xl 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_20px_rgba(124,58,237,0.15)]'
|
||||
: 'border-white/5 hover:border-white/20'
|
||||
]"
|
||||
>
|
||||
<div class="w-8 h-8 rounded-lg 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-xs"
|
||||
:class="(selectedEnvironment?.id === (env.id || env._id) || selectedEnvironment?._id === (env.id || env._id)) ? 'text-violet-400' : 'text-slate-500'"
|
||||
></i>
|
||||
</div>
|
||||
<span class="text-sm 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-xs"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="py-4 px-4 bg-slate-800/50 border border-white/5 rounded-2xl text-center">
|
||||
<p class="text-xs text-slate-600 uppercase m-0">No environments for this character</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full lg:w-80 flex flex-col gap-4">
|
||||
|
||||
@@ -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,
|
||||
@@ -1213,8 +1243,48 @@ watch(viewMode, (v) => {
|
||||
</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">
|
||||
<div class="flex flex-col gap-1">
|
||||
|
||||
@@ -65,13 +65,41 @@ const imageCount = ref(1)
|
||||
const sendToTelegram = ref(false)
|
||||
const telegramId = ref('')
|
||||
const useProfileImage = ref(true)
|
||||
const useEnvironment = ref(false)
|
||||
const isSubmittingGen = ref(false)
|
||||
|
||||
// Character & Assets
|
||||
const characters = ref([])
|
||||
const selectedCharacter = ref(null)
|
||||
const environments = ref([])
|
||||
const selectedEnvironment = ref(null)
|
||||
const selectedAssets = 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)
|
||||
})
|
||||
|
||||
const qualityOptions = ref([
|
||||
{ key: 'ONEK', value: '1K' },
|
||||
@@ -100,7 +128,9 @@ const restoreSettings = () => {
|
||||
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
|
||||
_savedEnvironmentId = s.selectedEnvironmentId || null
|
||||
if (s.selectedAssetIds && s.selectedAssetIds.length > 0) {
|
||||
selectedAssets.value = s.selectedAssetIds.map(id => ({
|
||||
id,
|
||||
@@ -123,14 +153,16 @@ const saveSettings = () => {
|
||||
sendToTelegram: sendToTelegram.value,
|
||||
telegramId: telegramId.value,
|
||||
useProfileImage: useProfileImage.value,
|
||||
useEnvironment: useEnvironment.value,
|
||||
selectedCharacterId: selectedCharacter.value?.id || null,
|
||||
selectedEnvironmentId: selectedEnvironment.value?.id || null,
|
||||
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, selectedCharacter, selectedAssets], saveSettings, { deep: true })
|
||||
watch([prompt, quality, aspectRatio, imageCount, selectedModel, sendToTelegram, telegramId, useProfileImage, useEnvironment, selectedCharacter, selectedEnvironment, selectedAssets], saveSettings, { deep: true })
|
||||
|
||||
onMounted(async () => {
|
||||
restoreSettings()
|
||||
@@ -144,7 +176,7 @@ const loadCharacters = async () => {
|
||||
try {
|
||||
characters.value = await dataService.getCharacters()
|
||||
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
|
||||
}
|
||||
@@ -411,8 +443,7 @@ const handleAssetPickerUpload = async (event) => {
|
||||
<!-- Character & Assets Row -->
|
||||
<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>
|
||||
<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="{
|
||||
@@ -442,7 +473,8 @@ const handleAssetPickerUpload = async (event) => {
|
||||
</Dropdown>
|
||||
|
||||
<div v-if="selectedCharacter"
|
||||
class="flex items-center gap-2 mt-2 px-1 animate-in fade-in slide-in-from-top-1">
|
||||
class="flex flex-wrap items-center gap-x-4 gap-y-2 mt-2 px-1 animate-in fade-in slide-in-from-top-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox v-model="useProfileImage" :binary="true" inputId="idea-use-profile-img"
|
||||
class="!border-white/20" :pt="{
|
||||
box: ({ props, state }) => ({
|
||||
@@ -450,9 +482,19 @@ const handleAssetPickerUpload = async (event) => {
|
||||
})
|
||||
}" />
|
||||
<label for="idea-use-profile-img"
|
||||
class="text-xs text-slate-300 cursor-pointer select-none">Use
|
||||
Character
|
||||
Photo</label>
|
||||
class="text-xs text-slate-300 cursor-pointer select-none">Use Photo</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox v-model="useEnvironment" :binary="true" inputId="idea-use-env"
|
||||
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-env"
|
||||
class="text-xs text-slate-300 cursor-pointer select-none">Use Environment</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -475,8 +517,47 @@ const handleAssetPickerUpload = async (event) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Environment Row (Below) -->
|
||||
<div v-if="selectedCharacter && useEnvironment" class="flex flex-col gap-1 animate-in fade-in slide-in-from-top-1 mt-2"> <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>
|
||||
|
||||
<!-- RIGHT COLUMN: Settings & Button -->
|
||||
<div class="w-full lg:w-72 flex flex-col gap-2">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
|
||||
Reference in New Issue
Block a user