Fix extension loading, enhance theming, add horizontal scrolling, improve emoji parsing, rename to Main Quests
This commit is contained in:
@@ -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 = `
|
||||
<button id="rpg-manual-update-mobile" class="rpg-mobile-refresh" title="Refresh RPG Info">
|
||||
<i class="fa-solid fa-sync"></i>
|
||||
</button>
|
||||
`;
|
||||
$('body').append(mobileRefreshHtml);
|
||||
|
||||
// Add debug toggle FAB button (same pattern as other mobile FABs)
|
||||
const debugToggleHtml = `
|
||||
<button id="rpg-debug-toggle" class="rpg-debug-toggle" title="Toggle Debug Logs">
|
||||
<i class="fa-solid fa-bug"></i>
|
||||
</button>
|
||||
`;
|
||||
$('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');
|
||||
});
|
||||
|
||||
$('#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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
+5
-12
@@ -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
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
|
||||
+31
-39
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = `
|
||||
<div id="rpg-max-lorebook-activations-container" class="rpg-lorebook-limiter-container">
|
||||
<label class="rpg-lorebook-limiter-label">
|
||||
<span class="rpg-lorebook-limiter-title">Max Lorebook Activations</span>
|
||||
<input type="number"
|
||||
id="rpg-max-activations-input"
|
||||
class="rpg-lorebook-limiter-input"
|
||||
min="0"
|
||||
max="9999"
|
||||
step="1"
|
||||
value="${maxActivations}"
|
||||
placeholder="0 = unlimited" />
|
||||
</label>
|
||||
<small class="rpg-lorebook-limiter-hint">Limit entries per generation (0 = unlimited)</small>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -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<string>} 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:
|
||||
<conversation>
|
||||
${context}
|
||||
</conversation>
|
||||
|
||||
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:
|
||||
<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"]
|
||||
}
|
||||
</examples>
|
||||
|
||||
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<Object>} Array of parsed memory entries
|
||||
*/
|
||||
function parseMemoryResponse(response) {
|
||||
try {
|
||||
// Try to extract JSON from code blocks
|
||||
const jsonMatch = response.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
|
||||
const jsonString = jsonMatch ? jsonMatch[1] : response;
|
||||
|
||||
// Parse JSON
|
||||
const memories = JSON.parse(jsonString.trim());
|
||||
|
||||
if (!Array.isArray(memories)) {
|
||||
throw new Error('Response is not an array');
|
||||
}
|
||||
|
||||
debugLog('[Memory Recollection] Parsed memories:', memories);
|
||||
return memories;
|
||||
|
||||
} catch (error) {
|
||||
debugLog('[Memory Recollection] Failed to parse response:', error);
|
||||
console.error('[Memory Recollection] Parse error:', error);
|
||||
console.error('[Memory Recollection] Raw response:', response);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a world info entry from a memory object
|
||||
* @param {string} lorebookUid - The UID of the lorebook
|
||||
* @param {Object} memory - The memory object
|
||||
* @param {number} index - The index for ordering
|
||||
*/
|
||||
async function createMemoryEntry(lorebookUid, memory, index) {
|
||||
const { characters: characterList, memory: content, keywords } = memory;
|
||||
|
||||
// Handle character filter - just use the character names directly
|
||||
let characterNames = [];
|
||||
|
||||
if (Array.isArray(characterList) && characterList.length > 0) {
|
||||
// New format: array of character names
|
||||
characterNames = characterList.map(name => name.trim());
|
||||
debugLog(`[Memory Recollection] Character names for filter:`, characterNames);
|
||||
} else if (typeof characterList === 'string' && characterList.trim() !== '') {
|
||||
// Legacy string format or comma-separated - parse it
|
||||
characterNames = characterList.split(',').map(n => n.trim()).filter(n => n !== '');
|
||||
debugLog(`[Memory Recollection] Character names for filter:`, characterNames);
|
||||
}
|
||||
|
||||
const entry = {
|
||||
uid: Date.now() + index, // Simple UID generation
|
||||
key: keywords || [],
|
||||
keysecondary: [],
|
||||
comment: `Memory: ${characterNames.join(', ')}`,
|
||||
content: content,
|
||||
constant: false,
|
||||
vectorized: false,
|
||||
selective: true,
|
||||
selectiveLogic: 0,
|
||||
addMemo: false,
|
||||
order: 100,
|
||||
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, // 0 = System role (matching the example)
|
||||
sticky: 0,
|
||||
cooldown: 0,
|
||||
delay: 0,
|
||||
triggers: [],
|
||||
displayIndex: index + 1,
|
||||
characterFilter: {
|
||||
isExclude: false,
|
||||
names: characterNames, // Array of character names
|
||||
tags: []
|
||||
},
|
||||
extensions: {
|
||||
position: 4, // at Depth
|
||||
depth: 1,
|
||||
role: 1
|
||||
}
|
||||
};
|
||||
|
||||
debugLog(`[Memory Recollection] Created entry for ${characterNames.join(', ')} with character filter:`, characterNames);
|
||||
return entry; // Return instead of saving
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a batch of messages and generate memory entries
|
||||
* @param {Array} messages - Array of messages to process
|
||||
* @param {string} lorebookUid - The UID of the lorebook
|
||||
* @param {boolean} isUpdate - Whether this is an update (true) or initial generation (false)
|
||||
* @param {number} startIndex - Starting index for entry ordering
|
||||
* @returns {Promise<Array>} Array of created entries
|
||||
*/
|
||||
async function processBatch(messages, lorebookUid, isUpdate, startIndex) {
|
||||
debugLog(`[Memory Recollection] Processing batch of ${messages.length} messages (isUpdate: ${isUpdate})`);
|
||||
|
||||
const prompt = generateMemoryPrompt(messages, isUpdate);
|
||||
|
||||
// Generate using SillyTavern's generateRaw
|
||||
const response = await generateRaw(prompt, '', false, false);
|
||||
|
||||
if (!response) {
|
||||
throw new Error('No response from AI');
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
const memories = parseMemoryResponse(response);
|
||||
|
||||
if (memories.length === 0) {
|
||||
debugLog('[Memory Recollection] No memories extracted from this batch');
|
||||
// Return -1 to signal parse failure (vs 0 for valid but empty response)
|
||||
throw new Error('Failed to parse memories from AI response. The response may be invalid or the service may be unavailable.');
|
||||
}
|
||||
|
||||
// Create entries for each memory (but don't save yet)
|
||||
const entries = [];
|
||||
for (let i = 0; i < memories.length; i++) {
|
||||
const entry = await createMemoryEntry(lorebookUid, memories[i], startIndex + i);
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
debugLog(`[Memory Recollection] Created ${entries.length} entries from batch`);
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to start memory recollection process
|
||||
* @param {Function} onProgress - Callback for progress updates (current, total)
|
||||
* @param {Function} onComplete - Callback when complete
|
||||
* @param {Function} onError - Callback for errors
|
||||
*/
|
||||
export async function startMemoryRecollection(onProgress, onComplete, onError) {
|
||||
try {
|
||||
debugLog('[Memory Recollection] Starting memory recollection process');
|
||||
|
||||
// Get or create the lorebook
|
||||
const lorebookUid = await getOrCreateMemoryLorebook();
|
||||
|
||||
// Get messages to process count from settings
|
||||
const messagesToProcess = extensionSettings.memoryMessagesToProcess || 16;
|
||||
|
||||
// Check if this is an update (lorebook already exists with entries)
|
||||
const world_info = window.world_info;
|
||||
const lorebook = world_info.globalSelect?.find(book => book.uid === lorebookUid);
|
||||
const existingEntryCount = lorebook?.entries ? Object.keys(lorebook.entries).length : 0;
|
||||
const isUpdate = existingEntryCount > 1; // More than just the header
|
||||
|
||||
let messagesToProcessArray;
|
||||
if (isUpdate) {
|
||||
// Process only the last batch
|
||||
const totalMessages = chat.length;
|
||||
const startIdx = Math.max(0, totalMessages - messagesToProcess);
|
||||
messagesToProcessArray = chat.slice(startIdx);
|
||||
debugLog(`[Memory Recollection] Update mode: Processing last ${messagesToProcess} messages`);
|
||||
} else {
|
||||
// Process entire chat in batches
|
||||
messagesToProcessArray = chat;
|
||||
debugLog(`[Memory Recollection] Initial mode: Processing all ${chat.length} messages`);
|
||||
}
|
||||
|
||||
const totalBatches = Math.ceil(messagesToProcessArray.length / messagesToProcess);
|
||||
let entryIndex = existingEntryCount;
|
||||
const allEntries = []; // Accumulate all entries here
|
||||
|
||||
for (let i = 0; i < totalBatches; i++) {
|
||||
const batchStart = i * messagesToProcess;
|
||||
const batchEnd = Math.min(batchStart + messagesToProcess, messagesToProcessArray.length);
|
||||
const batch = messagesToProcessArray.slice(batchStart, batchEnd);
|
||||
|
||||
onProgress(i + 1, totalBatches);
|
||||
|
||||
try {
|
||||
const batchEntries = await processBatch(batch, lorebookUid, isUpdate && i === 0, entryIndex);
|
||||
allEntries.push(...batchEntries); // Add to accumulator
|
||||
entryIndex += batchEntries.length;
|
||||
} catch (error) {
|
||||
// Batch failed - ask user if they want to retry
|
||||
debugLog('[Memory Recollection] Batch failed:', error.message);
|
||||
|
||||
const retry = await new Promise(resolve => {
|
||||
const retryModal = document.createElement('div');
|
||||
retryModal.className = 'rpg-memory-modal-overlay';
|
||||
retryModal.innerHTML = `
|
||||
<div class="rpg-memory-modal">
|
||||
<div class="rpg-memory-modal-header">
|
||||
<h3>⚠️ Generation Failed</h3>
|
||||
</div>
|
||||
<div class="rpg-memory-modal-body">
|
||||
<p><strong>Error:</strong> ${error.message}</p>
|
||||
<p>Batch ${i + 1} of ${totalBatches} failed to process.</p>
|
||||
<p>Would you like to retry this batch?</p>
|
||||
</div>
|
||||
<div class="rpg-memory-modal-footer">
|
||||
<button class="rpg-memory-modal-btn rpg-memory-cancel">Skip Batch</button>
|
||||
<button class="rpg-memory-modal-btn rpg-memory-proceed">Retry</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(retryModal);
|
||||
|
||||
retryModal.querySelector('.rpg-memory-cancel').addEventListener('click', () => {
|
||||
document.body.removeChild(retryModal);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
retryModal.querySelector('.rpg-memory-proceed').addEventListener('click', () => {
|
||||
document.body.removeChild(retryModal);
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
if (retry) {
|
||||
// Retry the same batch
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
// Otherwise skip this batch and continue
|
||||
}
|
||||
|
||||
// Small delay between batches to avoid rate limiting
|
||||
if (i < totalBatches - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
// Add the constant header entry at the end
|
||||
const headerEntry = createConstantHeaderEntry();
|
||||
allEntries.push(headerEntry); // Add to end of array
|
||||
|
||||
// Save all entries at once
|
||||
if (allEntries.length > 0) {
|
||||
debugLog(`[Memory Recollection] Saving ${allEntries.length} total entries (including header) to lorebook...`);
|
||||
await saveWorldInfoEntries(lorebookUid, allEntries);
|
||||
|
||||
// Trigger world info refresh by simulating the WI button click to reload the list
|
||||
// This ensures the newly created lorebook appears in the dropdown
|
||||
const wiButton = document.querySelector('#WIDrawerIcon');
|
||||
if (wiButton) {
|
||||
// Close and reopen to force refresh
|
||||
wiButton.click();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
wiButton.click();
|
||||
debugLog('[Memory Recollection] Triggered WI panel refresh');
|
||||
}
|
||||
|
||||
// Also emit the update event
|
||||
eventSource.emit(event_types.WORLDINFO_SETTINGS_UPDATED);
|
||||
}
|
||||
|
||||
debugLog('[Memory Recollection] Process complete');
|
||||
|
||||
// Open the World Info editor with the Memory Recollection lorebook
|
||||
try {
|
||||
await openWorldInfoEditor(lorebookUid);
|
||||
debugLog('[Memory Recollection] Opened World Info editor with Memory Recollection lorebook');
|
||||
} catch (err) {
|
||||
debugLog('[Memory Recollection] Could not open World Info editor:', err);
|
||||
}
|
||||
|
||||
onComplete(allEntries.length);
|
||||
|
||||
} catch (error) {
|
||||
debugLog('[Memory Recollection] Error:', error);
|
||||
onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show memory recollection confirmation modal
|
||||
*/
|
||||
export function showMemoryRecollectionModal() {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'rpg-memory-modal-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="rpg-memory-modal">
|
||||
<div class="rpg-memory-modal-header">
|
||||
<h3>⚠️ Memory Recollection</h3>
|
||||
</div>
|
||||
<div class="rpg-memory-modal-body">
|
||||
<p><strong>Warning!</strong> This process will trigger multiple generation requests and will take time.</p>
|
||||
<p>Ensure your currently selected model is the one you want to use for this task.</p>
|
||||
<p class="rpg-memory-modal-info">
|
||||
Messages per batch: <strong>${extensionSettings.memoryMessagesToProcess || 16}</strong>
|
||||
<br>
|
||||
<span class="rpg-memory-modal-hint">(You can change this in the extension settings)</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="rpg-memory-modal-footer">
|
||||
<button class="rpg-memory-modal-btn rpg-memory-cancel">Cancel</button>
|
||||
<button class="rpg-memory-modal-btn rpg-memory-proceed">Proceed</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Event listeners
|
||||
modal.querySelector('.rpg-memory-cancel').addEventListener('click', () => {
|
||||
document.body.removeChild(modal);
|
||||
});
|
||||
|
||||
modal.querySelector('.rpg-memory-proceed').addEventListener('click', () => {
|
||||
document.body.removeChild(modal);
|
||||
showMemoryProgressModal();
|
||||
});
|
||||
|
||||
// Click outside to close
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
document.body.removeChild(modal);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show progress modal during memory recollection
|
||||
*/
|
||||
function showMemoryProgressModal() {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'rpg-memory-modal-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="rpg-memory-modal">
|
||||
<div class="rpg-memory-modal-header">
|
||||
<h3>🧠 Processing Memories...</h3>
|
||||
</div>
|
||||
<div class="rpg-memory-modal-body">
|
||||
<p class="rpg-memory-progress-text">Processing batch <span class="rpg-memory-current">0</span> of <span class="rpg-memory-total">0</span></p>
|
||||
<div class="rpg-memory-progress-bar">
|
||||
<div class="rpg-memory-progress-fill"></div>
|
||||
</div>
|
||||
<p class="rpg-memory-status">Initializing...</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const currentSpan = modal.querySelector('.rpg-memory-current');
|
||||
const totalSpan = modal.querySelector('.rpg-memory-total');
|
||||
const progressFill = modal.querySelector('.rpg-memory-progress-fill');
|
||||
const statusText = modal.querySelector('.rpg-memory-status');
|
||||
|
||||
// Start the process
|
||||
startMemoryRecollection(
|
||||
(current, total) => {
|
||||
currentSpan.textContent = current;
|
||||
totalSpan.textContent = total;
|
||||
const percentage = (current / total) * 100;
|
||||
progressFill.style.width = `${percentage}%`;
|
||||
statusText.textContent = `Processing memories from batch ${current}...`;
|
||||
},
|
||||
(entriesCreated) => {
|
||||
statusText.innerHTML = `
|
||||
<strong>✅ Complete!</strong> Created ${entriesCreated} memory entries.<br>
|
||||
<small>The "Memory Recollection" lorebook has been created.</small><br>
|
||||
<strong style="color: #ffa500; margin-top: 10px; display: block;">⚠️ Please refresh SillyTavern to see the lorebook in the World Info dropdown.</strong>
|
||||
`;
|
||||
progressFill.style.width = '100%';
|
||||
|
||||
// Add close button
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'rpg-memory-modal-btn rpg-memory-close';
|
||||
closeBtn.textContent = 'Close';
|
||||
closeBtn.style.marginTop = '15px';
|
||||
closeBtn.addEventListener('click', () => {
|
||||
document.body.removeChild(modal);
|
||||
});
|
||||
modal.querySelector('.rpg-memory-modal-body').appendChild(closeBtn);
|
||||
},
|
||||
(error) => {
|
||||
statusText.textContent = `Error: ${error.message}`;
|
||||
statusText.style.color = '#e94560';
|
||||
|
||||
// Close after 5 seconds
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(modal);
|
||||
}, 5000);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the memory recollection button in World Info section
|
||||
*/
|
||||
export function setupMemoryRecollectionButton() {
|
||||
console.log('[Memory Recollection] Setting up button via event listener');
|
||||
|
||||
// Use SillyTavern's built-in event to know when WI is ready
|
||||
// This fires after the worldInfoSettings are loaded
|
||||
eventSource.on('worldInfoSettings', () => {
|
||||
console.log('[Memory Recollection] worldInfoSettings event fired');
|
||||
setTimeout(updateButton, 100);
|
||||
});
|
||||
|
||||
// Also try on app ready
|
||||
eventSource.on('app_ready', () => {
|
||||
console.log('[Memory Recollection] app_ready event fired');
|
||||
setTimeout(updateButton, 500);
|
||||
});
|
||||
|
||||
// Try immediately as well
|
||||
setTimeout(updateButton, 2000);
|
||||
|
||||
function updateButton() {
|
||||
const existingButton = document.querySelector('.rpg-memory-recollection-btn');
|
||||
|
||||
// If extension is disabled, remove button if it exists
|
||||
if (!extensionSettings.enabled) {
|
||||
if (existingButton) {
|
||||
console.log('[Memory Recollection] Extension disabled, removing button');
|
||||
existingButton.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Extension is enabled, add button if it doesn't exist
|
||||
addButton();
|
||||
}
|
||||
|
||||
function addButton() {
|
||||
// Check if button already exists
|
||||
if (document.querySelector('.rpg-memory-recollection-btn')) {
|
||||
console.log('[Memory Recollection] Button already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Memory Recollection] Attempting to add button...');
|
||||
|
||||
// World Info button bar is inside the world editor
|
||||
// Look for the specific button container
|
||||
const selectors = [
|
||||
'#world_editor_buttons',
|
||||
'#world_popup .world_button_bar',
|
||||
'#WorldInfo .world_button_bar',
|
||||
'.world_button_bar',
|
||||
'#world_popup .justifyLeft',
|
||||
'#WorldInfo .justifyLeft',
|
||||
'#world_popup',
|
||||
'#WorldInfo'
|
||||
];
|
||||
|
||||
let container = null;
|
||||
for (const selector of selectors) {
|
||||
container = document.querySelector(selector);
|
||||
if (container) {
|
||||
console.log(`[Memory Recollection] Found container with selector: ${selector}`, container);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!container) {
|
||||
console.log('[Memory Recollection] No suitable container found yet');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the button
|
||||
const button = document.createElement('button');
|
||||
button.id = 'rpg-memory-recollection-button';
|
||||
button.className = 'rpg-memory-recollection-btn menu_button';
|
||||
button.innerHTML = '<i class="fa-solid fa-brain"></i> Memory Recollection';
|
||||
button.title = 'Generate memory recollection entries from chat history';
|
||||
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showMemoryRecollectionModal();
|
||||
});
|
||||
|
||||
// Insert the button - prepend to put it first
|
||||
if (container.classList.contains('world_button_bar') || container.classList.contains('justifyLeft')) {
|
||||
container.insertBefore(button, container.firstChild);
|
||||
} else {
|
||||
// Find or create a button container
|
||||
let buttonContainer = container.querySelector('.world_button_bar') ||
|
||||
container.querySelector('.justifyLeft');
|
||||
|
||||
if (!buttonContainer) {
|
||||
buttonContainer = document.createElement('div');
|
||||
buttonContainer.className = 'world_button_bar justifyLeft';
|
||||
container.insertBefore(buttonContainer, container.firstChild);
|
||||
}
|
||||
|
||||
buttonContainer.insertBefore(button, buttonContainer.firstChild);
|
||||
}
|
||||
|
||||
console.log('[Memory Recollection] ✅ Button added successfully!');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update button visibility based on extension enabled state
|
||||
* Call this when the extension is toggled on/off
|
||||
*/
|
||||
export function updateMemoryRecollectionButton() {
|
||||
const existingButton = document.querySelector('.rpg-memory-recollection-btn');
|
||||
|
||||
if (!extensionSettings.enabled) {
|
||||
// Extension disabled - remove button if it exists
|
||||
if (existingButton) {
|
||||
console.log('[Memory Recollection] Extension disabled, removing button');
|
||||
existingButton.remove();
|
||||
}
|
||||
} else {
|
||||
// Extension enabled - ensure button exists
|
||||
if (!existingButton) {
|
||||
console.log('[Memory Recollection] Extension enabled, adding button');
|
||||
setTimeout(() => {
|
||||
setupMemoryRecollectionButton();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,11 @@ import {
|
||||
import { saveChatData } from '../../core/persistence.js';
|
||||
import { generateSeparateUpdatePrompt } from './promptBuilder.js';
|
||||
import { parseResponse, parseUserStats } from './parser.js';
|
||||
import { renderUserStats } from '../rendering/userStats.js';
|
||||
import { renderInfoBox } from '../rendering/infoBox.js';
|
||||
import { renderThoughts } from '../rendering/thoughts.js';
|
||||
import { renderInventory } from '../rendering/inventory.js';
|
||||
import { renderQuests } from '../rendering/quests.js';
|
||||
|
||||
// Store the original preset name to restore after tracker generation
|
||||
let originalPresetName = null;
|
||||
@@ -192,6 +197,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
renderInfoBox();
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
} else {
|
||||
// No assistant message to attach to - just update display
|
||||
if (parsedData.userStats) {
|
||||
@@ -201,6 +207,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
renderInfoBox();
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
}
|
||||
|
||||
// Save to chat metadata
|
||||
|
||||
@@ -7,6 +7,45 @@ import { extensionSettings, FEATURE_FLAGS, addDebugLog } from '../../core/state.
|
||||
import { saveSettings } from '../../core/persistence.js';
|
||||
import { extractInventory } from './inventoryParser.js';
|
||||
|
||||
/**
|
||||
* Helper to separate emoji from text in a string
|
||||
* Handles cases where there's no comma or space after emoji
|
||||
* @param {string} str - String potentially containing emoji followed by text
|
||||
* @returns {{emoji: string, text: string}} Separated emoji and text
|
||||
*/
|
||||
function separateEmojiFromText(str) {
|
||||
if (!str) return { emoji: '', text: '' };
|
||||
|
||||
str = str.trim();
|
||||
|
||||
// Regex to match emoji at the start (handles most emoji including compound ones)
|
||||
// This matches emoji sequences including skin tones, gender modifiers, etc.
|
||||
const emojiRegex = /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F910}-\u{1F96B}\u{1F980}-\u{1F9E0}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}]+/u;
|
||||
const emojiMatch = str.match(emojiRegex);
|
||||
|
||||
if (emojiMatch) {
|
||||
const emoji = emojiMatch[0];
|
||||
let text = str.substring(emoji.length).trim();
|
||||
|
||||
// Remove leading comma or space if present
|
||||
text = text.replace(/^[,\s]+/, '');
|
||||
|
||||
return { emoji, text };
|
||||
}
|
||||
|
||||
// No emoji found - check if there's a comma separator anyway
|
||||
const commaParts = str.split(',');
|
||||
if (commaParts.length >= 2) {
|
||||
return {
|
||||
emoji: commaParts[0].trim(),
|
||||
text: commaParts.slice(1).join(',').trim()
|
||||
};
|
||||
}
|
||||
|
||||
// No clear separation - return original as text
|
||||
return { emoji: '', text: str };
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to log to both console and debug logs array
|
||||
*/
|
||||
@@ -169,18 +208,36 @@ export function parseUserStats(statsText) {
|
||||
// Format 2: Status: [Emoji], [Conditions]
|
||||
// Format 3: [Emoji]: [Conditions] (legacy)
|
||||
// Format 4: Mood: [Emoji] - [Conditions]
|
||||
// Format 5: Status: [Emoji Conditions] (no separator - FIXED)
|
||||
let moodMatch = null;
|
||||
|
||||
// Try new format: Status: emoji, conditions
|
||||
const statusMatch = statsText.match(/Status:\s*(.+?),\s*(.+)/i);
|
||||
// Try new format: Status: emoji, conditions OR Status: emojiConditions
|
||||
const statusMatch = statsText.match(/Status:\s*(.+)/i);
|
||||
if (statusMatch) {
|
||||
moodMatch = [null, statusMatch[1].trim(), statusMatch[2].trim()];
|
||||
const statusContent = statusMatch[1].trim();
|
||||
const { emoji, text } = separateEmojiFromText(statusContent);
|
||||
if (emoji && text) {
|
||||
moodMatch = [null, emoji, text];
|
||||
} else if (statusContent.includes(',')) {
|
||||
// Fallback to comma split if emoji detection failed
|
||||
const parts = statusContent.split(',').map(p => p.trim());
|
||||
moodMatch = [null, parts[0], parts.slice(1).join(', ')];
|
||||
}
|
||||
// Try alternative: Mood: emoji, conditions
|
||||
else {
|
||||
const moodAltMatch = statsText.match(/Mood:\s*(.+?)[,\-]\s*(.+)/i);
|
||||
}
|
||||
|
||||
// Try alternative: Mood: emoji, conditions OR Mood: emojiConditions
|
||||
if (!moodMatch) {
|
||||
const moodAltMatch = statsText.match(/Mood:\s*(.+)/i);
|
||||
if (moodAltMatch) {
|
||||
moodMatch = [null, moodAltMatch[1].trim(), moodAltMatch[2].trim()];
|
||||
const moodContent = moodAltMatch[1].trim();
|
||||
const { emoji, text } = separateEmojiFromText(moodContent);
|
||||
if (emoji && text) {
|
||||
moodMatch = [null, emoji, text];
|
||||
} else if (moodContent.includes(',') || moodContent.includes('-')) {
|
||||
// Fallback to comma/dash split if emoji detection failed
|
||||
const parts = moodContent.split(/[,\-]/).map(p => p.trim());
|
||||
moodMatch = [null, parts[0], parts.slice(1).join(', ')];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +302,28 @@ export function parseUserStats(statsText) {
|
||||
extensionSettings.userStats.conditions = moodMatch[2].trim(); // Conditions
|
||||
}
|
||||
|
||||
// Extract quests
|
||||
const mainQuestMatch = statsText.match(/Main Quests?:\s*(.+)/i);
|
||||
if (mainQuestMatch) {
|
||||
extensionSettings.quests.main = mainQuestMatch[1].trim();
|
||||
debugLog('[RPG Parser] Main quests extracted:', mainQuestMatch[1].trim());
|
||||
}
|
||||
|
||||
const optionalQuestsMatch = statsText.match(/Optional Quests:\s*(.+)/i);
|
||||
if (optionalQuestsMatch) {
|
||||
const questsText = optionalQuestsMatch[1].trim();
|
||||
if (questsText && questsText !== 'None') {
|
||||
// Split by comma and clean up
|
||||
extensionSettings.quests.optional = questsText
|
||||
.split(',')
|
||||
.map(q => q.trim())
|
||||
.filter(q => q && q !== 'None');
|
||||
} else {
|
||||
extensionSettings.quests.optional = [];
|
||||
}
|
||||
debugLog('[RPG Parser] Optional quests extracted:', extensionSettings.quests.optional);
|
||||
}
|
||||
|
||||
debugLog('[RPG Parser] Final userStats after parsing:', {
|
||||
health: extensionSettings.userStats.health,
|
||||
satiety: extensionSettings.userStats.satiety,
|
||||
|
||||
@@ -122,9 +122,13 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
instructions += 'Assets: [Vehicles, property, major possessions, or "None"]\n';
|
||||
} else {
|
||||
// Legacy v1 format
|
||||
instructions += 'Inventory: [Clothing/Armor, Inventory Items (list of important items/none)]\n';
|
||||
instructions += 'Inventory: [Clothing/Armor, Inventory Items (list of important items, or "None")]\\n';
|
||||
}
|
||||
|
||||
// Add quests section
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -137,6 +141,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
instructions += 'Temperature: [Temperature in °C]\n';
|
||||
instructions += 'Time: [Time Start → Time End]\n';
|
||||
instructions += 'Location: [Location]\n';
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -208,9 +213,23 @@ export function generateContextualSummary() {
|
||||
if (stats.inventory) {
|
||||
const inventorySummary = buildInventorySummary(stats.inventory);
|
||||
if (inventorySummary && inventorySummary !== 'None') {
|
||||
summary += `Inventory:\n${inventorySummary}\n`;
|
||||
summary += `${inventorySummary}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add quests summary
|
||||
if (extensionSettings.quests) {
|
||||
if (extensionSettings.quests.main && extensionSettings.quests.main !== 'None') {
|
||||
summary += `Main Quests: ${extensionSettings.quests.main}\n`;
|
||||
}
|
||||
if (extensionSettings.quests.optional && extensionSettings.quests.optional.length > 0) {
|
||||
const optionalQuests = extensionSettings.quests.optional.filter(q => q && q !== 'None').join(', ');
|
||||
if (optionalQuests) {
|
||||
summary += `Optional Quests: ${optionalQuests}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Include classic stats (attributes) and dice roll only if there was a dice roll
|
||||
if (extensionSettings.lastDiceRoll) {
|
||||
const classicStats = extensionSettings.classicStats;
|
||||
@@ -224,7 +243,7 @@ export function generateContextualSummary() {
|
||||
if (extensionSettings.showInfoBox && committedTrackerData.infoBox) {
|
||||
// Parse info box data - support both new and legacy formats
|
||||
const lines = committedTrackerData.infoBox.split('\n');
|
||||
let date = '', weather = '', temp = '', time = '', location = '';
|
||||
let date = '', weather = '', temp = '', time = '', location = '', recentEvents = '';
|
||||
|
||||
// console.log('[RPG Companion] 🔍 Parsing Info Box lines:', lines);
|
||||
|
||||
@@ -242,6 +261,8 @@ export function generateContextualSummary() {
|
||||
time = line.replace('Time:', '').trim();
|
||||
} else if (line.startsWith('Location:')) {
|
||||
location = line.replace('Location:', '').trim();
|
||||
} else if (line.startsWith('Recent Events:')) {
|
||||
recentEvents = line.replace('Recent Events:', '').trim();
|
||||
}
|
||||
// Legacy format with emojis (for backward compatibility)
|
||||
else if (line.includes('🗓️:')) {
|
||||
@@ -264,7 +285,7 @@ export function generateContextualSummary() {
|
||||
|
||||
// console.log('[RPG Companion] 🔍 Parsed values - date:', date, 'weather:', weather, 'temp:', temp, 'time:', time, 'location:', location);
|
||||
|
||||
if (date || weather || temp || time || location) {
|
||||
if (date || weather || temp || time || location || recentEvents) {
|
||||
summary += `Information:\n`;
|
||||
summary += `Scene: `;
|
||||
if (date) summary += `${date}`;
|
||||
@@ -272,7 +293,9 @@ export function generateContextualSummary() {
|
||||
if (time) summary += ` | ${time}`;
|
||||
if (weather) summary += ` | ${weather}`;
|
||||
if (temp) summary += ` | ${temp}`;
|
||||
summary += `\n\n`;
|
||||
summary += `\n`;
|
||||
if (recentEvents) summary += `Recent Events: ${recentEvents}\n`;
|
||||
summary += `\n`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +340,22 @@ export function generateRPGPromptText() {
|
||||
} else {
|
||||
promptText += `Last ${userName}'s Stats:\nNone - this is the first update.\n\n`;
|
||||
}
|
||||
|
||||
// Add current quests to the previous data context
|
||||
if (extensionSettings.quests) {
|
||||
if (extensionSettings.quests.main && extensionSettings.quests.main !== 'None') {
|
||||
promptText += `Main Quests: ${extensionSettings.quests.main}\n`;
|
||||
} else {
|
||||
promptText += `Main Quests: None\n`;
|
||||
}
|
||||
if (extensionSettings.quests.optional && extensionSettings.quests.optional.length > 0) {
|
||||
const optionalQuests = extensionSettings.quests.optional.filter(q => q && q !== 'None').join(', ');
|
||||
promptText += `Optional Quests: ${optionalQuests || 'None'}\n`;
|
||||
} else {
|
||||
promptText += `Optional Quests: None\n`;
|
||||
}
|
||||
promptText += `\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionSettings.showInfoBox) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { renderUserStats } from '../rendering/userStats.js';
|
||||
import { renderInfoBox } from '../rendering/infoBox.js';
|
||||
import { renderThoughts, updateChatThoughts } from '../rendering/thoughts.js';
|
||||
import { renderInventory } from '../rendering/inventory.js';
|
||||
import { renderQuests } from '../rendering/quests.js';
|
||||
|
||||
// Utils
|
||||
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
||||
@@ -164,6 +165,7 @@ export async function onMessageReceived(data) {
|
||||
renderInfoBox();
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
|
||||
// Save to chat metadata
|
||||
saveChatData();
|
||||
@@ -213,6 +215,7 @@ export function onCharacterChanged() {
|
||||
renderInfoBox();
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
|
||||
// Update chat thought overlays
|
||||
updateChatThoughts();
|
||||
@@ -284,6 +287,7 @@ export function onMessageSwiped(messageIndex) {
|
||||
renderInfoBox();
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
|
||||
// Update chat thought overlays
|
||||
updateChatThoughts();
|
||||
|
||||
@@ -12,6 +12,44 @@ import {
|
||||
} from '../../core/state.js';
|
||||
import { saveChatData } from '../../core/persistence.js';
|
||||
|
||||
/**
|
||||
* Helper to separate emoji from text in a string
|
||||
* Handles cases where there's no comma or space after emoji
|
||||
* @param {string} str - String potentially containing emoji followed by text
|
||||
* @returns {{emoji: string, text: string}} Separated emoji and text
|
||||
*/
|
||||
function separateEmojiFromText(str) {
|
||||
if (!str) return { emoji: '', text: '' };
|
||||
|
||||
str = str.trim();
|
||||
|
||||
// Regex to match emoji at the start (handles most emoji including compound ones)
|
||||
const emojiRegex = /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F910}-\u{1F96B}\u{1F980}-\u{1F9E0}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}]+/u;
|
||||
const emojiMatch = str.match(emojiRegex);
|
||||
|
||||
if (emojiMatch) {
|
||||
const emoji = emojiMatch[0];
|
||||
let text = str.substring(emoji.length).trim();
|
||||
|
||||
// Remove leading comma or space if present
|
||||
text = text.replace(/^[,\s]+/, '');
|
||||
|
||||
return { emoji, text };
|
||||
}
|
||||
|
||||
// No emoji found - check if there's a comma separator anyway
|
||||
const commaParts = str.split(',');
|
||||
if (commaParts.length >= 2) {
|
||||
return {
|
||||
emoji: commaParts[0].trim(),
|
||||
text: commaParts.slice(1).join(',').trim()
|
||||
};
|
||||
}
|
||||
|
||||
// No clear separation - return original as text
|
||||
return { emoji: '', text: str };
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the info box as a visual dashboard with calendar, weather, temperature, clock, and map widgets.
|
||||
* Includes event listeners for editable fields.
|
||||
@@ -155,11 +193,24 @@ export function renderInfoBox() {
|
||||
}
|
||||
} else if (line.startsWith('Weather:')) {
|
||||
if (!parsedFields.weather) {
|
||||
// New text format: Weather: [Emoji], [Forecast]
|
||||
// New text format: Weather: [Emoji], [Forecast] OR Weather: [Emoji][Forecast] (no separator - FIXED)
|
||||
const weatherStr = line.replace('Weather:', '').trim();
|
||||
const { emoji, text } = separateEmojiFromText(weatherStr);
|
||||
|
||||
if (emoji && text) {
|
||||
data.weatherEmoji = emoji;
|
||||
data.weatherForecast = text;
|
||||
} else if (weatherStr.includes(',')) {
|
||||
// Fallback to comma split if emoji detection failed
|
||||
const weatherParts = weatherStr.split(',').map(p => p.trim());
|
||||
data.weatherEmoji = weatherParts[0] || '';
|
||||
data.weatherForecast = weatherParts[1] || '';
|
||||
} else {
|
||||
// No clear separation - assume it's all forecast text
|
||||
data.weatherEmoji = '🌤️'; // Default emoji
|
||||
data.weatherForecast = weatherStr;
|
||||
}
|
||||
|
||||
parsedFields.weather = true;
|
||||
}
|
||||
} else {
|
||||
@@ -217,8 +268,11 @@ export function renderInfoBox() {
|
||||
// });
|
||||
|
||||
// Build visual dashboard HTML
|
||||
// Wrap all content in a scrollable container
|
||||
let html = '<div class="rpg-info-content">';
|
||||
|
||||
// Row 1: Date, Weather, Temperature, Time widgets
|
||||
let html = '<div class="rpg-dashboard rpg-dashboard-row-1">';
|
||||
html += '<div class="rpg-dashboard rpg-dashboard-row-1">';
|
||||
|
||||
// Calendar widget - always show (editable even if empty)
|
||||
// Display abbreviated version but allow editing full value
|
||||
@@ -301,6 +355,67 @@ export function renderInfoBox() {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Row 3: Recent Events widget (notebook style) - dynamically show 1-3 events
|
||||
// Parse Recent Events from infoBox string
|
||||
let recentEvents = [];
|
||||
if (committedTrackerData.infoBox) {
|
||||
const recentEventsLine = committedTrackerData.infoBox.split('\n').find(line => line.startsWith('Recent Events:'));
|
||||
if (recentEventsLine) {
|
||||
const eventsString = recentEventsLine.replace('Recent Events:', '').trim();
|
||||
if (eventsString) {
|
||||
recentEvents = eventsString.split(',').map(e => e.trim()).filter(e => e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const validEvents = recentEvents.filter(e => e && e.trim() && e !== 'Event 1' && e !== 'Event 2' && e !== 'Event 3');
|
||||
|
||||
// If no valid events, show at least one placeholder
|
||||
if (validEvents.length === 0) {
|
||||
validEvents.push('Click to add event');
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="rpg-dashboard rpg-dashboard-row-3">
|
||||
<div class="rpg-dashboard-widget rpg-events-widget">
|
||||
<div class="rpg-notebook-header">
|
||||
<div class="rpg-notebook-ring"></div>
|
||||
<div class="rpg-notebook-ring"></div>
|
||||
<div class="rpg-notebook-ring"></div>
|
||||
</div>
|
||||
<div class="rpg-notebook-title">Recent Events</div>
|
||||
<div class="rpg-notebook-lines">
|
||||
`;
|
||||
|
||||
// Dynamically generate event lines (max 3)
|
||||
for (let i = 0; i < Math.min(validEvents.length, 3); i++) {
|
||||
html += `
|
||||
<div class="rpg-notebook-line">
|
||||
<span class="rpg-bullet">•</span>
|
||||
<span class="rpg-event-text rpg-editable" contenteditable="true" data-field="event${i + 1}" title="Click to edit">${validEvents[i]}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// If we have less than 3 events, add empty placeholders with + icon
|
||||
for (let i = validEvents.length; i < 3; i++) {
|
||||
html += `
|
||||
<div class="rpg-notebook-line rpg-event-add">
|
||||
<span class="rpg-bullet">+</span>
|
||||
<span class="rpg-event-text rpg-editable rpg-event-placeholder" contenteditable="true" data-field="event${i + 1}" title="Click to add event">Add event...</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Close the scrollable content wrapper
|
||||
html += '</div>';
|
||||
|
||||
$infoBoxContainer.html(html);
|
||||
|
||||
// Add event handlers for editable Info Box fields
|
||||
@@ -320,7 +435,12 @@ export function renderInfoBox() {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle recent events separately
|
||||
if (field === 'event1' || field === 'event2' || field === 'event3') {
|
||||
updateRecentEvent(field, value);
|
||||
} else {
|
||||
updateInfoBoxField(field, value);
|
||||
}
|
||||
});
|
||||
|
||||
// For date fields, show full value on focus
|
||||
@@ -610,3 +730,84 @@ export function updateInfoBoxField(field, value) {
|
||||
renderInfoBox();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a recent event in the committed tracker data
|
||||
* @param {string} field - event1, event2, or event3
|
||||
* @param {string} value - New event text
|
||||
*/
|
||||
function updateRecentEvent(field, value) {
|
||||
// Map field to index
|
||||
const eventIndex = {
|
||||
'event1': 0,
|
||||
'event2': 1,
|
||||
'event3': 2
|
||||
}[field];
|
||||
|
||||
if (eventIndex !== undefined) {
|
||||
// Parse current infoBox to get existing events
|
||||
const lines = (committedTrackerData.infoBox || '').split('\n');
|
||||
let recentEvents = [];
|
||||
|
||||
// Find existing Recent Events line
|
||||
const recentEventsLine = lines.find(line => line.startsWith('Recent Events:'));
|
||||
if (recentEventsLine) {
|
||||
const eventsString = recentEventsLine.replace('Recent Events:', '').trim();
|
||||
if (eventsString) {
|
||||
recentEvents = eventsString.split(',').map(e => e.trim()).filter(e => e);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure array has enough slots
|
||||
while (recentEvents.length <= eventIndex) {
|
||||
recentEvents.push('');
|
||||
}
|
||||
|
||||
// Update the specific event
|
||||
recentEvents[eventIndex] = value;
|
||||
|
||||
// Filter out empty events and rebuild the line
|
||||
const validEvents = recentEvents.filter(e => e && e.trim());
|
||||
const newRecentEventsLine = validEvents.length > 0
|
||||
? `Recent Events: ${validEvents.join(', ')}`
|
||||
: '';
|
||||
|
||||
// Update infoBox with new Recent Events line
|
||||
const updatedLines = lines.filter(line => !line.startsWith('Recent Events:'));
|
||||
if (newRecentEventsLine) {
|
||||
// Add Recent Events line at the end (before any empty lines)
|
||||
let insertIndex = updatedLines.length;
|
||||
for (let i = updatedLines.length - 1; i >= 0; i--) {
|
||||
if (updatedLines[i].trim() !== '') {
|
||||
insertIndex = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
updatedLines.splice(insertIndex, 0, newRecentEventsLine);
|
||||
}
|
||||
|
||||
committedTrackerData.infoBox = updatedLines.join('\n');
|
||||
lastGeneratedData.infoBox = updatedLines.join('\n');
|
||||
|
||||
// Update the message's swipe data
|
||||
const chat = getContext().chat;
|
||||
if (chat && chat.length > 0) {
|
||||
for (let i = chat.length - 1; i >= 0; i--) {
|
||||
const message = chat[i];
|
||||
if (!message.is_user) {
|
||||
if (message.extra && message.extra.rpg_companion_swipes) {
|
||||
const swipeId = message.swipe_id || 0;
|
||||
if (message.extra.rpg_companion_swipes[swipeId]) {
|
||||
message.extra.rpg_companion_swipes[swipeId].infoBox = updatedLines.join('\n');
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveChatData();
|
||||
renderInfoBox();
|
||||
console.log(`[RPG Companion] Updated recent event ${field}:`, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* Quests Rendering Module
|
||||
* Handles UI rendering for quests system (main and optional quests)
|
||||
*/
|
||||
|
||||
import { extensionSettings, $questsContainer } from '../../core/state.js';
|
||||
import { saveSettings } from '../../core/persistence.js';
|
||||
|
||||
/**
|
||||
* HTML escape helper
|
||||
* @param {string} text - Text to escape
|
||||
* @returns {string} Escaped HTML
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the quests sub-tab navigation (Main, Optional)
|
||||
* @param {string} activeTab - Currently active sub-tab ('main', 'optional')
|
||||
* @returns {string} HTML for sub-tab navigation
|
||||
*/
|
||||
export function renderQuestsSubTabs(activeTab = 'main') {
|
||||
return `
|
||||
<div class="rpg-quests-subtabs">
|
||||
<button class="rpg-quests-subtab ${activeTab === 'main' ? 'active' : ''}" data-tab="main">
|
||||
Main Quest
|
||||
</button>
|
||||
<button class="rpg-quests-subtab ${activeTab === 'optional' ? 'active' : ''}" data-tab="optional">
|
||||
Optional Quests
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the main quest view
|
||||
* @param {string} mainQuest - Current main quest title
|
||||
* @returns {string} HTML for main quest view
|
||||
*/
|
||||
export function renderMainQuestView(mainQuest) {
|
||||
const questDisplay = (mainQuest && mainQuest !== 'None') ? mainQuest : '';
|
||||
const hasQuest = questDisplay.length > 0;
|
||||
|
||||
return `
|
||||
<div class="rpg-quest-section">
|
||||
<div class="rpg-quest-header">
|
||||
<h3 class="rpg-quest-section-title">Main Quests</h3>
|
||||
${!hasQuest ? `<button class="rpg-add-quest-btn" data-action="add-quest" data-field="main" title="Add main quests">
|
||||
<i class="fa-solid fa-plus"></i> Add Quest
|
||||
</button>` : ''}
|
||||
</div>
|
||||
<div class="rpg-quest-content">
|
||||
${hasQuest ? `
|
||||
<div class="rpg-inline-form" id="rpg-edit-quest-form-main" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-edit-quest-main" value="${escapeHtml(questDisplay)}" />
|
||||
<div class="rpg-inline-buttons">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-edit-quest" data-field="main">
|
||||
<i class="fa-solid fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-edit-quest" data-field="main">
|
||||
<i class="fa-solid fa-check"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-quest-item" data-field="main">
|
||||
<div class="rpg-quest-title">${escapeHtml(questDisplay)}</div>
|
||||
<div class="rpg-quest-actions">
|
||||
<button class="rpg-quest-edit" data-action="edit-quest" data-field="main" title="Edit quest">
|
||||
<i class="fa-solid fa-edit"></i>
|
||||
</button>
|
||||
<button class="rpg-quest-remove" data-action="remove-quest" data-field="main" title="Complete/Remove quest">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
` : `
|
||||
<div class="rpg-inline-form" id="rpg-add-quest-form-main" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-quest-main" placeholder="Enter main quests title..." />
|
||||
<div class="rpg-inline-actions">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="main">
|
||||
<i class="fa-solid fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-quest" data-field="main">
|
||||
<i class="fa-solid fa-check"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-quest-empty">No active main quests</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="rpg-quest-hint">
|
||||
<i class="fa-solid fa-lightbulb"></i>
|
||||
The main quests represent your primary objective in the story.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the optional quests view
|
||||
* @param {string[]} optionalQuests - Array of optional quest titles
|
||||
* @returns {string} HTML for optional quests view
|
||||
*/
|
||||
export function renderOptionalQuestsView(optionalQuests) {
|
||||
const quests = optionalQuests.filter(q => q && q !== 'None');
|
||||
|
||||
let questsHtml = '';
|
||||
if (quests.length === 0) {
|
||||
questsHtml = '<div class="rpg-quest-empty">No active optional quests</div>';
|
||||
} else {
|
||||
questsHtml = quests.map((quest, index) => `
|
||||
<div class="rpg-quest-item" data-field="optional" data-index="${index}">
|
||||
<div class="rpg-quest-title rpg-editable" contenteditable="true" data-field="optional" data-index="${index}" title="Click to edit">${escapeHtml(quest)}</div>
|
||||
<div class="rpg-quest-actions">
|
||||
<button class="rpg-quest-remove" data-action="remove-quest" data-field="optional" data-index="${index}" title="Complete/Remove quest">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="rpg-quest-section">
|
||||
<div class="rpg-quest-header">
|
||||
<h3 class="rpg-quest-section-title">Optional Quests</h3>
|
||||
<button class="rpg-add-quest-btn" data-action="add-quest" data-field="optional" title="Add optional quest">
|
||||
<i class="fa-solid fa-plus"></i> Add Quest
|
||||
</button>
|
||||
</div>
|
||||
<div class="rpg-quest-content">
|
||||
<div class="rpg-inline-form" id="rpg-add-quest-form-optional" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-quest-optional" placeholder="Enter optional quest title..." />
|
||||
<div class="rpg-inline-buttons">
|
||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="optional">
|
||||
<i class="fa-solid fa-times"></i> Cancel
|
||||
</button>
|
||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-quest" data-field="optional">
|
||||
<i class="fa-solid fa-check"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-quest-list">
|
||||
${questsHtml}
|
||||
</div>
|
||||
<div class="rpg-quest-hint">
|
||||
<i class="fa-solid fa-info-circle"></i>
|
||||
Optional quests are side objectives that complement your main story.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main render function for quests
|
||||
*/
|
||||
export function renderQuests() {
|
||||
if (!extensionSettings.showInventory || !$questsContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current sub-tab from container or default to 'main'
|
||||
const activeSubTab = $questsContainer.data('active-subtab') || 'main';
|
||||
|
||||
// Get quests data
|
||||
const mainQuest = extensionSettings.quests.main || 'None';
|
||||
const optionalQuests = extensionSettings.quests.optional || [];
|
||||
|
||||
// Build HTML
|
||||
let html = '<div class="rpg-quests-wrapper">';
|
||||
html += renderQuestsSubTabs(activeSubTab);
|
||||
|
||||
// Render active sub-tab
|
||||
html += '<div class="rpg-quests-panels">';
|
||||
if (activeSubTab === 'main') {
|
||||
html += renderMainQuestView(mainQuest);
|
||||
} else {
|
||||
html += renderOptionalQuestsView(optionalQuests);
|
||||
}
|
||||
html += '</div></div>';
|
||||
|
||||
$questsContainer.html(html);
|
||||
|
||||
// Attach event handlers
|
||||
attachQuestEventHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach event handlers for quest interactions
|
||||
*/
|
||||
function attachQuestEventHandlers() {
|
||||
// Sub-tab switching
|
||||
$questsContainer.find('.rpg-quests-subtab').on('click', function() {
|
||||
const tab = $(this).data('tab');
|
||||
$questsContainer.data('active-subtab', tab);
|
||||
renderQuests();
|
||||
});
|
||||
|
||||
// Add quest button
|
||||
$questsContainer.find('[data-action="add-quest"]').on('click', function() {
|
||||
const field = $(this).data('field');
|
||||
$(`#rpg-add-quest-form-${field}`).show();
|
||||
$(`#rpg-new-quest-${field}`).focus();
|
||||
});
|
||||
|
||||
// Cancel add quest
|
||||
$questsContainer.find('[data-action="cancel-add-quest"]').on('click', function() {
|
||||
const field = $(this).data('field');
|
||||
$(`#rpg-add-quest-form-${field}`).hide();
|
||||
$(`#rpg-new-quest-${field}`).val('');
|
||||
});
|
||||
|
||||
// Save add quest
|
||||
$questsContainer.find('[data-action="save-add-quest"]').on('click', function() {
|
||||
const field = $(this).data('field');
|
||||
const input = $(`#rpg-new-quest-${field}`);
|
||||
const questTitle = input.val().trim();
|
||||
|
||||
if (questTitle) {
|
||||
if (field === 'main') {
|
||||
extensionSettings.quests.main = questTitle;
|
||||
} else {
|
||||
if (!extensionSettings.quests.optional) {
|
||||
extensionSettings.quests.optional = [];
|
||||
}
|
||||
extensionSettings.quests.optional.push(questTitle);
|
||||
}
|
||||
saveSettings();
|
||||
renderQuests();
|
||||
}
|
||||
});
|
||||
|
||||
// Edit quest (main only)
|
||||
$questsContainer.find('[data-action="edit-quest"]').on('click', function() {
|
||||
const field = $(this).data('field');
|
||||
$(`#rpg-edit-quest-form-${field}`).show();
|
||||
$('.rpg-quest-item[data-field="main"]').hide();
|
||||
$(`#rpg-edit-quest-${field}`).focus();
|
||||
});
|
||||
|
||||
// Cancel edit quest
|
||||
$questsContainer.find('[data-action="cancel-edit-quest"]').on('click', function() {
|
||||
const field = $(this).data('field');
|
||||
$(`#rpg-edit-quest-form-${field}`).hide();
|
||||
$('.rpg-quest-item[data-field="main"]').show();
|
||||
});
|
||||
|
||||
// Save edit quest
|
||||
$questsContainer.find('[data-action="save-edit-quest"]').on('click', function() {
|
||||
const field = $(this).data('field');
|
||||
const input = $(`#rpg-edit-quest-${field}`);
|
||||
const questTitle = input.val().trim();
|
||||
|
||||
if (questTitle) {
|
||||
extensionSettings.quests.main = questTitle;
|
||||
saveSettings();
|
||||
renderQuests();
|
||||
}
|
||||
});
|
||||
|
||||
// Remove quest
|
||||
$questsContainer.find('[data-action="remove-quest"]').on('click', function() {
|
||||
const field = $(this).data('field');
|
||||
const index = $(this).data('index');
|
||||
|
||||
if (field === 'main') {
|
||||
extensionSettings.quests.main = 'None';
|
||||
} else {
|
||||
extensionSettings.quests.optional.splice(index, 1);
|
||||
}
|
||||
saveSettings();
|
||||
renderQuests();
|
||||
});
|
||||
|
||||
// Inline editing for optional quests
|
||||
$questsContainer.find('.rpg-quest-title.rpg-editable').on('blur', function() {
|
||||
const $this = $(this);
|
||||
const field = $this.data('field');
|
||||
const index = $this.data('index');
|
||||
const newTitle = $this.text().trim();
|
||||
|
||||
if (newTitle && field === 'optional' && index !== undefined) {
|
||||
extensionSettings.quests.optional[index] = newTitle;
|
||||
saveSettings();
|
||||
}
|
||||
});
|
||||
|
||||
// Enter key to save in forms
|
||||
$questsContainer.find('.rpg-inline-input').on('keypress', function(e) {
|
||||
if (e.which === 13) {
|
||||
const field = $(this).attr('id').includes('edit') ?
|
||||
$(this).attr('id').replace('rpg-edit-quest-', '') :
|
||||
$(this).attr('id').replace('rpg-new-quest-', '');
|
||||
|
||||
if ($(this).attr('id').includes('edit')) {
|
||||
$(`[data-action="save-edit-quest"][data-field="${field}"]`).click();
|
||||
} else {
|
||||
$(`[data-action="save-add-quest"][data-field="${field}"]`).click();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -22,9 +22,10 @@ export function setupDesktopTabs() {
|
||||
const $infoBox = $('#rpg-info-box');
|
||||
const $thoughts = $('#rpg-thoughts');
|
||||
const $inventory = $('#rpg-inventory');
|
||||
const $quests = $('#rpg-quests');
|
||||
|
||||
// If no sections exist, nothing to organize
|
||||
if ($userStats.length === 0 && $infoBox.length === 0 && $thoughts.length === 0 && $inventory.length === 0) {
|
||||
if ($userStats.length === 0 && $infoBox.length === 0 && $thoughts.length === 0 && $inventory.length === 0 && $quests.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -39,12 +40,17 @@ export function setupDesktopTabs() {
|
||||
<i class="fa-solid fa-box"></i>
|
||||
<span>Inventory</span>
|
||||
</button>
|
||||
<button class="rpg-tab-btn" data-tab="quests">
|
||||
<i class="fa-solid fa-scroll"></i>
|
||||
<span>Quests</span>
|
||||
</button>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Create tab content containers
|
||||
const $statusTab = $('<div class="rpg-tab-content active" data-tab-content="status"></div>');
|
||||
const $inventoryTab = $('<div class="rpg-tab-content" data-tab-content="inventory"></div>');
|
||||
const $questsTab = $('<div class="rpg-tab-content" data-tab-content="quests"></div>');
|
||||
|
||||
// Move sections into their respective tabs (detach to preserve event handlers)
|
||||
if ($userStats.length > 0) {
|
||||
@@ -63,6 +69,10 @@ export function setupDesktopTabs() {
|
||||
$inventoryTab.append($inventory.detach());
|
||||
$inventory.show();
|
||||
}
|
||||
if ($quests.length > 0) {
|
||||
$questsTab.append($quests.detach());
|
||||
$quests.show();
|
||||
}
|
||||
|
||||
// Hide dividers on desktop tabs (tabs separate content naturally)
|
||||
$('.rpg-divider').hide();
|
||||
@@ -72,6 +82,7 @@ export function setupDesktopTabs() {
|
||||
$tabsContainer.append($tabNav);
|
||||
$tabsContainer.append($statusTab);
|
||||
$tabsContainer.append($inventoryTab);
|
||||
$tabsContainer.append($questsTab);
|
||||
|
||||
// Replace content box with tabs container
|
||||
$contentBox.html('').append($tabsContainer);
|
||||
@@ -102,6 +113,7 @@ export function removeDesktopTabs() {
|
||||
const $infoBox = $('#rpg-info-box').detach();
|
||||
const $thoughts = $('#rpg-thoughts').detach();
|
||||
const $inventory = $('#rpg-inventory').detach();
|
||||
const $quests = $('#rpg-quests').detach();
|
||||
|
||||
// Remove tabs container
|
||||
$('.rpg-tabs-container').remove();
|
||||
@@ -114,18 +126,20 @@ export function removeDesktopTabs() {
|
||||
// Restore original sections to content box in correct order
|
||||
const $contentBox = $('.rpg-content-box');
|
||||
|
||||
// Re-insert sections in original order: User Stats, Info Box, Thoughts, Inventory
|
||||
// Re-insert sections in original order: User Stats, Info Box, Thoughts, Inventory, Quests
|
||||
if ($dividerStats.length) {
|
||||
$dividerStats.before($userStats);
|
||||
$dividerInfo.before($infoBox);
|
||||
$dividerThoughts.before($thoughts);
|
||||
$contentBox.append($inventory);
|
||||
$contentBox.append($quests);
|
||||
} else {
|
||||
// Fallback if dividers don't exist
|
||||
$contentBox.append($userStats);
|
||||
$contentBox.append($infoBox);
|
||||
$contentBox.append($thoughts);
|
||||
$contentBox.append($inventory);
|
||||
$contentBox.append($quests);
|
||||
}
|
||||
|
||||
// Show sections and dividers
|
||||
|
||||
@@ -266,15 +266,11 @@ export function applyPanelPosition() {
|
||||
* Updates the UI based on generation mode selection.
|
||||
*/
|
||||
export function updateGenerationModeUI() {
|
||||
const $mobileBtn = $('#rpg-manual-update-mobile');
|
||||
|
||||
if (extensionSettings.generationMode === 'together') {
|
||||
// In "together" mode, hide both desktop and mobile refresh buttons
|
||||
// In "together" mode, manual update button is hidden
|
||||
$('#rpg-manual-update').hide();
|
||||
$mobileBtn.addClass('rpg-hidden-mode');
|
||||
} else {
|
||||
// In "separate" mode, show both desktop and mobile refresh buttons
|
||||
// In "separate" mode, manual update button is visible
|
||||
$('#rpg-manual-update').show();
|
||||
$mobileBtn.removeClass('rpg-hidden-mode');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,9 +520,10 @@ export function setupMobileTabs() {
|
||||
const $infoBox = $('#rpg-info-box');
|
||||
const $thoughts = $('#rpg-thoughts');
|
||||
const $inventory = $('#rpg-inventory');
|
||||
const $quests = $('#rpg-quests');
|
||||
|
||||
// If no sections exist, nothing to organize
|
||||
if ($userStats.length === 0 && $infoBox.length === 0 && $thoughts.length === 0 && $inventory.length === 0) {
|
||||
if ($userStats.length === 0 && $infoBox.length === 0 && $thoughts.length === 0 && $inventory.length === 0 && $quests.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -531,6 +532,7 @@ export function setupMobileTabs() {
|
||||
const hasStats = $userStats.length > 0;
|
||||
const hasInfo = $infoBox.length > 0 || $thoughts.length > 0;
|
||||
const hasInventory = $inventory.length > 0;
|
||||
const hasQuests = $quests.length > 0;
|
||||
|
||||
// Tab 1: Stats (User Stats only)
|
||||
if (hasStats) {
|
||||
@@ -544,6 +546,10 @@ export function setupMobileTabs() {
|
||||
if (hasInventory) {
|
||||
tabs.push('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="inventory"><i class="fa-solid fa-box"></i><span>Inventory</span></button>');
|
||||
}
|
||||
// Tab 4: Quests
|
||||
if (hasQuests) {
|
||||
tabs.push('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="quests"><i class="fa-solid fa-scroll"></i><span>Quests</span></button>');
|
||||
}
|
||||
|
||||
const $tabNav = $('<div class="rpg-mobile-tabs">' + tabs.join('') + '</div>');
|
||||
|
||||
@@ -552,11 +558,13 @@ export function setupMobileTabs() {
|
||||
if (hasStats) firstTab = 'stats';
|
||||
else if (hasInfo) firstTab = 'info';
|
||||
else if (hasInventory) firstTab = 'inventory';
|
||||
else if (hasQuests) firstTab = 'quests';
|
||||
|
||||
// Create tab content wrappers
|
||||
const $statsTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'stats' ? 'active' : '') + '" data-tab-content="stats"></div>');
|
||||
const $infoTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'info' ? 'active' : '') + '" data-tab-content="info"></div>');
|
||||
const $inventoryTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'inventory' ? 'active' : '') + '" data-tab-content="inventory"></div>');
|
||||
const $questsTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'quests' ? 'active' : '') + '" data-tab-content="quests"></div>');
|
||||
|
||||
// Move sections into their respective tabs (detach to preserve event handlers)
|
||||
// Stats tab: User Stats only
|
||||
@@ -581,6 +589,12 @@ export function setupMobileTabs() {
|
||||
$inventory.show();
|
||||
}
|
||||
|
||||
// Quests tab: Quests only
|
||||
if ($quests.length > 0) {
|
||||
$questsTab.append($quests.detach());
|
||||
$quests.show();
|
||||
}
|
||||
|
||||
// Hide dividers on mobile
|
||||
$('.rpg-divider').hide();
|
||||
|
||||
@@ -592,6 +606,8 @@ export function setupMobileTabs() {
|
||||
if (hasStats) $mobileContainer.append($statsTab);
|
||||
if (hasInfo) $mobileContainer.append($infoTab);
|
||||
if (hasInventory) $mobileContainer.append($inventoryTab);
|
||||
if (hasQuests) $mobileContainer.append($questsTab);
|
||||
if (hasInventory) $mobileContainer.append($inventoryTab);
|
||||
|
||||
// Insert mobile tab structure at the beginning of content box
|
||||
$contentBox.prepend($mobileContainer);
|
||||
@@ -619,6 +635,7 @@ export function removeMobileTabs() {
|
||||
const $infoBox = $('#rpg-info-box').detach();
|
||||
const $thoughts = $('#rpg-thoughts').detach();
|
||||
const $inventory = $('#rpg-inventory').detach();
|
||||
const $quests = $('#rpg-quests').detach();
|
||||
|
||||
// Remove mobile tab container
|
||||
$('.rpg-mobile-container').remove();
|
||||
@@ -631,14 +648,16 @@ export function removeMobileTabs() {
|
||||
// Restore original sections to content box in correct order
|
||||
const $contentBox = $('.rpg-content-box');
|
||||
|
||||
// Re-insert sections in original order: User Stats, Info Box, Thoughts, Inventory
|
||||
// Re-insert sections in original order: User Stats, Info Box, Thoughts, Inventory, Quests
|
||||
if ($dividerStats.length) {
|
||||
$dividerStats.before($userStats);
|
||||
$dividerInfo.before($infoBox);
|
||||
$dividerThoughts.before($thoughts);
|
||||
$contentBox.append($inventory);
|
||||
$contentBox.append($quests);
|
||||
} else {
|
||||
// Fallback if dividers don't exist
|
||||
$contentBox.prepend($quests);
|
||||
$contentBox.prepend($inventory);
|
||||
$contentBox.prepend($thoughts);
|
||||
$contentBox.prepend($infoBox);
|
||||
|
||||
@@ -52,6 +52,11 @@
|
||||
<div id="rpg-inventory" class="rpg-section rpg-inventory-section">
|
||||
<!-- Content will be populated by JavaScript -->
|
||||
</div>
|
||||
|
||||
<!-- Quests Section -->
|
||||
<div id="rpg-quests" class="rpg-section rpg-quests-section">
|
||||
<!-- Content will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HTML Prompt Toggle -->
|
||||
@@ -223,6 +228,12 @@
|
||||
<small>Number of recent messages to include (Separate mode only)</small>
|
||||
</div>
|
||||
|
||||
<div class="rpg-setting-row">
|
||||
<label for="rpg-memory-messages">Memory Batch Size:</label>
|
||||
<input type="number" id="rpg-memory-messages" min="4" max="50" value="16" class="rpg-input" />
|
||||
<small>Number of messages to process per batch in Memory Recollection</small>
|
||||
</div>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-use-separate-preset" />
|
||||
<span>Use model connected to RPG Companion Trackers preset</span>
|
||||
|
||||
Reference in New Issue
Block a user