Fix: Support multiple character variants in Present Characters panel

Fixed issues when AI generates multiple character variants (e.g.,
storyteller mode with 'Dottore (Prime)', 'Dottore (Beta)', etc.):

1. Escape quotes in character names to prevent HTML attribute breakage
   - Added escapeHtmlAttr() helper function
   - Prevents names like 'Marianna "Mari"' from breaking HTML

2. Restore avatar lookup for character variants
   - namesMatch() now strips parentheses and quotes from both sides
   - Allows 'Dottore (Prime)' to find 'Dottore' character card avatar
   - Each variant still gets its own card with separate attributes

3. Multiple characters now display correctly in panel
   - Each variant creates its own character object
   - Attributes (Details, Relationship, Stats, Thoughts) don't mix
   - All characters appear in the panel, not just the last one
This commit is contained in:
Spicy_Marinara
2025-11-13 16:18:35 +01:00
parent 172c8d6ab8
commit d658e337f6
2 changed files with 35 additions and 19 deletions
+1 -1
View File
@@ -256,7 +256,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
// Relationship line (only if relationships are enabled)
if (relationshipPlaceholders) {
instructions += `Relationship: [${relationshipPlaceholders}]\n`;
instructions += `Relationship: [(choose one: ${relationshipPlaceholders})]\n`;
}
// Stats line (if enabled)
+34 -18
View File
@@ -27,6 +27,14 @@ function debugLog(message, data = null) {
}
}
/**
* Escapes HTML attribute values to prevent quotes from breaking HTML
*/
function escapeHtmlAttr(str) {
if (!str) return '';
return String(str).replace(/"/g, '"').replace(/'/g, ''');
}
/**
* Interpolates color based on percentage value between low and high colors
* @param {number} percentage - Value from 0-100
@@ -78,10 +86,12 @@ function namesMatch(cardName, aiName) {
// 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();
// 2. Strip parentheses and quotes from both names and match
// This allows "Dottore (Prime)" to match "Dottore" card for avatar lookup
// and "Marianna "Mari"" to match "Marianna" or "Mari" cards
const stripParensAndQuotes = (s) => s.replace(/\s*\([^)]*\)/g, '').replace(/["']/g, '').trim();
const cardCore = stripParensAndQuotes(cardName).toLowerCase();
const aiCore = stripParensAndQuotes(aiName).toLowerCase();
if (cardCore === aiCore) return true;
// 3. Check if card name appears as complete word in AI name
@@ -249,17 +259,19 @@ export function renderThoughts() {
defaultName = characters[this_chid].name || 'Character';
}
const escapedDefaultName = escapeHtmlAttr(defaultName);
html += '<div class="rpg-thoughts-content">';
html += `
<div class="rpg-character-card" data-character-name="${defaultName}">
<div class="rpg-character-card" data-character-name="${escapedDefaultName}">
<div class="rpg-character-avatar">
<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>
<img src="${defaultPortrait}" alt="${escapedDefaultName}" onerror="this.style.opacity='0.5';this.onerror=null;" />
<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${escapedDefaultName}" data-field="relationship" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">⚖️</div>
</div>
<div class="rpg-character-info">
<div class="rpg-character-header">
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="emoji" title="Click to edit emoji">😊</span>
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="name" title="Click to edit name">${defaultName}</span>
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${escapedDefaultName}" data-field="emoji" title="Click to edit emoji">😊</span>
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${escapedDefaultName}" data-field="name" title="Click to edit name">${defaultName}</span>
</div>
`;
@@ -267,7 +279,7 @@ export function renderThoughts() {
for (const field of enabledFields) {
const fieldId = field.name.toLowerCase().replace(/\s+/g, '-');
html += `
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable" contenteditable="true" data-character="${defaultName}" data-field="${field.name}" title="Click to edit ${field.name}"></div>
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable" contenteditable="true" data-character="${escapedDefaultName}" data-field="${escapeHtmlAttr(field.name)}" title="Click to edit ${field.name}"></div>
`;
}
@@ -361,17 +373,20 @@ export function renderThoughts() {
debugLog(`[RPG Thoughts] Building HTML card for ${char.name}...`);
// Escape character name for use in HTML attributes
const escapedName = escapeHtmlAttr(char.name);
html += `
<div class="rpg-character-card" data-character-name="${char.name}">
<div class="rpg-character-card" data-character-name="${escapedName}">
<div class="rpg-character-avatar">
<img src="${characterPortrait}" alt="${char.name}" onerror="this.style.opacity='0.5';this.onerror=null;" />
${hasRelationshipEnabled ? `<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${relationshipFieldName}" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipBadge}</div>` : ''}
<img src="${characterPortrait}" alt="${escapedName}" onerror="this.style.opacity='0.5';this.onerror=null;" />
${hasRelationshipEnabled ? `<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${escapedName}" data-field="${relationshipFieldName}" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipBadge}</div>` : ''}
</div>
<div class="rpg-character-content">
<div class="rpg-character-info">
<div class="rpg-character-header">
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${char.name}" data-field="emoji" title="Click to edit emoji">${char.emoji}</span>
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="name" title="Click to edit name">${char.name}</span>
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${escapedName}" data-field="emoji" title="Click to edit emoji">${char.emoji}</span>
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${escapedName}" data-field="name" title="Click to edit name">${char.name}</span>
</div>
`;
@@ -380,7 +395,7 @@ export function renderThoughts() {
const fieldValue = char[field.name] || '';
const fieldId = field.name.toLowerCase().replace(/\s+/g, '-');
html += `
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="Click to edit ${field.name}">${fieldValue}</div>
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable" contenteditable="true" data-character="${escapedName}" data-field="${escapeHtmlAttr(field.name)}" title="Click to edit ${field.name}">${fieldValue}</div>
`;
}
@@ -396,7 +411,7 @@ export function renderThoughts() {
const statColor = getStatColor(statValue, extensionSettings.statBarColorLow, extensionSettings.statBarColorHigh);
html += `
<div class="rpg-character-stat">
<span class="rpg-stat-name">${stat.name}: </span><span class="rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${stat.name}" style="color: ${statColor}" title="Click to edit ${stat.name}">${statValue}%</span>
<span class="rpg-stat-name">${stat.name}: </span><span class="rpg-editable" contenteditable="true" data-character="${escapedName}" data-field="${escapeHtmlAttr(stat.name)}" style="color: ${statColor}" title="Click to edit ${stat.name}">${statValue}%</span>
</div>
`;
}
@@ -817,12 +832,13 @@ export function createThoughtPanel($message, thoughtsArray) {
// Build thought bubbles HTML
let thoughtsHtml = '';
thoughtsArray.forEach((thought, index) => {
const escapedThoughtName = escapeHtmlAttr(thought.name);
thoughtsHtml += `
<div class="rpg-thought-item">
<div class="rpg-thought-emoji-box">
${thought.emoji}
</div>
<div class="rpg-thought-content rpg-editable" contenteditable="true" data-character="${thought.name}" data-field="thoughts" title="Click to edit thoughts">
<div class="rpg-thought-content rpg-editable" contenteditable="true" data-character="${escapedThoughtName}" data-field="thoughts" title="Click to edit thoughts">
${thought.thought}
</div>
</div>