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:
@@ -256,7 +256,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
|||||||
|
|
||||||
// Relationship line (only if relationships are enabled)
|
// Relationship line (only if relationships are enabled)
|
||||||
if (relationshipPlaceholders) {
|
if (relationshipPlaceholders) {
|
||||||
instructions += `Relationship: [${relationshipPlaceholders}]\n`;
|
instructions += `Relationship: [(choose one: ${relationshipPlaceholders})]\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats line (if enabled)
|
// Stats line (if enabled)
|
||||||
|
|||||||
@@ -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
|
* Interpolates color based on percentage value between low and high colors
|
||||||
* @param {number} percentage - Value from 0-100
|
* @param {number} percentage - Value from 0-100
|
||||||
@@ -78,10 +86,12 @@ function namesMatch(cardName, aiName) {
|
|||||||
// 1. Exact match (fast path)
|
// 1. Exact match (fast path)
|
||||||
if (cardName.toLowerCase() === aiName.toLowerCase()) return true;
|
if (cardName.toLowerCase() === aiName.toLowerCase()) return true;
|
||||||
|
|
||||||
// 2. Strip parentheses and match
|
// 2. Strip parentheses and quotes from both names and match
|
||||||
const stripParens = (s) => s.replace(/\s*\([^)]*\)/g, '').trim();
|
// This allows "Dottore (Prime)" to match "Dottore" card for avatar lookup
|
||||||
const cardCore = stripParens(cardName).toLowerCase();
|
// and "Marianna "Mari"" to match "Marianna" or "Mari" cards
|
||||||
const aiCore = stripParens(aiName).toLowerCase();
|
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;
|
if (cardCore === aiCore) return true;
|
||||||
|
|
||||||
// 3. Check if card name appears as complete word in AI name
|
// 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';
|
defaultName = characters[this_chid].name || 'Character';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const escapedDefaultName = escapeHtmlAttr(defaultName);
|
||||||
|
|
||||||
html += '<div class="rpg-thoughts-content">';
|
html += '<div class="rpg-thoughts-content">';
|
||||||
html += `
|
html += `
|
||||||
<div class="rpg-character-card" data-character-name="${defaultName}">
|
<div class="rpg-character-card" data-character-name="${escapedDefaultName}">
|
||||||
<div class="rpg-character-avatar">
|
<div class="rpg-character-avatar">
|
||||||
<img src="${defaultPortrait}" alt="${defaultName}" onerror="this.style.opacity='0.5';this.onerror=null;" />
|
<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="${defaultName}" data-field="relationship" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">⚖️</div>
|
<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${escapedDefaultName}" data-field="relationship" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">⚖️</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-character-info">
|
<div class="rpg-character-info">
|
||||||
<div class="rpg-character-header">
|
<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-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="${defaultName}" data-field="name" title="Click to edit name">${defaultName}</span>
|
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${escapedDefaultName}" data-field="name" title="Click to edit name">${defaultName}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -267,7 +279,7 @@ export function renderThoughts() {
|
|||||||
for (const field of enabledFields) {
|
for (const field of enabledFields) {
|
||||||
const fieldId = field.name.toLowerCase().replace(/\s+/g, '-');
|
const fieldId = field.name.toLowerCase().replace(/\s+/g, '-');
|
||||||
html += `
|
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}...`);
|
debugLog(`[RPG Thoughts] Building HTML card for ${char.name}...`);
|
||||||
|
|
||||||
|
// Escape character name for use in HTML attributes
|
||||||
|
const escapedName = escapeHtmlAttr(char.name);
|
||||||
|
|
||||||
html += `
|
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">
|
<div class="rpg-character-avatar">
|
||||||
<img src="${characterPortrait}" alt="${char.name}" onerror="this.style.opacity='0.5';this.onerror=null;" />
|
<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="${char.name}" data-field="${relationshipFieldName}" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipBadge}</div>` : ''}
|
${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>
|
||||||
<div class="rpg-character-content">
|
<div class="rpg-character-content">
|
||||||
<div class="rpg-character-info">
|
<div class="rpg-character-info">
|
||||||
<div class="rpg-character-header">
|
<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-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="${char.name}" data-field="name" title="Click to edit name">${char.name}</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>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -380,7 +395,7 @@ export function renderThoughts() {
|
|||||||
const fieldValue = char[field.name] || '';
|
const fieldValue = char[field.name] || '';
|
||||||
const fieldId = field.name.toLowerCase().replace(/\s+/g, '-');
|
const fieldId = field.name.toLowerCase().replace(/\s+/g, '-');
|
||||||
html += `
|
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);
|
const statColor = getStatColor(statValue, extensionSettings.statBarColorLow, extensionSettings.statBarColorHigh);
|
||||||
html += `
|
html += `
|
||||||
<div class="rpg-character-stat">
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -817,12 +832,13 @@ export function createThoughtPanel($message, thoughtsArray) {
|
|||||||
// Build thought bubbles HTML
|
// Build thought bubbles HTML
|
||||||
let thoughtsHtml = '';
|
let thoughtsHtml = '';
|
||||||
thoughtsArray.forEach((thought, index) => {
|
thoughtsArray.forEach((thought, index) => {
|
||||||
|
const escapedThoughtName = escapeHtmlAttr(thought.name);
|
||||||
thoughtsHtml += `
|
thoughtsHtml += `
|
||||||
<div class="rpg-thought-item">
|
<div class="rpg-thought-item">
|
||||||
<div class="rpg-thought-emoji-box">
|
<div class="rpg-thought-emoji-box">
|
||||||
${thought.emoji}
|
${thought.emoji}
|
||||||
</div>
|
</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}
|
${thought.thought}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user