9595 # mixResults {margin-top : 14px }
9696 .hazard {border : 1px solid # E7C4B8 ;background : # FBEDE9 ;color : # 8A3A22 ;border-radius : 8px ;padding : 10px 13px ;margin-bottom : 8px ;font-size : 13px ;line-height : 1.5 }
9797 .hazard .cond {border-color : # E8D38A ;background : # FFF6E6 ;color : # 7A5A12 }
98+ .suggest-dd {position : absolute;z-index : 60 ;background : var (--panel );border : 1px solid var (--line );border-radius : 9px ;box-shadow : 0 8px 24px # 00000022 ;max-height : 340px ;overflow : auto}
99+ .sg-row {display : flex;gap : 10px ;align-items : center;padding : 8px 10px ;cursor : pointer;border-bottom : 1px solid var (--line )}
100+ .sg-row : last-child {border-bottom : none}
101+ .sg-row : hover {background : var (--accent-soft )}
102+ .sg-st {flex : 0 0 auto;width : 56px ;height : 42px ;background : # fff ;border : 1px solid var (--line );border-radius : 5px }
103+ .sg-st svg {width : 56px ;height : 42px }
104+ .sg-info {min-width : 0 }
105+ .sg-name {font-weight : 600 ;font-size : 13px }
106+ .sg-iupac {font-family : var (--mono );font-size : 10px ;color : var (--muted );word-break : break-all}
107+ .sg-smi {font-family : var (--mono );font-size : 11px ;color : var (--accent );word-break : break-all}
98108</ style >
99109</ head >
100110< body >
@@ -104,9 +114,8 @@ <h1>Flavor Workbench</h1>
104114</ header >
105115< main >
106116 < div class ="bar ">
107- < input id ="q " list =" suggest " placeholder ="Compound name or SMILES — e.g. vanillin, or OC(=O)CC(O)(CC(=O)O)C(=O)O "
117+ < input id ="q " placeholder ="Compound name or SMILES — e.g. vanillin, or OC(=O)CC(O)(CC(=O)O)C(=O)O "
108118 autocomplete ="off " spellcheck ="false ">
109- < datalist id ="suggest "> </ datalist >
110119 < button id ="go "> Read flavor</ button >
111120 </ div >
112121 < div class ="hint "> Type a < b > common or IUPAC name</ b > (e.g. < code > vanillin</ code > ) or a
@@ -251,9 +260,10 @@ <h2>Aroma</h2>
251260 p . sour && p . sour_reason && p . sour_reason . length ? `sour · ${ p . sour_reason [ 0 ] } ` : 'not sour' ) ) ;
252261 c . push ( chip ( 'salty' , p . salty === true , p . salty === true ?'var(--salty)' :null ,
253262 p . salty === true ? 'salty (known)' : 'salty — not in data' ) ) ;
254- if ( inDomain && p . sweet >= 0.5 && typeof p . sweet_intensity === 'number' ) {
263+ const logP = ( ( p . physchem || { } ) . computed || { } ) . logP ;
264+ if ( inDomain && p . sweet >= 0.5 && typeof p . sweet_intensity === 'number' && typeof logP === 'number' && logP < 2 ) {
255265 const x = Math . pow ( 10 , p . sweet_intensity ) ;
256- c . push ( chip ( 'intensity' , true , 'var(--sweet)' , `~${ x >= 10 ?Math . round ( x ) :x . toFixed ( 1 ) } × as sweet as sucrose` ) ) ;
266+ c . push ( chip ( 'intensity' , true , 'var(--sweet)' , `~${ x >= 10 ?Math . round ( x ) :x . toFixed ( 1 ) } × as sweet as sucrose (est.) ` ) ) ;
257267 }
258268 if ( inDomain && p . multitaste ) c . push ( chip ( 'multi' , true , 'var(--aroma)' , 'multi-taste' ) ) ;
259269 $ ( 'chips' ) . innerHTML = c . join ( '' ) ;
@@ -340,30 +350,40 @@ <h2>Aroma</h2>
340350 if ( nb && nb . dataset . smi ) { q . value = nb . dataset . smi ; window . scrollTo ( { top :0 , behavior :'smooth' } ) ; run ( ) ; }
341351} ) ;
342352
343- // --- name typeahead (datalist) over the curated flavor-volatile list, on any input ---
344- let suggestT ;
345- function wireSuggest ( input ) {
346- input . setAttribute ( 'list' , 'suggest' ) ;
353+ // --- rich typeahead dropdown (structure + names + SMILES) on any input ---
354+ function attachRichSuggest ( input , onPick ) {
355+ const dd = document . createElement ( 'div' ) ; dd . className = 'suggest-dd' ; dd . style . display = 'none' ;
356+ document . body . appendChild ( dd ) ;
357+ let t ;
358+ const close = ( ) => { dd . style . display = 'none' ; } ;
359+ const place = ( ) => { const r = input . getBoundingClientRect ( ) ; dd . style . left = ( r . left + window . scrollX ) + 'px' ; dd . style . top = ( r . bottom + window . scrollY + 4 ) + 'px' ; dd . style . width = r . width + 'px' ; } ;
347360 input . addEventListener ( 'input' , ( ) => {
348- clearTimeout ( suggestT ) ;
349- suggestT = setTimeout ( async ( ) => {
361+ clearTimeout ( t ) ;
362+ t = setTimeout ( async ( ) => {
350363 const v = input . value . trim ( ) ;
351- if ( v . length < 2 ) { $ ( 'suggest' ) . innerHTML = '' ; return ; }
364+ if ( v . length < 2 ) { close ( ) ; return ; }
352365 try {
353366 const r = await fetch ( '/api/suggest?qs=' + encodeURIComponent ( v ) ) ;
354- const d = await r . json ( ) ;
355- $ ( 'suggest' ) . innerHTML = ( d . items || [ ] ) . map ( it => `<option value="${ it . name } ">` ) . join ( '' ) ;
356- } catch ( _ ) { }
357- } , 160 ) ;
367+ const items = ( await r . json ( ) ) . items || [ ] ;
368+ if ( ! items . length ) { close ( ) ; return ; }
369+ dd . innerHTML = items . map ( it => `<div class="sg-row" data-smi="${ it . smiles } ">
370+ <div class="sg-st">${ it . svg || '' } </div>
371+ <div class="sg-info"><div class="sg-name">${ it . name } </div>${ it . iupac ?`<div class="sg-iupac">${ it . iupac } </div>` :'' } <div class="sg-smi">${ it . smiles } </div></div></div>` ) . join ( '' ) ;
372+ place ( ) ; dd . style . display = 'block' ;
373+ } catch ( _ ) { close ( ) ; }
374+ } , 150 ) ;
358375 } ) ;
376+ dd . addEventListener ( 'mousedown' , e => { const row = e . target . closest ( '.sg-row' ) ; if ( row ) { e . preventDefault ( ) ; input . value = row . dataset . smi ; close ( ) ; if ( onPick ) onPick ( ) ; } } ) ;
377+ input . addEventListener ( 'blur' , ( ) => setTimeout ( close , 150 ) ) ;
378+ window . addEventListener ( 'scroll' , ( ) => { if ( dd . style . display !== 'none' ) place ( ) ; } , true ) ;
359379}
360- wireSuggest ( q ) ;
380+ attachRichSuggest ( q , ( ) => run ( ) ) ;
361381
362382// --- mixture mode ---
363383function mixAddRow ( val ) {
364384 const div = document . createElement ( 'div' ) ; div . className = 'mix-row' ;
365385 const inp = document . createElement ( 'input' ) ; inp . placeholder = 'name or SMILES' ; inp . value = val || '' ;
366- wireSuggest ( inp ) ;
386+ attachRichSuggest ( inp ) ;
367387 const rm = document . createElement ( 'button' ) ; rm . className = 'rm' ; rm . textContent = '×' ; rm . title = 'remove' ;
368388 rm . onclick = ( ) => div . remove ( ) ;
369389 div . append ( inp , rm ) ; $ ( 'mixRows' ) . appendChild ( div ) ;
0 commit comments