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')
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-10-20 09:06:31 +11:00
parent 5ec12cbf10
commit 9b6d0d41cd
+31 -3
View File
@@ -16,6 +16,34 @@ import {
import { saveChatData } from '../../core/persistence.js'; import { saveChatData } from '../../core/persistence.js';
import { getSafeThumbnailUrl } from '../../utils/avatars.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. * Renders character thoughts (Present Characters) panel.
* Displays character cards with avatars, relationship badges, and traits. * Displays character cards with avatars, relationship badges, and traits.
@@ -139,7 +167,7 @@ export function renderThoughts() {
if (selected_group) { if (selected_group) {
const groupMembers = getGroupMembers(selected_group); const groupMembers = getGroupMembers(selected_group);
const matchingMember = groupMembers.find(member => 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') { 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 // For regular chats or if not found in group, search all characters
if (characterPortrait === FALLBACK_AVATAR_DATA_URI && characters && characters.length > 0) { if (characterPortrait === FALLBACK_AVATAR_DATA_URI && characters && characters.length > 0) {
const matchingCharacter = characters.find(c => 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') { 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 is the current character in a 1-on-1 chat, use their portrait
if (this_chid !== undefined && characters[this_chid] && 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); const thumbnailUrl = getSafeThumbnailUrl('avatar', characters[this_chid].avatar);
if (thumbnailUrl) { if (thumbnailUrl) {
characterPortrait = thumbnailUrl; characterPortrait = thumbnailUrl;