lhasa-ligand-builder 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.eslintrc.cjs +18 -0
  2. package/LICENSE +674 -0
  3. package/README.md +41 -0
  4. package/index.html +49 -0
  5. package/package.json +37 -0
  6. package/public/.gitkeep +0 -0
  7. package/public/Components-inchikey.ich +48167 -0
  8. package/public/icons/README +7 -0
  9. package/public/icons/dark/layla_3c.svg +82 -0
  10. package/public/icons/dark/layla_4c.svg +82 -0
  11. package/public/icons/dark/layla_5c.svg +82 -0
  12. package/public/icons/dark/layla_6arom.svg +89 -0
  13. package/public/icons/dark/layla_6c.svg +82 -0
  14. package/public/icons/dark/layla_7c.svg +82 -0
  15. package/public/icons/dark/layla_8c.svg +82 -0
  16. package/public/icons/dark/layla_charge_tool.svg +78 -0
  17. package/public/icons/dark/layla_delete_hydrogens_tool.svg +384 -0
  18. package/public/icons/dark/layla_double_bond.svg +78 -0
  19. package/public/icons/dark/layla_format_tool.svg +283 -0
  20. package/public/icons/dark/layla_geometry_tool.svg +105 -0
  21. package/public/icons/dark/layla_key.svg +76 -0
  22. package/public/icons/dark/layla_move_tool.svg +110 -0
  23. package/public/icons/dark/layla_single_bond.svg +73 -0
  24. package/public/icons/dark/layla_triple_bond.svg +87 -0
  25. package/public/icons/dark/lhasa_delete_tool.svg +401 -0
  26. package/public/icons/dark/lhasa_flip_x_tool.svg +106 -0
  27. package/public/icons/dark/lhasa_flip_y_tool.svg +106 -0
  28. package/public/icons/dark/lhasa_rotate_tool.svg +112 -0
  29. package/public/icons/icons/hicolor_apps_scalable_coot-layla.svg +105 -0
  30. package/public/icons/layla_3c.svg +82 -0
  31. package/public/icons/layla_4c.svg +82 -0
  32. package/public/icons/layla_5c.svg +82 -0
  33. package/public/icons/layla_6arom.svg +89 -0
  34. package/public/icons/layla_6c.svg +82 -0
  35. package/public/icons/layla_7c.svg +82 -0
  36. package/public/icons/layla_8c.svg +82 -0
  37. package/public/icons/layla_charge_tool.svg +78 -0
  38. package/public/icons/layla_delete_hydrogens_tool.svg +384 -0
  39. package/public/icons/layla_double_bond.svg +78 -0
  40. package/public/icons/layla_format_tool.svg +283 -0
  41. package/public/icons/layla_geometry_tool-dark.svg +105 -0
  42. package/public/icons/layla_geometry_tool.svg +105 -0
  43. package/public/icons/layla_key.svg +76 -0
  44. package/public/icons/layla_move_tool.svg +110 -0
  45. package/public/icons/layla_single_bond.svg +73 -0
  46. package/public/icons/layla_triple_bond.svg +87 -0
  47. package/public/icons/lhasa_delete_tool.svg +401 -0
  48. package/public/icons/lhasa_flip_x_tool.svg +106 -0
  49. package/public/icons/lhasa_flip_y_tool.svg +106 -0
  50. package/public/icons/lhasa_rotate_tool.svg +112 -0
  51. package/public/lhasa.js +2 -0
  52. package/public/lhasa.wasm +0 -0
  53. package/public/react.svg +1 -0
  54. package/src/Lhasa.tsx +1452 -0
  55. package/src/assets/.gitkeep +0 -0
  56. package/src/bansu_integration.tsx +315 -0
  57. package/src/customize_mui.scss +97 -0
  58. package/src/inchikey_database_parse.tsx +20 -0
  59. package/src/index.d.ts +11 -0
  60. package/src/index.scss +352 -0
  61. package/src/main.tsx +79 -0
  62. package/src/qed_property_infobox.tsx +14 -0
  63. package/src/types.d.ts +375 -0
  64. package/src/vite-env.d.ts +1 -0
  65. package/tsconfig.json +25 -0
  66. package/tsconfig.node.json +10 -0
  67. package/vite.config.ts +55 -0
File without changes
@@ -0,0 +1,315 @@
1
+ import { Popover, Button, Tooltip, StyledEngineProvider, AccordionSummary, AccordionDetails, Accordion, Input, Switch, Checkbox, FormControlLabel } from "@mui/material";
2
+ // import Grid from '@mui/material/Grid2';
3
+ import { useCallback, useEffect, useState } from "react";
4
+ // import WebSocket from 'ws';
5
+ // import * as http from 'http';
6
+
7
+ // Do I really need that here?
8
+ import './index.scss';
9
+ import './customize_mui.scss';
10
+
11
+ class BansuPopupProps {
12
+ smiles!: string;
13
+ anchorEl?: HTMLElement | null;
14
+ bansu_endpoint!: string;
15
+ dark_mode!: boolean;
16
+ // internal_id:
17
+ }
18
+
19
+ // Do I want to use an enum?
20
+ // Or do I way to express the state with variables alone?
21
+ enum BansuPopupState {
22
+ UserConfig,
23
+ SpawningJob,
24
+ ConnectingOnWebsocket,
25
+ Queued,
26
+ Waiting,
27
+ Ready,
28
+ Error
29
+ }
30
+
31
+ export function BansuButton(props: BansuPopupProps) {
32
+
33
+ const [popoverOpened, setPopoverOpened] = useState<boolean>(false);
34
+ const [userConsent, setUserConsent] = useState<boolean>(false);
35
+ const [state, setState] = useState<BansuPopupState>(BansuPopupState.UserConfig);
36
+ const [bansuEndpoint, setBansuEndpoint] = useState<string>(props.bansu_endpoint);
37
+ const [jobId, setJobId] = useState<string | null>(null);
38
+ const [posInQueue, setPosInQueue] = useState<number | null>(null);
39
+ const [finishedJobOutput, setFinishedJobOutput] = useState<string | null>(null);
40
+ const [errorString, setErrorString] = useState<string | null>(null);
41
+
42
+ const [workerPromise, setWorkerPromise] = useState<null | Promise<void>>(null);
43
+
44
+ const resetState = () => {
45
+ setState(BansuPopupState.UserConfig);
46
+ }
47
+
48
+ const popoverContent = useCallback(() => {
49
+ if(!popoverOpened) {
50
+ return <></>;
51
+ }
52
+ switch(state) {
53
+ case BansuPopupState.UserConfig:
54
+ return <div className="vertical_panel">
55
+ Bansu is a server-side computational API which enables CIF generation via running Acedrg.
56
+ <div className="warning_box">
57
+ <h2>WARNING!</h2>
58
+ <b>Usage of non-local instances of Bansu implies that your data will travel across the web to a remote webserver.</b><br/>
59
+ <b>Make sure that you're using an HTTPS endpoint</b> for transport security.<br/>
60
+ While Bansu does not store logs containing chemical data, please note that<br/><b>by using a remote instance of Bansu you're trusting the instance's owner with your data.</b><br/>
61
+ <br/>
62
+ <FormControlLabel
63
+ label="I understand and agree to proceed"
64
+ control={
65
+ <Checkbox
66
+ style={{marginLeft: '5px'}}
67
+ checked={userConsent}
68
+ onChange={() => setUserConsent(!userConsent)}
69
+ />}
70
+ />
71
+ </div>
72
+ <b>Bansu job configuration</b>
73
+ <div className="horizontal_container_centered">
74
+ Bansu instance
75
+ <Input defaultValue={bansuEndpoint} placeholder={props.bansu_endpoint} onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
76
+ if(event.target.value !== "") {
77
+ setBansuEndpoint(event.target.value);
78
+ } else {
79
+ setBansuEndpoint(props.bansu_endpoint);
80
+ }
81
+ }}
82
+ />
83
+ </div>
84
+ Commandline flags
85
+ <div className="horizontal_container_centered">
86
+ {/* -z
87
+ <Switch /> */}
88
+ <i>TODO</i>
89
+ </div>
90
+ <div className="horizontal_container_centered children_expanded">
91
+ <Button
92
+ onClick={() => setPopoverOpened(false)}
93
+ variant="contained"
94
+ >
95
+ Cancel
96
+ </Button>
97
+ <Button
98
+ onClick={() => setState(BansuPopupState.SpawningJob)}
99
+ variant="contained"
100
+ disabled={!userConsent}
101
+ >
102
+ Spawn Bansu job
103
+ </Button>
104
+ </div>
105
+ </div>;
106
+ case BansuPopupState.SpawningJob:
107
+ return <div className="vertical_panel">
108
+ Spawning Bansu job...
109
+ <small>Bansu instance <i>{bansuEndpoint}</i></small>
110
+ </div>;
111
+ case BansuPopupState.ConnectingOnWebsocket:
112
+ return <div className="vertical_panel">
113
+ Estabilishing event listener connection...
114
+ <small>Bansu instance <i>{bansuEndpoint}</i></small>
115
+ <small>Job id: {jobId}</small>
116
+ </div>;
117
+ case BansuPopupState.Queued:
118
+ return <div className="vertical_panel">
119
+ Job has been queued.
120
+ Waiting...
121
+ <small>Bansu instance <i>{bansuEndpoint}</i></small>
122
+ <small>Job id: {jobId}</small>
123
+ <small>Position in queue: {posInQueue}</small>
124
+ </div>;
125
+ case BansuPopupState.Waiting:
126
+ return <div className="vertical_panel">
127
+ Waiting for Bansu job to complete...
128
+ <small>Bansu instance <i>{bansuEndpoint}</i></small>
129
+ <small>Job id: {jobId}</small>
130
+ </div>;
131
+ case BansuPopupState.Ready:
132
+ return <div className="vertical_panel">
133
+ Ready
134
+ <small>Bansu instance <i>{bansuEndpoint}</i></small>
135
+ <small>Job id: {jobId}</small>
136
+ <Accordion>
137
+ <AccordionSummary>
138
+ Job output
139
+ </AccordionSummary>
140
+ <AccordionDetails
141
+ style={{maxWidth: '300px', wordBreak: 'break-word'}}
142
+ >
143
+ {finishedJobOutput}
144
+ </AccordionDetails>
145
+ </Accordion>
146
+ <div className="horizontal_container_centered children_expanded">
147
+ <Button
148
+ onClick={() => setPopoverOpened(false)}
149
+ variant="contained"
150
+ >
151
+ Close
152
+ </Button>
153
+ <Button
154
+ onClick={(e) => window.open(`${bansuEndpoint}/get_cif/${jobId}`)}
155
+ // style={{flex: 'auto'}}
156
+ variant="contained"
157
+ >
158
+ Download CIF
159
+ </Button>
160
+ </div>
161
+ </div>;
162
+ case BansuPopupState.Error:
163
+ return <div className="vertical_panel">
164
+ <span style={{color: 'red'}}>Error</span>
165
+ <span style={{color: 'darkred'}}>{errorString}</span>
166
+ <div className="horizontal_container_centered children_expanded">
167
+ <Button
168
+ onClick={() => resetState()}
169
+ variant="contained"
170
+ >
171
+ Retry
172
+ </Button>
173
+ <Button
174
+ onClick={() => setPopoverOpened(false)}
175
+ variant="contained"
176
+ >
177
+ Close
178
+ </Button>
179
+ </div>
180
+ </div>;
181
+ }
182
+ }, [state, popoverOpened, jobId, errorString, finishedJobOutput, posInQueue, userConsent]);
183
+
184
+ useEffect(() => {
185
+ // return;
186
+ if(popoverOpened && state == BansuPopupState.SpawningJob) {
187
+ let promise = new Promise<void>(async () => {
188
+ setState(BansuPopupState.SpawningJob);
189
+ const postData = JSON.stringify({
190
+ 'smiles': props.smiles,
191
+ 'commandline_args': []
192
+ });
193
+
194
+ try {
195
+ const res = await fetch(`${bansuEndpoint}/run_acedrg`, {
196
+ method: 'POST',
197
+ body: postData,
198
+ headers: {
199
+ 'Content-Type': 'application/json',
200
+ },
201
+ // mode: 'no-cors'
202
+ });
203
+ const jsonData = await res.json();
204
+ console.log("Got json from /run_acedrg: ", jsonData);
205
+ if(jsonData.job_id === null) {
206
+ let emsg = `Server returned null job id / no job id. Error message is: ${jsonData.error_message}`;
207
+ console.error(emsg);
208
+ setErrorString(emsg);
209
+ setState(BansuPopupState.Error);
210
+ }
211
+ setJobId(jsonData.job_id);
212
+ setState(BansuPopupState.ConnectingOnWebsocket);
213
+ console.log("Establishing WebSocket connection.");
214
+ // Create WebSocket connection.
215
+ let colonSlashSlashPos = bansuEndpoint.indexOf("://");
216
+ let bansuEndpoint_noprotocol = bansuEndpoint;
217
+ let bansuProtocol = bansuEndpoint.substring(0, colonSlashSlashPos);
218
+ let websocketMode = "ws";
219
+ if(bansuProtocol == "https") {
220
+ websocketMode = "wss";
221
+ }
222
+ if (colonSlashSlashPos != -1) {
223
+ bansuEndpoint_noprotocol = bansuEndpoint.substring(colonSlashSlashPos + 3);
224
+ }
225
+ const socket = new WebSocket(`${websocketMode}://${bansuEndpoint_noprotocol}/ws/${jsonData.job_id}`);
226
+
227
+ // Connection opened
228
+ socket.addEventListener("open", (event) => {
229
+ console.log("Connection on WebSocket established.");
230
+ // setState(BansuPopupState.Waiting);
231
+ });
232
+
233
+ socket.addEventListener("close", (event) => {
234
+ console.log("Connection on WebSocket closed.");
235
+ // process.exit(0);
236
+ });
237
+
238
+ socket.addEventListener("error", (event) => {
239
+ console.error("Connection on WebSocket errored-out: ", event);
240
+ setErrorString(`WebSocket error: ${event}`);
241
+ setState(BansuPopupState.Error);
242
+ });
243
+
244
+ // Listen for messages
245
+ socket.addEventListener("message", (event) => {
246
+ //console.debug("Websocket message from server ", event.data);
247
+ const json = JSON.parse(event.data);
248
+ console.debug("Got JSON from WS: ", json);
249
+ if(json.status == "Failed") {
250
+ setErrorString(`Job failed:\n Failure reason: ${json.failure_reason}\n ${json.error_message ? "Error message: " + json.error_message + "\n ": ""} Output:${JSON.stringify(json.job_output)}`);
251
+ setState(BansuPopupState.Error);
252
+ } else if(json.status == "Finished") {
253
+ setFinishedJobOutput(JSON.stringify(json.job_output));
254
+ setState(BansuPopupState.Ready);
255
+ } else if(json.status == "Pending") {
256
+ setState(BansuPopupState.Waiting);
257
+ } else if(json.status == "Queued") {
258
+ setPosInQueue(json.queue_position);
259
+ setState(BansuPopupState.Queued);
260
+ }
261
+ });
262
+
263
+ } catch(exception) {
264
+ console.error(`Problem with HTTP request: ${exception}`);
265
+ setErrorString(`${exception}`);
266
+ setState(BansuPopupState.Error);
267
+ }
268
+ });
269
+ setWorkerPromise(promise);
270
+ }
271
+
272
+ return () => {
273
+ // todo: cleanup
274
+ // if(workerPromise) {
275
+ // workerPromise
276
+ // }
277
+ };
278
+ }, [state, popoverOpened]);
279
+
280
+ return (
281
+ <StyledEngineProvider injectFirst>
282
+ <Tooltip
283
+ title={"Generate CIF via Bansu (runs Acedrg on a server)."}
284
+ enterDelay={1000}
285
+ enterNextDelay={1000}
286
+ disableInteractive
287
+ >
288
+ <Button
289
+ onClick={() => {
290
+ resetState();
291
+ setPopoverOpened(true);
292
+ }}
293
+ variant="contained"
294
+ >
295
+ Generate CIF
296
+ </Button>
297
+ </Tooltip>
298
+ <Popover
299
+ open={popoverOpened}
300
+ anchorEl={props.anchorEl}
301
+ anchorOrigin={{ vertical: 'center', horizontal: 'center'}}
302
+ transformOrigin={{ vertical: 'center', horizontal: 'center'}}
303
+ >
304
+ <div className={"vertical_popup lhasa_editor LhasaMuiStyling" + (props.dark_mode ? " lhasa_dark_mode" : "")} style={{maxWidth: '400px', maxHeight: '400px'}}>
305
+ <div className="vertical_popup_title">
306
+ CIF generation via Bansu
307
+ </div>
308
+ <div style={{alignSelf: "normal", overflow: 'auto'}}>
309
+ {popoverContent()}
310
+ </div>
311
+ </div>
312
+ </Popover>
313
+ </StyledEngineProvider>
314
+ );
315
+ }
@@ -0,0 +1,97 @@
1
+ @use 'index.scss';
2
+
3
+ @mixin normalize_text {
4
+ font-size: index.$lhasa_font_size;
5
+ line-height: normal;
6
+ text-transform: none;
7
+ letter-spacing: normal;
8
+ }
9
+
10
+ @mixin normalize_padding {
11
+ padding-right: index.$lhasa_base_padding;
12
+ padding-left: index.$lhasa_base_padding;
13
+ padding-top: index.$lhasa_padding_minor;
14
+ padding-bottom: index.$lhasa_padding_minor;
15
+ }
16
+
17
+ // This is a dummy
18
+ .LhasaMuiStyling {
19
+ text-align: left;
20
+ }
21
+
22
+ .LhasaMuiStyling .MuiAccordion-root {
23
+ background-color: index.$lhasa_background_color;
24
+ }
25
+
26
+ .LhasaMuiStyling.lhasa_dark_mode .MuiAccordion-root {
27
+ background-color: index.$lhasa_background_color_dark;
28
+ color: index.$lhasa_text_color_dark;
29
+ }
30
+
31
+ .LhasaMuiStyling .MuiFormGroup-root {
32
+ margin-left: index.$lhasa_base_padding;
33
+ }
34
+
35
+ .LhasaMuiStyling .MuiButtonBase-root {
36
+ @include normalize_text;
37
+ @include normalize_padding;
38
+ }
39
+
40
+ .LhasaMuiStyling.lhasa_dark_mode .MuiButtonBase-root {
41
+ color: index.$lhasa_text_color_dark;
42
+ border-color: index.$lhasa_border_color_dark;
43
+ // background-color: index.$lhasa_background_color_dark;
44
+ }
45
+
46
+ .LhasaMuiStyling.lhasa_dark_mode .MuiPaper-root {
47
+ color: index.$lhasa_text_color_dark;
48
+ background-color: index.$lhasa_background_color_dark;
49
+ }
50
+
51
+ .LhasaMuiStyling.lhasa_dark_mode .MuiInputLabel-root {
52
+ // background-color: index.$lhasa_background_color_dark;
53
+ color: index.$lhasa_text_color_dark;
54
+ }
55
+
56
+ // .LhasaMuiStyling.lhasa_dark_mode .MuiNotchedOutlined-root {
57
+ // // background-color: index.$lhasa_background_color_dark;
58
+ // border-color: index.$lhasa_border_color_dark
59
+ // }
60
+
61
+ .LhasaMuiStyling.lhasa_dark_mode .MuiOutlinedInput-notchedOutline {
62
+ // background-color: index.$lhasa_background_color_dark;
63
+ border-color: index.$lhasa_border_color_dark
64
+ }
65
+
66
+ // .LhasaMuiStyling.lhasa_dark_mode .MuiPopover-root {
67
+ // color: index.$lhasa_text_color_dark;
68
+ // background-color: index.$lhasa_background_color_dark;
69
+ // }
70
+
71
+ // .LhasaMuiStyling.lhasa_dark_mode .MuiPopover-paper {
72
+ // color: index.$lhasa_text_color_dark;
73
+ // background-color: index.$lhasa_background_color_dark;
74
+ // }
75
+
76
+ // .LhasaMuiStyling.lhasa_dark_mode .MuiMenu-paper {
77
+ // color: index.$lhasa_text_color_dark;
78
+ // background-color: index.$lhasa_background_color_dark;
79
+ // }
80
+
81
+ .LhasaMuiStyling .MuiTypography-root {
82
+ @include normalize_text;
83
+ }
84
+
85
+
86
+ .LhasaMuiStyling.lhasa_dark_mode .MuiTypography-root {
87
+ color: index.$lhasa_text_color_dark;
88
+ }
89
+
90
+ .LhasaMuiStyling .MuiSwitch-thumb {
91
+ margin-left: index.$lhasa_base_padding;
92
+ margin-top: index.$lhasa_base_padding * 2;
93
+ }
94
+
95
+ // // .LhasaMuiStyling .MuiSwitch-root {
96
+
97
+ // }
@@ -0,0 +1,20 @@
1
+
2
+ export function parseInchikeyDatabase(rawData: string): Map<string, [string, string]> {
3
+ const inchiMap = new Map<string, [string, string]>();
4
+ for (const line of rawData.split('\n')) {
5
+ if(line.length === 0) continue;
6
+ if(line.startsWith('#')) continue; // Skip comment lines
7
+ const segments = line.split('\t');
8
+ if (segments.length != 3) {
9
+ throw new Error("Unexpected number of segments in InchiKey database line: " + line);
10
+ }
11
+ // console.log(segments);
12
+ const inchiKey = segments[0];
13
+ const monomer_id = segments[1];
14
+ const chem_name = segments[2];
15
+ // console.log(inchiKey);
16
+ inchiMap.set(inchiKey, [monomer_id, chem_name]);
17
+ }
18
+ // console.log(inchiMap);
19
+ return inchiMap;
20
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { MainModule } from './types'
2
+
3
+ // This is work in progress.
4
+ // It will be useful for the LSP
5
+
6
+ // Currently probably broken.
7
+ declare global {
8
+ export interface Window {
9
+ LhasaModule: MainModule;
10
+ }
11
+ }