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:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user