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()
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-10-17 03:05:37 +11:00
parent 74e76ff224
commit 66712382d5
+95 -28
View File
@@ -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() {
<div class="rpg-stats-content">
<div class="rpg-stats-left">
<div style="display: flex; gap: clamp(4px, 0.8vh, 8px); align-items: center; flex-shrink: 0;">
<img src="${userPortrait}" alt="${userName}" class="rpg-user-portrait" onerror="this.src='img/user-default.png'" />
<img src="${userPortrait}" alt="${userName}" class="rpg-user-portrait" onerror="this.style.opacity='0.5';this.onerror=null;" />
<div class="rpg-inventory-box">
<div class="rpg-inventory-items rpg-editable" contenteditable="true" data-field="inventory" title="Click to edit">
${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 += `
<div class="rpg-character-card" data-character-name="${defaultName}">
<div class="rpg-character-avatar">
<img src="${defaultPortrait}" alt="${defaultName}" onerror="this.src='img/user-default.png'" />
<img src="${defaultPortrait}" alt="${defaultName}" onerror="this.style.opacity='0.5';this.onerror=null;" />
<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="relationship" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">⚖️</div>
</div>
<div class="rpg-character-info">
@@ -3282,7 +3328,9 @@ function renderThoughts() {
html += '<div class="rpg-thoughts-content">';
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 += `
<div class="rpg-character-card" data-character-name="${char.name}">
<div class="rpg-character-avatar">
<img src="${characterPortrait}" alt="${char.name}" onerror="this.src='img/user-default.png'" />
<img src="${characterPortrait}" alt="${char.name}" onerror="this.style.opacity='0.5';this.onerror=null;" />
<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${char.name}" data-field="relationship" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipEmoji}</div>
</div>
<div class="rpg-character-info">
@@ -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');
}
}
/**