Skip to content

Commit fdf8a9d

Browse files
Demo overhaul: dark mode, header/branding, mixture+map UX, social meta (#95)
- Dark mode theme (purple/teal brand accents; molecule drawings on light 'paper'). - Header: large centered logo, Cinzel Decorative wordmark, big uppercase warm-cream tagline; vendored OFL font (wordmark.ttf) served locally. - Flavor-space map + Mixture screen open by default; toggle labels match the browse head. - Mixture screen reworked into its own section (no longer hides search) with a plain explainer + clickable documented bad-combo examples (benzene, nitrosamine, acrylamide, ethyl carbamate). - Taste chips (taste-colored) on substitution cards; per-descriptor aroma colors. - Open Graph / Twitter meta + a 1280x640 social banner (static/social.png). - DATA-REQUIREMENTS: aroma presence/absence ships; intensity is the 'comes with your data'. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
1 parent 0ab747f commit fdf8a9d

5 files changed

Lines changed: 101 additions & 33 deletions

File tree

docs/DATA-REQUIREMENTS.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,17 @@ already exists in `predict.py`; supply the data and the feature reports real num
6868

6969
---
7070

71-
## 3. Aroma — odor-descriptor prediction (the marquee "comes with your data")
72-
73-
Aroma is **not** in the commercial edition (the rich public odor data is NonCommercial —
74-
the GS-LF set; see [`AROMA.md`](AROMA.md)). It is trained **for a customer on their own
75-
data**, on-prem, or on a **commercially-licensed** set (Leffingwell **PMP 2001**).
71+
## 3. Aroma — odor-descriptor prediction (intensity is the "comes with your data")
72+
73+
Aroma **ships in the commercial edition as presence/absence** — 13 RandomForest descriptor
74+
heads (citrus, floral, minty, almond, …) trained on **public-domain** PubChem/HSDB odor text
75+
(CV-AUROC 0.80–0.96; see [`AROMA.md`](AROMA.md)). What it does **not** have is **scored
76+
intensity***how strong* each note is — because **no public-domain intensity-scored odor
77+
data exists** (the one clean scored set, keller_2016, is naive-subject noise; every expert
78+
intensity set — GS-LF, Dravnieks — is NonCommercial/proprietary). So the marquee upgrade is
79+
**intensity**, trained **for a customer on their own panel data**, on-prem, or on a
80+
**commercially-licensed** set (Leffingwell **PMP 2001**). Richer/rarer descriptors come with
81+
that data too.
7682

7783
**What we need to train an aroma model:**
7884
- **Molecules:** `smiles` — or **GC-MS** output identifying the volatile compounds in your

training/static/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,10 @@ self-contained (no CDN dependency, works on an air-gapped/LAN box).
88
(permissive, commercial-OK with attribution). Renders the 3D conformer that
99
`/api/structure3d` generates (RDKit ETKDG embed + MMFF optimize). `3Dmol-min.js.LICENSE.txt`
1010
is the upstream license header extracted by the build.
11+
- **`wordmark.ttf`**[Cinzel Decorative](https://fonts.google.com/specimen/Cinzel+Decorative)
12+
(Natanael Gama), the header wordmark font. **License: SIL Open Font License 1.1** (free for
13+
commercial use). Served locally via `/static/{fname}`.
14+
- **`logo.png`** — the Flavormancer badge (256px), header logo.
15+
- **`social.png`** — 1280×640 Open-Graph / social-preview banner (logo + gradient wordmark).
16+
Referenced by the demo's OG meta tags; also upload it as the GitHub repo's Social preview
17+
(Settings → General → Social preview) so the repo link renders richly when shared.

training/static/social.png

120 KB
Loading

training/static/wordmark.ttf

58.9 KB
Binary file not shown.

training/workbench.html

Lines changed: 83 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,44 @@
33
<head>
44
<meta charset="utf-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1">
6-
<title>Flavormancer</title>
6+
<title>Flavormancer — taste &amp; aroma from chemical structure</title>
7+
<meta name="description" content="Flavormancer predicts taste &amp; aroma from a molecule's chemical structure — on-prem flavor prediction for food, beverage, and fragrance R&amp;D.">
8+
<meta property="og:type" content="website">
9+
<meta property="og:site_name" content="Flavormancer">
10+
<meta property="og:title" content="Flavormancer — taste &amp; aroma from chemical structure">
11+
<meta property="og:description" content="Predict taste &amp; aroma from chemical structure. On-prem, before you pour.">
12+
<meta property="og:image" content="/static/social.png">
13+
<meta name="twitter:card" content="summary_large_image">
14+
<meta name="twitter:title" content="Flavormancer — taste &amp; aroma from chemical structure">
15+
<meta name="twitter:description" content="Predict taste &amp; aroma from chemical structure. On-prem, before you pour.">
16+
<meta name="twitter:image" content="/static/social.png">
717
<style>
18+
@font-face{font-family:'Cinzel Decorative';src:url('/static/wordmark.ttf') format('truetype');font-weight:700;font-display:swap}
819
:root{
9-
--surface:#F4F6F4; --panel:#FFFFFF; --line:#DCE3DE; --ink:#172019;
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%);
13-
--sweet:#E0972E; --bitter:#6B4E9E; --umami:#C0563B; --sour:#9FB024; --salty:#4E84B0; --aroma:#2E7D6B;
20+
--surface:#0F1319; --panel:#181D24; --line:#2A323C; --ink:#E7EDEA;
21+
--muted:#94A2A9; --accent:#2BC4C4; --accent-soft:#2BC4C42A;
22+
--brand-1:#8A6BE0; --brand-2:#2BC4C4; --brand-accent:#E0913C; --cream:#D9AB74;
23+
--brand-grad:linear-gradient(100deg,#8A6BE0 0%,#4E84C8 45%,#2BC4C4 100%);
24+
--sweet:#E8A94A; --bitter:#A88AE0; --umami:#E0805E; --sour:#BFD24E; --salty:#63A6E0; --aroma:#3FC4AA;
25+
--struct-bg:#F5F7F5; /* light 'paper' for molecule drawings (RDKit draws dark structures) */
1426
--ui:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
1527
--mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;
1628
}
1729
*{box-sizing:border-box}
1830
body{margin:0;background:var(--surface);color:var(--ink);font-family:var(--ui);
1931
font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased}
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}
26-
header .sub{font-family:var(--mono);font-size:12px;color:var(--muted)}
32+
header{padding:34px 28px 26px;border-bottom:1px solid var(--line);display:flex;
33+
flex-direction:column;align-items:center;gap:6px;text-align:center;
34+
background:radial-gradient(130% 150% at 50% -30%, #1D2A38 0%, var(--surface) 62%)}
35+
header .logo{width:172px;height:172px;margin-bottom:16px;
36+
filter:drop-shadow(0 6px 30px rgba(120,150,230,.32))}
37+
header .brand{display:flex;flex-direction:column;align-items:center;gap:8px}
38+
header h1{margin:0;font-family:'Cinzel Decorative',"Luminari",Georgia,fantasy;
39+
font-size:46px;font-weight:700;letter-spacing:.02em;line-height:1;
40+
background:var(--brand-grad);-webkit-background-clip:text;background-clip:text;color:transparent;
41+
filter:drop-shadow(0 2px 16px rgba(110,140,220,.32))}
42+
header .sub{font-family:'Cinzel Decorative',"Luminari",Georgia,serif;font-size:15px;
43+
font-weight:700;text-transform:uppercase;letter-spacing:.16em;color:var(--cream)}
2744
main{max-width:980px;margin:0 auto;padding:26px 28px 60px}
2845
.bar{display:flex;gap:10px;align-items:stretch}
2946
.bar input{flex:1;font-family:var(--mono);font-size:15px;padding:13px 15px;
@@ -69,7 +86,10 @@
6986
.neighbor .nb-name{font-weight:600;font-size:13px;text-transform:capitalize;color:var(--ink)}
7087
.neighbor .nb-iupac{font-family:var(--mono);font-size:10px;color:var(--muted);word-break:break-all;margin:1px 0}
7188
.nb-aroma{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px}
72-
.nb-aroma .atag{font-size:10px;padding:1px 7px;border-radius:20px;background:#EAF3F0;color:var(--aroma);border:1px solid #CFE6DF;text-transform:capitalize}
89+
.nb-aroma .atag{font-size:10px;padding:1px 7px;border-radius:20px;background:transparent;color:var(--aroma);border:1px solid;text-transform:capitalize}
90+
.kt{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px}
91+
.kt .tchip{font-size:10px;font-weight:600;padding:1px 8px;border-radius:20px;color:#10140F;text-transform:capitalize}
92+
.kt .muted{color:var(--muted);font-size:11px}
7393
.browse{margin:6px 0 8px}
7494
.browse-head{font-size:12px;font-weight:650;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 0 9px;display:flex;align-items:center;justify-content:space-between;gap:8px}
7595
.browse-toggle{display:inline-flex}
@@ -94,8 +114,8 @@
94114
.bcell .bname{font-size:12px;font-weight:600;margin-top:5px;line-height:1.25;text-transform:capitalize;cursor:pointer}
95115
.bcell .bname:hover{color:var(--aroma)}
96116
.map-section{margin:16px 0 8px;border-top:1px solid #EEF0EC;padding-top:12px}
97-
.map-head{font-size:13px;font-weight:650;cursor:pointer;color:var(--ink)}
98-
.map-head .mapsub{font-weight:400;font-size:12px;color:var(--muted)}
117+
.map-head{font-size:12px;font-weight:650;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);cursor:pointer}
118+
.map-head .mapsub{font-weight:600;text-transform:none;letter-spacing:0;font-size:12px;color:var(--muted);opacity:.8}
99119
.map-legend{display:flex;flex-wrap:wrap;gap:10px;margin:10px 0 6px;font-size:12px}
100120
.map-legend span{display:inline-flex;align-items:center;gap:5px}
101121
.map-legend i{width:10px;height:10px;border-radius:50%;display:inline-block}
@@ -152,8 +172,13 @@
152172
.s3-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:13px}
153173
.domain-banner{background:#FFF6E6;border:1px solid #E8D38A;color:#7A5A12;border-radius:8px;padding:10px 13px;font-size:13px;margin-bottom:14px;line-height:1.5}
154174
.hint code{font-family:var(--mono);background:var(--accent-soft);padding:1px 5px;border-radius:4px;color:var(--accent)}
155-
.toggle{font-family:var(--mono);font-size:12px;color:var(--muted);margin:12px 2px 0;display:flex;align-items:center;gap:7px;cursor:pointer}
156-
#mixPanel{margin-top:16px;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:18px 20px}
175+
.toggle{font-size:12px;font-weight:650;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:14px 2px 0;display:flex;align-items:center;gap:7px;cursor:pointer}
176+
#mixPanel{margin-top:12px;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:18px 20px}
177+
.mix-explain{font-size:12.5px;color:var(--muted);line-height:1.55;margin:0 0 12px;max-width:840px}
178+
.mix-explain b{color:var(--ink)}
179+
.mix-examples{font-size:12px;color:var(--muted);margin:0 0 13px;display:flex;flex-wrap:wrap;gap:7px;align-items:center}
180+
.mix-ex{font:inherit;font-size:11.5px;padding:3px 10px;border:1px solid var(--line);border-radius:16px;background:transparent;color:var(--brand-2);cursor:pointer}
181+
.mix-ex:hover{border-color:var(--brand-2)}
157182
.mix-row{display:flex;gap:8px;margin-bottom:8px}
158183
.mix-row input{flex:1;font-family:var(--mono);font-size:14px;padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--surface)}
159184
.mix-row .rm{border:1px solid var(--line);background:var(--surface);border-radius:8px;padding:0 13px;cursor:pointer;color:var(--muted)}
@@ -171,7 +196,17 @@
171196
.sg-row{display:flex;gap:10px;align-items:center;padding:8px 10px;cursor:pointer;border-bottom:1px solid var(--line)}
172197
.sg-row:last-child{border-bottom:none}
173198
.sg-row:hover{background:var(--accent-soft)}
174-
.sg-st{flex:0 0 auto;width:56px;height:42px;background:#fff;border:1px solid var(--line);border-radius:5px}
199+
.sg-st{flex:0 0 auto;width:56px;height:42px;background:var(--struct-bg);border:1px solid var(--line);border-radius:5px}
200+
/* --- dark-mode surface fixes (molecule drawings stay on light 'paper' --struct-bg) --- */
201+
.cat,.st-tab,.mini-3d,.map-reset,.suggest-dd{background:var(--panel)}
202+
.structure,.structure3d,.neighbor .nb-struct,.bcell .bstruct{background:var(--struct-bg)}
203+
.bcell{background:var(--panel)}
204+
.track,.atrack,.tag.predicted{background:#242C35}
205+
#flavorMap{background:#0C1016}
206+
.odor-note{background:#1F2831;border-color:#2E3742}
207+
.aroma-card{background:linear-gradient(180deg,#17282A,#181D24)!important;border-color:#2C4A44!important}
208+
.err{background:#3A1E18;border-color:#5A2E22;color:#F0C6BA}
209+
.cat.on{color:#fff}
175210
.sg-st svg{width:56px;height:42px}
176211
.sg-info{min-width:0}
177212
.sg-name{font-weight:600;font-size:13px}
@@ -197,8 +232,20 @@ <h1>Flavormancer</h1>
197232
<b>SMILES</b> string (e.g. <code>O=Cc1ccc(O)c(OC)c1</code>). SMILES omit implicit
198233
hydrogens — water is just <code>O</code>. These models are for <b>organic</b> molecules.</div>
199234

200-
<label class="toggle"><input type="checkbox" id="mixToggle"> Mixture mode — screen 2+ ingredients for documented hazards</label>
201-
<div id="mixPanel" style="display:none">
235+
<label class="toggle"><input type="checkbox" id="mixToggle" checked> <span id="mixCaret"></span> Mixture screen — flag documented hazards when ingredients combine</label>
236+
<div id="mixPanel">
237+
<p class="mix-explain">Enter two or more ingredients and Flavormancer flags <b>documented
238+
precursor→product hazards</b> — combinations known to form a harmful compound (e.g. a
239+
preservative + an acid → a carcinogen). It's a <b>curated safety screen, not a general
240+
reaction predictor</b>: a hazard fires only when the required roles co-occur, and
241+
process-gated ones (heat / refining / fermentation) only when you mark that process.
242+
It also gives a per-ingredient taste read and a single-molecule palette match.</p>
243+
<div class="mix-examples"><span>Try a known bad combo:</span>
244+
<button type="button" class="mix-ex" data-ings="sodium benzoate|ascorbic acid">benzoate + vitamin&nbsp;C → benzene</button>
245+
<button type="button" class="mix-ex" data-ings="sodium nitrite|dimethylamine">nitrite + amine → nitrosamine</button>
246+
<button type="button" class="mix-ex" data-ings="glucose|asparagine" data-proc="high_heat">sugar + asparagine + heat → acrylamide</button>
247+
<button type="button" class="mix-ex" data-ings="ethanol|urea">ethanol + urea → ethyl carbamate</button>
248+
</div>
202249
<div id="mixRows"></div>
203250
<button class="mix-add" id="mixAdd">+ add ingredient</button>
204251
<div class="mix-proc">Process applied:
@@ -217,9 +264,9 @@ <h1>Flavormancer</h1>
217264
</div>
218265

219266
<div class="map-section">
220-
<div class="map-head" id="mapToggle"><span id="mapCaret"></span> Flavor-space map
267+
<div class="map-head" id="mapToggle"><span id="mapCaret"></span> Flavor-space map
221268
<span class="mapsub">— molecules placed by structural similarity; taste classes cluster</span></div>
222-
<div id="mapWrap" style="display:none">
269+
<div id="mapWrap">
223270
<div class="map-controls">
224271
<span class="map-toggle3d"><button type="button" id="map2d" class="st-tab on">2D</button><button type="button" id="map3d" class="st-tab">3D</button></span>
225272
<span id="mapLegend" class="map-legend"></span>
@@ -421,7 +468,7 @@ <h2>Aroma</h2>
421468
<div class="nb-name">${x.name||'—'}</div>
422469
${x.iupac?`<div class="nb-iupac">${x.iupac}</div>`:''}
423470
<div class="s">${x.smiles}</div>
424-
<div class="kt">${x.known_tastes.length?x.known_tastes.join(' · '):'—'}</div>
471+
<div class="kt">${x.known_tastes.length?x.known_tastes.map(t=>`<span class="tchip" style="background:${TASTE_COLORS[t]||'var(--muted)'}">${t}</span>`).join(''):'<span class="muted">no known taste</span>'}</div>
425472
${(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>`:''}
426473
</div>
427474
<div class="sim">${Math.round(x.similarity*100)}% match</div>
@@ -641,7 +688,7 @@ <h2>Aroma</h2>
641688
initBrowse();
642689

643690
// --- flavor-space map: 2D (zoom/pan) + rotatable 3D point cloud, colored by taste ---
644-
const MAP_COLORS = {sweet:'#E0972E',bitter:'#6B4E9E',umami:'#C0563B',sour:'#9FB024',salty:'#4E84B0',other:'#C3C9C0'};
691+
const MAP_COLORS = {sweet:'#E8A94A',bitter:'#A88AE0',umami:'#E0805E',sour:'#BFD24E',salty:'#63A6E0',other:'#8A96A0'};
645692
let _mapPts=null, _mapReady=false, _mapMode='2d', _mapProj=null;
646693
let _v2={s:1,ox:0,oy:0}, _v3={rx:-0.45,ry:0.6}, _drag=null, _dragged=false;
647694

@@ -712,6 +759,7 @@ <h2>Aroma</h2>
712759
cv.addEventListener('click', ()=>{ if(_dragged){ _dragged=false; return; } const p=_mapLastHover; if(p){ q.value=p.smiles; window.scrollTo({top:0,behavior:'smooth'}); run(); } });
713760
}
714761
let _mapLastHover=null;
762+
loadMap(); // map is open by default
715763

716764
// --- mixture mode ---
717765
function mixAddRow(val){
@@ -723,13 +771,20 @@ <h2>Aroma</h2>
723771
div.append(inp, rm); $('mixRows').appendChild(div);
724772
}
725773
$('mixAdd').addEventListener('click', ()=>mixAddRow());
774+
// mixture is its own collapsible section — it no longer hides the search bar/results
726775
$('mixToggle').addEventListener('change', e=>{
727776
const on=e.target.checked;
728-
$('mixPanel').style.display=on?'block':'none';
729-
document.querySelector('.bar').style.display=on?'none':'flex';
730-
['results','behaviorCard','aromaCard','footnote','err'].forEach(id=>$(id).style.display='none');
777+
$('mixPanel').style.display=on?'block':'none'; $('mixCaret').textContent=on?'▾':'▸';
731778
if(on && !$('mixRows').children.length){ mixAddRow(); mixAddRow(); }
732779
});
780+
if($('mixToggle').checked && !$('mixRows').children.length){ mixAddRow(); mixAddRow(); } // open by default
781+
// click a bad-combo example -> load its ingredients + process and run the screen
782+
document.querySelectorAll('.mix-ex').forEach(b=> b.addEventListener('click', ()=>{
783+
$('mixRows').innerHTML='';
784+
b.dataset.ings.split('|').forEach(s=>mixAddRow(s));
785+
document.querySelectorAll('.proc').forEach(c=>{ c.checked = (b.dataset.proc||'').split(',').includes(c.value); });
786+
$('mixCheck').click();
787+
}));
733788
$('mixResults').addEventListener('click', e=>{
734789
const card=e.target.closest('.mix-ing[data-smi]');
735790
if(card && card.dataset.smi){ $('mixToggle').checked=false; $('mixToggle').dispatchEvent(new Event('change')); q.value=card.dataset.smi; window.scrollTo({top:0,behavior:'smooth'}); run(); }

0 commit comments

Comments
 (0)