Arnavdarange commited on
Commit
41bc097
·
1 Parent(s): fc70237

Initail Commit

Browse files
.env.example DELETED
@@ -1,5 +0,0 @@
1
- # Get your FREE Groq API key at: https://console.groq.com/keys
2
- # 1. Sign up (free, no credit card)
3
- # 2. Go to API Keys -> Create API Key
4
- # 3. Paste it below (remove the brackets)
5
- GROQ_API_KEY=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
 
 
 
 
 
 
app.py CHANGED
@@ -1,20 +1,30 @@
1
- from flask import Flask, render_template, request, jsonify
2
- import json
3
  import os
4
 
5
- # Placeholder imports matching your engineering pipeline
6
- # from verify_v2 import verify_claim
7
- # from health_passport import generate_qr_code
 
8
 
9
  app = Flask(__name__)
10
- app.secret_key = "verimed_secure_session_key"
 
 
 
 
11
 
12
  # --- PAGE ROUTING ---
13
 
14
  @app.route('/')
15
  def home():
16
- # Home/Dashboard with overview metrics
17
- stats = {"total_checked": 142, "true_count": 58, "false_count": 64, "misleading_count": 20}
 
 
 
 
 
 
18
  return render_template('index.html', stats=stats)
19
 
20
  @app.route('/checker')
@@ -34,44 +44,128 @@ def passport():
34
  @app.route('/api/verify', methods=['POST'])
35
  def api_verify():
36
  """Handles multimodal/multilingual inputs (Text, URL, Images via OCR)"""
37
- claim_text = request.form.get('claim', '')
38
  language = request.form.get('language', 'en')
39
-
 
40
  # Handle Image Upload for OCR processing
41
  if 'image' in request.files and request.files['image'].filename != '':
42
  image_file = request.files['image']
43
- # text_extracted = easyocr_instance.readtext(image_file.read())
44
- claim_text = "Extracted text from uploaded screenshot sample"
45
-
46
- # Mock response demonstrating architecture values
47
- result = {
48
- "verdict": "False",
49
- "confidence": 94,
50
- "explanation": "This claim contradicts verified clinical trials and systemic data published by the WHO and CDC. There is no empirical medical evidence supporting this mechanism.",
51
- "entities": [
52
- {"text": "Garlic tea", "label": "Treatment"},
53
- {"text": "COVID-19", "label": "Disease_disorder"}
54
- ],
55
- "sources": ["WHO Fact Sheet 2024", "CDC Viral Pathogen Review", "PubMed Central PMC71123"],
56
- "language_processed": language
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  }
58
-
59
- return jsonify(result)
 
 
 
 
 
 
60
 
61
  @app.route('/api/predict_spread', methods=['POST'])
62
  def api_predict_spread():
63
- """Simulates GNN (GAT) Network Spread Risk Modeling"""
64
- claim = request.json.get('claim', '')
65
-
66
- # Simulating structural graph risk evaluation
 
 
 
 
 
 
67
  graph_data = {
68
  "virality_score": 87,
69
  "risk_level": "High Risk",
70
  "predicted_nodes_reached": 14200,
71
  "time_to_peak_hours": 12,
72
- "network_hubs_vulnerable": ["WhatsApp Forwards Cluster A", "Public FB Groups"]
 
73
  }
74
  return jsonify(graph_data)
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  if __name__ == '__main__':
77
- app.run(debug=True, port=5000)
 
1
+ from flask import Flask, render_template, request, jsonify, Response
 
2
  import os
3
 
4
+ from verify_v2 import verify_claim
5
+ import db
6
+ import health_passport as hp
7
+ import ocr_utils
8
 
9
  app = Flask(__name__)
10
+ app.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev-only-change-me")
11
+
12
+ # Make sure tables exist before anything else runs
13
+ db.init_db()
14
+ hp.init_passport_table()
15
 
16
  # --- PAGE ROUTING ---
17
 
18
  @app.route('/')
19
  def home():
20
+ raw_stats = db.get_stats()
21
+ by_verdict = raw_stats.get("by_verdict", {})
22
+ stats = {
23
+ "total_checked": raw_stats.get("total_checked", 0),
24
+ "true_count": by_verdict.get("True", 0),
25
+ "false_count": by_verdict.get("False", 0),
26
+ "misleading_count": by_verdict.get("Misleading", 0),
27
+ }
28
  return render_template('index.html', stats=stats)
29
 
30
  @app.route('/checker')
 
44
  @app.route('/api/verify', methods=['POST'])
45
  def api_verify():
46
  """Handles multimodal/multilingual inputs (Text, URL, Images via OCR)"""
47
+ claim_text = request.form.get('claim', '').strip()
48
  language = request.form.get('language', 'en')
49
+ ocr_used = False
50
+
51
  # Handle Image Upload for OCR processing
52
  if 'image' in request.files and request.files['image'].filename != '':
53
  image_file = request.files['image']
54
+ image_bytes = image_file.read()
55
+
56
+ if language != 'en':
57
+ return jsonify({
58
+ "verdict": "Unverified",
59
+ "confidence": 0,
60
+ "explanation": "Screenshot OCR currently only supports English. Please select English, or paste the claim as text instead.",
61
+ "entities": [],
62
+ "sources": [],
63
+ "language_processed": language,
64
+ }), 422
65
+
66
+ try:
67
+ extracted_text = ocr_utils.extract_text_from_image(image_bytes)
68
+ except Exception:
69
+ return jsonify({
70
+ "verdict": "Unverified",
71
+ "confidence": 0,
72
+ "explanation": "Something went wrong reading this image. Please try a clearer screenshot or paste the claim as text.",
73
+ "entities": [],
74
+ "sources": [],
75
+ "language_processed": language,
76
+ }), 500
77
+
78
+ if not extracted_text:
79
+ return jsonify({
80
+ "verdict": "Unverified",
81
+ "confidence": 0,
82
+ "explanation": "Couldn't find any readable text in this image. Try a clearer or higher-resolution screenshot, or paste the claim as text.",
83
+ "entities": [],
84
+ "sources": [],
85
+ "language_processed": language,
86
+ }), 422
87
+
88
+ claim_text = extracted_text
89
+ ocr_used = True
90
+
91
+ if not claim_text:
92
+ return jsonify({"error": "No claim text provided."}), 400
93
+
94
+ result = verify_claim(claim_text)
95
+
96
+ # Persist to history
97
+ db.save_result(claim_text, result)
98
+
99
+ response = {
100
+ "verdict": result.get("verdict"),
101
+ "confidence": result.get("confidence"),
102
+ "explanation": result.get("explanation"),
103
+ "entities": result.get("entities", []),
104
+ "sources": result.get("sources", []),
105
+ "language_processed": language,
106
+ "ocr_used": ocr_used,
107
+ "claim_text_used": claim_text if ocr_used else None,
108
  }
109
+ return jsonify(response)
110
+
111
+
112
+ @app.route('/api/history', methods=['GET'])
113
+ def api_history():
114
+ limit = request.args.get('limit', default=20, type=int)
115
+ return jsonify(db.get_history(limit=limit))
116
+
117
 
118
  @app.route('/api/predict_spread', methods=['POST'])
119
  def api_predict_spread():
120
+ """
121
+ Spread Risk Modeling.
122
+ NOTE: the real GNN/GAT layer (Phase 6) isn't built yet -- this is a
123
+ clearly-labeled heuristic placeholder, not the trained model described
124
+ in the pitch. Swap this out once Phase 6 lands.
125
+ """
126
+ claim = (request.json or {}).get('claim', '')
127
+ if not claim.strip():
128
+ return jsonify({"error": "No claim provided."}), 400
129
+
130
  graph_data = {
131
  "virality_score": 87,
132
  "risk_level": "High Risk",
133
  "predicted_nodes_reached": 14200,
134
  "time_to_peak_hours": 12,
135
+ "network_hubs_vulnerable": ["WhatsApp Forwards Cluster A", "Public FB Groups"],
136
+ "is_simulated": True, # tells the frontend to label this as a placeholder
137
  }
138
  return jsonify(graph_data)
139
 
140
+
141
+ @app.route('/api/passport', methods=['GET', 'POST'])
142
+ def api_passport():
143
+ if request.method == 'POST':
144
+ data = request.get_json(force=True) or {}
145
+ required_defaults = {
146
+ "full_name": "", "blood_group": "", "date_of_birth": "",
147
+ "allergies": "", "chronic_conditions": "", "current_medicines": "",
148
+ "emergency_contact_name": "", "emergency_contact_phone": "",
149
+ }
150
+ for key, default in required_defaults.items():
151
+ data.setdefault(key, default)
152
+ hp.save_passport(data)
153
+ return jsonify({"status": "saved"})
154
+
155
+ passport_data = hp.get_passport()
156
+ if not passport_data:
157
+ return jsonify(None)
158
+ return jsonify(passport_data)
159
+
160
+
161
+ @app.route('/api/passport/qr', methods=['GET'])
162
+ def api_passport_qr():
163
+ passport_data = hp.get_passport()
164
+ if not passport_data:
165
+ return jsonify({"error": "No passport saved yet."}), 404
166
+ png_bytes = hp.generate_qr_code(passport_data)
167
+ return Response(png_bytes, mimetype='image/png')
168
+
169
+
170
  if __name__ == '__main__':
171
+ app.run(debug=True, port=5000, use_reloader=False)
data/health_facts_seed.json CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:4376df4d1bff043b0c155e358792a701436252d3d47935b038dc288f8ae66474
3
- size 8202
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0b37db9fc22f16c8b3f0e01480b0c8e169341965227ee23456cf73ebc2416b58
3
+ size 8500
ocr_utils.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PHASE 7: OCR module for screenshot/image claim input.
3
+
4
+ Extracts text from uploaded images (e.g. WhatsApp forward screenshots) using
5
+ EasyOCR, so claims that arrive as pictures -- not just typed text -- can be
6
+ verified.
7
+
8
+ NOTE ON LANGUAGE SCOPE:
9
+ English only for now. EasyOCR requires language packs to be loaded together
10
+ into one Reader, and not all language combinations are compatible with each
11
+ other -- so "just add every language" isn't a one-line change. Supporting
12
+ Hindi/Marathi screenshots properly means loading a second Reader instance for
13
+ those languages and routing to it based on the user's language selection.
14
+ That's flagged as follow-up work, not done here.
15
+
16
+ First run downloads EasyOCR's detection + recognition models (~100MB) --
17
+ same one-time-download pattern as the NER model in ner_utils.py.
18
+ """
19
+
20
+ import easyocr
21
+
22
+ # Load once at import time, reuse across calls (loading the reader is the
23
+ # slow part -- don't do this per-request).
24
+ _reader = easyocr.Reader(['en'], gpu=False)
25
+
26
+ # Below this confidence, EasyOCR's guess is unreliable enough that including
27
+ # it does more harm than good to the downstream claim text.
28
+ MIN_CONFIDENCE = 0.4
29
+
30
+
31
+ def extract_text_from_image(image_bytes: bytes) -> str:
32
+ """
33
+ Runs OCR on raw image bytes and returns the concatenated recognized text,
34
+ in reading order top-to-bottom as EasyOCR detects it.
35
+
36
+ Returns an empty string if nothing readable was found above the
37
+ confidence threshold -- callers should treat that as "OCR failed" and
38
+ not silently pass empty text further down the pipeline.
39
+ """
40
+ results = _reader.readtext(image_bytes)
41
+
42
+ lines = [text.strip() for (_bbox, text, confidence) in results if confidence >= MIN_CONFIDENCE]
43
+ return " ".join(lines).strip()
44
+
45
+
46
+ if __name__ == "__main__":
47
+ # Quick manual test -- run: python ocr_utils.py path/to/screenshot.png
48
+ import sys
49
+
50
+ if len(sys.argv) < 2:
51
+ print("Usage: python ocr_utils.py <path_to_image>")
52
+ sys.exit(1)
53
+
54
+ with open(sys.argv[1], "rb") as f:
55
+ image_bytes = f.read()
56
+
57
+ text = extract_text_from_image(image_bytes)
58
+ if text:
59
+ print(f"Extracted text:\n{text}")
60
+ else:
61
+ print("No readable text found above confidence threshold.")
requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
  # ==== PHASE 1-3: Core pipeline (install this first) ====
 
2
  groq
3
  chromadb
4
  sentence-transformers
@@ -14,7 +15,7 @@ torch
14
  # datasets
15
 
16
  # ==== PHASE 7: OCR ====
17
- # easyocr
18
 
19
  # ==== Data sources ====
20
  requests
@@ -22,4 +23,3 @@ biopython
22
 
23
  # ==== Health Passport (Day 4) ====
24
  qrcode[pil]
25
-
 
1
  # ==== PHASE 1-3: Core pipeline (install this first) ====
2
+ Flask
3
  groq
4
  chromadb
5
  sentence-transformers
 
15
  # datasets
16
 
17
  # ==== PHASE 7: OCR ====
18
+ easyocr
19
 
20
  # ==== Data sources ====
21
  requests
 
23
 
24
  # ==== Health Passport (Day 4) ====
25
  qrcode[pil]
 
static/css/static/css/style.css ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* VeriMed AI -- small additions beyond Tailwind's utility classes */
2
+
3
+ body {
4
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
5
+ }
6
+
7
+ /* Keep only the shareable card visible when exporting via window.print() */
8
+ @media print {
9
+ nav, footer, #checkerForm, #resultsPlaceholder,
10
+ #verdictBadge, .grid.grid-cols-1.md\:grid-cols-2 {
11
+ display: none !important;
12
+ }
13
+ #shareCardGraphic {
14
+ box-shadow: none !important;
15
+ }
16
+ }
17
+
18
+ /* Smooth fade-in for result panels */
19
+ #resultsCard, #predictOutputPanel {
20
+ animation: fadeIn 0.25s ease-in-out;
21
+ }
22
+
23
+ @keyframes fadeIn {
24
+ from { opacity: 0; transform: translateY(4px); }
25
+ to { opacity: 1; transform: translateY(0); }
26
+ }
static/css/style.css CHANGED
@@ -1,16 +1,26 @@
1
- @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
2
 
3
  body {
4
- font-family: 'Plus Jakarta Sans', sans-serif;
5
  }
6
 
7
- .glass-card {
8
- background: rgba(255, 255, 255, 0.75);
9
- backdrop-filter: blur(12px);
10
- -webkit-backdrop-filter: blur(12px);
 
 
 
 
 
11
  }
12
 
13
- /* Custom Color Maps for Verification Verdict Returns */
14
- .verdict-true { background: rgba(16, 185, 129, 0.08); border-color: rgba(16, 185, 129, 0.3); color: #065f46; }
15
- .verdict-false { background: rgba(239, 68, 68, 0.08); border-color: rgba(239, 68, 68, 0.3); color: #991b1b; }
16
- .verdict-misleading { background: rgba(245, 158, 11, 0.08); border-color: rgba(245, 158, 11, 0.3); color: #92400e; }
 
 
 
 
 
 
1
+ /* VeriMed AI -- small additions beyond Tailwind's utility classes */
2
 
3
  body {
4
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
5
  }
6
 
7
+ /* Keep only the shareable card visible when exporting via window.print() */
8
+ @media print {
9
+ nav, footer, #checkerForm, #resultsPlaceholder,
10
+ #verdictBadge, .grid.grid-cols-1.md\:grid-cols-2 {
11
+ display: none !important;
12
+ }
13
+ #shareCardGraphic {
14
+ box-shadow: none !important;
15
+ }
16
  }
17
 
18
+ /* Smooth fade-in for result panels */
19
+ #resultsCard, #predictOutputPanel {
20
+ animation: fadeIn 0.25s ease-in-out;
21
+ }
22
+
23
+ @keyframes fadeIn {
24
+ from { opacity: 0; transform: translateY(4px); }
25
+ to { opacity: 1; transform: translateY(0); }
26
+ }
static/js/main.js CHANGED
@@ -1,96 +1,255 @@
1
- // Handles Multimodal Fact Checker Forms
2
- const checkerForm = document.getElementById('checkerForm');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  if (checkerForm) {
4
- checkerForm.addEventListener('submit', async (e) => {
 
 
 
 
 
 
 
 
 
 
 
5
  e.preventDefault();
6
- const formData = new FormData(checkerForm);
7
-
8
- // Show loading progress state
9
- document.getElementById('resultsPlaceholder').classList.add('hidden');
10
- const resultsCard = document.getElementById('resultsCard');
11
- resultsCard.classList.remove('hidden');
12
- document.getElementById('verdictLabel').innerText = "Processing Pipeline... 🧬";
13
- document.getElementById('explanationText').innerText = "Extracting entities via BioBERT & checking RAG sources[cite: 1]...";
14
 
15
  try {
16
- const response = await fetch('/api/verify', { method: 'POST', body: formData });
17
- const data = await response.json();
18
-
19
- // Render Dynamic UI Updates based on response
20
- const badge = document.getElementById('verdictBadge');
21
- badge.className = `p-5 rounded-2xl border flex flex-col gap-2 verdict-${data.verdict.toLowerCase()}`;
22
-
23
- document.getElementById('verdictLabel').innerText = `Verdict: ${data.verdict}`;
24
- document.getElementById('explanationText').innerText = data.explanation;
25
- document.getElementById('confidenceValue').innerText = `${data.confidence}%`;
26
- document.getElementById('confidenceBar').style.width = `${data.confidence}%`;
27
-
28
- // Build Medical Entity Chips[cite: 1]
29
- const entityContainer = document.getElementById('entityContainer');
30
- entityContainer.innerHTML = data.entities.map(e =>
31
- `<span class="bg-blue-50 text-blue-700 text-[11px] font-bold px-2.5 py-1 rounded-full border border-blue-200">${e.text} &middot; ${e.label}</span>`
32
- ).join('');
33
-
34
- // Build Source Citation Chips[cite: 1]
35
- const sourceContainer = document.getElementById('sourceContainer');
36
- sourceContainer.innerHTML = data.sources.map(s =>
37
- `<span class="flex items-center gap-1.5"><i class="fa-solid fa-circle-check text-emerald-500 text-[10px]"></i> ${s}</span>`
38
- ).join('');
39
-
40
- // Fill Shareable Notice Card
41
- document.getElementById('shareVerdict').innerText = `🚨 MYTH CHECK: ${data.verdict}`;
42
- document.getElementById('shareExplanation').innerText = `"${data.explanation}"`;
43
 
 
 
 
 
 
 
 
44
  } catch (err) {
45
- console.error("Pipeline failure: ", err);
 
 
 
 
 
 
 
 
46
  }
47
  });
48
  }
49
 
50
- // Handles GNN Structural Virality Simulators[cite: 1]
51
- async function runPredictionPipeline() {
52
- const claim = document.getElementById('predictInput').value;
53
- const panel = document.getElementById('predictOutputPanel');
54
- if (!claim) return alert("Please specify narrative terms.");
55
 
56
- panel.innerHTML = `<div class="m-auto text-xs font-bold animate-pulse text-purple-400"><i class="fa-solid fa-spinner fa-spin text-lg mr-2"></i>Executing topological structural edge learning evaluation matrices[cite: 1]...</div>`;
 
 
 
 
 
 
 
 
 
 
 
57
 
58
- const response = await fetch('/api/predict_spread', {
59
- method: 'POST',
60
- headers: { 'Content-Type': 'application/json' },
61
- body: JSON.stringify({ claim })
 
 
 
 
 
 
 
 
 
 
 
 
62
  });
63
- const data = await response.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  panel.innerHTML = `
66
- <div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-center">
67
- <div class="bg-slate-800/60 p-4 rounded-xl border border-purple-900/40">
68
- <span class="text-[10px] text-purple-400 uppercase font-bold tracking-wider">Virality Risk Score[cite: 1]</span>
69
- <h4 class="text-2xl font-black text-purple-400 mt-1">${data.virality_score}%</h4>
70
- </div>
71
- <div class="bg-slate-800/60 p-4 rounded-xl border border-purple-900/40">
72
- <span class="text-[10px] text-purple-400 uppercase font-bold tracking-wider">Node Critical Level[cite: 1]</span>
73
- <h4 class="text-2xl font-black text-red-400 mt-1">${data.risk_level}</h4>
74
- </div>
75
- <div class="bg-slate-800/60 p-4 rounded-xl border border-purple-900/40">
76
- <span class="text-[10px] text-purple-400 uppercase font-bold tracking-wider">Peak Horizon Reach[cite: 1]</span>
77
- <h4 class="text-2xl font-black text-cyan-400 mt-1">${data.time_to_peak_hours} Hours</h4>
78
- </div>
79
- </div>
80
- <div class="bg-slate-800/40 p-4 rounded-xl border border-slate-800">
81
- <span class="text-[10px] uppercase text-slate-400 font-bold block mb-2">High-Risk Hub Vulnerability Map</span>
82
- <ul class="text-xs text-slate-300 flex flex-col gap-1.5">
83
- ${data.network_hubs_vulnerable.map(h => `<li><i class="fa-solid fa-triangle-exclamation text-amber-500 mr-2"></i> At Risk: <b>${h}</b></li>`).join('')}
84
- </ul>
85
  </div>
86
  `;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
 
89
- // File Input Name Decorator
90
- const imgInput = document.getElementById('imageInput');
91
- if (imgInput) {
92
- imgInput.addEventListener('change', (e) => {
93
- const file = e.target.files[0];
94
- if (file) document.getElementById('fileName').innerText = `Selected File: ${file.name} (OCR Ready)[cite: 1]`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  });
96
- }
 
 
 
 
 
 
 
 
1
+ // VeriMed AI -- frontend logic
2
+ // Wires the checker, predictor, and passport pages to the Flask API.
3
+
4
+ // ---------- Helpers ----------
5
+
6
+ function verdictColorClasses(verdict) {
7
+ switch ((verdict || "").toLowerCase()) {
8
+ case "true":
9
+ return { badge: "bg-emerald-50 border-emerald-200 text-emerald-700", bar: "bg-emerald-600" };
10
+ case "false":
11
+ return { badge: "bg-red-50 border-red-200 text-red-700", bar: "bg-red-600" };
12
+ case "misleading":
13
+ return { badge: "bg-amber-50 border-amber-200 text-amber-700", bar: "bg-amber-600" };
14
+ default:
15
+ return { badge: "bg-slate-50 border-slate-200 text-slate-700", bar: "bg-slate-500" };
16
+ }
17
+ }
18
+
19
+ function escapeHtml(str) {
20
+ const div = document.createElement("div");
21
+ div.textContent = str == null ? "" : String(str);
22
+ return div.innerHTML;
23
+ }
24
+
25
+ // ---------- Checker page ----------
26
+
27
+ const checkerForm = document.getElementById("checkerForm");
28
  if (checkerForm) {
29
+ const imageInput = document.getElementById("imageInput");
30
+ const fileNameLabel = document.getElementById("fileName");
31
+
32
+ if (imageInput) {
33
+ imageInput.addEventListener("change", () => {
34
+ if (imageInput.files.length > 0) {
35
+ fileNameLabel.textContent = `Selected: ${imageInput.files[0].name}`;
36
+ }
37
+ });
38
+ }
39
+
40
+ checkerForm.addEventListener("submit", async (e) => {
41
  e.preventDefault();
42
+
43
+ const submitBtn = checkerForm.querySelector("button[type='submit']");
44
+ const originalBtnHtml = submitBtn.innerHTML;
45
+ submitBtn.disabled = true;
46
+ submitBtn.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> Analyzing...`;
47
+
48
+ const placeholder = document.getElementById("resultsPlaceholder");
49
+ const resultsCard = document.getElementById("resultsCard");
50
 
51
  try {
52
+ const formData = new FormData(checkerForm);
53
+ const res = await fetch("/api/verify", { method: "POST", body: formData });
54
+ const data = await res.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ if (!res.ok) {
57
+ throw new Error(data.explanation || data.error || "Verification failed.");
58
+ }
59
+
60
+ renderCheckerResult(data);
61
+ placeholder.classList.add("hidden");
62
+ resultsCard.classList.remove("hidden");
63
  } catch (err) {
64
+ placeholder.classList.remove("hidden");
65
+ resultsCard.classList.add("hidden");
66
+ placeholder.innerHTML = `
67
+ <i class="fa-solid fa-triangle-exclamation text-4xl mb-3 text-red-400"></i>
68
+ <p class="font-bold text-sm text-red-500">${escapeHtml(err.message)}</p>
69
+ `;
70
+ } finally {
71
+ submitBtn.disabled = false;
72
+ submitBtn.innerHTML = originalBtnHtml;
73
  }
74
  });
75
  }
76
 
77
+ function renderCheckerResult(data) {
78
+ const colors = verdictColorClasses(data.verdict);
79
+
80
+ const verdictBadge = document.getElementById("verdictBadge");
81
+ verdictBadge.className = `p-5 rounded-2xl border flex flex-col gap-2 ${colors.badge}`;
82
 
83
+ // If the claim came from an uploaded screenshot, show the OCR'd text so
84
+ // the user can confirm it was read correctly before trusting the verdict.
85
+ let ocrNote = "";
86
+ const existingOcrNote = document.getElementById("ocrExtractedNote");
87
+ if (existingOcrNote) existingOcrNote.remove();
88
+ if (data.ocr_used && data.claim_text_used) {
89
+ ocrNote = document.createElement("div");
90
+ ocrNote.id = "ocrExtractedNote";
91
+ ocrNote.className = "text-[11px] font-semibold text-slate-500 bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 mb-1";
92
+ ocrNote.innerHTML = `<i class="fa-solid fa-text-height mr-1"></i> Text read from image: "${escapeHtml(data.claim_text_used)}"`;
93
+ verdictBadge.parentElement.insertBefore(ocrNote, verdictBadge);
94
+ }
95
 
96
+ document.getElementById("verdictLabel").textContent = data.verdict || "Unverified";
97
+ document.getElementById("explanationText").textContent = data.explanation || "";
98
+
99
+ const confidence = Number(data.confidence) || 0;
100
+ document.getElementById("confidenceValue").textContent = `${confidence}%`;
101
+ const bar = document.getElementById("confidenceBar");
102
+ bar.style.width = `${confidence}%`;
103
+ bar.className = `h-full transition-all duration-500 ${colors.bar}`;
104
+
105
+ const entityContainer = document.getElementById("entityContainer");
106
+ entityContainer.innerHTML = "";
107
+ (data.entities || []).forEach((ent) => {
108
+ const chip = document.createElement("span");
109
+ chip.className = "text-[11px] font-bold bg-blue-50 text-blue-700 px-2.5 py-1 rounded-full border border-blue-100";
110
+ chip.textContent = `${ent.text} · ${ent.label}`;
111
+ entityContainer.appendChild(chip);
112
  });
113
+ if (!data.entities || data.entities.length === 0) {
114
+ entityContainer.innerHTML = `<span class="text-xs text-slate-400">No entities detected.</span>`;
115
+ }
116
+
117
+ const sourceContainer = document.getElementById("sourceContainer");
118
+ sourceContainer.innerHTML = "";
119
+ (data.sources || []).forEach((src) => {
120
+ const line = document.createElement("div");
121
+ line.innerHTML = `<i class="fa-solid fa-check text-emerald-500 mr-1"></i> ${escapeHtml(src)}`;
122
+ sourceContainer.appendChild(line);
123
+ });
124
+ if (!data.sources || data.sources.length === 0) {
125
+ sourceContainer.innerHTML = `<span class="text-xs text-slate-400">No sources returned.</span>`;
126
+ }
127
+
128
+ document.getElementById("shareVerdict").textContent = `Verdict: ${data.verdict || "Unverified"}`;
129
+ document.getElementById("shareExplanation").textContent = data.explanation || "";
130
+ }
131
+
132
+ // ---------- Predictor page ----------
133
+
134
+ async function runPredictionPipeline() {
135
+ const input = document.getElementById("predictInput");
136
+ const panel = document.getElementById("predictOutputPanel");
137
+ const claim = input.value.trim();
138
+
139
+ if (!claim) {
140
+ panel.innerHTML = `<div class="m-auto text-center text-amber-400 font-bold text-xs">Enter a claim first.</div>`;
141
+ return;
142
+ }
143
 
144
  panel.innerHTML = `
145
+ <div class="m-auto text-center text-slate-500 font-bold text-xs flex flex-col gap-2 items-center">
146
+ <i class="fa-solid fa-circle-nodes text-3xl text-purple-500/50 animate-spin"></i>
147
+ Running network spread simulation...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  </div>
149
  `;
150
+
151
+ try {
152
+ const res = await fetch("/api/predict_spread", {
153
+ method: "POST",
154
+ headers: { "Content-Type": "application/json" },
155
+ body: JSON.stringify({ claim }),
156
+ });
157
+ const data = await res.json();
158
+ if (!res.ok) throw new Error(data.error || "Prediction failed.");
159
+
160
+ const riskColor = data.risk_level === "High Risk" ? "text-red-400" : "text-emerald-400";
161
+ panel.innerHTML = `
162
+ ${data.is_simulated ? `<div class="text-[10px] uppercase tracking-wider font-bold text-amber-400 mb-1">
163
+ <i class="fa-solid fa-flask mr-1"></i> Simulated placeholder -- GNN model not yet trained
164
+ </div>` : ""}
165
+ <div class="grid grid-cols-2 gap-4">
166
+ <div class="bg-slate-800/60 rounded-xl p-4">
167
+ <span class="text-[10px] uppercase font-bold text-slate-400">Virality Score</span>
168
+ <div class="text-3xl font-black mt-1">${escapeHtml(data.virality_score)}</div>
169
+ </div>
170
+ <div class="bg-slate-800/60 rounded-xl p-4">
171
+ <span class="text-[10px] uppercase font-bold text-slate-400">Risk Level</span>
172
+ <div class="text-xl font-black mt-1 ${riskColor}">${escapeHtml(data.risk_level)}</div>
173
+ </div>
174
+ <div class="bg-slate-800/60 rounded-xl p-4">
175
+ <span class="text-[10px] uppercase font-bold text-slate-400">Predicted Nodes Reached</span>
176
+ <div class="text-2xl font-black mt-1">${escapeHtml(data.predicted_nodes_reached)}</div>
177
+ </div>
178
+ <div class="bg-slate-800/60 rounded-xl p-4">
179
+ <span class="text-[10px] uppercase font-bold text-slate-400">Time To Peak</span>
180
+ <div class="text-2xl font-black mt-1">${escapeHtml(data.time_to_peak_hours)}h</div>
181
+ </div>
182
+ </div>
183
+ <div class="bg-slate-800/60 rounded-xl p-4">
184
+ <span class="text-[10px] uppercase font-bold text-slate-400 block mb-2">Vulnerable Network Hubs</span>
185
+ <div class="flex flex-wrap gap-2">
186
+ ${(data.network_hubs_vulnerable || []).map(h => `<span class="text-[11px] font-bold bg-purple-500/10 text-purple-300 px-2.5 py-1 rounded-full border border-purple-500/20">${escapeHtml(h)}</span>`).join("")}
187
+ </div>
188
+ </div>
189
+ `;
190
+ } catch (err) {
191
+ panel.innerHTML = `<div class="m-auto text-center text-red-400 font-bold text-xs">${escapeHtml(err.message)}</div>`;
192
+ }
193
  }
194
 
195
+ // ---------- Passport page ----------
196
+
197
+ const passportForm = document.getElementById("passportForm");
198
+ if (passportForm) {
199
+ // Load any existing saved passport on page load
200
+ fetch("/api/passport")
201
+ .then((res) => res.json())
202
+ .then((data) => {
203
+ if (!data) return;
204
+ document.getElementById("passName").value = data.full_name || "";
205
+ document.getElementById("passBlood").value = data.blood_group || "";
206
+ document.getElementById("passAllergies").value = data.allergies || "";
207
+ document.getElementById("passMeds").value = data.current_medicines || "";
208
+ document.getElementById("passContact").value = data.emergency_contact_name || "";
209
+ document.getElementById("passPhone").value = data.emergency_contact_phone || "";
210
+ showQrCode();
211
+ })
212
+ .catch(() => {});
213
+
214
+ passportForm.addEventListener("submit", async (e) => {
215
+ e.preventDefault();
216
+
217
+ const payload = {
218
+ full_name: document.getElementById("passName").value,
219
+ blood_group: document.getElementById("passBlood").value,
220
+ date_of_birth: "",
221
+ allergies: document.getElementById("passAllergies").value,
222
+ chronic_conditions: "",
223
+ current_medicines: document.getElementById("passMeds").value,
224
+ emergency_contact_name: document.getElementById("passContact").value,
225
+ emergency_contact_phone: document.getElementById("passPhone").value,
226
+ };
227
+
228
+ const submitBtn = passportForm.querySelector("button[type='submit']");
229
+ const originalText = submitBtn.textContent;
230
+ submitBtn.disabled = true;
231
+ submitBtn.textContent = "Saving...";
232
+
233
+ try {
234
+ const res = await fetch("/api/passport", {
235
+ method: "POST",
236
+ headers: { "Content-Type": "application/json" },
237
+ body: JSON.stringify(payload),
238
+ });
239
+ if (!res.ok) throw new Error("Failed to save passport.");
240
+ showQrCode();
241
+ } catch (err) {
242
+ alert(err.message);
243
+ } finally {
244
+ submitBtn.disabled = false;
245
+ submitBtn.textContent = originalText;
246
+ }
247
  });
248
+ }
249
+
250
+ function showQrCode() {
251
+ const qrContainer = document.getElementById("qrContainer");
252
+ if (!qrContainer) return;
253
+ // Cache-bust so the browser doesn't show a stale QR after an update
254
+ qrContainer.innerHTML = `<img src="/api/passport/qr?t=${Date.now()}" alt="Health Passport QR Code" class="w-40 h-40 object-contain" />`;
255
+ }
static/js/static/js/main.js ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // VeriMed AI -- frontend logic
2
+ // Wires the checker, predictor, and passport pages to the Flask API.
3
+
4
+ // ---------- Helpers ----------
5
+
6
+ function verdictColorClasses(verdict) {
7
+ switch ((verdict || "").toLowerCase()) {
8
+ case "true":
9
+ return { badge: "bg-emerald-50 border-emerald-200 text-emerald-700", bar: "bg-emerald-600" };
10
+ case "false":
11
+ return { badge: "bg-red-50 border-red-200 text-red-700", bar: "bg-red-600" };
12
+ case "misleading":
13
+ return { badge: "bg-amber-50 border-amber-200 text-amber-700", bar: "bg-amber-600" };
14
+ default:
15
+ return { badge: "bg-slate-50 border-slate-200 text-slate-700", bar: "bg-slate-500" };
16
+ }
17
+ }
18
+
19
+ function escapeHtml(str) {
20
+ const div = document.createElement("div");
21
+ div.textContent = str == null ? "" : String(str);
22
+ return div.innerHTML;
23
+ }
24
+
25
+ // ---------- Checker page ----------
26
+
27
+ const checkerForm = document.getElementById("checkerForm");
28
+ if (checkerForm) {
29
+ const imageInput = document.getElementById("imageInput");
30
+ const fileNameLabel = document.getElementById("fileName");
31
+
32
+ if (imageInput) {
33
+ imageInput.addEventListener("change", () => {
34
+ if (imageInput.files.length > 0) {
35
+ fileNameLabel.textContent = `Selected: ${imageInput.files[0].name}`;
36
+ }
37
+ });
38
+ }
39
+
40
+ checkerForm.addEventListener("submit", async (e) => {
41
+ e.preventDefault();
42
+
43
+ const submitBtn = checkerForm.querySelector("button[type='submit']");
44
+ const originalBtnHtml = submitBtn.innerHTML;
45
+ submitBtn.disabled = true;
46
+ submitBtn.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> Analyzing...`;
47
+
48
+ const placeholder = document.getElementById("resultsPlaceholder");
49
+ const resultsCard = document.getElementById("resultsCard");
50
+
51
+ try {
52
+ const formData = new FormData(checkerForm);
53
+ const res = await fetch("/api/verify", { method: "POST", body: formData });
54
+ const data = await res.json();
55
+
56
+ if (!res.ok) {
57
+ throw new Error(data.explanation || data.error || "Verification failed.");
58
+ }
59
+
60
+ renderCheckerResult(data);
61
+ placeholder.classList.add("hidden");
62
+ resultsCard.classList.remove("hidden");
63
+ } catch (err) {
64
+ placeholder.classList.remove("hidden");
65
+ resultsCard.classList.add("hidden");
66
+ placeholder.innerHTML = `
67
+ <i class="fa-solid fa-triangle-exclamation text-4xl mb-3 text-red-400"></i>
68
+ <p class="font-bold text-sm text-red-500">${escapeHtml(err.message)}</p>
69
+ `;
70
+ } finally {
71
+ submitBtn.disabled = false;
72
+ submitBtn.innerHTML = originalBtnHtml;
73
+ }
74
+ });
75
+ }
76
+
77
+ function renderCheckerResult(data) {
78
+ const colors = verdictColorClasses(data.verdict);
79
+
80
+ const verdictBadge = document.getElementById("verdictBadge");
81
+ verdictBadge.className = `p-5 rounded-2xl border flex flex-col gap-2 ${colors.badge}`;
82
+ document.getElementById("verdictLabel").textContent = data.verdict || "Unverified";
83
+ document.getElementById("explanationText").textContent = data.explanation || "";
84
+
85
+ const confidence = Number(data.confidence) || 0;
86
+ document.getElementById("confidenceValue").textContent = `${confidence}%`;
87
+ const bar = document.getElementById("confidenceBar");
88
+ bar.style.width = `${confidence}%`;
89
+ bar.className = `h-full transition-all duration-500 ${colors.bar}`;
90
+
91
+ const entityContainer = document.getElementById("entityContainer");
92
+ entityContainer.innerHTML = "";
93
+ (data.entities || []).forEach((ent) => {
94
+ const chip = document.createElement("span");
95
+ chip.className = "text-[11px] font-bold bg-blue-50 text-blue-700 px-2.5 py-1 rounded-full border border-blue-100";
96
+ chip.textContent = `${ent.text} · ${ent.label}`;
97
+ entityContainer.appendChild(chip);
98
+ });
99
+ if (!data.entities || data.entities.length === 0) {
100
+ entityContainer.innerHTML = `<span class="text-xs text-slate-400">No entities detected.</span>`;
101
+ }
102
+
103
+ const sourceContainer = document.getElementById("sourceContainer");
104
+ sourceContainer.innerHTML = "";
105
+ (data.sources || []).forEach((src) => {
106
+ const line = document.createElement("div");
107
+ line.innerHTML = `<i class="fa-solid fa-check text-emerald-500 mr-1"></i> ${escapeHtml(src)}`;
108
+ sourceContainer.appendChild(line);
109
+ });
110
+ if (!data.sources || data.sources.length === 0) {
111
+ sourceContainer.innerHTML = `<span class="text-xs text-slate-400">No sources returned.</span>`;
112
+ }
113
+
114
+ document.getElementById("shareVerdict").textContent = `Verdict: ${data.verdict || "Unverified"}`;
115
+ document.getElementById("shareExplanation").textContent = data.explanation || "";
116
+ }
117
+
118
+ // ---------- Predictor page ----------
119
+
120
+ async function runPredictionPipeline() {
121
+ const input = document.getElementById("predictInput");
122
+ const panel = document.getElementById("predictOutputPanel");
123
+ const claim = input.value.trim();
124
+
125
+ if (!claim) {
126
+ panel.innerHTML = `<div class="m-auto text-center text-amber-400 font-bold text-xs">Enter a claim first.</div>`;
127
+ return;
128
+ }
129
+
130
+ panel.innerHTML = `
131
+ <div class="m-auto text-center text-slate-500 font-bold text-xs flex flex-col gap-2 items-center">
132
+ <i class="fa-solid fa-circle-nodes text-3xl text-purple-500/50 animate-spin"></i>
133
+ Running network spread simulation...
134
+ </div>
135
+ `;
136
+
137
+ try {
138
+ const res = await fetch("/api/predict_spread", {
139
+ method: "POST",
140
+ headers: { "Content-Type": "application/json" },
141
+ body: JSON.stringify({ claim }),
142
+ });
143
+ const data = await res.json();
144
+ if (!res.ok) throw new Error(data.error || "Prediction failed.");
145
+
146
+ const riskColor = data.risk_level === "High Risk" ? "text-red-400" : "text-emerald-400";
147
+ panel.innerHTML = `
148
+ ${data.is_simulated ? `<div class="text-[10px] uppercase tracking-wider font-bold text-amber-400 mb-1">
149
+ <i class="fa-solid fa-flask mr-1"></i> Simulated placeholder -- GNN model not yet trained
150
+ </div>` : ""}
151
+ <div class="grid grid-cols-2 gap-4">
152
+ <div class="bg-slate-800/60 rounded-xl p-4">
153
+ <span class="text-[10px] uppercase font-bold text-slate-400">Virality Score</span>
154
+ <div class="text-3xl font-black mt-1">${escapeHtml(data.virality_score)}</div>
155
+ </div>
156
+ <div class="bg-slate-800/60 rounded-xl p-4">
157
+ <span class="text-[10px] uppercase font-bold text-slate-400">Risk Level</span>
158
+ <div class="text-xl font-black mt-1 ${riskColor}">${escapeHtml(data.risk_level)}</div>
159
+ </div>
160
+ <div class="bg-slate-800/60 rounded-xl p-4">
161
+ <span class="text-[10px] uppercase font-bold text-slate-400">Predicted Nodes Reached</span>
162
+ <div class="text-2xl font-black mt-1">${escapeHtml(data.predicted_nodes_reached)}</div>
163
+ </div>
164
+ <div class="bg-slate-800/60 rounded-xl p-4">
165
+ <span class="text-[10px] uppercase font-bold text-slate-400">Time To Peak</span>
166
+ <div class="text-2xl font-black mt-1">${escapeHtml(data.time_to_peak_hours)}h</div>
167
+ </div>
168
+ </div>
169
+ <div class="bg-slate-800/60 rounded-xl p-4">
170
+ <span class="text-[10px] uppercase font-bold text-slate-400 block mb-2">Vulnerable Network Hubs</span>
171
+ <div class="flex flex-wrap gap-2">
172
+ ${(data.network_hubs_vulnerable || []).map(h => `<span class="text-[11px] font-bold bg-purple-500/10 text-purple-300 px-2.5 py-1 rounded-full border border-purple-500/20">${escapeHtml(h)}</span>`).join("")}
173
+ </div>
174
+ </div>
175
+ `;
176
+ } catch (err) {
177
+ panel.innerHTML = `<div class="m-auto text-center text-red-400 font-bold text-xs">${escapeHtml(err.message)}</div>`;
178
+ }
179
+ }
180
+
181
+ // ---------- Passport page ----------
182
+
183
+ const passportForm = document.getElementById("passportForm");
184
+ if (passportForm) {
185
+ // Load any existing saved passport on page load
186
+ fetch("/api/passport")
187
+ .then((res) => res.json())
188
+ .then((data) => {
189
+ if (!data) return;
190
+ document.getElementById("passName").value = data.full_name || "";
191
+ document.getElementById("passBlood").value = data.blood_group || "";
192
+ document.getElementById("passAllergies").value = data.allergies || "";
193
+ document.getElementById("passMeds").value = data.current_medicines || "";
194
+ document.getElementById("passContact").value = data.emergency_contact_name || "";
195
+ document.getElementById("passPhone").value = data.emergency_contact_phone || "";
196
+ showQrCode();
197
+ })
198
+ .catch(() => {});
199
+
200
+ passportForm.addEventListener("submit", async (e) => {
201
+ e.preventDefault();
202
+
203
+ const payload = {
204
+ full_name: document.getElementById("passName").value,
205
+ blood_group: document.getElementById("passBlood").value,
206
+ date_of_birth: "",
207
+ allergies: document.getElementById("passAllergies").value,
208
+ chronic_conditions: "",
209
+ current_medicines: document.getElementById("passMeds").value,
210
+ emergency_contact_name: document.getElementById("passContact").value,
211
+ emergency_contact_phone: document.getElementById("passPhone").value,
212
+ };
213
+
214
+ const submitBtn = passportForm.querySelector("button[type='submit']");
215
+ const originalText = submitBtn.textContent;
216
+ submitBtn.disabled = true;
217
+ submitBtn.textContent = "Saving...";
218
+
219
+ try {
220
+ const res = await fetch("/api/passport", {
221
+ method: "POST",
222
+ headers: { "Content-Type": "application/json" },
223
+ body: JSON.stringify(payload),
224
+ });
225
+ if (!res.ok) throw new Error("Failed to save passport.");
226
+ showQrCode();
227
+ } catch (err) {
228
+ alert(err.message);
229
+ } finally {
230
+ submitBtn.disabled = false;
231
+ submitBtn.textContent = originalText;
232
+ }
233
+ });
234
+ }
235
+
236
+ function showQrCode() {
237
+ const qrContainer = document.getElementById("qrContainer");
238
+ if (!qrContainer) return;
239
+ // Cache-bust so the browser doesn't show a stale QR after an update
240
+ qrContainer.innerHTML = `<img src="/api/passport/qr?t=${Date.now()}" alt="Health Passport QR Code" class="w-40 h-40 object-contain" />`;
241
+ }