From 66712382d5c895c8e53b6ea1081bd7887bd69793 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Fri, 17 Oct 2025 03:05:37 +1100 Subject: [PATCH 1/2] fix: eliminate 400 Bad Request errors for persona avatar thumbnails - Add getSafeThumbnailUrl() helper function with comprehensive error handling - Replace all getThumbnailUrl() calls with safe wrapper that validates results - Use SVG data URI placeholder instead of 'img/user-default.png' to avoid 400 errors - Update img onerror handlers to fade opacity instead of trying invalid fallback paths - Add detailed console logging for debugging avatar loading issues - Improve updatePersonaAvatar() to only update src when valid URL is available This fixes persistent 400 errors on some Ubuntu systems where directory names with spaces (e.g., "User Avatars") caused thumbnail URL construction to fail. Affected functions: - getSafeThumbnailUrl() (new) - updatePersonaAvatar() - renderUserStats() - renderCharacterThoughts() --- index.js | 123 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 95 insertions(+), 28 deletions(-) diff --git a/index.js b/index.js index 68864a1..17c9e62 100644 --- a/index.js +++ b/index.js @@ -93,6 +93,46 @@ let $userStatsContainer = null; let $infoBoxContainer = null; let $thoughtsContainer = null; +/** + * Safely attempts to get a thumbnail URL with proper error handling. + * Returns null if the URL cannot be generated to avoid 400 Bad Request errors. + * + * @param {string} type - The type of thumbnail ('persona' or 'avatar') + * @param {string} filename - The filename to get thumbnail for + * @returns {string|null} - The thumbnail URL or null if it fails + */ +function getSafeThumbnailUrl(type, filename) { + // Return null if no filename provided + if (!filename || filename === 'none') { + console.log(`[RPG Companion] No valid filename provided for ${type} thumbnail`); + return null; + } + + try { + // Attempt to get thumbnail URL from SillyTavern API + const url = getThumbnailUrl(type, filename); + + // Validate that we got a string back + if (typeof url !== 'string' || url.trim() === '') { + console.warn(`[RPG Companion] getThumbnailUrl returned invalid result for ${type}:`, filename); + return null; + } + + console.log(`[RPG Companion] Successfully generated ${type} thumbnail URL for: ${filename}`); + return url; + } catch (error) { + // Log detailed error information for debugging + console.error(`[RPG Companion] Failed to get ${type} thumbnail for "${filename}":`, error); + console.error('[RPG Companion] Error details:', { + type, + filename, + errorMessage: error.message, + errorStack: error.stack + }); + return null; + } +} + /** * Loads the extension settings from the global settings object. */ @@ -2739,14 +2779,15 @@ function renderUserStats() { } // Get user portrait - handle both default-user and custom persona folders - let userPortrait = 'img/user-default.png'; // fallback + // Use a transparent placeholder as fallback to avoid 400 errors + const transparentPixel = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="100" height="100"%3E%3Crect width="100" height="100" fill="%23cccccc" opacity="0.3"/%3E%3Ctext x="50%25" y="50%25" text-anchor="middle" dy=".3em" fill="%23666" font-size="40"%3E%3F%3C/text%3E%3C/svg%3E'; + let userPortrait = transparentPixel; + if (user_avatar) { - // Try to get the thumbnail, but have a fallback - try { - userPortrait = getThumbnailUrl('persona', user_avatar) || 'img/user-default.png'; - } catch (e) { - console.warn('[RPG Companion] Could not load user avatar, using default', e); - userPortrait = 'img/user-default.png'; + // Try to get the thumbnail using our safe helper + const thumbnailUrl = getSafeThumbnailUrl('persona', user_avatar); + if (thumbnailUrl) { + userPortrait = thumbnailUrl; } } @@ -2757,7 +2798,7 @@ function renderUserStats() {
- ${userName} + ${userName}
${stats.inventory || 'None'} @@ -3251,12 +3292,17 @@ function renderThoughts() { // If no characters parsed, show a placeholder editable card if (presentCharacters.length === 0) { // Get default character portrait (try to use the current character if in 1-on-1 chat) - let defaultPortrait = 'img/user-default.png'; + // Use a transparent placeholder as fallback to avoid 400 errors + const transparentPixel = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="100" height="100"%3E%3Crect width="100" height="100" fill="%23cccccc" opacity="0.3"/%3E%3Ctext x="50%25" y="50%25" text-anchor="middle" dy=".3em" fill="%23666" font-size="40"%3E%3F%3C/text%3E%3C/svg%3E'; + let defaultPortrait = transparentPixel; let defaultName = 'Character'; if (this_chid !== undefined && characters[this_chid]) { if (characters[this_chid].avatar && characters[this_chid].avatar !== 'none') { - defaultPortrait = getThumbnailUrl('avatar', characters[this_chid].avatar); + const thumbnailUrl = getSafeThumbnailUrl('avatar', characters[this_chid].avatar); + if (thumbnailUrl) { + defaultPortrait = thumbnailUrl; + } } defaultName = characters[this_chid].name || 'Character'; } @@ -3265,7 +3311,7 @@ function renderThoughts() { html += `
- ${defaultName} + ${defaultName}
⚖️
@@ -3282,7 +3328,9 @@ function renderThoughts() { html += '
'; for (const char of presentCharacters) { // Find character portrait - let characterPortrait = 'img/user-default.png'; + // Use a transparent placeholder as fallback to avoid 400 errors + const transparentPixel = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="100" height="100"%3E%3Crect width="100" height="100" fill="%23cccccc" opacity="0.3"/%3E%3Ctext x="50%25" y="50%25" text-anchor="middle" dy=".3em" fill="%23666" font-size="40"%3E%3F%3C/text%3E%3C/svg%3E'; + let characterPortrait = transparentPixel; // console.log('[RPG Companion] Looking for avatar for:', char.name); @@ -3294,25 +3342,34 @@ function renderThoughts() { ); if (matchingMember && matchingMember.avatar && matchingMember.avatar !== 'none') { - characterPortrait = getThumbnailUrl('avatar', matchingMember.avatar); + const thumbnailUrl = getSafeThumbnailUrl('avatar', matchingMember.avatar); + if (thumbnailUrl) { + characterPortrait = thumbnailUrl; + } } } // For regular chats or if not found in group, search all characters - if (characterPortrait === 'img/user-default.png' && characters && characters.length > 0) { + if (characterPortrait === transparentPixel && characters && characters.length > 0) { const matchingCharacter = characters.find(c => c && c.name && c.name.toLowerCase() === char.name.toLowerCase() ); if (matchingCharacter && matchingCharacter.avatar && matchingCharacter.avatar !== 'none') { - characterPortrait = getThumbnailUrl('avatar', matchingCharacter.avatar); + const thumbnailUrl = getSafeThumbnailUrl('avatar', matchingCharacter.avatar); + if (thumbnailUrl) { + characterPortrait = thumbnailUrl; + } } } // If this is the current character in a 1-on-1 chat, use their portrait if (this_chid !== undefined && characters[this_chid] && characters[this_chid].name && characters[this_chid].name.toLowerCase() === char.name.toLowerCase()) { - characterPortrait = getThumbnailUrl('avatar', characters[this_chid].avatar); + const thumbnailUrl = getSafeThumbnailUrl('avatar', characters[this_chid].avatar); + if (thumbnailUrl) { + characterPortrait = thumbnailUrl; + } } // Get relationship emoji @@ -3321,7 +3378,7 @@ function renderThoughts() { html += `
- ${char.name} + ${char.name}
${relationshipEmoji}
@@ -4594,23 +4651,33 @@ async function ensureHtmlCleaningRegex() { */ function updatePersonaAvatar() { const portraitImg = document.querySelector('.rpg-user-portrait'); - if (!portraitImg) return; + if (!portraitImg) { + console.log('[RPG Companion] Portrait image element not found in DOM'); + return; + } // Get current user_avatar from context instead of using imported value const context = getContext(); const currentUserAvatar = context.user_avatar || user_avatar; - let userPortrait = 'img/user-default.png'; - if (currentUserAvatar) { - try { - userPortrait = getThumbnailUrl('persona', currentUserAvatar) || 'img/user-default.png'; - } catch (e) { - console.warn('[RPG Companion] Could not load user avatar, using default', e); - userPortrait = 'img/user-default.png'; - } - } + console.log('[RPG Companion] Attempting to update persona avatar:', currentUserAvatar); - portraitImg.src = userPortrait; + // Try to get a valid thumbnail URL using our safe helper + if (currentUserAvatar) { + const thumbnailUrl = getSafeThumbnailUrl('persona', currentUserAvatar); + + if (thumbnailUrl) { + // Only update the src if we got a valid URL + portraitImg.src = thumbnailUrl; + console.log('[RPG Companion] Persona avatar updated successfully'); + } else { + // Don't update the src if we couldn't get a valid URL + // This prevents 400 errors and keeps the existing image + console.warn('[RPG Companion] Could not get valid thumbnail URL for persona avatar, keeping existing image'); + } + } else { + console.log('[RPG Companion] No user avatar configured, keeping existing image'); + } } /** From 1db709693d77e2d3b4dcd09d192783cee58e0367 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Fri, 17 Oct 2025 03:15:23 +1100 Subject: [PATCH 2/2] fix: use base64-encoded SVG for avatar fallback to prevent HTML parsing errors The previous URL-encoded SVG had unencoded quotes that broke HTML attribute parsing. The browser would misinterpret xmlns="http://www.w3.org/2000/svg" as separate HTML attributes, causing broken image rendering. Changes: - Add FALLBACK_AVATAR_DATA_URI constant with base64-encoded SVG - Replace all instances of broken inline transparentPixel variable (3 locations) - Update comparison check to use the new constant The base64 encoding ensures the data URI is safely embedded in HTML src attributes without any quote-escaping issues. --- index.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 17c9e62..bb483f8 100644 --- a/index.js +++ b/index.js @@ -87,6 +87,10 @@ let isPlotProgression = false; // Temporary storage for pending dice roll (not saved until user clicks "Save Roll") let pendingDiceRoll = null; +// Fallback avatar image (base64-encoded SVG with "?" icon) +// Using base64 to avoid quote-encoding issues in HTML attributes +const FALLBACK_AVATAR_DATA_URI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2NjY2NjYyIgb3BhY2l0eT0iMC4zIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjNjY2IiBmb250LXNpemU9IjQwIj4/PC90ZXh0Pjwvc3ZnPg=='; + // UI Elements let $panelContainer = null; let $userStatsContainer = null; @@ -2779,9 +2783,8 @@ function renderUserStats() { } // Get user portrait - handle both default-user and custom persona folders - // Use a transparent placeholder as fallback to avoid 400 errors - const transparentPixel = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="100" height="100"%3E%3Crect width="100" height="100" fill="%23cccccc" opacity="0.3"/%3E%3Ctext x="50%25" y="50%25" text-anchor="middle" dy=".3em" fill="%23666" font-size="40"%3E%3F%3C/text%3E%3C/svg%3E'; - let userPortrait = transparentPixel; + // Use a base64-encoded SVG placeholder as fallback to avoid 400 errors + let userPortrait = FALLBACK_AVATAR_DATA_URI; if (user_avatar) { // Try to get the thumbnail using our safe helper @@ -3292,9 +3295,8 @@ function renderThoughts() { // If no characters parsed, show a placeholder editable card if (presentCharacters.length === 0) { // Get default character portrait (try to use the current character if in 1-on-1 chat) - // Use a transparent placeholder as fallback to avoid 400 errors - const transparentPixel = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="100" height="100"%3E%3Crect width="100" height="100" fill="%23cccccc" opacity="0.3"/%3E%3Ctext x="50%25" y="50%25" text-anchor="middle" dy=".3em" fill="%23666" font-size="40"%3E%3F%3C/text%3E%3C/svg%3E'; - let defaultPortrait = transparentPixel; + // Use a base64-encoded SVG placeholder as fallback to avoid 400 errors + let defaultPortrait = FALLBACK_AVATAR_DATA_URI; let defaultName = 'Character'; if (this_chid !== undefined && characters[this_chid]) { @@ -3328,9 +3330,8 @@ function renderThoughts() { html += '
'; for (const char of presentCharacters) { // Find character portrait - // Use a transparent placeholder as fallback to avoid 400 errors - const transparentPixel = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="100" height="100"%3E%3Crect width="100" height="100" fill="%23cccccc" opacity="0.3"/%3E%3Ctext x="50%25" y="50%25" text-anchor="middle" dy=".3em" fill="%23666" font-size="40"%3E%3F%3C/text%3E%3C/svg%3E'; - let characterPortrait = transparentPixel; + // Use a base64-encoded SVG placeholder as fallback to avoid 400 errors + let characterPortrait = FALLBACK_AVATAR_DATA_URI; // console.log('[RPG Companion] Looking for avatar for:', char.name); @@ -3350,7 +3351,7 @@ function renderThoughts() { } // For regular chats or if not found in group, search all characters - if (characterPortrait === transparentPixel && characters && characters.length > 0) { + if (characterPortrait === FALLBACK_AVATAR_DATA_URI && characters && characters.length > 0) { const matchingCharacter = characters.find(c => c && c.name && c.name.toLowerCase() === char.name.toLowerCase() );