fixed up re-rendering images when right clicking

This commit is contained in:
munimunigamer
2025-12-26 00:54:44 -06:00
parent de11f6f7e2
commit 2df173e6af
3 changed files with 446 additions and 264 deletions
+375 -163
View File
@@ -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<string|null>} 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<void>}
*/
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<void>}
* 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<void>} 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<string|null>} 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<string|null>} 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]
};
}
+31 -81
View File
@@ -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<string|null>} 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<boolean>} 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...');
// 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();
});
// Save to chat metadata
saveChatData();
// 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<string>} Array of character names needing prompts
* @returns {Array<string>} 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<string>} characterNames - Array of character names needing prompts
* @returns {Promise<Object>} 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 {};
}
+34 -14
View File
@@ -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 += '<div class="rpg-thoughts-content">';
html += `
<div class="rpg-character-card" data-character-name="${escapedDefaultName}">
<div class="rpg-character-avatar rpg-avatar-upload" data-character="${escapedDefaultName}" title="Click to upload custom avatar&#10;Right-click to remove custom avatar">
<div class="rpg-character-avatar rpg-avatar-upload" data-character="${escapedDefaultName}" title="Click to upload custom avatar&#10;Right-click to regenerate avatar">
<img src="${defaultPortrait}" alt="${escapedDefaultName}" onerror="this.style.opacity='0.5';this.onerror=null;" />
<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${escapedDefaultName}" data-field="relationship" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">⚖️</div>
</div>
@@ -542,7 +537,7 @@ export function renderThoughts() {
html += `
<div class="rpg-character-card" data-character-name="${escapedName}">
<div class="rpg-character-avatar rpg-avatar-upload ${isCurrentlyGenerating ? 'rpg-avatar-generating' : ''}" data-character="${escapedName}" title="Click to upload custom avatar&#10;Right-click to remove custom avatar">
<div class="rpg-character-avatar rpg-avatar-upload ${isCurrentlyGenerating ? 'rpg-avatar-generating' : ''}" data-character="${escapedName}" title="Click to upload custom avatar&#10;Right-click to regenerate avatar">
<img src="${characterPortrait}" alt="${escapedName}" onerror="this.style.opacity='0.5';this.onerror=null;" />
${isCurrentlyGenerating ? '<div class="rpg-generating-overlay"><i class="fa-solid fa-spinner fa-spin"></i></div>' : ''}
${hasRelationshipEnabled ? `<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${escapedName}" data-field="${relationshipFieldName}" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipBadge}</div>` : ''}
@@ -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
// 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]) {
// 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();
}
return;
}
// Show generating state with spinner overlay
$avatarEl.addClass('rpg-avatar-generating');
if (!$avatarEl.find('.rpg-generating-overlay').length) {
$avatarEl.append('<div class="rpg-generating-overlay"><i class="fa-solid fa-spinner fa-spin"></i></div>');
}
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