Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 91 additions & 6 deletions training/workbench.html
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,17 @@
.studio-count.on{min-width:18px;width:auto;padding:0 6px;margin-left:8px;opacity:1}
.schip{font:inherit;font-size:12px;padding:3px 11px;border:1px solid var(--line);border-radius:20px;background:var(--panel);color:var(--ink);cursor:pointer;text-transform:capitalize}
.schip:hover{border-color:var(--brand-2)}
/* one filter across all 300+ chips, plus perceptual family blocks so notes are scannable */
.chip-filter{position:relative;margin:0 0 12px}
.chip-filter input{width:100%;font:inherit;font-size:13px;padding:9px 13px;border-radius:9px;
border:1px solid var(--line);background:var(--panel);color:var(--ink)}
.chip-filter input:focus{outline:none;border-color:var(--brand-2)}
.cf-none{position:absolute;right:12px;top:50%;transform:translateY(-50%);font-family:var(--mono);
font-size:11px;color:var(--muted)}
.fam{margin:0 0 10px}
.fam-label{font-family:var(--mono);font-size:10.5px;letter-spacing:.07em;text-transform:uppercase;
color:var(--muted);margin:0 0 5px;display:flex;align-items:baseline;gap:7px}
.fam-n{font-size:10px;opacity:.6}
.schip-note{border-style:dashed}
/* mouthfeel chips: a distinct dotted edge + amber cast so the sensation modality reads apart
from odour notes at a glance (cooling/pungent legitimately appear in both rows) */
Expand Down Expand Up @@ -1210,6 +1221,37 @@ <h4>Software &amp; type</h4>
alcoholic:'#9FB6C4',musky:'#8B7BA5',putrid:'#6E7A4A',soapy:'#B9C6D0',vegetable:'#6FA36B',bready:'#C7A06A',odorless:'#9AA4AC',
onion:'#B7A64B',neroli:'#F0A5A0',aldehydic:'#E8C070',animalic:'#8A6B5A',amber:'#C08A4E',leathery:'#6E5544',powdery:'#D9C6D0',resinous:'#7A8B5A',terpenic:'#6B9E5A',ozonic:'#6FC2C4',hyacinth:'#8FC46A',cooling:'#6FD0C0',metallic:'#9BB0BE',lactonic:'#E3C99A',popcorn:'#D9B87A',celery:'#A8C46A',maple:'#B5773A',chamomile:'#E8D888',tobacco:'#8A6B4A',carnation:'#D9607A',mimosa:'#E8D77A',magnolia:'#E6E29A',narcissus:'#E0C878',saffron:'#E0700E',tuberose:'#EBA9C0',gardenia:'#E8C9D6',ylang:'#D9A6C4',lilac:'#C9A6D6',osmanthus:'#E8B48A',geranium:'#D77A8E',eucalyptus:'#5FA98A',corky:'#8A8A6E',violetleaf:'#7DB36A',fir:'#3E7A5A',oakmoss:'#6E7A4A',vetiver:'#7A6B4A',patchouli:'#8A5A3A',sandalwood:'#C9A87A',cedarwood:'#9C6B4A',cognac:'#A85A2E',grapefruit:'#E8846A',bergamot:'#C8D45A',mandarin:'#EE9A2E',lime:'#8FC44A',yuzu:'#D4D46A',honeysuckle:'#E8C86A',freesia:'#E0D0A0',cyclamen:'#D98AB0',linden:'#C8D488',coriander:'#9DBE6A',cumin:'#B5883A',passionfruit:'#E0C84A',rosemary:'#5A8A6A',blackpepper:'#6E5A4A',nutmeg:'#A8703A',sage:'#8AA07A',thyme:'#7A9A5A',oregano:'#8A9A4A',mustard:'#D9C84A',pineapple:'#EBD24A',strawberry:'#E0506A',raspberry:'#C0396B',myrrh:'#8A6B4E',frankincense:'#B0A07A',musty:'#8AA07A',hazelnut:'#C6B07A',blueberry:'#C08A6A',guava:'#9AA6B0',juniper:'#C6B07A',allspice:'#B0A07A',dill:'#8AA07A',tomato:'#C08A6A',fig:'#C08A6A',mango:'#B0A07A',champaca:'#C6B07A',styrax:'#8AA07A',davana:'#B0A07A',truffle:'#8AA07A',clarysage:'#C08A6A',opoponax:'#8AA07A',turmeric:'#9AA6B0',costus:'#C6B07A',elemi:'#9AA6B0',labdanum:'#C6B07A',};
const aromaColor = n => AROMA_COLORS[n] || 'var(--aroma)';
// Perceptual families for the 160+ odour notes. A flat alphabetical wall of chips is unusable —
// a flavorist thinks in families, so the picker groups by them. Anything not listed lands in
// "Other", so a newly-trained head is never silently dropped from the UI.
const AROMA_FAMILIES = [
['Citrus', ['citrus','orange','lime','grapefruit','bergamot','mandarin','yuzu','neroli']],
['Fruity', ['fruity','apple','pear','quince','peach','apricot','plum','cherry','berry','strawberry',
'raspberry','blueberry','blackberry','cassis','grape','banana','melon','watermelon','lychee','fig',
'mango','pineapple','tropical','passionfruit','guava']],
['Floral', ['floral','rose','jasmine','violet','lilac','lavender','muguet','ylang','gardenia','tuberose',
'honeysuckle','magnolia','mimosa','narcissus','hyacinth','freesia','cyclamen','osmanthus','champaca',
'linden','carnation','geranium','elderflower','boronia','chamomile','immortelle','tagetes']],
['Green &amp; vegetal', ['green','grassy','hay','herbal','vegetable','tomato','celery','cucumber',
'violetleaf','coriander','angelica','wormwood','tea']],
['Spice', ['spicy','cinnamon','clove','nutmeg','allspice','blackpepper','cumin','cardamom','ginger',
'anise','fennel','dill','saffron','turmeric','mustard','thyme','oregano','sage','rosemary','juniper',
'clarysage']],
['Wood, resin &amp; amber', ['woody','cedarwood','sandalwood','vetiver','patchouli','pine','fir','terpenic',
'resinous','amber','labdanum','frankincense','myrrh','opoponax','elemi','styrax','oakmoss','costus',
'davana','balsamic']],
['Sweet &amp; bakery', ['vanilla','caramel','maple','honey','tonka','bready','popcorn','malty','nutty',
'hazelnut','almond','cocoa','coffee','coconut','lactonic']],
['Dairy &amp; fatty', ['buttery','creamy','cheesy','fatty','waxy','rancid']],
['Savoury &amp; sulfur', ['meaty','truffle','garlic','onion','sulfurous','mushroom','fishy']],
['Earthy, tobacco &amp; leather', ['earthy','musty','corky','tobacco','leathery']],
['Fresh, cool &amp; marine', ['fresh','marine','ozonic','aldehydic','minty','cooling','camphor',
'eucalyptus','wintergreen']],
['Animalic &amp; musk', ['animalic','musky','powdery']],
['Sharp &amp; chemical', ['acidic','alcoholic','ammoniacal','ethereal','petroleum','phenolic','medicinal',
'smoky','tarry','burnt','putrid','metallic','soapy','pungent','winey','cognac']],
['Base taste', ['sweet','bitter']],
];

const $ = id => document.getElementById(id);
const go = $('go'), q = $('q');
Expand Down Expand Up @@ -2302,16 +2344,31 @@ <h4>Software &amp; type</h4>
other:'other',misc:'other'};
const svgIcon = c => `<svg class="ic"><use href="#ic-${CAT_ICON[String(c).toLowerCase()]||'other'}"/></svg>`;
// Flavors — named flavors grouped by category, each with its glyph
let html = `<div class="studio-section"><div class="ss-head"><svg class="ic"><use href="#ic-flavors"/></svg> Flavors <span class="ss-sub">a flavor you know → the molecule that makes it</span></div><div class="ss-body">`;
// one filter across every chip — with 300+ of them, scanning is not a search strategy
let html = `<div class="chip-filter"><input id="studioFilter" type="search" autocomplete="off"
placeholder="Filter ${cats.reduce((a,c)=>a+(c.flavors||[]).length,0)+notes.length+mouth.length} flavors, notes &amp; sensations — e.g. citrus, vanilla, cool">
<span id="studioFilterNone" class="cf-none" hidden>no match</span></div>`;
html += `<div class="studio-section"><div class="ss-head"><svg class="ic"><use href="#ic-flavors"/></svg> Flavors <span class="ss-sub">a flavor you know → the molecule that makes it</span></div><div class="ss-body">`;
html += cats.map(c=>`<div class="studio-group"><span class="sg-label" title="${esc(c.category)}"><span class="sg-ico">${svgIcon(c.category)}</span>${c.category}</span>`+
c.flavors.map(f=>`<button type="button" class="schip schip-flavor" data-term="${esc(f)}">${f}</button>`).join('')+`</div>`).join('');
html += `</div></div>`;
// Notes — model-predicted aroma/taste qualities
// Notes — grouped into perceptual families, because 170+ chips in one alphabetical row is
// unreadable. Unlisted heads fall into "Other" so nothing is silently dropped.
if(notes.length){
html += `<div class="studio-section"><div class="ss-head"><svg class="ic"><use href="#ic-notes"/></svg> Notes <span class="ss-sub">an aroma/taste quality → molecules that carry it</span></div><div class="ss-body">`+
`<div class="studio-group studio-group-notes">`+
notes.map(n=>`<button type="button" class="schip schip-note" data-term="${esc(n)}">${n}</button>`).join('')+
`</div></div></div>`;
const left = new Set(notes), groups = [];
for(const [fam, members] of AROMA_FAMILIES){
const have = members.filter(m=>left.has(m));
have.forEach(m=>left.delete(m));
if(have.length) groups.push([fam, have]);
}
if(left.size) groups.push(['Other', [...left].sort()]);
html += `<div class="studio-section"><div class="ss-head"><svg class="ic"><use href="#ic-notes"/></svg> Notes <span class="ss-sub">an aroma quality → molecules that carry it</span></div><div class="ss-body">`+
groups.map(([fam, ms])=>
`<div class="fam"><div class="fam-label">${fam} <span class="fam-n">${ms.length}</span></div>`+
`<div class="studio-group studio-group-notes">`+
ms.map(n=>`<button type="button" class="schip schip-note" data-term="${esc(n)}" title="${esc(AROMA_HELP[n]||n)}">${n}</button>`).join('')+
`</div></div>`).join('')+
`</div></div>`;
}
// Mouthfeel — trained trigeminal sensations (a different modality from taste and aroma)
if(mouth.length){
Expand All @@ -2326,8 +2383,36 @@ <h4>Software &amp; type</h4>
const t=b.dataset.term; if(_studioSel.has(t)){ _studioSel.delete(t); b.classList.remove('on'); } else { _studioSel.add(t); b.classList.add('on'); }
syncStudioCount();
}));
wireStudioFilter();
}catch(_){ setTimeout(initStudio, 4000); }
}
// Live filter over every chip. Hides non-matching chips, then collapses any family / category /
// section left empty, so filtering never leaves a trail of orphan headings. A selected chip always
// stays visible — otherwise your own picks vanish as you type and the count looks wrong.
function wireStudioFilter(){
const inp=$('studioFilter'); if(!inp) return;
const apply=()=>{
const q=inp.value.trim().toLowerCase();
let shown=0;
$('studioChips').querySelectorAll('.schip').forEach(b=>{
const hit = !q || b.textContent.toLowerCase().includes(q) || b.classList.contains('on');
b.hidden = !hit; if(hit) shown++;
});
// roll empties up: chip rows, then family blocks / flavor categories, then whole sections
$('studioChips').querySelectorAll('.studio-group').forEach(g=>{
g.hidden = !g.querySelector('.schip:not([hidden])');
});
$('studioChips').querySelectorAll('.fam').forEach(f=>{
f.hidden = !f.querySelector('.schip:not([hidden])');
});
$('studioChips').querySelectorAll('.studio-section').forEach(s=>{
s.hidden = !s.querySelector('.schip:not([hidden])');
});
const none=$('studioFilterNone'); if(none) none.hidden = shown>0;
};
inp.addEventListener('input', apply);
inp.addEventListener('search', apply); // clicking the native clear (×)
}
function syncStudioCount(){
const n=_studioSel.size, el=$('studioGoCount'); if(!el) return;
el.textContent = n ? String(n) : '';
Expand Down
Loading