diff --git a/index.js b/index.js index 288a562..80ac29e 100644 --- a/index.js +++ b/index.js @@ -90,6 +90,12 @@ import { import { initTrackerEditor } from './src/systems/ui/trackerEditor.js'; +import { + initChapterCheckpointUI, + injectCheckpointButton, + updateAllCheckpointIndicators +} from './src/systems/ui/checkpointUI.js'; +import { restoreCheckpointOnLoad } from './src/systems/features/chapterCheckpoint.js'; import { togglePlotButtons, updateCollapseToggleIcon, @@ -129,7 +135,8 @@ import { onCharacterChanged, onMessageSwiped, updatePersonaAvatar, - clearExtensionPrompts + clearExtensionPrompts, + onGenerationEnded } from './src/systems/integration/sillytavern.js'; // Old state variable declarations removed - now imported from core modules @@ -366,6 +373,11 @@ async function initUI() { saveSettings(); }); + $('#rpg-save-tracker-history').on('change', function() { + extensionSettings.saveTrackerHistory = $(this).prop('checked'); + saveSettings(); + }); + $('#rpg-toggle-plot-buttons').on('change', function() { extensionSettings.enablePlotButtons = $(this).prop('checked'); // console.log('[RPG Companion] Toggle enablePlotButtons changed to:', extensionSettings.enablePlotButtons); @@ -478,6 +490,7 @@ async function initUI() { $('#rpg-custom-highlight').val(extensionSettings.customColors.highlight); $('#rpg-generation-mode').val(extensionSettings.generationMode); $('#rpg-skip-guided-mode').val(extensionSettings.skipInjectionsForGuided); + $('#rpg-save-tracker-history').prop('checked', extensionSettings.saveTrackerHistory); updatePanelVisibility(); updateSectionVisibility(); @@ -515,6 +528,10 @@ async function initUI() { setupContentEditableScrolling(); initInventoryEventListeners(); + // Initialize chapter checkpoint UI + initChapterCheckpointUI(); + injectCheckpointButton(); + // Setup Memory Recollection button in World Info setupMemoryRecollectionButton(); @@ -683,7 +700,9 @@ jQuery(async () => { [event_types.MESSAGE_SENT]: onMessageSent, [event_types.GENERATION_STARTED]: onGenerationStarted, [event_types.MESSAGE_RECEIVED]: onMessageReceived, - [event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar], + [event_types.GENERATION_STOPPED]: onGenerationEnded, + [event_types.GENERATION_ENDED]: onGenerationEnded, + [event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad], [event_types.MESSAGE_SWIPED]: onMessageSwiped, [event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar, [event_types.SETTINGS_UPDATED]: updatePersonaAvatar @@ -693,6 +712,9 @@ jQuery(async () => { throw error; // This is critical - can't continue without events } + // Restore checkpoint state if one exists + await restoreCheckpointOnLoad(); + console.log('[RPG Companion] ✅ Extension loaded successfully'); } catch (error) { console.error('[RPG Companion] ❌ Critical initialization failure:', error); diff --git a/src/core/config.js b/src/core/config.js index 4901cec..2377186 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -42,6 +42,7 @@ export const defaultSettings = { // This setting helps compatibility with other extensions like GuidedGenerations. skipInjectionsForGuided: 'none', enablePlotButtons: true, // Show plot progression buttons above chat input + saveTrackerHistory: false, // Save tracker data in chat history for each message panelPosition: 'right', // 'left', 'right', or 'top' theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom customColors: { diff --git a/src/core/state.js b/src/core/state.js index 64cbf68..d3e0e99 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -24,6 +24,7 @@ export let extensionSettings = { customHtmlPrompt: '', // Custom HTML prompt text (empty = use default) skipInjectionsForGuided: 'none', // skip injections for instruct injections and quiet prompts (GuidedGenerations compatibility) enablePlotButtons: true, // Show plot progression buttons above chat input + saveTrackerHistory: false, // Save tracker data in chat history for each message panelPosition: 'right', // 'left', 'right', or 'top' theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom customColors: { diff --git a/src/i18n/en.json b/src/i18n/en.json index 8e7f0fd..846199f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1,165 +1,169 @@ -{ - "settings.language.label": "Language", - "settings.language.option.en": "English", - "settings.language.option.zh-tw": "繁體中文", - "settings.extensionEnabled": "Enable RPG Companion", - "settings.note": "Toggle to enable/disable the RPG Companion extension. Configure additional settings within the panel itself.", - "template.settingsTitle": "RPG Companion Settings", - "template.settingsModal.themeTitle": "Theme", - "template.settingsModal.themeLabel": "Visual Theme:", - "template.settingsModal.themeOptions.default": "Default", - "template.settingsModal.themeOptions.sciFi": "Sci-Fi (Synthwave)", - "template.settingsModal.themeOptions.fantasy": "Fantasy (Rustic Parchment)", - "template.settingsModal.themeOptions.cyberpunk": "Cyberpunk (Neon Grid)", - "template.settingsModal.themeOptions.custom": "Custom", - "template.settingsModal.themeOptions.custom.background": "Background:", - "template.settingsModal.themeOptions.custom.accent": "Accent:", - "template.settingsModal.themeOptions.custom.text": "Text:", - "template.settingsModal.themeOptions.custom.highlight": "Highlight:", - "template.settingsModal.theme.statBarLow": "Stat Bar Color (Low):", - "template.settingsModal.theme.statBarLowNote": "Color when stats are at 0%", - "template.settingsModal.theme.statBarHigh": "Stat Bar Color (High):", - "template.settingsModal.theme.statBarHighNote": "Color when stats are at 100%", - "template.settingsModal.displayTitle": "Display Options", - "template.settingsModal.displayNote": "Use the Extensions tab to enable/disable the RPG Companion extension.", - "template.settingsModal.display.panelPosition": "Panel Position:", - "template.settingsModal.display.panelPositionOptions.right": "Right Sidebar", - "template.settingsModal.display.panelPositionOptions.left": "Left Sidebar", - "template.settingsModal.display.toggleAutoUpdate": "Auto-update after messages", - "template.settingsModal.display.showUserStats": "Show User Stats", - "template.settingsModal.display.showInfoBox": "Show Info Box", - "template.settingsModal.display.showPresentCharacters": "Show Present Characters", - "template.settingsModal.display.showInventory": "Show Inventory", - "template.settingsModal.display.showThoughtsInChat": "Show Thoughts in Chat", - "template.settingsModal.display.showThoughtsInChatNote": "Display character thoughts as overlay bubbles next to their messages", - "template.settingsModal.display.alwaysShowThoughtBubble": "Always Show Thought Bubble", - "template.settingsModal.display.alwaysShowThoughtBubbleNote": "Auto-expand thought bubble without clicking the icon first", - "template.settingsModal.display.enableAnimations": "Enable Animations", - "template.settingsModal.display.enableAnimationsNote": "Smooth transitions for stats, content updates, and dice rolls", - "template.settingsModal.display.showPlotProgressionButtons": "Show Plot Progression Buttons", - "template.settingsModal.display.showPlotProgressionButtonsNote": "Display buttons above chat input for plot progression prompts", - "template.settingsModal.display.enableDebugMode": "Enable Debug Mode", - "template.settingsModal.display.enableDebugModeNote": "Shows parser logs in a mobile-friendly UI panel. Useful for troubleshooting. Look for the red bug button.", - "template.settingsModal.advancedTitle": "Advanced", - "template.settingsModal.advanced.generationMode": "Generation Mode:", - "template.settingsModal.advanced.generationModeOptions.together": "Together with Main Generation", - "template.settingsModal.advanced.generationModeOptions.separate": "Separate Generation", - "template.settingsModal.advanced.generationModeNote": "Together: Adds RPG tracking to main roleplay. Separate: Generates RPG data separately (manual or auto).", - "template.settingsModal.advanced.contextMessages": "Context Messages:", - "template.settingsModal.advanced.contextMessagesNote": "Number of recent messages to include (Separate mode only)", - "template.settingsModal.advanced.memoryBatchSize": "Memory Batch Size:", - "template.settingsModal.advanced.memoryBatchSizeNote": "Number of messages to process per batch in Memory Recollection", - "template.settingsModal.advanced.useSeparatePreset": "Use model connected to RPG Companion Trackers preset", - "template.settingsModal.advanced.useSeparatePresetNote": "Separate mode only. When enabled, tracker generation will use the model from the \"RPG Companion Trackers\" preset instead of your main API model. The preset will be switched automatically during generation and restored afterward. Select the desired model in that preset and make sure the \"Bind presets to API connections\" toggle is on (next to the import/export preset buttons).", - "template.settingsModal.advanced.skipInjections": "Skip Injections during Guided Generations:", - "template.settingsModal.advanced.skipInjectionsOptions.none": "Never skip", - "template.settingsModal.advanced.skipInjectionsOptions.impersonation": "Only on impersonation requests", - "template.settingsModal.advanced.skipInjectionsOptions.guided": "Always for guided or quiet prompts", - "template.settingsModal.advanced.skipInjectionsNote": "When set, the extension will not inject tracker prompts, examples, or HTML instructions according to the selected mode when a guided generation (via `instruct` or `quiet_prompt`) is detected. Useful when using GuidedGenerations or similar extensions.", - "template.settingsModal.advanced.customHtmlPromptTitle": "Custom HTML Prompt:", - "template.settingsModal.advanced.restoreDefaultHtmlPrompt": "Restore Default", - "template.settingsModal.advanced.customHtmlPromptNote": "Customize the HTML prompt injected when \"Enable Immersive HTML\" is enabled. The default prompt is shown above - you can edit it directly or replace it entirely. Click \"Restore Default\" to reset. This affects all generation modes (together, separate, and plot progression).", - "template.settingsModal.advanced.clearCache": "Clear Extension Cache", - "template.settingsModal.advanced.resetFabPositions": "Reset Button Positions", - "template.settingsModal.advanced.resetFabPositionsNote": "Resets all floating action buttons (toggle, refresh, debug) to default top-left positions. Useful if buttons are off-screen.", - "template.trackerEditorModal.title": "Edit Trackers", - "template.trackerEditorModal.tabs.userStats": "User Stats", - "template.trackerEditorModal.tabs.infoBox": "Info Box", - "template.trackerEditorModal.tabs.presentCharacters": "Present Characters", - "template.trackerEditorModal.buttons.reset": "Reset to Defaults", - "template.trackerEditorModal.buttons.cancel": "Cancel", - "template.trackerEditorModal.buttons.save": "Save & Apply", - "template.trackerEditorModal.userStatsTab.customStatsTitle": "Custom Stats", - "template.trackerEditorModal.userStatsTab.addCustomStatButton": "Add Custom Stat", - "template.trackerEditorModal.userStatsTab.rpgAttributesTitle": "RPG Attributes", - "template.trackerEditorModal.userStatsTab.enableRpgAttributes": "Enable RPG Attributes Section", - "template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "Always Include Attributes in Prompt", - "template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "If disabled, attributes are only sent when a dice roll is active.", - "template.trackerEditorModal.userStatsTab.addAttributeButton": "Add Attribute", - "template.trackerEditorModal.userStatsTab.statusSectionTitle": "Status Section", - "template.trackerEditorModal.userStatsTab.enableStatusSection": "Enable Status Section", - "template.trackerEditorModal.userStatsTab.showMoodEmoji": "Show Mood Emoji", - "template.trackerEditorModal.userStatsTab.statusFieldsLabel": "Status Fields (comma-separated):", - "template.trackerEditorModal.userStatsTab.skillsSectionTitle": "Skills Section", - "template.trackerEditorModal.userStatsTab.enableSkillsSection": "Enable Skills Section", - "template.trackerEditorModal.userStatsTab.skillsLabelLabel": "Skills Label:", - "template.trackerEditorModal.userStatsTab.skillsListLabel": "Skills List (comma-separated):", - "template.trackerEditorModal.infoBoxTab.widgetsTitle": "Widgets", - "template.trackerEditorModal.infoBoxTab.dateWidget": "Date", - "template.trackerEditorModal.infoBoxTab.weatherWidget": "Weather", - "template.trackerEditorModal.infoBoxTab.temperatureWidget": "Temperature", - "template.trackerEditorModal.infoBoxTab.timeWidget": "Time", - "template.trackerEditorModal.infoBoxTab.locationWidget": "Location", - "template.trackerEditorModal.infoBoxTab.recentEventsWidget": "Recent Events", - "template.trackerEditorModal.presentCharactersTab.relationshipStatusTitle": "Relationship Status Fields", - "template.trackerEditorModal.presentCharactersTab.relationshipStatusHint": "Define relationship types with corresponding emojis shown on character portraits", - "template.trackerEditorModal.presentCharactersTab.newRelationshipButton": "New Relationship", - "template.trackerEditorModal.presentCharactersTab.appearanceDemeanorTitle": "Appearance/Demeanor Fields", - "template.trackerEditorModal.presentCharactersTab.appearanceDemeanorHint": "Fields shown below character name, separated by |", - "template.trackerEditorModal.presentCharactersTab.addCustomFieldButton": "Add Custom Field", - "template.trackerEditorModal.presentCharactersTab.thoughtsConfigTitle": "Thoughts Configuration", - "template.trackerEditorModal.presentCharactersTab.enableCharacterThoughts": "Enable Character Thoughts", - "template.trackerEditorModal.presentCharactersTab.thoughtsLabelLabel": "Thoughts Label:", - "template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "AI Instruction:", - "template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "Character Stats", - "template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "Track Character Stats", - "template.trackerEditorModal.presentCharactersTab.characterStatsHint": "Create stats to track for each character (displayed as colored bars)", - "template.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "Add Character Stat", - "template.mainPanel.title": "RPG Companion", - "template.mainPanel.lastRoll": "Last Roll:", - "template.mainPanel.clearLastRoll": "Clear last roll", - "template.mainPanel.enableImmersiveHtml": "Enable Immersive HTML", - "template.mainPanel.refreshRpgInfo": "Refresh RPG Info", - "template.mainPanel.updating": "Updating...", - "template.mainPanel.editTrackersButton": "Edit Trackers", - "template.mainPanel.settingsButton": "Settings", - "global.none": "None", - "global.add": "Add", - "global.cancel": "Cancel", - "global.listView": "List view", - "global.gridView": "Grid view", - "global.save": "Save", - "global.status":"Status", - "global.inventory":"Inventory", - "global.quests":"Quests", - "global.info":"Info", - "infobox.noData.title": "No data yet", - "infobox.noData.instruction": "Generate a new response in the roleplay or switch to \"Separate Generation\" in Settings to access and click the \"Refresh RPG Info\" button", - "infobox.recentEvents.title": "Recent Events", - "infobox.recentEvents.addEventPlaceholder": "Add event...", - "inventory.section.onPerson": "On Person", - "inventory.section.stored": "Stored", - "inventory.section.assets": "Assets", - "inventory.onPerson.empty": "No items carried", - "inventory.onPerson.title": "Items Currently Carried", - "inventory.onPerson.addItemButton": "Add Item", - "inventory.onPerson.addItemPlaceholder": "Enter item name...", - "inventory.stored.title": "Storage Locations", - "inventory.stored.addLocationButton": "Add Location", - "inventory.stored.addLocationPlaceholder": "Enter location name...", - "inventory.stored.saveButton": "Save", - "inventory.stored.empty": "No storage locations yet. Click \"Add Location\" to create one.", - "inventory.stored.noItems": "No items stored here", - "inventory.stored.addItemToLocationPlaceholder": "Enter item name...", - "inventory.stored.addItemButton": "Add Item", - "inventory.stored.confirmRemoveLocationMessage": "Remove \"${location}\"? This will delete all items stored there.", - "inventory.stored.confirmRemoveLocationConfirmButton": "Confirm", - "inventory.assets.empty": "No assets owned", - "inventory.assets.title": "Vehicles, Property & Major Possessions", - "inventory.assets.addAssetModalTitle": "Add Asset", - "inventory.assets.addAssetButton": "Add Asset", - "inventory.assets.addAssetPlaceholder": "Enter asset name...", - "inventory.assets.description": "Assets include vehicles (cars, motorcycles), property (homes, apartments), and major equipment (workshop tools, special items).", - "quests.section.main": "Main Quest", - "quests.section.optional": "Optional Quests", - "quests.main.title": "Main Quests", - "quests.main.addQuestButton": "Add Quest", - "quests.main.addQuestPlaceholder": "Enter main quest title...", - "quests.main.empty": "No active main quests", - "quests.main.hint": "The main quest represents your primary objective in the story.", - "quests.optional.title": "Optional Quests", - "quests.optional.addQuestButton": "Add Quest", - "quests.optional.addQuestPlaceholder": "Enter optional quest title...", - "quests.optional.empty": "No active optional quests", - "quests.optional.hint": "Optional quests are side objectives that complement your main story." -} \ No newline at end of file +{ + "settings.language.label": "Language", + "settings.language.option.en": "English", + "settings.language.option.zh-tw": "繁體中文", + "settings.extensionEnabled": "Enable RPG Companion", + "settings.note": "Toggle to enable/disable the RPG Companion extension. Configure additional settings within the panel itself.", + "template.settingsTitle": "RPG Companion Settings", + "template.settingsModal.themeTitle": "Theme", + "template.settingsModal.themeLabel": "Visual Theme:", + "template.settingsModal.themeOptions.default": "Default", + "template.settingsModal.themeOptions.sciFi": "Sci-Fi (Synthwave)", + "template.settingsModal.themeOptions.fantasy": "Fantasy (Rustic Parchment)", + "template.settingsModal.themeOptions.cyberpunk": "Cyberpunk (Neon Grid)", + "template.settingsModal.themeOptions.custom": "Custom", + "template.settingsModal.themeOptions.custom.background": "Background:", + "template.settingsModal.themeOptions.custom.accent": "Accent:", + "template.settingsModal.themeOptions.custom.text": "Text:", + "template.settingsModal.themeOptions.custom.highlight": "Highlight:", + "template.settingsModal.theme.statBarLow": "Stat Bar Color (Low):", + "template.settingsModal.theme.statBarLowNote": "Color when stats are at 0%", + "template.settingsModal.theme.statBarHigh": "Stat Bar Color (High):", + "template.settingsModal.theme.statBarHighNote": "Color when stats are at 100%", + "template.settingsModal.displayTitle": "Display Options", + "template.settingsModal.displayNote": "Use the Extensions tab to enable/disable the RPG Companion extension.", + "template.settingsModal.display.panelPosition": "Panel Position:", + "template.settingsModal.display.panelPositionOptions.right": "Right Sidebar", + "template.settingsModal.display.panelPositionOptions.left": "Left Sidebar", + "template.settingsModal.display.toggleAutoUpdate": "Auto-update after messages", + "template.settingsModal.display.showUserStats": "Show User Stats", + "template.settingsModal.display.showInfoBox": "Show Info Box", + "template.settingsModal.display.showPresentCharacters": "Show Present Characters", + "template.settingsModal.display.showInventory": "Show Inventory", + "template.settingsModal.display.showThoughtsInChat": "Show Thoughts in Chat", + "template.settingsModal.display.showThoughtsInChatNote": "Display character thoughts as overlay bubbles next to their messages", + "template.settingsModal.display.alwaysShowThoughtBubble": "Always Show Thought Bubble", + "template.settingsModal.display.alwaysShowThoughtBubbleNote": "Auto-expand thought bubble without clicking the icon first", + "template.settingsModal.display.enableAnimations": "Enable Animations", + "template.settingsModal.display.enableAnimationsNote": "Smooth transitions for stats, content updates, and dice rolls", + "template.settingsModal.display.showPlotProgressionButtons": "Show Plot Progression Buttons", + "template.settingsModal.display.showPlotProgressionButtonsNote": "Display buttons above chat input for plot progression prompts", + "template.settingsModal.display.enableDebugMode": "Enable Debug Mode", + "template.settingsModal.display.enableDebugModeNote": "Shows parser logs in a mobile-friendly UI panel. Useful for troubleshooting. Look for the red bug button.", + "template.settingsModal.advancedTitle": "Advanced", + "template.settingsModal.advanced.generationMode": "Generation Mode:", + "template.settingsModal.advanced.generationModeOptions.together": "Together with Main Generation", + "template.settingsModal.advanced.generationModeOptions.separate": "Separate Generation", + "template.settingsModal.advanced.generationModeNote": "Together: Adds RPG tracking to main roleplay. Separate: Generates RPG data separately (manual or auto).", + "template.settingsModal.advanced.contextMessages": "Context Messages:", + "template.settingsModal.advanced.contextMessagesNote": "Number of recent messages to include (Separate mode only)", + "template.settingsModal.advanced.memoryBatchSize": "Memory Batch Size:", + "template.settingsModal.advanced.memoryBatchSizeNote": "Number of messages to process per batch in Memory Recollection", + "template.settingsModal.advanced.useSeparatePreset": "Use model connected to RPG Companion Trackers preset", + "template.settingsModal.advanced.useSeparatePresetNote": "Separate mode only. When enabled, tracker generation will use the model from the \"RPG Companion Trackers\" preset instead of your main API model. The preset will be switched automatically during generation and restored afterward. Select the desired model in that preset and make sure the \"Bind presets to API connections\" toggle is on (next to the import/export preset buttons).", + "template.settingsModal.advanced.skipInjections": "Skip Injections during Guided Generations:", + "template.settingsModal.advanced.skipInjectionsOptions.none": "Never skip", + "template.settingsModal.advanced.skipInjectionsOptions.impersonation": "Only on impersonation requests", + "template.settingsModal.advanced.skipInjectionsOptions.guided": "Always for guided or quiet prompts", + "template.settingsModal.advanced.skipInjectionsNote": "When set, the extension will not inject tracker prompts, examples, or HTML instructions according to the selected mode when a guided generation (via `instruct` or `quiet_prompt`) is detected. Useful when using GuidedGenerations or similar extensions.", + "template.settingsModal.advanced.customHtmlPromptTitle": "Custom HTML Prompt:", + "template.settingsModal.advanced.restoreDefaultHtmlPrompt": "Restore Default", + "template.settingsModal.advanced.customHtmlPromptNote": "Customize the HTML prompt injected when \"Enable Immersive HTML\" is enabled. The default prompt is shown above - you can edit it directly or replace it entirely. Click \"Restore Default\" to reset. This affects all generation modes (together, separate, and plot progression).", + "template.settingsModal.advanced.clearCache": "Clear Extension Cache", + "template.settingsModal.advanced.resetFabPositions": "Reset Button Positions", + "template.settingsModal.advanced.resetFabPositionsNote": "Resets all floating action buttons (toggle, refresh, debug) to default top-left positions. Useful if buttons are off-screen.", + "template.trackerEditorModal.title": "Edit Trackers", + "template.trackerEditorModal.tabs.userStats": "User Stats", + "template.trackerEditorModal.tabs.infoBox": "Info Box", + "template.trackerEditorModal.tabs.presentCharacters": "Present Characters", + "template.trackerEditorModal.buttons.reset": "Reset to Defaults", + "template.trackerEditorModal.buttons.cancel": "Cancel", + "template.trackerEditorModal.buttons.save": "Save & Apply", + "template.trackerEditorModal.userStatsTab.customStatsTitle": "Custom Stats", + "template.trackerEditorModal.userStatsTab.addCustomStatButton": "Add Custom Stat", + "template.trackerEditorModal.userStatsTab.rpgAttributesTitle": "RPG Attributes", + "template.trackerEditorModal.userStatsTab.enableRpgAttributes": "Enable RPG Attributes Section", + "template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "Always Include Attributes in Prompt", + "template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "If disabled, attributes are only sent when a dice roll is active.", + "template.trackerEditorModal.userStatsTab.addAttributeButton": "Add Attribute", + "template.trackerEditorModal.userStatsTab.statusSectionTitle": "Status Section", + "template.trackerEditorModal.userStatsTab.enableStatusSection": "Enable Status Section", + "template.trackerEditorModal.userStatsTab.showMoodEmoji": "Show Mood Emoji", + "template.trackerEditorModal.userStatsTab.statusFieldsLabel": "Status Fields (comma-separated):", + "template.trackerEditorModal.userStatsTab.skillsSectionTitle": "Skills Section", + "template.trackerEditorModal.userStatsTab.enableSkillsSection": "Enable Skills Section", + "template.trackerEditorModal.userStatsTab.skillsLabelLabel": "Skills Label:", + "template.trackerEditorModal.userStatsTab.skillsListLabel": "Skills List (comma-separated):", + "template.trackerEditorModal.infoBoxTab.widgetsTitle": "Widgets", + "template.trackerEditorModal.infoBoxTab.dateWidget": "Date", + "template.trackerEditorModal.infoBoxTab.weatherWidget": "Weather", + "template.trackerEditorModal.infoBoxTab.temperatureWidget": "Temperature", + "template.trackerEditorModal.infoBoxTab.timeWidget": "Time", + "template.trackerEditorModal.infoBoxTab.locationWidget": "Location", + "template.trackerEditorModal.infoBoxTab.recentEventsWidget": "Recent Events", + "template.trackerEditorModal.presentCharactersTab.relationshipStatusTitle": "Relationship Status Fields", + "template.trackerEditorModal.presentCharactersTab.relationshipStatusHint": "Define relationship types with corresponding emojis shown on character portraits", + "template.trackerEditorModal.presentCharactersTab.newRelationshipButton": "New Relationship", + "template.trackerEditorModal.presentCharactersTab.appearanceDemeanorTitle": "Appearance/Demeanor Fields", + "template.trackerEditorModal.presentCharactersTab.appearanceDemeanorHint": "Fields shown below character name, separated by |", + "template.trackerEditorModal.presentCharactersTab.addCustomFieldButton": "Add Custom Field", + "template.trackerEditorModal.presentCharactersTab.thoughtsConfigTitle": "Thoughts Configuration", + "template.trackerEditorModal.presentCharactersTab.enableCharacterThoughts": "Enable Character Thoughts", + "template.trackerEditorModal.presentCharactersTab.thoughtsLabelLabel": "Thoughts Label:", + "template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "AI Instruction:", + "template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "Character Stats", + "template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "Track Character Stats", + "template.trackerEditorModal.presentCharactersTab.characterStatsHint": "Create stats to track for each character (displayed as colored bars)", + "template.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "Add Character Stat", + "template.mainPanel.title": "RPG Companion", + "template.mainPanel.lastRoll": "Last Roll:", + "template.mainPanel.clearLastRoll": "Clear last roll", + "template.mainPanel.enableImmersiveHtml": "Enable Immersive HTML", + "template.mainPanel.refreshRpgInfo": "Refresh RPG Info", + "template.mainPanel.updating": "Updating...", + "template.mainPanel.editTrackersButton": "Edit Trackers", + "template.mainPanel.settingsButton": "Settings", + "global.none": "None", + "global.add": "Add", + "global.cancel": "Cancel", + "global.listView": "List view", + "global.gridView": "Grid view", + "global.save": "Save", + "global.status":"Status", + "global.inventory":"Inventory", + "global.quests":"Quests", + "global.info":"Info", + "infobox.noData.title": "No data yet", + "infobox.noData.instruction": "Generate a new response in the roleplay or switch to \"Separate Generation\" in Settings to access and click the \"Refresh RPG Info\" button", + "infobox.recentEvents.title": "Recent Events", + "infobox.recentEvents.addEventPlaceholder": "Add event...", + "inventory.section.onPerson": "On Person", + "inventory.section.stored": "Stored", + "inventory.section.assets": "Assets", + "inventory.onPerson.empty": "No items carried", + "inventory.onPerson.title": "Items Currently Carried", + "inventory.onPerson.addItemButton": "Add Item", + "inventory.onPerson.addItemPlaceholder": "Enter item name...", + "inventory.stored.title": "Storage Locations", + "inventory.stored.addLocationButton": "Add Location", + "inventory.stored.addLocationPlaceholder": "Enter location name...", + "inventory.stored.saveButton": "Save", + "inventory.stored.empty": "No storage locations yet. Click \"Add Location\" to create one.", + "inventory.stored.noItems": "No items stored here", + "inventory.stored.addItemToLocationPlaceholder": "Enter item name...", + "inventory.stored.addItemButton": "Add Item", + "inventory.stored.confirmRemoveLocationMessage": "Remove \"${location}\"? This will delete all items stored there.", + "inventory.stored.confirmRemoveLocationConfirmButton": "Confirm", + "inventory.assets.empty": "No assets owned", + "inventory.assets.title": "Vehicles, Property & Major Possessions", + "inventory.assets.addAssetModalTitle": "Add Asset", + "inventory.assets.addAssetButton": "Add Asset", + "inventory.assets.addAssetPlaceholder": "Enter asset name...", + "inventory.assets.description": "Assets include vehicles (cars, motorcycles), property (homes, apartments), and major equipment (workshop tools, special items).", + "quests.section.main": "Main Quest", + "quests.section.optional": "Optional Quests", + "quests.main.title": "Main Quests", + "quests.main.addQuestButton": "Add Quest", + "quests.main.addQuestPlaceholder": "Enter main quest title...", + "quests.main.empty": "No active main quests", + "quests.main.hint": "The main quest represents your primary objective in the story.", + "quests.optional.title": "Optional Quests", + "quests.optional.addQuestButton": "Add Quest", + "quests.optional.addQuestPlaceholder": "Enter optional quest title...", + "quests.optional.empty": "No active optional quests", + "quests.optional.hint": "Optional quests are side objectives that complement your main story.", + "checkpoint.setChapterStart": "Set Chapter Start", + "checkpoint.clearChapterStart": "Clear Chapter Start", + "checkpoint.indicator": "Chapter Start", + "checkpoint.tooltip": "Messages before this point are excluded from context" +} diff --git a/src/systems/features/chapterCheckpoint.js b/src/systems/features/chapterCheckpoint.js new file mode 100644 index 0000000..0f8d24c --- /dev/null +++ b/src/systems/features/chapterCheckpoint.js @@ -0,0 +1,181 @@ +/** + * Chapter Checkpoint Module + * Allows users to mark messages as "chapter start" points to filter context + * Uses SillyTavern's /hide and /unhide commands to exclude messages from context + */ + +import { getContext } from '../../../../../../extensions.js'; +import { chat_metadata, saveChatDebounced } from '../../../../../../../script.js'; +import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js'; + +// Track the message range that is currently hidden +let currentlyHiddenRange = null; + +// Debounce restore to prevent loops +let isRestoring = false; +let restoreTimeout = null; + +/** + * Gets the current chapter checkpoint message ID for the active chat + * @returns {number|null} Message ID of the checkpoint, or null if none set + */ +export function getChapterCheckpoint() { + const context = getContext(); + if (!context || !chat_metadata) return null; + + return chat_metadata.rpg_companion_chapter_checkpoint || null; +} + +/** + * Sets a message as the chapter checkpoint + * Automatically clears any previous checkpoint (only one checkpoint allowed at a time) + * Hides all messages before the checkpoint + * @param {number} messageId - The chat message index to set as checkpoint + * @returns {Promise} True if successful + */ +export async function setChapterCheckpoint(messageId) { + const context = getContext(); + const chat = context.chat; + + if (!chat || messageId < 0 || messageId >= chat.length) { + console.error('[RPG Companion] Invalid message ID for checkpoint:', messageId); + return false; + } + + const previousCheckpoint = chat_metadata.rpg_companion_chapter_checkpoint; + + // If moving checkpoint, unhide the old range first + if (previousCheckpoint !== null && previousCheckpoint !== undefined && previousCheckpoint !== messageId && currentlyHiddenRange !== null) { + const { start, end } = currentlyHiddenRange; + await executeSlashCommandsOnChatInput(`/unhide ${start}-${end}`, { quiet: true }); + console.log(`[RPG Companion] Unhid previous range: ${start}-${end}`); + } + + // Store in chat metadata (this automatically overrides any previous checkpoint) + chat_metadata.rpg_companion_chapter_checkpoint = messageId; + saveChatDebounced(); + + // Hide all messages before the checkpoint + if (messageId > 0) { + const rangeEnd = messageId - 1; + await executeSlashCommandsOnChatInput(`/hide 0-${rangeEnd}`, { quiet: true }); + currentlyHiddenRange = { start: 0, end: rangeEnd }; + console.log(`[RPG Companion] Hidden messages 0-${rangeEnd} (checkpoint at ${messageId})`); + } + + if (previousCheckpoint !== null && previousCheckpoint !== undefined && previousCheckpoint !== messageId) { + console.log(`[RPG Companion] Chapter checkpoint moved from message ${previousCheckpoint} to ${messageId}`); + } else { + console.log('[RPG Companion] Chapter checkpoint set at message', messageId); + } + + // Emit event for UI updates + if (typeof document !== 'undefined') { + const event = new CustomEvent('rpg-companion-checkpoint-changed', { + detail: { messageId, previousCheckpoint } + }); + document.dispatchEvent(event); + } + + return true; +} + +/** + * Clears the chapter checkpoint and unhides all hidden messages + */ +export async function clearChapterCheckpoint() { + if (!chat_metadata) return; + + // Unhide any hidden messages + if (currentlyHiddenRange !== null) { + const { start, end } = currentlyHiddenRange; + await executeSlashCommandsOnChatInput(`/unhide ${start}-${end}`, { quiet: true }); + console.log(`[RPG Companion] Unhid messages ${start}-${end}`); + currentlyHiddenRange = null; + } + + delete chat_metadata.rpg_companion_chapter_checkpoint; + saveChatDebounced(); + + console.log('[RPG Companion] Chapter checkpoint cleared'); + + // Emit event for UI updates + if (typeof document !== 'undefined') { + const event = new CustomEvent('rpg-companion-checkpoint-changed', { + detail: { messageId: null } + }); + document.dispatchEvent(event); + } +} + +/** + * Checks if a message is the current checkpoint + * @param {number} messageId - The message index to check + * @returns {boolean} True if this is the checkpoint message + */ +export function isCheckpointMessage(messageId) { + const checkpointId = getChapterCheckpoint(); + return checkpointId === messageId; +} + +/** + * Restores checkpoint state after page reload or generation events + * Checks if a checkpoint exists and re-applies the /hide command + * Debounced to prevent loops when called from multiple events + */ +export async function restoreCheckpointOnLoad() { + // Prevent concurrent executions + if (isRestoring) { + return; + } + + // Clear any pending timeout + if (restoreTimeout) { + clearTimeout(restoreTimeout); + } + + // Debounce: wait 100ms before actually restoring + return new Promise((resolve) => { + restoreTimeout = setTimeout(async () => { + isRestoring = true; + try { + const checkpointId = getChapterCheckpoint(); + + if (checkpointId !== null && checkpointId !== undefined && checkpointId > 0) { + const context = getContext(); + const chat = context.chat; + + if (chat && checkpointId < chat.length) { + const rangeEnd = checkpointId - 1; + + // Check if messages are already hidden + let needsRestore = false; + let hiddenCount = 0; + let visibleCount = 0; + for (let i = 0; i <= rangeEnd; i++) { + if (chat[i]) { + if (chat[i].is_system) { + hiddenCount++; + } else { + visibleCount++; + needsRestore = true; + } + } + } + + if (needsRestore) { + await executeSlashCommandsOnChatInput(`/hide 0-${rangeEnd}`, { quiet: true }); + currentlyHiddenRange = { start: 0, end: rangeEnd }; + console.log(`[RPG Companion] Restored checkpoint: Hidden messages 0-${rangeEnd}`); + } else { + currentlyHiddenRange = { start: 0, end: rangeEnd }; + } + } + } + } finally { + isRestoring = false; + resolve(); + } + }, 100); + }); +} diff --git a/src/systems/generation/apiClient.js b/src/systems/generation/apiClient.js index fa155bd..c3e2c89 100644 --- a/src/systems/generation/apiClient.js +++ b/src/systems/generation/apiClient.js @@ -144,6 +144,35 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough // Store RPG data for the last assistant message (separate mode) const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null; // console.log('[RPG Companion] Last message is_user:', lastMessage ? lastMessage.is_user : 'no message'); + + // Update lastGeneratedData for display (regardless of message type) + if (parsedData.userStats) { + lastGeneratedData.userStats = parsedData.userStats; + parseUserStats(parsedData.userStats); + } + if (parsedData.infoBox) { + lastGeneratedData.infoBox = parsedData.infoBox; + } + if (parsedData.characterThoughts) { + lastGeneratedData.characterThoughts = parsedData.characterThoughts; + } + + // When saveTrackerHistory is enabled, store tracker data on the user's message too + // This allows scrolling through history and seeing trackers at each point + if (extensionSettings.saveTrackerHistory && lastMessage && lastMessage.is_user) { + if (!lastMessage.extra) { + lastMessage.extra = {}; + } + lastMessage.extra.rpg_companion_data = { + userStats: parsedData.userStats, + infoBox: parsedData.infoBox, + characterThoughts: parsedData.characterThoughts, + timestamp: Date.now() + }; + // console.log('[RPG Companion] 💾 Stored tracker data on user message for history'); + } + + // Also store on assistant message if present (existing behavior) if (lastMessage && !lastMessage.is_user) { if (!lastMessage.extra) { lastMessage.extra = {}; @@ -160,58 +189,31 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough }; // console.log('[RPG Companion] Stored separate mode RPG data for message swipe', currentSwipeId); - - // Update lastGeneratedData for display AND future commit - if (parsedData.userStats) { - lastGeneratedData.userStats = parsedData.userStats; - parseUserStats(parsedData.userStats); - } - if (parsedData.infoBox) { - lastGeneratedData.infoBox = parsedData.infoBox; - } - if (parsedData.characterThoughts) { - lastGeneratedData.characterThoughts = parsedData.characterThoughts; - } - // console.log('[RPG Companion] 💾 SEPARATE MODE: Updated lastGeneratedData:', { - // userStats: lastGeneratedData.userStats ? 'exists' : 'null', - // infoBox: lastGeneratedData.infoBox ? 'exists' : 'null', - // characterThoughts: lastGeneratedData.characterThoughts ? 'exists' : 'null' - // }); - - // Only auto-commit on TRULY first generation (no committed data exists at all) - // This prevents auto-commit after refresh when we have saved committed data - const hasAnyCommittedContent = ( - (committedTrackerData.userStats && committedTrackerData.userStats.trim() !== '') || - (committedTrackerData.infoBox && committedTrackerData.infoBox.trim() !== '' && committedTrackerData.infoBox !== 'Info Box\n---\n') || - (committedTrackerData.characterThoughts && committedTrackerData.characterThoughts.trim() !== '' && committedTrackerData.characterThoughts !== 'Present Characters\n---\n') - ); - - // Only commit if we have NO committed content at all (truly first time ever) - if (!hasAnyCommittedContent) { - committedTrackerData.userStats = parsedData.userStats; - committedTrackerData.infoBox = parsedData.infoBox; - committedTrackerData.characterThoughts = parsedData.characterThoughts; - // console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data'); - } - - // Render the updated data - renderUserStats(); - renderInfoBox(); - renderThoughts(); - renderInventory(); - renderQuests(); - } else { - // No assistant message to attach to - just update display - if (parsedData.userStats) { - parseUserStats(parsedData.userStats); - } - renderUserStats(); - renderInfoBox(); - renderThoughts(); - renderInventory(); - renderQuests(); } + // Only commit on TRULY first generation (no committed data exists at all) + // This prevents auto-commit after refresh when we have saved committed data + const hasAnyCommittedContent = ( + (committedTrackerData.userStats && committedTrackerData.userStats.trim() !== '') || + (committedTrackerData.infoBox && committedTrackerData.infoBox.trim() !== '' && committedTrackerData.infoBox !== 'Info Box\n---\n') || + (committedTrackerData.characterThoughts && committedTrackerData.characterThoughts.trim() !== '' && committedTrackerData.characterThoughts !== 'Present Characters\n---\n') + ); + + // Only commit if we have NO committed content at all (truly first time ever) + if (!hasAnyCommittedContent) { + committedTrackerData.userStats = parsedData.userStats; + committedTrackerData.infoBox = parsedData.infoBox; + committedTrackerData.characterThoughts = parsedData.characterThoughts; + // console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data'); + } + + // Render the updated data (outside the message check, always render) + renderUserStats(); + renderInfoBox(); + renderThoughts(); + renderInventory(); + renderQuests(); + // Save to chat metadata saveChatData(); } diff --git a/src/systems/generation/injector.js b/src/systems/generation/injector.js index b9831d1..637d0e7 100644 --- a/src/systems/generation/injector.js +++ b/src/systems/generation/injector.js @@ -21,6 +21,7 @@ import { generateContextualSummary, DEFAULT_HTML_PROMPT } from './promptBuilder.js'; +import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js'; /** * Event handler for generation start. @@ -29,7 +30,7 @@ import { * @param {string} type - Event type * @param {Object} data - Event data */ -export function onGenerationStarted(type, data) { +export async function onGenerationStarted(type, data) { // console.log('[RPG Companion] onGenerationStarted called'); // console.log('[RPG Companion] enabled:', extensionSettings.enabled); // console.log('[RPG Companion] generationMode:', extensionSettings.generationMode); @@ -68,6 +69,10 @@ export function onGenerationStarted(type, data) { setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false); } + + // Ensure checkpoint is applied before generation + await restoreCheckpointOnLoad(); + const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null; // For SEPARATE mode only: Check if we need to commit extension data diff --git a/src/systems/generation/parser.js b/src/systems/generation/parser.js index 3a6d020..ce95b05 100644 --- a/src/systems/generation/parser.js +++ b/src/systems/generation/parser.js @@ -159,6 +159,38 @@ export function parseResponse(responseText) { cleanedResponse = cleanedResponse.replace(/[\s\S]*?<\/thinking>/gi, ''); debugLog('[RPG Parser] Removed thinking tags, new length:', cleanedResponse.length + ' chars'); + // Check if response uses XML tags (new format) + const xmlMatch = cleanedResponse.match(/([\s\S]*?)<\/trackers>/i); + if (xmlMatch) { + debugLog('[RPG Parser] ✓ Found XML tags, using XML parser'); + const trackersContent = xmlMatch[1].trim(); + + // Extract sections from XML content (sections are not in code blocks) + const statsMatch = trackersContent.match(/(User )?Stats\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*(Info Box|Present Characters)|$)/i); + if (statsMatch) { + result.userStats = stripBrackets(statsMatch[0].trim()); + debugLog('[RPG Parser] ✓ Extracted Stats from XML'); + } + + const infoBoxMatch = trackersContent.match(/Info Box\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*Present Characters|$)/i); + if (infoBoxMatch) { + result.infoBox = stripBrackets(infoBoxMatch[0].trim()); + debugLog('[RPG Parser] ✓ Extracted Info Box from XML'); + } + + const charactersMatch = trackersContent.match(/Present Characters\s*\n\s*---[\s\S]*$/i); + if (charactersMatch) { + result.characterThoughts = stripBrackets(charactersMatch[0].trim()); + debugLog('[RPG Parser] ✓ Extracted Present Characters from XML'); + } + + debugLog('[RPG Parser] Parsed from XML:', result); + return result; + } + + // Fallback to markdown code block parsing (old format) + debugLog('[RPG Parser] No XML tags found, using code block parser'); + // Extract code blocks const codeBlockRegex = /```([^`]+)```/g; const matches = [...cleanedResponse.matchAll(codeBlockRegex)]; diff --git a/src/systems/generation/promptBuilder.js b/src/systems/generation/promptBuilder.js index 040a942..829293c 100644 --- a/src/systems/generation/promptBuilder.js +++ b/src/systems/generation/promptBuilder.js @@ -5,7 +5,7 @@ import { getContext } from '../../../../../../extensions.js'; import { chat, getCurrentChatDetails, characters, this_chid } from '../../../../../../../script.js'; -import { selected_group, getGroupMembers, getGroupChat } from '../../../../../../group-chats.js'; +import { selected_group, getGroupMembers, getGroupChat, groups } from '../../../../../../group-chats.js'; import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../core/state.js'; // Type imports @@ -25,7 +25,8 @@ async function getCharacterCardsInfo() { // Check if in group chat if (selected_group) { - const group = await getGroupChat(selected_group); + // Find the current group directly from the groups array + const group = groups.find(g => g.id === selected_group); const groupMembers = getGroupMembers(selected_group); if (groupMembers && groupMembers.length > 0) { @@ -33,13 +34,15 @@ async function getCharacterCardsInfo() { // Filter out disabled (muted) members const disabledMembers = group?.disabled_members || []; + console.log('[RPG Companion] 🔍 Group ID:', selected_group, '| Disabled members:', disabledMembers); let characterIndex = 0; groupMembers.forEach((member) => { if (!member || !member.name) return; - // Skip muted characters + // Skip muted characters - check against avatar filename if (member.avatar && disabledMembers.includes(member.avatar)) { + console.log(`[RPG Companion] ❌ Skipping muted: ${member.name} (${member.avatar})`); return; } @@ -204,16 +207,27 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon // Only add tracker instructions if at least one tracker is enabled if (hasAnyTrackers) { + // Determine format based on saveTrackerHistory setting + const useXmlTags = extensionSettings.saveTrackerHistory; + const openTag = useXmlTags ? '\n' : ''; + const closeTag = useXmlTags ? '\n' : ''; + const codeBlockMarker = useXmlTags ? '' : '```'; + // Universal instruction header - instructions += `\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the same format as below, enclosed in separate Markdown code fences. Replace X with actual numbers (e.g., 69) and replace all [placeholders] with concrete in-world details that ${userName} perceives about the current scene and the present characters. Do NOT keep the brackets or placeholder text in your response. For example: [Location] becomes Forest Clearing, [Mood Emoji] becomes 😊. Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs). + if (useXmlTags) { + instructions += `\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the same format as below, enclosed in XML tags. Replace X with actual numbers (e.g., 69) and replace all [placeholders] with concrete in-world details that ${userName} perceives about the current scene and the present characters. Do NOT keep the brackets or placeholder text in your response. For example: [Location] becomes Forest Clearing, [Mood Emoji] becomes 😊. Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs). `; + } else { + instructions += `\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the same format as below, enclosed in separate Markdown code fences. Replace X with actual numbers (e.g., 69) and replace all [placeholders] with concrete in-world details that ${userName} perceives about the current scene and the present characters. Do NOT keep the brackets or placeholder text in your response. For example: [Location] becomes Forest Clearing, [Mood Emoji] becomes 😊. Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs). +`; + } // Add format specifications for each enabled tracker if (extensionSettings.showUserStats) { const userStatsConfig = trackerConfig?.userStats; const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || []; - instructions += '```\n'; + instructions += codeBlockMarker + '\n'; instructions += `${userName}'s Stats\n`; instructions += '---\n'; @@ -258,14 +272,14 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon instructions += 'Main Quests: [Short title of the currently active main quest (for example, "Save the world"), or "None"]\n'; instructions += 'Optional Quests: [Short titles of the currently active optional quests (for example, "Find Zandik\'s book"), or "None"]\n'; - instructions += '```\n\n'; + instructions += codeBlockMarker + '\n\n'; } if (extensionSettings.showInfoBox) { const infoBoxConfig = trackerConfig?.infoBox; const widgets = infoBoxConfig?.widgets || {}; - instructions += '```\n'; + instructions += codeBlockMarker + '\n'; instructions += 'Info Box\n'; instructions += '---\n'; @@ -290,7 +304,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon instructions += 'Recent Events: [Up to three past events leading to the ongoing scene (short descriptors with no details, for example, "last-night date with Mary")]\n'; } - instructions += '```\n\n'; + instructions += codeBlockMarker + '\n\n'; } if (extensionSettings.showCharacterThoughts) { @@ -301,7 +315,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon const characterStats = presentCharsConfig?.characterStats; const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || []; - instructions += '```\n'; + instructions += codeBlockMarker + '\n'; instructions += 'Present Characters\n'; instructions += '---\n'; @@ -346,7 +360,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon instructions += `- … (Repeat the format above for every other present major character)\n`; - instructions += '```\n\n'; + instructions += codeBlockMarker + '\n\n'; } // Only add continuation instruction if includeContinuation is true @@ -560,8 +574,10 @@ export async function generateSeparateUpdatePrompt() { content: systemMessage }); + // /hide command automatically handles checkpoint filtering // Add chat history as separate user/assistant messages const recentMessages = chat.slice(-depth); + for (const message of recentMessages) { messages.push({ role: message.is_user ? 'user' : 'assistant', diff --git a/src/systems/integration/sillytavern.js b/src/systems/integration/sillytavern.js index f490555..29d56ba 100644 --- a/src/systems/integration/sillytavern.js +++ b/src/systems/integration/sillytavern.js @@ -34,6 +34,10 @@ import { renderQuests } from '../rendering/quests.js'; // Utils import { getSafeThumbnailUrl } from '../../utils/avatars.js'; +// Chapter checkpoint +import { updateAllCheckpointIndicators } from '../ui/checkpointUI.js'; +import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js'; + /** * Commits the tracker data from the last assistant message to be used as source for next generation. * This should be called when the user has replied to a message, ensuring all swipes of the next @@ -158,14 +162,20 @@ export async function onMessageReceived(data) { // Remove the tracker code blocks from the visible message let cleanedMessage = responseText; - // Remove all code blocks that contain tracker data - cleanedMessage = cleanedMessage.replace(/```[^`]*?Stats\s*\n\s*---[^`]*?```\s*/gi, ''); - cleanedMessage = cleanedMessage.replace(/```[^`]*?Info Box\s*\n\s*---[^`]*?```\s*/gi, ''); - cleanedMessage = cleanedMessage.replace(/```[^`]*?Present Characters\s*\n\s*---[^`]*?```\s*/gi, ''); - // Remove any stray "---" dividers that might appear after the code blocks - cleanedMessage = cleanedMessage.replace(/^\s*---\s*$/gm, ''); - // Clean up multiple consecutive newlines - cleanedMessage = cleanedMessage.replace(/\n{3,}/g, '\n\n'); + + // Only remove trackers if saveTrackerHistory is disabled + // When enabled, trackers are in XML tags which SillyTavern auto-hides + if (!extensionSettings.saveTrackerHistory) { + // Remove all code blocks that contain tracker data + cleanedMessage = cleanedMessage.replace(/```[^`]*?Stats\s*\n\s*---[^`]*?```\s*/gi, ''); + cleanedMessage = cleanedMessage.replace(/```[^`]*?Info Box\s*\n\s*---[^`]*?```\s*/gi, ''); + cleanedMessage = cleanedMessage.replace(/```[^`]*?Present Characters\s*\n\s*---[^`]*?```\s*/gi, ''); + // Remove any stray "---" dividers that might appear after the code blocks + cleanedMessage = cleanedMessage.replace(/^\s*---\s*$/gm, ''); + // Clean up multiple consecutive newlines + cleanedMessage = cleanedMessage.replace(/\n{3,}/g, '\n\n'); + } + // Note: XML tags are automatically hidden by SillyTavern // Update the message in chat history lastMessage.mes = cleanedMessage.trim(); @@ -213,6 +223,9 @@ export async function onMessageReceived(data) { setIsPlotProgression(false); // console.log('[RPG Companion] Plot progression generation completed'); } + + // Re-apply checkpoint in case SillyTavern unhid messages during generation + await restoreCheckpointOnLoad(); } /** @@ -243,6 +256,9 @@ export function onCharacterChanged() { // Update chat thought overlays updateChatThoughts(); + + // Update checkpoint indicators for the loaded chat + updateAllCheckpointIndicators(); } /** @@ -362,3 +378,13 @@ export function clearExtensionPrompts() { // Note: rpg-companion-plot is not cleared here since it's passed via quiet_prompt option // console.log('[RPG Companion] Cleared all extension prompts'); } + +/** + * Event handler for when generation stops or ends + * Re-applies checkpoint if SillyTavern unhid messages + */ +export async function onGenerationEnded() { + // SillyTavern may auto-unhide messages when generation stops + // Re-apply checkpoint if one exists + await restoreCheckpointOnLoad(); +} diff --git a/src/systems/ui/checkpointUI.js b/src/systems/ui/checkpointUI.js new file mode 100644 index 0000000..54cfeec --- /dev/null +++ b/src/systems/ui/checkpointUI.js @@ -0,0 +1,285 @@ +/** + * Chapter Checkpoint UI Module + * Adds UI elements for chapter checkpoint functionality + */ + +import { getContext } from '../../../../../../extensions.js'; +import { i18n } from '../../core/i18n.js'; +import { + setChapterCheckpoint, + clearChapterCheckpoint, + isCheckpointMessage +} from '../features/chapterCheckpoint.js'; + +/** + * Adds the chapter checkpoint button to a message's extra menu + * @param {number} messageId - The message index + * @param {HTMLElement} menu - The message menu element + */ +export function addCheckpointButtonToMessage(messageId, menu) { + if (!menu) return; + + const isCheckpoint = isCheckpointMessage(messageId); + + // Create the menu item + const menuItem = document.createElement('div'); + menuItem.className = 'extraMesButtonsHint list-group-item flex-container flexGap5'; + const translationKey = isCheckpoint ? 'checkpoint.clearChapterStart' : 'checkpoint.setChapterStart'; + menuItem.setAttribute('data-i18n', translationKey); + menuItem.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'; + + // Icon only (no text label) + const icon = document.createElement('i'); + icon.className = isCheckpoint ? 'fa-solid fa-bookmark' : 'fa-regular fa-bookmark'; + icon.style.color = isCheckpoint ? '#4a9eff' : ''; + + menuItem.appendChild(icon); + + // Click handler + menuItem.addEventListener('click', (e) => { + e.stopPropagation(); + + const wasCheckpoint = isCheckpointMessage(messageId); + + if (wasCheckpoint) { + clearChapterCheckpoint(); + } else { + setChapterCheckpoint(messageId); + } + + // Update this button immediately + const newIsCheckpoint = isCheckpointMessage(messageId); + icon.className = newIsCheckpoint ? 'fa-solid fa-bookmark' : 'fa-regular fa-bookmark'; + icon.style.color = newIsCheckpoint ? '#4a9eff' : ''; + menuItem.title = newIsCheckpoint + ? 'Clear Chapter Start' + : 'Set Chapter Start: When bookmarked, this message will count as the first message in the chat history, skipping earlier ones'; + const newTranslationKey = newIsCheckpoint ? 'checkpoint.clearChapterStart' : 'checkpoint.setChapterStart'; + menuItem.setAttribute('data-i18n', newTranslationKey); + + // Update indicators in all messages + updateAllCheckpointIndicators(); + }); + + return menuItem; +} + +/** + * Adds visual indicators to messages that are checkpoints + * @param {number} messageId - The message index + * @param {HTMLElement} messageBlock - The message DOM element + */ +export function addCheckpointIndicator(messageId, messageBlock) { + if (!messageBlock) return; + + const isCheckpoint = isCheckpointMessage(messageId); + + // Remove existing indicator if present + const existingIndicator = messageBlock.querySelector('.rpg-checkpoint-indicator'); + if (existingIndicator) { + existingIndicator.remove(); + } + + if (!isCheckpoint) return; + + // Add checkpoint indicator + const indicator = document.createElement('div'); + indicator.className = 'rpg-checkpoint-indicator'; + const indicatorText = i18n.getTranslation('checkpoint.indicator') || 'Chapter Start'; + const tooltipText = i18n.getTranslation('checkpoint.tooltip') || 'Messages before this point are excluded from context'; + indicator.innerHTML = ` + + ${indicatorText} + `; + indicator.title = tooltipText; + + // Insert at the beginning of the message + const mesText = messageBlock.querySelector('.mes_text'); + if (mesText && mesText.parentNode) { + mesText.parentNode.insertBefore(indicator, mesText); + } +} + +/** + * Updates checkpoint indicators for all messages + */ +export function updateAllCheckpointIndicators() { + const context = getContext(); + const chat = context.chat; + + if (!chat) return; + + // Clear all processed flags so buttons can be updated + document.querySelectorAll('.extraMesButtons[data-checkpoint-processed]').forEach(menu => { + delete menu.dataset.checkpointProcessed; + }); + + // Update all message blocks + const messageBlocks = document.querySelectorAll('.mes'); + messageBlocks.forEach((block) => { + // Get the actual message ID from the mesid attribute + const messageId = Number(block.getAttribute('mesid')); + + if (isNaN(messageId)) return; + + addCheckpointIndicator(messageId, block); + + // Also update any open menus for this message + const menu = block.querySelector('.extraMesButtons'); + if (menu) { + updateCheckpointButtonInMenu(menu, messageId); + } + }); +} + +/** + * Initializes the chapter checkpoint UI + */ +export function initChapterCheckpointUI() { + // Listen for checkpoint changes + document.addEventListener('rpg-companion-checkpoint-changed', () => { + updateAllCheckpointIndicators(); + }); + + // Listen for chat changes to update indicators + const context = getContext(); + if (context && context.eventSource) { + // Update checkpoint indicators when messages are rendered + const observer = new MutationObserver((mutations) => { + let shouldUpdate = false; + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE && + node.classList && node.classList.contains('mes')) { + shouldUpdate = true; + } + }); + }); + + if (shouldUpdate) { + // Debounce updates to avoid excessive re-rendering + clearTimeout(window.rpgCheckpointUpdateTimeout); + window.rpgCheckpointUpdateTimeout = setTimeout(() => { + updateAllCheckpointIndicators(); + }, 100); + } + }); + + const chatContainer = document.getElementById('chat'); + if (chatContainer) { + observer.observe(chatContainer, { + childList: true, + subtree: false + }); + } + } + + // Update indicators on initialization + updateAllCheckpointIndicators(); +} + +/** + * Injects checkpoint button into message menus + * This should be called when SillyTavern renders message menus + */ +export function injectCheckpointButton() { + // Direct approach: Hook into when extraMesButtons elements appear or are populated + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + // Check for added nodes + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + // Check if extraMesButtons container was added + if (node.classList && node.classList.contains('extraMesButtons')) { + processExtraMesButtons(node); + } + + // Also check if extraMesButtons exists within added subtree + if (node.querySelector) { + const extraButtons = node.querySelectorAll('.extraMesButtons'); + extraButtons.forEach(processExtraMesButtons); + } + } + }); + + // Check if nodes were added TO an extraMesButtons container + if (mutation.target && mutation.target.classList && + mutation.target.classList.contains('extraMesButtons')) { + processExtraMesButtons(mutation.target); + } + }); + }); + + // Observe the chat container + const chatContainer = document.getElementById('chat'); + if (chatContainer) { + observer.observe(chatContainer, { + childList: true, + subtree: true + }); + + // Process any existing menus on initialization + const existingMenus = chatContainer.querySelectorAll('.extraMesButtons'); + existingMenus.forEach(processExtraMesButtons); + } +} + +/** + * Process an extraMesButtons container to add checkpoint button + * @param {HTMLElement} menu - The extraMesButtons container + */ +function processExtraMesButtons(menu) { + if (!menu) return; + + // Find the message block + const messageBlock = menu.closest('.mes'); + if (!messageBlock) return; + + // Get the message ID from the mesid attribute (SillyTavern's standard way) + const messageId = Number(messageBlock.getAttribute('mesid')); + + if (isNaN(messageId)) return; + + // Check if button already exists + if (!menu.dataset.checkpointProcessed) { + // Mark as processed + menu.dataset.checkpointProcessed = 'true'; + + // Add checkpoint button + const checkpointBtn = addCheckpointButtonToMessage(messageId, menu); + if (checkpointBtn) { + checkpointBtn.classList.add('rpg-checkpoint-button'); + menu.appendChild(checkpointBtn); + } + } +} + +/** + * Update the checkpoint button in an existing menu + * @param {HTMLElement} menu - The extraMesButtons container + * @param {number} messageId - The message index + */ +function updateCheckpointButtonInMenu(menu, messageId) { + if (!menu) return; + + const existingButton = menu.querySelector('.rpg-checkpoint-button'); + 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/style.css b/style.css index deda25c..db72d6a 100644 --- a/style.css +++ b/style.css @@ -6637,3 +6637,54 @@ body:has(.rpg-panel.rpg-position-left) #sheld { font-size: clamp(14px, 3vw, 18px) !important; } } + +/* ============================================ + CHAPTER CHECKPOINT STYLES + ============================================ */ + +/* Checkpoint indicator in messages */ +.rpg-checkpoint-indicator { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + margin-bottom: 12px; + background: linear-gradient(135deg, rgba(74, 158, 255, 0.15) 0%, rgba(74, 158, 255, 0.05) 100%); + border-left: 4px solid #4a9eff; + border-radius: 4px; + font-size: 13px; + font-weight: 600; + color: #4a9eff; + animation: checkpoint-fade-in 0.3s ease-out; +} + +.rpg-checkpoint-indicator i { + font-size: 16px; +} + +@keyframes checkpoint-fade-in { + from { + opacity: 0; + transform: translateX(-10px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +/* Optional: Add a subtle glow effect for checkpoint messages */ +.mes:has(.rpg-checkpoint-indicator) { + position: relative; +} + +.mes:has(.rpg-checkpoint-indicator)::before { + content: ''; + position: absolute; + left: -4px; + top: 0; + bottom: 0; + width: 3px; + background: linear-gradient(180deg, #4a9eff 0%, transparent 100%); + opacity: 0.5; +} diff --git a/template.html b/template.html index 76af63e..47f3021 100644 --- a/template.html +++ b/template.html @@ -267,6 +267,14 @@ When set, the extension will not inject tracker prompts, examples, or HTML instructions according to the selected mode when a guided generation (via `instruct` or `quiet_prompt`) is detected. Useful when using GuidedGenerations or similar extensions. + + + When enabled, tracker data is saved in chat history for each message. In Together mode, trackers appear in <trackers> XML tags (hidden from display). In Separate mode, tracker data is stored in message metadata. When disabled, only the most recent trackers are kept. + +