nusaibah0110 commited on
Commit
c6fefb4
·
1 Parent(s): 68aadac

fix: Robust JSON parsing and field mapping for report generation

Browse files

Frontend improvements:
- Added comprehensive JSON parsing with multiple fallback strategies
- Handle both object and string responses from backend
- Added field-level validation to ensure each report field gets distinct data
- Warning if colposcopicFindings contains JSON (indicates parsing failure)
- Detailed console logging for debugging field population

Backend improvements:
- Validate AI response is valid JSON before returning
- Strip markdown code fences in backend (before sending to frontend)
- Better error messages for JSON parsing failures
- Log parsed JSON keys to verify structure

This ensures report data populates correctly into individual form fields.

Files changed (2) hide show
  1. backend/app.py +24 -5
  2. src/pages/ReportPage.tsx +66 -38
backend/app.py CHANGED
@@ -13,6 +13,7 @@ import uvicorn
13
  import traceback
14
  import json
15
  from typing import List, Dict, Optional
 
16
 
17
  # Load .env file for local development.
18
  # Search from this file's directory upward so it works whether the server
@@ -394,11 +395,29 @@ Do NOT include any other keys. Do NOT wrap in markdown. Return raw JSON only."""
394
  if not response_text:
395
  raise Exception("All model attempts failed. Please check API key and model availability.")
396
 
397
- return JSONResponse({
398
- "status": "success",
399
- "report": response_text,
400
- "model": used_model
401
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
 
403
  except Exception as e:
404
  error_msg = str(e)
 
13
  import traceback
14
  import json
15
  from typing import List, Dict, Optional
16
+ import re
17
 
18
  # Load .env file for local development.
19
  # Search from this file's directory upward so it works whether the server
 
395
  if not response_text:
396
  raise Exception("All model attempts failed. Please check API key and model availability.")
397
 
398
+ # Ensure response_text is valid JSON before returning
399
+ try:
400
+ # Strip markdown if present
401
+ cleaned_text = response_text.strip()
402
+ if cleaned_text.startswith('```'):
403
+ cleaned_text = re.sub(r'^```[a-z]*\n?', '', cleaned_text, flags=re.IGNORECASE)
404
+ cleaned_text = re.sub(r'\n?```\s*$', '', cleaned_text)
405
+ cleaned_text = cleaned_text.strip()
406
+
407
+ # Parse to verify it's valid JSON
408
+ parsed_json = json.loads(cleaned_text)
409
+ print(f"✅ Report is valid JSON with keys: {list(parsed_json.keys())}")
410
+
411
+ # Return as JSON object (not string) so it's properly encoded by FastAPI
412
+ return JSONResponse({
413
+ "status": "success",
414
+ "report": cleaned_text, # Return the cleaned JSON string
415
+ "model": used_model
416
+ })
417
+ except json.JSONDecodeError as je:
418
+ print(f"⚠️ Response is not valid JSON: {je}")
419
+ print(f"Response text: {response_text[:500]}")
420
+ raise Exception(f"Gemini returned invalid JSON: {str(je)}")
421
 
422
  except Exception as e:
423
  error_msg = str(e)
src/pages/ReportPage.tsx CHANGED
@@ -206,38 +206,61 @@ export function ReportPage({
206
  throw new Error('Invalid response from backend');
207
  }
208
 
209
- // Strip possible markdown code fences before parsing
210
- let reportText: string = data.report.trim();
211
- console.log('[ReportPage] Raw AI response:', reportText.substring(0, 200));
212
-
213
- if (reportText.startsWith('```')) {
214
- reportText = reportText.replace(/^```[a-z]*\n?/i, '').replace(/```\s*$/, '').trim();
215
- console.log('[ReportPage] Stripped markdown, new length:', reportText.length);
216
- }
217
-
218
- // Parse structured JSON returned by Gemini
219
  let parsed: Record<string, string> = {};
220
- try {
221
- parsed = JSON.parse(reportText);
222
- console.log('[ReportPage] Successfully parsed JSON with keys:', Object.keys(parsed));
223
- } catch (parseErr) {
224
- console.error('JSON parsing failed. Attempting to extract JSON from response...', parseErr);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
- // Try to find JSON object in the response
227
- const jsonMatch = reportText.match(/\{[\s\S]*\}/);
228
- if (jsonMatch) {
229
- try {
230
- parsed = JSON.parse(jsonMatch[0]);
231
- console.log('[ReportPage] Extracted JSON from response:', Object.keys(parsed));
232
- } catch (innerErr) {
233
- console.error('Failed to extract JSON:', innerErr);
234
- throw new Error('Could not parse AI response as valid JSON');
 
 
 
 
 
 
 
 
 
 
 
 
235
  }
236
- } else {
237
- throw new Error('No JSON object found in AI response');
238
  }
239
  }
240
 
 
 
 
 
 
 
241
  // Validate that all required keys are present
242
  const requiredKeys = ['examQuality', 'transformationZone', 'acetowL', 'nativeFindings',
243
  'aceticFindings', 'biopsySites', 'biopsyNotes', 'colposcopicFindings',
@@ -262,19 +285,24 @@ export function ReportPage({
262
  biopsyNotes: String(parsed.biopsyNotes || '').trim(),
263
  };
264
 
265
- console.log('[ReportPage] AI generated fields:', {
266
- examQuality: updatedFormData.examQuality.substring(0, 30),
267
- transformationZone: updatedFormData.transformationZone.substring(0, 30),
268
- acetowL: updatedFormData.acetowL.substring(0, 30),
269
- nativeFindings: updatedFormData.nativeFindings.substring(0, 50),
270
- aceticFindings: updatedFormData.aceticFindings.substring(0, 50),
271
- colposcopicFindings: updatedFormData.colposcopicFindings.substring(0, 50),
272
- treatmentPlan: updatedFormData.treatmentPlan.substring(0, 50),
273
- followUp: updatedFormData.followUp.substring(0, 50),
274
- biopsySites: updatedFormData.biopsySites.substring(0, 30),
275
- biopsyNotes: updatedFormData.biopsyNotes.substring(0, 50),
276
  });
277
 
 
 
 
 
 
 
 
 
278
  setFormData(prev => ({
279
  ...prev,
280
  ...updatedFormData
@@ -284,7 +312,7 @@ export function ReportPage({
284
  if (missingKeys.length === 0) {
285
  setAiError(null);
286
  } else {
287
- setAiError(`✓ Report generated! Note: Some fields were not returned by AI (${missingKeys.join(', ')})`);
288
  }
289
 
290
  } catch (err: any) {
 
206
  throw new Error('Invalid response from backend');
207
  }
208
 
209
+ // Get the report text - it should be a JSON string
210
+ let reportText: string = String(data.report).trim();
 
 
 
 
 
 
 
 
211
  let parsed: Record<string, string> = {};
212
+
213
+ console.log('[ReportPage] Raw response type:', typeof data.report);
214
+ console.log('[ReportPage] Raw response preview:', reportText.substring(0, 300));
215
+
216
+ // If response is already an object (shouldn't happen but handle it)
217
+ if (typeof data.report === 'object' && data.report !== null) {
218
+ console.log('[ReportPage] Response is already an object, using directly');
219
+ parsed = data.report;
220
+ } else {
221
+ // Response is a string - need to parse it
222
+ reportText = String(data.report).trim();
223
+
224
+ // Remove markdown code fence wrappers if present
225
+ if (reportText.startsWith('```')) {
226
+ reportText = reportText.replace(/^```(?:json)?\n/i, '').replace(/\n```\s*$/i, '').trim();
227
+ console.log('[ReportPage] Removed markdown fence');
228
+ }
229
+
230
+ // Clean up any escape sequences
231
+ reportText = reportText.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
232
 
233
+ console.log('[ReportPage] Cleaned text preview:', reportText.substring(0, 200));
234
+
235
+ // Try direct JSON parse first
236
+ try {
237
+ parsed = JSON.parse(reportText);
238
+ console.log('[ReportPage] Direct parse successful, keys:', Object.keys(parsed));
239
+ } catch (directErr) {
240
+ console.log('[ReportPage] Direct parse failed, attempting JSON extraction...');
241
+
242
+ // Try to find and extract the JSON object from the string
243
+ const jsonMatch = reportText.match(/\{[\s\S]*\}/);
244
+ if (jsonMatch) {
245
+ try {
246
+ parsed = JSON.parse(jsonMatch[0]);
247
+ console.log('[ReportPage] ✅ Extracted JSON successful, keys:', Object.keys(parsed));
248
+ } catch (extractErr) {
249
+ console.error('[ReportPage] JSON extraction failed:', extractErr);
250
+ throw new Error('Could not parse AI response as valid JSON');
251
+ }
252
+ } else {
253
+ throw new Error('No JSON object found in response');
254
  }
 
 
255
  }
256
  }
257
 
258
+ // At this point, parsed should be an object with the report fields
259
+ // Validate structure
260
+ if (!parsed || typeof parsed !== 'object') {
261
+ throw new Error('Parsed response is not a valid object');
262
+ }
263
+
264
  // Validate that all required keys are present
265
  const requiredKeys = ['examQuality', 'transformationZone', 'acetowL', 'nativeFindings',
266
  'aceticFindings', 'biopsySites', 'biopsyNotes', 'colposcopicFindings',
 
285
  biopsyNotes: String(parsed.biopsyNotes || '').trim(),
286
  };
287
 
288
+ // Verify each field is populated correctly
289
+ console.log('[ReportPage] Field validation:');
290
+ Object.entries(updatedFormData).forEach(([key, value]) => {
291
+ if (value.length > 0) {
292
+ console.log(` ✅ ${key}: ${value.substring(0, 60)}...`);
293
+ } else {
294
+ console.warn(` ❌ ${key}: EMPTY!`);
295
+ }
 
 
 
296
  });
297
 
298
+ // Additional safety check - if colposcopicFindings contains JSON structure, something went wrong
299
+ if (updatedFormData.colposcopicFindings.includes('{') && updatedFormData.colposcopicFindings.includes('"')) {
300
+ console.error('[ReportPage] ⚠️ WARNING: colposcopicFindings contains JSON! Parsing may have failed.');
301
+ console.error('Value:', updatedFormData.colposcopicFindings.substring(0, 200));
302
+ }
303
+
304
+ console.log('[ReportPage] AI generated fields:', updatedFormData);
305
+
306
  setFormData(prev => ({
307
  ...prev,
308
  ...updatedFormData
 
312
  if (missingKeys.length === 0) {
313
  setAiError(null);
314
  } else {
315
+ setAiError(`✓ Report generated! Some fields were missing: ${missingKeys.join(', ')}`);
316
  }
317
 
318
  } catch (err: any) {