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('
No character data generated yet
'); return; @@ -193,7 +193,7 @@ export function renderThoughts({ preserveScroll = false } = {}) { const hasRelationshipEnabled = relationshipFields.length > 0; // Use committedTrackerData as fallback if lastGeneratedData is empty (e.g., after page refresh) - const characterThoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts || ''; + const characterThoughtsData = lastGeneratedData.characterThoughts || (useCommittedFallback ? committedTrackerData.characterThoughts : null) || ''; // console.log('[RPG Companion] renderThoughts - Reading from lastGeneratedData:', JSON.stringify(lastGeneratedData.characterThoughts)); // console.log('[RPG Companion] renderThoughts - Reading from committedTrackerData:', JSON.stringify(committedTrackerData.characterThoughts)); @@ -839,7 +839,7 @@ export function removeCharacter(characterName) { 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; + setMessageSwipeTrackerField(message, swipeId, 'characterThoughts', lastGeneratedData.characterThoughts); } } break; @@ -969,7 +969,7 @@ export function addNewCharacter() { 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; + setMessageSwipeTrackerField(message, swipeId, 'characterThoughts', lastGeneratedData.characterThoughts); } } break; @@ -1152,7 +1152,7 @@ export function updateCharacterField(characterName, 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].characterThoughts = lastGeneratedData.characterThoughts; + setMessageSwipeTrackerField(message, swipeId, 'characterThoughts', lastGeneratedData.characterThoughts); } } break; @@ -1381,7 +1381,7 @@ export function updateCharacterField(characterName, 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].characterThoughts = lines.join('\n'); + setMessageSwipeTrackerField(message, swipeId, 'characterThoughts', lines.join('\n')); } } break; @@ -1438,66 +1438,265 @@ function renderThoughtsSidebarOnly() { } /** - * Updates or removes thought overlays in the chat. - * Creates floating thought bubbles positioned near character avatars. + * Updates or removes thoughts shown in chat. + * Renders either the original corner bubbles or inline dropdown cards. */ -export function updateChatThoughts() { - // console.log('[RPG Companion] ======== updateChatThoughts called ========'); - // console.log('[RPG Companion] Extension enabled:', extensionSettings.enabled); - // console.log('[RPG Companion] showThoughtsInChat setting:', extensionSettings.showThoughtsInChat); - // console.log('[RPG Companion] Toggle element checked:', $('#rpg-toggle-thoughts-in-chat').prop('checked')); - // console.log('[RPG Companion] lastGeneratedData.characterThoughts:', lastGeneratedData.characterThoughts); - // Remove existing thought panel and icon +let inlineThoughtsObserver = null; +let inlineThoughtsRefreshTimeout = null; +let isRefreshingInlineThoughts = false; + +export function updateChatThoughts(attempt = 0) { + const thoughtsStyle = extensionSettings.thoughtsInChatStyle || 'corner'; + const openInlineThoughts = getOpenInlineThoughts(); + + // Remove old floating thought panel/icon (legacy cleanup) $('#rpg-thought-panel').remove(); $('#rpg-thought-icon').remove(); $('#chat').off('scroll.thoughtPanel'); $(window).off('resize.thoughtPanel'); $(document).off('click.thoughtPanel'); - // If extension is disabled, thoughts in chat are disabled, or no thoughts, just return - if (!extensionSettings.enabled || !extensionSettings.showThoughtsInChat || !lastGeneratedData.characterThoughts) { - // console.log('[RPG Companion] Thoughts in chat disabled or no data'); + // Remove any existing inline thought dropdowns from previous renders + $('.rpg-inline-thoughts, .rpg-inline-thought').remove(); + + const canRenderThoughts = extensionSettings.enabled + && extensionSettings.showThoughtsInChat + && !!lastGeneratedData.characterThoughts; + + if (!canRenderThoughts) { + teardownInlineThoughtsObserver(); return; } - // Parse the Present Characters data to get thoughts - let thoughtsArray = []; // Array of {name, emoji, thought} + const thoughtsArray = parseThoughtsArray(); + + if (thoughtsArray.length === 0) { + teardownInlineThoughtsObserver(); + return; + } + + const targetInfo = findThoughtTargetMessage(); + let $targetMessage = targetInfo.$message; + + if ((!$targetMessage || !$targetMessage.length || !$targetMessage.find('.mes_text').length) && attempt < 10) { + setTimeout(() => updateChatThoughts(attempt + 1), 120); + return; + } + + if (!$targetMessage || !$targetMessage.length) { + teardownInlineThoughtsObserver(); + return; + } + + if (thoughtsStyle === 'inline') { + insertInlineThoughts($targetMessage, thoughtsArray, openInlineThoughts); + ensureInlineThoughtsObserver(); + } else { + teardownInlineThoughtsObserver(); + createThoughtPanel($targetMessage, thoughtsArray); + } +} + +function findThoughtTargetMessage() { + const context = getContext(); + const chat = context?.chat || []; + const currentThoughts = normalizeThoughtPayload(lastGeneratedData.characterThoughts); + + let fallbackIndex = -1; + + // Match the currently displayed thoughts against stored swipe payloads so the + // UI stays attached to the visible assistant reply after swipes, deletes, and reloads. + for (let i = chat.length - 1; i >= 0; i--) { + const message = chat[i]; + if (message?.is_user) continue; + + if (fallbackIndex === -1) { + fallbackIndex = i; + } + + const messageThoughts = getMessageThoughtPayload(message); + if (!currentThoughts || !messageThoughts) continue; + + if (messageThoughts === currentThoughts) { + const $message = $(`#chat .mes[mesid="${i}"]`); + return { index: i, $message }; + } + } + + if (fallbackIndex !== -1) { + return { + index: fallbackIndex, + $message: $(`#chat .mes[mesid="${fallbackIndex}"]`) + }; + } + + return { index: -1, $message: $() }; +} + +function getMessageThoughtPayload(message) { + if (!message || message.is_user) { + return null; + } + + const swipeData = getCurrentMessageSwipeTrackerData(message); + return normalizeThoughtPayload(swipeData?.characterThoughts ?? null); +} + +function normalizeThoughtPayload(payload) { + if (!payload) { + return null; + } + + if (typeof payload === 'object') { + return stableStringify(payload); + } + + if (typeof payload !== 'string') { + return String(payload); + } + + const trimmed = payload.trim(); + if (!trimmed) { + return null; + } + + try { + return stableStringify(JSON.parse(trimmed)); + } catch { + return trimmed.replace(/\r\n/g, '\n'); + } +} + +function stableStringify(value) { + if (Array.isArray(value)) { + return `[${value.map(item => stableStringify(item)).join(',')}]`; + } + + if (value && typeof value === 'object') { + return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`; + } + + return JSON.stringify(value); +} + +function getOpenInlineThoughts() { + const openThoughts = new Set(); + + $('.rpg-inline-thought[open]').each(function () { + const characterName = ($(this).attr('data-character') || '').trim(); + if (characterName) { + openThoughts.add(characterName); + } + }); + + return openThoughts; +} + +function ensureInlineThoughtsObserver() { + if (inlineThoughtsObserver) { + return; + } + + const chatElement = document.getElementById('chat'); + if (!chatElement) { + return; + } + + inlineThoughtsObserver = new MutationObserver((mutations) => { + if (isRefreshingInlineThoughts) { + return; + } + + // SillyTavern rerenders message DOM after swipes, deletes, and message cleanup. + // Watch for those chat-level mutations and reattach inline thoughts once the + // target message body exists again, while ignoring our own refresh churn. + const shouldRefresh = mutations.some((mutation) => { + if (mutation.type !== 'childList') { + return false; + } + + const touchedThoughtNode = [...mutation.addedNodes, ...mutation.removedNodes].some((node) => { + if (!(node instanceof HTMLElement)) { + return false; + } + + return node.classList?.contains('mes') + || node.classList?.contains('mes_text') + || node.querySelector?.('.mes, .mes_text'); + }); + + return touchedThoughtNode; + }); + + if (!shouldRefresh) { + return; + } + + clearTimeout(inlineThoughtsRefreshTimeout); + inlineThoughtsRefreshTimeout = setTimeout(() => { + if (!extensionSettings.enabled + || !extensionSettings.showThoughtsInChat + || (extensionSettings.thoughtsInChatStyle || 'corner') !== 'inline') { + teardownInlineThoughtsObserver(); + return; + } + + isRefreshingInlineThoughts = true; + try { + updateChatThoughts(); + } finally { + isRefreshingInlineThoughts = false; + } + }, 50); + }); + + inlineThoughtsObserver.observe(chatElement, { + childList: true, + subtree: true + }); +} + +function teardownInlineThoughtsObserver() { + if (inlineThoughtsRefreshTimeout) { + clearTimeout(inlineThoughtsRefreshTimeout); + inlineThoughtsRefreshTimeout = null; + } + + if (inlineThoughtsObserver) { + inlineThoughtsObserver.disconnect(); + inlineThoughtsObserver = null; + } +} + +function parseThoughtsArray() { + let thoughtsArray = []; const thoughtsConfig = extensionSettings.trackerConfig?.presentCharacters?.thoughts; const thoughtsLabel = thoughtsConfig?.name || 'Thoughts'; - // Try JSON format first try { const parsed = typeof lastGeneratedData.characterThoughts === 'string' ? JSON.parse(lastGeneratedData.characterThoughts) : lastGeneratedData.characterThoughts; - // Handle both {characters: [...]} and direct array formats const charactersArray = Array.isArray(parsed) ? parsed : (parsed.characters || []); if (charactersArray.length > 0) { - // Extract thoughts from JSON character objects + const offScene = /\b(not\s+(currently\s+)?(in|at|present|in\s+the)\s+(the\s+)?(scene|area|room|location|vicinity))\b|\b(off[\s-]?scene)\b|\b(not\s+present)\b|\b(absent)\b|\b(away\s+from\s+(the\s+)?scene)\b/i; thoughtsArray = charactersArray - .filter(char => char.thoughts && char.thoughts.content) + .filter(char => char.thoughts && char.thoughts.content && !offScene.test(char.thoughts.content)) .map(char => ({ - name: (char.name || '').toLowerCase(), + name: (char.name || ''), emoji: char.emoji || '👤', thought: char.thoughts.content })); - - debugLog('[RPG Thoughts Bubble] ✓ Parsed JSON format, thoughts:', thoughtsArray.length); } } catch (e) { debugLog('[RPG Thoughts Bubble] Not JSON format, falling back to text parsing'); } - // If JSON parsing failed or returned empty, try text format - if (thoughtsArray.length === 0) { + if (thoughtsArray.length === 0 && lastGeneratedData.characterThoughts) { const lines = lastGeneratedData.characterThoughts.split('\n'); - - // console.log('[RPG Companion] Parsing thoughts from lines:', lines); - - // Parse new format to build character map and thoughts let currentCharName = null; let currentCharEmoji = null; @@ -1513,74 +1712,103 @@ export function updateChatThoughts() { continue; } - // Check if this is a character name line (starts with "- ") if (line.startsWith('- ')) { const name = line.substring(2).trim(); if (name && name.toLowerCase() !== 'unavailable') { currentCharName = name; - currentCharEmoji = null; // Reset emoji for new character + currentCharEmoji = null; } else { currentCharName = null; currentCharEmoji = null; } - } - // Check if this is a Details line (contains the emoji) - else if (line.startsWith('Details:') && currentCharName) { + } else if (line.startsWith('Details:') && currentCharName) { const detailsContent = line.substring(line.indexOf(':') + 1).trim(); const parts = detailsContent.split('|').map(p => p.trim()); - - // First part is the emoji if (parts.length > 0) { currentCharEmoji = parts[0]; } - } - // Check if this is a Thoughts line - else if (line.startsWith(thoughtsLabel + ':') && currentCharName && currentCharEmoji) { + } else if (line.startsWith(thoughtsLabel + ':') && currentCharName && currentCharEmoji) { const thoughtContent = line.substring(thoughtsLabel.length + 1).trim(); - - // The thought content is just the text (no emoji prefix in new format) if (thoughtContent) { thoughtsArray.push({ - name: currentCharName.toLowerCase(), + name: currentCharName, emoji: currentCharEmoji, thought: thoughtContent }); } } } - } // End of text format parsing for thoughts bubbles + } - debugLog('[RPG Thoughts] Parsed thoughts:', thoughtsArray); + return thoughtsArray; +} - // If no thoughts parsed, return - if (thoughtsArray.length === 0) { - // console.log('[RPG Companion] No thoughts parsed, returning'); +function escapeInlineThoughtHtml(value) { + return String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function insertInlineThoughts($message, thoughtsArray, openThoughts = new Set()) { + const $mesText = $message.find('.mes_text'); + if (!$mesText.length) { return; } - // console.log('[RPG Companion] Total thoughts:', thoughtsArray.length); - // console.log('[RPG Companion] Thoughts array:', thoughtsArray); - - // Find the last message to position near - const $messages = $('#chat .mes'); - let $targetMessage = null; - - // Find the most recent non-user message - for (let i = $messages.length - 1; i >= 0; i--) { - const $message = $messages.eq(i); - if ($message.attr('is_user') !== 'true') { - $targetMessage = $message; - break; - } + const thoughtsMap = {}; + for (const thoughtData of thoughtsArray) { + thoughtsMap[(thoughtData.name || '').toLowerCase()] = thoughtData; } - if (!$targetMessage) { - // console.log('[RPG Companion] No target message found'); + const $container = $('
'); + bindInlineThoughtEvents($container); + + for (const [, thoughtData] of Object.entries(thoughtsMap)) { + const $dropdown = createInlineThoughtDropdown(thoughtData, openThoughts); + $container.append($dropdown); + } + + if (!$container.children().length) { return; } - // Create the thought panel with all thoughts - createThoughtPanel($targetMessage, thoughtsArray); + // Mount outside .mes_text so SillyTavern's click-to-edit handlers do not + // intercept summary clicks before the details element can toggle. + const $mediaWrapper = $message.find('.mes_media_wrapper').first(); + if ($mediaWrapper.length) { + $container.insertBefore($mediaWrapper); + } else { + $container.insertAfter($mesText.last()); + } +} + +function bindInlineThoughtEvents($container) { + $container.on('click mousedown touchstart', '.rpg-inline-thought, .rpg-inline-thought-summary, .rpg-inline-thought-content', function (e) { + e.stopPropagation(); + }); +} + +function createInlineThoughtDropdown(thoughtData, openThoughts = new Set()) { + const characterName = thoughtData.name || ''; + const characterEmoji = thoughtData.emoji || '👤'; + const thoughtText = thoughtData.thought || ''; + const normalizedCharacterName = characterName.toLowerCase(); + const openAttribute = openThoughts.has(normalizedCharacterName) ? ' open' : ''; + + return $(` +
+ + ${escapeInlineThoughtHtml(characterEmoji)} + ${escapeInlineThoughtHtml(characterName)}'s thoughts + +
+
${escapeInlineThoughtHtml(thoughtText)}
+
+
+ `); } // ===== GLOBAL DRAGGING SETUP FOR THOUGHT ICON (MOBILE ONLY) ===== diff --git a/style.css b/style.css index 68b01ef..3375c37 100644 --- a/style.css +++ b/style.css @@ -11677,3 +11677,66 @@ body:has(.rpg-panel[data-theme="light"]) .rpg-strip-widget { min-width: 2.5rem !important; } } + +.rpg-inline-thoughts { + display: flex; + flex-direction: column; + gap: 0.5em; + margin: 0.2em 0 0.35em; +} + +.rpg-inline-thought { + margin: 0; + border: 1px solid rgba(255, 255, 255, 0.12); + border-left: 2px solid rgba(255, 120, 140, 0.6); + border-radius: 0.5em; + background: rgba(255, 255, 255, 0.03); + overflow: hidden; +} + +.rpg-inline-thought[open] { + background: rgba(255, 255, 255, 0.045); +} + +.rpg-inline-thought-summary { + display: flex; + align-items: center; + gap: 0.45em; + padding: 0.45em 0.7em; + cursor: pointer; + list-style: none; + font-weight: 600; + color: var(--SmartThemeBodyColor); +} + +.rpg-inline-thought-summary:hover { + background: rgba(255, 255, 255, 0.035); +} + +.rpg-inline-thought-summary::-webkit-details-marker { + display: none; +} + +.rpg-inline-thought-icon { + opacity: 0.9; + flex: 0 0 auto; +} + +.rpg-inline-thought-name { + line-height: 1.2; +} + +.rpg-inline-thought-content { + padding: 0 0.7em 0.65em; +} + +.rpg-inline-thought-text { + margin: 0; + padding: 0.6em 0.75em; + border-radius: 0.4em; + background: rgba(0, 0, 0, 0.28); + color: var(--SmartThemeBodyColor); + font-style: italic; + line-height: 1.45; +} + diff --git a/template.html b/template.html index bc0095d..0590f1b 100644 --- a/template.html +++ b/template.html @@ -367,6 +367,14 @@ Display character thoughts as overlay bubbles next to their messages. + + + Switch between the default corner thought bubbles and thought cards below the message text. + +