feat: add mobile-friendly debug mode for parser troubleshooting

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.
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-10-22 07:22:28 +11:00
parent 74d6174bb7
commit b5d35ac2b0
7 changed files with 424 additions and 36 deletions
+13
View File
@@ -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();
}
+6
View File
@@ -11,6 +11,12 @@
</label>
<small class="notes">Toggle to enable/disable the RPG Companion extension. Configure additional settings within the panel itself.</small>
<label class="checkbox_label" for="rpg-debug-mode" style="margin-top: 10px;">
<input type="checkbox" id="rpg-debug-mode" />
<span>🔍 Debug Mode (Mobile-Friendly)</span>
</label>
<small class="notes">Enable debug logging visible in UI. Useful for troubleshooting parsing issues on mobile devices. Shows a red bug button to view parser logs.</small>
<div style="margin-top: 10px; display: flex; gap: 10px;">
<a href="https://discord.com/invite/KdAkTg94ME" target="_blank" class="menu_button" style="flex: 1; text-align: center; text-decoration: none;">
<i class="fa-brands fa-discord"></i> Discord
+2 -1
View File
@@ -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)
};
+29
View File
@@ -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;
}
+47 -35
View File
@@ -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);
}
}
+156
View File
@@ -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 = `
<div id="rpg-debug-panel" class="rpg-debug-panel">
<div class="rpg-debug-header">
<h3>🔍 Debug Logs</h3>
<div class="rpg-debug-actions">
<button id="rpg-debug-copy" title="Copy logs to clipboard">
<i class="fa-solid fa-copy"></i>
</button>
<button id="rpg-debug-clear" title="Clear logs">
<i class="fa-solid fa-trash"></i>
</button>
<button id="rpg-debug-close" title="Close debug panel">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
<div id="rpg-debug-logs" class="rpg-debug-logs"></div>
</div>
`;
// Create debug toggle button (FAB-style)
const debugToggleHtml = `
<button id="rpg-debug-toggle" class="rpg-debug-toggle" title="Toggle Debug Logs">
<i class="fa-solid fa-bug"></i>
</button>
`;
// 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('<div class="rpg-debug-empty">No logs yet. Logs will appear when parser runs.</div>');
return;
}
// Build logs HTML
const logsHtml = logs.map(log => {
let html = `<div class="rpg-debug-entry">`;
html += `<span class="rpg-debug-time">[${log.timestamp}]</span> `;
html += `<span class="rpg-debug-message">${escapeHtml(log.message)}</span>`;
if (log.data) {
html += `<pre class="rpg-debug-data">${escapeHtml(log.data)}</pre>`;
}
html += `</div>`;
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();
}
}
+171
View File
@@ -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 */
}
}