diff --git a/training/workbench.html b/training/workbench.html index 09b7953..9fa1d07 100644 --- a/training/workbench.html +++ b/training/workbench.html @@ -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) */ @@ -1210,6 +1221,37 @@

Software & type

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 & 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 & amber', ['woody','cedarwood','sandalwood','vetiver','patchouli','pine','fir','terpenic', + 'resinous','amber','labdanum','frankincense','myrrh','opoponax','elemi','styrax','oakmoss','costus', + 'davana','balsamic']], + ['Sweet & bakery', ['vanilla','caramel','maple','honey','tonka','bready','popcorn','malty','nutty', + 'hazelnut','almond','cocoa','coffee','coconut','lactonic']], + ['Dairy & fatty', ['buttery','creamy','cheesy','fatty','waxy','rancid']], + ['Savoury & sulfur', ['meaty','truffle','garlic','onion','sulfurous','mushroom','fishy']], + ['Earthy, tobacco & leather', ['earthy','musty','corky','tobacco','leathery']], + ['Fresh, cool & marine', ['fresh','marine','ozonic','aldehydic','minty','cooling','camphor', + 'eucalyptus','wintergreen']], + ['Animalic & musk', ['animalic','musky','powdery']], + ['Sharp & 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'); @@ -2302,16 +2344,31 @@

Software & type

other:'other',misc:'other'}; const svgIcon = c => ``; // Flavors — named flavors grouped by category, each with its glyph - let html = `
Flavors a flavor you know → the molecule that makes it
`; + // one filter across every chip — with 300+ of them, scanning is not a search strategy + let html = `
+
`; + html += `
Flavors a flavor you know → the molecule that makes it
`; html += cats.map(c=>`
${svgIcon(c.category)}${c.category}`+ c.flavors.map(f=>``).join('')+`
`).join(''); html += `
`; - // 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 += `
Notes an aroma/taste quality → molecules that carry it
`+ - `
`+ - notes.map(n=>``).join('')+ - `
`; + 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 += `
Notes an aroma quality → molecules that carry it
`+ + groups.map(([fam, ms])=> + `
${fam} ${ms.length}
`+ + `
`+ + ms.map(n=>``).join('')+ + `
`).join('')+ + `
`; } // Mouthfeel — trained trigeminal sensations (a different modality from taste and aroma) if(mouth.length){ @@ -2326,8 +2383,36 @@

Software & type

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) : '';