Hanan-Alnakhal commited on
Commit
8a693e2
Β·
verified Β·
1 Parent(s): 8a2666a

Upload 12 files

Browse files
.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ venv/
7
+ env/
8
+ .env
9
+ .venv
10
+ chroma_db/
11
+ *.pdf
12
+ .DS_Store
app.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Lab Report Decoder - Flask Application
3
+ Professional web interface for lab report analysis
4
+ """
5
+
6
+ from flask import Flask, render_template, request, jsonify, session
7
+ from werkzeug.utils import secure_filename
8
+ import os
9
+ import tempfile
10
+ import secrets
11
+ from pdf_extractor import LabReportExtractor
12
+ from rag_engine import LabReportRAG
13
+ from dotenv import load_dotenv
14
+
15
+ load_dotenv()
16
+
17
+ app = Flask(__name__)
18
+ app.secret_key = os.getenv('SECRET_KEY', secrets.token_hex(16))
19
+
20
+ # Note: No OpenAI API key needed - using Hugging Face models!
21
+ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
22
+ app.config['UPLOAD_FOLDER'] = tempfile.gettempdir()
23
+
24
+ # Initialize RAG system (singleton)
25
+ rag_system = None
26
+
27
+ def get_rag_system():
28
+ """Lazy load RAG system"""
29
+ global rag_system
30
+ if rag_system is None:
31
+ rag_system = LabReportRAG()
32
+ return rag_system
33
+
34
+ @app.route('/')
35
+ def index():
36
+ """Main page"""
37
+ return render_template('index.html')
38
+
39
+ @app.route('/api/upload', methods=['POST'])
40
+ def upload_file():
41
+ """Handle PDF upload and extraction"""
42
+ try:
43
+ if 'file' not in request.files:
44
+ return jsonify({'error': 'No file provided'}), 400
45
+
46
+ file = request.files['file']
47
+
48
+ if file.filename == '':
49
+ return jsonify({'error': 'No file selected'}), 400
50
+
51
+ if not file.filename.lower().endswith('.pdf'):
52
+ return jsonify({'error': 'Only PDF files are allowed'}), 400
53
+
54
+ # Save file temporarily
55
+ filename = secure_filename(file.filename)
56
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
57
+ file.save(filepath)
58
+
59
+ try:
60
+ # Extract lab results
61
+ extractor = LabReportExtractor()
62
+ results = extractor.extract_from_pdf(filepath)
63
+
64
+ if not results:
65
+ return jsonify({'error': 'No lab results found in PDF'}), 400
66
+
67
+ # Convert to JSON-serializable format
68
+ results_data = [
69
+ {
70
+ 'test_name': r.test_name,
71
+ 'value': r.value,
72
+ 'unit': r.unit,
73
+ 'reference_range': r.reference_range,
74
+ 'status': r.status
75
+ }
76
+ for r in results
77
+ ]
78
+
79
+ # Store in session
80
+ session['results'] = results_data
81
+
82
+ return jsonify({
83
+ 'success': True,
84
+ 'results': results_data,
85
+ 'count': len(results_data)
86
+ })
87
+
88
+ finally:
89
+ # Clean up temp file
90
+ if os.path.exists(filepath):
91
+ os.remove(filepath)
92
+
93
+ except Exception as e:
94
+ return jsonify({'error': str(e)}), 500
95
+
96
+ @app.route('/api/explain', methods=['POST'])
97
+ def explain_results():
98
+ """Generate explanations for lab results"""
99
+ try:
100
+ results_data = session.get('results')
101
+
102
+ if not results_data:
103
+ return jsonify({'error': 'No results found. Please upload a PDF first.'}), 400
104
+
105
+ # Convert back to LabResult objects
106
+ from pdf_extractor import LabResult
107
+ results = [
108
+ LabResult(
109
+ test_name=r['test_name'],
110
+ value=r['value'],
111
+ unit=r['unit'],
112
+ reference_range=r['reference_range'],
113
+ status=r['status']
114
+ )
115
+ for r in results_data
116
+ ]
117
+
118
+ # Generate explanations
119
+ rag = get_rag_system()
120
+ explanations = rag.explain_all_results(results)
121
+
122
+ return jsonify({
123
+ 'success': True,
124
+ 'explanations': explanations
125
+ })
126
+
127
+ except Exception as e:
128
+ return jsonify({'error': str(e)}), 500
129
+
130
+ @app.route('/api/ask', methods=['POST'])
131
+ def ask_question():
132
+ """Answer follow-up questions"""
133
+ try:
134
+ data = request.get_json()
135
+ question = data.get('question', '').strip()
136
+
137
+ if not question:
138
+ return jsonify({'error': 'No question provided'}), 400
139
+
140
+ results_data = session.get('results')
141
+
142
+ if not results_data:
143
+ return jsonify({'error': 'No results found. Please upload a PDF first.'}), 400
144
+
145
+ # Convert back to LabResult objects
146
+ from pdf_extractor import LabResult
147
+ results = [
148
+ LabResult(
149
+ test_name=r['test_name'],
150
+ value=r['value'],
151
+ unit=r['unit'],
152
+ reference_range=r['reference_range'],
153
+ status=r['status']
154
+ )
155
+ for r in results_data
156
+ ]
157
+
158
+ # Get answer
159
+ rag = get_rag_system()
160
+ answer = rag.answer_followup_question(question, results)
161
+
162
+ return jsonify({
163
+ 'success': True,
164
+ 'question': question,
165
+ 'answer': answer
166
+ })
167
+
168
+ except Exception as e:
169
+ return jsonify({'error': str(e)}), 500
170
+
171
+ @app.route('/api/summary', methods=['GET'])
172
+ def get_summary():
173
+ """Generate overall summary"""
174
+ try:
175
+ results_data = session.get('results')
176
+
177
+ if not results_data:
178
+ return jsonify({'error': 'No results found. Please upload a PDF first.'}), 400
179
+
180
+ # Convert back to LabResult objects
181
+ from pdf_extractor import LabResult
182
+ results = [
183
+ LabResult(
184
+ test_name=r['test_name'],
185
+ value=r['value'],
186
+ unit=r['unit'],
187
+ reference_range=r['reference_range'],
188
+ status=r['status']
189
+ )
190
+ for r in results_data
191
+ ]
192
+
193
+ # Generate summary
194
+ rag = get_rag_system()
195
+ summary = rag.generate_summary(results)
196
+
197
+ # Calculate statistics
198
+ stats = {
199
+ 'total': len(results),
200
+ 'normal': sum(1 for r in results if r.status == 'normal'),
201
+ 'high': sum(1 for r in results if r.status == 'high'),
202
+ 'low': sum(1 for r in results if r.status == 'low'),
203
+ 'unknown': sum(1 for r in results if r.status == 'unknown')
204
+ }
205
+
206
+ return jsonify({
207
+ 'success': True,
208
+ 'summary': summary,
209
+ 'stats': stats
210
+ })
211
+
212
+ except Exception as e:
213
+ return jsonify({'error': str(e)}), 500
214
+
215
+ @app.route('/api/clear', methods=['POST'])
216
+ def clear_session():
217
+ """Clear session data"""
218
+ session.clear()
219
+ return jsonify({'success': True})
220
+
221
+ @app.errorhandler(413)
222
+ def request_entity_too_large(error):
223
+ return jsonify({'error': 'File too large. Maximum size is 16MB.'}), 413
224
+
225
+ @app.errorhandler(500)
226
+ def internal_error(error):
227
+ return jsonify({'error': 'Internal server error'}), 500
228
+
229
+ if __name__ == '__main__':
230
+ if not(os.path.isdir('chroma_db/')):
231
+ os.system("python build_vector_db.py")
232
+ #any available port
233
+ port = int(os.environ.get("PORT", 5000))
234
+ app.run(host="0.0.0.0", port=port)
build_vector_db.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Build Vector Database for Lab Report Decoder
3
+ Uses Hugging Face sentence-transformers for embeddings
4
+ """
5
+
6
+ import os
7
+ from pathlib import Path
8
+ from sentence_transformers import SentenceTransformer
9
+ import chromadb
10
+ from chromadb.config import Settings
11
+ import glob
12
+
13
+ def load_documents_from_directory(directory: str) -> list:
14
+ """Load all text files from a directory"""
15
+ documents = []
16
+
17
+ if not os.path.exists(directory):
18
+ print(f"⚠️ Directory not found: {directory}")
19
+ return documents
20
+
21
+ # Find all .txt files
22
+ txt_files = glob.glob(os.path.join(directory, "**", "*.txt"), recursive=True)
23
+
24
+ for filepath in txt_files:
25
+ try:
26
+ with open(filepath, 'r', encoding='utf-8') as f:
27
+ content = f.read()
28
+ if content.strip():
29
+ documents.append({
30
+ 'content': content,
31
+ 'source': filepath,
32
+ 'filename': os.path.basename(filepath)
33
+ })
34
+ except Exception as e:
35
+ print(f"Error reading {filepath}: {e}")
36
+
37
+ return documents
38
+
39
+ def chunk_text(text: str, chunk_size: int = 1000, overlap: int = 200) -> list:
40
+ """Split text into overlapping chunks"""
41
+ chunks = []
42
+ start = 0
43
+
44
+ while start < len(text):
45
+ end = start + chunk_size
46
+ chunk = text[start:end]
47
+
48
+ # Try to break at sentence boundary
49
+ if end < len(text):
50
+ last_period = chunk.rfind('.')
51
+ last_newline = chunk.rfind('\n')
52
+ break_point = max(last_period, last_newline)
53
+
54
+ if break_point > chunk_size * 0.5: # Only if break point is reasonable
55
+ chunk = chunk[:break_point + 1]
56
+ end = start + break_point + 1
57
+
58
+ chunks.append(chunk.strip())
59
+ start = end - overlap
60
+
61
+ return chunks
62
+
63
+ def build_knowledge_base():
64
+ """Build the vector database from medical documents"""
65
+
66
+ print("πŸ“š Loading medical documents...")
67
+
68
+ # Load documents from data directory
69
+ data_dir = 'data/'
70
+ all_documents = []
71
+
72
+ if not os.path.exists(data_dir):
73
+ print(f"⚠️ Creating data directory: {data_dir}")
74
+ os.makedirs(data_dir, exist_ok=True)
75
+ os.makedirs(os.path.join(data_dir, 'lab_markers'), exist_ok=True)
76
+ os.makedirs(os.path.join(data_dir, 'nutrition'), exist_ok=True)
77
+ os.makedirs(os.path.join(data_dir, 'conditions'), exist_ok=True)
78
+ print("⚠️ Please add medical reference documents to the data/ folder")
79
+ return None
80
+
81
+ # Load from all subdirectories
82
+ for subdir in ['lab_markers', 'nutrition', 'conditions']:
83
+ subdir_path = os.path.join(data_dir, subdir)
84
+ docs = load_documents_from_directory(subdir_path)
85
+ all_documents.extend(docs)
86
+
87
+ if not all_documents:
88
+ print("⚠️ No documents found in data/ directory")
89
+ print("Please add .txt files with medical information")
90
+ return None
91
+
92
+ print(f"βœ… Loaded {len(all_documents)} documents")
93
+
94
+ # Chunk documents
95
+ print("βœ‚οΈ Splitting documents into chunks...")
96
+ all_chunks = []
97
+ all_metadata = []
98
+
99
+ for doc in all_documents:
100
+ chunks = chunk_text(doc['content'], chunk_size=1000, overlap=200)
101
+ for i, chunk in enumerate(chunks):
102
+ all_chunks.append(chunk)
103
+ all_metadata.append({
104
+ 'source': doc['source'],
105
+ 'filename': doc['filename'],
106
+ 'chunk_id': i
107
+ })
108
+
109
+ print(f"βœ… Created {len(all_chunks)} text chunks")
110
+
111
+ # Load embedding model
112
+ print("🧠 Loading embedding model (this may take a moment)...")
113
+ embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
114
+ print("βœ… Embedding model loaded")
115
+
116
+ # Create embeddings
117
+ print("πŸ”„ Creating embeddings (this may take a few minutes)...")
118
+ embeddings = embedding_model.encode(
119
+ all_chunks,
120
+ show_progress_bar=True,
121
+ convert_to_numpy=True
122
+ )
123
+ print(f"βœ… Created {len(embeddings)} embeddings")
124
+
125
+ # Create ChromaDB collection
126
+ print("πŸ’Ύ Building ChromaDB vector store...")
127
+
128
+ # Initialize client
129
+ db_path = "./chroma_db"
130
+ client = chromadb.PersistentClient(path=db_path)
131
+
132
+ # Delete existing collection if it exists
133
+ try:
134
+ client.delete_collection("lab_reports")
135
+ print("πŸ—‘οΈ Deleted existing collection")
136
+ except:
137
+ pass
138
+
139
+ # Create new collection
140
+ collection = client.create_collection(
141
+ name="lab_reports",
142
+ metadata={"description": "Medical lab report information"}
143
+ )
144
+
145
+ # Add documents in batches
146
+ batch_size = 100
147
+ for i in range(0, len(all_chunks), batch_size):
148
+ batch_chunks = all_chunks[i:i + batch_size]
149
+ batch_embeddings = embeddings[i:i + batch_size].tolist()
150
+ batch_ids = [f"doc_{j}" for j in range(i, i + len(batch_chunks))]
151
+ batch_metadata = all_metadata[i:i + batch_size]
152
+
153
+ collection.add(
154
+ documents=batch_chunks,
155
+ embeddings=batch_embeddings,
156
+ ids=batch_ids,
157
+ metadatas=batch_metadata
158
+ )
159
+
160
+ print("βœ… Vector database built successfully!")
161
+ print(f"πŸ“ Database location: {db_path}")
162
+ print(f"πŸ“Š Total vectors: {len(all_chunks)}")
163
+
164
+ return collection
165
+
166
+ def test_retrieval(collection):
167
+ """Test the retrieval system"""
168
+ if collection is None:
169
+ print("\n⚠️ No collection to test")
170
+ return
171
+
172
+ print("\nπŸ” Testing retrieval system...")
173
+
174
+ # Load embedding model for queries
175
+ embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
176
+
177
+ test_queries = [
178
+ "What does low hemoglobin mean?",
179
+ "What foods are high in iron?",
180
+ "Normal range for glucose"
181
+ ]
182
+
183
+ for query in test_queries:
184
+ print(f"\nπŸ“ Query: {query}")
185
+
186
+ # Create query embedding
187
+ query_embedding = embedding_model.encode(query).tolist()
188
+
189
+ # Search
190
+ results = collection.query(
191
+ query_embeddings=[query_embedding],
192
+ n_results=2
193
+ )
194
+
195
+ if results and results['documents']:
196
+ print(f" βœ… Found {len(results['documents'][0])} relevant documents")
197
+ print(f" πŸ“„ Top result preview: {results['documents'][0][0][:150]}...")
198
+ else:
199
+ print(" ❌ No results found")
200
+
201
+ if __name__ == "__main__":
202
+ print("πŸš€ Building Lab Report Decoder Vector Database\n")
203
+
204
+ # Build the database
205
+ collection = build_knowledge_base()
206
+
207
+ # Test it
208
+ if collection:
209
+ test_retrieval(collection)
210
+ print("\nπŸŽ‰ Setup complete! You can now run the Flask application.")
211
+ else:
212
+ print("\n⚠️ Please add medical documents to the data/ folder and run again.")
data/conditions/anemia.txt ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Understanding Anemia
2
+
3
+ WHAT IS ANEMIA?
4
+ Anemia occurs when you don't have enough healthy red blood cells to carry adequate oxygen to your body's tissues. This makes you feel tired and weak.
5
+
6
+ TYPES OF ANEMIA:
7
+
8
+ Iron-Deficiency Anemia (Most Common)
9
+ - Causes: Not enough iron in diet, blood loss, pregnancy, poor absorption
10
+ - Symptoms: Fatigue, weakness, pale skin, cold hands and feet, brittle nails
11
+ - Treatment: Iron-rich diet, iron supplements, address underlying cause
12
+
13
+ Vitamin Deficiency Anemia
14
+ - Causes: Lack of vitamin B12 or folate
15
+ - Symptoms: Similar to iron deficiency, plus numbness, balance problems
16
+ - Treatment: Dietary changes, supplements, B12 injections if needed
17
+
18
+ Anemia of Chronic Disease
19
+ - Causes: Cancer, kidney disease, rheumatoid arthritis, Crohn's disease
20
+ - Treatment: Treat underlying condition, may need erythropoietin therapy
21
+
22
+ SYMPTOMS TO WATCH FOR:
23
+ - Persistent fatigue and weakness
24
+ - Pale or yellowish skin
25
+ - Irregular heartbeat
26
+ - Shortness of breath
27
+ - Dizziness or lightheadedness
28
+ - Chest pain
29
+ - Cold hands and feet
30
+ - Headaches
31
+
32
+ WHEN TO SEE A DOCTOR:
33
+ - Severe fatigue interfering with daily activities
34
+ - Persistent symptoms despite dietary changes
35
+ - Family history of inherited anemias
36
+ - Heavy menstrual bleeding
37
+ - Signs of internal bleeding
38
+
39
+ PREVENTION:
40
+ - Eat iron-rich foods regularly
41
+ - Include vitamin C to help absorption
42
+ - Don't skip meals
43
+ - If vegetarian, plan meals carefully for adequate iron
44
+ - Consider supplementation if at risk (pregnant women, heavy periods)
45
+
46
+ LIFESTYLE TIPS:
47
+ - Pace yourself - take breaks when tired
48
+ - Stay hydrated
49
+ - Avoid excessive exercise until levels improve
50
+ - Manage stress
51
+ - Get adequate sleep
data/lab_markers/complete_blood_count.txt ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Complete Blood Count (CBC)
2
+
3
+ A Complete Blood Count measures several components of your blood to assess overall health and detect disorders.
4
+
5
+ WHITE BLOOD CELLS (WBC/Leukocytes)
6
+ Normal Range: 4,500-11,000 cells/mcL
7
+ What it measures: White blood cells fight infections
8
+ High WBC: May indicate infection, inflammation, leukemia, stress, or tissue damage
9
+ Low WBC: May indicate bone marrow problems, autoimmune disorders, or certain medications
10
+ Dietary support: Vitamin C (citrus, bell peppers), Vitamin E (nuts, seeds), zinc (shellfish, legumes)
11
+
12
+ RED BLOOD CELLS (RBC/Erythrocytes)
13
+ Normal Range:
14
+ - Men: 4.7-6.1 million cells/mcL
15
+ - Women: 4.2-5.4 million cells/mcL
16
+ What it measures: Red blood cells carry oxygen throughout your body
17
+ High RBC: May indicate dehydration, lung disease, or polycythemia
18
+ Low RBC: May indicate anemia, blood loss, or nutritional deficiencies
19
+
20
+ HEMOGLOBIN (Hgb)
21
+ Normal Range:
22
+ - Men: 13.5-17.5 g/dL
23
+ - Women: 12.0-15.5 g/dL
24
+ What it measures: The oxygen-carrying protein in red blood cells
25
+ High Hemoglobin: Dehydration, lung disease, smoking, high altitude
26
+ Low Hemoglobin (Anemia): Iron deficiency, B12 deficiency, chronic disease, blood loss
27
+ Foods to increase: Red meat, liver, spinach, lentils, fortified cereals, pumpkin seeds
28
+ Take with Vitamin C to enhance iron absorption
29
+
30
+ HEMATOCRIT (Hct)
31
+ Normal Range:
32
+ - Men: 38.3-48.6%
33
+ - Women: 35.5-44.9%
34
+ What it measures: Percentage of blood volume made up of red blood cells
35
+ High: Dehydration, lung disease, polycythemia
36
+ Low: Anemia, blood loss, malnutrition
37
+
38
+ PLATELETS
39
+ Normal Range: 150,000-400,000 platelets/mcL
40
+ What it measures: Blood clotting cells
41
+ High: May indicate inflammation, iron deficiency, or bone marrow disorders
42
+ Low: May indicate bleeding disorders, autoimmune conditions, or certain medications
data/lab_markers/metabolic_panel.txt ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Comprehensive Metabolic Panel (CMP)
2
+
3
+ GLUCOSE (Blood Sugar)
4
+ Normal Range: 70-100 mg/dL (fasting)
5
+ What it measures: Amount of sugar in your blood
6
+ High (Hyperglycemia): May indicate diabetes, prediabetes, stress, or certain medications
7
+ - Symptoms: Increased thirst, frequent urination, fatigue, blurred vision
8
+ - Dietary changes: Reduce refined carbs, increase fiber, choose whole grains
9
+ - Lifestyle: Regular exercise, maintain healthy weight, manage stress
10
+ Low (Hypoglycemia): May indicate insulin overproduction or certain medications
11
+ - Symptoms: Shakiness, sweating, confusion, hunger
12
+ - What to do: Eat small frequent meals, balance carbs with protein
13
+
14
+ CALCIUM
15
+ Normal Range: 8.5-10.5 mg/dL
16
+ What it measures: Calcium levels for bone health and muscle function
17
+ High: May indicate hyperparathyroidism, certain cancers, or excess vitamin D
18
+ Low: May indicate vitamin D deficiency, malabsorption, or kidney disease
19
+ Foods rich in calcium: Dairy products, leafy greens, fortified plant milk, sardines with bones
20
+
21
+ SODIUM
22
+ Normal Range: 135-145 mEq/L
23
+ What it measures: Electrolyte balance and hydration
24
+ High: Dehydration, excess salt intake, kidney problems
25
+ Low: Overhydration, heart failure, kidney disease, certain medications
26
+ Balance tips: Moderate salt intake, stay hydrated, eat potassium-rich foods
27
+
28
+ POTASSIUM
29
+ Normal Range: 3.5-5.0 mEq/L
30
+ What it measures: Electrolyte important for heart and muscle function
31
+ High: Kidney disease, certain medications, excess supplementation
32
+ Low: Diuretics, vomiting, diarrhea, poor diet
33
+ Foods high in potassium: Bananas, potatoes, spinach, beans, avocados
34
+
35
+ BUN (Blood Urea Nitrogen)
36
+ Normal Range: 7-20 mg/dL
37
+ What it measures: Kidney function
38
+ High: Dehydration, kidney disease, high protein diet
39
+ Low: Liver disease, malnutrition, overhydration
40
+
41
+ CREATININE
42
+ Normal Range: 0.7-1.3 mg/dL (men), 0.6-1.1 mg/dL (women)
43
+ What it measures: Kidney function
44
+ High: Kidney disease, dehydration, muscle injury
45
+ Low: Low muscle mass, malnutrition
data/nutrition/iron_deficiency.txt ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Iron Deficiency and Dietary Solutions
2
+
3
+ IRON-RICH FOODS
4
+
5
+ Heme Iron (Better Absorbed - from animal sources):
6
+ - Red meat (beef, lamb): 2-3 mg per 3 oz
7
+ - Liver (beef, chicken): 5-7 mg per 3 oz
8
+ - Oysters and clams: 8-24 mg per 3 oz
9
+ - Sardines: 2-3 mg per 3 oz
10
+ - Dark meat poultry: 1-2 mg per 3 oz
11
+
12
+ Non-Heme Iron (from plant sources):
13
+ - Lentils: 3 mg per 1/2 cup cooked
14
+ - Spinach: 3 mg per 1/2 cup cooked
15
+ - Tofu: 3 mg per 1/2 cup
16
+ - Beans (kidney, black): 2-3 mg per 1/2 cup
17
+ - Fortified cereals: 4-18 mg per serving
18
+ - Pumpkin seeds: 4 mg per 1 oz
19
+ - Quinoa: 2 mg per cup cooked
20
+ - Dark chocolate: 3 mg per 1 oz
21
+
22
+ ENHANCING IRON ABSORPTION
23
+
24
+ Pair iron-rich foods with Vitamin C:
25
+ - Citrus fruits (oranges, grapefruit, lemon)
26
+ - Bell peppers
27
+ - Strawberries
28
+ - Tomatoes
29
+ - Broccoli
30
+ Example: Spinach salad with strawberries and orange segments
31
+
32
+ FOODS THAT BLOCK IRON ABSORPTION (avoid with iron-rich meals):
33
+ - Coffee and tea (wait 1-2 hours)
34
+ - Dairy products (calcium competes with iron)
35
+ - High-fiber foods in excess
36
+ - Antacids
37
+
38
+ MEAL IDEAS FOR IRON BOOST:
39
+ Breakfast: Fortified cereal with strawberries and orange juice
40
+ Lunch: Spinach salad with chickpeas, bell peppers, and lemon dressing
41
+ Dinner: Lean beef with broccoli and tomato sauce
42
+ Snack: Pumpkin seeds with dried apricots
43
+
44
+ SUPPLEMENTATION TIPS:
45
+ - Take iron supplements on an empty stomach with vitamin C
46
+ - May cause constipation - increase fiber and water
47
+ - Avoid taking with calcium supplements or antacids
48
+ - Consult doctor for proper dosage
pdf_extractor.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PDF Extraction Module for Lab Reports
3
+ Extracts lab test names, values, and ranges from uploaded PDF files
4
+ """
5
+
6
+ import pdfplumber
7
+ import re
8
+ from typing import Dict, List, Optional
9
+ from dataclasses import dataclass
10
+
11
+ @dataclass
12
+ class LabResult:
13
+ """Represents a single lab test result"""
14
+ test_name: str
15
+ value: str
16
+ unit: str
17
+ reference_range: str
18
+ status: str # 'normal', 'high', 'low', 'unknown'
19
+
20
+ class LabReportExtractor:
21
+ """Extract structured data from lab report PDFs"""
22
+
23
+ def __init__(self):
24
+ # Common lab test patterns
25
+ self.test_patterns = [
26
+ r'(Hemoglobin|Hgb|Hb)\s*:?\s*([\d.]+)\s*([a-zA-Z/]+)?\s*(?:Ref\.?\s*Range:?\s*)?([\d.\-\s]+)',
27
+ r'(WBC|White Blood Cell|Leukocyte)\s*:?\s*([\d.]+)\s*([a-zA-Z/]+)?\s*(?:Ref\.?\s*Range:?\s*)?([\d.\-\s]+)',
28
+ r'(Glucose|Blood Sugar)\s*:?\s*([\d.]+)\s*([a-zA-Z/]+)?\s*(?:Ref\.?\s*Range:?\s*)?([\d.\-\s]+)',
29
+ r'(Iron|Ferritin)\s*:?\s*([\d.]+)\s*([a-zA-Z/]+)?\s*(?:Ref\.?\s*Range:?\s*)?([\d.\-\s]+)',
30
+ r'(Cholesterol|LDL|HDL)\s*:?\s*([\d.]+)\s*([a-zA-Z/]+)?\s*(?:Ref\.?\s*Range:?\s*)?([\d.\-\s]+)',
31
+ ]
32
+
33
+ def extract_from_pdf(self, pdf_path: str) -> List[LabResult]:
34
+ """Extract lab results from PDF file"""
35
+ results = []
36
+
37
+ with pdfplumber.open(pdf_path) as pdf:
38
+ for page in pdf.pages:
39
+ text = page.extract_text()
40
+
41
+ # Try to extract tables first (more structured)
42
+ tables = page.extract_tables()
43
+ if tables:
44
+ results.extend(self._parse_tables(tables))
45
+
46
+ # Fall back to pattern matching
47
+ results.extend(self._parse_text(text))
48
+
49
+ # Remove duplicates
50
+ unique_results = self._deduplicate_results(results)
51
+
52
+ return unique_results
53
+
54
+ def _parse_tables(self, tables: List) -> List[LabResult]:
55
+ """Parse lab results from extracted tables"""
56
+ results = []
57
+
58
+ for table in tables:
59
+ if not table or len(table) < 2:
60
+ continue
61
+
62
+ # Assume first row is header
63
+ headers = [h.lower() if h else '' for h in table[0]]
64
+
65
+ # Find relevant columns
66
+ test_col = self._find_column(headers, ['test', 'name', 'component'])
67
+ value_col = self._find_column(headers, ['value', 'result'])
68
+ unit_col = self._find_column(headers, ['unit', 'units'])
69
+ range_col = self._find_column(headers, ['range', 'reference', 'normal'])
70
+
71
+ # Parse data rows
72
+ for row in table[1:]:
73
+ if not row or len(row) <= max(test_col or 0, value_col or 0):
74
+ continue
75
+
76
+ test_name = row[test_col] if test_col is not None else ''
77
+ value = row[value_col] if value_col is not None else ''
78
+ unit = row[unit_col] if unit_col is not None else ''
79
+ ref_range = row[range_col] if range_col is not None else ''
80
+
81
+ if test_name and value:
82
+ status = self._determine_status(value, ref_range)
83
+ results.append(LabResult(
84
+ test_name=test_name.strip(),
85
+ value=str(value).strip(),
86
+ unit=str(unit).strip() if unit else '',
87
+ reference_range=str(ref_range).strip() if ref_range else '',
88
+ status=status
89
+ ))
90
+
91
+ return results
92
+
93
+ def _parse_text(self, text: str) -> List[LabResult]:
94
+ """Parse lab results using regex patterns"""
95
+ results = []
96
+
97
+ for pattern in self.test_patterns:
98
+ matches = re.finditer(pattern, text, re.IGNORECASE)
99
+ for match in matches:
100
+ groups = match.groups()
101
+ if len(groups) >= 2:
102
+ test_name = groups[0]
103
+ value = groups[1]
104
+ unit = groups[2] if len(groups) > 2 else ''
105
+ ref_range = groups[3] if len(groups) > 3 else ''
106
+
107
+ status = self._determine_status(value, ref_range)
108
+ results.append(LabResult(
109
+ test_name=test_name,
110
+ value=value,
111
+ unit=unit or '',
112
+ reference_range=ref_range or '',
113
+ status=status
114
+ ))
115
+
116
+ return results
117
+
118
+ def _find_column(self, headers: List[str], keywords: List[str]) -> Optional[int]:
119
+ """Find column index by keywords"""
120
+ for i, header in enumerate(headers):
121
+ for keyword in keywords:
122
+ if keyword in header:
123
+ return i
124
+ return None
125
+
126
+ def _determine_status(self, value: str, ref_range: str) -> str:
127
+ """Determine if value is normal, high, or low"""
128
+ try:
129
+ val = float(value.replace(',', ''))
130
+
131
+ # Parse reference range
132
+ range_match = re.search(r'([\d.]+)\s*-\s*([\d.]+)', ref_range)
133
+ if range_match:
134
+ low = float(range_match.group(1))
135
+ high = float(range_match.group(2))
136
+
137
+ if val < low:
138
+ return 'low'
139
+ elif val > high:
140
+ return 'high'
141
+ else:
142
+ return 'normal'
143
+ except (ValueError, AttributeError):
144
+ pass
145
+
146
+ return 'unknown'
147
+
148
+ def _deduplicate_results(self, results: List[LabResult]) -> List[LabResult]:
149
+ """Remove duplicate test results"""
150
+ seen = set()
151
+ unique = []
152
+
153
+ for result in results:
154
+ key = (result.test_name.lower(), result.value)
155
+ if key not in seen:
156
+ seen.add(key)
157
+ unique.append(result)
158
+
159
+ return unique
160
+
161
+ # Example usage
162
+ if __name__ == "__main__":
163
+ extractor = LabReportExtractor()
164
+ results = extractor.extract_from_pdf("sample_lab_report.pdf")
165
+
166
+ for result in results:
167
+ print(f"{result.test_name}: {result.value} {result.unit} [{result.status}]")
168
+ print(f" Reference: {result.reference_range}")
rag_engine.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RAG Query Engine for Lab Report Decoder
3
+ Uses Hugging Face models for embeddings and generation
4
+ """
5
+
6
+ from sentence_transformers import SentenceTransformer
7
+ from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
8
+ import chromadb
9
+ from chromadb.config import Settings
10
+ from typing import List, Dict
11
+ from pdf_extractor import LabResult
12
+ import torch
13
+
14
+ class LabReportRAG:
15
+ """RAG system for explaining lab results using Hugging Face models"""
16
+
17
+ def __init__(self, db_path: str = "./chroma_db"):
18
+ """Initialize the RAG system with Hugging Face models"""
19
+
20
+ print("πŸ”„ Loading Hugging Face models...")
21
+
22
+ # Use smaller, faster models for embeddings
23
+ self.embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
24
+
25
+ # Use a medical-focused or general LLM
26
+ # Options:
27
+ # - "microsoft/Phi-3-mini-4k-instruct" (good balance)
28
+ # - "google/flan-t5-base" (lighter)
29
+ # - "meta-llama/Llama-2-7b-chat-hf" (requires auth)
30
+
31
+ model_name = "microsoft/Phi-3-mini-4k-instruct"
32
+
33
+ try:
34
+ self.tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
35
+ self.llm = AutoModelForCausalLM.from_pretrained(
36
+ model_name,
37
+ trust_remote_code=True,
38
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
39
+ device_map="auto" if torch.cuda.is_available() else None
40
+ )
41
+ print(f"βœ… Loaded model: {model_name}")
42
+ except Exception as e:
43
+ print(f"⚠️ Could not load {model_name}, falling back to simpler model")
44
+ # Fallback to lighter model
45
+ self.text_generator = pipeline(
46
+ "text-generation",
47
+ model="google/flan-t5-base",
48
+ max_length=512
49
+ )
50
+ self.llm = None
51
+
52
+ # Load vector store
53
+ try:
54
+ self.client = chromadb.PersistentClient(path=db_path)
55
+ self.collection = self.client.get_collection("lab_reports")
56
+ print("βœ… Vector database loaded")
57
+ except Exception as e:
58
+ print(f"⚠️ No vector database found. Please run build_vector_db.py first.")
59
+ self.collection = None
60
+
61
+ def _generate_with_phi(self, prompt: str, max_tokens: int = 512) -> str:
62
+ """Generate text using Phi-3 model"""
63
+ inputs = self.tokenizer(prompt, return_tensors="pt", truncation=True, max_length=2048)
64
+
65
+ if torch.cuda.is_available():
66
+ inputs = {k: v.to('cuda') for k, v in inputs.items()}
67
+
68
+ outputs = self.llm.generate(
69
+ **inputs,
70
+ max_new_tokens=max_tokens,
71
+ temperature=0.7,
72
+ do_sample=True,
73
+ top_p=0.9
74
+ )
75
+
76
+ response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
77
+ # Remove the prompt from response
78
+ response = response.replace(prompt, "").strip()
79
+ return response
80
+
81
+ def _generate_with_fallback(self, prompt: str) -> str:
82
+ """Generate text using fallback pipeline"""
83
+ result = self.text_generator(prompt, max_length=512, num_return_sequences=1)
84
+ return result[0]['generated_text']
85
+
86
+ def _generate_text(self, prompt: str) -> str:
87
+ """Generate text using available model"""
88
+ try:
89
+ if self.llm is not None:
90
+ return self._generate_with_phi(prompt)
91
+ else:
92
+ return self._generate_with_fallback(prompt)
93
+ except Exception as e:
94
+ print(f"Generation error: {e}")
95
+ return "Sorry, I encountered an error generating the explanation."
96
+
97
+ def _retrieve_context(self, query: str, k: int = 3) -> str:
98
+ """Retrieve relevant context from vector database"""
99
+ if self.collection is None:
100
+ return "No medical reference data available."
101
+
102
+ try:
103
+ # Create query embedding
104
+ query_embedding = self.embedding_model.encode(query).tolist()
105
+
106
+ # Query the collection
107
+ results = self.collection.query(
108
+ query_embeddings=[query_embedding],
109
+ n_results=k
110
+ )
111
+
112
+ # Combine documents
113
+ if results and results['documents']:
114
+ context = "\n\n".join(results['documents'][0])
115
+ return context
116
+ else:
117
+ return "No relevant information found."
118
+ except Exception as e:
119
+ print(f"Retrieval error: {e}")
120
+ return "Error retrieving medical information."
121
+
122
+ def explain_result(self, result: LabResult) -> str:
123
+ """Generate explanation for a single lab result"""
124
+
125
+ # Retrieve relevant context
126
+ query = f"{result.test_name} {result.status} meaning causes treatment"
127
+ context = self._retrieve_context(query, k=3)
128
+
129
+ # Create prompt
130
+ prompt = f"""You are a helpful medical assistant. Explain this lab result in simple terms.
131
+
132
+ Medical Information:
133
+ {context}
134
+
135
+ Lab Test: {result.test_name}
136
+ Value: {result.value} {result.unit}
137
+ Reference Range: {result.reference_range}
138
+ Status: {result.status}
139
+
140
+ Please explain:
141
+ 1. What this test measures
142
+ 2. What this result means
143
+ 3. Possible causes if abnormal
144
+ 4. Dietary recommendations if applicable
145
+
146
+ Keep it simple and clear. Answer:"""
147
+
148
+ # Generate explanation
149
+ explanation = self._generate_text(prompt)
150
+
151
+ return explanation
152
+
153
+ def explain_all_results(self, results: List[LabResult]) -> Dict[str, str]:
154
+ """Generate explanations for all lab results"""
155
+ explanations = {}
156
+
157
+ for result in results:
158
+ print(f"Explaining {result.test_name}...")
159
+ explanation = self.explain_result(result)
160
+ explanations[result.test_name] = explanation
161
+
162
+ return explanations
163
+
164
+ def answer_followup_question(self, question: str, lab_results: List[LabResult]) -> str:
165
+ """Answer follow-up questions about lab results"""
166
+
167
+ # Create context from lab results
168
+ results_context = "\n".join([
169
+ f"{r.test_name}: {r.value} {r.unit} (Status: {r.status}, Range: {r.reference_range})"
170
+ for r in lab_results
171
+ ])
172
+
173
+ # Retrieve relevant medical information
174
+ medical_context = self._retrieve_context(question, k=3)
175
+
176
+ # Create prompt
177
+ prompt = f"""You are a medical assistant. Answer this question based on the patient's lab results and medical information.
178
+
179
+ Patient's Lab Results:
180
+ {results_context}
181
+
182
+ Medical Information:
183
+ {medical_context}
184
+
185
+ Question: {question}
186
+
187
+ Provide a clear, helpful answer. Answer:"""
188
+
189
+ # Generate answer
190
+ answer = self._generate_text(prompt)
191
+
192
+ return answer
193
+
194
+ def generate_summary(self, results: List[LabResult]) -> str:
195
+ """Generate overall summary of lab results"""
196
+
197
+ abnormal = [r for r in results if r.status in ['high', 'low']]
198
+ normal = [r for r in results if r.status == 'normal']
199
+
200
+ if not abnormal:
201
+ return "βœ… Great news! All your lab results are within normal ranges. Keep up the good work with your health!"
202
+
203
+ # Get context about abnormal results
204
+ queries = [f"{r.test_name} {r.status}" for r in abnormal]
205
+ combined_query = " ".join(queries)
206
+ context = self._retrieve_context(combined_query, k=4)
207
+
208
+ # Create summary prompt
209
+ abnormal_list = "\n".join([
210
+ f"- {r.test_name}: {r.value} {r.unit} ({r.status})"
211
+ for r in abnormal
212
+ ])
213
+
214
+ prompt = f"""Provide a brief summary of these lab results.
215
+
216
+ Normal Results: {len(normal)} tests
217
+ Abnormal Results: {len(abnormal)} tests
218
+
219
+ Abnormal Tests:
220
+ {abnormal_list}
221
+
222
+ Medical Context:
223
+ {context}
224
+
225
+ Write a 2-3 paragraph summary explaining what these results mean overall and general recommendations. Be reassuring but honest. Summary:"""
226
+
227
+ # Generate summary
228
+ summary = self._generate_text(prompt)
229
+
230
+ return summary
231
+
232
+
233
+ # Example usage
234
+ if __name__ == "__main__":
235
+ from pdf_extractor import LabResult
236
+
237
+ # Initialize RAG system
238
+ print("Initializing RAG system...")
239
+ rag = LabReportRAG()
240
+
241
+ # Example result
242
+ test_result = LabResult(
243
+ test_name="Hemoglobin",
244
+ value="10.5",
245
+ unit="g/dL",
246
+ reference_range="12.0-15.5",
247
+ status="low"
248
+ )
249
+
250
+ # Generate explanation
251
+ print("\nGenerating explanation...")
252
+ explanation = rag.explain_result(test_result)
253
+ print(f"\n{explanation}")
static/script.js ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== GLOBAL STATE =====
2
+ let currentResults = null;
3
+ let explanations = null;
4
+
5
+ // ===== INITIALIZATION =====
6
+ document.addEventListener('DOMContentLoaded', () => {
7
+ // Navigation is now handled via onclick="showSection", but we init other listeners
8
+ initFileUpload();
9
+ initScrollAnimations();
10
+ initEnterKey();
11
+ });
12
+
13
+ // ===== NAVIGATION (Compatible with Tailwind HTML) =====
14
+ function showSection(sectionId) {
15
+ // Hide all sections
16
+ document.querySelectorAll('.section').forEach(sec => {
17
+ sec.classList.remove('active');
18
+ sec.style.display = 'none';
19
+ });
20
+
21
+ // Show target section
22
+ const target = document.getElementById(sectionId);
23
+ if (target) {
24
+ target.style.display = 'block';
25
+ // Small timeout to allow display:block to apply before adding active class for animation
26
+ setTimeout(() => {
27
+ target.classList.add('active');
28
+ }, 10);
29
+
30
+ // Scroll to top
31
+ window.scrollTo({ top: 0, behavior: 'smooth' });
32
+ }
33
+ }
34
+
35
+ // ===== SCROLL ANIMATIONS =====
36
+ function initScrollAnimations() {
37
+ const observer = new IntersectionObserver((entries) => {
38
+ entries.forEach(entry => {
39
+ if (entry.isIntersecting) {
40
+ entry.target.classList.remove('opacity-0', 'translate-y-4');
41
+ entry.target.classList.add('opacity-100', 'translate-y-0');
42
+ }
43
+ });
44
+ }, { threshold: 0.1 });
45
+
46
+ // Add animation classes to cards
47
+ document.querySelectorAll('.feature-card, .upload-card').forEach(card => {
48
+ card.classList.add('transition-all', 'duration-700', 'opacity-0', 'translate-y-4');
49
+ observer.observe(card);
50
+ });
51
+ }
52
+
53
+ // ===== FILE UPLOAD =====
54
+ function initFileUpload() {
55
+ const fileInput = document.getElementById('fileInput');
56
+ const uploadArea = document.getElementById('uploadArea');
57
+
58
+ if (!fileInput || !uploadArea) return;
59
+
60
+ // Click to upload (handled by onclick in HTML, but keeping listener just in case)
61
+ fileInput.addEventListener('change', handleFileSelect);
62
+
63
+ // Drag and drop Visuals
64
+ uploadArea.addEventListener('dragover', (e) => {
65
+ e.preventDefault();
66
+ // Add Tailwind classes for drag state
67
+ uploadArea.classList.add('border-secondary', 'bg-blue-50', 'scale-[1.02]');
68
+ });
69
+
70
+ uploadArea.addEventListener('dragleave', () => {
71
+ // Remove Tailwind classes
72
+ uploadArea.classList.remove('border-secondary', 'bg-blue-50', 'scale-[1.02]');
73
+ });
74
+
75
+ uploadArea.addEventListener('drop', (e) => {
76
+ e.preventDefault();
77
+ uploadArea.classList.remove('border-secondary', 'bg-blue-50', 'scale-[1.02]');
78
+
79
+ const file = e.dataTransfer.files[0];
80
+ if (file && file.type === 'application/pdf') {
81
+ uploadFile(file);
82
+ } else {
83
+ showToast('Please upload a PDF file', 'error');
84
+ }
85
+ });
86
+ }
87
+
88
+ function handleFileSelect(e) {
89
+ const file = e.target.files[0];
90
+ if (file) {
91
+ uploadFile(file);
92
+ }
93
+ }
94
+
95
+ async function uploadFile(file) {
96
+ const formData = new FormData();
97
+ formData.append('file', file);
98
+
99
+ // Show progress
100
+ document.getElementById('uploadArea').classList.add('hidden');
101
+ document.getElementById('uploadProgress').classList.remove('hidden');
102
+
103
+ try {
104
+ const response = await fetch('/api/upload', {
105
+ method: 'POST',
106
+ body: formData
107
+ });
108
+
109
+ const data = await response.json();
110
+
111
+ if (!response.ok) {
112
+ throw new Error(data.error || 'Upload failed');
113
+ }
114
+
115
+ currentResults = data.results;
116
+ showToast(`βœ“ Found ${data.count} lab results!`, 'success');
117
+
118
+ // Generate explanations
119
+ await generateExplanations();
120
+
121
+ // Display results
122
+ displayResults();
123
+
124
+ // Switch to results tab automatically
125
+ showSection('results-section');
126
+
127
+ } catch (error) {
128
+ showToast(error.message, 'error');
129
+ resetUploadArea();
130
+ }
131
+ }
132
+
133
+ async function generateExplanations() {
134
+ // Optional: show a mini toast or loading indicator
135
+ try {
136
+ const response = await fetch('/api/explain', { method: 'POST' });
137
+ const data = await response.json();
138
+ if (!response.ok) throw new Error(data.error);
139
+ explanations = data.explanations;
140
+ } catch (error) {
141
+ console.warn('Auto-explanation generation failed:', error);
142
+ // We continue anyway, results will just say "Loading..." or show basic info
143
+ }
144
+ }
145
+
146
+ function displayResults() {
147
+ const container = document.getElementById('resultsContainer');
148
+
149
+ if (!currentResults || currentResults.length === 0) {
150
+ container.innerHTML = '<div class="text-center text-gray-500 py-10">No results found</div>';
151
+ return;
152
+ }
153
+
154
+ // Update summary stats in the Summary tab
155
+ updateSummaryStats();
156
+
157
+ // Clear container
158
+ container.innerHTML = '';
159
+
160
+ // Create result cards with Tailwind Styling
161
+ currentResults.forEach((result, index) => {
162
+ const card = createResultCard(result, index);
163
+ container.appendChild(card);
164
+ });
165
+
166
+ // Reset upload area for next time
167
+ resetUploadArea();
168
+ }
169
+
170
+ function updateSummaryStats() {
171
+ const stats = { normal: 0, high: 0, low: 0 };
172
+
173
+ currentResults.forEach(result => {
174
+ // Normalize status string
175
+ const status = result.status ? result.status.toLowerCase() : 'normal';
176
+ if (status.includes('high')) stats.high++;
177
+ else if (status.includes('low')) stats.low++;
178
+ else stats.normal++;
179
+ });
180
+
181
+ // Update the DOM elements in the Summary Section
182
+ document.getElementById('normalCount').textContent = stats.normal;
183
+ document.getElementById('highCount').textContent = stats.high;
184
+ document.getElementById('lowCount').textContent = stats.low;
185
+
186
+ // Reveal the stats container
187
+ const statsContainer = document.getElementById('summaryStats');
188
+ if(statsContainer) statsContainer.classList.remove('hidden');
189
+
190
+ const generateBtn = document.getElementById('generateSummaryBtn');
191
+ if(generateBtn) generateBtn.classList.remove('hidden');
192
+ }
193
+
194
+ function createResultCard(result, index) {
195
+ const card = document.createElement('div');
196
+ // TAILWIND STYLING: Card container
197
+ card.className = 'bg-white rounded-xl shadow-sm border border-slate-100 p-5 mb-4 hover:shadow-md transition-shadow';
198
+
199
+ // Status colors
200
+ let statusColors = 'bg-green-100 text-green-700'; // Default normal
201
+ let borderClass = 'border-l-4 border-green-500';
202
+
203
+ const statusLower = result.status ? result.status.toLowerCase() : '';
204
+ if (statusLower.includes('high')) {
205
+ statusColors = 'bg-red-100 text-red-700';
206
+ borderClass = 'border-l-4 border-red-500';
207
+ } else if (statusLower.includes('low')) {
208
+ statusColors = 'bg-yellow-100 text-yellow-800';
209
+ borderClass = 'border-l-4 border-yellow-500';
210
+ }
211
+
212
+ const explanation = explanations && explanations[result.test_name]
213
+ ? explanations[result.test_name]
214
+ : 'Analysis available in Chat or Summary.';
215
+
216
+ // HTML Structure using Tailwind
217
+ card.innerHTML = `
218
+ <div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 ${borderClass} pl-4">
219
+ <div class="flex-grow">
220
+ <h3 class="text-lg font-bold text-slate-800">${escapeHtml(result.test_name)}</h3>
221
+ <p class="text-sm text-slate-500">Ref Range: ${escapeHtml(result.reference_range || 'N/A')}</p>
222
+ </div>
223
+
224
+ <div class="flex items-center gap-3 w-full md:w-auto justify-between md:justify-end">
225
+ <span class="text-xl font-bold text-slate-700">${escapeHtml(result.value)} <span class="text-sm font-normal text-slate-500">${escapeHtml(result.unit)}</span></span>
226
+ <span class="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wide ${statusColors}">
227
+ ${escapeHtml(result.status)}
228
+ </span>
229
+ </div>
230
+ </div>
231
+
232
+ <div class="mt-4 pt-3 border-t border-slate-50">
233
+ <p class="text-sm text-slate-600 leading-relaxed">
234
+ <span class="font-semibold text-primary">Insight:</span> ${escapeHtml(explanation)}
235
+ </p>
236
+ </div>
237
+ `;
238
+
239
+ return card;
240
+ }
241
+
242
+ function resetUploadArea() {
243
+ document.getElementById('uploadArea').classList.remove('hidden');
244
+ document.getElementById('uploadProgress').classList.add('hidden');
245
+ document.getElementById('fileInput').value = '';
246
+ }
247
+
248
+ // ===== SUMMARY GENERATION =====
249
+ async function generateSummary() {
250
+ if (!currentResults) {
251
+ showToast('Please upload a lab report first', 'error');
252
+ return;
253
+ }
254
+
255
+ const contentDiv = document.getElementById('summaryContent');
256
+ contentDiv.innerHTML = '<div class="flex justify-center p-8"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div></div>';
257
+
258
+ try {
259
+ const response = await fetch('/api/summary');
260
+ const data = await response.json();
261
+
262
+ if (!response.ok) throw new Error(data.error);
263
+
264
+ // Render summary with Markdown-like paragraphs
265
+ contentDiv.innerHTML = `
266
+ <div class="prose prose-slate max-w-none">
267
+ <h3 class="text-xl font-semibold mb-4 text-primary">Analysis Report</h3>
268
+ <div class="text-slate-700 leading-relaxed whitespace-pre-line">
269
+ ${escapeHtml(data.summary).replace(/\n/g, '<br>')}
270
+ </div>
271
+ </div>
272
+ `;
273
+
274
+ } catch (error) {
275
+ showToast('Error: ' + error.message, 'error');
276
+ contentDiv.innerHTML = '<p class="text-red-500 text-center">Failed to generate summary.</p>';
277
+ }
278
+ }
279
+
280
+ // ===== CHAT FUNCTIONALITY =====
281
+ function initEnterKey() {
282
+ const input = document.getElementById('chatInput');
283
+ if (input) {
284
+ input.addEventListener('keypress', (e) => {
285
+ if (e.key === 'Enter') askQuestion();
286
+ });
287
+ }
288
+ }
289
+
290
+ async function askQuestion() {
291
+ if (!currentResults) {
292
+ showToast('Please upload a lab report first', 'error');
293
+ return;
294
+ }
295
+
296
+ const input = document.getElementById('chatInput');
297
+ const question = input.value.trim();
298
+
299
+ if (!question) return;
300
+
301
+ // Clear input
302
+ input.value = '';
303
+
304
+ // Add user message
305
+ addChatMessage(question, 'user');
306
+
307
+ // Show loading
308
+ const loadingId = addChatMessage('Analyzing...', 'assistant', true);
309
+
310
+ try {
311
+ const response = await fetch('/api/ask', {
312
+ method: 'POST',
313
+ headers: { 'Content-Type': 'application/json' },
314
+ body: JSON.stringify({ question })
315
+ });
316
+
317
+ const data = await response.json();
318
+ if (!response.ok) throw new Error(data.error);
319
+
320
+ // Remove loading
321
+ const loadingEl = document.getElementById(loadingId);
322
+ if(loadingEl) loadingEl.remove();
323
+
324
+ // Add response
325
+ addChatMessage(data.answer, 'assistant');
326
+
327
+ } catch (error) {
328
+ const loadingEl = document.getElementById(loadingId);
329
+ if(loadingEl) loadingEl.remove();
330
+ showToast(error.message, 'error');
331
+ }
332
+ }
333
+
334
+ function addChatMessage(text, sender, isLoading = false) {
335
+ const container = document.getElementById('chatMessages');
336
+
337
+ const wrapper = document.createElement('div');
338
+ // Flex alignment based on sender
339
+ wrapper.className = `flex w-full mb-4 ${sender === 'user' ? 'justify-end' : 'justify-start'}`;
340
+
341
+ const bubble = document.createElement('div');
342
+ // Bubble Styling
343
+ const baseStyle = "max-w-[85%] rounded-2xl px-5 py-3 shadow-sm text-sm leading-relaxed";
344
+ const userStyle = "bg-secondary text-white rounded-br-none"; // Blue bubble
345
+ const aiStyle = "bg-slate-100 text-slate-800 rounded-tl-none border border-slate-200"; // Grey bubble
346
+
347
+ bubble.className = `${baseStyle} ${sender === 'user' ? userStyle : aiStyle} ${isLoading ? 'animate-pulse' : ''}`;
348
+
349
+ bubble.innerHTML = escapeHtml(text).replace(/\n/g, '<br>');
350
+ if (isLoading) bubble.id = `loading-${Date.now()}`;
351
+
352
+ wrapper.appendChild(bubble);
353
+ container.appendChild(wrapper);
354
+
355
+ // Scroll to bottom
356
+ container.scrollTop = container.scrollHeight;
357
+
358
+ return bubble.id;
359
+ }
360
+
361
+ // ===== TOAST NOTIFICATIONS =====
362
+ function showToast(message, type = 'info') {
363
+ const toast = document.getElementById('toast');
364
+ toast.textContent = message;
365
+
366
+ // Tailwind classes for toast
367
+ toast.className = `fixed bottom-5 right-5 px-6 py-3 rounded-lg shadow-2xl transform transition-all duration-300 z-50 text-white font-medium translate-y-0 opacity-100`;
368
+
369
+ if (type === 'error') toast.classList.add('bg-red-600');
370
+ else if (type === 'success') toast.classList.add('bg-green-600');
371
+ else toast.classList.add('bg-slate-800');
372
+
373
+ setTimeout(() => {
374
+ toast.classList.remove('translate-y-0', 'opacity-100');
375
+ toast.classList.add('translate-y-20', 'opacity-0');
376
+ }, 4000);
377
+ }
378
+
379
+ // ===== UTILS =====
380
+ function escapeHtml(text) {
381
+ if (!text) return '';
382
+ const div = document.createElement('div');
383
+ div.textContent = text;
384
+ return div.innerHTML;
385
+ }
static/style.css ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* We are moving the styling to the HTML.
2
+ This file now only handles the visibility logic. */
3
+
4
+ .section {
5
+ display: none; /* Hide all sections by default */
6
+ animation: fadeIn 0.3s ease-in-out;
7
+ }
8
+
9
+ .section.active {
10
+ display: block; /* Only show the active section */
11
+ }
12
+
13
+ /* Smooth fade in animation */
14
+ @keyframes fadeIn {
15
+ from { opacity: 0; transform: translateY(10px); }
16
+ to { opacity: 1; transform: translateY(0); }
17
+ }
18
+
19
+ /* Custom scrollbar for chat */
20
+ .chat-messages::-webkit-scrollbar {
21
+ width: 6px;
22
+ }
23
+ .chat-messages::-webkit-scrollbar-track {
24
+ background: #f1f1f1;
25
+ }
26
+ .chat-messages::-webkit-scrollbar-thumb {
27
+ background: #cbd5e1;
28
+ border-radius: 3px;
29
+ }
30
+ .chat-messages::-webkit-scrollbar-thumb:hover {
31
+ background: #94a3b8;
32
+ }
templates/index.html ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Lab Report Decoder - AI-Powered Medical Analysis</title>
7
+
8
+ <!-- Load Tailwind CSS -->
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+
11
+ <!-- Link to our simple CSS for toggling sections -->
12
+ <link
13
+ rel="stylesheet"
14
+ href="{{ url_for('static', filename='css/styley.css') }}"
15
+ />
16
+
17
+ <!-- Font -->
18
+ <link
19
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
20
+ rel="stylesheet"
21
+ />
22
+
23
+ <!-- Configure Tailwind Colors -->
24
+ <script>
25
+ tailwind.config = {
26
+ theme: {
27
+ extend: {
28
+ colors: {
29
+ primary: "#659BB9" /* Deep Blue */,
30
+ secondary: "rgb(51 99 176)" /* Bright Blue */,
31
+ surface: "#ffffff" /* White */,
32
+ background: "#f8fafc" /* Very Light Blue-Grey */,
33
+ },
34
+ fontFamily: {
35
+ sans: ["Inter", "sans-serif"],
36
+ },
37
+ },
38
+ },
39
+ };
40
+ </script>
41
+ </head>
42
+ <body class="bg-background font-sans text-slate-800 h-screen flex flex-col">
43
+ <!-- Header -->
44
+ <header class="bg-primary text-white shadow-lg sticky top-0 z-50">
45
+ <div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
46
+ <div class="flex items-center justify-between h-16">
47
+ <!-- Logo -->
48
+ <div
49
+ class="flex items-center space-x-3 cursor-pointer"
50
+ onclick="showSection('upload-section')"
51
+ >
52
+ <span class="text-2xl">πŸ”¬</span>
53
+ <span class="font-bold text-xl tracking-tight"
54
+ >Lab Report Decoder</span
55
+ >
56
+ </div>
57
+
58
+ <!-- Navigation -->
59
+ <nav class="hidden md:flex space-x-1">
60
+ <button
61
+ onclick="showSection('upload-section')"
62
+ class="nav-btn px-4 py-2 rounded-lg text-sm font-medium hover:bg-white/10 transition-colors text-white/90 hover:text-white"
63
+ >
64
+ Upload
65
+ </button>
66
+ <button
67
+ onclick="showSection('results-section')"
68
+ class="nav-btn px-4 py-2 rounded-lg text-sm font-medium hover:bg-white/10 transition-colors text-white/90 hover:text-white"
69
+ >
70
+ Results
71
+ </button>
72
+ <button
73
+ onclick="showSection('chat-section')"
74
+ class="nav-btn px-4 py-2 rounded-lg text-sm font-medium hover:bg-white/10 transition-colors text-white/90 hover:text-white"
75
+ >
76
+ Ask Questions
77
+ </button>
78
+ <button
79
+ onclick="showSection('summary-section')"
80
+ class="nav-btn px-4 py-2 rounded-lg text-sm font-medium hover:bg-white/10 transition-colors text-white/90 hover:text-white"
81
+ >
82
+ Summary
83
+ </button>
84
+ </nav>
85
+ </div>
86
+ </div>
87
+ </header>
88
+
89
+ <!-- Main Content -->
90
+ <main class="flex-grow max-w-5xl w-full mx-auto px-4 sm:px-6 py-12">
91
+ <!-- SECTION 1: UPLOAD (Default Active) -->
92
+ <div id="upload-section" class="section active space-y-8">
93
+ <!-- Hero Text -->
94
+ <div class="text-center max-w-2xl mx-auto mb-10">
95
+ <h1 class="text-4xl font-extrabold text-primary mb-4 tracking-tight">
96
+ Understand Your Lab Results
97
+ </h1>
98
+ <p class="text-lg text-slate-500">
99
+ Upload your PDF report and get plain English explanations, dietary
100
+ advice, and AI-powered insights.
101
+ </p>
102
+ </div>
103
+
104
+ <!-- Upload Card -->
105
+ <div
106
+ class="bg-surface rounded-2xl shadow-xl border border-slate-100 p-10 max-w-3xl mx-auto transition-transform hover:scale-[1.01] duration-300"
107
+ >
108
+ <div
109
+ class="upload-area group border-2 border-dashed border-blue-200 bg-blue-50/50 rounded-xl p-12 text-center hover:border-secondary hover:bg-blue-50 transition-all cursor-pointer"
110
+ id="uploadArea"
111
+ onclick="document.getElementById('fileInput').click()"
112
+ >
113
+ <div
114
+ class="text-5xl mb-4 group-hover:scale-110 transition-transform duration-300"
115
+ >
116
+ πŸ“„
117
+ </div>
118
+ <h3 class="text-xl font-semibold text-slate-700 mb-2">
119
+ Drop your lab report PDF here
120
+ </h3>
121
+ <p class="text-slate-500 text-sm mb-6">
122
+ or click to browse (Max 16MB)
123
+ </p>
124
+
125
+ <input type="file" id="fileInput" accept=".pdf" hidden />
126
+
127
+ <button
128
+ class="bg-secondary text-white px-8 py-3 rounded-full font-medium shadow-md shadow-blue-500/30 hover:bg-blue-600 hover:shadow-blue-500/40 transition-all transform hover:-translate-y-0.5"
129
+ >
130
+ Choose File
131
+ </button>
132
+ </div>
133
+
134
+ <!-- Progress Indicator (Hidden by default) -->
135
+ <div id="uploadProgress" class="hidden mt-8 text-center">
136
+ <div
137
+ class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-200 border-t-secondary"
138
+ ></div>
139
+ <p class="text-secondary font-medium mt-3 animate-pulse">
140
+ Analyzing your report...
141
+ </p>
142
+ </div>
143
+ </div>
144
+
145
+ <!-- Features Grid -->
146
+ <div class="grid md:grid-cols-3 gap-6 max-w-4xl mx-auto mt-12">
147
+ <div
148
+ class="bg-surface p-6 rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition-shadow"
149
+ >
150
+ <div class="text-3xl mb-3">πŸ”</div>
151
+ <h3 class="font-semibold text-slate-800">Automatic Extraction</h3>
152
+ <p class="text-sm text-slate-500 mt-1">
153
+ Extracts all test values instantly
154
+ </p>
155
+ </div>
156
+ <div
157
+ class="bg-surface p-6 rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition-shadow"
158
+ >
159
+ <div class="text-3xl mb-3">🧠</div>
160
+ <h3 class="font-semibold text-slate-800">AI Analysis</h3>
161
+ <p class="text-sm text-slate-500 mt-1">
162
+ Medical-grade explanations
163
+ </p>
164
+ </div>
165
+ <div
166
+ class="bg-surface p-6 rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition-shadow"
167
+ >
168
+ <div class="text-3xl mb-3">🍎</div>
169
+ <h3 class="font-semibold text-slate-800">Dietary Tips</h3>
170
+ <p class="text-sm text-slate-500 mt-1">
171
+ Personalized nutrition advice
172
+ </p>
173
+ </div>
174
+ </div>
175
+ </div>
176
+
177
+ <!-- SECTION 2: RESULTS -->
178
+ <div id="results-section" class="section space-y-8 mt-12">
179
+ <h2 class="text-2xl font-bold text-slate-800 mb-6 flex items-center">
180
+ <span class="bg-blue-100 text-blue-600 p-2 rounded-lg mr-3 text-xl"
181
+ >πŸ“‹</span
182
+ >
183
+ Your Results
184
+ </h2>
185
+ <div
186
+ id="resultsContainer"
187
+ class="bg-surface rounded-xl shadow-lg border border-slate-100 min-h-[400px] p-6"
188
+ >
189
+ <div
190
+ class="flex flex-col items-center justify-center h-full text-slate-400 py-20"
191
+ >
192
+ <div class="text-6xl mb-4 opacity-20">πŸ“Š</div>
193
+ <p>Upload a lab report to view results here</p>
194
+ </div>
195
+ </div>
196
+ </div>
197
+
198
+ <!-- SECTION 3: CHAT -->
199
+ <div id="chat-section" class="section space-y-8 mt-12">
200
+ <div
201
+ class="flex flex-col h-full bg-surface rounded-xl shadow-lg border border-slate-100 overflow-hidden"
202
+ >
203
+ <div class="bg-slate-50 border-b border-slate-100 p-4">
204
+ <h2 class="font-bold text-slate-700">Ask Questions</h2>
205
+ <p class="text-xs text-slate-500">
206
+ Ask about your specific values, diet, or lifestyle.
207
+ </p>
208
+ </div>
209
+
210
+ <!-- Chat Area -->
211
+ <div
212
+ id="chatMessages"
213
+ class="flex-grow overflow-y-auto p-6 space-y-8 bg-white"
214
+ >
215
+ <div class="text-center text-slate-400 py-10">
216
+ <p class="mb-4">Try asking:</p>
217
+ <div class="inline-flex flex-col gap-2 text-sm text-blue-600">
218
+ <span
219
+ class="bg-blue-50 px-3 py-1 rounded-full cursor-pointer hover:bg-blue-100"
220
+ >"Is my cholesterol dangerous?"</span
221
+ >
222
+ <span
223
+ class="bg-blue-50 px-3 py-1 rounded-full cursor-pointer hover:bg-blue-100"
224
+ >"What foods lower iron?"</span
225
+ >
226
+ </div>
227
+ </div>
228
+ </div>
229
+
230
+ <!-- Input Area -->
231
+ <div class="p-4 border-t border-slate-100 bg-slate-50">
232
+ <div class="flex gap-2">
233
+ <input
234
+ type="text"
235
+ id="chatInput"
236
+ class="flex-grow px-4 py-3 rounded-xl border border-slate-300 focus:border-secondary focus:ring-2 focus:ring-blue-100 outline-none transition-all"
237
+ placeholder="Type your medical question..."
238
+ />
239
+ <button
240
+ onclick="askQuestion()"
241
+ class="bg-secondary text-white px-6 rounded-xl font-medium hover:bg-blue-600 transition-colors shadow-sm"
242
+ >
243
+ Send
244
+ </button>
245
+ </div>
246
+ </div>
247
+ </div>
248
+ </div>
249
+
250
+ <!-- SECTION 4: SUMMARY -->
251
+ <div id="summary-section" class="section mt-12">
252
+ <h2 class="text-2xl font-bold text-slate-800 mb-6 flex items-center">
253
+ <span
254
+ class="bg-purple-100 text-purple-600 p-2 rounded-lg mr-3 text-xl"
255
+ >πŸ“</span
256
+ >
257
+ Health Summary
258
+ </h2>
259
+
260
+ <!-- Stats Row -->
261
+ <div id="summaryStats" class="grid grid-cols-3 gap-4 mb-6 hidden">
262
+ <div class="bg-green-50 p-4 rounded-xl border border-green-100">
263
+ <div class="text-3xl font-bold text-green-600" id="normalCount">
264
+ 0
265
+ </div>
266
+ <div class="text-sm text-green-700 font-medium">Normal Ranges</div>
267
+ </div>
268
+ <div class="bg-red-50 p-4 rounded-xl border border-red-100">
269
+ <div class="text-3xl font-bold text-red-600" id="highCount">0</div>
270
+ <div class="text-sm text-red-700 font-medium">High Values</div>
271
+ </div>
272
+ <div class="bg-yellow-50 p-4 rounded-xl border border-yellow-100">
273
+ <div class="text-3xl font-bold text-yellow-600" id="lowCount">
274
+ 0
275
+ </div>
276
+ <div class="text-sm text-yellow-700 font-medium">Low Values</div>
277
+ </div>
278
+ </div>
279
+
280
+ <div
281
+ id="summaryContent"
282
+ class="bg-surface rounded-xl shadow-lg border border-slate-100 p-8 min-h-[200px]"
283
+ >
284
+ <div class="text-center text-slate-400 py-10">
285
+ <p>Analysis summary will appear here after upload</p>
286
+ </div>
287
+ </div>
288
+
289
+ <button
290
+ id="generateSummaryBtn"
291
+ onclick="generateSummary()"
292
+ class="hidden mt-6 w-full py-3 bg-white border-2 border-secondary text-secondary font-bold rounded-xl hover:bg-blue-50 transition-colors"
293
+ >
294
+ Regenerate Detailed Summary
295
+ </button>
296
+ </div>
297
+ </main>
298
+
299
+ <!-- Footer -->
300
+ <footer class="bg-white border-t border-slate-200 py-6 mt-20">
301
+ <div class="max-w-4xl mx-auto text-center px-4">
302
+ <p
303
+ class="text-sm text-red-500 font-medium bg-red-50 inline-block px-4 py-1 rounded-full mb-2"
304
+ >
305
+ ⚠️ Educational Purpose Only - Not Medical Advice
306
+ </p>
307
+ <p class="text-xs text-slate-400">
308
+ Always consult a professional healthcare provider for diagnosis and
309
+ treatment.
310
+ </p>
311
+ </div>
312
+ </footer>
313
+
314
+ <!-- Toast Notification -->
315
+ <div
316
+ id="toast"
317
+ class="fixed bottom-5 right-5 bg-slate-800 text-white px-6 py-3 rounded-lg shadow-2xl transform translate-y-20 opacity-0 transition-all duration-300 z-50"
318
+ ></div>
319
+
320
+ <script src="{{ url_for('static', filename='script.js') }}"></script>
321
+ </body>
322
+ </html>