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 += ` -
-
- ${char.name} -
${relationshipEmoji}
-
-
-
- ${char.emoji} - ${char.name} + // Get relationship emoji + const relationshipEmoji = relationshipEmojis[char.relationship] || '⚖️'; + + debugLog(`[RPG Thoughts] Building HTML card for ${char.name}...`); + + html += ` +
+
+ ${char.name} +
${relationshipEmoji}
+
+
+
+ ${char.emoji} + ${char.name} +
+
${char.traits}
-
${char.traits}
-
- `; + `; + + debugLog(`[RPG Thoughts] ✓ Successfully built HTML for ${char.name}`); + + } catch (charError) { + debugLog(`[RPG Thoughts] ✗ ERROR building HTML for ${char.name}:`, charError.message); + debugLog('[RPG Thoughts] Error stack:', charError.stack); + // Continue with next character instead of crashing + } } + + debugLog('[RPG Thoughts] Finished building all character cards'); html += '
'; } $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 += `
${escapeHtml(log.data)}
`; + } + html += `
`; + return html; + }).join(''); + + $logsContainer.html(logsHtml); + + // Auto-scroll to bottom + $logsContainer[0].scrollTop = $logsContainer[0].scrollHeight; +} + +/** + * Escapes HTML to prevent XSS + */ +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +/** + * Shows or hides debug UI based on debug mode setting + * 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(); + } +} diff --git a/src/systems/ui/layout.js b/src/systems/ui/layout.js index 9993154..597183c 100644 --- a/src/systems/ui/layout.js +++ b/src/systems/ui/layout.js @@ -266,11 +266,15 @@ export function applyPanelPosition() { * Updates the UI based on generation mode selection. */ export function updateGenerationModeUI() { + const $mobileBtn = $('#rpg-manual-update-mobile'); + if (extensionSettings.generationMode === 'together') { - // In "together" mode, manual update button is hidden + // In "together" mode, hide both desktop and mobile refresh buttons $('#rpg-manual-update').hide(); + $mobileBtn.addClass('rpg-hidden-mode'); } else { - // In "separate" mode, manual update button is visible + // In "separate" mode, show both desktop and mobile refresh buttons $('#rpg-manual-update').show(); + $mobileBtn.removeClass('rpg-hidden-mode'); } } diff --git a/src/systems/ui/mobile.js b/src/systems/ui/mobile.js index f2648b3..73a4ff1 100644 --- a/src/systems/ui/mobile.js +++ b/src/systems/ui/mobile.js @@ -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; + }); +} diff --git a/style.css b/style.css index e9e266d..280f771 100644 --- a/style.css +++ b/style.css @@ -2054,6 +2054,34 @@ body:has(.rpg-panel.rpg-position-left) #sheld { transform: translateY(0); } +.rpg-btn-reset-fab { + width: 100%; + padding: 0.625em; + background: rgba(52, 152, 219, 0.2); + border: 2px solid rgba(52, 152, 219, 0.5); + border-radius: 0.5em; + color: #5dade2; + font-size: 1.2vw; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5em; + transition: all 0.3s ease; +} + +.rpg-btn-reset-fab:hover { + background: rgba(52, 152, 219, 0.3); + border-color: rgba(52, 152, 219, 0.8); + color: #85c1e9; + transform: translateY(-0.062rem); +} + +.rpg-btn-reset-fab:active { + transform: translateY(0); +} + /* ============================================ THEME VARIATIONS ============================================ */ @@ -3277,6 +3305,66 @@ body:has(.rpg-panel.rpg-position-left) #sheld { transform: rotate(180deg); } +/* ============================================ + MOBILE REFRESH FAB BUTTON (Same pattern as mobile toggle) + ============================================ */ +.rpg-mobile-refresh { + display: none; + align-items: center; + justify-content: center; + position: fixed; + /* Position set by JavaScript based on saved settings */ + width: 44px; + height: 44px; + border-radius: 50%; + background: var(--SmartThemeBlurTintColor); + border: 2px solid var(--SmartThemeBorderColor); + color: var(--SmartThemeBodyColor); + font-size: 1.125rem; + cursor: grab; + z-index: 1001; /* Below mobile toggle (10002), above debug (1000) */ + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + transition: opacity 0.3s ease, transform 0.2s ease, top 0.3s ease, left 0.3s ease, right 0.3s ease, bottom 0.3s ease; + user-select: none; + -webkit-user-select: none; + will-change: top, left; + /* Hidden by default - shown when panel open AND separate mode */ + opacity: 0; + pointer-events: none; +} + +.rpg-mobile-refresh.dragging { + transition: none; + cursor: grabbing; +} + +.rpg-mobile-refresh:hover { + transform: scale(1.1); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); +} + +.rpg-mobile-refresh:active { + transform: scale(0.95); +} + +/* Spinning animation when refreshing */ +.rpg-mobile-refresh.spinning i { + animation: rpg-spin 0.8s linear infinite; +} + +.rpg-mobile-refresh i { + pointer-events: none; +} + +@keyframes rpg-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + /* Mobile overlay backdrop */ .rpg-mobile-overlay { display: none; @@ -3302,7 +3390,23 @@ body:has(.rpg-panel.rpg-position-left) #sheld { display: flex; } - /* Hide FAB when panel is open */ + /* Show the mobile FAB refresh button (but hidden by opacity) */ + .rpg-mobile-refresh { + display: flex; + } + + /* Show refresh button when panel is open AND not hidden by generation mode */ + body:has(.rpg-panel.rpg-mobile-open) .rpg-mobile-refresh:not(.rpg-hidden-mode) { + opacity: 1; + pointer-events: auto; + } + + /* Hide desktop refresh button on mobile */ + #rpg-manual-update { + display: none !important; + } + + /* Hide toggle FAB when panel is open */ body:has(.rpg-panel.rpg-mobile-open) .rpg-mobile-toggle { opacity: 0; pointer-events: none; @@ -3960,6 +4064,11 @@ body:has(.rpg-panel.rpg-position-left) #sheld { font-size: clamp(20px, 5.1vw, 26px) !important; } + /* Larger mobile refresh icon */ + .rpg-mobile-refresh { + font-size: clamp(20px, 5.1vw, 26px) !important; + } + /* ======================================== MOBILE SETTINGS POPUP ======================================== */ @@ -3994,6 +4103,10 @@ body:has(.rpg-panel.rpg-position-left) #sheld { .rpg-btn-clear-cache { font-size: clamp(13px, 3.3vw, 17px) !important; } + + .rpg-btn-reset-fab { + font-size: clamp(13px, 3.3vw, 17px) !important; + } } /* Very narrow screens - single column layout for all stats */ @@ -4675,3 +4788,239 @@ body:has(.rpg-panel.rpg-position-left) #sheld { min-height: 2rem; } } + +/* =================================================================== + Debug Panel Styles - Mobile-Friendly Debug Log Viewer + =================================================================== */ + +/* ============================================ + DEBUG TOGGLE FAB BUTTON (Same pattern as mobile FABs) + ============================================ */ +.rpg-debug-toggle { + display: none; /* Hidden by default, shown when debugMode is enabled */ + align-items: center; + justify-content: center; + position: fixed; + /* Position set by JavaScript based on saved settings */ + width: 44px; + height: 44px; + border-radius: 50%; + background: var(--SmartThemeBlurTintColor); + border: 2px solid var(--SmartThemeBorderColor); + color: var(--rpg-text, #ecf0f1); + font-size: 1.85vw; + cursor: grab; + z-index: 1000; /* Below refresh (1001) and mobile toggle (10002) */ + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + transition: opacity 0.3s ease, transform 0.2s ease, top 0.3s ease, left 0.3s ease, right 0.3s ease, bottom 0.3s ease; + user-select: none; + -webkit-user-select: none; + will-change: top, left; +} + +/* Disable transitions while actively dragging */ +.rpg-debug-toggle.dragging { + transition: none; + cursor: grabbing; +} + +.rpg-debug-toggle:hover { + transform: scale(1.1); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); +} + +.rpg-debug-toggle:active { + transform: scale(0.95); +} + +/* 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-actions button i { + pointer-events: none; /* Prevent icon from blocking clicks */ +} + +.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; +} + +/* Mobile view - slide from right like main panel */ +@media (max-width: 1000px) { + .rpg-debug-panel { + /* Reset bottom slide positioning */ + transform: none; + transition: none; + bottom: auto; + + /* Mobile panel - slide from right */ + position: fixed !important; + top: var(--topBarBlockSize) !important; + right: 0 !important; + left: auto !important; + + /* Mobile sizing using dynamic viewport units */ + width: 85dvw !important; + max-width: 400px !important; + height: calc(100dvh - var(--topBarBlockSize)) !important; + + /* Hidden by default */ + display: none !important; + + /* Mobile scrolling */ + overflow-y: auto !important; + -webkit-overflow-scrolling: touch; + + /* Styling */ + border-radius: 20px 0 0 0; + border-left: 1px solid var(--SmartThemeBorderColor); + border-top: 1px solid var(--SmartThemeBorderColor); + border-bottom: none; + backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2)); + box-shadow: -5px 0 20px rgba(0, 0, 0, 0.5); + } + + /* Show panel when opened with slide-in animation */ + .rpg-debug-panel.rpg-mobile-open { + display: flex !important; + z-index: 10002; + animation: rpgSlideInFromRight 0.3s ease-in-out; + } + + /* Closing animation - slide out to right */ + .rpg-debug-panel.rpg-mobile-closing { + display: flex !important; + z-index: 10002; + animation: rpgSlideOutToRight 0.3s ease-in-out; + } + + /* Debug logs container needs to stay scrollable */ + .rpg-debug-logs { + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + + /* Debug toggle button on mobile */ + .rpg-debug-toggle { + font-size: clamp(20px, 5.1vw, 26px) !important; + } +} + +/* Desktop view - smaller panel in bottom right */ +@media (min-width: 1001px) { + .rpg-debug-panel { + bottom: 20px; + left: auto; + right: 20px; + width: 600px; + max-width: 90vw; + height: 400px; + border-radius: 12px; + border: 2px solid var(--SmartThemeBorderColor, #333); + } +} diff --git a/template.html b/template.html index 8d24685..b72fb5e 100644 --- a/template.html +++ b/template.html @@ -195,6 +195,14 @@ Display buttons above chat input for plot progression prompts + + + + Shows parser logs in a mobile-friendly UI panel. Useful for troubleshooting. Look for the red bug button. +
@@ -229,6 +237,16 @@ Clear Extension Cache
+ + +
+ + + Resets all floating action buttons (toggle, refresh, debug) to default top-left positions. Useful if buttons are off-screen. + +