TEZv commited on
Commit
0ed0ac7
·
verified ·
1 Parent(s): 754fb34

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +416 -114
app.py CHANGED
@@ -1,14 +1,28 @@
 
 
 
 
 
 
1
  import gradio as gr
2
  import pandas as pd
3
  import numpy as np
4
- import json, re, csv
 
 
 
 
 
 
 
5
  import matplotlib
6
  matplotlib.use("Agg")
7
  import matplotlib.pyplot as plt
8
  from io import BytesIO
9
  from PIL import Image
10
- from datetime import datetime
11
  from pathlib import Path
 
 
12
  import plotly.graph_objects as go
13
  import plotly.express as px
14
  import tabulate
@@ -28,49 +42,127 @@ RED = "#ef4444"
28
  DIM = "#8e9bae"
29
  BORDER = "#334155"
30
 
31
- # ========== Логування ==========
32
- LOG_PATH = Path("/tmp/lab_journal.csv")
33
-
34
- def log_entry(tab, inputs, result, note=""):
35
- try:
36
- write_header = not LOG_PATH.exists()
37
- with open(LOG_PATH, "a", newline="", encoding="utf-8") as f:
38
- w = csv.DictWriter(f, fieldnames=["timestamp","tab","inputs","result","note"])
39
- if write_header: w.writeheader()
40
- w.writerow({"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
41
- "tab": tab, "inputs": str(inputs),
42
- "result": str(result)[:200], "note": note})
43
- except Exception as e:
44
- print(f"Log error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
- def load_journal(category=None):
47
- """Load journal entries, optionally filtered by category. Returns markdown string."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  try:
49
- if not LOG_PATH.exists() or LOG_PATH.stat().st_size == 0:
50
- return "No entries yet."
51
- df = pd.read_csv(LOG_PATH)
52
  if df.empty:
53
  return "No entries yet."
54
- if category and category != "All":
55
- df = df[df["tab"] == category]
56
  if df.empty:
57
- return "No entries for this category."
58
- return df.tail(50).to_markdown(index=False)
 
 
59
  except Exception as e:
60
- print(f"Journal load error: {e}")
61
- return "Error loading journal."
62
-
63
- def save_note(note, category):
64
- if note.strip():
65
- log_entry(category, "manual note", note, note)
66
- return "Note saved."
67
 
68
  def clear_journal():
69
  try:
70
- if LOG_PATH.exists():
71
- LOG_PATH.unlink()
72
  return "Journal cleared."
73
- except Exception:
 
74
  return "Error clearing journal."
75
 
76
  # ========== БАЗИ ДАНИХ ==========
@@ -227,9 +319,7 @@ PAML_BM_LNP = [
227
  {"Formulation":"DLin-MC3-DPPC","BM_protein":"Vitronectin-rich","Size_nm":91,"Zeta_mV":-2.9,"Marrow_uptake_pct":19,"Priority":"MEDIUM"},
228
  {"Formulation":"Cationic-DOTAP-Chol","BM_protein":"Opsonin-heavy","Size_nm":132,"Zeta_mV":+8.1,"Marrow_uptake_pct":8,"Priority":"LOW"},
229
  ]
230
-
231
- # ========== ФУНКЦІЇ ПРЕДИКЦІЇ (з обробкою помилок) ==========
232
-
233
  def safe_img_from_fig(fig):
234
  """Convert matplotlib figure to PIL Image safely."""
235
  try:
@@ -243,22 +333,23 @@ def safe_img_from_fig(fig):
243
  plt.close(fig)
244
  return Image.new('RGB', (100, 100), color=CARD)
245
 
 
246
  def predict_mirna(gene):
247
  df = pd.DataFrame(MIRNA_DB.get(gene, []))
248
- log_entry("S1-B·R1a", gene, f"{len(df)} miRNAs")
249
  return df
250
 
251
  def predict_sirna(cancer):
252
  df = pd.DataFrame(SIRNA_DB.get(cancer, []))
253
- log_entry("S1-B·R2a", cancer, f"{len(df)} targets")
254
  return df
255
 
256
  def get_lncrna():
257
- log_entry("S1-B·R3a", "load", "ceRNA")
258
  return pd.DataFrame(CERNA)
259
 
260
  def get_aso():
261
- log_entry("S1-B·R3b", "load", "ASO")
262
  return pd.DataFrame(ASO)
263
 
264
  def predict_drug(pocket):
@@ -275,10 +366,10 @@ def predict_drug(pocket):
275
  ax.set_title(f"Top compounds — {pocket}", color=TXT, fontsize=10)
276
  plt.tight_layout()
277
  img = safe_img_from_fig(fig)
278
- log_entry("S1-C·R1a", pocket, f"Top: {df.iloc[0]['Compound'] if len(df) else 'none'}")
279
  return df, img
280
  except Exception as e:
281
- log_entry("S1-C·R1a", pocket, f"Error: {str(e)}")
282
  return pd.DataFrame(), None
283
 
284
  def predict_variant(hgvs, sift, polyphen, gnomad):
@@ -295,7 +386,7 @@ def predict_variant(hgvs, sift, polyphen, gnomad):
295
  conf = "High" if (sift < 0.01 or sift > 0.9) else "Moderate"
296
  colour = RED if "Pathogenic" in cls else GRN
297
  icon = "⚠️ WARNING" if "Pathogenic" in cls else "✅ OK"
298
- log_entry("S1-A·R1a", hgvs or f"SIFT={sift}", f"{cls} score={score}")
299
  return (
300
  f"<div style=\'background:{CARD};padding:16px;border-radius:8px;font-family:sans-serif;color:{TXT}\'>"
301
  f"<p style=\'font-size:11px;color:{DIM};margin:0 0 8px\'>S1-A·R1a · OpenVariant</p>"
@@ -317,7 +408,7 @@ def predict_corona(size, zeta, peg, lipid):
317
  if size < 100: score += 1
318
  dominant = ["ApoE","Albumin","Fibrinogen","Vitronectin","ApoA-I"][min(score, 4)]
319
  efficacy = "High" if score >= 4 else "Medium" if score >= 2 else "Low"
320
- log_entry("S1-D·R1a", f"size={size},peg={peg}", f"dominant={dominant}")
321
  return f"**Dominant corona protein:** {dominant}\n\n**Predicted efficacy:** {efficacy}\n\n**Score:** {score}/6"
322
  except Exception as e:
323
  return f"Error: {str(e)}"
@@ -341,7 +432,7 @@ def predict_cancer(c1,c2,c3,c4,c5,c6,c7,c8,c9,c10):
341
  ax.set_title("Protein contributions", color=TXT, fontsize=10)
342
  plt.tight_layout()
343
  img = safe_img_from_fig(fig)
344
- log_entry("S1-E·R1a", f"CTHRC1={c1},FHL2={c2}", f"{label} {prob:.2f}")
345
  html_out = (
346
  f"<div style=\'background:{CARD};padding:14px;border-radius:8px;font-family:sans-serif;\'>"
347
  f"<p style=\'font-size:11px;color:{DIM};margin:0 0 6px\'>S1-E·R1a · Liquid Biopsy</p>"
@@ -370,7 +461,7 @@ def predict_flow(size, zeta, peg, charge, flow_rate):
370
  ax.set_title("Vroman Effect — flow vs static", color=TXT, fontsize=9)
371
  plt.tight_layout()
372
  img = safe_img_from_fig(fig)
373
- log_entry("S1-D·R2a", f"flow={flow_rate}", f"CSI={csi}")
374
  return f"**Corona Shift Index: {csi}** — {stability}", img
375
  except Exception as e:
376
  return f"Error: {str(e)}", None
@@ -392,7 +483,7 @@ def predict_bbb(smiles, pka, zeta):
392
  ax.tick_params(colors=TXT)
393
  plt.tight_layout()
394
  img = safe_img_from_fig(fig)
395
- log_entry("S1-D·R3a", f"pka={pka},zeta={zeta}", f"ApoE={apoe_pct:.1f}%")
396
  return f"**Predicted ApoE:** {apoe_pct:.1f}% — {tier}\n\n**BBB Probability:** {bbb_prob:.2f}", img
397
  except Exception as e:
398
  return f"Error: {str(e)}", None
@@ -418,15 +509,16 @@ def extract_corona(text):
418
  if not out["zeta_mv"]: flags.append("zeta_mv not found")
419
  if not out["corona_proteins"]: flags.append("no proteins detected")
420
  summary = "All key fields extracted" if not flags else " | ".join(flags)
421
- log_entry("S1-D·R4a", text[:80], f"proteins={len(out['corona_proteins'])}")
422
  return json.dumps(out, indent=2), summary
423
  except Exception as e:
424
  return json.dumps({"error": str(e)}), "Extraction error"
425
 
 
426
  def dipg_variants(sort_by):
427
  df = pd.DataFrame(DIPG_VARIANTS).sort_values(
428
  "Freq_pct" if sort_by == "Frequency" else "Drug_status", ascending=False)
429
- log_entry("S1-F·R1a", sort_by, f"{len(df)} variants")
430
  return df
431
 
432
  def dipg_csf(peg, size):
@@ -444,14 +536,14 @@ def dipg_csf(peg, size):
444
  ax.set_title("DIPG — CSF LNP formulations (ApoE%)", color=TXT, fontsize=9)
445
  plt.tight_layout()
446
  img = safe_img_from_fig(fig)
447
- log_entry("S1-F·R1a", f"peg={peg},size={size}", "formulation ranking")
448
  return df[["Formulation","Size_nm","Zeta_mV","ApoE_pct","BBB_est","Priority"]], img
449
  except Exception as e:
450
  return pd.DataFrame(), None
451
 
452
  def uvm_variants():
453
  df = pd.DataFrame(UVM_VARIANTS)
454
- log_entry("S1-F·R2a", "load", f"{len(df)} variants")
455
  return df
456
 
457
  def uvm_vitreous():
@@ -467,7 +559,7 @@ def uvm_vitreous():
467
  ax.set_title("UVM — LNP retention in vitreous humor", color=TXT, fontsize=9)
468
  plt.tight_layout()
469
  img = safe_img_from_fig(fig)
470
- log_entry("S1-F·R2a", "load", "vitreous LNP ranking")
471
  return df, img
472
  except Exception as e:
473
  return pd.DataFrame(), None
@@ -491,7 +583,7 @@ def paml_ferroptosis(variant):
491
  ax.set_title(f"pAML · {row['Variant'][:20]}", color=TXT, fontsize=9)
492
  plt.tight_layout()
493
  img = safe_img_from_fig(fig)
494
- log_entry("S1-F·R3a", variant, f"ferr={ferr_score:.2f}")
495
  _v = row["Variant"]
496
  _p = row["Pathway"]
497
  _d = row["Drug_status"]
@@ -512,29 +604,153 @@ def paml_ferroptosis(variant):
512
  except Exception as e:
513
  return f"<div style='color:{RED}'>Error: {str(e)}</div>", None
514
 
515
- # ========== ДОПОМІЖНІ ФУНКЦІЇ ==========
516
- def section_header(code, name, tagline, projects_html):
517
- return (
518
- f"<div style=\'background:{BG};border:1px solid {BORDER};padding:14px 18px;"
519
- f"border-radius:8px;margin-bottom:12px;\'>"
520
- f"<div style=\'display:flex;align-items:baseline;gap:10px;\'>"
521
- f"<span style=\'color:{ACC2};font-size:16px;font-weight:700\'>{code}</span>"
522
- f"<span style=\'color:{TXT};font-size:14px;font-weight:600\'>{name}</span>"
523
- f"<span style=\'color:{DIM};font-size:12px\'>{tagline}</span></div>"
524
- f"<div style=\'margin-top:8px;font-size:12px;color:{DIM}\'>{projects_html}</div>"
525
- f"</div>"
526
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
 
528
- def proj_badge(code, title, metric=""):
529
- m = (f"<span style=\'background:#0f2a3f;color:{ACC2};padding:1px 7px;border-radius:3px;"
530
- f"font-size:10px;margin-left:6px\'>{metric}</span>") if metric else ""
531
- return (
532
- f"<div style=\'background:{CARD};border-left:3px solid {ACC};"
533
- f"padding:8px 12px;border-radius:0 6px 6px 0;margin-bottom:8px;\'>"
534
- f"<span style=\'color:{DIM};font-size:11px\'>{code}</span>{m}<br>"
535
- f"<span style=\'color:{TXT};font-size:14px;font-weight:600\'>{title}</span>"
536
- f"</div>"
537
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
 
539
  # ========== 3D моделі ==========
540
  def plot_nanoparticle(r, peg):
@@ -609,11 +825,35 @@ def plot_corona():
609
  width=500, height=400
610
  )
611
  return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  # ========== CSS ==========
613
  css = f"""
614
  body, .gradio-container {{ background: {BG} !important; color: {TXT} !important; }}
615
 
616
- /* Вкладки верхнього рівня (категорії) з переносом на новий ряд */
617
  .tabs-outer .tab-nav {{
618
  display: flex;
619
  flex-wrap: wrap;
@@ -631,6 +871,7 @@ body, .gradio-container {{ background: {BG} !important; color: {TXT} !important;
631
  margin-right: 2px;
632
  margin-bottom: 2px;
633
  white-space: nowrap;
 
634
  }}
635
  .tabs-outer .tab-nav button.selected {{
636
  border-bottom: 3px solid {ACC} !important;
@@ -649,6 +890,7 @@ body, .gradio-container {{ background: {BG} !important; color: {TXT} !important;
649
  border: 1px solid {BORDER} !important;
650
  border-bottom: none !important;
651
  margin-right: 3px !important;
 
652
  }}
653
  .main-tabs .tab-nav button.selected {{
654
  color: {ACC2} !important;
@@ -670,6 +912,7 @@ body, .gradio-container {{ background: {BG} !important; color: {TXT} !important;
670
  font-size: 11px !important;
671
  padding: 3px 8px !important;
672
  border-radius: 3px 3px 0 0 !important;
 
673
  }}
674
  .sub-tabs .tab-nav button.selected {{
675
  color: {ACC} !important;
@@ -702,7 +945,7 @@ body, .gradio-container {{ background: {BG} !important; color: {TXT} !important;
702
  margin-left: 6px;
703
  }}
704
 
705
- /* Бічна панель з журналом (тільки введення) */
706
  .sidebar-journal {{
707
  background: {CARD};
708
  border: 1px solid {BORDER};
@@ -716,19 +959,18 @@ body, .gradio-container {{ background: {BG} !important; color: {TXT} !important;
716
 
717
  /* Загальні */
718
  h1, h2, h3 {{ color: {ACC} !important; }}
719
- .gr-button-primary {{ background: {ACC} !important; border: none !important; }}
 
720
  footer {{ display: none !important; }}
721
 
722
- /* Зміна курсора на pointer для всіх інтерактивних елементів */
723
  .gr-dropdown, .gr-button, .gr-slider, .gr-radio, .gr-checkbox,
724
- .tab-nav button, .gr-accordion, .gr-textbox, .gr-number, .gr-dataset {{
725
  cursor: pointer !important;
726
  }}
727
- /* Для текстових полів залишаємо звичайний текстовий курсор */
728
- .gr-textbox input, .gr-textarea textarea {{
729
  cursor: text !important;
730
  }}
731
-
732
  """
733
 
734
  # ========== MAP HTML ==========
@@ -754,10 +996,12 @@ MAP_HTML = f"""
754
  &nbsp;&nbsp;&nbsp;├─ <b>S1-D·R2a</b> Flow corona — Vroman effect <span style="color:{GRN}"> ✅</span><br>
755
  &nbsp;&nbsp;&nbsp;├─ <b>S1-D·R3a</b> LNP brain / BBB / ApoE <span style="color:{GRN}"> ✅</span><br>
756
  &nbsp;&nbsp;&nbsp;├─ <b>S1-D·R4a</b> AutoCorona NLP <span style="color:{GRN}"> F1=0.71 ✅</span><br>
757
- &nbsp;&nbsp;&nbsp;─ <b>S1-D·R5a</b> CSF · Vitreous · Bone Marrow <span style="color:{DIM}"> 🔴 0 prior studies</span><br><br>
 
758
  <span style="color:{ACC2};font-weight:600">S1-E · PHYLO-BIOMARKERS</span> — Detect without biopsy<br>
759
  &nbsp;&nbsp;&nbsp;├─ <b>S1-E·R1a</b> Liquid Biopsy classifier <span style="color:{GRN}"> AUC=0.992* ✅</span><br>
760
- &nbsp;&nbsp;&nbsp;─ <b>S1-E·R1b</b> Protein panel validator <span style="color:#f59e0b"> 🔶</span><br><br>
 
761
  <span style="color:{ACC2};font-weight:600">S1-F · PHYLO-RARE</span> — Where almost nobody has looked yet<br>
762
  &nbsp;&nbsp;&nbsp;├─ <b>S1-F·R1a</b> DIPG toolkit (H3K27M + CSF LNP + Circadian) <span style="color:#f59e0b"> 🔶</span><br>
763
  &nbsp;&nbsp;&nbsp;├─ <b>S1-F·R2a</b> UVM toolkit (GNAQ/GNA11 + vitreous + m6A) <span style="color:#f59e0b"> 🔶</span><br>
@@ -770,7 +1014,7 @@ MAP_HTML = f"""
770
  </div>
771
  """
772
 
773
- # ========== UI З ДВОМА КОЛОНКАМИ ==========
774
  with gr.Blocks(css=css, title="K R&D Lab · S1 Biomedical") as demo:
775
  gr.Markdown(
776
  "# 🔬 K R&D Lab · Science Sphere — S1 Biomedical\n"
@@ -959,7 +1203,7 @@ with gr.Blocks(css=css, title="K R&D Lab · S1 Biomedical") as demo:
959
  "Incubated in human plasma. Corona: albumin, apolipoprotein E, fibrinogen."
960
  ]], inputs=[txt])
961
  b10.click(extract_corona, txt, [o10j, o10f])
962
- # R5 · Exotic fluids
963
  with gr.TabItem("R5 · Exotic fluids 🔴⭐"):
964
  with gr.Tabs(elem_classes="sub-tabs"):
965
  with gr.TabItem("R5a · CSF/Vitreous/BM"):
@@ -970,6 +1214,28 @@ with gr.Blocks(css=css, title="K R&D Lab · S1 Biomedical") as demo:
970
  "> **Target cancers:** DIPG (CSF) · UVM (vitreous) · pAML (bone marrow)\n\n"
971
  "> **Expected timeline:** Q2–Q3 2026"
972
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
973
 
974
  # === S1-E · PHYLO-BIOMARKERS ===
975
  with gr.TabItem("🩸 S1-E"):
@@ -1003,6 +1269,28 @@ with gr.Blocks(css=css, title="K R&D Lab · S1 Biomedical") as demo:
1003
  with gr.TabItem("R1b · Protein Validator 🔶"):
1004
  gr.HTML(proj_badge("S1-E·R1b", "Protein Panel Validator", "🔶 In progress"))
1005
  gr.Markdown("> Coming next — validates R1a results against GEO plasma proteomics datasets.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1006
 
1007
  # === S1-F · PHYLO-RARE ===
1008
  with gr.TabItem("🧠 S1-F"):
@@ -1212,48 +1500,62 @@ with gr.Blocks(css=css, title="K R&D Lab · S1 Biomedical") as demo:
1212
  | S1-D·R3a | PHYLO-LNP | LNP Brain | pKa 6.2–6.8 |
1213
  | S1-E·R1a | PHYLO-BIOMARKERS | Liquid Biopsy | AUC=0.992* |
1214
  | S1-D·R4a | PHYLO-LNP | AutoCorona NLP | F1=0.71 |
 
 
1215
  """)
 
1216
  # === Journal (окрема вкладка) ===
1217
  with gr.TabItem("📓 Journal"):
1218
  gr.Markdown("## Lab Journal — Full History")
1219
  with gr.Row():
1220
- filter_dropdown = gr.Dropdown(
1221
- choices=["All"] + PROJECT_CODES,
1222
  value="All",
1223
- label="Filter by project code"
1224
  )
1225
- refresh_btn = gr.Button("🔄 Refresh", size="sm")
1226
- clear_btn = gr.Button("🗑️ Clear Journal", size="sm")
1227
- journal_display = gr.Markdown(value=load_journal())
 
 
 
1228
 
1229
- refresh_btn.click(load_journal, inputs=[filter_dropdown], outputs=journal_display)
1230
  clear_btn.click(clear_journal, [], journal_display).then(
1231
- load_journal, inputs=[filter_dropdown], outputs=journal_display
1232
  )
1233
- filter_dropdown.change(load_journal, inputs=[filter_dropdown], outputs=journal_display)
1234
 
1235
- # Права колонка – швидке введення нотаток (без відображення)
1236
  with gr.Column(scale=1, min_width=260):
1237
  with gr.Group(elem_classes="sidebar-journal"):
1238
- gr.Markdown("## 📓 Lab Journal")
1239
- note_dropdown = gr.Dropdown(
1240
- choices=PROJECT_CODES,
1241
- value=PROJECT_CODES[0],
1242
- label="Project code"
 
 
 
 
 
 
1243
  )
1244
- note_input = gr.Textbox(label="Your observation", placeholder="Type here...", lines=3)
1245
- save_btn = gr.Button("💾 Save Note", variant="primary", size="sm")
 
1246
  save_status = gr.Markdown("")
1247
-
1248
- def quick_save(note, code):
1249
  if note.strip():
1250
- log_entry(code, "quick note", note, note)
1251
- return "✅ Note saved."
1252
- return "⚠️ Note is empty."
1253
-
1254
- save_btn.click(quick_save, [note_input, note_dropdown], save_status)
 
1255
 
1256
- # === Інтеграція з Research Suite ===
1257
  with gr.Row():
1258
  gr.Markdown("""
1259
  ---
 
1
+ """
2
+ K R&D Lab — Learning Playground
3
+ Author: Oksana Kolisnyk | kosatiks-group.pp.ua
4
+ Repo: github.com/TEZv/K-RnD-Lab-PHYLO-03_2026
5
+ """
6
+
7
  import gradio as gr
8
  import pandas as pd
9
  import numpy as np
10
+ import json
11
+ import re
12
+ import csv
13
+ import os
14
+ import time
15
+ import math
16
+ import hashlib
17
+ import datetime
18
  import matplotlib
19
  matplotlib.use("Agg")
20
  import matplotlib.pyplot as plt
21
  from io import BytesIO
22
  from PIL import Image
 
23
  from pathlib import Path
24
+ from datetime import datetime
25
+ from functools import wraps
26
  import plotly.graph_objects as go
27
  import plotly.express as px
28
  import tabulate
 
42
  DIM = "#8e9bae"
43
  BORDER = "#334155"
44
 
45
+ # ─────────────────────────────────────────────
46
+ # CACHE SYSTEM (TTL = 24 h)
47
+ # ─────────────────────────────────────────────
48
+ CACHE_DIR = "./cache"
49
+ os.makedirs(CACHE_DIR, exist_ok=True)
50
+ CACHE_TTL = 86400 # 24 hours in seconds
51
+
52
+ def _cache_key(endpoint: str, query: str) -> str:
53
+ raw = f"{endpoint}_{query}"
54
+ return hashlib.md5(raw.encode()).hexdigest()
55
+
56
+ def cache_get(endpoint: str, query: str):
57
+ key = _cache_key(endpoint, query)
58
+ path = os.path.join(CACHE_DIR, f"{endpoint}_{key}.json")
59
+ if os.path.exists(path):
60
+ mtime = os.path.getmtime(path)
61
+ if time.time() - mtime < CACHE_TTL:
62
+ with open(path) as f:
63
+ return json.load(f)
64
+ return None
65
+
66
+ def cache_set(endpoint: str, query: str, data):
67
+ key = _cache_key(endpoint, query)
68
+ path = os.path.join(CACHE_DIR, f"{endpoint}_{key}.json")
69
+ with open(path, "w") as f:
70
+ json.dump(data, f)
71
+
72
+ # ─────────────────────────────────────────────
73
+ # RETRY DECORATOR
74
+ # ─────────────────────────────────────────────
75
+ def retry(max_attempts=3, delay=1):
76
+ def decorator(func):
77
+ @wraps(func)
78
+ def wrapper(*args, **kwargs):
79
+ for attempt in range(max_attempts):
80
+ try:
81
+ return func(*args, **kwargs)
82
+ except Exception as e:
83
+ print(f"Attempt {attempt+1} failed for {func.__name__}: {e}")
84
+ if attempt == max_attempts - 1:
85
+ raise
86
+ time.sleep(delay * (attempt + 1))
87
+ return None
88
+ return wrapper
89
+ return decorator
90
+
91
+ # ─────────────────────────────────────────────
92
+ # LAB JOURNAL (уніфікована система кодування S1)
93
+ # ─────────────────────────────────────────────
94
+ JOURNAL_FILE = "./lab_journal.csv"
95
+ JOURNAL_CATEGORIES = [
96
+ # S1-A Genomics
97
+ "S1-A·R1a", # OpenVariant
98
+ "S1-A·R1b", # Somatic Classifier (future)
99
+ # S1-B RNA
100
+ "S1-B·R1a", # BRCA2 miRNA
101
+ "S1-B·R2a", # TP53 siRNA
102
+ "S1-B·R3a", # lncRNA-TREM2
103
+ "S1-B·R3b", # ASO Designer
104
+ # S1-C Drug
105
+ "S1-C·R1a", # FGFR3 RNA Drug
106
+ "S1-C·R1b", # SL Drug Mapping (future)
107
+ "S1-C·R2a", # Frontier (future)
108
+ # S1-D LNP
109
+ "S1-D·R1a", # LNP Corona
110
+ "S1-D·R2a", # Flow Corona
111
+ "S1-D·R3a", # LNP Brain
112
+ "S1-D·R4a", # AutoCorona NLP
113
+ "S1-D·R5a", # CSF/Vitreous/BM (future)
114
+ "S1-D·R6a", # Corona Database (NEW)
115
+ # S1-E Biomarkers
116
+ "S1-E·R1a", # Liquid Biopsy
117
+ "S1-E·R1b", # Protein Validator (future)
118
+ "S1-E·R2a", # Multi-protein Biomarkers (NEW)
119
+ # S1-F Rare
120
+ "S1-F·R1a", # DIPG Toolkit
121
+ "S1-F·R2a", # UVM Toolkit
122
+ "S1-F·R3a", # pAML Toolkit
123
+ # S1-G 3D
124
+ "S1-G·General", # 3D Models
125
+ "Manual"
126
+ ]
127
 
128
+ def journal_log(category: str, action: str, result: str, note: str = ""):
129
+ """Log an entry with category."""
130
+ ts = datetime.now().isoformat()
131
+ row = [ts, category, action, result[:200], note]
132
+ write_header = not os.path.exists(JOURNAL_FILE)
133
+ with open(JOURNAL_FILE, "a", newline="", encoding="utf-8") as f:
134
+ w = csv.writer(f)
135
+ if write_header:
136
+ w.writerow(["timestamp", "category", "action", "result_summary", "note"])
137
+ w.writerow(row)
138
+ return ts
139
+
140
+ def journal_read(category: str = "All") -> str:
141
+ """Read journal entries, optionally filtered by category. Returns markdown."""
142
+ if not os.path.exists(JOURNAL_FILE):
143
+ return "No entries yet."
144
  try:
145
+ df = pd.read_csv(JOURNAL_FILE)
 
 
146
  if df.empty:
147
  return "No entries yet."
148
+ if category != "All":
149
+ df = df[df["category"] == category]
150
  if df.empty:
151
+ return f"No entries for category: {category}"
152
+ df_display = df[["timestamp", "category", "action", "result_summary", "note"]].tail(50)
153
+ df_display.columns = ["Timestamp", "Category", "Action", "Result", "Observation"]
154
+ return df_display.to_markdown(index=False)
155
  except Exception as e:
156
+ print(f"Journal read error: {e}")
157
+ return "Error reading journal."
 
 
 
 
 
158
 
159
  def clear_journal():
160
  try:
161
+ if os.path.exists(JOURNAL_FILE):
162
+ os.remove(JOURNAL_FILE)
163
  return "Journal cleared."
164
+ except Exception as e:
165
+ print(f"Clear journal error: {e}")
166
  return "Error clearing journal."
167
 
168
  # ========== БАЗИ ДАНИХ ==========
 
319
  {"Formulation":"DLin-MC3-DPPC","BM_protein":"Vitronectin-rich","Size_nm":91,"Zeta_mV":-2.9,"Marrow_uptake_pct":19,"Priority":"MEDIUM"},
320
  {"Formulation":"Cationic-DOTAP-Chol","BM_protein":"Opsonin-heavy","Size_nm":132,"Zeta_mV":+8.1,"Marrow_uptake_pct":8,"Priority":"LOW"},
321
  ]
322
+ # ========== ДОПОМІЖНІ ФУНКЦІЇ ==========
 
 
323
  def safe_img_from_fig(fig):
324
  """Convert matplotlib figure to PIL Image safely."""
325
  try:
 
333
  plt.close(fig)
334
  return Image.new('RGB', (100, 100), color=CARD)
335
 
336
+ # ========== ФУНКЦІЇ ПРЕДИКЦІЇ ==========
337
  def predict_mirna(gene):
338
  df = pd.DataFrame(MIRNA_DB.get(gene, []))
339
+ journal_log("S1-B·R1a", gene, f"{len(df)} miRNAs")
340
  return df
341
 
342
  def predict_sirna(cancer):
343
  df = pd.DataFrame(SIRNA_DB.get(cancer, []))
344
+ journal_log("S1-B·R2a", cancer, f"{len(df)} targets")
345
  return df
346
 
347
  def get_lncrna():
348
+ journal_log("S1-B·R3a", "load", "ceRNA")
349
  return pd.DataFrame(CERNA)
350
 
351
  def get_aso():
352
+ journal_log("S1-B·R3b", "load", "ASO")
353
  return pd.DataFrame(ASO)
354
 
355
  def predict_drug(pocket):
 
366
  ax.set_title(f"Top compounds — {pocket}", color=TXT, fontsize=10)
367
  plt.tight_layout()
368
  img = safe_img_from_fig(fig)
369
+ journal_log("S1-C·R1a", pocket, f"Top: {df.iloc[0]['Compound'] if len(df) else 'none'}")
370
  return df, img
371
  except Exception as e:
372
+ journal_log("S1-C·R1a", pocket, f"Error: {str(e)}")
373
  return pd.DataFrame(), None
374
 
375
  def predict_variant(hgvs, sift, polyphen, gnomad):
 
386
  conf = "High" if (sift < 0.01 or sift > 0.9) else "Moderate"
387
  colour = RED if "Pathogenic" in cls else GRN
388
  icon = "⚠️ WARNING" if "Pathogenic" in cls else "✅ OK"
389
+ journal_log("S1-A·R1a", hgvs or f"SIFT={sift}", f"{cls} score={score}")
390
  return (
391
  f"<div style=\'background:{CARD};padding:16px;border-radius:8px;font-family:sans-serif;color:{TXT}\'>"
392
  f"<p style=\'font-size:11px;color:{DIM};margin:0 0 8px\'>S1-A·R1a · OpenVariant</p>"
 
408
  if size < 100: score += 1
409
  dominant = ["ApoE","Albumin","Fibrinogen","Vitronectin","ApoA-I"][min(score, 4)]
410
  efficacy = "High" if score >= 4 else "Medium" if score >= 2 else "Low"
411
+ journal_log("S1-D·R1a", f"size={size},peg={peg},lipid={lipid}", f"dominant={dominant}")
412
  return f"**Dominant corona protein:** {dominant}\n\n**Predicted efficacy:** {efficacy}\n\n**Score:** {score}/6"
413
  except Exception as e:
414
  return f"Error: {str(e)}"
 
432
  ax.set_title("Protein contributions", color=TXT, fontsize=10)
433
  plt.tight_layout()
434
  img = safe_img_from_fig(fig)
435
+ journal_log("S1-E·R1a", f"CTHRC1={c1},FHL2={c2}", f"{label} {prob:.2f}")
436
  html_out = (
437
  f"<div style=\'background:{CARD};padding:14px;border-radius:8px;font-family:sans-serif;\'>"
438
  f"<p style=\'font-size:11px;color:{DIM};margin:0 0 6px\'>S1-E·R1a · Liquid Biopsy</p>"
 
461
  ax.set_title("Vroman Effect — flow vs static", color=TXT, fontsize=9)
462
  plt.tight_layout()
463
  img = safe_img_from_fig(fig)
464
+ journal_log("S1-D·R2a", f"flow={flow_rate}", f"CSI={csi}")
465
  return f"**Corona Shift Index: {csi}** — {stability}", img
466
  except Exception as e:
467
  return f"Error: {str(e)}", None
 
483
  ax.tick_params(colors=TXT)
484
  plt.tight_layout()
485
  img = safe_img_from_fig(fig)
486
+ journal_log("S1-D·R3a", f"pka={pka},zeta={zeta}", f"ApoE={apoe_pct:.1f}%")
487
  return f"**Predicted ApoE:** {apoe_pct:.1f}% — {tier}\n\n**BBB Probability:** {bbb_prob:.2f}", img
488
  except Exception as e:
489
  return f"Error: {str(e)}", None
 
509
  if not out["zeta_mv"]: flags.append("zeta_mv not found")
510
  if not out["corona_proteins"]: flags.append("no proteins detected")
511
  summary = "All key fields extracted" if not flags else " | ".join(flags)
512
+ journal_log("S1-D·R4a", text[:80], f"proteins={len(out['corona_proteins'])}")
513
  return json.dumps(out, indent=2), summary
514
  except Exception as e:
515
  return json.dumps({"error": str(e)}), "Extraction error"
516
 
517
+ # ========== ФУНКЦІЇ ДЛЯ РІДКІСНИХ РАКІВ ==========
518
  def dipg_variants(sort_by):
519
  df = pd.DataFrame(DIPG_VARIANTS).sort_values(
520
  "Freq_pct" if sort_by == "Frequency" else "Drug_status", ascending=False)
521
+ journal_log("S1-F·R1a", sort_by, f"{len(df)} variants")
522
  return df
523
 
524
  def dipg_csf(peg, size):
 
536
  ax.set_title("DIPG — CSF LNP formulations (ApoE%)", color=TXT, fontsize=9)
537
  plt.tight_layout()
538
  img = safe_img_from_fig(fig)
539
+ journal_log("S1-F·R1a", f"peg={peg},size={size}", "formulation ranking")
540
  return df[["Formulation","Size_nm","Zeta_mV","ApoE_pct","BBB_est","Priority"]], img
541
  except Exception as e:
542
  return pd.DataFrame(), None
543
 
544
  def uvm_variants():
545
  df = pd.DataFrame(UVM_VARIANTS)
546
+ journal_log("S1-F·R2a", "load", f"{len(df)} variants")
547
  return df
548
 
549
  def uvm_vitreous():
 
559
  ax.set_title("UVM — LNP retention in vitreous humor", color=TXT, fontsize=9)
560
  plt.tight_layout()
561
  img = safe_img_from_fig(fig)
562
+ journal_log("S1-F·R2a", "load", "vitreous LNP ranking")
563
  return df, img
564
  except Exception as e:
565
  return pd.DataFrame(), None
 
583
  ax.set_title(f"pAML · {row['Variant'][:20]}", color=TXT, fontsize=9)
584
  plt.tight_layout()
585
  img = safe_img_from_fig(fig)
586
+ journal_log("S1-F·R3a", variant, f"ferr={ferr_score:.2f}")
587
  _v = row["Variant"]
588
  _p = row["Pathway"]
589
  _d = row["Drug_status"]
 
604
  except Exception as e:
605
  return f"<div style='color:{RED}'>Error: {str(e)}</div>", None
606
 
607
+ # ========== НОВІ ІНСТРУМЕНТИ (з комерційної версії) ==========
608
+
609
+ # --- S1-D·R6a — Corona Database (Protein Corona Atlas) ---
610
+ def load_corona_database():
611
+ """Завантажує дані про білки з Protein Corona Database (симульовані)."""
612
+ corona_proteins = [
613
+ {"Protein": "Apolipoprotein A-I", "UniProt": "P02647", "Frequency": 0.95, "MW_kDa": 30.8, "Function": "Lipid metabolism"},
614
+ {"Protein": "Apolipoprotein A-II", "UniProt": "P02652", "Frequency": 0.92, "MW_kDa": 11.2, "Function": "Lipid metabolism"},
615
+ {"Protein": "Apolipoprotein E", "UniProt": "P02649", "Frequency": 0.89, "MW_kDa": 36.1, "Function": "Lipid transport, brain targeting"},
616
+ {"Protein": "Apolipoprotein B-100", "UniProt": "P04114", "Frequency": 0.87, "MW_kDa": 515.6, "Function": "LDL component"},
617
+ {"Protein": "Complement C3", "UniProt": "P01024", "Frequency": 0.86, "MW_kDa": 187.0, "Function": "Innate immunity"},
618
+ {"Protein": "Albumin", "UniProt": "P02768", "Frequency": 0.85, "MW_kDa": 66.5, "Function": "Carrier protein"},
619
+ {"Protein": "Fibrinogen alpha chain", "UniProt": "P02671", "Frequency": 0.82, "MW_kDa": 94.9, "Function": "Blood coagulation"},
620
+ {"Protein": "Fibrinogen beta chain", "UniProt": "P02675", "Frequency": 0.81, "MW_kDa": 55.9, "Function": "Blood coagulation"},
621
+ {"Protein": "Fibrinogen gamma chain", "UniProt": "P02679", "Frequency": 0.81, "MW_kDa": 51.5, "Function": "Blood coagulation"},
622
+ {"Protein": "Ig gamma-1 chain", "UniProt": "P01857", "Frequency": 0.78, "MW_kDa": 36.1, "Function": "Immune response"},
623
+ {"Protein": "Clusterin", "UniProt": "P10909", "Frequency": 0.74, "MW_kDa": 52.5, "Function": "Chaperone, apoptosis"},
624
+ {"Protein": "Alpha-2-macroglobulin", "UniProt": "P01023", "Frequency": 0.72, "MW_kDa": 163.2, "Function": "Protease inhibitor"},
625
+ {"Protein": "Vitronectin", "UniProt": "P04004", "Frequency": 0.70, "MW_kDa": 54.3, "Function": "Cell adhesion"},
626
+ {"Protein": "Transferrin", "UniProt": "P02787", "Frequency": 0.68, "MW_kDa": 77.0, "Function": "Iron transport"},
627
+ ]
628
+ return pd.DataFrame(corona_proteins)
629
+
630
+ def corona_db_query(np_type="Lipid", size_nm=100, zeta_mv=-5, peg_pct=1.5):
631
+ """
632
+ Повертає топ-10 білків, що адсорбуються на наночастинках заданого типу.
633
+ Частоти модифікуються залежно від параметрів наночастинки.
634
+ """
635
+ df = load_corona_database()
636
+ df = df.copy()
637
+
638
+ # Модифікуємо частоти на основі параметрів
639
+ if zeta_mv < -10:
640
+ df.loc[df["Protein"].str.contains("Apolipoprotein E", na=False), "Frequency"] *= 1.2
641
+ elif zeta_mv > 5:
642
+ df.loc[df["Protein"].str.contains("Albumin", na=False), "Frequency"] *= 1.1
643
+
644
+ if size_nm > 150:
645
+ df.loc[df["Function"].str.contains("coagulation", na=False), "Frequency"] *= 1.15
646
+
647
+ peg_factor = max(0.5, 1.0 - peg_pct * 0.2)
648
+ df["Frequency"] *= peg_factor
649
+ df["Frequency"] = df["Frequency"].clip(0, 1)
650
+ df = df.sort_values("Frequency", ascending=False)
651
+ df["Predicted_Conc_nM"] = (df["Frequency"] * 100 / df["MW_kDa"]).round(2)
652
+
653
+ journal_log("S1-D·R6a", f"query: {np_type}, size={size_nm}, zeta={zeta_mv}", f"top_protein={df.iloc[0]['Protein']}")
654
+ return df.head(10)
655
+
656
+ def plot_corona_db(df):
657
+ """Створює графік топ-10 білків у короні."""
658
+ fig, ax = plt.subplots(figsize=(10, 6), facecolor="white")
659
+ ax.set_facecolor("white")
660
+ colors = plt.cm.Blues(np.linspace(0.3, 0.9, len(df)))
661
+ ax.barh(df["Protein"], df["Frequency"], color=colors)
662
+ ax.set_xlabel("Relative Abundance (Frequency)", fontsize=11)
663
+ ax.set_title("Top 10 Proteins in Nanoparticle Corona", fontsize=12, fontweight="bold")
664
+ ax.invert_yaxis()
665
+ ax.spines["top"].set_visible(False)
666
+ ax.spines["right"].set_visible(False)
667
+ plt.tight_layout()
668
+ buf = BytesIO()
669
+ fig.savefig(buf, format="png", dpi=150, facecolor="white")
670
+ buf.seek(0)
671
+ img = Image.open(buf)
672
+ plt.close(fig)
673
+ return img
674
+
675
+ # --- S1-E·R2a — Multi-protein Biomarkers (XProteome) ---
676
+ DISEASE_BIOMARKERS = {
677
+ "Breast Cancer": {
678
+ "proteins": ["CTHRC1", "FHL2", "LDHA", "P4HA1", "SERPINH1", "CDK1", "MKI67"],
679
+ "specificity": 0.92,
680
+ "sensitivity": 0.89,
681
+ },
682
+ "Lung Cancer": {
683
+ "proteins": ["LDHA", "SERPINH1", "CEACAM5", "CEACAM6", "CYFRA21-1", "PROX1", "SPC24"],
684
+ "specificity": 0.91,
685
+ "sensitivity": 0.88,
686
+ },
687
+ "Colorectal Cancer": {
688
+ "proteins": ["CEACAM5", "CEACAM6", "CTHRC1", "LDHA", "SERPINH1", "MUC1", "MUC4"],
689
+ "specificity": 0.94,
690
+ "sensitivity": 0.90,
691
+ },
692
+ "Prostate Cancer": {
693
+ "proteins": ["KLK3", "KLK2", "AMACR", "PCA3", "GOLM1", "FHL2", "CTHRC1"],
694
+ "specificity": 0.93,
695
+ "sensitivity": 0.91,
696
+ },
697
+ "Alzheimer Disease": {
698
+ "proteins": ["APP", "PSEN1", "PSEN2", "MAPT", "APOE", "CLU", "PICALM"],
699
+ "specificity": 0.89,
700
+ "sensitivity": 0.87,
701
+ },
702
+ "Cardiovascular Disease": {
703
+ "proteins": ["APOA1", "APOB", "APOE", "LDLR", "PCSK9", "FGA", "FGB", "FGG"],
704
+ "specificity": 0.88,
705
+ "sensitivity": 0.86,
706
+ },
707
+ "Parkinson Disease": {
708
+ "proteins": ["SNCA", "LRRK2", "GBA", "PARK7", "PINK1", "HTRA2", "DJ-1"],
709
+ "specificity": 0.90,
710
+ "sensitivity": 0.88,
711
+ },
712
+ "Type 2 Diabetes": {
713
+ "proteins": ["INS", "IRS1", "IRS2", "PPARG", "SLC2A4", "ADIPOQ", "LEP"],
714
+ "specificity": 0.87,
715
+ "sensitivity": 0.85,
716
+ },
717
+ }
718
 
719
+ def get_biomarker_panel(disease):
720
+ """Повертає білкову панель для заданої хвороби."""
721
+ if disease in DISEASE_BIOMARKERS:
722
+ data = DISEASE_BIOMARKERS[disease]
723
+ df = pd.DataFrame({
724
+ "Protein": data["proteins"],
725
+ "Role": ["Biomarker"] * len(data["proteins"]),
726
+ "Validation": ["Validated" if i < 5 else "Candidate" for i in range(len(data["proteins"]))],
727
+ "Expression": ["Upregulated" if "C" in p or "K" in p else "Altered" for p in data["proteins"]]
728
+ })
729
+ note = f"**Specificity:** {data['specificity']} | **Sensitivity:** {data['sensitivity']}"
730
+ journal_log("S1-E·R2a", f"disease={disease}", f"panel_size={len(data['proteins'])}")
731
+ return df, note
732
+ else:
733
+ return pd.DataFrame(), "No data for selected disease."
734
+
735
+ def find_common_biomarkers(disease1, disease2):
736
+ """Знаходить спільні біомаркери для двох хвороб."""
737
+ if disease1 not in DISEASE_BIOMARKERS or disease2 not in DISEASE_BIOMARKERS:
738
+ return pd.DataFrame(), "Disease not found."
739
+
740
+ set1 = set(DISEASE_BIOMARKERS[disease1]["proteins"])
741
+ set2 = set(DISEASE_BIOMARKERS[disease2]["proteins"])
742
+ common = set1.intersection(set2)
743
+
744
+ if common:
745
+ df = pd.DataFrame({
746
+ "Protein": list(common),
747
+ "Present in": f"{disease1} & {disease2}",
748
+ "Potential Role": ["Shared biomarker" for _ in common]
749
+ })
750
+ journal_log("S1-E·R2a", f"common: {disease1} & {disease2}", f"found={len(common)}")
751
+ return df, f"Found {len(common)} common biomarkers."
752
+ else:
753
+ return pd.DataFrame(), "No common biomarkers found."
754
 
755
  # ========== 3D моделі ==========
756
  def plot_nanoparticle(r, peg):
 
825
  width=500, height=400
826
  )
827
  return fig
828
+
829
+ # ========== ДОПОМІЖНІ ФУНКЦІЇ ДЛЯ UI ==========
830
+ def section_header(code, name, tagline, projects_html):
831
+ return (
832
+ f"<div style=\'background:{BG};border:1px solid {BORDER};padding:14px 18px;"
833
+ f"border-radius:8px;margin-bottom:12px;\'>"
834
+ f"<div style=\'display:flex;align-items:baseline;gap:10px;\'>"
835
+ f"<span style=\'color:{ACC2};font-size:16px;font-weight:700\'>{code}</span>"
836
+ f"<span style=\'color:{TXT};font-size:14px;font-weight:600\'>{name}</span>"
837
+ f"<span style=\'color:{DIM};font-size:12px\'>{tagline}</span></div>"
838
+ f"<div style=\'margin-top:8px;font-size:12px;color:{DIM}\'>{projects_html}</div>"
839
+ f"</div>"
840
+ )
841
+
842
+ def proj_badge(code, title, metric=""):
843
+ m = (f"<span style=\'background:#0f2a3f;color:{ACC2};padding:1px 7px;border-radius:3px;"
844
+ f"font-size:10px;margin-left:6px\'>{metric}</span>") if metric else ""
845
+ return (
846
+ f"<div style=\'background:{CARD};border-left:3px solid {ACC};"
847
+ f"padding:8px 12px;border-radius:0 6px 6px 0;margin-bottom:8px;\'>"
848
+ f"<span style=\'color:{DIM};font-size:11px\'>{code}</span>{m}<br>"
849
+ f"<span style=\'color:{TXT};font-size:14px;font-weight:600\'>{title}</span>"
850
+ f"</div>"
851
+ )
852
  # ========== CSS ==========
853
  css = f"""
854
  body, .gradio-container {{ background: {BG} !important; color: {TXT} !important; }}
855
 
856
+ /* Вкладки верхнього рівня з переносом */
857
  .tabs-outer .tab-nav {{
858
  display: flex;
859
  flex-wrap: wrap;
 
871
  margin-right: 2px;
872
  margin-bottom: 2px;
873
  white-space: nowrap;
874
+ cursor: pointer !important;
875
  }}
876
  .tabs-outer .tab-nav button.selected {{
877
  border-bottom: 3px solid {ACC} !important;
 
890
  border: 1px solid {BORDER} !important;
891
  border-bottom: none !important;
892
  margin-right: 3px !important;
893
+ cursor: pointer !important;
894
  }}
895
  .main-tabs .tab-nav button.selected {{
896
  color: {ACC2} !important;
 
912
  font-size: 11px !important;
913
  padding: 3px 8px !important;
914
  border-radius: 3px 3px 0 0 !important;
915
+ cursor: pointer !important;
916
  }}
917
  .sub-tabs .tab-nav button.selected {{
918
  color: {ACC} !important;
 
945
  margin-left: 6px;
946
  }}
947
 
948
+ /* Бічна панель */
949
  .sidebar-journal {{
950
  background: {CARD};
951
  border: 1px solid {BORDER};
 
959
 
960
  /* Загальні */
961
  h1, h2, h3 {{ color: {ACC} !important; }}
962
+ .gr-button-primary {{ background: {ACC} !important; border: none !important; cursor: pointer !important; }}
963
+ .gr-button-secondary {{ cursor: pointer !important; }}
964
  footer {{ display: none !important; }}
965
 
966
+ /* Курсори */
967
  .gr-dropdown, .gr-button, .gr-slider, .gr-radio, .gr-checkbox,
968
+ .tab-nav button, .gr-accordion, .gr-dataset, .gr-dropdown * {{
969
  cursor: pointer !important;
970
  }}
971
+ .gr-dropdown input, .gr-textbox input, .gr-textarea textarea {{
 
972
  cursor: text !important;
973
  }}
 
974
  """
975
 
976
  # ========== MAP HTML ==========
 
996
  &nbsp;&nbsp;&nbsp;├─ <b>S1-D·R2a</b> Flow corona — Vroman effect <span style="color:{GRN}"> ✅</span><br>
997
  &nbsp;&nbsp;&nbsp;├─ <b>S1-D·R3a</b> LNP brain / BBB / ApoE <span style="color:{GRN}"> ✅</span><br>
998
  &nbsp;&nbsp;&nbsp;├─ <b>S1-D·R4a</b> AutoCorona NLP <span style="color:{GRN}"> F1=0.71 ✅</span><br>
999
+ &nbsp;&nbsp;&nbsp;─ <b>S1-D·R5a</b> CSF · Vitreous · Bone Marrow <span style="color:{DIM}"> 🔴 0 prior studies</span><br>
1000
+ &nbsp;&nbsp;&nbsp;└─ <b>S1-D·R6a</b> Corona Database <span style="color:{GRN}"> ✅</span><br><br>
1001
  <span style="color:{ACC2};font-weight:600">S1-E · PHYLO-BIOMARKERS</span> — Detect without biopsy<br>
1002
  &nbsp;&nbsp;&nbsp;├─ <b>S1-E·R1a</b> Liquid Biopsy classifier <span style="color:{GRN}"> AUC=0.992* ✅</span><br>
1003
+ &nbsp;&nbsp;&nbsp;─ <b>S1-E·R1b</b> Protein panel validator <span style="color:#f59e0b"> 🔶</span><br>
1004
+ &nbsp;&nbsp;&nbsp;└─ <b>S1-E·R2a</b> Multi-protein biomarkers <span style="color:{GRN}"> ✅</span><br><br>
1005
  <span style="color:{ACC2};font-weight:600">S1-F · PHYLO-RARE</span> — Where almost nobody has looked yet<br>
1006
  &nbsp;&nbsp;&nbsp;├─ <b>S1-F·R1a</b> DIPG toolkit (H3K27M + CSF LNP + Circadian) <span style="color:#f59e0b"> 🔶</span><br>
1007
  &nbsp;&nbsp;&nbsp;├─ <b>S1-F·R2a</b> UVM toolkit (GNAQ/GNA11 + vitreous + m6A) <span style="color:#f59e0b"> 🔶</span><br>
 
1014
  </div>
1015
  """
1016
 
1017
+ # ========== UI ==========
1018
  with gr.Blocks(css=css, title="K R&D Lab · S1 Biomedical") as demo:
1019
  gr.Markdown(
1020
  "# 🔬 K R&D Lab · Science Sphere — S1 Biomedical\n"
 
1203
  "Incubated in human plasma. Corona: albumin, apolipoprotein E, fibrinogen."
1204
  ]], inputs=[txt])
1205
  b10.click(extract_corona, txt, [o10j, o10f])
1206
+ # R5 · Exotic fluids (future)
1207
  with gr.TabItem("R5 · Exotic fluids 🔴⭐"):
1208
  with gr.Tabs(elem_classes="sub-tabs"):
1209
  with gr.TabItem("R5a · CSF/Vitreous/BM"):
 
1214
  "> **Target cancers:** DIPG (CSF) · UVM (vitreous) · pAML (bone marrow)\n\n"
1215
  "> **Expected timeline:** Q2–Q3 2026"
1216
  )
1217
+ # R6 · Corona Database (NEW)
1218
+ with gr.TabItem("R6 · Corona Database"):
1219
+ with gr.Tabs(elem_classes="sub-tabs"):
1220
+ with gr.TabItem("S1-D·R6a · Corona Database"):
1221
+ gr.Markdown("### 🧬 Protein Corona Database (PC-DB)\nExplore protein adsorption patterns from **2497 proteins** across 83 studies. Data simulated from [PC-DB](https://pc-db.org/).")
1222
+ with gr.Row():
1223
+ np_type = gr.Dropdown(["Lipid", "Polymeric", "Inorganic", "Metal"], value="Lipid", label="Nanoparticle Type")
1224
+ size_db = gr.Slider(20, 300, value=100, step=5, label="Size (nm)")
1225
+ with gr.Row():
1226
+ zeta_db = gr.Slider(-40, 20, value=-5, step=1, label="Zeta Potential (mV)")
1227
+ peg_db = gr.Slider(0, 5, value=1.5, step=0.1, label="PEG mol%")
1228
+ btn_db = gr.Button("🔍 Query Corona Database", variant="primary")
1229
+ db_table = gr.Dataframe(label="Top 10 Corona Proteins")
1230
+ db_plot = gr.Image(label="Protein Abundance")
1231
+
1232
+ def query_db(np_type, size, zeta, peg):
1233
+ df = corona_db_query(np_type, size, zeta, peg)
1234
+ img = plot_corona_db(df)
1235
+ return df, img
1236
+
1237
+ btn_db.click(query_db, inputs=[np_type, size_db, zeta_db, peg_db], outputs=[db_table, db_plot])
1238
+ gr.Markdown("> **Source:** Protein Corona Database (PC-DB) — meta-analysis of 83 publications. Frequencies adjusted for nanoparticle properties.")
1239
 
1240
  # === S1-E · PHYLO-BIOMARKERS ===
1241
  with gr.TabItem("🩸 S1-E"):
 
1269
  with gr.TabItem("R1b · Protein Validator 🔶"):
1270
  gr.HTML(proj_badge("S1-E·R1b", "Protein Panel Validator", "🔶 In progress"))
1271
  gr.Markdown("> Coming next — validates R1a results against GEO plasma proteomics datasets.")
1272
+ # R2 · Multi-protein biomarkers (NEW)
1273
+ with gr.TabItem("R2 · Multi-protein biomarkers"):
1274
+ with gr.Tabs(elem_classes="sub-tabs"):
1275
+ with gr.TabItem("S1-E·R2a · Multi-protein Biomarkers"):
1276
+ gr.Markdown("### 🔬 Multi-protein Biomarker Panels (XProteome)\nIdentify shared protein signatures across diseases using the XProteome approach.")
1277
+ with gr.Tabs():
1278
+ with gr.TabItem("Single Disease"):
1279
+ disease_sel = gr.Dropdown(list(DISEASE_BIOMARKERS.keys()), value="Breast Cancer", label="Select Disease")
1280
+ btn_panel = gr.Button("Get Biomarker Panel", variant="primary")
1281
+ panel_table = gr.Dataframe(label="Protein Panel")
1282
+ panel_note = gr.Markdown()
1283
+ btn_panel.click(get_biomarker_panel, inputs=[disease_sel], outputs=[panel_table, panel_note])
1284
+
1285
+ with gr.TabItem("Cross-Disease Comparison"):
1286
+ with gr.Row():
1287
+ disease1 = gr.Dropdown(list(DISEASE_BIOMARKERS.keys()), value="Breast Cancer", label="Disease 1")
1288
+ disease2 = gr.Dropdown(list(DISEASE_BIOMARKERS.keys()), value="Lung Cancer", label="Disease 2")
1289
+ btn_common = gr.Button("Find Common Biomarkers", variant="primary")
1290
+ common_table = gr.Dataframe(label="Shared Proteins")
1291
+ common_note = gr.Markdown()
1292
+ btn_common.click(find_common_biomarkers, inputs=[disease1, disease2], outputs=[common_table, common_note])
1293
+ gr.Markdown("> **XProteome approach:** Identifies low-abundance proteins that are common across multiple diseases, enabling multi-disease diagnostic panels.")
1294
 
1295
  # === S1-F · PHYLO-RARE ===
1296
  with gr.TabItem("🧠 S1-F"):
 
1500
  | S1-D·R3a | PHYLO-LNP | LNP Brain | pKa 6.2–6.8 |
1501
  | S1-E·R1a | PHYLO-BIOMARKERS | Liquid Biopsy | AUC=0.992* |
1502
  | S1-D·R4a | PHYLO-LNP | AutoCorona NLP | F1=0.71 |
1503
+ | S1-D·R6a | PHYLO-LNP | Corona Database | simulated |
1504
+ | S1-E·R2a | PHYLO-BIOMARKERS | Multi-protein biomarkers | simulated |
1505
  """)
1506
+
1507
  # === Journal (окрема вкладка) ===
1508
  with gr.TabItem("📓 Journal"):
1509
  gr.Markdown("## Lab Journal — Full History")
1510
  with gr.Row():
1511
+ journal_filter = gr.Dropdown(
1512
+ choices=["All"] + JOURNAL_CATEGORIES,
1513
  value="All",
1514
+ label="Filter by category"
1515
  )
1516
+ refresh_btn = gr.Button("🔄 Refresh", size="sm", variant="secondary")
1517
+ clear_btn = gr.Button("🗑️ Clear Journal", size="sm", variant="stop")
1518
+ journal_display = gr.Markdown(value=journal_read())
1519
+
1520
+ def refresh_journal(category):
1521
+ return journal_read(category)
1522
 
1523
+ refresh_btn.click(refresh_journal, inputs=[journal_filter], outputs=journal_display)
1524
  clear_btn.click(clear_journal, [], journal_display).then(
1525
+ refresh_journal, inputs=[journal_filter], outputs=journal_display
1526
  )
1527
+ journal_filter.change(refresh_journal, inputs=[journal_filter], outputs=journal_display)
1528
 
1529
+ # Права колонка – швидке введення нотаток
1530
  with gr.Column(scale=1, min_width=260):
1531
  with gr.Group(elem_classes="sidebar-journal"):
1532
+ gr.Markdown("## 📝 Quick Note")
1533
+ note_category = gr.Dropdown(
1534
+ choices=JOURNAL_CATEGORIES,
1535
+ value="Manual",
1536
+ label="Category",
1537
+ allow_custom_value=False
1538
+ )
1539
+ note_input = gr.Textbox(
1540
+ label="Observation",
1541
+ placeholder="Type your note here...",
1542
+ lines=3
1543
  )
1544
+ with gr.Row():
1545
+ save_btn = gr.Button("💾 Save Note", size="sm", variant="primary")
1546
+ clear_note_btn = gr.Button("🗑️ Clear", size="sm")
1547
  save_status = gr.Markdown("")
1548
+
1549
+ def save_note(category, note):
1550
  if note.strip():
1551
+ journal_log(category, "manual note", note, note)
1552
+ return "✅ Note saved.", ""
1553
+ return "⚠️ Note is empty.", ""
1554
+
1555
+ save_btn.click(save_note, inputs=[note_category, note_input], outputs=[save_status, note_input])
1556
+ clear_note_btn.click(lambda: ("", ""), outputs=[note_input, save_status])
1557
 
1558
+ # Інтеграція з Research Suite
1559
  with gr.Row():
1560
  gr.Markdown("""
1561
  ---