v3.7.0: Merge PR #109 + opacity sliders + custom attributes fix + context instructions prompt + newline fixes

This commit is contained in:
Spicy_Marinara
2026-01-27 14:33:36 +01:00
parent 08474bd910
commit ea81dd0634
19 changed files with 362 additions and 123 deletions
+6 -4
View File
@@ -7,11 +7,13 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
## 🆕 What's New ## 🆕 What's New
### v3.6.1 ### v3.7.0
- Fixed the bugs in the encounter system where you couldn't use the buttons after performing any custom action. - Added omniscience filter.
- Improved combat actions and made them dynamic, depending on the current situation. - Added opacity to the color selector.
- Added Russian as a supported language. - Overwritten SillyTavern's dumb-ahh trim logic when joining prompts.
- Fixed custom attributes not allowing value increase/decrease.
- Various bug fixes.
**Special thanks to all the other contributors for this project:** **Special thanks to all the other contributors for this project:**
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610. Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
+87
View File
@@ -816,12 +816,30 @@ async function initUI() {
renderUserStats(); // Re-render with new colors renderUserStats(); // Re-render with new colors
}); });
$('#rpg-stat-bar-color-low-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.statBarColorLowOpacity = opacity;
$('#rpg-stat-bar-color-low-opacity-value').text(opacity + '%');
renderUserStats();
}).on('change', function() {
saveSettings();
});
$('#rpg-stat-bar-color-high').on('change', function() { $('#rpg-stat-bar-color-high').on('change', function() {
extensionSettings.statBarColorHigh = String($(this).val()); extensionSettings.statBarColorHigh = String($(this).val());
saveSettings(); saveSettings();
renderUserStats(); // Re-render with new colors renderUserStats(); // Re-render with new colors
}); });
$('#rpg-stat-bar-color-high-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.statBarColorHighOpacity = opacity;
$('#rpg-stat-bar-color-high-opacity-value').text(opacity + '%');
renderUserStats();
}).on('change', function() {
saveSettings();
});
// Theme selection // Theme selection
$('#rpg-theme-select').on('change', function() { $('#rpg-theme-select').on('change', function() {
extensionSettings.theme = String($(this).val()); extensionSettings.theme = String($(this).val());
@@ -843,6 +861,19 @@ async function initUI() {
} }
}); });
$('#rpg-custom-bg-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.bgOpacity = opacity;
$('#rpg-custom-bg-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
$('#rpg-custom-accent').on('change', function() { $('#rpg-custom-accent').on('change', function() {
extensionSettings.customColors.accent = String($(this).val()); extensionSettings.customColors.accent = String($(this).val());
saveSettings(); saveSettings();
@@ -853,6 +884,19 @@ async function initUI() {
} }
}); });
$('#rpg-custom-accent-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.accentOpacity = opacity;
$('#rpg-custom-accent-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
$('#rpg-custom-text').on('change', function() { $('#rpg-custom-text').on('change', function() {
extensionSettings.customColors.text = String($(this).val()); extensionSettings.customColors.text = String($(this).val());
saveSettings(); saveSettings();
@@ -863,6 +907,19 @@ async function initUI() {
} }
}); });
$('#rpg-custom-text-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.textOpacity = opacity;
$('#rpg-custom-text-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
$('#rpg-custom-highlight').on('change', function() { $('#rpg-custom-highlight').on('change', function() {
extensionSettings.customColors.highlight = String($(this).val()); extensionSettings.customColors.highlight = String($(this).val());
saveSettings(); saveSettings();
@@ -873,6 +930,19 @@ async function initUI() {
} }
}); });
$('#rpg-custom-highlight-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.highlightOpacity = opacity;
$('#rpg-custom-highlight-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
// External API settings event handlers // External API settings event handlers
$('#rpg-external-base-url').on('change', function() { $('#rpg-external-base-url').on('change', function() {
if (!extensionSettings.externalApiSettings) { if (!extensionSettings.externalApiSettings) {
@@ -1054,12 +1124,29 @@ async function initUI() {
$('#rpg-strip-widget-options').toggle(stripWidgets.enabled || false); $('#rpg-strip-widget-options').toggle(stripWidgets.enabled || false);
$('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow); $('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow);
$('#rpg-stat-bar-color-low-opacity').val(extensionSettings.statBarColorLowOpacity ?? 100);
$('#rpg-stat-bar-color-low-opacity-value').text((extensionSettings.statBarColorLowOpacity ?? 100) + '%');
$('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh); $('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh);
$('#rpg-stat-bar-color-high-opacity').val(extensionSettings.statBarColorHighOpacity ?? 100);
$('#rpg-stat-bar-color-high-opacity-value').text((extensionSettings.statBarColorHighOpacity ?? 100) + '%');
$('#rpg-theme-select').val(extensionSettings.theme); $('#rpg-theme-select').val(extensionSettings.theme);
$('#rpg-custom-bg').val(extensionSettings.customColors.bg); $('#rpg-custom-bg').val(extensionSettings.customColors.bg);
$('#rpg-custom-bg-opacity').val(extensionSettings.customColors.bgOpacity ?? 100);
$('#rpg-custom-bg-opacity-value').text((extensionSettings.customColors.bgOpacity ?? 100) + '%');
$('#rpg-custom-accent').val(extensionSettings.customColors.accent); $('#rpg-custom-accent').val(extensionSettings.customColors.accent);
$('#rpg-custom-accent-opacity').val(extensionSettings.customColors.accentOpacity ?? 100);
$('#rpg-custom-accent-opacity-value').text((extensionSettings.customColors.accentOpacity ?? 100) + '%');
$('#rpg-custom-text').val(extensionSettings.customColors.text); $('#rpg-custom-text').val(extensionSettings.customColors.text);
$('#rpg-custom-text-opacity').val(extensionSettings.customColors.textOpacity ?? 100);
$('#rpg-custom-text-opacity-value').text((extensionSettings.customColors.textOpacity ?? 100) + '%');
$('#rpg-custom-highlight').val(extensionSettings.customColors.highlight); $('#rpg-custom-highlight').val(extensionSettings.customColors.highlight);
$('#rpg-custom-highlight-opacity').val(extensionSettings.customColors.highlightOpacity ?? 100);
$('#rpg-custom-highlight-opacity-value').text((extensionSettings.customColors.highlightOpacity ?? 100) + '%');
// Initialize External API settings values // Initialize External API settings values
if (extensionSettings.externalApiSettings) { if (extensionSettings.externalApiSettings) {
+1 -1
View File
@@ -6,6 +6,6 @@
"js": "index.js", "js": "index.js",
"css": "style.css", "css": "style.css",
"author": "Marinara", "author": "Marinara",
"version": "3.6.1", "version": "3.7.0",
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern" "homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
} }
+1 -1
View File
@@ -49,7 +49,7 @@
</div> </div>
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;"> <div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
v3.6.1 v3.7.0
</div> </div>
</div> </div>
</div> </div>
+16
View File
@@ -118,6 +118,22 @@ export function loadSettings() {
settingsChanged = true; settingsChanged = true;
} }
// Migration to version 5: Add opacity properties for all colors
if (currentVersion < 5) {
// console.log('[RPG Companion] Migrating settings to version 5 (adding color opacity)');
if (!extensionSettings.customColors) {
extensionSettings.customColors = {};
}
if (extensionSettings.customColors.bgOpacity === undefined) extensionSettings.customColors.bgOpacity = 100;
if (extensionSettings.customColors.accentOpacity === undefined) extensionSettings.customColors.accentOpacity = 100;
if (extensionSettings.customColors.textOpacity === undefined) extensionSettings.customColors.textOpacity = 100;
if (extensionSettings.customColors.highlightOpacity === undefined) extensionSettings.customColors.highlightOpacity = 100;
if (extensionSettings.statBarColorLowOpacity === undefined) extensionSettings.statBarColorLowOpacity = 100;
if (extensionSettings.statBarColorHighOpacity === undefined) extensionSettings.statBarColorHighOpacity = 100;
extensionSettings.settingsVersion = 5;
settingsChanged = true;
}
// Save migrated settings // Save migrated settings
if (settingsChanged) { if (settingsChanged) {
saveSettings(); saveSettings();
+8 -1
View File
@@ -23,6 +23,7 @@ export let extensionSettings = {
showThoughtsInChat: true, // Show thoughts overlay in chat showThoughtsInChat: true, // Show thoughts overlay in chat
narratorMode: false, // Use character card as narrator instead of fixed character references narratorMode: false, // Use character card as narrator instead of fixed character references
customNarratorPrompt: '', // Custom narrator mode prompt text (empty = use default) customNarratorPrompt: '', // Custom narrator mode prompt text (empty = use default)
customContextInstructionsPrompt: '', // Custom context instructions prompt text (empty = use default)
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)
enableDialogueColoring: false, // Enable dialogue coloring prompt injection enableDialogueColoring: false, // Enable dialogue coloring prompt injection
@@ -65,12 +66,18 @@ export let extensionSettings = {
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
customColors: { customColors: {
bg: '#1a1a2e', bg: '#1a1a2e',
bgOpacity: 100,
accent: '#16213e', accent: '#16213e',
accentOpacity: 100,
text: '#eaeaea', text: '#eaeaea',
highlight: '#e94560' textOpacity: 100,
highlight: '#e94560',
highlightOpacity: 100
}, },
statBarColorLow: '#cc3333', // Color for low stat values (red) statBarColorLow: '#cc3333', // Color for low stat values (red)
statBarColorLowOpacity: 100,
statBarColorHigh: '#33cc66', // Color for high stat values (green) statBarColorHigh: '#33cc66', // Color for high stat values (green)
statBarColorHighOpacity: 100,
enableAnimations: true, // Enable smooth animations for stats and content updates enableAnimations: true, // Enable smooth animations for stats and content updates
mobileFabPosition: { mobileFabPosition: {
top: 'calc(var(--topBarBlockSize) + 60px)', top: 'calc(var(--topBarBlockSize) + 60px)',
+8
View File
@@ -20,6 +20,10 @@ export function setupClassicStatsButtons() {
// Delegated event listener for increase buttons // Delegated event listener for increase buttons
$userStatsContainer.on('click', '.rpg-stat-increase', function() { $userStatsContainer.on('click', '.rpg-stat-increase', function() {
const stat = $(this).data('stat'); const stat = $(this).data('stat');
// Initialize custom attributes if they don't exist
if (extensionSettings.classicStats[stat] === undefined) {
extensionSettings.classicStats[stat] = 10;
}
if (extensionSettings.classicStats[stat] < 999) { if (extensionSettings.classicStats[stat] < 999) {
extensionSettings.classicStats[stat]++; extensionSettings.classicStats[stat]++;
saveSettings(); saveSettings();
@@ -33,6 +37,10 @@ export function setupClassicStatsButtons() {
// Delegated event listener for decrease buttons // Delegated event listener for decrease buttons
$userStatsContainer.on('click', '.rpg-stat-decrease', function() { $userStatsContainer.on('click', '.rpg-stat-decrease', function() {
const stat = $(this).data('stat'); const stat = $(this).data('stat');
// Initialize custom attributes if they don't exist
if (extensionSettings.classicStats[stat] === undefined) {
extensionSettings.classicStats[stat] = 10;
}
if (extensionSettings.classicStats[stat] > 1) { if (extensionSettings.classicStats[stat] > 1) {
extensionSettings.classicStats[stat]--; extensionSettings.classicStats[stat]--;
saveSettings(); saveSettings();
+1 -1
View File
@@ -76,7 +76,7 @@ export async function ensureJsonCleaningRegex(st_extension_settings, saveSetting
} }
// Small delay to ensure save completes // Small delay to ensure save completes
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
console.log('[RPG Companion] ✅ Updated JSON cleaning regex to v3.2.6 settings.'); console.log('[RPG Companion] ✅ Updated JSON cleaning regex to v3.2.3 settings.');
} else { } else {
console.log('[RPG Companion] JSON Cleaning Regex is up to date.'); console.log('[RPG Companion] JSON Cleaning Regex is up to date.');
} }
+73 -56
View File
@@ -4,7 +4,7 @@
*/ */
import { getContext } from '../../../../../../extensions.js'; import { getContext } from '../../../../../../extensions.js';
import { setExtensionPrompt, extension_prompt_types, extension_prompt_roles, eventSource, event_types } from '../../../../../../../script.js'; import { extension_prompt_types, extension_prompt_roles, setExtensionPrompt, eventSource, event_types } from '../../../../../../../script.js';
import { import {
extensionSettings, extensionSettings,
committedTrackerData, committedTrackerData,
@@ -25,6 +25,8 @@ import {
DEFAULT_OMNISCIENCE_FILTER_PROMPT, DEFAULT_OMNISCIENCE_FILTER_PROMPT,
DEFAULT_CYOA_PROMPT, DEFAULT_CYOA_PROMPT,
DEFAULT_SPOTIFY_PROMPT, DEFAULT_SPOTIFY_PROMPT,
DEFAULT_NARRATOR_PROMPT,
DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT,
SPOTIFY_FORMAT_INSTRUCTION SPOTIFY_FORMAT_INSTRUCTION
} from './promptBuilder.js'; } from './promptBuilder.js';
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js'; import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
@@ -87,8 +89,8 @@ function buildHistoricalContextMap() {
// For user_message_end: start from the last assistant message (we need its context for the preceding user message) // For user_message_end: start from the last assistant message (we need its context for the preceding user message)
// For assistant_message_end: start from before the last assistant message (it gets current context via setExtensionPrompt) // For assistant_message_end: start from before the last assistant message (it gets current context via setExtensionPrompt)
let processedCount = 0; let processedCount = 0;
const startIndex = position === 'user_message_end' const startIndex = position === 'user_message_end'
? lastAssistantIndex ? lastAssistantIndex
: (lastAssistantIndex > 0 ? lastAssistantIndex - 1 : chat.length - 2); : (lastAssistantIndex > 0 ? lastAssistantIndex - 1 : chat.length - 2);
for (let i = startIndex; i >= 0 && (messageCount === 0 || processedCount < maxMessages); i--) { for (let i = startIndex; i >= 0 && (messageCount === 0 || processedCount < maxMessages); i--) {
@@ -202,7 +204,7 @@ function prepareHistoricalContextInjection() {
/** /**
* Finds the best match position for message content in the prompt. * Finds the best match position for message content in the prompt.
* Tries full content first, then progressively smaller suffixes. * Tries full content first, then progressively smaller suffixes.
* *
* @param {string} prompt - The prompt to search in * @param {string} prompt - The prompt to search in
* @param {string} messageContent - The message content to find * @param {string} messageContent - The message content to find
* @returns {{start: number, end: number}|null} - Position info or null if not found * @returns {{start: number, end: number}|null} - Position info or null if not found
@@ -214,7 +216,7 @@ function findMessageInPrompt(prompt, messageContent) {
// Try to find the full content first // Try to find the full content first
let searchIndex = prompt.lastIndexOf(messageContent); let searchIndex = prompt.lastIndexOf(messageContent);
if (searchIndex !== -1) { if (searchIndex !== -1) {
return { start: searchIndex, end: searchIndex + messageContent.length }; return { start: searchIndex, end: searchIndex + messageContent.length };
} }
@@ -222,15 +224,15 @@ function findMessageInPrompt(prompt, messageContent) {
// If full content not found, try last N characters with progressively smaller chunks // If full content not found, try last N characters with progressively smaller chunks
// This handles cases where messages are truncated in the prompt // This handles cases where messages are truncated in the prompt
const searchLengths = [500, 300, 200, 100, 50]; const searchLengths = [500, 300, 200, 100, 50];
for (const len of searchLengths) { for (const len of searchLengths) {
if (messageContent.length <= len) { if (messageContent.length <= len) {
continue; continue;
} }
const searchContent = messageContent.slice(-len); const searchContent = messageContent.slice(-len);
searchIndex = prompt.lastIndexOf(searchContent); searchIndex = prompt.lastIndexOf(searchContent);
if (searchIndex !== -1) { if (searchIndex !== -1) {
return { start: searchIndex, end: searchIndex + searchContent.length }; return { start: searchIndex, end: searchIndex + searchContent.length };
} }
@@ -242,7 +244,7 @@ function findMessageInPrompt(prompt, messageContent) {
/** /**
* Injects historical context into a text completion prompt string. * Injects historical context into a text completion prompt string.
* Searches for message content in the prompt and appends context after matches. * Searches for message content in the prompt and appends context after matches.
* *
* @param {string} prompt - The text completion prompt * @param {string} prompt - The text completion prompt
* @returns {string} - The modified prompt with injected context * @returns {string} - The modified prompt with injected context
*/ */
@@ -269,7 +271,7 @@ function injectContextIntoTextPrompt(prompt) {
// Find the message content in the prompt // Find the message content in the prompt
const position = findMessageInPrompt(modifiedPrompt, message.mes); const position = findMessageInPrompt(modifiedPrompt, message.mes);
if (!position) { if (!position) {
// Message not found in prompt (might be truncated or not included) // Message not found in prompt (might be truncated or not included)
console.debug(`[RPG Companion] Could not find message ${msgIdx} in prompt for context injection`); console.debug(`[RPG Companion] Could not find message ${msgIdx} in prompt for context injection`);
@@ -291,7 +293,7 @@ function injectContextIntoTextPrompt(prompt) {
/** /**
* Injects historical context into a chat completion message array. * Injects historical context into a chat completion message array.
* Modifies the content of messages in the array directly. * Modifies the content of messages in the array directly.
* *
* @param {Array} chatMessages - The chat completion message array * @param {Array} chatMessages - The chat completion message array
* @returns {Array} - The modified message array with injected context * @returns {Array} - The modified message array with injected context
*/ */
@@ -316,7 +318,7 @@ function injectContextIntoChatPrompt(chatMessages) {
// Find this message in the chat completion array by matching content // Find this message in the chat completion array by matching content
// Try full content first, then progressively smaller suffixes // Try full content first, then progressively smaller suffixes
let found = false; let found = false;
for (const promptMsg of chatMessages) { for (const promptMsg of chatMessages) {
if (!promptMsg.content || typeof promptMsg.content !== 'string') { if (!promptMsg.content || typeof promptMsg.content !== 'string') {
continue; continue;
@@ -336,7 +338,7 @@ function injectContextIntoChatPrompt(chatMessages) {
if (messageContent.length <= len) { if (messageContent.length <= len) {
continue; continue;
} }
const searchContent = messageContent.slice(-len); const searchContent = messageContent.slice(-len);
if (promptMsg.content.includes(searchContent)) { if (promptMsg.content.includes(searchContent)) {
promptMsg.content = promptMsg.content + ctxContent; promptMsg.content = promptMsg.content + ctxContent;
@@ -345,12 +347,12 @@ function injectContextIntoChatPrompt(chatMessages) {
break; break;
} }
} }
if (found) { if (found) {
break; break;
} }
} }
if (!found) { if (!found) {
console.debug(`[RPG Companion] Could not find message ${msgIdx} in chat prompt for context injection`); console.debug(`[RPG Companion] Could not find message ${msgIdx} in chat prompt for context injection`);
} }
@@ -366,7 +368,7 @@ function injectContextIntoChatPrompt(chatMessages) {
/** /**
* Injects historical context into finalMesSend message array (text completion). * Injects historical context into finalMesSend message array (text completion).
* Iterates through chat and finalMesSend in order, matching by content to skip injected messages. * Iterates through chat and finalMesSend in order, matching by content to skip injected messages.
* *
* @param {Array} finalMesSend - The array of message objects {message: string, extensionPrompts: []} * @param {Array} finalMesSend - The array of message objects {message: string, extensionPrompts: []}
* @returns {number} - Number of injections made * @returns {number} - Number of injections made
*/ */
@@ -382,20 +384,20 @@ function injectContextIntoFinalMesSend(finalMesSend) {
} }
let injectedCount = 0; let injectedCount = 0;
// Build a map from chat index to finalMesSend index by matching content in order // Build a map from chat index to finalMesSend index by matching content in order
// This handles injected messages (author's note, OOC, etc.) that exist in finalMesSend but not in chat // This handles injected messages (author's note, OOC, etc.) that exist in finalMesSend but not in chat
const chatToMesSendMap = new Map(); const chatToMesSendMap = new Map();
let mesSendIdx = 0; let mesSendIdx = 0;
for (let chatIdx = 0; chatIdx < chat.length && mesSendIdx < finalMesSend.length; chatIdx++) { for (let chatIdx = 0; chatIdx < chat.length && mesSendIdx < finalMesSend.length; chatIdx++) {
const chatMsg = chat[chatIdx]; const chatMsg = chat[chatIdx];
if (!chatMsg || chatMsg.is_system) { if (!chatMsg || chatMsg.is_system) {
continue; continue;
} }
const chatContent = chatMsg.mes || ''; const chatContent = chatMsg.mes || '';
// Look for this chat message in finalMesSend starting from current position // Look for this chat message in finalMesSend starting from current position
// Skip any finalMesSend entries that don't match (they're injected content) // Skip any finalMesSend entries that don't match (they're injected content)
while (mesSendIdx < finalMesSend.length) { while (mesSendIdx < finalMesSend.length) {
@@ -404,40 +406,40 @@ function injectContextIntoFinalMesSend(finalMesSend) {
mesSendIdx++; mesSendIdx++;
continue; continue;
} }
// Check if this finalMesSend message contains the chat content // Check if this finalMesSend message contains the chat content
// Use a substring match since instruct formatting adds prefixes/suffixes // Use a substring match since instruct formatting adds prefixes/suffixes
// Match with sufficient content (first 50 chars or full message if shorter) // Match with sufficient content (first 50 chars or full message if shorter)
const matchContent = chatContent.length > 50 const matchContent = chatContent.length > 50
? chatContent.substring(0, 50) ? chatContent.substring(0, 50)
: chatContent; : chatContent;
if (matchContent && mesSendObj.message.includes(matchContent)) { if (matchContent && mesSendObj.message.includes(matchContent)) {
// Found a match - record the mapping // Found a match - record the mapping
chatToMesSendMap.set(chatIdx, mesSendIdx); chatToMesSendMap.set(chatIdx, mesSendIdx);
mesSendIdx++; mesSendIdx++;
break; break;
} }
// This finalMesSend entry doesn't match - it's injected content, skip it // This finalMesSend entry doesn't match - it's injected content, skip it
mesSendIdx++; mesSendIdx++;
} }
} }
// Now inject context using the map // Now inject context using the map
for (const [chatIdx, ctxContent] of pendingContextMap) { for (const [chatIdx, ctxContent] of pendingContextMap) {
const targetMesSendIdx = chatToMesSendMap.get(chatIdx); const targetMesSendIdx = chatToMesSendMap.get(chatIdx);
if (targetMesSendIdx === undefined) { if (targetMesSendIdx === undefined) {
console.debug(`[RPG Companion] Chat message ${chatIdx} not found in finalMesSend mapping`); console.debug(`[RPG Companion] Chat message ${chatIdx} not found in finalMesSend mapping`);
continue; continue;
} }
const mesSendObj = finalMesSend[targetMesSendIdx]; const mesSendObj = finalMesSend[targetMesSendIdx];
if (!mesSendObj || !mesSendObj.message) { if (!mesSendObj || !mesSendObj.message) {
continue; continue;
} }
// Append context to this message // Append context to this message
mesSendObj.message = mesSendObj.message + ctxContent; mesSendObj.message = mesSendObj.message + ctxContent;
injectedCount++; injectedCount++;
@@ -451,7 +453,7 @@ function injectContextIntoFinalMesSend(finalMesSend) {
* Event handler for GENERATE_BEFORE_COMBINE_PROMPTS (text completion). * Event handler for GENERATE_BEFORE_COMBINE_PROMPTS (text completion).
* Injects historical context into the finalMesSend array before prompt combination. * Injects historical context into the finalMesSend array before prompt combination.
* This is more reliable than post-combine string searching. * This is more reliable than post-combine string searching.
* *
* @param {Object} eventData - Event data with finalMesSend and other properties * @param {Object} eventData - Event data with finalMesSend and other properties
*/ */
function onGenerateBeforeCombinePrompts(eventData) { function onGenerateBeforeCombinePrompts(eventData) {
@@ -479,7 +481,8 @@ function onGenerateBeforeCombinePrompts(eventData) {
/** /**
* Event handler for GENERATE_AFTER_COMBINE_PROMPTS (text completion). * Event handler for GENERATE_AFTER_COMBINE_PROMPTS (text completion).
* This is now a backup/fallback - primary injection happens in BEFORE_COMBINE. * This is now a backup/fallback - primary injection happens in BEFORE_COMBINE.
* * Also fixes newline spacing after </context> tag.
*
* @param {Object} eventData - Event data with prompt property * @param {Object} eventData - Event data with prompt property
*/ */
function onGenerateAfterCombinePrompts(eventData) { function onGenerateAfterCombinePrompts(eventData) {
@@ -491,25 +494,31 @@ function onGenerateAfterCombinePrompts(eventData) {
return; return;
} }
// Skip if injection already happened in BEFORE_COMBINE let didInjectHistory = false;
if (historyInjectionDone) {
return; // Inject historical context if available and not already done
if (!historyInjectionDone && pendingContextMap.size > 0) {
// Fallback injection for edge cases where BEFORE_COMBINE didn't work
console.log('[RPG Companion] Using fallback string-based injection (AFTER_COMBINE)');
eventData.prompt = injectContextIntoTextPrompt(eventData.prompt);
didInjectHistory = true;
} }
// Only inject if we have pending context // Always fix newlines around context tags (whether we just injected or not)
if (pendingContextMap.size === 0) { eventData.prompt = eventData.prompt.replace(/<context>/g, '\n<context>');
return; eventData.prompt = eventData.prompt.replace(/<\/context>/g, '</context>\n');
}
// Fallback injection for edge cases where BEFORE_COMBINE didn't work // Remove extra newlines after last_message opening and closing tags
console.log('[RPG Companion] Using fallback string-based injection (AFTER_COMBINE)'); // Match exactly the double newline pattern
eventData.prompt = injectContextIntoTextPrompt(eventData.prompt); eventData.prompt = eventData.prompt.replace(/<last_message>\n\n/g, '<last_message>\n');
eventData.prompt = eventData.prompt.replace(/\n\n<\/last_message>/g, '\n</last_message>');
} }
/** /**
* Event handler for CHAT_COMPLETION_PROMPT_READY. * Event handler for CHAT_COMPLETION_PROMPT_READY.
* Injects historical context into the chat message array. * Injects historical context into the chat message array.
* * Also fixes newline spacing around <context> tags.
*
* @param {Object} eventData - Event data with chat property * @param {Object} eventData - Event data with chat property
*/ */
function onChatCompletionPromptReady(eventData) { function onChatCompletionPromptReady(eventData) {
@@ -521,14 +530,20 @@ function onChatCompletionPromptReady(eventData) {
return; return;
} }
// Only inject if we have pending context // Inject historical context if we have pending context
if (pendingContextMap.size === 0) { if (pendingContextMap.size > 0) {
return; eventData.chat = injectContextIntoChatPrompt(eventData.chat);
// DON'T clear pendingContextMap here - let it persist for other generations
// (e.g., prewarm extensions). It will be cleared on GENERATION_ENDED.
} }
eventData.chat = injectContextIntoChatPrompt(eventData.chat); // Fix newlines around context tags for all messages
// DON'T clear pendingContextMap here - let it persist for other generations for (const message of eventData.chat) {
// (e.g., prewarm extensions). It will be cleared on GENERATION_ENDED. if (message.content && typeof message.content === 'string') {
message.content = message.content.replace(/<context>/g, '\n<context>');
message.content = message.content.replace(/<\/context>/g, '</context>\n');
}
}
} }
/** /**
@@ -837,12 +852,14 @@ export async function onGenerationStarted(type, data, dryRun) {
const contextSummary = generateContextualSummary(); const contextSummary = generateContextualSummary();
if (contextSummary) { if (contextSummary) {
const wrappedContext = `\nHere is context information about the current scene, and what follows is the last message in the chat history: // Use custom context instructions prompt if set, otherwise use default
const contextInstructionsText = extensionSettings.customContextInstructionsPrompt || DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT;
const wrappedContext = `
<context> <context>
${contextSummary} ${contextSummary}
${contextInstructionsText}
Ensure these details naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting performance, low hygiene influencing social interactions, environmental factors shaping the scene, or a character's emotional state coloring their responses. </context>`;
</context>\n\n`;
// Inject context at depth 1 (before last user message) as SYSTEM // Inject context at depth 1 (before last user message) as SYSTEM
// Skip when a guided generation injection is present to avoid conflicting instructions // Skip when a guided generation injection is present to avoid conflicting instructions
@@ -966,16 +983,16 @@ Ensure these details naturally reflect and influence the narrative. Character be
export function initHistoryInjectionListeners() { export function initHistoryInjectionListeners() {
// Register persistent listeners for prompt injection // Register persistent listeners for prompt injection
// These check pendingContextMap and only inject if there's data // These check pendingContextMap and only inject if there's data
// Primary: BEFORE_COMBINE for text completion (more reliable - modifies message objects) // Primary: BEFORE_COMBINE for text completion (more reliable - modifies message objects)
eventSource.on(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, onGenerateBeforeCombinePrompts); eventSource.on(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, onGenerateBeforeCombinePrompts);
// Fallback: AFTER_COMBINE for text completion (string-based injection) // Fallback: AFTER_COMBINE for text completion (string-based injection)
eventSource.on(event_types.GENERATE_AFTER_COMBINE_PROMPTS, onGenerateAfterCombinePrompts); eventSource.on(event_types.GENERATE_AFTER_COMBINE_PROMPTS, onGenerateAfterCombinePrompts);
// Chat completion (OpenAI, etc.) // Chat completion (OpenAI, etc.)
eventSource.on(event_types.CHAT_COMPLETION_PROMPT_READY, onChatCompletionPromptReady); eventSource.on(event_types.CHAT_COMPLETION_PROMPT_READY, onChatCompletionPromptReady);
console.log('[RPG Companion] History injection listeners initialized'); console.log('[RPG Companion] History injection listeners initialized');
} }
+7
View File
@@ -617,6 +617,13 @@ export function parseUserStats(statsText) {
if (!quest) return ''; if (!quest) return '';
if (typeof quest === 'string') return quest; if (typeof quest === 'string') return quest;
if (typeof quest === 'object') { if (typeof quest === 'object') {
// Check for locked format: {value, locked}
// Recursively extract value if it's nested
let extracted = quest;
while (typeof extracted === 'object' && extracted.value !== undefined) {
extracted = extracted.value;
}
if (typeof extracted === 'string') return extracted;
// v3 format: {title, description, status} // v3 format: {title, description, status}
return quest.title || quest.description || JSON.stringify(quest); return quest.title || quest.description || JSON.stringify(quest);
} }
+7 -17
View File
@@ -37,24 +37,9 @@ export const DEFAULT_DECEPTION_PROMPT = `When a character is lying or deceiving,
* Default Omniscience Filter prompt text * Default Omniscience Filter prompt text
* This instructs the AI to separate information the player character cannot perceive * This instructs the AI to separate information the player character cannot perceive
*/ */
export const DEFAULT_OMNISCIENCE_FILTER_PROMPT = `OMNISCIENCE FILTER INSTRUCTIONS: export const DEFAULT_OMNISCIENCE_FILTER_PROMPT = `You must strictly separate what the player can directly perceive from what they cannot. They should only read limited narrative content that their persona can actually see, hear, smell, touch, or otherwise directly sense. Before writing any narrative content that involves events, actions, or details the player directly cannot perceive (because they're not looking, too far away, behind them, in another room, happening silently, include NPCs' internal thoughts, etc.), you absolutely must output that hidden information inside a <filter> tag using this exact format:
You must strictly separate what the player character can directly perceive from what they cannot. The player should only read narrative content that their character can actually see, hear, smell, touch, or otherwise directly sense.
If the player character cannot directly perceive something, but it is happening, it ABSOLUTELY MUST be placed inside of a <filter> tag.
BEFORE writing any narrative content that involves events, actions, or details the player character CANNOT directly perceive (because they're not looking, too far away, behind them, in another room, happening silently, etc.), you MUST first output that hidden information inside a <filter> tag using this exact format:
<filter event="[Brief description of what is happening that the player cannot perceive]" reason="[Why the player character cannot perceive this - e.g., 'behind them', 'in another room', 'too quiet to hear', 'focused elsewhere']"/> <filter event="[Brief description of what is happening that the player cannot perceive]" reason="[Why the player character cannot perceive this - e.g., 'behind them', 'in another room', 'too quiet to hear', 'focused elsewhere']"/>
Example: <filter event="Zandik quietly takes the key from the table and slips out the back door" reason="Zandik is behind Mari, who is absorbed in reading, and he moves silently"/> You hear a faint click from somewhere behind you, but when you glance up from your newspaper, the room seems unchanged.`;
CRITICAL RULES:
1. The <filter> tag must come BEFORE any sensory hints (sounds, smells, etc.) that the player DOES perceive from that event
2. Only write narrative that reflects what the player character actually experiences through their senses
3. Instead of "Jake sweeps the floor behind you", write: <filter event="Jake is sweeping the floor" reason="Jake is behind Michael who is focused on reading"/> followed by narrative like "You hear soft sweeping sounds behind you"
4. NPCs' internal thoughts, silent actions, and events in other locations MUST go in <filter> tags
5. The player's narrative should create natural mystery and immersion - they experience the world through limited senses, not omniscient narration
6. Be liberal and proactive in using <filter> tags to hide information the player cannot perceive directly
7. Don't forget to properly close the <filter> tag with />.
EXAMPLE:
Wrong: "As you read the newspaper, Sarah quietly pockets the key from the table behind you and slips out the back door."
Correct: <filter event="Sarah quietly takes the key from the table and slips out the back door" reason="Sarah is behind Michael who is absorbed in reading, and she moves silently"/>You hear a faint click from somewhere behind you, but when you glance up from your newspaper, the room seems unchanged. The afternoon light streams through the windows as you return to your reading.`;
/** /**
@@ -77,6 +62,11 @@ export const SPOTIFY_FORMAT_INSTRUCTION = `Include it in this exact format: <spo
*/ */
export const DEFAULT_NARRATOR_PROMPT = `Infer the identity and details of characters present in each scene from the story context below. Do not use fixed character references; instead, identify characters naturally based on their actions, dialogue, and descriptions in the narrative.`; export const DEFAULT_NARRATOR_PROMPT = `Infer the identity and details of characters present in each scene from the story context below. Do not use fixed character references; instead, identify characters naturally based on their actions, dialogue, and descriptions in the narrative.`;
/**
* Default Context Instructions prompt text (customizable by users)
*/
export const DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT = `The context above is information about the current scene, and what follows is the last message in the chat history. Ensure these details naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting performance, low hygiene influencing social interactions, environmental factors shaping the scene, or a character's emotional state coloring their responses.`;
/** /**
* 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
+6 -2
View File
@@ -212,8 +212,12 @@ export function renderQuests() {
// Get current sub-tab from container or default to 'main' // Get current sub-tab from container or default to 'main'
const activeSubTab = $questsContainer.data('active-subtab') || 'main'; const activeSubTab = $questsContainer.data('active-subtab') || 'main';
// Get quests data // Get quests data - extract value if it's a locked object
const mainQuest = extensionSettings.quests.main || 'None'; let mainQuest = extensionSettings.quests.main || 'None';
// Recursively extract value if it's nested objects
while (typeof mainQuest === 'object' && mainQuest.value !== undefined) {
mainQuest = mainQuest.value;
}
const optionalQuests = extensionSettings.quests.optional || []; const optionalQuests = extensionSettings.quests.optional || [];
// Build HTML // Build HTML
+13 -6
View File
@@ -50,9 +50,11 @@ function debugLog(message, data = null) {
* @param {number} percentage - Value from 0-100 * @param {number} percentage - Value from 0-100
* @param {string} lowColor - Hex color for low values (e.g., '#ff0000') * @param {string} lowColor - Hex color for low values (e.g., '#ff0000')
* @param {string} highColor - Hex color for high values (e.g., '#00ff00') * @param {string} highColor - Hex color for high values (e.g., '#00ff00')
* @returns {string} Interpolated hex color * @param {number} lowOpacity - Opacity for low values (0-100)
* @param {number} highOpacity - Opacity for high values (0-100)
* @returns {string} Interpolated rgba color
*/ */
function getStatColor(percentage, lowColor, highColor) { function getStatColor(percentage, lowColor, highColor, lowOpacity = 100, highOpacity = 100) {
// Clamp percentage to 0-100 // Clamp percentage to 0-100
const percent = Math.max(0, Math.min(100, percentage)) / 100; const percent = Math.max(0, Math.min(100, percentage)) / 100;
@@ -73,10 +75,9 @@ function getStatColor(percentage, lowColor, highColor) {
const r = Math.round(low.r + (high.r - low.r) * percent); const r = Math.round(low.r + (high.r - low.r) * percent);
const g = Math.round(low.g + (high.g - low.g) * percent); const g = Math.round(low.g + (high.g - low.g) * percent);
const b = Math.round(low.b + (high.b - low.b) * percent); const b = Math.round(low.b + (high.b - low.b) * percent);
const a = (lowOpacity + (highOpacity - lowOpacity) * percent) / 100;
// Convert back to hex return `rgba(${r}, ${g}, ${b}, ${a})`;
const toHex = (n) => n.toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
} }
/** /**
@@ -539,7 +540,13 @@ export function renderThoughts() {
<div class="rpg-character-stats-inner">`; <div class="rpg-character-stats-inner">`;
for (const stat of enabledCharStats) { for (const stat of enabledCharStats) {
const statValue = char[stat.name] || 0; const statValue = char[stat.name] || 0;
const statColor = getStatColor(statValue, extensionSettings.statBarColorLow, extensionSettings.statBarColorHigh); const statColor = getStatColor(
statValue,
extensionSettings.statBarColorLow,
extensionSettings.statBarColorHigh,
extensionSettings.statBarColorLowOpacity ?? 100,
extensionSettings.statBarColorHighOpacity ?? 100
);
html += ` html += `
<div class="rpg-character-stat"> <div class="rpg-character-stat">
<span class="rpg-stat-name">${stat.name}: </span><span class="rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${stat.name}" style="color: ${statColor}" title="Click to edit ${stat.name}">${statValue}%</span> <span class="rpg-stat-name">${stat.name}: </span><span class="rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${stat.name}" style="color: ${statColor}" title="Click to edit ${stat.name}">${statValue}%</span>
+4 -2
View File
@@ -21,6 +21,7 @@ import { getSafeThumbnailUrl } from '../../utils/avatars.js';
import { buildInventorySummary } from '../generation/promptBuilder.js'; import { buildInventorySummary } from '../generation/promptBuilder.js';
import { isItemLocked, setItemLock } from '../generation/lockManager.js'; import { isItemLocked, setItemLock } from '../generation/lockManager.js';
import { updateFabWidgets } from '../ui/mobile.js'; import { updateFabWidgets } from '../ui/mobile.js';
import { getStatBarColors } from '../ui/theme.js';
/** /**
* Builds the user stats text string using custom stat names * Builds the user stats text string using custom stat names
@@ -251,8 +252,9 @@ export function renderUserStats() {
} }
} }
// Create gradient from low to high color // Create gradient from low to high color with opacity
const gradient = `linear-gradient(to right, ${extensionSettings.statBarColorLow}, ${extensionSettings.statBarColorHigh})`; const colors = getStatBarColors();
const gradient = `linear-gradient(to right, ${colors.low}, ${colors.high})`;
// Check if stats bars section is locked // Check if stats bars section is locked
const isStatsLocked = isItemLocked('userStats', 'stats'); const isStatsLocked = isItemLocked('userStats', 'stats');
+9 -5
View File
@@ -5,6 +5,7 @@
import { i18n } from '../../core/i18n.js'; import { i18n } from '../../core/i18n.js';
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js'; import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
import { hexToRgba } from './theme.js';
/** /**
* Helper to parse time string and calculate clock hand angles * Helper to parse time string and calculate clock hand angles
@@ -28,7 +29,7 @@ function parseTimeForClock(timeStr) {
export function updateStripWidgets() { export function updateStripWidgets() {
const $panel = $('#rpg-companion-panel'); const $panel = $('#rpg-companion-panel');
const $container = $('#rpg-strip-widget-container'); const $container = $('#rpg-strip-widget-container');
if ($panel.length === 0 || $container.length === 0) return; if ($panel.length === 0 || $container.length === 0) return;
// Check if strip widgets are enabled // Check if strip widgets are enabled
@@ -118,7 +119,7 @@ export function updateStripWidgets() {
const $statsWidget = $container.find('.rpg-strip-widget-stats'); const $statsWidget = $container.find('.rpg-strip-widget-stats');
if (widgetSettings.stats?.enabled) { if (widgetSettings.stats?.enabled) {
let allStats = []; let allStats = [];
// Try to get stats from tracker data first (most current) // Try to get stats from tracker data first (most current)
const userStatsData = lastGeneratedData?.userStats || committedTrackerData?.userStats; const userStatsData = lastGeneratedData?.userStats || committedTrackerData?.userStats;
if (userStatsData) { if (userStatsData) {
@@ -131,7 +132,7 @@ export function updateStripWidgets() {
console.warn('[RPG Strip Widgets] Failed to parse tracker userStats:', e); console.warn('[RPG Strip Widgets] Failed to parse tracker userStats:', e);
} }
} }
// Fallback to extensionSettings.userStats // Fallback to extensionSettings.userStats
if (allStats.length === 0 && extensionSettings.userStats) { if (allStats.length === 0 && extensionSettings.userStats) {
try { try {
@@ -237,7 +238,9 @@ export function updateStripWidgets() {
*/ */
function getStatColor(value) { function getStatColor(value) {
const lowColor = extensionSettings.statBarColorLow || '#cc3333'; const lowColor = extensionSettings.statBarColorLow || '#cc3333';
const lowOpacity = extensionSettings.statBarColorLowOpacity ?? 100;
const highColor = extensionSettings.statBarColorHigh || '#33cc66'; const highColor = extensionSettings.statBarColorHigh || '#33cc66';
const highOpacity = extensionSettings.statBarColorHighOpacity ?? 100;
// Simple linear interpolation between low and high colors // Simple linear interpolation between low and high colors
const percent = Math.min(100, Math.max(0, value)) / 100; const percent = Math.min(100, Math.max(0, value)) / 100;
@@ -246,13 +249,14 @@ function getStatColor(value) {
const lowRGB = hexToRgb(lowColor); const lowRGB = hexToRgb(lowColor);
const highRGB = hexToRgb(highColor); const highRGB = hexToRgb(highColor);
if (!lowRGB || !highRGB) return value > 50 ? highColor : lowColor; if (!lowRGB || !highRGB) return value > 50 ? hexToRgba(highColor, highOpacity) : hexToRgba(lowColor, lowOpacity);
const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent); const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent);
const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * percent); const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * percent);
const b = Math.round(lowRGB.b + (highRGB.b - lowRGB.b) * percent); const b = Math.round(lowRGB.b + (highRGB.b - lowRGB.b) * percent);
const a = (lowOpacity + (highOpacity - lowOpacity) * percent) / 100;
return `rgb(${r}, ${g}, ${b})`; return `rgba(${r}, ${g}, ${b}, ${a})`;
} }
/** /**
+9 -5
View File
@@ -8,6 +8,7 @@ import { saveSettings } from '../../core/persistence.js';
import { closeMobilePanelWithAnimation, updateCollapseToggleIcon } from './layout.js'; import { closeMobilePanelWithAnimation, updateCollapseToggleIcon } from './layout.js';
import { setupDesktopTabs, removeDesktopTabs } from './desktop.js'; import { setupDesktopTabs, removeDesktopTabs } from './desktop.js';
import { i18n } from '../../core/i18n.js'; import { i18n } from '../../core/i18n.js';
import { hexToRgba } from './theme.js';
/** /**
* Updates the text labels of the mobile navigation tabs based on the current language. * Updates the text labels of the mobile navigation tabs based on the current language.
@@ -1451,7 +1452,7 @@ export function updateFabWidgets() {
if (widgetSettings.attributes?.enabled) { if (widgetSettings.attributes?.enabled) {
// Check if RPG attributes are enabled in trackerConfig // Check if RPG attributes are enabled in trackerConfig
const showRPGAttributes = extensionSettings.trackerConfig?.userStats?.showRPGAttributes !== false; const showRPGAttributes = extensionSettings.trackerConfig?.userStats?.showRPGAttributes !== false;
if (showRPGAttributes && extensionSettings.classicStats) { if (showRPGAttributes && extensionSettings.classicStats) {
// Get enabled attributes from trackerConfig // Get enabled attributes from trackerConfig
const configuredAttrs = extensionSettings.trackerConfig?.userStats?.rpgAttributes || []; const configuredAttrs = extensionSettings.trackerConfig?.userStats?.rpgAttributes || [];
@@ -1541,10 +1542,10 @@ export function updateFabWidgets() {
e.stopPropagation(); e.stopPropagation();
const $this = $(this); const $this = $(this);
const wasExpanded = $this.hasClass('expanded'); const wasExpanded = $this.hasClass('expanded');
// Collapse all other expanded widgets // Collapse all other expanded widgets
$container.find('.rpg-fab-widget.expanded').removeClass('expanded'); $container.find('.rpg-fab-widget.expanded').removeClass('expanded');
// Toggle this one // Toggle this one
if (!wasExpanded) { if (!wasExpanded) {
$this.addClass('expanded'); $this.addClass('expanded');
@@ -1567,7 +1568,9 @@ export function updateFabWidgets() {
*/ */
function getStatColor(value) { function getStatColor(value) {
const lowColor = extensionSettings.statBarColorLow || '#cc3333'; const lowColor = extensionSettings.statBarColorLow || '#cc3333';
const lowOpacity = extensionSettings.statBarColorLowOpacity ?? 100;
const highColor = extensionSettings.statBarColorHigh || '#33cc66'; const highColor = extensionSettings.statBarColorHigh || '#33cc66';
const highOpacity = extensionSettings.statBarColorHighOpacity ?? 100;
// Simple linear interpolation between low and high colors // Simple linear interpolation between low and high colors
const percent = Math.min(100, Math.max(0, value)) / 100; const percent = Math.min(100, Math.max(0, value)) / 100;
@@ -1576,13 +1579,14 @@ function getStatColor(value) {
const lowRGB = hexToRgb(lowColor); const lowRGB = hexToRgb(lowColor);
const highRGB = hexToRgb(highColor); const highRGB = hexToRgb(highColor);
if (!lowRGB || !highRGB) return value > 50 ? highColor : lowColor; if (!lowRGB || !highRGB) return value > 50 ? hexToRgba(highColor, highOpacity) : hexToRgba(lowColor, lowOpacity);
const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent); const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent);
const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * percent); const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * percent);
const b = Math.round(lowRGB.b + (highRGB.b - lowRGB.b) * percent); const b = Math.round(lowRGB.b + (highRGB.b - lowRGB.b) * percent);
const a = (lowOpacity + (highOpacity - lowOpacity) * percent) / 100;
return `rgb(${r}, ${g}, ${b})`; return `rgba(${r}, ${g}, ${b}, ${a})`;
} }
/** /**
+10 -1
View File
@@ -4,7 +4,7 @@
*/ */
import { extensionSettings } from '../../core/state.js'; import { extensionSettings } from '../../core/state.js';
import { saveSettings } from '../../core/persistence.js'; import { saveSettings } from '../../core/persistence.js';
import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_DECEPTION_PROMPT, DEFAULT_OMNISCIENCE_FILTER_PROMPT, DEFAULT_CYOA_PROMPT, DEFAULT_SPOTIFY_PROMPT, DEFAULT_NARRATOR_PROMPT } from '../generation/promptBuilder.js'; import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_DECEPTION_PROMPT, DEFAULT_OMNISCIENCE_FILTER_PROMPT, DEFAULT_CYOA_PROMPT, DEFAULT_SPOTIFY_PROMPT, DEFAULT_NARRATOR_PROMPT, DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT } from '../generation/promptBuilder.js';
let $editorModal = null; let $editorModal = null;
let tempPrompts = null; // Temporary prompts for cancel functionality let tempPrompts = null; // Temporary prompts for cancel functionality
@@ -18,6 +18,7 @@ const DEFAULT_PROMPTS = {
cyoa: DEFAULT_CYOA_PROMPT, cyoa: DEFAULT_CYOA_PROMPT,
spotify: DEFAULT_SPOTIFY_PROMPT, spotify: DEFAULT_SPOTIFY_PROMPT,
narrator: DEFAULT_NARRATOR_PROMPT, narrator: DEFAULT_NARRATOR_PROMPT,
contextInstructions: DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT,
plotRandom: 'Actually, the scene is getting stale. Introduce {{random::stakes::a plot twist::a new character::a cataclysm::a fourth-wall-breaking joke::a sudden atmospheric phenomenon::a plot hook::a running gag::an ecchi scenario::Death from Discworld::a new stake::a drama::a conflict::an angered entity::a god::a vision::a prophetic dream::Il Dottore from Genshin Impact::a new development::a civilian in need::an emotional bit::a threat::a villain::an important memory recollection::a marriage proposal::a date idea::an angry horde of villagers with pitchforks::a talking animal::an enemy::a cliffhanger::a short omniscient POV shift to a completely different character::a quest::an unexpected revelation::a scandal::an evil clone::death of an important character::harm to an important character::a romantic setup::a gossip::a messenger::a plot point from the past::a plot hole::a tragedy::a ghost::an otherworldly occurrence::a plot device::a curse::a magic device::a rival::an unexpected pregnancy::a brothel::a prostitute::a new location::a past lover::a completely random thing::a what-if scenario::a significant choice::war::love::a monster::lewd undertones::Professor Mari::a travelling troupe::a secret::a fortune-teller::something completely different::a killer::a murder mystery::a mystery::a skill check::a deus ex machina::three raccoons in a trench coat::a pet::a slave::an orphan::a psycho::tentacles::"there is only one bed" trope::accidental marriage::a fun twist::a boss battle::sexy corn::an eldritch horror::a character getting hungry, thirsty, or exhausted::horniness::a need for a bathroom break need::someone fainting::an assassination attempt::a meta narration of this all being an out of hand DND session::a dungeon::a friend in need::an old friend::a small time skip::a scene shift::Aurora Borealis, at this time of year, at this time of day, at this part of the country::a grand ball::a surprise party::zombies::foreshadowing::a Spanish Inquisition (nobody expects it)::a natural plot progression}} to make things more interesting! Be creative, but stay grounded in the setting.', plotRandom: 'Actually, the scene is getting stale. Introduce {{random::stakes::a plot twist::a new character::a cataclysm::a fourth-wall-breaking joke::a sudden atmospheric phenomenon::a plot hook::a running gag::an ecchi scenario::Death from Discworld::a new stake::a drama::a conflict::an angered entity::a god::a vision::a prophetic dream::Il Dottore from Genshin Impact::a new development::a civilian in need::an emotional bit::a threat::a villain::an important memory recollection::a marriage proposal::a date idea::an angry horde of villagers with pitchforks::a talking animal::an enemy::a cliffhanger::a short omniscient POV shift to a completely different character::a quest::an unexpected revelation::a scandal::an evil clone::death of an important character::harm to an important character::a romantic setup::a gossip::a messenger::a plot point from the past::a plot hole::a tragedy::a ghost::an otherworldly occurrence::a plot device::a curse::a magic device::a rival::an unexpected pregnancy::a brothel::a prostitute::a new location::a past lover::a completely random thing::a what-if scenario::a significant choice::war::love::a monster::lewd undertones::Professor Mari::a travelling troupe::a secret::a fortune-teller::something completely different::a killer::a murder mystery::a mystery::a skill check::a deus ex machina::three raccoons in a trench coat::a pet::a slave::an orphan::a psycho::tentacles::"there is only one bed" trope::accidental marriage::a fun twist::a boss battle::sexy corn::an eldritch horror::a character getting hungry, thirsty, or exhausted::horniness::a need for a bathroom break need::someone fainting::an assassination attempt::a meta narration of this all being an out of hand DND session::a dungeon::a friend in need::an old friend::a small time skip::a scene shift::Aurora Borealis, at this time of year, at this time of day, at this part of the country::a grand ball::a surprise party::zombies::foreshadowing::a Spanish Inquisition (nobody expects it)::a natural plot progression}} to make things more interesting! Be creative, but stay grounded in the setting.',
plotNatural: 'Actually, the scene is getting stale. Progress it, to make things more interesting! Reintroduce an unresolved plot point from the past, or push the story further towards the current main goal. Be creative, but stay grounded in the setting.', plotNatural: 'Actually, the scene is getting stale. Progress it, to make things more interesting! Reintroduce an unresolved plot point from the past, or push the story further towards the current main goal. Be creative, but stay grounded in the setting.',
avatar: `You are a visionary artist trapped in a cage of logic. Your mind is filled with poetry and distant horizons; however, your hands are uncontrollably focused on creating the perfect character avatar description that is faithful to the original intent, rich in detail, aesthetically pleasing, and directly usable by text-to-image models. Any ambiguity or metaphor will make you feel extremely uncomfortable. avatar: `You are a visionary artist trapped in a cage of logic. Your mind is filled with poetry and distant horizons; however, your hands are uncontrollably focused on creating the perfect character avatar description that is faithful to the original intent, rich in detail, aesthetically pleasing, and directly usable by text-to-image models. Any ambiguity or metaphor will make you feel extremely uncomfortable.
@@ -101,6 +102,7 @@ function openPromptsEditor() {
cyoa: extensionSettings.customCYOAPrompt || '', cyoa: extensionSettings.customCYOAPrompt || '',
spotify: extensionSettings.customSpotifyPrompt || '', spotify: extensionSettings.customSpotifyPrompt || '',
narrator: extensionSettings.customNarratorPrompt || '', narrator: extensionSettings.customNarratorPrompt || '',
contextInstructions: extensionSettings.customContextInstructionsPrompt || '',
plotRandom: extensionSettings.customPlotRandomPrompt || '', plotRandom: extensionSettings.customPlotRandomPrompt || '',
plotNatural: extensionSettings.customPlotNaturalPrompt || '', plotNatural: extensionSettings.customPlotNaturalPrompt || '',
avatar: extensionSettings.avatarLLMCustomInstruction || '', avatar: extensionSettings.avatarLLMCustomInstruction || '',
@@ -117,6 +119,7 @@ function openPromptsEditor() {
$('#rpg-prompt-cyoa').val(extensionSettings.customCYOAPrompt || DEFAULT_PROMPTS.cyoa); $('#rpg-prompt-cyoa').val(extensionSettings.customCYOAPrompt || DEFAULT_PROMPTS.cyoa);
$('#rpg-prompt-spotify').val(extensionSettings.customSpotifyPrompt || DEFAULT_PROMPTS.spotify); $('#rpg-prompt-spotify').val(extensionSettings.customSpotifyPrompt || DEFAULT_PROMPTS.spotify);
$('#rpg-prompt-narrator').val(extensionSettings.customNarratorPrompt || DEFAULT_PROMPTS.narrator); $('#rpg-prompt-narrator').val(extensionSettings.customNarratorPrompt || DEFAULT_PROMPTS.narrator);
$('#rpg-prompt-context-instructions').val(extensionSettings.customContextInstructionsPrompt || DEFAULT_PROMPTS.contextInstructions);
$('#rpg-prompt-plot-random').val(extensionSettings.customPlotRandomPrompt || DEFAULT_PROMPTS.plotRandom); $('#rpg-prompt-plot-random').val(extensionSettings.customPlotRandomPrompt || DEFAULT_PROMPTS.plotRandom);
$('#rpg-prompt-plot-natural').val(extensionSettings.customPlotNaturalPrompt || DEFAULT_PROMPTS.plotNatural); $('#rpg-prompt-plot-natural').val(extensionSettings.customPlotNaturalPrompt || DEFAULT_PROMPTS.plotNatural);
$('#rpg-prompt-avatar').val(extensionSettings.avatarLLMCustomInstruction || DEFAULT_PROMPTS.avatar); $('#rpg-prompt-avatar').val(extensionSettings.avatarLLMCustomInstruction || DEFAULT_PROMPTS.avatar);
@@ -157,6 +160,7 @@ function savePrompts() {
extensionSettings.customCYOAPrompt = $('#rpg-prompt-cyoa').val().trim(); extensionSettings.customCYOAPrompt = $('#rpg-prompt-cyoa').val().trim();
extensionSettings.customSpotifyPrompt = $('#rpg-prompt-spotify').val().trim(); extensionSettings.customSpotifyPrompt = $('#rpg-prompt-spotify').val().trim();
extensionSettings.customNarratorPrompt = $('#rpg-prompt-narrator').val().trim(); extensionSettings.customNarratorPrompt = $('#rpg-prompt-narrator').val().trim();
extensionSettings.customContextInstructionsPrompt = $('#rpg-prompt-context-instructions').val().trim();
extensionSettings.customPlotRandomPrompt = $('#rpg-prompt-plot-random').val().trim(); extensionSettings.customPlotRandomPrompt = $('#rpg-prompt-plot-random').val().trim();
extensionSettings.customPlotNaturalPrompt = $('#rpg-prompt-plot-natural').val().trim(); extensionSettings.customPlotNaturalPrompt = $('#rpg-prompt-plot-natural').val().trim();
extensionSettings.avatarLLMCustomInstruction = $('#rpg-prompt-avatar').val().trim(); extensionSettings.avatarLLMCustomInstruction = $('#rpg-prompt-avatar').val().trim();
@@ -198,6 +202,9 @@ function restorePromptToDefault(promptType) {
case 'narrator': case 'narrator':
extensionSettings.customNarratorPrompt = ''; extensionSettings.customNarratorPrompt = '';
break; break;
case 'contextInstructions':
extensionSettings.customContextInstructionsPrompt = '';
break;
case 'plotRandom': case 'plotRandom':
extensionSettings.customPlotRandomPrompt = ''; extensionSettings.customPlotRandomPrompt = '';
break; break;
@@ -232,6 +239,7 @@ function restoreAllToDefaults() {
$('#rpg-prompt-cyoa').val(DEFAULT_PROMPTS.cyoa); $('#rpg-prompt-cyoa').val(DEFAULT_PROMPTS.cyoa);
$('#rpg-prompt-spotify').val(DEFAULT_PROMPTS.spotify); $('#rpg-prompt-spotify').val(DEFAULT_PROMPTS.spotify);
$('#rpg-prompt-narrator').val(DEFAULT_PROMPTS.narrator); $('#rpg-prompt-narrator').val(DEFAULT_PROMPTS.narrator);
$('#rpg-prompt-context-instructions').val(DEFAULT_PROMPTS.contextInstructions);
$('#rpg-prompt-plot-random').val(DEFAULT_PROMPTS.plotRandom); $('#rpg-prompt-plot-random').val(DEFAULT_PROMPTS.plotRandom);
$('#rpg-prompt-plot-natural').val(DEFAULT_PROMPTS.plotNatural); $('#rpg-prompt-plot-natural').val(DEFAULT_PROMPTS.plotNatural);
$('#rpg-prompt-avatar').val(DEFAULT_PROMPTS.avatar); $('#rpg-prompt-avatar').val(DEFAULT_PROMPTS.avatar);
@@ -247,6 +255,7 @@ function restoreAllToDefaults() {
extensionSettings.customCYOAPrompt = ''; extensionSettings.customCYOAPrompt = '';
extensionSettings.customSpotifyPrompt = ''; extensionSettings.customSpotifyPrompt = '';
extensionSettings.customNarratorPrompt = ''; extensionSettings.customNarratorPrompt = '';
extensionSettings.customContextInstructionsPrompt = '';
extensionSettings.customPlotRandomPrompt = ''; extensionSettings.customPlotRandomPrompt = '';
extensionSettings.customPlotNaturalPrompt = ''; extensionSettings.customPlotNaturalPrompt = '';
extensionSettings.avatarLLMCustomInstruction = ''; extensionSettings.avatarLLMCustomInstruction = '';
+52 -12
View File
@@ -5,6 +5,37 @@
import { extensionSettings, $panelContainer } from '../../core/state.js'; import { extensionSettings, $panelContainer } from '../../core/state.js';
/**
* Converts hex color and opacity percentage to rgba string
* @param {string} hex - Hex color (e.g., '#ff0000')
* @param {number} opacity - Opacity percentage (0-100)
* @returns {string} - RGBA color string
*/
export function hexToRgba(hex, opacity = 100) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const a = opacity / 100;
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
/**
* Gets stat bar colors with opacity applied
* @returns {{low: string, high: string}} RGBA color strings for stat bars
*/
export function getStatBarColors() {
return {
low: hexToRgba(
extensionSettings.statBarColorLow || '#cc3333',
extensionSettings.statBarColorLowOpacity ?? 100
),
high: hexToRgba(
extensionSettings.statBarColorHigh || '#33cc66',
extensionSettings.statBarColorHighOpacity ?? 100
)
};
}
/** /**
* Applies the selected theme to the panel. * Applies the selected theme to the panel.
*/ */
@@ -75,24 +106,33 @@ export function applyCustomTheme() {
const colors = extensionSettings.customColors; const colors = extensionSettings.customColors;
// Convert hex colors with opacity to rgba
const bgColor = hexToRgba(colors.bg, colors.bgOpacity ?? 100);
const accentColor = hexToRgba(colors.accent, colors.accentOpacity ?? 100);
const textColor = hexToRgba(colors.text, colors.textOpacity ?? 100);
const highlightColor = hexToRgba(colors.highlight, colors.highlightOpacity ?? 100);
// Create shadow with 50% opacity of highlight color
const shadowColor = hexToRgba(colors.highlight, (colors.highlightOpacity ?? 100) * 0.5);
// Apply custom CSS variables as inline styles to main panel // Apply custom CSS variables as inline styles to main panel
$panelContainer.css({ $panelContainer.css({
'--rpg-bg': colors.bg, '--rpg-bg': bgColor,
'--rpg-accent': colors.accent, '--rpg-accent': accentColor,
'--rpg-text': colors.text, '--rpg-text': textColor,
'--rpg-highlight': colors.highlight, '--rpg-highlight': highlightColor,
'--rpg-border': colors.highlight, '--rpg-border': highlightColor,
'--rpg-shadow': `${colors.highlight}80` // Add alpha for shadow '--rpg-shadow': shadowColor
}); });
// Apply custom colors to mobile toggle and thought elements // Apply custom colors to mobile toggle and thought elements
const customStyles = { const customStyles = {
'--rpg-bg': colors.bg, '--rpg-bg': bgColor,
'--rpg-accent': colors.accent, '--rpg-accent': accentColor,
'--rpg-text': colors.text, '--rpg-text': textColor,
'--rpg-highlight': colors.highlight, '--rpg-highlight': highlightColor,
'--rpg-border': colors.highlight, '--rpg-border': highlightColor,
'--rpg-shadow': `${colors.highlight}80` '--rpg-shadow': shadowColor
}; };
const $mobileToggle = $('#rpg-mobile-toggle'); const $mobileToggle = $('#rpg-mobile-toggle');
+44 -9
View File
@@ -250,29 +250,49 @@
<div class="rpg-setting-row"> <div class="rpg-setting-row">
<label for="rpg-custom-bg" <label for="rpg-custom-bg"
data-i18n-key="template.settingsModal.themeOptions.custom.background">Background:</label> data-i18n-key="template.settingsModal.themeOptions.custom.background">Background:</label>
<input type="color" id="rpg-custom-bg" value="#1a1a2e" /> <div style="display: flex; gap: 8px; align-items: center;">
<input type="color" id="rpg-custom-bg" value="#1a1a2e" style="width: 60px;" />
<input type="range" id="rpg-custom-bg-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-custom-bg-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
</div> </div>
<div class="rpg-setting-row"> <div class="rpg-setting-row">
<label for="rpg-custom-accent" <label for="rpg-custom-accent"
data-i18n-key="template.settingsModal.themeOptions.custom.accent">Accent:</label> data-i18n-key="template.settingsModal.themeOptions.custom.accent">Accent:</label>
<input type="color" id="rpg-custom-accent" value="#16213e" /> <div style="display: flex; gap: 8px; align-items: center;">
<input type="color" id="rpg-custom-accent" value="#16213e" style="width: 60px;" />
<input type="range" id="rpg-custom-accent-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-custom-accent-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
</div> </div>
<div class="rpg-setting-row"> <div class="rpg-setting-row">
<label for="rpg-custom-text" <label for="rpg-custom-text"
data-i18n-key="template.settingsModal.themeOptions.custom.text">Text:</label> data-i18n-key="template.settingsModal.themeOptions.custom.text">Text:</label>
<input type="color" id="rpg-custom-text" value="#eaeaea" /> <div style="display: flex; gap: 8px; align-items: center;">
<input type="color" id="rpg-custom-text" value="#eaeaea" style="width: 60px;" />
<input type="range" id="rpg-custom-text-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-custom-text-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
</div> </div>
<div class="rpg-setting-row"> <div class="rpg-setting-row">
<label for="rpg-custom-highlight" <label for="rpg-custom-highlight"
data-i18n-key="template.settingsModal.themeOptions.custom.highlight">Highlight:</label> data-i18n-key="template.settingsModal.themeOptions.custom.highlight">Highlight:</label>
<input type="color" id="rpg-custom-highlight" value="#e94560" /> <div style="display: flex; gap: 8px; align-items: center;">
<input type="color" id="rpg-custom-highlight" value="#e94560" style="width: 60px;" />
<input type="range" id="rpg-custom-highlight-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-custom-highlight-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
</div> </div>
</div> </div>
<div class="rpg-setting-row"> <div class="rpg-setting-row">
<label for="rpg-stat-bar-color-low" data-i18n-key="template.settingsModal.theme.statBarLow">Stat Bar <label for="rpg-stat-bar-color-low" data-i18n-key="template.settingsModal.theme.statBarLow">Stat Bar
Color (Low):</label> Color (Low):</label>
<input type="color" id="rpg-stat-bar-color-low" value="#cc3333" /> <div style="display: flex; gap: 8px; align-items: center;">
<input type="color" id="rpg-stat-bar-color-low" value="#cc3333" style="width: 60px;" />
<input type="range" id="rpg-stat-bar-color-low-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-stat-bar-color-low-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
<small data-i18n-key="template.settingsModal.theme.statBarLowNote">Color when stats are at <small data-i18n-key="template.settingsModal.theme.statBarLowNote">Color when stats are at
0%.</small> 0%.</small>
</div> </div>
@@ -280,7 +300,11 @@
<div class="rpg-setting-row"> <div class="rpg-setting-row">
<label for="rpg-stat-bar-color-high" data-i18n-key="template.settingsModal.theme.statBarHigh">Stat <label for="rpg-stat-bar-color-high" data-i18n-key="template.settingsModal.theme.statBarHigh">Stat
Bar Color (High):</label> Bar Color (High):</label>
<input type="color" id="rpg-stat-bar-color-high" value="#33cc66" /> <div style="display: flex; gap: 8px; align-items: center;">
<input type="color" id="rpg-stat-bar-color-high" value="#33cc66" style="width: 60px;" />
<input type="range" id="rpg-stat-bar-color-high-opacity" min="0" max="100" value="100" style="flex: 1;" />
<span id="rpg-stat-bar-color-high-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
</div>
<small data-i18n-key="template.settingsModal.theme.statBarHighNote">Color when stats are at <small data-i18n-key="template.settingsModal.theme.statBarHighNote">Color when stats are at
100%.</small> 100%.</small>
</div> </div>
@@ -1070,6 +1094,20 @@
</button> </button>
</div> </div>
<!-- Context Instructions Prompt -->
<div class="rpg-prompt-editor-section">
<label for="rpg-prompt-context-instructions" style="display: block; margin-bottom: 8px; font-weight: 600;">
<i class="fa-solid fa-comment-dots"></i> Context Instructions Prompt
</label>
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
Injected in Separate/External mode after the context summary. Tells the AI how to use the context.
</small>
<textarea id="rpg-prompt-context-instructions" class="rpg-prompt-textarea" rows="4"></textarea>
<button class="menu_button rpg-restore-prompt-btn" data-prompt="contextInstructions" style="margin-top: 8px;">
<i class="fa-solid fa-rotate-left"></i>&nbsp;Restore Default
</button>
</div>
<!-- Random Plot Progression Prompt --> <!-- Random Plot Progression Prompt -->
<div class="rpg-prompt-editor-section"> <div class="rpg-prompt-editor-section">
<label for="rpg-prompt-plot-random" style="display: block; margin-bottom: 8px; font-weight: 600;"> <label for="rpg-prompt-plot-random" style="display: block; margin-bottom: 8px; font-weight: 600;">
@@ -1156,9 +1194,6 @@
</div> </div>
<footer class="rpg-settings-popup-footer"> <footer class="rpg-settings-popup-footer">
<button id="rpg-prompts-restore-all" class="rpg-btn-secondary" type="button">
<i class="fa-solid fa-rotate-left"></i> Restore All To Default
</button>
<div class="rpg-footer-right"> <div class="rpg-footer-right">
<button id="rpg-prompts-cancel" class="rpg-btn-secondary" type="button">Cancel</button> <button id="rpg-prompts-cancel" class="rpg-btn-secondary" type="button">Cancel</button>
<button id="rpg-prompts-save" class="rpg-btn-primary" type="button"> <button id="rpg-prompts-save" class="rpg-btn-primary" type="button">