feat: message interception

This commit is contained in:
Subarashimo
2025-12-05 11:40:50 +01:00
parent 271c69ec49
commit 806a7078a7
12 changed files with 374 additions and 69 deletions
+129 -1
View File
@@ -119,7 +119,7 @@ import { setupClassicStatsButtons } from './src/systems/features/classicStats.js
import { ensureHtmlCleaningRegex, detectConflictingRegexScripts } from './src/systems/features/htmlCleaning.js'; import { ensureHtmlCleaningRegex, detectConflictingRegexScripts } from './src/systems/features/htmlCleaning.js';
import { setupMemoryRecollectionButton, updateMemoryRecollectionButton } from './src/systems/features/memoryRecollection.js'; import { setupMemoryRecollectionButton, updateMemoryRecollectionButton } from './src/systems/features/memoryRecollection.js';
import { initLorebookLimiter } from './src/systems/features/lorebookLimiter.js'; import { initLorebookLimiter } from './src/systems/features/lorebookLimiter.js';
import { DEFAULT_HTML_PROMPT, DEFAULT_JSON_TRACKER_PROMPT } from './src/systems/generation/promptBuilder.js'; import { DEFAULT_HTML_PROMPT, DEFAULT_JSON_TRACKER_PROMPT, DEFAULT_MESSAGE_INTERCEPTION_PROMPT } from './src/systems/generation/promptBuilder.js';
// Integration modules // Integration modules
import { import {
@@ -170,6 +170,95 @@ function updateDynamicLabels() {
// Update mobile tab labels // Update mobile tab labels
updateMobileTabLabels(); updateMobileTabLabels();
// Update inline interception toggle text if present
updateInterceptionToggleState();
}
/**
* Updates the inline interception toggle text and styling near the send form.
*/
function updateInterceptionToggleState() {
const $toggle = $('#rpg-interception-toggle');
if ($toggle.length === 0) {
return;
}
const active = extensionSettings.messageInterceptionActive !== false;
const labelKey = active
? 'template.settingsModal.advanced.interceptionOn'
: 'template.settingsModal.advanced.interceptionOff';
const label = i18n.getTranslation(labelKey) || (active ? 'Interception On' : 'Interception Off');
const prefix = i18n.getTranslation('template.settingsModal.advanced.interceptionModeLabel') || 'Interception:';
const icon = active ? 'fa-bolt' : 'fa-ban';
const background = active ? '#4a90e2' : '#666';
$toggle
.css({
'background-color': background,
color: '#fff'
})
.html(`<i class="fa-solid ${icon}"></i> ${prefix} ${label}`);
}
/**
* Shows/hides the inline interception toggle based on interception setting.
*/
function updateInterceptionToggleVisibility() {
const $toggle = $('#rpg-interception-toggle');
if ($toggle.length === 0) {
return;
}
$toggle.toggle(extensionSettings.enableMessageInterception);
if (extensionSettings.enableMessageInterception) {
updateInterceptionToggleState();
}
}
/**
* Ensures the extension buttons wrapper exists above the send form.
*/
function ensureExtensionButtonsWrapper() {
if ($('#extension-buttons-wrapper').length === 0) {
$('#send_form').prepend('<div id="extension-buttons-wrapper" style="text-align: center; margin: 5px auto;"></div>');
}
}
/**
* Renders the inline interception toggle near plot buttons.
*/
function renderInterceptionToggle() {
ensureExtensionButtonsWrapper();
if ($('#rpg-interception-toggle').length === 0) {
const buttonHtml = `
<button id="rpg-interception-toggle" class="menu_button interactable" style="
background-color: #e94560;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
margin: 0 4px;
display: inline-block;
" tabindex="0" role="button">
<i class="fa-solid fa-bolt"></i> Interception: On
</button>
`;
$('#extension-buttons-wrapper').append(buttonHtml);
$('#rpg-interception-toggle').on('click', () => {
const active = extensionSettings.messageInterceptionActive !== false;
extensionSettings.messageInterceptionActive = !active;
saveSettings();
updateInterceptionToggleState();
});
}
updateInterceptionToggleVisibility();
} }
/** /**
@@ -285,6 +374,14 @@ async function initUI() {
saveSettings(); saveSettings();
}); });
$('#rpg-message-interception-depth').on('change', function() {
const value = parseInt(String($(this).val()));
if (!Number.isNaN(value)) {
extensionSettings.messageInterceptionContextDepth = value;
saveSettings();
}
});
$('#rpg-memory-messages').on('change', function() { $('#rpg-memory-messages').on('change', function() {
const value = $(this).val(); const value = $(this).val();
extensionSettings.memoryMessagesToProcess = parseInt(String(value)); extensionSettings.memoryMessagesToProcess = parseInt(String(value));
@@ -409,6 +506,24 @@ async function initUI() {
toastr.success('HTML prompt restored to default'); toastr.success('HTML prompt restored to default');
}); });
$('#rpg-toggle-message-interception').on('change', function() {
extensionSettings.enableMessageInterception = $(this).prop('checked');
saveSettings();
updateInterceptionToggleVisibility();
});
$('#rpg-custom-message-interception-prompt').on('input', function() {
extensionSettings.customMessageInterceptionPrompt = $(this).val().trim();
saveSettings();
});
$('#rpg-restore-default-message-interception-prompt').on('click', function() {
extensionSettings.customMessageInterceptionPrompt = '';
$('#rpg-custom-message-interception-prompt').val(DEFAULT_MESSAGE_INTERCEPTION_PROMPT);
saveSettings();
toastr.success('Message interception prompt restored to default');
});
// Custom Tracker Prompt handlers // Custom Tracker Prompt handlers
$('#rpg-custom-tracker-prompt').on('input', function() { $('#rpg-custom-tracker-prompt').on('input', function() {
extensionSettings.customTrackerPrompt = $(this).val().trim(); extensionSettings.customTrackerPrompt = $(this).val().trim();
@@ -529,6 +644,8 @@ async function initUI() {
$('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat); $('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat);
$('#rpg-toggle-always-show-bubble').prop('checked', extensionSettings.alwaysShowThoughtBubble); $('#rpg-toggle-always-show-bubble').prop('checked', extensionSettings.alwaysShowThoughtBubble);
$('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt); $('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt);
$('#rpg-toggle-message-interception').prop('checked', extensionSettings.enableMessageInterception);
updateInterceptionToggleVisibility();
// Set default HTML prompt as actual text if no custom prompt exists // Set default HTML prompt as actual text if no custom prompt exists
$('#rpg-custom-html-prompt').val(extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT); $('#rpg-custom-html-prompt').val(extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT);
@@ -536,6 +653,16 @@ async function initUI() {
// Set default tracker prompt as actual text if no custom prompt exists // Set default tracker prompt as actual text if no custom prompt exists
$('#rpg-custom-tracker-prompt').val(extensionSettings.customTrackerPrompt || DEFAULT_JSON_TRACKER_PROMPT); $('#rpg-custom-tracker-prompt').val(extensionSettings.customTrackerPrompt || DEFAULT_JSON_TRACKER_PROMPT);
// Set default message interception prompt as actual text if no custom prompt exists
$('#rpg-custom-message-interception-prompt').val(
extensionSettings.customMessageInterceptionPrompt || DEFAULT_MESSAGE_INTERCEPTION_PROMPT
);
// Message interception depth
$('#rpg-message-interception-depth').val(
extensionSettings.messageInterceptionContextDepth || extensionSettings.updateDepth || 4
);
$('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons); $('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons);
$('#rpg-toggle-animations').prop('checked', extensionSettings.enableAnimations); $('#rpg-toggle-animations').prop('checked', extensionSettings.enableAnimations);
$('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow); $('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow);
@@ -581,6 +708,7 @@ async function initUI() {
initTrackerEditor(); initTrackerEditor();
addDiceQuickReply(); addDiceQuickReply();
setupPlotButtons(sendPlotProgression); setupPlotButtons(sendPlotProgression);
renderInterceptionToggle();
setupMobileKeyboardHandling(); setupMobileKeyboardHandling();
setupContentEditableScrolling(); setupContentEditableScrolling();
initInventoryEventListeners(); initInventoryEventListeners();
+3
View File
@@ -25,6 +25,7 @@ export const defaultSettings = {
enabled: true, enabled: true,
autoUpdate: true, autoUpdate: true,
updateDepth: 4, // How many messages to include in the context updateDepth: 4, // How many messages to include in the context
messageInterceptionContextDepth: 4, // How many recent messages to send when intercepting user messages
generationMode: 'together', // 'separate' or 'together' - whether to generate with main response or separately generationMode: 'together', // 'separate' or 'together' - whether to generate with main response or separately
useSeparatePreset: false, // Use 'RPG Companion Trackers' preset for tracker generation instead of main API model useSeparatePreset: false, // Use 'RPG Companion Trackers' preset for tracker generation instead of main API model
showUserStats: true, showUserStats: true,
@@ -34,6 +35,8 @@ export const defaultSettings = {
showThoughtsInChat: true, // Show thoughts overlay in chat showThoughtsInChat: true, // Show thoughts overlay in chat
alwaysShowThoughtBubble: false, // Auto-expand thought bubble without clicking icon alwaysShowThoughtBubble: false, // Auto-expand thought bubble without clicking icon
enableHtmlPrompt: false, // Enable immersive HTML prompt injection enableHtmlPrompt: false, // Enable immersive HTML prompt injection
enableMessageInterception: false, // Enable intercepting user messages with LLM rewrite
messageInterceptionActive: true, // Runtime toggle to allow/skip interception
// Controls when the extension skips injecting tracker instructions/examples/HTML // Controls when the extension skips injecting tracker instructions/examples/HTML
// into generations that appear to be user-injected instructions. Valid values: // into generations that appear to be user-injected instructions. Valid values:
// - 'none' -> never skip (legacy behavior: always inject) // - 'none' -> never skip (legacy behavior: always inject)
+4
View File
@@ -13,6 +13,7 @@ export let extensionSettings = {
enabled: true, enabled: true,
autoUpdate: true, autoUpdate: true,
updateDepth: 4, // How many messages to include in the context updateDepth: 4, // How many messages to include in the context
messageInterceptionContextDepth: 4, // How many recent messages to send when intercepting user messages
generationMode: 'together', // 'separate' or 'together' - whether to generate with main response or separately generationMode: 'together', // 'separate' or 'together' - whether to generate with main response or separately
useSeparatePreset: false, // Use 'RPG Companion Trackers' preset for tracker generation instead of main API model useSeparatePreset: false, // Use 'RPG Companion Trackers' preset for tracker generation instead of main API model
showUserStats: true, showUserStats: true,
@@ -28,6 +29,9 @@ export let extensionSettings = {
enableHtmlPrompt: false, // Enable immersive HTML prompt injection enableHtmlPrompt: false, // Enable immersive HTML prompt injection
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default) customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
customTrackerPrompt: '', // Custom tracker instruction prompt (empty = use default) customTrackerPrompt: '', // Custom tracker instruction prompt (empty = use default)
enableMessageInterception: false, // Enable intercepting user messages with LLM rewrite
messageInterceptionActive: true, // Runtime toggle to allow/skip interception
customMessageInterceptionPrompt: '', // Custom prompt for message interception (empty = 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
panelPosition: 'right', // 'left', 'right', or 'top' panelPosition: 'right', // 'left', 'right', or 'top'
+10 -1
View File
@@ -65,6 +65,16 @@
"template.settingsModal.advanced.skipInjectionsOptions.impersonation": "Only on impersonation requests", "template.settingsModal.advanced.skipInjectionsOptions.impersonation": "Only on impersonation requests",
"template.settingsModal.advanced.skipInjectionsOptions.guided": "Always for guided or quiet prompts", "template.settingsModal.advanced.skipInjectionsOptions.guided": "Always for guided or quiet prompts",
"template.settingsModal.advanced.skipInjectionsNote": "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.", "template.settingsModal.advanced.skipInjectionsNote": "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.",
"template.settingsModal.advanced.enableMessageInterception": "Intercept & rewrite user messages with AI",
"template.settingsModal.advanced.enableMessageInterceptionNote": "When enabled, user messages are sent to the AI and rewritten in-place.",
"template.settingsModal.advanced.interceptionModeLabel": "Interception:",
"template.settingsModal.advanced.interceptionOn": "On",
"template.settingsModal.advanced.interceptionOff": "Off",
"template.settingsModal.advanced.messageInterceptionDepth": "Interception Context Messages:",
"template.settingsModal.advanced.messageInterceptionDepthNote": "How many recent messages to send with the interception prompt.",
"template.settingsModal.advanced.customMessageInterceptionPromptTitle": "Custom Message Interception Prompt:",
"template.settingsModal.advanced.restoreDefaultMessageInterceptionPrompt": "Restore Default",
"template.settingsModal.advanced.customMessageInterceptionPromptNote": "Customize the instructions sent to the AI when rewriting user messages. Leave empty to use the default guidance. The AI receives this prompt, the current RPG state JSON, and the recent messages you specify above.",
"template.settingsModal.advanced.customTrackerPromptTitle": "Custom Tracker Prompt:", "template.settingsModal.advanced.customTrackerPromptTitle": "Custom Tracker Prompt:",
"template.settingsModal.advanced.restoreDefaultTrackerPrompt": "Restore Default", "template.settingsModal.advanced.restoreDefaultTrackerPrompt": "Restore Default",
"template.settingsModal.advanced.customTrackerPromptNote": "Customize the instructions sent to the AI for generating tracker data. Use {{user}} as a placeholder for the user's name. This is the main prompt that tells the AI how to format and update the RPG trackers.", "template.settingsModal.advanced.customTrackerPromptNote": "Customize the instructions sent to the AI for generating tracker data. Use {{user}} as a placeholder for the user's name. This is the main prompt that tells the AI how to format and update the RPG trackers.",
@@ -88,7 +98,6 @@
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "Always Include Attributes in Prompt", "template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "Always Include Attributes in Prompt",
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "If disabled, attributes are only sent when a dice roll is active.", "template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "If disabled, attributes are only sent when a dice roll is active.",
"template.trackerEditorModal.userStatsTab.allowAIUpdateAttributes": "Allow AI to Update RPG Attributes", "template.trackerEditorModal.userStatsTab.allowAIUpdateAttributes": "Allow AI to Update RPG Attributes",
"template.trackerEditorModal.userStatsTab.allowAIUpdateAttributesNote": "If enabled, the AI can update attribute values and level from its responses. If disabled, attributes are read-only and can only be changed manually.",
"template.trackerEditorModal.userStatsTab.addAttributeButton": "Add Attribute", "template.trackerEditorModal.userStatsTab.addAttributeButton": "Add Attribute",
"template.trackerEditorModal.userStatsTab.statusSectionTitle": "Status Section", "template.trackerEditorModal.userStatsTab.statusSectionTitle": "Status Section",
"template.trackerEditorModal.userStatsTab.enableStatusSection": "Enable Status Section", "template.trackerEditorModal.userStatsTab.enableStatusSection": "Enable Status Section",
+10 -1
View File
@@ -65,6 +65,16 @@
"template.settingsModal.advanced.skipInjectionsOptions.impersonation": "僅在模擬請求時跳過", "template.settingsModal.advanced.skipInjectionsOptions.impersonation": "僅在模擬請求時跳過",
"template.settingsModal.advanced.skipInjectionsOptions.guided": "始終跳過引導", "template.settingsModal.advanced.skipInjectionsOptions.guided": "始終跳過引導",
"template.settingsModal.advanced.skipInjectionsNote": "當設置後,擴充功能在檢測到引導生成(通過 `instruct` 或 `quiet_prompt`)時,將根據所選模式不注入追蹤提示詞、範例或 HTML 指令。當與 GuidedGenerations 或類似擴充功能一起使用時非常有用。", "template.settingsModal.advanced.skipInjectionsNote": "當設置後,擴充功能在檢測到引導生成(通過 `instruct` 或 `quiet_prompt`)時,將根據所選模式不注入追蹤提示詞、範例或 HTML 指令。當與 GuidedGenerations 或類似擴充功能一起使用時非常有用。",
"template.settingsModal.advanced.enableMessageInterception": "以 AI 攔截並改寫使用者訊息",
"template.settingsModal.advanced.enableMessageInterceptionNote": "啟用後,使用者訊息會連同當前 RPG 狀態與近期聊天內容一起送交 AI,然後在原位改寫。",
"template.settingsModal.advanced.interceptionModeLabel": "攔截模式:",
"template.settingsModal.advanced.interceptionOn": "開啟",
"template.settingsModal.advanced.interceptionOff": "關閉",
"template.settingsModal.advanced.messageInterceptionDepth": "攔截時的上下文訊息數:",
"template.settingsModal.advanced.messageInterceptionDepthNote": "攔截時要附帶的近期訊息數量。",
"template.settingsModal.advanced.customMessageInterceptionPromptTitle": "自訂訊息攔截提示:",
"template.settingsModal.advanced.restoreDefaultMessageInterceptionPrompt": "恢復預設",
"template.settingsModal.advanced.customMessageInterceptionPromptNote": "自訂 AI 改寫使用者訊息時的指令。留空則使用預設指引。AI 會收到這段提示詞、目前的 RPG 狀態 JSON,以及上方設定的近期訊息。",
"template.settingsModal.advanced.customTrackerPromptTitle": "自訂追蹤器提示詞:", "template.settingsModal.advanced.customTrackerPromptTitle": "自訂追蹤器提示詞:",
"template.settingsModal.advanced.restoreDefaultTrackerPrompt": "恢復預設", "template.settingsModal.advanced.restoreDefaultTrackerPrompt": "恢復預設",
"template.settingsModal.advanced.customTrackerPromptNote": "自訂發送給 AI 生成追蹤器數據的指令。使用 {{user}} 作為使用者名稱的佔位符。這是告訴 AI 如何格式化和更新 RPG 追蹤器的主要提示詞。", "template.settingsModal.advanced.customTrackerPromptNote": "自訂發送給 AI 生成追蹤器數據的指令。使用 {{user}} 作為使用者名稱的佔位符。這是告訴 AI 如何格式化和更新 RPG 追蹤器的主要提示詞。",
@@ -88,7 +98,6 @@
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "始終發送屬性(prompt", "template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "始終發送屬性(prompt",
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "將 RPG 屬性始終包含在提示詞中,即使它們未顯示在面板上也一樣。", "template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "將 RPG 屬性始終包含在提示詞中,即使它們未顯示在面板上也一樣。",
"template.trackerEditorModal.userStatsTab.allowAIUpdateAttributes": "允許 AI 更新 RPG 屬性", "template.trackerEditorModal.userStatsTab.allowAIUpdateAttributes": "允許 AI 更新 RPG 屬性",
"template.trackerEditorModal.userStatsTab.allowAIUpdateAttributesNote": "如果啟用,AI 可以從其 JSON 回應中更新屬性值和等級。如果禁用,屬性為唯讀,只能手動更改。",
"template.trackerEditorModal.userStatsTab.addAttributeButton": "添加屬性", "template.trackerEditorModal.userStatsTab.addAttributeButton": "添加屬性",
"template.trackerEditorModal.userStatsTab.statusSectionTitle": "狀態欄", "template.trackerEditorModal.userStatsTab.statusSectionTitle": "狀態欄",
"template.trackerEditorModal.userStatsTab.enableStatusSection": "啟用狀態欄", "template.trackerEditorModal.userStatsTab.enableStatusSection": "啟用狀態欄",
+9 -5
View File
@@ -155,6 +155,10 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
// console.log('[RPG Companion] Parsed data:', parsedData); // console.log('[RPG Companion] Parsed data:', parsedData);
// console.log('[RPG Companion] parsedData.userStats:', parsedData.userStats ? parsedData.userStats.substring(0, 100) + '...' : 'null'); // console.log('[RPG Companion] parsedData.userStats:', parsedData.userStats ? parsedData.userStats.substring(0, 100) + '...' : 'null');
// Legacy text parsing does not provide structured characters; clear stale structured data
extensionSettings.charactersData = [];
const parsedCharacterThoughts = parsedData.characterThoughts || '';
// DON'T update lastGeneratedData here - it should only reflect the data // DON'T update lastGeneratedData here - it should only reflect the data
// from the assistant message the user replied to, not auto-generated updates // from the assistant message the user replied to, not auto-generated updates
// This ensures swipes/regenerations use consistent source data // This ensures swipes/regenerations use consistent source data
@@ -174,7 +178,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = { lastMessage.extra.rpg_companion_swipes[currentSwipeId] = {
userStats: parsedData.userStats, userStats: parsedData.userStats,
infoBox: parsedData.infoBox, infoBox: parsedData.infoBox,
characterThoughts: parsedData.characterThoughts characterThoughts: parsedCharacterThoughts
}; };
// 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);
@@ -190,9 +194,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
if (parsedData.infoBox) { if (parsedData.infoBox) {
lastGeneratedData.infoBox = parsedData.infoBox; lastGeneratedData.infoBox = parsedData.infoBox;
} }
if (parsedData.characterThoughts) { lastGeneratedData.characterThoughts = parsedCharacterThoughts;
lastGeneratedData.characterThoughts = parsedData.characterThoughts;
}
// console.log('[RPG Companion] 💾 SEPARATE MODE: Updated lastGeneratedData:', { // console.log('[RPG Companion] 💾 SEPARATE MODE: Updated lastGeneratedData:', {
// userStats: lastGeneratedData.userStats ? 'exists' : 'null', // userStats: lastGeneratedData.userStats ? 'exists' : 'null',
// infoBox: lastGeneratedData.infoBox ? 'exists' : 'null', // infoBox: lastGeneratedData.infoBox ? 'exists' : 'null',
@@ -211,13 +213,14 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
if (!hasAnyCommittedContent) { if (!hasAnyCommittedContent) {
committedTrackerData.userStats = parsedData.userStats; committedTrackerData.userStats = parsedData.userStats;
committedTrackerData.infoBox = parsedData.infoBox; committedTrackerData.infoBox = parsedData.infoBox;
committedTrackerData.characterThoughts = parsedData.characterThoughts; committedTrackerData.characterThoughts = parsedCharacterThoughts;
// 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
renderUserStats(); renderUserStats();
renderInfoBox(); renderInfoBox();
lastGeneratedData.characterThoughts = parsedCharacterThoughts;
renderThoughts(); renderThoughts();
renderInventory(); renderInventory();
renderQuests(); renderQuests();
@@ -228,6 +231,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
} }
renderUserStats(); renderUserStats();
renderInfoBox(); renderInfoBox();
lastGeneratedData.characterThoughts = parsedCharacterThoughts;
renderThoughts(); renderThoughts();
renderInventory(); renderInventory();
renderQuests(); renderQuests();
+44 -39
View File
@@ -4,7 +4,7 @@
* Supports both legacy text format and new JSON format * Supports both legacy text format and new JSON format
*/ */
import { extensionSettings, FEATURE_FLAGS, addDebugLog, lastGeneratedData } from '../../core/state.js'; import { extensionSettings, FEATURE_FLAGS, addDebugLog, lastGeneratedData, committedTrackerData } from '../../core/state.js';
import { saveSettings, saveChatData } from '../../core/persistence.js'; import { saveSettings, saveChatData } from '../../core/persistence.js';
import { extractInventory } from './inventoryParser.js'; import { extractInventory } from './inventoryParser.js';
import { validateTrackerData, mergeTrackerData } from '../../types/trackerData.js'; import { validateTrackerData, mergeTrackerData } from '../../types/trackerData.js';
@@ -317,47 +317,52 @@ export function parseJSONTrackerData(jsonData) {
} }
// Parse characters - store for UI rendering AND generate text format for thought bubbles // Parse characters - store for UI rendering AND generate text format for thought bubbles
if (jsonData.characters && Array.isArray(jsonData.characters)) { const parsedCharacters = Array.isArray(jsonData.characters) ? jsonData.characters : [];
extensionSettings.charactersData = jsonData.characters; extensionSettings.charactersData = parsedCharacters;
debugLog('[RPG Parser] Characters:', jsonData.characters.length); debugLog('[RPG Parser] Characters:', parsedCharacters.length);
// Generate text format for lastGeneratedData.characterThoughts (needed for thought bubbles)
const config = extensionSettings.trackerConfig?.presentCharacters;
const thoughtsFieldName = config?.thoughts?.name || 'Thoughts';
const lines = [];
for (const char of parsedCharacters) {
// Character name line
lines.push(`- ${char.name || 'Unknown'}`);
// Generate text format for lastGeneratedData.characterThoughts (needed for thought bubbles) // Details line with emoji and fields
const config = extensionSettings.trackerConfig?.presentCharacters; const details = [char.emoji || '😶'];
const thoughtsFieldName = config?.thoughts?.name || 'Thoughts'; const charFields = char.fields || {};
const lines = []; for (const [key, value] of Object.entries(charFields)) {
for (const char of jsonData.characters) { if (value) details.push(`${key}: ${value}`);
// Character name line
lines.push(`- ${char.name || 'Unknown'}`);
// Details line with emoji and fields
const details = [char.emoji || '😶'];
const charFields = char.fields || {};
for (const [key, value] of Object.entries(charFields)) {
if (value) details.push(`${key}: ${value}`);
}
lines.push(`Details: ${details.join(' | ')}`);
// Relationship line
if (char.relationship) {
lines.push(`Relationship: ${char.relationship}`);
}
// Stats line
const charStats = char.stats || {};
if (Object.keys(charStats).length > 0) {
const statsStr = Object.entries(charStats).map(([k, v]) => `${k}: ${v}%`).join(' | ');
lines.push(`Stats: ${statsStr}`);
}
// Thoughts line
if (char.thoughts) {
lines.push(`${thoughtsFieldName}: ${char.thoughts}`);
}
} }
if (lines.length > 0) { lines.push(`Details: ${details.join(' | ')}`);
lastGeneratedData.characterThoughts = lines.join('\n');
debugLog('[RPG Parser] Generated text format for characterThoughts'); // Relationship line
if (char.relationship) {
lines.push(`Relationship: ${char.relationship}`);
} }
// Stats line
const charStats = char.stats || {};
if (Object.keys(charStats).length > 0) {
const statsStr = Object.entries(charStats).map(([k, v]) => `${k}: ${v}%`).join(' | ');
lines.push(`Stats: ${statsStr}`);
}
// Thoughts line
if (char.thoughts) {
lines.push(`${thoughtsFieldName}: ${char.thoughts}`);
}
}
if (lines.length > 0) {
lastGeneratedData.characterThoughts = lines.join('\n');
committedTrackerData.characterThoughts = lines.join('\n');
debugLog('[RPG Parser] Generated text format for characterThoughts');
} else {
// No characters provided in the JSON response - clear any stale state/UI data
lastGeneratedData.characterThoughts = '';
committedTrackerData.characterThoughts = '';
debugLog('[RPG Parser] No characters present; cleared characterThoughts state');
} }
// Parse inventory (structured format) // Parse inventory (structured format)
+13 -3
View File
@@ -24,11 +24,17 @@ export const DEFAULT_HTML_PROMPT = `If appropriate, include inline HTML, CSS, an
*/ */
export const DEFAULT_JSON_TRACKER_PROMPT = `At the start of every reply, output a JSON object inside a markdown code fence (with \`\`\`json). This tracks {{user}}'s stats, inventory, skills, and scene information. Follow the exact schema shown below. Use concrete values - no placeholders or brackets. Update stats realistically based on actions and time (0% change for minutes, 1-5% normally, 5%+ only for major events). Items and skills have "name" and "description" fields. Items can grant skills via "grantsSkill", and skills show their source via "grantedBy".`; export const DEFAULT_JSON_TRACKER_PROMPT = `At the start of every reply, output a JSON object inside a markdown code fence (with \`\`\`json). This tracks {{user}}'s stats, inventory, skills, and scene information. Follow the exact schema shown below. Use concrete values - no placeholders or brackets. Update stats realistically based on actions and time (0% change for minutes, 1-5% normally, 5%+ only for major events). Items and skills have "name" and "description" fields. Items can grant skills via "grantsSkill", and skills show their source via "grantedBy".`;
/**
* Default message interception prompt text
* Guides the LLM to rewrite the user's message based on current RPG state and recent chat
*/
export const DEFAULT_MESSAGE_INTERCEPTION_PROMPT = `Act as an uncompromising Immersive Copy Editor who rewrites the user's draft to strictly adhere to {{user}}'s persona and RPG state (JSON). You must validate the feasibility of the user's intended actions against the JSON state; if the draft contradicts the state (e.g., acting smart while 'Intelligence' is low, or running while having a 'Leg Injury'), you are required to override the core intent, rewriting the action to portray immediate failure, struggle, or involuntary reaction instead of the user's desired success. Even further, if the intended course of action is physically impossible via the state or represents a thought process conceptually alien to the character's nature or current state, you are mandated to completely overwrite the user's intent. Aggressively rephrase vocabulary and syntax to match the character's specific cognitive capacity and tone. Keep the output concise and devoid of fluff; do not expand the narrative beyond the necessary state-enforced correction. Return ONLY the modified message text.`;
/** /**
* Gets character card information for current chat (handles both single and group chats) * Gets character card information for current chat (handles both single and group chats)
* @returns {string} Formatted character information * @returns {string} Formatted character information
*/ */
async function getCharacterCardsInfo() { export async function getCharacterCardsInfo() {
let characterInfo = ''; let characterInfo = '';
// Check if in group chat // Check if in group chat
@@ -196,6 +202,7 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
const showSkills = extensionSettings.showSkills; const showSkills = extensionSettings.showSkills;
const showQuests = extensionSettings.showQuests; const showQuests = extensionSettings.showQuests;
const enableItemSkillLinks = extensionSettings.enableItemSkillLinks; const enableItemSkillLinks = extensionSettings.enableItemSkillLinks;
const deleteSkillWithItem = extensionSettings.deleteSkillWithItem;
const hasAnyTrackers = showStats || showInfoBox || showCharacters || showInventory || showSkills || showQuests; const hasAnyTrackers = showStats || showInfoBox || showCharacters || showInventory || showSkills || showQuests;
@@ -400,8 +407,9 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
instructions += '- Level is a numeric value (typically 1+, represents character progression)\n'; instructions += '- Level is a numeric value (typically 1+, represents character progression)\n';
} }
instructions += '- Characters should be removed as soon as they leave the scene\n';
instructions += '- Your list of characters must never include {{user}}\n';
instructions += '- Empty arrays [] for sections with no items\n'; instructions += '- Empty arrays [] for sections with no items\n';
instructions += '- Items may be added or removed from all sections\n';
instructions += '- null for main quest if none active\n'; instructions += '- null for main quest if none active\n';
// Add stat descriptions if any have descriptions // Add stat descriptions if any have descriptions
@@ -438,7 +446,9 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
if (enableItemSkillLinks) { if (enableItemSkillLinks) {
instructions += '- Items can grant skills: add {"grantsSkill": "Skill Name"} to the item object\n'; instructions += '- Items can grant skills: add {"grantsSkill": "Skill Name"} to the item object\n';
instructions += '- When a skill comes from an item, add {"grantedBy": "Item Name"} to that skill object\n'; instructions += '- When a skill comes from an item, add {"grantedBy": "Item Name"} to that skill object\n';
instructions += '- If an item is removed/lost, also remove any skill it granted\n'; if (deleteSkillWithItem) {
instructions += '- If an item is removed/lost, also remove any skill it granted\n';
}
} }
instructions += '\n'; instructions += '\n';
+107 -10
View File
@@ -4,7 +4,14 @@
*/ */
import { getContext } from '../../../../../../extensions.js'; import { getContext } from '../../../../../../extensions.js';
import { chat, user_avatar, setExtensionPrompt, extension_prompt_types, updateMessageBlock } from '../../../../../../../script.js'; import {
chat,
user_avatar,
setExtensionPrompt,
extension_prompt_types,
updateMessageBlock,
generateRaw
} from '../../../../../../../script.js';
// Core modules // Core modules
import { import {
@@ -14,15 +21,14 @@ import {
lastActionWasSwipe, lastActionWasSwipe,
isPlotProgression, isPlotProgression,
setLastActionWasSwipe, setLastActionWasSwipe,
setIsPlotProgression, setIsPlotProgression
updateLastGeneratedData,
updateCommittedTrackerData
} from '../../core/state.js'; } from '../../core/state.js';
import { saveChatData, loadChatData } from '../../core/persistence.js'; import { saveChatData, loadChatData } from '../../core/persistence.js';
// Generation & Parsing // Generation & Parsing
import { parseResponse, parseUserStats, parseSkills, tryParseJSONResponse } from '../generation/parser.js'; import { parseResponse, parseUserStats, parseSkills, tryParseJSONResponse } from '../generation/parser.js';
import { updateRPGData } from '../generation/apiClient.js'; import { updateRPGData } from '../generation/apiClient.js';
import { generateContextualSummary, DEFAULT_MESSAGE_INTERCEPTION_PROMPT } from '../generation/promptBuilder.js';
// Rendering // Rendering
import { renderUserStats } from '../rendering/userStats.js'; import { renderUserStats } from '../rendering/userStats.js';
@@ -76,13 +82,22 @@ export function commitTrackerData() {
* Sets the flag to indicate this is NOT a swipe. * Sets the flag to indicate this is NOT a swipe.
* In separate mode with auto-update disabled, commits the displayed tracker data. * In separate mode with auto-update disabled, commits the displayed tracker data.
*/ */
export function onMessageSent() { export async function onMessageSent() {
if (!extensionSettings.enabled) return; if (!extensionSettings.enabled) return;
// User sent a new message - NOT a swipe // User sent a new message - NOT a swipe
setLastActionWasSwipe(false); setLastActionWasSwipe(false);
// console.log('[RPG Companion] 🟢 EVENT: onMessageSent - lastActionWasSwipe =', lastActionWasSwipe); // console.log('[RPG Companion] 🟢 EVENT: onMessageSent - lastActionWasSwipe =', lastActionWasSwipe);
// Optionally intercept and rewrite the user message via LLM
if (extensionSettings.enableMessageInterception && extensionSettings.messageInterceptionActive !== false) {
try {
await interceptAndModifyUserMessage();
} catch (error) {
console.error('[RPG Companion] Message interception failed:', error);
}
}
// In separate mode with auto-update disabled, commit displayed tracker when user sends a message // In separate mode with auto-update disabled, commit displayed tracker when user sends a message
if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) { if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) {
// Commit whatever is currently displayed in lastGeneratedData // Commit whatever is currently displayed in lastGeneratedData
@@ -99,6 +114,84 @@ export function onMessageSent() {
} }
} }
/**
* Intercepts the last user message, asks the LLM to rewrite it using RPG state and recent chat,
* and updates the chat/DOM with the modified content.
*/
async function interceptAndModifyUserMessage() {
const context = getContext();
const chatHistory = context.chat || chat;
if (!chatHistory || chatHistory.length === 0) {
return;
}
const lastMessage = chatHistory[chatHistory.length - 1];
if (!lastMessage || !lastMessage.is_user) {
return; // Only rewrite user messages
}
const originalText = lastMessage.mes || '';
const stateJson = generateContextualSummary();
const depth = extensionSettings.messageInterceptionContextDepth || extensionSettings.updateDepth || 4;
const startIndex = Math.max(0, chatHistory.length - 1 - depth);
const recentMessages = chatHistory.slice(startIndex, chatHistory.length - 1);
const recentContext = recentMessages
.map((m) => {
const role = m.is_system ? 'system' : m.is_user ? '{{user}}' : '{{char}}';
const content = (m.mes || '').replace(/\s+/g, ' ').trim();
return `- ${role}: ${content}`;
})
.join('\n');
const basePrompt =
(extensionSettings.customMessageInterceptionPrompt || '').trim() ||
DEFAULT_MESSAGE_INTERCEPTION_PROMPT;
const promptMessages = [
{
role: 'system',
content: basePrompt
},
{
role: 'system',
content: `{{user}}'s persona definition:\n{{persona}}`
},
{
role: 'system',
content: `Current RPG state (JSON):\n${stateJson ? `\`\`\`json\n${stateJson}\n\`\`\`` : 'None'}`
},
{
role: 'system',
content: `Recent messages (newest last):\n${recentContext || 'None'}`
},
{
role: 'user',
content: `User draft message:\n${originalText}\n\nReturn only the modified message text.`
}
];
const response = await generateRaw({
prompt: promptMessages,
quietToLoud: false
});
if (!response || typeof response !== 'string') {
return;
}
const cleaned = response.trim();
if (!cleaned) {
return;
}
// Update chat history and DOM
lastMessage.mes = cleaned;
const messageId = chatHistory.length - 1;
updateMessageBlock(messageId, lastMessage, { rerenderMessage: true });
}
/** /**
* Event handler for when a message is generated. * Event handler for when a message is generated.
*/ */
@@ -137,6 +230,10 @@ export async function onMessageReceived(data) {
const parsedData = parseResponse(responseText); const parsedData = parseResponse(responseText);
// console.log('[RPG Companion] Parsed data:', parsedData); // console.log('[RPG Companion] Parsed data:', parsedData);
// Legacy text parsing does not produce structured characters; clear old state to avoid stale UI/state
extensionSettings.charactersData = [];
const parsedCharacterThoughts = parsedData.characterThoughts || '';
// Update stored data // Update stored data
if (parsedData.userStats) { if (parsedData.userStats) {
lastGeneratedData.userStats = parsedData.userStats; lastGeneratedData.userStats = parsedData.userStats;
@@ -148,9 +245,9 @@ export async function onMessageReceived(data) {
if (parsedData.infoBox) { if (parsedData.infoBox) {
lastGeneratedData.infoBox = parsedData.infoBox; lastGeneratedData.infoBox = parsedData.infoBox;
} }
if (parsedData.characterThoughts) {
lastGeneratedData.characterThoughts = parsedData.characterThoughts; // Response omitted characters section - clear any previous thoughts to reflect removal
} lastGeneratedData.characterThoughts = parsedCharacterThoughts;
// Store RPG data for this specific swipe in the message's extra field // Store RPG data for this specific swipe in the message's extra field
if (!lastMessage.extra) { if (!lastMessage.extra) {
@@ -164,7 +261,7 @@ export async function onMessageReceived(data) {
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = { lastMessage.extra.rpg_companion_swipes[currentSwipeId] = {
userStats: parsedData.userStats, userStats: parsedData.userStats,
infoBox: parsedData.infoBox, infoBox: parsedData.infoBox,
characterThoughts: parsedData.characterThoughts characterThoughts: parsedCharacterThoughts
}; };
// console.log('[RPG Companion] Stored RPG data for swipe', currentSwipeId); // console.log('[RPG Companion] Stored RPG data for swipe', currentSwipeId);
@@ -173,7 +270,7 @@ export async function onMessageReceived(data) {
if (!committedTrackerData.userStats && !committedTrackerData.infoBox && !committedTrackerData.characterThoughts) { if (!committedTrackerData.userStats && !committedTrackerData.infoBox && !committedTrackerData.characterThoughts) {
committedTrackerData.userStats = parsedData.userStats; committedTrackerData.userStats = parsedData.userStats;
committedTrackerData.infoBox = parsedData.infoBox; committedTrackerData.infoBox = parsedData.infoBox;
committedTrackerData.characterThoughts = parsedData.characterThoughts; committedTrackerData.characterThoughts = parsedCharacterThoughts;
// console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data'); // console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data');
} else { } else {
// console.log('[RPG Companion] Data will be committed when user replies'); // console.log('[RPG Companion] Data will be committed when user replies');
+2 -1
View File
@@ -163,7 +163,8 @@ export function renderThoughts() {
const hasRelationshipEnabled = relationshipFields.length > 0; const hasRelationshipEnabled = relationshipFields.length > 0;
// Convert structured character data to text format for the original fancy renderer // Convert structured character data to text format for the original fancy renderer
let characterThoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts || ''; // Use nullish coalescing so an empty string from the latest response clears UI
let characterThoughtsData = lastGeneratedData.characterThoughts ?? committedTrackerData.characterThoughts ?? '';
// If we have structured data, convert it to text format // If we have structured data, convert it to text format
if (extensionSettings.charactersData && Array.isArray(extensionSettings.charactersData) && extensionSettings.charactersData.length > 0) { if (extensionSettings.charactersData && Array.isArray(extensionSettings.charactersData) && extensionSettings.charactersData.length > 0) {
+7 -8
View File
@@ -234,6 +234,13 @@ function renderUserStatsTab() {
html += `<label for="rpg-show-rpg-attrs">${i18n.getTranslation('template.trackerEditorModal.userStatsTab.enableRpgAttributes')}</label>`; html += `<label for="rpg-show-rpg-attrs">${i18n.getTranslation('template.trackerEditorModal.userStatsTab.enableRpgAttributes')}</label>`;
html += '</div>'; html += '</div>';
// Allow AI to update attributes toggle
const allowAIUpdateAttributes = config.allowAIUpdateAttributes !== undefined ? config.allowAIUpdateAttributes : true;
html += '<div class="rpg-editor-toggle-row">';
html += `<input type="checkbox" id="rpg-allow-ai-update-attrs" ${allowAIUpdateAttributes ? 'checked' : ''}>`;
html += `<label for="rpg-allow-ai-update-attrs">${i18n.getTranslation('template.trackerEditorModal.userStatsTab.allowAIUpdateAttributes')}</label>`;
html += '</div>';
// Always send attributes toggle // Always send attributes toggle
const alwaysSendAttributes = config.alwaysSendAttributes !== undefined ? config.alwaysSendAttributes : false; const alwaysSendAttributes = config.alwaysSendAttributes !== undefined ? config.alwaysSendAttributes : false;
html += '<div class="rpg-editor-toggle-row">'; html += '<div class="rpg-editor-toggle-row">';
@@ -242,14 +249,6 @@ function renderUserStatsTab() {
html += '</div>'; html += '</div>';
html += `<small class="rpg-editor-note">${i18n.getTranslation('template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote')}</small>`; html += `<small class="rpg-editor-note">${i18n.getTranslation('template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote')}</small>`;
// Allow AI to update attributes toggle
const allowAIUpdateAttributes = config.allowAIUpdateAttributes !== undefined ? config.allowAIUpdateAttributes : true;
html += '<div class="rpg-editor-toggle-row">';
html += `<input type="checkbox" id="rpg-allow-ai-update-attrs" ${allowAIUpdateAttributes ? 'checked' : ''}>`;
html += `<label for="rpg-allow-ai-update-attrs">${i18n.getTranslation('template.trackerEditorModal.userStatsTab.allowAIUpdateAttributes')}</label>`;
html += '</div>';
html += `<small class="rpg-editor-note">${i18n.getTranslation('template.trackerEditorModal.userStatsTab.allowAIUpdateAttributesNote')}</small>`;
html += '<div class="rpg-editor-stats-list" id="rpg-editor-attrs-list">'; html += '<div class="rpg-editor-stats-list" id="rpg-editor-attrs-list">';
// Ensure rpgAttributes exists in the actual config (not just local fallback) // Ensure rpgAttributes exists in the actual config (not just local fallback)
+36
View File
@@ -289,6 +289,20 @@
<small data-i18n-key="template.settingsModal.advanced.contextMessagesNote">Number of recent messages to include (Separate mode only)</small> <small data-i18n-key="template.settingsModal.advanced.contextMessagesNote">Number of recent messages to include (Separate mode only)</small>
</div> </div>
<label class="checkbox_label">
<input type="checkbox" id="rpg-toggle-message-interception" />
<span data-i18n-key="template.settingsModal.advanced.enableMessageInterception">Intercept & rewrite user messages with AI</span>
</label>
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;" data-i18n-key="template.settingsModal.advanced.enableMessageInterceptionNote">
When enabled, user messages are sent to the AI along with current RPG state and recent chat, then rewritten in-place.
</small>
<div class="rpg-setting-row">
<label for="rpg-message-interception-depth" data-i18n-key="template.settingsModal.advanced.messageInterceptionDepth">Interception Context Messages:</label>
<input type="number" id="rpg-message-interception-depth" min="1" max="20" value="4" class="rpg-input" />
<small data-i18n-key="template.settingsModal.advanced.messageInterceptionDepthNote">How many recent messages to send with the interception prompt.</small>
</div>
<div class="rpg-setting-row"> <div class="rpg-setting-row">
<label for="rpg-memory-messages" data-i18n-key="template.settingsModal.advanced.memoryBatchSize">Memory Batch Size:</label> <label for="rpg-memory-messages" data-i18n-key="template.settingsModal.advanced.memoryBatchSize">Memory Batch Size:</label>
<input type="number" id="rpg-memory-messages" min="4" max="50" value="16" class="rpg-input" /> <input type="number" id="rpg-memory-messages" min="4" max="50" value="16" class="rpg-input" />
@@ -359,6 +373,28 @@
</small> </small>
</div> </div>
<!-- Custom Message Interception Prompt Editor -->
<div class="rpg-setting-row" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--rpg-border);">
<label for="rpg-custom-message-interception-prompt" style="display: block; margin-bottom: 8px; font-weight: 600;" data-i18n-key="template.settingsModal.advanced.customMessageInterceptionPromptTitle">
<i class="fa-solid fa-pen-to-square" aria-hidden="true"></i> Custom Message Interception Prompt:
</label>
<textarea id="rpg-custom-message-interception-prompt"
style="width: 100%; min-height: 120px; padding: 10px; border-radius: 4px;
border: 1px solid var(--SmartThemeBorderColor); background: var(--SmartThemeBlurTintColor);
color: var(--SmartThemeBodyColor); font-family: 'Courier New', monospace; font-size: 12px;
resize: vertical; line-height: 1.5;"
placeholder=""></textarea>
<div style="margin-top: 8px; display: flex; gap: 8px;">
<button id="rpg-restore-default-message-interception-prompt" class="menu_button" style="flex: 1;">
<i class="fa-solid fa-rotate-left" aria-hidden="true"></i> <span data-i18n-key="template.settingsModal.advanced.restoreDefaultMessageInterceptionPrompt">Restore Default</span>
</button>
</div>
<small style="display: block; margin-top: 8px; color: #888; font-size: 11px;" data-i18n-key="template.settingsModal.advanced.customMessageInterceptionPromptNote">
Customize the instructions sent to the AI when rewriting user messages. Leave empty to use the default guidance. The AI receives this prompt, the current RPG state JSON, and the recent messages you specify above.
</small>
</div>
<!-- Clear Cache Button --> <!-- Clear Cache Button -->
<div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--rpg-border);"> <div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--rpg-border);">
<button id="rpg-clear-cache" class="rpg-btn-clear-cache"> <button id="rpg-clear-cache" class="rpg-btn-clear-cache">