Skip to content

Commit efbf324

Browse files
Brand the demo: logo, gradient theme, per-descriptor aroma colors, reorder (#91)
- Header now shows the Flavormancer flask badge + a purple->teal gradient wordmark (vendored static/logo.png, 256px; served via a generalized /static/{fname} route that also covers 3Dmol.js). Primary button + focus use the brand gradient/teal. - Each aroma descriptor gets its own color (fruity/citrus/floral/minty/…), applied to the prediction bars and the substitution-card aroma tags — so notes read at a glance, the way the tastes already have distinct colors. - Move Mixture mode above the browse library. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
1 parent 4a9f120 commit efbf324

3 files changed

Lines changed: 50 additions & 27 deletions

File tree

training/app.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,15 @@ def api_structure3d(q: Query):
197197
return {"molblock": None}
198198

199199

200-
@app.get("/static/3Dmol-min.js")
201-
def _threedmol_js():
202-
"""Serve the vendored 3Dmol.js (BSD-3-Clause) locally so the demo stays self-contained."""
200+
@app.get("/static/{fname}")
201+
def _static(fname: str):
202+
"""Serve vendored static assets (3Dmol.js, the logo) locally so the demo is self-contained."""
203+
from fastapi import HTTPException
203204
from fastapi.responses import FileResponse
204-
return FileResponse("static/3Dmol-min.js", media_type="application/javascript")
205+
p = Path("static") / Path(fname).name # basename only — no path traversal
206+
if p.exists():
207+
return FileResponse(str(p))
208+
raise HTTPException(status_code=404)
205209

206210

207211
class MixtureQuery(BaseModel):

training/static/logo.png

56.3 KB
Loading

training/workbench.html

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,30 @@
77
<style>
88
:root{
99
--surface:#F4F6F4; --panel:#FFFFFF; --line:#DCE3DE; --ink:#172019;
10-
--muted:#5C6B63; --accent:#0F5C4D; --accent-soft:#0F5C4D14;
10+
--muted:#5C6B63; --accent:#2A7C86; --accent-soft:#2A7C8614;
11+
--brand-1:#3A2E7C; --brand-2:#1AA0A8; --brand-accent:#E0913C;
12+
--brand-grad:linear-gradient(100deg,#3A2E7C 0%,#2A5E9E 45%,#1AA0A8 100%);
1113
--sweet:#E0972E; --bitter:#6B4E9E; --umami:#C0563B; --sour:#9FB024; --salty:#4E84B0; --aroma:#2E7D6B;
1214
--ui:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
1315
--mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;
1416
}
1517
*{box-sizing:border-box}
1618
body{margin:0;background:var(--surface);color:var(--ink);font-family:var(--ui);
1719
font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased}
18-
header{padding:22px 28px;border-bottom:1px solid var(--line);display:flex;
19-
align-items:baseline;gap:14px}
20-
header h1{margin:0;font-size:18px;font-weight:650;letter-spacing:-.01em}
20+
header{padding:16px 28px;border-bottom:1px solid var(--line);display:flex;
21+
align-items:center;gap:15px;background:linear-gradient(180deg,#FBFCFB,#F4F6F4)}
22+
header .logo{width:54px;height:54px;flex:0 0 auto}
23+
header .brand{display:flex;flex-direction:column;gap:1px;min-width:0}
24+
header h1{margin:0;font-size:23px;font-weight:750;letter-spacing:-.01em;line-height:1.1;
25+
background:var(--brand-grad);-webkit-background-clip:text;background-clip:text;color:transparent}
2126
header .sub{font-family:var(--mono);font-size:12px;color:var(--muted)}
2227
main{max-width:980px;margin:0 auto;padding:26px 28px 60px}
2328
.bar{display:flex;gap:10px;align-items:stretch}
2429
.bar input{flex:1;font-family:var(--mono);font-size:15px;padding:13px 15px;
2530
border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--ink)}
2631
.bar input:focus{outline:2px solid var(--accent);border-color:transparent}
2732
.bar button{font-family:var(--ui);font-weight:600;font-size:15px;padding:0 22px;
28-
border:none;border-radius:9px;background:var(--accent);color:#fff;cursor:pointer}
33+
border:none;border-radius:9px;background:var(--brand-grad);color:#fff;cursor:pointer}
2934
.bar button:disabled{opacity:.5;cursor:default}
3035
.hint{font-family:var(--mono);font-size:12px;color:var(--muted);margin:8px 2px 0}
3136
.grid{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-top:24px}
@@ -161,8 +166,11 @@
161166
</head>
162167
<body>
163168
<header>
164-
<h1>Flavormancer</h1>
165-
<span class="sub">taste &amp; aroma prediction from chemical structure — before you pour</span>
169+
<img class="logo" src="/static/logo.png" alt="Flavormancer logo">
170+
<div class="brand">
171+
<h1>Flavormancer</h1>
172+
<span class="sub">taste &amp; aroma prediction from chemical structure — before you pour</span>
173+
</div>
166174
</header>
167175
<main>
168176
<div class="bar">
@@ -174,6 +182,19 @@ <h1>Flavormancer</h1>
174182
<b>SMILES</b> string (e.g. <code>O=Cc1ccc(O)c(OC)c1</code>). SMILES omit implicit
175183
hydrogens — water is just <code>O</code>. These models are for <b>organic</b> molecules.</div>
176184

185+
<label class="toggle"><input type="checkbox" id="mixToggle"> Mixture mode — screen 2+ ingredients for documented hazards</label>
186+
<div id="mixPanel" style="display:none">
187+
<div id="mixRows"></div>
188+
<button class="mix-add" id="mixAdd">+ add ingredient</button>
189+
<div class="mix-proc">Process applied:
190+
<label><input type="checkbox" class="proc" value="high_heat"> high heat</label>
191+
<label><input type="checkbox" class="proc" value="refining"> refining</label>
192+
<label><input type="checkbox" class="proc" value="fermentation"> fermentation</label>
193+
</div>
194+
<button class="mix-btn" id="mixCheck">Check mixture</button>
195+
<div id="mixResults"></div>
196+
</div>
197+
177198
<div id="browse" class="browse">
178199
<div class="browse-head">Or browse the library — pick a category, then a molecule
179200
<span class="browse-toggle"><button type="button" id="bmode2d" class="st-tab on">2D</button><button type="button" id="bmode3d" class="st-tab">3D</button></span>
@@ -192,20 +213,6 @@ <h1>Flavormancer</h1>
192213
</div>
193214
</div>
194215

195-
<label class="toggle"><input type="checkbox" id="mixToggle"> Mixture mode — screen 2+ ingredients for documented hazards</label>
196-
197-
<div id="mixPanel" style="display:none">
198-
<div id="mixRows"></div>
199-
<button class="mix-add" id="mixAdd">+ add ingredient</button>
200-
<div class="mix-proc">Process applied:
201-
<label><input type="checkbox" class="proc" value="high_heat"> high heat</label>
202-
<label><input type="checkbox" class="proc" value="refining"> refining</label>
203-
<label><input type="checkbox" class="proc" value="fermentation"> fermentation</label>
204-
</div>
205-
<button class="mix-btn" id="mixCheck">Check mixture</button>
206-
<div id="mixResults"></div>
207-
</div>
208-
209216
<div id="err" class="err" style="display:none"></div>
210217

211218
<div class="grid" id="results" style="display:none">
@@ -267,6 +274,18 @@ <h2>Aroma</h2>
267274
const TASTE_COLORS = {sweet:'var(--sweet)',bitter:'var(--bitter)',umami:'var(--umami)',
268275
sour:'var(--sour)',salty:'var(--salty)'};
269276
const MODEL_TASTES = ['sweet','bitter','umami'];
277+
// a distinct color per aroma descriptor, so each note reads at a glance
278+
const AROMA_COLORS = {fruity:'#E8913C',citrus:'#EFC01F',floral:'#E86AA6',minty:'#2FB37E',
279+
sweet:'#E0972E',woody:'#9C6B3F',spicy:'#D9534F',almond:'#C9A66B',vanilla:'#D6A93C',
280+
camphor:'#4FB0C4',medicinal:'#7E57C2',ammoniacal:'#8A97A3',ethereal:'#66C2D6',
281+
pungent:'#E4572E',garlic:'#8FA31E',fishy:'#4F8FC0',sulfurous:'#B8A93A',rose:'#E07A9E',
282+
herbal:'#5AA469',green:'#6DB33F',earthy:'#8B7355',caramel:'#C68A3E',nutty:'#B0813F',
283+
buttery:'#E6C34D',balsamic:'#9A6B8C',smoky:'#7A6B5D',anise:'#C05A6E',coconut:'#CBB994',
284+
cinnamon:'#B5561F',clove:'#8A5A2B',coffee:'#6A4632',cocoa:'#6B4226',meaty:'#A0522D',
285+
cheesy:'#D9B84D',creamy:'#E3CE9A',waxy:'#C4BE96',fresh:'#4FC0B0',grassy:'#7DAF3A',
286+
rancid:'#9A8B5A',tarry:'#4A4038',winey:'#8E4B6E',honey:'#E0A83C',apple:'#8DBF4A',
287+
cherry:'#C0392B',pine:'#3FA06B',banana:'#E6C84D'};
288+
const aromaColor = n => AROMA_COLORS[n] || 'var(--aroma)';
270289

271290
const $ = id => document.getElementById(id);
272291
const go = $('go'), q = $('q');
@@ -377,7 +396,7 @@ <h2>Aroma</h2>
377396
${x.iupac?`<div class="nb-iupac">${x.iupac}</div>`:''}
378397
<div class="s">${x.smiles}</div>
379398
<div class="kt">${x.known_tastes.length?x.known_tastes.join(' · '):'—'}</div>
380-
${(x.aroma&&x.aroma.length)?`<div class="nb-aroma" title="aroma (found = documented, else predicted)">${x.aroma.map(a=>`<span class="atag">${a.odor}${a.source==='predicted'?' •':''}</span>`).join('')}</div>`:''}
399+
${(x.aroma&&x.aroma.length)?`<div class="nb-aroma" title="aroma (found = documented, else predicted)">${x.aroma.map(a=>`<span class="atag" style="color:${aromaColor(a.odor)};border-color:${aromaColor(a.odor)}44">${a.odor}${a.source==='predicted'?' •':''}</span>`).join('')}</div>`:''}
381400
</div>
382401
<div class="sim">${Math.round(x.similarity*100)}% match</div>
383402
</div>`).join('');
@@ -444,7 +463,7 @@ <h2>Aroma</h2>
444463
if(pred && (pred.descriptors||[]).length){
445464
const lowconf = pred.any_confident===false;
446465
html += `<div class="aroma-prev-tag">Predicted descriptors · model (presence/absence, public-domain HSDB)${lowconf?' — low confidence (no strong descriptor)':''}</div>`;
447-
html += pred.descriptors.map(d=>{ const pct=Math.round(d.score*100); const au=d.auroc?` · AUROC ${d.auroc}`:''; return `<div class="ameter"><div class="arow"><span class="aname">${d.odor}</span><span class="aval">${pct}%${au}</span></div><div class="atrack"><div class="afill" style="width:${pct}%"></div></div></div>`; }).join('');
466+
html += pred.descriptors.map(d=>{ const pct=Math.round(d.score*100); const col=aromaColor(d.odor); const au=d.auroc?` · AUROC ${d.auroc}`:''; return `<div class="ameter"><div class="arow"><span class="aname" style="color:${col}">${d.odor}</span><span class="aval">${pct}%${au}</span></div><div class="atrack"><div class="afill" style="width:${pct}%;background:${col}"></div></div></div>`; }).join('');
448467
}
449468
const doc = ar.documented;
450469
if(doc && (doc.notes||[]).length){

0 commit comments

Comments
 (0)