From 5e3c95d163c938fa1920cd51d189c4df2edafe64 Mon Sep 17 00:00:00 2001 From: Daniel Onyejesi Date: Tue, 24 Mar 2026 19:01:16 -0400 Subject: [PATCH] Fix JSON parse for Sonnet 4.6: escape literal newlines in strings Sonnet 4.6 outputs literal newline characters inside JSON string values (e.g. in HTML body field) instead of \n escape sequences. Added sanitizeJsonString that walks the string character by character and escapes unescaped control chars inside quoted values. --- src/routes/learningAI.js | 42 ++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/routes/learningAI.js b/src/routes/learningAI.js index 86f6526..8b18b55 100644 --- a/src/routes/learningAI.js +++ b/src/routes/learningAI.js @@ -242,22 +242,44 @@ router.post('/ai-generate', upload.single('file'), async function(req, res) { var lastBrace = raw.lastIndexOf('}'); if (lastBrace !== -1 && lastBrace < raw.length - 1) raw = raw.substring(0, lastBrace + 1); + // Fix unescaped control characters inside JSON string values. + // Models sometimes output literal newlines/tabs inside strings instead of \n \t. + function sanitizeJsonString(s) { + var inStr = false, escaped = false, out = ''; + for (var i = 0; i < s.length; i++) { + var c = s[i]; + if (escaped) { out += c; escaped = false; continue; } + if (c === '\\' && inStr) { out += c; escaped = true; continue; } + if (c === '"') { inStr = !inStr; out += c; continue; } + if (inStr && c === '\n') { out += '\\n'; continue; } + if (inStr && c === '\r') { continue; } + if (inStr && c === '\t') { out += '\\t'; continue; } + out += c; + } + return out; + } + var parsed; try { parsed = JSON.parse(raw); } catch (e) { - // Try extracting from first { to last } - var firstBrace = raw.indexOf('{'); - var extractedJson = firstBrace >= 0 ? raw.substring(firstBrace, lastBrace + 1) : null; - if (extractedJson) { - try { parsed = JSON.parse(extractedJson); } - catch (e2) { - console.error('[LearningAI] JSON parse failed. Raw (first 500):', raw.substring(0, 500)); + console.error('[LearningAI] Direct parse failed:', e.message, '| Pos:', e.message.match(/position (\d+)/)?.[1], '| Char context:', raw.substring(parseInt(e.message.match(/position (\d+)/)?.[1] || 0) - 40, parseInt(e.message.match(/position (\d+)/)?.[1] || 0) + 40)); + // Attempt 2: sanitize control chars and retry + try { parsed = JSON.parse(sanitizeJsonString(raw)); } + catch (e2) { + // Attempt 3: extract first { to last } + var firstBrace = raw.indexOf('{'); + var extractedJson = firstBrace >= 0 ? raw.substring(firstBrace, lastBrace + 1) : null; + if (extractedJson) { + try { parsed = JSON.parse(sanitizeJsonString(extractedJson)); } + catch (e3) { + console.error('[LearningAI] All parse attempts failed:', e3.message); + return res.status(500).json({ error: 'AI returned invalid JSON. Try again.' }); + } + } else { + console.error('[LearningAI] No JSON braces found'); return res.status(500).json({ error: 'AI returned invalid JSON. Try again.' }); } - } else { - console.error('[LearningAI] No JSON found. Raw (first 500):', raw.substring(0, 500)); - return res.status(500).json({ error: 'AI returned invalid JSON. Try again.' }); } }