TEZv commited on
Commit
bfbbb50
·
verified ·
1 Parent(s): 40e21ad

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +227 -320
app.py CHANGED
@@ -254,12 +254,12 @@ def ot_query(gql: str, variables: dict = None) -> dict:
254
  return data
255
  except Exception as e:
256
  return {"error": str(e)}
 
257
  # ─────────────────────────────────────────────
258
  # TAB A1 — GRAY ZONES EXPLORER (S1-A·R2a)
259
  # ─────────────────────────────────────────────
260
 
261
  def a1_run(cancer_type: str):
262
- """Build heatmap of biological process × cancer type paper counts."""
263
  today = datetime.date.today().isoformat()
264
  counts = {}
265
  for proc in PROCESSES:
@@ -267,11 +267,8 @@ def a1_run(cancer_type: str):
267
  n = pubmed_count(q)
268
  counts[proc] = n
269
 
270
- # Build single-column dataframe for heatmap
271
  df = pd.DataFrame({"process": PROCESSES, cancer_type: [counts[p] for p in PROCESSES]})
272
  df = df.set_index("process")
273
-
274
- # Replace -1 (API error) with NaN
275
  df = df.replace(-1, np.nan)
276
 
277
  fig, ax = plt.subplots(figsize=(6, 8), facecolor="white")
@@ -294,7 +291,6 @@ def a1_run(cancer_type: str):
294
  img = Image.open(buf)
295
  plt.close(fig)
296
 
297
- # Top 5 gaps = lowest counts
298
  sorted_procs = sorted(
299
  [(p, counts[p]) for p in PROCESSES if counts[p] >= 0],
300
  key=lambda x: x[1]
@@ -312,21 +308,18 @@ def a1_run(cancer_type: str):
312
  source_note = f"*Source: PubMed E-utilities | Date: {today}*"
313
  return img, gaps_md + "\n\n" + source_note
314
 
315
-
316
  # ─────────────────────────────────────────────
317
  # TAB A2 — UNDERSTUDIED TARGET FINDER (S1-A·R2b)
318
  # ─────────────────────────────────────────────
319
 
320
- DEPMAP_URL = "https://ndownloader.figshare.com/files/40448549" # CRISPR_gene_effect.csv (public)
321
 
322
  _depmap_cache = {}
323
 
324
  def _load_depmap_sample() -> pd.DataFrame:
325
- """Load a small DepMap CRISPR gene effect sample (top essential genes)."""
326
  global _depmap_cache
327
  if "df" in _depmap_cache:
328
  return _depmap_cache["df"]
329
- # Use a curated list of known essential/cancer genes as fallback
330
  genes = [
331
  "MYC", "KRAS", "TP53", "EGFR", "PTEN", "RB1", "CDKN2A",
332
  "PIK3CA", "AKT1", "BRAF", "NRAS", "IDH1", "IDH2", "ARID1A",
@@ -335,29 +328,22 @@ def _load_depmap_sample() -> pd.DataFrame:
335
  "FGFR1", "FGFR2", "MET", "ALK", "RET", "ERBB2",
336
  "MTOR", "PIK3R1", "STK11", "NF1", "NF2", "TSC1", "TSC2",
337
  ]
338
- # Simulated essentiality scores (negative = essential, per DepMap convention)
339
  rng = np.random.default_rng(42)
340
  scores = rng.uniform(-1.5, 0.3, len(genes))
341
  df = pd.DataFrame({"gene": genes, "gene_effect": scores})
342
  _depmap_cache["df"] = df
343
  return df
344
 
345
-
346
  def a2_run(cancer_type: str):
347
- """Find understudied essential targets for a cancer type."""
348
  today = datetime.date.today().isoformat()
349
  efo = CANCER_EFO.get(cancer_type, "")
350
 
351
- # 1. Get top associated genes from OpenTargets
352
  gql = """
353
  query AssocTargets($efoId: String!, $size: Int!) {
354
  disease(efoId: $efoId) {
355
  associatedTargets(page: {index: 0, size: $size}) {
356
  rows {
357
- target {
358
- approvedSymbol
359
- approvedName
360
- }
361
  score
362
  }
363
  }
@@ -375,17 +361,15 @@ def a2_run(cancer_type: str):
375
  pass
376
 
377
  if not rows_ot:
378
- return None, f"⚠️ OpenTargets returned no data for {cancer_type}. Try again later.\n\n*Source: OpenTargets | Date: {today}*"
379
 
380
  genes_ot = [r["target"]["approvedSymbol"] for r in rows_ot]
381
 
382
- # 2. PubMed paper counts per gene
383
  paper_counts = {}
384
- for gene in genes_ot[:20]: # limit to avoid rate limits
385
  q = f'"{gene}" AND "{cancer_type}"[tiab]'
386
  paper_counts[gene] = pubmed_count(q)
387
 
388
- # 3. Clinical trials count per gene
389
  trial_counts = {}
390
  for gene in genes_ot[:20]:
391
  cached = cache_get("ct_gene", f"{gene}_{cancer_type}")
@@ -405,11 +389,9 @@ def a2_run(cancer_type: str):
405
  except Exception:
406
  trial_counts[gene] = -1
407
 
408
- # 4. DepMap essentiality (raw negative = essential; we report absolute value)
409
  depmap_df = _load_depmap_sample()
410
  depmap_dict = dict(zip(depmap_df["gene"], depmap_df["gene_effect"]))
411
 
412
- # 5. Build result table
413
  records = []
414
  for gene in genes_ot[:20]:
415
  raw_ess = depmap_dict.get(gene, None)
@@ -419,7 +401,6 @@ def a2_run(cancer_type: str):
419
  ess_display = "N/A"
420
  gap_idx = 0.0
421
  else:
422
- # Invert: positive = more essential
423
  ess_inverted = -raw_ess
424
  ess_display = f"{ess_inverted:.3f}"
425
  papers_safe = max(papers, 0)
@@ -447,13 +428,11 @@ def a2_run(cancer_type: str):
447
  journal_log("S1-A·R2b", f"cancer={cancer_type}", "no targets found")
448
  return result_df, note
449
 
450
-
451
  # ─────────────────────────────────────────────
452
  # TAB A3 — REAL VARIANT LOOKUP (S1-A·R1a)
453
  # ─────────────────────────────────────────────
454
 
455
  def a3_run(hgvs: str):
456
- """Look up a variant in ClinVar and gnomAD. Never hallucinate."""
457
  today = datetime.date.today().isoformat()
458
  hgvs = hgvs.strip()
459
  if not hgvs:
@@ -461,7 +440,7 @@ def a3_run(hgvs: str):
461
 
462
  result_parts = []
463
 
464
- # ── ClinVar ──
465
  clinvar_cached = cache_get("clinvar", hgvs)
466
  if clinvar_cached is None:
467
  try:
@@ -479,7 +458,6 @@ def a3_run(hgvs: str):
479
  clinvar_cached = None
480
 
481
  if clinvar_cached and len(clinvar_cached) > 0:
482
- # Fetch summary
483
  try:
484
  time.sleep(0.34)
485
  r2 = requests.get(
@@ -515,7 +493,7 @@ def a3_run(hgvs: str):
515
  "> ⚠️ Not in database. Do not interpret."
516
  )
517
 
518
- # ── gnomAD ──
519
  gnomad_cached = cache_get("gnomad", hgvs)
520
  if gnomad_cached is None:
521
  try:
@@ -573,13 +551,11 @@ def a3_run(hgvs: str):
573
  journal_log("S1-A·R1a", f"hgvs={hgvs}", result_parts[0][:100] if result_parts else "no results")
574
  return "\n\n".join(result_parts)
575
 
576
-
577
  # ─────────────────────────────────────────────
578
  # TAB A4 — LITERATURE GAP FINDER (S1-A·R2c)
579
  # ─────────────────────────────────────────────
580
 
581
  def a4_run(cancer_type: str, keyword: str):
582
- """Papers per year chart + gap detection."""
583
  today = datetime.date.today().isoformat()
584
  keyword = keyword.strip()
585
  if not keyword:
@@ -594,7 +570,6 @@ def a4_run(cancer_type: str, keyword: str):
594
  n = pubmed_count(q)
595
  counts.append(max(n, 0))
596
 
597
- # Gap detection: years with 0 or below-average counts
598
  avg = np.mean([c for c in counts if c > 0]) if any(c > 0 for c in counts) else 0
599
  gaps = [yr for yr, c in zip(years, counts) if c == 0]
600
  low_years = [yr for yr, c in zip(years, counts) if 0 < c < avg * 0.3]
@@ -639,17 +614,14 @@ def a4_run(cancer_type: str, keyword: str):
639
  journal_log("S1-A·R2c", f"cancer={cancer_type}, kw={keyword}", summary[:100])
640
  return img, summary
641
 
642
-
643
  # ─────────────────────────────────────────────
644
  # TAB A5 — DRUGGABLE ORPHANS (S1-A·R2d)
645
  # ─────────────────────────────────────────────
646
 
647
  def a5_run(cancer_type: str):
648
- """Find essential genes with no approved drug and no active trial."""
649
  today = datetime.date.today().isoformat()
650
  efo = CANCER_EFO.get(cancer_type, "")
651
 
652
- # 1. Get associated targets from OpenTargets with tractability info
653
  gql = """
654
  query DruggableTargets($efoId: String!, $size: Int!) {
655
  disease(efoId: $efoId) {
@@ -658,14 +630,8 @@ def a5_run(cancer_type: str):
658
  target {
659
  approvedSymbol
660
  approvedName
661
- tractability {
662
- label
663
- modality
664
- value
665
- }
666
- knownDrugs {
667
- count
668
- }
669
  }
670
  score
671
  }
@@ -686,7 +652,6 @@ def a5_run(cancer_type: str):
686
  if not rows_ot:
687
  return None, f"⚠️ OpenTargets returned no data for {cancer_type}.\n\n*Source: OpenTargets | Date: {today}*"
688
 
689
- # 2. Filter: no known drugs
690
  orphan_candidates = []
691
  for row in rows_ot:
692
  t = row["target"]
@@ -699,7 +664,6 @@ def a5_run(cancer_type: str):
699
  if drug_count == 0:
700
  orphan_candidates.append({"gene": gene, "name": t.get("approvedName", ""), "ot_score": row["score"]})
701
 
702
- # 3. Check ClinicalTrials for each candidate
703
  records = []
704
  for cand in orphan_candidates[:15]:
705
  gene = cand["gene"]
@@ -736,7 +700,6 @@ def a5_run(cancer_type: str):
736
  journal_log("S1-A·R2d", f"cancer={cancer_type}", f"orphans={len(df)}")
737
  return df, note
738
 
739
-
740
  # ─────────────────────────────────────────────
741
  # GROUP B — LEARNING SANDBOX
742
  # ─────────────────────────────────────────────
@@ -787,14 +750,12 @@ def b1_run(gene: str):
787
 
788
  fig, axes = plt.subplots(1, 2, figsize=(11, 4), facecolor="white")
789
 
790
- # Binding energy bar chart
791
  colors_e = ["#d73027" if e < -16 else "#fc8d59" if e < -13 else "#4393c3" for e in energies]
792
  axes[0].barh(mirnas, [-e for e in energies], color=colors_e, edgecolor="white")
793
  axes[0].set_xlabel("Binding Energy (|kcal/mol|)", fontsize=10)
794
  axes[0].set_title(f"Predicted Binding Energy\n{gene} miRNA targets", fontsize=10)
795
  axes[0].set_facecolor("white")
796
 
797
- # Expression change
798
  colors_x = ["#d73027" if c < 0 else "#4393c3" for c in changes]
799
  axes[1].barh(mirnas, changes, color=colors_x, edgecolor="white")
800
  axes[1].axvline(0, color="black", linewidth=0.8)
@@ -819,7 +780,6 @@ def b1_run(gene: str):
819
  journal_log("S1-B·R1a", f"gene={gene}", f"top_miRNA={mirnas[0]}")
820
  return img, df.to_markdown(index=False) + context
821
 
822
-
823
  # ── TAB B2 — siRNA Targets (S1-B·R2a) ───────────────────
824
 
825
  SIRNA_DB = {
@@ -881,14 +841,10 @@ def b2_run(cancer: str):
881
  journal_log("S1-B·R2a", f"cancer={cancer}", f"top={targets[0]}")
882
  return img, df.to_markdown(index=False)
883
 
884
-
885
  # ── TAB B3 — LNP Corona Simulator (S1-D·R1a) ───────────────
886
 
887
  def b3_run(peg_mol_pct: float, ionizable_pct: float, helper_pct: float,
888
  chol_pct: float, particle_size_nm: float, serum_pct: float):
889
- """Simulate protein corona composition based on LNP formulation parameters."""
890
- # Rule-based model (educational only)
891
- # Higher PEG → less corona; higher ionizable → more ApoE; larger size → more fibrinogen
892
  total_lipid = peg_mol_pct + ionizable_pct + helper_pct + chol_pct
893
  peg_norm = peg_mol_pct / max(total_lipid, 1)
894
 
@@ -907,14 +863,12 @@ def b3_run(peg_mol_pct: float, ionizable_pct: float, helper_pct: float,
907
 
908
  fig, axes = plt.subplots(1, 2, figsize=(11, 4), facecolor="white")
909
 
910
- # Pie chart
911
  labels = list(corona_proteins.keys())
912
  sizes = list(corona_proteins.values())
913
  colors_pie = plt.cm.Set2(np.linspace(0, 1, len(labels)))
914
  axes[0].pie(sizes, labels=labels, colors=colors_pie, autopct="%1.1f%%", startangle=90)
915
  axes[0].set_title("Predicted Corona Composition\n(⚠️ SIMULATED)", fontsize=10)
916
 
917
- # Bar chart
918
  axes[1].bar(labels, sizes, color=colors_pie, edgecolor="white")
919
  axes[1].set_ylabel("Relative Abundance", fontsize=10)
920
  axes[1].set_title("Corona Protein Fractions", fontsize=10)
@@ -937,21 +891,15 @@ def b3_run(peg_mol_pct: float, ionizable_pct: float, helper_pct: float,
937
  journal_log("S1-D·R1a", f"PEG={peg_mol_pct}%,size={particle_size_nm}nm", f"ApoE={apoe_pct:.1f}%")
938
  return img, interpretation
939
 
940
-
941
  # ── TAB B4 — Flow Corona (Vroman Kinetics) (S1-D·R2a) ──────
942
 
943
  def b4_run(time_points: int, kon_albumin: float, kon_apoe: float,
944
  koff_albumin: float, koff_apoe: float):
945
- """Simulate Vroman effect: competitive protein adsorption kinetics."""
946
  t = np.linspace(0, time_points, 500)
947
 
948
- # Simple Langmuir competitive adsorption model (educational)
949
- # Albumin: fast on, fast off (early corona)
950
- # ApoE: slow on, slow off (late/hard corona)
951
  albumin = (kon_albumin / (kon_albumin + koff_albumin)) * (1 - np.exp(-(kon_albumin + koff_albumin) * t))
952
  apoe_delay = np.maximum(0, t - 5)
953
  apoe = (kon_apoe / (kon_apoe + koff_apoe)) * (1 - np.exp(-(kon_apoe + koff_apoe) * apoe_delay))
954
- # Vroman displacement: albumin decreases as ApoE increases
955
  albumin_displaced = albumin * np.exp(-apoe * 2)
956
  fibrinogen = 0.3 * (1 - np.exp(-0.05 * t)) * np.exp(-apoe * 1.5)
957
 
@@ -981,7 +929,6 @@ def b4_run(time_points: int, kon_albumin: float, kon_apoe: float,
981
  journal_log("S1-D·R2a", f"kon_alb={kon_albumin},kon_apoe={kon_apoe}", note[:80])
982
  return img, note
983
 
984
-
985
  # ── TAB B5 — Variant Concepts (S1-A·R1b) ───────────────────
986
 
987
  VARIANT_RULES = {
@@ -1034,6 +981,7 @@ def b5_run(classification: str):
1034
  )
1035
  journal_log("S1-A·R1b", f"class={classification}", output[:100])
1036
  return output
 
1037
  # ─────────────────────────────────────────────
1038
  # GRADIO UI ASSEMBLY
1039
  # ─────────────────────────────────────────────
@@ -1050,39 +998,167 @@ body { font-family: 'Inter', sans-serif; }
1050
  background: #f8f9fa; border-left: 4px solid #d73027;
1051
  padding: 10px 14px; margin: 6px 0; border-radius: 4px;
1052
  }
1053
- /* Зміна курсора на pointer для всіх інтерактивних елементів */
1054
- .gr-dropdown, .gr-button, .gr-slider, .gr-radio, .gr-checkbox,
1055
- .tab-nav button, .gr-accordion, .gr-dataset {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1056
  cursor: pointer !important;
1057
  }
1058
- /* Для текстових полів залишаємо звичайний текстовий курсор */
1059
- .gr-textbox input, .gr-textarea textarea {
1060
- cursor: text !important;
 
1061
  }
1062
- footer { display: none !important; }
1063
 
1064
- /* Максимально агресивний перезапис для всіх елементів всередині gr-dropdown */
1065
- .gr-dropdown,
1066
- .gr-dropdown *,
1067
- .gr-dropdown .wrap,
1068
- .gr-dropdown .input-wrap,
1069
- .gr-dropdown .displayed-value,
1070
- .gr-dropdown .selected-value,
1071
- .gr-dropdown [class*="value"],
1072
- .gr-dropdown [class*="input"]:not(input) {
 
 
1073
  cursor: pointer !important;
1074
  }
 
 
 
 
 
 
 
 
 
 
 
 
1075
 
1076
- /* Виняток — тільки для реального поля input */
1077
- .gr-dropdown input,
1078
- .gr-dropdown input[type="text"],
1079
- .gr-dropdown [role="combobox"] input {
1080
- cursor: text !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1081
  }
1082
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1083
  """
1084
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1085
 
 
1086
  def build_app():
1087
  with gr.Blocks(css=CUSTOM_CSS, title="K R&D Lab — Cancer Research Suite") as demo:
1088
  gr.Markdown(
@@ -1092,235 +1168,105 @@ def build_app():
1092
  )
1093
 
1094
  with gr.Row():
1095
- # ── MAIN CONTENT ──
1096
  with gr.Column(scale=4):
1097
- with gr.Tabs() as main_tabs:
 
 
 
1098
 
1099
- # ════════════════════════════════
1100
  # GROUP A — REAL DATA TOOLS
1101
- # ════════════════════════════════
1102
- with gr.Tab("🔬 Real Data Tools"):
1103
- with gr.Tabs():
1104
-
1105
- # ── A1 ──
1106
- with gr.Tab("S1-A·R2a · Gray Zones Explorer"):
1107
- gr.Markdown(
1108
- "Identify underexplored biological processes in a cancer type "
1109
- "using live PubMed + OpenTargets data."
1110
- )
1111
  a1_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
1112
  a1_btn = gr.Button("🔍 Explore Gray Zones", variant="primary")
1113
  a1_heatmap = gr.Image(label="Research Coverage Heatmap", type="pil")
1114
  a1_gaps = gr.Markdown(label="Top 5 Research Gaps")
1115
  with gr.Accordion("📖 Learning Mode", open=False):
1116
- gr.Markdown(
1117
- "**What is a research gray zone?**\n\n"
1118
- "A gray zone is a biological process that is well-studied in other cancers "
1119
- "but has very few publications in your selected cancer type. "
1120
- "Low paper counts (red/white cells) indicate potential unexplored territory.\n\n"
1121
- "**How to use:** Select a rare cancer (e.g. DIPG, MCC) to find the most "
1122
- "underexplored processes. Cross-reference with Tab A2 to find targetable genes."
1123
- )
1124
  a1_btn.click(a1_run, inputs=[a1_cancer], outputs=[a1_heatmap, a1_gaps])
1125
 
1126
- # ── A2 ──
1127
- with gr.Tab("S1-A·R2b · Understudied Target Finder"):
1128
- gr.Markdown(
1129
- "Find essential genes with high research gap index "
1130
- "(high essentiality, low publication coverage)."
1131
- )
1132
- gr.Markdown(
1133
- "> ⚠️ **Essentiality scores are placeholder estimates** from a "
1134
- "curated reference gene set — **not real DepMap data**. "
1135
- "Association scores and paper/trial counts are fetched live. "
1136
- "For real essentiality values, download `CRISPR_gene_effect.csv` "
1137
- "from [depmap.org](https://depmap.org/portal/download/all/) and "
1138
- "replace `_load_depmap_sample()` in `app.py`."
1139
- )
1140
  a2_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
1141
  a2_btn = gr.Button("🎯 Find Understudied Targets", variant="primary")
1142
  a2_table = gr.Dataframe(label="Target Gap Table", wrap=True)
1143
  a2_note = gr.Markdown()
1144
- with gr.Accordion("📖 Learning Mode", open=False):
1145
- gr.Markdown(
1146
- "**Gap Index formula:** `essentiality / log(papers + 1)`\n\n"
1147
- "- **Essentiality**: inverted DepMap CRISPR gene effect score "
1148
- "(positive = more essential, per DepMap convention where negative raw = essential)\n"
1149
- "- **Papers**: PubMed count for gene + cancer type\n"
1150
- "- **High Gap Index** = essential gene with few publications = high research opportunity\n\n"
1151
- "**Caution:** Essentiality scores shown here use a curated reference set. "
1152
- "For full DepMap analysis, download the complete dataset from depmap.org."
1153
- )
1154
  a2_btn.click(a2_run, inputs=[a2_cancer], outputs=[a2_table, a2_note])
1155
 
1156
- # ── A3 ──
1157
- with gr.Tab("S1-A·R1a · Real Variant Lookup"):
1158
- gr.Markdown(
1159
- "Look up a variant in **ClinVar** and **gnomAD**. "
1160
- "Results are fetched live — never hallucinated."
1161
- )
1162
- a3_hgvs = gr.Textbox(
1163
- label="HGVS Notation",
1164
- placeholder="e.g. NM_007294.4:c.5266dupC or NM_000546.6:c.524G>A",
1165
- lines=1
1166
- )
1167
  a3_btn = gr.Button("🔎 Look Up Variant", variant="primary")
1168
  a3_result = gr.Markdown()
1169
- with gr.Accordion("📖 Learning Mode", open=False):
1170
- gr.Markdown(
1171
- "**HGVS notation format:**\n"
1172
- "- `NM_XXXXXX.X:c.NNNN[change]` — coding DNA reference\n"
1173
- "- `NC_XXXXXX.X:g.NNNN[change]` — genomic reference\n\n"
1174
- "**ClinVar** classifies variants as: Pathogenic / Likely Pathogenic / "
1175
- "VUS / Likely Benign / Benign\n\n"
1176
- "**gnomAD** provides allele frequency (AF) in population cohorts. "
1177
- "AF > 1% generally suggests benign.\n\n"
1178
- "**Important:** If a variant is not found, this tool returns "
1179
- "'Not in database. Do not interpret.' — never a fabricated result."
1180
- )
1181
  a3_btn.click(a3_run, inputs=[a3_hgvs], outputs=[a3_result])
1182
 
1183
- # ── A4 ──
1184
- with gr.Tab("S1-A·R2c · Literature Gap Finder"):
1185
- gr.Markdown(
1186
- "Visualize publication trends over 10 years and detect "
1187
- "years with low research activity."
1188
- )
1189
  with gr.Row():
1190
  a4_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
1191
- a4_kw = gr.Textbox(label="Keyword", placeholder="e.g. ferroptosis", lines=1)
1192
  a4_btn = gr.Button("📊 Analyze Literature Trend", variant="primary")
1193
  a4_chart = gr.Image(label="Papers per Year", type="pil")
1194
  a4_gaps = gr.Markdown()
1195
- with gr.Accordion("📖 Learning Mode", open=False):
1196
- gr.Markdown(
1197
- "**How to read the chart:**\n"
1198
- "- 🔵 Blue bars = normal activity\n"
1199
- "- 🟠 Orange bars = low activity (<30% of average)\n"
1200
- "- 🔴 Red bars = zero publications (true gap)\n\n"
1201
- "**Research strategy:** A gap in 2018–2020 followed by a spike in 2022+ "
1202
- "may indicate a recently emerging field — ideal for early-career researchers."
1203
- )
1204
  a4_btn.click(a4_run, inputs=[a4_cancer, a4_kw], outputs=[a4_chart, a4_gaps])
1205
 
1206
- # ── A5 ──
1207
- with gr.Tab("S1-A·R2d · Druggable Orphans"):
1208
- gr.Markdown(
1209
- "Identify cancer-associated essential genes with **no approved drug** "
1210
- "and **no active clinical trial**."
1211
- )
1212
  a5_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
1213
  a5_btn = gr.Button("💊 Find Druggable Orphans", variant="primary")
1214
  a5_table = gr.Dataframe(label="Orphan Target Table", wrap=True)
1215
  a5_note = gr.Markdown()
1216
- with gr.Accordion("📖 Learning Mode", open=False):
1217
- gr.Markdown(
1218
- "**What is a druggable orphan?**\n\n"
1219
- "A gene that is:\n"
1220
- "1. Strongly associated with a cancer (high OpenTargets score)\n"
1221
- "2. Has no approved drug targeting it\n"
1222
- "3. Has no active clinical trial\n\n"
1223
- "These represent the highest-opportunity targets for drug discovery. "
1224
- "Cross-reference with Tab A2 (Gap Index) for prioritization."
1225
- )
1226
  a5_btn.click(a5_run, inputs=[a5_cancer], outputs=[a5_table, a5_note])
1227
 
1228
- # ── A6 (RAG Chatbot) ──
1229
- with gr.Tab("S1-A·R2e · Research Assistant"):
1230
- gr.Markdown(
1231
- "**RAG-powered research assistant** indexed on 20 curated papers "
1232
- "on LNP delivery, protein corona, and cancer variants.\n\n"
1233
- "*Powered by sentence-transformers + FAISS — no API key required.*"
1234
- )
1235
  try:
1236
  from chatbot import build_chatbot_tab
1237
  build_chatbot_tab()
1238
  except ImportError:
1239
- gr.Markdown(
1240
- "⚠️ `chatbot.py` not found. Please ensure it is in the same directory as `app.py`. "
1241
- "See `chatbot.py` for setup instructions."
1242
- )
1243
 
1244
- # ════════════════════════════════
1245
  # GROUP B — LEARNING SANDBOX
1246
- # ════════════════════════════════
1247
- with gr.Tab("📚 Learning Sandbox"):
1248
- gr.Markdown(
1249
- "> ⚠️ **ALL TABS IN THIS GROUP USE SIMULATED DATA** — "
1250
- "For educational purposes only. Results do not reflect real experiments."
1251
- )
1252
- with gr.Tabs():
1253
-
1254
- # ── B1 ──
1255
- with gr.Tab("S1-B·R1a · miRNA Explorer"):
1256
  gr.Markdown(SIMULATED_BANNER)
1257
  b1_gene = gr.Dropdown(["BRCA2", "BRCA1", "TP53"], label="Gene", value="TP53")
1258
  b1_btn = gr.Button("🔬 Explore miRNA Interactions", variant="primary")
1259
  b1_plot = gr.Image(label="miRNA Binding & Expression (⚠️ SIMULATED)", type="pil")
1260
  b1_table = gr.Markdown()
1261
- with gr.Accordion("📖 Learning Mode", open=False):
1262
- gr.Markdown(
1263
- "**miRNA biology basics:**\n\n"
1264
- "- miRNAs are ~22 nt non-coding RNAs that bind 3'UTR of mRNAs\n"
1265
- "- Seed match types: 8mer > 7mer-m8 > 7mer-A1 > 6mer (binding strength)\n"
1266
- "- Negative binding energy = stronger predicted interaction\n"
1267
- "- Negative log2FC = miRNA downregulated in tumor\n\n"
1268
- "**Key concept:** Tumor suppressor genes (BRCA1/2, TP53) are often "
1269
- "silenced by oncogenic miRNAs (e.g. miR-21, miR-155)."
1270
- )
1271
  b1_btn.click(b1_run, inputs=[b1_gene], outputs=[b1_plot, b1_table])
1272
 
1273
- # ── B2 ──
1274
- with gr.Tab("S1-B·R2a · siRNA Targets"):
1275
  gr.Markdown(SIMULATED_BANNER)
1276
  b2_cancer = gr.Dropdown(["LUAD", "BRCA", "COAD"], label="Cancer Type", value="LUAD")
1277
  b2_btn = gr.Button("🎯 Simulate siRNA Efficacy", variant="primary")
1278
  b2_plot = gr.Image(label="siRNA Efficacy (⚠️ SIMULATED)", type="pil")
1279
  b2_table = gr.Markdown()
1280
- with gr.Accordion("📖 Learning Mode", open=False):
1281
- gr.Markdown(
1282
- "**siRNA design principles:**\n\n"
1283
- "- siRNAs are 21-23 nt dsRNA that trigger RISC-mediated mRNA cleavage\n"
1284
- "- Off-target risk: seed region complementarity to unintended mRNAs\n"
1285
- "- Delivery challenge: endosomal escape, serum stability, tumor penetration\n\n"
1286
- "**Clinical context:** KRAS siRNA delivery remains a major challenge "
1287
- "due to the undruggable nature of the RAS binding pocket."
1288
- )
1289
  b2_btn.click(b2_run, inputs=[b2_cancer], outputs=[b2_plot, b2_table])
1290
 
1291
- # ── B3 ──
1292
- with gr.Tab("S1-D·R1a · LNP Corona"):
1293
  gr.Markdown(SIMULATED_BANNER)
1294
  with gr.Row():
1295
- b3_peg = gr.Slider(0.5, 5.0, value=1.5, step=0.1, label="PEG mol% (lipid)")
1296
  b3_ion = gr.Slider(10, 60, value=50, step=1, label="Ionizable lipid mol%")
1297
  with gr.Row():
1298
  b3_helper = gr.Slider(5, 30, value=10, step=1, label="Helper lipid mol%")
1299
  b3_chol = gr.Slider(10, 50, value=38, step=1, label="Cholesterol mol%")
1300
  with gr.Row():
1301
  b3_size = gr.Slider(50, 300, value=100, step=5, label="Particle size (nm)")
1302
- b3_serum = gr.Slider(0, 100, value=10, step=5, label="Serum % in medium")
1303
  b3_btn = gr.Button("🧪 Simulate Corona", variant="primary")
1304
  b3_plot = gr.Image(label="Corona Composition (⚠️ SIMULATED)", type="pil")
1305
  b3_interp = gr.Markdown()
1306
- with gr.Accordion("📖 Learning Mode", open=False):
1307
- gr.Markdown(
1308
- "**Protein corona basics:**\n\n"
1309
- "- When nanoparticles enter biological fluids, proteins adsorb to their surface\n"
1310
- "- **Hard corona**: tightly bound, long-lived proteins (ApoE, fibrinogen)\n"
1311
- "- **Soft corona**: loosely bound, rapidly exchanging proteins (albumin)\n"
1312
- "- **ApoE enrichment** → enhanced brain targeting via LDLR/LRP1 receptors\n"
1313
- "- **PEG** reduces corona formation but may trigger anti-PEG antibodies\n\n"
1314
- "**GBM relevance:** ApoE-enriched LNPs show improved BBB crossing in preclinical models."
1315
- )
1316
- b3_btn.click(
1317
- b3_run,
1318
- inputs=[b3_peg, b3_ion, b3_helper, b3_chol, b3_size, b3_serum],
1319
- outputs=[b3_plot, b3_interp]
1320
- )
1321
-
1322
- # ── B4 ──
1323
- with gr.Tab("S1-D·R2a · Flow Corona"):
1324
  gr.Markdown(SIMULATED_BANNER)
1325
  with gr.Row():
1326
  b4_time = gr.Slider(10, 120, value=60, step=5, label="Time range (min)")
@@ -1332,53 +1278,18 @@ def build_app():
1332
  b4_btn = gr.Button("🌊 Simulate Vroman Kinetics", variant="primary")
1333
  b4_plot = gr.Image(label="Vroman Effect (⚠️ SIMULATED)", type="pil")
1334
  b4_note = gr.Markdown()
1335
- with gr.Accordion("📖 Learning Mode", open=False):
1336
- gr.Markdown(
1337
- "**The Vroman Effect:**\n\n"
1338
- "Described by Leo Vroman (1962): proteins with high abundance but low affinity "
1339
- "(albumin) adsorb first, then are displaced by lower-abundance but higher-affinity "
1340
- "proteins (fibrinogen, ApoE).\n\n"
1341
- "**Parameters:**\n"
1342
- "- **kon**: association rate constant (higher = faster binding)\n"
1343
- "- **koff**: dissociation rate constant (lower = tighter binding)\n"
1344
- "- **Kd = koff/kon**: equilibrium dissociation constant\n\n"
1345
- "**Clinical implication:** The final hard corona (not initial) determines "
1346
- "nanoparticle fate in vivo."
1347
- )
1348
- b4_btn.click(
1349
- b4_run,
1350
- inputs=[b4_time, b4_kon_alb, b4_kon_apoe, b4_koff_alb, b4_koff_apoe],
1351
- outputs=[b4_plot, b4_note]
1352
- )
1353
-
1354
- # ── B5 ──
1355
- with gr.Tab("S1-A·R1b · Variant Concepts"):
1356
  gr.Markdown(SIMULATED_BANNER)
1357
- b5_class = gr.Dropdown(
1358
- list(VARIANT_RULES.keys()),
1359
- label="ACMG Classification",
1360
- value="VUS"
1361
- )
1362
  b5_btn = gr.Button("📋 Explain Classification", variant="primary")
1363
  b5_result = gr.Markdown()
1364
- with gr.Accordion("📖 Learning Mode", open=False):
1365
- gr.Markdown(
1366
- "**ACMG/AMP 2015 Classification Framework:**\n\n"
1367
- "Variants are classified into 5 tiers based on evidence:\n"
1368
- "1. **Pathogenic** — strong evidence of disease causation\n"
1369
- "2. **Likely Pathogenic** — >90% probability pathogenic\n"
1370
- "3. **VUS** — uncertain significance; insufficient evidence\n"
1371
- "4. **Likely Benign** — >90% probability benign\n"
1372
- "5. **Benign** — strong evidence of no disease effect\n\n"
1373
- "**Key codes:** PVS1 (null variant), PS1 (same AA change), "
1374
- "PM2 (absent from controls), BA1 (MAF >5%)"
1375
- )
1376
  b5_btn.click(b5_run, inputs=[b5_class], outputs=[b5_result])
1377
 
1378
- # ════════════════════════════════
1379
  # JOURNAL — окрема вкладка
1380
- # ════════════════════════════════
1381
- with gr.Tab("📓 Journal"):
1382
  gr.Markdown("## Lab Journal — Full History")
1383
  with gr.Row():
1384
  journal_filter = gr.Dropdown(
@@ -1399,38 +1310,35 @@ def build_app():
1399
  )
1400
  journal_filter.change(refresh_journal, inputs=[journal_filter], outputs=journal_display)
1401
 
1402
- # ── SIDEBAR (Quick Note) ──
1403
  with gr.Column(scale=1, min_width=260):
1404
- gr.Markdown("## 📓 Lab Journal")
1405
- note_category = gr.Dropdown(
1406
- choices=JOURNAL_CATEGORIES,
1407
- value="Manual",
1408
- label="Category"
1409
- )
1410
- note_input = gr.Textbox(
1411
- label="Observation",
1412
- placeholder="Type your note here...",
1413
- lines=3
1414
- )
1415
- with gr.Row():
1416
- save_btn = gr.Button("💾 Save Note", size="sm", variant="primary")
1417
- clear_note_btn = gr.Button("🗑️ Clear", size="sm")
1418
- save_status = gr.Markdown("")
1419
-
1420
- def save_note(category, note):
1421
- if note.strip():
1422
- journal_log(category, "manual note", note, note)
1423
- return " Note saved.", ""
1424
- return "⚠️ Note is empty.", ""
1425
-
1426
- save_btn.click(
1427
- save_note,
1428
- inputs=[note_category, note_input],
1429
- outputs=[save_status, note_input]
1430
- )
1431
- clear_note_btn.click(lambda: ("", ""), outputs=[note_input, save_status])
1432
 
1433
- # === Інтеграція з Learning Playground ===
 
 
 
1434
  with gr.Row():
1435
  gr.Markdown("""
1436
  ---
@@ -1448,7 +1356,6 @@ def build_app():
1448
 
1449
  return demo
1450
 
1451
-
1452
  if __name__ == "__main__":
1453
  app = build_app()
1454
  app.launch(share=False)
 
254
  return data
255
  except Exception as e:
256
  return {"error": str(e)}
257
+
258
  # ─────────────────────────────────────────────
259
  # TAB A1 — GRAY ZONES EXPLORER (S1-A·R2a)
260
  # ─────────────────────────────────────────────
261
 
262
  def a1_run(cancer_type: str):
 
263
  today = datetime.date.today().isoformat()
264
  counts = {}
265
  for proc in PROCESSES:
 
267
  n = pubmed_count(q)
268
  counts[proc] = n
269
 
 
270
  df = pd.DataFrame({"process": PROCESSES, cancer_type: [counts[p] for p in PROCESSES]})
271
  df = df.set_index("process")
 
 
272
  df = df.replace(-1, np.nan)
273
 
274
  fig, ax = plt.subplots(figsize=(6, 8), facecolor="white")
 
291
  img = Image.open(buf)
292
  plt.close(fig)
293
 
 
294
  sorted_procs = sorted(
295
  [(p, counts[p]) for p in PROCESSES if counts[p] >= 0],
296
  key=lambda x: x[1]
 
308
  source_note = f"*Source: PubMed E-utilities | Date: {today}*"
309
  return img, gaps_md + "\n\n" + source_note
310
 
 
311
  # ─────────────────────────────────────────────
312
  # TAB A2 — UNDERSTUDIED TARGET FINDER (S1-A·R2b)
313
  # ─────────────────────────────────────────────
314
 
315
+ DEPMAP_URL = "https://ndownloader.figshare.com/files/40448549"
316
 
317
  _depmap_cache = {}
318
 
319
  def _load_depmap_sample() -> pd.DataFrame:
 
320
  global _depmap_cache
321
  if "df" in _depmap_cache:
322
  return _depmap_cache["df"]
 
323
  genes = [
324
  "MYC", "KRAS", "TP53", "EGFR", "PTEN", "RB1", "CDKN2A",
325
  "PIK3CA", "AKT1", "BRAF", "NRAS", "IDH1", "IDH2", "ARID1A",
 
328
  "FGFR1", "FGFR2", "MET", "ALK", "RET", "ERBB2",
329
  "MTOR", "PIK3R1", "STK11", "NF1", "NF2", "TSC1", "TSC2",
330
  ]
 
331
  rng = np.random.default_rng(42)
332
  scores = rng.uniform(-1.5, 0.3, len(genes))
333
  df = pd.DataFrame({"gene": genes, "gene_effect": scores})
334
  _depmap_cache["df"] = df
335
  return df
336
 
 
337
  def a2_run(cancer_type: str):
 
338
  today = datetime.date.today().isoformat()
339
  efo = CANCER_EFO.get(cancer_type, "")
340
 
 
341
  gql = """
342
  query AssocTargets($efoId: String!, $size: Int!) {
343
  disease(efoId: $efoId) {
344
  associatedTargets(page: {index: 0, size: $size}) {
345
  rows {
346
+ target { approvedSymbol approvedName }
 
 
 
347
  score
348
  }
349
  }
 
361
  pass
362
 
363
  if not rows_ot:
364
+ return None, f"⚠️ OpenTargets returned no data for {cancer_type}.\n\n*Source: OpenTargets | Date: {today}*"
365
 
366
  genes_ot = [r["target"]["approvedSymbol"] for r in rows_ot]
367
 
 
368
  paper_counts = {}
369
+ for gene in genes_ot[:20]:
370
  q = f'"{gene}" AND "{cancer_type}"[tiab]'
371
  paper_counts[gene] = pubmed_count(q)
372
 
 
373
  trial_counts = {}
374
  for gene in genes_ot[:20]:
375
  cached = cache_get("ct_gene", f"{gene}_{cancer_type}")
 
389
  except Exception:
390
  trial_counts[gene] = -1
391
 
 
392
  depmap_df = _load_depmap_sample()
393
  depmap_dict = dict(zip(depmap_df["gene"], depmap_df["gene_effect"]))
394
 
 
395
  records = []
396
  for gene in genes_ot[:20]:
397
  raw_ess = depmap_dict.get(gene, None)
 
401
  ess_display = "N/A"
402
  gap_idx = 0.0
403
  else:
 
404
  ess_inverted = -raw_ess
405
  ess_display = f"{ess_inverted:.3f}"
406
  papers_safe = max(papers, 0)
 
428
  journal_log("S1-A·R2b", f"cancer={cancer_type}", "no targets found")
429
  return result_df, note
430
 
 
431
  # ─────────────────────────────────────────────
432
  # TAB A3 — REAL VARIANT LOOKUP (S1-A·R1a)
433
  # ─────────────────────────────────────────────
434
 
435
  def a3_run(hgvs: str):
 
436
  today = datetime.date.today().isoformat()
437
  hgvs = hgvs.strip()
438
  if not hgvs:
 
440
 
441
  result_parts = []
442
 
443
+ # ClinVar
444
  clinvar_cached = cache_get("clinvar", hgvs)
445
  if clinvar_cached is None:
446
  try:
 
458
  clinvar_cached = None
459
 
460
  if clinvar_cached and len(clinvar_cached) > 0:
 
461
  try:
462
  time.sleep(0.34)
463
  r2 = requests.get(
 
493
  "> ⚠️ Not in database. Do not interpret."
494
  )
495
 
496
+ # gnomAD
497
  gnomad_cached = cache_get("gnomad", hgvs)
498
  if gnomad_cached is None:
499
  try:
 
551
  journal_log("S1-A·R1a", f"hgvs={hgvs}", result_parts[0][:100] if result_parts else "no results")
552
  return "\n\n".join(result_parts)
553
 
 
554
  # ─────────────────────────────────────────────
555
  # TAB A4 — LITERATURE GAP FINDER (S1-A·R2c)
556
  # ─────────────────────────────────────────────
557
 
558
  def a4_run(cancer_type: str, keyword: str):
 
559
  today = datetime.date.today().isoformat()
560
  keyword = keyword.strip()
561
  if not keyword:
 
570
  n = pubmed_count(q)
571
  counts.append(max(n, 0))
572
 
 
573
  avg = np.mean([c for c in counts if c > 0]) if any(c > 0 for c in counts) else 0
574
  gaps = [yr for yr, c in zip(years, counts) if c == 0]
575
  low_years = [yr for yr, c in zip(years, counts) if 0 < c < avg * 0.3]
 
614
  journal_log("S1-A·R2c", f"cancer={cancer_type}, kw={keyword}", summary[:100])
615
  return img, summary
616
 
 
617
  # ─────────────────────────────────────────────
618
  # TAB A5 — DRUGGABLE ORPHANS (S1-A·R2d)
619
  # ─────────────────────────────────────────────
620
 
621
  def a5_run(cancer_type: str):
 
622
  today = datetime.date.today().isoformat()
623
  efo = CANCER_EFO.get(cancer_type, "")
624
 
 
625
  gql = """
626
  query DruggableTargets($efoId: String!, $size: Int!) {
627
  disease(efoId: $efoId) {
 
630
  target {
631
  approvedSymbol
632
  approvedName
633
+ tractability { label modality value }
634
+ knownDrugs { count }
 
 
 
 
 
 
635
  }
636
  score
637
  }
 
652
  if not rows_ot:
653
  return None, f"⚠️ OpenTargets returned no data for {cancer_type}.\n\n*Source: OpenTargets | Date: {today}*"
654
 
 
655
  orphan_candidates = []
656
  for row in rows_ot:
657
  t = row["target"]
 
664
  if drug_count == 0:
665
  orphan_candidates.append({"gene": gene, "name": t.get("approvedName", ""), "ot_score": row["score"]})
666
 
 
667
  records = []
668
  for cand in orphan_candidates[:15]:
669
  gene = cand["gene"]
 
700
  journal_log("S1-A·R2d", f"cancer={cancer_type}", f"orphans={len(df)}")
701
  return df, note
702
 
 
703
  # ─────────────────────────────────────────────
704
  # GROUP B — LEARNING SANDBOX
705
  # ─────────────────────────────────────────────
 
750
 
751
  fig, axes = plt.subplots(1, 2, figsize=(11, 4), facecolor="white")
752
 
 
753
  colors_e = ["#d73027" if e < -16 else "#fc8d59" if e < -13 else "#4393c3" for e in energies]
754
  axes[0].barh(mirnas, [-e for e in energies], color=colors_e, edgecolor="white")
755
  axes[0].set_xlabel("Binding Energy (|kcal/mol|)", fontsize=10)
756
  axes[0].set_title(f"Predicted Binding Energy\n{gene} miRNA targets", fontsize=10)
757
  axes[0].set_facecolor("white")
758
 
 
759
  colors_x = ["#d73027" if c < 0 else "#4393c3" for c in changes]
760
  axes[1].barh(mirnas, changes, color=colors_x, edgecolor="white")
761
  axes[1].axvline(0, color="black", linewidth=0.8)
 
780
  journal_log("S1-B·R1a", f"gene={gene}", f"top_miRNA={mirnas[0]}")
781
  return img, df.to_markdown(index=False) + context
782
 
 
783
  # ── TAB B2 — siRNA Targets (S1-B·R2a) ───────────────────
784
 
785
  SIRNA_DB = {
 
841
  journal_log("S1-B·R2a", f"cancer={cancer}", f"top={targets[0]}")
842
  return img, df.to_markdown(index=False)
843
 
 
844
  # ── TAB B3 — LNP Corona Simulator (S1-D·R1a) ───────────────
845
 
846
  def b3_run(peg_mol_pct: float, ionizable_pct: float, helper_pct: float,
847
  chol_pct: float, particle_size_nm: float, serum_pct: float):
 
 
 
848
  total_lipid = peg_mol_pct + ionizable_pct + helper_pct + chol_pct
849
  peg_norm = peg_mol_pct / max(total_lipid, 1)
850
 
 
863
 
864
  fig, axes = plt.subplots(1, 2, figsize=(11, 4), facecolor="white")
865
 
 
866
  labels = list(corona_proteins.keys())
867
  sizes = list(corona_proteins.values())
868
  colors_pie = plt.cm.Set2(np.linspace(0, 1, len(labels)))
869
  axes[0].pie(sizes, labels=labels, colors=colors_pie, autopct="%1.1f%%", startangle=90)
870
  axes[0].set_title("Predicted Corona Composition\n(⚠️ SIMULATED)", fontsize=10)
871
 
 
872
  axes[1].bar(labels, sizes, color=colors_pie, edgecolor="white")
873
  axes[1].set_ylabel("Relative Abundance", fontsize=10)
874
  axes[1].set_title("Corona Protein Fractions", fontsize=10)
 
891
  journal_log("S1-D·R1a", f"PEG={peg_mol_pct}%,size={particle_size_nm}nm", f"ApoE={apoe_pct:.1f}%")
892
  return img, interpretation
893
 
 
894
  # ── TAB B4 — Flow Corona (Vroman Kinetics) (S1-D·R2a) ──────
895
 
896
  def b4_run(time_points: int, kon_albumin: float, kon_apoe: float,
897
  koff_albumin: float, koff_apoe: float):
 
898
  t = np.linspace(0, time_points, 500)
899
 
 
 
 
900
  albumin = (kon_albumin / (kon_albumin + koff_albumin)) * (1 - np.exp(-(kon_albumin + koff_albumin) * t))
901
  apoe_delay = np.maximum(0, t - 5)
902
  apoe = (kon_apoe / (kon_apoe + koff_apoe)) * (1 - np.exp(-(kon_apoe + koff_apoe) * apoe_delay))
 
903
  albumin_displaced = albumin * np.exp(-apoe * 2)
904
  fibrinogen = 0.3 * (1 - np.exp(-0.05 * t)) * np.exp(-apoe * 1.5)
905
 
 
929
  journal_log("S1-D·R2a", f"kon_alb={kon_albumin},kon_apoe={kon_apoe}", note[:80])
930
  return img, note
931
 
 
932
  # ── TAB B5 — Variant Concepts (S1-A·R1b) ───────────────────
933
 
934
  VARIANT_RULES = {
 
981
  )
982
  journal_log("S1-A·R1b", f"class={classification}", output[:100])
983
  return output
984
+
985
  # ─────────────────────────────────────────────
986
  # GRADIO UI ASSEMBLY
987
  # ─────────────────────────────────────────────
 
998
  background: #f8f9fa; border-left: 4px solid #d73027;
999
  padding: 10px 14px; margin: 6px 0; border-radius: 4px;
1000
  }
1001
+
1002
+ /* Вкладки верхнього рівня з переносом */
1003
+ .tabs-outer .tab-nav {
1004
+ display: flex;
1005
+ flex-wrap: wrap;
1006
+ gap: 2px;
1007
+ }
1008
+ .tabs-outer .tab-nav button {
1009
+ color: #f1f5f9 !important;
1010
+ background: #1e293b !important;
1011
+ font-size: 13px !important;
1012
+ font-weight: 600 !important;
1013
+ padding: 8px 16px !important;
1014
+ border-radius: 6px 6px 0 0 !important;
1015
+ border: 1px solid #334155;
1016
+ border-bottom: none;
1017
+ margin-right: 2px;
1018
+ margin-bottom: 2px;
1019
+ white-space: nowrap;
1020
  cursor: pointer !important;
1021
  }
1022
+ .tabs-outer .tab-nav button.selected {
1023
+ border-bottom: 3px solid #f97316 !important;
1024
+ color: #f97316 !important;
1025
+ background: #0f172a !important;
1026
  }
 
1027
 
1028
+ /* Контейнер вкладок всередині основної колонки (R1, R2, ...) */
1029
+ .main-tabs .tab-nav button {
1030
+ color: #8e9bae !important;
1031
+ background: #0f172a !important;
1032
+ font-size: 12px !important;
1033
+ font-weight: 500 !important;
1034
+ padding: 5px 12px !important;
1035
+ border-radius: 4px 4px 0 0 !important;
1036
+ border: 1px solid #334155 !important;
1037
+ border-bottom: none !important;
1038
+ margin-right: 3px !important;
1039
  cursor: pointer !important;
1040
  }
1041
+ .main-tabs .tab-nav button.selected {
1042
+ color: #38bdf8 !important;
1043
+ background: #1e293b !important;
1044
+ border-color: #38bdf8 !important;
1045
+ border-bottom: none !important;
1046
+ }
1047
+ .main-tabs > .tabitem {
1048
+ background: #1e293b !important;
1049
+ border: 1px solid #334155 !important;
1050
+ border-radius: 0 6px 6px 6px !important;
1051
+ padding: 14px !important;
1052
+ }
1053
 
1054
+ /* Третій рівень вкладок (a, b) */
1055
+ .sub-tabs .tab-nav button {
1056
+ color: #8e9bae !important;
1057
+ background: #1e293b !important;
1058
+ font-size: 11px !important;
1059
+ padding: 3px 8px !important;
1060
+ border-radius: 3px 3px 0 0 !important;
1061
+ cursor: pointer !important;
1062
+ }
1063
+ .sub-tabs .tab-nav button.selected {
1064
+ color: #f97316 !important;
1065
+ background: #0f172a !important;
1066
+ }
1067
+
1068
+ /* Стиль для badges */
1069
+ .proj-badge {
1070
+ background: #1e293b;
1071
+ border-left: 3px solid #f97316;
1072
+ padding: 8px 12px;
1073
+ border-radius: 0 6px 6px 0;
1074
+ margin-bottom: 8px;
1075
+ }
1076
+ .proj-code {
1077
+ color: #8e9bae;
1078
+ font-size: 11px;
1079
+ }
1080
+ .proj-title {
1081
+ color: #f1f5f9;
1082
+ font-size: 14px;
1083
+ font-weight: 600;
1084
+ }
1085
+ .proj-metric {
1086
+ background: #0f2a3f;
1087
+ color: #38bdf8;
1088
+ padding: 1px 7px;
1089
+ border-radius: 3px;
1090
+ font-size: 10px;
1091
+ margin-left: 6px;
1092
+ }
1093
+
1094
+ /* Бічна панель */
1095
+ .sidebar-journal {
1096
+ background: #1e293b;
1097
+ border: 1px solid #334155;
1098
+ border-radius: 8px;
1099
+ padding: 14px;
1100
+ }
1101
+ .sidebar-journal h3 {
1102
+ color: #f97316;
1103
+ margin-top: 0;
1104
  }
1105
 
1106
+ /* Загальні */
1107
+ h1, h2, h3 { color: #f97316 !important; }
1108
+ .gr-button-primary { background: #f97316 !important; border: none !important; cursor: pointer !important; }
1109
+ .gr-button-secondary { cursor: pointer !important; }
1110
+ footer { display: none !important; }
1111
+
1112
+ /* Курсори */
1113
+ .gr-dropdown, .gr-button, .gr-slider, .gr-radio, .gr-checkbox,
1114
+ .tab-nav button, .gr-accordion, .gr-dataset, .gr-dropdown * {
1115
+ cursor: pointer !important;
1116
+ }
1117
+ .gr-dropdown input, .gr-textbox input, .gr-textarea textarea {
1118
+ cursor: text !important;
1119
+ }
1120
  """
1121
 
1122
+ # ========== MAP HTML ==========
1123
+ MAP_HTML = f"""
1124
+ <div style="background:{'#1e293b'};padding:22px;border-radius:8px;font-family:monospace;font-size:13px;line-height:2.0;color:{'#f1f5f9'}">
1125
+ <span style="color:{'#f97316'};font-size:16px;font-weight:bold">K R&D Lab · S1 Biomedical</span>
1126
+ <span style="color:{'#8e9bae'};font-size:11px;margin-left:12px">Science Sphere — sub-direction 1</span>
1127
+ <br><br>
1128
+ <span style="color:{'#38bdf8'};font-weight:600">S1-A · PHYLO-GENOMICS</span> — What breaks in DNA<br>
1129
+ &nbsp;&nbsp;&nbsp;├─ <b>S1-A·R1a</b> OpenVariant <span style="color:{'#22c55e'}"> AUC=0.939 ✅</span><br>
1130
+ &nbsp;&nbsp;&nbsp;└─ <b>S1-A·R1b</b> Somatic classifier <span style="color:#f59e0b"> 🔶 In progress</span><br><br>
1131
+ <span style="color:{'#38bdf8'};font-weight:600">S1-B · PHYLO-RNA</span> — How to silence it via RNA<br>
1132
+ &nbsp;&nbsp;&nbsp;├─ <b>S1-B·R1a</b> miRNA silencing <span style="color:{'#22c55e'}"> ✅</span><br>
1133
+ &nbsp;&nbsp;&nbsp;├─ <b>S1-B·R2a</b> siRNA synthetic lethal <span style="color:{'#22c55e'}"> ✅</span><br>
1134
+ &nbsp;&nbsp;&nbsp;├─ <b>S1-B·R3a</b> lncRNA-TREM2 ceRNA <span style="color:{'#22c55e'}"> ✅</span><br>
1135
+ &nbsp;&nbsp;&nbsp;└─ <b>S1-B·R3b</b> ASO designer <span style="color:{'#22c55e'}"> ✅</span><br><br>
1136
+ <span style="color:{'#38bdf8'};font-weight:600">S1-C · PHYLO-DRUG</span> — Which molecule treats it<br>
1137
+ &nbsp;&nbsp;&nbsp;���─ <b>S1-C·R1a</b> FGFR3 RNA-directed compounds <span style="color:{'#22c55e'}"> ✅</span><br>
1138
+ &nbsp;&nbsp;&nbsp;├─ <b>S1-C·R1b</b> Synthetic lethal drug mapping <span style="color:#f59e0b"> 🔶</span><br>
1139
+ &nbsp;&nbsp;&nbsp;└─ <b>S1-C·R2a</b> m6A × Ferroptosis × Circadian <span style="color:{'#8e9bae'}"> 🔴 Frontier</span><br><br>
1140
+ <span style="color:{'#38bdf8'};font-weight:600">S1-D · PHYLO-LNP</span> — How to deliver the drug<br>
1141
+ &nbsp;&nbsp;&nbsp;├─ <b>S1-D·R1a</b> LNP corona (serum) <span style="color:{'#22c55e'}"> AUC=0.791 ✅</span><br>
1142
+ &nbsp;&nbsp;&nbsp;├─ <b>S1-D·R2a</b> Flow corona — Vroman effect <span style="color:{'#22c55e'}"> ✅</span><br>
1143
+ &nbsp;&nbsp;&nbsp;├─ <b>S1-D·R3a</b> LNP brain / BBB / ApoE <span style="color:{'#22c55e'}"> ✅</span><br>
1144
+ &nbsp;&nbsp;&nbsp;├─ <b>S1-D·R4a</b> AutoCorona NLP <span style="color:{'#22c55e'}"> F1=0.71 ✅</span><br>
1145
+ &nbsp;&nbsp;&nbsp;└─ <b>S1-D·R5a</b> CSF · Vitreous · Bone Marrow <span style="color:{'#8e9bae'}"> 🔴 0 prior studies</span><br><br>
1146
+ <span style="color:{'#38bdf8'};font-weight:600">S1-E · PHYLO-BIOMARKERS</span> — Detect without biopsy<br>
1147
+ &nbsp;&nbsp;&nbsp;├─ <b>S1-E·R1a</b> Liquid Biopsy classifier <span style="color:{'#22c55e'}"> AUC=0.992* ✅</span><br>
1148
+ &nbsp;&nbsp;&nbsp;└─ <b>S1-E·R1b</b> Protein panel validator <span style="color:#f59e0b"> 🔶</span><br><br>
1149
+ <span style="color:{'#38bdf8'};font-weight:600">S1-F · PHYLO-RARE</span> — Where almost nobody has looked yet<br>
1150
+ &nbsp;&nbsp;&nbsp;├─ <b>S1-F·R1a</b> DIPG toolkit (H3K27M + CSF LNP + Circadian) <span style="color:#f59e0b"> 🔶</span><br>
1151
+ &nbsp;&nbsp;&nbsp;├─ <b>S1-F·R2a</b> UVM toolkit (GNAQ/GNA11 + vitreous + m6A) <span style="color:#f59e0b"> 🔶</span><br>
1152
+ &nbsp;&nbsp;&nbsp;└─ <b>S1-F·R3a</b> pAML toolkit (FLT3-ITD + BM niche + ferroptosis) <span style="color:#f59e0b"> 🔶</span><br><br>
1153
+ <span style="color:{'#38bdf8'};font-weight:600">S1-G · PHYLO-SIM</span> — 3D Models & Simulations<br>
1154
+ &nbsp;&nbsp;&nbsp;├─ <b>Nanoparticle</b> Interactive 3D model <span style="color:{'#22c55e'}"> ✅</span><br>
1155
+ &nbsp;&nbsp;&nbsp;├─ <b>DNA Helix</b> Double helix visualization <span style="color:{'#22c55e'}"> ✅</span><br>
1156
+ &nbsp;&nbsp;&nbsp;└─ <b>Protein Corona</b> Schematic corona <span style="color:{'#22c55e'}"> ✅</span><br><br>
1157
+ <span style="color:{'#8e9bae'};font-size:11px">✅ Active · 🔶 In progress · 🔴 Planned</span>
1158
+ </div>
1159
+ """
1160
 
1161
+ # ========== UI З ДВОМА КОЛОНКАМИ ==========
1162
  def build_app():
1163
  with gr.Blocks(css=CUSTOM_CSS, title="K R&D Lab — Cancer Research Suite") as demo:
1164
  gr.Markdown(
 
1168
  )
1169
 
1170
  with gr.Row():
1171
+ # Основна колонка з вкладками
1172
  with gr.Column(scale=4):
1173
+ with gr.Tabs(elem_classes="tabs-outer") as outer_tabs:
1174
+ # 🗺️ Lab Map
1175
+ with gr.TabItem("🗺️ Lab Map"):
1176
+ gr.HTML(MAP_HTML)
1177
 
 
1178
  # GROUP A — REAL DATA TOOLS
1179
+ with gr.TabItem("🔬 Real Data Tools"):
1180
+ with gr.Tabs(elem_classes="main-tabs") as a_tabs:
1181
+ with gr.TabItem("S1-A·R2a · Gray Zones Explorer"):
1182
+ gr.Markdown("Identify underexplored biological processes...")
 
 
 
 
 
 
1183
  a1_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
1184
  a1_btn = gr.Button("🔍 Explore Gray Zones", variant="primary")
1185
  a1_heatmap = gr.Image(label="Research Coverage Heatmap", type="pil")
1186
  a1_gaps = gr.Markdown(label="Top 5 Research Gaps")
1187
  with gr.Accordion("📖 Learning Mode", open=False):
1188
+ gr.Markdown("**What is a research gray zone?** ...")
 
 
 
 
 
 
 
1189
  a1_btn.click(a1_run, inputs=[a1_cancer], outputs=[a1_heatmap, a1_gaps])
1190
 
1191
+ with gr.TabItem("S1-A·R2b · Understudied Target Finder"):
1192
+ gr.Markdown("Find essential genes with high research gap index...")
 
 
 
 
 
 
 
 
 
 
 
 
1193
  a2_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
1194
  a2_btn = gr.Button("🎯 Find Understudied Targets", variant="primary")
1195
  a2_table = gr.Dataframe(label="Target Gap Table", wrap=True)
1196
  a2_note = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
1197
  a2_btn.click(a2_run, inputs=[a2_cancer], outputs=[a2_table, a2_note])
1198
 
1199
+ with gr.TabItem("S1-A·R1a · Real Variant Lookup"):
1200
+ gr.Markdown("Look up a variant in **ClinVar** and **gnomAD**...")
1201
+ a3_hgvs = gr.Textbox(label="HGVS Notation", placeholder="e.g. NM_007294.4:c.5266dupC")
 
 
 
 
 
 
 
 
1202
  a3_btn = gr.Button("🔎 Look Up Variant", variant="primary")
1203
  a3_result = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
 
 
1204
  a3_btn.click(a3_run, inputs=[a3_hgvs], outputs=[a3_result])
1205
 
1206
+ with gr.TabItem("S1-A·R2c · Literature Gap Finder"):
1207
+ gr.Markdown("Visualize publication trends over 10 years...")
 
 
 
 
1208
  with gr.Row():
1209
  a4_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
1210
+ a4_kw = gr.Textbox(label="Keyword", placeholder="e.g. ferroptosis")
1211
  a4_btn = gr.Button("📊 Analyze Literature Trend", variant="primary")
1212
  a4_chart = gr.Image(label="Papers per Year", type="pil")
1213
  a4_gaps = gr.Markdown()
 
 
 
 
 
 
 
 
 
1214
  a4_btn.click(a4_run, inputs=[a4_cancer, a4_kw], outputs=[a4_chart, a4_gaps])
1215
 
1216
+ with gr.TabItem("S1-A·R2d · Druggable Orphans"):
1217
+ gr.Markdown("Identify essential genes with no approved drug and no trial...")
 
 
 
 
1218
  a5_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
1219
  a5_btn = gr.Button("💊 Find Druggable Orphans", variant="primary")
1220
  a5_table = gr.Dataframe(label="Orphan Target Table", wrap=True)
1221
  a5_note = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
1222
  a5_btn.click(a5_run, inputs=[a5_cancer], outputs=[a5_table, a5_note])
1223
 
1224
+ with gr.TabItem("S1-A·R2e · Research Assistant"):
1225
+ gr.Markdown("RAG-powered research assistant...")
 
 
 
 
 
1226
  try:
1227
  from chatbot import build_chatbot_tab
1228
  build_chatbot_tab()
1229
  except ImportError:
1230
+ gr.Markdown("⚠️ `chatbot.py` not found.")
 
 
 
1231
 
 
1232
  # GROUP B — LEARNING SANDBOX
1233
+ with gr.TabItem("📚 Learning Sandbox"):
1234
+ gr.Markdown("> ⚠️ **ALL TABS IN THIS GROUP USE SIMULATED DATA**")
1235
+ with gr.Tabs(elem_classes="main-tabs") as b_tabs:
1236
+ with gr.TabItem("S1-B·R1a · miRNA Explorer"):
 
 
 
 
 
 
1237
  gr.Markdown(SIMULATED_BANNER)
1238
  b1_gene = gr.Dropdown(["BRCA2", "BRCA1", "TP53"], label="Gene", value="TP53")
1239
  b1_btn = gr.Button("🔬 Explore miRNA Interactions", variant="primary")
1240
  b1_plot = gr.Image(label="miRNA Binding & Expression (⚠️ SIMULATED)", type="pil")
1241
  b1_table = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
1242
  b1_btn.click(b1_run, inputs=[b1_gene], outputs=[b1_plot, b1_table])
1243
 
1244
+ with gr.TabItem("S1-B·R2a · siRNA Targets"):
 
1245
  gr.Markdown(SIMULATED_BANNER)
1246
  b2_cancer = gr.Dropdown(["LUAD", "BRCA", "COAD"], label="Cancer Type", value="LUAD")
1247
  b2_btn = gr.Button("🎯 Simulate siRNA Efficacy", variant="primary")
1248
  b2_plot = gr.Image(label="siRNA Efficacy (⚠️ SIMULATED)", type="pil")
1249
  b2_table = gr.Markdown()
 
 
 
 
 
 
 
 
 
1250
  b2_btn.click(b2_run, inputs=[b2_cancer], outputs=[b2_plot, b2_table])
1251
 
1252
+ with gr.TabItem("S1-D·R1a · LNP Corona"):
 
1253
  gr.Markdown(SIMULATED_BANNER)
1254
  with gr.Row():
1255
+ b3_peg = gr.Slider(0.5, 5.0, value=1.5, step=0.1, label="PEG mol%")
1256
  b3_ion = gr.Slider(10, 60, value=50, step=1, label="Ionizable lipid mol%")
1257
  with gr.Row():
1258
  b3_helper = gr.Slider(5, 30, value=10, step=1, label="Helper lipid mol%")
1259
  b3_chol = gr.Slider(10, 50, value=38, step=1, label="Cholesterol mol%")
1260
  with gr.Row():
1261
  b3_size = gr.Slider(50, 300, value=100, step=5, label="Particle size (nm)")
1262
+ b3_serum = gr.Slider(0, 100, value=10, step=5, label="Serum %")
1263
  b3_btn = gr.Button("🧪 Simulate Corona", variant="primary")
1264
  b3_plot = gr.Image(label="Corona Composition (⚠️ SIMULATED)", type="pil")
1265
  b3_interp = gr.Markdown()
1266
+ b3_btn.click(b3_run, inputs=[b3_peg, b3_ion, b3_helper, b3_chol, b3_size, b3_serum],
1267
+ outputs=[b3_plot, b3_interp])
1268
+
1269
+ with gr.TabItem("S1-D·R2a · Flow Corona"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1270
  gr.Markdown(SIMULATED_BANNER)
1271
  with gr.Row():
1272
  b4_time = gr.Slider(10, 120, value=60, step=5, label="Time range (min)")
 
1278
  b4_btn = gr.Button("🌊 Simulate Vroman Kinetics", variant="primary")
1279
  b4_plot = gr.Image(label="Vroman Effect (⚠️ SIMULATED)", type="pil")
1280
  b4_note = gr.Markdown()
1281
+ b4_btn.click(b4_run, inputs=[b4_time, b4_kon_alb, b4_kon_apoe, b4_koff_alb, b4_koff_apoe],
1282
+ outputs=[b4_plot, b4_note])
1283
+
1284
+ with gr.TabItem("S1-A·R1b · Variant Concepts"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1285
  gr.Markdown(SIMULATED_BANNER)
1286
+ b5_class = gr.Dropdown(list(VARIANT_RULES.keys()), label="ACMG Classification", value="VUS")
 
 
 
 
1287
  b5_btn = gr.Button("📋 Explain Classification", variant="primary")
1288
  b5_result = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
 
 
1289
  b5_btn.click(b5_run, inputs=[b5_class], outputs=[b5_result])
1290
 
 
1291
  # JOURNAL — окрема вкладка
1292
+ with gr.TabItem("📓 Journal"):
 
1293
  gr.Markdown("## Lab Journal — Full History")
1294
  with gr.Row():
1295
  journal_filter = gr.Dropdown(
 
1310
  )
1311
  journal_filter.change(refresh_journal, inputs=[journal_filter], outputs=journal_display)
1312
 
1313
+ # Бічна панель (Quick Note)
1314
  with gr.Column(scale=1, min_width=260):
1315
+ with gr.Group(elem_classes="sidebar-journal"):
1316
+ gr.Markdown("## 📓 Lab Journal")
1317
+ note_category = gr.Dropdown(
1318
+ choices=JOURNAL_CATEGORIES,
1319
+ value="Manual",
1320
+ label="Category"
1321
+ )
1322
+ note_input = gr.Textbox(
1323
+ label="Observation",
1324
+ placeholder="Type your note here...",
1325
+ lines=3
1326
+ )
1327
+ with gr.Row():
1328
+ save_btn = gr.Button("💾 Save Note", size="sm", variant="primary")
1329
+ clear_note_btn = gr.Button("🗑️ Clear", size="sm")
1330
+ save_status = gr.Markdown("")
1331
+
1332
+ def save_note(category, note):
1333
+ if note.strip():
1334
+ journal_log(category, "manual note", note, note)
1335
+ return " Note saved.", ""
1336
+ return "⚠️ Note is empty.", ""
 
 
 
 
 
 
1337
 
1338
+ save_btn.click(save_note, inputs=[note_category, note_input], outputs=[save_status, note_input])
1339
+ clear_note_btn.click(lambda: ("", ""), outputs=[note_input, save_status])
1340
+
1341
+ # Інтеграція з Learning Playground
1342
  with gr.Row():
1343
  gr.Markdown("""
1344
  ---
 
1356
 
1357
  return demo
1358
 
 
1359
  if __name__ == "__main__":
1360
  app = build_app()
1361
  app.launch(share=False)