From 5c34407d2c109c9b533c304fb7acd7f8b42be149 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Fri, 17 Oct 2025 09:13:19 +1100 Subject: [PATCH] refactor(core): extract core modules (state, persistence, config, events) Extract core system modules from monolithic index.js into modular architecture: - src/core/state.js: All extension state variables with controlled setters - src/core/persistence.js: Settings and chat data persistence functions - src/core/config.js: Extension metadata and default configuration - src/core/events.js: SillyTavern event system wrapper Updated index.js to import and use new core modules. Removed ~220 lines of state/persistence code from index.js. Part of Epic 1: Foundation & Core Systems (Phase 1.1-1.2) --- index.js | 258 +++++++--------------------------------- src/core/config.js | 66 ++++++++++ src/core/events.js | 47 ++++++++ src/core/persistence.js | 140 ++++++++++++++++++++++ src/core/state.js | 168 ++++++++++++++++++++++++++ 5 files changed, 462 insertions(+), 217 deletions(-) create mode 100644 src/core/config.js create mode 100644 src/core/events.js create mode 100644 src/core/persistence.js create mode 100644 src/core/state.js diff --git a/index.js b/index.js index bb483f8..e6e4388 100644 --- a/index.js +++ b/index.js @@ -3,99 +3,41 @@ import { eventSource, event_types, substituteParams, chat, generateRaw, saveSett import { selected_group, getGroupMembers } from '../../../group-chats.js'; import { power_user } from '../../../power-user.js'; -const extensionName = 'third-party/rpg-companion-sillytavern'; +// Core modules +import { extensionName, extensionFolderPath } from './src/core/config.js'; +import { + extensionSettings, + lastGeneratedData, + committedTrackerData, + lastActionWasSwipe, + isGenerating, + isPlotProgression, + pendingDiceRoll, + FALLBACK_AVATAR_DATA_URI, + $panelContainer, + $userStatsContainer, + $infoBoxContainer, + $thoughtsContainer, + setExtensionSettings, + updateExtensionSettings, + setLastGeneratedData, + updateLastGeneratedData, + setCommittedTrackerData, + updateCommittedTrackerData, + setLastActionWasSwipe, + setIsGenerating, + setIsPlotProgression, + setPendingDiceRoll, + setPanelContainer, + setUserStatsContainer, + setInfoBoxContainer, + setThoughtsContainer +} from './src/core/state.js'; +import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData } from './src/core/persistence.js'; +import { on as eventOn, event_types as coreEventTypes } from './src/core/events.js'; -// Dynamically determine extension path based on current location -// This supports both global (public/extensions) and user-specific (data/default-user/extensions) installations -const currentScriptPath = import.meta.url; -const isUserExtension = currentScriptPath.includes('/data/') || currentScriptPath.includes('\\data\\'); -const extensionFolderPath = isUserExtension - ? `data/default-user/extensions/${extensionName}` - : `scripts/extensions/${extensionName}`; - -let extensionSettings = { - enabled: true, - autoUpdate: true, - updateDepth: 4, // How many messages to include in the context - generationMode: 'together', // 'separate' or 'together' - whether to generate with main response or separately - showUserStats: true, - showInfoBox: true, - showCharacterThoughts: true, - showThoughtsInChat: true, // Show thoughts overlay in chat - enableHtmlPrompt: false, // Enable immersive HTML prompt injection - enablePlotButtons: true, // Show plot progression buttons above chat input - panelPosition: 'right', // 'left', 'right', or 'top' - theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom - customColors: { - bg: '#1a1a2e', - accent: '#16213e', - text: '#eaeaea', - highlight: '#e94560' - }, - statBarColorLow: '#cc3333', // Color for low stat values (red) - statBarColorHigh: '#33cc66', // Color for high stat values (green) - enableAnimations: true, // Enable smooth animations for stats and content updates - mobileFabPosition: { - top: 'calc(var(--topBarBlockSize) + 60px)', - right: '12px' - }, // Saved position for mobile FAB button - userStats: { - health: 100, - satiety: 100, - energy: 100, - hygiene: 100, - arousal: 0, - mood: '😐', - conditions: 'None', - inventory: 'None' - }, - classicStats: { - str: 10, - dex: 10, - con: 10, - int: 10, - wis: 10, - cha: 10 - }, - lastDiceRoll: null // Store last dice roll result -}; - -let lastGeneratedData = { - userStats: null, - infoBox: null, - characterThoughts: null, - html: null -}; - -// Tracks the "committed" tracker data that should be used as source for next generation -// This gets updated when user sends a new message or first time generation -let committedTrackerData = { - userStats: null, - infoBox: null, - characterThoughts: null -}; - -// Tracks whether the last action was a swipe (for separate mode) -// Used to determine whether to commit lastGeneratedData to committedTrackerData -let lastActionWasSwipe = false; - -let isGenerating = false; - -// Tracks if we're currently doing a plot progression -let isPlotProgression = false; - -// Temporary storage for pending dice roll (not saved until user clicks "Save Roll") -let pendingDiceRoll = null; - -// Fallback avatar image (base64-encoded SVG with "?" icon) -// Using base64 to avoid quote-encoding issues in HTML attributes -const FALLBACK_AVATAR_DATA_URI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2NjY2NjYyIgb3BhY2l0eT0iMC4zIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjNjY2IiBmb250LXNpemU9IjQwIj4/PC90ZXh0Pjwvc3ZnPg=='; - -// UI Elements -let $panelContainer = null; -let $userStatsContainer = null; -let $infoBoxContainer = null; -let $thoughtsContainer = null; +// Old state variable declarations removed - now imported from core modules +// (extensionSettings, lastGeneratedData, committedTrackerData, etc. are now in src/core/state.js) /** * Safely attempts to get a thumbnail URL with proper error handling. @@ -137,126 +79,8 @@ function getSafeThumbnailUrl(type, filename) { } } -/** - * Loads the extension settings from the global settings object. - */ -function loadSettings() { - if (power_user.extensions && power_user.extensions[extensionName]) { - Object.assign(extensionSettings, power_user.extensions[extensionName]); - // console.log('[RPG Companion] Settings loaded:', extensionSettings); - } else { - // console.log('[RPG Companion] No saved settings found, using defaults'); - } -} - -/** - * Saves the extension settings to the global settings object. - */ -function saveSettings() { - if (!power_user.extensions) { - power_user.extensions = {}; - } - power_user.extensions[extensionName] = extensionSettings; - saveSettingsDebounced(); -} - -/** - * Saves RPG data to the current chat's metadata. - */ -function saveChatData() { - if (!chat_metadata) { - return; - } - - chat_metadata.rpg_companion = { - userStats: extensionSettings.userStats, - classicStats: extensionSettings.classicStats, - lastGeneratedData: lastGeneratedData, - timestamp: Date.now() - }; - - saveChatDebounced(); -} - -/** - * Updates the last assistant message's swipe data with current tracker data. - * This ensures user edits are preserved across swipes and included in generation context. - */ -function updateMessageSwipeData() { - const chat = getContext().chat; - if (!chat || chat.length === 0) { - return; - } - - // Find the last assistant message - for (let i = chat.length - 1; i >= 0; i--) { - const message = chat[i]; - if (!message.is_user) { - // Found last assistant message - update its swipe data - if (!message.extra) { - message.extra = {}; - } - if (!message.extra.rpg_companion_swipes) { - message.extra.rpg_companion_swipes = {}; - } - - const swipeId = message.swipe_id || 0; - message.extra.rpg_companion_swipes[swipeId] = { - userStats: lastGeneratedData.userStats, - infoBox: lastGeneratedData.infoBox, - characterThoughts: lastGeneratedData.characterThoughts - }; - - // console.log('[RPG Companion] Updated message swipe data after user edit'); - break; - } - } -} - -/** - * Loads RPG data from the current chat's metadata. - */ -function loadChatData() { - if (!chat_metadata || !chat_metadata.rpg_companion) { - // Reset to defaults if no data exists - extensionSettings.userStats = { - health: 100, - satiety: 100, - energy: 100, - hygiene: 100, - arousal: 0, - mood: '😐', - conditions: 'None', - inventory: 'None' - }; - lastGeneratedData = { - userStats: null, - infoBox: null, - characterThoughts: null, - html: null - }; - return; - } - - const savedData = chat_metadata.rpg_companion; - - // Restore stats - if (savedData.userStats) { - extensionSettings.userStats = { ...savedData.userStats }; - } - - // Restore classic stats - if (savedData.classicStats) { - extensionSettings.classicStats = { ...savedData.classicStats }; - } - - // Restore last generated data - if (savedData.lastGeneratedData) { - lastGeneratedData = { ...savedData.lastGeneratedData }; - } - - // console.log('[RPG Companion] Loaded chat data:', savedData); -} +// Persistence functions removed - now imported from src/core/persistence.js +// (loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData) /** * Applies the selected theme to the panel. @@ -371,11 +195,11 @@ async function initUI() { `; $('body').append(mobileToggleHtml); - // Cache UI elements - $panelContainer = $('#rpg-companion-panel'); - $userStatsContainer = $('#rpg-user-stats'); - $infoBoxContainer = $('#rpg-info-box'); - $thoughtsContainer = $('#rpg-thoughts'); + // Cache UI elements using state setters + setPanelContainer($('#rpg-companion-panel')); + setUserStatsContainer($('#rpg-user-stats')); + setInfoBoxContainer($('#rpg-info-box')); + setThoughtsContainer($('#rpg-thoughts')); // Set up event listeners (enable/disable is handled in Extensions tab) $('#rpg-toggle-auto-update').on('change', function() { diff --git a/src/core/config.js b/src/core/config.js new file mode 100644 index 0000000..de67a2e --- /dev/null +++ b/src/core/config.js @@ -0,0 +1,66 @@ +/** + * Core Configuration Module + * Extension metadata and configuration constants + */ + +export const extensionName = 'third-party/rpg-companion-sillytavern'; + +/** + * Dynamically determine extension path based on current location + * This supports both global (public/extensions) and user-specific (data/default-user/extensions) installations + */ +const currentScriptPath = import.meta.url; +const isUserExtension = currentScriptPath.includes('/data/') || currentScriptPath.includes('\\data\\'); +export const extensionFolderPath = isUserExtension + ? `data/default-user/extensions/${extensionName}` + : `scripts/extensions/${extensionName}`; + +/** + * Default extension settings + */ +export const defaultSettings = { + enabled: true, + autoUpdate: true, + updateDepth: 4, // How many messages to include in the context + generationMode: 'together', // 'separate' or 'together' - whether to generate with main response or separately + showUserStats: true, + showInfoBox: true, + showCharacterThoughts: true, + showThoughtsInChat: true, // Show thoughts overlay in chat + enableHtmlPrompt: false, // Enable immersive HTML prompt injection + enablePlotButtons: true, // Show plot progression buttons above chat input + panelPosition: 'right', // 'left', 'right', or 'top' + theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom + customColors: { + bg: '#1a1a2e', + accent: '#16213e', + text: '#eaeaea', + highlight: '#e94560' + }, + statBarColorLow: '#cc3333', // Color for low stat values (red) + statBarColorHigh: '#33cc66', // Color for high stat values (green) + enableAnimations: true, // Enable smooth animations for stats and content updates + mobileFabPosition: { + top: 'calc(var(--topBarBlockSize) + 60px)', + right: '12px' + }, // Saved position for mobile FAB button + userStats: { + health: 100, + satiety: 100, + energy: 100, + hygiene: 100, + arousal: 0, + mood: '😐', + conditions: 'None', + inventory: 'None' + }, + classicStats: { + str: 10, + dex: 10, + con: 10, + int: 10, + wis: 10, + cha: 10 + }, + lastDiceRoll: null // Store last dice roll result +}; diff --git a/src/core/events.js b/src/core/events.js new file mode 100644 index 0000000..9d8c370 --- /dev/null +++ b/src/core/events.js @@ -0,0 +1,47 @@ +/** + * Core Events Module + * Wrapper for SillyTavern event system + */ + +import { eventSource, event_types } from '../../../../../../script.js'; + +/** + * Register an event handler + * @param {string} eventType - Event type from event_types + * @param {Function} handler - Event handler function + */ +export function on(eventType, handler) { + eventSource.on(eventType, handler); +} + +/** + * Register a one-time event handler + * @param {string} eventType - Event type from event_types + * @param {Function} handler - Event handler function + */ +export function once(eventType, handler) { + eventSource.once(eventType, handler); +} + +/** + * Remove an event handler + * @param {string} eventType - Event type from event_types + * @param {Function} handler - Event handler function to remove + */ +export function off(eventType, handler) { + eventSource.off(eventType, handler); +} + +/** + * Emit an event + * @param {string} eventType - Event type to emit + * @param {...*} args - Arguments to pass to handlers + */ +export function emit(eventType, ...args) { + eventSource.emit(eventType, ...args); +} + +/** + * Re-export event types for convenience + */ +export { event_types }; diff --git a/src/core/persistence.js b/src/core/persistence.js new file mode 100644 index 0000000..64e838e --- /dev/null +++ b/src/core/persistence.js @@ -0,0 +1,140 @@ +/** + * Core Persistence Module + * Handles saving/loading extension settings and chat data + */ + +import { saveSettingsDebounced, chat_metadata, saveChatDebounced } from '../../../../../../script.js'; +import { power_user } from '../../../../../power-user.js'; +import { getContext } from '../../../../../extensions.js'; +import { + extensionSettings, + lastGeneratedData, + setExtensionSettings, + updateExtensionSettings, + setLastGeneratedData +} from './state.js'; + +const extensionName = 'third-party/rpg-companion-sillytavern'; + +/** + * Loads the extension settings from the global settings object. + */ +export function loadSettings() { + if (power_user.extensions && power_user.extensions[extensionName]) { + updateExtensionSettings(power_user.extensions[extensionName]); + // console.log('[RPG Companion] Settings loaded:', extensionSettings); + } else { + // console.log('[RPG Companion] No saved settings found, using defaults'); + } +} + +/** + * Saves the extension settings to the global settings object. + */ +export function saveSettings() { + if (!power_user.extensions) { + power_user.extensions = {}; + } + power_user.extensions[extensionName] = extensionSettings; + saveSettingsDebounced(); +} + +/** + * Saves RPG data to the current chat's metadata. + */ +export function saveChatData() { + if (!chat_metadata) { + return; + } + + chat_metadata.rpg_companion = { + userStats: extensionSettings.userStats, + classicStats: extensionSettings.classicStats, + lastGeneratedData: lastGeneratedData, + timestamp: Date.now() + }; + + saveChatDebounced(); +} + +/** + * Updates the last assistant message's swipe data with current tracker data. + * This ensures user edits are preserved across swipes and included in generation context. + */ +export function updateMessageSwipeData() { + const chat = getContext().chat; + if (!chat || chat.length === 0) { + return; + } + + // Find the last assistant message + for (let i = chat.length - 1; i >= 0; i--) { + const message = chat[i]; + if (!message.is_user) { + // Found last assistant message - update its swipe data + if (!message.extra) { + message.extra = {}; + } + if (!message.extra.rpg_companion_swipes) { + message.extra.rpg_companion_swipes = {}; + } + + const swipeId = message.swipe_id || 0; + message.extra.rpg_companion_swipes[swipeId] = { + userStats: lastGeneratedData.userStats, + infoBox: lastGeneratedData.infoBox, + characterThoughts: lastGeneratedData.characterThoughts + }; + + // console.log('[RPG Companion] Updated message swipe data after user edit'); + break; + } + } +} + +/** + * Loads RPG data from the current chat's metadata. + */ +export function loadChatData() { + if (!chat_metadata || !chat_metadata.rpg_companion) { + // Reset to defaults if no data exists + updateExtensionSettings({ + userStats: { + health: 100, + satiety: 100, + energy: 100, + hygiene: 100, + arousal: 0, + mood: '😐', + conditions: 'None', + inventory: 'None' + } + }); + setLastGeneratedData({ + userStats: null, + infoBox: null, + characterThoughts: null, + html: null + }); + return; + } + + const savedData = chat_metadata.rpg_companion; + + // Restore stats + if (savedData.userStats) { + extensionSettings.userStats = { ...savedData.userStats }; + } + + // Restore classic stats + if (savedData.classicStats) { + extensionSettings.classicStats = { ...savedData.classicStats }; + } + + // Restore last generated data + if (savedData.lastGeneratedData) { + setLastGeneratedData({ ...savedData.lastGeneratedData }); + } + + // console.log('[RPG Companion] Loaded chat data:', savedData); +} diff --git a/src/core/state.js b/src/core/state.js new file mode 100644 index 0000000..530d590 --- /dev/null +++ b/src/core/state.js @@ -0,0 +1,168 @@ +/** + * Core State Management Module + * Centralizes all extension state variables + */ + +/** + * Extension settings - persisted to SillyTavern settings + */ +export let extensionSettings = { + enabled: true, + autoUpdate: true, + updateDepth: 4, // How many messages to include in the context + generationMode: 'together', // 'separate' or 'together' - whether to generate with main response or separately + showUserStats: true, + showInfoBox: true, + showCharacterThoughts: true, + showThoughtsInChat: true, // Show thoughts overlay in chat + enableHtmlPrompt: false, // Enable immersive HTML prompt injection + enablePlotButtons: true, // Show plot progression buttons above chat input + panelPosition: 'right', // 'left', 'right', or 'top' + theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom + customColors: { + bg: '#1a1a2e', + accent: '#16213e', + text: '#eaeaea', + highlight: '#e94560' + }, + statBarColorLow: '#cc3333', // Color for low stat values (red) + statBarColorHigh: '#33cc66', // Color for high stat values (green) + enableAnimations: true, // Enable smooth animations for stats and content updates + mobileFabPosition: { + top: 'calc(var(--topBarBlockSize) + 60px)', + right: '12px' + }, // Saved position for mobile FAB button + userStats: { + health: 100, + satiety: 100, + energy: 100, + hygiene: 100, + arousal: 0, + mood: '😐', + conditions: 'None', + inventory: 'None' + }, + classicStats: { + str: 10, + dex: 10, + con: 10, + int: 10, + wis: 10, + cha: 10 + }, + lastDiceRoll: null // Store last dice roll result +}; + +/** + * Last generated data from AI response + */ +export let lastGeneratedData = { + userStats: null, + infoBox: null, + characterThoughts: null, + html: null +}; + +/** + * Tracks the "committed" tracker data that should be used as source for next generation + * This gets updated when user sends a new message or first time generation + */ +export let committedTrackerData = { + userStats: null, + infoBox: null, + characterThoughts: null +}; + +/** + * Tracks whether the last action was a swipe (for separate mode) + * Used to determine whether to commit lastGeneratedData to committedTrackerData + */ +export let lastActionWasSwipe = false; + +/** + * Flag indicating if generation is in progress + */ +export let isGenerating = false; + +/** + * Tracks if we're currently doing a plot progression + */ +export let isPlotProgression = false; + +/** + * Temporary storage for pending dice roll (not saved until user clicks "Save Roll") + */ +export let pendingDiceRoll = null; + +/** + * Fallback avatar image (base64-encoded SVG with "?" icon) + * Using base64 to avoid quote-encoding issues in HTML attributes + */ +export const FALLBACK_AVATAR_DATA_URI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2NjY2NjYyIgb3BhY2l0eT0iMC4zIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjNjY2IiBmb250LXNpemU9IjQwIj4/PC90ZXh0Pjwvc3ZnPg=='; + +/** + * UI Element References (jQuery objects) + */ +export let $panelContainer = null; +export let $userStatsContainer = null; +export let $infoBoxContainer = null; +export let $thoughtsContainer = null; + +/** + * State setters - provide controlled mutation of state variables + */ +export function setExtensionSettings(newSettings) { + extensionSettings = newSettings; +} + +export function updateExtensionSettings(updates) { + Object.assign(extensionSettings, updates); +} + +export function setLastGeneratedData(data) { + lastGeneratedData = data; +} + +export function updateLastGeneratedData(updates) { + Object.assign(lastGeneratedData, updates); +} + +export function setCommittedTrackerData(data) { + committedTrackerData = data; +} + +export function updateCommittedTrackerData(updates) { + Object.assign(committedTrackerData, updates); +} + +export function setLastActionWasSwipe(value) { + lastActionWasSwipe = value; +} + +export function setIsGenerating(value) { + isGenerating = value; +} + +export function setIsPlotProgression(value) { + isPlotProgression = value; +} + +export function setPendingDiceRoll(roll) { + pendingDiceRoll = roll; +} + +export function setPanelContainer($element) { + $panelContainer = $element; +} + +export function setUserStatsContainer($element) { + $userStatsContainer = $element; +} + +export function setInfoBoxContainer($element) { + $infoBoxContainer = $element; +} + +export function setThoughtsContainer($element) { + $thoughtsContainer = $element; +}