diff --git a/static/index.html b/static/index.html index 91a79fd6..1e4997a8 100644 --- a/static/index.html +++ b/static/index.html @@ -401,6 +401,7 @@ + diff --git a/static/style.css b/static/style.css index 5f15ecbe..c0c63196 100644 --- a/static/style.css +++ b/static/style.css @@ -838,10 +838,14 @@ .reasoning-option:hover{background:rgba(255,255,255,.07);} .reasoning-option.selected{background:var(--accent-bg);} .composer-model-wrap{position:relative;flex:0 1 auto;min-width:0;} - .composer-model-chip{display:inline-flex;align-items:center;gap:8px;max-width:220px;padding:8px 10px 8px 12px;border-radius:999px;border:1px solid transparent;background-color:transparent;color:var(--muted);font-weight:500;cursor:pointer;transition:color .15s,background-color .15s,border-color .15s;} + .composer-model-chip{display:inline-flex;align-items:center;gap:8px;max-width:280px;padding:8px 10px 8px 12px;border-radius:999px;border:1px solid transparent;background-color:transparent;color:var(--muted);font-weight:500;cursor:pointer;transition:color .15s,background-color .15s,border-color .15s;} .composer-model-chip:hover{color:var(--text);background-color:var(--hover-bg);} .composer-model-chip.active{color:var(--text);background:var(--accent-bg);border-color:var(--accent-bg);} .composer-model-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} + .composer-model-badge{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;padding:2px 7px;border-radius:999px;font-size:10px;font-weight:700;letter-spacing:.02em;text-transform:uppercase;border:1px solid transparent;} + .composer-model-badge[hidden]{display:none;} + .composer-model-badge--primary{background:rgba(50,184,198,.16);border-color:rgba(50,184,198,.32);color:#8fe7ef;} + .composer-model-badge--fallback{background:rgba(255,184,77,.14);border-color:rgba(255,184,77,.28);color:#ffd18a;} .composer-model-icon,.composer-model-chevron{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;} .composer-model-select{position:absolute!important;left:-9999px!important;width:1px!important;height:1px!important;opacity:0!important;pointer-events:none!important;} .composer-right{display:flex;gap:8px;align-items:center;flex-shrink:0;} diff --git a/static/ui.js b/static/ui.js index a64a34a8..9af09710 100644 --- a/static/ui.js +++ b/static/ui.js @@ -337,12 +337,28 @@ function syncModelChip(){ const sel=$('modelSelect'); const chip=$('composerModelChip'); const label=$('composerModelLabel'); + const badgeEl=$('composerModelBadge'); const dd=$('composerModelDropdown'); if(!sel||!chip||!label) return; // Don't show a model label until boot has finished loading to prevent flash of wrong default - if(!S._bootReady){ label.textContent=''; chip.title='Conversation model'; return; } + if(!S._bootReady){ + label.textContent=''; + chip.title='Conversation model'; + if(badgeEl){ + badgeEl.textContent=''; + badgeEl.hidden=true; + badgeEl.className='composer-model-badge'; + } + return; + } const opt=_selectedModelOption(); label.textContent=opt?opt.textContent:getModelLabel(sel.value||''); + const badge=_getConfiguredModelBadge(sel.value||'',window._configuredModelBadges||{}); + if(badgeEl){ + badgeEl.textContent=badge&&badge.label?badge.label:''; + badgeEl.hidden=!badgeEl.textContent; + badgeEl.className='composer-model-badge'+(badge&&badge.role?` composer-model-badge--${badge.role}`:''); + } chip.title=sel.value||'Conversation model'; chip.classList.toggle('active',!!(dd&&dd.classList.contains('open'))); } diff --git a/tests/test_model_picker_badges.py b/tests/test_model_picker_badges.py index 775e31f4..ca036459 100644 --- a/tests/test_model_picker_badges.py +++ b/tests/test_model_picker_badges.py @@ -89,7 +89,11 @@ def test_get_available_models_cache_preserves_configured_model_badges(tmp_path, def test_ui_renders_model_badges_from_api_payload(): - js = (Path(__file__).resolve().parent.parent / "static" / "ui.js").read_text(encoding="utf-8") + root = Path(__file__).resolve().parent.parent + js = (root / "static" / "ui.js").read_text(encoding="utf-8") + html = (root / "static" / "index.html").read_text(encoding="utf-8") + css = (root / "static" / "style.css").read_text(encoding="utf-8") + assert "window._configuredModelBadges=data.configured_model_badges||{};" in js, ( "populateModelDropdown() deve guardar configured_model_badges do /api/models " "para que o dropdown reflita a cadeia configurada atual." @@ -102,3 +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 'id="composerModelBadge"' in html, ( + "O chip principal do modelo precisa de um container dedicado para exibir o badge " + "do modelo selecionado fora do dropdown." + ) + assert "composer-model-badge" in css, ( + "O badge do chip principal precisa de estilo próprio para ficar visível ao lado " + "do nome do modelo selecionado." + ) + assert "const badge=_getConfiguredModelBadge(sel.value||'',window._configuredModelBadges||{});" in js, ( + "syncModelChip() deve buscar o badge configurado do modelo selecionado e projetá-lo " + "no chip principal da composer." + ) + assert "badgeEl.textContent=badge&&badge.label?badge.label:'';" in js, ( + "syncModelChip() deve preencher o texto do badge visível no chip principal quando houver metadata configurada." + )