TEZv commited on
Commit
1e6ff6c
·
verified ·
1 Parent(s): d70b294

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +129 -64
app.py CHANGED
@@ -22,6 +22,7 @@ import matplotlib.colors as mcolors
22
  from matplotlib import cm
23
  import io
24
  from PIL import Image
 
25
 
26
  # ─────────────────────────────────────────────
27
  # CACHE SYSTEM (TTL = 24 h)
@@ -50,29 +51,66 @@ def cache_set(endpoint: str, query: str, data):
50
  with open(path, "w") as f:
51
  json.dump(data, f)
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  # ─────────────────────────────────────────────
54
  # LAB JOURNAL
55
  # ─────────────────────────────────────────────
56
  JOURNAL_FILE = "./lab_journal.csv"
 
 
 
 
 
57
 
58
- def journal_log(tab: str, action: str, result: str, note: str = ""):
 
59
  ts = datetime.datetime.utcnow().isoformat()
60
- row = [ts, tab, action, result[:200], note]
61
  write_header = not os.path.exists(JOURNAL_FILE)
62
  with open(JOURNAL_FILE, "a", newline="") as f:
63
  w = csv.writer(f)
64
  if write_header:
65
- w.writerow(["timestamp", "tab", "action", "result_summary", "note"])
66
  w.writerow(row)
67
  return ts
68
 
69
- def journal_read() -> str:
 
70
  if not os.path.exists(JOURNAL_FILE):
71
  return "No entries yet."
72
- df = pd.read_csv(JOURNAL_FILE)
73
- if df.empty:
74
- return "No entries yet."
75
- return df.tail(20).to_markdown(index=False)
 
 
 
 
 
 
 
 
 
 
76
 
77
  # ─────────────────────────────────────────────
78
  # CONSTANTS
@@ -111,29 +149,38 @@ GNOMAD_GQL = "https://gnomad.broadinstitute.org/api"
111
  CT_BASE = "https://clinicaltrials.gov/api/v2"
112
 
113
  # ─────────────────────────────────────────────
114
- # SHARED API HELPERS
115
  # ─────────────────────────────────────────────
116
 
 
117
  def pubmed_count(query: str) -> int:
118
  """Return paper count for a PubMed query (cached)."""
119
  cached = cache_get("pubmed_count", query)
120
  if cached is not None:
121
  return cached
122
  try:
123
- time.sleep(0.34)
124
  r = requests.get(
125
  f"{PUBMED_BASE}/esearch.fcgi",
126
  params={"db": "pubmed", "term": query, "rettype": "count", "retmode": "json"},
127
- timeout=10
128
  )
129
  r.raise_for_status()
130
- count = int(r.json()["esearchresult"]["count"])
 
131
  cache_set("pubmed_count", query, count)
132
  return count
133
- except Exception:
 
 
 
 
 
 
 
134
  return -1
135
 
136
-
137
  def pubmed_search(query: str, retmax: int = 10) -> list:
138
  """Return list of PMIDs (cached)."""
139
  cached = cache_get("pubmed_search", f"{query}_{retmax}")
@@ -144,16 +191,17 @@ def pubmed_search(query: str, retmax: int = 10) -> list:
144
  r = requests.get(
145
  f"{PUBMED_BASE}/esearch.fcgi",
146
  params={"db": "pubmed", "term": query, "retmax": retmax, "retmode": "json"},
147
- timeout=10
148
  )
149
  r.raise_for_status()
150
- ids = r.json()["esearchresult"]["idlist"]
 
151
  cache_set("pubmed_search", f"{query}_{retmax}", ids)
152
  return ids
153
  except Exception:
154
  return []
155
 
156
-
157
  def pubmed_summary(pmids: list) -> list:
158
  """Fetch summaries for a list of PMIDs."""
159
  if not pmids:
@@ -176,7 +224,7 @@ def pubmed_summary(pmids: list) -> list:
176
  except Exception:
177
  return []
178
 
179
-
180
  def ot_query(gql: str, variables: dict = None) -> dict:
181
  """Run an OpenTargets GraphQL query (cached)."""
182
  key = json.dumps({"q": gql, "v": variables}, sort_keys=True)
@@ -191,12 +239,14 @@ def ot_query(gql: str, variables: dict = None) -> dict:
191
  )
192
  r.raise_for_status()
193
  data = r.json()
 
 
 
194
  cache_set("ot_gql", key, data)
195
  return data
196
  except Exception as e:
 
197
  return {"error": str(e)}
198
-
199
-
200
  # ─────────────────────────────────────────────
201
  # TAB A1 — GRAY ZONES EXPLORER
202
  # ─────────────────────────────────────────────
@@ -219,7 +269,8 @@ def a1_run(cancer_type: str):
219
 
220
  fig, ax = plt.subplots(figsize=(6, 8), facecolor="white")
221
  valid = df[cancer_type].fillna(0).values.reshape(-1, 1)
222
- cmap = plt.cm.get_cmap("YlOrRd")
 
223
  cmap.set_bad("white")
224
  masked = np.ma.masked_where(df[cancer_type].isna().values.reshape(-1, 1), valid)
225
  im = ax.imshow(masked, aspect="auto", cmap=cmap, vmin=0)
@@ -270,7 +321,6 @@ def _load_depmap_sample() -> pd.DataFrame:
270
  if "df" in _depmap_cache:
271
  return _depmap_cache["df"]
272
  # Use a curated list of known essential/cancer genes as fallback
273
- # (full DepMap CSV is ~500 MB; we use the public summary endpoint instead)
274
  genes = [
275
  "MYC", "KRAS", "TP53", "EGFR", "PTEN", "RB1", "CDKN2A",
276
  "PIK3CA", "AKT1", "BRAF", "NRAS", "IDH1", "IDH2", "ARID1A",
@@ -309,6 +359,9 @@ def a2_run(cancer_type: str):
309
  }
310
  """
311
  ot_data = ot_query(gql, {"efoId": efo, "size": 40})
 
 
 
312
  rows_ot = []
313
  try:
314
  rows_ot = ot_data["data"]["disease"]["associatedTargets"]["rows"]
@@ -351,8 +404,6 @@ def a2_run(cancer_type: str):
351
  depmap_dict = dict(zip(depmap_df["gene"], depmap_df["gene_effect"]))
352
 
353
  # 5. Build result table
354
- # Research gap index = |essentiality| / log(papers + 1)
355
- # Per know-how: DepMap scores are negative for essential genes
356
  records = []
357
  for gene in genes_ot[:20]:
358
  raw_ess = depmap_dict.get(gene, None)
@@ -362,12 +413,10 @@ def a2_run(cancer_type: str):
362
  ess_display = "N/A"
363
  gap_idx = 0.0
364
  else:
365
- # Invert: positive = more essential (per DepMap know-how: negative raw = essential)
366
  ess_inverted = -raw_ess
367
  ess_display = f"{ess_inverted:.3f}"
368
  papers_safe = max(papers, 0)
369
- # Use log(papers + 2) to guarantee denominator >= log(2) ≈ 0.693
370
- # preventing division-by-near-zero for genes with 0 publications
371
  gap_idx = ess_inverted / math.log(papers_safe + 2) if ess_inverted > 0 else 0.0
372
  records.append({
373
  "Gene": gene,
@@ -386,7 +435,10 @@ def a2_run(cancer_type: str):
386
  f"For real analysis, download `CRISPR_gene_effect.csv` from [depmap.org](https://depmap.org/portal/download/all/) "
387
  f"and replace `_load_depmap_sample()` in `app.py`."
388
  )
389
- journal_log("A2-TargetFinder", f"cancer={cancer_type}", f"top_gap={result_df.iloc[0]['Gene'] if len(result_df) else 'none'}")
 
 
 
390
  return result_df, note
391
 
392
 
@@ -409,7 +461,7 @@ def a3_run(hgvs: str):
409
  try:
410
  time.sleep(0.34)
411
  r = requests.get(
412
- f"{PUBMED_BASE.replace('entrez/eutils','entrez/eutils')}/esearch.fcgi",
413
  params={"db": "clinvar", "term": hgvs, "retmode": "json", "retmax": 5},
414
  timeout=10
415
  )
@@ -425,7 +477,7 @@ def a3_run(hgvs: str):
425
  try:
426
  time.sleep(0.34)
427
  r2 = requests.get(
428
- f"{PUBMED_BASE.replace('entrez/eutils','entrez/eutils')}/esummary.fcgi",
429
  params={"db": "clinvar", "id": ",".join(clinvar_cached[:3]), "retmode": "json"},
430
  timeout=10
431
  )
@@ -458,8 +510,6 @@ def a3_run(hgvs: str):
458
  )
459
 
460
  # ── gnomAD ──
461
- # gnomAD GraphQL expects rsID or gene-level; HGVS lookup is limited
462
- # We attempt a search via the variant endpoint
463
  gnomad_cached = cache_get("gnomad", hgvs)
464
  if gnomad_cached is None:
465
  try:
@@ -514,7 +564,7 @@ def a3_run(hgvs: str):
514
  )
515
 
516
  result_parts.append(f"\n*Source: ClinVar E-utilities + gnomAD GraphQL | Date: {today}*")
517
- journal_log("A3-VariantLookup", f"hgvs={hgvs}", result_parts[0][:100])
518
  return "\n\n".join(result_parts)
519
 
520
 
@@ -618,6 +668,9 @@ def a5_run(cancer_type: str):
618
  }
619
  """
620
  ot_data = ot_query(gql, {"efoId": efo, "size": 50})
 
 
 
621
  rows_ot = []
622
  try:
623
  rows_ot = ot_data["data"]["disease"]["associatedTargets"]["rows"]
@@ -676,8 +729,6 @@ def a5_run(cancer_type: str):
676
  )
677
  journal_log("A5-DruggableOrphans", f"cancer={cancer_type}", f"orphans={len(df)}")
678
  return df, note
679
-
680
-
681
  # ─────────────────────────────────────────────
682
  # GROUP B — LEARNING SANDBOX
683
  # ─────────────────────────────────────────────
@@ -996,23 +1047,6 @@ body { font-family: 'Inter', sans-serif; }
996
  footer { display: none !important; }
997
  """
998
 
999
- def build_journal_sidebar():
1000
- with gr.Column(scale=1, min_width=260):
1001
- gr.Markdown("## 📓 Lab Journal")
1002
- note_input = gr.Textbox(label="Add note", placeholder="Your observation...", lines=2)
1003
- save_btn = gr.Button("💾 Save Note", size="sm")
1004
- refresh_btn = gr.Button("🔄 Refresh Journal", size="sm")
1005
- journal_display = gr.Markdown(value="*Click Refresh to load entries.*")
1006
-
1007
- def save_note(note):
1008
- if note.strip():
1009
- journal_log("Manual", "note", note.strip(), note.strip())
1010
- return journal_read()
1011
-
1012
- save_btn.click(save_note, inputs=[note_input], outputs=[journal_display])
1013
- refresh_btn.click(lambda: journal_read(), outputs=[journal_display])
1014
- return note_input, journal_display
1015
-
1016
 
1017
  def build_app():
1018
  with gr.Blocks(css=CUSTOM_CSS, title="K R&D Lab — Cancer Research Suite") as demo:
@@ -1306,29 +1340,60 @@ def build_app():
1306
  )
1307
  b5_btn.click(b5_run, inputs=[b5_class], outputs=[b5_result])
1308
 
1309
- # ── SIDEBAR ──
1310
  with gr.Column(scale=1, min_width=260):
1311
  gr.Markdown("## 📓 Lab Journal")
1312
- note_input = gr.Textbox(label="Add note", placeholder="Your observation...", lines=2)
1313
- save_btn = gr.Button("💾 Save Note", size="sm")
1314
- refresh_btn = gr.Button("🔄 Refresh Journal", size="sm")
1315
- journal_display = gr.Markdown(value="*Click Refresh to load entries.*")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1316
 
1317
- def save_note(note):
1318
  if note.strip():
1319
- journal_log("Manual", "note", note.strip(), note.strip())
1320
- return journal_read()
 
 
 
 
1321
 
1322
- save_btn.click(save_note, inputs=[note_input], outputs=[journal_display])
1323
- refresh_btn.click(lambda: journal_read(), outputs=[journal_display])
 
 
 
 
 
 
1324
 
1325
  # === Інтеграція з Learning Playground ===
1326
  with gr.Row():
1327
  gr.Markdown("""
 
1328
  ### 🧪 New to the concepts?
1329
  Explore our **[Learning Playground](https://huggingface.co/spaces/K-RnD-Lab/Learning-Playground_03-2026)** with simulated environments.
1330
  """)
1331
-
1332
  gr.Markdown(
1333
  "---\n"
1334
  "*K R&D Lab Cancer Research Suite · "
@@ -1342,4 +1407,4 @@ def build_app():
1342
 
1343
  if __name__ == "__main__":
1344
  app = build_app()
1345
- app.launch(share=False)
 
22
  from matplotlib import cm
23
  import io
24
  from PIL import Image
25
+ from functools import wraps
26
 
27
  # ─────────────────────────────────────────────
28
  # CACHE SYSTEM (TTL = 24 h)
 
51
  with open(path, "w") as f:
52
  json.dump(data, f)
53
 
54
+ # ─────────────────────────────────────────────
55
+ # RETRY DECORATOR
56
+ # ─────────────────────────────────────────────
57
+ def retry(max_attempts=3, delay=1):
58
+ """Decorator to retry a function on exception."""
59
+ def decorator(func):
60
+ @wraps(func)
61
+ def wrapper(*args, **kwargs):
62
+ for attempt in range(max_attempts):
63
+ try:
64
+ return func(*args, **kwargs)
65
+ except Exception as e:
66
+ print(f"Attempt {attempt+1} failed for {func.__name__}: {e}")
67
+ if attempt == max_attempts - 1:
68
+ raise
69
+ time.sleep(delay * (attempt + 1))
70
+ return None
71
+ return wrapper
72
+ return decorator
73
+
74
  # ─────────────────────────────────────────────
75
  # LAB JOURNAL
76
  # ─────────────────────────────────────────────
77
  JOURNAL_FILE = "./lab_journal.csv"
78
+ JOURNAL_CATEGORIES = [
79
+ "A1-GrayZones", "A2-TargetFinder", "A3-VariantLookup", "A4-LitGap", "A5-Orphans", "A6-Chatbot",
80
+ "B1-miRNA", "B2-siRNA", "B3-LNPCorona", "B4-FlowCorona", "B5-VariantConcepts",
81
+ "Manual"
82
+ ]
83
 
84
+ def journal_log(category: str, action: str, result: str, note: str = ""):
85
+ """Log an entry with category."""
86
  ts = datetime.datetime.utcnow().isoformat()
87
+ row = [ts, category, action, result[:200], note]
88
  write_header = not os.path.exists(JOURNAL_FILE)
89
  with open(JOURNAL_FILE, "a", newline="") as f:
90
  w = csv.writer(f)
91
  if write_header:
92
+ w.writerow(["timestamp", "category", "action", "result_summary", "note"])
93
  w.writerow(row)
94
  return ts
95
 
96
+ def journal_read(category: str = "All") -> str:
97
+ """Read journal entries, optionally filtered by category. Returns markdown."""
98
  if not os.path.exists(JOURNAL_FILE):
99
  return "No entries yet."
100
+ try:
101
+ df = pd.read_csv(JOURNAL_FILE)
102
+ if df.empty:
103
+ return "No entries yet."
104
+ if category != "All":
105
+ df = df[df["category"] == category]
106
+ if df.empty:
107
+ return f"No entries for category: {category}"
108
+ # Format for better readability
109
+ df_display = df[["timestamp", "category", "action", "result_summary"]].tail(20)
110
+ return df_display.to_markdown(index=False)
111
+ except Exception as e:
112
+ print(f"Journal read error: {e}")
113
+ return "Error reading journal."
114
 
115
  # ─────────────────────────────────────────────
116
  # CONSTANTS
 
149
  CT_BASE = "https://clinicaltrials.gov/api/v2"
150
 
151
  # ─────────────────────────────────────────────
152
+ # SHARED API HELPERS (with retry)
153
  # ─────────────────────────────────────────────
154
 
155
+ @retry(max_attempts=3, delay=1)
156
  def pubmed_count(query: str) -> int:
157
  """Return paper count for a PubMed query (cached)."""
158
  cached = cache_get("pubmed_count", query)
159
  if cached is not None:
160
  return cached
161
  try:
162
+ time.sleep(0.34) # Rate limit compliance
163
  r = requests.get(
164
  f"{PUBMED_BASE}/esearch.fcgi",
165
  params={"db": "pubmed", "term": query, "rettype": "count", "retmode": "json"},
166
+ timeout=15
167
  )
168
  r.raise_for_status()
169
+ data = r.json()
170
+ count = int(data.get("esearchresult", {}).get("count", 0))
171
  cache_set("pubmed_count", query, count)
172
  return count
173
+ except requests.exceptions.Timeout:
174
+ print(f"Timeout for query: {query}")
175
+ return -1
176
+ except requests.exceptions.RequestException as e:
177
+ print(f"Request error for {query}: {e}")
178
+ return -1
179
+ except (KeyError, ValueError) as e:
180
+ print(f"Parsing error for {query}: {e}")
181
  return -1
182
 
183
+ @retry(max_attempts=3, delay=1)
184
  def pubmed_search(query: str, retmax: int = 10) -> list:
185
  """Return list of PMIDs (cached)."""
186
  cached = cache_get("pubmed_search", f"{query}_{retmax}")
 
191
  r = requests.get(
192
  f"{PUBMED_BASE}/esearch.fcgi",
193
  params={"db": "pubmed", "term": query, "retmax": retmax, "retmode": "json"},
194
+ timeout=15
195
  )
196
  r.raise_for_status()
197
+ data = r.json()
198
+ ids = data.get("esearchresult", {}).get("idlist", [])
199
  cache_set("pubmed_search", f"{query}_{retmax}", ids)
200
  return ids
201
  except Exception:
202
  return []
203
 
204
+ @retry(max_attempts=3, delay=1)
205
  def pubmed_summary(pmids: list) -> list:
206
  """Fetch summaries for a list of PMIDs."""
207
  if not pmids:
 
224
  except Exception:
225
  return []
226
 
227
+ @retry(max_attempts=3, delay=1)
228
  def ot_query(gql: str, variables: dict = None) -> dict:
229
  """Run an OpenTargets GraphQL query (cached)."""
230
  key = json.dumps({"q": gql, "v": variables}, sort_keys=True)
 
239
  )
240
  r.raise_for_status()
241
  data = r.json()
242
+ if "errors" in data:
243
+ print(f"GraphQL errors: {data['errors']}")
244
+ return {"error": data["errors"]}
245
  cache_set("ot_gql", key, data)
246
  return data
247
  except Exception as e:
248
+ print(f"OT query error: {e}")
249
  return {"error": str(e)}
 
 
250
  # ─────────────────────────────────────────────
251
  # TAB A1 — GRAY ZONES EXPLORER
252
  # ─────────────────────────────────────────────
 
269
 
270
  fig, ax = plt.subplots(figsize=(6, 8), facecolor="white")
271
  valid = df[cancer_type].fillna(0).values.reshape(-1, 1)
272
+ # Виправлено deprecated get_cmap
273
+ cmap = plt.colormaps.get_cmap("YlOrRd")
274
  cmap.set_bad("white")
275
  masked = np.ma.masked_where(df[cancer_type].isna().values.reshape(-1, 1), valid)
276
  im = ax.imshow(masked, aspect="auto", cmap=cmap, vmin=0)
 
321
  if "df" in _depmap_cache:
322
  return _depmap_cache["df"]
323
  # Use a curated list of known essential/cancer genes as fallback
 
324
  genes = [
325
  "MYC", "KRAS", "TP53", "EGFR", "PTEN", "RB1", "CDKN2A",
326
  "PIK3CA", "AKT1", "BRAF", "NRAS", "IDH1", "IDH2", "ARID1A",
 
359
  }
360
  """
361
  ot_data = ot_query(gql, {"efoId": efo, "size": 40})
362
+ if "error" in ot_data:
363
+ return None, f"⚠️ OpenTargets API error: {ot_data['error']}\n\n*Source: OpenTargets | Date: {today}*"
364
+
365
  rows_ot = []
366
  try:
367
  rows_ot = ot_data["data"]["disease"]["associatedTargets"]["rows"]
 
404
  depmap_dict = dict(zip(depmap_df["gene"], depmap_df["gene_effect"]))
405
 
406
  # 5. Build result table
 
 
407
  records = []
408
  for gene in genes_ot[:20]:
409
  raw_ess = depmap_dict.get(gene, None)
 
413
  ess_display = "N/A"
414
  gap_idx = 0.0
415
  else:
416
+ # Invert: positive = more essential
417
  ess_inverted = -raw_ess
418
  ess_display = f"{ess_inverted:.3f}"
419
  papers_safe = max(papers, 0)
 
 
420
  gap_idx = ess_inverted / math.log(papers_safe + 2) if ess_inverted > 0 else 0.0
421
  records.append({
422
  "Gene": gene,
 
435
  f"For real analysis, download `CRISPR_gene_effect.csv` from [depmap.org](https://depmap.org/portal/download/all/) "
436
  f"and replace `_load_depmap_sample()` in `app.py`."
437
  )
438
+ if not result_df.empty:
439
+ journal_log("A2-TargetFinder", f"cancer={cancer_type}", f"top_gap={result_df.iloc[0]['Gene']}")
440
+ else:
441
+ journal_log("A2-TargetFinder", f"cancer={cancer_type}", "no targets found")
442
  return result_df, note
443
 
444
 
 
461
  try:
462
  time.sleep(0.34)
463
  r = requests.get(
464
+ f"{PUBMED_BASE}/esearch.fcgi",
465
  params={"db": "clinvar", "term": hgvs, "retmode": "json", "retmax": 5},
466
  timeout=10
467
  )
 
477
  try:
478
  time.sleep(0.34)
479
  r2 = requests.get(
480
+ f"{PUBMED_BASE}/esummary.fcgi",
481
  params={"db": "clinvar", "id": ",".join(clinvar_cached[:3]), "retmode": "json"},
482
  timeout=10
483
  )
 
510
  )
511
 
512
  # ── gnomAD ──
 
 
513
  gnomad_cached = cache_get("gnomad", hgvs)
514
  if gnomad_cached is None:
515
  try:
 
564
  )
565
 
566
  result_parts.append(f"\n*Source: ClinVar E-utilities + gnomAD GraphQL | Date: {today}*")
567
+ journal_log("A3-VariantLookup", f"hgvs={hgvs}", result_parts[0][:100] if result_parts else "no results")
568
  return "\n\n".join(result_parts)
569
 
570
 
 
668
  }
669
  """
670
  ot_data = ot_query(gql, {"efoId": efo, "size": 50})
671
+ if "error" in ot_data:
672
+ return None, f"⚠️ OpenTargets API error: {ot_data['error']}\n\n*Source: OpenTargets | Date: {today}*"
673
+
674
  rows_ot = []
675
  try:
676
  rows_ot = ot_data["data"]["disease"]["associatedTargets"]["rows"]
 
729
  )
730
  journal_log("A5-DruggableOrphans", f"cancer={cancer_type}", f"orphans={len(df)}")
731
  return df, note
 
 
732
  # ─────────────────────────────────────────────
733
  # GROUP B — LEARNING SANDBOX
734
  # ─────────────────────────────────────────────
 
1047
  footer { display: none !important; }
1048
  """
1049
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1050
 
1051
  def build_app():
1052
  with gr.Blocks(css=CUSTOM_CSS, title="K R&D Lab — Cancer Research Suite") as demo:
 
1340
  )
1341
  b5_btn.click(b5_run, inputs=[b5_class], outputs=[b5_result])
1342
 
1343
+ # ── SIDEBAR (JOURNAL) ──
1344
  with gr.Column(scale=1, min_width=260):
1345
  gr.Markdown("## 📓 Lab Journal")
1346
+ note_category = gr.Dropdown(
1347
+ choices=JOURNAL_CATEGORIES,
1348
+ value="Manual",
1349
+ label="Category"
1350
+ )
1351
+ note_input = gr.Textbox(
1352
+ label="Observation",
1353
+ placeholder="Type your note here...",
1354
+ lines=3
1355
+ )
1356
+ with gr.Row():
1357
+ save_btn = gr.Button("💾 Save Note", size="sm", variant="primary")
1358
+ clear_note_btn = gr.Button("🗑️ Clear", size="sm")
1359
+ save_status = gr.Markdown("")
1360
+
1361
+ gr.Markdown("---")
1362
+ gr.Markdown("## 🔍 Full Journal")
1363
+ journal_filter = gr.Dropdown(
1364
+ choices=["All"] + JOURNAL_CATEGORIES,
1365
+ value="All",
1366
+ label="Filter by category"
1367
+ )
1368
+ refresh_btn = gr.Button("🔄 Refresh", size="sm")
1369
+ journal_display = gr.Markdown(value=journal_read())
1370
 
1371
+ def save_note(category, note):
1372
  if note.strip():
1373
+ journal_log(category, "manual note", note, note)
1374
+ return "✅ Note saved.", ""
1375
+ return "⚠️ Note is empty.", ""
1376
+
1377
+ def refresh_journal(category):
1378
+ return journal_read(category)
1379
 
1380
+ save_btn.click(
1381
+ save_note,
1382
+ inputs=[note_category, note_input],
1383
+ outputs=[save_status, note_input]
1384
+ )
1385
+ clear_note_btn.click(lambda: ("", ""), outputs=[note_input, save_status])
1386
+ refresh_btn.click(refresh_journal, inputs=[journal_filter], outputs=journal_display)
1387
+ journal_filter.change(refresh_journal, inputs=[journal_filter], outputs=journal_display)
1388
 
1389
  # === Інтеграція з Learning Playground ===
1390
  with gr.Row():
1391
  gr.Markdown("""
1392
+ ---
1393
  ### 🧪 New to the concepts?
1394
  Explore our **[Learning Playground](https://huggingface.co/spaces/K-RnD-Lab/Learning-Playground_03-2026)** with simulated environments.
1395
  """)
1396
+
1397
  gr.Markdown(
1398
  "---\n"
1399
  "*K R&D Lab Cancer Research Suite · "
 
1407
 
1408
  if __name__ == "__main__":
1409
  app = build_app()
1410
+ app.launch(share=False)