fix: add comprehensive debug logging and resilient parsing for AI responses

- Add detailed console logging throughout parseResponse() and parseUserStats()
  to help diagnose parsing issues reported by users
- Make parser more resilient to format variations:
  - Accept "Stats", "User Stats", "Player Stats" headers
  - Accept "Info Box", "Scene Info", "Information" headers
  - Accept "Present Characters", "Characters", "Character Thoughts" headers
  - Add keyword-based fallback when headers are missing
  - Support "Mood:" prefix in addition to "Status:" for mood/conditions
  - Support dash separator in addition to comma
- Add length check (<=10 chars) for emoji/mood to avoid false matches
- Log full parsing pipeline: input -> matches -> extraction -> final values
- Log error stack traces for better debugging

This should help diagnose issues where attributes vanish, characters show
as placeholder, or data is generated but not displayed/refreshed correctly.
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-10-22 07:13:15 +11:00
parent 9d8f6e0118
commit 74d6174bb7
+122 -29
View File
@@ -21,41 +21,74 @@ export function parseResponse(responseText) {
characterThoughts: null characterThoughts: null
}; };
// DEBUG: Log full response for troubleshooting
console.log('[RPG Parser] ==================== PARSING AI RESPONSE ====================');
console.log('[RPG Parser] Response length:', responseText.length, 'chars');
console.log('[RPG Parser] First 500 chars:', responseText.substring(0, 500));
// Extract code blocks // Extract code blocks
const codeBlockRegex = /```([^`]+)```/g; const codeBlockRegex = /```([^`]+)```/g;
const matches = [...responseText.matchAll(codeBlockRegex)]; const matches = [...responseText.matchAll(codeBlockRegex)];
// console.log('[RPG Companion] Found', matches.length, 'code blocks'); console.log('[RPG Parser] Found', matches.length, 'code blocks');
for (const match of matches) { for (let i = 0; i < matches.length; i++) {
const match = matches[i];
const content = match[1].trim(); const content = match[1].trim();
// console.log('[RPG Companion] Checking code block (first 200 chars):', content.substring(0, 200)); console.log(`[RPG Parser] --- Code Block ${i + 1} ---`);
console.log('[RPG Parser] First 300 chars:', content.substring(0, 300));
// Match Stats section // Match Stats section - flexible patterns
if (content.match(/Stats\s*\n\s*---/i)) { const isStats =
content.match(/Stats\s*\n\s*---/i) ||
content.match(/User Stats\s*\n\s*---/i) ||
content.match(/Player Stats\s*\n\s*---/i) ||
// Fallback: look for stat keywords without strict header
(content.match(/Health:\s*\d+%/i) && content.match(/Energy:\s*\d+%/i));
// Match Info Box section - flexible patterns
const isInfoBox =
content.match(/Info Box\s*\n\s*---/i) ||
content.match(/Scene Info\s*\n\s*---/i) ||
content.match(/Information\s*\n\s*---/i) ||
// Fallback: look for info box keywords
(content.match(/Date:/i) && content.match(/Location:/i) && content.match(/Time:/i));
// Match Present Characters section - flexible patterns
const isCharacters =
content.match(/Present Characters\s*\n\s*---/i) ||
content.match(/Characters\s*\n\s*---/i) ||
content.match(/Character Thoughts\s*\n\s*---/i) ||
// Fallback: look for table-like structure with emoji and pipes
(content.includes(" | ") && (content.includes("Thoughts") || content.includes("💭")));
if (isStats) {
result.userStats = content; result.userStats = content;
// console.log('[RPG Companion] ✓ Found Stats section'); console.log('[RPG Parser] ✓ Matched: Stats section');
} } else if (isInfoBox) {
// Match Info Box section
else if (content.match(/Info Box\s*\n\s*---/i)) {
result.infoBox = content; result.infoBox = content;
// console.log('[RPG Companion] ✓ Found Info Box section'); console.log('[RPG Parser] ✓ Matched: Info Box section');
} } else if (isCharacters) {
// Match Present Characters section - flexible matching
else if (content.match(/Present Characters\s*\n\s*---/i) || content.includes(" | ")) {
result.characterThoughts = content; result.characterThoughts = content;
// console.log('[RPG Companion] ✓ Found Present Characters section:', content); console.log('[RPG Parser] ✓ Matched: Present Characters section');
console.log('[RPG Parser] Full content:', content);
} else { } else {
// console.log('[RPG Companion] ✗ Code block did not match any section'); console.log('[RPG Parser] ✗ No match - checking patterns:');
console.log('[RPG Parser] - Has "Stats\\n---"?', !!content.match(/Stats\s*\n\s*---/i));
console.log('[RPG Parser] - Has stat keywords?', !!(content.match(/Health:\s*\d+%/i) && content.match(/Energy:\s*\d+%/i)));
console.log('[RPG Parser] - Has "Info Box\\n---"?', !!content.match(/Info Box\s*\n\s*---/i));
console.log('[RPG Parser] - Has info keywords?', !!(content.match(/Date:/i) && content.match(/Location:/i)));
console.log('[RPG Parser] - Has "Present Characters\\n---"?', !!content.match(/Present Characters\s*\n\s*---/i));
console.log('[RPG Parser] - Has " | " + thoughts?', !!(content.includes(" | ") && (content.includes("Thoughts") || content.includes("💭"))));
} }
} }
// console.log('[RPG Companion] Parse results:', { console.log('[RPG Parser] ==================== PARSE RESULTS ====================');
// hasStats: !!result.userStats, console.log('[RPG Parser] Found Stats:', !!result.userStats);
// hasInfoBox: !!result.infoBox, console.log('[RPG Parser] Found Info Box:', !!result.infoBox);
// hasThoughts: !!result.characterThoughts console.log('[RPG Parser] Found Characters:', !!result.characterThoughts);
// }); console.log('[RPG Parser] =======================================================');
return result; return result;
} }
@@ -67,6 +100,10 @@ export function parseResponse(responseText) {
* @param {string} statsText - The raw stats text from AI response * @param {string} statsText - The raw stats text from AI response
*/ */
export function parseUserStats(statsText) { export function parseUserStats(statsText) {
console.log('[RPG Parser] ==================== PARSING USER STATS ====================');
console.log('[RPG Parser] Stats text length:', statsText.length, 'chars');
console.log('[RPG Parser] Stats text preview:', statsText.substring(0, 200));
try { try {
// Extract percentages and mood/conditions // Extract percentages and mood/conditions
const healthMatch = statsText.match(/Health:\s*(\d+)%/); const healthMatch = statsText.match(/Health:\s*(\d+)%/);
@@ -75,43 +112,85 @@ export function parseUserStats(statsText) {
const hygieneMatch = statsText.match(/Hygiene:\s*(\d+)%/); const hygieneMatch = statsText.match(/Hygiene:\s*(\d+)%/);
const arousalMatch = statsText.match(/Arousal:\s*(\d+)%/); const arousalMatch = statsText.match(/Arousal:\s*(\d+)%/);
// Match new format: Status: [Emoji, Conditions] console.log('[RPG Parser] Stat matches:', {
// Also support legacy format: [Emoji]: [Conditions] for backward compatibility 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; let moodMatch = null;
// Try new format: Status: emoji, conditions
const statusMatch = statsText.match(/Status:\s*(.+?),\s*(.+)/i); const statusMatch = statsText.match(/Status:\s*(.+?),\s*(.+)/i);
if (statusMatch) { if (statusMatch) {
// New format: Status: [Emoji, Conditions]
moodMatch = [null, statusMatch[1].trim(), statusMatch[2].trim()]; 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'); const lines = statsText.split('\n');
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim(); const line = lines[i].trim();
// Skip lines with percentages or "Inventory:" or "Status:" // Skip lines with percentages or known keywords
if (line.includes('%') || line.toLowerCase().startsWith('inventory:') || line.toLowerCase().startsWith('status:')) continue; if (line.includes('%') ||
// Match emoji followed by colon and conditions 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*(.+)$/); const match = line.match(/^(.+?):\s*(.+)$/);
if (match) { if (match && match[1].length <= 10) { // Emoji/mood should be short
moodMatch = match; moodMatch = match;
break; break;
} }
} }
} }
console.log('[RPG Parser] Mood/Status match:', {
found: !!moodMatch,
emoji: moodMatch ? moodMatch[1] : 'NOT FOUND',
conditions: moodMatch ? moodMatch[2] : 'NOT FOUND'
});
// Extract inventory - use v2 parser if feature flag enabled, otherwise fallback to v1 // Extract inventory - use v2 parser if feature flag enabled, otherwise fallback to v1
if (FEATURE_FLAGS.useNewInventory) { if (FEATURE_FLAGS.useNewInventory) {
const inventoryData = extractInventory(statsText); const inventoryData = extractInventory(statsText);
if (inventoryData) { if (inventoryData) {
extensionSettings.userStats.inventory = inventoryData; extensionSettings.userStats.inventory = inventoryData;
console.log('[RPG Parser] Inventory v2 extracted:', inventoryData);
} else {
console.log('[RPG Parser] Inventory v2 extraction failed');
} }
} else { } else {
// Legacy v1 parsing for backward compatibility // Legacy v1 parsing for backward compatibility
const inventoryMatch = statsText.match(/Inventory:\s*(.+)/i); const inventoryMatch = statsText.match(/Inventory:\s*(.+)/i);
if (inventoryMatch) { if (inventoryMatch) {
extensionSettings.userStats.inventory = inventoryMatch[1].trim(); extensionSettings.userStats.inventory = inventoryMatch[1].trim();
console.log('[RPG Parser] Inventory v1 extracted:', inventoryMatch[1].trim());
} else {
console.log('[RPG Parser] Inventory v1 not found');
} }
} }
// Update extension settings
if (healthMatch) extensionSettings.userStats.health = parseInt(healthMatch[1]); if (healthMatch) extensionSettings.userStats.health = parseInt(healthMatch[1]);
if (satietyMatch) extensionSettings.userStats.satiety = parseInt(satietyMatch[1]); if (satietyMatch) extensionSettings.userStats.satiety = parseInt(satietyMatch[1]);
if (energyMatch) extensionSettings.userStats.energy = parseInt(energyMatch[1]); if (energyMatch) extensionSettings.userStats.energy = parseInt(energyMatch[1]);
@@ -122,9 +201,23 @@ export function parseUserStats(statsText) {
extensionSettings.userStats.conditions = moodMatch[2].trim(); // Conditions extensionSettings.userStats.conditions = moodMatch[2].trim(); // Conditions
} }
console.log('[RPG Parser] Final userStats after parsing:', {
health: extensionSettings.userStats.health,
satiety: extensionSettings.userStats.satiety,
energy: extensionSettings.userStats.energy,
hygiene: extensionSettings.userStats.hygiene,
arousal: extensionSettings.userStats.arousal,
mood: extensionSettings.userStats.mood,
conditions: extensionSettings.userStats.conditions,
inventory: FEATURE_FLAGS.useNewInventory ? 'v2 object' : extensionSettings.userStats.inventory
});
saveSettings(); saveSettings();
console.log('[RPG Parser] Settings saved successfully');
console.log('[RPG Parser] =======================================================');
} catch (error) { } catch (error) {
console.error('[RPG Companion] Error parsing user stats:', error); console.error('[RPG Companion] Error parsing user stats:', error);
console.error('[RPG Companion] Stack trace:', error.stack);
} }
} }