diff --git a/index.js b/index.js index 4237e5c..1f80678 100644 --- a/index.js +++ b/index.js @@ -150,7 +150,10 @@ import { onMessageSent, onMessageReceived, onCharacterChanged, + onChatLoaded, + onMessageDeleted, onMessageSwiped, + scheduleChatStateRehydration, updatePersonaAvatar, clearExtensionPrompts, onGenerationEnded, @@ -229,6 +232,7 @@ async function addExtensionSettings() { // Enabling extension - initialize UI await initUI(); loadChatData(); // Load chat data for current chat + scheduleChatStateRehydration(); updateChatThoughts(); // Create thought bubbles if data exists injectCheckpointButton(); // Re-add checkpoint buttons updateAllCheckpointIndicators(); // Update button states @@ -367,6 +371,12 @@ async function initUI() { updateChatThoughts(); }); + $('#rpg-toggle-inline-thoughts').on('change', function() { + extensionSettings.thoughtsInChatStyle = $(this).prop('checked') ? 'inline' : 'corner'; + saveSettings(); + updateChatThoughts(); + }); + $('#rpg-toggle-html-prompt').on('change', function() { extensionSettings.enableHtmlPrompt = $(this).prop('checked'); // console.log('[RPG Companion] Toggle enableHtmlPrompt changed to:', extensionSettings.enableHtmlPrompt); @@ -1047,6 +1057,7 @@ async function initUI() { $('#rpg-toggle-quests').prop('checked', extensionSettings.showQuests); $('#rpg-toggle-lock-icons').prop('checked', extensionSettings.showLockIcons ?? true); $('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat); + $('#rpg-toggle-inline-thoughts').prop('checked', (extensionSettings.thoughtsInChatStyle || 'corner') === 'inline'); $('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt); $('#rpg-toggle-dialogue-coloring').prop('checked', extensionSettings.enableDialogueColoring); $('#rpg-toggle-deception').prop('checked', extensionSettings.enableDeceptionSystem ?? false); @@ -1283,6 +1294,7 @@ jQuery(async () => { // Load chat-specific data for current chat try { loadChatData(); + scheduleChatStateRehydration(); // Initialize FAB widgets and strip widgets with any loaded data updateFabWidgets(); updateStripWidgets(); @@ -1353,6 +1365,9 @@ jQuery(async () => { [event_types.GENERATION_STOPPED]: onGenerationEnded, [event_types.GENERATION_ENDED]: onGenerationEnded, [event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts], + [event_types.CHAT_LOADED]: onChatLoaded, + [event_types.MESSAGE_DELETED]: onMessageDeleted, + [event_types.MESSAGE_SWIPE_DELETED]: onMessageDeleted, [event_types.MESSAGE_SWIPED]: onMessageSwiped, [event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar, [event_types.SETTINGS_UPDATED]: updatePersonaAvatar diff --git a/src/core/config.js b/src/core/config.js index 7c7750e..09b9c71 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -33,6 +33,7 @@ export const defaultSettings = { showQuests: true, // Show quests section showLockIcons: true, // Show lock/unlock icons on tracker items showThoughtsInChat: true, // Show thoughts overlay in chat + thoughtsInChatStyle: 'corner', // 'corner' or 'inline' enableHtmlPrompt: false, // Enable immersive HTML prompt injection enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs) customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default) diff --git a/src/core/persistence.js b/src/core/persistence.js index e6343c8..cb0bc93 100644 --- a/src/core/persistence.js +++ b/src/core/persistence.js @@ -21,6 +21,242 @@ import { migrateToV3JSON } from '../utils/jsonMigration.js'; const extensionName = 'third-party/rpg-companion-sillytavern'; +function hasTrackerPayload(payload) { + return !!(payload && typeof payload === 'object' && ( + payload.userStats + || payload.infoBox + || payload.characterThoughts + )); +} + +function getCurrentTrackerPayloadFromSwipeStore(store, preferredSwipeId = 0) { + if (!store) { + return null; + } + + if (hasTrackerPayload(store)) { + return store; + } + + const preferredKey = String(preferredSwipeId); + const preferredPayload = store[preferredKey] ?? store[preferredSwipeId]; + if (hasTrackerPayload(preferredPayload)) { + return preferredPayload; + } + + return null; +} + +function getTrackerPayloadFromSwipeStore(store, preferredSwipeId = 0) { + const currentPayload = getCurrentTrackerPayloadFromSwipeStore(store, preferredSwipeId); + if (currentPayload) { + return currentPayload; + } + + if (!store || typeof store !== 'object') { + return null; + } + + const numericKeys = Object.keys(store) + .filter(key => /^\d+$/.test(key)) + .sort((a, b) => Number(b) - Number(a)); + + for (const key of numericKeys) { + const payload = store[key]; + if (hasTrackerPayload(payload)) { + return payload; + } + } + + for (const payload of Object.values(store)) { + if (hasTrackerPayload(payload)) { + return payload; + } + } + + return null; +} + +function ensureTrackerPayloadSlot(store, swipeId = 0) { + if (!store || typeof store !== 'object' || Array.isArray(store)) { + return null; + } + + if (hasTrackerPayload(store)) { + return store; + } + + if (!store[swipeId] || typeof store[swipeId] !== 'object' || Array.isArray(store[swipeId])) { + store[swipeId] = {}; + } + + return store[swipeId]; +} + +function ensureSwipeInfoEntry(message, swipeId = 0) { + if (!Array.isArray(message?.swipe_info)) { + return null; + } + + if (!message.swipe_info[swipeId] || typeof message.swipe_info[swipeId] !== 'object') { + message.swipe_info[swipeId] = { + send_date: message.send_date, + gen_started: message.gen_started, + gen_finished: message.gen_finished, + extra: {} + }; + } + + if (!message.swipe_info[swipeId].extra || typeof message.swipe_info[swipeId].extra !== 'object') { + message.swipe_info[swipeId].extra = {}; + } + + return message.swipe_info[swipeId]; +} + +export function getCurrentMessageSwipeTrackerData(message) { + if (!message || message.is_user) { + return null; + } + + const swipeId = Number(message.swipe_id ?? 0); + + const preferredSources = [ + message.extra?.rpg_companion_swipes, + message.swipe_info?.[swipeId]?.extra?.rpg_companion_swipes + ]; + + for (const source of preferredSources) { + const payload = getCurrentTrackerPayloadFromSwipeStore(source, swipeId); + if (payload) { + return payload; + } + } + + return null; +} + +export function getMessageSwipeTrackerData(message) { + if (!message || message.is_user) { + return null; + } + + const swipeId = Number(message.swipe_id ?? 0); + const currentPayload = getCurrentMessageSwipeTrackerData(message); + if (currentPayload) { + return currentPayload; + } + + const preferredSources = [ + message.extra?.rpg_companion_swipes, + message.swipe_info?.[swipeId]?.extra?.rpg_companion_swipes + ]; + + for (const source of preferredSources) { + const payload = getTrackerPayloadFromSwipeStore(source, swipeId); + if (payload) { + return payload; + } + } + + if (Array.isArray(message.swipe_info)) { + for (let i = message.swipe_info.length - 1; i >= 0; i--) { + const payload = getTrackerPayloadFromSwipeStore(message.swipe_info[i]?.extra?.rpg_companion_swipes, swipeId); + if (payload) { + return payload; + } + } + } + + return null; +} + +export function getLatestTrackerDataFromChat(chatMessages) { + if (!Array.isArray(chatMessages)) { + return null; + } + + for (let i = chatMessages.length - 1; i >= 0; i--) { + const message = chatMessages[i]; + if (message?.is_user) continue; + + const swipeData = getCurrentMessageSwipeTrackerData(message); + if (!swipeData) continue; + + return { + userStats: swipeData.userStats || null, + infoBox: swipeData.infoBox || null, + characterThoughts: typeof swipeData.characterThoughts === 'object' + ? JSON.stringify(swipeData.characterThoughts, null, 2) + : (swipeData.characterThoughts || null) + }; + } + + return null; +} + +export function restoreLatestTrackerStateFromChat(chatMessages) { + const latestData = getLatestTrackerDataFromChat(chatMessages); + if (!latestData) { + return false; + } + + setLastGeneratedData({ + userStats: latestData.userStats || null, + infoBox: latestData.infoBox || null, + characterThoughts: latestData.characterThoughts || null, + html: lastGeneratedData.html || null + }); + + setCommittedTrackerData({ + userStats: latestData.userStats || committedTrackerData.userStats || null, + infoBox: latestData.infoBox || committedTrackerData.infoBox || null, + characterThoughts: latestData.characterThoughts || committedTrackerData.characterThoughts || null + }); + + return true; +} + +export function setMessageSwipeTrackerData(message, swipeId = 0, trackerData = {}) { + if (!message || message.is_user || !trackerData || typeof trackerData !== 'object') { + return null; + } + + if (!message.extra || typeof message.extra !== 'object') { + message.extra = {}; + } + if (!message.extra.rpg_companion_swipes || typeof message.extra.rpg_companion_swipes !== 'object' || Array.isArray(message.extra.rpg_companion_swipes)) { + message.extra.rpg_companion_swipes = {}; + } + + const extraPayload = ensureTrackerPayloadSlot(message.extra.rpg_companion_swipes, swipeId); + if (extraPayload) { + Object.assign(extraPayload, trackerData); + } + + const swipeInfoEntry = ensureSwipeInfoEntry(message, swipeId); + if (swipeInfoEntry) { + if (!swipeInfoEntry.extra.rpg_companion_swipes || typeof swipeInfoEntry.extra.rpg_companion_swipes !== 'object' || Array.isArray(swipeInfoEntry.extra.rpg_companion_swipes)) { + swipeInfoEntry.extra.rpg_companion_swipes = {}; + } + + const swipePayload = ensureTrackerPayloadSlot(swipeInfoEntry.extra.rpg_companion_swipes, swipeId); + if (swipePayload) { + Object.assign(swipePayload, trackerData); + } + } + + return extraPayload; +} + +export function setMessageSwipeTrackerField(message, swipeId = 0, field, value) { + if (!field) { + return null; + } + + return setMessageSwipeTrackerData(message, swipeId, { [field]: value }); +} + /** * Validates extension settings structure * @param {Object} settings - Settings object to validate @@ -134,6 +370,12 @@ export function loadSettings() { settingsChanged = true; } + // Normalize additive settings without introducing another schema bump. + if (!extensionSettings.thoughtsInChatStyle) { + extensionSettings.thoughtsInChatStyle = 'corner'; + settingsChanged = true; + } + // Save migrated settings if (settingsChanged) { saveSettings(); @@ -247,11 +489,11 @@ export function updateMessageSwipeData() { } const swipeId = message.swipe_id || 0; - message.extra.rpg_companion_swipes[swipeId] = { + setMessageSwipeTrackerData(message, swipeId, { userStats: lastGeneratedData.userStats, infoBox: lastGeneratedData.infoBox, characterThoughts: lastGeneratedData.characterThoughts - }; + }); // console.log('[RPG Companion] Updated message swipe data after user edit'); break; @@ -264,8 +506,10 @@ export function updateMessageSwipeData() { * Automatically migrates v1 inventory to v2 format if needed. */ export function loadChatData() { - if (!chat_metadata || !chat_metadata.rpg_companion) { - // Reset to defaults if no data exists + const savedData = chat_metadata?.rpg_companion; + + if (!savedData) { + // Reset to defaults if no metadata exists, then try to rebuild from message swipe data below. updateExtensionSettings({ userStats: { health: 100, @@ -299,23 +543,20 @@ export function loadChatData() { infoBox: null, characterThoughts: null }); - return; } - const savedData = chat_metadata.rpg_companion; - // Restore stats - if (savedData.userStats) { + if (savedData?.userStats) { extensionSettings.userStats = { ...savedData.userStats }; } // Restore classic stats - if (savedData.classicStats) { + if (savedData?.classicStats) { extensionSettings.classicStats = { ...savedData.classicStats }; } // Restore quests - if (savedData.quests) { + if (savedData?.quests) { extensionSettings.quests = { ...savedData.quests }; } else { // Initialize with defaults if not present @@ -326,7 +567,7 @@ export function loadChatData() { } // Restore committed tracker data first - if (savedData.committedTrackerData) { + if (savedData?.committedTrackerData) { // console.log('[RPG Companion] 📥 loadChatData restoring committedTrackerData:', { // userStats: savedData.committedTrackerData.userStats ? `${savedData.committedTrackerData.userStats.substring(0, 50)}...` : 'null', // infoBox: savedData.committedTrackerData.infoBox ? 'exists' : 'null', @@ -343,7 +584,7 @@ export function loadChatData() { // Restore last generated data (for display) // Always prefer lastGeneratedData as it contains the most recent generation (including swipes) - if (savedData.lastGeneratedData) { + if (savedData?.lastGeneratedData) { // console.log('[RPG Companion] 📥 loadChatData restoring lastGeneratedData'); setLastGeneratedData({ ...savedData.lastGeneratedData }); } else { @@ -363,6 +604,19 @@ export function loadChatData() { // Validate inventory structure (Bug #3 fix) validateInventoryStructure(extensionSettings.userStats.inventory, 'chat'); + + // Sync display data from the latest assistant message's stored swipe payload. + // This is more reliable than chat metadata alone on chat re-entry because the + // latest rendered swipe data may exist on the message even if the debounced + // metadata save did not flush yet. + try { + const chatContext = getContext(); + const chatMessages = chatContext?.chat; + restoreLatestTrackerStateFromChat(chatMessages); + } catch (e) { + console.warn('[RPG Companion] Per-message data sync skipped:', e.message); + } + // console.log('[RPG Companion] Loaded chat data:', savedData); } diff --git a/src/core/state.js b/src/core/state.js index 693072a..e85f3aa 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -10,7 +10,7 @@ * Extension settings - persisted to SillyTavern settings */ export let extensionSettings = { - settingsVersion: 4, // Version number for settings migrations (v4 = FAB widgets enabled by default) + settingsVersion: 4, // Version number for settings migrations enabled: true, autoUpdate: false, updateDepth: 4, // How many messages to include in the context @@ -21,6 +21,7 @@ export let extensionSettings = { showInventory: true, // Show inventory section (v2 system) showQuests: true, // Show quests section showThoughtsInChat: true, // Show thoughts overlay in chat + thoughtsInChatStyle: 'corner', // 'corner' or 'inline' narratorMode: false, // Use character card as narrator instead of fixed character references customNarratorPrompt: '', // Custom narrator mode prompt text (empty = use default) customContextInstructionsPrompt: '', // Custom context instructions prompt text (empty = use default) diff --git a/src/i18n/en.json b/src/i18n/en.json index ba49c5a..7c4ac74 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -273,4 +273,4 @@ "stats.int": "INT", "stats.wis": "WIS", "stats.cha": "CHA" -} \ No newline at end of file +} diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 118abe0..f96d213 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -274,4 +274,4 @@ "stats.int": "INT", "stats.wis": "VOL", "stats.cha": "CHA" -} \ No newline at end of file +} diff --git a/src/systems/generation/apiClient.js b/src/systems/generation/apiClient.js index 4c38f68..5b0da18 100644 --- a/src/systems/generation/apiClient.js +++ b/src/systems/generation/apiClient.js @@ -20,7 +20,7 @@ import { setLastActionWasSwipe, $musicPlayerContainer } from '../../core/state.js'; -import { saveChatData } from '../../core/persistence.js'; +import { saveChatData, setMessageSwipeTrackerData } from '../../core/persistence.js'; import { generateSeparateUpdatePrompt } from './promptBuilder.js'; @@ -317,11 +317,11 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough } const currentSwipeId = lastMessage.swipe_id || 0; - lastMessage.extra.rpg_companion_swipes[currentSwipeId] = { + setMessageSwipeTrackerData(lastMessage, currentSwipeId, { userStats: parsedData.userStats, infoBox: parsedData.infoBox, characterThoughts: parsedData.characterThoughts - }; + }); // console.log('[RPG Companion] Stored separate mode RPG data for message swipe', currentSwipeId); } diff --git a/src/systems/integration/sillytavern.js b/src/systems/integration/sillytavern.js index 3af43a8..3743e22 100644 --- a/src/systems/integration/sillytavern.js +++ b/src/systems/integration/sillytavern.js @@ -4,7 +4,7 @@ */ import { getContext } from '../../../../../../extensions.js'; -import { chat, user_avatar, setExtensionPrompt, extension_prompt_types, saveChatDebounced } from '../../../../../../../script.js'; +import { chat, chat_metadata, user_avatar, setExtensionPrompt, extension_prompt_types } from '../../../../../../../script.js'; // Core modules import { @@ -22,7 +22,7 @@ import { updateCommittedTrackerData, $musicPlayerContainer } from '../../core/state.js'; -import { saveChatData, loadChatData, autoSwitchPresetForEntity } from '../../core/persistence.js'; +import { saveChatData, loadChatData, autoSwitchPresetForEntity, getMessageSwipeTrackerData, getCurrentMessageSwipeTrackerData, restoreLatestTrackerStateFromChat, setMessageSwipeTrackerData } from '../../core/persistence.js'; import { i18n } from '../../core/i18n.js'; // Generation & Parsing @@ -51,6 +51,8 @@ import { updateStripWidgets } from '../ui/desktop.js'; import { updateAllCheckpointIndicators } from '../ui/checkpointUI.js'; import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js'; +let chatStateRehydrateRunId = 0; + /** * Commits the tracker data from the last assistant message to be used as source for next generation. * This should be called when the user has replied to a message, ensuring all swipes of the next @@ -87,6 +89,237 @@ export function commitTrackerData() { } } +function getSwipeTrackerData(message) { + return getMessageSwipeTrackerData(message); +} + +function getCurrentSwipeTrackerData(message) { + return getCurrentMessageSwipeTrackerData(message); +} + +function hasAssistantMessageBody() { + const $messages = $('#chat .mes'); + + for (let i = $messages.length - 1; i >= 0; i--) { + const $message = $messages.eq(i); + if ($message.attr('is_user') === 'true') continue; + if ($message.find('.mes_text').length > 0) { + return true; + } + } + + return false; +} + +function hasAnyTrackerStateInChat() { + const chatMessages = getContext()?.chat || []; + + for (let i = chatMessages.length - 1; i >= 0; i--) { + const swipeData = getSwipeTrackerData(chatMessages[i]); + if (swipeData?.userStats || swipeData?.infoBox || swipeData?.characterThoughts) { + return true; + } + } + + return false; +} + +function hasAssistantMessagesInChat() { + const chatMessages = getContext()?.chat || []; + return chatMessages.some(message => message && !message.is_user && !message.is_system); +} + +function hasPotentialTrackerSourceInChat() { + const chatMessages = getContext()?.chat || []; + + for (const message of chatMessages) { + if (!message || message.is_user || message.is_system) { + continue; + } + + if (message.extra?.rpg_companion_swipes) { + return true; + } + + if (Array.isArray(message.swipe_info) && message.swipe_info.some(info => info?.extra?.rpg_companion_swipes)) { + return true; + } + + if (Array.isArray(message.swipes) && message.swipes.length > 1) { + return true; + } + } + + return false; +} + +function maybeRehydrateUserStatsFromDisplayData() { + const hasSavedUserStats = !!chat_metadata?.rpg_companion?.userStats; + if (!hasSavedUserStats && lastGeneratedData.userStats) { + try { + parseUserStats(lastGeneratedData.userStats); + } catch (error) { + console.warn('[RPG Companion] Failed to rebuild user stats from display data:', error); + } + } +} + +function getCurrentSwipeText(message) { + const swipeId = Number(message?.swipe_id ?? 0); + + if (Array.isArray(message?.swipes) && typeof message.swipes[swipeId] === 'string' && message.swipes[swipeId].trim()) { + return message.swipes[swipeId]; + } + + return typeof message?.mes === 'string' ? message.mes : ''; +} + +function repairLatestTrackerStateFromCurrentSwipeContent(chatMessages = getContext()?.chat || []) { + for (let i = chatMessages.length - 1; i >= 0; i--) { + const message = chatMessages[i]; + if (!message || message.is_user || message.is_system) { + continue; + } + + const swipeId = Number(message.swipe_id ?? 0); + if (getCurrentSwipeTrackerData(message)) { + continue; + } + + const currentSwipeText = getCurrentSwipeText(message); + if (!currentSwipeText) { + continue; + } + + const parsedData = parseResponse(currentSwipeText); + if (parsedData.userStats) { + parsedData.userStats = removeLocks(parsedData.userStats); + } + if (parsedData.infoBox) { + parsedData.infoBox = removeLocks(parsedData.infoBox); + } + if (parsedData.characterThoughts) { + parsedData.characterThoughts = removeLocks(parsedData.characterThoughts); + } + + if (!parsedData.userStats && !parsedData.infoBox && !parsedData.characterThoughts) { + continue; + } + + setMessageSwipeTrackerData(message, swipeId, { + userStats: parsedData.userStats || null, + infoBox: parsedData.infoBox || null, + characterThoughts: parsedData.characterThoughts || null + }); + + return true; + } + + return false; +} + +function restoreOrRepairLatestTrackerState() { + const chatMessages = getContext()?.chat || []; + let restored = restoreLatestTrackerStateFromChat(chatMessages); + + if (!restored) { + const repaired = repairLatestTrackerStateFromCurrentSwipeContent(chatMessages); + if (repaired) { + restored = restoreLatestTrackerStateFromChat(chatMessages); + } + } + + return restored; +} + +function rerenderRpgState() { + renderUserStats(); + renderInfoBox(); + renderThoughts(); + renderInventory(); + renderQuests(); + renderMusicPlayer($musicPlayerContainer[0]); + updateFabWidgets(); + updateStripWidgets(); +} + +export function scheduleChatStateRehydration() { + chatStateRehydrateRunId++; + const runId = chatStateRehydrateRunId; + let attempts = 0; + const maxAttempts = 15; + const eagerRetryAttempts = 4; + + const tryRestoreState = () => { + if (runId !== chatStateRehydrateRunId) { + return; + } + + attempts++; + + loadChatData(); + restoreOrRepairLatestTrackerState(); + maybeRehydrateUserStatsFromDisplayData(); + rerenderRpgState(); + + const hasRestoredTrackerState = !!( + lastGeneratedData.userStats + || lastGeneratedData.infoBox + || lastGeneratedData.characterThoughts + || committedTrackerData.userStats + || committedTrackerData.infoBox + || committedTrackerData.characterThoughts + ); + const hasStoredTrackerState = !!chat_metadata?.rpg_companion || hasAnyTrackerStateInChat(); + const hasAssistantMessages = hasAssistantMessagesInChat(); + const hasPotentialTrackerSource = hasPotentialTrackerSourceInChat(); + const chatBodyReady = hasAssistantMessageBody(); + + if (chatBodyReady) { + updateChatThoughts(); + } + + const shouldRetryForRestore = !hasRestoredTrackerState && ( + hasStoredTrackerState + || (hasAssistantMessages && attempts < eagerRetryAttempts) + || (hasPotentialTrackerSource && attempts < maxAttempts) + ); + + const shouldRetryForDom = !chatBodyReady && hasAssistantMessages; + + if ((shouldRetryForRestore || shouldRetryForDom) && attempts < maxAttempts) { + setTimeout(tryRestoreState, 200); + } + }; + + setTimeout(tryRestoreState, 200); +} + +export function onChatLoaded() { + loadChatData(); + restoreOrRepairLatestTrackerState(); + maybeRehydrateUserStatsFromDisplayData(); + rerenderRpgState(); + scheduleChatStateRehydration(); + updateAllCheckpointIndicators(); +} + +function syncDisplayedTrackerStateFromChat() { + const restored = restoreOrRepairLatestTrackerState(); + + if (!restored) { + lastGeneratedData.userStats = null; + lastGeneratedData.infoBox = null; + lastGeneratedData.characterThoughts = null; + committedTrackerData.userStats = null; + committedTrackerData.infoBox = null; + committedTrackerData.characterThoughts = null; + } + + rerenderRpgState(); + updateChatThoughts(); +} + /** * Event handler for when the user sends a message. * Sets the flag to indicate this is NOT a swipe. @@ -193,11 +426,11 @@ export async function onMessageReceived(data) { } const currentSwipeId = lastMessage.swipe_id || 0; - lastMessage.extra.rpg_companion_swipes[currentSwipeId] = { + setMessageSwipeTrackerData(lastMessage, currentSwipeId, { userStats: parsedData.userStats, infoBox: parsedData.infoBox, characterThoughts: parsedData.characterThoughts - }; + }); // console.log('[RPG Companion] Stored RPG data for swipe', currentSwipeId); @@ -244,6 +477,11 @@ export async function onMessageReceived(data) { // console.log('[RPG Companion] Cleaned message, removed tracker code blocks from DOM'); + // Re-insert chat thoughts after SillyTavern finishes rerendering the cleaned message DOM. + if (parsedData.characterThoughts) { + setTimeout(() => updateChatThoughts(), 100); + } + // Save to chat metadata saveChatData(); } @@ -310,6 +548,7 @@ export function onCharacterChanged() { // Remove thought panel and icon when changing characters $('#rpg-thought-panel').remove(); $('#rpg-thought-icon').remove(); + $('.rpg-inline-thoughts, .rpg-inline-thought').remove(); $('#chat').off('scroll.thoughtPanel'); $(window).off('resize.thoughtPanel'); $(document).off('click.thoughtPanel'); @@ -328,20 +567,9 @@ export function onCharacterChanged() { // already contains the committed state from when we last left this chat. // commitTrackerData() will be called naturally when new messages arrive. - // Re-render with the loaded data - renderUserStats(); - renderInfoBox(); - renderThoughts(); - renderInventory(); - renderQuests(); - renderMusicPlayer($musicPlayerContainer[0]); - - // Update FAB widgets and strip widgets with loaded data - updateFabWidgets(); - updateStripWidgets(); - - // Update chat thought overlays - updateChatThoughts(); + // Re-render with the loaded data and retry once SillyTavern finishes restoring chat state. + rerenderRpgState(); + scheduleChatStateRehydration(); // Update checkpoint indicators for the loaded chat updateAllCheckpointIndicators(); @@ -366,6 +594,7 @@ export function onMessageSwiped(messageIndex) { } const currentSwipeId = message.swipe_id || 0; + const swipeCount = Array.isArray(message.swipes) ? message.swipes.length : 0; // Only set flag to true if this swipe will trigger a NEW generation // Check if the swipe already exists (has content in the swipes array) @@ -373,6 +602,8 @@ export function onMessageSwiped(messageIndex) { message.swipes[currentSwipeId] !== undefined && message.swipes[currentSwipeId] !== null && message.swipes[currentSwipeId].length > 0; + const swipeData = getCurrentSwipeTrackerData(message); + const isPendingNewSwipe = currentSwipeId >= swipeCount; if (!isExistingSwipe) { // This is a NEW swipe that will trigger generation @@ -384,13 +615,16 @@ export function onMessageSwiped(messageIndex) { // console.log('[RPG Companion] 🔵 EXISTING swipe navigation - lastActionWasSwipe unchanged =', lastActionWasSwipe); } + if (isPendingNewSwipe) { + lastGeneratedData.characterThoughts = null; + } + // console.log('[RPG Companion] Loading data for swipe', currentSwipeId); // IMPORTANT: onMessageSwiped is for DISPLAY only! // lastGeneratedData is for DISPLAY, committedTrackerData is for GENERATION // It's safe to load swipe data into lastGeneratedData - it won't be committed due to !lastActionWasSwipe check - if (message.extra && message.extra.rpg_companion_swipes && message.extra.rpg_companion_swipes[currentSwipeId]) { - const swipeData = message.extra.rpg_companion_swipes[currentSwipeId]; + if (swipeData) { // Load swipe data into lastGeneratedData for display (both modes) lastGeneratedData.userStats = swipeData.userStats || null; @@ -417,7 +651,7 @@ export function onMessageSwiped(messageIndex) { // Re-render the panels renderUserStats(); renderInfoBox(); - renderThoughts(); + renderThoughts({ useCommittedFallback: !isPendingNewSwipe }); renderInventory(); renderQuests(); renderMusicPlayer($musicPlayerContainer[0]); @@ -426,6 +660,15 @@ export function onMessageSwiped(messageIndex) { updateChatThoughts(); } +export function onMessageDeleted() { + if (!extensionSettings.enabled) { + return; + } + + syncDisplayedTrackerStateFromChat(); + saveChatData(); +} + /** * Update the persona avatar image when user switches personas */ diff --git a/src/systems/rendering/infoBox.js b/src/systems/rendering/infoBox.js index c0ca5f4..9ff36f9 100644 --- a/src/systems/rendering/infoBox.js +++ b/src/systems/rendering/infoBox.js @@ -10,7 +10,7 @@ import { committedTrackerData, $infoBoxContainer } from '../../core/state.js'; -import { saveChatData } from '../../core/persistence.js'; +import { saveChatData, setMessageSwipeTrackerField } from '../../core/persistence.js'; import { i18n } from '../../core/i18n.js'; import { isItemLocked } from '../generation/lockManager.js'; import { repairJSON } from '../../utils/jsonRepair.js'; @@ -989,7 +989,7 @@ export function updateInfoBoxField(field, value) { 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].infoBox = updatedLines.join('\n'); + setMessageSwipeTrackerField(message, swipeId, 'infoBox', updatedLines.join('\n')); // console.log('[RPG Companion] Updated infoBox in message swipe data'); } } @@ -1074,7 +1074,7 @@ function updateRecentEvent(field, value) { 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].infoBox = updatedLines.join('\n'); + setMessageSwipeTrackerField(message, swipeId, 'infoBox', updatedLines.join('\n')); } } break; diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index 6a90349..dbed30e 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -15,7 +15,7 @@ import { addDebugLog } from '../../core/state.js'; import { i18n } from '../../core/i18n.js'; -import { saveChatData, saveSettings } from '../../core/persistence.js'; +import { saveChatData, saveSettings, getCurrentMessageSwipeTrackerData, setMessageSwipeTrackerField } from '../../core/persistence.js'; import { getSafeThumbnailUrl } from '../../utils/avatars.js'; import { isItemLocked, setItemLock } from '../generation/lockManager.js'; @@ -154,7 +154,7 @@ function namesMatch(cardName, aiName) { * Displays character cards with avatars, relationship badges, and traits. * Includes event listeners for editable character fields. */ -export function renderThoughts({ preserveScroll = false } = {}) { +export function renderThoughts({ preserveScroll = false, useCommittedFallback = true } = {}) { if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) { return; } @@ -169,7 +169,7 @@ export function renderThoughts({ preserveScroll = false } = {}) { } // Don't render if no data exists (e.g., after cache clear) - const thoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts; + const thoughtsData = lastGeneratedData.characterThoughts || (useCommittedFallback ? committedTrackerData.characterThoughts : null); if (!thoughtsData) { $thoughtsContainer.html('