From b3957ad4634e416faab47ff31aef8ec40ea19e3d Mon Sep 17 00:00:00 2001 From: xds Date: Thu, 5 Feb 2026 21:10:29 +0300 Subject: [PATCH] feat: Add image generation and image-to-prompt features, integrate Telegram for generation results, and enhance asset management. --- src/router/index.js | 10 + src/services/dataService.js | 22 + src/views/AssetsView.vue | 18 +- src/views/CharacterDetailView.vue | 734 +++++++++++++++++++----------- src/views/CharactersView.vue | 15 +- src/views/ImageGenerationView.vue | 693 ++++++++++++++++++++++++++++ src/views/ImageToPromptView.vue | 238 ++++++++++ 7 files changed, 1458 insertions(+), 272 deletions(-) create mode 100644 src/views/ImageGenerationView.vue create mode 100644 src/views/ImageToPromptView.vue diff --git a/src/router/index.js b/src/router/index.js index 7140fae..16b5612 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -28,6 +28,16 @@ const router = createRouter({ path: '/characters/:id', name: 'character-detail', component: () => import('../views/CharacterDetailView.vue') + }, + { + path: '/image-to-prompt', + name: 'image-to-prompt', + component: () => import('../views/ImageToPromptView.vue') + }, + { + path: '/generation', + name: 'generation', + component: () => import('../views/ImageGenerationView.vue') } ] }) diff --git a/src/services/dataService.js b/src/services/dataService.js index adffcf7..fdabca3 100644 --- a/src/services/dataService.js +++ b/src/services/dataService.js @@ -41,5 +41,27 @@ export const dataService = { } }) return response.data + }, + + generatePromptFromImage: async (files, prompt) => { + const formData = new FormData() + + // Handle single or multiple files + const fileArray = Array.isArray(files) ? files : [files] + fileArray.forEach(file => { + formData.append('images', file) + }) + + if (prompt) { + formData.append('prompt', prompt) + } + + const response = await api.post('/generations/prompt-from-image', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + + return response.data.prompt } } diff --git a/src/views/AssetsView.vue b/src/views/AssetsView.vue index 0e7401c..373a099 100644 --- a/src/views/AssetsView.vue +++ b/src/views/AssetsView.vue @@ -96,17 +96,26 @@ const handleLogout = () => {
+ @click="router.push('/')" v-tooltip.right="'Home'"> 🏠
-
📂
+
+ @click="router.push('/generation')" v-tooltip.right="'Image Generation'"> + 🎨 +
+
👥
+
+ +
-
diff --git a/src/views/CharacterDetailView.vue b/src/views/CharacterDetailView.vue index e92947b..49ccb63 100644 --- a/src/views/CharacterDetailView.vue +++ b/src/views/CharacterDetailView.vue @@ -10,9 +10,11 @@ import Dialog from 'primevue/dialog' import Textarea from 'primevue/textarea' import SelectButton from 'primevue/selectbutton' import FileUpload from 'primevue/fileupload' +import Checkbox from 'primevue/checkbox' import ProgressBar from 'primevue/progressbar' import Message from 'primevue/message' import ProgressSpinner from 'primevue/progressspinner' +import InputText from 'primevue/inputtext' import Tabs from 'primevue/tabs' import TabList from 'primevue/tablist' import Tab from 'primevue/tab' @@ -176,6 +178,16 @@ const prompt = ref('') const isGenerating = ref(false) const generationStatus = ref('') const generationProgress = ref(0) +const sendToTelegram = ref(false) +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 generatedResult = ref(null) @@ -197,6 +209,10 @@ const toggleAssetSelection = (asset) => { } } +const removeSelectedAsset = (index) => { + selectedAssets.value.splice(index, 1) +} + const quality = ref({ key: 'TWOK', value: '2K' @@ -231,6 +247,63 @@ const onAssetsPage = (event) => { loadAssets() } +// Hover Zoom Logic +const hoveredThumbnail = ref(null) +let hoverTimeout = null + +const onThumbnailEnter = (event, url) => { + if (hoverTimeout) clearTimeout(hoverTimeout) + const rect = event.target.getBoundingClientRect() + hoveredThumbnail.value = { + url, + style: { + top: rect.top + 'px', + left: rect.left + 'px', + width: rect.width + 'px', + height: rect.height + 'px' + } + } +} + +const onThumbnailLeave = () => { + hoverTimeout = setTimeout(() => { + hoveredThumbnail.value = null + }, 50) +} + +// Global Asset Selection +const allAssets = ref([]) +const isAssetSelectionVisible = ref(false) +const modalAssetsFirst = ref(0) +const modalAssetsRows = ref(20) +const modalAssetsTotal = ref(0) + +const loadAllAssets = async () => { + try { + const response = await dataService.getAssets(modalAssetsRows.value, modalAssetsFirst.value, 'all') + if (response && response.assets) { + allAssets.value = response.assets + modalAssetsTotal.value = response.total_count || 0 + } else { + allAssets.value = [] + modalAssetsTotal.value = 0 + } + } catch (e) { + console.error('Failed to load all assets', e) + } +} + +const openAssetSelectionModal = () => { + isAssetSelectionVisible.value = true + loadAllAssets() +} + +const onModalAssetsPage = (event) => { + modalAssetsFirst.value = event.first + modalAssetsRows.value = event.rows + loadAllAssets() +} + const pollStatus = async (id) => { let completed = false while (!completed && isGenerating.value) { @@ -251,7 +324,11 @@ const pollStatus = async (id) => { const resultAssets = assets.filter(a => response.assets_list.includes(a.id)) generatedResult.value = { type: 'assets', - assets: resultAssets + assets: resultAssets, + tech_prompt: response.tech_prompt, + execution_time: response.execution_time_seconds, + api_execution_time: response.api_execution_time_seconds, + token_usage: response.token_usage } } @@ -291,7 +368,11 @@ const restoreGeneration = async (gen) => { selectedAssets.value = assets.filter(a => gen.assets_list.includes(a.id)) generatedResult.value = { type: 'assets', - assets: selectedAssets.value + assets: selectedAssets.value, + tech_prompt: gen.tech_prompt, + execution_time: gen.execution_time_seconds, + api_execution_time: gen.api_execution_time_seconds, + token_usage: gen.token_usage } generationSuccess.value = true } @@ -357,12 +438,23 @@ const handleGenerate = async () => { generatedResult.value = null try { + if (sendToTelegram.value && !telegramId.value) { + alert("Please enter your Telegram ID") + return + } + + if (telegramId.value && telegramId.value !== localStorage.getItem('telegram_id')) { + localStorage.setItem('telegram_id', telegramId.value) + isTelegramIdSaved.value = true + } + const payload = { linked_character_id: character.value?.id, aspect_ratio: aspectRatio.value.key, quality: quality.value.key, 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 } const response = await aiService.runGeneration(payload) @@ -393,31 +485,40 @@ const handleLogout = () => {
-
- -
-
- -
- + +
+
+ +
+ +
+
+ +
+
+
+ No assets selected +
-
- - Success! - +
+
+ +
+
+ +
+

+ {{ generationStatus || 'Creating...' }}

+

Processing using AI

+
+ + {{ generationProgress }}% +
+ +
+
+

Result

+
+
+
+ +
+
+ +
+
+
+ +
+
+ {{ generatedResult.content || generatedResult }} +
+ + +
+
+

Technical + Prompt

+

{{ + generatedResult.tech_prompt }}

+
+
+ + +
+
+ {{ generatedResult.execution_time.toFixed(2) + }}s +
+
+ {{ + generatedResult.api_execution_time.toFixed(2) }}s +
+
+ {{ generatedResult.token_usage }} toks +
+
+
+ +
+ +

Ready

+
+ + +
+
+

History + ({{ + historyTotal }})

+
+ +
+ No previous generations. +
+ +
+
+
+ +
+ +
+
+
+

+ {{ + gen.prompt }}

+ + +

+ {{ gen.tech_prompt }} +

+ +
+ {{ new Date(gen.created_at).toLocaleDateString() }} + {{ + gen.status }} +
+ +
+ {{ + gen.execution_time_seconds.toFixed(1) }}s + {{ + gen.api_execution_time_seconds.toFixed(1) }}s + {{ gen.token_usage }} +
+
+
+
+ + +
+ +
+
+
+ +
+
+
+
+
+ + + +
+
+

Linked Assets ({{ assetsTotalRecords }})

+
+
+
+ +
+ No assets linked to this character. +
+ +
+
+
+
+ + +
+
+
-
- -
-

- {{ generationStatus || 'Creating...' }}

-

Processing using AI

-
- - {{ generationProgress }}% -
- -
-
-

Result

-
-
-
- -
-
- -
-
-
- -
-
- {{ generatedResult.content || generatedResult }} -
-
- -
- -

Ready

-
- - -
-
-

Recent - Generations ({{ historyTotal }})

-
- -
- No previous generations. -
- -
-
-
- -
- -
-
-
-
-

{{ - gen.prompt }}

- -
-
- {{ new Date(gen.created_at).toLocaleDateString() }} - - {{ gen.quality }} - - {{ gen.aspect_ratio }} -
-
-
-
- - -
- -
-
- -
-
-
-
+ class="absolute top-2 right-2 bg-black/60 backdrop-blur-sm px-2 py-1 rounded text-xs uppercase text-white"> + {{ asset.type }}
- +
+

+ {{ asset.name }} +

+
+
+
- -
-
-

Linked Assets ({{ assetsTotalRecords }})

-
-
+
+ -
-
+
+ +
+
+ +
+ + + +
+
+
+
-
- No assets linked to this character. -
+
+ Character not found. +
-
-
-
-
- - -
-
- -
-
- -
- {{ asset.type }} -
-
-
-

- {{ asset.name }} -

-
-
-
- - -
- -
+ +
+ +
+

{{ selectedAsset.name }}

+

{{ selectedAsset.type }}

+
+
+
+ + +
+
+
+
+ +
+
+
- - - - - - -
- -
-
- -
- - -
-
+
+ No assets found +
-
- Character not found. -
- - -
- -
-

{{ selectedAsset.name }}

-

{{ selectedAsset.type }}

+
+ {{ selectedAssets.length }} selected +
+ +
-
+
+
diff --git a/src/views/ImageToPromptView.vue b/src/views/ImageToPromptView.vue new file mode 100644 index 0000000..ef65f22 --- /dev/null +++ b/src/views/ImageToPromptView.vue @@ -0,0 +1,238 @@ + + +