Compare commits

...

3 Commits

Author SHA1 Message Date
Spicy_Marinara ddc02d9bbc Release v3.3.2: Fix auto-update on chat switch & restore character removal 2026-01-09 10:04:29 +01:00
Spicy Marinara 659b5bb82b Merge pull request #87 from tomt610/fix/quest-removal-sync
Fix: Sync quest changes to committedTrackerData
2026-01-09 09:29:28 +01:00
tomt610 5f72e6f549 Fix: Sync quest changes to committedTrackerData
When manually adding/editing/removing quests via UI, the changes were
only saved to extensionSettings but not to committedTrackerData.userStats.
This caused the AI to see stale quest data on the next external server
update, resulting in removed quests reappearing.

- Add syncQuestsToCommittedData() function to update JSON quest data
- Call sync and saveChatData() on all quest modification actions
- Imports committedTrackerData, lastGeneratedData, saveChatData
2026-01-09 00:23:23 +00:00
8 changed files with 195 additions and 52 deletions
+3 -4
View File
@@ -7,11 +7,10 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
## 🆕 What's New
### v3.3.1
### v3.3.2
- Thought bubble can now be collapsed into an icon.
- Fixed a bug for Past Events being parsed incorrectly.
- Added event emission on when the tracker generation is complete.
- Fixed the auto-generation triggering on switching/starting new chats in separate generation mode.
- Restored the option to remove generated characters from the panel.
**Special thanks to all the other contributors for this project:**
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
+1
View File
@@ -64,6 +64,7 @@ import { renderInfoBox, updateInfoBoxField } from './src/systems/rendering/infoB
import {
renderThoughts,
updateCharacterField,
removeCharacter,
updateChatThoughts,
createThoughtPanel
} from './src/systems/rendering/thoughts.js';
+1 -1
View File
@@ -6,6 +6,6 @@
"js": "index.js",
"css": "style.css",
"author": "Marinara",
"version": "3.3.1",
"version": "3.3.2",
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
}
+1 -1
View File
@@ -48,7 +48,7 @@
</div>
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
v3.3.1
v3.3.2
</div>
</div>
</div>
+10
View File
@@ -308,6 +308,12 @@ export let isGenerating = false;
*/
export let isPlotProgression = false;
/**
* Flag indicating if we're actively expecting a new message from generation
* (as opposed to loading chat history)
*/
export let isAwaitingNewMessage = false;
/**
* Temporary storage for pending dice roll (not saved until user clicks "Save Roll")
*/
@@ -408,6 +414,10 @@ export function setIsPlotProgression(value) {
isPlotProgression = value;
}
export function setIsAwaitingNewMessage(value) {
isAwaitingNewMessage = value;
}
export function setPendingDiceRoll(roll) {
pendingDiceRoll = roll;
}
+12 -1
View File
@@ -13,9 +13,11 @@ import {
committedTrackerData,
lastActionWasSwipe,
isPlotProgression,
isAwaitingNewMessage,
setLastActionWasSwipe,
setIsPlotProgression,
setIsGenerating,
setIsAwaitingNewMessage,
updateLastGeneratedData,
updateCommittedTrackerData,
$musicPlayerContainer
@@ -105,6 +107,10 @@ export function onMessageSent() {
// console.log('[RPG Companion] 🟢 EVENT: onMessageSent (after placeholder check)');
// console.log('[RPG Companion] 🟢 NOTE: lastActionWasSwipe will be reset in onMessageReceived after generation completes');
// Set flag to indicate we're expecting a new message from generation
// This allows auto-update to distinguish between new generations and loading chat history
setIsAwaitingNewMessage(true);
// For separate mode with auto-update disabled, commit displayed tracker
if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) {
if (lastGeneratedData.userStats || lastGeneratedData.infoBox || lastGeneratedData.characterThoughts) {
@@ -250,13 +256,17 @@ export async function onMessageReceived(data) {
}
// Trigger auto-update if enabled (for both separate and external modes)
if (extensionSettings.autoUpdate) {
// Only trigger if this is a newly generated message, not loading chat history
if (extensionSettings.autoUpdate && isAwaitingNewMessage) {
setTimeout(async () => {
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
}, 500);
}
}
// Reset the awaiting flag after processing the message
setIsAwaitingNewMessage(false);
// Reset the swipe flag after generation completes
// This ensures that if the user swiped → auto-reply generated → flag is now cleared
// so the next user message will be treated as a new message (not a swipe)
@@ -340,6 +350,7 @@ export function onMessageSwiped(messageIndex) {
if (!isExistingSwipe) {
// This is a NEW swipe that will trigger generation
setLastActionWasSwipe(true);
setIsAwaitingNewMessage(true);
// console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true');
} else {
// This is navigating to an EXISTING swipe - don't change the flag
+39 -2
View File
@@ -3,10 +3,35 @@
* Handles UI rendering for quests system (main and optional quests)
*/
import { extensionSettings, $questsContainer } from '../../core/state.js';
import { saveSettings } from '../../core/persistence.js';
import { extensionSettings, $questsContainer, committedTrackerData, lastGeneratedData } from '../../core/state.js';
import { saveSettings, saveChatData } from '../../core/persistence.js';
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
/**
* Syncs the current extensionSettings.quests to committedTrackerData.userStats
* This ensures quest changes made via UI are reflected in the data sent to AI
*/
function syncQuestsToCommittedData() {
const currentData = committedTrackerData.userStats || lastGeneratedData.userStats;
if (!currentData) return;
const trimmed = currentData.trim();
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
try {
const jsonData = JSON.parse(currentData);
if (jsonData && typeof jsonData === 'object') {
// Update quests in the JSON data
jsonData.quests = extensionSettings.quests || { main: 'None', optional: [] };
const updatedJSON = JSON.stringify(jsonData, null, 2);
committedTrackerData.userStats = updatedJSON;
lastGeneratedData.userStats = updatedJSON;
}
} catch (e) {
console.warn('[RPG Quests] Failed to sync quests to committed data:', e);
}
}
}
/**
* Helper to generate lock icon HTML if setting is enabled
* @param {string} tracker - Tracker name
@@ -250,7 +275,10 @@ function attachQuestEventHandlers() {
}
extensionSettings.quests.optional.push(questTitle);
}
// Sync quest changes to committedTrackerData so AI sees the addition
syncQuestsToCommittedData();
saveSettings();
saveChatData();
renderQuests();
}
});
@@ -278,7 +306,10 @@ function attachQuestEventHandlers() {
if (questTitle) {
extensionSettings.quests.main = questTitle;
// Sync quest changes to committedTrackerData so AI sees the edit
syncQuestsToCommittedData();
saveSettings();
saveChatData();
renderQuests();
}
});
@@ -293,7 +324,10 @@ function attachQuestEventHandlers() {
} else {
extensionSettings.quests.optional.splice(index, 1);
}
// Sync quest changes to committedTrackerData so AI sees the removal
syncQuestsToCommittedData();
saveSettings();
saveChatData();
renderQuests();
});
@@ -306,7 +340,10 @@ function attachQuestEventHandlers() {
if (newTitle && field === 'optional' && index !== undefined) {
extensionSettings.quests.optional[index] = newTitle;
// Sync quest changes to committedTrackerData so AI sees the edit
syncQuestsToCommittedData();
saveSettings();
saveChatData();
}
});
+128 -43
View File
@@ -391,50 +391,10 @@ export function renderThoughts() {
debugLog('[RPG Thoughts] ==================== BUILDING HTML ====================');
debugLog('[RPG Thoughts] Starting HTML generation for', presentCharacters.length + ' characters');
// If no characters parsed, show a placeholder editable card
// If no characters parsed, show empty state (no placeholder)
if (presentCharacters.length === 0) {
debugLog('[RPG Thoughts] ⚠ No characters parsed - showing placeholder card');
// Get default character portrait
let defaultPortrait = FALLBACK_AVATAR_DATA_URI;
let defaultName = 'Character';
if (this_chid !== undefined && characters[this_chid]) {
if (characters[this_chid].avatar && characters[this_chid].avatar !== 'none') {
const thumbnailUrl = getSafeThumbnailUrl('avatar', characters[this_chid].avatar);
if (thumbnailUrl) {
defaultPortrait = thumbnailUrl;
}
}
defaultName = characters[this_chid].name || 'Character';
}
html += '<div class="rpg-thoughts-content">';
html += `
<div class="rpg-character-card" data-character-name="${defaultName}">
<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>
</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>
</div>
`;
// Add custom fields dynamically
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>
`;
}
html += `
</div>
</div>
`;
html += '</div>';
debugLog('[RPG Thoughts] ⚠ No characters parsed - showing empty state');
html += '<div class="rpg-thoughts-content"></div>';
} else {
html += '<div class="rpg-thoughts-content">';
@@ -540,6 +500,7 @@ export function renderThoughts() {
<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>
<button class="rpg-character-remove" data-character="${char.name}" title="Remove character">×</button>
</div>
`;
@@ -650,6 +611,15 @@ export function renderThoughts() {
saveSettings();
});
// Add event listener for character remove button
$thoughtsContainer.find('.rpg-character-remove').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
const characterName = $(this).data('character');
removeCharacter(characterName);
});
// Add event listener for avatar upload clicks
$thoughtsContainer.find('.rpg-avatar-upload').on('click', function(e) {
e.preventDefault();
@@ -703,6 +673,121 @@ export function renderThoughts() {
}
}
/**
* Removes a character from Present Characters data and re-renders.
*
* @param {string} characterName - Name of the character to remove
*/
export function removeCharacter(characterName) {
if (!lastGeneratedData.characterThoughts) {
return;
}
// Check if data is in JSON format
let isJSON = false;
let parsedData = null;
try {
parsedData = typeof lastGeneratedData.characterThoughts === 'string'
? JSON.parse(lastGeneratedData.characterThoughts)
: lastGeneratedData.characterThoughts;
if (Array.isArray(parsedData) || (parsedData && parsedData.characters)) {
isJSON = true;
}
} catch (e) {
// Not JSON, treat as text format
}
if (isJSON) {
// JSON format - remove character from array
let characters = Array.isArray(parsedData) ? parsedData : parsedData.characters;
characters = characters.filter(char => char.name !== characterName);
if (Array.isArray(parsedData)) {
parsedData = characters;
} else {
parsedData.characters = characters;
}
const updatedJSON = JSON.stringify(parsedData, null, 2);
lastGeneratedData.characterThoughts = updatedJSON;
committedTrackerData.characterThoughts = updatedJSON;
} else {
// Text format - remove character block
const lines = lastGeneratedData.characterThoughts.split('\n');
const dividerIndex = lines.findIndex(line => line.includes('---'));
if (dividerIndex === -1) return;
// Find the character block to remove
let startLineIndex = -1;
let endLineIndex = -1;
for (let i = dividerIndex + 1; i < lines.length; i++) {
const line = lines[i].trim();
// Check if this is the start of the character block
if (line.startsWith('Name:')) {
const nameMatch = line.match(/^Name:\s*(.+)/);
if (nameMatch && nameMatch[1].trim() === characterName) {
startLineIndex = i;
}
}
// If we found the start, look for the end
if (startLineIndex !== -1 && i > startLineIndex) {
// End of block is either another "Name:" line or end of content
if (line.startsWith('Name:') || i === lines.length - 1) {
endLineIndex = line.startsWith('Name:') ? i - 1 : i;
// Remove empty lines at the end of the block
while (endLineIndex > startLineIndex && !lines[endLineIndex].trim()) {
endLineIndex--;
}
break;
}
}
}
// Remove the character block
if (startLineIndex !== -1 && endLineIndex !== -1) {
lines.splice(startLineIndex, endLineIndex - startLineIndex + 1);
// Remove empty lines after removal to keep formatting clean
let i = startLineIndex;
while (i < lines.length && !lines[i].trim()) {
lines.splice(i, 1);
}
}
lastGeneratedData.characterThoughts = lines.join('\n');
committedTrackerData.characterThoughts = lines.join('\n');
}
// Update message swipe data
const chat = getContext().chat;
if (chat && chat.length > 0) {
for (let i = chat.length - 1; i >= 0; i--) {
const message = chat[i];
if (!message.is_user) {
if (message.extra && message.extra.rpg_companion_swipes) {
const swipeId = message.swipe_id || 0;
if (message.extra.rpg_companion_swipes[swipeId]) {
message.extra.rpg_companion_swipes[swipeId].characterThoughts = lastGeneratedData.characterThoughts;
}
}
break;
}
}
}
saveChatData();
// Re-render to show updated character list
renderThoughts();
}
/**
* Updates a specific character field in Present Characters data and re-renders.
* Works with the new multi-line format.