fix(ui): show configured fallback models missing from catalog

From PR #1322.

Co-authored-by: renatomott <renato.mott@gmail.com>
This commit is contained in:
nesquena-hermes
2026-04-30 15:24:34 +00:00
parent 92121324a0
commit e86de0aff3
2 changed files with 67 additions and 11 deletions
+55 -11
View File
@@ -469,6 +469,18 @@ function renderModelDropdown(){
_modelData.push({value:child.value,name:esc(child.textContent||getModelLabel(child.value)),id:esc(child.value),group:'',badge:_getConfiguredModelBadge(child.value,_badgeMap)});
}
}
const _existingConfiguredKeys=new Set(_modelData.map(existing=>_normalizeConfiguredModelKey(existing.value)));
for(const [modelId,badge] of Object.entries(_badgeMap)){
if(_existingConfiguredKeys.has(_normalizeConfiguredModelKey(modelId))) continue;
_modelData.push({
value:modelId,
name:esc(getModelLabel(modelId)),
id:esc(modelId),
group:'',
badge,
});
_existingConfiguredKeys.add(_normalizeConfiguredModelKey(modelId));
}
// Create search input FIRST before filterModels definition
const _scopeNote=document.createElement('div');
_scopeNote.className='model-scope-note';
@@ -487,6 +499,15 @@ function renderModelDropdown(){
_custRow.innerHTML=`<input class="model-custom-input" type="text" placeholder="${esc(t('model_custom_placeholder')||'e.g. openai/gpt-5.4')}" spellcheck="false" autocomplete="off"><button class="model-custom-btn" title="Use this model">${li('plus',12)}</button>`;
const _ci=_custRow.querySelector('.model-custom-input');
const _cb=_custRow.querySelector('.model-custom-btn');
const _configuredRank=(badge)=>{
if(!badge) return Number.POSITIVE_INFINITY;
if(badge.role==='primary') return 0;
if(badge.role==='fallback'){
const m=String(badge.label||'').match(/fallback\s+(\d+)/i);
return m?Number(m[1]):999;
}
return 500;
};
// Filter function (defined AFTER _searchRow and _cust* are created)
const _filterModels=(term)=>{
term=term.trim().toLowerCase();
@@ -498,6 +519,16 @@ function renderModelDropdown(){
found.add(m.value);
}
}
const matches=(m)=>!term||found.has(m.value);
const configuredModels=_modelData
.filter(m=>m.badge&&matches(m))
.sort((a,b)=>{
const configuredRankA=_configuredRank(a.badge);
const configuredRankB=_configuredRank(b.badge);
if(configuredRankA!==configuredRankB) return configuredRankA-configuredRankB;
return a.name.localeCompare(b.name);
});
const configuredIds=new Set(configuredModels.map(m=>m.value));
// Clear and rebuild
dd.innerHTML='';
// Add search and custom elements first (CRITICAL: must be before models)
@@ -505,17 +536,12 @@ function renderModelDropdown(){
dd.appendChild(_searchRow);
dd.appendChild(_custSep);
dd.appendChild(_custRow);
// Add models matching filter
let _lastGroup=null;
for(const m of _modelData){
if(!term||found.has(m.value)){
if(m.group&&m.group!==_lastGroup){
const heading=document.createElement('div');
heading.className='model-group';
heading.textContent=m.group;
dd.appendChild(heading);
_lastGroup=m.group;
}
if(configuredModels.length){
const configuredHeading=document.createElement('div');
configuredHeading.className='model-group';
configuredHeading.textContent=t('model_group_configured')||'Configured';
dd.appendChild(configuredHeading);
for(const m of configuredModels){
const row=document.createElement('div');
row.className='model-opt'+(m.value===sel.value?' active':'');
const badgeHtml=m.badge?`<span class="model-opt-badge model-opt-badge--${esc(m.badge.role||'configured')}">${esc(m.badge.label||'Configured')}</span>`:'';
@@ -524,6 +550,24 @@ function renderModelDropdown(){
dd.appendChild(row);
}
}
// Add remaining models matching filter
let _lastGroup=null;
for(const m of _modelData){
if(configuredIds.has(m.value)||!matches(m)) continue;
if(m.group&&m.group!==_lastGroup){
const heading=document.createElement('div');
heading.className='model-group';
heading.textContent=m.group;
dd.appendChild(heading);
_lastGroup=m.group;
}
const row=document.createElement('div');
row.className='model-opt'+(m.value===sel.value?' active':'');
const badgeHtml=m.badge?`<span class="model-opt-badge model-opt-badge--${esc(m.badge.role||'configured')}">${esc(m.badge.label||'Configured')}</span>`:'';
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${m.name}</span>${badgeHtml}</div><span class="model-opt-id">${m.id}</span>`;
row.onclick=()=>selectModelFromDropdown(m.value);
dd.appendChild(row);
}
// Show "No results" if filtered and nothing matched
if(term&&found.size===0){
const noResult=document.createElement('div');
+12
View File
@@ -106,6 +106,18 @@ def test_ui_renders_model_badges_from_api_payload():
"A UI precisa de um helper de matching resiliente para religar badges mesmo quando "
"o update do catálogo mudar prefixos/formas do model ID."
)
assert "model_group_configured" in js, (
"renderModelDropdown() deve expor uma seção Configured no topo para destacar a "
"cadeia primária + fallback antes dos providers completos."
)
assert "configuredRank" in js, (
"A UI deve calcular uma prioridade estável (primary -> fallback 1 -> fallback N) "
"para renderizar os modelos configurados no topo do dropdown."
)
assert "Object.entries(_badgeMap)" in js and "_normalizeConfiguredModelKey(existing.value)" in js, (
"renderModelDropdown() deve sintetizar entradas para modelos configurados ausentes "
"do catálogo atual, senão fallbacks locais/Ollama desaparecem da seção Configured."
)
# Chip-projected badge was removed in v0.50.243 (added too much width to the
# composer chip; signal value low since the model name is right next to it).
# Badges remain in the dropdown rows (model-opt-badge) for picker rows.