v3.7.2: Fix status field key generation for parenthetical names & scroll preservation

- Fix: Status fields with parenthetical descriptions (e.g., 'Conditions (up to 5 traits)') now use the base name for the JSON key ('conditions' instead of 'conditions_up_to_5_traits')
- Fix: Status field value templates no longer repeat the field name with numbered suffixes
- Fix: Editing fields in Present Characters no longer scrolls the panel to the top
- Updated jsonPromptHelpers.js, parser.js, and userStats.js to use new toFieldKey() helper
- Added scroll position preservation to renderThoughts() when re-rendering after field edits
This commit is contained in:
Spicy_Marinara
2026-02-13 18:34:44 +01:00
parent 5498c64f5d
commit 105e20e97a
7 changed files with 76 additions and 19 deletions
+2 -5
View File
@@ -7,12 +7,9 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
## 🆕 What's New ## 🆕 What's New
### v3.7.1 ### v3.7.2
- Improved instructions for the model to generate dynamic weather. - Minor bug fixes
- Small fixes and updates to descriptions.
- Fixed a scroll/viewport bug that moved everything up after you edited fields.
- Changed the display of avatars for present characters.
**Special thanks to all the other contributors for this project:** **Special thanks to all the other contributors for this project:**
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Jakstein! Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Jakstein!
+1 -1
View File
@@ -6,6 +6,6 @@
"js": "index.js", "js": "index.js",
"css": "style.css", "css": "style.css",
"author": "Marinara", "author": "Marinara",
"version": "3.7.1", "version": "3.7.2",
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern" "homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
} }
+1 -1
View File
@@ -49,7 +49,7 @@
</div> </div>
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;"> <div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
v3.7.1 v3.7.2
</div> </div>
</div> </div>
</div> </div>
+15 -2
View File
@@ -21,6 +21,19 @@ function toSnakeCase(name) {
.replace(/^_+|_+$/g, ''); .replace(/^_+|_+$/g, '');
} }
/**
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
* Parenthetical content is treated as a description/hint, not part of the key.
* Example: "Conditions (up to 5 traits)" -> "conditions"
* Example: "Status Effects" -> "status_effects"
* @param {string} name - Field name, possibly with parenthetical description
* @returns {string} snake_case key from the base name only
*/
function toFieldKey(name) {
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
return toSnakeCase(baseName);
}
/** /**
* Builds User Stats JSON format instruction * Builds User Stats JSON format instruction
* @returns {string} JSON format instruction for user stats * @returns {string} JSON format instruction for user stats
@@ -60,12 +73,12 @@ export function buildUserStatsJSONInstruction() {
if (customFields.length > 0) { if (customFields.length > 0) {
for (let i = 0; i < customFields.length; i++) { for (let i = 0; i < customFields.length; i++) {
const fieldName = customFields[i].toLowerCase(); const fieldName = customFields[i].toLowerCase();
const fieldKey = toSnakeCase(fieldName); const fieldKey = toFieldKey(fieldName);
const comma = (i === customFields.length - 1 && !userStatsConfig.statusSection.showMoodEmoji) ? '' : (userStatsConfig.statusSection.showMoodEmoji || i < customFields.length - 1 ? ',\n' : '\n'); const comma = (i === customFields.length - 1 && !userStatsConfig.statusSection.showMoodEmoji) ? '' : (userStatsConfig.statusSection.showMoodEmoji || i < customFields.length - 1 ? ',\n' : '\n');
if (i === 0 && userStatsConfig.statusSection.showMoodEmoji) { if (i === 0 && userStatsConfig.statusSection.showMoodEmoji) {
instruction += ',\n'; instruction += ',\n';
} }
instruction += ` "${fieldKey}": "[${fieldName}1, ${fieldName}2]"${comma}`; instruction += ` "${fieldKey}": "[${fieldName}]"${comma}`;
} }
} }
if (!userStatsConfig.statusSection.showMoodEmoji && customFields.length > 0) { if (!userStatsConfig.statusSection.showMoodEmoji && customFields.length > 0) {
+20 -4
View File
@@ -9,6 +9,20 @@ import { saveSettings } from '../../core/persistence.js';
import { extractInventory } from './inventoryParser.js'; import { extractInventory } from './inventoryParser.js';
import { repairJSON } from '../../utils/jsonRepair.js'; import { repairJSON } from '../../utils/jsonRepair.js';
/**
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
* Example: "Conditions (up to 5 traits)" -> "conditions"
* @param {string} name - Field name, possibly with parenthetical description
* @returns {string} snake_case key from the base name only
*/
function toFieldKey(name) {
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
return baseName
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '');
}
/** /**
* Helper to separate emoji from text in a string * Helper to separate emoji from text in a string
* Handles cases where there's no comma or space after emoji * Handles cases where there's no comma or space after emoji
@@ -559,10 +573,12 @@ export function parseUserStats(statsText) {
const trackerConfig = extensionSettings.trackerConfig; const trackerConfig = extensionSettings.trackerConfig;
const customFields = trackerConfig?.userStats?.statusSection?.customFields || []; const customFields = trackerConfig?.userStats?.statusSection?.customFields || [];
for (const fieldName of customFields) { for (const fieldName of customFields) {
const fieldKey = fieldName.toLowerCase(); const fieldKey = toFieldKey(fieldName);
if (statsData.status[fieldKey]) { // Try the base key first (e.g., "conditions"), then fall back to full lowercase name
extensionSettings.userStats[fieldKey] = statsData.status[fieldKey]; const value = statsData.status[fieldKey] || statsData.status[fieldName.toLowerCase()];
// console.log(`[RPG Parser] ✓ Set ${fieldKey} =`, statsData.status[fieldKey]); if (value) {
extensionSettings.userStats[fieldKey] = value;
// console.log(`[RPG Parser] ✓ Set ${fieldKey} =`, value);
} }
} }
} }
+20 -3
View File
@@ -153,11 +153,20 @@ function namesMatch(cardName, aiName) {
* Displays character cards with avatars, relationship badges, and traits. * Displays character cards with avatars, relationship badges, and traits.
* Includes event listeners for editable character fields. * Includes event listeners for editable character fields.
*/ */
export function renderThoughts() { export function renderThoughts({ preserveScroll = false } = {}) {
if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) { if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) {
return; return;
} }
// Save scroll position before re-render if requested
let savedContentScroll = 0;
if (preserveScroll) {
const $content = $thoughtsContainer.find('.rpg-thoughts-content');
if ($content.length) {
savedContentScroll = $content[0].scrollTop;
}
}
// Don't render if no data exists (e.g., after cache clear) // Don't render if no data exists (e.g., after cache clear)
const thoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts; const thoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts;
if (!thoughtsData) { if (!thoughtsData) {
@@ -714,6 +723,14 @@ export function renderThoughts() {
setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600); setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600);
} }
// Restore scroll position after re-render
if (preserveScroll) {
const $content = $thoughtsContainer.find('.rpg-thoughts-content');
if ($content.length) {
$content[0].scrollTop = savedContentScroll;
}
}
// Update chat overlay if enabled // Update chat overlay if enabled
if (extensionSettings.showThoughtsInChat) { if (extensionSettings.showThoughtsInChat) {
updateChatThoughts(); updateChatThoughts();
@@ -1147,8 +1164,8 @@ export function updateCharacterField(characterName, field, value) {
// console.log('[RPG Companion] JSON format updated successfully'); // console.log('[RPG Companion] JSON format updated successfully');
// console.log('[RPG Companion] Updated data:', lastGeneratedData.characterThoughts); // console.log('[RPG Companion] Updated data:', lastGeneratedData.characterThoughts);
// Re-render the thoughts panel to show updated value // Re-render the thoughts panel to show updated value (preserve scroll position)
renderThoughts(); renderThoughts({ preserveScroll: true });
// Update chat thought overlays if editing thoughts // Update chat thought overlays if editing thoughts
const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts'; const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts';
+17 -3
View File
@@ -23,6 +23,20 @@ import { isItemLocked, setItemLock } from '../generation/lockManager.js';
import { updateFabWidgets } from '../ui/mobile.js'; import { updateFabWidgets } from '../ui/mobile.js';
import { getStatBarColors } from '../ui/theme.js'; import { getStatBarColors } from '../ui/theme.js';
/**
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
* Example: "Conditions (up to 5 traits)" -> "conditions"
* @param {string} name - Field name, possibly with parenthetical description
* @returns {string} snake_case key from the base name only
*/
function toFieldKey(name) {
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
return baseName
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '');
}
/** /**
* Builds the user stats text string using custom stat names * Builds the user stats text string using custom stat names
* @returns {string} Formatted stats text for tracker * @returns {string} Formatted stats text for tracker
@@ -107,7 +121,7 @@ function updateUserStatsData() {
// Then, add any other numeric stats from extensionSettings that aren't in config // Then, add any other numeric stats from extensionSettings that aren't in config
// (these could be custom stats the AI added or disabled stats) // (these could be custom stats the AI added or disabled stats)
const customFields = config.statusSection?.customFields || []; const customFields = config.statusSection?.customFields || [];
const excludeFields = new Set(['mood', ...customFields.map(f => f.toLowerCase()), 'inventory', 'skills', 'level']); const excludeFields = new Set(['mood', ...customFields.map(f => toFieldKey(f)), 'inventory', 'skills', 'level']);
Object.entries(stats).forEach(([key, value]) => { Object.entries(stats).forEach(([key, value]) => {
if (!processedIds.has(key) && !excludeFields.has(key) && typeof value === 'number') { if (!processedIds.has(key) && !excludeFields.has(key) && typeof value === 'number') {
statsArray.push({ statsArray.push({
@@ -127,7 +141,7 @@ function updateUserStatsData() {
// Add all custom status fields // Add all custom status fields
for (const fieldName of customFields) { for (const fieldName of customFields) {
const fieldKey = fieldName.toLowerCase(); const fieldKey = toFieldKey(fieldName);
jsonData.status[fieldKey] = stats[fieldKey] || 'None'; jsonData.status[fieldKey] = stats[fieldKey] || 'None';
} }
@@ -334,7 +348,7 @@ export function renderUserStats() {
// Render custom status fields // Render custom status fields
if (config.statusSection.customFields && config.statusSection.customFields.length > 0) { if (config.statusSection.customFields && config.statusSection.customFields.length > 0) {
for (const fieldName of config.statusSection.customFields) { for (const fieldName of config.statusSection.customFields) {
const fieldKey = fieldName.toLowerCase(); const fieldKey = toFieldKey(fieldName);
let fieldValue = stats[fieldKey] || 'None'; let fieldValue = stats[fieldKey] || 'None';
// Handle array format (from JSON) // Handle array format (from JSON)
if (Array.isArray(fieldValue)) { if (Array.isArray(fieldValue)) {