RaPTOR / static /index.html
Mimi Lavin
initial deploy
e9c697f
Raw
History Blame Contribute Delete
15 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>radical philicity predictor</title>
<style>
:root{
--bg: #ffffff;
--card: #ffffff;
--muted: #6b7280;
--text: #111827;
--border: #e5e7eb;
--shadow: 0 10px 30px rgba(0,0,0,.06);
--shadow2: 0 8px 20px rgba(0,0,0,.05);
--radius: 16px;
--font-ui: Avenir, "Avenir Next", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-mono: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
body {
font-family: var(--font-ui);
margin: 36px auto;
padding: 0 18px;
max-width: 1100px;
background: var(--bg);
color: var(--text);
}
header{
display:flex;
align-items:flex-end;
justify-content:space-between;
gap:16px;
margin-bottom: 18px;
}
h1 {
margin: 0;
font-family: Avenir, "Avenir Next", -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, sans-serif;
font-weight: 400;
font-size: 28px;
letter-spacing: -0.02em;
}
.sub { color: var(--muted); font-size: 13px; line-height: 1.3; }
code { background: #f6f6f6; padding: 2px 6px; border-radius: 8px; }
.card {
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 18px;
margin-top: 16px;
background: var(--card);
box-shadow: var(--shadow);
}
label { display: block; font-weight: 400; margin-top: 8px; font-size: 13px; }
input {
width: 100%;
box-sizing: border-box;
padding: 10px 12px;
margin-top: 6px;
border: 1px solid #d1d5db;
border-radius: 12px;
font-size: 14px;
outline: none;
}
input:focus { border-color: #9ca3af; box-shadow: 0 0 0 4px rgba(156,163,175,.15); }
.controls{
display:block;
}
.inputRow{
display:flex;
gap: 12px;
align-items: center; /* aligns buttons to the input */
}
.titleWrap {
display: flex;
align-items: center;
gap: 12px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between; /* 🔑 this does the work */
margin-bottom: 18px;
}
.headerLeft,
.headerRight {
display: flex;
align-items: center;
gap: 12px;
}
.header h1 {
font-family: Avenir, "Avenir Next", -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, sans-serif;
font-weight: 400; /* non-bold */
font-size: 28px;
letter-spacing: -0.02em;
margin: 0;
}
.logo {
height: 60px;
width: auto;
display: block;
}
.btnRow{
display:flex;
gap: 12px;
align-items:center;
flex: 0 0 auto;
}
.inputRow input{
flex: 1 1 auto; /* input takes remaining width */
min-width: 0; /* prevents overflow in flex */
}
/* optional: keep buttons same height as input */
button{
height: 40px;
}
button {
height: 40px;
padding: 0 14px;
border: 1px solid #111827;
border-radius: 12px;
cursor: pointer;
font-weight: 400;
background: #111827;
color: white;
box-shadow: var(--shadow2);
}
button.secondary{
border-color: #d1d5db;
background: white;
color: #111827;
}
button:disabled { opacity: 0.6; cursor: not-allowed; }
.error {
color: #b00020;
font-weight: 400;
margin-top: 12px;
white-space: pre-wrap;
}
.spinner{
display:inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(255,255,255,.35);
border-top-color: rgba(255,255,255,1);
border-radius: 999px;
animation: spin .8s linear infinite;
vertical-align: -2px;
margin-right: 8px;
}
@keyframes spin { to { transform: rotate(360deg);} }
/* Results layout */
.resultsHeader{
display:flex;
align-items:baseline;
justify-content:space-between;
gap:12px;
margin-bottom: 10px;
}
.pill{
display:inline-flex;
align-items:center;
gap:8px;
border: 1px solid var(--border);
border-radius: 999px;
padding: 6px 10px;
color: var(--muted);
font-size: 12px;
background:#fafafa;
}
.twoCol{
display:grid;
grid-template-columns: 360px 1fr;
gap: 16px;
align-items:start;
}
@media (max-width: 900px){
.twoCol{ grid-template-columns: 1fr; }
}
.svgWrap{
border: 1px solid var(--border);
border-radius: 14px;
padding: 10px;
background:#fafafa;
overflow:hidden;
}
.svgWrap svg{
width:100%;
height:auto;
display:block;
}
.toolbar{
display:flex;
flex-wrap:wrap;
gap:10px;
align-items:center;
justify-content:space-between;
margin-bottom: 12px;
}
.toolbar .left, .toolbar .right{
display:flex;
gap:10px;
align-items:center;
flex-wrap:wrap;
}
select, .search{
height: 36px;
border-radius: 12px;
border: 1px solid #d1d5db;
padding: 0 10px;
font-size: 13px;
background:white;
outline:none;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 14px;
}
.radCard{
border: 1px solid var(--border);
border-radius: 16px;
background:white;
box-shadow: var(--shadow2);
overflow:hidden;
display:flex;
flex-direction:column;
}
.radCard .top{
padding: 10px 10px 0 10px;
}
.radCard .svgWrap{
margin: 0;
border: none;
border-radius: 12px;
background:#f8fafc;
padding: 6px;
}
.radCard .meta{
padding: 10px 12px 12px 12px;
display:flex;
flex-direction:column;
gap:8px;
}
.phi{
font-size: 18px;
font-weight: 400;
letter-spacing:-0.01em;
}
.smiles{
font-family: var(--font-mono);
font-size: 11px;
color: #374151;
background:#f3f4f6;
border-radius: 10px;
padding: 8px;
overflow:auto;
max-height: 70px;
}
.copyRow{
display:flex;
gap:8px;
align-items:center;
justify-content:space-between;
}
.miniBtn{
height: 30px;
padding: 0 10px;
border-radius: 10px;
border: 1px solid #d1d5db;
background: white;
font-weight: 400;
cursor: pointer;
font-size: 12px;
color: #111827; /* ← THIS IS THE FIX */
}
.hint{ color: var(--muted); font-size: 12px; }
</style>
</head>
<body>
<header class="header">
<div class="headerLeft">
<h1>radical philicity predictor</h1>
</div>
<div class="headerRight">
<img src="/static/logo.png" alt="Logo" class="logo" />
<h1>Tekle-Smith Lab</h1>
</div>
</header>
<div class="card">
<div class="controls">
<div class="inputBlock">
<label for="orig">closed-shell SMILES</label>
<div class="inputRow">
<input id="orig" value="CNCC(CCI)CO" />
<div class="btnRow">
<button id="btn">predict radicals</button>
<button id="btnDemo" class="secondary" title="Load a demo SMILES">demo</button>
</div>
</div>
</div>
</div>
<div id="err" class="error"></div>
</div>
<div id="results" class="card" style="display:none;">
<div class="resultsHeader">
<div>
<div style="font-weight:400; font-size:18px;">results</div>
<div class="sub" id="summary"></div>
</div>
<div class="pill" id="countPill"></div>
</div>
<div class="twoCol">
<div>
<div class="sub" style="margin-bottom:8px;"></div>
<div class="svgWrap" id="origSvg"></div>
</div>
<div>
<div class="toolbar">
<div class="left">
<select id="sort">
<option value="desc">sort: philicity high → low</option>
<option value="asc">sort: philicity low → high</option>
</select>
<input class="search" id="search" placeholder="Filter by radical SMILES substring…" />
</div>
<div class="right">
<button class="miniBtn" id="btnDownload">download CSV</button>
<button class="miniBtn" id="btnClear">clear</button>
</div>
</div>
<div class="grid" id="grid"></div>
</div>
</div>
</div>
<script>
const btn = document.getElementById("btn");
const btnDemo = document.getElementById("btnDemo");
const err = document.getElementById("err");
const results = document.getElementById("results");
const origSvg = document.getElementById("origSvg");
const grid = document.getElementById("grid");
const summary = document.getElementById("summary");
const countPill = document.getElementById("countPill");
const sortSel = document.getElementById("sort");
const searchBox = document.getElementById("search");
const btnClear = document.getElementById("btnClear");
const btnDownload = document.getElementById("btnDownload");
const statusPill = document.getElementById("statusPill");
let lastRows = []; // current radicals (for filtering/sorting + CSV)
function setLoading(isLoading) {
btn.disabled = isLoading;
btn.innerHTML = isLoading ? `<span class="spinner"></span>Predicting…` : "predict radicals";
btnDemo.disabled = isLoading;
}
function escapeHtml(s) {
return String(s)
.replaceAll("&","&amp;")
.replaceAll("<","&lt;")
.replaceAll(">","&gt;")
.replaceAll('"',"&quot;")
.replaceAll("'","&#039;");
}
function toCSV(rows) {
const header = ["radical_smiles","radical_center","philicity"];
const lines = [header.join(",")];
for (const r of rows) {
const vals = [
`"${String(r.radical_smiles).replaceAll('"','""')}"`,
r.radical_center,
r.philicity
];
lines.push(vals.join(","));
}
return lines.join("\n");
}
function downloadText(filename, text) {
const blob = new Blob([text], {type: "text/plain;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function renderGrid() {
const q = searchBox.value.trim().toLowerCase();
const order = sortSel.value;
let rows = [...lastRows];
if (q) {
rows = rows.filter(r => String(r.display_smiles).toLowerCase().includes(q));
}
rows.sort((a,b) => order === "desc" ? (b.philicity - a.philicity) : (a.philicity - b.philicity));
grid.innerHTML = "";
for (const r of rows) {
const card = document.createElement("div");
card.className = "radCard";
const phi = Number(r.philicity);
const phiText = Number.isFinite(phi) ? phi.toFixed(3) : String(r.philicity);
card.innerHTML = `
<div class="top">
<div class="svgWrap">${r.svg}</div>
</div>
<div class="meta">
<div class="copyRow">
<div class="phi">ω = ${phiText} eV</div>
<button class="miniBtn" data-copy="${escapeHtml(r.display_smiles)}">Copy</button>
</div>
<div class="smiles">${escapeHtml(r.display_smiles)}</div>
</div>
`;
grid.appendChild(card);
}
// attach copy handlers
grid.querySelectorAll("button[data-copy]").forEach(b => {
b.addEventListener("click", async () => {
const txt = b.getAttribute("data-copy");
try {
await navigator.clipboard.writeText(txt);
const prev = b.textContent;
b.textContent = "Copied!";
setTimeout(() => b.textContent = prev, 800);
} catch (e) {
alert("Could not copy to clipboard.");
}
});
});
countPill.textContent = `${rows.length} radicals shown`;
}
async function predictAll() {
err.textContent = "";
results.style.display = "none";
grid.innerHTML = "";
origSvg.innerHTML = "";
summary.textContent = "";
const original_smiles = document.getElementById("orig").value.trim();
if (!original_smiles) {
err.textContent = "please enter an original SMILES string.";
return;
}
setLoading(true);
try {
const resp = await fetch("/predict_all", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ original_smiles })
});
const data = await resp.json();
if (!resp.ok) {
err.textContent = data?.detail ?? "Request failed.";
return;
}
// show results
results.style.display = "block";
origSvg.innerHTML = data.original_svg || "";
lastRows = (data.radicals || []).map(r => ({
radical_smiles: r.radical_smiles, // internal (mapped) for copy/download if you want
display_smiles: r.display_smiles || r.radical_smiles, // pretty
radical_site: r.radical_site ?? r.radical_center, // tolerate old key names
philicity: r.philicity,
svg: r.svg
}));
summary.textContent = `${data.original_smiles} • generated ${data.n_radicals ?? lastRows.length} radicals`;
renderGrid();
} catch (e) {
err.textContent = String(e);
} finally {
setLoading(false);
}
}
btn.addEventListener("click", predictAll);
btnDemo.addEventListener("click", () => {
const demos = [
"CNCC(CCI)CO",
"c1ccccc1",
"CC(C)C1=CC=CC=C1",
"CCN(CC)CCO",
"CCOC(=O)C1=CC=CC=C1"
];
document.getElementById("orig").value = demos[Math.floor(Math.random()*demos.length)];
});
sortSel.addEventListener("change", renderGrid);
searchBox.addEventListener("input", renderGrid);
btnClear.addEventListener("click", () => {
err.textContent = "";
results.style.display = "none";
grid.innerHTML = "";
origSvg.innerHTML = "";
summary.textContent = "";
lastRows = [];
searchBox.value = "";
});
btnDownload.addEventListener("click", () => {
if (!lastRows.length) return;
const csv = toCSV(lastRows);
downloadText("radicals_philicity.csv", csv);
});
// initial status ping
checkHealth();
</script>
</body>
</html>