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.
-