// Educational browser model source snapshot. Not the native oNeura engine or a validated clinical model.
// Generated from the inline script in smiles.html. No patient decisions.
// ─── DRUG GALLERY ─────────────────────────────────────────
const DRUGS = [
{ name: 'Aspirin', smiles: 'CC(=O)Oc1ccccc1C(=O)O' },
{ name: 'Ibuprofen', smiles: 'CC(C)Cc1ccc(cc1)[C@@H](C)C(=O)O' },
{ name: 'Caffeine', smiles: 'Cn1cnc2c1c(=O)n(c(=O)n2C)C' },
{ name: 'Warfarin', smiles: 'CC(=O)c1cccc(O)c1C1=C(Oc2ccccc2)C(=O)c2ccccc21' },
{ name: 'Metoprolol', smiles: 'COCCc1ccc(OCC(O)CNC(C)C)cc1' },
{ name: 'Atorvastatin', smiles: 'CC(C)C1=CC=C(C=C1)C2C(C(C(=O)O2)(C)C)C(=O)NC3CC(C=C3)O' },
{ name: 'Omeprazole', smiles: 'COc1ccc2[nH]c(nc2c1)[S@@](=O)Cc3ncc(C)c(OC)c3' },
{ name: 'Diazepam', smiles: 'CN1C(=O)CN=C(C2=CC=CC=C2)C1=C3C=CC=CC3Cl' },
{ name: 'Morphine', smiles: 'CN1CCC23C4OC5OC(CO)C=CC5C4C(O)C2C(=CC3)C1' },
{ name: 'Amoxicillin', smiles: 'CC1(C)S[C@@H]2[C@H](NC(=O)[C@H](N)c3ccc(O)cc3)C(=O)N2[C@@H]1C(=O)O' }
];
// ─── ATOMIC DATA ──────────────────────────────────────────
const ATOM_MASS = {
H:1.008,He:4.003,Li:6.941,Be:9.012,B:10.81,C:12.011,N:14.007,
O:15.999,F:18.998,Ne:20.18,Na:22.99,Mg:24.305,Al:26.982,Si:28.086,
P:30.974,S:32.065,Cl:35.453,Ar:39.948,K:39.098,Ca:40.078,
Br:79.904,I:126.904,Fe:55.845,Zn:65.38
};
const VALENCE = {
H:1, B:3, C:4, N:3, O:2, F:1, Si:4, P:3, S:2, Cl:1, Br:1, I:1
};
// Wildman-Crippen-style LogP atom contributions (simplified)
const LOGP_CONTRIB = {
C: 0.1441, N: -0.7567, O: -0.2893, S: 0.6237,
F: 0.4115, Cl: 0.6895, Br: 0.8813, I: 1.0500,
H: 0.1230, P: 0.2650, B: -0.2000, Si: 0.3600
};
// PSA contributions from polar atoms (simplified Ertl method)
const PSA_CONTRIB = {
N: 12.36, O: 13.14, S: 25.30
};
// ─── PARSE SMILES ─────────────────────────────────────────
function parseSMILES(smiles) {
var atoms = [];
var bonds = [];
var stack = []; // for branches
var ringOpens = {}; // ring digit -> atom index
var pos = 0;
var prevAtom = -1;
var nextBondType = null; // null=single/aro, '=', '#', '/'
var aromaticAtoms = [];
function addAtom(symbol, aromatic) {
var idx = atoms.length;
var atom = {
symbol: symbol,
aromatic: !!aromatic,
charge: 0,
isotope: 0,
implicitH: 0,
index: idx
};
atoms.push(atom);
if (aromatic) {
aromaticAtoms.push(idx);
}
if (prevAtom >= 0) {
var btype = nextBondType !== null ? nextBondType : (aromatic ? ':' : '-');
bonds.push({ from: prevAtom, to: idx, type: btype });
}
prevAtom = idx;
nextBondType = null;
return idx;
}
function parseBracket() {
// bracket atom like [C@@H], [NH3+], [13C], [nH]
var i = pos;
var isotope = 0;
// isotope
if (i < smiles.length && smiles[i] >= '0' && smiles[i] <= '9') {
var isoStr = '';
while (i < smiles.length && smiles[i] >= '0' && smiles[i] <= '9') {
isoStr += smiles[i]; i++;
}
isotope = parseInt(isoStr, 10);
}
// symbol
var aromatic = false;
var symbol = '';
if (i < smiles.length) {
if (smiles[i] >= 'a' && smiles[i] <= 'z') {
aromatic = true;
symbol = smiles[i].toUpperCase(); i++;
if (i < smiles.length && smiles[i] >= 'a' && smiles[i] <= 'z') {
// two-letter lowercase aromatic like "se" → Se
// not common, but handle
}
} else if (i < smiles.length && smiles[i] >= 'A' && smiles[i] <= 'Z') {
symbol = smiles[i]; i++;
if (i < smiles.length && smiles[i] >= 'a' && smiles[i] <= 'z') {
symbol += smiles[i]; i++;
}
}
}
// chirality
if (i < smiles.length && (smiles[i] === '@')) {
i++; // skip @
if (i < smiles.length && smiles[i] === '@') i++; // @@
// skip any digits after
while (i < smiles.length && smiles[i] >= '0' && smiles[i] <= '9') i++;
}
// H count
var hCount = 0;
if (i < smiles.length && smiles[i] === 'H') {
i++;
if (i < smiles.length && smiles[i] >= '0' && smiles[i] <= '9') {
hCount = parseInt(smiles[i], 10); i++;
} else {
hCount = 1;
}
}
// charge
var charge = 0;
if (i < smiles.length && (smiles[i] === '+' || smiles[i] === '-')) {
var sign = smiles[i] === '+' ? 1 : -1; i++;
if (i < smiles.length && smiles[i] >= '0' && smiles[i] <= '9') {
charge = sign * parseInt(smiles[i], 10); i++;
} else {
charge = sign;
// multiple +/- signs
while (i < smiles.length && smiles[i] === (sign > 0 ? '+' : '-')) {
charge += sign; i++;
}
}
}
pos = i;
var idx = addAtom(symbol, aromatic);
atoms[idx].charge = charge;
atoms[idx].isotope = isotope;
atoms[idx].explicitH = hCount;
return idx;
}
while (pos < smiles.length) {
var c = smiles[pos];
// Bond characters
if (c === '-' || c === '=' || c === '#' || c === ':' || c === '/' || c === '\\') {
nextBondType = c;
pos++;
continue;
}
// Branch
if (c === '(') {
stack.push(prevAtom);
pos++;
continue;
}
if (c === ')') {
if (stack.length > 0) {
prevAtom = stack.pop();
}
nextBondType = null;
pos++;
continue;
}
// Ring closure
if ((c >= '0' && c <= '9') || c === '%') {
var ringNum;
if (c === '%') {
ringNum = parseInt(smiles.substring(pos + 1, pos + 3), 10);
pos += 3;
} else {
ringNum = parseInt(c, 10);
pos++;
}
if (ringOpens[ringNum] !== undefined) {
// close ring
var fromIdx = ringOpens[ringNum];
var btype = nextBondType !== null ? nextBondType : (atoms[fromIdx].aromatic && atoms[prevAtom].aromatic ? ':' : '-');
bonds.push({ from: prevAtom, to: fromIdx, type: btype });
delete ringOpens[ringNum];
} else {
ringOpens[ringNum] = prevAtom;
}
nextBondType = null;
continue;
}
// Bracket atom
if (c === '[') {
pos++; // skip '['
parseBracket();
// skip closing ']'
if (pos < smiles.length && smiles[pos] === ']') pos++;
continue;
}
// Organic subset atoms (uppercase = aliphatic)
if (c === 'B') {
if (pos + 1 < smiles.length && smiles[pos + 1] === 'r') {
addAtom('Br', false); pos += 2;
} else {
addAtom('B', false); pos++;
}
continue;
}
if (c === 'C') {
if (pos + 1 < smiles.length && smiles[pos + 1] === 'l') {
addAtom('Cl', false); pos += 2;
} else {
addAtom('C', false); pos++;
}
continue;
}
if (c === 'N') { addAtom('N', false); pos++; continue; }
if (c === 'O') { addAtom('O', false); pos++; continue; }
if (c === 'S') { addAtom('S', false); pos++; continue; }
if (c === 'P') { addAtom('P', false); pos++; continue; }
if (c === 'F') { addAtom('F', false); pos++; continue; }
if (c === 'I') { addAtom('I', false); pos++; continue; }
// Aromatic atoms (lowercase)
if (c === 'c') { addAtom('C', true); pos++; continue; }
if (c === 'n') { addAtom('N', true); pos++; continue; }
if (c === 'o') { addAtom('O', true); pos++; continue; }
if (c === 's') { addAtom('S', true); pos++; continue; }
if (c === 'p') { addAtom('P', true); pos++; continue; }
// Dot (disconnected)
if (c === '.') {
prevAtom = -1;
nextBondType = null;
pos++;
continue;
}
// Unknown char, skip
pos++;
}
// Compute implicit H for each atom
for (var i = 0; i < atoms.length; i++) {
var a = atoms[i];
if (a.explicitH !== undefined) {
a.implicitH = a.explicitH;
} else {
var bondCount = 0;
var aromaticBondCount = 0;
for (var j = 0; j < bonds.length; j++) {
if (bonds[j].from === i || bonds[j].to === i) {
if (bonds[j].type === '=') bondCount += 2;
else if (bonds[j].type === '#') bondCount += 3;
else if (bonds[j].type === ':') { bondCount += 1; aromaticBondCount++; }
else bondCount += 1;
}
}
var maxVal = VALENCE[a.symbol] || 4;
var implicit = maxVal - bondCount;
if (a.charge !== 0) {
implicit -= a.charge; // positive charge reduces H
}
if (a.aromatic && a.symbol === 'N') {
// Check if this N has a lone pair (contributes to aromaticity)
// if bondCount is 2 (in ring with 2 bonds), then 1 H possible if maxVal=3
}
a.implicitH = Math.max(0, implicit);
}
}
return { atoms: atoms, bonds: bonds, aromaticAtoms: aromaticAtoms };
}
// ─── COMPUTE PROPERTIES ───────────────────────────────────
function computeProps(parsed) {
var atoms = parsed.atoms;
var bonds = parsed.bonds;
var aromaticAtoms = parsed.aromaticAtoms;
// Molecular Weight
var mw = 0;
for (var i = 0; i < atoms.length; i++) {
var sym = atoms[i].symbol;
var mass = ATOM_MASS[sym] || 0;
mw += mass;
// add implicit H
mw += atoms[i].implicitH * ATOM_MASS['H'];
}
// Heavy atoms (non-H)
var heavyAtoms = atoms.length;
// LogP (Wildman-Crippen simplified)
var logp = 0;
for (var i = 0; i < atoms.length; i++) {
var sym = atoms[i].symbol;
var contrib = LOGP_CONTRIB[sym] || 0;
logp += contrib;
// H contributions
logp += atoms[i].implicitH * (LOGP_CONTRIB['H'] || 0);
}
// aromatic correction
var aromaticRingCount = countAromaticRings(atoms, bonds, aromaticAtoms);
logp -= aromaticRingCount * 0.32;
// PSA
var psa = 0;
for (var i = 0; i < atoms.length; i++) {
var sym = atoms[i].symbol;
if (PSA_CONTRIB[sym]) {
psa += PSA_CONTRIB[sym];
}
}
// HBD: OH and NH groups
var hbd = 0;
for (var i = 0; i < atoms.length; i++) {
var a = atoms[i];
if (a.symbol === 'O' || a.symbol === 'N') {
if (a.implicitH > 0) hbd += a.implicitH;
// also check bonds for explicit H connections would go here
}
}
// HBA: N and O atoms
var hba = 0;
for (var i = 0; i < atoms.length; i++) {
if (atoms[i].symbol === 'N' || atoms[i].symbol === 'O') {
hba++;
}
}
// Rotatable bonds: single, non-ring, not terminal, not amide C-N
var rotBonds = 0;
for (var i = 0; i < bonds.length; i++) {
var b = bonds[i];
if (b.type !== '-') continue; // only single bonds
var a1 = atoms[b.from];
var a2 = atoms[b.to];
// skip if terminal (H or halogen)
if (a1.symbol === 'H' || a2.symbol === 'H') continue;
if (a1.symbol === 'F' || a2.symbol === 'F') continue;
if (a1.symbol === 'Cl' || a2.symbol === 'Cl') continue;
if (a1.symbol === 'Br' || a2.symbol === 'Br') continue;
if (a1.symbol === 'I' || a2.symbol === 'I') continue;
// skip ring bonds (simplified: check if both atoms share another bond path)
if (isRingBond(bonds, b.from, b.to)) continue;
rotBonds++;
}
return {
mw: mw,
logp: logp,
psa: psa,
hbd: hbd,
hba: hba,
rotBonds: rotBonds,
heavyAtoms: heavyAtoms,
aromaticRings: aromaticRingCount
};
}
function isRingBond(bonds, a, b) {
// BFS from a to b without using direct bond
var visited = {};
var queue = [a];
visited[a] = true;
// Build adjacency (excluding the direct a-b connection we're testing)
var adj = {};
for (var i = 0; i < bonds.length; i++) {
var f = bonds[i].from;
var t = bonds[i].to;
if ((f === a && t === b) || (f === b && t === a)) continue;
if (!adj[f]) adj[f] = [];
if (!adj[t]) adj[t] = [];
adj[f].push(t);
adj[t].push(f);
}
while (queue.length > 0) {
var node = queue.shift();
if (node === b) return true;
var neighbors = adj[node] || [];
for (var i = 0; i < neighbors.length; i++) {
if (!visited[neighbors[i]]) {
visited[neighbors[i]] = true;
queue.push(neighbors[i]);
}
}
}
return false;
}
function countAromaticRings(atoms, bonds, aromaticAtoms) {
if (aromaticAtoms.length === 0) return 0;
// Count connected components of aromatic atoms that form cycles
// Simplified: use aromatic bonds to find rings
var aromaticBonds = [];
for (var i = 0; i < bonds.length; i++) {
if (bonds[i].type === ':' ||
(atoms[bonds[i].from].aromatic && atoms[bonds[i].to].aromatic)) {
aromaticBonds.push(bonds[i]);
}
}
// Build adjacency for aromatic subgraph
var aSet = {};
for (var i = 0; i < aromaticAtoms.length; i++) aSet[aromaticAtoms[i]] = true;
var adj = {};
for (var i = 0; i < aromaticBonds.length; i++) {
var f = aromaticBonds[i].from;
var t = aromaticBonds[i].to;
if (!adj[f]) adj[f] = [];
if (!adj[t]) adj[t] = [];
adj[f].push(t);
adj[t].push(f);
}
// Find rings using DFS - count distinct cycles
// Simplified: estimate rings = edges - nodes + components (for planar aromatic system)
var edgeCount = aromaticBonds.length;
var nodeCount = aromaticAtoms.length;
if (nodeCount === 0) return 0;
// Count connected components
var visited = {};
var components = 0;
for (var i = 0; i < aromaticAtoms.length; i++) {
var start = aromaticAtoms[i];
if (visited[start]) continue;
components++;
var stack = [start];
while (stack.length > 0) {
var node = stack.pop();
if (visited[node]) continue;
visited[node] = true;
var neighbors = adj[node] || [];
for (var j = 0; j < neighbors.length; j++) {
if (!visited[neighbors[j]]) stack.push(neighbors[j]);
}
}
}
// Euler formula for planar graph: rings = edges - nodes + components
var rings = edgeCount - nodeCount + components;
return Math.max(0, rings);
}
// ─── LIPINSKI ─────────────────────────────────────────────
function checkLipinski(props) {
var rules = [
{ name: 'MW', value: props.mw, limit: 500, label: 'MW < 500' },
{ name: 'LogP', value: props.logp, limit: 5, label: 'LogP < 5' },
{ name: 'HBD', value: props.hbd, limit: 5, label: 'HBD < 5' },
{ name: 'HBA', value: props.hba, limit: 10, label: 'HBA < 10' }
];
var violations = 0;
for (var i = 0; i < rules.length; i++) {
rules[i].pass = rules[i].value < rules[i].limit;
if (!rules[i].pass) violations++;
}
return { rules: rules, violations: violations, pass: violations <= 1 };
}
// ─── DRUG CLASS DETECTION ─────────────────────────────────
function detectDrugClass(smiles) {
var s = smiles.toLowerCase();
// NSAID patterns
if (s.match(/c\(=o\)o/) && s.match(/c1ccccc1/)) return 'NSAID / Salicylate';
if (s.match(/c\(=o\)o/) && s.match(/c1ccc/) && (s.match(/cc\[ch\]/) || s.match(/isopropyl/) || s.includes('c(cc'))) return 'NSAID / Propionic Acid Derivative';
// Beta blocker: aryloxypropanolamine
if (s.match(/c1ccc.*occ/) && s.match(/oh/) && s.match(/nc/)) return 'Beta Blocker';
// Statin: HMG-CoA reductase inhibitor
if (s.match(/c\(=o\)o/) && s.match(/c1ccc/) && s.length > 40) return 'Statin / Lipid Regulator';
// Benzodiazepine
if (s.match(/c1=cc=cc=c1.*c=c.*c\(=o\)n/) && s.match(/cl/)) return 'Benzodiazepine / Anxiolytic';
if (s.match(/c\(=o\)n/) && s.match(/c1ccccc1/) && s.match(/n.*c=c/)) return 'Benzodiazepine / Anxiolytic';
// Opioid
if (s.match(/n1ccc/) && s.match(/c.*c.*o/) && s.length > 30 && s.match(/c\(o\)/)) return 'Opioid Analgesic';
// PPI (proton pump inhibitor)
if (s.match(/s.*=o/) && s.match(/c1ccc.*n/) && s.match(/c\(o[cn]/)) return 'Proton Pump Inhibitor';
// Penicillin / Beta-lactam
if (s.match(/c\(=o\)n.*c.*s/) && s.match(/c\(=o\)o/)) return 'Beta-Lactam Antibiotic';
// Fluoroquinolone
if (s.match(/f/) && s.match(/c\(=o\)c.*n.*c.*c\(=o\)o/)) return 'Fluoroquinolone Antibiotic';
// Xanthine stimulant
if (s.match(/c\(=o\)n.*c\(=o\)n/) && s.match(/c1ncnc2/)) return 'Xanthine Derivative / Stimulant';
// General carboxylic acid drug
if (s.match(/c\(=o\)o/)) return 'Carboxylic Acid Drug';
// General amine drug
if (s.match(/n[c\(]/)) return 'Amino Compound';
// Phenol-containing
if (s.match(/oc1ccc/) || s.match(/oc1cccc/)) return 'Phenolic Compound';
// Sulfonamide
if (s.match(/s\(=o\)\(=o\)/) || s.match(/s\(=o\)\(=o\)n/)) return 'Sulfonamide';
// General heterocyclic
if (s.match(/n.*c.*c.*n/) || s.match(/n1ccc/)) return 'Nitrogen Heterocycle';
// Aromatic compound
if (s.match(/c1cc/) || s.match(/c1cccc/)) return 'Aromatic Compound';
return 'Unclassified Small Molecule';
}
// ─── MOLECULE ENGINE ──────────────────────────────────────
// Primary: OpenChemLib (handles any valid SMILES + draws the structure).
// Fallback: the built-in parser above, if OCL fails to load (offline).
function analyzeWithOCL(smiles) {
var mol = OCL.Molecule.fromSmiles(smiles); // throws on invalid SMILES
if (!mol || mol.getAllAtoms() === 0) throw new Error('empty');
mol.ensureHelperArrays(OCL.Molecule.cHelperRings);
var mf = mol.getMolecularFormula();
var mp = new OCL.MoleculeProperties(mol);
var heavy = 0, n = mol.getAllAtoms();
for (var i = 0; i < n; i++) { if (mol.getAtomicNo(i) !== 1) heavy++; }
var aromRings = 0;
try { var rs = mol.getRingSet(); for (var r = 0; r < rs.getSize(); r++) { if (rs.isAromatic(r)) aromRings++; } } catch (e) {}
var svg = '';
try { svg = mol.toSVG(300, 220, undefined, { autoCrop: true, autoCropMargin: 14, suppressChiralText: true }); } catch (e) {}
var canonical = smiles;
try { canonical = mol.toSmiles(); } catch (e) {}
return {
ok: true, engine: 'OpenChemLib',
props: {
mw: mf.relativeWeight, logp: mp.logP, psa: mp.polarSurfaceArea,
hbd: mp.donorCount, hba: mp.acceptorCount, rotBonds: mp.rotatableBondCount,
heavyAtoms: heavy, aromaticRings: aromRings
},
formula: mf.formula, canonical: canonical, svg: svg
};
}
function analyzeWithBuiltin(smiles) {
var parsed = parseSMILES(smiles);
if (parsed.atoms.length === 0) throw new Error('empty');
return { ok: true, engine: 'built-in', props: computeProps(parsed), formula: '', canonical: smiles, svg: '' };
}
function analyzeMolecule(smiles) {
if (window.OCL) {
// OCL is the authority on validity: if it rejects the string, it's invalid.
try { return analyzeWithOCL(smiles); }
catch (e) { return { ok: false, error: 'Could not parse that SMILES string. Please check the syntax.' }; }
}
// OCL not loaded (offline / still downloading) → best-effort built-in parser.
try { return analyzeWithBuiltin(smiles); }
catch (e) { return { ok: false, error: 'Could not parse that SMILES string. Please check the syntax.' }; }
}
function renderStructure(res) {
var box = document.getElementById('structureSvg');
if (res.svg) box.innerHTML = res.svg;
else box.innerHTML = 'Structure depiction unavailable offline';
document.getElementById('propFormula').textContent = res.formula || '-';
document.getElementById('propCanonical').textContent = res.canonical || '-';
}
// ─── UI UPDATE ────────────────────────────────────────────
function compute() {
var smiles = document.getElementById('smilesInput').value.trim();
var errEl = document.getElementById('smilesError');
errEl.textContent = '';
if (!smiles) { errEl.textContent = 'Enter a SMILES string to analyze.'; return; }
var res = analyzeMolecule(smiles);
if (!res.ok) {
errEl.textContent = res.error;
document.getElementById('engineTag').textContent = '';
return;
}
updatePropsUI(res.props);
updateLipinskiUI(checkLipinski(res.props), res.props);
updateDrugClassUI(detectDrugClass(smiles));
renderStructure(res);
document.getElementById('engineTag').textContent = 'Engine: ' + res.engine;
animatePipeline();
}
function updatePropsUI(props) {
document.getElementById('propMW').textContent = props.mw.toFixed(2);
document.getElementById('propLogP').textContent = props.logp.toFixed(2);
document.getElementById('propPSA').textContent = props.psa.toFixed(1) + ' \u00C5\u00B2';
document.getElementById('propHBD').textContent = props.hbd;
document.getElementById('propHBA').textContent = props.hba;
document.getElementById('propRotBonds').textContent = props.rotBonds;
document.getElementById('propHeavyAtoms').textContent = props.heavyAtoms;
document.getElementById('propAromaticRings').textContent = props.aromaticRings;
}
function updateLipinskiUI(lipinski, props) {
var ids = ['MW', 'LogP', 'HBD', 'HBA'];
var vals = [props.mw, props.logp, props.hbd, props.hba];
var limits = [500, 5, 5, 10];
for (var i = 0; i < ids.length; i++) {
var rule = lipinski.rules[i];
document.getElementById('lip' + ids[i]).textContent = vals[i].toFixed(ids[i] === 'MW' ? 2 : (ids[i] === 'LogP' ? 2 : 0)) + ' / ' + limits[i];
var badge = document.getElementById('lip' + ids[i] + 'badge');
badge.textContent = rule.pass ? 'PASS' : 'FAIL';
badge.className = 'badge ' + (rule.pass ? 'badge-pass' : 'badge-fail');
}
var overall = document.getElementById('lipinskiOverall');
overall.style.display = 'block';
if (lipinski.pass) {
overall.textContent = 'Lipinski Compliant (' + lipinski.violations + ' violation' + (lipinski.violations !== 1 ? 's' : '') + ')';
overall.className = 'lipinski-overall pass';
} else {
overall.textContent = 'Lipinski Violations: ' + lipinski.violations + ' of 4 rules exceeded';
overall.className = 'lipinski-overall fail';
}
}
function updateDrugClassUI(drugClass) {
document.getElementById('drugClassValue').textContent = drugClass;
}
function animatePipeline() {
var steps = document.querySelectorAll('.pipeline-step');
for (var i = 0; i < steps.length; i++) {
steps[i].classList.remove('active');
}
// Animate sequentially
var idx = 0;
function activate() {
if (idx < steps.length) {
steps[idx].classList.add('active');
idx++;
setTimeout(activate, 120);
}
}
activate();
}
// ─── GALLERY INIT ─────────────────────────────────────────
function initGallery() {
var container = document.getElementById('gallery');
for (var i = 0; i < DRUGS.length; i++) {
(function(drug) {
var chip = document.createElement('button');
chip.className = 'chip';
chip.innerHTML = '' + drug.name + '';
chip.onclick = function() {
document.getElementById('smilesInput').value = drug.smiles;
compute();
};
container.appendChild(chip);
})(DRUGS[i]);
}
}
// Allow Enter key to trigger compute
document.getElementById('smilesInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter') compute();
});
// Wait for the OpenChemLib engine (deferred script) before the first compute,
// so the page opens with a fully-rendered structure rather than the fallback.
function oclReady(cb) {
if (window.OCL) return cb();
var s = document.getElementById('ocl-script');
if (!s) return cb();
s.addEventListener('load', cb, { once: true });
s.addEventListener('error', cb, { once: true }); // fall back to built-in
}
// Init
initGallery();
// Auto-run Aspirin so page never opens with blank --
document.getElementById('smilesInput').value = DRUGS[0].smiles;
oclReady(compute);