Alnzac commited on
Commit
f5fad1d
Β·
1 Parent(s): e5c33cd

Fix AI report: JSON field mapping, local dev proxy, dotenv loading, fuller exam data sent to LLM

Browse files
Files changed (5) hide show
  1. .env.example +2 -2
  2. README.md +56 -3
  3. backend/app.py +27 -1
  4. src/pages/ReportPage.tsx +113 -69
  5. vite.config.ts +14 -0
.env.example CHANGED
@@ -1,7 +1,7 @@
1
  # Gemini API Key for LLM functionality
2
  # Get your API key from: https://makersuite.google.com/app/apikey
3
- GEMINI_API_KEY=your_gemini_api_key_here
4
- VITE_GEMINI_API_KEY=your_gemini_api_key_here
5
 
6
  # Optional: Model Configuration
7
  # GEMINI_MODEL=gemini-1.5-flash
 
1
  # Gemini API Key for LLM functionality
2
  # Get your API key from: https://makersuite.google.com/app/apikey
3
+ GEMINI_API_KEY=AIzaSyD9rMJVxxop-FusPIDfcdhJkIIdxa3X4vc
4
+ VITE_GEMINI_API_KEY=AIzaSyD9rMJVxxop-FusPIDfcdhJkIIdxa3X4vc
5
 
6
  # Optional: Model Configuration
7
  # GEMINI_MODEL=gemini-1.5-flash
README.md CHANGED
@@ -15,21 +15,74 @@ A comprehensive React-based colposcopy assistant application with image annotati
15
 
16
  ## Local Development
17
 
18
- 1. Install dependencies:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  ```bash
20
  npm install
21
  ```
22
 
23
- 2. Run development server:
 
 
 
24
  ```bash
25
  npm run dev
26
  ```
27
 
28
- 3. Build for production:
 
 
 
29
  ```bash
30
  npm run build
31
  ```
32
 
 
33
  ## Deploy to Hugging Face Spaces
34
 
35
  This project is configured for deployment on Hugging Face Spaces using Docker runtime.
 
15
 
16
  ## Local Development
17
 
18
+ The app has two parts that need to run at the same time:
19
+ - **Backend** – FastAPI (Python) on `http://localhost:8000`
20
+ - **Frontend** – Vite dev server (React) on `http://localhost:5173`
21
+
22
+ Vite is pre-configured to proxy all `/api/*` and `/infer/*` requests to the backend.
23
+
24
+ ### Step 1 – Set up your Gemini API key
25
+
26
+ Copy `.env.example` to `.env` and fill in your key:
27
+
28
+ ```bash
29
+ cp .env.example .env # or rename a copy manually on Windows
30
+ ```
31
+
32
+ Edit `.env`:
33
+ ```
34
+ GEMINI_API_KEY=your_actual_gemini_api_key_here
35
+ ```
36
+
37
+ Get a free key at: https://makersuite.google.com/app/apikey
38
+
39
+ ### Step 2 – Install Python dependencies (backend)
40
+
41
+ It is recommended to use a virtual environment:
42
+
43
+ ```bash
44
+ # Create and activate a virtual environment (Windows)
45
+ python -m venv .venv
46
+ .venv\Scripts\activate
47
+
48
+ # Install backend packages
49
+ pip install -r backend/requirements.txt
50
+ ```
51
+
52
+ ### Step 3 – Start the FastAPI backend
53
+
54
+ From the **project root folder**, run:
55
+
56
+ ```bash
57
+ uvicorn backend.app:app --host 0.0.0.0 --port 8000 --reload
58
+ ```
59
+
60
+ The API will be available at `http://localhost:8000`.
61
+ You can test the health endpoint: `http://localhost:8000/health`
62
+
63
+ ### Step 4 – Install Node.js dependencies (frontend)
64
+
65
  ```bash
66
  npm install
67
  ```
68
 
69
+ ### Step 5 – Start the Vite dev server
70
+
71
+ In a **separate terminal**, from the project root:
72
+
73
  ```bash
74
  npm run dev
75
  ```
76
 
77
+ The app will be available at `http://localhost:5173`.
78
+
79
+ ### Step 6 – Build for production
80
+
81
  ```bash
82
  npm run build
83
  ```
84
 
85
+
86
  ## Deploy to Hugging Face Spaces
87
 
88
  This project is configured for deployment on Hugging Face Spaces using Docker runtime.
backend/app.py CHANGED
@@ -13,7 +13,33 @@ import uvicorn
13
  import traceback
14
  import json
15
  from typing import List, Dict, Optional
16
- from .inference import infer_aw_contour, analyze_frame, analyze_video_frame, infer_cervix_bbox
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  # Import Google Gemini (optional - graceful degradation if not installed)
19
  try:
 
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
19
+ # is launched from project root (uvicorn backend.app:app) or from
20
+ # inside the backend/ folder (python app.py).
21
+ try:
22
+ from dotenv import load_dotenv
23
+ _here = os.path.dirname(os.path.abspath(__file__))
24
+ # Try backend/.env first, then project root .env
25
+ for _env_path in [
26
+ os.path.join(_here, ".env"),
27
+ os.path.join(_here, "..", ".env"),
28
+ ]:
29
+ if os.path.isfile(_env_path):
30
+ load_dotenv(_env_path)
31
+ print(f"βœ… Loaded .env from: {os.path.abspath(_env_path)}")
32
+ break
33
+ else:
34
+ print("⚠️ No .env file found. Set GEMINI_API_KEY in your environment.")
35
+ except ImportError:
36
+ pass
37
+
38
+
39
+ try:
40
+ from .inference import infer_aw_contour, analyze_frame, analyze_video_frame, infer_cervix_bbox
41
+ except ImportError:
42
+ from inference import infer_aw_contour, analyze_frame, analyze_video_frame, infer_cervix_bbox
43
 
44
  # Import Google Gemini (optional - graceful degradation if not installed)
45
  try:
src/pages/ReportPage.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import { useEffect, useRef, useState } from 'react';
2
  import { ArrowLeft, Download, FileText, CheckCircle, Camera, Sparkles, Loader2 } from 'lucide-react';
3
  import html2pdf from 'html2pdf.js';
4
- import { SYSTEM_PROMPT, DEMO_STEP_FINDINGS } from '../config/geminiConfig';
5
  import { sessionStore } from '../store/sessionStore';
6
 
7
 
@@ -97,26 +97,7 @@ export function ReportPage({
97
  // eslint-disable-next-line react-hooks/exhaustive-deps
98
  }, [JSON.stringify(formData)]);
99
 
100
- /** Convert a data-URL or object-URL to a Gemini-compatible base64 image part */
101
- const toImagePart = async (src: string): Promise<{ inlineData: { data: string; mimeType: string } } | null> => {
102
- try {
103
- const res = await fetch(src);
104
- const blob = await res.blob();
105
- return new Promise(resolve => {
106
- const reader = new FileReader();
107
- reader.onloadend = () => {
108
- const dataUrl = reader.result as string;
109
- const [header, data] = dataUrl.split(',');
110
- const mimeType = header.match(/:(.*?);/)?.[1] ?? 'image/png';
111
- resolve({ inlineData: { data, mimeType } });
112
- };
113
- reader.onerror = () => resolve(null);
114
- reader.readAsDataURL(blob);
115
- });
116
- } catch {
117
- return null;
118
- }
119
- };
120
 
121
  // On mount: restore all previously saved session data into the form
122
  useEffect(() => {
@@ -177,11 +158,10 @@ export function ReportPage({
177
  setAiError(null);
178
 
179
  try {
180
- // Prepare data for backend API
181
  const session = sessionStore.get();
182
  const mergedFindings = { ...(session.stepFindings ?? {}), ...(stepFindings as Record<string, any>) };
183
 
184
- // Prepare patient data
185
  const patientDataPayload = {
186
  name: formData.name,
187
  age: formData.age,
@@ -194,21 +174,24 @@ export function ReportPage({
194
  patientHistory: session.patientHistory,
195
  };
196
 
197
- // Prepare exam findings
198
  const examFindingsPayload = {
199
- native: mergedFindings.native || {},
200
  acetowhite: mergedFindings.acetowhite || {},
201
  greenFilter: mergedFindings.greenFilter || {},
202
  lugol: mergedFindings.lugol || {},
 
 
203
  };
204
 
205
- // Call backend API
206
  const response = await fetch('/api/generate-report', {
207
  method: 'POST',
208
  headers: { 'Content-Type': 'application/json' },
209
  body: JSON.stringify({
210
  patient_data: patientDataPayload,
211
  exam_findings: examFindingsPayload,
 
212
  }),
213
  });
214
 
@@ -218,21 +201,42 @@ export function ReportPage({
218
  }
219
 
220
  const data = await response.json();
221
-
222
  if (data.status !== 'success' || !data.report) {
223
  throw new Error('Invalid response from backend');
224
  }
225
 
226
- // Parse the report text and extract fields
227
- // The backend returns a formatted report, we'll try to parse it
228
- const reportText = data.report;
229
-
230
- // For now, put the entire report in colposcopicFindings
231
- // In the future, you could add more sophisticated parsing
 
 
 
 
 
 
 
 
 
 
 
 
232
  setFormData(prev => ({
233
  ...prev,
234
- colposcopicFindings: reportText,
 
 
 
 
 
 
 
 
 
235
  }));
 
236
  } catch (err: any) {
237
  console.error('Gemini error:', err);
238
  const msg = err?.message || err?.toString() || 'Unknown error';
@@ -242,6 +246,7 @@ export function ReportPage({
242
  }
243
  };
244
 
 
245
  const handleExportPDF = () => {
246
  if (!reportContentRef.current) return;
247
 
@@ -477,7 +482,7 @@ export function ReportPage({
477
  >
478
  <CheckCircle className="w-4 h-6" />
479
  Save & Continue
480
-
481
  </button>
482
  </div>
483
  {aiError && (
@@ -584,21 +589,21 @@ export function ReportPage({
584
  const session = sessionStore.get();
585
  const ph = session.patientHistory;
586
  if (!ph) return <p className="text-gray-500 italic text-xs">No patient history recorded</p>;
587
-
588
  const symptoms = [
589
  ph.postCoitalBleeding && 'Post-coital bleeding',
590
  ph.interMenstrualBleeding && 'Inter-menstrual bleeding',
591
  ph.persistentDischarge && 'Persistent discharge',
592
  ].filter(Boolean).join(', ');
593
-
594
  const pastProc = ph.pastProcedures
595
  ? Object.entries(ph.pastProcedures).filter(([, v]) => v).map(([k]) => k.toUpperCase()).join(', ')
596
  : '';
597
-
598
  const immunoList = ph.immunosuppression
599
  ? Object.entries(ph.immunosuppression).filter(([, v]) => v).map(([k]) => k.toUpperCase()).join(', ')
600
  : '';
601
-
602
  return (
603
  <div className="space-y-1.5 text-xs">
604
  {/* Pregnancy Status */}
@@ -606,32 +611,32 @@ export function ReportPage({
606
  {ph.pregnancyStatus === 'Pregnant' && ph.gestationalAgeWeeks && <p><strong>Gestational Age:</strong> {ph.gestationalAgeWeeks} weeks</p>}
607
  {ph.pregnancyStatus === 'Postpartum' && ph.monthsSinceLastDelivery && <p><strong>Months Since Last Delivery:</strong> {ph.monthsSinceLastDelivery}</p>}
608
  {ph.pregnancyStatus === 'Post-abortion' && ph.monthsSinceAbortion && <p><strong>Months Since Abortion:</strong> {ph.monthsSinceAbortion}</p>}
609
-
610
  {/* Demographics & Lifestyle */}
611
  {ph.menstrualStatus && <p><strong>Menstrual Status:</strong> {ph.menstrualStatus}</p>}
612
  {ph.sexualHistory && <p><strong>Sexual History:</strong> {ph.sexualHistory}</p>}
613
-
614
  {/* HPV Information */}
615
  {ph.hpvStatus && <p><strong>HPV Status:</strong> {ph.hpvStatus}</p>}
616
  {ph.hpvVaccination && <p><strong>HPV Vaccination:</strong> {ph.hpvVaccination}</p>}
617
  {ph.hpvDnaTypes && <p><strong>HPV DNA Types:</strong> {ph.hpvDnaTypes}</p>}
618
-
619
  {/* Presenting Symptoms */}
620
  {symptoms && <p><strong>Presenting Symptoms:</strong> {symptoms}</p>}
621
  {ph.symptomsNotes && <p><strong>Additional Symptoms:</strong> {ph.symptomsNotes}</p>}
622
-
623
  {/* Screening History */}
624
  {ph.papSmearResult && <p><strong>PAP Smear Result:</strong> {ph.papSmearResult}</p>}
625
  {ph.screeningNotes && <p><strong>Screening Notes:</strong> {ph.screeningNotes}</p>}
626
-
627
  {/* Past Procedures */}
628
  {pastProc && <p><strong>Past Cervical Procedures:</strong> {pastProc}</p>}
629
-
630
  {/* Risk Factors */}
631
  {ph.smoking && <p><strong>Smoking History:</strong> {ph.smoking}</p>}
632
  {immunoList && <p><strong>Immunosuppression:</strong> {immunoList}</p>}
633
  {ph.riskFactorsNotes && <p><strong>Risk Factors Notes:</strong> {ph.riskFactorsNotes}</p>}
634
-
635
  {/* Additional Patient Notes */}
636
  {ph.patientProfileNotes && <p><strong>Patient Profile Notes:</strong> {ph.patientProfileNotes}</p>}
637
  </div>
@@ -645,33 +650,72 @@ export function ReportPage({
645
  <div className="bg-[#05998c]/8 border-l-4 border-[#05998c] px-4 py-2.5 print:py-1.5">
646
  <h3 className="text-xs font-bold text-[#05998c] uppercase tracking-wider print:text-[10px]">Examination Findings</h3>
647
  </div>
648
- <div className="p-4 print:p-2 grid grid-cols-1 md:grid-cols-2 gap-4 print:gap-3">
649
- {/* Native Examination Findings */}
650
- <div className="border border-gray-200 rounded-lg p-3 print:border-gray-300">
651
- <h4 className="text-xs font-bold text-[#047569] mb-2 print:mb-1">πŸ“· Native Examination</h4>
652
- {formData.nativeFindings ? (
653
- <div className="text-xs whitespace-pre-wrap">{formData.nativeFindings}</div>
654
- ) : (
655
- <p className="text-gray-500 italic text-xs">Click &quot;Generate with AI&quot; to populate findings</p>
656
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657
  </div>
658
-
659
- {/* Acetic Acid Findings */}
660
- <div className="border border-gray-200 rounded-lg p-3 print:border-gray-300">
661
- <h4 className="text-xs font-bold text-[#047569] mb-2 print:mb-1">πŸ”¬ Acetic Acid Findings</h4>
662
- {formData.aceticFindings ? (
663
- <div className="text-xs whitespace-pre-wrap">{formData.aceticFindings}</div>
664
- ) : (
665
- <p className="text-gray-500 italic text-xs">Click &quot;Generate with AI&quot; to populate findings</p>
666
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
667
  </div>
668
  </div>
669
  </div>
670
 
 
671
  {/* ── EXAMINATION IMAGES ──────────────────────────── */}
672
- <div
673
  className="border border-gray-200 rounded-xl overflow-hidden print:rounded-none print:border-gray-300"
674
- style={{ marginTop: '150px' }}
675
  >
676
  <div className="bg-[#05998c]/8 border-l-4 border-[#05998c] px-4 py-2.5 print:py-1.5">
677
  <h3 className="text-xs font-bold text-[#05998c] uppercase tracking-wider print:text-[10px]">Examination Images</h3>
@@ -769,7 +813,7 @@ export function ReportPage({
769
  <p><strong>Iodine Staining:</strong> {bm.swedeScores.iodineStaining ?? 0}/2</p>
770
  </div>
771
  </div>
772
-
773
  {/* Total Score & Risk */}
774
  <div className="bg-gradient-to-br from-[#05998c] to-[#047569] rounded-lg p-3 print:bg-white print:border print:border-teal-200 text-white print:text-gray-900">
775
  <p className="text-[10px] font-bold uppercase tracking-wide mb-2 print:mb-1 opacity-90 print:opacity-100">Total Score</p>
@@ -799,13 +843,13 @@ export function ReportPage({
799
  <span className="text-[10px] font-bold text-[#05998c] uppercase tracking-wide print:text-[9px] block mb-1">Colposcopic Findings (Including Swede Score)</span>
800
  <textarea value={formData.colposcopicFindings} onChange={(e) => handleFormChange('colposcopicFindings', e.target.value)} placeholder="Describe colposcopic findings and include Swede score interpretation..." className="w-full text-gray-900 print:text-xs border border-gray-200 focus:border-[#05998c] outline-none bg-white p-2 print:p-1 resize-none rounded transition-colors" rows={3} />
801
  </div>
802
-
803
  {/* Treatment Plan */}
804
  <div>
805
  <span className="text-[10px] font-bold text-[#05998c] uppercase tracking-wide print:text-[9px] block mb-1">Treatment Plan</span>
806
  <textarea value={formData.treatmentPlan} onChange={(e) => handleFormChange('treatmentPlan', e.target.value)} placeholder="Recommended treatment plan..." className="w-full text-gray-900 print:text-xs border border-gray-200 focus:border-[#05998c] outline-none bg-transparent p-2 print:p-1 resize-none rounded transition-colors" rows={3} />
807
  </div>
808
-
809
  {/* Follow-up Plan */}
810
  <div>
811
  <span className="text-[10px] font-bold text-[#05998c] uppercase tracking-wide print:text-[9px] block mb-1">Follow-up Plan</span>
 
1
  import { useEffect, useRef, useState } from 'react';
2
  import { ArrowLeft, Download, FileText, CheckCircle, Camera, Sparkles, Loader2 } from 'lucide-react';
3
  import html2pdf from 'html2pdf.js';
4
+ import { SYSTEM_PROMPT } from '../config/geminiConfig';
5
  import { sessionStore } from '../store/sessionStore';
6
 
7
 
 
97
  // eslint-disable-next-line react-hooks/exhaustive-deps
98
  }, [JSON.stringify(formData)]);
99
 
100
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
  // On mount: restore all previously saved session data into the form
103
  useEffect(() => {
 
158
  setAiError(null);
159
 
160
  try {
 
161
  const session = sessionStore.get();
162
  const mergedFindings = { ...(session.stepFindings ?? {}), ...(stepFindings as Record<string, any>) };
163
 
164
+ // Build patient data payload
165
  const patientDataPayload = {
166
  name: formData.name,
167
  age: formData.age,
 
174
  patientHistory: session.patientHistory,
175
  };
176
 
177
+ // Build exam findings payload β€” include ALL steps + acetic categories + biopsy/swede
178
  const examFindingsPayload = {
179
+ native: mergedFindings.native || session.nativeFindings || {},
180
  acetowhite: mergedFindings.acetowhite || {},
181
  greenFilter: mergedFindings.greenFilter || {},
182
  lugol: mergedFindings.lugol || {},
183
+ aceticFindings: session.aceticFindings || {},
184
+ biopsyMarkings: session.biopsyMarkings || {},
185
  };
186
 
187
+ // Call backend β€” send SYSTEM_PROMPT so Gemini returns strict JSON
188
  const response = await fetch('/api/generate-report', {
189
  method: 'POST',
190
  headers: { 'Content-Type': 'application/json' },
191
  body: JSON.stringify({
192
  patient_data: patientDataPayload,
193
  exam_findings: examFindingsPayload,
194
+ system_prompt: SYSTEM_PROMPT, // ← tells Gemini to return JSON
195
  }),
196
  });
197
 
 
201
  }
202
 
203
  const data = await response.json();
 
204
  if (data.status !== 'success' || !data.report) {
205
  throw new Error('Invalid response from backend');
206
  }
207
 
208
+ // Strip possible markdown code fences before parsing
209
+ let reportText: string = data.report.trim();
210
+ if (reportText.startsWith('```')) {
211
+ reportText = reportText.replace(/^```[a-z]*\n?/i, '').replace(/```\s*$/, '').trim();
212
+ }
213
+
214
+ // Parse structured JSON returned by Gemini
215
+ let parsed: Record<string, string> = {};
216
+ try {
217
+ parsed = JSON.parse(reportText);
218
+ } catch {
219
+ // If Gemini didn't return valid JSON, fall back: put full text into colposcopicFindings
220
+ console.warn('AI response was not valid JSON, using as plain text:', reportText);
221
+ setFormData(prev => ({ ...prev, colposcopicFindings: reportText }));
222
+ return;
223
+ }
224
+
225
+ // Map every JSON key to the correct form field
226
  setFormData(prev => ({
227
  ...prev,
228
+ examQuality: parsed.examQuality ?? prev.examQuality,
229
+ transformationZone: parsed.transformationZone ?? prev.transformationZone,
230
+ acetowL: parsed.acetowL ?? prev.acetowL,
231
+ nativeFindings: parsed.nativeFindings ?? prev.nativeFindings,
232
+ aceticFindings: parsed.aceticFindings ?? prev.aceticFindings,
233
+ colposcopicFindings: parsed.colposcopicFindings ?? prev.colposcopicFindings,
234
+ treatmentPlan: parsed.treatmentPlan ?? prev.treatmentPlan,
235
+ followUp: parsed.followUp ?? prev.followUp,
236
+ biopsySites: parsed.biopsySites ?? prev.biopsySites,
237
+ biopsyNotes: parsed.biopsyNotes ?? prev.biopsyNotes,
238
  }));
239
+
240
  } catch (err: any) {
241
  console.error('Gemini error:', err);
242
  const msg = err?.message || err?.toString() || 'Unknown error';
 
246
  }
247
  };
248
 
249
+
250
  const handleExportPDF = () => {
251
  if (!reportContentRef.current) return;
252
 
 
482
  >
483
  <CheckCircle className="w-4 h-6" />
484
  Save & Continue
485
+
486
  </button>
487
  </div>
488
  {aiError && (
 
589
  const session = sessionStore.get();
590
  const ph = session.patientHistory;
591
  if (!ph) return <p className="text-gray-500 italic text-xs">No patient history recorded</p>;
592
+
593
  const symptoms = [
594
  ph.postCoitalBleeding && 'Post-coital bleeding',
595
  ph.interMenstrualBleeding && 'Inter-menstrual bleeding',
596
  ph.persistentDischarge && 'Persistent discharge',
597
  ].filter(Boolean).join(', ');
598
+
599
  const pastProc = ph.pastProcedures
600
  ? Object.entries(ph.pastProcedures).filter(([, v]) => v).map(([k]) => k.toUpperCase()).join(', ')
601
  : '';
602
+
603
  const immunoList = ph.immunosuppression
604
  ? Object.entries(ph.immunosuppression).filter(([, v]) => v).map(([k]) => k.toUpperCase()).join(', ')
605
  : '';
606
+
607
  return (
608
  <div className="space-y-1.5 text-xs">
609
  {/* Pregnancy Status */}
 
611
  {ph.pregnancyStatus === 'Pregnant' && ph.gestationalAgeWeeks && <p><strong>Gestational Age:</strong> {ph.gestationalAgeWeeks} weeks</p>}
612
  {ph.pregnancyStatus === 'Postpartum' && ph.monthsSinceLastDelivery && <p><strong>Months Since Last Delivery:</strong> {ph.monthsSinceLastDelivery}</p>}
613
  {ph.pregnancyStatus === 'Post-abortion' && ph.monthsSinceAbortion && <p><strong>Months Since Abortion:</strong> {ph.monthsSinceAbortion}</p>}
614
+
615
  {/* Demographics & Lifestyle */}
616
  {ph.menstrualStatus && <p><strong>Menstrual Status:</strong> {ph.menstrualStatus}</p>}
617
  {ph.sexualHistory && <p><strong>Sexual History:</strong> {ph.sexualHistory}</p>}
618
+
619
  {/* HPV Information */}
620
  {ph.hpvStatus && <p><strong>HPV Status:</strong> {ph.hpvStatus}</p>}
621
  {ph.hpvVaccination && <p><strong>HPV Vaccination:</strong> {ph.hpvVaccination}</p>}
622
  {ph.hpvDnaTypes && <p><strong>HPV DNA Types:</strong> {ph.hpvDnaTypes}</p>}
623
+
624
  {/* Presenting Symptoms */}
625
  {symptoms && <p><strong>Presenting Symptoms:</strong> {symptoms}</p>}
626
  {ph.symptomsNotes && <p><strong>Additional Symptoms:</strong> {ph.symptomsNotes}</p>}
627
+
628
  {/* Screening History */}
629
  {ph.papSmearResult && <p><strong>PAP Smear Result:</strong> {ph.papSmearResult}</p>}
630
  {ph.screeningNotes && <p><strong>Screening Notes:</strong> {ph.screeningNotes}</p>}
631
+
632
  {/* Past Procedures */}
633
  {pastProc && <p><strong>Past Cervical Procedures:</strong> {pastProc}</p>}
634
+
635
  {/* Risk Factors */}
636
  {ph.smoking && <p><strong>Smoking History:</strong> {ph.smoking}</p>}
637
  {immunoList && <p><strong>Immunosuppression:</strong> {immunoList}</p>}
638
  {ph.riskFactorsNotes && <p><strong>Risk Factors Notes:</strong> {ph.riskFactorsNotes}</p>}
639
+
640
  {/* Additional Patient Notes */}
641
  {ph.patientProfileNotes && <p><strong>Patient Profile Notes:</strong> {ph.patientProfileNotes}</p>}
642
  </div>
 
650
  <div className="bg-[#05998c]/8 border-l-4 border-[#05998c] px-4 py-2.5 print:py-1.5">
651
  <h3 className="text-xs font-bold text-[#05998c] uppercase tracking-wider print:text-[10px]">Examination Findings</h3>
652
  </div>
653
+ <div className="p-4 print:p-2 space-y-3 print:space-y-2">
654
+ {/* Exam Quality / TZ / Acetowhite row */}
655
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3 print:gap-2">
656
+ <div>
657
+ <span className="text-[10px] font-bold text-[#05998c] uppercase tracking-wide print:text-[9px] block mb-1">Exam Quality</span>
658
+ <input
659
+ type="text"
660
+ value={formData.examQuality}
661
+ onChange={(e) => handleFormChange('examQuality', e.target.value)}
662
+ placeholder="Adequate / Inadequate"
663
+ className="block w-full font-medium text-gray-900 py-1 print:py-0.5 print:text-xs border-b-2 border-gray-200 focus:border-[#05998c] outline-none bg-transparent transition-colors print:border-gray-400"
664
+ />
665
+ </div>
666
+ <div>
667
+ <span className="text-[10px] font-bold text-[#05998c] uppercase tracking-wide print:text-[9px] block mb-1">Transformation Zone</span>
668
+ <input
669
+ type="text"
670
+ value={formData.transformationZone}
671
+ onChange={(e) => handleFormChange('transformationZone', e.target.value)}
672
+ placeholder="Type I / II / III"
673
+ className="block w-full font-medium text-gray-900 py-1 print:py-0.5 print:text-xs border-b-2 border-gray-200 focus:border-[#05998c] outline-none bg-transparent transition-colors print:border-gray-400"
674
+ />
675
+ </div>
676
+ <div>
677
+ <span className="text-[10px] font-bold text-[#05998c] uppercase tracking-wide print:text-[9px] block mb-1">Acetowhite Lesion</span>
678
+ <input
679
+ type="text"
680
+ value={formData.acetowL}
681
+ onChange={(e) => handleFormChange('acetowL', e.target.value)}
682
+ placeholder="Present / Absent"
683
+ className="block w-full font-medium text-gray-900 py-1 print:py-0.5 print:text-xs border-b-2 border-gray-200 focus:border-[#05998c] outline-none bg-transparent transition-colors print:border-gray-400"
684
+ />
685
+ </div>
686
  </div>
687
+
688
+ {/* Native & Acetic findings side-by-side */}
689
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 print:gap-2">
690
+ <div className="border border-gray-200 rounded-lg p-3 print:border-gray-300">
691
+ <h4 className="text-xs font-bold text-[#047569] mb-2 print:mb-1">οΏ½ Native Examination</h4>
692
+ <textarea
693
+ value={formData.nativeFindings}
694
+ onChange={(e) => handleFormChange('nativeFindings', e.target.value)}
695
+ placeholder="Click &quot;Generate with AI&quot; to populate findings"
696
+ className="w-full text-gray-900 text-xs border border-gray-200 focus:border-[#05998c] outline-none bg-transparent p-2 print:p-1 resize-none rounded transition-colors"
697
+ rows={4}
698
+ />
699
+ </div>
700
+ <div className="border border-gray-200 rounded-lg p-3 print:border-gray-300">
701
+ <h4 className="text-xs font-bold text-[#047569] mb-2 print:mb-1">πŸ”¬ Acetic Acid Findings</h4>
702
+ <textarea
703
+ value={formData.aceticFindings}
704
+ onChange={(e) => handleFormChange('aceticFindings', e.target.value)}
705
+ placeholder="Click &quot;Generate with AI&quot; to populate findings"
706
+ className="w-full text-gray-900 text-xs border border-gray-200 focus:border-[#05998c] outline-none bg-transparent p-2 print:p-1 resize-none rounded transition-colors"
707
+ rows={4}
708
+ />
709
+ </div>
710
  </div>
711
  </div>
712
  </div>
713
 
714
+
715
  {/* ── EXAMINATION IMAGES ──────────────────────────── */}
716
+ <div
717
  className="border border-gray-200 rounded-xl overflow-hidden print:rounded-none print:border-gray-300"
718
+ style={{ marginTop: '150px' }}
719
  >
720
  <div className="bg-[#05998c]/8 border-l-4 border-[#05998c] px-4 py-2.5 print:py-1.5">
721
  <h3 className="text-xs font-bold text-[#05998c] uppercase tracking-wider print:text-[10px]">Examination Images</h3>
 
813
  <p><strong>Iodine Staining:</strong> {bm.swedeScores.iodineStaining ?? 0}/2</p>
814
  </div>
815
  </div>
816
+
817
  {/* Total Score & Risk */}
818
  <div className="bg-gradient-to-br from-[#05998c] to-[#047569] rounded-lg p-3 print:bg-white print:border print:border-teal-200 text-white print:text-gray-900">
819
  <p className="text-[10px] font-bold uppercase tracking-wide mb-2 print:mb-1 opacity-90 print:opacity-100">Total Score</p>
 
843
  <span className="text-[10px] font-bold text-[#05998c] uppercase tracking-wide print:text-[9px] block mb-1">Colposcopic Findings (Including Swede Score)</span>
844
  <textarea value={formData.colposcopicFindings} onChange={(e) => handleFormChange('colposcopicFindings', e.target.value)} placeholder="Describe colposcopic findings and include Swede score interpretation..." className="w-full text-gray-900 print:text-xs border border-gray-200 focus:border-[#05998c] outline-none bg-white p-2 print:p-1 resize-none rounded transition-colors" rows={3} />
845
  </div>
846
+
847
  {/* Treatment Plan */}
848
  <div>
849
  <span className="text-[10px] font-bold text-[#05998c] uppercase tracking-wide print:text-[9px] block mb-1">Treatment Plan</span>
850
  <textarea value={formData.treatmentPlan} onChange={(e) => handleFormChange('treatmentPlan', e.target.value)} placeholder="Recommended treatment plan..." className="w-full text-gray-900 print:text-xs border border-gray-200 focus:border-[#05998c] outline-none bg-transparent p-2 print:p-1 resize-none rounded transition-colors" rows={3} />
851
  </div>
852
+
853
  {/* Follow-up Plan */}
854
  <div>
855
  <span className="text-[10px] font-bold text-[#05998c] uppercase tracking-wide print:text-[9px] block mb-1">Follow-up Plan</span>
vite.config.ts CHANGED
@@ -4,4 +4,18 @@ import react from '@vitejs/plugin-react'
4
  // https://vitejs.dev/config/
5
  export default defineConfig({
6
  plugins: [react()],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  })
 
4
  // https://vitejs.dev/config/
5
  export default defineConfig({
6
  plugins: [react()],
7
+ server: {
8
+ proxy: {
9
+ // Forward all /api/* requests to the FastAPI backend
10
+ '/api': {
11
+ target: 'http://localhost:8000',
12
+ changeOrigin: true,
13
+ },
14
+ // Forward all /infer/* requests to the FastAPI backend
15
+ '/infer': {
16
+ target: 'http://localhost:8000',
17
+ changeOrigin: true,
18
+ },
19
+ },
20
+ },
21
  })