From 2df173e6af2e516aff60783f8d5cc8897fbc2973 Mon Sep 17 00:00:00 2001 From: munimunigamer Date: Fri, 26 Dec 2025 00:54:44 -0600 Subject: [PATCH] fixed up re-rendering images when right clicking --- src/systems/features/avatarGenerator.js | 538 +++++++++++++++++------- src/systems/generation/apiClient.js | 114 ++--- src/systems/rendering/thoughts.js | 58 ++- 3 files changed, 446 insertions(+), 264 deletions(-) diff --git a/src/systems/features/avatarGenerator.js b/src/systems/features/avatarGenerator.js index 50eb912..b7f28df 100644 --- a/src/systems/features/avatarGenerator.js +++ b/src/systems/features/avatarGenerator.js @@ -1,170 +1,36 @@ /** * Avatar Generator Module - * Handles automatic avatar generation for characters without images + * Handles automatic and manual avatar generation for NPC characters + * + * Features: + * - Batch generation with awaitable completion + * - Batch prompt generation via LLM + * - Individual image generation via /sd command + * - Manual regeneration support */ +import { generateRaw, characters, this_chid } from '../../../../../../../script.js'; import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js'; -import { extensionSettings, sessionAvatarPrompts } from '../../core/state.js'; +import { selected_group, getGroupMembers } from '../../../../../../group-chats.js'; +import { extensionSettings, sessionAvatarPrompts, setSessionAvatarPrompt } from '../../core/state.js'; import { saveSettings } from '../../core/persistence.js'; +import { generateAvatarPromptGenerationPrompt, parseAvatarPromptsResponse } from '../generation/promptBuilder.js'; +import { getCurrentPresetName, switchToPreset } from '../generation/apiClient.js'; -// Track pending avatar generations to avoid duplicate requests +// Generation state - tracks characters currently being generated const pendingGenerations = new Set(); /** - * Callback for when all avatar generations complete - * Used to trigger UI updates - */ -let onGenerationCompleteCallback = null; - -/** - * Builds the generation prompt for a character - * Uses LLM-generated prompt from session storage - * @param {string} characterName - Name of the character - * @returns {string} Full prompt for /sd command - */ -function buildGenerationPrompt(characterName) { - const llmPrompt = sessionAvatarPrompts[characterName]; - if (llmPrompt) { - console.log(`[RPG Avatar] Using LLM prompt for ${characterName}`); - return llmPrompt; - } - - console.warn(`[RPG Avatar] No LLM prompt generated for ${characterName}, skipping generation`); - return null; -} - -/** - * Sets a callback to be called when all avatar generations complete - * @param {Function} callback - Function to call when all generations are done - */ -export function setOnGenerationComplete(callback) { - onGenerationCompleteCallback = callback; -} - -/** - * Triggers the completion callback if all generations are done - */ -function checkAndTriggerCompletionCallback() { - if (pendingGenerations.size === 0 && onGenerationCompleteCallback) { - onGenerationCompleteCallback(); - onGenerationCompleteCallback = null; - } -} - -/** - * Generates an avatar for a character using /sd command - * @param {string} characterName - Name of the character to generate avatar for - * @returns {Promise} Avatar URL or null if failed - */ -export async function generateAvatar(characterName) { - // Skip if already generating - if (pendingGenerations.has(characterName)) { - console.log(`[RPG Avatar] Already generating avatar for: ${characterName}`); - return null; - } - - // Skip if disabled - if (!extensionSettings.autoGenerateAvatars) { - return null; - } - - // Skip if custom avatar already exists - if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[characterName]) { - return null; - } - - pendingGenerations.add(characterName); - console.log(`[RPG Avatar] Starting generation for: ${characterName}`); - - try { - const prompt = buildGenerationPrompt(characterName); - - // Skip if no prompt was generated (LLM hasn't generated one yet) - if (!prompt) { - console.log(`[RPG Avatar] No prompt available for ${characterName}, skipping`); - return null; - } - - // Execute /sd command with quiet=true - // IMPORTANT: quiet=true must come BEFORE the prompt - // This suppresses chat output and returns the image URL via pipe - const result = await executeSlashCommandsOnChatInput( - `/sd quiet=true ${prompt}`, - { clearChatInput: true } - ); - - // The result might be an object with various properties - // We need to extract the actual image URL if available - let imageUrl = null; - - if (result) { - // Handle different result formats - if (typeof result === 'string') { - imageUrl = result; - } else if (result.pipe) { - imageUrl = result.pipe; - } else if (result.output || result.image || result.url) { - imageUrl = result.output || result.image || result.url; - } - - // Only store if we got a valid string URL - if (imageUrl && typeof imageUrl === 'string') { - if (!extensionSettings.npcAvatars) { - extensionSettings.npcAvatars = {}; - } - extensionSettings.npcAvatars[characterName] = imageUrl; - saveSettings(); - - console.log(`[RPG Avatar] Generation complete for: ${characterName}`); - return imageUrl; - } else { - console.warn(`[RPG Avatar] Generation result for ${characterName} was not a valid URL:`, result); - } - } - - return null; - } catch (error) { - console.error(`[RPG Avatar] Generation failed for ${characterName}:`, error); - return null; - } finally { - pendingGenerations.delete(characterName); - // Check if all generations are complete and trigger callback - checkAndTriggerCompletionCallback(); - } -} - -/** - * Checks if a character needs an avatar and triggers generation - * @param {string} characterName - Name of the character to check - * @param {boolean} hasAvatar - Whether the character already has an avatar - * @returns {Promise} - */ -export function checkAndGenerateAvatar(characterName, hasAvatar) { - // Only generate if no avatar exists and feature is enabled - if (hasAvatar || !extensionSettings.autoGenerateAvatars) { - return; - } - - // Check if we already have a custom NPC avatar - if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[characterName]) { - return; - } - - // Trigger generation (non-blocking) - generateAvatar(characterName); -} - -/** - * Checks if an avatar is currently being generated for a character - * @param {string} characterName - Name of the character to check - * @returns {boolean} True if generation is in progress + * Checks if a character is pending generation (waiting or actively generating) + * @param {string} characterName - Name of character to check + * @returns {boolean} True if generation is pending */ export function isGenerating(characterName) { return pendingGenerations.has(characterName); } /** - * Checks if ANY avatars are currently being generated + * Checks if any avatars are currently being generated * @returns {boolean} True if any generation is in progress */ export function isAnyGenerating() { @@ -172,20 +38,366 @@ export function isAnyGenerating() { } /** - * Waits for all pending avatar generations to complete - * @returns {Promise} + * Gets all characters currently pending generation + * @returns {string[]} Array of character names */ -export function waitForAllGenerations() { - if (pendingGenerations.size === 0) { - return Promise.resolve(); +export function getPendingGenerations() { + return [...pendingGenerations]; +} + +/** + * Helper to check if two character names match (case-insensitive, handles partial matches) + * @param {string} cardName - Name from character card + * @param {string} aiName - Name from AI response + * @returns {boolean} True if names match + */ +function namesMatch(cardName, aiName) { + if (!cardName || !aiName) return false; + const cardLower = cardName.toLowerCase().trim(); + const aiLower = aiName.toLowerCase().trim(); + if (cardLower === aiLower) return true; + const cardCore = cardLower.split(/[\s,'"]+/)[0]; + const aiCore = aiLower.split(/[\s,'"]+/)[0]; + if (cardCore === aiCore) return true; + const escapedCardCore = cardCore.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const wordBoundary = new RegExp(`\\b${escapedCardCore}\\b`); + return wordBoundary.test(aiCore); +} + +/** + * Checks if a character already has an avatar (custom NPC avatar or from character card) + * @param {string} characterName - Name of character to check + * @returns {boolean} True if character has an avatar + */ +export function hasExistingAvatar(characterName) { + // Check for custom NPC avatar first + if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[characterName]) { + const avatar = extensionSettings.npcAvatars[characterName]; + if (typeof avatar === 'string' && avatar) { + return true; + } } - return new Promise((resolve) => { - const checkInterval = setInterval(() => { - if (pendingGenerations.size === 0) { - clearInterval(checkInterval); - resolve(); + // Check group members for avatar + if (selected_group) { + try { + const groupMembers = getGroupMembers(selected_group); + if (groupMembers && groupMembers.length > 0) { + const matchingMember = groupMembers.find(member => + member && member.name && namesMatch(member.name, characterName) + ); + if (matchingMember && matchingMember.avatar && matchingMember.avatar !== 'none') { + return true; + } } - }, 100); - }); + } catch (e) { + // Ignore errors + } + } + + // Check all characters for avatar + if (characters && characters.length > 0) { + const matchingCharacter = characters.find(c => + c && c.name && namesMatch(c.name, characterName) + ); + if (matchingCharacter && matchingCharacter.avatar && matchingCharacter.avatar !== 'none') { + return true; + } + } + + // Check current character in 1-on-1 chat + if (this_chid !== undefined && characters[this_chid] && + characters[this_chid].name && namesMatch(characters[this_chid].name, characterName)) { + if (characters[this_chid].avatar && characters[this_chid].avatar !== 'none') { + return true; + } + } + + return false; +} + +/** + * Generates avatars for multiple characters and waits for all to complete. + * This is the main entry point for auto-generation within a workflow. + * + * @param {string[]} characterNames - Array of character names to generate avatars for + * @param {Function} onStarted - Optional callback when generation starts (to update UI) + * @returns {Promise} Resolves when all generations complete + */ +export async function generateAvatarsForCharacters(characterNames, onStarted = null) { + if (!extensionSettings.autoGenerateAvatars) { + return; + } + + // Filter to characters that need avatars + const needsGeneration = characterNames.filter(name => { + // Skip if already pending + if (pendingGenerations.has(name)) { + return false; + } + // Skip if has avatar + if (hasExistingAvatar(name)) { + return false; + } + return true; + }); + + if (needsGeneration.length === 0) { + return; + } + + console.log('[RPG Avatar] Starting batch generation for:', needsGeneration); + + // Mark all as pending IMMEDIATELY (before any async work) + for (const name of needsGeneration) { + pendingGenerations.add(name); + } + + // Trigger UI update to show loading spinners + if (onStarted) { + try { + onStarted([...needsGeneration]); + } catch (e) { + console.error('[RPG Avatar] Error in onStarted callback:', e); + } + } + + try { + // Generate LLM prompts for all characters that don't have them + const needsPrompts = needsGeneration.filter(name => !sessionAvatarPrompts[name]); + if (needsPrompts.length > 0) { + await generateLLMPrompts(needsPrompts); + } + + // Generate images one at a time + for (const characterName of needsGeneration) { + // Skip if somehow already has avatar now + if (hasExistingAvatar(characterName)) { + pendingGenerations.delete(characterName); + continue; + } + + await generateSingleAvatar(characterName); + pendingGenerations.delete(characterName); + + // Small delay between generations to avoid overwhelming the API + if (needsGeneration.indexOf(characterName) < needsGeneration.length - 1) { + await new Promise(resolve => setTimeout(resolve, 500)); + } + } + } finally { + // Ensure all are removed from pending even if there's an error + for (const name of needsGeneration) { + pendingGenerations.delete(name); + } + } + + console.log('[RPG Avatar] Batch generation complete'); +} + +/** + * Regenerates avatar for a specific character + * Clears existing avatar and prompt, then generates new ones + * Handles preset switching if useSeparatePreset is enabled + * + * @param {string} characterName - Name of character to regenerate + * @returns {Promise} New avatar URL or null if failed + */ +export async function regenerateAvatar(characterName) { + console.log('[RPG Avatar] Regenerating avatar for:', characterName); + + // Mark as pending immediately + pendingGenerations.add(characterName); + + // Clear existing avatar + if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[characterName]) { + delete extensionSettings.npcAvatars[characterName]; + saveSettings(); + } + + // Clear existing prompt to force new LLM generation + if (sessionAvatarPrompts[characterName]) { + delete sessionAvatarPrompts[characterName]; + } + + // Save current preset and switch to RPG Companion Trackers if enabled + let originalPresetName = null; + if (extensionSettings.useSeparatePreset) { + originalPresetName = await getCurrentPresetName(); + if (originalPresetName) { + console.log(`[RPG Avatar] Switching from "${originalPresetName}" to RPG Companion Trackers preset`); + await switchToPreset('RPG Companion Trackers'); + } + } + + try { + // Generate new LLM prompt + await generateLLMPrompts([characterName]); + + // Generate the avatar + return await generateSingleAvatar(characterName); + } finally { + // Restore original preset if we switched + if (originalPresetName && extensionSettings.useSeparatePreset) { + console.log(`[RPG Avatar] Restoring original preset: "${originalPresetName}"`); + await switchToPreset(originalPresetName); + } + + // Remove from pending when done + pendingGenerations.delete(characterName); + } +} + +/** + * Generates LLM prompts for multiple characters in a single API call + * + * @param {string[]} characterNames - Names of characters needing prompts + */ +async function generateLLMPrompts(characterNames) { + if (characterNames.length === 0) return; + + try { + console.log('[RPG Avatar] Generating LLM prompts for:', characterNames); + + const promptMessages = await generateAvatarPromptGenerationPrompt(characterNames); + const response = await generateRaw({ + prompt: promptMessages, + quietToLoud: false + }); + + if (response) { + const prompts = parseAvatarPromptsResponse(response); + console.log('[RPG Avatar] Generated prompts:', prompts); + + // Store prompts in session storage + for (const [name, prompt] of Object.entries(prompts)) { + setSessionAvatarPrompt(name, prompt); + } + } + } catch (error) { + console.error('[RPG Avatar] Failed to generate LLM prompts:', error); + } +} + +/** + * Builds a fallback prompt when LLM prompt generation fails or isn't available + * Uses information embedded in the character name if present (e.g., from malformed tracker output) + * + * @param {string} characterName - Character name (may contain additional details) + * @returns {string} A basic prompt for image generation + */ +function buildFallbackPrompt(characterName) { + // Check if the name contains embedded details (malformed format from weaker models) + // e.g., "Eris Details: 🌟 | beautiful girl with white hair | kind expression" + if (characterName.includes('Details:') || characterName.includes('|')) { + // Extract useful description parts + const parts = characterName.split(/Details:|[|]/).map(p => p.trim()).filter(p => p && !p.match(/^[\p{Emoji}]+$/u)); + if (parts.length > 1) { + // First part is likely the name, rest are descriptions + const name = parts[0]; + const descriptions = parts.slice(1).join(', '); + return `portrait of ${name}, ${descriptions}, fantasy art style, detailed`; + } + } + + // Simple fallback - just use the name + return `portrait of ${characterName}, character portrait, fantasy art style, detailed face, high quality`; +} + +/** + * Generates a single avatar using the /sd command + * + * @param {string} characterName - Name of character to generate avatar for + * @returns {Promise} Avatar URL or null if failed + */ +async function generateSingleAvatar(characterName) { + // Get the prompt from session storage, or build a fallback + let prompt = sessionAvatarPrompts[characterName]; + if (!prompt) { + console.log(`[RPG Avatar] No LLM prompt for ${characterName}, using fallback prompt`); + prompt = buildFallbackPrompt(characterName); + } + + console.log(`[RPG Avatar] Starting image generation for: ${characterName}`); + + try { + // Execute /sd command with quiet=true to suppress chat output + const result = await executeSlashCommandsOnChatInput( + `/sd quiet=true ${prompt}`, + { clearChatInput: true } + ); + + // Extract image URL from result + const imageUrl = extractImageUrl(result); + + if (imageUrl) { + // Store the avatar + if (!extensionSettings.npcAvatars) { + extensionSettings.npcAvatars = {}; + } + extensionSettings.npcAvatars[characterName] = imageUrl; + saveSettings(); + + console.log(`[RPG Avatar] Successfully generated avatar for: ${characterName}`); + return imageUrl; + } else { + console.warn(`[RPG Avatar] Failed to extract image URL for ${characterName}:`, result); + return null; + } + } catch (error) { + console.error(`[RPG Avatar] Generation failed for ${characterName}:`, error); + return null; + } +} + +/** + * Extracts image URL from /sd command result + * Handles various result formats + * + * @param {any} result - Result from executeSlashCommandsOnChatInput + * @returns {string|null} Image URL or null + */ +function extractImageUrl(result) { + if (!result) return null; + + // Handle string result + if (typeof result === 'string') { + // Validate it looks like a URL or data URI + if (result.startsWith('http') || result.startsWith('data:') || result.startsWith('/')) { + return result; + } + return null; + } + + // Handle object result with various possible properties + if (typeof result === 'object') { + // Try common properties + const url = result.pipe || result.output || result.image || result.url || result.result; + + if (url && typeof url === 'string') { + if (url.startsWith('http') || url.startsWith('data:') || url.startsWith('/')) { + return url; + } + } + } + + return null; +} + +/** + * Clears all pending generations and resets state + */ +export function clearPendingGenerations() { + pendingGenerations.clear(); +} + +/** + * Gets the current generation status for display + * @returns {{pending: number, names: string[]}} + */ +export function getGenerationStatus() { + return { + pending: pendingGenerations.size, + names: [...pendingGenerations] + }; } diff --git a/src/systems/generation/apiClient.js b/src/systems/generation/apiClient.js index 9d787fb..2c74834 100644 --- a/src/systems/generation/apiClient.js +++ b/src/systems/generation/apiClient.js @@ -12,14 +12,11 @@ import { isGenerating, lastActionWasSwipe, setIsGenerating, - setLastActionWasSwipe, - sessionAvatarPrompts + setLastActionWasSwipe } from '../../core/state.js'; import { saveChatData } from '../../core/persistence.js'; import { - generateSeparateUpdatePrompt, - generateAvatarPromptGenerationPrompt, - parseAvatarPromptsResponse + generateSeparateUpdatePrompt } from './promptBuilder.js'; import { parseResponse, parseUserStats } from './parser.js'; import { renderUserStats } from '../rendering/userStats.js'; @@ -28,7 +25,7 @@ import { renderThoughts } from '../rendering/thoughts.js'; import { renderInventory } from '../rendering/inventory.js'; import { renderQuests } from '../rendering/quests.js'; import { i18n } from '../../core/i18n.js'; -import { setOnGenerationComplete, waitForAllGenerations } from '../features/avatarGenerator.js'; +import { generateAvatarsForCharacters } from '../features/avatarGenerator.js'; // Store the original preset name to restore after tracker generation let originalPresetName = null; @@ -37,7 +34,7 @@ let originalPresetName = null; * Gets the current preset name using the /preset command * @returns {Promise} Current preset name or null if unavailable */ -async function getCurrentPresetName() { +export async function getCurrentPresetName() { try { // Use /preset without arguments to get the current preset name const result = await executeSlashCommandsOnChatInput('/preset', { quiet: true }); @@ -61,12 +58,14 @@ async function getCurrentPresetName() { console.error('[RPG Companion] Error getting current preset:', error); return null; } -}/** +} + +/** * Switches to a specific preset by name using the /preset slash command * @param {string} presetName - Name of the preset to switch to * @returns {Promise} True if switching succeeded, false otherwise */ -async function switchToPreset(presetName) { +export async function switchToPreset(presetName) { try { // Use the /preset slash command to switch presets // This is the proper way to change presets in SillyTavern @@ -163,15 +162,6 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough lastGeneratedData.characterThoughts = parsedData.characterThoughts; } - // Generate avatar prompts if auto-generate is enabled and characters need avatars - if (extensionSettings.autoGenerateAvatars) { - const charactersNeedingPrompts = parseCharactersWithoutAvatars(parsedData.characterThoughts); - if (charactersNeedingPrompts.length > 0) { - console.log('[RPG Companion] Generating LLM avatar prompts for:', charactersNeedingPrompts); - await generateAvatarPrompts(charactersNeedingPrompts); - } - } - // When saveTrackerHistory is enabled, store tracker data on the user's message too // This allows scrolling through history and seeing trackers at each point if (extensionSettings.saveTrackerHistory && lastMessage && lastMessage.is_user) { @@ -222,31 +212,40 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough // console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data'); } - // Render the updated data (outside the message check, always render) + // Render the updated data renderUserStats(); renderInfoBox(); renderThoughts(); renderInventory(); renderQuests(); - // Set up callback to re-render thoughts when avatars finish generating - setOnGenerationComplete(() => { - console.log('[RPG Companion] Avatar generation complete, re-rendering thoughts...'); - renderThoughts(); - }); - // Save to chat metadata saveChatData(); + + // Generate avatars if auto-generate is enabled (runs within this workflow) + // This uses the RPG Companion Trackers preset and keeps the button spinning + if (extensionSettings.autoGenerateAvatars) { + const charactersNeedingAvatars = parseCharactersFromThoughts(parsedData.characterThoughts); + if (charactersNeedingAvatars.length > 0) { + console.log('[RPG Companion] Generating avatars for:', charactersNeedingAvatars); + + // Generate avatars - this awaits completion + await generateAvatarsForCharacters(charactersNeedingAvatars, (names) => { + // Callback when generation starts - re-render to show loading spinners + console.log('[RPG Companion] Avatar generation started, showing spinners...'); + renderThoughts(); + }); + + // Re-render once all avatars are generated + console.log('[RPG Companion] All avatars generated, re-rendering...'); + renderThoughts(); + } + } } } catch (error) { console.error('[RPG Companion] Error updating RPG data:', error); } finally { - // Wait for all avatar generations to complete before finishing - console.log('[RPG Companion] Waiting for avatar generations to complete...'); - await waitForAllGenerations(); - console.log('[RPG Companion] All avatar generations complete.'); - // Restore original preset if we switched to a separate one if (originalPresetName && extensionSettings.useSeparatePreset) { console.log(`[RPG Companion] Restoring original preset: "${originalPresetName}"`); @@ -269,11 +268,11 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough } /** - * Parses character thoughts to find characters that need avatar prompts + * Parses character names from Present Characters thoughts data * @param {string} characterThoughtsData - Raw character thoughts data - * @returns {Array} Array of character names needing prompts + * @returns {Array} Array of character names found */ -function parseCharactersWithoutAvatars(characterThoughtsData) { +function parseCharactersFromThoughts(characterThoughtsData) { if (!characterThoughtsData) return []; const lines = characterThoughtsData.split('\n'); @@ -283,58 +282,9 @@ function parseCharactersWithoutAvatars(characterThoughtsData) { if (line.trim().startsWith('- ')) { const name = line.trim().substring(2).trim(); if (name && name.toLowerCase() !== 'unavailable') { - // Skip if already has custom avatar - if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[name]) { - continue; - } - // Skip if already has session prompt - if (sessionAvatarPrompts[name]) { - continue; - } characters.push(name); } } } return characters; } - -/** - * Generates LLM-based avatar prompts for specified characters - * Called during batch RPG data refresh when avatar generation is enabled - * - * @param {Array} characterNames - Array of character names needing prompts - * @returns {Promise} Map of character name to generated prompt - */ -export async function generateAvatarPrompts(characterNames) { - if (!characterNames || characterNames.length === 0) { - return {}; - } - - try { - console.log('[RPG Avatar] Generating LLM prompts for characters:', characterNames); - - const prompt = await generateAvatarPromptGenerationPrompt(characterNames); - - // Generate using raw prompt - const response = await generateRaw({ - prompt: prompt, - quietToLoud: false - }); - - if (response) { - const prompts = parseAvatarPromptsResponse(response); - console.log('[RPG Avatar] Generated prompts:', prompts); - - // Store in session-only storage - for (const [name, prompt] of Object.entries(prompts)) { - sessionAvatarPrompts[name] = prompt; - } - - return prompts; - } - } catch (error) { - console.error('[RPG Avatar] LLM prompt generation failed:', error); - } - - return {}; -} diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index 5162c68..b75f5d8 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -17,7 +17,7 @@ import { import { saveChatData } from '../../core/persistence.js'; import { getSafeThumbnailUrl } from '../../utils/avatars.js'; import { saveSettings } from '../../core/persistence.js'; -import { checkAndGenerateAvatar, isGenerating } from '../features/avatarGenerator.js'; +import { isGenerating, regenerateAvatar } from '../features/avatarGenerator.js'; /** * Helper to log to both console and debug logs array @@ -174,11 +174,6 @@ function getCharacterAvatar(characterName) { } } - // Trigger auto-generation if no avatar was found - if (!hasAvatar) { - checkAndGenerateAvatar(characterName, false); - } - return characterPortrait; } @@ -481,7 +476,7 @@ export function renderThoughts() { html += '
'; html += `
-
+
${escapedDefaultName}
⚖️
@@ -542,7 +537,7 @@ export function renderThoughts() { html += `
-
+
${escapedName} ${isCurrentlyGenerating ? '
' : ''} ${hasRelationshipEnabled ? `
${relationshipBadge}
` : ''} @@ -628,8 +623,8 @@ export function renderThoughts() { uploadNpcAvatar(characterName); }); - // Add event handler for removing custom avatars (right-click) - $thoughtsContainer.find('.rpg-avatar-upload').on('contextmenu', function(e) { + // Add event handler for regenerating avatars (right-click) + $thoughtsContainer.find('.rpg-avatar-upload').on('contextmenu', async function(e) { // Prevent triggering if clicking on the relationship badge if ($(e.target).hasClass('rpg-relationship-badge') || $(e.target).closest('.rpg-relationship-badge').length > 0) { return; @@ -637,17 +632,42 @@ export function renderThoughts() { e.preventDefault(); // Prevent default context menu const characterName = $(this).data('character'); + const $avatarEl = $(this); - // Check if this character has a custom avatar - if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[characterName]) { - // Remove the custom avatar - delete extensionSettings.npcAvatars[characterName]; - saveSettings(); - console.log(`[RPG Companion] Removed custom avatar for: ${characterName}`); - - // Re-render to show the default avatar - renderThoughts(); + // Check if auto-generation is enabled + if (!extensionSettings.autoGenerateAvatars) { + // If auto-generation is disabled, just remove the custom avatar + if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[characterName]) { + delete extensionSettings.npcAvatars[characterName]; + saveSettings(); + console.log(`[RPG Companion] Removed custom avatar for: ${characterName}`); + renderThoughts(); + } + return; } + + // Show generating state with spinner overlay + $avatarEl.addClass('rpg-avatar-generating'); + if (!$avatarEl.find('.rpg-generating-overlay').length) { + $avatarEl.append('
'); + } + console.log(`[RPG Companion] Regenerating avatar for: ${characterName}`); + + try { + // Regenerate the avatar + const newUrl = await regenerateAvatar(characterName); + + if (newUrl) { + console.log(`[RPG Companion] Successfully regenerated avatar for: ${characterName}`); + } else { + console.warn(`[RPG Companion] Failed to regenerate avatar for: ${characterName}`); + } + } catch (error) { + console.error(`[RPG Companion] Error regenerating avatar for ${characterName}:`, error); + } + + // Re-render to show the new avatar (or fallback) + renderThoughts(); }); // Add event handler for character removal