This commit is contained in:
xds
2026-02-20 10:29:11 +03:00
parent 489fd14903
commit b0ce251914
8 changed files with 97 additions and 38 deletions

View File

@@ -58,6 +58,12 @@
font-weight: normal; font-weight: normal;
} }
input,
textarea,
select {
font-size: 16px;
}
body { body {
min-height: 100vh; min-height: 100vh;
color: var(--color-text); color: var(--color-text);

View File

@@ -218,19 +218,27 @@ h1, h2, h3, h4, h5, h6 {
/* --- Textarea / Inputs --- */ /* --- Textarea / Inputs --- */
.p-textarea, .p-textarea,
.p-inputtext { .p-inputtext,
.p-dropdown,
.p-multiselect,
.p-autocomplete,
.p-inputnumber input {
width: 100%; width: 100%;
background: rgba(15, 23, 42, 0.6) !important; background: rgba(15, 23, 42, 0.6) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important; border: 1px solid rgba(255, 255, 255, 0.1) !important;
border-radius: 8px !important; border-radius: 8px !important;
padding: 0.5rem !important; padding: 0.5rem !important;
color: white !important; color: white !important;
font-size: 0.8125rem !important; font-size: 1rem !important;
transition: all 0.3s ease !important; transition: all 0.3s ease !important;
} }
.p-textarea:focus, .p-textarea:focus,
.p-inputtext:focus { .p-inputtext:focus,
.p-dropdown:focus,
.p-multiselect:focus,
.p-autocomplete:focus,
.p-inputnumber input:focus {
outline: none !important; outline: none !important;
border-color: #8b5cf6 !important; border-color: #8b5cf6 !important;
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.1) !important; box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.1) !important;

View File

@@ -41,7 +41,8 @@ const isEnvAssetPickerVisible = ref(false)
const isDeletingEnv = ref(false) const isDeletingEnv = ref(false)
const envForm = ref({ const envForm = ref({
name: '', name: '',
asset_ids: [] asset_ids: [],
assets_list: []
}) })
const editingEnvId = ref(null) const editingEnvId = ref(null)
@@ -53,28 +54,36 @@ const envModalRows = ref(20)
const envModalTotal = ref(0) const envModalTotal = ref(0)
const isEnvModalLoading = ref(false) const isEnvModalLoading = ref(false)
const envAssetScrollContainer = ref(null) const envAssetScrollContainer = ref(null)
const envAssetScrollSentinel = ref(null)
const envAssetPickerFileInput = ref(null) const envAssetPickerFileInput = ref(null)
const envUploadProgress = ref(0) const envUploadProgress = ref(0)
const isEnvUploading = ref(false) const isEnvUploading = ref(false)
const envCurrentEnvAssets = ref([])
let envAssetObserver = null let envAssetObserver = null
const envSelectedAssets = computed(() => { const envSelectedAssets = computed(() => {
// We check against all known assets or just the ones in picker // We check against all known assets, picker assets and current environment assets
// Since picker is for the character, we can look into characterAssets too return [...characterAssets.value, ...envModalAssets.value, ...envCurrentEnvAssets.value]
return [...characterAssets.value, ...envModalAssets.value] .filter((a, index, self) => self.findIndex(t => (t.id || t._id) === (a.id || a._id)) === index) // Unique
.filter((a, index, self) => self.findIndex(t => t.id === a.id) === index) // Unique .filter(a => envForm.value.asset_ids.includes(a.id) || (a._id && envForm.value.asset_ids.includes(a._id)))
.filter(a => envForm.value.asset_ids.includes(a.id))
}) })
const loadEnvModalAssets = async (isNewTab = false) => { const loadEnvModalAssets = async (isNewTab = false) => {
if (isEnvModalLoading.value) return if (isEnvModalLoading.value) return
isEnvModalLoading.value = true
if (isNewTab) { if (isNewTab) {
envModalFirst.value = 0 envModalFirst.value = 0
envModalAssets.value = [] envModalAssets.value = []
} else {
// Increment offset for pagination
envModalFirst.value += envModalRows.value
} }
if (envModalTotal.value > 0 && envModalFirst.value >= envModalTotal.value && !isNewTab) {
return
}
isEnvModalLoading.value = true
try { try {
const response = await dataService.getAssetsByCharacterId( const response = await dataService.getAssetsByCharacterId(
route.params.id, route.params.id,
@@ -89,14 +98,15 @@ const loadEnvModalAssets = async (isNewTab = false) => {
} }
} catch (e) { } catch (e) {
console.error('Failed to load env modal assets', e) console.error('Failed to load env modal assets', e)
// Rollback offset on failure if not first page
if (!isNewTab) envModalFirst.value -= envModalRows.value
} finally { } finally {
isEnvModalLoading.value = false isEnvModalLoading.value = false
} }
} }
const handleEnvAssetInfiniteScroll = (entries) => { const handleEnvAssetInfiniteScroll = (entries) => {
if (entries[0].isIntersecting && !isEnvModalLoading.value && envModalAssets.value.length < envModalTotal.value) { if (entries[0].isIntersecting && !isEnvModalLoading.value && (envModalTotal.value === 0 || envModalAssets.value.length < envModalTotal.value)) {
envModalFirst.value += envModalRows.value
loadEnvModalAssets() loadEnvModalAssets()
} }
} }
@@ -128,8 +138,14 @@ watch(envAssetPickerTab, () => {
const toggleEnvAssetSelection = (id) => { const toggleEnvAssetSelection = (id) => {
const idx = envForm.value.asset_ids.indexOf(id) const idx = envForm.value.asset_ids.indexOf(id)
if (idx > -1) envForm.value.asset_ids.splice(idx, 1) if (idx > -1) {
else envForm.value.asset_ids.push(id) envForm.value.asset_ids.splice(idx, 1)
const listIdx = envForm.value.assets_list.indexOf(id)
if (listIdx > -1) envForm.value.assets_list.splice(listIdx, 1)
} else {
envForm.value.asset_ids.push(id)
envForm.value.assets_list.push(id)
}
} }
const triggerEnvAssetUpload = () => { const triggerEnvAssetUpload = () => {
@@ -154,6 +170,7 @@ const handleEnvAssetUpload = async (event) => {
if (response && response.id) { if (response && response.id) {
if (!envForm.value.asset_ids.includes(response.id)) { if (!envForm.value.asset_ids.includes(response.id)) {
envForm.value.asset_ids.push(response.id) envForm.value.asset_ids.push(response.id)
envForm.value.assets_list.push(response.id)
} }
} }
} catch (e) { } catch (e) {
@@ -167,7 +184,11 @@ const handleEnvAssetUpload = async (event) => {
const removeEnvAsset = (id) => { const removeEnvAsset = (id) => {
const idx = envForm.value.asset_ids.indexOf(id) const idx = envForm.value.asset_ids.indexOf(id)
if (idx > -1) envForm.value.asset_ids.splice(idx, 1) if (idx > -1) {
envForm.value.asset_ids.splice(idx, 1)
const listIdx = envForm.value.assets_list.indexOf(id)
if (listIdx > -1) envForm.value.assets_list.splice(listIdx, 1)
}
} }
const loadEnvironments = async () => { const loadEnvironments = async () => {
@@ -179,18 +200,41 @@ const loadEnvironments = async () => {
} }
} }
const openEnvModal = (env = null) => { const openEnvModal = async (env = null) => {
envCurrentEnvAssets.value = []
if (env) { if (env) {
editingEnvId.value = env.id || env._id editingEnvId.value = env.id || env._id
const initialAssets = [...(env.asset_ids || [])]
envForm.value = { envForm.value = {
name: env.name, name: env.name,
asset_ids: env.asset_ids || [] asset_ids: initialAssets,
assets_list: [...initialAssets]
}
// Fetch current environment assets if not already in memory
if (initialAssets.length > 0) {
const missingIds = initialAssets.filter(id =>
!characterAssets.value.find(a => (a.id || a._id) === id) &&
!envModalAssets.value.find(a => (a.id || a._id) === id)
)
if (missingIds.length > 0) {
try {
const fetchedAssets = await Promise.all(
missingIds.map(id => dataService.getAsset(id))
)
envCurrentEnvAssets.value = fetchedAssets.filter(a => !!a)
} catch (e) {
console.error('Failed to fetch missing env assets', e)
}
}
} }
} else { } else {
editingEnvId.value = null editingEnvId.value = null
envForm.value = { envForm.value = {
name: '', name: '',
asset_ids: [] asset_ids: [],
assets_list: []
} }
} }
isEnvModalVisible.value = true isEnvModalVisible.value = true
@@ -202,6 +246,7 @@ const saveEnvironment = async () => {
...envForm.value, ...envForm.value,
character_id: route.params.id character_id: route.params.id
} }
console.log('Saving environment with payload:', payload)
if (editingEnvId.value) { if (editingEnvId.value) {
await dataService.updateEnvironment(editingEnvId.value, payload) await dataService.updateEnvironment(editingEnvId.value, payload)
} else { } else {
@@ -1048,7 +1093,7 @@ const handleGenerate = async () => {
<div v-if="sendToTelegram && !isTelegramIdSaved" <div v-if="sendToTelegram && !isTelegramIdSaved"
class="animate-in fade-in slide-in-from-top-1 duration-200"> class="animate-in fade-in slide-in-from-top-1 duration-200">
<InputText v-model="telegramId" placeholder="Enter Telegram ID" <InputText v-model="telegramId" placeholder="Enter Telegram ID"
class="w-full !text-[10px] !py-1" @blur="saveTelegramId" /> class="w-full !text-[16px] !py-1" @blur="saveTelegramId" />
</div> </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"
@@ -1572,11 +1617,11 @@ const handleGenerate = async () => {
</div> </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-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" <div v-for="asset in envSelectedAssets" :key="asset.id || asset._id"
class="relative w-12 h-12 rounded overflow-hidden border border-violet-500/50 group"> class="relative w-12 h-12 rounded overflow-hidden border border-violet-500/50 group">
<img :src="API_URL + asset.url + '?thumbnail=true'" <img :src="API_URL + asset.url + '?thumbnail=true'"
class="w-full h-full object-cover" /> class="w-full h-full object-cover" />
<div @click="removeEnvAsset(asset.id)" <div @click="removeEnvAsset(asset.id || asset._id)"
class="absolute inset-0 bg-black/60 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer"> 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> <i class="pi pi-times text-white text-[10px]"></i>
</div> </div>
@@ -1632,12 +1677,12 @@ const handleGenerate = async () => {
<div ref="envAssetScrollContainer" class="flex-1 overflow-y-auto p-1 text-slate-100 custom-scrollbar"> <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-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)" <div v-for="asset in envModalAssets" :key="asset.id || asset._id" @click="toggleEnvAssetSelection(asset.id || asset._id)"
class="aspect-square rounded-xl overflow-hidden cursor-pointer relative border transition-all" 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'"> :class="(envForm.asset_ids.includes(asset.id) || (asset._id && 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'" <img :src="API_URL + asset.url + '?thumbnail=true'"
class="w-full h-full object-cover" /> class="w-full h-full object-cover" />
<div v-if="envForm.asset_ids.includes(asset.id)" <div v-if="envForm.asset_ids.includes(asset.id) || (asset._id && envForm.asset_ids.includes(asset._id))"
class="absolute inset-0 bg-violet-600/30 flex items-center justify-center"> class="absolute inset-0 bg-violet-600/30 flex items-center justify-center">
<div class="bg-violet-600 rounded-full p-1 shadow-lg"> <div class="bg-violet-600 rounded-full p-1 shadow-lg">
<i class="pi pi-check text-white text-xs font-bold"></i> <i class="pi pi-check text-white text-xs font-bold"></i>

View File

@@ -865,27 +865,27 @@ const confirmAddToAlbum = async () => {
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<Dropdown v-model="filterCharacter" :options="characters" optionLabel="name" <Dropdown v-model="filterCharacter" :options="characters" optionLabel="name"
placeholder="All Characters" showClear placeholder="All Characters" showClear
class="!w-40 !bg-slate-800/60 !border-white/10 !text-white !rounded-lg !text-[10px]" :pt="{ class="!w-40 !bg-slate-800/60 !border-white/10 !text-white !rounded-lg !text-[16px]" :pt="{
root: { class: '!bg-slate-800/60 !h-7' }, root: { class: '!bg-slate-800/60 !h-7' },
input: { class: '!text-white !text-[10px] !py-0.5 !px-2' }, input: { class: '!text-white !text-[16px] !py-0.5 !px-2' },
trigger: { class: '!text-slate-400 !w-5' }, trigger: { class: '!text-slate-400 !w-5' },
panel: { class: '!bg-slate-800 !border-white/10' }, panel: { class: '!bg-slate-800 !border-white/10' },
item: { class: '!text-slate-300 hover:!bg-white/10 hover:!text-white !text-[10px] !py-1' }, item: { class: '!text-slate-300 hover:!bg-white/10 hover:!text-white !text-[16px] !py-1' },
clearIcon: { class: '!text-slate-400 hover:!text-white !text-[8px]' } clearIcon: { class: '!text-slate-400 hover:!text-white !text-[8px]' }
}"> }">
<template #value="slotProps"> <template #value="slotProps">
<div v-if="slotProps.value" class="flex items-center gap-1"> <div v-if="slotProps.value" class="flex items-center gap-1">
<img v-if="slotProps.value.avatar_image" :src="API_URL + slotProps.value.avatar_image" <img v-if="slotProps.value.avatar_image" :src="API_URL + slotProps.value.avatar_image"
class="w-4 h-4 rounded-full object-cover" /> class="w-4 h-4 rounded-full object-cover" />
<span class="text-[10px]">{{ slotProps.value.name }}</span> <span class="text-[16px]">{{ slotProps.value.name }}</span>
</div> </div>
<span v-else class="text-[10px] text-slate-400">{{ slotProps.placeholder }}</span> <span v-else class="text-[16px] text-slate-400">{{ slotProps.placeholder }}</span>
</template> </template>
<template #option="slotProps"> <template #option="slotProps">
<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="text-[10px]">{{ slotProps.option.name }}</span> <span class="text-[16px]">{{ slotProps.option.name }}</span>
</div> </div>
</template> </template>
</Dropdown> </Dropdown>
@@ -1365,7 +1365,7 @@ const confirmAddToAlbum = async () => {
</div> </div>
<div v-if="sendToTelegram" class="animate-in fade-in slide-in-from-top-1"> <div v-if="sendToTelegram" class="animate-in fade-in slide-in-from-top-1">
<InputText v-model="telegramId" placeholder="Telegram ID" <InputText v-model="telegramId" placeholder="Telegram ID"
class="w-full !text-xs !bg-slate-900 !border-white/10 !text-white !py-1.5" /> class="w-full !text-[16px] !bg-slate-900 !border-white/10 !text-white !py-1.5" />
</div> </div>
</div> </div>

View File

@@ -918,7 +918,7 @@ watch(viewMode, (v) => {
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div v-if="isEditingName" class="flex items-center gap-2"> <div v-if="isEditingName" class="flex items-center gap-2">
<InputText v-model="editableName" <InputText v-model="editableName"
class="idea-name-input !bg-slate-800 !border-violet-500/50 !text-white !py-0.5 !h-7 !text-sm !font-bold" class="idea-name-input !bg-slate-800 !border-violet-500/50 !text-white !py-0.5 !h-7 !text-[16px] !font-bold"
@keyup.enter="saveName" @keyup.enter="saveName"
@blur="saveName" @blur="saveName"
/> />
@@ -1176,7 +1176,7 @@ watch(viewMode, (v) => {
</div> </div>
</div> </div>
<Textarea v-model="prompt" rows="2" placeholder="Describe what you want to create..." <Textarea v-model="prompt" rows="2" placeholder="Describe what you want to create..."
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" /> class="w-full !h-28 bg-slate-800 !text-[16px] 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>
<div class="flex flex-col md:flex-row gap-2"> <div class="flex flex-col md:flex-row gap-2">
@@ -1340,7 +1340,7 @@ watch(viewMode, (v) => {
</div> </div>
<div v-if="sendToTelegram" class="animate-in fade-in slide-in-from-top-1"> <div v-if="sendToTelegram" class="animate-in fade-in slide-in-from-top-1">
<InputText v-model="telegramId" placeholder="Telegram ID" <InputText v-model="telegramId" placeholder="Telegram ID"
class="w-full !text-xs !bg-slate-900 !border-white/10 !text-white !py-1.5" /> class="w-full !text-[16px] !bg-slate-900 !border-white/10 !text-white !py-1.5" />
</div> </div>
</div> </div>

View File

@@ -437,7 +437,7 @@ const handleAssetPickerUpload = async (event) => {
</div> </div>
<Textarea v-model="prompt" rows="2" <Textarea v-model="prompt" rows="2"
placeholder="Describe what you want to create... (Auto-starts new session)" placeholder="Describe what you want to create... (Auto-starts new session)"
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" /> class="w-full !h-28 bg-slate-800 !text-[16px] 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>
<!-- Character & Assets Row --> <!-- Character & Assets Row -->
@@ -598,7 +598,7 @@ const handleAssetPickerUpload = async (event) => {
</div> </div>
<div v-if="sendToTelegram" class="animate-in fade-in slide-in-from-top-1"> <div v-if="sendToTelegram" class="animate-in fade-in slide-in-from-top-1">
<InputText v-model="telegramId" placeholder="Telegram ID" <InputText v-model="telegramId" placeholder="Telegram ID"
class="w-full !text-xs !bg-slate-900 !border-white/10 !text-white !py-1.5" /> class="w-full !text-[16px] !bg-slate-900 !border-white/10 !text-white !py-1.5" />
</div> </div>
</div> </div>

View File

@@ -546,7 +546,7 @@ onMounted(() => {
<div v-if="sendToTelegram && !isTelegramIdSaved" <div v-if="sendToTelegram && !isTelegramIdSaved"
class="animate-in fade-in slide-in-from-top-1 duration-200"> class="animate-in fade-in slide-in-from-top-1 duration-200">
<InputText v-model="telegramId" placeholder="Enter Telegram ID" <InputText v-model="telegramId" placeholder="Enter Telegram ID"
class="w-full !text-xs !py-1.5" @blur="saveTelegramId" /> 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 <small class="text-[10px] text-slate-500 block mt-0.5">ID will be saved for future
use</small> use</small>
</div> </div>

View File

@@ -45,7 +45,7 @@
<div v-if="isOwner" class="mb-6"> <div v-if="isOwner" class="mb-6">
<div class="flex gap-2"> <div class="flex gap-2">
<InputText v-model="inviteUsername" placeholder="Username to add" <InputText v-model="inviteUsername" placeholder="Username to add"
class="w-full p-inputtext-sm" @keyup.enter="addMember" /> class="w-full" @keyup.enter="addMember" />
<Button label="Add" icon="pi pi-user-plus" size="small" @click="addMember" <Button label="Add" icon="pi pi-user-plus" size="small" @click="addMember"
:loading="inviting" :disabled="!inviteUsername.trim()" /> :loading="inviting" :disabled="!inviteUsername.trim()" />
</div> </div>