eacortes commited on
Commit
2ca94d3
·
1 Parent(s): b61cc5f

push new db or demo version

Browse files
requirements.txt CHANGED
@@ -7,4 +7,5 @@ redis-om>=0.2.0
7
  numpy>=1.24.0
8
  python-dotenv>=1.0.0
9
  scikit-learn>=1.3.0
10
- rdkit>=2025.3.2
 
 
7
  numpy>=1.24.0
8
  python-dotenv>=1.0.0
9
  scikit-learn>=1.3.0
10
+ rdkit>=2025.3.2
11
+ pytest
src/app.py CHANGED
@@ -78,7 +78,7 @@ class App:
78
  )
79
  sample_btn.click(
80
  fn=None,
81
- js=f"() => {{window.setJSMESmiles('{row['smiles']}');}}",
82
  )
83
 
84
  @staticmethod
@@ -100,28 +100,31 @@ class App:
100
  def create_gradio_interface(self):
101
  """Create the Gradio interface optimized for JavaScript client usage"""
102
  head_scripts = """
103
- <link rel="preload" href="gradio_api/file=src/static/jsme/jsme.nocache.js" as="script">
 
 
104
  <link rel="preload" href="gradio_api/file=src/static/main.min.js" as="script">
105
- <link rel="preload" href="gradio_api/file=src/static/jsme/4277561D0E87B89F4DFCCC3A712D5B19.cache.js" as="script">
106
- <script src="gradio_api/file=src/static/jsme/jsme.nocache.js" defer></script>
 
107
  <script src="gradio_api/file=src/static/main.min.js" defer></script>
108
  """
109
 
110
  with gr.Blocks(
111
  title="Chem-MRL: Molecular Similarity Search Demo",
112
- theme=gr.themes.Soft(),
113
  head=head_scripts,
114
  ) as demo:
115
  gr.Markdown("""
116
  # 🧪 Chem-MRL: Molecular Similarity Search Demo
117
 
118
- Use the JSME editor to draw a molecule or input a SMILES string.<br/>
119
  The backend encodes the molecule using the Chem-MRL model to produce a vector embedding.<br/>
120
  Similarity search is performed via an HNSW-indexed Redis vector store to retrieve closest matches.
121
  """)
122
  gr.HTML(
123
  """
124
- The Redis database indexes <a href="https://isomerdesign.com/pihkal/home">Isomer Design</a> molecular library.
125
  <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">
126
  <img src="https://mirrors.creativecommons.org/presskit/buttons/80x15/svg/by-nc-sa.svg" alt="License: CC BY-NC-SA 4.0"
127
  style="display:inline; height:15px; vertical-align:middle; margin-left:4px;"/>
@@ -134,20 +137,35 @@ class App:
134
  with gr.Tab("🔬 Molecular Search"), gr.Row():
135
  with gr.Column(scale=1):
136
  gr.Markdown("### Molecule Input")
137
- gr.HTML("<div id='jsme_container'></div>")
 
 
 
 
138
 
139
  smiles_input = gr.Textbox(
140
  label="SMILES String",
141
  placeholder="Draw a molecule above or enter SMILES here (e.g., CCO for ethanol)",
142
  lines=2,
143
  elem_id="smiles_input",
 
 
 
 
 
 
 
 
 
144
  )
145
 
146
  canonical_smiles_output = gr.Textbox(
147
  label="Canonical SMILES",
148
  placeholder="Canonical representation will appear here",
 
149
  interactive=False,
150
  elem_id="canonical_smiles_output",
 
151
  )
152
 
153
  embedding_dimension = gr.Dropdown(
@@ -186,12 +204,11 @@ class App:
186
  elem_id="similar_molecules_output",
187
  )
188
 
189
- molecule_image = gr.Image(label="Similiar Molecules Grid", type="pil")
190
 
191
  with gr.Tab("📊 Sample Molecules"):
192
  gr.Markdown("""
193
- ### Sample Molecules in Database
194
- Click any button below to load the molecule into the JSME editor:
195
  """)
196
 
197
  with gr.Row():
@@ -210,6 +227,12 @@ class App:
210
  api_name="get_canonical_smiles",
211
  )
212
 
 
 
 
 
 
 
213
  search_btn.click(
214
  fn=self.handle_search,
215
  inputs=[smiles_input, embedding_dimension],
@@ -225,7 +248,7 @@ class App:
225
  # Clear UI state
226
  clear_btn.click(
227
  fn=self.clear_all,
228
- js="window.clearJSME",
229
  outputs=[
230
  smiles_input,
231
  canonical_smiles_output,
 
78
  )
79
  sample_btn.click(
80
  fn=None,
81
+ js=f"() => {{window.setCWSmiles('{row['smiles']}');}}",
82
  )
83
 
84
  @staticmethod
 
100
  def create_gradio_interface(self):
101
  """Create the Gradio interface optimized for JavaScript client usage"""
102
  head_scripts = """
103
+ <link rel="preload" href="gradio_api/file=src/static/chemwriter/chemwriter.css" as="style">
104
+ <link rel="preload" href="gradio_api/file=src/static/chemwriter/chemwriter-user.css" as="style">
105
+ <link rel="preload" href="gradio_api/file=src/static/chemwriter/chemwriter.js" as="script">
106
  <link rel="preload" href="gradio_api/file=src/static/main.min.js" as="script">
107
+ <link rel="stylesheet" href="gradio_api/file=src/static/chemwriter/chemwriter.css">
108
+ <link rel="stylesheet" href="gradio_api/file=src/static/chemwriter/chemwriter-user.css">
109
+ <script src="gradio_api/file=src/static/chemwriter/chemwriter.js" defer></script>
110
  <script src="gradio_api/file=src/static/main.min.js" defer></script>
111
  """
112
 
113
  with gr.Blocks(
114
  title="Chem-MRL: Molecular Similarity Search Demo",
115
+ theme=gr.themes.Soft(), # type: ignore
116
  head=head_scripts,
117
  ) as demo:
118
  gr.Markdown("""
119
  # 🧪 Chem-MRL: Molecular Similarity Search Demo
120
 
121
+ Use the ChemWriter editor to draw a molecule or input a SMILES string.<br/>
122
  The backend encodes the molecule using the Chem-MRL model to produce a vector embedding.<br/>
123
  Similarity search is performed via an HNSW-indexed Redis vector store to retrieve closest matches.
124
  """)
125
  gr.HTML(
126
  """
127
+ The Redis database indexes <a href="https://isomerdesign.com/pihkal/home">Isomer Design's</a> molecular library.
128
  <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">
129
  <img src="https://mirrors.creativecommons.org/presskit/buttons/80x15/svg/by-nc-sa.svg" alt="License: CC BY-NC-SA 4.0"
130
  style="display:inline; height:15px; vertical-align:middle; margin-left:4px;"/>
 
137
  with gr.Tab("🔬 Molecular Search"), gr.Row():
138
  with gr.Column(scale=1):
139
  gr.Markdown("### Molecule Input")
140
+ gr.HTML(
141
+ '<div id="editor" class="chemwriter" '
142
+ 'data-chemwriter-ui="editor" '
143
+ 'data-chemwriter-width="100%" data-chemwriter-height="450"></div>'
144
+ )
145
 
146
  smiles_input = gr.Textbox(
147
  label="SMILES String",
148
  placeholder="Draw a molecule above or enter SMILES here (e.g., CCO for ethanol)",
149
  lines=2,
150
  elem_id="smiles_input",
151
+ show_copy_button=True,
152
+ )
153
+
154
+ mol_input = gr.Textbox(
155
+ label="Molecule Input",
156
+ interactive=False,
157
+ elem_id="mol_input",
158
+ show_copy_button=True,
159
+ visible=False,
160
  )
161
 
162
  canonical_smiles_output = gr.Textbox(
163
  label="Canonical SMILES",
164
  placeholder="Canonical representation will appear here",
165
+ lines=2,
166
  interactive=False,
167
  elem_id="canonical_smiles_output",
168
+ show_copy_button=True,
169
  )
170
 
171
  embedding_dimension = gr.Dropdown(
 
204
  elem_id="similar_molecules_output",
205
  )
206
 
207
+ molecule_image = gr.Image(label="Similar Molecules Grid", type="pil")
208
 
209
  with gr.Tab("📊 Sample Molecules"):
210
  gr.Markdown("""
211
+ Click any button below to load the molecule into the ChemWriter editor:
 
212
  """)
213
 
214
  with gr.Row():
 
227
  api_name="get_canonical_smiles",
228
  )
229
 
230
+ mol_input.change(
231
+ fn=self.embedding_service.get_smiles_from_mol_file,
232
+ inputs=[mol_input],
233
+ outputs=[smiles_input],
234
+ )
235
+
236
  search_btn.click(
237
  fn=self.handle_search,
238
  inputs=[smiles_input, embedding_dimension],
 
248
  # Clear UI state
249
  clear_btn.click(
250
  fn=self.clear_all,
251
+ js="window.clearCW",
252
  outputs=[
253
  smiles_input,
254
  canonical_smiles_output,
src/data/README.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Data Directory
2
+
3
+ This directory contains molecular data used by the Chem-MRL demo application.
4
+
5
+ ## Dataset Information
6
+
7
+ ### Isomer Design Dataset
8
+
9
+ The molecular data used in this application is sourced from the **Isomer Design** molecular library.
10
+
11
+ - **Dataset Source**: [Isomer Design](https://isomerdesign.com/pihkal/home)
12
+ - **License**: [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) [![License: CC BY-NC-SA 4.0](https://mirrors.creativecommons.org/presskit/buttons/80x15/svg/by-nc-sa.svg)](https://creativecommons.org/licenses/by-nc-sa/4.0/)
13
+ - **License Type**: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
14
+
15
+ ### License Terms
16
+
17
+ This dataset is licensed under CC BY-NC-SA 4.0, which means:
18
+
19
+ - ✅ **Attribution**: You must give appropriate credit to the original source
20
+ - ❌ **NonCommercial**: You may not use the material for commercial purposes
21
+ - ✅ **ShareAlike**: If you remix, transform, or build upon the material, you must distribute your contributions under the same license
22
+
23
+ ### Usage in This Project
24
+
25
+ The dataset is used to:
26
+ - Populate the Redis vector database with molecular embeddings
27
+ - Provide sample molecules for demonstration purposes
28
+ - Enable similarity search functionality through HNSW indexing
29
+
30
+ ### Data Processing
31
+
32
+ The original SMILES data from Isomer Design has been processed through the following pipeline:
33
+
34
+ 1. **Canonicalization**: SMILES strings were canonicalized using RDKit's implementation to ensure consistent molecular representations
35
+ 2. **Embedding Generation**: Canonical SMILES were processed using the Chem-MRL model to generate molecular embeddings at various dimensions (2, 4, 32, 128, 512, 1024)
36
+ 3. **Vector Storage**: The resulting embeddings are stored in the Redis vector database and indexed using HNSW for efficient similarity search operations
37
+
38
+ ### Citation
39
+
40
+ If you use this data in your research or applications, please cite the original Isomer Design dataset and respect the CC BY-NC-SA 4.0 license terms.
src/service.py CHANGED
@@ -9,6 +9,7 @@ import redis
9
  import torch
10
  from chem_mrl.molecular_fingerprinter import MorganFingerprinter
11
  from dotenv import load_dotenv
 
12
  from redis.commands.search.field import TextField, VectorField
13
  from redis.commands.search.indexDefinition import IndexDefinition, IndexType
14
  from redis.commands.search.query import Query
@@ -29,6 +30,7 @@ def setup_logger(clear_handler=False):
29
  if clear_handler:
30
  for handler in logging.root.handlers[:]:
31
  logging.root.removeHandler(handler) # issue with sentence-transformer's logging handler
 
32
  logging.basicConfig(format="%(asctime)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", level=logging.INFO)
33
  logger = logging.getLogger(__name__)
34
  return logger
@@ -159,7 +161,7 @@ class MolecularEmbeddingService:
159
 
160
  self.redis_client.hset(
161
  key,
162
- mapping=mapping,
163
  )
164
 
165
  except Exception as e:
@@ -209,7 +211,12 @@ class MolecularEmbeddingService:
209
  .dialect(2)
210
  )
211
 
212
- results = self.redis_client.ft(self.index_name).search(query, query_params={"vec": query_vector})
 
 
 
 
 
213
 
214
  neighbors: list[SimilarMolecule] = [
215
  {"smiles": doc.smiles, "name": doc.name, "properties": doc.properties, "score": float(doc.score)}
@@ -222,15 +229,27 @@ class MolecularEmbeddingService:
222
  logger.error(f"Failed to find similar molecules: {e}")
223
  return []
224
 
225
- def get_canonical_smiles(self, smiles: str) -> str:
 
226
  """Convert SMILES to canonical SMILES representation"""
227
  if not smiles or smiles.strip() == "":
228
  return ""
229
 
230
  canonical = MorganFingerprinter.canonicalize_smiles(smiles.strip())
231
- if canonical:
232
- return canonical
233
- return smiles.strip()
 
 
 
 
 
 
 
 
 
 
 
234
 
235
  @staticmethod
236
  def embedding_field_name(dim: int) -> str:
 
9
  import torch
10
  from chem_mrl.molecular_fingerprinter import MorganFingerprinter
11
  from dotenv import load_dotenv
12
+ from rdkit import Chem, RDLogger
13
  from redis.commands.search.field import TextField, VectorField
14
  from redis.commands.search.indexDefinition import IndexDefinition, IndexType
15
  from redis.commands.search.query import Query
 
30
  if clear_handler:
31
  for handler in logging.root.handlers[:]:
32
  logging.root.removeHandler(handler) # issue with sentence-transformer's logging handler
33
+ RDLogger.DisableLog("rdApp.*") # type: ignore - DisableLog is an exported function
34
  logging.basicConfig(format="%(asctime)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", level=logging.INFO)
35
  logger = logging.getLogger(__name__)
36
  return logger
 
161
 
162
  self.redis_client.hset(
163
  key,
164
+ mapping=mapping, # type: ignore
165
  )
166
 
167
  except Exception as e:
 
211
  .dialect(2)
212
  )
213
 
214
+ results = self.redis_client.ft(self.index_name).search(
215
+ query,
216
+ query_params={
217
+ "vec": query_vector, # type: ignore
218
+ },
219
+ )
220
 
221
  neighbors: list[SimilarMolecule] = [
222
  {"smiles": doc.smiles, "name": doc.name, "properties": doc.properties, "score": float(doc.score)}
 
229
  logger.error(f"Failed to find similar molecules: {e}")
230
  return []
231
 
232
+ @staticmethod
233
+ def get_canonical_smiles(smiles: str | None) -> str:
234
  """Convert SMILES to canonical SMILES representation"""
235
  if not smiles or smiles.strip() == "":
236
  return ""
237
 
238
  canonical = MorganFingerprinter.canonicalize_smiles(smiles.strip())
239
+ if canonical is None:
240
+ return smiles.strip()
241
+ return canonical
242
+
243
+ @staticmethod
244
+ def get_smiles_from_mol_file(mol_file: str) -> str:
245
+ """Convert SMILES to canonical SMILES representation"""
246
+ if not mol_file or mol_file.strip() == "":
247
+ return ""
248
+
249
+ mol = Chem.rdmolfiles.MolFromMolBlock(mol_file)
250
+ if mol is None:
251
+ return ""
252
+ return Chem.MolToSmiles(mol, canonical=True)
253
 
254
  @staticmethod
255
  def embedding_field_name(dim: int) -> str:
src/static/chemwriter/chemwriter-style.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "border": 0.35,
3
+ "display-methyl": false,
4
+ "line-color": "#202020",
5
+ "line-width": 0.06,
6
+ "line-offset": 0.19,
7
+ "line-end-padding": 0.12,
8
+ "node-cap-height": 0.38,
9
+ "node-colors": {
10
+ "H": "#D0D0D0",
11
+ "N": "#2060FF",
12
+ "P": "#FF7F00",
13
+ "O": "#EE2010",
14
+ "S": "#FFC70A",
15
+ "F": "#CC0092",
16
+ "Cl": "#1FF01F",
17
+ "Br": "#A62929",
18
+ "I": "#D700D7",
19
+ "D": "#0000FF",
20
+ "Li": "#660000",
21
+ "Be": "#00CC66",
22
+ "B": "#00FF33",
23
+ "Na": "#0000FF",
24
+ "Mg": "#336633",
25
+ "Al": "#999999",
26
+ "Si": "#FF9933",
27
+ "Ca": "#999999",
28
+ "Cr": "#00FF66",
29
+ "Mn": "#999999",
30
+ "Fe": "#FFCC00",
31
+ "Ni": "#993333",
32
+ "Cu": "#CC6600",
33
+ "Zn": "#CC66CC",
34
+ "Ag": "#999999"
35
+ },
36
+ "stereo-width": 0.18,
37
+ "unknown-node-color": "#555555"
38
+ }
src/static/chemwriter/chemwriter-symbols.woff ADDED
Binary file (11.2 kB). View file
 
src/static/chemwriter/chemwriter-user.css ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * ChemWriter (R)
3
+ * Copyright (C) 2007-2019 Metamolecular, LLC
4
+ *
5
+ * Use this stylesheet as a template for customizing Editor
6
+ * and Image appearance.
7
+ *
8
+ * Contact: http://metamolecular.com <[email protected]>
9
+ */
10
+
11
+ /*
12
+ * Main Editor window
13
+ */
14
+ .chemwriter-editor {
15
+ width: 100%;
16
+ height: 100%;
17
+ position: relative;
18
+ border: 2px solid gray;
19
+ border-radius: 5px;
20
+ background-color: #dcdcdc;
21
+ box-sizing: border-box;
22
+ -moz-box-sizing: border-box;
23
+ }
24
+
25
+ /*
26
+ * Editor button
27
+ */
28
+ .chemwriter-button {
29
+ position: relative;
30
+ font-size: 20px;
31
+ border-radius: 3px;
32
+ cursor: default;
33
+ display: inline-block;
34
+ text-align: center;
35
+ color: #444444;
36
+ text-shadow: 0 1px #ffffff;
37
+ }
38
+
39
+ /*
40
+ * Hovering over an enabled button
41
+ */
42
+ .chemwriter-button-enabled:hover {
43
+ background-color: #bbbbbb;
44
+ }
45
+
46
+ /*
47
+ * Disabled Editor button
48
+ */
49
+ .chemwriter-button-disabled {
50
+ color: #b0b0b0;
51
+ }
52
+
53
+ /*
54
+ * Pressed button
55
+ */
56
+ .chemwriter-button-pressed {
57
+ background: #bbbbbb;
58
+ }
59
+
60
+ /*
61
+ * About button
62
+ */
63
+ .chemwriter-button-about {
64
+ position: absolute;
65
+ bottom: 0; left: 0;
66
+ }
67
+
68
+ /*
69
+ * Editor button icon. Font "ChemWriter Symbols" is defined in
70
+ * chemwriter.css. To change button icons, use a different font.
71
+ */
72
+ .chemwriter-icon {
73
+ font-family: "Chemwriter Symbols";
74
+ font-size: 26px;
75
+ }
76
+
77
+ /*
78
+ * The small triangle that appears to the lower-left of button icons.
79
+ */
80
+ .chemwriter-detail-disclosure {
81
+ position: absolute;
82
+ top: 0; right: 0; bottom: 0; left: 0;
83
+ font-family: "Chemwriter Symbols";
84
+ font-size: 30px;
85
+ }
86
+
87
+ /*
88
+ * Use a small triangle shape
89
+ */
90
+ .chemwriter-detail-disclosure:after {
91
+ content: 'y';
92
+ }
93
+
94
+ /*
95
+ * The main structure display area.
96
+ */
97
+ .chemwriter-document-view {
98
+ height: 100%;
99
+ width: 100%;
100
+ }
101
+
102
+ /*
103
+ * Override Gradio color
104
+ */
105
+ .chemwriter-code-editor-front,
106
+ .chemwriter-code-editor-back,
107
+ .chemwriter-text-button,
108
+ .chemwriter-button,
109
+ .chemwriter-icon,
110
+ .chemwriter-select,
111
+ .chemwriter-select select,
112
+ .chemwriter-select option {
113
+ color: #444444 !important;
114
+ }
115
+
116
+ /*
117
+ * RIP Richard. It was a pleasure working with you. /Steve
118
+ */
119
+
120
+ div.chemwriter-editor,
121
+ div.chemwriter-image {
122
+ svg.chemwriter-graphics > g.chemwriter-group:first-child {
123
+ display: none;
124
+ }
125
+ a[href="http://chemwriter.com/plans/"] {
126
+ display: none;
127
+ }
128
+ }
src/static/chemwriter/chemwriter.css ADDED
@@ -0,0 +1,605 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * ChemWriter (R)
3
+ * Copyright (C) 2007-2022 Metamolecular, LLC
4
+ *
5
+ * This source code is the exclusive property of Metamolecular, LLC.
6
+ * Unauthorized duplication in any form without written consent
7
+ * is prohibited.
8
+ *
9
+ * Contact: http://metamolecular.com <[email protected]>
10
+ */
11
+
12
+ @font-face {
13
+ font-family: 'ChemWriter Symbols';
14
+ src: url('chemwriter-symbols.woff') format('woff');
15
+ }
16
+
17
+ .chemwriter {
18
+ position: relative;
19
+ font-family: "Lucida Sans", Arial, sans-serif;
20
+ box-sizing: border-box;
21
+ -moz-box-sizing: border-box;
22
+ text-align: initial;
23
+ }
24
+
25
+ .chemwriter-hide {
26
+ display: none;
27
+ }
28
+
29
+ .chemwriter-fullscreen {
30
+ position: fixed;
31
+ min-width: 100%;
32
+ min-height: 100%;
33
+ top: 0; right: 0; bottom: 0; left: 0;
34
+ overflow: hidden;
35
+ z-index: 1000;
36
+ }
37
+
38
+ .chemwriter-fullscreen .chemwriter-editor {
39
+ border: 0px;
40
+ border-radius: 0;
41
+ }
42
+
43
+ .chemwriter svg {
44
+ overflow: hidden;
45
+ }
46
+
47
+ .chemwriter svg path, .chemwriter svg line {
48
+ stroke-linecap: round;
49
+ }
50
+
51
+ .chemwriter svg path {
52
+ stroke-linejoin: round;
53
+ }
54
+
55
+ .chemwriter ul {
56
+ margin: 0;
57
+ }
58
+
59
+ .chemwriter li {
60
+ line-height: 1em;
61
+ }
62
+
63
+ .chemwriter-default-cursor {
64
+ cursor: default;
65
+ }
66
+
67
+ .chemwriter-move-cursor {
68
+ cursor: move !important;
69
+ }
70
+
71
+ .chemwriter-button {
72
+ position: relative;
73
+ font-size: 20px;
74
+ border-radius: 3px;
75
+ cursor: default;
76
+ display: inline-block;
77
+ text-align: center;
78
+ color: #444444;
79
+ text-shadow: 0 1px #ffffff;
80
+ }
81
+
82
+ .chemwriter-palette .chemwriter-button {
83
+ width: 30px;
84
+ height: 30px;
85
+ margin: 4px 0 0 4px;
86
+ line-height: 30px;
87
+ box-sizing: border-box;
88
+ }
89
+
90
+ .chemwriter-button-enabled:hover {
91
+ background-color: #bbbbbb;
92
+ }
93
+
94
+ .chemwriter-button-disabled {
95
+ color: #b0b0b0;
96
+ }
97
+
98
+ .chemwriter-button-pressed {
99
+ background-color: #bbbbbb;
100
+ }
101
+
102
+ .chemwriter-button-about {
103
+ position: absolute;
104
+ bottom: 0; left: 0;
105
+ }
106
+
107
+ .chemwriter-icon {
108
+ font-family: "Chemwriter Symbols";
109
+ font-size: 25px;
110
+ line-height: 25px;
111
+ margin-top: 3px;
112
+ box-sizing: border-box;
113
+ }
114
+
115
+ .chemwriter-detail-disclosure {
116
+ position: absolute;
117
+ top: 0; right: 0; bottom: 0; left: 0;
118
+ font-family: "Chemwriter Symbols";
119
+ font-size: 30px;
120
+ }
121
+
122
+ .chemwriter-detail-disclosure:after {
123
+ content: 'y';
124
+ }
125
+
126
+ .chemwriter-canvas {
127
+ height: 100%;
128
+ position: relative;
129
+ }
130
+
131
+ .chemwriter-canvas:focus {
132
+ outline:none;
133
+ }
134
+
135
+ .chemwriter-graphics {
136
+ height: 100%;
137
+ width: 100%;
138
+ }
139
+
140
+ .chemwriter-dynamic-palette {
141
+ position: absolute;
142
+ top: 0; right: 0; bottom: 0; left: 0;
143
+ visibility: hidden;
144
+ /* IE 9/10 hack to allow palette dismissal by re-pressing button */
145
+ background-color: rgba(0, 0, 0, 0);
146
+ }
147
+
148
+ .chemwriter-appear {
149
+ visibility: visible;
150
+ }
151
+
152
+ .chemwriter-overlay {
153
+ position: absolute;
154
+ top: 0; right: 0; bottom: 0; left: 0;
155
+ border-radius: 5px;
156
+ visibility: hidden;
157
+ opacity: 0;
158
+ transition-duration: 1s;
159
+ }
160
+
161
+ .chemwriter-overlay-no-transition {
162
+ position: absolute;
163
+ top: 0; right: 0; bottom: 0; left: 0;
164
+ border-radius: 5px;
165
+ display: none;
166
+ }
167
+
168
+ .chemwriter-show {
169
+ visibility: visible;
170
+ opacity: 1;
171
+ transition-duration: 1s;
172
+ }
173
+
174
+ .chemwriter-show-no-transition {
175
+ display: block;
176
+ }
177
+
178
+ .chemwriter-rev {
179
+ margin-bottom: 0.1em;
180
+ }
181
+
182
+ .chemwriter-authors {
183
+ margin: 1em 0 2em 0;
184
+ }
185
+
186
+ .chemwriter-copyright {
187
+ font-size: 10px;
188
+ }
189
+
190
+ .chemwriter-logo {
191
+ width: 75px;
192
+ height: 75px;
193
+ margin: 15px auto 15px auto;
194
+ font-family: "ChemWriter Symbols";
195
+ font-size: 70px;
196
+ }
197
+ .chemwriter-logo:after {
198
+ content: "B";
199
+ }
200
+
201
+ .chemwriter-product-name {
202
+ font-size: 28px;
203
+ margin-bottom: 0.5em;
204
+ }
205
+
206
+ .chemwriter-dialog {
207
+ position: absolute;
208
+ top: 0; right: 0; bottom: 0px; left: 0;
209
+ border-top-left-radius: 5px;
210
+ border-top-right-radius: 5px;
211
+ }
212
+
213
+ .chemwriter-dialog .chemwriter-content {
214
+ position: absolute;
215
+ top: 0; right: 0; bottom: 30px; left: 0;
216
+ }
217
+
218
+ .chemwriter-about-panel {
219
+ position: absolute;
220
+ top: 0; bottom: 0; left: 0;
221
+ width: 200px;
222
+ background-color: #466bb0;
223
+ border-top-left-radius: 5px;
224
+ text-align: center;
225
+ color: white;
226
+ font-size: 12px;
227
+ padding: 0 0.5em 0 0.5em;
228
+ }
229
+
230
+ .chemwriter-about-panel a {
231
+ color: white;
232
+ }
233
+
234
+ .chemwriter-documentation-panel {
235
+ position: absolute;
236
+ top: 20px; bottom: 0; right: 0;
237
+ left: 212px;
238
+ padding: 15px 20px 15px 20px;
239
+ text-align: center;
240
+ font-size: 16px;
241
+ }
242
+
243
+ .chemwriter-documentation-panel a {
244
+ color: white;
245
+ }
246
+
247
+ .chemwriter-list {
248
+ margin-left: 0;
249
+ padding-left: 0;
250
+ list-style: none;
251
+ }
252
+
253
+ .chemwriter-list-item {
254
+ margin-left: 0;
255
+ padding-left: 0;
256
+ margin-bottom: 1em;
257
+ }
258
+
259
+ .chemwriter-super {
260
+ vertical-align: super;
261
+ font-size: 50%;
262
+ }
263
+
264
+ .chemwriter-clipboard-panel {
265
+ position: absolute;
266
+ top: 0; bottom: 0; left: 0; right: 0;
267
+ }
268
+
269
+ .chemwriter-left-panel {
270
+ position: absolute;
271
+ top: 0; bottom: 0; left: 0; width: 70%;
272
+ background-color: green;
273
+ }
274
+
275
+ .chemwriter-right-panel {
276
+ position: absolute;
277
+ top: 0; bottom: 0; right: 0; width: 30%;
278
+ background-color: white;
279
+ }
280
+
281
+ .chemwriter-code-editor {
282
+ position: absolute;
283
+ top: 0; bottom: 0; right: 0; left: 0;
284
+ background-color: black;
285
+ }
286
+
287
+ .chemwriter-code-editor-front {
288
+ position: absolute;
289
+ top: 0; left: 0;
290
+ width: 100%;
291
+ height: 100%;
292
+ font-family: monospace;
293
+ font-size: 12px;
294
+ white-space: pre;
295
+ line-height: 12px;
296
+ background-color: transparent;
297
+ color: #00ff00;
298
+ }
299
+
300
+ .chemwriter-code-editor-front::-moz-selection {
301
+ background-color: rgba(100, 200, 100, 0.5);
302
+ }
303
+
304
+ .chemwriter-code-editor-back {
305
+ position: absolute;
306
+ top: 0; left: 0;
307
+ width: 100%;
308
+ height: 100%;
309
+ font-family: monospace;
310
+ font-size: 12px;
311
+ white-space: pre;
312
+ line-height: 12px;
313
+ background-color: #000000;
314
+ color: #ff0000;
315
+ }
316
+
317
+ .chemwriter-text-area {
318
+ width: 100%;
319
+ height: 100%;
320
+ background-color: transparent;
321
+ font-family: monospace;
322
+ font-size: 12px;
323
+ white-space: pre;
324
+ }
325
+
326
+ .chemwriter-button-row {
327
+ position: absolute;
328
+ right: 0; bottom: 0; left: 0;
329
+ height: 30px;
330
+ text-align: center;
331
+ background-color: #d2d2d2;
332
+ }
333
+
334
+ .chemwriter-text-button {
335
+ padding: 0.1em 0.4em 0.1em 0.4em;
336
+ margin: 7px 1em 7px 0;
337
+ border-radius: 5px;
338
+ display: inline-block;
339
+ cursor: default;
340
+ font-size: 12px;
341
+ border: 1px solid #888888;
342
+ }
343
+
344
+ .chemwriter-editor {
345
+ width: 100%;
346
+ height: 100%;
347
+ position: relative;
348
+ border: 2px solid gray;
349
+ border-radius: 5px;
350
+ background: #dcdcdc;
351
+ box-sizing: border-box;
352
+ -moz-box-sizing: border-box;
353
+ }
354
+
355
+ .chemwriter-element-palette {
356
+ right: 38px; bottom: 0; left: 38px;
357
+ height: 38px;
358
+ }
359
+
360
+ .chemwriter-palette {
361
+ position: absolute;
362
+ }
363
+
364
+ .chemwriter-palette-float {
365
+ position: absolute;
366
+ top: 50px; left: 39px;
367
+ height: 38px;
368
+ border-radius: 0 5px 5px 0;
369
+ padding-right: 4px;
370
+ background-color: #dcdcdc;
371
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.5);
372
+ border-right: 1px solid gray;
373
+ border-bottom: 1px solid gray;
374
+ }
375
+
376
+ .chemwriter-palette-left {
377
+ position: absolute;
378
+ top: 0; bottom: 38px;
379
+ width: 38px;
380
+ }
381
+
382
+ .chemwriter-palette-bottom {
383
+ position: absolute;
384
+ right: 38px; bottom: 0; left: 38px;
385
+ max-height: 38px;
386
+ overflow: hidden;
387
+ }
388
+
389
+ .chemwriter-palette-bottom .chemwriter-button {
390
+ font-weight: bold;
391
+ }
392
+
393
+ .chemwriter-palette-box {
394
+ position: relative;
395
+ display: inline-table;
396
+ }
397
+
398
+ .chemwriter-flex-box {
399
+ display: table-cell;
400
+ height: 38px;
401
+ }
402
+
403
+ .chemwriter-stiff-box {
404
+ display: table-cell;
405
+ height: 38px;
406
+ white-space: nowrap;
407
+ }
408
+
409
+ .chemwriter-element-select {
410
+ display: table;
411
+ white-space: nowrap;
412
+ height: 38px;
413
+ line-height: 38px;
414
+ }
415
+
416
+ .chemwriter-element-select .chemwriter-button {
417
+ margin-right: 10px;
418
+ }
419
+
420
+ .chemwriter-select {
421
+ display: table-cell;
422
+ vertical-align: middle;
423
+ }
424
+
425
+ .chemwriter-select select {
426
+ font-size: 16px;
427
+ }
428
+
429
+ .chemwriter-palette-right {
430
+ position: absolute;
431
+ top: 0; right: 0; bottom: 38px;
432
+ right: 0; bottom: 38px;
433
+ width: 38px;
434
+ }
435
+
436
+ .chemwriter-palette-bottom-right {
437
+ position: absolute;
438
+ bottom: 0; right: 0;
439
+ width: 38px; height: 38px;
440
+ }
441
+
442
+ .chemwriter-canvas {
443
+ position: absolute;
444
+ top: 0;
445
+ right: 38px;
446
+ bottom: 38px;
447
+ left: 38px;
448
+ height: auto;
449
+ background-color: white;
450
+ border-radius: 0 0 5px 5px;
451
+ box-sizing: border-box;
452
+ -moz-box-sizing: border-box;
453
+ border-left: 1px solid #555555;
454
+ border-right: 1px solid #555555;
455
+ border-bottom: 1px solid #555555;
456
+ }
457
+
458
+ .chemwriter-document-view {
459
+ height: 100%;
460
+ width: 100%;
461
+ }
462
+
463
+ .chemwriter-image {
464
+ position: relative;
465
+ height: 100%;
466
+ text-align: center;
467
+ }
468
+
469
+ .chemwriter-error-image {
470
+ font-family: "ChemWriter Symbols";
471
+ position: absolute;
472
+ width: 85px;
473
+ height: 85px;
474
+ background-color: inherit;
475
+ color: #ff0000;
476
+ text-shadow: 1px 1px #202020;
477
+ }
478
+
479
+ .chemwriter-error-image:after {
480
+ vertical-align: middle;
481
+ content: "z";
482
+ }
483
+
484
+ /* Button Icons */
485
+
486
+ .chemwriter-button-move .chemwriter-icon:after {
487
+ content: "a";
488
+ }
489
+
490
+ .chemwriter-button-delete .chemwriter-icon:after {
491
+ content: "b";
492
+ }
493
+
494
+ .chemwriter-button-saturate .chemwriter-icon:after {
495
+ content: "D";
496
+ }
497
+
498
+ .chemwriter-button-single-bond .chemwriter-icon:after {
499
+ content: "c";
500
+ }
501
+
502
+ .chemwriter-button-wedge-bond .chemwriter-icon:after {
503
+ content: "d";
504
+ }
505
+
506
+ .chemwriter-button-hash-bond .chemwriter-icon:after {
507
+ content: "e";
508
+ }
509
+
510
+ .chemwriter-button-wavy-bond .chemwriter-icon:after {
511
+ content: "f";
512
+ }
513
+
514
+ .chemwriter-button-crossed-bond .chemwriter-icon:after {
515
+ content: "g";
516
+ }
517
+
518
+ .chemwriter-button-benzene .chemwriter-icon:after {
519
+ content: "h";
520
+ }
521
+
522
+ .chemwriter-button-cyclohexane .chemwriter-icon:after {
523
+ content: "i";
524
+ }
525
+
526
+ .chemwriter-button-cyclopentane .chemwriter-icon:after {
527
+ content: "j";
528
+ }
529
+
530
+ .chemwriter-button-cyclopropane .chemwriter-icon:after {
531
+ content: "k";
532
+ }
533
+
534
+ .chemwriter-button-cyclobutane .chemwriter-icon:after {
535
+ content: "l";
536
+ }
537
+
538
+ .chemwriter-button-cycloheptane .chemwriter-icon:after {
539
+ content: "A";
540
+ }
541
+
542
+ .chemwriter-button-cyclooctane .chemwriter-icon:after {
543
+ content: "m";
544
+ }
545
+
546
+ .chemwriter-button-increase-charge .chemwriter-icon:after {
547
+ content: "n";
548
+ }
549
+
550
+ .chemwriter-button-decrease-charge .chemwriter-icon:after {
551
+ content: "o";
552
+ }
553
+
554
+ .chemwriter-button-increase-radical .chemwriter-icon:after {
555
+ content: "C";
556
+ }
557
+
558
+ .chemwriter-button-next-isotope .chemwriter-icon:after {
559
+ content: "p";
560
+ }
561
+
562
+ .chemwriter-button-mark-atom .chemwriter-icon:after {
563
+ content: "E";
564
+ }
565
+
566
+ .chemwriter-button-undo .chemwriter-icon:after {
567
+ content: "r";
568
+ }
569
+
570
+ .chemwriter-button-redo .chemwriter-icon:after {
571
+ content: "q";
572
+ }
573
+
574
+ .chemwriter-button-new-document .chemwriter-icon:after {
575
+ content: "s";
576
+ }
577
+
578
+ .chemwriter-button-edit-document .chemwriter-icon:after {
579
+ content: "t";
580
+ }
581
+
582
+ .chemwriter-button-reset-view .chemwriter-icon:after {
583
+ content: "u";
584
+ }
585
+
586
+ .chemwriter-button-about .chemwriter-icon:after {
587
+ content: "v";
588
+ }
589
+
590
+ .chemwriter-button-full-screen .chemwriter-icon:after {
591
+ content: "w";
592
+ }
593
+
594
+ .chemwriter-fullscreen .chemwriter-button-full-screen .chemwriter-icon:after {
595
+ content: "x";
596
+ }
597
+
598
+ /* IE8 and lower only */
599
+ .chemwriter-fallback-content {
600
+ background-position-x: center;
601
+ background-position-y: center;
602
+ background-repeat: no-repeat;
603
+ background-size: cover ;
604
+ background-image: url('data:image/png;base64,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');
605
+ }
src/static/chemwriter/chemwriter.js ADDED
The diff for this file is too large to render. See raw diff
 
src/static/main.js CHANGED
@@ -1,165 +1,71 @@
1
  /**
2
- * @fileoverview JSME (JavaScript Molecule Editor) integration with Gradio interface
3
- * Handles bidirectional synchronization between JSME applet and Gradio textbox
4
  * @author Manny Cortes ('[email protected]')
5
- * @version 0.2.0
6
  */
7
 
8
  // ============================================================================
9
  // GLOBAL VARIABLES
10
  // ============================================================================
11
 
12
- /** @type {Object|null} The JSME applet instance */
13
- let jsmeApplet = null;
14
-
15
- /** @type {string} Last known value of the textbox to prevent infinite loops */
16
- let lastTextboxValue = "";
17
 
18
  // ============================================================================
19
  // CONSTANTS
20
  // ============================================================================
21
 
22
  /** @const {string} Default SMILES for initial molecule (ethanol) */
23
- const DEFAULT_SMILES = "CCO";
24
-
25
- /** @const {string} Container height for JSME applet */
26
- const CONTAINER_HEIGHT = "450px";
27
 
28
  /** @const {string} CSS selector for the Gradio SMILES input element */
29
  const SMILES_INPUT_SELECTOR = "#smiles_input textarea, #smiles_input input";
30
 
 
 
 
31
  /** @const {number} Delay for paste event handling (ms) */
32
  const PASTE_DELAY = 50;
33
 
34
  /** @const {number} Delay for initialization retry (ms) */
35
- const INIT_RETRY_DELAY = 2000;
36
 
37
  /** @const {string[]} Events to trigger for Gradio change detection */
38
- const GRADIO_CHANGE_EVENTS = ["input", "change", "keyup"];
39
 
40
  // ============================================================================
41
  // CORE INITIALIZATION
42
  // ============================================================================
43
 
44
  /**
45
- * Initializes the JSME applet after the library has been loaded
46
- * Sets up the molecular editor with default options and callbacks
47
- * @throws {Error} When JSME initialization fails
48
- */
49
- function initializeJSME() {
50
- try {
51
- console.log("Initializing JSME...");
52
- // https://github.com/jsme-editor/jsme-editor.github.io
53
- // https://jsme-editor.github.io/dist/api_javadoc/index.html
54
- // http://wiki.jmol.org/index.php/Jmol_JavaScript_Object/JME/Options
55
- jsmeApplet = new JSApplet.JSME(
56
- "jsme_container",
57
- getJsmeContainerWidthPx(),
58
- CONTAINER_HEIGHT,
59
- {
60
- options:
61
- "NOcanonize,rButton,zoom,zoomgui,newLook,star,multipart,polarnitro,NOexportInChI,NOexportInChIkey,NOsearchInChIkey,NOexportSVG,NOpaste",
62
- }
63
- );
64
-
65
- jsmeApplet.setCallBack("AfterStructureModified", handleJSMEStructureChange);
66
- jsmeApplet.setMenuScale(getJsmeGuiScale());
67
- jsmeApplet.setUserInterfaceBackgroundColor("#adadad");
68
-
69
- // Set initial molecule and sync state
70
- jsmeApplet.readGenericMolecularInput(DEFAULT_SMILES);
71
- lastTextboxValue = DEFAULT_SMILES;
72
-
73
- setupTextboxEventListeners();
74
- window.addEventListener("resize", handleResize);
75
-
76
- console.log("JSME initialized successfully");
77
- } catch (error) {
78
- console.error("Error initializing JSME:", error);
79
- throw error;
80
- }
81
- }
82
-
83
- /**
84
- * Handles structure changes in the JSME applet
85
- * Converts the structure to SMILES and updates the Gradio textbox
86
- * @param {Event} event - The JSME structure modification event
87
  */
88
- function handleJSMEStructureChange(event) {
89
  try {
90
- const smiles = jsmeApplet.smiles();
91
- updateGradioTextbox(smiles);
 
 
92
  } catch (error) {
93
- console.error("Error getting SMILES from JSME:", error);
94
  }
95
  }
96
 
97
- /**
98
- * Calculates the appropriate GUI scale for the JSME applet based on container width
99
- * Uses breakpoints to determine optimal scaling for different screen sizes
100
- * @returns {number} The scale factor for the JSME GUI (0.88 to 2.0)
101
- */
102
- function getJsmeGuiScale() {
103
- const width = getJsmeContainerWidthNumber();
104
- if (width == null || width <= 0) {
105
- return 1;
106
- }
107
- let menuScale;
108
- if (width > 460) {
109
- menuScale = 1.3;
110
- } else if (width > 420) {
111
- menuScale = 1.1;
112
- } else if (width > 370) {
113
- menuScale = 1.05;
114
- } else if (width > 300) {
115
- menuScale = 0.88;
116
- } else {
117
- menuScale = 2;
118
- }
119
- return menuScale;
120
- }
121
-
122
- /**
123
- * Gets the JSME container width as a CSS-compatible string value
124
- * Returns either a pixel value or percentage based on available width
125
- * @returns {string} Width as "100%" or "{width}px" format
126
- */
127
- function getJsmeContainerWidthPx() {
128
- const parentWidth = getJsmeContainerWidthNumber();
129
- if (parentWidth == null || parentWidth <= 0) {
130
- return "100%";
131
- }
132
- return `${parentWidth}px`;
133
- }
134
-
135
- /**
136
- * Gets the numeric width of the JSME container's parent element
137
- * Used for responsive scaling calculations
138
- * @returns {number|null} Width in pixels, or null if container not found
139
- */
140
- function getJsmeContainerWidthNumber() {
141
- const container = document.getElementById("jsme_container");
142
- return container?.parentNode?.offsetWidth;
143
- }
144
-
145
  // ============================================================================
146
- // GRADIO INTEGRATION
147
  // ============================================================================
148
 
149
  /**
150
- * Updates the Gradio textbox with a SMILES string
151
  * Triggers appropriate events to ensure Gradio detects the change
152
- * @param {string} smiles - The SMILES string to set in the textbox
153
  */
154
- function updateGradioTextbox(smiles) {
155
  try {
156
- const textbox = document.querySelector(SMILES_INPUT_SELECTOR);
157
- if (textbox?.value === smiles) {
158
- return;
159
- }
160
-
161
- textbox.value = smiles;
162
- lastTextboxValue = smiles;
163
 
164
  // Trigger events to ensure Gradio detects the change
165
  GRADIO_CHANGE_EVENTS.forEach((eventType) => {
@@ -167,31 +73,23 @@ function updateGradioTextbox(smiles) {
167
  bubbles: true,
168
  cancelable: true,
169
  });
170
- textbox.dispatchEvent(event);
171
  });
172
  } catch (error) {
173
  console.error("Error updating Gradio textbox:", error);
174
  }
175
  }
176
 
177
- // ============================================================================
178
- // JSME UPDATE FUNCTIONS
179
- // ============================================================================
180
-
181
  /**
182
- * Updates the JSME applet with a SMILES string from the textbox
183
- * @param {string} smiles - The SMILES string to display in JSME
184
  */
185
- function updateJSMEFromTextbox(smiles) {
186
  try {
187
- if (smiles?.trim() !== "") {
188
- jsmeApplet?.readGenericMolecularInput(smiles.trim());
189
- } else {
190
- jsmeApplet?.reset();
191
- }
192
- lastTextboxValue = smiles;
193
  } catch (error) {
194
- console.error("Error updating JSME from textbox:", error);
195
  }
196
  }
197
 
@@ -199,19 +97,19 @@ function updateJSMEFromTextbox(smiles) {
199
  // UI MONITORING
200
  // ============================================================================
201
 
202
- /**
203
- * Finds the textbox element and sets up event listeners
204
- */
205
- function setupTextboxEventListeners() {
206
  const textbox = document.querySelector(SMILES_INPUT_SELECTOR);
207
  if (!textbox) {
208
  return;
209
  }
210
-
211
  textbox.addEventListener("input", handleTextboxChange);
212
  textbox.addEventListener("change", handleTextboxChange);
213
  textbox.addEventListener("paste", handleTextboxPaste);
214
- textbox.addEventListener("keyup", handleTextboxChange);
 
 
 
 
215
  }
216
 
217
  /**
@@ -219,9 +117,7 @@ function setupTextboxEventListeners() {
219
  * @param {Event} event - The change event
220
  */
221
  function handleTextboxChange(event) {
222
- if (event.target.value !== lastTextboxValue) {
223
- updateJSMEFromTextbox(event.target.value);
224
- }
225
  }
226
 
227
  /**
@@ -230,47 +126,29 @@ function handleTextboxChange(event) {
230
  */
231
  function handleTextboxPaste(event) {
232
  setTimeout(() => {
233
- updateJSMEFromTextbox(event.target.value);
234
  }, PASTE_DELAY);
235
  }
236
 
237
- /**
238
- * Handles window resize events and updates JSME applet width
239
- */
240
- function handleResize() {
241
- try {
242
- jsmeApplet?.setMenuScale(getJsmeGuiScale());
243
- jsmeApplet?.setWidth(getJsmeContainerWidthPx());
244
- } catch (error) {
245
- console.error("Error resizing JSME applet:", error);
246
- }
247
- }
248
-
249
  // ============================================================================
250
  // PUBLIC API
251
  // ============================================================================
252
 
253
  /**
254
- * Sets a SMILES string in both JSME and Gradio textbox
255
  * @param {string} smiles - The SMILES string to set
256
- * @returns {string} The SMILES string that was set
257
  * @public
258
  */
259
- window.setJSMESmiles = function (smiles) {
260
- updateJSMEFromTextbox(smiles);
261
- updateGradioTextbox(smiles);
262
- return smiles;
263
  };
264
 
265
  /**
266
- * Clears both JSME and Gradio textbox
267
- * @returns {Array} Array containing cleared state for Gradio components
268
  * @public
269
  */
270
- window.clearJSME = function () {
271
- jsmeApplet?.reset();
272
- updateGradioTextbox("");
273
- return ["", "", [], [], "Cleared - Draw a new molecule or enter SMILES"];
274
  };
275
 
276
  // ============================================================================
@@ -278,30 +156,22 @@ window.clearJSME = function () {
278
  // ============================================================================
279
 
280
  /**
281
- * Checks if JSME library is loaded and initializes JSME applet
282
- * Retries until the library becomes available
283
  */
284
  function initializeWhenReady() {
285
- if (typeof JSApplet !== "undefined" && JSApplet.JSME) {
286
- console.log("JSME library loaded, initializing...");
287
- initializeJSME();
288
- } else {
289
- console.log("JSME library not ready, retrying...");
290
- setTimeout(initializeWhenReady, INIT_RETRY_DELAY);
291
- }
292
- }
293
-
294
- /**
295
- * Starts the initialization process based on document ready state
296
- */
297
- function startInitialization() {
298
- if (document.readyState === "loading") {
299
- document.addEventListener("DOMContentLoaded", () => {
300
- setTimeout(initializeWhenReady, INIT_RETRY_DELAY);
301
- });
302
  } else {
 
303
  setTimeout(initializeWhenReady, INIT_RETRY_DELAY);
304
  }
305
  }
306
 
307
- startInitialization();
 
1
  /**
2
+ * @fileoverview ChemWriter integration with Gradio interface
3
+ * Handles bidirectional synchronization between ChemWriter editor and Gradio textbox
4
  * @author Manny Cortes ('[email protected]')
5
+ * @version 0.3.0
6
  */
7
 
8
  // ============================================================================
9
  // GLOBAL VARIABLES
10
  // ============================================================================
11
 
12
+ /** @type {Object|null} The ChemWriter editor instance */
13
+ let editor = null;
14
+ let chemwriter = null;
 
 
15
 
16
  // ============================================================================
17
  // CONSTANTS
18
  // ============================================================================
19
 
20
  /** @const {string} Default SMILES for initial molecule (ethanol) */
21
+ const DEFAULT_SMILES = "CN(C)CCC1=CNC2=C1C(=CC=C2)OP(=O)(O)O";
 
 
 
22
 
23
  /** @const {string} CSS selector for the Gradio SMILES input element */
24
  const SMILES_INPUT_SELECTOR = "#smiles_input textarea, #smiles_input input";
25
 
26
+ /** @const {string} CSS selector for the Gradio Mol file input element */
27
+ const MOL_INPUT_SELECTOR = "#mol_input textarea, #mol_input input";
28
+
29
  /** @const {number} Delay for paste event handling (ms) */
30
  const PASTE_DELAY = 50;
31
 
32
  /** @const {number} Delay for initialization retry (ms) */
33
+ const INIT_RETRY_DELAY = 250;
34
 
35
  /** @const {string[]} Events to trigger for Gradio change detection */
36
+ const GRADIO_CHANGE_EVENTS = ["input", "change"];
37
 
38
  // ============================================================================
39
  // CORE INITIALIZATION
40
  // ============================================================================
41
 
42
  /**
43
+ * Initializes ChemWriter editor and sets up event handlers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  */
45
+ function initializeChemWriter() {
46
  try {
47
+ setupSmilesTextboxEventListeners();
48
+ setupChemWriterEventListeners();
49
+ editor.setSMILES(DEFAULT_SMILES);
50
+ console.log("ChemWriter initialized successfully");
51
  } catch (error) {
52
+ console.error("Error initializing ChemWriter:", error);
53
  }
54
  }
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  // ============================================================================
57
+ // GRADIO AND CHEMWRITER INTEGRATION
58
  // ============================================================================
59
 
60
  /**
61
+ * Updates the mol_input Gradio textbox with a mol file string
62
  * Triggers appropriate events to ensure Gradio detects the change
 
63
  */
64
+ function updateGradioTextbox() {
65
  try {
66
+ const molTextbox = document.querySelector(MOL_INPUT_SELECTOR);
67
+ const molFile = editor?.getMolfile();
68
+ molTextbox.value = molFile;
 
 
 
 
69
 
70
  // Trigger events to ensure Gradio detects the change
71
  GRADIO_CHANGE_EVENTS.forEach((eventType) => {
 
73
  bubbles: true,
74
  cancelable: true,
75
  });
76
+ molTextbox.dispatchEvent(event);
77
  });
78
  } catch (error) {
79
  console.error("Error updating Gradio textbox:", error);
80
  }
81
  }
82
 
 
 
 
 
83
  /**
84
+ * Updates the ChemWriter editor with a SMILES string from the textbox
85
+ * @param {string} smiles - The SMILES string to display in ChemWriter
86
  */
87
+ function updateChemWriterFromTextbox(smiles) {
88
  try {
89
+ smiles = smiles.trim();
90
+ editor?.setSMILES(smiles);
 
 
 
 
91
  } catch (error) {
92
+ console.error("Error updating ChemWriter from textbox:", error);
93
  }
94
  }
95
 
 
97
  // UI MONITORING
98
  // ============================================================================
99
 
100
+ function setupSmilesTextboxEventListeners() {
 
 
 
101
  const textbox = document.querySelector(SMILES_INPUT_SELECTOR);
102
  if (!textbox) {
103
  return;
104
  }
 
105
  textbox.addEventListener("input", handleTextboxChange);
106
  textbox.addEventListener("change", handleTextboxChange);
107
  textbox.addEventListener("paste", handleTextboxPaste);
108
+ }
109
+
110
+ function setupChemWriterEventListeners() {
111
+ window.addEventListener("resize", () => editor.jd());
112
+ editor.addEventListener('document-edited', updateGradioTextbox);
113
  }
114
 
115
  /**
 
117
  * @param {Event} event - The change event
118
  */
119
  function handleTextboxChange(event) {
120
+ updateChemWriterFromTextbox(event.target.value);
 
 
121
  }
122
 
123
  /**
 
126
  */
127
  function handleTextboxPaste(event) {
128
  setTimeout(() => {
129
+ updateChemWriterFromTextbox(event.target.value);
130
  }, PASTE_DELAY);
131
  }
132
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  // ============================================================================
134
  // PUBLIC API
135
  // ============================================================================
136
 
137
  /**
138
+ * Sets ChemWriter SMILES string
139
  * @param {string} smiles - The SMILES string to set
 
140
  * @public
141
  */
142
+ window.setCWSmiles = function (smiles) {
143
+ updateChemWriterFromTextbox(smiles);
 
 
144
  };
145
 
146
  /**
147
+ * Clears both ChemWriter and Gradio textbox
 
148
  * @public
149
  */
150
+ window.clearCW = function () {
151
+ editor.setMolfile('\nCWRITER06142521562D\nCreated with ChemWriter - https://chemwriter.com\n 0 0 0 0 0 0 0 0 0 0999 V2000\nM END');
 
 
152
  };
153
 
154
  // ============================================================================
 
156
  // ============================================================================
157
 
158
  /**
159
+ * Checks if ChemWriter library is loaded and initializes ChemWriter editor
 
160
  */
161
  function initializeWhenReady() {
162
+ chemwriter = window?.chemwriter;
163
+ // The ChemWriter library normally sets up a window load event listener: window.addEventListener("load", function(){Z.De()}, false)
164
+ // However, due to race conditions, the "load" event listener may not be added or triggered in time for proper initialization.
165
+ // So we call the initialization function directly here.
166
+ chemwriter?.System?.De();
167
+ editor = chemwriter?.components?.editor;
168
+ if (typeof chemwriter?.System?.De !== "undefined" && typeof editor !== "undefined") {
169
+ console.log("ChemWriter library loaded, initializing...");
170
+ chemwriter.System.ready(initializeChemWriter);
 
 
 
 
 
 
 
 
171
  } else {
172
+ console.log("ChemWriter library not ready, retrying...");
173
  setTimeout(initializeWhenReady, INIT_RETRY_DELAY);
174
  }
175
  }
176
 
177
+ initializeWhenReady();
src/static/main.min.js CHANGED
@@ -1 +1 @@
1
- function initializeJSME(){try{jsmeApplet=new JSApplet.JSME("jsme_container",getJsmeContainerWidthPx(),"450px",{options:"NOcanonize,rButton,zoom,zoomgui,newLook,star,multipart,polarnitro,NOexportInChI,NOexportInChIkey,NOsearchInChIkey,NOexportSVG,NOpaste"}),jsmeApplet.setCallBack("AfterStructureModified",handleJSMEStructureChange),jsmeApplet.setMenuScale(getJsmeGuiScale()),jsmeApplet.setUserInterfaceBackgroundColor("#adadad"),jsmeApplet.readGenericMolecularInput("CCO"),lastTextboxValue="CCO",setupTextboxEventListeners(),window.addEventListener("resize",handleResize)}catch(e){throw e}}function handleJSMEStructureChange(){try{updateGradioTextbox(jsmeApplet.smiles())}catch(e){}}function getJsmeGuiScale(){const e=getJsmeContainerWidthNumber();if(null==e||e<=0)return 1;let t;return t=e>460?1.3:e>420?1.1:e>370?1.05:e>300?.88:2,t}function getJsmeContainerWidthPx(){const e=getJsmeContainerWidthNumber();return null==e||e<=0?"100%":`${e}px`}function getJsmeContainerWidthNumber(){const e=document.getElementById("jsme_container");return e?.parentNode?.offsetWidth}function updateGradioTextbox(e){try{const t=document.querySelector(SMILES_INPUT_SELECTOR);if(t?.value===e)return;t.value=e,lastTextboxValue=e,GRADIO_CHANGE_EVENTS.forEach((e=>{const n=new Event(e,{bubbles:!0,cancelable:!0});t.dispatchEvent(n)}))}catch(e){}}function updateJSMEFromTextbox(e){try{""!==e?.trim()?jsmeApplet?.readGenericMolecularInput(e.trim()):jsmeApplet?.reset(),lastTextboxValue=e}catch(e){}}function setupTextboxEventListeners(){const e=document.querySelector(SMILES_INPUT_SELECTOR);e&&(e.addEventListener("input",handleTextboxChange),e.addEventListener("change",handleTextboxChange),e.addEventListener("paste",handleTextboxPaste),e.addEventListener("keyup",handleTextboxChange))}function handleTextboxChange(e){e.target.value!==lastTextboxValue&&updateJSMEFromTextbox(e.target.value)}function handleTextboxPaste(e){setTimeout((()=>{updateJSMEFromTextbox(e.target.value)}),50)}function handleResize(){try{jsmeApplet?.setMenuScale(getJsmeGuiScale()),jsmeApplet?.setWidth(getJsmeContainerWidthPx())}catch(e){}}function initializeWhenReady(){"undefined"!=typeof JSApplet&&JSApplet.JSME?initializeJSME():setTimeout(initializeWhenReady,2e3)}function startInitialization(){"loading"===document.readyState?document.addEventListener("DOMContentLoaded",(()=>{setTimeout(initializeWhenReady,2e3)})):setTimeout(initializeWhenReady,2e3)}let jsmeApplet=null,lastTextboxValue="";const DEFAULT_SMILES="CCO",CONTAINER_HEIGHT="450px",SMILES_INPUT_SELECTOR="#smiles_input textarea, #smiles_input input",PASTE_DELAY=50,INIT_RETRY_DELAY=2e3,GRADIO_CHANGE_EVENTS=["input","change","keyup"];window.setJSMESmiles=function(e){return updateJSMEFromTextbox(e),updateGradioTextbox(e),e},window.clearJSME=function(){return jsmeApplet?.reset(),updateGradioTextbox(""),["","",[],[],"Cleared - Draw a new molecule or enter SMILES"]},startInitialization();
 
1
+ function initializeChemWriter(){try{setupSmilesTextboxEventListeners(),setupChemWriterEventListeners(),editor.setSMILES(DEFAULT_SMILES)}catch(e){}}function updateGradioTextbox(){try{const e=document.querySelector(MOL_INPUT_SELECTOR),t=editor?.getMolfile();e.value=t,GRADIO_CHANGE_EVENTS.forEach((t=>{const i=new Event(t,{bubbles:!0,cancelable:!0});e.dispatchEvent(i)}))}catch(e){}}function updateChemWriterFromTextbox(e){try{e=e.trim(),editor?.setSMILES(e)}catch(e){}}function setupSmilesTextboxEventListeners(){const e=document.querySelector(SMILES_INPUT_SELECTOR);e&&(e.addEventListener("input",handleTextboxChange),e.addEventListener("change",handleTextboxChange),e.addEventListener("paste",handleTextboxPaste))}function setupChemWriterEventListeners(){window.addEventListener("resize",(()=>editor.jd())),editor.addEventListener("document-edited",updateGradioTextbox)}function handleTextboxChange(e){updateChemWriterFromTextbox(e.target.value)}function handleTextboxPaste(e){setTimeout((()=>{updateChemWriterFromTextbox(e.target.value)}),50)}function initializeWhenReady(){chemwriter=window?.chemwriter,chemwriter?.System?.De(),editor=chemwriter?.components?.editor,void 0!==chemwriter?.System?.De&&void 0!==editor?chemwriter.System.ready(initializeChemWriter):setTimeout(initializeWhenReady,250)}let editor=null,chemwriter=null;const DEFAULT_SMILES="CN(C)CCC1=CNC2=C1C(=CC=C2)OP(=O)(O)O",SMILES_INPUT_SELECTOR="#smiles_input textarea, #smiles_input input",MOL_INPUT_SELECTOR="#mol_input textarea, #mol_input input",PASTE_DELAY=50,INIT_RETRY_DELAY=250,GRADIO_CHANGE_EVENTS=["input","change"];window.setCWSmiles=function(e){updateChemWriterFromTextbox(e)},window.clearCW=function(){editor.setMolfile("\nCWRITER06142521562D\nCreated with ChemWriter - https://chemwriter.com\n 0 0 0 0 0 0 0 0 0 0999 V2000\nM END")},initializeWhenReady();