From f1179d3b83d318d3c476995b6bc2c14e6c259879 Mon Sep 17 00:00:00 2001 From: Spicy_Marinara Date: Thu, 8 Jan 2026 21:52:31 +0100 Subject: [PATCH] v3.3.0: Fix encounter UI theming and JSON cleaning regex properties --- README.md | 7 +- manifest.json | 2 +- settings.html | 2 +- src/systems/features/jsonCleaning.js | 41 ++++---- src/systems/generation/apiClient.js | 23 +++-- src/systems/generation/encounterPrompts.js | 10 +- src/systems/generation/injector.js | 4 +- src/systems/rendering/thoughts.js | 10 +- src/systems/ui/encounterUI.js | 113 ++++++--------------- style.css | 96 +++++++++++++---- 10 files changed, 163 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index 3a106c8..c5e7cc9 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/manifest.json b/manifest.json index 319e980..97a1a31 100644 --- a/manifest.json +++ b/manifest.json @@ -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" } diff --git a/settings.html b/settings.html index b86a6d4..a04a767 100644 --- a/settings.html +++ b/settings.html @@ -48,7 +48,7 @@
- v3.2.5 + v3.2.6
diff --git a/src/systems/features/jsonCleaning.js b/src/systems/features/jsonCleaning.js index 64d6df7..d40bc4f 100644 --- a/src/systems/features/jsonCleaning.js +++ b/src/systems/features/jsonCleaning.js @@ -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 diff --git a/src/systems/generation/apiClient.js b/src/systems/generation/apiClient.js index 0d206bd..d46abd8 100644 --- a/src/systems/generation/apiClient.js +++ b/src/systems/generation/apiClient.js @@ -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.' }; } diff --git a/src/systems/generation/encounterPrompts.js b/src/systems/generation/encounterPrompts.js index 3f5b54d..e08051b 100644 --- a/src/systems/generation/encounterPrompts.js +++ b/src/systems/generation/encounterPrompts.js @@ -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('{'); diff --git a/src/systems/generation/injector.js b/src/systems/generation/injector.js index bfbbed0..2e64dad 100644 --- a/src/systems/generation/injector.js +++ b/src/systems/generation/injector.js @@ -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'); diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index db5c5f3..c916fc7 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -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 { diff --git a/src/systems/ui/encounterUI.js b/src/systems/ui/encounterUI.js index aa6c055..60afc1c 100644 --- a/src/systems/ui/encounterUI.js +++ b/src/systems/ui/encounterUI.js @@ -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 `
- ${enemy.sprite || '👹'} + ${avatarUrl ? `${enemy.name}` : sprite}

${enemy.name}

@@ -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 = `
-

Wrong Format Detected

-

${message}

-
- - -
-
- `; - - // 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 = ` -
- -

Wrong Format Detected

-

${message}

+

Wrong Format Detected

+

${message}