v3.3.0: Fix encounter UI theming and JSON cleaning regex properties

This commit is contained in:
Spicy_Marinara
2026-01-08 21:52:31 +01:00
parent 045d1da88b
commit f1179d3b83
10 changed files with 163 additions and 145 deletions
+5 -2
View File
@@ -7,9 +7,12 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
## 🆕 What's New
### v3.2.5
### v3.3.0
- Minor bug fixes.
- Small upgrades to the combat system.
- Regex fix.
- Fixed External API logic.
- Even more minor bug fixes.
**Special thanks to all the other contributors for this project:**
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, and Amauragis.
+1 -1
View File
@@ -6,6 +6,6 @@
"js": "index.js",
"css": "style.css",
"author": "Marinara",
"version": "3.2.5",
"version": "3.3.0",
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
}
+1 -1
View File
@@ -48,7 +48,7 @@
</div>
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
v3.2.5
v3.2.6
</div>
</div>
</div>
+18 -23
View File
@@ -33,7 +33,7 @@ export async function ensureJsonCleaningRegex(st_extension_settings, saveSetting
if (existingScript) {
// Update existing script with new regex pattern if it's different
const newPattern = '/```json[\\s\\S]*?```/gim';
const newPattern = '/```(?:json|markdown)?[\\s\\S]*?```/gim';
// Always ensure these properties are set correctly
let needsSave = false;
@@ -58,18 +58,8 @@ export async function ensureJsonCleaningRegex(st_extension_settings, saveSetting
needsSave = true;
}
if (existingScript.displayOnly !== true) {
existingScript.displayOnly = true; // Alter Chat Display
needsSave = true;
}
if (existingScript.onlyFormatDisplay !== true) {
existingScript.onlyFormatDisplay = true; // Alter Chat Display
needsSave = true;
}
if (existingScript.onlyFormatPrompt !== true) {
existingScript.onlyFormatPrompt = true; // Alter Outgoing Prompt
if (existingScript.markdownOnly !== true) {
existingScript.markdownOnly = true; // Only process markdown
needsSave = true;
}
@@ -78,11 +68,19 @@ export async function ensureJsonCleaningRegex(st_extension_settings, saveSetting
needsSave = true;
}
if (existingScript.filterGaslighting !== true) {
existingScript.filterGaslighting = true; // Ephemerality
if (needsSave && typeof saveSettingsDebounced === 'function') {
// Force immediate save and wait for it
const saveResult = saveSettingsDebounced();
if (saveResult && typeof saveResult.then === 'function') {
await saveResult;
}
// Small delay to ensure save completes
await new Promise(resolve => setTimeout(resolve, 100));
console.log('[RPG Companion] ✅ Updated JSON cleaning regex to v3.2.6 settings.');
} else {
console.log('[RPG Companion] JSON Cleaning Regex is up to date.');
}
console.log('[RPG Companion] JSON Cleaning Regex is already downloaded and active.');
return;
}
@@ -96,25 +94,22 @@ export async function ensureJsonCleaningRegex(st_extension_settings, saveSetting
};
// Create the regex script object for cleaning JSON tracker data
// This regex matches ```json...``` code blocks containing tracker data
// This regex matches ```json...```, ```markdown...```, or plain ```...``` code blocks
// The prompt now explicitly instructs models to use this format
// Updated to handle various whitespace scenarios and ensure it catches all variations
const regexScript = {
id: uuidv4(),
scriptName: scriptName,
// Match ```json...``` code blocks (handles spaces, newlines, any content)
// Match ```json...```, ```markdown...```, or ```...``` code blocks (handles spaces, newlines, any content)
// Using a more permissive pattern to catch all variations
findRegex: '/```json[\\s\\S]*?```/gim',
findRegex: '/```(?:json|markdown)?[\\s\\S]*?```/gim',
replaceString: '',
trimStrings: [],
placement: [2], // 2 = AI Output
disabled: false,
markdownOnly: false,
markdownOnly: true,
promptOnly: true, // Enable prompt processing
runOnEdit: true,
onlyFormatDisplay: true, // Alter Chat Display (enabled)
onlyFormatPrompt: true, // Alter Outgoing Prompt (enabled)
filterGaslighting: true, // Ephemerality - makes it only affect display
substituteRegex: 0,
minDepth: null,
maxDepth: null
+12 -11
View File
@@ -51,9 +51,6 @@ export async function generateWithExternalAPI(messages) {
if (!baseUrl || !baseUrl.trim()) {
throw new Error('External API base URL is not configured');
}
if (!apiKey || !apiKey.trim()) {
throw new Error('External API key is not found. If you switched browsers or cleared your cache, please re-enter your API key in the extension settings.');
}
if (!model || !model.trim()) {
throw new Error('External API model is not configured');
}
@@ -64,13 +61,19 @@ export async function generateWithExternalAPI(messages) {
// console.log(`[RPG Companion] Calling external API: ${normalizedBaseUrl} with model: ${model}`);
// Prepare headers - only include Authorization if API key is provided
const headers = {
'Content-Type': 'application/json'
};
if (apiKey && apiKey.trim()) {
headers['Authorization'] = `Bearer ${apiKey.trim()}`;
}
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey.trim()}`
},
headers: headers,
body: JSON.stringify({
model: model.trim(),
messages: messages,
@@ -122,12 +125,10 @@ export async function testExternalAPIConnection() {
const { baseUrl, model } = extensionSettings.externalApiSettings || {};
const apiKey = localStorage.getItem('rpg_companion_external_api_key');
if (!baseUrl || !apiKey || !model) {
if (!baseUrl || !model) {
return {
success: false,
message: !apiKey
? 'API Key not found. Please re-enter it in settings (keys are stored locally per-browser).'
: 'Please fill in all required fields (Base URL, API Key, and Model)'
message: 'Please fill in all required fields (Base URL and Model). API Key is optional for local servers.'
};
}
+6 -4
View File
@@ -291,7 +291,7 @@ export async function buildEncounterInitPrompt() {
initInstruction += ` ],\n`;
initInstruction += ` "environment": "Brief description of the combat environment",\n`;
initInstruction += ` "styleNotes": {\n`;
initInstruction += ` "environmentType": "forest|dungeon|desert|cave|city|ruins|snow|water|castle|wasteland|plains|mountains|swamp|volcanic",\n`;
initInstruction += ` "environmentType": "forest|dungeon|desert|cave|city|ruins|snow|water|castle|wasteland|plains|mountains|swamp|volcanic|spaceship|mansion",\n`;
initInstruction += ` "atmosphere": "bright|dark|foggy|stormy|calm|eerie|chaotic|peaceful",\n`;
initInstruction += ` "timeOfDay": "dawn|day|dusk|night|twilight",\n`;
initInstruction += ` "weather": "clear|rainy|snowy|windy|stormy|overcast"\n`;
@@ -724,9 +724,11 @@ export function parseEncounterJSON(response) {
// Remove code blocks if present
let cleaned = response.trim();
// Remove ```json and ``` markers
cleaned = cleaned.replace(/```json\s*/gi, '');
cleaned = cleaned.replace(/```\s*/g, '');
// Remove ```json, ```markdown, and ``` markers (more comprehensive)
cleaned = cleaned.replace(/```(?:json|markdown)?\s*/gi, '');
// Remove any remaining backticks
cleaned = cleaned.replace(/`/g, '');
// Find the first { and last }
const firstBrace = cleaned.indexOf('{');
+2 -2
View File
@@ -155,11 +155,11 @@ export async function onGenerationStarted(type, data, dryRun) {
// });
}
// For SEPARATE mode only: Check if we need to commit extension data
// For SEPARATE and EXTERNAL modes: Check if we need to commit extension data
// BUT: Only do this for the MAIN generation, not the tracker update generation
// If isGenerating is true, this is the tracker update generation (second call), so skip flag logic
// console.log('[RPG Companion DEBUG] Before generating:', lastGeneratedData.characterThoughts, ' , committed - ', committedTrackerData.characterThoughts);
if (extensionSettings.generationMode === 'separate' && !isGenerating) {
if ((extensionSettings.generationMode === 'separate' || extensionSettings.generationMode === 'external') && !isGenerating) {
if (!lastActionWasSwipe) {
// User sent a new message - commit lastGeneratedData before generation
// console.log('[RPG Companion] 📝 COMMIT: New message - committing lastGeneratedData');
+8 -2
View File
@@ -451,8 +451,14 @@ export function renderThoughts() {
debugLog(`[RPG Thoughts] Looking up avatar for: ${char.name}`);
// For group chats, search through group members first
if (selected_group) {
// First, check if user manually uploaded a custom avatar
if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[char.name]) {
characterPortrait = extensionSettings.npcAvatars[char.name];
debugLog('[RPG Thoughts] Found custom uploaded avatar');
}
// For group chats, search through group members
if (characterPortrait === FALLBACK_AVATAR_DATA_URI && selected_group) {
debugLog('[RPG Thoughts] In group chat, checking group members...');
try {
+33 -80
View File
@@ -95,7 +95,7 @@ export class EncounterModal {
const combatData = parseEncounterJSON(response);
if (!combatData || !combatData.party || !combatData.enemies) {
this.showErrorWithRegenerate('Invalid JSON format detected. The AI returned malformed data.');
this.showErrorWithRegenerate('Invalid JSON format detected. The AI returned malformed data. Ensure the Max Response Length is set to at least 2048 tokens, otherwise the model might run out of tokens and produce unfinished structures.');
return;
}
@@ -417,10 +417,17 @@ export class EncounterModal {
const hpPercent = (enemy.hp / enemy.maxHp) * 100;
const isDead = enemy.hp <= 0;
// Try to find avatar for enemy (they might be a character from the chat or Present Characters)
const avatarUrl = this.getCharacterAvatar(enemy.name);
const sprite = enemy.sprite || '👹';
// Fallback SVG if no avatar found
const fallbackSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2NjY2NjYyIgb3BhY2l0eT0iMC4zIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjNjY2IiBmb250LXNpemU9IjQwIj4/PC90ZXh0Pjwvc3ZnPg==';
return `
<div class="rpg-encounter-card ${isDead ? 'rpg-encounter-dead' : ''}" data-enemy-index="${index}">
<div class="rpg-encounter-card-sprite">
${enemy.sprite || '👹'}
${avatarUrl ? `<img src="${avatarUrl}" alt="${enemy.name}" onerror="this.parentElement.innerHTML='${sprite}';this.onerror=null;">` : sprite}
</div>
<div class="rpg-encounter-card-info">
<h4>${enemy.name}</h4>
@@ -461,7 +468,7 @@ export class EncounterModal {
}
} else {
// Try to find character avatar by name
avatarUrl = this.getPartyMemberAvatar(member.name);
avatarUrl = this.getCharacterAvatar(member.name);
}
// Fallback SVG if no avatar found
@@ -488,17 +495,31 @@ export class EncounterModal {
}
/**
* Gets avatar for a party member by name
* @param {string} name - Party member name
* Gets avatar for a character by name (works for party members, enemies, and NPCs)
* @param {string} name - Character name
* @returns {string} Avatar URL or null
*/
getPartyMemberAvatar(name) {
// Try to get from NPC avatars first
getCharacterAvatar(name) {
// Priority 1: Check custom uploaded avatars first (from Present Characters panel)
if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[name]) {
return extensionSettings.npcAvatars[name];
}
// Try to find character by name in loaded characters
// Priority 2: Check if character is in the current group
if (selected_group) {
const groupMembers = getGroupMembers(selected_group);
if (groupMembers && groupMembers.length > 0) {
const matchingMember = groupMembers.find(member =>
member && member.name && member.name.toLowerCase() === name.toLowerCase()
);
if (matchingMember && matchingMember.avatar) {
return getSafeThumbnailUrl('avatar', matchingMember.avatar);
}
}
}
// Priority 3: Search all loaded characters
if (characters && Array.isArray(characters)) {
const matchingChar = characters.find(char =>
char && char.name && char.name.toLowerCase() === name.toLowerCase()
@@ -509,7 +530,7 @@ export class EncounterModal {
}
}
// Check if it's the current character
// Priority 4: Check if it's the current character
if (this_chid !== undefined && characters && characters[this_chid]) {
const currentChar = characters[this_chid];
if (currentChar.name && currentChar.name.toLowerCase() === name.toLowerCase()) {
@@ -793,7 +814,7 @@ export class EncounterModal {
const result = parseEncounterJSON(response);
if (!result || !result.combatStats) {
this.showErrorWithRegenerate('Invalid JSON format detected. The AI returned malformed data.');
this.showErrorWithRegenerate('Invalid JSON format detected. The AI returned malformed data. Ensure the Max Response Length is set to at least 2048 tokens, otherwise the model might run out of tokens and produce unfinished structures.');
return;
}
@@ -1223,76 +1244,8 @@ export class EncounterModal {
loadingContent.innerHTML = `
<div class="rpg-encounter-error-box">
<i class="fa-solid fa-exclamation-triangle" style="color: #e94560; font-size: 48px; margin-bottom: 1em;"></i>
<h3 style="color: #e94560; margin: 0 0 0.5em 0;">Wrong Format Detected</h3>
<p style="color: #ccc; margin: 0 0 1.5em 0; max-width: 500px;">${message}</p>
<div style="display: flex; gap: 1em;">
<button id="rpg-error-regenerate" class="rpg-btn rpg-btn-primary">
<i class="fa-solid fa-rotate-right"></i> Regenerate
</button>
<button id="rpg-error-close" class="rpg-btn rpg-btn-secondary">
<i class="fa-solid fa-times"></i> Close
</button>
</div>
</div>
`;
// Add event listeners
const regenerateBtn = loadingContent.querySelector('#rpg-error-regenerate');
const closeBtn = loadingContent.querySelector('#rpg-error-close');
if (regenerateBtn) {
regenerateBtn.addEventListener('click', () => this.regenerateLastRequest());
}
if (closeBtn) {
closeBtn.addEventListener('click', () => this.close());
}
}
}
/**
* Regenerates the last failed request
*/
async regenerateLastRequest() {
if (!this.lastRequest) {
console.warn('[RPG Companion] No request to regenerate');
return;
}
// console.log('[RPG Companion] Regenerating request:', this.lastRequest.type);
if (this.lastRequest.type === 'init') {
// Retry initialization
this.isInitializing = true;
await this.initialize();
} else if (this.lastRequest.type === 'action') {
// Retry action
this.isProcessing = true;
await this.processCombatAction(this.lastRequest.action);
}
}
/**
* Shows an error message with a regenerate button
* @param {string} message - Error message to display
*/
showErrorWithRegenerate(message) {
const loadingContent = this.modal.querySelector('#rpg-encounter-loading');
const combatContent = this.modal.querySelector('#rpg-encounter-content');
// Hide combat content if visible
if (combatContent) {
combatContent.style.display = 'none';
}
// Show error in loading area
if (loadingContent) {
loadingContent.style.display = 'flex';
loadingContent.innerHTML = `
<div class="rpg-encounter-error-box">
<i class="fa-solid fa-exclamation-triangle" style="color: #e94560; font-size: 48px; margin-bottom: 1em;"></i>
<h3 style="color: #e94560; margin: 0 0 0.5em 0;">Wrong Format Detected</h3>
<p style="color: #ccc; margin: 0 0 1.5em 0; max-width: 500px;">${message}</p>
<p style="color: #e94560; font-weight: bold; font-size: 1.2em; margin: 0 0 0.5em 0;">Wrong Format Detected</p>
<p style="color: var(--rpg-text, #ccc); margin: 0 0 1.5em 0; max-width: 500px;">${message}</p>
<div style="display: flex; gap: 1em;">
<button id="rpg-error-regenerate" class="rpg-btn rpg-btn-primary">
<i class="fa-solid fa-rotate-right"></i> Regenerate
+77 -19
View File
@@ -7624,9 +7624,9 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
/* Conclude Button */
.rpg-encounter-conclude-btn {
background: rgba(255, 165, 0, 0.2);
border: 1px solid rgba(255, 165, 0, 0.4);
color: #ffa500;
background: var(--rpg-highlight, rgba(255, 165, 0, 0.2));
border: 1px solid var(--rpg-highlight, rgba(255, 165, 0, 0.4));
color: var(--rpg-text, #eaeaea);
font-size: clamp(12px, 1vw, 14px);
font-family: var(--rpg-font-body, 'Open Sans', sans-serif);
font-weight: 600;
@@ -7641,10 +7641,10 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
}
.rpg-encounter-conclude-btn:hover {
background: rgba(255, 165, 0, 0.3);
border-color: rgba(255, 165, 0, 0.6);
background: var(--rpg-highlight, rgba(255, 165, 0, 0.3));
border-color: var(--rpg-highlight, rgba(255, 165, 0, 0.6));
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(255, 165, 0, 0.3);
box-shadow: 0 4px 8px var(--rpg-highlight, rgba(255, 165, 0, 0.3));
}
/* Content Area - Main scrollable container */
@@ -7684,19 +7684,13 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
align-items: center;
justify-content: center;
padding: 2em;
background: rgba(0, 0, 0, 0.5);
border: 2px solid #e94560;
background: var(--rpg-bg, rgba(0, 0, 0, 0.5));
border: 2px solid var(--rpg-highlight, #e94560);
border-radius: 12px;
max-width: 600px;
text-align: center;
}
.rpg-encounter-error-box h3 {
font-family: var(--rpg-font-heading, 'Cinzel', serif);
font-size: 1.5em;
font-weight: 700;
}
.rpg-encounter-error-box p {
line-height: 1.6;
}
@@ -7716,19 +7710,19 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
}
.rpg-encounter-error-box .rpg-btn-primary {
background: #e94560;
color: #fff;
background: var(--rpg-highlight, #e94560);
color: var(--rpg-text, #eaeaea);
}
.rpg-encounter-error-box .rpg-btn-primary:hover {
background: #ff5577;
background: var(--rpg-highlight-hover, #ff5577);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(233, 69, 96, 0.4);
}
.rpg-encounter-error-box .rpg-btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: #ccc;
background: var(--rpg-border, rgba(255, 255, 255, 0.1));
color: var(--rpg-text, #eaeaea);
border: 1px solid rgba(255, 255, 255, 0.2);
}
@@ -7770,6 +7764,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
display: flex;
align-items: center;
gap: clamp(0.375rem, 0.5vw, 0.5rem);
border-left: none !important;
padding-left: 0 !important;
}
.rpg-encounter-section h3 i {
border-left: none !important;
padding-left: 0 !important;
color: var(--rpg-text, #eaeaea);
}
.rpg-encounter-section {
@@ -7814,6 +7816,18 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
text-align: center;
margin-bottom: 0.4vh;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
min-height: clamp(40px, 4vw, 60px);
}
.rpg-encounter-card-sprite img {
width: clamp(40px, 4vw, 60px);
height: clamp(40px, 4vw, 60px);
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--rpg-border, #4a7ba7);
}
.rpg-encounter-card-avatar {
@@ -7888,6 +7902,20 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
/* Combat Log */
.rpg-encounter-log-section h3 {
margin: 0 0 0.5vh 0;
font-size: clamp(14px, 1.2vw, 18px);
font-weight: 700;
color: var(--rpg-text, #eaeaea);
display: flex;
align-items: center;
gap: clamp(0.375rem, 0.5vw, 0.5rem);
border-left: none !important;
padding-left: 0 !important;
}
.rpg-encounter-log-section h3 i {
border-left: none !important;
padding-left: 0 !important;
color: var(--rpg-text, #eaeaea);
}
.rpg-encounter-log {
@@ -7959,6 +7987,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
display: flex;
align-items: center;
gap: clamp(0.375rem, 0.5vw, 0.5rem);
border-left: none !important;
padding-left: 0 !important;
}
.rpg-encounter-controls h3 i {
border-left: none !important;
padding-left: 0 !important;
color: var(--rpg-text, #eaeaea);
}
.rpg-encounter-controls h4 {
@@ -8272,6 +8308,28 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
border-color: #8b0000;
}
/* Spaceship */
.rpg-encounter-modal[data-environment="spaceship"] .rpg-encounter-overlay {
background: linear-gradient(135deg, rgba(30, 30, 50, 0.4), rgba(10, 10, 30, 0.6));
}
.rpg-encounter-modal[data-environment="spaceship"] .rpg-encounter-container {
background: linear-gradient(to bottom, rgba(50, 60, 80, 0.95), rgba(30, 40, 60, 0.9));
border-color: #1e3a5f;
box-shadow: 0 0 20px rgba(0, 150, 255, 0.3), inset 0 0 20px rgba(0, 100, 200, 0.1);
}
/* Mansion */
.rpg-encounter-modal[data-environment="mansion"] .rpg-encounter-overlay {
background: linear-gradient(135deg, rgba(139, 69, 19, 0.3), rgba(101, 67, 33, 0.5));
}
.rpg-encounter-modal[data-environment="mansion"] .rpg-encounter-container {
background: linear-gradient(to bottom, rgba(120, 80, 50, 0.95), rgba(90, 60, 40, 0.9));
border-color: #654321;
box-shadow: 0 0 15px rgba(218, 165, 32, 0.2), inset 0 0 15px rgba(139, 90, 43, 0.1);
}
/* Atmosphere Modifiers */
/* Dark atmosphere - add shadow overlay */