diff --git a/index.js b/index.js
index 160c515..b5ae314 100644
--- a/index.js
+++ b/index.js
@@ -98,12 +98,17 @@ import {
setupMobileTabs,
removeMobileTabs,
setupMobileKeyboardHandling,
- setupContentEditableScrolling
+ setupContentEditableScrolling,
+ setupRefreshButtonDrag,
+ setupDebugButtonDrag
} from './src/systems/ui/mobile.js';
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';
@@ -209,6 +214,22 @@ async function initUI() {
`;
$('body').append(mobileToggleHtml);
+ // Add mobile refresh button (same pattern as toggle button)
+ const mobileRefreshHtml = `
+
+ `;
+ $('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'));
@@ -263,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() {
@@ -291,18 +316,77 @@ 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();
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
+ $('#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() {
@@ -380,6 +464,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);
@@ -422,7 +507,12 @@ async function initUI() {
setupPlotButtons(sendPlotProgression);
setupMobileKeyboardHandling();
setupContentEditableScrolling();
+ setupRefreshButtonDrag();
+ setupDebugButtonDrag();
initInventoryEventListeners();
+
+ // Initialize debug UI if debug mode is enabled
+ updateDebugUIVisibility();
}
diff --git a/src/core/config.js b/src/core/config.js
index 93d2495..8e5b1ba 100644
--- a/src/core/config.js
+++ b/src/core/config.js
@@ -46,9 +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: {
+ top: 'calc(var(--topBarBlockSize) + 80px)',
+ left: '12px'
+ }, // Saved position for mobile refresh button (below toggle button)
+ debugFabPosition: {
+ top: 'calc(var(--topBarBlockSize) + 140px)',
+ left: '12px'
+ }, // Saved position for debug FAB button (below refresh button)
userStats: {
health: 100,
satiety: 100,
@@ -74,5 +82,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 4e86ae3..23d0b18 100644
--- a/src/core/state.js
+++ b/src/core/state.js
@@ -34,9 +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: {
+ top: 'calc(var(--topBarBlockSize) + 80px)',
+ left: '12px'
+ }, // Saved position for mobile refresh button (below toggle button)
+ debugFabPosition: {
+ top: 'calc(var(--topBarBlockSize) + 140px)',
+ left: '12px'
+ }, // Saved position for debug FAB button (below refresh button)
userStats: {
health: 100,
satiety: 100,
@@ -68,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)
};
/**
@@ -119,6 +128,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
@@ -200,3 +215,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 e3a3c85..d3ce1c4 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,16 +32,23 @@ export function parseResponse(responseText) {
characterThoughts: null
};
+ // DEBUG: Log full response for troubleshooting
+ 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 Companion] Found', matches.length, 'code blocks');
+ debugLog('[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));
+ debugLog(`[RPG Parser] --- Code Block ${i + 1} ---`);
+ debugLog('[RPG Parser] First 300 chars:', content.substring(0, 300));
// Check if this is a combined code block with multiple sections
const hasMultipleSections = (
@@ -41,55 +58,81 @@ export function parseResponse(responseText) {
if (hasMultipleSections) {
// Split the combined code block into individual sections
- // console.log('[RPG Companion] ✓ Found combined code block with multiple sections');
+ debugLog('[RPG Parser] ✓ Found combined code block with multiple sections');
// Extract User Stats section
const statsMatch = content.match(/(User )?Stats\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*(Info Box|Present Characters)|$)/i);
if (statsMatch && !result.userStats) {
result.userStats = statsMatch[0].trim();
- // console.log('[RPG Companion] ✓ Extracted Stats from combined block');
+ debugLog('[RPG Parser] ✓ Extracted Stats from combined block');
}
// Extract Info Box section
const infoBoxMatch = content.match(/Info Box\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*Present Characters|$)/i);
if (infoBoxMatch && !result.infoBox) {
result.infoBox = infoBoxMatch[0].trim();
- // console.log('[RPG Companion] ✓ Extracted Info Box from combined block');
+ debugLog('[RPG Parser] ✓ Extracted Info Box from combined block');
}
// Extract Present Characters section
const charactersMatch = content.match(/Present Characters\s*\n\s*---[\s\S]*$/i);
if (charactersMatch && !result.characterThoughts) {
result.characterThoughts = charactersMatch[0].trim();
- // console.log('[RPG Companion] ✓ Extracted Present Characters from combined block');
+ debugLog('[RPG Parser] ✓ Extracted Present Characters from combined block');
}
} else {
- // Handle separate code blocks (original behavior)
- // Match Stats section
- if (content.match(/Stats\s*\n\s*---/i) && !result.userStats) {
+ // Handle separate code blocks with flexible pattern matching
+ // 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) {
result.userStats = content;
- // console.log('[RPG Companion] ✓ Found Stats section');
- }
- // Match Info Box section
- else if (content.match(/Info Box\s*\n\s*---/i) && !result.infoBox) {
+ debugLog('[RPG Parser] ✓ Matched: Stats section');
+ } else if (isInfoBox && !result.infoBox) {
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(" | ")) && !result.characterThoughts) {
+ debugLog('[RPG Parser] ✓ Matched: Info Box section');
+ } else if (isCharacters && !result.characterThoughts) {
result.characterThoughts = content;
- // console.log('[RPG Companion] ✓ Found Present Characters section:', content);
+ debugLog('[RPG Parser] ✓ Matched: Present Characters section');
+ debugLog('[RPG Parser] Full content:', content);
} else {
- // console.log('[RPG Companion] ✗ Code block did not match any section');
+ 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 Companion] Parse results:', {
- // hasStats: !!result.userStats,
- // hasInfoBox: !!result.infoBox,
- // hasThoughts: !!result.characterThoughts
- // });
+ 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;
}
@@ -101,6 +144,10 @@ export function parseResponse(responseText) {
* @param {string} statsText - The raw stats text from AI response
*/
export function parseUserStats(statsText) {
+ 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
const healthMatch = statsText.match(/Health:\s*(\d+)%/);
@@ -109,43 +156,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
+ debugLog('[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;
}
}
}
+ debugLog('[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;
+ debugLog('[RPG Parser] Inventory v2 extracted:', inventoryData);
+ } else {
+ 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();
+ debugLog('[RPG Parser] Inventory v1 extracted:', inventoryMatch[1].trim());
+ } else {
+ debugLog('[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]);
@@ -156,9 +245,25 @@ export function parseUserStats(statsText) {
extensionSettings.userStats.conditions = moodMatch[2].trim(); // Conditions
}
+ debugLog('[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();
+ 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/rendering/thoughts.js b/src/systems/rendering/thoughts.js
index 7edb78f..02f073c 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"
@@ -40,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);
}
@@ -54,6 +67,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 +81,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 +119,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 +130,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 +140,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 +172,19 @@ 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);
+ 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');
// 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;
@@ -180,75 +219,114 @@ 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 += `
-
-
-
-
${relationshipEmoji}
-
-
-
- ${char.emoji}
- ${char.name}
+ // Get relationship emoji
+ const relationshipEmoji = relationshipEmojis[char.relationship] || '⚖️';
+
+ debugLog(`[RPG Thoughts] Building HTML card for ${char.name}...`);
+
+ html += `
+
+
+
+
${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 += '
';
}
$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');
diff --git a/src/systems/ui/debug.js b/src/systems/ui/debug.js
new file mode 100644
index 0000000..c6030b0
--- /dev/null
+++ b/src/systems/ui/debug.js
@@ -0,0 +1,220 @@
+/**
+ * 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
+ * Note: Debug toggle button is created in index.js, not here
+ */
+export function createDebugPanel() {
+ // Remove existing debug panel if any
+ $('#rpg-debug-panel').remove();
+
+ // Create debug panel HTML
+ const debugPanelHtml = `
+
+
+
🔍 Debug Logs
+
+
+
+
+
+
+
+
+ `;
+
+ // Append to body
+ $('body').append(debugPanelHtml);
+
+ // Set up event handlers
+ setupDebugEventHandlers();
+
+ // Initial log render
+ renderDebugLogs();
+}
+
+/**
+ * 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
+ $(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
+ $(document).on('click.rpgDebug', '#rpg-debug-close', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ closeDebugPanel();
+ });
+
+ // Copy button
+ $(document).on('click.rpgDebug', '#rpg-debug-copy', 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
+ $(document).on('click.rpgDebug', '#rpg-debug-clear', 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 += `