Files
rpg-companion-sillytavern/src/systems/rendering/userStats.js
T
Lucas 'Paperboy' Rose-Winters f09c42ec6e fix(ai-context): sync manual edits to committed tracker data
Fixes critical issue where manual edits (add location, add item, change
stats, etc.) were invisible to AI in next generation, causing edits to be
immediately overwritten.

Root Cause:
- Manual edits updated extensionSettings and lastGeneratedData
- AI prompt builder used committedTrackerData (NOT extensionSettings)
- Manual edits were never synced to committedTrackerData
- Result: AI didn't see manual changes, overwrote them

Solution - Sync to Both Data Stores:

All manual edit points now update BOTH:
1. lastGeneratedData (for display)
2. committedTrackerData (for AI context)

Files Modified:

1. **src/systems/interaction/inventoryActions.js**
   - updateLastGeneratedDataInventory() now sets committedTrackerData.userStats
   - Affects: add/remove items, add/remove locations

2. **src/systems/rendering/userStats.js**
   - All 3 edit handlers now set committedTrackerData.userStats
   - Affects: stat values (health, etc.), mood emoji, conditions
   - Also fixed: now uses buildInventorySummary() for proper v2 format

3. **src/systems/rendering/infoBox.js**
   - updateInfoBoxField() now sets committedTrackerData.infoBox
   - Affects: date, weather, temperature, time, location

4. **src/systems/rendering/thoughts.js**
   - updateCharacterField() now sets committedTrackerData.characterThoughts
   - Affects: character emoji, name, traits, thoughts, relationship

Impact - Manual Edits Now Persist:

Before:
- Add location "Home" → Next generation → Location gone 
- Add item "Sword" → Next generation → Item gone 
- Change health to 25% → AI ignores it 

After:
- Add location "Home" → Next generation → Location persists ✓
- Add item "Sword" → Next generation → Item included ✓
- Change health to 25% → AI acknowledges low health ✓

Works in Both Modes:
- Together mode: AI sees manual edits in injected prompt ✓
- Separate mode: AI sees manual edits in context ✓

User Experience:
- "I edited it, so it should stay" - now works as expected
- AI builds on manual changes instead of overwriting them
- Minimal overhead (just string copies)

Fixes: Manual inventory/stats edits being overwritten by AI generation
2025-10-20 08:05:08 +11:00

240 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* User Stats Rendering Module
* Handles rendering of the user stats panel with progress bars and classic RPG stats
*/
import { getContext } from '../../../../../../extensions.js';
import { user_avatar } from '../../../../../../../script.js';
import {
extensionSettings,
lastGeneratedData,
committedTrackerData,
$userStatsContainer,
FALLBACK_AVATAR_DATA_URI
} from '../../core/state.js';
import {
saveSettings,
saveChatData,
updateMessageSwipeData
} from '../../core/persistence.js';
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
import { buildInventorySummary } from '../generation/promptBuilder.js';
/**
* Renders the user stats panel with health bars, mood, inventory, and classic stats.
* Includes event listeners for editable fields.
*/
export function renderUserStats() {
if (!extensionSettings.showUserStats || !$userStatsContainer) {
return;
}
const stats = extensionSettings.userStats;
const userName = getContext().name1;
// Initialize lastGeneratedData.userStats if it doesn't exist
if (!lastGeneratedData.userStats) {
lastGeneratedData.userStats = `Health: ${stats.health}%\nSatiety: ${stats.satiety}%\nEnergy: ${stats.energy}%\nHygiene: ${stats.hygiene}%\nArousal: ${stats.arousal}%\n${stats.mood}: ${stats.conditions}\nInventory: ${stats.inventory}`;
}
// Get user portrait - handle both default-user and custom persona folders
// Use a base64-encoded SVG placeholder as fallback to avoid 400 errors
let userPortrait = FALLBACK_AVATAR_DATA_URI;
if (user_avatar) {
// Try to get the thumbnail using our safe helper
const thumbnailUrl = getSafeThumbnailUrl('persona', user_avatar);
if (thumbnailUrl) {
userPortrait = thumbnailUrl;
}
}
// Create gradient from low to high color
const gradient = `linear-gradient(to right, ${extensionSettings.statBarColorLow}, ${extensionSettings.statBarColorHigh})`;
const html = `
<div class="rpg-stats-content">
<div class="rpg-stats-left">
<div style="display: flex; gap: clamp(4px, 0.8vh, 8px); align-items: center; justify-content: center; flex-shrink: 0;">
<img src="${userPortrait}" alt="${userName}" class="rpg-user-portrait" onerror="this.style.opacity='0.5';this.onerror=null;" />
</div>
<div class="rpg-stats-grid">
<div class="rpg-stat-row">
<span class="rpg-stat-label">Health:</span>
<div class="rpg-stat-bar" style="background: ${gradient}">
<div class="rpg-stat-fill" style="width: ${100 - stats.health}%"></div>
</div>
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="health" title="Click to edit">${stats.health}%</span>
</div>
<div class="rpg-stat-row">
<span class="rpg-stat-label">Satiety:</span>
<div class="rpg-stat-bar" style="background: ${gradient}">
<div class="rpg-stat-fill" style="width: ${100 - stats.satiety}%"></div>
</div>
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="satiety" title="Click to edit">${stats.satiety}%</span>
</div>
<div class="rpg-stat-row">
<span class="rpg-stat-label">Energy:</span>
<div class="rpg-stat-bar" style="background: ${gradient}">
<div class="rpg-stat-fill" style="width: ${100 - stats.energy}%"></div>
</div>
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="energy" title="Click to edit">${stats.energy}%</span>
</div>
<div class="rpg-stat-row">
<span class="rpg-stat-label">Hygiene:</span>
<div class="rpg-stat-bar" style="background: ${gradient}">
<div class="rpg-stat-fill" style="width: ${100 - stats.hygiene}%"></div>
</div>
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="hygiene" title="Click to edit">${stats.hygiene}%</span>
</div>
<div class="rpg-stat-row">
<span class="rpg-stat-label">Arousal:</span>
<div class="rpg-stat-bar" style="background: ${gradient}">
<div class="rpg-stat-fill" style="width: ${100 - stats.arousal}%"></div>
</div>
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="arousal" title="Click to edit">${stats.arousal}%</span>
</div>
</div>
<div class="rpg-mood">
<div class="rpg-mood-emoji rpg-editable" contenteditable="true" data-field="mood" title="Click to edit emoji">${stats.mood}</div>
<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="conditions" title="Click to edit conditions">${stats.conditions}</div>
</div>
</div>
<div class="rpg-stats-right">
<div class="rpg-classic-stats">
<div class="rpg-classic-stats-grid">
<div class="rpg-classic-stat" data-stat="str">
<span class="rpg-classic-stat-label">STR</span>
<div class="rpg-classic-stat-buttons">
<button class="rpg-classic-stat-btn rpg-stat-decrease" data-stat="str"></button>
<span class="rpg-classic-stat-value">${extensionSettings.classicStats.str}</span>
<button class="rpg-classic-stat-btn rpg-stat-increase" data-stat="str">+</button>
</div>
</div>
<div class="rpg-classic-stat" data-stat="dex">
<span class="rpg-classic-stat-label">DEX</span>
<div class="rpg-classic-stat-buttons">
<button class="rpg-classic-stat-btn rpg-stat-decrease" data-stat="dex"></button>
<span class="rpg-classic-stat-value">${extensionSettings.classicStats.dex}</span>
<button class="rpg-classic-stat-btn rpg-stat-increase" data-stat="dex">+</button>
</div>
</div>
<div class="rpg-classic-stat" data-stat="con">
<span class="rpg-classic-stat-label">CON</span>
<div class="rpg-classic-stat-buttons">
<button class="rpg-classic-stat-btn rpg-stat-decrease" data-stat="con"></button>
<span class="rpg-classic-stat-value">${extensionSettings.classicStats.con}</span>
<button class="rpg-classic-stat-btn rpg-stat-increase" data-stat="con">+</button>
</div>
</div>
<div class="rpg-classic-stat" data-stat="int">
<span class="rpg-classic-stat-label">INT</span>
<div class="rpg-classic-stat-buttons">
<button class="rpg-classic-stat-btn rpg-stat-decrease" data-stat="int"></button>
<span class="rpg-classic-stat-value">${extensionSettings.classicStats.int}</span>
<button class="rpg-classic-stat-btn rpg-stat-increase" data-stat="int">+</button>
</div>
</div>
<div class="rpg-classic-stat" data-stat="wis">
<span class="rpg-classic-stat-label">WIS</span>
<div class="rpg-classic-stat-buttons">
<button class="rpg-classic-stat-btn rpg-stat-decrease" data-stat="wis"></button>
<span class="rpg-classic-stat-value">${extensionSettings.classicStats.wis}</span>
<button class="rpg-classic-stat-btn rpg-stat-increase" data-stat="wis">+</button>
</div>
</div>
<div class="rpg-classic-stat" data-stat="cha">
<span class="rpg-classic-stat-label">CHA</span>
<div class="rpg-classic-stat-buttons">
<button class="rpg-classic-stat-btn rpg-stat-decrease" data-stat="cha"></button>
<span class="rpg-classic-stat-value">${extensionSettings.classicStats.cha}</span>
<button class="rpg-classic-stat-btn rpg-stat-increase" data-stat="cha">+</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
$userStatsContainer.html(html);
// Add event listeners for editable stat values
$('.rpg-editable-stat').on('blur', function() {
const field = $(this).data('field');
const textValue = $(this).text().replace('%', '').trim();
let value = parseInt(textValue);
// Validate and clamp value between 0 and 100
if (isNaN(value)) {
value = 0;
}
value = Math.max(0, Math.min(100, value));
// Update the setting
extensionSettings.userStats[field] = value;
// Rebuild userStats text with proper inventory format
const stats = extensionSettings.userStats;
const inventorySummary = buildInventorySummary(stats.inventory);
const statsText = `Health: ${stats.health}%\nSatiety: ${stats.satiety}%\nEnergy: ${stats.energy}%\nHygiene: ${stats.hygiene}%\nArousal: ${stats.arousal}%\n${stats.mood}: ${stats.conditions}\n${inventorySummary}`;
// Update BOTH lastGeneratedData AND committedTrackerData
// This makes manual edits immediately visible to AI
lastGeneratedData.userStats = statsText;
committedTrackerData.userStats = statsText;
saveSettings();
saveChatData();
updateMessageSwipeData();
// Re-render to update the bar
renderUserStats();
});
// Add event listeners for mood/conditions editing
$('.rpg-mood-emoji.rpg-editable').on('blur', function() {
const value = $(this).text().trim();
extensionSettings.userStats.mood = value || '😐';
// Rebuild userStats text with proper inventory format
const stats = extensionSettings.userStats;
const inventorySummary = buildInventorySummary(stats.inventory);
const statsText = `Health: ${stats.health}%\nSatiety: ${stats.satiety}%\nEnergy: ${stats.energy}%\nHygiene: ${stats.hygiene}%\nArousal: ${stats.arousal}%\n${stats.mood}: ${stats.conditions}\n${inventorySummary}`;
// Update BOTH lastGeneratedData AND committedTrackerData
// This makes manual edits immediately visible to AI
lastGeneratedData.userStats = statsText;
committedTrackerData.userStats = statsText;
saveSettings();
saveChatData();
updateMessageSwipeData();
});
$('.rpg-mood-conditions.rpg-editable').on('blur', function() {
const value = $(this).text().trim();
extensionSettings.userStats.conditions = value || 'None';
// Rebuild userStats text with proper inventory format
const stats = extensionSettings.userStats;
const inventorySummary = buildInventorySummary(stats.inventory);
const statsText = `Health: ${stats.health}%\nSatiety: ${stats.satiety}%\nEnergy: ${stats.energy}%\nHygiene: ${stats.hygiene}%\nArousal: ${stats.arousal}%\n${stats.mood}: ${stats.conditions}\n${inventorySummary}`;
// Update BOTH lastGeneratedData AND committedTrackerData
// This makes manual edits immediately visible to AI
lastGeneratedData.userStats = statsText;
committedTrackerData.userStats = statsText;
saveSettings();
saveChatData();
updateMessageSwipeData();
});
}