Sketch a molecule or pull one from PubChem, then predict how cancer cells respond to it across 985 cell lines. This repo (deepresponse-ui) is the web front end: a chemistry editor, a molecule search, and a results table over a trained model.
I made this for a Pharmacogenomics course at UBA (Facultad de Farmacia y Bioquímica). Drug response screening usually means lab time and tooling that is nowhere near a browser. I wanted the fast version: draw a compound, hit predict, and see a ranked table of cancer cell lines with a potency estimate for each. The model behind it is trained on GDSC data and runs as a separate service. What lives here is the interface: the molecule handling and the calls out to the model.
- Draw a molecule in a full Ketcher editor and get its SMILES automatically
- Search PubChem by name with live autocomplete, and load the compound straight into the canvas
- Predict pIC50 across 985 cancer cell lines from a single SMILES string
- Filter and sort the results by tissue and cancer type, with a potency label per row
- Export the ranked table to PDF, with the molecule, the applied filters and a timestamp on the report
- A walkthrough of the model itself: what pIC50 means, the two encoders, how it was trained and what it cannot tell you
It is a Next.js app. The molecule editor runs entirely in the browser and PubChem is called straight from the client. Prediction goes through one route of its own (app/api/predict/route.ts), which forwards the request to a FastAPI model hosted on Modal.
flowchart LR
U[Draw or type] --> K[Ketcher editor]
S[Search box] -->|name| PC[PubChem]
PC -->|CID then SMILES| K
K -->|SMILES| R["/api/predict route"]
R -->|server side| API[DeepResponse model on Modal]
API -->|pIC50 x 985 cell lines| T[Filterable results table]
Turning a name into a structure takes two hops through PubChem. As you type (debounced at 500 ms so it does not fire on every keystroke) it hits PubChem's autocomplete for compound names. Pick one and it runs the resolution: name to CID, then CID to SMILES. It asks for several SMILES flavors and takes the first that comes back (Canonical, then Isomeric, then Connectivity). That chain is why typing "imatinib" and clicking a suggestion drops a real, drawable structure onto the canvas.
Ketcher is loaded with ssr: false and a StandaloneStructServiceProvider, so all the structure parsing happens in the browser through Ketcher's WASM. There is no chemistry server to run. The SMILES string and the canvas can each change the other. PubChem sets the SMILES, which must redraw the canvas, and drawing on the canvas must update the SMILES. Left alone, that loops. The editor guards it with refs. They track whether an update came from the editor itself and what the last SMILES was. So an external SMILES gets pushed into the canvas without bouncing back out as a fresh change event.
flowchart LR
P[PubChem sets SMILES] --> S[SMILES state]
S -->|redraw| K[Ketcher canvas]
K -->|edit emits SMILES| S
G[Ref guard] -.->|blocks echo| S
The client cleans the SMILES before sending. Ketcher can emit CXSMILES with an extension block after a |, and the model wants a plain string, so it splits that off first. The cleaned string is POSTed to /api/predict, which forwards it to Modal from the server. That hop exists because the model endpoint sends no Access-Control-Allow-Origin header, so a browser cannot call it directly. Back comes a pIC50 for every one of the 985 cell lines, each with its tissue and cancer type. There is also a linear calibration hook (pIC50 = a * raw + b) that can stretch and shift the raw output. It is off by default, so what you see is the model's own numbers.
PubChem allows five requests per second per IP and answers 503 above that. A page of ten result cards would otherwise fire around twenty-one requests at once: the autocomplete, one name-to-CID lookup per card, and ten structure thumbnails. Lookups run three at a time, every call retries 503 and 429 with an exponential backoff, and picking a card retires the in-flight formula lookups so the request you are waiting on gets the budget.
The SMILES field takes free text, so it can hold something unparseable. Ketcher reports back whether it managed to build a molecule from the current string, and Predict stays disabled while it could not. Without that, an invalid structure costs a full round trip to the model before failing.
The model's pIC50 outputs cluster in a narrow band, roughly 4.7 to 6.6. The activity labels (Very Potent down to Very Weak) use thresholds fitted to that observed range.
Frontend: Next.js 16, React 19, TypeScript, Tailwind CSS v4
Chemistry: Ketcher (ketcher-react, ketcher-standalone, indigo-ketcher), smiles-drawer
Data: PubChem REST (autocomplete, name to CID, CID to SMILES)
Model: a DeepResponse FastAPI service on Modal, trained on GDSC, returning pIC50 per cell line
alchem.is/
app/
page.tsx search, editor, predict, and the results table
api/predict/route.ts same-origin proxy to the Modal endpoint
components/
KetcherEditor.tsx Ketcher with two-way SMILES sync
ModelExplainer.tsx how the model works, with data-scaled diagrams
MolecularBackground.tsx procedurally generated skeletal structures
npm install
npm run dev # http://localhost:3000The prediction endpoint is set in app/api/predict/route.ts. Point it at your own model service if you are running the DeepResponse backend yourself.
Working front end. The model that produces the predictions lives on Modal and is not in this repo, so this build depends on that service being up. A prediction takes roughly 40 to 60 seconds because it scores all 985 cell lines in one request.
