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 @@
+
+
+ {{ icon }}
+ {{ label }}
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+ mdi-image
+ {{ variationLabel }}
+
+
+
+
+
+ Image editing backend is not configured. Configure an image editing backend in Visual Agent settings to generate variations.
+
+
+
+
+ {{ variationLabel }}
+
+
+
+
+
+
+
+
+
+
+ mdi-image-plus
+ Generate New
+
+
+ Create a completely new {{ newLabel }} 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 {{ createWarningSubject }}.
+
+
+
+
+ Generate New
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ noReferencesText }}
+
+
+
+
+ Loading reference images...
+
+
+
+
+
+
+
mdi-information-outline
+
+
Why this reference was chosen:
+
+ {{ selectionReason }}
+
+
+
+
+
+
+
+ Single
+ Batch
+
+
+
+
+
+
+
+
+
+
+
+
+
mdi-information-outline
+
+
+ {{ batchInfoText }}
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+ {{ mode === 'batch' ? 'Queue Batch' : 'Generate' }}
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+ Generate
+
+
+
+
+
+
+
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 @@
+
+
+
+ mdi-image-off-outline
+
+
+
+
+
+
+
+
+ mdi-tray-arrow-down
+ Drop image
+
+
+
+ {{ dropLabel }}
+
+
+
+
+
+
+
+ {{ asset.meta?.name || asset.id.slice(0, 10) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- mdi-check
- Default
-
-
- mdi-account
- Current
-
-
-
-
- {{ asset.meta?.name || asset.id.slice(0, 10) }}
-
-
- mdi-tag-off
- No Tags
-
-
-
-
-
-
- mdi-check
-
- Set as Default
-
-
-
- mdi-account
-
- Set as Current
-
-
-
-
- mdi-eye-outline
-
- View Image
-
-
-
- mdi-image-multiple-outline
-
- Open in Visual Library
- Edit tags here
-
-
-
-
- mdi-close-box-outline
-
- Delete
-
-
-
-
-
+
+
+ No portraits found for {{ character.name }}
+ Generate a CHARACTER_PORTRAIT image in the Visual Library to add portraits.
+
+
+
+
+
+
+
+
+
+
+
+ mdi-check
+
+ Set as Default
+
+
+
+ mdi-account
+
+ Set as Current
+
+
+
+
+ mdi-eye-outline
+
+ View Image
+
+
+
+ mdi-image-multiple-outline
+
+ Open in Visual Library
+ Edit tags here
+
+
+
+
+ mdi-close-box-outline
+
+ 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
-
-
-
-
+
+
+
+ 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.
+
+
+
-
-
-
-
-
- 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.).
+
+
+
-
-
-
-
- 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
-
-
-
-
+
+
+ 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.
+
+
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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- mdi-check
- Current
-
-
-
- {{ asset.meta?.name || asset.id.slice(0, 10) }}
-
-
-
-
-
-
- mdi-check
-
- Set as Cover Image
-
-
-
-
- mdi-image-frame
-
- Set as Scene Cover Image
-
-
-
-
- mdi-eye-outline
-
- View Image
-
-
-
- mdi-image-multiple-outline
-
- Open in Visual Library
-
-
-
-
- mdi-close-box-outline
-
- Delete
-
-
-
-
-
+
+
+ No cover images found for {{ character.name }}
+ Generate a CHARACTER_CARD image in the Visual Library to add cover images.
+
+
+
+
+
+
+
+ mdi-check
+
+ Set as Cover Image
+
+
+
+
+ mdi-image-frame
+
+ Set as Scene Cover Image
+
+
+
+
+ mdi-eye-outline
+
+ View Image
+
+
+
+ mdi-image-multiple-outline
+
+ Open in Visual Library
+
+
+
+
+ mdi-close-box-outline
+
+ 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
-
-
-
-
+
+
+
+ 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.
+
+
+
-
-
-
-
-
- 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.).
+
+
+
-
-
-
-
- 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
-
-
-
-
+
+
+ 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.
+
+
-
+
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 }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- mdi-image-area
- Backdrop
-
-
- mdi-image-frame
- Cover
-
-
-
- {{ asset.meta?.name || asset.id.slice(0, 10) }}
-
-
-
-
-
-
- mdi-image-frame
-
- Set as Scene Cover Image
-
-
-
- mdi-image-area
-
- Set as Scene Backdrop
-
-
-
- mdi-image-remove-outline
-
- Unset Scene Backdrop
-
-
-
-
- mdi-eye-outline
-
- View Image
-
-
-
- mdi-image-multiple-outline
-
- Open in Visual Library
-
-
-
-
- mdi-close-box-outline
-
- Delete
-
-
-
-
-
+
+
+ No {{ typeConfig.pluralLabel }} found for this scene
+ Generate one below, or drop an image onto the upload card.
+
+
+
+
+
+
+
+
+ mdi-image-frame
+
+ Set as Scene Cover Image
+
+
+
+ mdi-image-area
+
+ Set as Scene Backdrop
+
+
+
+ mdi-image-remove-outline
+
+ Unset Scene Backdrop
+
+
+
+
+ mdi-eye-outline
+
+ View Image
+
+
+
+ mdi-image-multiple-outline
+
+ Open in Visual Library
+
+
+
+
+ mdi-close-box-outline
+
+ 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
-
-
-
-
+
+
+ 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.
+
+
-
-
-
-
-
- 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').
+
+
-
-
-
-
- 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
-
-
-
-
+
+
+ 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.
+
+
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 {
},
}
-
-