feat: replace model select with searchable dropdown in Chat tab
Custom dropdown with search filter and max-height scroll (220px). Models grouped by provider with sticky group headers. Click outside to close. Handles 60+ models without overwhelming the UI.
This commit is contained in:
@@ -115,7 +115,19 @@ tbody tr:last-child td{border-bottom:none}
|
||||
.ch-c{display:flex;flex-direction:column;height:calc(100vh - 56px - 3rem);overflow:hidden}
|
||||
.ch-t{display:flex;gap:.5rem;align-items:center;padding:.6rem .85rem;background:var(--bg1);border:1px solid var(--bd);
|
||||
border-radius:var(--rl) var(--rl) 0 0;font-size:.74rem;flex-wrap:wrap;box-shadow:var(--sh1)}
|
||||
.ch-t select{max-width:260px;font-size:.74rem;padding:.35rem .5rem}.ch-t select.cmod-sel{max-width:320px}
|
||||
.ch-t select{max-width:260px;font-size:.74rem;padding:.35rem .5rem}
|
||||
.mdrop{position:relative;display:inline-block}
|
||||
.mdrop-btn{background:var(--bg2);border:1px solid var(--bd);border-radius:8px;padding:.35rem .6rem;font-size:.74rem;cursor:pointer;max-width:320px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center;gap:.3rem;min-width:180px}
|
||||
.mdrop-btn:hover{border-color:var(--ac)}
|
||||
.mdrop-btn .arr{font-size:.55rem;margin-left:auto;opacity:.5}
|
||||
.mdrop-dd{position:absolute;top:100%;left:0;z-index:100;background:var(--bg1);border:1px solid var(--bd);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.12);margin-top:4px;min-width:320px;max-width:400px;display:none}
|
||||
.mdrop-dd.open{display:block}
|
||||
.mdrop-dd input{width:100%;padding:.45rem .6rem;border:none;border-bottom:1px solid var(--bd);font-size:.74rem;background:var(--bg2);border-radius:10px 10px 0 0;outline:none;box-sizing:border-box}
|
||||
.mdrop-list{max-height:220px;overflow-y:auto;padding:.25rem 0}
|
||||
.mdrop-list .grp{font-size:.64rem;font-weight:700;color:var(--t3);padding:.4rem .7rem .15rem;text-transform:uppercase;letter-spacing:.04em}
|
||||
.mdrop-list .itm{padding:.3rem .7rem;font-size:.74rem;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.mdrop-list .itm:hover{background:var(--bg3)}
|
||||
.mdrop-list .itm.sel{background:var(--pl);color:var(--p);font-weight:600}
|
||||
.ch-params{padding:.55rem .85rem;background:var(--bg1);border-left:1px solid var(--bd);border-right:1px solid var(--bd);border-bottom:1px solid var(--bg3);font-size:.72rem}
|
||||
.ch-params .g3{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem;margin-bottom:.3rem}
|
||||
.ch-params label{font-weight:600;color:var(--t3);font-size:.66rem;margin-bottom:2px;display:block}
|
||||
@@ -265,14 +277,19 @@ function rPg(){switch(S.tab){case'chat':return rChat();case'explorer':return rEx
|
||||
function rChat(){
|
||||
const ms=S.msgs.length===0?'<div class="emp"><div class="eic">🤖</div><p>Inicie uma conversa com o agente.</p><p style="font-size:.74rem;margin-top:.35rem">Selecione um modelo ou use o agente local.</p></div>'
|
||||
:S.msgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div></div>`).join('');
|
||||
// Build model select with optgroups
|
||||
const presetOpts=S.genaiCfg.map(g=>{const mi=S.models[g.model_id];return`<option value="cfg:${g.id}" ${S.chatModel==='cfg:'+g.id?'selected':''}>${g.name||mi?.name||g.model_id} (${g.genai_region})</option>`}).join('');
|
||||
// Build custom dropdown items
|
||||
const skip=new Set(['openai.gpt-image-1','openai.gpt-image-1.5','openai.gpt-audio','meta.llama-guard-4-12b','protectai.deberta-v3-base-prompt-injection-v2',
|
||||
'openai.gpt-5.3-codex','openai.gpt-5.2-codex','openai.gpt-5.1-codex','openai.gpt-5.1-codex-max','openai.gpt-5.1-codex-mini','openai.gpt-5-codex',
|
||||
'openai.gpt-oss-120b','openai.gpt-oss-20b']);
|
||||
const provs={};for(const[mid,info]of Object.entries(S.models)){if(skip.has(mid))continue;const p=info.provider||'other';if(!provs[p])provs[p]=[];provs[p].push({id:mid,name:info.name})}
|
||||
const provOrder=['openai','google','meta','cohere','xai'];
|
||||
const catalogOpts=provOrder.filter(p=>provs[p]).map(p=>{const label=p.charAt(0).toUpperCase()+p.slice(1);const opts=provs[p].map(m=>`<option value="${m.id}" ${S.chatModel===m.id?'selected':''}>${m.name}</option>`).join('');return`<optgroup label="${label}">${opts}</optgroup>`}).join('');
|
||||
let ddItems=`<div class="itm${!S.chatModel?' sel':''}" onclick="pickModel('')">Agente Local (sem IA)</div>`;
|
||||
if(S.genaiCfg.length){ddItems+=`<div class="grp">Configs Salvas</div>`;S.genaiCfg.forEach(g=>{const mi=S.models[g.model_id];ddItems+=`<div class="itm${S.chatModel==='cfg:'+g.id?' sel':''}" onclick="pickModel('cfg:${g.id}')">${g.name||mi?.name||g.model_id} (${g.genai_region})</div>`})}
|
||||
provOrder.filter(p=>provs[p]).forEach(p=>{ddItems+=`<div class="grp">${p.charAt(0).toUpperCase()+p.slice(1)}</div>`;provs[p].forEach(m=>{ddItems+=`<div class="itm${S.chatModel===m.id?' sel':''}" onclick="pickModel('${m.id}')">${m.name}</div>`})});
|
||||
// Current label
|
||||
let curLabel='Agente Local (sem IA)';
|
||||
if(S.chatModel.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===S.chatModel.slice(4));const mi=g?S.models[g.model_id]:null;curLabel=g?(g.name||mi?.name||g.model_id)+' ('+g.genai_region+')':'Config...'}
|
||||
else if(S.chatModel){const mi=S.models[S.chatModel];curLabel=mi?mi.name:S.chatModel}
|
||||
const isDirect=S.chatModel&&!S.chatModel.startsWith('cfg:');
|
||||
const hasModel=!!S.chatModel;
|
||||
// OCI selector for direct mode
|
||||
@@ -291,14 +308,21 @@ function rChat(){
|
||||
<div><label>Pres Penalty</label><input type="number" value="${pp.presence_penalty}" step="0.1" min="0" max="2" onchange="S.chatParams.presence_penalty=+this.value"></div>
|
||||
</div></div>`:'';
|
||||
return`<div class="ch-c">
|
||||
<div class="ch-t"><label>🧠 Modelo:</label><select id="cmod" class="cmod-sel" onchange="chatModelChanged()"><option value="">Agente Local (sem IA)</option>${presetOpts?'<optgroup label="Configs Salvas">'+presetOpts+'</optgroup>':''}${catalogOpts}</select>
|
||||
<div class="ch-t"><label>🧠 Modelo:</label><div class="mdrop"><div class="mdrop-btn" onclick="toggleModelDrop()">${curLabel} <span class="arr">▼</span></div><div class="mdrop-dd" id="mdd"><input type="text" id="mds" placeholder="Buscar modelo..." oninput="filterModels(this.value)"><div class="mdrop-list" id="mdl">${ddItems}</div></div></div>
|
||||
${ociSel}${gearBtn}${ragBadge}<div style="flex:1"></div><button class="btn bd bsm" onclick="clrChat()">Limpar</button></div>
|
||||
${paramsPanel}<div class="ch-m" id="chm">${ms}</div>
|
||||
<div class="ch-i"><input type="text" id="chi" placeholder="Digite sua mensagem..." onkeydown="if(event.key==='Enter')sChat()">
|
||||
<button class="btn bp" onclick="sChat()">Enviar →</button></div></div>`}
|
||||
function chatModelChanged(){S.chatModel=document.getElementById('cmod').value;
|
||||
if(S.chatModel&&!S.chatModel.startsWith('cfg:')&&!S.chatOci&&S.ociCfg.length){S.chatOci=S.ociCfg[0].id;const c=S.ociCfg[0];S.chatRegion=c.region;S.chatCompartment=c.compartment_id||''}
|
||||
function toggleModelDrop(){const dd=document.getElementById('mdd');dd.classList.toggle('open');if(dd.classList.contains('open')){const inp=document.getElementById('mds');inp.value='';inp.focus();filterModels('')}}
|
||||
function pickModel(v){S.chatModel=v;document.getElementById('mdd').classList.remove('open');
|
||||
if(v&&!v.startsWith('cfg:')&&!S.chatOci&&S.ociCfg.length){S.chatOci=S.ociCfg[0].id;const c=S.ociCfg[0];S.chatRegion=c.region;S.chatCompartment=c.compartment_id||''}
|
||||
R()}
|
||||
function filterModels(q){const list=document.getElementById('mdl');const items=list.querySelectorAll('.itm,.grp');const ql=q.toLowerCase();
|
||||
let lastGrp=null,grpHasVisible=false;
|
||||
items.forEach(el=>{if(el.classList.contains('grp')){if(lastGrp)lastGrp.style.display=grpHasVisible?'':'none';lastGrp=el;grpHasVisible=false}
|
||||
else{const show=!q||el.textContent.toLowerCase().includes(ql);el.style.display=show?'':'none';if(show)grpHasVisible=true}});
|
||||
if(lastGrp)lastGrp.style.display=grpHasVisible?'':'none'}
|
||||
document.addEventListener('click',e=>{const dd=document.getElementById('mdd');if(dd&&dd.classList.contains('open')&&!e.target.closest('.mdrop'))dd.classList.remove('open')})
|
||||
function chatOciChanged(){S.chatOci=document.getElementById('coci').value;const c=S.ociCfg.find(x=>x.id===S.chatOci);if(c){S.chatRegion=c.region;S.chatCompartment=c.compartment_id||''}}
|
||||
function toggleChatParams(){S.chatParamsOpen=!S.chatParamsOpen;R()}
|
||||
function fm(t){return t.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>').replace(/`(.*?)`/g,'<code>$1</code>').replace(/\n/g,'<br>')}
|
||||
|
||||
Reference in New Issue
Block a user