From 2a52349668c1068935415f3fae8613b98dfae864 Mon Sep 17 00:00:00 2001 From: oliver Date: Tue, 11 Aug 2026 05:05:52 +0800 Subject: [PATCH] Restyle chat model picker as a themed custom dropdown. Drop the bordered native select chrome and open a soft popup that follows light/dark theme tokens. Co-authored-by: Cursor --- interfaces/admin/static/chat.html | 84 +++++++++++------ interfaces/admin/static/chat.js | 108 ++++++++++++++++------ interfaces/admin/static/theme-presets.css | 39 +++++--- 3 files changed, 164 insertions(+), 67 deletions(-) diff --git a/interfaces/admin/static/chat.html b/interfaces/admin/static/chat.html index e624c93f..09d62fe8 100644 --- a/interfaces/admin/static/chat.html +++ b/interfaces/admin/static/chat.html @@ -337,55 +337,87 @@ display: flex; flex-wrap: wrap; align-items: center; - gap: 8px; - padding: 2px 4px 8px; + gap: 4px; + padding: 0 2px 6px; } .chat-composer-chip, .chat-composer-chip--select { appearance: none; -webkit-appearance: none; - height: 30px; - min-height: 30px; - max-height: 30px; - padding: 0 12px; + height: 28px; + min-height: 28px; + max-height: 28px; + padding: 0 10px; border-radius: 999px; - border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.28)); - background-color: var(--ds-surface-2, #152033); - color: var(--ds-text, #e2e8f0); + border: none; + background-color: transparent; + color: var(--ds-text-muted, rgba(148, 163, 184, 0.92)); font-size: 12px; font-weight: 500; line-height: 1; box-sizing: border-box; cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease; } .chat-composer-chip--select { - min-width: 148px; - max-width: 240px; - padding-right: 28px; - background-color: var(--ds-surface-2, #152033); + min-width: 0; + max-width: 220px; + width: auto; + padding-right: 22px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + background-color: transparent; background-image: - linear-gradient(45deg, transparent 50%, var(--ds-text-muted, rgba(148, 163, 184, 0.85)) 50%), - linear-gradient(135deg, var(--ds-text-muted, rgba(148, 163, 184, 0.85)) 50%, transparent 50%); + linear-gradient(45deg, transparent 50%, var(--ds-text-muted, rgba(148, 163, 184, 0.7)) 50%), + linear-gradient(135deg, var(--ds-text-muted, rgba(148, 163, 184, 0.7)) 50%, transparent 50%); background-position: - calc(100% - 14px) calc(50% - 2px), - calc(100% - 9px) calc(50% - 2px); - background-size: 5px 5px, 5px 5px; + calc(100% - 10px) calc(50% - 1.5px), + calc(100% - 6px) calc(50% - 1.5px); + background-size: 4px 4px, 4px 4px; background-repeat: no-repeat; } - .chat-composer-chip--select option { - background-color: var(--ds-surface, #0f172a); - color: var(--ds-text, #e2e8f0); - } .chat-composer-chip:hover, .chat-composer-chip--select:hover { - background-color: var(--ds-menu-hover-bg, rgba(148, 163, 184, 0.16)); - border-color: var(--ds-accent-border, rgba(94, 179, 255, 0.35)); + background-color: var(--ds-menu-hover-bg, rgba(148, 163, 184, 0.12)); + color: var(--ds-text, #e2e8f0); } .chat-composer-chip:focus-visible, .chat-composer-chip--select:focus-visible { outline: none; - border-color: var(--ds-accent-border-strong, rgba(94, 179, 255, 0.5)); - box-shadow: 0 0 0 2px var(--ds-accent-soft, rgba(94, 179, 255, 0.2)); + background-color: var(--ds-accent-soft, rgba(94, 179, 255, 0.14)); + color: var(--ds-text, #e2e8f0); + box-shadow: none; + } + .chat-model-select { + position: relative; + display: inline-flex; + max-width: 100%; + } + .chat-model-select-pop { + border-radius: 12px; + padding: 6px; + min-width: 11rem; + max-width: min(320px, calc(100vw - 16px)); + max-height: min(320px, 50vh); + overflow-y: auto; + border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.18)); + background: var(--ds-panel, var(--ds-surface, #0f172a)); + box-shadow: var(--ds-pop-shadow, 0 12px 32px rgba(2, 6, 23, 0.45)); + } + .chat-model-select-pop .chat-sess-menu-item { + border-radius: 8px; + padding: 9px 12px; + font-size: 13px; + line-height: 1.35; + } + .chat-model-select-pop .chat-sess-menu-item--active { + background: var(--ds-accent-soft, rgba(59, 130, 246, 0.18)); + color: var(--ds-text, #e2e8f0); + font-weight: 600; + } + .chat-model-select-pop .chat-sess-menu-item--active:hover { + background: var(--ds-accent-soft, rgba(59, 130, 246, 0.22)); } .chat-composer-shell { border-radius: 12px; diff --git a/interfaces/admin/static/chat.js b/interfaces/admin/static/chat.js index d9eb5073..fef673c5 100644 --- a/interfaces/admin/static/chat.js +++ b/interfaces/admin/static/chat.js @@ -3473,11 +3473,71 @@ async function renderChatUi() { }); let globalMenuModeValue = String(localStorage.getItem(CHAT_USER_MENU_MODE_KEY) || MAIN_MODE_VALUE).toLowerCase(); if (globalMenuModeValue === "comprehensive" || globalMenuModeValue === "main") globalMenuModeValue = MAIN_MODE_VALUE; - const modelSelect = el("select", { + const modelSelectBtn = el("button", { + type: "button", class: "chat-composer-chip chat-composer-chip--select", title: t("chat.activeModelLabel"), + text: "-", }); + const modelSelectWrap = el("div", { class: "chat-model-select" }, [modelSelectBtn]); let modelSelectNameToId = new Map(); + let modelSelectOptions = []; + let modelSelectedKey = ""; + const syncModelSelectBtn = () => { + const hit = modelSelectOptions.find((o) => o.key === modelSelectedKey); + modelSelectBtn.textContent = String((hit && hit.label) || "-"); + }; + const openModelSelectMenu = (anchorEl) => { + dismissChatMenus(); + const items = modelSelectOptions.length + ? modelSelectOptions.map((opt) => + el("button", { + type: "button", + class: + "chat-sess-menu-item" + + (opt.key === modelSelectedKey ? " chat-sess-menu-item--active" : ""), + text: opt.label, + onclick: async (ev) => { + ev.stopPropagation(); + dismissChatMenus(); + const key = String(opt.key || "").trim(); + if (!key || key === modelSelectedKey) return; + modelSelectedKey = key; + syncModelSelectBtn(); + const pid = String(modelSelectNameToId.get(key) || "").trim(); + if (!pid) return; + try { + await apiPost("/admin/api/models/active", { profile_id: pid }); + await refreshActiveModelText(); + } catch (e) { + statusBar.textContent = `${t("chat.error")}: ${String(e)}`; + } + }, + }), + ) + : [el("div", { class: "chat-sess-menu-item muted", text: "-" })]; + const menu = el("div", { class: "chat-sess-menu-pop chat-model-select-pop u-pop-menu" }, items); + menu.style.position = "fixed"; + menu.style.zIndex = "300"; + const rect = (anchorEl || modelSelectBtn).getBoundingClientRect(); + attachChatMenuDismiss(menu); + const mrect = menu.getBoundingClientRect(); + const pad = 8; + let left = rect.left; + let top = rect.bottom + 6; + if (top + mrect.height > window.innerHeight - pad) { + top = Math.max(pad, rect.top - 6 - mrect.height); + } + left = Math.max(pad, Math.min(left, window.innerWidth - pad - mrect.width)); + menu.style.left = `${left}px`; + menu.style.top = `${top}px`; + menu.style.minWidth = `${Math.max(168, Math.ceil(rect.width))}px`; + }; + modelSelectBtn.addEventListener("click", (ev) => { + ev.stopPropagation(); + if (modelSelectBtn.disabled) return; + openModelSelectMenu(ev.currentTarget); + }); const normalizeExecutionMode = (v) => { const raw = String(v || "").trim().toLowerCase(); return raw === EXECUTION_MODE_PLAN ? EXECUTION_MODE_PLAN : EXECUTION_MODE_AGENT; @@ -3585,8 +3645,8 @@ async function renderChatUi() { const activeModelText = el("span", { class: "muted", text: `${t("chat.activeModelLabel")}: -` }); const applyModelSelectorVisibility = (modelsState) => { const show = !(modelsState && modelsState.chat_model_selector_visible === false); - modelSelect.style.display = show ? "" : "none"; - modelSelect.disabled = !show; + modelSelectWrap.style.display = show ? "" : "none"; + modelSelectBtn.disabled = !show; }; const refreshActiveModelText = async () => { try { @@ -3597,8 +3657,8 @@ async function renderChatUi() { const p = profiles.find((x) => String(x.id || "") === aid) || null; const modelName = String((p && p.model) || "").trim() || "-"; activeModelText.textContent = `${t("chat.activeModelLabel")}: ${modelName}`; - modelSelect.innerHTML = ""; modelSelectNameToId = new Map(); + modelSelectOptions = []; const usedKeys = new Set(); profiles.forEach((row) => { const pid = String(row && row.id ? row.id : ""); @@ -3613,38 +3673,30 @@ async function renderChatUi() { } usedKeys.add(key); modelSelectNameToId.set(key, pid); - const display = String(row.model || row.name || pid); - modelSelect.appendChild(el("option", { value: key, text: display })); + modelSelectOptions.push({ + key, + id: pid, + label: String(row.model || row.name || pid), + }); }); if (p) { - const rawName = String(p.name || "").trim(); const firstKey = Array.from(modelSelectNameToId.keys()).find((k) => modelSelectNameToId.get(k) === aid) || ""; - const key = firstKey || rawName; - if (key && Array.from(modelSelect.options).some((o) => String(o.value || "") === key)) { - modelSelect.value = key; - } - } else if (modelSelect.options.length > 0) { - modelSelect.value = String(modelSelect.options[0].value || ""); + modelSelectedKey = firstKey || ""; + } else if (modelSelectOptions.length > 0) { + modelSelectedKey = String(modelSelectOptions[0].key || ""); + } else { + modelSelectedKey = ""; } + syncModelSelectBtn(); } catch (_) { applyModelSelectorVisibility(null); activeModelText.textContent = `${t("chat.activeModelLabel")}: -`; - modelSelect.innerHTML = ""; - modelSelect.appendChild(el("option", { value: "", text: "-" })); - modelSelect.value = ""; + modelSelectNameToId = new Map(); + modelSelectOptions = []; + modelSelectedKey = ""; + syncModelSelectBtn(); } }; - modelSelect.addEventListener("change", async () => { - const selectedKey = String(modelSelect.value || "").trim(); - const pid = String(modelSelectNameToId.get(selectedKey) || "").trim(); - if (!pid) return; - try { - await apiPost("/admin/api/models/active", { profile_id: pid }); - await refreshActiveModelText(); - } catch (e) { - statusBar.textContent = `${t("chat.error")}: ${String(e)}`; - } - }); const loadSessionModePreference = async () => { if (!activeId) { setExecutionMode(currentExecutionMode, { persistLocal: true, saveSession: false }); @@ -3781,7 +3833,7 @@ async function renderChatUi() { }); const composerMetaBar = el("div", { class: "chat-composer-meta" }, [ execSelectWrap, - modelSelect, + modelSelectWrap, compressBtn, ]); diff --git a/interfaces/admin/static/theme-presets.css b/interfaces/admin/static/theme-presets.css index 136e761b..ee6e4fb0 100644 --- a/interfaces/admin/static/theme-presets.css +++ b/interfaces/admin/static/theme-presets.css @@ -321,32 +321,45 @@ body.theme-ds-body[data-admin-theme="light"] .chat-composer { body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip, body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select { - background-color: #ffffff; - border-color: rgba(15, 23, 42, 0.16); - color: #0f172a; + background-color: transparent; + border: none; + color: rgba(15, 23, 42, 0.62); } body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select { - background-color: #ffffff; + background-color: transparent; background-image: - linear-gradient(45deg, transparent 50%, rgba(15, 23, 42, 0.55) 50%), - linear-gradient(135deg, rgba(15, 23, 42, 0.55) 50%, transparent 50%); + linear-gradient(45deg, transparent 50%, rgba(15, 23, 42, 0.45) 50%), + linear-gradient(135deg, rgba(15, 23, 42, 0.45) 50%, transparent 50%); background-position: - calc(100% - 14px) calc(50% - 2px), - calc(100% - 9px) calc(50% - 2px); - background-size: 5px 5px, 5px 5px; + calc(100% - 10px) calc(50% - 1.5px), + calc(100% - 6px) calc(50% - 1.5px); + background-size: 4px 4px, 4px 4px; background-repeat: no-repeat; } -body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select option { - background-color: #ffffff; +body.theme-ds-body[data-admin-theme="light"] .chat-model-select-pop { + background: #ffffff; + border-color: rgba(15, 23, 42, 0.12); + box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12); +} + +body.theme-ds-body[data-admin-theme="light"] .chat-model-select-pop .chat-sess-menu-item--active { + background: rgba(37, 99, 235, 0.12); color: #0f172a; } body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip:hover, body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select:hover { - background-color: #f8fafc; - border-color: rgba(37, 99, 235, 0.35); + background-color: rgba(15, 23, 42, 0.05); + color: #0f172a; +} + +body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip:focus-visible, +body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select:focus-visible { + background-color: rgba(37, 99, 235, 0.1); + color: #0f172a; + box-shadow: none; } body.theme-ds-body[data-admin-theme="light"] .chat-composer-shell {