Merge: Combined code block parsing + flexible pattern matching + debug logging

- Combined block parsing: Detects and splits multi-section code blocks
- Flexible patterns: Supports variations like 'User Stats', 'Player Stats', etc.
- Enhanced debugging: Debug logs with pattern match details
- Fallback matching: Uses keyword detection when headers are malformed
- Duplicate prevention: Checks prevent overwriting already-found sections
This commit is contained in:
Spicy_Marinara
2025-10-22 11:03:26 +02:00
10 changed files with 1453 additions and 111 deletions
+140 -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,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);
}
}
+140 -62
View File
@@ -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 += '</div>';
} else {
html += '<div class="rpg-thoughts-content">';
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 += `
<div class="rpg-character-card" data-character-name="${char.name}">
<div class="rpg-character-avatar">
<img src="${characterPortrait}" alt="${char.name}" onerror="this.style.opacity='0.5';this.onerror=null;" />
<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${char.name}" data-field="relationship" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipEmoji}</div>
</div>
<div class="rpg-character-info">
<div class="rpg-character-header">
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${char.name}" data-field="emoji" title="Click to edit emoji">${char.emoji}</span>
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="name" title="Click to edit name">${char.name}</span>
// Get relationship emoji
const relationshipEmoji = relationshipEmojis[char.relationship] || '⚖️';
debugLog(`[RPG Thoughts] Building HTML card for ${char.name}...`);
html += `
<div class="rpg-character-card" data-character-name="${char.name}">
<div class="rpg-character-avatar">
<img src="${characterPortrait}" alt="${char.name}" onerror="this.style.opacity='0.5';this.onerror=null;" />
<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${char.name}" data-field="relationship" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipEmoji}</div>
</div>
<div class="rpg-character-info">
<div class="rpg-character-header">
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${char.name}" data-field="emoji" title="Click to edit emoji">${char.emoji}</span>
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="name" title="Click to edit name">${char.name}</span>
</div>
<div class="rpg-character-traits rpg-editable" contenteditable="true" data-character="${char.name}" data-field="traits" title="Click to edit traits">${char.traits}</div>
</div>
<div class="rpg-character-traits rpg-editable" contenteditable="true" data-character="${char.name}" data-field="traits" title="Click to edit traits">${char.traits}</div>
</div>
</div>
`;
`;
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 += '</div>';
}
$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');
+220
View File
@@ -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 = `
<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>
`;
// 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 = $('<div class="rpg-mobile-overlay"></div>');
$('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('<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
* 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();
}
} else {
// Hide debug toggle button
$debugToggle.css('display', 'none');
// Remove debug panel
$('#rpg-debug-panel').remove();
}
}
+6 -2
View File
@@ -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');
}
}
+431
View File
@@ -716,3 +716,434 @@ export function setupContentEditableScrolling() {
}, 300);
});
}
/**
* Sets up the mobile refresh button with drag functionality.
* Same pattern as mobile toggle button.
* Tap = refresh, drag = reposition
*/
export function setupRefreshButtonDrag() {
const $refreshBtn = $('#rpg-manual-update-mobile');
if ($refreshBtn.length === 0) {
console.warn('[RPG Mobile] Refresh button not found in DOM');
return;
}
console.log('[RPG Mobile] setupRefreshButtonDrag called');
// Load and apply saved position
if (extensionSettings.mobileRefreshPosition) {
const pos = extensionSettings.mobileRefreshPosition;
console.log('[RPG Mobile] Loading saved refresh button position:', pos);
// Apply saved position
if (pos.top) $refreshBtn.css('top', pos.top);
if (pos.right) $refreshBtn.css('right', pos.right);
if (pos.bottom) $refreshBtn.css('bottom', pos.bottom);
if (pos.left) $refreshBtn.css('left', pos.left);
// Constrain to viewport after position is applied
requestAnimationFrame(() => constrainFabToViewport($refreshBtn));
}
// 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) {
$refreshBtn.css({
left: pendingX + 'px',
top: pendingY + 'px',
right: 'auto',
bottom: 'auto'
});
pendingX = null;
pendingY = null;
}
rafId = null;
}
// Touch start
$refreshBtn.on('touchstart', function(e) {
const touch = e.originalEvent.touches[0];
touchStartTime = Date.now();
touchStartX = touch.clientX;
touchStartY = touch.clientY;
const offset = $refreshBtn.offset();
buttonStartX = offset.left;
buttonStartY = offset.top;
isDragging = false;
});
// Touch move
$refreshBtn.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;
$refreshBtn.addClass('dragging');
}
if (isDragging) {
e.preventDefault();
let newX = buttonStartX + deltaX;
let newY = buttonStartY + deltaY;
const buttonWidth = $refreshBtn.outerWidth();
const buttonHeight = $refreshBtn.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
$refreshBtn.on('touchend', function(e) {
if (isDragging) {
// Save new position
const offset = $refreshBtn.offset();
const newPosition = {
left: offset.left + 'px',
top: offset.top + 'px'
};
extensionSettings.mobileRefreshPosition = newPosition;
saveSettings();
setTimeout(() => {
$refreshBtn.removeClass('dragging');
}, 50);
// Set flag to prevent click handler from firing
$refreshBtn.data('just-dragged', true);
setTimeout(() => {
$refreshBtn.data('just-dragged', false);
}, 100);
isDragging = false;
}
});
// Mouse support for desktop
let mouseDown = false;
$refreshBtn.on('mousedown', function(e) {
e.preventDefault();
touchStartTime = Date.now();
touchStartX = e.clientX;
touchStartY = e.clientY;
const offset = $refreshBtn.offset();
buttonStartX = offset.left;
buttonStartY = offset.top;
mouseDown = true;
isDragging = false;
});
$(document).on('mousemove', 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;
$refreshBtn.addClass('dragging');
}
if (isDragging) {
let newX = buttonStartX + deltaX;
let newY = buttonStartY + deltaY;
const buttonWidth = $refreshBtn.outerWidth();
const buttonHeight = $refreshBtn.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', function(e) {
if (mouseDown && isDragging) {
const offset = $refreshBtn.offset();
const newPosition = {
left: offset.left + 'px',
top: offset.top + 'px'
};
extensionSettings.mobileRefreshPosition = newPosition;
saveSettings();
setTimeout(() => {
$refreshBtn.removeClass('dragging');
}, 50);
$refreshBtn.data('just-dragged', true);
setTimeout(() => {
$refreshBtn.data('just-dragged', false);
}, 100);
}
mouseDown = false;
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;
});
}