From 9b6d0d41cddf0373be9a9b689ac45ad1301c1e50 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Mon, 20 Oct 2025 09:06:31 +1100 Subject: [PATCH] fix(avatars): add fuzzy name matching for character portraits - Added namesMatch() helper function with three matching strategies: 1. Exact match (fast path) 2. Strip parentheses match (handles 'Sabrina' vs 'Sabrina (Avatar)') 3. Word boundary match (handles 'Sabrina' vs 'Princess Sabrina') - Replaced exact string comparison with fuzzy matching in 3 places: - Group member lookup - All characters search - Current character 1-on-1 chat - Fixes issue where character portraits showed placeholder when AI added parenthetical or title additions to character names - Prevents false positives (e.g., 'Sabrina' won't match 'Sabrina's Mother') --- src/systems/rendering/thoughts.js | 34 ++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index 1ef272c..25d90bc 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -16,6 +16,34 @@ import { import { saveChatData } from '../../core/persistence.js'; import { getSafeThumbnailUrl } from '../../utils/avatars.js'; +/** + * Fuzzy name matching that handles: + * - Exact matches: "Sabrina" === "Sabrina" + * - Parenthetical additions: "Sabrina" matches "Sabrina (Margrokha's Avatar)" + * - Title additions: "Sabrina" matches "Princess Sabrina" + * - Word boundaries: "Sabrina" won't match "Sabrina's Mother" + * + * @param {string} cardName - Name from the character card + * @param {string} aiName - Name generated by the AI + * @returns {boolean} True if names match + */ +function namesMatch(cardName, aiName) { + if (!cardName || !aiName) return false; + + // 1. Exact match (fast path) + if (cardName.toLowerCase() === aiName.toLowerCase()) return true; + + // 2. Strip parentheses and match + const stripParens = (s) => s.replace(/\s*\([^)]*\)/g, '').trim(); + const cardCore = stripParens(cardName).toLowerCase(); + const aiCore = stripParens(aiName).toLowerCase(); + if (cardCore === aiCore) return true; + + // 3. Check if card name appears as complete word in AI name + const wordBoundary = new RegExp(`\\b${cardCore}\\b`); + return wordBoundary.test(aiCore); +} + /** * Renders character thoughts (Present Characters) panel. * Displays character cards with avatars, relationship badges, and traits. @@ -139,7 +167,7 @@ export function renderThoughts() { if (selected_group) { const groupMembers = getGroupMembers(selected_group); const matchingMember = groupMembers.find(member => - member && member.name && member.name.toLowerCase() === char.name.toLowerCase() + member && member.name && namesMatch(member.name, char.name) ); if (matchingMember && matchingMember.avatar && matchingMember.avatar !== 'none') { @@ -153,7 +181,7 @@ export function renderThoughts() { // For regular chats or if not found in group, search all characters if (characterPortrait === FALLBACK_AVATAR_DATA_URI && characters && characters.length > 0) { const matchingCharacter = characters.find(c => - c && c.name && c.name.toLowerCase() === char.name.toLowerCase() + c && c.name && namesMatch(c.name, char.name) ); if (matchingCharacter && matchingCharacter.avatar && matchingCharacter.avatar !== 'none') { @@ -166,7 +194,7 @@ export function renderThoughts() { // 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()) { + characters[this_chid].name && namesMatch(characters[this_chid].name, char.name)) { const thumbnailUrl = getSafeThumbnailUrl('avatar', characters[this_chid].avatar); if (thumbnailUrl) { characterPortrait = thumbnailUrl;