ZhaohanM commited on
Commit
ba4f1b4
Β·
1 Parent(s): 01d9e59

Polish ExplainBind demo UI

Browse files
Files changed (2) hide show
  1. .gitignore +1 -0
  2. app.py +274 -50
.gitignore CHANGED
@@ -2,5 +2,6 @@
2
  .ipynb_checkpoints/
3
  __pycache__/
4
  *.pyc
 
5
  bin/
6
  *.log
 
2
  .ipynb_checkpoints/
3
  __pycache__/
4
  *.pyc
5
+ .venv/
6
  bin/
7
  *.log
app.py CHANGED
@@ -60,7 +60,7 @@ LOSCAZLO_B64 = _load_logo_b64(LOSCAZLO_LOGO)
60
  # UI-visible names (Halogen bonding removed)
61
  INTERACTION_NAMES = [
62
  "Hydrogen bonding",
63
- "Salt Bridging",
64
  "π–π Stacking",
65
  "Cation–π",
66
  "Hydrophobic",
@@ -76,9 +76,26 @@ N_VISIBLE_SPEC = len(VISIBLE2UNDERLYING) # 6
76
  # ───── Helper utilities ───────────────────────────────────────────
77
  three2one = {k.upper(): v for k, v in IUPACData.protein_letters_3to1.items()}
78
  three2one.update({"MSE": "M", "SEC": "C", "PYL": "K"})
 
 
 
 
 
 
 
79
  STANDARD_AA_SET = set("ACDEFGHIKLMNPQRSTVWY") # Uppercase FASTA amino acids
80
 
81
 
 
 
 
 
 
 
 
 
 
 
82
  def simple_seq_from_structure(path: str) -> str:
83
  """Extract the longest chain and return standard 1-letter amino acid sequence."""
84
  parser = MMCIFParser(QUIET=True) if path.endswith(".cif") else PDBParser(QUIET=True)
@@ -549,7 +566,7 @@ def visualize_attention_and_ranges(
549
  rows.append(
550
  f"<tr>"
551
  f"<td style='border:1px solid #ddd;padding:6px'><strong>Top {rank}</strong></td>"
552
- f"<td style='border:1px solid #ddd;padding:6px'>Protein: <strong>{j+1}:{p_tokens[j]}</strong></td>"
553
  f"<td style='border:1px solid #ddd;padding:6px'>Ligand: <strong>{i+1}:{d_tokens[i]}</strong></td>"
554
  f"<td style='border:1px solid #ddd;padding:6px'>Score: <strong>{val.item():.6f}</strong></td>"
555
  f"</tr>"
@@ -586,16 +603,15 @@ def visualize_attention_and_ranges(
586
  f"<tr>"
587
  f"<td style='border:1px solid #ddd;padding:6px'><strong>Top {rank}</strong></td>"
588
  f"<td style='border:1px solid #ddd;padding:6px'>"
589
- f"Protein residue: <strong>{j+1}:{p_tokens[j]}</strong>"
590
  f"</td>"
591
  f"<td style='border:1px solid #ddd;padding:6px'>"
592
- f"Aggregated Score: <strong>{val.item():.6f}</strong>"
593
  f"</td>"
594
  f"</tr>"
595
  )
596
 
597
  ranges_html = (
598
- "<h4 style='margin:12px 0 6px'>Top-K Residues (ranked by aggregated attention)</h4>"
599
  "<table style='border-collapse:collapse;margin:6px 0 16px;width:100%'>"
600
  "<thead><tr style='background:#f5f5f5'>"
601
  "<th style='border:1px solid #ddd;padding:6px'>Rank</th>"
@@ -737,6 +753,10 @@ def visualize_attention_and_ranges(
737
  return prob_html, ranges_html, heat_html
738
 
739
 
 
 
 
 
740
 
741
 
742
  # ───── Gradio callbacks ─────────────────────────────────────────
@@ -834,10 +854,10 @@ def inference_cb(prot_seq, drug_seq, head_choice, topk_choice):
834
  # Input validation
835
  # ------------------------------
836
  if not prot_seq or not prot_seq.strip():
837
- return "<p style='color:red'>Please extract or enter a protein sequence first.</p>"
838
 
839
  if not drug_seq or not drug_seq.strip():
840
- return "<p style='color:red'>Please enter a ligand sequence (SELFIES or SMILES).</p>"
841
 
842
  prot_seq = prot_seq.strip()
843
  drug_seq_in = drug_seq.strip()
@@ -851,10 +871,8 @@ def inference_cb(prot_seq, drug_seq, head_choice, topk_choice):
851
  if ltype == "smiles":
852
  conv = smiles_to_selfies(drug_seq_in)
853
  if conv is None:
854
- return (
855
- "<p style='color:red'>SMILES→SELFIES conversion failed. "
856
- "The SMILES appears invalid.</p>",
857
- "",
858
  )
859
  drug_seq_for_tokenizer = conv
860
  else:
@@ -962,54 +980,159 @@ def inference_cb(prot_seq, drug_seq, head_choice, topk_choice):
962
  raw_selfies=raw_selfies,
963
  )
964
 
965
- full_html = prob_html + table_html + heat_html # βœ… εΌΊεˆΆδΈŠδΈ‹ι‘ΊεΊ
966
- return full_html
967
 
968
  def clear_cb():
969
- return "", "", "", None, ""
970
- # protein_seq, drug_seq, output_full, structure_file, status_box
971
 
972
 
973
  # ───── Gradio interface definition ───────────────────────────────
974
  css = """
975
  :root{
976
- --bg:#f8fafc; --card:#f8fafc; --text:#0f172a;
977
- --muted:#6b7280; --border:#e5e7eb; --shadow:0 6px 24px rgba(2,6,23,.06);
978
- --radius:14px; --icon-size:20px;
979
  }
980
 
981
  *{box-sizing:border-box}
982
- html,body{background:#fff!important;color:var(--text)!important}
983
- .gradio-container{max-width:1120px;margin:0 auto}
984
 
985
  /* Title and subtitle */
986
  h1{
 
 
 
987
  font-family:Inter,ui-sans-serif;letter-spacing:.2px;font-weight:700;
988
- font-size:32px;margin:22px 0 12px;text-align:center
 
 
 
 
 
 
 
989
  }
990
  .subtle{color:var(--muted);font-size:14px;text-align:center;margin:-6px 0 18px}
991
 
992
  /* Card style */
993
  .card{
994
- background:var(--card); border:1px solid var(--border); border-radius:var(--radius);
995
- box-shadow:var(--shadow); padding:22px;
 
 
 
 
996
  }
 
 
 
 
997
 
998
  /* Top links */
999
  .link-row{display:flex;justify-content:center;gap:14px;margin:0 auto 18px;flex-wrap:wrap}
1000
 
1001
- /* Two-column grid: left=input, right=controls */
1002
- .grid-2{display:grid;grid-template-columns:1.4fr .9fr;gap:16px}
1003
  .grid-2 .col{display:flex;flex-direction:column;gap:12px}
1004
 
1005
  /* Buttons */
1006
  .gr-button{border-radius:12px !important;font-weight:700 !important;letter-spacing:.2px}
1007
  #extract-btn{background:linear-gradient(90deg,#EFAFB2,#EFAFB2); color:#0f172a}
1008
- #inference-btn{background:linear-gradient(90deg,#B2CBDF,#B2CBDF); color:#0f172a}
1009
- #clear-btn{background:#FFE2B5; color:#0A0A0A; border:1px solid var(--border)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1010
 
1011
  /* Result spacing */
1012
  #result-table{margin-bottom:16px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1013
 
1014
  /* Figure container */
1015
  .figure-wrap{border:1px solid var(--border);border-radius:12px;overflow:hidden;box-shadow:var(--shadow)}
@@ -1095,9 +1218,97 @@ h1{
1095
  .project-links{
1096
  display:flex !important;
1097
  justify-content:center !important;
1098
- gap:28px !important;
1099
  flex-wrap:wrap !important;
1100
- margin-bottom:32px !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1101
  }
1102
 
1103
  #example-btn {
@@ -1111,13 +1322,13 @@ h1{
1111
  }
1112
 
1113
  #extract-sa-btn{
1114
- background:#EADCF8 !important;
1115
  color:#0f172a !important;
1116
  }
1117
 
1118
 
1119
  """
1120
- with gr.Blocks() as demo:
1121
 
1122
  gr.Markdown("<h1>ExplainBind: Token-level Protein–Ligand Interaction Visualiser</h1>")
1123
 
@@ -1163,10 +1374,13 @@ with gr.Blocks() as demo:
1163
  # ───────────────────────────────
1164
  # Guidelines
1165
  # ───────────────────────────────
1166
- with gr.Accordion("Guidelines for Users", open=True, elem_classes=["card"]):
1167
- gr.HTML("""
1168
- <ol style="font-size:1rem;line-height:1.6;margin-left:22px;">
1169
- <li>
 
 
 
1170
  <strong>Input formats:</strong>
1171
  The system supports either <em>structure-aware (SA)</em> sequences derived from
1172
  protein structures or conventional <em>FASTA</em> sequences.
@@ -1174,23 +1388,27 @@ with gr.Blocks() as demo:
1174
  <code>.cif</code> files to extract the corresponding sequence representation.
1175
  Ligands can be provided in <em>SMILES</em> or <em>SELFIES</em> format.
1176
  </li>
1177
-
1178
- <li>
 
1179
  <strong>Interaction channel selection:</strong>
1180
  Users may select a specific non-covalent interaction type
1181
  (e.g., hydrogen bonding, hydrophobic interactions) or the
1182
  overall interaction channel to visualise the corresponding
1183
  token-level binding patterns.
1184
  </li>
1185
-
1186
- <li>
 
1187
  <strong>Model outputs:</strong>
1188
  The system reports (i) a predicted binding probability for the
1189
- protein–ligand pair, (ii) a ranked Top-K residue table, and (iii) a token-level interaction
1190
  heat map illustrating spatial interaction patterns.
1191
  </li>
1192
  </ol>
1193
- """)
 
 
1194
 
1195
 
1196
  # ───────────────────────────────
@@ -1226,7 +1444,7 @@ with gr.Blocks() as demo:
1226
  render=False,
1227
  )
1228
 
1229
- with gr.Group():
1230
 
1231
  gr.Markdown("### Example")
1232
 
@@ -1273,8 +1491,9 @@ with gr.Blocks() as demo:
1273
  top_k_dd = gr.Dropdown(
1274
  label="Top-K residue",
1275
  choices=[str(i) for i in range(1, 21)],
1276
- value="1",
1277
- interactive=True,
 
1278
  )
1279
  with gr.Row():
1280
  btn_infer = gr.Button(
@@ -1292,7 +1511,12 @@ with gr.Blocks() as demo:
1292
  # ───────────────────────────────
1293
  with gr.Column(elem_classes=["card"]):
1294
  status_box = gr.HTML(elem_id="status-box")
1295
- output_full = gr.HTML(elem_id="result-full")
 
 
 
 
 
1296
 
1297
 
1298
  # ───────────────────────────────
@@ -1328,7 +1552,7 @@ with gr.Blocks() as demo:
1328
  btn_infer.click(
1329
  fn=inference_cb,
1330
  inputs=[protein_seq, drug_seq, head_dd, top_k_dd],
1331
- outputs=[output_full],
1332
  )
1333
 
1334
  clear_btn.click(
@@ -1337,7 +1561,9 @@ with gr.Blocks() as demo:
1337
  outputs=[
1338
  protein_seq,
1339
  drug_seq,
1340
- output_full,
 
 
1341
  structure_file,
1342
  status_box,
1343
  ],
@@ -1346,7 +1572,5 @@ with gr.Blocks() as demo:
1346
 
1347
 
1348
  demo.launch(
1349
- theme=gr.themes.Default(),
1350
- css=css,
1351
  show_error=True
1352
- )
 
60
  # UI-visible names (Halogen bonding removed)
61
  INTERACTION_NAMES = [
62
  "Hydrogen bonding",
63
+ "Salt Bridges",
64
  "π–π Stacking",
65
  "Cation–π",
66
  "Hydrophobic",
 
76
  # ───── Helper utilities ───────────────────────────────────────────
77
  three2one = {k.upper(): v for k, v in IUPACData.protein_letters_3to1.items()}
78
  three2one.update({"MSE": "M", "SEC": "C", "PYL": "K"})
79
+ one2three = {
80
+ "A": "ALA", "R": "ARG", "N": "ASN", "D": "ASP", "C": "CYS",
81
+ "Q": "GLN", "E": "GLU", "G": "GLY", "H": "HIS", "I": "ILE",
82
+ "L": "LEU", "K": "LYS", "M": "MET", "F": "PHE", "P": "PRO",
83
+ "S": "SER", "T": "THR", "W": "TRP", "Y": "TYR", "V": "VAL",
84
+ "U": "SEC", "O": "PYL", "B": "ASX", "Z": "GLX", "X": "UNK",
85
+ }
86
  STANDARD_AA_SET = set("ACDEFGHIKLMNPQRSTVWY") # Uppercase FASTA amino acids
87
 
88
 
89
+ def format_residue_label(token: str, position: int) -> str:
90
+ """Format tokenizer residue tokens as three-letter amino-acid labels."""
91
+ clean_token = (token or "").replace("▁", "").strip()
92
+ residue = clean_token[:1].upper()
93
+ residue_3 = one2three.get(residue)
94
+ if residue_3:
95
+ return f"{residue_3} ({position})"
96
+ return f"{clean_token or '?'} ({position})"
97
+
98
+
99
  def simple_seq_from_structure(path: str) -> str:
100
  """Extract the longest chain and return standard 1-letter amino acid sequence."""
101
  parser = MMCIFParser(QUIET=True) if path.endswith(".cif") else PDBParser(QUIET=True)
 
566
  rows.append(
567
  f"<tr>"
568
  f"<td style='border:1px solid #ddd;padding:6px'><strong>Top {rank}</strong></td>"
569
+ f"<td style='border:1px solid #ddd;padding:6px'>Protein: <strong>{format_residue_label(p_tokens[j], j+1)}</strong></td>"
570
  f"<td style='border:1px solid #ddd;padding:6px'>Ligand: <strong>{i+1}:{d_tokens[i]}</strong></td>"
571
  f"<td style='border:1px solid #ddd;padding:6px'>Score: <strong>{val.item():.6f}</strong></td>"
572
  f"</tr>"
 
603
  f"<tr>"
604
  f"<td style='border:1px solid #ddd;padding:6px'><strong>Top {rank}</strong></td>"
605
  f"<td style='border:1px solid #ddd;padding:6px'>"
606
+ f"<strong>{format_residue_label(p_tokens[j], j+1)}</strong>"
607
  f"</td>"
608
  f"<td style='border:1px solid #ddd;padding:6px'>"
609
+ f"<strong>{val.item():.6f}</strong>"
610
  f"</td>"
611
  f"</tr>"
612
  )
613
 
614
  ranges_html = (
 
615
  "<table style='border-collapse:collapse;margin:6px 0 16px;width:100%'>"
616
  "<thead><tr style='background:#f5f5f5'>"
617
  "<th style='border:1px solid #ddd;padding:6px'>Rank</th>"
 
753
  return prob_html, ranges_html, heat_html
754
 
755
 
756
+ def error_outputs(message: str):
757
+ return f"<p style='color:red'>{message}</p>", "", ""
758
+
759
+
760
 
761
 
762
  # ───── Gradio callbacks ─────────────────────────────────────────
 
854
  # Input validation
855
  # ------------------------------
856
  if not prot_seq or not prot_seq.strip():
857
+ return error_outputs("Please extract or enter a protein sequence first.")
858
 
859
  if not drug_seq or not drug_seq.strip():
860
+ return error_outputs("Please enter a ligand sequence (SELFIES or SMILES).")
861
 
862
  prot_seq = prot_seq.strip()
863
  drug_seq_in = drug_seq.strip()
 
871
  if ltype == "smiles":
872
  conv = smiles_to_selfies(drug_seq_in)
873
  if conv is None:
874
+ return error_outputs(
875
+ "SMILES→SELFIES conversion failed. The SMILES appears invalid."
 
 
876
  )
877
  drug_seq_for_tokenizer = conv
878
  else:
 
980
  raw_selfies=raw_selfies,
981
  )
982
 
983
+ return prob_html, table_html, heat_html
 
984
 
985
  def clear_cb():
986
+ return "", "", "", "", "", None, ""
987
+ # protein_seq, drug_seq, binding_probability, binding_locations, interaction_map, structure_file, status_box
988
 
989
 
990
  # ───── Gradio interface definition ───────────────────────────────
991
  css = """
992
  :root{
993
+ --page:#0b1020; --bg:#f8fafc; --card:#ffffff; --text:#0f172a;
994
+ --muted:#475569; --border:#dbe3ee; --shadow:0 16px 44px rgba(2,6,23,.18);
995
+ --radius:14px; --icon-size:20px; --accent:#2563eb; --accent-soft:#eff6ff;
996
  }
997
 
998
  *{box-sizing:border-box}
999
+ html,body{background:var(--page)!important;color:var(--text)!important}
1000
+ .gradio-container{max-width:1120px;margin:0 auto;padding:0 20px 32px}
1001
 
1002
  /* Title and subtitle */
1003
  h1{
1004
+ display:block;
1005
+ width:fit-content;
1006
+ max-width:100%;
1007
  font-family:Inter,ui-sans-serif;letter-spacing:.2px;font-weight:700;
1008
+ font-size:32px;line-height:1.22;margin:24px auto 18px;text-align:center;
1009
+ padding:14px 22px;
1010
+ border:1px solid #dbe3ee;
1011
+ border-radius:16px;
1012
+ background:#ffffff;
1013
+ color:#0f172a!important;
1014
+ box-shadow:0 14px 36px rgba(2,6,23,.16);
1015
+ text-shadow:none;
1016
  }
1017
  .subtle{color:var(--muted);font-size:14px;text-align:center;margin:-6px 0 18px}
1018
 
1019
  /* Card style */
1020
  .card{
1021
+ background:linear-gradient(180deg,#ffffff 0%,#f8fafc 100%)!important;
1022
+ border:1px solid var(--border);
1023
+ border-radius:var(--radius);
1024
+ box-shadow:var(--shadow);
1025
+ padding:22px;
1026
+ color:var(--text)!important;
1027
  }
1028
+ .card label,
1029
+ .card p,
1030
+ .card li,
1031
+ .card strong{color:var(--text)!important}
1032
 
1033
  /* Top links */
1034
  .link-row{display:flex;justify-content:center;gap:14px;margin:0 auto 18px;flex-wrap:wrap}
1035
 
1036
+ /* Input and controls are stacked for better scanning on narrow screens. */
1037
+ .grid-2{display:flex;flex-direction:column;gap:16px}
1038
  .grid-2 .col{display:flex;flex-direction:column;gap:12px}
1039
 
1040
  /* Buttons */
1041
  .gr-button{border-radius:12px !important;font-weight:700 !important;letter-spacing:.2px}
1042
  #extract-btn{background:linear-gradient(90deg,#EFAFB2,#EFAFB2); color:#0f172a}
1043
+ #inference-btn{background:linear-gradient(90deg,#B2CBDF,#B2CBDF)!important; color:#0f172a!important}
1044
+ #clear-btn{background:#FFE2B5!important; color:#0A0A0A!important; border:1px solid var(--border)!important}
1045
+
1046
+ #left,
1047
+ #right{
1048
+ background:#ffffff!important;
1049
+ border:1px solid var(--border);
1050
+ border-radius:12px;
1051
+ padding:14px;
1052
+ }
1053
+
1054
+ #protein-seq textarea,
1055
+ #drug-seq textarea{
1056
+ background:#ffffff!important;
1057
+ color:#0f172a!important;
1058
+ border:1px solid #cbd5e1!important;
1059
+ border-radius:10px!important;
1060
+ }
1061
+
1062
+ #right input,
1063
+ #right button{
1064
+ color:#0f172a!important;
1065
+ }
1066
+
1067
+ #right [role="listbox"],
1068
+ #right [role="option"]{
1069
+ background:#ffffff!important;
1070
+ color:#0f172a!important;
1071
+ border-color:#cbd5e1!important;
1072
+ }
1073
+
1074
+ #right [role="option"]:hover,
1075
+ #right [role="option"][aria-selected="true"]{
1076
+ background:#eff6ff!important;
1077
+ color:#0f172a!important;
1078
+ }
1079
+
1080
+ #structure-file button{
1081
+ background:#ffffff!important;
1082
+ color:#0f172a!important;
1083
+ border:1px dashed #94a3b8!important;
1084
+ }
1085
+
1086
+ #structure-file svg{
1087
+ color:#2563eb!important;
1088
+ }
1089
+
1090
+ #example-panel{
1091
+ background:#f8fafc!important;
1092
+ border:1px solid #dbe3ee!important;
1093
+ border-radius:12px!important;
1094
+ padding:12px!important;
1095
+ }
1096
+
1097
+ #example-panel table,
1098
+ #example-panel iframe,
1099
+ #example-panel .table-wrap{
1100
+ background:#ffffff!important;
1101
+ color:#0f172a!important;
1102
+ }
1103
 
1104
  /* Result spacing */
1105
  #result-table{margin-bottom:16px}
1106
+ .result-heading{
1107
+ display:flex;
1108
+ align-items:center;
1109
+ gap:12px;
1110
+ margin:22px 0 12px;
1111
+ padding:10px 12px;
1112
+ border:1px solid #dbe3ee;
1113
+ border-left:4px solid #2563eb;
1114
+ border-radius:10px;
1115
+ background:#f8fafc;
1116
+ color:#0f172a !important;
1117
+ }
1118
+ .result-index{
1119
+ display:inline-flex;
1120
+ align-items:center;
1121
+ justify-content:center;
1122
+ min-width:34px;
1123
+ height:24px;
1124
+ border-radius:9999px;
1125
+ background:#0f172a;
1126
+ color:#fff !important;
1127
+ font-size:12px;
1128
+ font-weight:700;
1129
+ }
1130
+ .result-title{
1131
+ font-size:18px;
1132
+ font-weight:700;
1133
+ line-height:1.25;
1134
+ color:#0f172a !important;
1135
+ }
1136
 
1137
  /* Figure container */
1138
  .figure-wrap{border:1px solid var(--border);border-radius:12px;overflow:hidden;box-shadow:var(--shadow)}
 
1218
  .project-links{
1219
  display:flex !important;
1220
  justify-content:center !important;
1221
+ gap:14px !important;
1222
  flex-wrap:wrap !important;
1223
+ margin-bottom:24px !important;
1224
+ }
1225
+
1226
+ .guidelines-panel{
1227
+ margin:0 auto 18px;
1228
+ border:1px solid #dbe3ee;
1229
+ border-left:4px solid var(--accent);
1230
+ border-radius:14px;
1231
+ background:linear-gradient(180deg,#ffffff 0%,#f7fbff 100%);
1232
+ box-shadow:var(--shadow);
1233
+ color:#0f172a!important;
1234
+ overflow:hidden;
1235
+ }
1236
+
1237
+ .guidelines-panel summary{
1238
+ display:flex;
1239
+ align-items:center;
1240
+ justify-content:space-between;
1241
+ cursor:pointer;
1242
+ padding:16px 18px;
1243
+ color:#0f172a!important;
1244
+ font-size:18px;
1245
+ font-weight:800;
1246
+ border-bottom:1px solid #e2e8f0;
1247
+ }
1248
+
1249
+ .guidelines-panel summary::-webkit-details-marker{display:none}
1250
+ .guidelines-panel summary::after{
1251
+ content:"v";
1252
+ display:inline-flex;
1253
+ align-items:center;
1254
+ justify-content:center;
1255
+ width:26px;
1256
+ height:26px;
1257
+ border-radius:9999px;
1258
+ background:#eaf2ff;
1259
+ color:#1d4ed8;
1260
+ font-size:12px;
1261
+ font-weight:800;
1262
+ }
1263
+
1264
+ .guidelines-copy{
1265
+ padding:16px 18px 18px;
1266
+ color:#0f172a!important;
1267
+ }
1268
+
1269
+ .guidelines-list{
1270
+ list-style:none;
1271
+ display:grid;
1272
+ gap:12px;
1273
+ margin:0!important;
1274
+ padding:0!important;
1275
+ }
1276
+
1277
+ .guideline-item{
1278
+ position:relative;
1279
+ min-height:72px;
1280
+ padding:12px 14px 12px 48px;
1281
+ border:1px solid #e2e8f0;
1282
+ border-radius:12px;
1283
+ background:#ffffff;
1284
+ color:#0f172a!important;
1285
+ line-height:1.55;
1286
+ }
1287
+
1288
+ .guideline-number{
1289
+ position:absolute;
1290
+ left:14px;
1291
+ top:14px;
1292
+ display:inline-flex;
1293
+ align-items:center;
1294
+ justify-content:center;
1295
+ width:24px;
1296
+ height:24px;
1297
+ border-radius:9999px;
1298
+ background:#0f172a;
1299
+ color:#fff!important;
1300
+ font-size:12px;
1301
+ font-weight:800;
1302
+ }
1303
+
1304
+ .guideline-item strong{color:#0f172a!important}
1305
+ .guideline-item em{color:#1d4ed8!important;font-style:normal;font-weight:650}
1306
+ .guideline-item code{
1307
+ background:#eef2ff;
1308
+ color:#1e3a8a!important;
1309
+ border:1px solid #dbe3ff;
1310
+ border-radius:6px;
1311
+ padding:1px 5px;
1312
  }
1313
 
1314
  #example-btn {
 
1322
  }
1323
 
1324
  #extract-sa-btn{
1325
+ background:#eadcf8 !important;
1326
  color:#0f172a !important;
1327
  }
1328
 
1329
 
1330
  """
1331
+ with gr.Blocks(theme=gr.themes.Default(), css=css) as demo:
1332
 
1333
  gr.Markdown("<h1>ExplainBind: Token-level Protein–Ligand Interaction Visualiser</h1>")
1334
 
 
1374
  # ───────────────────────────────
1375
  # Guidelines
1376
  # ───────────────────────────────
1377
+ gr.HTML("""
1378
+ <details class="guidelines-panel" open>
1379
+ <summary>Guidelines for Users</summary>
1380
+ <div class="guidelines-copy">
1381
+ <ol class="guidelines-list">
1382
+ <li class="guideline-item">
1383
+ <span class="guideline-number">1</span>
1384
  <strong>Input formats:</strong>
1385
  The system supports either <em>structure-aware (SA)</em> sequences derived from
1386
  protein structures or conventional <em>FASTA</em> sequences.
 
1388
  <code>.cif</code> files to extract the corresponding sequence representation.
1389
  Ligands can be provided in <em>SMILES</em> or <em>SELFIES</em> format.
1390
  </li>
1391
+
1392
+ <li class="guideline-item">
1393
+ <span class="guideline-number">2</span>
1394
  <strong>Interaction channel selection:</strong>
1395
  Users may select a specific non-covalent interaction type
1396
  (e.g., hydrogen bonding, hydrophobic interactions) or the
1397
  overall interaction channel to visualise the corresponding
1398
  token-level binding patterns.
1399
  </li>
1400
+
1401
+ <li class="guideline-item">
1402
+ <span class="guideline-number">3</span>
1403
  <strong>Model outputs:</strong>
1404
  The system reports (i) a predicted binding probability for the
1405
+ protein-ligand pair, (ii) a ranked Top-K residue table, and (iii) a token-level interaction
1406
  heat map illustrating spatial interaction patterns.
1407
  </li>
1408
  </ol>
1409
+ </div>
1410
+ </details>
1411
+ """)
1412
 
1413
 
1414
  # ───────────────────────────────
 
1444
  render=False,
1445
  )
1446
 
1447
+ with gr.Group(elem_id="example-panel"):
1448
 
1449
  gr.Markdown("### Example")
1450
 
 
1491
  top_k_dd = gr.Dropdown(
1492
  label="Top-K residue",
1493
  choices=[str(i) for i in range(1, 21)],
1494
+ value="5",
1495
+ interactive=False,
1496
+ visible=False,
1497
  )
1498
  with gr.Row():
1499
  btn_infer = gr.Button(
 
1511
  # ───────────────────────────────
1512
  with gr.Column(elem_classes=["card"]):
1513
  status_box = gr.HTML(elem_id="status-box")
1514
+ gr.HTML("<div class='result-heading'><span class='result-index'>01</span><span class='result-title'>Binding Probability</span></div>")
1515
+ binding_probability = gr.HTML(elem_id="binding-probability")
1516
+ gr.HTML("<div class='result-heading'><span class='result-index'>02</span><span class='result-title'>Binding-site Locations</span></div>")
1517
+ binding_locations = gr.HTML(elem_id="binding-site-locations")
1518
+ gr.HTML("<div class='result-heading'><span class='result-index'>03</span><span class='result-title'>Interaction Map</span></div>")
1519
+ interaction_map = gr.HTML(elem_id="interaction-map")
1520
 
1521
 
1522
  # ───────────────────────────────
 
1552
  btn_infer.click(
1553
  fn=inference_cb,
1554
  inputs=[protein_seq, drug_seq, head_dd, top_k_dd],
1555
+ outputs=[binding_probability, binding_locations, interaction_map],
1556
  )
1557
 
1558
  clear_btn.click(
 
1561
  outputs=[
1562
  protein_seq,
1563
  drug_seq,
1564
+ binding_probability,
1565
+ binding_locations,
1566
+ interaction_map,
1567
  structure_file,
1568
  status_box,
1569
  ],
 
1572
 
1573
 
1574
  demo.launch(
 
 
1575
  show_error=True
1576
+ )