Add chapter checkpoint feature
- New feature: bookmark messages to exclude earlier history from context - Saves tokens by marking chapter start points in long chats - Uses SillyTavern's /hide and /unhide slash commands - Persists checkpoint across page reloads and generation events - UI: bookmark icon in message menus with visual indicators - Debounced restore function prevents concurrent executions - Pre-generation checkpoint application ensures messages stay hidden - Clean production-ready code with proper error handling
This commit is contained in:
@@ -90,6 +90,12 @@ import {
|
|||||||
import {
|
import {
|
||||||
initTrackerEditor
|
initTrackerEditor
|
||||||
} from './src/systems/ui/trackerEditor.js';
|
} from './src/systems/ui/trackerEditor.js';
|
||||||
|
import {
|
||||||
|
initChapterCheckpointUI,
|
||||||
|
injectCheckpointButton,
|
||||||
|
updateAllCheckpointIndicators
|
||||||
|
} from './src/systems/ui/checkpointUI.js';
|
||||||
|
import { restoreCheckpointOnLoad } from './src/systems/features/chapterCheckpoint.js';
|
||||||
import {
|
import {
|
||||||
togglePlotButtons,
|
togglePlotButtons,
|
||||||
updateCollapseToggleIcon,
|
updateCollapseToggleIcon,
|
||||||
@@ -129,7 +135,8 @@ import {
|
|||||||
onCharacterChanged,
|
onCharacterChanged,
|
||||||
onMessageSwiped,
|
onMessageSwiped,
|
||||||
updatePersonaAvatar,
|
updatePersonaAvatar,
|
||||||
clearExtensionPrompts
|
clearExtensionPrompts,
|
||||||
|
onGenerationEnded
|
||||||
} from './src/systems/integration/sillytavern.js';
|
} from './src/systems/integration/sillytavern.js';
|
||||||
|
|
||||||
// Old state variable declarations removed - now imported from core modules
|
// Old state variable declarations removed - now imported from core modules
|
||||||
@@ -366,6 +373,11 @@ async function initUI() {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#rpg-save-tracker-history').on('change', function() {
|
||||||
|
extensionSettings.saveTrackerHistory = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
$('#rpg-toggle-plot-buttons').on('change', function() {
|
$('#rpg-toggle-plot-buttons').on('change', function() {
|
||||||
extensionSettings.enablePlotButtons = $(this).prop('checked');
|
extensionSettings.enablePlotButtons = $(this).prop('checked');
|
||||||
// console.log('[RPG Companion] Toggle enablePlotButtons changed to:', extensionSettings.enablePlotButtons);
|
// console.log('[RPG Companion] Toggle enablePlotButtons changed to:', extensionSettings.enablePlotButtons);
|
||||||
@@ -478,6 +490,7 @@ async function initUI() {
|
|||||||
$('#rpg-custom-highlight').val(extensionSettings.customColors.highlight);
|
$('#rpg-custom-highlight').val(extensionSettings.customColors.highlight);
|
||||||
$('#rpg-generation-mode').val(extensionSettings.generationMode);
|
$('#rpg-generation-mode').val(extensionSettings.generationMode);
|
||||||
$('#rpg-skip-guided-mode').val(extensionSettings.skipInjectionsForGuided);
|
$('#rpg-skip-guided-mode').val(extensionSettings.skipInjectionsForGuided);
|
||||||
|
$('#rpg-save-tracker-history').prop('checked', extensionSettings.saveTrackerHistory);
|
||||||
|
|
||||||
updatePanelVisibility();
|
updatePanelVisibility();
|
||||||
updateSectionVisibility();
|
updateSectionVisibility();
|
||||||
@@ -515,6 +528,10 @@ async function initUI() {
|
|||||||
setupContentEditableScrolling();
|
setupContentEditableScrolling();
|
||||||
initInventoryEventListeners();
|
initInventoryEventListeners();
|
||||||
|
|
||||||
|
// Initialize chapter checkpoint UI
|
||||||
|
initChapterCheckpointUI();
|
||||||
|
injectCheckpointButton();
|
||||||
|
|
||||||
// Setup Memory Recollection button in World Info
|
// Setup Memory Recollection button in World Info
|
||||||
setupMemoryRecollectionButton();
|
setupMemoryRecollectionButton();
|
||||||
|
|
||||||
@@ -683,7 +700,9 @@ jQuery(async () => {
|
|||||||
[event_types.MESSAGE_SENT]: onMessageSent,
|
[event_types.MESSAGE_SENT]: onMessageSent,
|
||||||
[event_types.GENERATION_STARTED]: onGenerationStarted,
|
[event_types.GENERATION_STARTED]: onGenerationStarted,
|
||||||
[event_types.MESSAGE_RECEIVED]: onMessageReceived,
|
[event_types.MESSAGE_RECEIVED]: onMessageReceived,
|
||||||
[event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar],
|
[event_types.GENERATION_STOPPED]: onGenerationEnded,
|
||||||
|
[event_types.GENERATION_ENDED]: onGenerationEnded,
|
||||||
|
[event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad],
|
||||||
[event_types.MESSAGE_SWIPED]: onMessageSwiped,
|
[event_types.MESSAGE_SWIPED]: onMessageSwiped,
|
||||||
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
|
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
|
||||||
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar
|
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar
|
||||||
@@ -693,6 +712,9 @@ jQuery(async () => {
|
|||||||
throw error; // This is critical - can't continue without events
|
throw error; // This is critical - can't continue without events
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore checkpoint state if one exists
|
||||||
|
await restoreCheckpointOnLoad();
|
||||||
|
|
||||||
console.log('[RPG Companion] ✅ Extension loaded successfully');
|
console.log('[RPG Companion] ✅ Extension loaded successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[RPG Companion] ❌ Critical initialization failure:', error);
|
console.error('[RPG Companion] ❌ Critical initialization failure:', error);
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const defaultSettings = {
|
|||||||
// This setting helps compatibility with other extensions like GuidedGenerations.
|
// This setting helps compatibility with other extensions like GuidedGenerations.
|
||||||
skipInjectionsForGuided: 'none',
|
skipInjectionsForGuided: 'none',
|
||||||
enablePlotButtons: true, // Show plot progression buttons above chat input
|
enablePlotButtons: true, // Show plot progression buttons above chat input
|
||||||
|
saveTrackerHistory: false, // Save tracker data in chat history for each message
|
||||||
panelPosition: 'right', // 'left', 'right', or 'top'
|
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: {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export let extensionSettings = {
|
|||||||
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
|
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
|
||||||
skipInjectionsForGuided: 'none', // skip injections for instruct injections and quiet prompts (GuidedGenerations compatibility)
|
skipInjectionsForGuided: 'none', // skip injections for instruct injections and quiet prompts (GuidedGenerations compatibility)
|
||||||
enablePlotButtons: true, // Show plot progression buttons above chat input
|
enablePlotButtons: true, // Show plot progression buttons above chat input
|
||||||
|
saveTrackerHistory: false, // Save tracker data in chat history for each message
|
||||||
panelPosition: 'right', // 'left', 'right', or 'top'
|
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: {
|
||||||
|
|||||||
+5
-1
@@ -161,5 +161,9 @@
|
|||||||
"quests.optional.addQuestButton": "Add Quest",
|
"quests.optional.addQuestButton": "Add Quest",
|
||||||
"quests.optional.addQuestPlaceholder": "Enter optional quest title...",
|
"quests.optional.addQuestPlaceholder": "Enter optional quest title...",
|
||||||
"quests.optional.empty": "No active optional quests",
|
"quests.optional.empty": "No active optional quests",
|
||||||
"quests.optional.hint": "Optional quests are side objectives that complement your main story."
|
"quests.optional.hint": "Optional quests are side objectives that complement your main story.",
|
||||||
|
"checkpoint.setChapterStart": "Set Chapter Start",
|
||||||
|
"checkpoint.clearChapterStart": "Clear Chapter Start",
|
||||||
|
"checkpoint.indicator": "Chapter Start",
|
||||||
|
"checkpoint.tooltip": "Messages before this point are excluded from context"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
/**
|
||||||
|
* Chapter Checkpoint Module
|
||||||
|
* Allows users to mark messages as "chapter start" points to filter context
|
||||||
|
* Uses SillyTavern's /hide and /unhide commands to exclude messages from context
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
|
import { chat_metadata, saveChatDebounced } from '../../../../../../../script.js';
|
||||||
|
import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js';
|
||||||
|
|
||||||
|
// Track the message range that is currently hidden
|
||||||
|
let currentlyHiddenRange = null;
|
||||||
|
|
||||||
|
// Debounce restore to prevent loops
|
||||||
|
let isRestoring = false;
|
||||||
|
let restoreTimeout = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current chapter checkpoint message ID for the active chat
|
||||||
|
* @returns {number|null} Message ID of the checkpoint, or null if none set
|
||||||
|
*/
|
||||||
|
export function getChapterCheckpoint() {
|
||||||
|
const context = getContext();
|
||||||
|
if (!context || !chat_metadata) return null;
|
||||||
|
|
||||||
|
return chat_metadata.rpg_companion_chapter_checkpoint || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a message as the chapter checkpoint
|
||||||
|
* Automatically clears any previous checkpoint (only one checkpoint allowed at a time)
|
||||||
|
* Hides all messages before the checkpoint
|
||||||
|
* @param {number} messageId - The chat message index to set as checkpoint
|
||||||
|
* @returns {Promise<boolean>} True if successful
|
||||||
|
*/
|
||||||
|
export async function setChapterCheckpoint(messageId) {
|
||||||
|
const context = getContext();
|
||||||
|
const chat = context.chat;
|
||||||
|
|
||||||
|
if (!chat || messageId < 0 || messageId >= chat.length) {
|
||||||
|
console.error('[RPG Companion] Invalid message ID for checkpoint:', messageId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousCheckpoint = chat_metadata.rpg_companion_chapter_checkpoint;
|
||||||
|
|
||||||
|
// If moving checkpoint, unhide the old range first
|
||||||
|
if (previousCheckpoint !== null && previousCheckpoint !== undefined && previousCheckpoint !== messageId && currentlyHiddenRange !== null) {
|
||||||
|
const { start, end } = currentlyHiddenRange;
|
||||||
|
await executeSlashCommandsOnChatInput(`/unhide ${start}-${end}`, { quiet: true });
|
||||||
|
console.log(`[RPG Companion] Unhid previous range: ${start}-${end}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in chat metadata (this automatically overrides any previous checkpoint)
|
||||||
|
chat_metadata.rpg_companion_chapter_checkpoint = messageId;
|
||||||
|
saveChatDebounced();
|
||||||
|
|
||||||
|
// Hide all messages before the checkpoint
|
||||||
|
if (messageId > 0) {
|
||||||
|
const rangeEnd = messageId - 1;
|
||||||
|
await executeSlashCommandsOnChatInput(`/hide 0-${rangeEnd}`, { quiet: true });
|
||||||
|
currentlyHiddenRange = { start: 0, end: rangeEnd };
|
||||||
|
console.log(`[RPG Companion] Hidden messages 0-${rangeEnd} (checkpoint at ${messageId})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousCheckpoint !== null && previousCheckpoint !== undefined && previousCheckpoint !== messageId) {
|
||||||
|
console.log(`[RPG Companion] Chapter checkpoint moved from message ${previousCheckpoint} to ${messageId}`);
|
||||||
|
} else {
|
||||||
|
console.log('[RPG Companion] Chapter checkpoint set at message', messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit event for UI updates
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
const event = new CustomEvent('rpg-companion-checkpoint-changed', {
|
||||||
|
detail: { messageId, previousCheckpoint }
|
||||||
|
});
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the chapter checkpoint and unhides all hidden messages
|
||||||
|
*/
|
||||||
|
export async function clearChapterCheckpoint() {
|
||||||
|
if (!chat_metadata) return;
|
||||||
|
|
||||||
|
// Unhide any hidden messages
|
||||||
|
if (currentlyHiddenRange !== null) {
|
||||||
|
const { start, end } = currentlyHiddenRange;
|
||||||
|
await executeSlashCommandsOnChatInput(`/unhide ${start}-${end}`, { quiet: true });
|
||||||
|
console.log(`[RPG Companion] Unhid messages ${start}-${end}`);
|
||||||
|
currentlyHiddenRange = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete chat_metadata.rpg_companion_chapter_checkpoint;
|
||||||
|
saveChatDebounced();
|
||||||
|
|
||||||
|
console.log('[RPG Companion] Chapter checkpoint cleared');
|
||||||
|
|
||||||
|
// Emit event for UI updates
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
const event = new CustomEvent('rpg-companion-checkpoint-changed', {
|
||||||
|
detail: { messageId: null }
|
||||||
|
});
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a message is the current checkpoint
|
||||||
|
* @param {number} messageId - The message index to check
|
||||||
|
* @returns {boolean} True if this is the checkpoint message
|
||||||
|
*/
|
||||||
|
export function isCheckpointMessage(messageId) {
|
||||||
|
const checkpointId = getChapterCheckpoint();
|
||||||
|
return checkpointId === messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores checkpoint state after page reload or generation events
|
||||||
|
* Checks if a checkpoint exists and re-applies the /hide command
|
||||||
|
* Debounced to prevent loops when called from multiple events
|
||||||
|
*/
|
||||||
|
export async function restoreCheckpointOnLoad() {
|
||||||
|
// Prevent concurrent executions
|
||||||
|
if (isRestoring) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any pending timeout
|
||||||
|
if (restoreTimeout) {
|
||||||
|
clearTimeout(restoreTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce: wait 100ms before actually restoring
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
restoreTimeout = setTimeout(async () => {
|
||||||
|
isRestoring = true;
|
||||||
|
try {
|
||||||
|
const checkpointId = getChapterCheckpoint();
|
||||||
|
|
||||||
|
if (checkpointId !== null && checkpointId !== undefined && checkpointId > 0) {
|
||||||
|
const context = getContext();
|
||||||
|
const chat = context.chat;
|
||||||
|
|
||||||
|
if (chat && checkpointId < chat.length) {
|
||||||
|
const rangeEnd = checkpointId - 1;
|
||||||
|
|
||||||
|
// Check if messages are already hidden
|
||||||
|
let needsRestore = false;
|
||||||
|
let hiddenCount = 0;
|
||||||
|
let visibleCount = 0;
|
||||||
|
for (let i = 0; i <= rangeEnd; i++) {
|
||||||
|
if (chat[i]) {
|
||||||
|
if (chat[i].is_system) {
|
||||||
|
hiddenCount++;
|
||||||
|
} else {
|
||||||
|
visibleCount++;
|
||||||
|
needsRestore = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsRestore) {
|
||||||
|
await executeSlashCommandsOnChatInput(`/hide 0-${rangeEnd}`, { quiet: true });
|
||||||
|
currentlyHiddenRange = { start: 0, end: rangeEnd };
|
||||||
|
console.log(`[RPG Companion] Restored checkpoint: Hidden messages 0-${rangeEnd}`);
|
||||||
|
} else {
|
||||||
|
currentlyHiddenRange = { start: 0, end: rangeEnd };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isRestoring = false;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -144,6 +144,35 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
// Store RPG data for the last assistant message (separate mode)
|
// Store RPG data for the last assistant message (separate mode)
|
||||||
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
|
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
|
||||||
// console.log('[RPG Companion] Last message is_user:', lastMessage ? lastMessage.is_user : 'no message');
|
// console.log('[RPG Companion] Last message is_user:', lastMessage ? lastMessage.is_user : 'no message');
|
||||||
|
|
||||||
|
// Update lastGeneratedData for display (regardless of message type)
|
||||||
|
if (parsedData.userStats) {
|
||||||
|
lastGeneratedData.userStats = parsedData.userStats;
|
||||||
|
parseUserStats(parsedData.userStats);
|
||||||
|
}
|
||||||
|
if (parsedData.infoBox) {
|
||||||
|
lastGeneratedData.infoBox = parsedData.infoBox;
|
||||||
|
}
|
||||||
|
if (parsedData.characterThoughts) {
|
||||||
|
lastGeneratedData.characterThoughts = parsedData.characterThoughts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When saveTrackerHistory is enabled, store tracker data on the user's message too
|
||||||
|
// This allows scrolling through history and seeing trackers at each point
|
||||||
|
if (extensionSettings.saveTrackerHistory && lastMessage && lastMessage.is_user) {
|
||||||
|
if (!lastMessage.extra) {
|
||||||
|
lastMessage.extra = {};
|
||||||
|
}
|
||||||
|
lastMessage.extra.rpg_companion_data = {
|
||||||
|
userStats: parsedData.userStats,
|
||||||
|
infoBox: parsedData.infoBox,
|
||||||
|
characterThoughts: parsedData.characterThoughts,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
// console.log('[RPG Companion] 💾 Stored tracker data on user message for history');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also store on assistant message if present (existing behavior)
|
||||||
if (lastMessage && !lastMessage.is_user) {
|
if (lastMessage && !lastMessage.is_user) {
|
||||||
if (!lastMessage.extra) {
|
if (!lastMessage.extra) {
|
||||||
lastMessage.extra = {};
|
lastMessage.extra = {};
|
||||||
@@ -160,25 +189,9 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
};
|
};
|
||||||
|
|
||||||
// console.log('[RPG Companion] Stored separate mode RPG data for message swipe', currentSwipeId);
|
// console.log('[RPG Companion] Stored separate mode RPG data for message swipe', currentSwipeId);
|
||||||
|
}
|
||||||
|
|
||||||
// Update lastGeneratedData for display AND future commit
|
// Only commit on TRULY first generation (no committed data exists at all)
|
||||||
if (parsedData.userStats) {
|
|
||||||
lastGeneratedData.userStats = parsedData.userStats;
|
|
||||||
parseUserStats(parsedData.userStats);
|
|
||||||
}
|
|
||||||
if (parsedData.infoBox) {
|
|
||||||
lastGeneratedData.infoBox = parsedData.infoBox;
|
|
||||||
}
|
|
||||||
if (parsedData.characterThoughts) {
|
|
||||||
lastGeneratedData.characterThoughts = parsedData.characterThoughts;
|
|
||||||
}
|
|
||||||
// console.log('[RPG Companion] 💾 SEPARATE MODE: Updated lastGeneratedData:', {
|
|
||||||
// userStats: lastGeneratedData.userStats ? 'exists' : 'null',
|
|
||||||
// infoBox: lastGeneratedData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: lastGeneratedData.characterThoughts ? 'exists' : 'null'
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Only auto-commit on TRULY first generation (no committed data exists at all)
|
|
||||||
// This prevents auto-commit after refresh when we have saved committed data
|
// This prevents auto-commit after refresh when we have saved committed data
|
||||||
const hasAnyCommittedContent = (
|
const hasAnyCommittedContent = (
|
||||||
(committedTrackerData.userStats && committedTrackerData.userStats.trim() !== '') ||
|
(committedTrackerData.userStats && committedTrackerData.userStats.trim() !== '') ||
|
||||||
@@ -194,23 +207,12 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
// console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data');
|
// console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the updated data
|
// Render the updated data (outside the message check, always render)
|
||||||
renderUserStats();
|
renderUserStats();
|
||||||
renderInfoBox();
|
renderInfoBox();
|
||||||
renderThoughts();
|
renderThoughts();
|
||||||
renderInventory();
|
renderInventory();
|
||||||
renderQuests();
|
renderQuests();
|
||||||
} else {
|
|
||||||
// No assistant message to attach to - just update display
|
|
||||||
if (parsedData.userStats) {
|
|
||||||
parseUserStats(parsedData.userStats);
|
|
||||||
}
|
|
||||||
renderUserStats();
|
|
||||||
renderInfoBox();
|
|
||||||
renderThoughts();
|
|
||||||
renderInventory();
|
|
||||||
renderQuests();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to chat metadata
|
// Save to chat metadata
|
||||||
saveChatData();
|
saveChatData();
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
generateContextualSummary,
|
generateContextualSummary,
|
||||||
DEFAULT_HTML_PROMPT
|
DEFAULT_HTML_PROMPT
|
||||||
} from './promptBuilder.js';
|
} from './promptBuilder.js';
|
||||||
|
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event handler for generation start.
|
* Event handler for generation start.
|
||||||
@@ -29,7 +30,7 @@ import {
|
|||||||
* @param {string} type - Event type
|
* @param {string} type - Event type
|
||||||
* @param {Object} data - Event data
|
* @param {Object} data - Event data
|
||||||
*/
|
*/
|
||||||
export function onGenerationStarted(type, data) {
|
export async function onGenerationStarted(type, data) {
|
||||||
// console.log('[RPG Companion] onGenerationStarted called');
|
// console.log('[RPG Companion] onGenerationStarted called');
|
||||||
// console.log('[RPG Companion] enabled:', extensionSettings.enabled);
|
// console.log('[RPG Companion] enabled:', extensionSettings.enabled);
|
||||||
// console.log('[RPG Companion] generationMode:', extensionSettings.generationMode);
|
// console.log('[RPG Companion] generationMode:', extensionSettings.generationMode);
|
||||||
@@ -68,6 +69,10 @@ export function onGenerationStarted(type, data) {
|
|||||||
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
|
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure checkpoint is applied before generation
|
||||||
|
await restoreCheckpointOnLoad();
|
||||||
|
|
||||||
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
|
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
|
||||||
|
|
||||||
// For SEPARATE mode only: Check if we need to commit extension data
|
// For SEPARATE mode only: Check if we need to commit extension data
|
||||||
|
|||||||
@@ -159,6 +159,38 @@ export function parseResponse(responseText) {
|
|||||||
cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
|
cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
|
||||||
debugLog('[RPG Parser] Removed thinking tags, new length:', cleanedResponse.length + ' chars');
|
debugLog('[RPG Parser] Removed thinking tags, new length:', cleanedResponse.length + ' chars');
|
||||||
|
|
||||||
|
// Check if response uses XML <trackers> tags (new format)
|
||||||
|
const xmlMatch = cleanedResponse.match(/<trackers>([\s\S]*?)<\/trackers>/i);
|
||||||
|
if (xmlMatch) {
|
||||||
|
debugLog('[RPG Parser] ✓ Found XML <trackers> tags, using XML parser');
|
||||||
|
const trackersContent = xmlMatch[1].trim();
|
||||||
|
|
||||||
|
// Extract sections from XML content (sections are not in code blocks)
|
||||||
|
const statsMatch = trackersContent.match(/(User )?Stats\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*(Info Box|Present Characters)|$)/i);
|
||||||
|
if (statsMatch) {
|
||||||
|
result.userStats = stripBrackets(statsMatch[0].trim());
|
||||||
|
debugLog('[RPG Parser] ✓ Extracted Stats from XML');
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoBoxMatch = trackersContent.match(/Info Box\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*Present Characters|$)/i);
|
||||||
|
if (infoBoxMatch) {
|
||||||
|
result.infoBox = stripBrackets(infoBoxMatch[0].trim());
|
||||||
|
debugLog('[RPG Parser] ✓ Extracted Info Box from XML');
|
||||||
|
}
|
||||||
|
|
||||||
|
const charactersMatch = trackersContent.match(/Present Characters\s*\n\s*---[\s\S]*$/i);
|
||||||
|
if (charactersMatch) {
|
||||||
|
result.characterThoughts = stripBrackets(charactersMatch[0].trim());
|
||||||
|
debugLog('[RPG Parser] ✓ Extracted Present Characters from XML');
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('[RPG Parser] Parsed from XML:', result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to markdown code block parsing (old format)
|
||||||
|
debugLog('[RPG Parser] No XML tags found, using code block parser');
|
||||||
|
|
||||||
// Extract code blocks
|
// Extract code blocks
|
||||||
const codeBlockRegex = /```([^`]+)```/g;
|
const codeBlockRegex = /```([^`]+)```/g;
|
||||||
const matches = [...cleanedResponse.matchAll(codeBlockRegex)];
|
const matches = [...cleanedResponse.matchAll(codeBlockRegex)];
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { getContext } from '../../../../../../extensions.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
import { chat, getCurrentChatDetails, characters, this_chid } from '../../../../../../../script.js';
|
import { chat, getCurrentChatDetails, characters, this_chid } from '../../../../../../../script.js';
|
||||||
import { selected_group, getGroupMembers, getGroupChat } from '../../../../../../group-chats.js';
|
import { selected_group, getGroupMembers, getGroupChat, groups } from '../../../../../../group-chats.js';
|
||||||
import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../core/state.js';
|
import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../core/state.js';
|
||||||
|
|
||||||
// Type imports
|
// Type imports
|
||||||
@@ -25,7 +25,8 @@ async function getCharacterCardsInfo() {
|
|||||||
|
|
||||||
// Check if in group chat
|
// Check if in group chat
|
||||||
if (selected_group) {
|
if (selected_group) {
|
||||||
const group = await getGroupChat(selected_group);
|
// Find the current group directly from the groups array
|
||||||
|
const group = groups.find(g => g.id === selected_group);
|
||||||
const groupMembers = getGroupMembers(selected_group);
|
const groupMembers = getGroupMembers(selected_group);
|
||||||
|
|
||||||
if (groupMembers && groupMembers.length > 0) {
|
if (groupMembers && groupMembers.length > 0) {
|
||||||
@@ -33,13 +34,15 @@ async function getCharacterCardsInfo() {
|
|||||||
|
|
||||||
// Filter out disabled (muted) members
|
// Filter out disabled (muted) members
|
||||||
const disabledMembers = group?.disabled_members || [];
|
const disabledMembers = group?.disabled_members || [];
|
||||||
|
console.log('[RPG Companion] 🔍 Group ID:', selected_group, '| Disabled members:', disabledMembers);
|
||||||
let characterIndex = 0;
|
let characterIndex = 0;
|
||||||
|
|
||||||
groupMembers.forEach((member) => {
|
groupMembers.forEach((member) => {
|
||||||
if (!member || !member.name) return;
|
if (!member || !member.name) return;
|
||||||
|
|
||||||
// Skip muted characters
|
// Skip muted characters - check against avatar filename
|
||||||
if (member.avatar && disabledMembers.includes(member.avatar)) {
|
if (member.avatar && disabledMembers.includes(member.avatar)) {
|
||||||
|
console.log(`[RPG Companion] ❌ Skipping muted: ${member.name} (${member.avatar})`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,16 +207,27 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
|||||||
|
|
||||||
// Only add tracker instructions if at least one tracker is enabled
|
// Only add tracker instructions if at least one tracker is enabled
|
||||||
if (hasAnyTrackers) {
|
if (hasAnyTrackers) {
|
||||||
|
// Determine format based on saveTrackerHistory setting
|
||||||
|
const useXmlTags = extensionSettings.saveTrackerHistory;
|
||||||
|
const openTag = useXmlTags ? '<trackers>\n' : '';
|
||||||
|
const closeTag = useXmlTags ? '\n</trackers>' : '';
|
||||||
|
const codeBlockMarker = useXmlTags ? '' : '```';
|
||||||
|
|
||||||
// Universal instruction header
|
// Universal instruction header
|
||||||
|
if (useXmlTags) {
|
||||||
|
instructions += `\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the same format as below, enclosed in <trackers></trackers> XML tags. Replace X with actual numbers (e.g., 69) and replace all [placeholders] with concrete in-world details that ${userName} perceives about the current scene and the present characters. Do NOT keep the brackets or placeholder text in your response. For example: [Location] becomes Forest Clearing, [Mood Emoji] becomes 😊. Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs).
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
instructions += `\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the same format as below, enclosed in separate Markdown code fences. Replace X with actual numbers (e.g., 69) and replace all [placeholders] with concrete in-world details that ${userName} perceives about the current scene and the present characters. Do NOT keep the brackets or placeholder text in your response. For example: [Location] becomes Forest Clearing, [Mood Emoji] becomes 😊. Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs).
|
instructions += `\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the same format as below, enclosed in separate Markdown code fences. Replace X with actual numbers (e.g., 69) and replace all [placeholders] with concrete in-world details that ${userName} perceives about the current scene and the present characters. Do NOT keep the brackets or placeholder text in your response. For example: [Location] becomes Forest Clearing, [Mood Emoji] becomes 😊. Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs).
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// Add format specifications for each enabled tracker
|
// Add format specifications for each enabled tracker
|
||||||
if (extensionSettings.showUserStats) {
|
if (extensionSettings.showUserStats) {
|
||||||
const userStatsConfig = trackerConfig?.userStats;
|
const userStatsConfig = trackerConfig?.userStats;
|
||||||
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||||
|
|
||||||
instructions += '```\n';
|
instructions += codeBlockMarker + '\n';
|
||||||
instructions += `${userName}'s Stats\n`;
|
instructions += `${userName}'s Stats\n`;
|
||||||
instructions += '---\n';
|
instructions += '---\n';
|
||||||
|
|
||||||
@@ -258,14 +272,14 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
|||||||
instructions += 'Main Quests: [Short title of the currently active main quest (for example, "Save the world"), or "None"]\n';
|
instructions += 'Main Quests: [Short title of the currently active main quest (for example, "Save the world"), or "None"]\n';
|
||||||
instructions += 'Optional Quests: [Short titles of the currently active optional quests (for example, "Find Zandik\'s book"), or "None"]\n';
|
instructions += 'Optional Quests: [Short titles of the currently active optional quests (for example, "Find Zandik\'s book"), or "None"]\n';
|
||||||
|
|
||||||
instructions += '```\n\n';
|
instructions += codeBlockMarker + '\n\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extensionSettings.showInfoBox) {
|
if (extensionSettings.showInfoBox) {
|
||||||
const infoBoxConfig = trackerConfig?.infoBox;
|
const infoBoxConfig = trackerConfig?.infoBox;
|
||||||
const widgets = infoBoxConfig?.widgets || {};
|
const widgets = infoBoxConfig?.widgets || {};
|
||||||
|
|
||||||
instructions += '```\n';
|
instructions += codeBlockMarker + '\n';
|
||||||
instructions += 'Info Box\n';
|
instructions += 'Info Box\n';
|
||||||
instructions += '---\n';
|
instructions += '---\n';
|
||||||
|
|
||||||
@@ -290,7 +304,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
|||||||
instructions += 'Recent Events: [Up to three past events leading to the ongoing scene (short descriptors with no details, for example, "last-night date with Mary")]\n';
|
instructions += 'Recent Events: [Up to three past events leading to the ongoing scene (short descriptors with no details, for example, "last-night date with Mary")]\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
instructions += '```\n\n';
|
instructions += codeBlockMarker + '\n\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extensionSettings.showCharacterThoughts) {
|
if (extensionSettings.showCharacterThoughts) {
|
||||||
@@ -301,7 +315,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
|||||||
const characterStats = presentCharsConfig?.characterStats;
|
const characterStats = presentCharsConfig?.characterStats;
|
||||||
const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||||
|
|
||||||
instructions += '```\n';
|
instructions += codeBlockMarker + '\n';
|
||||||
instructions += 'Present Characters\n';
|
instructions += 'Present Characters\n';
|
||||||
instructions += '---\n';
|
instructions += '---\n';
|
||||||
|
|
||||||
@@ -346,7 +360,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
|||||||
|
|
||||||
instructions += `- … (Repeat the format above for every other present major character)\n`;
|
instructions += `- … (Repeat the format above for every other present major character)\n`;
|
||||||
|
|
||||||
instructions += '```\n\n';
|
instructions += codeBlockMarker + '\n\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add continuation instruction if includeContinuation is true
|
// Only add continuation instruction if includeContinuation is true
|
||||||
@@ -560,8 +574,10 @@ export async function generateSeparateUpdatePrompt() {
|
|||||||
content: systemMessage
|
content: systemMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// /hide command automatically handles checkpoint filtering
|
||||||
// Add chat history as separate user/assistant messages
|
// Add chat history as separate user/assistant messages
|
||||||
const recentMessages = chat.slice(-depth);
|
const recentMessages = chat.slice(-depth);
|
||||||
|
|
||||||
for (const message of recentMessages) {
|
for (const message of recentMessages) {
|
||||||
messages.push({
|
messages.push({
|
||||||
role: message.is_user ? 'user' : 'assistant',
|
role: message.is_user ? 'user' : 'assistant',
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ import { renderQuests } from '../rendering/quests.js';
|
|||||||
// Utils
|
// Utils
|
||||||
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
||||||
|
|
||||||
|
// Chapter checkpoint
|
||||||
|
import { updateAllCheckpointIndicators } from '../ui/checkpointUI.js';
|
||||||
|
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Commits the tracker data from the last assistant message to be used as source for next generation.
|
* Commits the tracker data from the last assistant message to be used as source for next generation.
|
||||||
* This should be called when the user has replied to a message, ensuring all swipes of the next
|
* This should be called when the user has replied to a message, ensuring all swipes of the next
|
||||||
@@ -158,6 +162,10 @@ export async function onMessageReceived(data) {
|
|||||||
|
|
||||||
// Remove the tracker code blocks from the visible message
|
// Remove the tracker code blocks from the visible message
|
||||||
let cleanedMessage = responseText;
|
let cleanedMessage = responseText;
|
||||||
|
|
||||||
|
// Only remove trackers if saveTrackerHistory is disabled
|
||||||
|
// When enabled, trackers are in <trackers> XML tags which SillyTavern auto-hides
|
||||||
|
if (!extensionSettings.saveTrackerHistory) {
|
||||||
// Remove all code blocks that contain tracker data
|
// Remove all code blocks that contain tracker data
|
||||||
cleanedMessage = cleanedMessage.replace(/```[^`]*?Stats\s*\n\s*---[^`]*?```\s*/gi, '');
|
cleanedMessage = cleanedMessage.replace(/```[^`]*?Stats\s*\n\s*---[^`]*?```\s*/gi, '');
|
||||||
cleanedMessage = cleanedMessage.replace(/```[^`]*?Info Box\s*\n\s*---[^`]*?```\s*/gi, '');
|
cleanedMessage = cleanedMessage.replace(/```[^`]*?Info Box\s*\n\s*---[^`]*?```\s*/gi, '');
|
||||||
@@ -166,6 +174,8 @@ export async function onMessageReceived(data) {
|
|||||||
cleanedMessage = cleanedMessage.replace(/^\s*---\s*$/gm, '');
|
cleanedMessage = cleanedMessage.replace(/^\s*---\s*$/gm, '');
|
||||||
// Clean up multiple consecutive newlines
|
// Clean up multiple consecutive newlines
|
||||||
cleanedMessage = cleanedMessage.replace(/\n{3,}/g, '\n\n');
|
cleanedMessage = cleanedMessage.replace(/\n{3,}/g, '\n\n');
|
||||||
|
}
|
||||||
|
// Note: <trackers> XML tags are automatically hidden by SillyTavern
|
||||||
|
|
||||||
// Update the message in chat history
|
// Update the message in chat history
|
||||||
lastMessage.mes = cleanedMessage.trim();
|
lastMessage.mes = cleanedMessage.trim();
|
||||||
@@ -213,6 +223,9 @@ export async function onMessageReceived(data) {
|
|||||||
setIsPlotProgression(false);
|
setIsPlotProgression(false);
|
||||||
// console.log('[RPG Companion] Plot progression generation completed');
|
// console.log('[RPG Companion] Plot progression generation completed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-apply checkpoint in case SillyTavern unhid messages during generation
|
||||||
|
await restoreCheckpointOnLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -243,6 +256,9 @@ export function onCharacterChanged() {
|
|||||||
|
|
||||||
// Update chat thought overlays
|
// Update chat thought overlays
|
||||||
updateChatThoughts();
|
updateChatThoughts();
|
||||||
|
|
||||||
|
// Update checkpoint indicators for the loaded chat
|
||||||
|
updateAllCheckpointIndicators();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -362,3 +378,13 @@ export function clearExtensionPrompts() {
|
|||||||
// Note: rpg-companion-plot is not cleared here since it's passed via quiet_prompt option
|
// Note: rpg-companion-plot is not cleared here since it's passed via quiet_prompt option
|
||||||
// console.log('[RPG Companion] Cleared all extension prompts');
|
// console.log('[RPG Companion] Cleared all extension prompts');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler for when generation stops or ends
|
||||||
|
* Re-applies checkpoint if SillyTavern unhid messages
|
||||||
|
*/
|
||||||
|
export async function onGenerationEnded() {
|
||||||
|
// SillyTavern may auto-unhide messages when generation stops
|
||||||
|
// Re-apply checkpoint if one exists
|
||||||
|
await restoreCheckpointOnLoad();
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
/**
|
||||||
|
* Chapter Checkpoint UI Module
|
||||||
|
* Adds UI elements for chapter checkpoint functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
import {
|
||||||
|
setChapterCheckpoint,
|
||||||
|
clearChapterCheckpoint,
|
||||||
|
isCheckpointMessage
|
||||||
|
} from '../features/chapterCheckpoint.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the chapter checkpoint button to a message's extra menu
|
||||||
|
* @param {number} messageId - The message index
|
||||||
|
* @param {HTMLElement} menu - The message menu element
|
||||||
|
*/
|
||||||
|
export function addCheckpointButtonToMessage(messageId, menu) {
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
const isCheckpoint = isCheckpointMessage(messageId);
|
||||||
|
|
||||||
|
// Create the menu item
|
||||||
|
const menuItem = document.createElement('div');
|
||||||
|
menuItem.className = 'extraMesButtonsHint list-group-item flex-container flexGap5';
|
||||||
|
const translationKey = isCheckpoint ? 'checkpoint.clearChapterStart' : 'checkpoint.setChapterStart';
|
||||||
|
menuItem.setAttribute('data-i18n', translationKey);
|
||||||
|
menuItem.title = isCheckpoint
|
||||||
|
? 'Clear Chapter Start'
|
||||||
|
: 'Set Chapter Start: When bookmarked, this message will count as the first message in the chat history, skipping earlier ones';
|
||||||
|
|
||||||
|
// Icon only (no text label)
|
||||||
|
const icon = document.createElement('i');
|
||||||
|
icon.className = isCheckpoint ? 'fa-solid fa-bookmark' : 'fa-regular fa-bookmark';
|
||||||
|
icon.style.color = isCheckpoint ? '#4a9eff' : '';
|
||||||
|
|
||||||
|
menuItem.appendChild(icon);
|
||||||
|
|
||||||
|
// Click handler
|
||||||
|
menuItem.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const wasCheckpoint = isCheckpointMessage(messageId);
|
||||||
|
|
||||||
|
if (wasCheckpoint) {
|
||||||
|
clearChapterCheckpoint();
|
||||||
|
} else {
|
||||||
|
setChapterCheckpoint(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update this button immediately
|
||||||
|
const newIsCheckpoint = isCheckpointMessage(messageId);
|
||||||
|
icon.className = newIsCheckpoint ? 'fa-solid fa-bookmark' : 'fa-regular fa-bookmark';
|
||||||
|
icon.style.color = newIsCheckpoint ? '#4a9eff' : '';
|
||||||
|
menuItem.title = newIsCheckpoint
|
||||||
|
? 'Clear Chapter Start'
|
||||||
|
: 'Set Chapter Start: When bookmarked, this message will count as the first message in the chat history, skipping earlier ones';
|
||||||
|
const newTranslationKey = newIsCheckpoint ? 'checkpoint.clearChapterStart' : 'checkpoint.setChapterStart';
|
||||||
|
menuItem.setAttribute('data-i18n', newTranslationKey);
|
||||||
|
|
||||||
|
// Update indicators in all messages
|
||||||
|
updateAllCheckpointIndicators();
|
||||||
|
});
|
||||||
|
|
||||||
|
return menuItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds visual indicators to messages that are checkpoints
|
||||||
|
* @param {number} messageId - The message index
|
||||||
|
* @param {HTMLElement} messageBlock - The message DOM element
|
||||||
|
*/
|
||||||
|
export function addCheckpointIndicator(messageId, messageBlock) {
|
||||||
|
if (!messageBlock) return;
|
||||||
|
|
||||||
|
const isCheckpoint = isCheckpointMessage(messageId);
|
||||||
|
|
||||||
|
// Remove existing indicator if present
|
||||||
|
const existingIndicator = messageBlock.querySelector('.rpg-checkpoint-indicator');
|
||||||
|
if (existingIndicator) {
|
||||||
|
existingIndicator.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCheckpoint) return;
|
||||||
|
|
||||||
|
// Add checkpoint indicator
|
||||||
|
const indicator = document.createElement('div');
|
||||||
|
indicator.className = 'rpg-checkpoint-indicator';
|
||||||
|
const indicatorText = i18n.getTranslation('checkpoint.indicator') || 'Chapter Start';
|
||||||
|
const tooltipText = i18n.getTranslation('checkpoint.tooltip') || 'Messages before this point are excluded from context';
|
||||||
|
indicator.innerHTML = `
|
||||||
|
<i class="fa-solid fa-bookmark"></i>
|
||||||
|
<span>${indicatorText}</span>
|
||||||
|
`;
|
||||||
|
indicator.title = tooltipText;
|
||||||
|
|
||||||
|
// Insert at the beginning of the message
|
||||||
|
const mesText = messageBlock.querySelector('.mes_text');
|
||||||
|
if (mesText && mesText.parentNode) {
|
||||||
|
mesText.parentNode.insertBefore(indicator, mesText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates checkpoint indicators for all messages
|
||||||
|
*/
|
||||||
|
export function updateAllCheckpointIndicators() {
|
||||||
|
const context = getContext();
|
||||||
|
const chat = context.chat;
|
||||||
|
|
||||||
|
if (!chat) return;
|
||||||
|
|
||||||
|
// Clear all processed flags so buttons can be updated
|
||||||
|
document.querySelectorAll('.extraMesButtons[data-checkpoint-processed]').forEach(menu => {
|
||||||
|
delete menu.dataset.checkpointProcessed;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update all message blocks
|
||||||
|
const messageBlocks = document.querySelectorAll('.mes');
|
||||||
|
messageBlocks.forEach((block) => {
|
||||||
|
// Get the actual message ID from the mesid attribute
|
||||||
|
const messageId = Number(block.getAttribute('mesid'));
|
||||||
|
|
||||||
|
if (isNaN(messageId)) return;
|
||||||
|
|
||||||
|
addCheckpointIndicator(messageId, block);
|
||||||
|
|
||||||
|
// Also update any open menus for this message
|
||||||
|
const menu = block.querySelector('.extraMesButtons');
|
||||||
|
if (menu) {
|
||||||
|
updateCheckpointButtonInMenu(menu, messageId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the chapter checkpoint UI
|
||||||
|
*/
|
||||||
|
export function initChapterCheckpointUI() {
|
||||||
|
// Listen for checkpoint changes
|
||||||
|
document.addEventListener('rpg-companion-checkpoint-changed', () => {
|
||||||
|
updateAllCheckpointIndicators();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for chat changes to update indicators
|
||||||
|
const context = getContext();
|
||||||
|
if (context && context.eventSource) {
|
||||||
|
// Update checkpoint indicators when messages are rendered
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
let shouldUpdate = false;
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
mutation.addedNodes.forEach((node) => {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE &&
|
||||||
|
node.classList && node.classList.contains('mes')) {
|
||||||
|
shouldUpdate = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldUpdate) {
|
||||||
|
// Debounce updates to avoid excessive re-rendering
|
||||||
|
clearTimeout(window.rpgCheckpointUpdateTimeout);
|
||||||
|
window.rpgCheckpointUpdateTimeout = setTimeout(() => {
|
||||||
|
updateAllCheckpointIndicators();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatContainer = document.getElementById('chat');
|
||||||
|
if (chatContainer) {
|
||||||
|
observer.observe(chatContainer, {
|
||||||
|
childList: true,
|
||||||
|
subtree: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update indicators on initialization
|
||||||
|
updateAllCheckpointIndicators();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects checkpoint button into message menus
|
||||||
|
* This should be called when SillyTavern renders message menus
|
||||||
|
*/
|
||||||
|
export function injectCheckpointButton() {
|
||||||
|
// Direct approach: Hook into when extraMesButtons elements appear or are populated
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
// Check for added nodes
|
||||||
|
mutation.addedNodes.forEach((node) => {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
// Check if extraMesButtons container was added
|
||||||
|
if (node.classList && node.classList.contains('extraMesButtons')) {
|
||||||
|
processExtraMesButtons(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check if extraMesButtons exists within added subtree
|
||||||
|
if (node.querySelector) {
|
||||||
|
const extraButtons = node.querySelectorAll('.extraMesButtons');
|
||||||
|
extraButtons.forEach(processExtraMesButtons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if nodes were added TO an extraMesButtons container
|
||||||
|
if (mutation.target && mutation.target.classList &&
|
||||||
|
mutation.target.classList.contains('extraMesButtons')) {
|
||||||
|
processExtraMesButtons(mutation.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Observe the chat container
|
||||||
|
const chatContainer = document.getElementById('chat');
|
||||||
|
if (chatContainer) {
|
||||||
|
observer.observe(chatContainer, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process any existing menus on initialization
|
||||||
|
const existingMenus = chatContainer.querySelectorAll('.extraMesButtons');
|
||||||
|
existingMenus.forEach(processExtraMesButtons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process an extraMesButtons container to add checkpoint button
|
||||||
|
* @param {HTMLElement} menu - The extraMesButtons container
|
||||||
|
*/
|
||||||
|
function processExtraMesButtons(menu) {
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
// Find the message block
|
||||||
|
const messageBlock = menu.closest('.mes');
|
||||||
|
if (!messageBlock) return;
|
||||||
|
|
||||||
|
// Get the message ID from the mesid attribute (SillyTavern's standard way)
|
||||||
|
const messageId = Number(messageBlock.getAttribute('mesid'));
|
||||||
|
|
||||||
|
if (isNaN(messageId)) return;
|
||||||
|
|
||||||
|
// Check if button already exists
|
||||||
|
if (!menu.dataset.checkpointProcessed) {
|
||||||
|
// Mark as processed
|
||||||
|
menu.dataset.checkpointProcessed = 'true';
|
||||||
|
|
||||||
|
// Add checkpoint button
|
||||||
|
const checkpointBtn = addCheckpointButtonToMessage(messageId, menu);
|
||||||
|
if (checkpointBtn) {
|
||||||
|
checkpointBtn.classList.add('rpg-checkpoint-button');
|
||||||
|
menu.appendChild(checkpointBtn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the checkpoint button in an existing menu
|
||||||
|
* @param {HTMLElement} menu - The extraMesButtons container
|
||||||
|
* @param {number} messageId - The message index
|
||||||
|
*/
|
||||||
|
function updateCheckpointButtonInMenu(menu, messageId) {
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
const existingButton = menu.querySelector('.rpg-checkpoint-button');
|
||||||
|
if (!existingButton) return;
|
||||||
|
|
||||||
|
const isCheckpoint = isCheckpointMessage(messageId);
|
||||||
|
|
||||||
|
// Update icon
|
||||||
|
const icon = existingButton.querySelector('i');
|
||||||
|
if (icon) {
|
||||||
|
icon.className = isCheckpoint ? 'fa-solid fa-bookmark' : 'fa-regular fa-bookmark';
|
||||||
|
icon.style.color = isCheckpoint ? '#4a9eff' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update tooltip
|
||||||
|
existingButton.title = isCheckpoint
|
||||||
|
? 'Clear Chapter Start'
|
||||||
|
: 'Set Chapter Start: When bookmarked, this message will count as the first message in the chat history, skipping earlier ones';
|
||||||
|
const translationKey = isCheckpoint ? 'checkpoint.clearChapterStart' : 'checkpoint.setChapterStart';
|
||||||
|
existingButton.setAttribute('data-i18n', translationKey);
|
||||||
|
}
|
||||||
@@ -6637,3 +6637,54 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
font-size: clamp(14px, 3vw, 18px) !important;
|
font-size: clamp(14px, 3vw, 18px) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
CHAPTER CHECKPOINT STYLES
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Checkpoint indicator in messages */
|
||||||
|
.rpg-checkpoint-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: linear-gradient(135deg, rgba(74, 158, 255, 0.15) 0%, rgba(74, 158, 255, 0.05) 100%);
|
||||||
|
border-left: 4px solid #4a9eff;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4a9eff;
|
||||||
|
animation: checkpoint-fade-in 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-checkpoint-indicator i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes checkpoint-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: Add a subtle glow effect for checkpoint messages */
|
||||||
|
.mes:has(.rpg-checkpoint-indicator) {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mes:has(.rpg-checkpoint-indicator)::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -4px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 3px;
|
||||||
|
background: linear-gradient(180deg, #4a9eff 0%, transparent 100%);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|||||||
@@ -267,6 +267,14 @@
|
|||||||
When set, the extension will not inject tracker prompts, examples, or HTML instructions according to the selected mode when a guided generation (via `instruct` or `quiet_prompt`) is detected. Useful when using GuidedGenerations or similar extensions.
|
When set, the extension will not inject tracker prompts, examples, or HTML instructions according to the selected mode when a guided generation (via `instruct` or `quiet_prompt`) is detected. Useful when using GuidedGenerations or similar extensions.
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label" style="margin-top: 16px;">
|
||||||
|
<input type="checkbox" id="rpg-save-tracker-history" />
|
||||||
|
<span data-i18n-key="template.settingsModal.advanced.saveTrackerHistory">Save Tracker History in Chat</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;" data-i18n-key="template.settingsModal.advanced.saveTrackerHistoryNote">
|
||||||
|
When enabled, tracker data is saved in chat history for each message. In Together mode, trackers appear in <trackers> XML tags (hidden from display). In Separate mode, tracker data is stored in message metadata. When disabled, only the most recent trackers are kept.
|
||||||
|
</small>
|
||||||
|
|
||||||
<!-- Custom HTML Prompt Editor -->
|
<!-- Custom HTML Prompt Editor -->
|
||||||
<div class="rpg-setting-row" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--rpg-border);">
|
<div class="rpg-setting-row" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--rpg-border);">
|
||||||
<label for="rpg-custom-html-prompt" style="display: block; margin-bottom: 8px; font-weight: 600;" data-i18n-key="template.settingsModal.advanced.customHtmlPromptTitle">
|
<label for="rpg-custom-html-prompt" style="display: block; margin-bottom: 8px; font-weight: 600;" data-i18n-key="template.settingsModal.advanced.customHtmlPromptTitle">
|
||||||
|
|||||||
Reference in New Issue
Block a user