From 98ef751a9fa59e141fd43150a8f0b0fc89712d5f Mon Sep 17 00:00:00 2001 From: tomt610 Date: Fri, 9 Jan 2026 19:39:05 +0000 Subject: [PATCH] Implement historical context injection for chat messages and enhance settings for persistence --- index.js | 5 +- src/core/state.js | 60 ++-- src/systems/generation/injector.js | 199 +++++++++++- src/systems/generation/promptBuilder.js | 288 ++++++++++++++++- src/systems/integration/sillytavern.js | 5 +- src/systems/ui/trackerEditor.js | 394 ++++++++++++++++++++++-- template.html | 5 + 7 files changed, 899 insertions(+), 57 deletions(-) diff --git a/index.js b/index.js index 26170ce..f63c253 100644 --- a/index.js +++ b/index.js @@ -55,7 +55,7 @@ import { } from './src/systems/generation/promptBuilder.js'; import { parseResponse, parseUserStats } from './src/systems/generation/parser.js'; import { updateRPGData, testExternalAPIConnection } from './src/systems/generation/apiClient.js'; -import { onGenerationStarted } from './src/systems/generation/injector.js'; +import { onGenerationStarted, initHistoricalContextInjection } from './src/systems/generation/injector.js'; // Rendering modules import { getSafeThumbnailUrl } from './src/utils/avatars.js'; @@ -1031,6 +1031,9 @@ jQuery(async () => { [event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar, [event_types.SETTINGS_UPDATED]: updatePersonaAvatar }); + + // Initialize historical context injection (uses CHAT_COMPLETION_PROMPT_READY event) + initHistoricalContextInjection(); } catch (error) { console.error('[RPG Companion] Event registration failed:', error); throw error; // This is critical - can't continue without events diff --git a/src/core/state.js b/src/core/state.js index ea758aa..2be2ed4 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -43,6 +43,13 @@ export let extensionSettings = { enableRandomizedPlot: true, // Show randomized plot progression button above chat input enableNaturalPlot: true, // Show natural plot progression button above chat input saveTrackerHistory: false, // Save tracker data in chat history for each message + // History persistence settings - inject selected tracker data into historical messages + historyPersistence: { + enabled: false, // Master toggle for history persistence feature + messageCount: 5, // Number of messages to include (0 = all available) + injectionPosition: 'assistant_message_end', // 'user_message_end', 'assistant_message_end', 'extra_user_message', 'extra_assistant_message' + contextPreamble: '' // Optional custom preamble text (empty = use default short one) + }, panelPosition: 'right', // 'left', 'right', or 'top' theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom customColors: { @@ -91,45 +98,51 @@ export let extensionSettings = { userStats: { // Array of custom stats (allows add/remove/rename) customStats: [ - { id: 'health', name: 'Health', enabled: true }, - { id: 'satiety', name: 'Satiety', enabled: true }, - { id: 'energy', name: 'Energy', enabled: true }, - { id: 'hygiene', name: 'Hygiene', enabled: true }, - { id: 'arousal', name: 'Arousal', enabled: true } + { id: 'health', name: 'Health', enabled: true, persistInHistory: false }, + { id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false }, + { id: 'energy', name: 'Energy', enabled: true, persistInHistory: false }, + { id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false }, + { id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false } ], // RPG Attributes (customizable D&D-style attributes) showRPGAttributes: true, showLevel: true, // Show/hide level in UI and prompts alwaysSendAttributes: false, // If true, always send attributes; if false, only send with dice rolls rpgAttributes: [ - { id: 'str', name: 'STR', enabled: true }, - { id: 'dex', name: 'DEX', enabled: true }, - { id: 'con', name: 'CON', enabled: true }, - { id: 'int', name: 'INT', enabled: true }, - { id: 'wis', name: 'WIS', enabled: true }, - { id: 'cha', name: 'CHA', enabled: true } + { id: 'str', name: 'STR', enabled: true, persistInHistory: false }, + { id: 'dex', name: 'DEX', enabled: true, persistInHistory: false }, + { id: 'con', name: 'CON', enabled: true, persistInHistory: false }, + { id: 'int', name: 'INT', enabled: true, persistInHistory: false }, + { id: 'wis', name: 'WIS', enabled: true, persistInHistory: false }, + { id: 'cha', name: 'CHA', enabled: true, persistInHistory: false } ], // Status section config statusSection: { enabled: true, showMoodEmoji: true, - customFields: ['Conditions'] // User can edit what to track + customFields: ['Conditions'], // User can edit what to track + persistInHistory: false // Persist status in historical messages }, // Optional skills field skillsSection: { enabled: false, label: 'Skills', // User-editable - customFields: [] // Array of skill names - } + customFields: [], // Array of skill names + persistInHistory: false // Persist skills in historical messages + }, + // Inventory persistence + inventoryPersistInHistory: false, // Persist inventory in historical messages + // Quests persistence + questsPersistInHistory: false // Persist quests in historical messages }, infoBox: { widgets: { - date: { enabled: true, format: 'Weekday, Month, Year' }, // Format options in UI - weather: { enabled: true }, - temperature: { enabled: true, unit: 'C' }, // 'C' or 'F' - time: { enabled: true }, - location: { enabled: true }, - recentEvents: { enabled: true } + date: { enabled: true, format: 'Weekday, Month, Year', persistInHistory: true }, // Date enabled by default for history + weather: { enabled: true, persistInHistory: true }, // Weather enabled by default for history + temperature: { enabled: true, unit: 'C', persistInHistory: false }, // 'C' or 'F' + time: { enabled: true, persistInHistory: true }, // Time enabled by default for history + location: { enabled: true, persistInHistory: true }, // Location enabled by default for history + recentEvents: { enabled: true, persistInHistory: false } } }, presentCharacters: { @@ -159,14 +172,15 @@ export let extensionSettings = { }, // Custom fields (appearance, demeanor, etc. - shown after relationship, separated by |) customFields: [ - { id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)' }, - { id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state' } + { id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)', persistInHistory: false }, + { id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state', persistInHistory: false } ], // Thoughts configuration (separate line) thoughts: { enabled: true, name: 'Thoughts', - description: 'Internal Monologue (in first person from character\'s POV, up to three sentences long)' + description: 'Internal Monologue (in first person from character\'s POV, up to three sentences long)', + persistInHistory: false }, // Character stats toggle (optional feature) characterStats: { diff --git a/src/systems/generation/injector.js b/src/systems/generation/injector.js index 2e64dad..0b82d66 100644 --- a/src/systems/generation/injector.js +++ b/src/systems/generation/injector.js @@ -4,7 +4,7 @@ */ import { getContext } from '../../../../../../extensions.js'; -import { setExtensionPrompt, extension_prompt_types, extension_prompt_roles } from '../../../../../../../script.js'; +import { setExtensionPrompt, extension_prompt_types, extension_prompt_roles, eventSource, event_types } from '../../../../../../../script.js'; import { extensionSettings, committedTrackerData, @@ -20,6 +20,7 @@ import { generateTrackerExample, generateTrackerInstructions, generateContextualSummary, + formatHistoricalTrackerData, DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_SPOTIFY_PROMPT, @@ -27,12 +28,182 @@ import { } from './promptBuilder.js'; import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js'; +// Track suppression state for event handler +let currentSuppressionState = false; + // Type imports /** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */ // Track last chat length we committed at to prevent duplicate commits from streaming let lastCommittedChatLength = -1; +// Store original message content for restoration after generation +// Map of message index -> original mes content +let originalMessageContent = new Map(); + +/** + * Builds a map of historical context data from ST chat messages with rpg_companion_swipes data. + * Returns a map keyed by message index with formatted context strings. + * + * @returns {Map} Map of message index to context data + */ +function buildHistoricalContextMap() { + const historyPersistence = extensionSettings.historyPersistence; + if (!historyPersistence || !historyPersistence.enabled) { + return new Map(); + } + + const context = getContext(); + const chat = context.chat; + if (!chat || chat.length < 2) { + return new Map(); + } + + const trackerConfig = extensionSettings.trackerConfig; + const userName = context.name1; + const contextMap = new Map(); + + // Determine how many messages to include (0 = all available) + const messageCount = historyPersistence.messageCount || 0; + const maxMessages = messageCount === 0 ? chat.length : Math.min(messageCount, chat.length); + + // Start from the second-to-last message (skip the most recent one as it gets current context) + // and work backwards + let processedCount = 0; + + for (let i = chat.length - 2; i >= 0 && (messageCount === 0 || processedCount < maxMessages); i--) { + const message = chat[i]; + + // Get the rpg_companion_swipes data for current swipe + const swipeData = message.extra?.rpg_companion_swipes; + if (!swipeData) { + continue; + } + + const currentSwipeId = message.swipe_id || 0; + const trackerData = swipeData[currentSwipeId]; + if (!trackerData) { + continue; + } + + // Format the historical tracker data using the shared function + const formattedContext = formatHistoricalTrackerData(trackerData, trackerConfig, userName); + if (!formattedContext) { + continue; + } + + // Build the context wrapper + const preamble = historyPersistence.contextPreamble || '[Context at this point:]'; + const wrappedContext = `\n${preamble}\n${formattedContext}`; + + // Store with message index and whether it's a user message + contextMap.set(i, { + context: wrappedContext, + isUserMessage: message.is_user + }); + + processedCount++; + } + + return contextMap; +} + +/** + * Injects historical context into chat messages by modifying them in-place. + * Stores original content for restoration after generation. + * This approach works for ALL API types (text completion and chat completion). + */ +function injectHistoricalContextIntoChat() { + const historyPersistence = extensionSettings.historyPersistence; + if (!historyPersistence || !historyPersistence.enabled) { + console.log('[RPG Companion] History persistence not enabled, skipping injection'); + return; + } + + if (currentSuppressionState || !extensionSettings.enabled) { + console.log('[RPG Companion] Skipping history injection: suppressed or disabled'); + return; + } + + const context = getContext(); + const chat = context.chat; + if (!chat || chat.length < 2) { + console.log('[RPG Companion] Chat too short, skipping history injection'); + return; + } + + // Build the context map + const contextMap = buildHistoricalContextMap(); + if (contextMap.size === 0) { + console.log('[RPG Companion] No historical context to inject'); + return; + } + + console.log(`[RPG Companion] Injecting historical context into ${contextMap.size} messages`); + + const position = historyPersistence.injectionPosition || 'assistant_message_end'; + + // Clear any previous stored content + originalMessageContent.clear(); + + let injectedCount = 0; + for (const [msgIdx, data] of contextMap) { + const message = chat[msgIdx]; + if (!message || typeof message.mes !== 'string') { + continue; + } + + const { context: ctxContent, isUserMessage } = data; + + // Determine if we should inject based on position and message type + let shouldInject = false; + + if (position === 'user_message_end' && isUserMessage) { + shouldInject = true; + } else if (position === 'assistant_message_end' && !isUserMessage) { + shouldInject = true; + } else if (position === 'extra_user_message' || position === 'extra_assistant_message') { + // For these positions, inject regardless of message type + shouldInject = true; + } + + if (shouldInject) { + // Store original content for restoration + originalMessageContent.set(msgIdx, message.mes); + + // Modify the message in-place + message.mes = message.mes + ctxContent; + injectedCount++; + console.log(`[RPG Companion] Injected context into message ${msgIdx}`); + } + } + + console.log(`[RPG Companion] Successfully injected historical context into ${injectedCount} messages`); +} + +/** + * Restores original message content after generation completes. + * This ensures the injected context doesn't persist in the actual chat data. + */ +function restoreOriginalMessageContent() { + if (originalMessageContent.size === 0) { + return; + } + + const context = getContext(); + const chat = context.chat; + + console.log(`[RPG Companion] Restoring ${originalMessageContent.size} messages to original content`); + + for (const [msgIdx, originalContent] of originalMessageContent) { + if (chat[msgIdx]) { + chat[msgIdx].mes = originalContent; + } + } + + originalMessageContent.clear(); +} + /** * Event handler for generation start. * Manages tracker data commitment and prompt injection based on generation mode. @@ -355,4 +526,30 @@ Ensure these details naturally reflect and influence the narrative. Character be setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false); setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false); } + + // Set suppression state for the historical context injection + currentSuppressionState = shouldSuppress; + + // Inject historical context directly into chat messages + // This temporarily modifies messages and will be restored after generation + injectHistoricalContextIntoChat(); +} + +/** + * Called when generation ends to restore original message content. + * This should be called from the GENERATION_ENDED event handler. + */ +export function onGenerationEndedCleanup() { + restoreOriginalMessageContent(); +} + +/** + * Initialize the historical context injection event listener + * This should be called once during extension initialization + */ +export function initHistoricalContextInjection() { + // Historical context injection is now handled directly in onGenerationStarted + // by temporarily modifying chat messages. This works for ALL API types. + // Restoration happens in onGenerationEndedCleanup. + console.log('[RPG Companion] Historical context injection initialized (direct chat modification mode)'); } diff --git a/src/systems/generation/promptBuilder.js b/src/systems/generation/promptBuilder.js index a9c8a95..b861e4d 100644 --- a/src/systems/generation/promptBuilder.js +++ b/src/systems/generation/promptBuilder.js @@ -728,6 +728,216 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) { } } +/** + * Formats historical tracker data from a message's rpg_companion_swipes data. + * Only includes tracker fields that have persistInHistory enabled in trackerConfig. + * Uses the same formatting as formatTrackerDataForContext but filtered by persistence settings. + * + * @param {Object} trackerData - The tracker data from message.extra.rpg_companion_swipes[swipeId] + * @param {Object} trackerConfig - The tracker configuration from extensionSettings.trackerConfig + * @param {string} userName - The user's name for personalization + * @returns {string} Formatted historical context or empty string if nothing to include + */ +export function formatHistoricalTrackerData(trackerData, trackerConfig, userName) { + if (!trackerData || !trackerConfig) { + return ''; + } + + let formatted = ''; + + // Helper to safely get values + const getValue = (field) => { + if (field === null || field === undefined) return ''; + if (field && typeof field === 'object' && !Array.isArray(field) && 'value' in field) { + return getValue(field.value); + } + if (typeof field !== 'object') { + return String(field); + } + if (Array.isArray(field)) { + return field.map(item => getValue(item)).filter(Boolean).join(', '); + } + if (field && typeof field === 'object') { + if ('start' in field && 'end' in field) { + return `${getValue(field.start)} - ${getValue(field.end)}`; + } + if ('emoji' in field && 'forecast' in field) { + return `${getValue(field.emoji)} ${getValue(field.forecast)}`; + } + if ('name' in field) { + const name = getValue(field.name); + if ('quantity' in field && field.quantity > 1) { + return `${name} (x${field.quantity})`; + } + return name; + } + if ('title' in field) { + return getValue(field.title); + } + } + return ''; + }; + + try { + // Process userStats if present and has persistence-enabled fields + if (trackerData.userStats) { + const userStatsConfig = trackerConfig.userStats; + const userStatsData = typeof trackerData.userStats === 'string' + ? JSON.parse(trackerData.userStats) + : trackerData.userStats; + + let statsFormatted = ''; + + // Custom stats with persistInHistory enabled + if (userStatsData.stats && Array.isArray(userStatsData.stats)) { + for (const stat of userStatsData.stats) { + const configStat = userStatsConfig.customStats.find(s => s.id === stat.id); + if (configStat?.persistInHistory && stat.value !== undefined) { + const statName = stat.name || configStat.name || stat.id; + statsFormatted += `${statName}: ${stat.value}, `; + } + } + } + + // Status section + if (userStatsConfig.statusSection?.persistInHistory && userStatsData.status) { + const mood = getValue(userStatsData.status.mood || userStatsData.status); + const conditions = getValue(userStatsData.status.conditions); + if (mood) statsFormatted += `Mood: ${mood}, `; + if (conditions && conditions !== 'None') statsFormatted += `Conditions: ${conditions}, `; + } + + // Skills section + if (userStatsConfig.skillsSection?.persistInHistory && userStatsData.skills) { + const skillsList = Array.isArray(userStatsData.skills) + ? userStatsData.skills.map(s => getValue(s)).filter(s => s).join(', ') + : getValue(userStatsData.skills); + if (skillsList) statsFormatted += `Skills: ${skillsList}, `; + } + + // Inventory + if (userStatsConfig.inventoryPersistInHistory && userStatsData.inventory) { + const inv = userStatsData.inventory; + if (inv.onPerson && Array.isArray(inv.onPerson) && inv.onPerson.length > 0) { + const items = inv.onPerson.map(i => getValue(i)).filter(i => i); + if (items.length > 0) statsFormatted += `On Person: ${items.join(', ')}, `; + } + if (inv.clothing && Array.isArray(inv.clothing) && inv.clothing.length > 0) { + const items = inv.clothing.map(i => getValue(i)).filter(i => i); + if (items.length > 0) statsFormatted += `Clothing: ${items.join(', ')}, `; + } + } + + // Quests + if (userStatsConfig.questsPersistInHistory && userStatsData.quests) { + const quests = userStatsData.quests; + if (quests.main) { + const mainQuest = getValue(quests.main); + if (mainQuest && mainQuest !== 'None') statsFormatted += `Quest: ${mainQuest}, `; + } + } + + if (statsFormatted) { + formatted += `${userName}: ${statsFormatted.slice(0, -2)}\n`; + } + } + + // Process infoBox if present and has persistence-enabled widgets + if (trackerData.infoBox) { + const infoBoxConfig = trackerConfig.infoBox; + const infoBoxData = typeof trackerData.infoBox === 'string' + ? JSON.parse(trackerData.infoBox) + : trackerData.infoBox; + + let infoFormatted = ''; + + // Date + if (infoBoxConfig.widgets.date?.persistInHistory && infoBoxData.date) { + const date = getValue(infoBoxData.date); + if (date) infoFormatted += `Date: ${date}, `; + } + + // Time + if (infoBoxConfig.widgets.time?.persistInHistory && infoBoxData.time) { + const time = getValue(infoBoxData.time); + if (time) infoFormatted += `Time: ${time}, `; + } + + // Weather + if (infoBoxConfig.widgets.weather?.persistInHistory && infoBoxData.weather) { + const weather = getValue(infoBoxData.weather); + if (weather) infoFormatted += `Weather: ${weather}, `; + } + + // Temperature + if (infoBoxConfig.widgets.temperature?.persistInHistory && infoBoxData.temperature) { + const temp = getValue(infoBoxData.temperature); + if (temp) infoFormatted += `Temp: ${temp}, `; + } + + // Location + if (infoBoxConfig.widgets.location?.persistInHistory && infoBoxData.location) { + const location = getValue(infoBoxData.location); + if (location) infoFormatted += `Location: ${location}, `; + } + + // Recent Events + if (infoBoxConfig.widgets.recentEvents?.persistInHistory && infoBoxData.recentEvents) { + const events = getValue(infoBoxData.recentEvents); + if (events) infoFormatted += `Events: ${events}, `; + } + + if (infoFormatted) { + formatted += infoFormatted.slice(0, -2) + '\n'; + } + } + + // Process characterThoughts if present and has persistence-enabled fields + if (trackerData.characterThoughts) { + const charsConfig = trackerConfig.presentCharacters; + const charsData = typeof trackerData.characterThoughts === 'string' + ? JSON.parse(trackerData.characterThoughts) + : trackerData.characterThoughts; + + // Characters can be an array or wrapped in an object + const characters = Array.isArray(charsData) ? charsData : (charsData.characters || []); + + for (const char of characters) { + if (!char || !char.name) continue; + + let charFormatted = ''; + + // Custom fields (appearance, demeanor, etc.) + if (char.details && typeof char.details === 'object') { + for (const field of charsConfig.customFields) { + if (field.persistInHistory && char.details[field.id]) { + const value = getValue(char.details[field.id]); + if (value) charFormatted += `${field.name}: ${value}, `; + } + } + } + + // Thoughts + if (charsConfig.thoughts?.persistInHistory && char.thoughts) { + const thoughts = typeof char.thoughts === 'object' && char.thoughts.content + ? getValue(char.thoughts.content) + : getValue(char.thoughts); + if (thoughts) charFormatted += `Thinking: ${thoughts}, `; + } + + if (charFormatted) { + formatted += `${getValue(char.name)}: ${charFormatted.slice(0, -2)}\n`; + } + } + } + + return formatted.trim(); + } catch (e) { + console.warn('[RPG Companion] Failed to format historical tracker data:', e); + return ''; + } +} + /** * Generates a formatted contextual summary for SEPARATE mode injection. * Includes the full tracker data in original format (without code fences and separators). @@ -883,6 +1093,8 @@ export function generateRPGPromptText() { export async function generateSeparateUpdatePrompt() { const depth = extensionSettings.updateDepth; const userName = getContext().name1; + const trackerConfig = extensionSettings.trackerConfig; + const historyPersistence = extensionSettings.historyPersistence; const messages = []; @@ -899,6 +1111,7 @@ export async function generateSeparateUpdatePrompt() { systemMessage += `Here is the description of the protagonist for reference:\n`; systemMessage += `\n{{persona}}\n\n`; systemMessage += `\n`; + systemMessage += `Here are the last few messages in the conversation history (between the user and the roleplayer assistant) you should reference when responding:\n`; messages.push({ @@ -907,13 +1120,34 @@ export async function generateSeparateUpdatePrompt() { }); // /hide command automatically handles checkpoint filtering - // Add chat history as separate user/assistant messages + // Add chat history as separate user/assistant messages with per-message historical context const recentMessages = chat.slice(-depth); + const startIndex = chat.length - depth; + + for (let i = 0; i < recentMessages.length; i++) { + const message = recentMessages[i]; + const chatIndex = startIndex + i; + let content = message.mes; + + // Append historical tracker context to this message if enabled and available + if (historyPersistence?.enabled && chatIndex < chat.length - 1) { + const swipeData = message.extra?.rpg_companion_swipes; + if (swipeData) { + const currentSwipeId = message.swipe_id || 0; + const trackerData = swipeData[currentSwipeId]; + if (trackerData) { + const formattedContext = formatHistoricalTrackerData(trackerData, trackerConfig, userName); + if (formattedContext) { + const preamble = historyPersistence.contextPreamble || '[Context at this point:]'; + content += `\n${preamble}\n${formattedContext}`; + } + } + } + } - for (const message of recentMessages) { messages.push({ role: message.is_user ? 'user' : 'assistant', - content: message.mes + content: content }); } @@ -930,6 +1164,54 @@ export async function generateSeparateUpdatePrompt() { return messages; } +/** + * Builds historical tracker context for AI generation prompts. + * Iterates through recent messages and extracts tracker data for persistence-enabled fields. + * + * @param {number} depth - Number of messages to look back + * @param {Object} trackerConfig - The tracker configuration + * @param {string} userName - The user's name + * @returns {string} Formatted historical context or empty string + */ +function buildHistoricalContextForGeneration(depth, trackerConfig, userName) { + if (!chat || chat.length < 2) { + return ''; + } + + const historyPersistence = extensionSettings.historyPersistence; + const messageCount = historyPersistence?.messageCount || 0; + const maxMessages = messageCount === 0 ? depth : Math.min(messageCount, depth); + + let historicalContext = ''; + let processedCount = 0; + let messageIndex = 0; + + // Start from older messages and work forward for chronological order + const startIndex = Math.max(0, chat.length - 1 - maxMessages); + for (let i = startIndex; i < chat.length - 1 && processedCount < maxMessages; i++) { + const message = chat[i]; + const swipeData = message.extra?.rpg_companion_swipes; + if (!swipeData) { + continue; + } + + const currentSwipeId = message.swipe_id || 0; + const trackerData = swipeData[currentSwipeId]; + if (!trackerData) { + continue; + } + + const formattedContext = formatHistoricalTrackerData(trackerData, trackerConfig, userName); + if (formattedContext) { + messageIndex++; + historicalContext += `[Message ${messageIndex}]\n${formattedContext}\n`; + processedCount++; + } + } + + return historicalContext.trim(); +} + /** * Default custom instruction for avatar prompt generation */ diff --git a/src/systems/integration/sillytavern.js b/src/systems/integration/sillytavern.js index 4b534a6..0064fff 100644 --- a/src/systems/integration/sillytavern.js +++ b/src/systems/integration/sillytavern.js @@ -30,7 +30,7 @@ import { parseResponse, parseUserStats } from '../generation/parser.js'; import { parseAndStoreSpotifyUrl, convertToEmbedUrl } from '../features/musicPlayer.js'; import { updateRPGData } from '../generation/apiClient.js'; import { removeLocks } from '../generation/lockManager.js'; -import { onGenerationStarted } from '../generation/injector.js'; +import { onGenerationStarted, onGenerationEndedCleanup } from '../generation/injector.js'; // Rendering import { renderUserStats } from '../rendering/userStats.js'; @@ -453,6 +453,9 @@ export function clearExtensionPrompts() { export async function onGenerationEnded() { // console.log('[RPG Companion] 🏁 onGenerationEnded called'); + // Restore original message content that was modified for historical context injection + onGenerationEndedCleanup(); + // Note: isGenerating flag is cleared in onMessageReceived after parsing (together mode) // or in apiClient.js after separate generation completes (separate mode) diff --git a/src/systems/ui/trackerEditor.js b/src/systems/ui/trackerEditor.js index 183390d..2ebe29b 100644 --- a/src/systems/ui/trackerEditor.js +++ b/src/systems/ui/trackerEditor.js @@ -267,40 +267,44 @@ function resetToDefaults() { extensionSettings.trackerConfig = { userStats: { customStats: [ - { id: 'health', name: 'Health', enabled: true }, - { id: 'satiety', name: 'Satiety', enabled: true }, - { id: 'energy', name: 'Energy', enabled: true }, - { id: 'hygiene', name: 'Hygiene', enabled: true }, - { id: 'arousal', name: 'Arousal', enabled: true } + { id: 'health', name: 'Health', enabled: true, persistInHistory: false }, + { id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false }, + { id: 'energy', name: 'Energy', enabled: true, persistInHistory: false }, + { id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false }, + { id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false } ], showRPGAttributes: true, rpgAttributes: [ - { id: 'str', name: 'STR', enabled: true }, - { id: 'dex', name: 'DEX', enabled: true }, - { id: 'con', name: 'CON', enabled: true }, - { id: 'int', name: 'INT', enabled: true }, - { id: 'wis', name: 'WIS', enabled: true }, - { id: 'cha', name: 'CHA', enabled: true } + { id: 'str', name: 'STR', enabled: true, persistInHistory: false }, + { id: 'dex', name: 'DEX', enabled: true, persistInHistory: false }, + { id: 'con', name: 'CON', enabled: true, persistInHistory: false }, + { id: 'int', name: 'INT', enabled: true, persistInHistory: false }, + { id: 'wis', name: 'WIS', enabled: true, persistInHistory: false }, + { id: 'cha', name: 'CHA', enabled: true, persistInHistory: false } ], statusSection: { enabled: true, showMoodEmoji: true, - customFields: ['Conditions'] + customFields: ['Conditions'], + persistInHistory: false }, skillsSection: { enabled: false, label: 'Skills', - customFields: [] - } + customFields: [], + persistInHistory: false + }, + inventoryPersistInHistory: false, + questsPersistInHistory: false }, infoBox: { widgets: { - date: { enabled: true, format: 'Weekday, Month, Year' }, - weather: { enabled: true }, - temperature: { enabled: true, unit: 'C' }, - time: { enabled: true }, - location: { enabled: true }, - recentEvents: { enabled: true } + date: { enabled: true, format: 'Weekday, Month, Year', persistInHistory: true }, + weather: { enabled: true, persistInHistory: true }, + temperature: { enabled: true, unit: 'C', persistInHistory: false }, + time: { enabled: true, persistInHistory: true }, + location: { enabled: true, persistInHistory: true }, + recentEvents: { enabled: true, persistInHistory: false } } }, presentCharacters: { @@ -325,13 +329,14 @@ function resetToDefaults() { 'Neutral': '⚖️' }, customFields: [ - { id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)' }, - { id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state' } + { id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)', persistInHistory: false }, + { id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state', persistInHistory: false } ], thoughts: { enabled: true, name: 'Thoughts', - description: 'Internal Monologue (in first person from character\'s POV, up to three sentences long)' + description: 'Internal Monologue (in first person from character\'s POV, up to three sentences long)', + persistInHistory: false }, characterStats: { enabled: false, @@ -342,6 +347,13 @@ function resetToDefaults() { } } }; + // Reset history persistence settings + extensionSettings.historyPersistence = { + enabled: false, + messageCount: 5, + injectionPosition: 'assistant_message_end', + contextPreamble: '' + }; } /** @@ -351,13 +363,15 @@ function exportTrackerPreset() { try { // Get the current tracker configuration const config = extensionSettings.trackerConfig; + const historyPersistence = extensionSettings.historyPersistence; // Create a preset object with metadata const preset = { name: 'Custom Tracker Preset', - version: '1.0', + version: '1.1', // Bumped version for historyPersistence support exportDate: new Date().toISOString(), - trackerConfig: JSON.parse(JSON.stringify(config)) // Deep copy + trackerConfig: JSON.parse(JSON.stringify(config)), // Deep copy + historyPersistence: historyPersistence ? JSON.parse(JSON.stringify(historyPersistence)) : null // Include history persistence settings }; // Convert to JSON @@ -422,9 +436,75 @@ function migrateTrackerPreset(config) { if (!migrated.presentCharacters.relationships.relationshipEmojis) { migrated.presentCharacters.relationships.relationshipEmojis = {}; } + + // Add persistInHistory to customFields if missing (v3.4.0) + if (migrated.presentCharacters.customFields) { + migrated.presentCharacters.customFields = migrated.presentCharacters.customFields.map(field => ({ + ...field, + persistInHistory: field.persistInHistory ?? false + })); + } + + // Add persistInHistory to thoughts if missing (v3.4.0) + if (migrated.presentCharacters.thoughts && migrated.presentCharacters.thoughts.persistInHistory === undefined) { + migrated.presentCharacters.thoughts.persistInHistory = false; + } } - // Add any other migration logic here for future format changes + // Add persistInHistory to userStats fields if missing (v3.4.0) + if (migrated.userStats) { + // Custom stats + if (migrated.userStats.customStats) { + migrated.userStats.customStats = migrated.userStats.customStats.map(stat => ({ + ...stat, + persistInHistory: stat.persistInHistory ?? false + })); + } + + // RPG Attributes + if (migrated.userStats.rpgAttributes) { + migrated.userStats.rpgAttributes = migrated.userStats.rpgAttributes.map(attr => ({ + ...attr, + persistInHistory: attr.persistInHistory ?? false + })); + } + + // Status section + if (migrated.userStats.statusSection && migrated.userStats.statusSection.persistInHistory === undefined) { + migrated.userStats.statusSection.persistInHistory = false; + } + + // Skills section + if (migrated.userStats.skillsSection && migrated.userStats.skillsSection.persistInHistory === undefined) { + migrated.userStats.skillsSection.persistInHistory = false; + } + + // Inventory and quests persistence + if (migrated.userStats.inventoryPersistInHistory === undefined) { + migrated.userStats.inventoryPersistInHistory = false; + } + if (migrated.userStats.questsPersistInHistory === undefined) { + migrated.userStats.questsPersistInHistory = false; + } + } + + // Add persistInHistory to infoBox widgets if missing (v3.4.0) + if (migrated.infoBox && migrated.infoBox.widgets) { + const defaultPersistence = { + date: true, + weather: true, + temperature: false, + time: true, + location: true, + recentEvents: false + }; + + for (const [widgetId, widget] of Object.entries(migrated.infoBox.widgets)) { + if (widget.persistInHistory === undefined) { + widget.persistInHistory = defaultPersistence[widgetId] ?? false; + } + } + } return migrated; } @@ -459,8 +539,11 @@ function importTrackerPreset() { // Migrate old preset format to current format const migratedConfig = migrateTrackerPreset(data.trackerConfig); + // Extract historyPersistence if present in the import file + const historyPersistence = data.historyPersistence || null; + // Show import mode selection dialog - showImportModeDialog(migratedConfig, data.name || file.name.replace('.json', '')); + showImportModeDialog(migratedConfig, data.name || file.name.replace('.json', ''), historyPersistence); } catch (error) { console.error('[RPG Companion] Error importing tracker preset:', error); toastr.error(i18n.getTranslation('template.trackerEditorModal.messages.importError') || @@ -476,8 +559,9 @@ function importTrackerPreset() { * Show dialog to choose import mode * @param {Object} migratedConfig - The migrated tracker config * @param {string} suggestedName - Suggested name for new preset + * @param {Object|null} historyPersistence - The history persistence settings from import (if any) */ -function showImportModeDialog(migratedConfig, suggestedName) { +function showImportModeDialog(migratedConfig, suggestedName, historyPersistence = null) { // Create dialog overlay const dialogHtml = `
@@ -509,6 +593,11 @@ function showImportModeDialog(migratedConfig, suggestedName) { // Apply the migrated configuration to current extensionSettings.trackerConfig = migratedConfig; + // Apply historyPersistence settings if present in import + if (historyPersistence) { + extensionSettings.historyPersistence = historyPersistence; + } + // Save to the active preset (saveToPreset uses current trackerConfig) const activePresetId = getActivePresetId(); if (activePresetId) { @@ -532,6 +621,11 @@ function showImportModeDialog(migratedConfig, suggestedName) { // Set the migrated config as current first extensionSettings.trackerConfig = migratedConfig; + // Apply historyPersistence settings if present in import + if (historyPersistence) { + extensionSettings.historyPersistence = historyPersistence; + } + // Create new preset (createPreset uses current trackerConfig) const newPresetId = createPreset(presetName); if (newPresetId) { @@ -563,6 +657,7 @@ function renderEditorUI() { renderUserStatsTab(); renderInfoBoxTab(); renderPresentCharactersTab(); + renderHistoryPersistenceTab(); } /** @@ -1265,3 +1360,246 @@ function setupPresentCharactersListeners() { extensionSettings.trackerConfig.presentCharacters.characterStats.customStats[index].name = $(this).val(); }); } + +/** + * Render History Persistence configuration tab + * Allows users to select which tracker data should be injected into historical messages + */ +function renderHistoryPersistenceTab() { + const historyPersistence = extensionSettings.historyPersistence || { + enabled: false, + messageCount: 5, + injectionPosition: 'assistant_message_end', + contextPreamble: '' + }; + const userStatsConfig = extensionSettings.trackerConfig.userStats; + const infoBoxConfig = extensionSettings.trackerConfig.infoBox; + const presentCharsConfig = extensionSettings.trackerConfig.presentCharacters; + + let html = '
'; + + // Main toggle and settings + html += `

History Persistence Settings

`; + html += `

Inject selected tracker data into historical messages to help the AI maintain continuity for time-sensitive events, weather changes, and location tracking.

`; + + // Enable toggle + html += '
'; + html += ``; + html += ``; + html += '
'; + + // Message count + html += '
'; + html += ``; + html += ``; + html += '
'; + + // Injection position + html += '
'; + html += ``; + html += ``; + html += '
'; + + // Custom preamble + html += '
'; + html += ``; + html += ``; + html += '
'; + + // User Stats section - which stats to persist + html += `

User Stats

`; + html += `

Select which stats should be included in historical messages.

`; + + // Custom stats + html += '
'; + userStatsConfig.customStats.forEach((stat, index) => { + if (stat.enabled) { + html += ` +
+ + +
+ `; + } + }); + + // Status section + if (userStatsConfig.statusSection?.enabled) { + html += ` +
+ + +
+ `; + } + + // Skills section + if (userStatsConfig.skillsSection?.enabled) { + html += ` +
+ + +
+ `; + } + + // Inventory + html += ` +
+ + +
+ `; + + // Quests + html += ` +
+ + +
+ `; + html += '
'; + + // Info Box section - which widgets to persist + html += `

Info Box

`; + html += `

Select which info box fields should be included in historical messages. These are recommended for time tracking.

`; + + html += '
'; + const widgetLabels = { + date: 'Date', + weather: 'Weather', + temperature: 'Temperature', + time: 'Time', + location: 'Location', + recentEvents: 'Recent Events' + }; + + for (const [widgetId, widget] of Object.entries(infoBoxConfig.widgets)) { + if (widget.enabled) { + html += ` +
+ + +
+ `; + } + } + html += '
'; + + // Present Characters section + html += `

Present Characters

`; + html += `

Select which character fields should be included in historical messages.

`; + + html += '
'; + + // Custom fields (appearance, demeanor, etc.) + presentCharsConfig.customFields.forEach((field, index) => { + if (field.enabled) { + html += ` +
+ + +
+ `; + } + }); + + // Thoughts + if (presentCharsConfig.thoughts?.enabled) { + html += ` +
+ + +
+ `; + } + html += '
'; + + html += '
'; + + $('#rpg-editor-tab-historyPersistence').html(html); + setupHistoryPersistenceListeners(); +} + +/** + * Set up event listeners for History Persistence tab + */ +function setupHistoryPersistenceListeners() { + // Ensure historyPersistence object exists + if (!extensionSettings.historyPersistence) { + extensionSettings.historyPersistence = { + enabled: false, + messageCount: 5, + injectionPosition: 'assistant_message_end', + contextPreamble: '' + }; + } + + // Main toggle + $('#rpg-history-persistence-enabled').off('change').on('change', function() { + extensionSettings.historyPersistence.enabled = $(this).is(':checked'); + }); + + // Message count + $('#rpg-history-message-count').off('change').on('change', function() { + extensionSettings.historyPersistence.messageCount = parseInt($(this).val()) || 0; + }); + + // Injection position + $('#rpg-history-injection-position').off('change').on('change', function() { + extensionSettings.historyPersistence.injectionPosition = $(this).val(); + }); + + // Context preamble + $('#rpg-history-context-preamble').off('blur').on('blur', function() { + extensionSettings.historyPersistence.contextPreamble = $(this).val(); + }); + + // User Stats toggles + $('.rpg-history-stat-toggle').off('change').on('change', function() { + const index = $(this).data('index'); + extensionSettings.trackerConfig.userStats.customStats[index].persistInHistory = $(this).is(':checked'); + }); + + // Status section + $('#rpg-history-status').off('change').on('change', function() { + extensionSettings.trackerConfig.userStats.statusSection.persistInHistory = $(this).is(':checked'); + }); + + // Skills section + $('#rpg-history-skills').off('change').on('change', function() { + extensionSettings.trackerConfig.userStats.skillsSection.persistInHistory = $(this).is(':checked'); + }); + + // Inventory + $('#rpg-history-inventory').off('change').on('change', function() { + extensionSettings.trackerConfig.userStats.inventoryPersistInHistory = $(this).is(':checked'); + }); + + // Quests + $('#rpg-history-quests').off('change').on('change', function() { + extensionSettings.trackerConfig.userStats.questsPersistInHistory = $(this).is(':checked'); + }); + + // Info Box widget toggles + $('.rpg-history-widget-toggle').off('change').on('change', function() { + const widgetId = $(this).data('widget'); + extensionSettings.trackerConfig.infoBox.widgets[widgetId].persistInHistory = $(this).is(':checked'); + }); + + // Present Characters field toggles + $('.rpg-history-charfield-toggle').off('change').on('change', function() { + const index = $(this).data('index'); + extensionSettings.trackerConfig.presentCharacters.customFields[index].persistInHistory = $(this).is(':checked'); + }); + + // Thoughts + $('#rpg-history-thoughts').off('change').on('change', function() { + extensionSettings.trackerConfig.presentCharacters.thoughts.persistInHistory = $(this).is(':checked'); + }); +} diff --git a/template.html b/template.html index cf3d7db..524c718 100644 --- a/template.html +++ b/template.html @@ -709,6 +709,10 @@ Present Characters +
@@ -716,6 +720,7 @@
+