serene-abyss commited on
Commit
85b4e65
·
verified ·
1 Parent(s): a94e53c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +141 -62
app.py CHANGED
@@ -10,6 +10,7 @@ import io
10
  import gc
11
  import librosa
12
  import soundfile as sf
 
13
 
14
  # ==========================================
15
  # 1. CONFIGURATION
@@ -18,42 +19,42 @@ MODELS = {
18
  "lungs": {
19
  "type": "image",
20
  "id": "nickmuchi/vit-finetuned-chest-xray-pneumonia",
21
- "desc": "Tuberculosis & Pneumonia (Chest X-Ray)",
22
  "safe": ["NORMAL", "normal", "No Pneumonia"],
23
  "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Upload B&W X-Ray."}
24
  },
25
  "cough": {
26
  "type": "audio",
27
  "id": "MIT/ast-finetuned-audioset-10-10-0.4593",
28
- "desc": "COPD & Respiratory Screening (Cough Audio)",
29
  "target_labels": ["Cough", "Throat clearing", "Respiratory sounds", "Wheeze"],
30
  "rules": {"min_duration": 0.5, "reject_msg": "Invalid: Audio too short or silent."}
31
  },
32
  "fracture": {
33
  "type": "image",
34
  "id": "dima806/bone_fracture_detection",
35
- "desc": "Bone Fracture Detection (X-Ray)",
36
  "safe": ["normal", "healed"],
37
  "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Upload B&W X-Ray."}
38
  },
39
  "brain": {
40
  "type": "image",
41
  "id": "Hemgg/brain-tumor-classification",
42
- "desc": "Brain Tumor & Stroke Screening (MRI/CT)",
43
  "safe": ["no_tumor"],
44
  "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Upload B&W MRI Scan."}
45
  },
46
  "eye": {
47
  "type": "image",
48
  "id": "AventIQ-AI/resnet18-cataract-detection-system",
49
- "desc": "Cataract Detection (Eye Photo)",
50
  "safe": ["Normal", "normal", "healthy"],
51
  "rules": {"min_sat": 20, "min_white": 0.05, "reject_msg": "Invalid: No eye detected."}
52
  },
53
  "skin": {
54
  "type": "image",
55
  "id": "Anwarkh1/Skin_Cancer-Image_Classification",
56
- "desc": "Dermatology Analysis (Lesion Photo)",
57
  "safe": ["Benign", "benign", "nv", "bkl"],
58
  "rules": {"min_sat": 20, "max_white": 0.25, "reject_msg": "Invalid: Not a skin close-up."}
59
  }
@@ -79,7 +80,6 @@ class MedicalEngine:
79
  stat = ImageStat.Stat(img_hsv)
80
  avg_sat = stat.mean[1]
81
 
82
- # Forensics
83
  img_np = np.array(img_hsv)
84
  white_pixels = np.logical_and(img_np[:,:,1] < 40, img_np[:,:,2] > 180)
85
  white_ratio = np.sum(white_pixels) / white_pixels.size
@@ -162,11 +162,39 @@ class MedicalEngine:
162
  app = FastAPI()
163
  engine = MedicalEngine()
164
 
 
 
 
165
  @app.post("/predict/{task}")
166
- async def predict_route(task: str, file: UploadFile = File(...)):
167
  if task not in MODELS: return {"error": "Invalid Task"}
 
168
  content = await file.read()
169
- return engine.predict(content, task)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
  @app.get("/", response_class=HTMLResponse)
172
  def home():
@@ -194,20 +222,13 @@ def home():
194
  </p>
195
  </div>
196
  </div>
197
-
198
  <div class="flex flex-col items-end gap-2">
199
  <select id="lang-select" onchange="changeLanguage()" class="bg-gray-100 border border-gray-300 text-gray-700 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2">
200
  <option value="en">🇬🇧 English</option>
201
- <option value="hi">🇮🇳 हिंदी (Hindi)</option>
202
- <option value="as">🇮🇳 অসমীয়া (Assamese)</option>
203
  <option value="kha">🌲 Khasi</option>
204
  <option value="gar">⛰️ Garo</option>
205
  </select>
206
-
207
- <div class="hidden md:flex items-center gap-2 bg-blue-50 px-3 py-1 rounded text-xs font-bold border border-blue-100 text-blue-800">
208
- <img src="https://upload.wikimedia.org/wikipedia/en/c/cf/Aadhaar_Logo.svg" class="h-4">
209
- <span data-translate="abha_link">ABHA Linked</span>
210
- </div>
211
  </div>
212
  </div>
213
  </nav>
@@ -242,8 +263,13 @@ def home():
242
  </div>
243
 
244
  <div class="bg-white rounded-2xl shadow-xl p-6 border border-gray-100">
245
- <h2 id="header-text" class="text-xl font-bold text-gray-700 mb-6 text-center border-b pb-4">Select a Category</h2>
246
 
 
 
 
 
 
247
  <div id="inputs" class="opacity-50 pointer-events-none transition-opacity mb-6">
248
  <div class="grid grid-cols-2 gap-4 mb-4">
249
  <div>
@@ -252,18 +278,16 @@ def home():
252
  </div>
253
  <div>
254
  <label class="text-xs font-bold text-gray-400 uppercase" data-translate="lbl_age">Age / ID</label>
255
- <input type="text" class="w-full border p-2 rounded bg-gray-50 outline-none focus:border-blue-500">
256
  </div>
257
  </div>
258
 
259
  <div onclick="document.getElementById('file-input').click()" class="border-2 border-dashed border-gray-300 rounded-xl p-8 text-center cursor-pointer hover:bg-blue-50 transition group">
260
  <input type="file" id="file-input" class="hidden" onchange="showPreview(event)" onclick="this.value=null">
261
-
262
  <div id="placeholder" class="group-hover:scale-105 transition-transform">
263
  <i id="upload-icon" class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-2"></i>
264
  <p id="upload-text" class="text-gray-500 font-medium" data-translate="txt_upload">Tap to upload</p>
265
  </div>
266
-
267
  <img id="img-preview" class="hidden mx-auto max-h-56 rounded shadow object-contain">
268
  <audio id="audio-preview" controls class="hidden w-full mt-4"></audio>
269
  </div>
@@ -295,18 +319,43 @@ def home():
295
  <span id="alert-text">--</span>
296
  </div>
297
  </div>
298
-
299
- <div class="mt-4 flex items-center justify-between text-xs text-gray-500 border-t pt-2">
300
- <span class="font-bold flex items-center gap-1"><i class="fas fa-database"></i> <span data-translate="lbl_sync">Govt Sync</span></span>
301
- <span id="sync-msg" class="text-yellow-600"><i class="fas fa-sync fa-spin"></i> Pending...</span>
302
- </div>
303
  </div>
 
304
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  </div>
306
  </div>
307
 
308
  <script>
309
- // TRANSLATION DICTIONARY
 
 
 
 
 
 
 
 
310
  const TRANSLATIONS = {
311
  en: {
312
  govt_title: "GOVERNMENT OF MEGHALAYA",
@@ -318,7 +367,7 @@ def home():
318
  btn_run: "Run Diagnosis",
319
  txt_analyzing: "Downloading AI Model & Analyzing...",
320
  lbl_result: "Analysis Result", lbl_conf: "Confidence",
321
- lbl_action: "Action Required", lbl_sync: "Govt Sync"
322
  },
323
  hi: {
324
  govt_title: "मेघालय सरकार",
@@ -330,21 +379,9 @@ def home():
330
  btn_run: "जांच शुरू करें",
331
  txt_analyzing: "विश्लेषण कर रहा है...",
332
  lbl_result: "परिणाम", lbl_conf: "विश्वास स्तर",
333
- lbl_action: "आवश्यक कार्रवाई", lbl_sync: "सरकारी सिंक"
334
- },
335
- as: {
336
- govt_title: "মেঘালয় চৰকাৰ",
337
- online_status: "অনলাইন ন'ড: শ্বিলং",
338
- abha_link: "ABHA সংযুক্ত",
339
- btn_lungs: "হাঁওফাঁও", btn_cough: "কাহ", btn_bone: "হাৰ ভঙা", btn_brain: "মগজু", btn_eye: "চকু", btn_skin: "ছাল",
340
- lbl_name: "ৰোগীৰ নাম", lbl_age: "বয়স / আইডি",
341
- txt_upload: "ফটো আপলোড কৰক",
342
- btn_run: "পৰীক্ষা কৰক",
343
- txt_analyzing: "বিশ্লেষণ চলি আছে...",
344
- lbl_result: "ফলাফল", lbl_conf: "নিশ্চয়তা",
345
- lbl_action: "পৰৱৰ্তী পদক্ষেপ", lbl_sync: "চৰকাৰী ডাটাবেচ"
346
  },
347
- kha: { // Khasi Language
348
  govt_title: "SORKAR MEGHALAYA",
349
  online_status: "Online: Shillong",
350
  abha_link: "ABHA Link",
@@ -354,9 +391,9 @@ def home():
354
  btn_run: "Leh Test",
355
  txt_analyzing: "Dang Check...",
356
  lbl_result: "Result", lbl_conf: "Jingshisha",
357
- lbl_action: "Leh ia kane", lbl_sync: "Sorkar Sync"
358
  },
359
- gar: { // Garo Language
360
  govt_title: "MEGHALAYA SORKARI",
361
  online_status: "Online: Shillong",
362
  abha_link: "ABHA Link",
@@ -366,7 +403,7 @@ def home():
366
  btn_run: "Porikka Ka'bo",
367
  txt_analyzing: "Niyenga...",
368
  lbl_result: "Result", lbl_conf: "Bebera'ani",
369
- lbl_action: "Nangchongmotgipa Kam", lbl_sync: "Sorkar Sync"
370
  }
371
  };
372
 
@@ -375,16 +412,16 @@ def home():
375
  let currFile = null;
376
  let currLang = 'en';
377
 
 
 
 
378
  function changeLanguage() {
379
  currLang = document.getElementById('lang-select').value;
380
  let t = TRANSLATIONS[currLang];
381
-
382
  document.querySelectorAll('[data-translate]').forEach(el => {
383
  let key = el.getAttribute('data-translate');
384
  if (t[key]) el.innerText = t[key];
385
  });
386
-
387
- // Refresh Header Text if task selected
388
  if (currTask) {
389
  let taskKey = "btn_" + currTask;
390
  document.getElementById('header-text').innerHTML = `Upload <span class="uppercase text-blue-600">${t[taskKey]}</span>`;
@@ -396,16 +433,24 @@ def home():
396
  currType = type;
397
  let t = TRANSLATIONS[currLang];
398
 
399
- // Buttons
400
  document.querySelectorAll('button[id^="btn-"]').forEach(b => b.classList.remove('ring-2', 'ring-blue-400', 'border-blue-500', 'bg-blue-50'));
401
  let btn = document.getElementById('btn-'+task);
402
  btn.classList.add('ring-2', 'ring-blue-400', 'border-blue-500', 'bg-blue-50');
403
 
404
- // Text
405
  let taskName = t["btn_" + task];
406
  document.getElementById('header-text').innerHTML = `Upload <span class="uppercase text-blue-600">${taskName}</span>`;
407
-
408
- // Input Logic
 
 
 
 
 
 
 
 
 
 
409
  let input = document.getElementById('file-input');
410
  let icon = document.getElementById('upload-icon');
411
  let text = document.getElementById('upload-text');
@@ -419,7 +464,6 @@ def home():
419
  }
420
  text.innerText = t['txt_upload'];
421
 
422
- // Reset UI
423
  document.getElementById('inputs').classList.remove('opacity-50', 'pointer-events-none');
424
  document.getElementById('result-box').classList.add('hidden');
425
  document.getElementById('run-btn').classList.add('hidden');
@@ -433,7 +477,6 @@ def home():
433
  if (event.target.files && event.target.files[0]) {
434
  currFile = event.target.files[0];
435
  let url = URL.createObjectURL(currFile);
436
-
437
  if (currType === 'audio') {
438
  let aud = document.getElementById('audio-preview');
439
  aud.src = url;
@@ -445,7 +488,6 @@ def home():
445
  img.classList.remove('hidden');
446
  document.getElementById('audio-preview').classList.add('hidden');
447
  }
448
-
449
  document.getElementById('placeholder').classList.add('hidden');
450
  document.getElementById('run-btn').classList.remove('hidden');
451
  document.getElementById('result-box').classList.add('hidden');
@@ -454,7 +496,9 @@ def home():
454
 
455
  async function analyze() {
456
  if (!currTask || !currFile) return;
457
- if (!document.getElementById('p-name').value) { alert("Please enter Patient Name."); return; }
 
 
458
 
459
  document.getElementById('run-btn').classList.add('hidden');
460
  document.getElementById('loader').classList.remove('hidden');
@@ -463,12 +507,17 @@ def home():
463
  let formData = new FormData();
464
  formData.append("file", currFile);
465
 
 
466
  try {
467
- let res = await fetch("/predict/" + currTask, { method: "POST", body: formData });
 
468
  let data = await res.json();
469
 
470
  document.getElementById('loader').classList.add('hidden');
471
  document.getElementById('result-box').classList.remove('hidden');
 
 
 
472
 
473
  if (data.risk === "INVALID") {
474
  document.getElementById('res-label').innerText = "Rejected";
@@ -494,11 +543,6 @@ def home():
494
  document.getElementById('alert-box').classList.add('hidden');
495
  }
496
 
497
- setTimeout(() => {
498
- document.getElementById('sync-msg').innerHTML = "<i class='fas fa-check-circle'></i> Synced!";
499
- document.getElementById('sync-msg').className = "text-green-600 font-bold";
500
- }, 2000);
501
-
502
  } catch (e) {
503
  alert("Connection Failed.");
504
  console.error(e);
@@ -507,6 +551,41 @@ def home():
507
  }
508
  }
509
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
  function updateBadge(text, bg, color) {
511
  let b = document.getElementById('res-badge');
512
  b.innerText = text;
 
10
  import gc
11
  import librosa
12
  import soundfile as sf
13
+ from datetime import datetime
14
 
15
  # ==========================================
16
  # 1. CONFIGURATION
 
19
  "lungs": {
20
  "type": "image",
21
  "id": "nickmuchi/vit-finetuned-chest-xray-pneumonia",
22
+ "desc": "Chest X-Ray Analysis",
23
  "safe": ["NORMAL", "normal", "No Pneumonia"],
24
  "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Upload B&W X-Ray."}
25
  },
26
  "cough": {
27
  "type": "audio",
28
  "id": "MIT/ast-finetuned-audioset-10-10-0.4593",
29
+ "desc": "Respiratory Audio Analysis",
30
  "target_labels": ["Cough", "Throat clearing", "Respiratory sounds", "Wheeze"],
31
  "rules": {"min_duration": 0.5, "reject_msg": "Invalid: Audio too short or silent."}
32
  },
33
  "fracture": {
34
  "type": "image",
35
  "id": "dima806/bone_fracture_detection",
36
+ "desc": "Bone Trauma X-Ray",
37
  "safe": ["normal", "healed"],
38
  "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Upload B&W X-Ray."}
39
  },
40
  "brain": {
41
  "type": "image",
42
  "id": "Hemgg/brain-tumor-classification",
43
+ "desc": "Brain MRI Scan Analysis",
44
  "safe": ["no_tumor"],
45
  "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Upload B&W MRI Scan."}
46
  },
47
  "eye": {
48
  "type": "image",
49
  "id": "AventIQ-AI/resnet18-cataract-detection-system",
50
+ "desc": "Ophthalmology Scan",
51
  "safe": ["Normal", "normal", "healthy"],
52
  "rules": {"min_sat": 20, "min_white": 0.05, "reject_msg": "Invalid: No eye detected."}
53
  },
54
  "skin": {
55
  "type": "image",
56
  "id": "Anwarkh1/Skin_Cancer-Image_Classification",
57
+ "desc": "Dermatology Lesion Scan",
58
  "safe": ["Benign", "benign", "nv", "bkl"],
59
  "rules": {"min_sat": 20, "max_white": 0.25, "reject_msg": "Invalid: Not a skin close-up."}
60
  }
 
80
  stat = ImageStat.Stat(img_hsv)
81
  avg_sat = stat.mean[1]
82
 
 
83
  img_np = np.array(img_hsv)
84
  white_pixels = np.logical_and(img_np[:,:,1] < 40, img_np[:,:,2] > 180)
85
  white_ratio = np.sum(white_pixels) / white_pixels.size
 
162
  app = FastAPI()
163
  engine = MedicalEngine()
164
 
165
+ # IN-MEMORY HISTORY STORAGE
166
+ HISTORY = []
167
+
168
  @app.post("/predict/{task}")
169
+ async def predict_route(task: str, patient: str, age: str, file: UploadFile = File(...)):
170
  if task not in MODELS: return {"error": "Invalid Task"}
171
+
172
  content = await file.read()
173
+ result = engine.predict(content, task)
174
+
175
+ # Save to History if valid
176
+ if "error" not in result and result.get("risk") != "INVALID":
177
+ record = {
178
+ "time": datetime.now().strftime("%H:%M:%S"),
179
+ "patient": patient,
180
+ "age": age,
181
+ "task": task.capitalize(),
182
+ "diagnosis": result["prediction"]["label"],
183
+ "risk": result["risk"]
184
+ }
185
+ HISTORY.insert(0, record) # Add to top
186
+
187
+ return result
188
+
189
+ @app.get("/history")
190
+ def get_history():
191
+ return HISTORY
192
+
193
+ @app.post("/reset_history")
194
+ def reset_history():
195
+ global HISTORY
196
+ HISTORY = []
197
+ return {"status": "cleared"}
198
 
199
  @app.get("/", response_class=HTMLResponse)
200
  def home():
 
222
  </p>
223
  </div>
224
  </div>
 
225
  <div class="flex flex-col items-end gap-2">
226
  <select id="lang-select" onchange="changeLanguage()" class="bg-gray-100 border border-gray-300 text-gray-700 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2">
227
  <option value="en">🇬🇧 English</option>
228
+ <option value="hi">🇮🇳 हिंदी</option>
 
229
  <option value="kha">🌲 Khasi</option>
230
  <option value="gar">⛰️ Garo</option>
231
  </select>
 
 
 
 
 
232
  </div>
233
  </div>
234
  </nav>
 
263
  </div>
264
 
265
  <div class="bg-white rounded-2xl shadow-xl p-6 border border-gray-100">
266
+ <h2 id="header-text" class="text-xl font-bold text-gray-700 mb-2 text-center">Select a Category</h2>
267
 
268
+ <div id="scope-box" class="hidden mb-6 text-center">
269
+ <p class="text-xs font-bold text-gray-400 uppercase mb-2" data-translate="lbl_scope">Scope of Detection</p>
270
+ <div id="scope-tags" class="flex flex-wrap justify-center gap-2"></div>
271
+ </div>
272
+
273
  <div id="inputs" class="opacity-50 pointer-events-none transition-opacity mb-6">
274
  <div class="grid grid-cols-2 gap-4 mb-4">
275
  <div>
 
278
  </div>
279
  <div>
280
  <label class="text-xs font-bold text-gray-400 uppercase" data-translate="lbl_age">Age / ID</label>
281
+ <input type="text" id="p-age" class="w-full border p-2 rounded bg-gray-50 outline-none focus:border-blue-500">
282
  </div>
283
  </div>
284
 
285
  <div onclick="document.getElementById('file-input').click()" class="border-2 border-dashed border-gray-300 rounded-xl p-8 text-center cursor-pointer hover:bg-blue-50 transition group">
286
  <input type="file" id="file-input" class="hidden" onchange="showPreview(event)" onclick="this.value=null">
 
287
  <div id="placeholder" class="group-hover:scale-105 transition-transform">
288
  <i id="upload-icon" class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-2"></i>
289
  <p id="upload-text" class="text-gray-500 font-medium" data-translate="txt_upload">Tap to upload</p>
290
  </div>
 
291
  <img id="img-preview" class="hidden mx-auto max-h-56 rounded shadow object-contain">
292
  <audio id="audio-preview" controls class="hidden w-full mt-4"></audio>
293
  </div>
 
319
  <span id="alert-text">--</span>
320
  </div>
321
  </div>
 
 
 
 
 
322
  </div>
323
+ </div>
324
 
325
+ <div class="mt-8">
326
+ <div class="flex justify-between items-center mb-4">
327
+ <h3 class="text-lg font-bold text-gray-700 uppercase tracking-wide"><i class="fas fa-history mr-2"></i> Recent Patients</h3>
328
+ <button onclick="clearHistory()" class="text-xs text-red-500 hover:text-red-700 font-bold border border-red-200 px-3 py-1 rounded hover:bg-red-50">CLEAR HISTORY</button>
329
+ </div>
330
+ <div class="bg-white rounded-xl shadow overflow-hidden border border-gray-100">
331
+ <table class="w-full text-sm text-left">
332
+ <thead class="bg-gray-50 text-gray-500 font-bold uppercase text-xs">
333
+ <tr>
334
+ <th class="px-6 py-3">Time</th>
335
+ <th class="px-6 py-3">Patient</th>
336
+ <th class="px-6 py-3">Category</th>
337
+ <th class="px-6 py-3">Diagnosis</th>
338
+ <th class="px-6 py-3">Risk</th>
339
+ </tr>
340
+ </thead>
341
+ <tbody id="history-table" class="divide-y divide-gray-100">
342
+ <tr class="text-gray-400 text-center"><td colspan="5" class="py-4">No records yet.</td></tr>
343
+ </tbody>
344
+ </table>
345
+ </div>
346
  </div>
347
  </div>
348
 
349
  <script>
350
+ const MODEL_SCOPES = {
351
+ lungs: ["Pneumonia", "Tuberculosis", "Viral Infection", "Normal Lung"],
352
+ cough: ["COPD Signs", "Whooping Cough", "Wheezing", "Respiratory Infection"],
353
+ fracture: ["Bone Fracture", "Dislocation", "Healthy Bone Structure"],
354
+ brain: ["Glioma Tumor", "Meningioma Tumor", "Pituitary Tumor", "No Tumor"],
355
+ eye: ["Cataract", "Diabetic Retinopathy", "Glaucoma", "Normal Eye"],
356
+ skin: ["Melanoma", "Basal Cell Carcinoma", "Nevus (Mole)", "Benign Keratosis"]
357
+ };
358
+
359
  const TRANSLATIONS = {
360
  en: {
361
  govt_title: "GOVERNMENT OF MEGHALAYA",
 
367
  btn_run: "Run Diagnosis",
368
  txt_analyzing: "Downloading AI Model & Analyzing...",
369
  lbl_result: "Analysis Result", lbl_conf: "Confidence",
370
+ lbl_action: "Action Required", lbl_scope: "Scope of Detection"
371
  },
372
  hi: {
373
  govt_title: "मेघालय सरकार",
 
379
  btn_run: "जांच शुरू करें",
380
  txt_analyzing: "विश्लेषण कर रहा है...",
381
  lbl_result: "परिणाम", lbl_conf: "विश्वास स्तर",
382
+ lbl_action: "आवश्यक कार्रवाई", lbl_scope: "जांच का दायरा"
 
 
 
 
 
 
 
 
 
 
 
 
383
  },
384
+ kha: {
385
  govt_title: "SORKAR MEGHALAYA",
386
  online_status: "Online: Shillong",
387
  abha_link: "ABHA Link",
 
391
  btn_run: "Leh Test",
392
  txt_analyzing: "Dang Check...",
393
  lbl_result: "Result", lbl_conf: "Jingshisha",
394
+ lbl_action: "Leh ia kane", lbl_scope: "Jingshem Jong Ka Test"
395
  },
396
+ gar: {
397
  govt_title: "MEGHALAYA SORKARI",
398
  online_status: "Online: Shillong",
399
  abha_link: "ABHA Link",
 
403
  btn_run: "Porikka Ka'bo",
404
  txt_analyzing: "Niyenga...",
405
  lbl_result: "Result", lbl_conf: "Bebera'ani",
406
+ lbl_action: "Nangchongmotgipa Kam", lbl_scope: "Am·sandiani"
407
  }
408
  };
409
 
 
412
  let currFile = null;
413
  let currLang = 'en';
414
 
415
+ // Load History on Start
416
+ updateHistoryTable();
417
+
418
  function changeLanguage() {
419
  currLang = document.getElementById('lang-select').value;
420
  let t = TRANSLATIONS[currLang];
 
421
  document.querySelectorAll('[data-translate]').forEach(el => {
422
  let key = el.getAttribute('data-translate');
423
  if (t[key]) el.innerText = t[key];
424
  });
 
 
425
  if (currTask) {
426
  let taskKey = "btn_" + currTask;
427
  document.getElementById('header-text').innerHTML = `Upload <span class="uppercase text-blue-600">${t[taskKey]}</span>`;
 
433
  currType = type;
434
  let t = TRANSLATIONS[currLang];
435
 
 
436
  document.querySelectorAll('button[id^="btn-"]').forEach(b => b.classList.remove('ring-2', 'ring-blue-400', 'border-blue-500', 'bg-blue-50'));
437
  let btn = document.getElementById('btn-'+task);
438
  btn.classList.add('ring-2', 'ring-blue-400', 'border-blue-500', 'bg-blue-50');
439
 
 
440
  let taskName = t["btn_" + task];
441
  document.getElementById('header-text').innerHTML = `Upload <span class="uppercase text-blue-600">${taskName}</span>`;
442
+
443
+ let scopeBox = document.getElementById('scope-box');
444
+ let scopeTags = document.getElementById('scope-tags');
445
+ scopeTags.innerHTML = "";
446
+ MODEL_SCOPES[task].forEach(disease => {
447
+ let tag = document.createElement("span");
448
+ tag.className = "px-2 py-1 bg-blue-50 text-blue-800 text-xs rounded border border-blue-100 font-semibold";
449
+ tag.innerText = disease;
450
+ scopeTags.appendChild(tag);
451
+ });
452
+ scopeBox.classList.remove('hidden');
453
+
454
  let input = document.getElementById('file-input');
455
  let icon = document.getElementById('upload-icon');
456
  let text = document.getElementById('upload-text');
 
464
  }
465
  text.innerText = t['txt_upload'];
466
 
 
467
  document.getElementById('inputs').classList.remove('opacity-50', 'pointer-events-none');
468
  document.getElementById('result-box').classList.add('hidden');
469
  document.getElementById('run-btn').classList.add('hidden');
 
477
  if (event.target.files && event.target.files[0]) {
478
  currFile = event.target.files[0];
479
  let url = URL.createObjectURL(currFile);
 
480
  if (currType === 'audio') {
481
  let aud = document.getElementById('audio-preview');
482
  aud.src = url;
 
488
  img.classList.remove('hidden');
489
  document.getElementById('audio-preview').classList.add('hidden');
490
  }
 
491
  document.getElementById('placeholder').classList.add('hidden');
492
  document.getElementById('run-btn').classList.remove('hidden');
493
  document.getElementById('result-box').classList.add('hidden');
 
496
 
497
  async function analyze() {
498
  if (!currTask || !currFile) return;
499
+ let pname = document.getElementById('p-name').value;
500
+ let page = document.getElementById('p-age').value;
501
+ if (!pname) { alert("Please enter Patient Name."); return; }
502
 
503
  document.getElementById('run-btn').classList.add('hidden');
504
  document.getElementById('loader').classList.remove('hidden');
 
507
  let formData = new FormData();
508
  formData.append("file", currFile);
509
 
510
+ // Pass patient details to backend to save history
511
  try {
512
+ let url = `/predict/${currTask}?patient=${encodeURIComponent(pname)}&age=${encodeURIComponent(page)}`;
513
+ let res = await fetch(url, { method: "POST", body: formData });
514
  let data = await res.json();
515
 
516
  document.getElementById('loader').classList.add('hidden');
517
  document.getElementById('result-box').classList.remove('hidden');
518
+
519
+ // Refresh History Table
520
+ updateHistoryTable();
521
 
522
  if (data.risk === "INVALID") {
523
  document.getElementById('res-label').innerText = "Rejected";
 
543
  document.getElementById('alert-box').classList.add('hidden');
544
  }
545
 
 
 
 
 
 
546
  } catch (e) {
547
  alert("Connection Failed.");
548
  console.error(e);
 
551
  }
552
  }
553
 
554
+ async function updateHistoryTable() {
555
+ try {
556
+ let res = await fetch("/history");
557
+ let data = await res.json();
558
+ let tbody = document.getElementById('history-table');
559
+ tbody.innerHTML = "";
560
+
561
+ if(data.length === 0) {
562
+ tbody.innerHTML = '<tr class="text-gray-400 text-center"><td colspan="5" class="py-4">No records yet.</td></tr>';
563
+ return;
564
+ }
565
+
566
+ data.forEach(row => {
567
+ let color = row.risk === "HIGH" ? "text-red-600 font-bold" : row.risk === "MODERATE" ? "text-yellow-600 font-bold" : "text-green-600";
568
+ let tr = `
569
+ <tr class="bg-white border-b hover:bg-gray-50">
570
+ <td class="px-6 py-4 text-gray-500">${row.time}</td>
571
+ <td class="px-6 py-4 font-bold text-gray-800">${row.patient}</td>
572
+ <td class="px-6 py-4 text-gray-600">${row.task}</td>
573
+ <td class="px-6 py-4 text-gray-800">${row.diagnosis}</td>
574
+ <td class="px-6 py-4 ${color}">${row.risk}</td>
575
+ </tr>
576
+ `;
577
+ tbody.innerHTML += tr;
578
+ });
579
+ } catch(e) { console.error("History fetch failed"); }
580
+ }
581
+
582
+ async function clearHistory() {
583
+ if(confirm("Delete all history?")) {
584
+ await fetch("/reset_history", { method: "POST" });
585
+ updateHistoryTable();
586
+ }
587
+ }
588
+
589
  function updateBadge(text, bg, color) {
590
  let b = document.getElementById('res-badge');
591
  b.innerText = text;