diff --git a/index.js b/index.js
index b5ae314..0c4e2e1 100644
--- a/index.js
+++ b/index.js
@@ -19,6 +19,7 @@ import {
$infoBoxContainer,
$thoughtsContainer,
$inventoryContainer,
+ $questsContainer,
setExtensionSettings,
updateExtensionSettings,
setLastGeneratedData,
@@ -33,7 +34,8 @@ import {
setUserStatsContainer,
setInfoBoxContainer,
setThoughtsContainer,
- setInventoryContainer
+ setInventoryContainer,
+ setQuestsContainer
} from './src/core/state.js';
import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData } from './src/core/persistence.js';
import { registerAllEvents } from './src/core/events.js';
@@ -61,6 +63,7 @@ import {
createThoughtPanel
} from './src/systems/rendering/thoughts.js';
import { renderInventory } from './src/systems/rendering/inventory.js';
+import { renderQuests } from './src/systems/rendering/quests.js';
// Interaction modules
import { initInventoryEventListeners } from './src/systems/interaction/inventoryActions.js';
@@ -98,22 +101,19 @@ import {
setupMobileTabs,
removeMobileTabs,
setupMobileKeyboardHandling,
- setupContentEditableScrolling,
- setupRefreshButtonDrag,
- setupDebugButtonDrag
+ setupContentEditableScrolling
} from './src/systems/ui/mobile.js';
import {
setupDesktopTabs,
removeDesktopTabs
} from './src/systems/ui/desktop.js';
-import {
- updateDebugUIVisibility
-} from './src/systems/ui/debug.js';
// Feature modules
import { setupPlotButtons, sendPlotProgression } from './src/systems/features/plotProgression.js';
import { setupClassicStatsButtons } from './src/systems/features/classicStats.js';
import { ensureHtmlCleaningRegex, detectConflictingRegexScripts } from './src/systems/features/htmlCleaning.js';
+import { setupMemoryRecollectionButton, updateMemoryRecollectionButton } from './src/systems/features/memoryRecollection.js';
+import { initLorebookLimiter } from './src/systems/features/lorebookLimiter.js';
// Integration modules
import {
@@ -193,6 +193,9 @@ function addExtensionSettings() {
// Re-create thought bubbles when re-enabled
updateChatThoughts(); // This will re-create the thought bubble if data exists
}
+
+ // Update Memory Recollection button visibility
+ updateMemoryRecollectionButton();
});
}
@@ -214,28 +217,13 @@ async function initUI() {
`;
$('body').append(mobileToggleHtml);
- // Add mobile refresh button (same pattern as toggle button)
- const mobileRefreshHtml = `
-
- `;
- $('body').append(mobileRefreshHtml);
-
- // Add debug toggle FAB button (same pattern as other mobile FABs)
- const debugToggleHtml = `
-
- `;
- $('body').append(debugToggleHtml);
-
// Cache UI elements using state setters
setPanelContainer($('#rpg-companion-panel'));
setUserStatsContainer($('#rpg-user-stats'));
setInfoBoxContainer($('#rpg-info-box'));
setThoughtsContainer($('#rpg-thoughts'));
setInventoryContainer($('#rpg-inventory'));
+ setQuestsContainer($('#rpg-quests'));
// Set up event listeners (enable/disable is handled in Extensions tab)
$('#rpg-toggle-auto-update').on('change', function() {
@@ -257,6 +245,12 @@ async function initUI() {
saveSettings();
});
+ $('#rpg-memory-messages').on('change', function() {
+ const value = $(this).val();
+ extensionSettings.memoryMessagesToProcess = parseInt(String(value));
+ saveSettings();
+ });
+
$('#rpg-generation-mode').on('change', function() {
extensionSettings.generationMode = String($(this).val());
saveSettings();
@@ -284,10 +278,6 @@ async function initUI() {
extensionSettings.showCharacterThoughts = $(this).prop('checked');
saveSettings();
updateSectionVisibility();
- // Refresh the content when toggling on/off
- if (extensionSettings.showCharacterThoughts) {
- renderThoughts();
- }
});
$('#rpg-toggle-inventory').on('change', function() {
@@ -316,77 +306,18 @@ async function initUI() {
togglePlotButtons();
});
- $('#rpg-toggle-debug-mode').on('change', function() {
- extensionSettings.debugMode = $(this).prop('checked');
- saveSettings();
- updateDebugUIVisibility();
- });
-
$('#rpg-toggle-animations').on('change', function() {
extensionSettings.enableAnimations = $(this).prop('checked');
saveSettings();
toggleAnimations();
});
- // Bind to both desktop and mobile refresh buttons
- $('#rpg-manual-update, #rpg-manual-update-mobile').on('click', async function() {
- // Get mobile button reference
- const $mobileBtn = $('#rpg-manual-update-mobile');
-
- // Skip if we just finished dragging the mobile button
- if ($mobileBtn.data('just-dragged')) {
- console.log('[RPG Companion] Click blocked - just finished dragging refresh button');
- return;
- }
-
+ $('#rpg-manual-update').on('click', async function() {
if (!extensionSettings.enabled) {
// console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.');
return;
}
-
- // Remove focus to prevent sticky black state on mobile
- $(this).blur();
-
- // Add spinning animation to mobile button
- $mobileBtn.addClass('spinning');
-
- try {
- await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
- } finally {
- // Remove spinning animation when done
- $mobileBtn.removeClass('spinning');
- }
- });
-
- // Reset FAB positions button
- $('#rpg-reset-fab-positions').on('click', function() {
- console.log('[RPG Companion] Resetting FAB positions to defaults');
-
- // Reset to defaults (top-left stacked)
- extensionSettings.mobileFabPosition = {
- top: 'calc(var(--topBarBlockSize) + 20px)',
- left: '12px'
- };
- extensionSettings.mobileRefreshPosition = {
- top: 'calc(var(--topBarBlockSize) + 80px)',
- left: '12px'
- };
- extensionSettings.debugFabPosition = {
- top: 'calc(var(--topBarBlockSize) + 140px)',
- left: '12px'
- };
-
- // Save settings
- saveSettings();
-
- // Apply positions immediately to visible buttons
- $('#rpg-mobile-toggle').css(extensionSettings.mobileFabPosition);
- $('#rpg-manual-update-mobile').css(extensionSettings.mobileRefreshPosition);
- $('#rpg-debug-toggle').css(extensionSettings.debugFabPosition);
-
- // Show success feedback
- toastr.success('Button positions reset to defaults', 'RPG Companion');
- console.log('[RPG Companion] FAB positions reset successfully');
+ await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
});
$('#rpg-stat-bar-color-low').on('change', function() {
@@ -456,6 +387,7 @@ async function initUI() {
$('#rpg-toggle-auto-update').prop('checked', extensionSettings.autoUpdate);
$('#rpg-position-select').val(extensionSettings.panelPosition);
$('#rpg-update-depth').val(extensionSettings.updateDepth);
+ $('#rpg-memory-messages').val(extensionSettings.memoryMessagesToProcess || 16);
$('#rpg-use-separate-preset').prop('checked', extensionSettings.useSeparatePreset);
$('#rpg-toggle-user-stats').prop('checked', extensionSettings.showUserStats);
$('#rpg-toggle-info-box').prop('checked', extensionSettings.showInfoBox);
@@ -464,7 +396,6 @@ async function initUI() {
$('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat);
$('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt);
$('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons);
- $('#rpg-toggle-debug-mode').prop('checked', extensionSettings.debugMode);
$('#rpg-toggle-animations').prop('checked', extensionSettings.enableAnimations);
$('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow);
$('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh);
@@ -499,6 +430,7 @@ async function initUI() {
renderInfoBox();
renderThoughts();
renderInventory();
+ renderQuests();
updateDiceDisplay();
setupDiceRoller();
setupClassicStatsButtons();
@@ -507,12 +439,13 @@ async function initUI() {
setupPlotButtons(sendPlotProgression);
setupMobileKeyboardHandling();
setupContentEditableScrolling();
- setupRefreshButtonDrag();
- setupDebugButtonDrag();
initInventoryEventListeners();
- // Initialize debug UI if debug mode is enabled
- updateDebugUIVisibility();
+ // Setup Memory Recollection button in World Info
+ setupMemoryRecollectionButton();
+
+ // Initialize Lorebook Limiter
+ initLorebookLimiter();
}
diff --git a/src/core/config.js b/src/core/config.js
index 8e5b1ba..fc633b6 100644
--- a/src/core/config.js
+++ b/src/core/config.js
@@ -46,17 +46,9 @@ export const defaultSettings = {
statBarColorHigh: '#33cc66', // Color for high stat values (green)
enableAnimations: true, // Enable smooth animations for stats and content updates
mobileFabPosition: {
- top: 'calc(var(--topBarBlockSize) + 20px)',
- left: '12px'
- }, // Saved position for mobile FAB button (top-left, stacked vertically)
- mobileRefreshPosition: {
- top: 'calc(var(--topBarBlockSize) + 80px)',
- left: '12px'
- }, // Saved position for mobile refresh button (below toggle button)
- debugFabPosition: {
- top: 'calc(var(--topBarBlockSize) + 140px)',
- left: '12px'
- }, // Saved position for debug FAB button (below refresh button)
+ top: 'calc(var(--topBarBlockSize) + 60px)',
+ right: '12px'
+ }, // Saved position for mobile FAB button
userStats: {
health: 100,
satiety: 100,
@@ -83,5 +75,6 @@ export const defaultSettings = {
},
lastDiceRoll: null, // Store last dice roll result
collapsedInventoryLocations: [], // Array of collapsed storage location names
- debugMode: false // Enable debug logging visible in UI (for mobile debugging)
+ debugMode: false, // Enable debug logging visible in UI (for mobile debugging)
+ memoryMessagesToProcess: 16 // Number of messages to process per batch in memory recollection
};
diff --git a/src/core/persistence.js b/src/core/persistence.js
index a0f3c69..8fd7897 100644
--- a/src/core/persistence.js
+++ b/src/core/persistence.js
@@ -128,6 +128,7 @@ export function saveChatData() {
chat_metadata.rpg_companion = {
userStats: extensionSettings.userStats,
classicStats: extensionSettings.classicStats,
+ quests: extensionSettings.quests,
lastGeneratedData: lastGeneratedData,
committedTrackerData: committedTrackerData,
timestamp: Date.now()
@@ -222,6 +223,17 @@ export function loadChatData() {
extensionSettings.classicStats = { ...savedData.classicStats };
}
+ // Restore quests
+ if (savedData.quests) {
+ extensionSettings.quests = { ...savedData.quests };
+ } else {
+ // Initialize with defaults if not present
+ extensionSettings.quests = {
+ main: "None",
+ optional: []
+ };
+ }
+
// Restore last generated data
if (savedData.lastGeneratedData) {
setLastGeneratedData({ ...savedData.lastGeneratedData });
diff --git a/src/core/state.js b/src/core/state.js
index 23d0b18..d191ea6 100644
--- a/src/core/state.js
+++ b/src/core/state.js
@@ -34,17 +34,9 @@ export let extensionSettings = {
statBarColorHigh: '#33cc66', // Color for high stat values (green)
enableAnimations: true, // Enable smooth animations for stats and content updates
mobileFabPosition: {
- top: 'calc(var(--topBarBlockSize) + 20px)',
- left: '12px'
- }, // Saved position for mobile FAB button (top-left, stacked vertically)
- mobileRefreshPosition: {
- top: 'calc(var(--topBarBlockSize) + 80px)',
- left: '12px'
- }, // Saved position for mobile refresh button (below toggle button)
- debugFabPosition: {
- top: 'calc(var(--topBarBlockSize) + 140px)',
- left: '12px'
- }, // Saved position for debug FAB button (below refresh button)
+ top: 'calc(var(--topBarBlockSize) + 60px)',
+ right: '12px'
+ }, // Saved position for mobile FAB button
userStats: {
health: 100,
satiety: 100,
@@ -61,6 +53,10 @@ export let extensionSettings = {
assets: "None"
}
},
+ quests: {
+ main: "None", // Current main quest title
+ optional: [] // Array of optional quest titles
+ },
level: 1, // User's character level
classicStats: {
str: 10,
@@ -77,7 +73,8 @@ export let extensionSettings = {
stored: 'list', // 'list' or 'grid' view mode for Stored section
assets: 'list' // 'list' or 'grid' view mode for Assets section
},
- debugMode: false // Enable debug logging visible in UI (for mobile debugging)
+ debugMode: false, // Enable debug logging visible in UI (for mobile debugging)
+ memoryMessagesToProcess: 16 // Number of messages to process per batch in memory recollection
};
/**
@@ -121,6 +118,25 @@ export let isPlotProgression = false;
*/
export let pendingDiceRoll = null;
+/**
+ * Debug logs array for troubleshooting
+ */
+export let debugLogs = [];
+
+/**
+ * Add a debug log entry
+ * @param {string} message - The log message
+ * @param {any} data - Optional data to log
+ */
+export function addDebugLog(message, data = null) {
+ const timestamp = new Date().toISOString();
+ debugLogs.push({ timestamp, message, data });
+ // Keep only last 100 logs
+ if (debugLogs.length > 100) {
+ debugLogs.shift();
+ }
+}
+
/**
* Feature flags for gradual rollout of new features
*/
@@ -128,12 +144,6 @@ export const FEATURE_FLAGS = {
useNewInventory: true // Enable v2 inventory system with categorized storage
};
-/**
- * Debug logs storage for mobile-friendly debugging
- * Stores parser logs that can be viewed in UI
- */
-export let debugLogs = [];
-
/**
* Fallback avatar image (base64-encoded SVG with "?" icon)
* Using base64 to avoid quote-encoding issues in HTML attributes
@@ -148,6 +158,7 @@ export let $userStatsContainer = null;
export let $infoBoxContainer = null;
export let $thoughtsContainer = null;
export let $inventoryContainer = null;
+export let $questsContainer = null;
/**
* State setters - provide controlled mutation of state variables
@@ -216,25 +227,6 @@ export function setInventoryContainer($element) {
$inventoryContainer = $element;
}
-export function addDebugLog(message, data = null) {
- const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; // HH:MM:SS
- const logEntry = {
- timestamp,
- message,
- data: data ? JSON.stringify(data, null, 2) : null
- };
- debugLogs.push(logEntry);
-
- // Keep only last 100 entries to avoid memory issues
- if (debugLogs.length > 100) {
- debugLogs.shift();
- }
-}
-
-export function clearDebugLogs() {
- debugLogs = [];
-}
-
-export function getDebugLogs() {
- return debugLogs;
+export function setQuestsContainer($element) {
+ $questsContainer = $element;
}
diff --git a/src/systems/features/lorebookLimiter.js b/src/systems/features/lorebookLimiter.js
new file mode 100644
index 0000000..9d2e6a5
--- /dev/null
+++ b/src/systems/features/lorebookLimiter.js
@@ -0,0 +1,267 @@
+/**
+ * Lorebook Limiter Module
+ * Adds maximum activation limit to SillyTavern's World Info system
+ */
+
+import { eventSource, event_types } from '../../../../../../../script.js';
+
+let maxActivations = 0; // 0 = unlimited
+let settingsInitialized = false;
+let activatedEntriesThisGeneration = [];
+
+/**
+ * Initialize the lorebook limiter
+ */
+export function initLorebookLimiter() {
+ console.log('[Lorebook Limiter] Initializing...');
+
+ // Load saved setting
+ const saved = localStorage.getItem('rpg_max_lorebook_activations');
+ if (saved !== null) {
+ maxActivations = parseInt(saved, 10);
+ }
+
+ // Wait for World Info settings to be ready
+ eventSource.on('worldInfoSettings', () => {
+ setTimeout(() => {
+ if (!settingsInitialized) {
+ injectMaxActivationsUI();
+ settingsInitialized = true;
+ }
+ }, 100);
+ });
+
+ // Try when the WI drawer is opened
+ const tryInjectOnClick = () => {
+ const wiButton = document.querySelector('#WIDrawerIcon');
+ if (wiButton) {
+ wiButton.addEventListener('click', () => {
+ setTimeout(() => {
+ if (!settingsInitialized) {
+ injectMaxActivationsUI();
+ settingsInitialized = true;
+ }
+ }, 300);
+ });
+ console.log('[Lorebook Limiter] Attached to WI drawer button');
+ }
+ };
+
+ // Also try on app ready
+ eventSource.on('app_ready', () => {
+ setTimeout(() => {
+ tryInjectOnClick();
+ if (!settingsInitialized) {
+ injectMaxActivationsUI();
+ settingsInitialized = true;
+ }
+ }, 1000);
+ });
+
+ // Patch the world info activation system
+ patchWorldInfoActivation();
+}
+
+/**
+ * Inject the Maximum Activations UI into World Info settings
+ */
+function injectMaxActivationsUI() {
+ console.log('[Lorebook Limiter] Injecting UI...');
+
+ // Check if already injected
+ if (document.querySelector('#rpg-max-lorebook-activations-container')) {
+ console.log('[Lorebook Limiter] UI already injected');
+ return;
+ }
+
+ // Find the Memory Recollection button - we'll add our UI right after it
+ const memoryButton = document.querySelector('.rpg-memory-recollection-btn');
+
+ if (!memoryButton) {
+ console.log('[Lorebook Limiter] Memory Recollection button not found yet');
+ return;
+ }
+
+ const container = memoryButton.parentElement;
+ if (!container) {
+ console.log('[Lorebook Limiter] Could not find button container');
+ return;
+ }
+
+ console.log('[Lorebook Limiter] Found Memory Recollection button, injecting slider after it');
+
+ // Create the UI - styled to match the extension's theme
+ const settingHTML = `
+
+
+ Limit entries per generation (0 = unlimited)
+
+ `;
+
+ // Insert after the Memory Recollection button
+ memoryButton.insertAdjacentHTML('afterend', settingHTML);
+
+ // Add event listener
+ const input = document.querySelector('#rpg-max-activations-input');
+
+ if (input) {
+ input.addEventListener('input', (e) => {
+ let value = parseInt(e.target.value, 10);
+ if (isNaN(value) || value < 0) value = 0;
+ if (value > 9999) value = 9999;
+
+ maxActivations = value;
+ e.target.value = value;
+ localStorage.setItem('rpg_max_lorebook_activations', value.toString());
+ console.log(`[Lorebook Limiter] Max activations set to: ${value}`);
+ });
+
+ console.log('[Lorebook Limiter] ✅ UI injected successfully');
+ }
+}
+
+/**
+ * Patch the world info activation system to enforce the limit
+ */
+function patchWorldInfoActivation() {
+ console.log('[Lorebook Limiter] Setting up activation limiter...');
+
+ // We need to intercept at the module level
+ // Use a Proxy on the module loader
+ const originalDefine = window.define;
+ const originalRequire = window.require;
+
+ // Try multiple approaches to hook into the WI system
+ const attemptPatch = () => {
+ // Approach 1: Direct window access
+ if (window.getWorldInfoPrompt) {
+ const original = window.getWorldInfoPrompt;
+ window.getWorldInfoPrompt = async function(...args) {
+ const result = await original.apply(this, args);
+
+ if (maxActivations > 0 && result) {
+ // Count entries in the worldInfoString
+ const lines = (result.worldInfoBefore + result.worldInfoAfter).split('\n').filter(l => l.trim());
+ if (lines.length > maxActivations) {
+ console.log(`[Lorebook Limiter] Limiting ${lines.length} WI lines to ${maxActivations}`);
+
+ // Trim the strings
+ const limitedLines = lines.slice(0, maxActivations);
+ result.worldInfoBefore = limitedLines.join('\n');
+ result.worldInfoAfter = '';
+ result.worldInfoString = result.worldInfoBefore;
+
+ console.log(`[Lorebook Limiter] ✅ Limited from ${lines.length} to ${limitedLines.length} entries`);
+ }
+ }
+
+ return result;
+ };
+
+ console.log('[Lorebook Limiter] ✅ Patched window.getWorldInfoPrompt');
+ return true;
+ }
+
+ // Approach 2: Through SillyTavern context
+ if (window.SillyTavern?.getContext) {
+ const ctx = window.SillyTavern.getContext();
+ if (ctx.getWorldInfoPrompt) {
+ const original = ctx.getWorldInfoPrompt;
+ ctx.getWorldInfoPrompt = async function(...args) {
+ const result = await original.apply(this, args);
+
+ if (maxActivations > 0 && result) {
+ const lines = (result.worldInfoBefore + result.worldInfoAfter).split('\n').filter(l => l.trim());
+ if (lines.length > maxActivations) {
+ console.log(`[Lorebook Limiter] Limiting ${lines.length} WI entries to ${maxActivations}`);
+ const limitedLines = lines.slice(0, maxActivations);
+ result.worldInfoBefore = limitedLines.join('\n');
+ result.worldInfoAfter = '';
+ result.worldInfoString = result.worldInfoBefore;
+ }
+ }
+
+ return result;
+ };
+
+ console.log('[Lorebook Limiter] ✅ Patched SillyTavern.getContext().getWorldInfoPrompt');
+ return true;
+ }
+
+ // Try checkWorldInfo instead
+ if (ctx.checkWorldInfo) {
+ const original = ctx.checkWorldInfo;
+ ctx.checkWorldInfo = async function(...args) {
+ const result = await original.apply(this, args);
+
+ if (maxActivations > 0 && result?.allActivatedEntries?.size > maxActivations) {
+ console.log(`[Lorebook Limiter] Limiting ${result.allActivatedEntries.size} entries to ${maxActivations}`);
+
+ // Keep only first N entries
+ const entries = Array.from(result.allActivatedEntries.entries());
+ result.allActivatedEntries = new Map(entries.slice(0, maxActivations));
+
+ // Also limit the string output
+ const lines = (result.worldInfoBefore + result.worldInfoAfter).split('\n').filter(l => l.trim());
+ if (lines.length > maxActivations) {
+ const limitedLines = lines.slice(0, maxActivations);
+ result.worldInfoBefore = limitedLines.join('\n');
+ result.worldInfoAfter = '';
+ }
+
+ console.log(`[Lorebook Limiter] ✅ Limited to ${result.allActivatedEntries.size} entries`);
+ }
+
+ return result;
+ };
+
+ console.log('[Lorebook Limiter] ✅ Patched SillyTavern.getContext().checkWorldInfo');
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ // Try immediately
+ if (!attemptPatch()) {
+ // Retry after delays
+ setTimeout(() => attemptPatch() || setTimeout(() => attemptPatch(), 2000), 1000);
+ }
+}
+
+/**
+ * Update the maximum activations limit
+ */
+export function setMaxActivations(value) {
+ maxActivations = parseInt(value, 10);
+ localStorage.setItem('rpg_max_lorebook_activations', value.toString());
+
+ // Update UI if it exists
+ const valueDisplay = document.querySelector('#rpg-max-activations-value');
+ const slider = document.querySelector('#rpg-max-activations-slider');
+
+ if (valueDisplay) {
+ valueDisplay.textContent = value;
+ }
+ if (slider) {
+ slider.value = value;
+ }
+}
+
+/**
+ * Get current maximum activations limit
+ */
+export function getMaxActivations() {
+ return maxActivations;
+}
diff --git a/src/systems/features/memoryRecollection.js b/src/systems/features/memoryRecollection.js
new file mode 100644
index 0000000..95c3396
--- /dev/null
+++ b/src/systems/features/memoryRecollection.js
@@ -0,0 +1,843 @@
+/**
+ * Memory Recollection Module
+ * Handles generation of lorebook entries from chat history
+ */
+
+import { chat, characters, this_chid, generateRaw, substituteParams, eventSource, event_types } from '../../../../../../../script.js';
+import { selected_group } from '../../../../../../group-chats.js';
+import { extensionSettings, addDebugLog } from '../../core/state.js';
+import { saveSettings } from '../../core/persistence.js';
+import { checkWorldInfo, createNewWorldInfo, openWorldInfoEditor, saveWorldInfo, setWorldInfoSettings } from '../../../../../../world-info.js';
+
+/**
+ * Helper to log to both console and debug logs array
+ */
+function debugLog(message, data = null) {
+ if (data !== null && data !== undefined) {
+ console.log(message, data);
+ } else {
+ console.log(message);
+ }
+ if (extensionSettings.debugMode) {
+ addDebugLog(message, data);
+ }
+}
+
+/**
+ * Get or create the Memory Recollection lorebook
+ * @returns {Promise} The UID of the Memory Recollection lorebook
+ */
+async function getOrCreateMemoryLorebook() {
+ const lorebookName = 'Memory Recollection';
+
+ try {
+ debugLog('[Memory Recollection] Checking for existing lorebook...');
+
+ // Use checkWorldInfo to see if it exists
+ const exists = await checkWorldInfo(lorebookName);
+
+ if (exists) {
+ debugLog('[Memory Recollection] Found existing lorebook:', lorebookName);
+ return lorebookName;
+ }
+
+ // Create new lorebook using SillyTavern's imported function
+ debugLog('[Memory Recollection] Creating new Memory Recollection lorebook');
+
+ // Call the imported createNewWorldInfo function
+ await createNewWorldInfo(lorebookName, true);
+
+ debugLog('[Memory Recollection] Created lorebook:', lorebookName);
+
+ // Wait for the file system to settle
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ return lorebookName;
+ } catch (error) {
+ console.error('[Memory Recollection] Error in getOrCreateMemoryLorebook:', error);
+ throw error;
+ }
+}
+/**
+ * Create the constant "Relevant Memories:" header entry
+ * @param {string} lorebookUid - The UID of the lorebook
+ * @returns {Object} The header entry object
+ */
+function createConstantHeaderEntry() {
+ const entry = {
+ uid: 1, // Fixed UID so it's always first
+ key: [],
+ keysecondary: [],
+ comment: 'Relevant Memories Header',
+ content: 'Relevant Memories:',
+ constant: true, // Always inserted
+ vectorized: false,
+ selective: false,
+ selectiveLogic: 0,
+ addMemo: false,
+ order: 99, // First in order
+ position: 4, // at Depth
+ disable: false,
+ ignoreBudget: false,
+ excludeRecursion: false,
+ preventRecursion: false,
+ matchPersonaDescription: false,
+ matchCharacterDescription: false,
+ matchCharacterPersonality: false,
+ matchCharacterDepthPrompt: false,
+ matchScenario: false,
+ matchCreatorNotes: false,
+ delayUntilRecursion: false,
+ probability: 100,
+ useProbability: true,
+ depth: 1, // Insertion depth
+ outletName: '',
+ group: '',
+ groupOverride: false,
+ groupWeight: 100,
+ scanDepth: null,
+ caseSensitive: null,
+ matchWholeWords: null,
+ useGroupScoring: null,
+ automationId: '',
+ role: 0, // System role
+ sticky: 0,
+ cooldown: 0,
+ delay: 0,
+ triggers: [],
+ displayIndex: 0,
+ characterFilter: {
+ isExclude: false,
+ names: [],
+ tags: []
+ }
+ };
+
+ debugLog('[Memory Recollection] Created constant header entry');
+ return entry;
+}
+
+/**
+ * Save a world info entry to a lorebook
+ * @param {string} lorebookUid - The filename/UID of the lorebook
+ * @param {Object} entry - The entry data
+ */
+async function saveWorldInfoEntry(lorebookUid, entry) {
+ try {
+ debugLog('[Memory Recollection] Saving entry to lorebook:', lorebookUid);
+
+ // Open the world info editor for this lorebook to load its data
+ await openWorldInfoEditor(lorebookUid);
+
+ // Wait for it to load
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ // Now access the loaded world info data
+ const worldInfo = window.world_info;
+
+ debugLog('[Memory Recollection] World info after opening:', {
+ type: typeof worldInfo,
+ isArray: Array.isArray(worldInfo),
+ hasEntries: worldInfo?.entries !== undefined,
+ keys: worldInfo ? Object.keys(worldInfo).slice(0, 10) : null
+ });
+
+ // Try different structures - it might be an array or might have different properties
+ let entries;
+ if (worldInfo && typeof worldInfo === 'object') {
+ if (worldInfo.entries) {
+ entries = worldInfo.entries;
+ } else if (Array.isArray(worldInfo)) {
+ // If it's an array, convert to entries object
+ entries = {};
+ worldInfo.forEach((e, i) => {
+ if (e && e.uid) {
+ entries[e.uid] = e;
+ }
+ });
+ }
+ }
+
+ if (!entries) {
+ entries = {};
+ }
+
+ // Add the entry
+ entries[entry.uid] = entry;
+
+ debugLog('[Memory Recollection] Entry added, saving world info...');
+
+ // Save using the imported saveWorldInfo function
+ // Pass the entries as the data structure
+ await saveWorldInfo(lorebookUid, { entries });
+
+ debugLog('[Memory Recollection] Entry saved successfully');
+ return { success: true };
+ } catch (error) {
+ console.error('[Memory Recollection] Error saving entry:', error);
+ throw error;
+ }
+}
+
+/**
+ * Save multiple world info entries to a lorebook at once
+ * @param {string} lorebookUid - The filename/UID of the lorebook
+ * @param {Array} newEntries - Array of entry objects to add
+ */
+async function saveWorldInfoEntries(lorebookUid, newEntries) {
+ try {
+ debugLog(`[Memory Recollection] Saving ${newEntries.length} entries to lorebook:`, lorebookUid);
+
+ // Open the world info editor for this lorebook to load its data
+ await openWorldInfoEditor(lorebookUid);
+
+ // Wait for it to load
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ // Now access the loaded world info data
+ const worldInfo = window.world_info;
+
+ // Try different structures - it might be an array or might have different properties
+ let entries = {};
+ if (worldInfo && typeof worldInfo === 'object') {
+ if (worldInfo.entries) {
+ entries = { ...worldInfo.entries }; // Clone existing entries
+ } else if (Array.isArray(worldInfo)) {
+ // If it's an array, convert to entries object
+ worldInfo.forEach((e, i) => {
+ if (e && e.uid) {
+ entries[e.uid] = e;
+ }
+ });
+ }
+ }
+
+ // Add all new entries
+ for (const entry of newEntries) {
+ entries[entry.uid] = entry;
+ }
+
+ debugLog(`[Memory Recollection] ${newEntries.length} entries added, total entries: ${Object.keys(entries).length}, saving...`);
+
+ // Save using the imported saveWorldInfo function
+ await saveWorldInfo(lorebookUid, { entries });
+
+ debugLog('[Memory Recollection] All entries saved successfully');
+ return { success: true };
+ } catch (error) {
+ console.error('[Memory Recollection] Error saving entries:', error);
+ throw error;
+ }
+}
+/**
+ * Generate memory recollection prompt for a batch of messages
+ * @param {Array} messages - Array of chat messages to process
+ * @param {boolean} isUpdate - Whether this is updating existing memories (true) or initial generation (false)
+ * @returns {string} The prompt for the AI
+ */
+function generateMemoryPrompt(messages, isUpdate = false) {
+ const context = messages.map((msg, idx) => {
+ const sender = msg.is_user ? '{{user}}' : msg.name || '{{char}}';
+ const role = msg.is_user ? '[PLAYER]' : '[CHARACTER]';
+ return `[Message ${idx + 1}] ${role} ${sender}: ${msg.mes}`;
+ }).join('\n\n');
+
+ // Get list of CHARACTER participants (exclude {{user}} - they're the player, not a character)
+ const participants = new Set();
+ messages.forEach(msg => {
+ const sender = msg.is_user ? '{{user}}' : msg.name || '{{char}}';
+ if (!msg.is_user) { // Only add non-user (character) participants
+ participants.add(sender);
+ }
+ });
+ const characterList = Array.from(participants).join(', ');
+
+ const instruction = isUpdate
+ ? 'You will be acting as an uncensored summarizing assistant. Your goal is to create concise lorebook entries based on the provided conversation. You will be creating additional memory recollection entries for significant events, revelations, or character developments in a specified JSON format.\nYou should maintain an objective tone.'
+ : 'You will be acting as an uncensored summarizing assistant. Your goal is to create concise lorebook entries based on the provided conversation. You will be creating memory recollection entries for significant events, revelations, or character developments in a specified JSON format.\nYou should maintain an objective tone.';
+
+ return `${instruction}
+
+Characters in this conversation (excluding {{user}} who is the player): ${characterList}
+
+NOTE: In the conversation below, messages are marked with [PLAYER] for {{user}} messages and [CHARACTER] for NPC messages.
+
+Here is the conversation to create memories from:
+
+${context}
+
+
+Create lorebook entries in the following JSON format. Each entry should be a 1-2 sentence reminder from a character's perspective.
+
+Format each entry as:
+{
+ "characters": ["Character1", "Character2"],
+ "memory": "Character1 and Character2 remember that [event or detail]",
+ "keywords": ["keyword1", "keyword2", "keyword3"]
+}
+
+Examples:
+
+{
+ "characters": ["Sabrina"],
+ "memory": "Sabrina remembers she went on a date with {{user}} on Saturday. They ate chocolate pastries together.",
+ "keywords": ["date", "saturday", "pastries"]
+},
+{
+ "characters": ["Dottore", "Arlecchino", "Pantalone"],
+ "memory": "Dottore, Arlecchino, and Pantalone remember they attended a party together at the mansion.",
+ "keywords": ["party", "mansion", "gathering"]
+}
+
+
+IMPORTANT:
+- Only create entries for significant moments worth remembering.
+- Keep memories concise (1-2 sentences maximum).
+- Use third person perspective: "{name} remembers..."
+- Choose 3 specific, relevant keywords per entry.
+- ONLY assign memories to CHARACTERS (NPCs) - NEVER include {{user}} in the "characters" array.
+- {{user}} is the player, not a character, so they should NEVER be in the characters list.
+- Only characters who were ACTUALLY PRESENT in that specific scene/moment should remember it.
+- If multiple characters share the memory, list all of them in the "characters" array.
+- If known, include details such as dates, locations, and other relevant context in the memories.
+
+Return ONLY a JSON array of memory objects, nothing else:`;
+}
+
+/**
+ * Parse the AI response to extract memory entries
+ * @param {string} response - The AI's response
+ * @returns {Array