Implement historical context injection for chat messages and enhance settings for persistence
This commit is contained in:
@@ -55,7 +55,7 @@ import {
|
|||||||
} from './src/systems/generation/promptBuilder.js';
|
} from './src/systems/generation/promptBuilder.js';
|
||||||
import { parseResponse, parseUserStats } from './src/systems/generation/parser.js';
|
import { parseResponse, parseUserStats } from './src/systems/generation/parser.js';
|
||||||
import { updateRPGData, testExternalAPIConnection } from './src/systems/generation/apiClient.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
|
// Rendering modules
|
||||||
import { getSafeThumbnailUrl } from './src/utils/avatars.js';
|
import { getSafeThumbnailUrl } from './src/utils/avatars.js';
|
||||||
@@ -1031,6 +1031,9 @@ jQuery(async () => {
|
|||||||
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
|
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
|
||||||
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar
|
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize historical context injection (uses CHAT_COMPLETION_PROMPT_READY event)
|
||||||
|
initHistoricalContextInjection();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[RPG Companion] Event registration failed:', error);
|
console.error('[RPG Companion] Event registration failed:', error);
|
||||||
throw error; // This is critical - can't continue without events
|
throw error; // This is critical - can't continue without events
|
||||||
|
|||||||
+37
-23
@@ -43,6 +43,13 @@ export let extensionSettings = {
|
|||||||
enableRandomizedPlot: true, // Show randomized plot progression button above chat input
|
enableRandomizedPlot: true, // Show randomized plot progression button above chat input
|
||||||
enableNaturalPlot: true, // Show natural 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
|
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'
|
panelPosition: 'right', // 'left', 'right', or 'top'
|
||||||
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
|
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
|
||||||
customColors: {
|
customColors: {
|
||||||
@@ -91,45 +98,51 @@ export let extensionSettings = {
|
|||||||
userStats: {
|
userStats: {
|
||||||
// Array of custom stats (allows add/remove/rename)
|
// Array of custom stats (allows add/remove/rename)
|
||||||
customStats: [
|
customStats: [
|
||||||
{ id: 'health', name: 'Health', enabled: true },
|
{ id: 'health', name: 'Health', enabled: true, persistInHistory: false },
|
||||||
{ id: 'satiety', name: 'Satiety', enabled: true },
|
{ id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false },
|
||||||
{ id: 'energy', name: 'Energy', enabled: true },
|
{ id: 'energy', name: 'Energy', enabled: true, persistInHistory: false },
|
||||||
{ id: 'hygiene', name: 'Hygiene', enabled: true },
|
{ id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false },
|
||||||
{ id: 'arousal', name: 'Arousal', enabled: true }
|
{ id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false }
|
||||||
],
|
],
|
||||||
// RPG Attributes (customizable D&D-style attributes)
|
// RPG Attributes (customizable D&D-style attributes)
|
||||||
showRPGAttributes: true,
|
showRPGAttributes: true,
|
||||||
showLevel: true, // Show/hide level in UI and prompts
|
showLevel: true, // Show/hide level in UI and prompts
|
||||||
alwaysSendAttributes: false, // If true, always send attributes; if false, only send with dice rolls
|
alwaysSendAttributes: false, // If true, always send attributes; if false, only send with dice rolls
|
||||||
rpgAttributes: [
|
rpgAttributes: [
|
||||||
{ id: 'str', name: 'STR', enabled: true },
|
{ id: 'str', name: 'STR', enabled: true, persistInHistory: false },
|
||||||
{ id: 'dex', name: 'DEX', enabled: true },
|
{ id: 'dex', name: 'DEX', enabled: true, persistInHistory: false },
|
||||||
{ id: 'con', name: 'CON', enabled: true },
|
{ id: 'con', name: 'CON', enabled: true, persistInHistory: false },
|
||||||
{ id: 'int', name: 'INT', enabled: true },
|
{ id: 'int', name: 'INT', enabled: true, persistInHistory: false },
|
||||||
{ id: 'wis', name: 'WIS', enabled: true },
|
{ id: 'wis', name: 'WIS', enabled: true, persistInHistory: false },
|
||||||
{ id: 'cha', name: 'CHA', enabled: true }
|
{ id: 'cha', name: 'CHA', enabled: true, persistInHistory: false }
|
||||||
],
|
],
|
||||||
// Status section config
|
// Status section config
|
||||||
statusSection: {
|
statusSection: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
showMoodEmoji: 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
|
// Optional skills field
|
||||||
skillsSection: {
|
skillsSection: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
label: 'Skills', // User-editable
|
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: {
|
infoBox: {
|
||||||
widgets: {
|
widgets: {
|
||||||
date: { enabled: true, format: 'Weekday, Month, Year' }, // Format options in UI
|
date: { enabled: true, format: 'Weekday, Month, Year', persistInHistory: true }, // Date enabled by default for history
|
||||||
weather: { enabled: true },
|
weather: { enabled: true, persistInHistory: true }, // Weather enabled by default for history
|
||||||
temperature: { enabled: true, unit: 'C' }, // 'C' or 'F'
|
temperature: { enabled: true, unit: 'C', persistInHistory: false }, // 'C' or 'F'
|
||||||
time: { enabled: true },
|
time: { enabled: true, persistInHistory: true }, // Time enabled by default for history
|
||||||
location: { enabled: true },
|
location: { enabled: true, persistInHistory: true }, // Location enabled by default for history
|
||||||
recentEvents: { enabled: true }
|
recentEvents: { enabled: true, persistInHistory: false }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
presentCharacters: {
|
presentCharacters: {
|
||||||
@@ -159,14 +172,15 @@ export let extensionSettings = {
|
|||||||
},
|
},
|
||||||
// Custom fields (appearance, demeanor, etc. - shown after relationship, separated by |)
|
// Custom fields (appearance, demeanor, etc. - shown after relationship, separated by |)
|
||||||
customFields: [
|
customFields: [
|
||||||
{ id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)' },
|
{ 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' }
|
{ id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state', persistInHistory: false }
|
||||||
],
|
],
|
||||||
// Thoughts configuration (separate line)
|
// Thoughts configuration (separate line)
|
||||||
thoughts: {
|
thoughts: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
name: 'Thoughts',
|
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)
|
// Character stats toggle (optional feature)
|
||||||
characterStats: {
|
characterStats: {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getContext } from '../../../../../../extensions.js';
|
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 {
|
import {
|
||||||
extensionSettings,
|
extensionSettings,
|
||||||
committedTrackerData,
|
committedTrackerData,
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
generateTrackerExample,
|
generateTrackerExample,
|
||||||
generateTrackerInstructions,
|
generateTrackerInstructions,
|
||||||
generateContextualSummary,
|
generateContextualSummary,
|
||||||
|
formatHistoricalTrackerData,
|
||||||
DEFAULT_HTML_PROMPT,
|
DEFAULT_HTML_PROMPT,
|
||||||
DEFAULT_DIALOGUE_COLORING_PROMPT,
|
DEFAULT_DIALOGUE_COLORING_PROMPT,
|
||||||
DEFAULT_SPOTIFY_PROMPT,
|
DEFAULT_SPOTIFY_PROMPT,
|
||||||
@@ -27,12 +28,182 @@ import {
|
|||||||
} from './promptBuilder.js';
|
} from './promptBuilder.js';
|
||||||
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
||||||
|
|
||||||
|
// Track suppression state for event handler
|
||||||
|
let currentSuppressionState = false;
|
||||||
|
|
||||||
// Type imports
|
// Type imports
|
||||||
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
||||||
|
|
||||||
// Track last chat length we committed at to prevent duplicate commits from streaming
|
// Track last chat length we committed at to prevent duplicate commits from streaming
|
||||||
let lastCommittedChatLength = -1;
|
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<number, {context: string, isUserMessage: boolean}>} 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.
|
* Event handler for generation start.
|
||||||
* Manages tracker data commitment and prompt injection based on generation mode.
|
* 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-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
setExtensionPrompt('rpg-companion-spotify', '', 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)');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
* Generates a formatted contextual summary for SEPARATE mode injection.
|
||||||
* Includes the full tracker data in original format (without code fences and separators).
|
* Includes the full tracker data in original format (without code fences and separators).
|
||||||
@@ -883,6 +1093,8 @@ export function generateRPGPromptText() {
|
|||||||
export async function generateSeparateUpdatePrompt() {
|
export async function generateSeparateUpdatePrompt() {
|
||||||
const depth = extensionSettings.updateDepth;
|
const depth = extensionSettings.updateDepth;
|
||||||
const userName = getContext().name1;
|
const userName = getContext().name1;
|
||||||
|
const trackerConfig = extensionSettings.trackerConfig;
|
||||||
|
const historyPersistence = extensionSettings.historyPersistence;
|
||||||
|
|
||||||
const messages = [];
|
const messages = [];
|
||||||
|
|
||||||
@@ -899,6 +1111,7 @@ export async function generateSeparateUpdatePrompt() {
|
|||||||
systemMessage += `Here is the description of the protagonist for reference:\n`;
|
systemMessage += `Here is the description of the protagonist for reference:\n`;
|
||||||
systemMessage += `<protagonist>\n{{persona}}\n</protagonist>\n`;
|
systemMessage += `<protagonist>\n{{persona}}\n</protagonist>\n`;
|
||||||
systemMessage += `\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<history>`;
|
systemMessage += `Here are the last few messages in the conversation history (between the user and the roleplayer assistant) you should reference when responding:\n<history>`;
|
||||||
|
|
||||||
messages.push({
|
messages.push({
|
||||||
@@ -907,13 +1120,34 @@ export async function generateSeparateUpdatePrompt() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// /hide command automatically handles checkpoint filtering
|
// /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 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({
|
messages.push({
|
||||||
role: message.is_user ? 'user' : 'assistant',
|
role: message.is_user ? 'user' : 'assistant',
|
||||||
content: message.mes
|
content: content
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -930,6 +1164,54 @@ export async function generateSeparateUpdatePrompt() {
|
|||||||
return messages;
|
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
|
* Default custom instruction for avatar prompt generation
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { parseResponse, parseUserStats } from '../generation/parser.js';
|
|||||||
import { parseAndStoreSpotifyUrl, convertToEmbedUrl } from '../features/musicPlayer.js';
|
import { parseAndStoreSpotifyUrl, convertToEmbedUrl } from '../features/musicPlayer.js';
|
||||||
import { updateRPGData } from '../generation/apiClient.js';
|
import { updateRPGData } from '../generation/apiClient.js';
|
||||||
import { removeLocks } from '../generation/lockManager.js';
|
import { removeLocks } from '../generation/lockManager.js';
|
||||||
import { onGenerationStarted } from '../generation/injector.js';
|
import { onGenerationStarted, onGenerationEndedCleanup } from '../generation/injector.js';
|
||||||
|
|
||||||
// Rendering
|
// Rendering
|
||||||
import { renderUserStats } from '../rendering/userStats.js';
|
import { renderUserStats } from '../rendering/userStats.js';
|
||||||
@@ -453,6 +453,9 @@ export function clearExtensionPrompts() {
|
|||||||
export async function onGenerationEnded() {
|
export async function onGenerationEnded() {
|
||||||
// console.log('[RPG Companion] 🏁 onGenerationEnded called');
|
// 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)
|
// Note: isGenerating flag is cleared in onMessageReceived after parsing (together mode)
|
||||||
// or in apiClient.js after separate generation completes (separate mode)
|
// or in apiClient.js after separate generation completes (separate mode)
|
||||||
|
|
||||||
|
|||||||
+366
-28
@@ -267,40 +267,44 @@ function resetToDefaults() {
|
|||||||
extensionSettings.trackerConfig = {
|
extensionSettings.trackerConfig = {
|
||||||
userStats: {
|
userStats: {
|
||||||
customStats: [
|
customStats: [
|
||||||
{ id: 'health', name: 'Health', enabled: true },
|
{ id: 'health', name: 'Health', enabled: true, persistInHistory: false },
|
||||||
{ id: 'satiety', name: 'Satiety', enabled: true },
|
{ id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false },
|
||||||
{ id: 'energy', name: 'Energy', enabled: true },
|
{ id: 'energy', name: 'Energy', enabled: true, persistInHistory: false },
|
||||||
{ id: 'hygiene', name: 'Hygiene', enabled: true },
|
{ id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false },
|
||||||
{ id: 'arousal', name: 'Arousal', enabled: true }
|
{ id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false }
|
||||||
],
|
],
|
||||||
showRPGAttributes: true,
|
showRPGAttributes: true,
|
||||||
rpgAttributes: [
|
rpgAttributes: [
|
||||||
{ id: 'str', name: 'STR', enabled: true },
|
{ id: 'str', name: 'STR', enabled: true, persistInHistory: false },
|
||||||
{ id: 'dex', name: 'DEX', enabled: true },
|
{ id: 'dex', name: 'DEX', enabled: true, persistInHistory: false },
|
||||||
{ id: 'con', name: 'CON', enabled: true },
|
{ id: 'con', name: 'CON', enabled: true, persistInHistory: false },
|
||||||
{ id: 'int', name: 'INT', enabled: true },
|
{ id: 'int', name: 'INT', enabled: true, persistInHistory: false },
|
||||||
{ id: 'wis', name: 'WIS', enabled: true },
|
{ id: 'wis', name: 'WIS', enabled: true, persistInHistory: false },
|
||||||
{ id: 'cha', name: 'CHA', enabled: true }
|
{ id: 'cha', name: 'CHA', enabled: true, persistInHistory: false }
|
||||||
],
|
],
|
||||||
statusSection: {
|
statusSection: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
showMoodEmoji: true,
|
showMoodEmoji: true,
|
||||||
customFields: ['Conditions']
|
customFields: ['Conditions'],
|
||||||
|
persistInHistory: false
|
||||||
},
|
},
|
||||||
skillsSection: {
|
skillsSection: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
label: 'Skills',
|
label: 'Skills',
|
||||||
customFields: []
|
customFields: [],
|
||||||
}
|
persistInHistory: false
|
||||||
|
},
|
||||||
|
inventoryPersistInHistory: false,
|
||||||
|
questsPersistInHistory: false
|
||||||
},
|
},
|
||||||
infoBox: {
|
infoBox: {
|
||||||
widgets: {
|
widgets: {
|
||||||
date: { enabled: true, format: 'Weekday, Month, Year' },
|
date: { enabled: true, format: 'Weekday, Month, Year', persistInHistory: true },
|
||||||
weather: { enabled: true },
|
weather: { enabled: true, persistInHistory: true },
|
||||||
temperature: { enabled: true, unit: 'C' },
|
temperature: { enabled: true, unit: 'C', persistInHistory: false },
|
||||||
time: { enabled: true },
|
time: { enabled: true, persistInHistory: true },
|
||||||
location: { enabled: true },
|
location: { enabled: true, persistInHistory: true },
|
||||||
recentEvents: { enabled: true }
|
recentEvents: { enabled: true, persistInHistory: false }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
presentCharacters: {
|
presentCharacters: {
|
||||||
@@ -325,13 +329,14 @@ function resetToDefaults() {
|
|||||||
'Neutral': '⚖️'
|
'Neutral': '⚖️'
|
||||||
},
|
},
|
||||||
customFields: [
|
customFields: [
|
||||||
{ id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)' },
|
{ 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' }
|
{ id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state', persistInHistory: false }
|
||||||
],
|
],
|
||||||
thoughts: {
|
thoughts: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
name: 'Thoughts',
|
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: {
|
characterStats: {
|
||||||
enabled: false,
|
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 {
|
try {
|
||||||
// Get the current tracker configuration
|
// Get the current tracker configuration
|
||||||
const config = extensionSettings.trackerConfig;
|
const config = extensionSettings.trackerConfig;
|
||||||
|
const historyPersistence = extensionSettings.historyPersistence;
|
||||||
|
|
||||||
// Create a preset object with metadata
|
// Create a preset object with metadata
|
||||||
const preset = {
|
const preset = {
|
||||||
name: 'Custom Tracker Preset',
|
name: 'Custom Tracker Preset',
|
||||||
version: '1.0',
|
version: '1.1', // Bumped version for historyPersistence support
|
||||||
exportDate: new Date().toISOString(),
|
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
|
// Convert to JSON
|
||||||
@@ -422,9 +436,75 @@ function migrateTrackerPreset(config) {
|
|||||||
if (!migrated.presentCharacters.relationships.relationshipEmojis) {
|
if (!migrated.presentCharacters.relationships.relationshipEmojis) {
|
||||||
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;
|
return migrated;
|
||||||
}
|
}
|
||||||
@@ -459,8 +539,11 @@ function importTrackerPreset() {
|
|||||||
// Migrate old preset format to current format
|
// Migrate old preset format to current format
|
||||||
const migratedConfig = migrateTrackerPreset(data.trackerConfig);
|
const migratedConfig = migrateTrackerPreset(data.trackerConfig);
|
||||||
|
|
||||||
|
// Extract historyPersistence if present in the import file
|
||||||
|
const historyPersistence = data.historyPersistence || null;
|
||||||
|
|
||||||
// Show import mode selection dialog
|
// Show import mode selection dialog
|
||||||
showImportModeDialog(migratedConfig, data.name || file.name.replace('.json', ''));
|
showImportModeDialog(migratedConfig, data.name || file.name.replace('.json', ''), historyPersistence);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[RPG Companion] Error importing tracker preset:', error);
|
console.error('[RPG Companion] Error importing tracker preset:', error);
|
||||||
toastr.error(i18n.getTranslation('template.trackerEditorModal.messages.importError') ||
|
toastr.error(i18n.getTranslation('template.trackerEditorModal.messages.importError') ||
|
||||||
@@ -476,8 +559,9 @@ function importTrackerPreset() {
|
|||||||
* Show dialog to choose import mode
|
* Show dialog to choose import mode
|
||||||
* @param {Object} migratedConfig - The migrated tracker config
|
* @param {Object} migratedConfig - The migrated tracker config
|
||||||
* @param {string} suggestedName - Suggested name for new preset
|
* @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
|
// Create dialog overlay
|
||||||
const dialogHtml = `
|
const dialogHtml = `
|
||||||
<div id="rpg-import-mode-dialog" class="rpg-import-dialog-overlay">
|
<div id="rpg-import-mode-dialog" class="rpg-import-dialog-overlay">
|
||||||
@@ -509,6 +593,11 @@ function showImportModeDialog(migratedConfig, suggestedName) {
|
|||||||
// Apply the migrated configuration to current
|
// Apply the migrated configuration to current
|
||||||
extensionSettings.trackerConfig = migratedConfig;
|
extensionSettings.trackerConfig = migratedConfig;
|
||||||
|
|
||||||
|
// Apply historyPersistence settings if present in import
|
||||||
|
if (historyPersistence) {
|
||||||
|
extensionSettings.historyPersistence = historyPersistence;
|
||||||
|
}
|
||||||
|
|
||||||
// Save to the active preset (saveToPreset uses current trackerConfig)
|
// Save to the active preset (saveToPreset uses current trackerConfig)
|
||||||
const activePresetId = getActivePresetId();
|
const activePresetId = getActivePresetId();
|
||||||
if (activePresetId) {
|
if (activePresetId) {
|
||||||
@@ -532,6 +621,11 @@ function showImportModeDialog(migratedConfig, suggestedName) {
|
|||||||
// Set the migrated config as current first
|
// Set the migrated config as current first
|
||||||
extensionSettings.trackerConfig = migratedConfig;
|
extensionSettings.trackerConfig = migratedConfig;
|
||||||
|
|
||||||
|
// Apply historyPersistence settings if present in import
|
||||||
|
if (historyPersistence) {
|
||||||
|
extensionSettings.historyPersistence = historyPersistence;
|
||||||
|
}
|
||||||
|
|
||||||
// Create new preset (createPreset uses current trackerConfig)
|
// Create new preset (createPreset uses current trackerConfig)
|
||||||
const newPresetId = createPreset(presetName);
|
const newPresetId = createPreset(presetName);
|
||||||
if (newPresetId) {
|
if (newPresetId) {
|
||||||
@@ -563,6 +657,7 @@ function renderEditorUI() {
|
|||||||
renderUserStatsTab();
|
renderUserStatsTab();
|
||||||
renderInfoBoxTab();
|
renderInfoBoxTab();
|
||||||
renderPresentCharactersTab();
|
renderPresentCharactersTab();
|
||||||
|
renderHistoryPersistenceTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1265,3 +1360,246 @@ function setupPresentCharactersListeners() {
|
|||||||
extensionSettings.trackerConfig.presentCharacters.characterStats.customStats[index].name = $(this).val();
|
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 = '<div class="rpg-editor-section">';
|
||||||
|
|
||||||
|
// Main toggle and settings
|
||||||
|
html += `<h4><i class="fa-solid fa-clock-rotate-left"></i> History Persistence Settings</h4>`;
|
||||||
|
html += `<p class="rpg-editor-hint">Inject selected tracker data into historical messages to help the AI maintain continuity for time-sensitive events, weather changes, and location tracking.</p>`;
|
||||||
|
|
||||||
|
// Enable toggle
|
||||||
|
html += '<div class="rpg-editor-toggle-row">';
|
||||||
|
html += `<input type="checkbox" id="rpg-history-persistence-enabled" ${historyPersistence.enabled ? 'checked' : ''}>`;
|
||||||
|
html += `<label for="rpg-history-persistence-enabled">Enable History Persistence</label>`;
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Message count
|
||||||
|
html += '<div class="rpg-editor-input-row" style="margin-top: 12px;">';
|
||||||
|
html += `<label for="rpg-history-message-count">Number of messages to include (0 = all available):</label>`;
|
||||||
|
html += `<input type="number" id="rpg-history-message-count" min="0" max="50" value="${historyPersistence.messageCount}" class="rpg-input" style="width: 80px; margin-left: 8px;">`;
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Injection position
|
||||||
|
html += '<div class="rpg-editor-input-row" style="margin-top: 12px;">';
|
||||||
|
html += `<label for="rpg-history-injection-position">Injection Position:</label>`;
|
||||||
|
html += `<select id="rpg-history-injection-position" class="rpg-select" style="margin-left: 8px;">`;
|
||||||
|
html += `<option value="user_message_end" ${historyPersistence.injectionPosition === 'user_message_end' ? 'selected' : ''}>End of User Message</option>`;
|
||||||
|
html += `<option value="assistant_message_end" ${historyPersistence.injectionPosition === 'assistant_message_end' ? 'selected' : ''}>End of Assistant Message</option>`;
|
||||||
|
html += `<option value="extra_user_message" ${historyPersistence.injectionPosition === 'extra_user_message' ? 'selected' : ''}>Extra User Message</option>`;
|
||||||
|
html += `<option value="extra_assistant_message" ${historyPersistence.injectionPosition === 'extra_assistant_message' ? 'selected' : ''}>Extra Assistant Message</option>`;
|
||||||
|
html += `</select>`;
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Custom preamble
|
||||||
|
html += '<div class="rpg-editor-input-row" style="margin-top: 12px;">';
|
||||||
|
html += `<label for="rpg-history-context-preamble">Custom Context Preamble (optional):</label>`;
|
||||||
|
html += `<input type="text" id="rpg-history-context-preamble" value="${historyPersistence.contextPreamble || ''}" class="rpg-text-input" placeholder="Leave empty for default: [Context at this point:]" style="width: 100%; margin-top: 4px;">`;
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// User Stats section - which stats to persist
|
||||||
|
html += `<h4 style="margin-top: 20px;"><i class="fa-solid fa-heart-pulse"></i> User Stats</h4>`;
|
||||||
|
html += `<p class="rpg-editor-hint">Select which stats should be included in historical messages.</p>`;
|
||||||
|
|
||||||
|
// Custom stats
|
||||||
|
html += '<div class="rpg-history-persist-list">';
|
||||||
|
userStatsConfig.customStats.forEach((stat, index) => {
|
||||||
|
if (stat.enabled) {
|
||||||
|
html += `
|
||||||
|
<div class="rpg-editor-toggle-row">
|
||||||
|
<input type="checkbox" id="rpg-history-stat-${stat.id}" class="rpg-history-stat-toggle" data-index="${index}" ${stat.persistInHistory ? 'checked' : ''}>
|
||||||
|
<label for="rpg-history-stat-${stat.id}">${stat.name}</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status section
|
||||||
|
if (userStatsConfig.statusSection?.enabled) {
|
||||||
|
html += `
|
||||||
|
<div class="rpg-editor-toggle-row">
|
||||||
|
<input type="checkbox" id="rpg-history-status" ${userStatsConfig.statusSection.persistInHistory ? 'checked' : ''}>
|
||||||
|
<label for="rpg-history-status">Status (Mood/Conditions)</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skills section
|
||||||
|
if (userStatsConfig.skillsSection?.enabled) {
|
||||||
|
html += `
|
||||||
|
<div class="rpg-editor-toggle-row">
|
||||||
|
<input type="checkbox" id="rpg-history-skills" ${userStatsConfig.skillsSection.persistInHistory ? 'checked' : ''}>
|
||||||
|
<label for="rpg-history-skills">${userStatsConfig.skillsSection.label || 'Skills'}</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inventory
|
||||||
|
html += `
|
||||||
|
<div class="rpg-editor-toggle-row">
|
||||||
|
<input type="checkbox" id="rpg-history-inventory" ${userStatsConfig.inventoryPersistInHistory ? 'checked' : ''}>
|
||||||
|
<label for="rpg-history-inventory">Inventory</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Quests
|
||||||
|
html += `
|
||||||
|
<div class="rpg-editor-toggle-row">
|
||||||
|
<input type="checkbox" id="rpg-history-quests" ${userStatsConfig.questsPersistInHistory ? 'checked' : ''}>
|
||||||
|
<label for="rpg-history-quests">Quests</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Info Box section - which widgets to persist
|
||||||
|
html += `<h4 style="margin-top: 20px;"><i class="fa-solid fa-info-circle"></i> Info Box</h4>`;
|
||||||
|
html += `<p class="rpg-editor-hint">Select which info box fields should be included in historical messages. These are recommended for time tracking.</p>`;
|
||||||
|
|
||||||
|
html += '<div class="rpg-history-persist-list">';
|
||||||
|
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 += `
|
||||||
|
<div class="rpg-editor-toggle-row">
|
||||||
|
<input type="checkbox" id="rpg-history-widget-${widgetId}" class="rpg-history-widget-toggle" data-widget="${widgetId}" ${widget.persistInHistory ? 'checked' : ''}>
|
||||||
|
<label for="rpg-history-widget-${widgetId}">${widgetLabels[widgetId] || widgetId}</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Present Characters section
|
||||||
|
html += `<h4 style="margin-top: 20px;"><i class="fa-solid fa-users"></i> Present Characters</h4>`;
|
||||||
|
html += `<p class="rpg-editor-hint">Select which character fields should be included in historical messages.</p>`;
|
||||||
|
|
||||||
|
html += '<div class="rpg-history-persist-list">';
|
||||||
|
|
||||||
|
// Custom fields (appearance, demeanor, etc.)
|
||||||
|
presentCharsConfig.customFields.forEach((field, index) => {
|
||||||
|
if (field.enabled) {
|
||||||
|
html += `
|
||||||
|
<div class="rpg-editor-toggle-row">
|
||||||
|
<input type="checkbox" id="rpg-history-charfield-${field.id}" class="rpg-history-charfield-toggle" data-index="${index}" ${field.persistInHistory ? 'checked' : ''}>
|
||||||
|
<label for="rpg-history-charfield-${field.id}">${field.name}</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Thoughts
|
||||||
|
if (presentCharsConfig.thoughts?.enabled) {
|
||||||
|
html += `
|
||||||
|
<div class="rpg-editor-toggle-row">
|
||||||
|
<input type="checkbox" id="rpg-history-thoughts" ${presentCharsConfig.thoughts.persistInHistory ? 'checked' : ''}>
|
||||||
|
<label for="rpg-history-thoughts">${presentCharsConfig.thoughts.name || 'Thoughts'}</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
$('#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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -709,6 +709,10 @@
|
|||||||
<i class="fa-solid fa-users"></i> <span
|
<i class="fa-solid fa-users"></i> <span
|
||||||
data-i18n-key="template.trackerEditorModal.tabs.presentCharacters">Present Characters</span>
|
data-i18n-key="template.trackerEditorModal.tabs.presentCharacters">Present Characters</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="rpg-editor-tab" data-tab="historyPersistence">
|
||||||
|
<i class="fa-solid fa-clock-rotate-left"></i> <span
|
||||||
|
data-i18n-key="template.trackerEditorModal.tabs.historyPersistence">History Persistence</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rpg-settings-popup-body">
|
<div class="rpg-settings-popup-body">
|
||||||
@@ -716,6 +720,7 @@
|
|||||||
<div id="rpg-editor-tab-userStats" class="rpg-editor-tab-content"></div>
|
<div id="rpg-editor-tab-userStats" class="rpg-editor-tab-content"></div>
|
||||||
<div id="rpg-editor-tab-infoBox" class="rpg-editor-tab-content" style="display: none;"></div>
|
<div id="rpg-editor-tab-infoBox" class="rpg-editor-tab-content" style="display: none;"></div>
|
||||||
<div id="rpg-editor-tab-presentCharacters" class="rpg-editor-tab-content" style="display: none;"></div>
|
<div id="rpg-editor-tab-presentCharacters" class="rpg-editor-tab-content" style="display: none;"></div>
|
||||||
|
<div id="rpg-editor-tab-historyPersistence" class="rpg-editor-tab-content" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="rpg-settings-popup-footer">
|
<footer class="rpg-settings-popup-footer">
|
||||||
|
|||||||
Reference in New Issue
Block a user