diff --git a/talemate_frontend/src/components/CharacterVisualReferenceMixin.js b/talemate_frontend/src/components/CharacterVisualReferenceMixin.js new file mode 100644 index 00000000..26ed63ee --- /dev/null +++ b/talemate_frontend/src/components/CharacterVisualReferenceMixin.js @@ -0,0 +1,156 @@ +/** + * Shared reference-asset selection logic for character visual managers + * (cover image, avatar). Complements VisualAssetGenerateMixin. + * + * Requirements: + * - Component must have `character` prop and `imageEditAvailable` prop + * - Component must provide `assets` computed (same-vis-type assets) + * - Component must use VisualAssetsMixin (getCharacterAssets, getWebsocket) + * - Component must provide a `referenceConfig` computed: + * - visType: target VIS_TYPE for generation/search + * - preferredId: asset id to prefer as reference (e.g. current cover image) + * - fallbackId: asset id to fall back to (e.g. the avatar) + * - Component must provide `initialVariationPrompt` / `initialNewPrompt` + * computeds — the prompt prefills used when no same-vis-type asset exists yet + * + * Provides: + * - referenceAssetIds / selection-reason state for VisualAssetGenerateDialog + * - checkReferenceAssets(): recompute reference options, searching the backend + * once per character when nothing is available locally + * - handleAssetSearchResults(data): websocket handler for the search response + * - onReferenceSelectionChange(newId): tracks manual selection changes + * - openGenerateDialog() / openGenerateNewDialog() with initial-prompt prefill + * - anyCharacterAssets / hasAnyCharacterAssets / shouldUseVariationForInitial / + * variationLabel computed + */ +import { computeCharacterReferenceOptions } from '../utils/characterReferenceOptions.js'; + +export default { + data() { + return { + referenceAssetIds: [], + hasCheckedReferences: false, + referenceSelectionReason: null, + userChangedReference: false, + } + }, + computed: { + anyCharacterAssets() { + // Get ALL assets for this character (any vis_type) + if (!this.character?.name) return []; + return this.getCharacterAssets(this.character.name); + }, + hasAnyCharacterAssets() { + return this.anyCharacterAssets.length > 0; + }, + shouldUseVariationForInitial() { + // Use variation flow for the initial image if: + // 1. No same-vis-type assets exist yet + // 2. Character has ANY assets + // 3. Image editing is available + return this.assets.length === 0 && + this.hasAnyCharacterAssets && + this.imageEditAvailable; + }, + variationLabel() { + return this.shouldUseVariationForInitial ? 'Generate from Reference' : 'Generate Variation'; + }, + }, + methods: { + checkReferenceAssets() { + if (!this.character?.name) return; + + const { visType, preferredId, fallbackId } = this.referenceConfig; + + // Use the shared helper to compute ordered options + // Pass same-vis-type assets first (this.assets), then all character assets, preferred ID, and fallback + const { selectedId, orderedIds, reason } = computeCharacterReferenceOptions( + visType, + this.anyCharacterAssets, + preferredId, + this.assets, + fallbackId + ); + + if (orderedIds.length > 0) { + this.referenceAssetIds = orderedIds; + this.selectedReferenceAssetId = selectedId; + this.referenceSelectionReason = reason || null; + this.userChangedReference = false; + this.hasCheckedReferences = true; + } else { + // No local assets found + this.referenceAssetIds = []; + this.selectedReferenceAssetId = null; + this.referenceSelectionReason = null; + this.userChangedReference = false; + + // Search once per character. Mark as checked BEFORE sending so re-entry + // (e.g. via watchers firing while waiting for the response) doesn't spam + // the backend with identical search requests. + if (!this.hasCheckedReferences) { + this.hasCheckedReferences = true; + this.getWebsocket().send(JSON.stringify({ + type: 'scene_assets', + action: 'search', + vis_type: visType, + character_name: this.character.name, + reference_vis_types: [visType], + })); + } + } + }, + + handleAssetSearchResults(data) { + if (data.type !== 'asset_search_results') return; + if (data.character_name !== this.character?.name || + data.vis_type !== this.referenceConfig.visType) return; + + const assetIds = data.asset_ids || []; + + // Apply results directly. Calling checkReferenceAssets() here would + // re-run the same logic, find no local assets, and fire another + // search — looping until the dialog is closed. Any local-asset + // changes that arrive after this request are picked up by the + // component's watchers. + if (assetIds.length > 0) { + this.referenceAssetIds = assetIds; + this.selectedReferenceAssetId = assetIds[0]; + this.referenceSelectionReason = 'Found via asset search'; + this.userChangedReference = false; + } + this.hasCheckedReferences = true; + }, + + onReferenceSelectionChange(newId) { + // Track that user manually changed the selection + if (newId !== this.selectedReferenceAssetId && this.referenceSelectionReason) { + this.userChangedReference = true; + } + }, + + openGenerateDialog() { + // Ensure reference assets are checked first + if (!this.hasCheckedReferences) { + this.checkReferenceAssets(); + } + + if (this.shouldUseVariationForInitial && !this.promptInput) { + this.promptInput = this.initialVariationPrompt; + } + + this.generateDialogOpen = true; + + // Ensure all reference assets are loaded for carousel + if (this.referenceAssetIds.length > 0) { + this.loadAssets(this.referenceAssetIds); + } + }, + + openGenerateNewDialog() { + this.generateNewDialogOpen = true; + // Prefill with the default prompt if no image of this type exists yet + this.generateNewPromptInput = this.assets.length === 0 ? this.initialNewPrompt : ''; + }, + }, +} diff --git a/talemate_frontend/src/components/VisualAssetBadge.vue b/talemate_frontend/src/components/VisualAssetBadge.vue new file mode 100644 index 00000000..8a688951 --- /dev/null +++ b/talemate_frontend/src/components/VisualAssetBadge.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/talemate_frontend/src/components/VisualAssetGenerateCards.vue b/talemate_frontend/src/components/VisualAssetGenerateCards.vue new file mode 100644 index 00000000..6cd4b547 --- /dev/null +++ b/talemate_frontend/src/components/VisualAssetGenerateCards.vue @@ -0,0 +1,106 @@ + + + diff --git a/talemate_frontend/src/components/VisualAssetGenerateDialog.vue b/talemate_frontend/src/components/VisualAssetGenerateDialog.vue new file mode 100644 index 00000000..df86496b --- /dev/null +++ b/talemate_frontend/src/components/VisualAssetGenerateDialog.vue @@ -0,0 +1,204 @@ + + + diff --git a/talemate_frontend/src/components/VisualAssetGenerateMixin.js b/talemate_frontend/src/components/VisualAssetGenerateMixin.js new file mode 100644 index 00000000..7fd996f7 --- /dev/null +++ b/talemate_frontend/src/components/VisualAssetGenerateMixin.js @@ -0,0 +1,107 @@ +/** + * Shared state and helpers for the Generate Variation / Generate New dialog flow + * used by the asset-grid based visual managers (character cover, character avatar, + * scene assets). + * + * Requirements: + * - Component must provide `referenceAssetIds` (data or computed) — the ordered + * reference options for the variation dialog + * - Component must implement `startSingleGeneration()` and + * `buildBatchRequests(prompts)` (returns the generation-request array for + * batch mode) + * + * Provides: + * - All dialog/prompt state consumed by VisualAssetGenerateDialog and + * VisualAssetGenerateNewDialog via v-model bindings + * - hasReferenceAssets / canGenerate computed + * - startGeneration(): dispatches to single or batch generation + * - startBatchGeneration(): queues buildBatchRequests() output and resets the dialog + * - closeGenerateDialog() / closeGenerateNewDialog() + * - handleImageGenerationFailed(data): websocket handler unlocking the dialogs + */ +export default { + data() { + return { + generateDialogOpen: false, + promptInput: '', + batchPrompts: [], + generationMode: 'single', + isGenerating: false, + generateNewDialogOpen: false, + generateNewPromptInput: '', + isGeneratingNew: false, + selectedReferenceAssetId: null, + pendingGenerationRequest: null, + pendingGenerateNewRequest: null, + } + }, + computed: { + hasReferenceAssets() { + return this.referenceAssetIds.length > 0; + }, + canGenerate() { + if (this.generationMode === 'batch') { + // Batch mode: need at least one prompt in the list + return !!(this.batchPrompts.length > 0 && this.selectedReferenceAssetId); + } + // Single mode: need prompt + return !!(this.promptInput.trim() && this.selectedReferenceAssetId); + }, + }, + methods: { + startGeneration() { + if (this.isGenerating) return; + + // Need at least one selected reference asset for IMAGE_EDIT + if (!this.selectedReferenceAssetId) { + console.warn('No reference asset selected for image generation'); + return; + } + + if (this.generationMode === 'batch') { + this.startBatchGeneration(); + } else { + this.startSingleGeneration(); + } + }, + + startBatchGeneration() { + if (this.batchPrompts.length === 0) return; + + const requests = this.buildBatchRequests(this.batchPrompts); + + if (this.addToVisualLibraryPendingQueue && typeof this.addToVisualLibraryPendingQueue === 'function') { + this.addToVisualLibraryPendingQueue(requests); + } else { + console.warn('addToVisualLibraryPendingQueue not available'); + } + + this.generateDialogOpen = false; + this.batchPrompts = []; + this.promptInput = ''; + }, + + closeGenerateDialog() { + if (!this.isGenerating) { + this.generateDialogOpen = false; + this.promptInput = ''; + this.batchPrompts = []; + this.generationMode = 'single'; + } + }, + + closeGenerateNewDialog() { + if (!this.isGeneratingNew) { + this.generateNewDialogOpen = false; + this.generateNewPromptInput = ''; + } + }, + + handleImageGenerationFailed(data) { + if (data.type !== 'image_generation_failed') return; + // Unlock dialogs to allow retry; prompts and dialogs stay open + this.isGenerating = false; + this.isGeneratingNew = false; + }, + }, +} diff --git a/talemate_frontend/src/components/VisualAssetGenerateNewDialog.vue b/talemate_frontend/src/components/VisualAssetGenerateNewDialog.vue new file mode 100644 index 00000000..0ca0fa95 --- /dev/null +++ b/talemate_frontend/src/components/VisualAssetGenerateNewDialog.vue @@ -0,0 +1,57 @@ + + + diff --git a/talemate_frontend/src/components/VisualAssetGrid.vue b/talemate_frontend/src/components/VisualAssetGrid.vue new file mode 100644 index 00000000..fd846df5 --- /dev/null +++ b/talemate_frontend/src/components/VisualAssetGrid.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/talemate_frontend/src/components/WorldStateManagerCharacterVisualsAvatar.vue b/talemate_frontend/src/components/WorldStateManagerCharacterVisualsAvatar.vue index f7a9a6b9..649f94a0 100644 --- a/talemate_frontend/src/components/WorldStateManagerCharacterVisualsAvatar.vue +++ b/talemate_frontend/src/components/WorldStateManagerCharacterVisualsAvatar.vue @@ -6,130 +6,83 @@ -
- mdi-image-off-outline -

No portraits found for {{ character.name }}

-

Generate a CHARACTER_PORTRAIT image in the Visual Library to add portraits.

-
- -
-
- -
-
- mdi-tray-arrow-down - Drop image -
-
- - Add Portrait - -
- - - - - - Set as Default - - - - Set as Current - - - - - View Image - - - - Open in Visual Library - Edit tags here - - - - - Delete - - - -
-
+ + + + + +

- Portraits are used in dialogue messages and character lists. They are typically + Portraits are used in dialogue messages and character lists. They are typically face-focused images with a square format.

@@ -137,254 +90,80 @@

- - - - - -
- mdi-image - Generate from Reference - Generate Variation -
-

- - Create your first portrait using an existing character image as reference. - Uses image editing to generate a close-up portrait based on your prompt. - - - Create a variation of an existing portrait by modifying its expression or appearance. - Uses image editing to transform a reference image based on your prompt. - -

- - Image editing backend is not configured. Configure an image editing backend in Visual Agent settings to generate variations. - -
- - - Generate from Reference - Generate Variation - - -
-
+ + + - - - - -
- mdi-image-plus - Generate New -
-

- Create a completely new portrait from scratch using natural language instructions. - The visual agent will generate a prompt and create a new image based on your description. -

- - Image creation backend is not configured. Configure a text-to-image backend in Visual Agent settings to generate new portraits. - -
- - - Generate New - - -
-
-
+ + + + - - - - - Generate from Reference for {{ character.name }} - Generate Variation for {{ character.name }} - - -

- - Enter a prompt to generate a close-up portrait of the character based on the reference image. - - - Enter a prompt to change the expression or appearance (e.g., 'change the expression to sad', 'make them happy', 'angry expression', etc.). - -

- - -
- - No reference images available for this character. - -
-
- - Loading reference images... - -
- - - -
- mdi-information-outline -
-
Why this reference was chosen:
-
- {{ referenceSelectionReason.reason }} -
-
-
-
-
- - - Single - Batch - - - - - - - - - - - -
- mdi-information-outline -
-
- Each prompt will create a separate generation using the same reference image and settings. Generations will be queued in the Visual Library. Tags can be added using {tag} syntax in each prompt. -
-
-
-
-
-
-
-
- - - Cancel - - {{ generationMode === 'batch' ? 'Queue Batch' : 'Generate' }} - - -
-
- - - - - - Generate New Portrait for {{ character.name }} - - -

- Enter a prompt to generate a new portrait. The visual agent will create an image based on your description. -

- - -
- - - Cancel - - Generate - - -
-
+ + + + Update tags: Open a portrait in the Visual Library to edit its tags.

- Configure feature: Enable and adjust portrait selection frequency in + Configure feature: Enable and adjust portrait selection frequency in World State Agent settings.

@@ -432,44 +211,36 @@ import { v4 as uuidv4 } from 'uuid'; import VisualAssetsMixin from './VisualAssetsMixin.js'; import AssetViewMixin from './AssetViewMixin.js'; +import VisualAssetGenerateMixin from './VisualAssetGenerateMixin.js'; +import CharacterVisualReferenceMixin from './CharacterVisualReferenceMixin.js'; import ConfirmActionPrompt from './ConfirmActionPrompt.vue'; -import VisualReferenceCarousel from './VisualReferenceCarousel.vue'; import AssetView from './AssetView.vue'; -import EditableList from './EditableList.vue'; -import { computeCharacterReferenceOptions } from '../utils/characterReferenceOptions.js'; +import VisualAssetGrid from './VisualAssetGrid.vue'; +import VisualAssetBadge from './VisualAssetBadge.vue'; +import VisualAssetGenerateCards from './VisualAssetGenerateCards.vue'; +import VisualAssetGenerateDialog from './VisualAssetGenerateDialog.vue'; +import VisualAssetGenerateNewDialog from './VisualAssetGenerateNewDialog.vue'; import { VIS_TYPE, FORMAT_TYPE, GEN_TYPE } from '@/constants/visual'; export default { name: 'WorldStateManagerCharacterVisualsAvatar', - mixins: [VisualAssetsMixin, AssetViewMixin], + mixins: [VisualAssetsMixin, AssetViewMixin, VisualAssetGenerateMixin, CharacterVisualReferenceMixin], components: { ConfirmActionPrompt, - VisualReferenceCarousel, AssetView, - EditableList, + VisualAssetGrid, + VisualAssetBadge, + VisualAssetGenerateCards, + VisualAssetGenerateDialog, + VisualAssetGenerateNewDialog, }, inject: ['openAgentSettings'], data() { return { - generateDialogOpen: false, - promptInput: '', - batchPrompts: [], - generationMode: 'single', - isGenerating: false, - generateNewDialogOpen: false, - generateNewPromptInput: '', - isGeneratingNew: false, - pendingGenerateNewRequest: null, - referenceAssetIds: [], - selectedReferenceAssetId: null, - hasCheckedReferences: false, - pendingGenerationRequest: null, defaultAvatarId: null, currentAvatarId: null, previousAssetsLength: 0, hasAttemptedAutoSetDefaultAvatar: false, - referenceSelectionReason: null, - userChangedReference: false, } }, props: { @@ -485,9 +256,6 @@ export default { default: false, }, }, - emits: [ - 'require-scene-save', - ], computed: { assets() { // Filter assets by CHARACTER_PORTRAIT vis_type and character name @@ -501,34 +269,23 @@ export default { character: this.character, }; }, - hasReferenceAssets() { - return this.referenceAssetIds.length > 0; + referenceConfig() { + return { + visType: VIS_TYPE.CHARACTER_PORTRAIT, + preferredId: this.defaultAvatarId, + fallbackId: this.character?.cover_image, + }; }, - anyCharacterAssets() { - // Get ALL assets for this character (any vis_type) - if (!this.character?.name) return []; - return this.getCharacterAssets(this.character.name); + variationPromptHint() { + return this.shouldUseVariationForInitial + ? 'e.g., generate close up of the character head with a neutral expression. Add tags using {tag} syntax, e.g., {happy} {portrait}' + : 'e.g., change the expression to sad. Add tags using {tag} syntax, e.g., {sad} {portrait}'; }, - hasAnyCharacterAssets() { - return this.anyCharacterAssets.length > 0; + initialVariationPrompt() { + return 'generate close up of the character\'s head with a neutral expression'; }, - shouldUseVariationForInitialAvatar() { - // Use variation flow for initial avatar if: - // 1. No CHARACTER_PORTRAIT avatars exist yet - // 2. Character has ANY assets - // 3. Image editing is available - return this.assets.length === 0 && - this.hasAnyCharacterAssets && - this.imageEditAvailable; - }, - canGenerate() { - if (this.generationMode === 'batch') { - // Batch mode: need at least one prompt in the list - return this.batchPrompts.length > 0 && this.selectedReferenceAssetId; - } else { - // Single mode: need prompt - return this.promptInput.trim() && this.selectedReferenceAssetId; - } + initialNewPrompt() { + return 'Create a portrait with a neutral expression'; }, }, watch: { @@ -555,7 +312,7 @@ export default { // Request base64 for new assets const assetIds = assets.map(a => a.id); this.loadAssets(assetIds); - + // Automatically set an avatar as default when none is set. // This is especially important for: // - the very first avatar created/uploaded @@ -582,7 +339,7 @@ export default { this.setDefaultAvatarForAsset(firstAssetId); } this.previousAssetsLength = assets.length; - + // Re-check reference assets when assets change (to handle fallback logic) this.checkReferenceAssets(); }, @@ -603,22 +360,29 @@ export default { }, }, 'character.cover_image': { - handler(newCoverImageId) { + handler() { // Re-check reference assets when cover image changes this.checkReferenceAssets(); }, }, }, methods: { + cardStateClass(asset) { + return { + 'current': this.defaultAvatarId === asset.id, + 'active': this.currentAvatarId === asset.id, + }; + }, + openWorldStateAgentSettings() { if (this.openAgentSettings && typeof this.openAgentSettings === 'function') { this.openAgentSettings('world_state', 'avatars'); } }, - + setDefaultAvatarForAsset(assetId) { if (!assetId) return; - + this.getWebsocket().send(JSON.stringify({ type: 'scene_assets', action: 'set_character_avatar', @@ -626,14 +390,14 @@ export default { character_name: this.character.name, avatar_type: 'default', })); - + // Request character details to sync up the UI after setting default avatar this.requestCharacterDetails(); }, - + setCurrentAvatarForAsset(assetId) { if (!assetId) return; - + this.getWebsocket().send(JSON.stringify({ type: 'scene_assets', action: 'set_character_avatar', @@ -642,119 +406,19 @@ export default { avatar_type: 'current', })); }, - - checkReferenceAssets() { - if (!this.character?.name) return; - - const targetVisType = VIS_TYPE.CHARACTER_PORTRAIT; - const coverImageId = this.character?.cover_image; - - // Use the shared helper to compute ordered options - // Pass same-vis-type assets first (this.assets), then all character assets, preferred ID, and fallback - const { selectedId, orderedIds, reason } = computeCharacterReferenceOptions( - targetVisType, - this.anyCharacterAssets, - this.defaultAvatarId, - this.assets, // same-vis-type assets (CHARACTER_PORTRAIT) - coverImageId // fallback: cover image - ); - - if (orderedIds.length > 0) { - this.referenceAssetIds = orderedIds; - this.selectedReferenceAssetId = selectedId; - this.referenceSelectionReason = reason ? { reason } : null; - this.userChangedReference = false; - this.hasCheckedReferences = true; - } else { - // No local assets found - this.referenceAssetIds = []; - this.selectedReferenceAssetId = null; - this.referenceSelectionReason = null; - this.userChangedReference = false; - // Search once per character. Mark as checked BEFORE sending so re-entry - // (e.g. via watchers firing while waiting for the response) doesn't spam - // the backend with identical search requests. - if (!this.hasCheckedReferences) { - this.hasCheckedReferences = true; - this.getWebsocket().send(JSON.stringify({ - type: 'scene_assets', - action: 'search', - vis_type: targetVisType, - character_name: this.character.name, - reference_vis_types: [targetVisType], - })); - } - } - }, - - onReferenceSelectionChange(newId) { - // Track that user manually changed the selection - if (newId !== this.selectedReferenceAssetId && this.referenceSelectionReason) { - this.userChangedReference = true; - } - }, - - openGenerateDialog() { - // Ensure reference assets are checked first - if (!this.hasCheckedReferences) { - this.checkReferenceAssets(); - } - - // Set default prompt for initial avatar generation if not already set - if (this.shouldUseVariationForInitialAvatar && !this.promptInput) { - this.promptInput = 'generate close up of the character\'s head with a neutral expression'; - } else if (!this.promptInput) { - // Clear prompt for normal variation generation - this.promptInput = ''; - } - - this.generateDialogOpen = true; - - // Ensure all reference assets are loaded for carousel - if (this.referenceAssetIds.length > 0) { - this.loadAssets(this.referenceAssetIds); - } - }, - - closeGenerateDialog() { - if (!this.isGenerating) { - this.generateDialogOpen = false; - this.promptInput = ''; - this.batchPrompts = []; - this.generationMode = 'single'; - } - }, - - openGenerateNewDialog() { - this.generateNewDialogOpen = true; - // Prefill with default prompt if there are no avatars yet - if (this.assets.length === 0) { - this.generateNewPromptInput = 'Create a portrait with a neutral expression'; - } else { - this.generateNewPromptInput = ''; - } - }, - - closeGenerateNewDialog() { - if (!this.isGeneratingNew) { - this.generateNewDialogOpen = false; - this.generateNewPromptInput = ''; - } - }, - startGenerateNew() { if (!this.generateNewPromptInput.trim() || this.isGeneratingNew) return; - + this.isGeneratingNew = true; - + // Store the request for saving later this.pendingGenerateNewRequest = { prompt: this.generateNewPromptInput.trim(), vis_type: VIS_TYPE.CHARACTER_PORTRAIT, character_name: this.character.name, }; - + // Use visualize action similar to VisualLibraryGenerate instruct mode const payload = { type: 'visual', @@ -766,35 +430,17 @@ export default { // (and we rely on backend auto-save, not a follow-up save_image request). asset_allow_override: true, }; - + this.getWebsocket().send(JSON.stringify(payload)); }, - - startGeneration() { - if (this.isGenerating) return; - - // Need at least one selected reference asset for IMAGE_EDIT - if (!this.selectedReferenceAssetId) { - console.warn('No reference asset selected for avatar generation'); - return; - } - - if (this.generationMode === 'batch') { - // Batch mode: parse lines and queue them - this.startBatchGeneration(); - } else { - // Single mode: existing behavior - this.startSingleGeneration(); - } - }, - + startSingleGeneration() { if (!this.promptInput.trim() || this.isGenerating) return; - + this.isGenerating = true; - + const isFirstAvatar = this.assets.length === 0 && !this.defaultAvatarId; - + // Store the generation request for saving later // Use the selected reference asset this.pendingGenerationRequest = { @@ -814,27 +460,21 @@ export default { asset_name: `avatar_${this.character.name}_${uuidv4().slice(0, 10)}`, }, }; - + // Generate image using prompt generation endpoint with IMAGE_EDIT const payload = { type: 'visual', action: 'generate', generation_request: this.pendingGenerationRequest, }; - + this.getWebsocket().send(JSON.stringify(payload)); }, - - startBatchGeneration() { - if (this.batchPrompts.length === 0) return; - - // Use prompts from the list - const prompts = this.batchPrompts; - + + buildBatchRequests(prompts) { const isFirstAvatar = this.assets.length === 0 && !this.defaultAvatarId; - - // Create generation request for each prompt - const requests = prompts.map((prompt, idx) => ({ + + return prompts.map((prompt, idx) => ({ prompt: prompt, negative_prompt: null, vis_type: VIS_TYPE.CHARACTER_PORTRAIT, @@ -850,72 +490,33 @@ export default { asset_name: `avatar_${this.character.name}_${uuidv4().slice(0, 10)}_${idx + 1}`, }, })); - - // Add to pending queue via injected method - if (this.addToVisualLibraryPendingQueue && typeof this.addToVisualLibraryPendingQueue === 'function') { - this.addToVisualLibraryPendingQueue(requests); - } else { - console.warn('addToVisualLibraryPendingQueue not available'); - } - - // Close dialog - this.generateDialogOpen = false; - this.batchPrompts = []; - this.promptInput = ''; }, - - + + handleMessage(data) { // Handle common scene_asset messages this.handleSceneAssetMessage(data); - - // Handle asset search results - if (data.type === 'asset_search_results') { - if (data.character_name === this.character?.name && - data.vis_type === VIS_TYPE.CHARACTER_PORTRAIT) { - const assetIds = data.asset_ids || []; - // Apply results directly. Calling checkReferenceAssets() here would - // re-run the same logic, find no local assets, and fire another - // search — looping until the dialog is closed. Any local-asset - // changes that arrive after this request are picked up by the - // assets / character.cover_image / character.avatar watchers. - if (assetIds.length > 0) { - this.referenceAssetIds = assetIds; - this.selectedReferenceAssetId = assetIds[0]; - this.referenceSelectionReason = { reason: 'Found via asset search' }; - this.userChangedReference = false; - } - this.hasCheckedReferences = true; - } - } - - // Handle image generation failure - if (data.type === 'image_generation_failed') { - // Unlock dialogs to allow retry, but keep prompts and dialogs open - if (this.isGenerating) { - this.isGenerating = false; - } - if (this.isGeneratingNew) { - this.isGeneratingNew = false; - } - } - + // Handle asset search results + this.handleAssetSearchResults(data); + + this.handleImageGenerationFailed(data); + // Handle image generation completion if (data.type === 'image_generated') { const request = data.data?.request; const base64 = data.data?.base64; - + if (!base64) return; - + // Check if this is from Generate New (visualize action) if (this.isGeneratingNew && this.pendingGenerateNewRequest) { // Verify it's for our character and vis_type - const matchesCharacter = !request || + const matchesCharacter = !request || (!request.character_name || request.character_name === this.character?.name); - const matchesVisType = !request || + const matchesVisType = !request || (!request.vis_type || request.vis_type === VIS_TYPE.CHARACTER_PORTRAIT); - + if (matchesCharacter && matchesVisType) { // Backend auto-saves when the visualize flow provides an AssetAttachmentContext // (see startGenerateNew: asset_allow_override / asset_allow_auto_attach). @@ -926,7 +527,7 @@ export default { return; } } - + // Check if this is from Generate Variation (IMAGE_EDIT) if (request && base64 && request.character_name === this.character?.name && @@ -939,7 +540,7 @@ export default { this.pendingGenerationRequest = null; } } - + // Handle default avatar changes if (data.type === 'scene_asset_character_avatar') { if (data.character === this.character?.name) { @@ -955,7 +556,7 @@ export default { this.requestCharacterDetails(); } } - + // Handle current avatar changes if (data.type === 'scene_asset_character_current_avatar') { if (data.character === this.character?.name) { @@ -979,172 +580,3 @@ export default { }, } - - - diff --git a/talemate_frontend/src/components/WorldStateManagerCharacterVisualsCover.vue b/talemate_frontend/src/components/WorldStateManagerCharacterVisualsCover.vue index 7c3a32dc..e258da65 100644 --- a/talemate_frontend/src/components/WorldStateManagerCharacterVisualsCover.vue +++ b/talemate_frontend/src/components/WorldStateManagerCharacterVisualsCover.vue @@ -6,115 +6,73 @@ -
- mdi-image-off-outline -

No cover images found for {{ character.name }}

-

Generate a CHARACTER_CARD image in the Visual Library to add cover images.

-
- -
-
- -
-
- mdi-tray-arrow-down - Drop image -
-
- - Add Cover - -
- - - - - - Set as Cover Image - - - - - Set as Scene Cover Image - - - - - View Image - - - - Open in Visual Library - - - - - Delete - - - -
-
+ + + + +

@@ -126,254 +84,79 @@

- - - - - -
- mdi-image - Generate from Reference - Generate Variation -
-

- - Create your first cover image using an existing character image as reference. - Uses image editing to generate a portrait-oriented cover image based on your prompt. - - - Create a variation of an existing cover image by modifying pose, clothing, setting, or overall appearance. - Uses image editing to transform a reference image based on your prompt. - -

- - Image editing backend is not configured. Configure an image editing backend in Visual Agent settings to generate variations. - -
- - - Generate from Reference - Generate Variation - - -
-
+ + + - - - - -
- mdi-image-plus - Generate New -
-

- Create a completely new cover image from scratch using natural language instructions. - The visual agent will generate a prompt and create a new image based on your description. -

- - Image creation backend is not configured. Configure a text-to-image backend in Visual Agent settings to generate new cover images. - -
- - - Generate New - - -
-
-
+ + + + - - - - - Generate from Reference for {{ character.name }} - Generate Variation for {{ character.name }} - - -

- - Enter a prompt to generate a portrait-oriented cover image of the character based on the reference image. - - - Enter a prompt to modify the character's pose, clothing, setting, or overall appearance (e.g., 'change pose to standing', 'add armor', 'change background to forest', 'different outfit', etc.). - -

- - -
- - No reference images available for this character. - -
-
- - Loading reference images... - -
- - - -
- mdi-information-outline -
-
Why this reference was chosen:
-
- {{ referenceSelectionReason.reason }} -
-
-
-
-
- - - Single - Batch - - - - - - - - - - - -
- mdi-information-outline -
-
- Each prompt will create a separate generation using the same reference image and settings. Generations will be queued in the Visual Library. -
-
-
-
-
-
-
-
- - - Cancel - - {{ generationMode === 'batch' ? 'Queue Batch' : 'Generate' }} - - -
-
- - - - - - Generate New Cover Image for {{ character.name }} - - -

- Enter a prompt to generate a new cover image. The visual agent will create an image based on your description. -

- - -
- - - Cancel - - Generate - - -
-
+ + + + - + 0; + referenceConfig() { + return { + visType: VIS_TYPE.CHARACTER_CARD, + preferredId: this.currentCoverImageId, + fallbackId: this.character?.avatar, + }; }, - hasAnyCharacterAssets() { - return this.anyCharacterAssets.length > 0; + variationPromptHint() { + return this.shouldUseVariationForInitial + ? 'e.g., Create a portrait-oriented cover image showcasing the character appearance and style, keeping the same art style' + : 'e.g., change pose to standing, add armor, different outfit'; }, - shouldUseVariationForInitialCover() { - // Use variation flow for initial cover if: - // 1. No CHARACTER_CARD cover images exist yet - // 2. Character has ANY assets - // 3. Image editing is available - return this.assets.length === 0 && - this.hasAnyCharacterAssets && - this.imageEditAvailable; + initialVariationPrompt() { + return 'Create a portrait-oriented cover image showcasing the character\'s appearance and style, keeping the same art style.'; }, - canGenerate() { - if (this.generationMode === 'batch') { - // Batch mode: need at least one prompt in the list - return this.batchPrompts.length > 0 && this.selectedReferenceAssetId; - } else { - // Single mode: need prompt - return this.promptInput.trim() && this.selectedReferenceAssetId; - } + initialNewPrompt() { + return 'Create a portrait-oriented cover image showcasing the character\'s appearance and style'; }, }, watch: { @@ -519,172 +280,69 @@ export default { // Request base64 for new assets const assetIds = assets.map(a => a.id); this.loadAssets(assetIds); - + // Re-check reference assets when assets change (to handle fallback logic) this.checkReferenceAssets(); }, immediate: true, }, 'character.cover_image': { - handler(newCoverImageId) { + handler() { // Re-check reference assets when cover image changes this.checkReferenceAssets(); }, }, 'character.avatar': { - handler(newAvatarId) { + handler() { // Re-check reference assets when avatar changes (Priority 5) this.checkReferenceAssets(); }, }, }, methods: { - - selectAsset(assetId) { - this.selectedAssetId = assetId; + cardStateClass(asset) { + return { + 'selected': this.selectedAssetId === asset.id, + 'current': this.currentCoverImageId === asset.id, + }; }, - - setCoverImage() { - if (!this.selectedAssetId) return; - this.setCoverImageForAsset(this.selectedAssetId); - }, - + setCoverImageForAsset(assetId) { if (!assetId) return; - + this.getWebsocket().send(JSON.stringify({ type: 'scene_assets', action: 'set_character_cover_image', asset_id: assetId, character_name: this.character.name, })); - + // Request character details to sync up the UI after setting cover image this.requestCharacterDetails(); }, - + setSceneCoverImage(assetId) { if (!assetId) return; - + this.getWebsocket().send(JSON.stringify({ type: 'scene_assets', action: 'set_scene_cover_image', asset_id: assetId, })); }, - - checkReferenceAssets() { - if (!this.character?.name) return; - - const targetVisType = VIS_TYPE.CHARACTER_CARD; - const avatarId = this.character?.avatar; - - // Use the shared helper to compute ordered options - // Pass same-vis-type assets first (this.assets), then all character assets, preferred ID, and fallback - const { selectedId, orderedIds, reason } = computeCharacterReferenceOptions( - targetVisType, - this.anyCharacterAssets, - this.currentCoverImageId, - this.assets, // same-vis-type assets (CHARACTER_CARD) - avatarId // fallback: avatar - ); - - if (orderedIds.length > 0) { - this.referenceAssetIds = orderedIds; - this.selectedReferenceAssetId = selectedId; - this.referenceSelectionReason = reason ? { reason } : null; - this.userChangedReference = false; - this.hasCheckedReferences = true; - } else { - // No local assets found - this.referenceAssetIds = []; - this.selectedReferenceAssetId = null; - this.referenceSelectionReason = null; - this.userChangedReference = false; - // Search once per character. Mark as checked BEFORE sending so re-entry - // (e.g. via watchers firing while waiting for the response) doesn't spam - // the backend with identical search requests. - if (!this.hasCheckedReferences) { - this.hasCheckedReferences = true; - this.getWebsocket().send(JSON.stringify({ - type: 'scene_assets', - action: 'search', - vis_type: targetVisType, - character_name: this.character.name, - reference_vis_types: [targetVisType], - })); - } - } - }, - - onReferenceSelectionChange(newId) { - // Track that user manually changed the selection - if (newId !== this.selectedReferenceAssetId && this.referenceSelectionReason) { - this.userChangedReference = true; - } - }, - - openGenerateDialog() { - // Ensure reference assets are checked first - if (!this.hasCheckedReferences) { - this.checkReferenceAssets(); - } - - // Set default prompt for initial cover generation if not already set - if (this.shouldUseVariationForInitialCover && !this.promptInput) { - this.promptInput = 'Create a portrait-oriented cover image showcasing the character\'s appearance and style, keeping the same art style.'; - } else if (!this.promptInput) { - // Clear prompt for normal variation generation - this.promptInput = ''; - } - - this.generateDialogOpen = true; - - // Ensure all reference assets are loaded for carousel - if (this.referenceAssetIds.length > 0) { - this.loadAssets(this.referenceAssetIds); - } - }, - - closeGenerateDialog() { - if (!this.isGenerating) { - this.generateDialogOpen = false; - this.promptInput = ''; - this.batchPrompts = []; - this.generationMode = 'single'; - } - }, - - openGenerateNewDialog() { - this.generateNewDialogOpen = true; - // Prefill with default prompt if there are no cover images yet - if (this.assets.length === 0) { - this.generateNewPromptInput = 'Create a portrait-oriented cover image showcasing the character\'s appearance and style'; - } else { - this.generateNewPromptInput = ''; - } - }, - - closeGenerateNewDialog() { - if (!this.isGeneratingNew) { - this.generateNewDialogOpen = false; - this.generateNewPromptInput = ''; - } - }, - startGenerateNew() { if (!this.generateNewPromptInput.trim() || this.isGeneratingNew) return; - + this.isGeneratingNew = true; - + // Store the request for saving later this.pendingGenerateNewRequest = { prompt: this.generateNewPromptInput.trim(), vis_type: VIS_TYPE.CHARACTER_CARD, character_name: this.character.name, }; - + // Use visualize action similar to VisualLibraryGenerate instruct mode const payload = { type: 'visual', @@ -693,33 +351,15 @@ export default { character_name: this.character.name, instructions: this.generateNewPromptInput.trim(), }; - + this.getWebsocket().send(JSON.stringify(payload)); }, - - startGeneration() { - if (this.isGenerating) return; - - // Need at least one selected reference asset for IMAGE_EDIT - if (!this.selectedReferenceAssetId) { - console.warn('No reference asset selected for cover image generation'); - return; - } - - if (this.generationMode === 'batch') { - // Batch mode: parse lines and queue them - this.startBatchGeneration(); - } else { - // Single mode: existing behavior - this.startSingleGeneration(); - } - }, - + startSingleGeneration() { if (!this.promptInput.trim() || this.isGenerating) return; - + this.isGenerating = true; - + // Store the generation request for saving later // Use the selected reference asset this.pendingGenerationRequest = { @@ -732,25 +372,19 @@ export default { reference_assets: [this.selectedReferenceAssetId], inline_reference: null, }; - + // Generate image using prompt generation endpoint with IMAGE_EDIT const payload = { type: 'visual', action: 'generate', generation_request: this.pendingGenerationRequest, }; - + this.getWebsocket().send(JSON.stringify(payload)); }, - - startBatchGeneration() { - if (this.batchPrompts.length === 0) return; - - // Use prompts from the list - const prompts = this.batchPrompts; - - // Create generation request for each prompt - const requests = prompts.map((prompt, idx) => ({ + + buildBatchRequests(prompts) { + return prompts.map((prompt, idx) => ({ prompt: prompt, negative_prompt: null, vis_type: VIS_TYPE.CHARACTER_CARD, @@ -764,72 +398,33 @@ export default { asset_name: `cover_${this.character.name}_${uuidv4().slice(0, 10)}_${idx + 1}`, }, })); - - // Add to pending queue via injected method - if (this.addToVisualLibraryPendingQueue && typeof this.addToVisualLibraryPendingQueue === 'function') { - this.addToVisualLibraryPendingQueue(requests); - } else { - console.warn('addToVisualLibraryPendingQueue not available'); - } - - // Close dialog - this.generateDialogOpen = false; - this.batchPrompts = []; - this.promptInput = ''; }, - - + + handleMessage(data) { // Handle common scene_asset messages this.handleSceneAssetMessage(data); - - // Handle asset search results - if (data.type === 'asset_search_results') { - if (data.character_name === this.character?.name && - data.vis_type === VIS_TYPE.CHARACTER_CARD) { - const assetIds = data.asset_ids || []; - // Apply results directly. Calling checkReferenceAssets() here would - // re-run the same logic, find no local assets, and fire another - // search — looping until the dialog is closed. Any local-asset - // changes that arrive after this request are picked up by the - // assets / character.cover_image / character.avatar watchers. - if (assetIds.length > 0) { - this.referenceAssetIds = assetIds; - this.selectedReferenceAssetId = assetIds[0]; - this.referenceSelectionReason = { reason: 'Found via asset search' }; - this.userChangedReference = false; - } - this.hasCheckedReferences = true; - } - } - - // Handle image generation failure - if (data.type === 'image_generation_failed') { - // Unlock dialogs to allow retry, but keep prompts and dialogs open - if (this.isGenerating) { - this.isGenerating = false; - } - if (this.isGeneratingNew) { - this.isGeneratingNew = false; - } - } - + // Handle asset search results + this.handleAssetSearchResults(data); + + this.handleImageGenerationFailed(data); + // Handle image generation completion if (data.type === 'image_generated') { const request = data.data?.request; const base64 = data.data?.base64; - + if (!base64) return; - + // Check if this is from Generate New (visualize action) if (this.isGeneratingNew && this.pendingGenerateNewRequest) { // Verify it's for our character and vis_type - const matchesCharacter = !request || + const matchesCharacter = !request || (!request.character_name || request.character_name === this.character?.name); - const matchesVisType = !request || + const matchesVisType = !request || (!request.vis_type || request.vis_type === VIS_TYPE.CHARACTER_CARD); - + if (matchesCharacter && matchesVisType) { // Use the request directly - it contains all the generation details including the generated prompt // Ensure character_name is set correctly @@ -838,13 +433,13 @@ export default { character_name: this.character.name, vis_type: request?.vis_type || VIS_TYPE.CHARACTER_CARD, }; - + // Save the generated image as a scene asset // If this is the first cover, set reference field to include both CHARACTER_PORTRAIT and CHARACTER_CARD const isFirstCover = this.assets.length === 0; const reference = isFirstCover ? [VIS_TYPE.CHARACTER_PORTRAIT, VIS_TYPE.CHARACTER_CARD] : null; this.saveGeneratedImage(base64, saveRequest, 'cover', reference); - + this.isGeneratingNew = false; this.generateNewDialogOpen = false; this.generateNewPromptInput = ''; @@ -852,7 +447,7 @@ export default { return; } } - + // Check if this is from Generate Variation (IMAGE_EDIT) if (request && base64 && request.character_name === this.character?.name && @@ -863,14 +458,14 @@ export default { const isFirstCover = this.assets.length === 0; const reference = isFirstCover ? [VIS_TYPE.CHARACTER_PORTRAIT, VIS_TYPE.CHARACTER_CARD] : null; this.saveGeneratedImage(base64, request, 'cover', reference); - + this.isGenerating = false; this.generateDialogOpen = false; this.promptInput = ''; this.pendingGenerationRequest = null; } } - + // Update selection when cover image changes if (data.type === 'scene_asset_character_cover_image') { if (data.character === this.character?.name) { @@ -901,107 +496,3 @@ export default { }, } - - - diff --git a/talemate_frontend/src/components/WorldStateManagerSceneVisualsAssets.vue b/talemate_frontend/src/components/WorldStateManagerSceneVisualsAssets.vue index b38cf873..a4d7a8ee 100644 --- a/talemate_frontend/src/components/WorldStateManagerSceneVisualsAssets.vue +++ b/talemate_frontend/src/components/WorldStateManagerSceneVisualsAssets.vue @@ -42,127 +42,83 @@ -
- mdi-image-off-outline -

No {{ typeConfig.pluralLabel }} found for this scene

-

Generate one below, or drop an image onto the upload card.

-
- -
-
- -
-
- mdi-tray-arrow-down - Drop image -
-
- - Add {{ typeConfig.label }} - -
- - - - - - Set as Scene Cover Image - - - - Set as Scene Backdrop - - - - Unset Scene Backdrop - - - - - View Image - - - - Open in Visual Library - - - - - Delete - - - -
-
+ + + + +

{{ typeConfig.description }}

@@ -171,210 +127,61 @@

- - - - - -
- mdi-image - Generate Variation -
-

- Create a variation of an existing scene image by modifying time of day, weather, mood, or details. - Uses image editing to transform a reference image based on your prompt. -

- - Image editing backend is not configured. Configure an image editing backend in Visual Agent settings to generate variations. - -
- - - Generate Variation - - -
-
+ + + - - - - -
- mdi-image-plus - Generate New -
-

- Create a completely new {{ typeConfig.label }} from scratch using natural language instructions. - The visual agent will generate a prompt and create a new image based on your description. -

- - Image creation backend is not configured. Configure a text-to-image backend in Visual Agent settings to generate new images. - -
- - - Generate New - - -
-
-
+ + + + - - - - - Generate {{ typeConfig.label }} variation - - -

- Enter a prompt to modify the reference image (e.g., 'make it night time', 'add rain', 'ruin the buildings', 'change season to winter'). -

- - -
- - No reference images available for this scene. - -
- - - Single - Batch - - - - - - - - - - - -
- mdi-information-outline -
-
- Each prompt will create a separate generation using the same reference image and settings. Generations will be queued in the Visual Library. -
-
-
-
-
-
-
-
- - - Cancel - - {{ generationMode === 'batch' ? 'Queue Batch' : 'Generate' }} - - -
-
- - - - - - Generate new {{ typeConfig.label }} - - -

- Enter instructions for the new image. The visual agent will build a prompt from the current scene state and your instructions. -

- - -
- - - Cancel - - Generate - - -
-
+ + + + id); return [...sameType, ...otherTypes]; }, - hasReferenceAssets() { - return this.referenceAssetIds.length > 0; - }, coverImageId() { return this.scene?.data?.assets?.cover_image || null; }, @@ -516,12 +314,6 @@ export default { character: null, }; }, - canGenerate() { - if (this.generationMode === 'batch') { - return this.batchPrompts.length > 0 && this.selectedReferenceAssetId; - } - return this.promptInput.trim() && this.selectedReferenceAssetId; - }, }, watch: { assets: { @@ -532,6 +324,12 @@ export default { }, }, methods: { + cardStateClass(asset) { + return { + 'current': this.coverImageId === asset.id || this.backdropAssetId === asset.id, + }; + }, + setSceneCoverImage(assetId) { if (!assetId) return; @@ -565,27 +363,11 @@ export default { } }, - closeGenerateDialog() { - if (!this.isGenerating) { - this.generateDialogOpen = false; - this.promptInput = ''; - this.batchPrompts = []; - this.generationMode = 'single'; - } - }, - openGenerateNewDialog() { this.generateNewDialogOpen = true; this.generateNewPromptInput = ''; }, - closeGenerateNewDialog() { - if (!this.isGeneratingNew) { - this.generateNewDialogOpen = false; - this.generateNewPromptInput = ''; - } - }, - startGenerateNew() { if (!this.generateNewPromptInput.trim() || this.isGeneratingNew) return; @@ -599,21 +381,6 @@ export default { })); }, - startGeneration() { - if (this.isGenerating) return; - - if (!this.selectedReferenceAssetId) { - console.warn('No reference asset selected for scene image generation'); - return; - } - - if (this.generationMode === 'batch') { - this.startBatchGeneration(); - } else { - this.startSingleGeneration(); - } - }, - buildGenerationRequest(prompt) { return { prompt: prompt, @@ -638,35 +405,20 @@ export default { })); }, - startBatchGeneration() { - if (this.batchPrompts.length === 0) return; - - const requests = this.batchPrompts.map((prompt, idx) => ({ + buildBatchRequests(prompts) { + return prompts.map((prompt, idx) => ({ ...this.buildGenerationRequest(prompt), asset_attachment_context: { allow_override: true, asset_name: `${this.typeConfig.namePrefix}_scene_${uuidv4().slice(0, 10)}_${idx + 1}`, }, })); - - if (this.addToVisualLibraryPendingQueue && typeof this.addToVisualLibraryPendingQueue === 'function') { - this.addToVisualLibraryPendingQueue(requests); - } else { - console.warn('addToVisualLibraryPendingQueue not available'); - } - - this.generateDialogOpen = false; - this.batchPrompts = []; - this.promptInput = ''; }, handleMessage(data) { this.handleSceneAssetMessage(data); - if (data.type === 'image_generation_failed') { - this.isGenerating = false; - this.isGeneratingNew = false; - } + this.handleImageGenerationFailed(data); if (data.type === 'image_generated') { const request = data.data?.request; @@ -709,85 +461,3 @@ export default { }, } - -