From 74d6174bb777cd67e1937025886c5f06ac3a534c Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Wed, 22 Oct 2025 07:13:15 +1100 Subject: [PATCH 01/15] fix: add comprehensive debug logging and resilient parsing for AI responses - Add detailed console logging throughout parseResponse() and parseUserStats() to help diagnose parsing issues reported by users - Make parser more resilient to format variations: - Accept "Stats", "User Stats", "Player Stats" headers - Accept "Info Box", "Scene Info", "Information" headers - Accept "Present Characters", "Characters", "Character Thoughts" headers - Add keyword-based fallback when headers are missing - Support "Mood:" prefix in addition to "Status:" for mood/conditions - Support dash separator in addition to comma - Add length check (<=10 chars) for emoji/mood to avoid false matches - Log full parsing pipeline: input -> matches -> extraction -> final values - Log error stack traces for better debugging This should help diagnose issues where attributes vanish, characters show as placeholder, or data is generated but not displayed/refreshed correctly. --- src/systems/generation/parser.js | 151 +++++++++++++++++++++++++------ 1 file changed, 122 insertions(+), 29 deletions(-) diff --git a/src/systems/generation/parser.js b/src/systems/generation/parser.js index 72ead86..6b8adab 100644 --- a/src/systems/generation/parser.js +++ b/src/systems/generation/parser.js @@ -21,41 +21,74 @@ export function parseResponse(responseText) { characterThoughts: null }; + // DEBUG: Log full response for troubleshooting + console.log('[RPG Parser] ==================== PARSING AI RESPONSE ===================='); + console.log('[RPG Parser] Response length:', responseText.length, 'chars'); + console.log('[RPG Parser] First 500 chars:', responseText.substring(0, 500)); + // Extract code blocks const codeBlockRegex = /```([^`]+)```/g; const matches = [...responseText.matchAll(codeBlockRegex)]; - // console.log('[RPG Companion] Found', matches.length, 'code blocks'); + console.log('[RPG Parser] Found', matches.length, 'code blocks'); - for (const match of matches) { + for (let i = 0; i < matches.length; i++) { + const match = matches[i]; const content = match[1].trim(); - // console.log('[RPG Companion] Checking code block (first 200 chars):', content.substring(0, 200)); + console.log(`[RPG Parser] --- Code Block ${i + 1} ---`); + console.log('[RPG Parser] First 300 chars:', content.substring(0, 300)); - // Match Stats section - if (content.match(/Stats\s*\n\s*---/i)) { + // Match Stats section - flexible patterns + const isStats = + content.match(/Stats\s*\n\s*---/i) || + content.match(/User Stats\s*\n\s*---/i) || + content.match(/Player Stats\s*\n\s*---/i) || + // Fallback: look for stat keywords without strict header + (content.match(/Health:\s*\d+%/i) && content.match(/Energy:\s*\d+%/i)); + + // Match Info Box section - flexible patterns + const isInfoBox = + content.match(/Info Box\s*\n\s*---/i) || + content.match(/Scene Info\s*\n\s*---/i) || + content.match(/Information\s*\n\s*---/i) || + // Fallback: look for info box keywords + (content.match(/Date:/i) && content.match(/Location:/i) && content.match(/Time:/i)); + + // Match Present Characters section - flexible patterns + const isCharacters = + content.match(/Present Characters\s*\n\s*---/i) || + content.match(/Characters\s*\n\s*---/i) || + content.match(/Character Thoughts\s*\n\s*---/i) || + // Fallback: look for table-like structure with emoji and pipes + (content.includes(" | ") && (content.includes("Thoughts") || content.includes("💭"))); + + if (isStats) { result.userStats = content; - // console.log('[RPG Companion] ✓ Found Stats section'); - } - // Match Info Box section - else if (content.match(/Info Box\s*\n\s*---/i)) { + console.log('[RPG Parser] ✓ Matched: Stats section'); + } else if (isInfoBox) { result.infoBox = content; - // console.log('[RPG Companion] ✓ Found Info Box section'); - } - // Match Present Characters section - flexible matching - else if (content.match(/Present Characters\s*\n\s*---/i) || content.includes(" | ")) { + console.log('[RPG Parser] ✓ Matched: Info Box section'); + } else if (isCharacters) { result.characterThoughts = content; - // console.log('[RPG Companion] ✓ Found Present Characters section:', content); + console.log('[RPG Parser] ✓ Matched: Present Characters section'); + console.log('[RPG Parser] Full content:', content); } else { - // console.log('[RPG Companion] ✗ Code block did not match any section'); + console.log('[RPG Parser] ✗ No match - checking patterns:'); + console.log('[RPG Parser] - Has "Stats\\n---"?', !!content.match(/Stats\s*\n\s*---/i)); + console.log('[RPG Parser] - Has stat keywords?', !!(content.match(/Health:\s*\d+%/i) && content.match(/Energy:\s*\d+%/i))); + console.log('[RPG Parser] - Has "Info Box\\n---"?', !!content.match(/Info Box\s*\n\s*---/i)); + console.log('[RPG Parser] - Has info keywords?', !!(content.match(/Date:/i) && content.match(/Location:/i))); + console.log('[RPG Parser] - Has "Present Characters\\n---"?', !!content.match(/Present Characters\s*\n\s*---/i)); + console.log('[RPG Parser] - Has " | " + thoughts?', !!(content.includes(" | ") && (content.includes("Thoughts") || content.includes("💭")))); } } - // console.log('[RPG Companion] Parse results:', { - // hasStats: !!result.userStats, - // hasInfoBox: !!result.infoBox, - // hasThoughts: !!result.characterThoughts - // }); + console.log('[RPG Parser] ==================== PARSE RESULTS ===================='); + console.log('[RPG Parser] Found Stats:', !!result.userStats); + console.log('[RPG Parser] Found Info Box:', !!result.infoBox); + console.log('[RPG Parser] Found Characters:', !!result.characterThoughts); + console.log('[RPG Parser] ======================================================='); return result; } @@ -67,6 +100,10 @@ export function parseResponse(responseText) { * @param {string} statsText - The raw stats text from AI response */ export function parseUserStats(statsText) { + console.log('[RPG Parser] ==================== PARSING USER STATS ===================='); + console.log('[RPG Parser] Stats text length:', statsText.length, 'chars'); + console.log('[RPG Parser] Stats text preview:', statsText.substring(0, 200)); + try { // Extract percentages and mood/conditions const healthMatch = statsText.match(/Health:\s*(\d+)%/); @@ -75,43 +112,85 @@ export function parseUserStats(statsText) { const hygieneMatch = statsText.match(/Hygiene:\s*(\d+)%/); const arousalMatch = statsText.match(/Arousal:\s*(\d+)%/); - // Match new format: Status: [Emoji, Conditions] - // Also support legacy format: [Emoji]: [Conditions] for backward compatibility + console.log('[RPG Parser] Stat matches:', { + health: healthMatch ? healthMatch[1] : 'NOT FOUND', + satiety: satietyMatch ? satietyMatch[1] : 'NOT FOUND', + energy: energyMatch ? energyMatch[1] : 'NOT FOUND', + hygiene: hygieneMatch ? hygieneMatch[1] : 'NOT FOUND', + arousal: arousalMatch ? arousalMatch[1] : 'NOT FOUND' + }); + + // Match mood/status with multiple format variations + // Format 1: Status: [Emoji, Conditions] + // Format 2: Status: [Emoji], [Conditions] + // Format 3: [Emoji]: [Conditions] (legacy) + // Format 4: Mood: [Emoji] - [Conditions] let moodMatch = null; + + // Try new format: Status: emoji, conditions const statusMatch = statsText.match(/Status:\s*(.+?),\s*(.+)/i); if (statusMatch) { - // New format: Status: [Emoji, Conditions] moodMatch = [null, statusMatch[1].trim(), statusMatch[2].trim()]; - } else { - // Legacy format: [Emoji]: [Conditions] + } + // Try alternative: Mood: emoji, conditions + else { + const moodAltMatch = statsText.match(/Mood:\s*(.+?)[,\-]\s*(.+)/i); + if (moodAltMatch) { + moodMatch = [null, moodAltMatch[1].trim(), moodAltMatch[2].trim()]; + } + } + + // Legacy format fallback: [Emoji]: [Conditions] + if (!moodMatch) { const lines = statsText.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); - // Skip lines with percentages or "Inventory:" or "Status:" - if (line.includes('%') || line.toLowerCase().startsWith('inventory:') || line.toLowerCase().startsWith('status:')) continue; - // Match emoji followed by colon and conditions + // Skip lines with percentages or known keywords + if (line.includes('%') || + line.toLowerCase().startsWith('inventory:') || + line.toLowerCase().startsWith('status:') || + line.toLowerCase().startsWith('health:') || + line.toLowerCase().startsWith('energy:') || + line.toLowerCase().startsWith('satiety:') || + line.toLowerCase().startsWith('hygiene:') || + line.toLowerCase().startsWith('arousal:')) continue; + + // Match emoji/mood followed by colon and conditions const match = line.match(/^(.+?):\s*(.+)$/); - if (match) { + if (match && match[1].length <= 10) { // Emoji/mood should be short moodMatch = match; break; } } } + console.log('[RPG Parser] Mood/Status match:', { + found: !!moodMatch, + emoji: moodMatch ? moodMatch[1] : 'NOT FOUND', + conditions: moodMatch ? moodMatch[2] : 'NOT FOUND' + }); + // Extract inventory - use v2 parser if feature flag enabled, otherwise fallback to v1 if (FEATURE_FLAGS.useNewInventory) { const inventoryData = extractInventory(statsText); if (inventoryData) { extensionSettings.userStats.inventory = inventoryData; + console.log('[RPG Parser] Inventory v2 extracted:', inventoryData); + } else { + console.log('[RPG Parser] Inventory v2 extraction failed'); } } else { // Legacy v1 parsing for backward compatibility const inventoryMatch = statsText.match(/Inventory:\s*(.+)/i); if (inventoryMatch) { extensionSettings.userStats.inventory = inventoryMatch[1].trim(); + console.log('[RPG Parser] Inventory v1 extracted:', inventoryMatch[1].trim()); + } else { + console.log('[RPG Parser] Inventory v1 not found'); } } + // Update extension settings if (healthMatch) extensionSettings.userStats.health = parseInt(healthMatch[1]); if (satietyMatch) extensionSettings.userStats.satiety = parseInt(satietyMatch[1]); if (energyMatch) extensionSettings.userStats.energy = parseInt(energyMatch[1]); @@ -122,9 +201,23 @@ export function parseUserStats(statsText) { extensionSettings.userStats.conditions = moodMatch[2].trim(); // Conditions } + console.log('[RPG Parser] Final userStats after parsing:', { + health: extensionSettings.userStats.health, + satiety: extensionSettings.userStats.satiety, + energy: extensionSettings.userStats.energy, + hygiene: extensionSettings.userStats.hygiene, + arousal: extensionSettings.userStats.arousal, + mood: extensionSettings.userStats.mood, + conditions: extensionSettings.userStats.conditions, + inventory: FEATURE_FLAGS.useNewInventory ? 'v2 object' : extensionSettings.userStats.inventory + }); + saveSettings(); + console.log('[RPG Parser] Settings saved successfully'); + console.log('[RPG Parser] ======================================================='); } catch (error) { console.error('[RPG Companion] Error parsing user stats:', error); + console.error('[RPG Companion] Stack trace:', error.stack); } } From b5d35ac2b0b9fbe2d012d341470cac3463ffc4b6 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Wed, 22 Oct 2025 07:22:28 +1100 Subject: [PATCH 02/15] feat: add mobile-friendly debug mode for parser troubleshooting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive debug logging system that's accessible on mobile devices where browser console is impractical. **New Features:** - Debug mode toggle in extension settings (🔍 Debug Mode) - Mobile-friendly debug panel with slide-up UI - Red bug FAB button to toggle debug log viewer - Copy logs to clipboard functionality - Auto-scrolling log display with timestamps - Stores last 100 log entries to prevent memory issues **Parser Enhancements:** - All parser logs now use debugLog() helper function - Logs only appear in UI when debug mode is enabled - Console.log still works for desktop debugging - Full visibility into parsing pipeline: - Raw AI response preview - Code blocks found and matched - Stats extraction (health, energy, mood, etc.) - Inventory parsing (v1 and v2) - Final values saved to settings **UI Components:** - src/systems/ui/debug.js: Debug panel creation and management - style.css: Mobile-first debug panel styles (FAB + slide-up panel) - Desktop view: Smaller panel in bottom-right corner **Settings:** - src/core/config.js: Added debugMode default (false) - src/core/state.js: Added debug logs storage array - settings.html: Added debug mode checkbox - index.js: Wire up debug toggle and initialize UI **Usage for Mobile Users:** 1. Enable "Debug Mode" in RPG Companion settings 2. Red bug button appears (bottom-left) 3. Tap bug button to view logs 4. Use "Copy" to share logs for troubleshooting 5. Logs show exactly what AI generated and how parser handled it This addresses the issue where users on mobile can't access browser console to diagnose parsing problems (vanishing attributes, placeholder characters, etc.). Now they can view and share logs directly. --- index.js | 13 +++ settings.html | 6 ++ src/core/config.js | 3 +- src/core/state.js | 29 ++++++ src/systems/generation/parser.js | 82 ++++++++------- src/systems/ui/debug.js | 156 ++++++++++++++++++++++++++++ style.css | 171 +++++++++++++++++++++++++++++++ 7 files changed, 424 insertions(+), 36 deletions(-) create mode 100644 src/systems/ui/debug.js diff --git a/index.js b/index.js index 037592d..f91172b 100644 --- a/index.js +++ b/index.js @@ -105,6 +105,9 @@ import { setupDesktopTabs, removeDesktopTabs } from './src/systems/ui/desktop.js'; +import { + updateDebugUIVisibility +} from './src/systems/ui/debug.js'; // Feature modules import { setupPlotButtons, sendPlotProgression } from './src/systems/features/plotProgression.js'; @@ -190,6 +193,13 @@ function addExtensionSettings() { updateChatThoughts(); // This will re-create the thought bubble if data exists } }); + + // Set up the debug mode toggle + $('#rpg-debug-mode').prop('checked', extensionSettings.debugMode).on('change', function() { + extensionSettings.debugMode = $(this).prop('checked'); + saveSettings(); + updateDebugUIVisibility(); + }); } /** @@ -455,6 +465,9 @@ async function initUI() { setupContentEditableScrolling(); setupRefreshButtonDrag(); initInventoryEventListeners(); + + // Initialize debug UI if debug mode is enabled + updateDebugUIVisibility(); } diff --git a/settings.html b/settings.html index a6d40ea..069a9fe 100644 --- a/settings.html +++ b/settings.html @@ -11,6 +11,12 @@ Toggle to enable/disable the RPG Companion extension. Configure additional settings within the panel itself. + + Enable debug logging visible in UI. Useful for troubleshooting parsing issues on mobile devices. Shows a red bug button to view parser logs. +
Discord diff --git a/src/core/config.js b/src/core/config.js index 2fe9030..82ad9c7 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -78,5 +78,6 @@ export const defaultSettings = { cha: 10 }, lastDiceRoll: null, // Store last dice roll result - collapsedInventoryLocations: [] // Array of collapsed storage location names + collapsedInventoryLocations: [], // Array of collapsed storage location names + debugMode: false // Enable debug logging visible in UI (for mobile debugging) }; diff --git a/src/core/state.js b/src/core/state.js index c837bba..6cff0d4 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -123,6 +123,12 @@ export const FEATURE_FLAGS = { useNewInventory: true // Enable v2 inventory system with categorized storage }; +/** + * Debug logs storage for mobile-friendly debugging + * Stores parser logs that can be viewed in UI + */ +export let debugLogs = []; + /** * Fallback avatar image (base64-encoded SVG with "?" icon) * Using base64 to avoid quote-encoding issues in HTML attributes @@ -204,3 +210,26 @@ export function setThoughtsContainer($element) { export function setInventoryContainer($element) { $inventoryContainer = $element; } + +export function addDebugLog(message, data = null) { + const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; // HH:MM:SS + const logEntry = { + timestamp, + message, + data: data ? JSON.stringify(data, null, 2) : null + }; + debugLogs.push(logEntry); + + // Keep only last 100 entries to avoid memory issues + if (debugLogs.length > 100) { + debugLogs.shift(); + } +} + +export function clearDebugLogs() { + debugLogs = []; +} + +export function getDebugLogs() { + return debugLogs; +} diff --git a/src/systems/generation/parser.js b/src/systems/generation/parser.js index 6b8adab..9026b45 100644 --- a/src/systems/generation/parser.js +++ b/src/systems/generation/parser.js @@ -3,10 +3,20 @@ * Handles parsing of AI responses to extract tracker data */ -import { extensionSettings, FEATURE_FLAGS } from '../../core/state.js'; +import { extensionSettings, FEATURE_FLAGS, addDebugLog } from '../../core/state.js'; import { saveSettings } from '../../core/persistence.js'; import { extractInventory } from './inventoryParser.js'; +/** + * Helper to log to both console and debug logs array + */ +function debugLog(message, data = null) { + console.log(message, data || ''); + if (extensionSettings.debugMode) { + addDebugLog(message, data); + } +} + /** * Parses the model response to extract the different data sections. * Extracts tracker data from markdown code blocks in the AI response. @@ -22,22 +32,22 @@ export function parseResponse(responseText) { }; // DEBUG: Log full response for troubleshooting - console.log('[RPG Parser] ==================== PARSING AI RESPONSE ===================='); - console.log('[RPG Parser] Response length:', responseText.length, 'chars'); - console.log('[RPG Parser] First 500 chars:', responseText.substring(0, 500)); + debugLog('[RPG Parser] ==================== PARSING AI RESPONSE ===================='); + debugLog('[RPG Parser] Response length:', responseText.length + ' chars'); + debugLog('[RPG Parser] First 500 chars:', responseText.substring(0, 500)); // Extract code blocks const codeBlockRegex = /```([^`]+)```/g; const matches = [...responseText.matchAll(codeBlockRegex)]; - console.log('[RPG Parser] Found', matches.length, 'code blocks'); + debugLog('[RPG Parser] Found', matches.length + ' code blocks'); for (let i = 0; i < matches.length; i++) { const match = matches[i]; const content = match[1].trim(); - console.log(`[RPG Parser] --- Code Block ${i + 1} ---`); - console.log('[RPG Parser] First 300 chars:', content.substring(0, 300)); + debugLog(`[RPG Parser] --- Code Block ${i + 1} ---`); + debugLog('[RPG Parser] First 300 chars:', content.substring(0, 300)); // Match Stats section - flexible patterns const isStats = @@ -65,30 +75,30 @@ export function parseResponse(responseText) { if (isStats) { result.userStats = content; - console.log('[RPG Parser] ✓ Matched: Stats section'); + debugLog('[RPG Parser] ✓ Matched: Stats section'); } else if (isInfoBox) { result.infoBox = content; - console.log('[RPG Parser] ✓ Matched: Info Box section'); + debugLog('[RPG Parser] ✓ Matched: Info Box section'); } else if (isCharacters) { result.characterThoughts = content; - console.log('[RPG Parser] ✓ Matched: Present Characters section'); - console.log('[RPG Parser] Full content:', content); + debugLog('[RPG Parser] ✓ Matched: Present Characters section'); + debugLog('[RPG Parser] Full content:', content); } else { - console.log('[RPG Parser] ✗ No match - checking patterns:'); - console.log('[RPG Parser] - Has "Stats\\n---"?', !!content.match(/Stats\s*\n\s*---/i)); - console.log('[RPG Parser] - Has stat keywords?', !!(content.match(/Health:\s*\d+%/i) && content.match(/Energy:\s*\d+%/i))); - console.log('[RPG Parser] - Has "Info Box\\n---"?', !!content.match(/Info Box\s*\n\s*---/i)); - console.log('[RPG Parser] - Has info keywords?', !!(content.match(/Date:/i) && content.match(/Location:/i))); - console.log('[RPG Parser] - Has "Present Characters\\n---"?', !!content.match(/Present Characters\s*\n\s*---/i)); - console.log('[RPG Parser] - Has " | " + thoughts?', !!(content.includes(" | ") && (content.includes("Thoughts") || content.includes("💭")))); + debugLog('[RPG Parser] ✗ No match - checking patterns:'); + debugLog('[RPG Parser] - Has "Stats\\n---"?', !!content.match(/Stats\s*\n\s*---/i)); + debugLog('[RPG Parser] - Has stat keywords?', !!(content.match(/Health:\s*\d+%/i) && content.match(/Energy:\s*\d+%/i))); + debugLog('[RPG Parser] - Has "Info Box\\n---"?', !!content.match(/Info Box\s*\n\s*---/i)); + debugLog('[RPG Parser] - Has info keywords?', !!(content.match(/Date:/i) && content.match(/Location:/i))); + debugLog('[RPG Parser] - Has "Present Characters\\n---"?', !!content.match(/Present Characters\s*\n\s*---/i)); + debugLog('[RPG Parser] - Has " | " + thoughts?', !!(content.includes(" | ") && (content.includes("Thoughts") || content.includes("💭")))); } } - console.log('[RPG Parser] ==================== PARSE RESULTS ===================='); - console.log('[RPG Parser] Found Stats:', !!result.userStats); - console.log('[RPG Parser] Found Info Box:', !!result.infoBox); - console.log('[RPG Parser] Found Characters:', !!result.characterThoughts); - console.log('[RPG Parser] ======================================================='); + debugLog('[RPG Parser] ==================== PARSE RESULTS ===================='); + debugLog('[RPG Parser] Found Stats:', !!result.userStats); + debugLog('[RPG Parser] Found Info Box:', !!result.infoBox); + debugLog('[RPG Parser] Found Characters:', !!result.characterThoughts); + debugLog('[RPG Parser] ======================================================='); return result; } @@ -100,9 +110,9 @@ export function parseResponse(responseText) { * @param {string} statsText - The raw stats text from AI response */ export function parseUserStats(statsText) { - console.log('[RPG Parser] ==================== PARSING USER STATS ===================='); - console.log('[RPG Parser] Stats text length:', statsText.length, 'chars'); - console.log('[RPG Parser] Stats text preview:', statsText.substring(0, 200)); + debugLog('[RPG Parser] ==================== PARSING USER STATS ===================='); + debugLog('[RPG Parser] Stats text length:', statsText.length + ' chars'); + debugLog('[RPG Parser] Stats text preview:', statsText.substring(0, 200)); try { // Extract percentages and mood/conditions @@ -112,7 +122,7 @@ export function parseUserStats(statsText) { const hygieneMatch = statsText.match(/Hygiene:\s*(\d+)%/); const arousalMatch = statsText.match(/Arousal:\s*(\d+)%/); - console.log('[RPG Parser] Stat matches:', { + debugLog('[RPG Parser] Stat matches:', { health: healthMatch ? healthMatch[1] : 'NOT FOUND', satiety: satietyMatch ? satietyMatch[1] : 'NOT FOUND', energy: energyMatch ? energyMatch[1] : 'NOT FOUND', @@ -164,7 +174,7 @@ export function parseUserStats(statsText) { } } - console.log('[RPG Parser] Mood/Status match:', { + debugLog('[RPG Parser] Mood/Status match:', { found: !!moodMatch, emoji: moodMatch ? moodMatch[1] : 'NOT FOUND', conditions: moodMatch ? moodMatch[2] : 'NOT FOUND' @@ -175,18 +185,18 @@ export function parseUserStats(statsText) { const inventoryData = extractInventory(statsText); if (inventoryData) { extensionSettings.userStats.inventory = inventoryData; - console.log('[RPG Parser] Inventory v2 extracted:', inventoryData); + debugLog('[RPG Parser] Inventory v2 extracted:', inventoryData); } else { - console.log('[RPG Parser] Inventory v2 extraction failed'); + debugLog('[RPG Parser] Inventory v2 extraction failed'); } } else { // Legacy v1 parsing for backward compatibility const inventoryMatch = statsText.match(/Inventory:\s*(.+)/i); if (inventoryMatch) { extensionSettings.userStats.inventory = inventoryMatch[1].trim(); - console.log('[RPG Parser] Inventory v1 extracted:', inventoryMatch[1].trim()); + debugLog('[RPG Parser] Inventory v1 extracted:', inventoryMatch[1].trim()); } else { - console.log('[RPG Parser] Inventory v1 not found'); + debugLog('[RPG Parser] Inventory v1 not found'); } } @@ -201,7 +211,7 @@ export function parseUserStats(statsText) { extensionSettings.userStats.conditions = moodMatch[2].trim(); // Conditions } - console.log('[RPG Parser] Final userStats after parsing:', { + debugLog('[RPG Parser] Final userStats after parsing:', { health: extensionSettings.userStats.health, satiety: extensionSettings.userStats.satiety, energy: extensionSettings.userStats.energy, @@ -213,11 +223,13 @@ export function parseUserStats(statsText) { }); saveSettings(); - console.log('[RPG Parser] Settings saved successfully'); - console.log('[RPG Parser] ======================================================='); + debugLog('[RPG Parser] Settings saved successfully'); + debugLog('[RPG Parser] ======================================================='); } catch (error) { console.error('[RPG Companion] Error parsing user stats:', error); console.error('[RPG Companion] Stack trace:', error.stack); + debugLog('[RPG Parser] ERROR:', error.message); + debugLog('[RPG Parser] Stack:', error.stack); } } diff --git a/src/systems/ui/debug.js b/src/systems/ui/debug.js new file mode 100644 index 0000000..ecb7a4d --- /dev/null +++ b/src/systems/ui/debug.js @@ -0,0 +1,156 @@ +/** + * Debug UI Module + * Provides mobile-friendly debug log viewer for troubleshooting parsing issues + */ + +import { extensionSettings, getDebugLogs, clearDebugLogs } from '../../core/state.js'; + +/** + * Creates and injects the debug panel into the page + */ +export function createDebugPanel() { + // Remove existing debug panel if any + $('#rpg-debug-panel').remove(); + $('#rpg-debug-toggle').remove(); + + // Create debug panel HTML + const debugPanelHtml = ` +
+
+

🔍 Debug Logs

+
+ + + +
+
+
+
+ `; + + // Create debug toggle button (FAB-style) + const debugToggleHtml = ` + + `; + + // Append to body + $('body').append(debugPanelHtml); + $('body').append(debugToggleHtml); + + // Set up event handlers + setupDebugEventHandlers(); + + // Initial log render + renderDebugLogs(); +} + +/** + * Sets up event handlers for debug panel + */ +function setupDebugEventHandlers() { + // Toggle button + $('#rpg-debug-toggle').on('click', function() { + $('#rpg-debug-panel').toggleClass('rpg-debug-open'); + renderDebugLogs(); // Refresh logs when opening + }); + + // Close button + $('#rpg-debug-close').on('click', function() { + $('#rpg-debug-panel').removeClass('rpg-debug-open'); + }); + + // Copy button + $('#rpg-debug-copy').on('click', function() { + const logs = getDebugLogs(); + const logsText = logs.map(log => { + let text = `[${log.timestamp}] ${log.message}`; + if (log.data) { + text += `\n${log.data}`; + } + return text; + }).join('\n\n'); + + navigator.clipboard.writeText(logsText).then(() => { + // Show feedback + const $btn = $(this); + const $icon = $btn.find('i'); + $icon.removeClass('fa-copy').addClass('fa-check'); + setTimeout(() => { + $icon.removeClass('fa-check').addClass('fa-copy'); + }, 1500); + }).catch(err => { + console.error('Failed to copy logs:', err); + alert('Failed to copy logs. Please use browser console instead.'); + }); + }); + + // Clear button + $('#rpg-debug-clear').on('click', function() { + if (confirm('Clear all debug logs?')) { + clearDebugLogs(); + renderDebugLogs(); + } + }); +} + +/** + * Renders debug logs to the panel + */ +function renderDebugLogs() { + const logs = getDebugLogs(); + const $logsContainer = $('#rpg-debug-logs'); + + if (logs.length === 0) { + $logsContainer.html('
No logs yet. Logs will appear when parser runs.
'); + return; + } + + // Build logs HTML + const logsHtml = logs.map(log => { + let html = `
`; + html += `[${log.timestamp}] `; + html += `${escapeHtml(log.message)}`; + if (log.data) { + html += `
${escapeHtml(log.data)}
`; + } + html += `
`; + return html; + }).join(''); + + $logsContainer.html(logsHtml); + + // Auto-scroll to bottom + $logsContainer[0].scrollTop = $logsContainer[0].scrollHeight; +} + +/** + * Escapes HTML to prevent XSS + */ +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +/** + * Shows or hides debug UI based on debug mode setting + */ +export function updateDebugUIVisibility() { + if (extensionSettings.debugMode) { + if ($('#rpg-debug-panel').length === 0) { + createDebugPanel(); + } + $('#rpg-debug-toggle').show(); + } else { + $('#rpg-debug-toggle').hide(); + $('#rpg-debug-panel').remove(); + } +} diff --git a/style.css b/style.css index d6b53af..56d1c8a 100644 --- a/style.css +++ b/style.css @@ -4779,3 +4779,174 @@ body:has(.rpg-panel.rpg-position-left) #sheld { min-height: 2rem; } } + +/* =================================================================== + Debug Panel Styles - Mobile-Friendly Debug Log Viewer + =================================================================== */ + +/* Debug toggle button (FAB-style) */ +.rpg-debug-toggle { + display: none; /* Hidden by default, shown when debugMode is enabled */ + align-items: center; + justify-content: center; + position: fixed; + bottom: 20px; + left: 20px; + width: 50px; + height: 50px; + border-radius: 50%; + background: #ff6b6b; + border: 2px solid #c92a2a; + color: white; + font-size: 1.5rem; + cursor: pointer; + z-index: 10003; /* Above everything else */ + box-shadow: 0 4px 12px rgba(255, 107, 107, 0.5); + transition: all 0.3s ease; +} + +.rpg-debug-toggle:hover { + transform: scale(1.1); + box-shadow: 0 6px 16px rgba(255, 107, 107, 0.7); +} + +.rpg-debug-toggle:active { + transform: scale(0.95); +} + +@media (max-width: 1000px) { + .rpg-debug-toggle { + display: flex; /* Show on mobile when debugMode is enabled */ + } +} + +/* Debug panel */ +.rpg-debug-panel { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 60vh; + background: var(--SmartThemeBlurTintColor, #1a1a1a); + border-top: 2px solid var(--SmartThemeBorderColor, #333); + z-index: 10002; + display: flex; + flex-direction: column; + transform: translateY(100%); + transition: transform 0.3s ease; + box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.5); +} + +.rpg-debug-panel.rpg-debug-open { + transform: translateY(0); +} + +.rpg-debug-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid var(--SmartThemeBorderColor, #333); + background: var(--SmartThemeBodyColor, #0d0d0d); +} + +.rpg-debug-header h3 { + margin: 0; + font-size: 1.2rem; + color: var(--rpg-text, #ecf0f1); +} + +.rpg-debug-actions { + display: flex; + gap: 0.5rem; +} + +.rpg-debug-actions button { + background: var(--SmartThemeBlurTintColor, #2a2a2a); + border: 1px solid var(--SmartThemeBorderColor, #444); + color: var(--rpg-text, #ecf0f1); + width: 36px; + height: 36px; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.rpg-debug-actions button:hover { + background: var(--SmartThemeBorderColor, #333); + transform: scale(1.05); +} + +.rpg-debug-actions button:active { + transform: scale(0.95); +} + +.rpg-debug-logs { + flex: 1; + overflow-y: auto; + padding: 1rem; + font-family: 'Courier New', Courier, monospace; + font-size: 0.85rem; + line-height: 1.4; + color: var(--rpg-text, #ecf0f1); +} + +.rpg-debug-empty { + text-align: center; + padding: 2rem; + color: #888; + font-style: italic; +} + +.rpg-debug-entry { + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.rpg-debug-entry:last-child { + border-bottom: none; +} + +.rpg-debug-time { + color: #888; + font-size: 0.75rem; +} + +.rpg-debug-message { + color: #4fc3f7; +} + +.rpg-debug-data { + margin: 0.5rem 0 0 0; + padding: 0.75rem; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + overflow-x: auto; + color: #9ccc65; + font-size: 0.8rem; + white-space: pre-wrap; + word-break: break-word; +} + +/* Desktop view - smaller panel in bottom right */ +@media (min-width: 1001px) { + .rpg-debug-panel { + bottom: 20px; + left: auto; + right: 20px; + width: 600px; + max-width: 90vw; + height: 400px; + border-radius: 12px; + border: 2px solid var(--SmartThemeBorderColor, #333); + } + + .rpg-debug-toggle { + display: flex; /* Show on desktop too when debugMode enabled */ + } +} From cafb72254edec018461faf6c9cbaaa19988b14a2 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Wed, 22 Oct 2025 07:31:21 +1100 Subject: [PATCH 03/15] fix: move debug mode toggle to proper settings location The debug toggle was incorrectly added to settings.html (SillyTavern Extensions tab). It should be in template.html (RPG Companion Settings popup) where all the other extension settings are. Changes: - template.html: Added debug mode checkbox in Display Options section - index.js: Added event listener and initial state setter - settings.html: Removed incorrect debug toggle placement Now users can find the debug toggle by clicking the gear icon in the RPG panel, under Display Options, right below "Show Plot Progression Buttons". --- index.js | 14 +++++++------- settings.html | 6 ------ template.html | 8 ++++++++ 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index f91172b..6bbc55d 100644 --- a/index.js +++ b/index.js @@ -193,13 +193,6 @@ function addExtensionSettings() { updateChatThoughts(); // This will re-create the thought bubble if data exists } }); - - // Set up the debug mode toggle - $('#rpg-debug-mode').prop('checked', extensionSettings.debugMode).on('change', function() { - extensionSettings.debugMode = $(this).prop('checked'); - saveSettings(); - updateDebugUIVisibility(); - }); } /** @@ -310,6 +303,12 @@ async function initUI() { togglePlotButtons(); }); + $('#rpg-toggle-debug-mode').on('change', function() { + extensionSettings.debugMode = $(this).prop('checked'); + saveSettings(); + updateDebugUIVisibility(); + }); + $('#rpg-toggle-animations').on('change', function() { extensionSettings.enableAnimations = $(this).prop('checked'); saveSettings(); @@ -421,6 +420,7 @@ async function initUI() { $('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat); $('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt); $('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons); + $('#rpg-toggle-debug-mode').prop('checked', extensionSettings.debugMode); $('#rpg-toggle-animations').prop('checked', extensionSettings.enableAnimations); $('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow); $('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh); diff --git a/settings.html b/settings.html index 069a9fe..a6d40ea 100644 --- a/settings.html +++ b/settings.html @@ -11,12 +11,6 @@ Toggle to enable/disable the RPG Companion extension. Configure additional settings within the panel itself. - - Enable debug logging visible in UI. Useful for troubleshooting parsing issues on mobile devices. Shows a red bug button to view parser logs. -
Discord diff --git a/template.html b/template.html index 8d24685..89cb99e 100644 --- a/template.html +++ b/template.html @@ -195,6 +195,14 @@ Display buttons above chat input for plot progression prompts + + + + Shows parser logs in a mobile-friendly UI panel. Useful for troubleshooting. Look for the red bug button. +
From 44240e6840ba67a2aa1b3d12b07955398af36a1a Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Wed, 22 Oct 2025 07:33:30 +1100 Subject: [PATCH 04/15] fix: debug panel close button not working Added event.preventDefault() and event.stopPropagation() to close button handler to prevent any interference from parent elements. Also added pointer-events: none to button icons to ensure clicks on the icon register on the button itself, not the element. Changes: - src/systems/ui/debug.js: Added e.preventDefault/stopPropagation to close handler - src/systems/ui/debug.js: Added console.log for debugging - style.css: Added pointer-events: none to .rpg-debug-actions button i --- src/systems/ui/debug.js | 5 ++++- style.css | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/systems/ui/debug.js b/src/systems/ui/debug.js index ecb7a4d..2836f68 100644 --- a/src/systems/ui/debug.js +++ b/src/systems/ui/debug.js @@ -63,7 +63,10 @@ function setupDebugEventHandlers() { }); // Close button - $('#rpg-debug-close').on('click', function() { + $('#rpg-debug-close').on('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + console.log('[RPG Debug] Close button clicked'); $('#rpg-debug-panel').removeClass('rpg-debug-open'); }); diff --git a/style.css b/style.css index 56d1c8a..70872c5 100644 --- a/style.css +++ b/style.css @@ -4884,6 +4884,10 @@ body:has(.rpg-panel.rpg-position-left) #sheld { transform: scale(0.95); } +.rpg-debug-actions button i { + pointer-events: none; /* Prevent icon from blocking clicks */ +} + .rpg-debug-logs { flex: 1; overflow-y: auto; From d4491a47054648b44121c01fa2d08fc6fa9f3016 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Wed, 22 Oct 2025 08:29:58 +1100 Subject: [PATCH 05/15] fix: add debug toggle as draggable mobile FAB button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROBLEM: - Debug logs only accessible via browser console (impractical on mobile) - User (Salixfire) reporting parsing issues but can't debug on mobile device - Need mobile-friendly debug mode for troubleshooting data display issues SOLUTION: Implemented debug toggle FAB button following exact pattern of existing mobile FABs: Files Changed: - src/core/state.js: Added debugFabPosition and debugMode to extensionSettings - src/core/config.js: Added debugFabPosition to defaultSettings (reference) - index.js: Created debug toggle button, imported setupDebugButtonDrag - style.css: Added debug toggle CSS matching mobile FAB pattern (44px, grab cursor, theme colors) - src/systems/ui/mobile.js: Added setupDebugButtonDrag() with drag-to-reposition - src/systems/ui/debug.js: Removed button creation, added just-dragged check, updated visibility control Implementation Details: - Button created in index.js (not debug.js) following mobile FAB pattern - CSS matches mobile toggle/refresh buttons (44px, theme colors, grab cursor, user-select: none) - Drag support with touch/mouse handlers, 200ms/10px threshold - Position saved to extensionSettings.debugFabPosition - Just-dragged flag prevents accidental clicks after drag - Mobile (≤1000px): slide from right with rpg-mobile-open/closing classes - Desktop (>1000px): slide from bottom with rpg-debug-open class - Event delegation for reliable click handling - Default position: bottom 140px, left 20px (below other FABs) Bug Fix: - Initial implementation had debugFabPosition only in config.js - extensionSettings uses state.js as source, not config.js - Without debugFabPosition in state.js, button had no position and was invisible - Now properly initialized in both files The debug button is hidden by default (debugMode: false) and shown when user enables debug mode in RPG Companion settings. This allows Salixfire to view parser logs on mobile and troubleshoot the data display issues. --- index.js | 12 ++- src/core/config.js | 4 + src/core/state.js | 7 +- src/systems/ui/debug.js | 101 ++++++++++++++---- src/systems/ui/mobile.js | 215 +++++++++++++++++++++++++++++++++++++++ style.css | 109 +++++++++++++++----- 6 files changed, 402 insertions(+), 46 deletions(-) diff --git a/index.js b/index.js index 6bbc55d..5d276fd 100644 --- a/index.js +++ b/index.js @@ -99,7 +99,8 @@ import { removeMobileTabs, setupMobileKeyboardHandling, setupContentEditableScrolling, - setupRefreshButtonDrag + setupRefreshButtonDrag, + setupDebugButtonDrag } from './src/systems/ui/mobile.js'; import { setupDesktopTabs, @@ -221,6 +222,14 @@ async function initUI() { `; $('body').append(mobileRefreshHtml); + // Add debug toggle FAB button (same pattern as other mobile FABs) + const debugToggleHtml = ` + + `; + $('body').append(debugToggleHtml); + // Cache UI elements using state setters setPanelContainer($('#rpg-companion-panel')); setUserStatsContainer($('#rpg-user-stats')); @@ -464,6 +473,7 @@ async function initUI() { setupMobileKeyboardHandling(); setupContentEditableScrolling(); setupRefreshButtonDrag(); + setupDebugButtonDrag(); initInventoryEventListeners(); // Initialize debug UI if debug mode is enabled diff --git a/src/core/config.js b/src/core/config.js index 82ad9c7..4287b6e 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -53,6 +53,10 @@ export const defaultSettings = { bottom: '80px', right: '20px' }, // Saved position for mobile refresh button + debugFabPosition: { + bottom: '140px', + left: '20px' + }, // Saved position for debug FAB button userStats: { health: 100, satiety: 100, diff --git a/src/core/state.js b/src/core/state.js index 6cff0d4..3442c6a 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -41,6 +41,10 @@ export let extensionSettings = { bottom: '80px', right: '20px' }, // Saved position for mobile refresh button + debugFabPosition: { + bottom: '140px', + left: '20px' + }, // Saved position for debug FAB button userStats: { health: 100, satiety: 100, @@ -72,7 +76,8 @@ export let extensionSettings = { onPerson: 'list', // 'list' or 'grid' view mode for On Person section stored: 'list', // 'list' or 'grid' view mode for Stored section assets: 'list' // 'list' or 'grid' view mode for Assets section - } + }, + debugMode: false // Enable debug logging visible in UI (for mobile debugging) }; /** diff --git a/src/systems/ui/debug.js b/src/systems/ui/debug.js index 2836f68..c6030b0 100644 --- a/src/systems/ui/debug.js +++ b/src/systems/ui/debug.js @@ -7,11 +7,11 @@ import { extensionSettings, getDebugLogs, clearDebugLogs } from '../../core/stat /** * Creates and injects the debug panel into the page + * Note: Debug toggle button is created in index.js, not here */ export function createDebugPanel() { // Remove existing debug panel if any $('#rpg-debug-panel').remove(); - $('#rpg-debug-toggle').remove(); // Create debug panel HTML const debugPanelHtml = ` @@ -34,16 +34,8 @@ export function createDebugPanel() {
`; - // Create debug toggle button (FAB-style) - const debugToggleHtml = ` - - `; - // Append to body $('body').append(debugPanelHtml); - $('body').append(debugToggleHtml); // Set up event handlers setupDebugEventHandlers(); @@ -53,25 +45,85 @@ export function createDebugPanel() { } /** - * Sets up event handlers for debug panel + * Closes the debug panel with proper animation (mobile or desktop) + */ +function closeDebugPanel() { + const $panel = $('#rpg-debug-panel'); + const isMobile = window.innerWidth <= 1000; + + if (isMobile) { + // Mobile: animate slide-out to right + $panel.removeClass('rpg-mobile-open').addClass('rpg-mobile-closing'); + + // Wait for animation to complete before hiding + $panel.one('animationend', function() { + $panel.removeClass('rpg-mobile-closing'); + $('.rpg-mobile-overlay').remove(); + }); + } else { + // Desktop: simple slide-down + $panel.removeClass('rpg-debug-open'); + } +} + +/** + * Sets up event handlers for debug panel using event delegation for mobile compatibility */ function setupDebugEventHandlers() { + // Use event delegation for better mobile compatibility and reliability with dynamic elements + // Remove any existing handlers first to prevent duplicates + $(document).off('click.rpgDebug'); + // Toggle button - $('#rpg-debug-toggle').on('click', function() { - $('#rpg-debug-panel').toggleClass('rpg-debug-open'); - renderDebugLogs(); // Refresh logs when opening + $(document).on('click.rpgDebug', '#rpg-debug-toggle', function() { + const $debugToggle = $(this); + + // Skip if we just finished dragging + if ($debugToggle.data('just-dragged')) { + console.log('[RPG Debug] Click blocked - just finished dragging'); + return; + } + + const $panel = $('#rpg-debug-panel'); + const isMobile = window.innerWidth <= 1000; + + if (isMobile) { + // Mobile: use rpg-mobile-open class with slide-from-right animation + const isOpen = $panel.hasClass('rpg-mobile-open'); + + if (isOpen) { + // Close with animation + closeDebugPanel(); + } else { + // Open with animation + $panel.addClass('rpg-mobile-open'); + renderDebugLogs(); + + // Create overlay for mobile + const $overlay = $('
'); + $('body').append($overlay); + + // Close when clicking overlay + $overlay.on('click', function() { + closeDebugPanel(); + }); + } + } else { + // Desktop: use rpg-debug-open class with slide-from-bottom animation + $panel.toggleClass('rpg-debug-open'); + renderDebugLogs(); + } }); // Close button - $('#rpg-debug-close').on('click', function(e) { + $(document).on('click.rpgDebug', '#rpg-debug-close', function(e) { e.preventDefault(); e.stopPropagation(); - console.log('[RPG Debug] Close button clicked'); - $('#rpg-debug-panel').removeClass('rpg-debug-open'); + closeDebugPanel(); }); // Copy button - $('#rpg-debug-copy').on('click', function() { + $(document).on('click.rpgDebug', '#rpg-debug-copy', function() { const logs = getDebugLogs(); const logsText = logs.map(log => { let text = `[${log.timestamp}] ${log.message}`; @@ -96,7 +148,7 @@ function setupDebugEventHandlers() { }); // Clear button - $('#rpg-debug-clear').on('click', function() { + $(document).on('click.rpgDebug', '#rpg-debug-clear', function() { if (confirm('Clear all debug logs?')) { clearDebugLogs(); renderDebugLogs(); @@ -145,15 +197,24 @@ function escapeHtml(text) { /** * Shows or hides debug UI based on debug mode setting + * Note: Debug toggle button always exists in DOM (created in index.js) */ export function updateDebugUIVisibility() { + const $debugToggle = $('#rpg-debug-toggle'); + if (extensionSettings.debugMode) { + // Show debug toggle button + $debugToggle.css('display', 'flex'); + + // Create debug panel if it doesn't exist if ($('#rpg-debug-panel').length === 0) { createDebugPanel(); } - $('#rpg-debug-toggle').show(); } else { - $('#rpg-debug-toggle').hide(); + // Hide debug toggle button + $debugToggle.css('display', 'none'); + + // Remove debug panel $('#rpg-debug-panel').remove(); } } diff --git a/src/systems/ui/mobile.js b/src/systems/ui/mobile.js index b3f7b95..d29d758 100644 --- a/src/systems/ui/mobile.js +++ b/src/systems/ui/mobile.js @@ -947,3 +947,218 @@ export function setupRefreshButtonDrag() { isDragging = false; }); } + +/** + * Sets up drag functionality for the debug toggle FAB button + * Same pattern as refresh button drag + */ +export function setupDebugButtonDrag() { + const $debugBtn = $('#rpg-debug-toggle'); + + if ($debugBtn.length === 0) { + console.warn('[RPG Mobile] Debug button not found in DOM'); + return; + } + + console.log('[RPG Mobile] setupDebugButtonDrag called'); + + // Load and apply saved position + if (extensionSettings.debugFabPosition) { + const pos = extensionSettings.debugFabPosition; + console.log('[RPG Mobile] Loading saved debug button position:', pos); + + // Apply saved position + if (pos.top) $debugBtn.css('top', pos.top); + if (pos.right) $debugBtn.css('right', pos.right); + if (pos.bottom) $debugBtn.css('bottom', pos.bottom); + if (pos.left) $debugBtn.css('left', pos.left); + + // Constrain to viewport after position is applied + requestAnimationFrame(() => constrainFabToViewport($debugBtn)); + } + + // Touch/drag state + let isDragging = false; + let touchStartTime = 0; + let touchStartX = 0; + let touchStartY = 0; + let buttonStartX = 0; + let buttonStartY = 0; + const LONG_PRESS_DURATION = 200; + const MOVE_THRESHOLD = 10; + let rafId = null; + let pendingX = null; + let pendingY = null; + + // Update position using requestAnimationFrame + function updatePosition() { + if (pendingX !== null && pendingY !== null) { + $debugBtn.css({ + left: pendingX + 'px', + top: pendingY + 'px', + right: 'auto', + bottom: 'auto' + }); + pendingX = null; + pendingY = null; + } + rafId = null; + } + + // Touch start + $debugBtn.on('touchstart', function(e) { + const touch = e.originalEvent.touches[0]; + touchStartTime = Date.now(); + touchStartX = touch.clientX; + touchStartY = touch.clientY; + + const offset = $debugBtn.offset(); + buttonStartX = offset.left; + buttonStartY = offset.top; + + isDragging = false; + }); + + // Touch move + $debugBtn.on('touchmove', function(e) { + const touch = e.originalEvent.touches[0]; + const deltaX = touch.clientX - touchStartX; + const deltaY = touch.clientY - touchStartY; + const timeSinceStart = Date.now() - touchStartTime; + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + + if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { + isDragging = true; + $debugBtn.addClass('dragging'); + } + + if (isDragging) { + e.preventDefault(); + + let newX = buttonStartX + deltaX; + let newY = buttonStartY + deltaY; + + const buttonWidth = $debugBtn.outerWidth(); + const buttonHeight = $debugBtn.outerHeight(); + + const minX = 10; + const maxX = window.innerWidth - buttonWidth - 10; + const minY = 10; + const maxY = window.innerHeight - buttonHeight - 10; + + newX = Math.max(minX, Math.min(maxX, newX)); + newY = Math.max(minY, Math.min(maxY, newY)); + + pendingX = newX; + pendingY = newY; + if (!rafId) { + rafId = requestAnimationFrame(updatePosition); + } + } + }); + + // Touch end + $debugBtn.on('touchend', function(e) { + if (isDragging) { + // Save new position + const offset = $debugBtn.offset(); + const newPosition = { + left: offset.left + 'px', + top: offset.top + 'px' + }; + + extensionSettings.debugFabPosition = newPosition; + saveSettings(); + + setTimeout(() => { + $debugBtn.removeClass('dragging'); + }, 50); + + // Set flag to prevent click handler from firing + $debugBtn.data('just-dragged', true); + setTimeout(() => { + $debugBtn.data('just-dragged', false); + }, 100); + + isDragging = false; + } + }); + + // Mouse support for desktop + let mouseDown = false; + + $debugBtn.on('mousedown', function(e) { + e.preventDefault(); + touchStartTime = Date.now(); + touchStartX = e.clientX; + touchStartY = e.clientY; + + const offset = $debugBtn.offset(); + buttonStartX = offset.left; + buttonStartY = offset.top; + + mouseDown = true; + isDragging = false; + }); + + $(document).on('mousemove.rpgDebugDrag', function(e) { + if (!mouseDown) return; + + const deltaX = e.clientX - touchStartX; + const deltaY = e.clientY - touchStartY; + const timeSinceStart = Date.now() - touchStartTime; + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + + if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) { + isDragging = true; + $debugBtn.addClass('dragging'); + } + + if (isDragging) { + let newX = buttonStartX + deltaX; + let newY = buttonStartY + deltaY; + + const buttonWidth = $debugBtn.outerWidth(); + const buttonHeight = $debugBtn.outerHeight(); + + const minX = 10; + const maxX = window.innerWidth - buttonWidth - 10; + const minY = 10; + const maxY = window.innerHeight - buttonHeight - 10; + + newX = Math.max(minX, Math.min(maxX, newX)); + newY = Math.max(minY, Math.min(maxY, newY)); + + pendingX = newX; + pendingY = newY; + if (!rafId) { + rafId = requestAnimationFrame(updatePosition); + } + } + }); + + $(document).on('mouseup.rpgDebugDrag', function(e) { + if (mouseDown && isDragging) { + const offset = $debugBtn.offset(); + const newPosition = { + left: offset.left + 'px', + top: offset.top + 'px' + }; + + extensionSettings.debugFabPosition = newPosition; + saveSettings(); + + setTimeout(() => { + $debugBtn.removeClass('dragging'); + }, 50); + + $debugBtn.data('just-dragged', true); + setTimeout(() => { + $debugBtn.data('just-dragged', false); + }, 100); + } + + mouseDown = false; + isDragging = false; + }); +} diff --git a/style.css b/style.css index 70872c5..e947926 100644 --- a/style.css +++ b/style.css @@ -4784,42 +4784,46 @@ body:has(.rpg-panel.rpg-position-left) #sheld { Debug Panel Styles - Mobile-Friendly Debug Log Viewer =================================================================== */ -/* Debug toggle button (FAB-style) */ +/* ============================================ + DEBUG TOGGLE FAB BUTTON (Same pattern as mobile FABs) + ============================================ */ .rpg-debug-toggle { display: none; /* Hidden by default, shown when debugMode is enabled */ align-items: center; justify-content: center; position: fixed; - bottom: 20px; - left: 20px; - width: 50px; - height: 50px; + /* Position set by JavaScript based on saved settings */ + width: 44px; + height: 44px; border-radius: 50%; - background: #ff6b6b; - border: 2px solid #c92a2a; - color: white; - font-size: 1.5rem; - cursor: pointer; - z-index: 10003; /* Above everything else */ - box-shadow: 0 4px 12px rgba(255, 107, 107, 0.5); - transition: all 0.3s ease; + background: var(--SmartThemeBlurTintColor); + border: 2px solid var(--SmartThemeBorderColor); + color: var(--rpg-text, #ecf0f1); + font-size: 1.85vw; + cursor: grab; + z-index: 1000; /* Below refresh (1001) and mobile toggle (10002) */ + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + transition: opacity 0.3s ease, transform 0.2s ease, top 0.3s ease, left 0.3s ease, right 0.3s ease, bottom 0.3s ease; + user-select: none; + -webkit-user-select: none; + will-change: top, left; +} + +/* Disable transitions while actively dragging */ +.rpg-debug-toggle.dragging { + transition: none; + cursor: grabbing; } .rpg-debug-toggle:hover { transform: scale(1.1); - box-shadow: 0 6px 16px rgba(255, 107, 107, 0.7); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); } .rpg-debug-toggle:active { transform: scale(0.95); } -@media (max-width: 1000px) { - .rpg-debug-toggle { - display: flex; /* Show on mobile when debugMode is enabled */ - } -} - /* Debug panel */ .rpg-debug-panel { position: fixed; @@ -4937,6 +4941,67 @@ body:has(.rpg-panel.rpg-position-left) #sheld { word-break: break-word; } +/* Mobile view - slide from right like main panel */ +@media (max-width: 1000px) { + .rpg-debug-panel { + /* Reset bottom slide positioning */ + transform: none; + transition: none; + bottom: auto; + + /* Mobile panel - slide from right */ + position: fixed !important; + top: var(--topBarBlockSize) !important; + right: 0 !important; + left: auto !important; + + /* Mobile sizing using dynamic viewport units */ + width: 85dvw !important; + max-width: 400px !important; + height: calc(100dvh - var(--topBarBlockSize)) !important; + + /* Hidden by default */ + display: none !important; + + /* Mobile scrolling */ + overflow-y: auto !important; + -webkit-overflow-scrolling: touch; + + /* Styling */ + border-radius: 20px 0 0 0; + border-left: 1px solid var(--SmartThemeBorderColor); + border-top: 1px solid var(--SmartThemeBorderColor); + border-bottom: none; + backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2)); + box-shadow: -5px 0 20px rgba(0, 0, 0, 0.5); + } + + /* Show panel when opened with slide-in animation */ + .rpg-debug-panel.rpg-mobile-open { + display: flex !important; + z-index: 10002; + animation: rpgSlideInFromRight 0.3s ease-in-out; + } + + /* Closing animation - slide out to right */ + .rpg-debug-panel.rpg-mobile-closing { + display: flex !important; + z-index: 10002; + animation: rpgSlideOutToRight 0.3s ease-in-out; + } + + /* Debug logs container needs to stay scrollable */ + .rpg-debug-logs { + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + + /* Debug toggle button on mobile */ + .rpg-debug-toggle { + font-size: clamp(20px, 5.1vw, 26px) !important; + } +} + /* Desktop view - smaller panel in bottom right */ @media (min-width: 1001px) { .rpg-debug-panel { @@ -4949,8 +5014,4 @@ body:has(.rpg-panel.rpg-position-left) #sheld { border-radius: 12px; border: 2px solid var(--SmartThemeBorderColor, #333); } - - .rpg-debug-toggle { - display: flex; /* Show on desktop too when debugMode enabled */ - } } From 37878fc6f0015b88cb095cdc76b7741be96f12f1 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Wed, 22 Oct 2025 08:31:22 +1100 Subject: [PATCH 06/15] fix: add debug mode toggle to settings panel Add debug mode checkbox to RPG Companion Settings popup (Display Options section). Users can enable debug mode to show the mobile-friendly debug panel with parser logs. This was missing from the previous commit. --- template.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template.html b/template.html index 89cb99e..b5c9247 100644 --- a/template.html +++ b/template.html @@ -198,7 +198,7 @@ Shows parser logs in a mobile-friendly UI panel. Useful for troubleshooting. Look for the red bug button. From fb14c951ac7cacb35b0e6902041bff4417d1120e Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Wed, 22 Oct 2025 08:45:28 +1100 Subject: [PATCH 07/15] fix: add debug logging and fix Present Characters rendering PROBLEM (reported by Salixfire): - Present Characters panel showing placeholder instead of actual characters - Thought bubbles work correctly but main panel doesn't - Need to toggle settings off/on to get thoughts to appear - No way to debug on mobile devices CHANGES: 1. Added comprehensive debug logging to renderThoughts() (src/systems/rendering/thoughts.js): - Log when function is called and with what data - Log each line being parsed and how many parts it has - Log character extraction (emoji, name, traits, relationship, thoughts) - Log why characters are accepted or rejected - Log final character count and whether showing placeholder - All logs visible in mobile-friendly debug panel 2. Fixed toggle to refresh content (index.js:283-291): - When user toggles "Show Present Characters" on, now calls renderThoughts() - Previously only showed/hid container without refreshing content - This ensures panel displays latest data when toggled 3. Normalized parsing logic (src/systems/rendering/thoughts.js:111): - Changed renderThoughts() to require >= 3 parts (was >= 2) - Now matches updateChatThoughts() requirement - Consistent with current prompt format: Emoji:Name | Relationship | Thoughts - Removed 2-part format fallback code (unreachable now) - Both functions now use same validation rules EXPECTED OUTCOME: - User can enable debug mode and see exactly what data is being parsed - Toggle will properly refresh the panel content - We can diagnose from debug logs why placeholder is shown - More consistent behavior between main panel and thought bubbles Debug logs will help us identify: - If characterThoughts data is empty/malformed when renderThoughts() is called - If parsing is rejecting valid character data - If there's a timing issue with data availability - What the actual AI response format looks like Related to previous commit (37878fc) that added debug mode toggle. --- index.js | 4 ++ src/systems/rendering/thoughts.js | 67 ++++++++++++++++++++++++------- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/index.js b/index.js index 5d276fd..dd0e699 100644 --- a/index.js +++ b/index.js @@ -284,6 +284,10 @@ async function initUI() { extensionSettings.showCharacterThoughts = $(this).prop('checked'); saveSettings(); updateSectionVisibility(); + // Refresh the content when toggling on/off + if (extensionSettings.showCharacterThoughts) { + renderThoughts(); + } }); $('#rpg-toggle-inventory').on('change', function() { diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index 7edb78f..af42aa6 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -11,11 +11,22 @@ import { lastGeneratedData, committedTrackerData, $thoughtsContainer, - FALLBACK_AVATAR_DATA_URI + FALLBACK_AVATAR_DATA_URI, + addDebugLog } from '../../core/state.js'; import { saveChatData } from '../../core/persistence.js'; import { getSafeThumbnailUrl } from '../../utils/avatars.js'; +/** + * Helper to log to both console and debug logs array + */ +function debugLog(message, data = null) { + console.log(message, data || ''); + if (extensionSettings.debugMode) { + addDebugLog(message, data); + } +} + /** * Fuzzy name matching that handles: * - Exact matches: "Sabrina" === "Sabrina" @@ -54,6 +65,10 @@ export function renderThoughts() { return; } + debugLog('[RPG Thoughts] ==================== RENDERING PRESENT CHARACTERS ===================='); + debugLog('[RPG Thoughts] showCharacterThoughts setting:', extensionSettings.showCharacterThoughts); + debugLog('[RPG Thoughts] Container exists:', !!$thoughtsContainer); + // Add updating class for animation if (extensionSettings.enableAnimations) { $thoughtsContainer.addClass('rpg-content-updating'); @@ -64,25 +79,36 @@ export function renderThoughts() { lastGeneratedData.characterThoughts = ''; } + debugLog('[RPG Thoughts] Raw characterThoughts data:', lastGeneratedData.characterThoughts); + debugLog('[RPG Thoughts] Data length:', lastGeneratedData.characterThoughts.length + ' chars'); + const lines = lastGeneratedData.characterThoughts.split('\n'); const presentCharacters = []; - // console.log('[RPG Companion] Raw Present Characters:', lastGeneratedData.characterThoughts); - // console.log('[RPG Companion] Split into lines:', lines); + debugLog('[RPG Thoughts] Split into lines count:', lines.length); + debugLog('[RPG Thoughts] Lines:', lines); // Parse format: [Emoji]: [Name, Status, Demeanor] | [Relationship] | [Thoughts] // Also supports 4-part format: [Emoji]: [Name, Status] | [Demeanor] | [Relationship] | [Thoughts] + let lineNumber = 0; for (const line of lines) { + lineNumber++; + // Skip empty lines, headers, dividers, and code fences if (line.trim() && !line.includes('Present Characters') && !line.includes('---') && !line.trim().startsWith('```')) { + debugLog(`[RPG Thoughts] Processing line ${lineNumber}:`, line); + // Match the new format with pipes const parts = line.split('|').map(p => p.trim()); + debugLog(`[RPG Thoughts] Split into ${parts.length} parts:`, parts); - if (parts.length >= 2) { + // Require at least 3 parts (Emoji:Name | Relationship | Thoughts) + // This matches updateChatThoughts() and the current prompt format + if (parts.length >= 3) { // First part: [Emoji]: [Name, Status, Demeanor] const firstPart = parts[0].trim(); const emojiMatch = firstPart.match(/^(.+?):\s*(.+)$/); @@ -91,6 +117,8 @@ export function renderThoughts() { const emoji = emojiMatch[1].trim(); const info = emojiMatch[2].trim(); + debugLog(`[RPG Thoughts] Emoji match found - emoji: "${emoji}", info: "${info}"`); + // Handle both 3-part and 4-part formats let relationship, thoughts, traits; @@ -100,7 +128,8 @@ export function renderThoughts() { thoughts = parts[2].trim(); const infoParts = info.split(',').map(p => p.trim()); traits = infoParts.slice(1).join(', '); - } else if (parts.length >= 4) { + debugLog('[RPG Thoughts] Parsed as 3-part format'); + } else { // 4-part format: Emoji:Name,traits | Demeanor | Relationship | Thoughts // Add the demeanor to traits and use last two parts for relationship/thoughts const demeanor = parts[1].trim(); @@ -109,23 +138,26 @@ export function renderThoughts() { const infoParts = info.split(',').map(p => p.trim()); const baseTraits = infoParts.slice(1).join(', '); traits = baseTraits ? `${baseTraits}, ${demeanor}` : demeanor; - } else { - // Fallback for 2-part format - relationship = parts[1].trim(); - thoughts = ''; - const infoParts = info.split(',').map(p => p.trim()); - traits = infoParts.slice(1).join(', '); + debugLog('[RPG Thoughts] Parsed as 4-part format'); } // Parse name from info (first part before comma) const infoParts = info.split(',').map(p => p.trim()); const name = infoParts[0] || ''; + debugLog(`[RPG Thoughts] Extracted - name: "${name}", traits: "${traits}", relationship: "${relationship}", thoughts: "${thoughts}"`); + if (name && name.toLowerCase() !== 'unavailable') { presentCharacters.push({ emoji, name, traits, relationship, thoughts }); - // console.log('[RPG Companion] Parsed character:', { name, relationship, thoughts }); + debugLog(`[RPG Thoughts] ✓ Added character: ${name}`); + } else { + debugLog(`[RPG Thoughts] ✗ Rejected character - name: "${name}" (unavailable or empty)`); } + } else { + debugLog('[RPG Thoughts] ✗ No emoji match found in first part'); } + } else { + debugLog(`[RPG Thoughts] ✗ Not enough parts (${parts.length} < 3, need at least Emoji:Name | Relationship | Thoughts)`); } } } @@ -138,14 +170,16 @@ export function renderThoughts() { 'Lover': '❤️' }; + debugLog('[RPG Thoughts] ==================== PARSING COMPLETE ===================='); + debugLog('[RPG Thoughts] Total characters parsed:', presentCharacters.length); + debugLog('[RPG Thoughts] Characters array:', presentCharacters); + // Build HTML let html = ''; - // console.log('[RPG Companion] Total characters parsed:', presentCharacters.length); - // console.log('[RPG Companion] Characters array:', presentCharacters); - // If no characters parsed, show a placeholder editable card if (presentCharacters.length === 0) { + debugLog('[RPG Thoughts] ⚠ No characters parsed - showing placeholder card'); // Get default character portrait (try to use the current character if in 1-on-1 chat) // Use a base64-encoded SVG placeholder as fallback to avoid 400 errors let defaultPortrait = FALLBACK_AVATAR_DATA_URI; @@ -249,6 +283,9 @@ export function renderThoughts() { $thoughtsContainer.html(html); + debugLog('[RPG Thoughts] ✓ HTML rendered to container'); + debugLog('[RPG Thoughts] ======================================================='); + // Add event handlers for editable character fields $thoughtsContainer.find('.rpg-editable').on('blur', function() { const character = $(this).data('character'); From 4b37d9965a7ded3f4b8c370510b6de597352e275 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Wed, 22 Oct 2025 09:14:38 +1100 Subject: [PATCH 08/15] fix: move FAB buttons to top-left to prevent off-screen rendering PROBLEM (reported by user testing on Xiaomi Redmi 11 Pro 5G): - FAB buttons (mobile toggle, refresh, debug) rendering off-screen - Users need to scroll right to find buttons and drag them back - Debug button invisible on some devices (never seen on Xiaomi) - Issue occurs on devices with different viewport handling (MIUI Chrome) ROOT CAUSE: - Default positions scattered (right side, bottom side) - Right-side positioning: buttons pushed off-screen on some devices - Bottom positioning: buttons below fold when browser UI visible - Fixed pixel values don't account for different screen sizes/viewports SOLUTION: Changed all FAB default positions to top-left stacked layout: 1. Mobile toggle FAB: - WAS: top + 60px, right: 12px (TOP-RIGHT) - NOW: top + 20px, left: 12px (TOP-LEFT) 2. Refresh button: - WAS: bottom: 80px, right: 20px (BOTTOM-RIGHT) - NOW: top + 80px, left: 12px (BELOW toggle) 3. Debug button: - WAS: bottom: 140px, left: 20px (BOTTOM-LEFT) - NOW: top + 140px, left: 12px (BELOW refresh) BENEFITS: - All buttons stacked vertically on LEFT side (always visible) - Positioned safely below SillyTavern top bar - 60px spacing between buttons (44px button + 16px gap) - No scrolling needed to find buttons on first load - calc(var(--topBarBlockSize) + Npx) accounts for dynamic top bar - Users can still drag to preferred positions (saved per user) NOTE: Only affects NEW users or users who clear their settings. Existing users with saved FAB positions will not be affected. Files changed: - src/core/state.js: Default extensionSettings positions - src/core/config.js: Reference default positions --- src/core/config.js | 18 +++++++++--------- src/core/state.js | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/core/config.js b/src/core/config.js index 4287b6e..8e5b1ba 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -46,17 +46,17 @@ export const defaultSettings = { statBarColorHigh: '#33cc66', // Color for high stat values (green) enableAnimations: true, // Enable smooth animations for stats and content updates mobileFabPosition: { - top: 'calc(var(--topBarBlockSize) + 60px)', - right: '12px' - }, // Saved position for mobile FAB button + top: 'calc(var(--topBarBlockSize) + 20px)', + left: '12px' + }, // Saved position for mobile FAB button (top-left, stacked vertically) mobileRefreshPosition: { - bottom: '80px', - right: '20px' - }, // Saved position for mobile refresh button + top: 'calc(var(--topBarBlockSize) + 80px)', + left: '12px' + }, // Saved position for mobile refresh button (below toggle button) debugFabPosition: { - bottom: '140px', - left: '20px' - }, // Saved position for debug FAB button + top: 'calc(var(--topBarBlockSize) + 140px)', + left: '12px' + }, // Saved position for debug FAB button (below refresh button) userStats: { health: 100, satiety: 100, diff --git a/src/core/state.js b/src/core/state.js index 3442c6a..23d0b18 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -34,17 +34,17 @@ export let extensionSettings = { statBarColorHigh: '#33cc66', // Color for high stat values (green) enableAnimations: true, // Enable smooth animations for stats and content updates mobileFabPosition: { - top: 'calc(var(--topBarBlockSize) + 60px)', - right: '12px' - }, // Saved position for mobile FAB button + top: 'calc(var(--topBarBlockSize) + 20px)', + left: '12px' + }, // Saved position for mobile FAB button (top-left, stacked vertically) mobileRefreshPosition: { - bottom: '80px', - right: '20px' - }, // Saved position for mobile refresh button + top: 'calc(var(--topBarBlockSize) + 80px)', + left: '12px' + }, // Saved position for mobile refresh button (below toggle button) debugFabPosition: { - bottom: '140px', - left: '20px' - }, // Saved position for debug FAB button + top: 'calc(var(--topBarBlockSize) + 140px)', + left: '12px' + }, // Saved position for debug FAB button (below refresh button) userStats: { health: 100, satiety: 100, From f7d8597f2489bf9cfb04499f2f15c69c897cc06d Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Wed, 22 Oct 2025 09:22:12 +1100 Subject: [PATCH 09/15] feat: add reset button positions to settings PROBLEM: - Existing users with saved off-screen FAB positions can't see buttons - No way to reset positions without clearing all extension settings - Salixfire and other users on Xiaomi/other devices need safe positions SOLUTION: Added "Reset Button Positions" button in Advanced settings section IMPLEMENTATION: 1. template.html (lines 241-249): - Added reset button in Advanced section after Clear Cache button - Blue-styled button with rotate icon - Help text explains it resets FAB positions to top-left 2. index.js (lines 361-390): - Added click handler for reset button - Resets all 3 FAB positions to safe top-left defaults: * Mobile toggle: top + 20px, left: 12px * Refresh: top + 80px, left: 12px * Debug: top + 140px, left: 12px - Saves settings immediately - Applies CSS positions to visible buttons (no page refresh needed) - Shows success toast notification 3. style.css (lines 2057-2083, 4123-4125): - Added .rpg-btn-reset-fab styles matching clear cache pattern - Blue color scheme (vs red for destructive clear cache) - Same sizing, padding, transitions as other buttons - Mobile responsive font-size with clamp() USAGE: Users experiencing off-screen buttons can now: 1. Open RPG Companion settings (gear icon) 2. Scroll to Advanced section 3. Click "Reset Button Positions" 4. All FAB buttons instantly move to safe top-left positions This fixes the issue for Salixfire and any other users who: - Have buttons saved in off-screen positions - Can't scroll to find buttons - Need to reset without clearing all settings Works immediately without page refresh or extension reload. --- index.js | 31 +++++++++++++++++++++++++++++++ style.css | 32 ++++++++++++++++++++++++++++++++ template.html | 10 ++++++++++ 3 files changed, 73 insertions(+) diff --git a/index.js b/index.js index dd0e699..b5ae314 100644 --- a/index.js +++ b/index.js @@ -358,6 +358,37 @@ async function initUI() { } }); + // Reset FAB positions button + $('#rpg-reset-fab-positions').on('click', function() { + console.log('[RPG Companion] Resetting FAB positions to defaults'); + + // Reset to defaults (top-left stacked) + extensionSettings.mobileFabPosition = { + top: 'calc(var(--topBarBlockSize) + 20px)', + left: '12px' + }; + extensionSettings.mobileRefreshPosition = { + top: 'calc(var(--topBarBlockSize) + 80px)', + left: '12px' + }; + extensionSettings.debugFabPosition = { + top: 'calc(var(--topBarBlockSize) + 140px)', + left: '12px' + }; + + // Save settings + saveSettings(); + + // Apply positions immediately to visible buttons + $('#rpg-mobile-toggle').css(extensionSettings.mobileFabPosition); + $('#rpg-manual-update-mobile').css(extensionSettings.mobileRefreshPosition); + $('#rpg-debug-toggle').css(extensionSettings.debugFabPosition); + + // Show success feedback + toastr.success('Button positions reset to defaults', 'RPG Companion'); + console.log('[RPG Companion] FAB positions reset successfully'); + }); + $('#rpg-stat-bar-color-low').on('change', function() { extensionSettings.statBarColorLow = String($(this).val()); saveSettings(); diff --git a/style.css b/style.css index e947926..a03ec60 100644 --- a/style.css +++ b/style.css @@ -2054,6 +2054,34 @@ body:has(.rpg-panel.rpg-position-left) #sheld { transform: translateY(0); } +.rpg-btn-reset-fab { + width: 100%; + padding: 0.625em; + background: rgba(52, 152, 219, 0.2); + border: 2px solid rgba(52, 152, 219, 0.5); + border-radius: 0.5em; + color: #5dade2; + font-size: 1.2vw; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5em; + transition: all 0.3s ease; +} + +.rpg-btn-reset-fab:hover { + background: rgba(52, 152, 219, 0.3); + border-color: rgba(52, 152, 219, 0.8); + color: #85c1e9; + transform: translateY(-0.062rem); +} + +.rpg-btn-reset-fab:active { + transform: translateY(0); +} + /* ============================================ THEME VARIATIONS ============================================ */ @@ -4091,6 +4119,10 @@ body:has(.rpg-panel.rpg-position-left) #sheld { .rpg-btn-clear-cache { font-size: clamp(13px, 3.3vw, 17px) !important; } + + .rpg-btn-reset-fab { + font-size: clamp(13px, 3.3vw, 17px) !important; + } } /* Very narrow screens - single column layout for all stats */ diff --git a/template.html b/template.html index b5c9247..b72fb5e 100644 --- a/template.html +++ b/template.html @@ -237,6 +237,16 @@ Clear Extension Cache
+ + +
+ + + Resets all floating action buttons (toggle, refresh, debug) to default top-left positions. Useful if buttons are off-screen. + +
From 88ba0a76abc133a0c15aa5eba13fac42b4b0b131 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Wed, 22 Oct 2025 09:50:59 +1100 Subject: [PATCH 10/15] fix: add comprehensive error handling to Present Characters HTML building MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROBLEM (from Salixfire's debug logs): - Parser successfully extracted 5 characters - Log showed complete characters array - Log stopped abruptly before "✓ HTML rendered to container" - This indicates exception thrown during HTML building (lines 217-281) DIAGNOSIS: - Parsing works perfectly (5 characters extracted) - Code crashes somewhere in the HTML building loop - User sees placeholder because exception prevents HTML from rendering - No error logs because crash happens silently LIKELY CAUSES: - getGroupMembers() throwing exception - Character avatar lookup failing - getSafeThumbnailUrl() failing - Missing null checks SOLUTION: Added comprehensive error handling and debug logging: 1. Added logging before HTML building starts - "Starting HTML generation for N characters" - This confirms code reaches HTML building phase 2. Wrapped each character in try-catch - Logs each character being processed: "Building HTML for character 1/5: Lady Julia" - Prevents one character error from crashing entire function - Code continues with other characters even if one fails 3. Added detailed avatar lookup logging: - "Looking up avatar for: {name}" - "In group chat, checking group members..." - "Group members count: N" - "Found avatar in group members/all characters/current character" - Shows final avatar URL (first 50 chars) 4. Wrapped getGroupMembers() in try-catch - Catches group-specific errors - Logs error but continues with regular character lookup 5. Added success/error logging for each character: - "✓ Successfully built HTML for {name}" - "✗ ERROR building HTML for {name}: {error.message}" - Logs full error stack for debugging 6. Added completion log: - "Finished building all character cards" - Confirms loop completed successfully EXPECTED OUTCOME: Next debug log from Salixfire will show EXACTLY: - Which character is causing the crash (if any) - What operation is failing (avatar lookup, HTML building, etc.) - Full error message and stack trace - Whether code completes or crashes This will allow us to identify and fix the root cause. Files changed: - src/systems/rendering/thoughts.js: Added try-catch blocks and comprehensive logging --- src/systems/rendering/thoughts.js | 133 +++++++++++++++++++----------- 1 file changed, 86 insertions(+), 47 deletions(-) diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index af42aa6..3d59c26 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -177,6 +177,9 @@ export function renderThoughts() { // Build HTML let html = ''; + debugLog('[RPG Thoughts] ==================== BUILDING HTML ===================='); + debugLog('[RPG Thoughts] Starting HTML generation for', presentCharacters.length + ' characters'); + // If no characters parsed, show a placeholder editable card if (presentCharacters.length === 0) { debugLog('[RPG Thoughts] ⚠ No characters parsed - showing placeholder card'); @@ -214,70 +217,106 @@ export function renderThoughts() { html += ''; } else { html += '
'; + + let characterIndex = 0; for (const char of presentCharacters) { - // Find character portrait - // Use a base64-encoded SVG placeholder as fallback to avoid 400 errors - let characterPortrait = FALLBACK_AVATAR_DATA_URI; + characterIndex++; - // console.log('[RPG Companion] Looking for avatar for:', char.name); + try { + debugLog(`[RPG Thoughts] Building HTML for character ${characterIndex}/${presentCharacters.length}:`, char.name); - // For group chats, search through group members first - if (selected_group) { - const groupMembers = getGroupMembers(selected_group); - const matchingMember = groupMembers.find(member => - member && member.name && namesMatch(member.name, char.name) - ); + // Find character portrait + // Use a base64-encoded SVG placeholder as fallback to avoid 400 errors + let characterPortrait = FALLBACK_AVATAR_DATA_URI; - if (matchingMember && matchingMember.avatar && matchingMember.avatar !== 'none') { - const thumbnailUrl = getSafeThumbnailUrl('avatar', matchingMember.avatar); - if (thumbnailUrl) { - characterPortrait = thumbnailUrl; + debugLog(`[RPG Thoughts] Looking up avatar for: ${char.name}`); + + // For group chats, search through group members first + if (selected_group) { + debugLog('[RPG Thoughts] In group chat, checking group members...'); + + try { + const groupMembers = getGroupMembers(selected_group); + debugLog('[RPG Thoughts] Group members count:', groupMembers ? groupMembers.length : 0); + + if (groupMembers && groupMembers.length > 0) { + const matchingMember = groupMembers.find(member => + member && member.name && namesMatch(member.name, char.name) + ); + + if (matchingMember && matchingMember.avatar && matchingMember.avatar !== 'none') { + const thumbnailUrl = getSafeThumbnailUrl('avatar', matchingMember.avatar); + if (thumbnailUrl) { + characterPortrait = thumbnailUrl; + debugLog('[RPG Thoughts] Found avatar in group members'); + } + } + } + } catch (groupError) { + debugLog('[RPG Thoughts] Error checking group members:', groupError.message); } } - } - // For regular chats or if not found in group, search all characters - if (characterPortrait === FALLBACK_AVATAR_DATA_URI && characters && characters.length > 0) { - const matchingCharacter = characters.find(c => - c && c.name && namesMatch(c.name, char.name) - ); + // For regular chats or if not found in group, search all characters + if (characterPortrait === FALLBACK_AVATAR_DATA_URI && characters && characters.length > 0) { + debugLog('[RPG Thoughts] Searching all characters...'); - if (matchingCharacter && matchingCharacter.avatar && matchingCharacter.avatar !== 'none') { - const thumbnailUrl = getSafeThumbnailUrl('avatar', matchingCharacter.avatar); - if (thumbnailUrl) { - characterPortrait = thumbnailUrl; + const matchingCharacter = characters.find(c => + c && c.name && namesMatch(c.name, char.name) + ); + + if (matchingCharacter && matchingCharacter.avatar && matchingCharacter.avatar !== 'none') { + const thumbnailUrl = getSafeThumbnailUrl('avatar', matchingCharacter.avatar); + if (thumbnailUrl) { + characterPortrait = thumbnailUrl; + debugLog('[RPG Thoughts] Found avatar in all characters'); + } } } - } - // If this is the current character in a 1-on-1 chat, use their portrait - if (this_chid !== undefined && characters[this_chid] && - characters[this_chid].name && namesMatch(characters[this_chid].name, char.name)) { - const thumbnailUrl = getSafeThumbnailUrl('avatar', characters[this_chid].avatar); - if (thumbnailUrl) { - characterPortrait = thumbnailUrl; + // If this is the current character in a 1-on-1 chat, use their portrait + if (this_chid !== undefined && characters[this_chid] && + characters[this_chid].name && namesMatch(characters[this_chid].name, char.name)) { + const thumbnailUrl = getSafeThumbnailUrl('avatar', characters[this_chid].avatar); + if (thumbnailUrl) { + characterPortrait = thumbnailUrl; + debugLog('[RPG Thoughts] Found avatar from current character'); + } } - } - // Get relationship emoji - const relationshipEmoji = relationshipEmojis[char.relationship] || '⚖️'; + debugLog(`[RPG Thoughts] Final avatar for ${char.name}:`, characterPortrait.substring(0, 50) + '...'); - html += ` -
-
- ${char.name} -
${relationshipEmoji}
-
-
-
- ${char.emoji} - ${char.name} + // Get relationship emoji + const relationshipEmoji = relationshipEmojis[char.relationship] || '⚖️'; + + debugLog(`[RPG Thoughts] Building HTML card for ${char.name}...`); + + html += ` +
+
+ ${char.name} +
${relationshipEmoji}
+
+
+
+ ${char.emoji} + ${char.name} +
+
${char.traits}
-
${char.traits}
-
- `; + `; + + debugLog(`[RPG Thoughts] ✓ Successfully built HTML for ${char.name}`); + + } catch (charError) { + debugLog(`[RPG Thoughts] ✗ ERROR building HTML for ${char.name}:`, charError.message); + debugLog('[RPG Thoughts] Error stack:', charError.stack); + // Continue with next character instead of crashing + } } + + debugLog('[RPG Thoughts] Finished building all character cards'); html += '
'; } From 1150786efd46778620a809f2d20b62ae1bf9a92a Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Wed, 22 Oct 2025 10:07:13 +1100 Subject: [PATCH 11/15] fix: escape special regex characters in namesMatch function Character names containing regex special chars (like brackets) were causing 'Invalid regular expression' errors when building character thoughts HTML. Now properly escapes characters before RegExp creation. --- src/systems/rendering/thoughts.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index 3d59c26..02f073c 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -51,7 +51,9 @@ function namesMatch(cardName, aiName) { if (cardCore === aiCore) return true; // 3. Check if card name appears as complete word in AI name - const wordBoundary = new RegExp(`\\b${cardCore}\\b`); + // Escape special regex characters to prevent "Invalid regular expression" errors + const escapedCardCore = cardCore.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const wordBoundary = new RegExp(`\\b${escapedCardCore}\\b`); return wordBoundary.test(aiCore); } From e2393fa73c1c0ff41b1b6baa249dc0f45e3465b7 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Wed, 22 Oct 2025 10:29:04 +1100 Subject: [PATCH 12/15] fix: add missing CSS for mobile refresh FAB button Mobile refresh button was created in HTML but had no CSS styling, making it invisible. Desktop refresh button was showing on mobile with wrong sizing. Changes: - Added .rpg-mobile-refresh FAB styles (44px, draggable, etc.) - Show mobile refresh FAB on mobile viewports - Hide desktop #rpg-manual-update button on mobile - Added responsive icon sizing for mobile refresh button Fixes issue where users only saw the desktop refresh button with incorrect DPI/sizing on mobile devices. --- style.css | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/style.css b/style.css index e69ae64..5b6bb8a 100644 --- a/style.css +++ b/style.css @@ -3305,6 +3305,45 @@ body:has(.rpg-panel.rpg-position-left) #sheld { transform: rotate(180deg); } +/* ============================================ + MOBILE REFRESH FAB BUTTON (Same pattern as mobile toggle) + ============================================ */ +.rpg-mobile-refresh { + display: none; + align-items: center; + justify-content: center; + position: fixed; + /* Position set by JavaScript based on saved settings */ + width: 44px; + height: 44px; + border-radius: 50%; + background: var(--SmartThemeBlurTintColor); + border: 2px solid var(--SmartThemeBorderColor); + color: var(--SmartThemeBodyColor); + font-size: 1.125rem; + cursor: grab; + z-index: 1001; /* Below mobile toggle (10002), above debug (1000) */ + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + transition: opacity 0.3s ease, transform 0.2s ease, top 0.3s ease, left 0.3s ease, right 0.3s ease, bottom 0.3s ease; + user-select: none; + -webkit-user-select: none; + will-change: top, left; +} + +.rpg-mobile-refresh.dragging { + transition: none; + cursor: grabbing; +} + +.rpg-mobile-refresh:hover { + transform: scale(1.1); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); +} + +.rpg-mobile-refresh:active { + transform: scale(0.95); +} + /* Mobile overlay backdrop */ .rpg-mobile-overlay { display: none; @@ -3330,6 +3369,16 @@ body:has(.rpg-panel.rpg-position-left) #sheld { display: flex; } + /* Show the mobile FAB refresh button */ + .rpg-mobile-refresh { + display: flex; + } + + /* Hide desktop refresh button on mobile */ + #rpg-manual-update { + display: none !important; + } + /* Hide FAB when panel is open */ body:has(.rpg-panel.rpg-mobile-open) .rpg-mobile-toggle { opacity: 0; @@ -3988,6 +4037,11 @@ body:has(.rpg-panel.rpg-position-left) #sheld { font-size: clamp(20px, 5.1vw, 26px) !important; } + /* Larger mobile refresh icon */ + .rpg-mobile-refresh { + font-size: clamp(20px, 5.1vw, 26px) !important; + } + /* ======================================== MOBILE SETTINGS POPUP ======================================== */ From 9a49433a28f6a8b844c2740618d034fe17805398 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Wed, 22 Oct 2025 10:34:31 +1100 Subject: [PATCH 13/15] fix: add spinner animation to mobile refresh FAB button The spinning animation when refreshing existed but only worked on the desktop button. Mobile FAB was never updated with the spinner. Changes: - Update both desktop and mobile buttons when refresh starts - Desktop shows: spinner + 'Updating...' text - Mobile FAB shows: spinner icon only (no text) - Both buttons restore properly when done Now mobile users see the spinner animation when tapping refresh! --- src/systems/generation/apiClient.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/systems/generation/apiClient.js b/src/systems/generation/apiClient.js index e30bec0..1f72fa3 100644 --- a/src/systems/generation/apiClient.js +++ b/src/systems/generation/apiClient.js @@ -97,11 +97,16 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough try { setIsGenerating(true); - // Update button to show "Updating..." state + // Update desktop button to show "Updating..." state const $updateBtn = $('#rpg-manual-update'); const originalHtml = $updateBtn.html(); $updateBtn.html(' Updating...').prop('disabled', true); + // Update mobile FAB to show spinner (icon only) + const $updateBtnMobile = $('#rpg-manual-update-mobile'); + const originalHtmlMobile = $updateBtnMobile.html(); + $updateBtnMobile.html('').prop('disabled', true); + // Save current preset name before switching (if we're going to switch) if (extensionSettings.useSeparatePreset) { originalPresetName = await getCurrentPresetName(); @@ -219,10 +224,14 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough setIsGenerating(false); - // Restore button to original state + // Restore desktop button to original state const $updateBtn = $('#rpg-manual-update'); $updateBtn.html(' Refresh RPG Info').prop('disabled', false); + // Restore mobile FAB to original state (icon only) + const $updateBtnMobile = $('#rpg-manual-update-mobile'); + $updateBtnMobile.html('').prop('disabled', false); + // Reset the flag after tracker generation completes // This ensures the flag persists through both main generation AND tracker generation // console.log('[RPG Companion] 🔄 Tracker generation complete - resetting lastActionWasSwipe to false'); From ea2231f6ba228431bbb655d32829e7db35ea76b3 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Wed, 22 Oct 2025 10:47:09 +1100 Subject: [PATCH 14/15] fix: restore proper spinning animation for mobile refresh FAB Reverted HTML replacement approach and restored the cleaner CSS-based animation from commit 1855085. Previous (wrong) approach: - Replaced button HTML with spinner - Modified both desktop and mobile buttons in apiClient.js - Messy and inconsistent Restored (correct) approach: - Add/remove .spinning CSS class in click handler - CSS animates only the icon inside the button - Button itself stays unchanged - Much cleaner implementation Changes: - Reverted apiClient.js changes from commit 9a49433 - Added .spinning CSS class and @keyframes rpg-spin - Updated index.js click handler to bind both buttons - Uses addClass/removeClass for clean animation control - Includes drag detection to prevent accidental clicks Now the mobile FAB icon spins smoothly when refreshing! --- index.js | 26 ++++++++++++++++++++++++-- src/systems/generation/apiClient.js | 13 ++----------- style.css | 18 ++++++++++++++++++ 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index 9c3f2de..b5ae314 100644 --- a/index.js +++ b/index.js @@ -328,12 +328,34 @@ async function initUI() { toggleAnimations(); }); - $('#rpg-manual-update').on('click', async function() { + // Bind to both desktop and mobile refresh buttons + $('#rpg-manual-update, #rpg-manual-update-mobile').on('click', async function() { + // Get mobile button reference + const $mobileBtn = $('#rpg-manual-update-mobile'); + + // Skip if we just finished dragging the mobile button + if ($mobileBtn.data('just-dragged')) { + console.log('[RPG Companion] Click blocked - just finished dragging refresh button'); + return; + } + if (!extensionSettings.enabled) { // console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.'); return; } - await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory); + + // Remove focus to prevent sticky black state on mobile + $(this).blur(); + + // Add spinning animation to mobile button + $mobileBtn.addClass('spinning'); + + try { + await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory); + } finally { + // Remove spinning animation when done + $mobileBtn.removeClass('spinning'); + } }); // Reset FAB positions button diff --git a/src/systems/generation/apiClient.js b/src/systems/generation/apiClient.js index 1f72fa3..e30bec0 100644 --- a/src/systems/generation/apiClient.js +++ b/src/systems/generation/apiClient.js @@ -97,16 +97,11 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough try { setIsGenerating(true); - // Update desktop button to show "Updating..." state + // Update button to show "Updating..." state const $updateBtn = $('#rpg-manual-update'); const originalHtml = $updateBtn.html(); $updateBtn.html(' Updating...').prop('disabled', true); - // Update mobile FAB to show spinner (icon only) - const $updateBtnMobile = $('#rpg-manual-update-mobile'); - const originalHtmlMobile = $updateBtnMobile.html(); - $updateBtnMobile.html('').prop('disabled', true); - // Save current preset name before switching (if we're going to switch) if (extensionSettings.useSeparatePreset) { originalPresetName = await getCurrentPresetName(); @@ -224,14 +219,10 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough setIsGenerating(false); - // Restore desktop button to original state + // Restore button to original state const $updateBtn = $('#rpg-manual-update'); $updateBtn.html(' Refresh RPG Info').prop('disabled', false); - // Restore mobile FAB to original state (icon only) - const $updateBtnMobile = $('#rpg-manual-update-mobile'); - $updateBtnMobile.html('').prop('disabled', false); - // Reset the flag after tracker generation completes // This ensures the flag persists through both main generation AND tracker generation // console.log('[RPG Companion] 🔄 Tracker generation complete - resetting lastActionWasSwipe to false'); diff --git a/style.css b/style.css index 5b6bb8a..4499224 100644 --- a/style.css +++ b/style.css @@ -3344,6 +3344,24 @@ body:has(.rpg-panel.rpg-position-left) #sheld { transform: scale(0.95); } +/* Spinning animation when refreshing */ +.rpg-mobile-refresh.spinning i { + animation: rpg-spin 0.8s linear infinite; +} + +.rpg-mobile-refresh i { + pointer-events: none; +} + +@keyframes rpg-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + /* Mobile overlay backdrop */ .rpg-mobile-overlay { display: none; From 27e1c30ea065bec46f620ac174a4774f45ef892e Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Wed, 22 Oct 2025 11:06:30 +1100 Subject: [PATCH 15/15] fix: only show mobile refresh FAB when panel open AND in Separate mode The mobile refresh button was always visible on mobile. It should only appear when BOTH conditions are met: - RPG panel is open - Generation mode is Separate (not Together) Changes: - Added opacity: 0 and pointer-events: none to base .rpg-mobile-refresh - CSS shows button when panel open AND not .rpg-hidden-mode class - Updated updateGenerationModeUI() to toggle .rpg-hidden-mode on mobile button - Together mode: adds .rpg-hidden-mode class (keeps button hidden) - Separate mode: removes .rpg-hidden-mode class (allows CSS to show it) Result: Mobile refresh FAB only appears when panel is open AND in Separate mode. Stays hidden when panel closed OR in Together mode. --- src/systems/ui/layout.js | 8 ++++++-- style.css | 13 +++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/systems/ui/layout.js b/src/systems/ui/layout.js index 9993154..597183c 100644 --- a/src/systems/ui/layout.js +++ b/src/systems/ui/layout.js @@ -266,11 +266,15 @@ export function applyPanelPosition() { * Updates the UI based on generation mode selection. */ export function updateGenerationModeUI() { + const $mobileBtn = $('#rpg-manual-update-mobile'); + if (extensionSettings.generationMode === 'together') { - // In "together" mode, manual update button is hidden + // In "together" mode, hide both desktop and mobile refresh buttons $('#rpg-manual-update').hide(); + $mobileBtn.addClass('rpg-hidden-mode'); } else { - // In "separate" mode, manual update button is visible + // In "separate" mode, show both desktop and mobile refresh buttons $('#rpg-manual-update').show(); + $mobileBtn.removeClass('rpg-hidden-mode'); } } diff --git a/style.css b/style.css index 4499224..280f771 100644 --- a/style.css +++ b/style.css @@ -3328,6 +3328,9 @@ body:has(.rpg-panel.rpg-position-left) #sheld { user-select: none; -webkit-user-select: none; will-change: top, left; + /* Hidden by default - shown when panel open AND separate mode */ + opacity: 0; + pointer-events: none; } .rpg-mobile-refresh.dragging { @@ -3387,17 +3390,23 @@ body:has(.rpg-panel.rpg-position-left) #sheld { display: flex; } - /* Show the mobile FAB refresh button */ + /* Show the mobile FAB refresh button (but hidden by opacity) */ .rpg-mobile-refresh { display: flex; } + /* Show refresh button when panel is open AND not hidden by generation mode */ + body:has(.rpg-panel.rpg-mobile-open) .rpg-mobile-refresh:not(.rpg-hidden-mode) { + opacity: 1; + pointer-events: auto; + } + /* Hide desktop refresh button on mobile */ #rpg-manual-update { display: none !important; } - /* Hide FAB when panel is open */ + /* Hide toggle FAB when panel is open */ body:has(.rpg-panel.rpg-mobile-open) .rpg-mobile-toggle { opacity: 0; pointer-events: none;