From 34553c18f707434c40ccc6db9eb8f5657940870f Mon Sep 17 00:00:00 2001 From: ARIA Date: Sun, 16 Aug 2026 21:42:14 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20comprehensive=20bug=20review=20=E2=80=94?= =?UTF-8?q?=20restore=20refactor=20regressions=20&=20fix=20critical/major/?= =?UTF-8?q?minor=20bugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical: - responseExtractor.js: fix import depth (7x ../ -> 6x) that broke the module graph for encounters, separate/external generation, and auto-avatars - settingsListeners.js: restore ~25 undefined functions, ~40 lost event bindings, and the entire 'Initialize UI state' block dropped by the c14c141 refactor; add initializeSettingsUIState() - Restore all External API mode handlers (base-url, api-key, model, max-tokens, temperature, key-visibility, test-connection) + value init - validator.js: convert CJS->ESM (crashed under type:module), fix glob.sync->globSync (v13 API), fix '!key in obj' precedence bugs Major: - sillytavern.js: add missing updateMessageBlock import - infoBox.js: add missing saveSettings import - userStats.js: fix double-escaped quantity regex Minor: - config.js: settingsVersion 6->7, restore showLockIcons, fix apiKey comment - events.js: off() tracking + unregister/re-register on disable/enable - escapeHtml: escape quotes (XSS) in all 3 copies - template.html: data-prompt dialogue-coloring -> dialogueColoring - trackerEditor.js: stale externalApiOnly -> sendAllEnabledOnRefresh - suppression.js: fix '[object Object]' on empty instruct value - persistence.js: await migrateToV3JSON (loadSettings now async) - weatherEffects.js: JSON.parse -> repairJSON - mobile.js: fix corrupted comment line - Add missing debugMode setting; settings.html v3.7.2 -> v3.7.4 Dead code removed: - Unused imports/vars (promptBuilder, jsonPromptHelpers, encounterPrompts, injector), withChangeDetection, trackJQueryHandler/cleanupJQueryEvents, renderThoughtsSidebarOnly, updateCheckpointButtonInMenu, addDiceQuickReply, setupRefreshButtonDrag/setupDebugButtonDrag (~430 lines) Verified: 78/78 tests pass, style.css in sync, validator runs clean. --- index.js | 145 ++-- settings.html | 2 +- src/core/config.js | 6 +- src/core/events.js | 12 + src/core/persistence.js | 4 +- src/core/settingsPanel.js | 11 +- src/core/state.js | 32 - src/i18n/validator.js | 19 +- src/systems/equipment/constants.js | 9 +- src/systems/features/dice.js | 11 +- src/systems/generation/encounterPrompts.js | 1 - src/systems/generation/injector.js | 4 - src/systems/generation/jsonPromptHelpers.js | 5 +- src/systems/generation/promptBuilder.js | 5 +- src/systems/generation/suppression.js | 2 +- src/systems/integration/sillytavern.js | 2 +- src/systems/interaction/inventoryActions.js | 1 - src/systems/rendering/infoBox.js | 2 +- src/systems/rendering/inventory.js | 9 +- src/systems/rendering/quests.js | 10 +- src/systems/rendering/renderUtils.js | 15 - src/systems/rendering/thoughts.js | 24 - src/systems/rendering/userStats.js | 2 +- src/systems/ui/checkpointUI.js | 27 - src/systems/ui/mobile.js | 436 +--------- src/systems/ui/modals.js | 11 +- src/systems/ui/settingsListeners.js | 832 ++++++++++++++++---- src/systems/ui/trackerEditor.js | 2 +- src/systems/ui/weatherEffects.js | 2 +- src/utils/responseExtractor.js | 2 +- template.html | 2 +- 31 files changed, 818 insertions(+), 829 deletions(-) diff --git a/index.js b/index.js index b6eade4..9664116 100644 --- a/index.js +++ b/index.js @@ -55,7 +55,7 @@ import { clearDebugLogs } from './src/core/state.js'; import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData, commitTrackerDataFromPriorMessage } from './src/core/persistence.js'; -import { registerAllEvents, on as onEvent } from './src/core/events.js'; +import { registerAllEvents, on as onEvent, unregisterAllEvents } from './src/core/events.js'; import { addExtensionSettings } from './src/core/settingsPanel.js'; // Generation & Parsing modules @@ -108,7 +108,6 @@ import { setupDiceRoller, setupSettingsPopup, updateDiceDisplay, - addDiceQuickReply, getSettingsModal, showWelcomeModalIfNeeded, showDeprecationModalIfNeeded @@ -187,7 +186,7 @@ import { } from './src/systems/integration/sillytavern.js'; // Settings UI event listeners (extracted from initUI) -import { bindSettingsListeners, updateWeatherSubOptionsVisibility } from './src/systems/ui/settingsListeners.js'; +import { bindSettingsListeners, initializeSettingsUIState } from './src/systems/ui/settingsListeners.js'; // Set up thought-based expressions refresh handler setThoughtBasedExpressionsRefreshHandler(() => { @@ -207,6 +206,71 @@ function updateDynamicLabels() { updateMobileTabLabels(); } +/** + * Registers all SillyTavern event handlers for the extension. + * Idempotent per enable cycle: call unregisterAllEvents() before re-registering + * (e.g. when the extension is re-enabled after being disabled). + */ +function registerExtensionEvents() { + registerAllEvents({ + [event_types.MESSAGE_SENT]: onMessageSent, + [event_types.GENERATION_STARTED]: onGenerationStarted, + [event_types.MESSAGE_RECEIVED]: onMessageReceived, + [event_types.GENERATION_STOPPED]: onGenerationEnded, + [event_types.GENERATION_ENDED]: onGenerationEnded, + [event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts, clearDebugLogs], + [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 + }); + + // Use tracked event registration (onEvent) instead of direct eventSource.on() + // This ensures all handlers are tracked for cleanup by unregisterAllEvents() + onEvent(event_types.CHARACTER_MESSAGE_RENDERED, (messageId) => { + if (!extensionSettings.enabled) return; + const renderedMessage = chat[messageId]; + if (renderedMessage && !renderedMessage.is_user && !renderedMessage.is_system) { + queueThoughtBasedExpressionsUpdate(); + } + }); + + onEvent(event_types.MESSAGE_UPDATED, (messageId) => { + if (!extensionSettings.enabled) return; + const updatedMessage = chat[messageId]; + if (updatedMessage && !updatedMessage.is_user && !updatedMessage.is_system) { + queueThoughtBasedExpressionsUpdate(); + } + }); + + onEvent(event_types.MESSAGE_SWIPED, (messageIndex) => { + if (!extensionSettings.enabled) return; + const swipedMessage = chat[messageIndex]; + if (swipedMessage && !swipedMessage.is_user && !swipedMessage.is_system) { + queueThoughtBasedExpressionsUpdate({ immediate: true }); + } + }); + + onEvent(event_types.CHAT_CHANGED, () => { + clearThoughtBasedExpressionsCache(); + setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0); + }); + + onEvent(event_types.MESSAGE_DELETED, () => { + if (!extensionSettings.enabled) return; + clearThoughtBasedExpressionsCache(); + setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0); + }); + + onEvent(event_types.MESSAGE_SWIPE_DELETED, () => { + if (!extensionSettings.enabled) return; + clearThoughtBasedExpressionsCache(); + setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0); + }); +} + /** * Initializes the UI for the extension. */ @@ -252,6 +316,19 @@ async function initUI() { // Bind all settings event listeners (extracted to settingsListeners.js) bindSettingsListeners($); + // Sync settings modal inputs with saved settings, then apply startup UI state + initializeSettingsUIState(); + updatePanelVisibility(); + updateSectionVisibility(); + updateGenerationModeUI(); + applyTheme(); + applyPanelPosition(); + toggleCustomColors(); + toggleAnimations(); + updateFeatureTogglesVisibility(); + togglePlotButtons(); + initWeatherEffects(); + // Initialize mobile UI setupMobileToggle(); constrainFabToViewport(); @@ -305,7 +382,7 @@ jQuery(async () => { // Load settings with validation try { - loadSettings(); + await loadSettings(); } catch (error) { console.error('[RPG Companion] Settings load failed, continuing with defaults:', error); } @@ -329,7 +406,7 @@ jQuery(async () => { // Add extension settings to Extensions tab try { - await addExtensionSettings($, renderExtensionTemplateAsync, clearExtensionPrompts, updateChatThoughts, cleanupCheckpointUI, clearThoughtBasedExpressionsCache, toggleDynamicWeather, initUI, loadChatData, scheduleChatStateRehydration, initThoughtBasedExpressions, injectCheckpointButton, updateAllCheckpointIndicators, removeAlternatePresentCharactersPanel); + await addExtensionSettings($, renderExtensionTemplateAsync, clearExtensionPrompts, updateChatThoughts, cleanupCheckpointUI, clearThoughtBasedExpressionsCache, toggleDynamicWeather, initUI, loadChatData, scheduleChatStateRehydration, initThoughtBasedExpressions, injectCheckpointButton, updateAllCheckpointIndicators, removeAlternatePresentCharactersPanel, registerExtensionEvents, unregisterAllEvents, initHistoryInjection); } catch (error) { console.error('[RPG Companion] Failed to add extension settings tab:', error); } @@ -373,63 +450,7 @@ jQuery(async () => { // Register all event listeners try { - registerAllEvents({ - [event_types.MESSAGE_SENT]: onMessageSent, - [event_types.GENERATION_STARTED]: onGenerationStarted, - [event_types.MESSAGE_RECEIVED]: onMessageReceived, - [event_types.GENERATION_STOPPED]: onGenerationEnded, - [event_types.GENERATION_ENDED]: onGenerationEnded, - [event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts, clearDebugLogs], - [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 - }); - - // Use tracked event registration (onEvent) instead of direct eventSource.on() - // This ensures all handlers are tracked for cleanup by unregisterAllEvents() - onEvent(event_types.CHARACTER_MESSAGE_RENDERED, (messageId) => { - if (!extensionSettings.enabled) return; - const renderedMessage = chat[messageId]; - if (renderedMessage && !renderedMessage.is_user && !renderedMessage.is_system) { - queueThoughtBasedExpressionsUpdate(); - } - }); - - onEvent(event_types.MESSAGE_UPDATED, (messageId) => { - if (!extensionSettings.enabled) return; - const updatedMessage = chat[messageId]; - if (updatedMessage && !updatedMessage.is_user && !updatedMessage.is_system) { - queueThoughtBasedExpressionsUpdate(); - } - }); - - onEvent(event_types.MESSAGE_SWIPED, (messageIndex) => { - if (!extensionSettings.enabled) return; - const swipedMessage = chat[messageIndex]; - if (swipedMessage && !swipedMessage.is_user && !swipedMessage.is_system) { - queueThoughtBasedExpressionsUpdate({ immediate: true }); - } - }); - - onEvent(event_types.CHAT_CHANGED, () => { - clearThoughtBasedExpressionsCache(); - setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0); - }); - - onEvent(event_types.MESSAGE_DELETED, () => { - if (!extensionSettings.enabled) return; - clearThoughtBasedExpressionsCache(); - setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0); - }); - - onEvent(event_types.MESSAGE_SWIPE_DELETED, () => { - if (!extensionSettings.enabled) return; - clearThoughtBasedExpressionsCache(); - setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0); - }); + registerExtensionEvents(); } catch (error) { console.error('[RPG Companion] Event registration failed:', error); throw error; diff --git a/settings.html b/settings.html index 3af9d0b..f338f41 100644 --- a/settings.html +++ b/settings.html @@ -58,7 +58,7 @@
- v3.7.2 + v3.7.4
diff --git a/src/core/config.js b/src/core/config.js index 8a008d9..4eaab98 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -23,8 +23,9 @@ export const extensionFolderPath = isUserExtension * Imported by state.js to initialize extensionSettings. */ export const defaultSettings = { - settingsVersion: 6, // Version number for settings migrations + settingsVersion: 7, // Version number for settings migrations enabled: true, + debugMode: false, // Enable verbose debug logging (developer flag) autoUpdate: false, updateDepth: 4, // How many messages to include in the context generationMode: 'together', // 'separate' or 'together' - whether to generate with main response or separately @@ -37,6 +38,7 @@ export const defaultSettings = { showInventory: true, // Show inventory section (v2 system) showEquipment: true, // Show equipment section 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' narratorMode: false, // Use character card as narrator instead of fixed character references @@ -315,7 +317,7 @@ export const defaultSettings = { // External API settings for 'external' generation mode externalApiSettings: { baseUrl: '', // OpenAI-compatible API base URL (e.g., "https://api.openai.com/v1") - // apiKey is NOT stored here for security. It is stored in localStorage('rpg_companion_api_key') + // apiKey is NOT stored here for security. It is stored in localStorage('rpg_companion_external_api_key') model: '', // Model identifier (e.g., "gpt-4o-mini") maxTokens: 8192, // Maximum tokens for generation temperature: 0.7 // Temperature setting for generation diff --git a/src/core/events.js b/src/core/events.js index fbb9742..e761d2b 100644 --- a/src/core/events.js +++ b/src/core/events.js @@ -42,6 +42,18 @@ export function once(eventType, handler) { */ export function off(eventType, handler) { eventSource.off(eventType, handler); + + // Remove from tracking so unregisterAllEvents() doesn't hold a stale reference + const handlers = registeredHandlers.get(eventType); + if (handlers) { + const index = handlers.indexOf(handler); + if (index !== -1) { + handlers.splice(index, 1); + } + if (handlers.length === 0) { + registeredHandlers.delete(eventType); + } + } } /** diff --git a/src/core/persistence.js b/src/core/persistence.js index d6f6e82..87bd141 100644 --- a/src/core/persistence.js +++ b/src/core/persistence.js @@ -559,7 +559,7 @@ function validateSettings(settings) { * Loads the extension settings from the global settings object. * Automatically migrates v1 inventory to v2 format if needed. */ -export function loadSettings() { +export async function loadSettings() { try { const context = getContext(); const extension_settings = context.extension_settings || context.extensionSettings; @@ -601,7 +601,7 @@ export function loadSettings() { // Migration to version 3: Convert text trackers to JSON format if (currentVersion < 3) { // console.log('[RPG Companion] Migrating settings to version 3 (JSON tracker format)'); - migrateToV3JSON(); + await migrateToV3JSON(); extensionSettings.settingsVersion = 3; settingsChanged = true; } diff --git a/src/core/settingsPanel.js b/src/core/settingsPanel.js index 6408b75..f18be6f 100644 --- a/src/core/settingsPanel.js +++ b/src/core/settingsPanel.js @@ -25,8 +25,11 @@ import { i18n } from './i18n.js'; * @param {Function} injectCheckpointButton - Add checkpoint buttons * @param {Function} updateAllCheckpointIndicators - Update checkpoint button states * @param {Function} removeAlternatePresentCharactersPanel - Remove alt characters panel + * @param {Function} registerExtensionEvents - (Re)register all SillyTavern event handlers + * @param {Function} unregisterAllEvents - Unregister all tracked SillyTavern event handlers + * @param {Function} initHistoryInjection - (Re)register history injection listeners */ -export async function addExtensionSettings($, renderExtensionTemplateAsync, clearExtensionPrompts, updateChatThoughts, cleanupCheckpointUI, clearThoughtBasedExpressionsCache, toggleDynamicWeather, initUI, loadChatData, scheduleChatStateRehydration, initThoughtBasedExpressions, injectCheckpointButton, updateAllCheckpointIndicators, removeAlternatePresentCharactersPanel) { +export async function addExtensionSettings($, renderExtensionTemplateAsync, clearExtensionPrompts, updateChatThoughts, cleanupCheckpointUI, clearThoughtBasedExpressionsCache, toggleDynamicWeather, initUI, loadChatData, scheduleChatStateRehydration, initThoughtBasedExpressions, injectCheckpointButton, updateAllCheckpointIndicators, removeAlternatePresentCharactersPanel, registerExtensionEvents, unregisterAllEvents, initHistoryInjection) { const settingsHtml = await renderExtensionTemplateAsync(extensionName, 'settings'); $('#extensions_settings2').append(settingsHtml); @@ -43,12 +46,18 @@ export async function addExtensionSettings($, renderExtensionTemplateAsync, clea clearThoughtBasedExpressionsCache(); toggleDynamicWeather(false); + // Unregister all tracked event handlers to avoid leaks while disabled + unregisterAllEvents(); + $('#rpg-companion-panel').remove(); $('#rpg-mobile-toggle').remove(); $('#rpg-collapse-toggle').remove(); $('#rpg-plot-buttons').remove(); removeAlternatePresentCharactersPanel(); } else if (extensionSettings.enabled && !wasEnabled) { + // Re-register event handlers that were unregistered on disable + registerExtensionEvents(); + initHistoryInjection(); await initUI(); loadChatData(); scheduleChatStateRehydration(); diff --git a/src/core/state.js b/src/core/state.js index ac687d2..9397967 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -287,36 +287,4 @@ export function clearDebugLogs() { debugLogs.length = 0; } -/** - * Tracks jQuery delegated event handlers for cleanup. - * Each handler is stored with its namespace for targeted unbinding. - */ -const jqueryEventHandlers = []; -/** - * Register a jQuery delegated event handler for tracking. - * @param {string} event - Event type with optional namespace (e.g., 'click.rpgCompanion') - * @param {string} selector - Delegated selector - * @param {Function} handler - Event handler function - * @returns {Function} The same handler for chaining - */ -export function trackJQueryHandler(event, selector, handler) { - jqueryEventHandlers.push({ event, selector, handler }); - return handler; -} - -/** - * Unbinds all tracked jQuery delegated event handlers. - * Call this when the extension is disabled or the panel is destroyed. - * @param {Function} $ - jQuery function (passed from caller) - */ -export function cleanupJQueryEvents($) { - for (const { event, selector, handler } of jqueryEventHandlers) { - if (handler) { - $(document).off(event, selector, handler); - } else { - $(document).off(event, selector); - } - } - jqueryEventHandlers.length = 0; -} diff --git a/src/i18n/validator.js b/src/i18n/validator.js index 47dcace..66072a1 100644 --- a/src/i18n/validator.js +++ b/src/i18n/validator.js @@ -1,7 +1,10 @@ -const fs = require('fs-extra'); -const path = require('path'); -const chokidar = require('chokidar'); -const glob = require('glob'); +import fs from 'fs-extra'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import chokidar from 'chokidar'; +import { globSync } from 'glob'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const COMPILED_DIR = __dirname // path.join(__dirname, 'compiled'); @@ -11,7 +14,7 @@ function findUnlocalizedText() { console.log(`\nšŸ”Ž Scanning for unlocalized text in ${srcDir}...`); - const files = glob.sync(`${srcDir}/**/*.{html,js,jsx}`, { + const files = globSync(`${srcDir}/**/*.{html,js,jsx}`, { ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'] }); @@ -101,7 +104,7 @@ function validateTranslations() { // Get all keys from reference locale const referenceKeys = Object.keys(translations[referenceLocale]); - console.log(`šŸ”¢ Reference locale has ${referenceKeys.size} unique keys`); + console.log(`šŸ”¢ Reference locale has ${referenceKeys.length} unique keys`); // Track statistics const stats = { @@ -127,7 +130,7 @@ function validateTranslations() { // Check for missing keys for (const key of referenceKeys) { - if (!key in translations[locale]) { + if (!(key in translations[locale])) { stats.missingKeys[locale].push(key); } else { // Check for type mismatches @@ -146,7 +149,7 @@ function validateTranslations() { // Check for extra keys for (const key of localeKeys) { - if (!key in translations[referenceLocale]) { + if (!(key in translations[referenceLocale])) { stats.extraKeys[locale].push(key); } } diff --git a/src/systems/equipment/constants.js b/src/systems/equipment/constants.js index ab514f9..09db771 100644 --- a/src/systems/equipment/constants.js +++ b/src/systems/equipment/constants.js @@ -35,7 +35,10 @@ export const SLOTS_LIST = Object.entries(EQUIPMENT_CATEGORIES).flatMap(([type, d */ export function escapeHtml(text) { if (!text) return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; + return String(text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); } diff --git a/src/systems/features/dice.js b/src/systems/features/dice.js index 0f8bbb9..b6286af 100644 --- a/src/systems/features/dice.js +++ b/src/systems/features/dice.js @@ -115,13 +115,4 @@ export function clearDiceRoll() { updateDiceDisplay(); } -/** - * Adds the Roll Dice quick reply button. - */ -export function addDiceQuickReply() { - // Create quick reply button if Quick Replies exist - if (window.quickReplyApi) { - // Quick Reply API integration would go here - // For now, the dice display in the sidebar serves as the button - } -} + diff --git a/src/systems/generation/encounterPrompts.js b/src/systems/generation/encounterPrompts.js index e858e3f..762a517 100644 --- a/src/systems/generation/encounterPrompts.js +++ b/src/systems/generation/encounterPrompts.js @@ -11,7 +11,6 @@ import { currentEncounter } from '../features/encounterState.js'; import { repairJSON } from '../../utils/jsonRepair.js'; import { isPresentCharactersEnabled } from '../../utils/presentCharacters.js'; import { buildInventorySummary, generateTrackerInstructions, generateTrackerExample } from './promptBuilder.js'; -import { applyLocks } from './lockManager.js'; /** * Gets character information from the current chat diff --git a/src/systems/generation/injector.js b/src/systems/generation/injector.js index 470b63a..f9ca6f0 100644 --- a/src/systems/generation/injector.js +++ b/src/systems/generation/injector.js @@ -39,10 +39,6 @@ let currentSuppressionState = false; // Type imports /** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */ -// Track the latest user message we committed for to prevent duplicate commits -// when GENERATION_STARTED can fire multiple times for the same turn. -let lastCommittedUserMessageSignature = null; - // Store context map for prompt injection (used by event handlers) let pendingContextMap = new Map(); diff --git a/src/systems/generation/jsonPromptHelpers.js b/src/systems/generation/jsonPromptHelpers.js index c7a7107..78fbc20 100644 --- a/src/systems/generation/jsonPromptHelpers.js +++ b/src/systems/generation/jsonPromptHelpers.js @@ -3,8 +3,7 @@ * Helper functions for building JSON format tracker prompts */ -import { extensionSettings, committedTrackerData } from '../../core/state.js'; -import { getContext } from '../../../../../../extensions.js'; +import { extensionSettings } from '../../core/state.js'; import { getWeatherKeywordsAsPromptString } from '../ui/weatherEffects.js'; import { i18n } from '../../core/i18n.js'; @@ -39,7 +38,6 @@ function toFieldKey(name) { * @returns {string} JSON format instruction for user stats */ export function buildUserStatsJSONInstruction() { - const userName = getContext().name1; const trackerConfig = extensionSettings.trackerConfig; const userStatsConfig = trackerConfig?.userStats; const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || []; @@ -184,7 +182,6 @@ export function buildInfoBoxJSONInstruction() { * @returns {string} JSON format instruction for present characters */ export function buildCharactersJSONInstruction() { - const userName = getContext().name1; const presentCharsConfig = extensionSettings.trackerConfig?.presentCharacters; const enabledFields = presentCharsConfig?.customFields?.filter(f => f && f.enabled && f.name) || []; const relationshipsEnabled = presentCharsConfig?.relationships?.enabled !== false; diff --git a/src/systems/generation/promptBuilder.js b/src/systems/generation/promptBuilder.js index b062656..b89e226 100644 --- a/src/systems/generation/promptBuilder.js +++ b/src/systems/generation/promptBuilder.js @@ -4,8 +4,8 @@ */ import { getContext } from '../../../../../../extensions.js'; -import { chat, getCurrentChatDetails, characters, this_chid } from '../../../../../../../script.js'; -import { selected_group, getGroupMembers, getGroupChat, groups } from '../../../../../../group-chats.js'; +import { chat, characters, this_chid } from '../../../../../../../script.js'; +import { selected_group, getGroupMembers, groups } from '../../../../../../group-chats.js'; import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../core/state.js'; import { buildUserStatsJSONInstruction, @@ -1246,7 +1246,6 @@ export async function generateSeparateUpdatePrompt() { // /hide command automatically handles checkpoint filtering // Add chat history as separate user/assistant messages with per-message historical context const recentMessages = chat.slice(-depth); - const startIndex = chat.length - depth; const position = historyPersistence?.injectionPosition || 'assistant_message_end'; // Build a map of which messages should get context based on position setting diff --git a/src/systems/generation/suppression.js b/src/systems/generation/suppression.js index aa0313b..0a14557 100644 --- a/src/systems/generation/suppression.js +++ b/src/systems/generation/suppression.js @@ -26,7 +26,7 @@ export function evaluateSuppression(extensionSettings, context, data) { let instructContent = ''; if (instructObj) { if (typeof instructObj === 'object') { - instructContent = String(instructObj.value || instructObj || ''); + instructContent = String(instructObj.value || ''); } else { instructContent = String(instructObj); } diff --git a/src/systems/integration/sillytavern.js b/src/systems/integration/sillytavern.js index 20f7df2..aa819ec 100644 --- a/src/systems/integration/sillytavern.js +++ b/src/systems/integration/sillytavern.js @@ -4,7 +4,7 @@ */ import { getContext } from '../../../../../../extensions.js'; -import { chat, chat_metadata, user_avatar, setExtensionPrompt, extension_prompt_types } from '../../../../../../../script.js'; +import { chat, chat_metadata, user_avatar, setExtensionPrompt, extension_prompt_types, updateMessageBlock } from '../../../../../../../script.js'; // Core modules import { diff --git a/src/systems/interaction/inventoryActions.js b/src/systems/interaction/inventoryActions.js index 71e75c8..8350736 100644 --- a/src/systems/interaction/inventoryActions.js +++ b/src/systems/interaction/inventoryActions.js @@ -5,7 +5,6 @@ import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js'; import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js'; -import { buildInventorySummary } from '../generation/promptBuilder.js'; import { buildUserStatsText } from '../rendering/userStats.js'; import { renderInventory, getLocationId } from '../rendering/inventory.js'; import { parseItems, serializeItems } from '../../utils/itemParser.js'; diff --git a/src/systems/rendering/infoBox.js b/src/systems/rendering/infoBox.js index 89bbd42..d090ae9 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, setMessageSwipeTrackerField } from '../../core/persistence.js'; +import { saveChatData, saveSettings, setMessageSwipeTrackerField } from '../../core/persistence.js'; import { i18n } from '../../core/i18n.js'; import { isItemLocked } from '../generation/lockManager.js'; import { repairJSON } from '../../utils/jsonRepair.js'; diff --git a/src/systems/rendering/inventory.js b/src/systems/rendering/inventory.js index 192f1c2..14a2abd 100644 --- a/src/systems/rendering/inventory.js +++ b/src/systems/rendering/inventory.js @@ -645,7 +645,10 @@ export function renderInventory() { */ function escapeHtml(text) { if (!text) return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; + return String(text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); } diff --git a/src/systems/rendering/quests.js b/src/systems/rendering/quests.js index 4a30ceb..aa83d6f 100644 --- a/src/systems/rendering/quests.js +++ b/src/systems/rendering/quests.js @@ -57,9 +57,13 @@ function getLockIconHtml(tracker, path) { * @returns {string} Escaped HTML */ function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; + if (!text) return ''; + return String(text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); } /** diff --git a/src/systems/rendering/renderUtils.js b/src/systems/rendering/renderUtils.js index 9a3623d..9b572c9 100644 --- a/src/systems/rendering/renderUtils.js +++ b/src/systems/rendering/renderUtils.js @@ -131,21 +131,6 @@ export function clearAllCaches() { htmlCache.clear(); } -/** - * Wraps an existing render function with change detection. - * The wrapped function generates HTML, compares it to the cache, - * and only updates the DOM if content changed. - * - * @param {Function} renderFn - Function that returns { $container, html } or calls updateIfChanged internally - * @param {string} cacheKey - Cache key for this render target - * @returns {Function} Wrapped render function - */ -export function withChangeDetection(renderFn, cacheKey) { - return function () { - return renderFn(); - }; -} - /** * Batches multiple DOM updates into a single reflow by using * document fragment pattern. Reduces layout thrashing. diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index b0b2398..778df3c 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -1301,30 +1301,6 @@ export function updateCharacterField(characterName, field, value) { // Note: Don't call renderThoughts() here - it would overwrite the user's edits } -/** - * Renders only the sidebar thoughts panel without updating chat bubbles - */ -function renderThoughtsSidebarOnly() { - if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) { - return; - } - - // This is a simplified version that only updates the sidebar - // Copy the rendering logic from renderThoughts but skip the updateChatThoughts call - const thoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts; - if (!thoughtsData) { - $thoughtsContainer.html('
' + (i18n.getTranslation('thoughts.empty') || 'No character data generated yet') + '
'); - return; - } - - // Re-render sidebar content (this would be the full logic from renderThoughts) - // For now, just call renderThoughts but set a flag - const originalShowInChat = extensionSettings.showThoughtsInChat; - extensionSettings.showThoughtsInChat = false; - renderThoughts(); - extensionSettings.showThoughtsInChat = originalShowInChat; -} - /** * Updates or removes thoughts shown in chat. * Renders either the original corner bubbles or inline dropdown cards. diff --git a/src/systems/rendering/userStats.js b/src/systems/rendering/userStats.js index 07ea2eb..3613380 100644 --- a/src/systems/rendering/userStats.js +++ b/src/systems/rendering/userStats.js @@ -143,7 +143,7 @@ function updateUserStatsData() { if (!itemString) return []; const items = itemString.split(',').map(s => s.trim()).filter(s => s); return items.map(item => { - const qtyMatch = item.match(/^(\\d+)x\\s+(.+)$/); + const qtyMatch = item.match(/^(\d+)x\s+(.+)$/); if (qtyMatch) { return { name: qtyMatch[2].trim(), quantity: parseInt(qtyMatch[1]) }; } diff --git a/src/systems/ui/checkpointUI.js b/src/systems/ui/checkpointUI.js index 95f0cc5..121516e 100644 --- a/src/systems/ui/checkpointUI.js +++ b/src/systems/ui/checkpointUI.js @@ -339,31 +339,4 @@ function processExpandedButton(messageBlock) { } -/** - * Update the checkpoint button in an existing menu - * @param {HTMLElement} menu - The extraMesButtons or mes_buttons container - * @param {number} messageId - The message index - */ -function updateCheckpointButtonInMenu(menu, messageId) { - if (!menu) return; - // Find the checkpoint button (either dropdown or expanded) - const existingButton = menu.querySelector('.rpg-checkpoint-button, .rpg-checkpoint-button-expanded'); - if (!existingButton) return; - - const isCheckpoint = isCheckpointMessage(messageId); - - // Update icon - const icon = existingButton.querySelector('i'); - if (icon) { - icon.className = isCheckpoint ? 'fa-solid fa-bookmark' : 'fa-regular fa-bookmark'; - icon.style.color = isCheckpoint ? '#4a9eff' : ''; - } - - // Update tooltip - existingButton.title = isCheckpoint - ? 'Clear Chapter Start' - : 'Set Chapter Start — When bookmarked, this message will count as the first message in the chat history, skipping earlier ones.'; - const translationKey = isCheckpoint ? 'checkpoint.clearChapterStart' : 'checkpoint.setChapterStart'; - existingButton.setAttribute('data-i18n', translationKey); -} diff --git a/src/systems/ui/mobile.js b/src/systems/ui/mobile.js index 60188c6..5134868 100644 --- a/src/systems/ui/mobile.js +++ b/src/systems/ui/mobile.js @@ -504,7 +504,10 @@ export function setupMobileToggle() { // top: $panel.css('top'), // bottom: $panel.css('top'), // transform: $panel.css('transform'), - // visibility: $panel.css('visibility')\n // }\n // });\n setupMobileTabs(); + // visibility: $panel.css('visibility') + // } + // }); + setupMobileTabs(); // Set initial icon for mobile updateCollapseToggleIcon(); // Show mobile toggle on mobile viewport @@ -861,437 +864,6 @@ export function setupContentEditableScrolling() { }); } -/** - * Sets up the mobile refresh button with drag functionality. - * Same pattern as mobile toggle button. - * Tap = refresh, drag = reposition - */ -export function setupRefreshButtonDrag() { - const $refreshBtn = $('#rpg-manual-update-mobile'); - - if ($refreshBtn.length === 0) { - console.warn('[RPG Mobile] Refresh button not found in DOM'); - return; - } - - // console.log('[RPG Mobile] setupRefreshButtonDrag called'); - - // Load and apply saved position - if (extensionSettings.mobileRefreshPosition) { - const pos = extensionSettings.mobileRefreshPosition; - // console.log('[RPG Mobile] Loading saved refresh button position:', pos); - - // Apply saved position - if (pos.top) $refreshBtn.css('top', pos.top); - if (pos.right) $refreshBtn.css('right', pos.right); - if (pos.bottom) $refreshBtn.css('bottom', pos.bottom); - if (pos.left) $refreshBtn.css('left', pos.left); - - // Constrain to viewport after position is applied - requestAnimationFrame(() => constrainFabToViewport($refreshBtn)); - } - - // Touch/drag state - let isDragging = false; - let touchStartTime = 0; - let touchStartX = 0; - let touchStartY = 0; - let buttonStartX = 0; - let buttonStartY = 0; - const LONG_PRESS_DURATION = 200; - const MOVE_THRESHOLD = 10; - let rafId = null; - let pendingX = null; - let pendingY = null; - - // Update position using requestAnimationFrame - function updatePosition() { - if (pendingX !== null && pendingY !== null) { - $refreshBtn.css({ - left: pendingX + 'px', - top: pendingY + 'px', - right: 'auto', - bottom: 'auto' - }); - pendingX = null; - pendingY = null; - } - rafId = null; - } - - // Touch start - $refreshBtn.on('touchstart', function(e) { - const touch = e.originalEvent.touches[0]; - touchStartTime = Date.now(); - touchStartX = touch.clientX; - touchStartY = touch.clientY; - - const offset = $refreshBtn.offset(); - buttonStartX = offset.left; - buttonStartY = offset.top; - - isDragging = false; - }); - - // Touch move - $refreshBtn.on('touchmove', function(e) { - const touch = e.originalEvent.touches[0]; - const deltaX = touch.clientX - touchStartX; - const deltaY = touch.clientY - touchStartY; - const timeSinceStart = Date.now() - touchStartTime; - const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - - if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { - isDragging = true; - $refreshBtn.addClass('dragging'); - } - - if (isDragging) { - e.preventDefault(); - - let newX = buttonStartX + deltaX; - let newY = buttonStartY + deltaY; - - const buttonWidth = $refreshBtn.outerWidth(); - const buttonHeight = $refreshBtn.outerHeight(); - - const minX = 10; - const maxX = window.innerWidth - buttonWidth - 10; - const minY = 10; - const maxY = window.innerHeight - buttonHeight - 10; - - newX = Math.max(minX, Math.min(maxX, newX)); - newY = Math.max(minY, Math.min(maxY, newY)); - - pendingX = newX; - pendingY = newY; - if (!rafId) { - rafId = requestAnimationFrame(updatePosition); - } - } - }); - - // Touch end - $refreshBtn.on('touchend', function(e) { - if (isDragging) { - // Save new position - const offset = $refreshBtn.offset(); - const newPosition = { - left: offset.left + 'px', - top: offset.top + 'px' - }; - - extensionSettings.mobileRefreshPosition = newPosition; - saveSettings(); - - setTimeout(() => { - $refreshBtn.removeClass('dragging'); - }, 50); - - // Set flag to prevent click handler from firing - $refreshBtn.data('just-dragged', true); - setTimeout(() => { - $refreshBtn.data('just-dragged', false); - }, 100); - - isDragging = false; - } - }); - - // Mouse support for desktop - let mouseDown = false; - - $refreshBtn.on('mousedown', function(e) { - e.preventDefault(); - touchStartTime = Date.now(); - touchStartX = e.clientX; - touchStartY = e.clientY; - - const offset = $refreshBtn.offset(); - buttonStartX = offset.left; - buttonStartY = offset.top; - - mouseDown = true; - isDragging = false; - }); - - $(document).on('mousemove', function(e) { - if (!mouseDown) return; - - const deltaX = e.clientX - touchStartX; - const deltaY = e.clientY - touchStartY; - const timeSinceStart = Date.now() - touchStartTime; - const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - - if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { - isDragging = true; - $refreshBtn.addClass('dragging'); - } - - if (isDragging) { - let newX = buttonStartX + deltaX; - let newY = buttonStartY + deltaY; - - const buttonWidth = $refreshBtn.outerWidth(); - const buttonHeight = $refreshBtn.outerHeight(); - - const minX = 10; - const maxX = window.innerWidth - buttonWidth - 10; - const minY = 10; - const maxY = window.innerHeight - buttonHeight - 10; - - newX = Math.max(minX, Math.min(maxX, newX)); - newY = Math.max(minY, Math.min(maxY, newY)); - - pendingX = newX; - pendingY = newY; - if (!rafId) { - rafId = requestAnimationFrame(updatePosition); - } - } - }); - - $(document).on('mouseup', function(e) { - if (mouseDown && isDragging) { - const offset = $refreshBtn.offset(); - const newPosition = { - left: offset.left + 'px', - top: offset.top + 'px' - }; - - extensionSettings.mobileRefreshPosition = newPosition; - saveSettings(); - - setTimeout(() => { - $refreshBtn.removeClass('dragging'); - }, 50); - - $refreshBtn.data('just-dragged', true); - setTimeout(() => { - $refreshBtn.data('just-dragged', false); - }, 100); - } - - mouseDown = false; - isDragging = false; - }); -} - -/** - * Sets up drag functionality for the debug toggle FAB button - * Same pattern as refresh button drag - */ -export function setupDebugButtonDrag() { - const $debugBtn = $('#rpg-debug-toggle'); - - if ($debugBtn.length === 0) { - console.warn('[RPG Mobile] Debug button not found in DOM'); - return; - } - - // console.log('[RPG Mobile] setupDebugButtonDrag called'); - - // Load and apply saved position - if (extensionSettings.debugFabPosition) { - const pos = extensionSettings.debugFabPosition; - // console.log('[RPG Mobile] Loading saved debug button position:', pos); - - // Apply saved position - if (pos.top) $debugBtn.css('top', pos.top); - if (pos.right) $debugBtn.css('right', pos.right); - if (pos.bottom) $debugBtn.css('bottom', pos.bottom); - if (pos.left) $debugBtn.css('left', pos.left); - - // Constrain to viewport after position is applied - requestAnimationFrame(() => constrainFabToViewport($debugBtn)); - } - - // Touch/drag state - let isDragging = false; - let touchStartTime = 0; - let touchStartX = 0; - let touchStartY = 0; - let buttonStartX = 0; - let buttonStartY = 0; - const LONG_PRESS_DURATION = 200; - const MOVE_THRESHOLD = 10; - let rafId = null; - let pendingX = null; - let pendingY = null; - - // Update position using requestAnimationFrame - function updatePosition() { - if (pendingX !== null && pendingY !== null) { - $debugBtn.css({ - left: pendingX + 'px', - top: pendingY + 'px', - right: 'auto', - bottom: 'auto' - }); - pendingX = null; - pendingY = null; - } - rafId = null; - } - - // Touch start - $debugBtn.on('touchstart', function(e) { - const touch = e.originalEvent.touches[0]; - touchStartTime = Date.now(); - touchStartX = touch.clientX; - touchStartY = touch.clientY; - - const offset = $debugBtn.offset(); - buttonStartX = offset.left; - buttonStartY = offset.top; - - isDragging = false; - }); - - // Touch move - $debugBtn.on('touchmove', function(e) { - const touch = e.originalEvent.touches[0]; - const deltaX = touch.clientX - touchStartX; - const deltaY = touch.clientY - touchStartY; - const timeSinceStart = Date.now() - touchStartTime; - const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - - if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { - isDragging = true; - $debugBtn.addClass('dragging'); - } - - if (isDragging) { - e.preventDefault(); - - let newX = buttonStartX + deltaX; - let newY = buttonStartY + deltaY; - - const buttonWidth = $debugBtn.outerWidth(); - const buttonHeight = $debugBtn.outerHeight(); - - const minX = 10; - const maxX = window.innerWidth - buttonWidth - 10; - const minY = 10; - const maxY = window.innerHeight - buttonHeight - 10; - - newX = Math.max(minX, Math.min(maxX, newX)); - newY = Math.max(minY, Math.min(maxY, newY)); - - pendingX = newX; - pendingY = newY; - if (!rafId) { - rafId = requestAnimationFrame(updatePosition); - } - } - }); - - // Touch end - $debugBtn.on('touchend', function(e) { - if (isDragging) { - // Save new position - const offset = $debugBtn.offset(); - const newPosition = { - left: offset.left + 'px', - top: offset.top + 'px' - }; - - extensionSettings.debugFabPosition = newPosition; - saveSettings(); - - setTimeout(() => { - $debugBtn.removeClass('dragging'); - }, 50); - - // Set flag to prevent click handler from firing - $debugBtn.data('just-dragged', true); - setTimeout(() => { - $debugBtn.data('just-dragged', false); - }, 100); - - isDragging = false; - } - }); - - // Mouse support for desktop - let mouseDown = false; - - $debugBtn.on('mousedown', function(e) { - e.preventDefault(); - touchStartTime = Date.now(); - touchStartX = e.clientX; - touchStartY = e.clientY; - - const offset = $debugBtn.offset(); - buttonStartX = offset.left; - buttonStartY = offset.top; - - mouseDown = true; - isDragging = false; - }); - - $(document).on('mousemove.rpgDebugDrag', function(e) { - if (!mouseDown) return; - - const deltaX = e.clientX - touchStartX; - const deltaY = e.clientY - touchStartY; - const timeSinceStart = Date.now() - touchStartTime; - const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - - if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { - isDragging = true; - $debugBtn.addClass('dragging'); - } - - if (isDragging) { - let newX = buttonStartX + deltaX; - let newY = buttonStartY + deltaY; - - const buttonWidth = $debugBtn.outerWidth(); - const buttonHeight = $debugBtn.outerHeight(); - - const minX = 10; - const maxX = window.innerWidth - buttonWidth - 10; - const minY = 10; - const maxY = window.innerHeight - buttonHeight - 10; - - newX = Math.max(minX, Math.min(maxX, newX)); - newY = Math.max(minY, Math.min(maxY, newY)); - - pendingX = newX; - pendingY = newY; - if (!rafId) { - rafId = requestAnimationFrame(updatePosition); - } - } - }); - - $(document).on('mouseup.rpgDebugDrag', function(e) { - if (mouseDown && isDragging) { - const offset = $debugBtn.offset(); - const newPosition = { - left: offset.left + 'px', - top: offset.top + 'px' - }; - - extensionSettings.debugFabPosition = newPosition; - saveSettings(); - - setTimeout(() => { - $debugBtn.removeClass('dragging'); - }, 50); - - $debugBtn.data('just-dragged', true); - setTimeout(() => { - $debugBtn.data('just-dragged', false); - }, 100); - } - - mouseDown = false; - isDragging = false; - }); -} - // ============================================ // FAB WIDGETS - Info display around FAB button // ============================================ diff --git a/src/systems/ui/modals.js b/src/systems/ui/modals.js index fe4cecf..89f3c89 100644 --- a/src/systems/ui/modals.js +++ b/src/systems/ui/modals.js @@ -26,8 +26,7 @@ import { renderEquipment } from '../rendering/equipment.js'; import { rollDice as rollDiceCore, clearDiceRoll as clearDiceRollCore, - updateDiceDisplay as updateDiceDisplayCore, - addDiceQuickReply as addDiceQuickReplyCore + updateDiceDisplay as updateDiceDisplayCore } from '../features/dice.js'; import { i18n } from '../../core/i18n.js'; @@ -578,14 +577,6 @@ export function updateDiceDisplay() { updateDiceDisplayCore(); } -/** - * Adds the Roll Dice quick reply button. - * Backwards compatible wrapper for dice module. - */ -export function addDiceQuickReply() { - addDiceQuickReplyCore(); -} - /** * Returns the SettingsModal instance for external use * @returns {SettingsModal} The global SettingsModal instance diff --git a/src/systems/ui/settingsListeners.js b/src/systems/ui/settingsListeners.js index 317fcd4..9fd161a 100644 --- a/src/systems/ui/settingsListeners.js +++ b/src/systems/ui/settingsListeners.js @@ -4,12 +4,39 @@ * Extracted from index.js initUI() to reduce main entry point size. */ -import { extensionSettings, updateExtensionSettings } from '../../core/state.js'; -import { saveSettings } from '../../core/persistence.js'; - -/* eslint-disable no-unused-vars */ -/* These functions receive jQuery-wrapped DOM elements from initUI() */ -/* eslint-disable no-unused-vars */ +import { getContext } from '../../../../../../extensions.js'; +import { extensionSettings, $musicPlayerContainer } from '../../core/state.js'; +import { saveSettings, commitTrackerDataFromPriorMessage } from '../../core/persistence.js'; +import { + applyPanelPosition, + updateGenerationModeUI, + updateSectionVisibility, + togglePlotButtons +} from './layout.js'; +import { renderThoughts, updateChatThoughts } from '../rendering/thoughts.js'; +import { renderUserStats } from '../rendering/userStats.js'; +import { renderInfoBox } from '../rendering/infoBox.js'; +import { renderInventory } from '../rendering/inventory.js'; +import { renderEquipment } from '../rendering/equipment.js'; +import { renderQuests } from '../rendering/quests.js'; +import { renderMusicPlayer } from '../rendering/musicPlayer.js'; +import { toggleDynamicWeather } from './weatherEffects.js'; +import { + applyTheme, + updateSettingsPopupTheme, + toggleCustomColors, + applyCustomTheme, + updateFeatureTogglesVisibility +} from './theme.js'; +import { + onAlternatePresentCharactersVisibilityChanged, + onThoughtBasedExpressionsSettingChanged, + onHideDefaultExpressionDisplaySettingChanged +} from '../integration/thoughtBasedExpressions.js'; +import { updateStripWidgets } from './desktop.js'; +import { updateFabWidgets } from './mobile.js'; +import { updateDiceDisplay, getSettingsModal } from './modals.js'; +import { updateRPGData, testExternalAPIConnection } from '../generation/apiClient.js'; /** * Bind all settings panel event listeners. @@ -29,6 +56,7 @@ export function bindSettingsListeners($) { extensionSettings.panelPosition = String($(this).val()); saveSettings(); applyPanelPosition(); + // Recreate thought bubbles to update their position updateChatThoughts(); }); @@ -47,11 +75,11 @@ export function bindSettingsListeners($) { }); // Section visibility toggles - bindSectionToggle($, 'rpg-toggle-user-stats', 'showUserStats', () => {}); - bindSectionToggle($, 'rpg-toggle-info-box', 'showInfoBox', () => {}); - bindSectionToggle($, 'rpg-toggle-inventory', 'showInventory', () => {}); - bindSectionToggle($, 'rpg-toggle-equipment', 'showEquipment', () => {}); - bindSectionToggle($, 'rpg-toggle-quests', 'showQuests', () => {}); + bindSectionToggle($, 'rpg-toggle-user-stats', 'showUserStats'); + bindSectionToggle($, 'rpg-toggle-info-box', 'showInfoBox'); + bindSectionToggle($, 'rpg-toggle-inventory', 'showInventory'); + bindSectionToggle($, 'rpg-toggle-equipment', 'showEquipment'); + bindSectionToggle($, 'rpg-toggle-quests', 'showQuests'); // Thoughts section with render callback $('#rpg-toggle-thoughts').on('change', function() { @@ -160,25 +188,12 @@ export function bindSettingsListeners($) { saveSettings(); }); - // Dismiss holiday promo - $('#rpg-dismiss-promo').on('click', function() { - extensionSettings.dismissedHolidayPromo = true; - saveSettings(); - $('#rpg-holiday-promo').fadeOut(300); - }); - // Skip injections for guided mode $('#rpg-skip-guided-mode').on('change', function() { extensionSettings.skipInjectionsForGuided = String($(this).val()); saveSettings(); }); - // Save tracker history - $('#rpg-save-tracker-history').on('change', function() { - extensionSettings.saveTrackerHistory = $(this).prop('checked'); - saveSettings(); - }); - // Randomized plot $('#rpg-toggle-randomized-plot').on('change', function() { extensionSettings.enableRandomizedPlot = $(this).prop('checked'); @@ -200,7 +215,7 @@ export function bindSettingsListeners($) { } extensionSettings.encounterSettings.enabled = $(this).prop('checked'); saveSettings(); - togglePlotButtons(); + togglePlotButtons(); // This also controls encounter button visibility }); // Encounter history depth @@ -222,171 +237,642 @@ export function bindSettingsListeners($) { saveSettings(); }); - // Combat narrative style settings - bindCombatNarrativeSetting($, 'rpg-combat-tense', 'tense'); - bindCombatNarrativeSetting($, 'rpg-combat-person', 'person'); - bindCombatNarrativeSetting($, 'rpg-combat-narration', 'narration'); - bindCombatNarrativeSetting($, 'rpg-combat-pov', 'pov'); - - // Summary narrative style settings - bindSummaryNarrativeSetting($, 'rpg-summary-tense', 'tense'); - bindSummaryNarrativeSetting($, 'rpg-summary-person', 'person'); - bindSummaryNarrativeSetting($, 'rpg-summary-narration', 'narration'); - bindSummaryNarrativeSetting($, 'rpg-summary-pov', 'pov'); - - // Theme selector - $('#rpg-theme-select').on('change', function() { - extensionSettings.theme = String($(this).val()); - saveSettings(); - applyTheme(); - updateSettingsPopupTheme(); - }); - - // Custom theme toggle - $('#rpg-toggle-custom-theme').on('change', function() { - extensionSettings.enableCustomTheme = $(this).prop('checked'); - saveSettings(); - toggleCustomColors(); - }); - - // Custom theme color pickers - $('#rpg-custom-bg-color').on('input', function() { - extensionSettings.customBgColor = $(this).val(); - saveSettings(); - applyCustomTheme(); - }); - $('#rpg-custom-accent-color').on('input', function() { - extensionSettings.customAccentColor = $(this).val(); - saveSettings(); - applyCustomTheme(); - }); - $('#rpg-custom-text-color').on('input', function() { - extensionSettings.customTextColor = $(this).val(); - saveSettings(); - applyCustomTheme(); - }); - $('#rpg-custom-highlight-color').on('input', function() { - extensionSettings.customHighlightColor = $(this).val(); - saveSettings(); - applyCustomTheme(); - }); - $('#rpg-custom-border-color').on('input', function() { - extensionSettings.customBorderColor = $(this).val(); - saveSettings(); - applyCustomTheme(); - }); - - // Custom reset button - $('#rpg-reset-custom-theme').on('click', function() { - extensionSettings.customBgColor = ''; - extensionSettings.customAccentColor = ''; - extensionSettings.customTextColor = ''; - extensionSettings.customHighlightColor = ''; - extensionSettings.customBorderColor = ''; - saveSettings(); - applyTheme(); - updateSettingsPopupTheme(); - }); - - // Animations toggle - $('#rpg-toggle-animations').on('change', function() { - extensionSettings.enableAnimations = $(this).prop('checked'); - saveSettings(); - toggleAnimations(); - }); - - // Panel width - $('#rpg-panel-width').on('change', function() { - extensionSettings.panelWidth = $(this).val(); - saveSettings(); - updatePanelVisibility(); - }); - - // Feature toggles visibility - $('#rpg-toggle-feature-toggles').on('change', function() { - extensionSettings.showFeatureToggles = $(this).prop('checked'); + // Feature toggle visibility controls + $('#rpg-toggle-show-html-toggle').on('change', function() { + extensionSettings.showHtmlToggle = $(this).prop('checked'); saveSettings(); updateFeatureTogglesVisibility(); }); - // Weather sub-options visibility toggle - $('#rpg-toggle-dynamic-weather-suboptions').on('change', function() { - extensionSettings.showDynamicWeatherToggle = $(this).prop('checked'); + $('#rpg-toggle-show-dialogue-coloring-toggle').on('change', function() { + extensionSettings.showDialogueColoringToggle = $(this).prop('checked'); saveSettings(); + updateFeatureTogglesVisibility(); + }); + + $('#rpg-toggle-show-deception-toggle').on('change', function() { + extensionSettings.showDeceptionToggle = $(this).prop('checked'); + saveSettings(); + updateFeatureTogglesVisibility(); + }); + + $('#rpg-toggle-show-omniscience-toggle').on('change', function() { + extensionSettings.showOmniscienceToggle = $(this).prop('checked'); + saveSettings(); + updateFeatureTogglesVisibility(); + }); + + $('#rpg-toggle-show-cyoa-toggle').on('change', function() { + extensionSettings.showCYOAToggle = $(this).prop('checked'); + saveSettings(); + updateFeatureTogglesVisibility(); + }); + + $('#rpg-toggle-show-spotify-toggle').on('change', function() { + extensionSettings.showSpotifyToggle = $(this).prop('checked'); + saveSettings(); + updateFeatureTogglesVisibility(); + }); + + $('#rpg-toggle-show-dynamic-weather-toggle').on('change', function() { + extensionSettings.showDynamicWeatherToggle = $(this).prop('checked'); + // Also disable the feature when hiding the toggle + if (!extensionSettings.showDynamicWeatherToggle) { + extensionSettings.enableDynamicWeather = false; + $('#rpg-toggle-dynamic-weather').prop('checked', false); + toggleDynamicWeather(false); + } + saveSettings(); + updateFeatureTogglesVisibility(); updateWeatherSubOptionsVisibility(); }); - // Strip widgets - bindStripWidgetToggle($, 'rpg-strip-widget-weather', 'weatherIcon'); - bindStripWidgetToggle($, 'rpg-strip-widget-clock', 'clock'); - bindStripWidgetToggle($, 'rpg-strip-widget-date', 'date'); - bindStripWidgetToggle($, 'rpg-strip-widget-location', 'location'); - bindStripWidgetToggle($, 'rpg-strip-widget-stats', 'stats'); - bindStripWidgetToggle($, 'rpg-strip-widget-attributes', 'attributes'); - - // Mobile panel position - $('#rpg-mobile-position-select').on('change', function() { - extensionSettings.mobilePanelPosition = String($(this).val()); - saveSettings(); - updateMobilePanelPosition(); + // Weather sub-options (background and foreground) - radio buttons + $('#rpg-toggle-weather-background').on('change', function() { + if ($(this).prop('checked')) { + extensionSettings.weatherBackground = true; + extensionSettings.weatherForeground = false; + saveSettings(); + // Re-apply weather effect + if (extensionSettings.enableDynamicWeather) { + toggleDynamicWeather(false); + toggleDynamicWeather(true); + } + } }); + + $('#rpg-toggle-weather-foreground').on('change', function() { + if ($(this).prop('checked')) { + extensionSettings.weatherBackground = false; + extensionSettings.weatherForeground = true; + saveSettings(); + // Re-apply weather effect + if (extensionSettings.enableDynamicWeather) { + toggleDynamicWeather(false); + toggleDynamicWeather(true); + } + } + }); + + $('#rpg-toggle-show-narrator-mode').on('change', function() { + extensionSettings.showNarratorMode = $(this).prop('checked'); + // Also disable the feature when hiding the toggle + if (!extensionSettings.showNarratorMode) { + extensionSettings.narratorMode = false; + $('#rpg-toggle-narrator').prop('checked', false); + } + saveSettings(); + updateFeatureTogglesVisibility(); + }); + + $('#rpg-toggle-show-auto-avatars').on('change', function() { + extensionSettings.showAutoAvatars = $(this).prop('checked'); + // Also disable the feature when hiding the toggle + if (!extensionSettings.showAutoAvatars) { + extensionSettings.autoGenerateAvatars = false; + $('#rpg-toggle-auto-avatars-panel').prop('checked', false); + } + saveSettings(); + updateFeatureTogglesVisibility(); + }); + + // Auto avatar generation panel toggle + $('#rpg-toggle-auto-avatars-panel').on('change', function() { + extensionSettings.autoGenerateAvatars = $(this).prop('checked'); + saveSettings(); + + // Re-render thoughts to update tooltips (regenerate vs delete) + renderThoughts(); + }); + + // Dice display toggle + $('#rpg-toggle-dice-display').on('change', function() { + extensionSettings.showDiceDisplay = $(this).prop('checked'); + saveSettings(); + updateDiceDisplay(); + }); + + // Mobile FAB Widget toggles - simplified, no position saving (auto-positioned) + $('#rpg-toggle-fab-widgets-enabled').on('change', function() { + if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {}; + extensionSettings.mobileFabWidgets.enabled = $(this).prop('checked'); + saveSettings(); + updateFabWidgets(); + $('#rpg-fab-widget-options').toggle(extensionSettings.mobileFabWidgets.enabled); + }); + + $('#rpg-toggle-fab-weather-icon').on('change', function() { + if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {}; + if (!extensionSettings.mobileFabWidgets.weatherIcon) extensionSettings.mobileFabWidgets.weatherIcon = {}; + extensionSettings.mobileFabWidgets.weatherIcon.enabled = $(this).prop('checked'); + saveSettings(); + updateFabWidgets(); + }); + + $('#rpg-toggle-fab-weather-desc').on('change', function() { + if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {}; + if (!extensionSettings.mobileFabWidgets.weatherDesc) extensionSettings.mobileFabWidgets.weatherDesc = {}; + extensionSettings.mobileFabWidgets.weatherDesc.enabled = $(this).prop('checked'); + saveSettings(); + updateFabWidgets(); + }); + + $('#rpg-toggle-fab-clock').on('change', function() { + if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {}; + if (!extensionSettings.mobileFabWidgets.clock) extensionSettings.mobileFabWidgets.clock = {}; + extensionSettings.mobileFabWidgets.clock.enabled = $(this).prop('checked'); + saveSettings(); + updateFabWidgets(); + }); + + $('#rpg-toggle-fab-date').on('change', function() { + if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {}; + if (!extensionSettings.mobileFabWidgets.date) extensionSettings.mobileFabWidgets.date = {}; + extensionSettings.mobileFabWidgets.date.enabled = $(this).prop('checked'); + saveSettings(); + updateFabWidgets(); + }); + + $('#rpg-toggle-fab-location').on('change', function() { + if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {}; + if (!extensionSettings.mobileFabWidgets.location) extensionSettings.mobileFabWidgets.location = {}; + extensionSettings.mobileFabWidgets.location.enabled = $(this).prop('checked'); + saveSettings(); + updateFabWidgets(); + }); + + $('#rpg-toggle-fab-stats').on('change', function() { + if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {}; + if (!extensionSettings.mobileFabWidgets.stats) extensionSettings.mobileFabWidgets.stats = {}; + extensionSettings.mobileFabWidgets.stats.enabled = $(this).prop('checked'); + saveSettings(); + updateFabWidgets(); + }); + + $('#rpg-toggle-fab-attributes').on('change', function() { + if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {}; + if (!extensionSettings.mobileFabWidgets.attributes) extensionSettings.mobileFabWidgets.attributes = {}; + extensionSettings.mobileFabWidgets.attributes.enabled = $(this).prop('checked'); + saveSettings(); + updateFabWidgets(); + }); + + // Desktop Strip Widget toggles + $('#rpg-toggle-strip-widgets-enabled').on('change', function() { + if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {}; + extensionSettings.desktopStripWidgets.enabled = $(this).prop('checked'); + saveSettings(); + updateStripWidgets(); + $('#rpg-strip-widget-options').toggle(extensionSettings.desktopStripWidgets.enabled); + }); + + $('#rpg-toggle-strip-weather-icon').on('change', function() { + if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {}; + if (!extensionSettings.desktopStripWidgets.weatherIcon) extensionSettings.desktopStripWidgets.weatherIcon = {}; + extensionSettings.desktopStripWidgets.weatherIcon.enabled = $(this).prop('checked'); + saveSettings(); + updateStripWidgets(); + }); + + $('#rpg-toggle-strip-clock').on('change', function() { + if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {}; + if (!extensionSettings.desktopStripWidgets.clock) extensionSettings.desktopStripWidgets.clock = {}; + extensionSettings.desktopStripWidgets.clock.enabled = $(this).prop('checked'); + saveSettings(); + updateStripWidgets(); + }); + + $('#rpg-toggle-strip-date').on('change', function() { + if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {}; + if (!extensionSettings.desktopStripWidgets.date) extensionSettings.desktopStripWidgets.date = {}; + extensionSettings.desktopStripWidgets.date.enabled = $(this).prop('checked'); + saveSettings(); + updateStripWidgets(); + }); + + $('#rpg-toggle-strip-location').on('change', function() { + if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {}; + if (!extensionSettings.desktopStripWidgets.location) extensionSettings.desktopStripWidgets.location = {}; + extensionSettings.desktopStripWidgets.location.enabled = $(this).prop('checked'); + saveSettings(); + updateStripWidgets(); + }); + + $('#rpg-toggle-strip-stats').on('change', function() { + if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {}; + if (!extensionSettings.desktopStripWidgets.stats) extensionSettings.desktopStripWidgets.stats = {}; + extensionSettings.desktopStripWidgets.stats.enabled = $(this).prop('checked'); + saveSettings(); + updateStripWidgets(); + }); + + $('#rpg-toggle-strip-attributes').on('change', function() { + if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {}; + if (!extensionSettings.desktopStripWidgets.attributes) extensionSettings.desktopStripWidgets.attributes = {}; + extensionSettings.desktopStripWidgets.attributes.enabled = $(this).prop('checked'); + saveSettings(); + updateStripWidgets(); + }); + + // Manual update button + $('#rpg-manual-update').on('click', async function() { + if (!extensionSettings.enabled) { + return; + } + const currentChat = getContext().chat; + let lastAssistantIndex = -1; + for (let i = currentChat.length - 1; i >= 0; i--) { + if (!currentChat[i].is_user && !currentChat[i].is_system) { + lastAssistantIndex = i; + break; + } + } + if (lastAssistantIndex !== -1) { + commitTrackerDataFromPriorMessage(lastAssistantIndex); + } + await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory); + }); + + // Strip widget refresh button - same functionality as main refresh button + $('#rpg-strip-refresh').on('click', async function() { + if (!extensionSettings.enabled) { + return; + } + const currentChat = getContext().chat; + let lastAssistantIndex = -1; + for (let i = currentChat.length - 1; i >= 0; i--) { + if (!currentChat[i].is_user && !currentChat[i].is_system) { + lastAssistantIndex = i; + break; + } + } + if (lastAssistantIndex !== -1) { + commitTrackerDataFromPriorMessage(lastAssistantIndex); + } + await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory); + }); + + // Stat bar colors + $('#rpg-stat-bar-color-low').on('change', function() { + extensionSettings.statBarColorLow = String($(this).val()); + saveSettings(); + renderUserStats(); // Re-render with new colors + }); + + $('#rpg-stat-bar-color-low-opacity').on('input', function() { + const opacity = Number($(this).val()); + extensionSettings.statBarColorLowOpacity = opacity; + $('#rpg-stat-bar-color-low-opacity-value').text(opacity + '%'); + renderUserStats(); + }).on('change', function() { + saveSettings(); + }); + + $('#rpg-stat-bar-color-high').on('change', function() { + extensionSettings.statBarColorHigh = String($(this).val()); + saveSettings(); + renderUserStats(); // Re-render with new colors + }); + + $('#rpg-stat-bar-color-high-opacity').on('input', function() { + const opacity = Number($(this).val()); + extensionSettings.statBarColorHighOpacity = opacity; + $('#rpg-stat-bar-color-high-opacity-value').text(opacity + '%'); + renderUserStats(); + }).on('change', function() { + saveSettings(); + }); + + // Theme selection + $('#rpg-theme-select').on('change', function() { + extensionSettings.theme = String($(this).val()); + saveSettings(); + applyTheme(); + toggleCustomColors(); + updateSettingsPopupTheme(getSettingsModal()); // Update popup theme instantly + updateChatThoughts(); // Recreate thought bubbles with new theme + }); + + // Custom color pickers + $('#rpg-custom-bg').on('change', function() { + extensionSettings.customColors.bg = String($(this).val()); + saveSettings(); + if (extensionSettings.theme === 'custom') { + applyCustomTheme(); + updateSettingsPopupTheme(getSettingsModal()); + updateChatThoughts(); + } + }); + + $('#rpg-custom-bg-opacity').on('input', function() { + const opacity = Number($(this).val()); + extensionSettings.customColors.bgOpacity = opacity; + $('#rpg-custom-bg-opacity-value').text(opacity + '%'); + if (extensionSettings.theme === 'custom') { + applyCustomTheme(); + updateSettingsPopupTheme(getSettingsModal()); + updateChatThoughts(); + } + }).on('change', function() { + saveSettings(); + }); + + $('#rpg-custom-accent').on('change', function() { + extensionSettings.customColors.accent = String($(this).val()); + saveSettings(); + if (extensionSettings.theme === 'custom') { + applyCustomTheme(); + updateSettingsPopupTheme(getSettingsModal()); + updateChatThoughts(); + } + }); + + $('#rpg-custom-accent-opacity').on('input', function() { + const opacity = Number($(this).val()); + extensionSettings.customColors.accentOpacity = opacity; + $('#rpg-custom-accent-opacity-value').text(opacity + '%'); + if (extensionSettings.theme === 'custom') { + applyCustomTheme(); + updateSettingsPopupTheme(getSettingsModal()); + updateChatThoughts(); + } + }).on('change', function() { + saveSettings(); + }); + + $('#rpg-custom-text').on('change', function() { + extensionSettings.customColors.text = String($(this).val()); + saveSettings(); + if (extensionSettings.theme === 'custom') { + applyCustomTheme(); + updateSettingsPopupTheme(getSettingsModal()); + updateChatThoughts(); + } + }); + + $('#rpg-custom-text-opacity').on('input', function() { + const opacity = Number($(this).val()); + extensionSettings.customColors.textOpacity = opacity; + $('#rpg-custom-text-opacity-value').text(opacity + '%'); + if (extensionSettings.theme === 'custom') { + applyCustomTheme(); + updateSettingsPopupTheme(getSettingsModal()); + updateChatThoughts(); + } + }).on('change', function() { + saveSettings(); + }); + + $('#rpg-custom-highlight').on('change', function() { + extensionSettings.customColors.highlight = String($(this).val()); + saveSettings(); + if (extensionSettings.theme === 'custom') { + applyCustomTheme(); + updateSettingsPopupTheme(getSettingsModal()); + updateChatThoughts(); + } + }); + + $('#rpg-custom-highlight-opacity').on('input', function() { + const opacity = Number($(this).val()); + extensionSettings.customColors.highlightOpacity = opacity; + $('#rpg-custom-highlight-opacity-value').text(opacity + '%'); + if (extensionSettings.theme === 'custom') { + applyCustomTheme(); + updateSettingsPopupTheme(getSettingsModal()); + updateChatThoughts(); + } + }).on('change', function() { + saveSettings(); + }); + + // External API settings event handlers + $('#rpg-external-base-url').on('change', function() { + if (!extensionSettings.externalApiSettings) { + extensionSettings.externalApiSettings = { + baseUrl: '', apiKey: '', model: '', maxTokens: 8192, temperature: 0.7 + }; + } + extensionSettings.externalApiSettings.baseUrl = String($(this).val()).trim(); + saveSettings(); + }); + + $('#rpg-external-api-key').on('change', function() { + // Securely store API key in localStorage instead of shared extension settings + const apiKey = String($(this).val()).trim(); + localStorage.setItem('rpg_companion_external_api_key', apiKey); + + // Ensure the externalApiSettings object exists, but don't store the key in it + if (!extensionSettings.externalApiSettings) { + extensionSettings.externalApiSettings = { + baseUrl: '', model: '', maxTokens: 8192, temperature: 0.7 + }; + saveSettings(); + } + }); + + $('#rpg-external-model').on('change', function() { + if (!extensionSettings.externalApiSettings) { + extensionSettings.externalApiSettings = { + baseUrl: '', apiKey: '', model: '', maxTokens: 8192, temperature: 0.7 + }; + } + extensionSettings.externalApiSettings.model = String($(this).val()).trim(); + saveSettings(); + }); + + $('#rpg-external-max-tokens').on('change', function() { + if (!extensionSettings.externalApiSettings) { + extensionSettings.externalApiSettings = { + baseUrl: '', apiKey: '', model: '', maxTokens: 8192, temperature: 0.7 + }; + } + extensionSettings.externalApiSettings.maxTokens = parseInt(String($(this).val())); + saveSettings(); + }); + + $('#rpg-external-temperature').on('change', function() { + if (!extensionSettings.externalApiSettings) { + extensionSettings.externalApiSettings = { + baseUrl: '', apiKey: '', model: '', maxTokens: 8192, temperature: 0.7 + }; + } + extensionSettings.externalApiSettings.temperature = parseFloat(String($(this).val())); + saveSettings(); + }); + + $('#rpg-toggle-api-key-visibility').on('click', function() { + const $input = $('#rpg-external-api-key'); + const type = $input.attr('type') === 'password' ? 'text' : 'password'; + $input.attr('type', type); + $(this).find('i').toggleClass('fa-eye fa-eye-slash'); + }); + + $('#rpg-test-external-api').on('click', async function() { + const $result = $('#rpg-external-api-test-result'); + const $btn = $(this); + const originalText = $btn.html(); + + $btn.html(' Testing...').prop('disabled', true); + $result.hide().removeClass('rpg-success-message rpg-error-message'); + + try { + const result = await testExternalAPIConnection(); + + if (result.success) { + $result.addClass('rpg-success-message') + .html(` ${result.message}`) + .slideDown(); + toastr.success(result.message); + } else { + $result.addClass('rpg-error-message') + .html(` ${result.message}`) + .slideDown(); + toastr.error(result.message); + } + } catch (error) { + $result.addClass('rpg-error-message') + .html(` Error: ${error.message}`) + .slideDown(); + } finally { + $btn.html(originalText).prop('disabled', false); + } + }); +} + +/** + * Synchronize all settings modal inputs with the currently saved settings. + * Called once during UI initialization after the template is in the DOM. + */ +export function initializeSettingsUIState() { + // Initialize UI state (enable/disable is in Extensions tab) + $('#rpg-toggle-auto-update').prop('checked', extensionSettings.autoUpdate); + $('#rpg-position-select').val(extensionSettings.panelPosition); + $('#rpg-update-depth').val(extensionSettings.updateDepth); + $('#rpg-toggle-user-stats').prop('checked', extensionSettings.showUserStats); + $('#rpg-toggle-info-box').prop('checked', extensionSettings.showInfoBox); + $('#rpg-toggle-thoughts').prop('checked', extensionSettings.showCharacterThoughts); + $('#rpg-toggle-alt-present-characters').prop('checked', extensionSettings.showAlternatePresentCharactersPanel ?? false); + $('#rpg-toggle-thought-based-expressions').prop('checked', extensionSettings.enableThoughtBasedExpressions === true); + $('#rpg-toggle-hide-default-expressions').prop('checked', extensionSettings.hideDefaultExpressionDisplay === true); + $('#rpg-toggle-inventory').prop('checked', extensionSettings.showInventory); + $('#rpg-toggle-equipment').prop('checked', extensionSettings.showEquipment); + $('#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); + $('#rpg-toggle-omniscience').prop('checked', extensionSettings.enableOmniscienceFilter ?? false); + $('#rpg-toggle-cyoa').prop('checked', extensionSettings.enableCYOA ?? false); + $('#rpg-toggle-spotify-music').prop('checked', extensionSettings.enableSpotifyMusic); + + $('#rpg-toggle-dynamic-weather').prop('checked', extensionSettings.enableDynamicWeather); + $('#rpg-toggle-narrator').prop('checked', extensionSettings.narratorMode); + + // Feature toggle visibility settings + $('#rpg-toggle-show-html-toggle').prop('checked', extensionSettings.showHtmlToggle ?? true); + $('#rpg-toggle-show-dialogue-coloring-toggle').prop('checked', extensionSettings.showDialogueColoringToggle ?? true); + $('#rpg-toggle-show-deception-toggle').prop('checked', extensionSettings.showDeceptionToggle ?? true); + $('#rpg-toggle-show-omniscience-toggle').prop('checked', extensionSettings.showOmniscienceToggle ?? true); + $('#rpg-toggle-show-cyoa-toggle').prop('checked', extensionSettings.showCYOAToggle ?? true); + $('#rpg-toggle-show-spotify-toggle').prop('checked', extensionSettings.showSpotifyToggle ?? true); + $('#rpg-toggle-show-dynamic-weather-toggle').prop('checked', extensionSettings.showDynamicWeatherToggle ?? true); + $('#rpg-toggle-weather-background').prop('checked', extensionSettings.weatherBackground ?? true); + $('#rpg-toggle-weather-foreground').prop('checked', extensionSettings.weatherForeground ?? false); + $('#rpg-toggle-show-narrator-mode').prop('checked', extensionSettings.showNarratorMode ?? true); + $('#rpg-toggle-show-auto-avatars').prop('checked', extensionSettings.showAutoAvatars ?? true); + + $('#rpg-toggle-randomized-plot').prop('checked', extensionSettings.enableRandomizedPlot ?? true); + $('#rpg-toggle-natural-plot').prop('checked', extensionSettings.enableNaturalPlot ?? true); + $('#rpg-toggle-encounters').prop('checked', extensionSettings.encounterSettings?.enabled ?? true); + $('#rpg-encounter-history-depth').val(extensionSettings.encounterSettings?.historyDepth ?? 8); + $('#rpg-toggle-autosave-logs').prop('checked', extensionSettings.encounterSettings?.autoSaveLogs ?? true); + + // Initialize avatar options (panel toggle) + $('#rpg-toggle-auto-avatars-panel').prop('checked', extensionSettings.autoGenerateAvatars || false); + + $('#rpg-toggle-dice-display').prop('checked', extensionSettings.showDiceDisplay); + + // Initialize Mobile FAB Widget checkboxes + const fabWidgets = extensionSettings.mobileFabWidgets || {}; + $('#rpg-toggle-fab-widgets-enabled').prop('checked', fabWidgets.enabled || false); + $('#rpg-toggle-fab-weather-icon').prop('checked', fabWidgets.weatherIcon?.enabled || false); + $('#rpg-toggle-fab-weather-desc').prop('checked', fabWidgets.weatherDesc?.enabled || false); + $('#rpg-toggle-fab-clock').prop('checked', fabWidgets.clock?.enabled || false); + $('#rpg-toggle-fab-date').prop('checked', fabWidgets.date?.enabled || false); + $('#rpg-toggle-fab-location').prop('checked', fabWidgets.location?.enabled || false); + $('#rpg-toggle-fab-stats').prop('checked', fabWidgets.stats?.enabled || false); + $('#rpg-toggle-fab-attributes').prop('checked', fabWidgets.attributes?.enabled || false); + // Toggle visibility of widget options based on master toggle + $('#rpg-fab-widget-options').toggle(fabWidgets.enabled || false); + + // Initialize Desktop Strip Widget checkboxes + const stripWidgets = extensionSettings.desktopStripWidgets || {}; + $('#rpg-toggle-strip-widgets-enabled').prop('checked', stripWidgets.enabled || false); + $('#rpg-toggle-strip-weather-icon').prop('checked', stripWidgets.weatherIcon?.enabled ?? true); + $('#rpg-toggle-strip-clock').prop('checked', stripWidgets.clock?.enabled ?? true); + $('#rpg-toggle-strip-date').prop('checked', stripWidgets.date?.enabled ?? true); + $('#rpg-toggle-strip-location').prop('checked', stripWidgets.location?.enabled ?? true); + $('#rpg-toggle-strip-stats').prop('checked', stripWidgets.stats?.enabled ?? true); + $('#rpg-toggle-strip-attributes').prop('checked', stripWidgets.attributes?.enabled ?? true); + // Toggle visibility of strip widget options based on master toggle + $('#rpg-strip-widget-options').toggle(stripWidgets.enabled || false); + + $('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow); + $('#rpg-stat-bar-color-low-opacity').val(extensionSettings.statBarColorLowOpacity ?? 100); + $('#rpg-stat-bar-color-low-opacity-value').text((extensionSettings.statBarColorLowOpacity ?? 100) + '%'); + + $('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh); + $('#rpg-stat-bar-color-high-opacity').val(extensionSettings.statBarColorHighOpacity ?? 100); + $('#rpg-stat-bar-color-high-opacity-value').text((extensionSettings.statBarColorHighOpacity ?? 100) + '%'); + + $('#rpg-theme-select').val(extensionSettings.theme); + $('#rpg-custom-bg').val(extensionSettings.customColors.bg); + $('#rpg-custom-bg-opacity').val(extensionSettings.customColors.bgOpacity ?? 100); + $('#rpg-custom-bg-opacity-value').text((extensionSettings.customColors.bgOpacity ?? 100) + '%'); + + $('#rpg-custom-accent').val(extensionSettings.customColors.accent); + $('#rpg-custom-accent-opacity').val(extensionSettings.customColors.accentOpacity ?? 100); + $('#rpg-custom-accent-opacity-value').text((extensionSettings.customColors.accentOpacity ?? 100) + '%'); + + $('#rpg-custom-text').val(extensionSettings.customColors.text); + $('#rpg-custom-text-opacity').val(extensionSettings.customColors.textOpacity ?? 100); + $('#rpg-custom-text-opacity-value').text((extensionSettings.customColors.textOpacity ?? 100) + '%'); + + $('#rpg-custom-highlight').val(extensionSettings.customColors.highlight); + $('#rpg-custom-highlight-opacity').val(extensionSettings.customColors.highlightOpacity ?? 100); + $('#rpg-custom-highlight-opacity-value').text((extensionSettings.customColors.highlightOpacity ?? 100) + '%'); + + // Initialize External API settings values + if (extensionSettings.externalApiSettings) { + $('#rpg-external-base-url').val(extensionSettings.externalApiSettings.baseUrl || ''); + + // Load API Key from secure localStorage + const storedApiKey = localStorage.getItem('rpg_companion_external_api_key') || ''; + $('#rpg-external-api-key').val(storedApiKey); + + $('#rpg-external-model').val(extensionSettings.externalApiSettings.model || ''); + $('#rpg-external-max-tokens').val(extensionSettings.externalApiSettings.maxTokens || 8192); + $('#rpg-external-temperature').val(extensionSettings.externalApiSettings.temperature ?? 0.7); + } + + $('#rpg-generation-mode').val(extensionSettings.generationMode); + $('#rpg-skip-guided-mode').val(extensionSettings.skipInjectionsForGuided); + + // Weather sub-options visibility follows the dynamic weather toggle visibility setting + updateWeatherSubOptionsVisibility(); } /** * Helper: Bind a simple section visibility toggle */ -function bindSectionToggle($, elementId, settingKey, callback) { +function bindSectionToggle($, elementId, settingKey) { $(`#${elementId}`).on('change', function() { extensionSettings[settingKey] = $(this).prop('checked'); saveSettings(); updateSectionVisibility(); - if (callback) callback(); - }); -} - -/** - * Helper: Bind combat narrative setting - */ -function bindCombatNarrativeSetting($, elementId, key) { - $(`#${elementId}`).on('change', function() { - if (!extensionSettings.encounterSettings) { - extensionSettings.encounterSettings = {}; - } - if (!extensionSettings.encounterSettings.combatNarrative) { - extensionSettings.encounterSettings.combatNarrative = {}; - } - extensionSettings.encounterSettings.combatNarrative[key] = $(this).val(); - saveSettings(); - }); -} - -/** - * Helper: Bind summary narrative setting - */ -function bindSummaryNarrativeSetting($, elementId, key) { - $(`#${elementId}`).on('change', function() { - if (!extensionSettings.encounterSettings) { - extensionSettings.encounterSettings = {}; - } - if (!extensionSettings.encounterSettings.summaryNarrative) { - extensionSettings.encounterSettings.summaryNarrative = {}; - } - extensionSettings.encounterSettings.summaryNarrative[key] = $(this).val(); - saveSettings(); - }); -} - -/** - * Helper: Bind strip widget toggle - */ -function bindStripWidgetToggle($, elementId, widgetKey) { - $(`#${elementId}`).on('change', function() { - if (!extensionSettings.stripWidgets) { - extensionSettings.stripWidgets = { weatherIcon: true, clock: true, date: true, location: true, stats: true, attributes: true }; - } - extensionSettings.stripWidgets[widgetKey] = $(this).prop('checked'); - saveSettings(); - updateStripWidgets(); }); } @@ -402,4 +888,4 @@ export function updateWeatherSubOptionsVisibility() { } else { $weatherSubOptions.hide(); } -} +} \ No newline at end of file diff --git a/src/systems/ui/trackerEditor.js b/src/systems/ui/trackerEditor.js index adf9526..0963f31 100644 --- a/src/systems/ui/trackerEditor.js +++ b/src/systems/ui/trackerEditor.js @@ -1671,7 +1671,7 @@ function setupHistoryPersistenceListeners() { messageCount: 5, injectionPosition: 'assistant_message_end', contextPreamble: '', - externalApiOnly: false + sendAllEnabledOnRefresh: false }; } diff --git a/src/systems/ui/weatherEffects.js b/src/systems/ui/weatherEffects.js index 0f76464..57eac6a 100644 --- a/src/systems/ui/weatherEffects.js +++ b/src/systems/ui/weatherEffects.js @@ -225,7 +225,7 @@ function getCurrentWeather() { // Try to parse as JSON first (new format) try { - const parsed = typeof infoBoxData === 'string' ? JSON.parse(infoBoxData) : infoBoxData; + const parsed = typeof infoBoxData === 'string' ? repairJSON(infoBoxData) : infoBoxData; if (parsed && parsed.weather) { // Return the forecast text from the weather object return parsed.weather.forecast || parsed.weather.emoji || null; diff --git a/src/utils/responseExtractor.js b/src/utils/responseExtractor.js index 7f4308c..e9cd110 100644 --- a/src/utils/responseExtractor.js +++ b/src/utils/responseExtractor.js @@ -10,7 +10,7 @@ * intercepts the raw fetch response as a fallback. */ -import { generateRaw } from '../../../../../../../script.js'; +import { generateRaw } from '../../../../../../script.js'; /** * Extracts text from any API response shape (Anthropic content-block arrays, diff --git a/template.html b/template.html index 8af4c0e..2950083 100644 --- a/template.html +++ b/template.html @@ -1114,7 +1114,7 @@ Injected when "Enable Dialogue Coloring" is enabled. Affects all generation modes. - -- 2.54.0