From 6628cd12206082b0933b18c082d9cbc258af2a2b Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 6 May 2026 18:58:01 +0800 Subject: [PATCH] feat(models): add toggle to hide chat model selector Expose a model-management setting to control chat-page model dropdown visibility, persist it in app settings, and apply it in chat UI rendering. Co-authored-by: Cursor --- interfaces/admin/models_api.py | 24 ++++++++++++++++++++++++ interfaces/admin/static/app.js | 21 +++++++++++++++++++++ interfaces/admin/static/chat.js | 7 +++++++ 3 files changed, 52 insertions(+) diff --git a/interfaces/admin/models_api.py b/interfaces/admin/models_api.py index 11b31e0b..1b28b14f 100644 --- a/interfaces/admin/models_api.py +++ b/interfaces/admin/models_api.py @@ -33,6 +33,16 @@ _LLM_MODE_OPTIONS = frozenset({"openai", "openai_responses", "anthropic", "googl _LLM_USER_CREATE_MODES = frozenset({"openai", "anthropic", "google", "ollama", "rule"}) _OPS_AI_ACTIVE_KEY = "ops_ai_active_llm_profile_id" _OPS_AI_BINDINGS_KEY = "ops_ai_agent_profile_bindings" +_CHAT_MODEL_SELECTOR_VISIBLE_KEY = "AIA_CHAT_MODEL_SELECTOR_VISIBLE" + + +def _truthy(v: Any, *, default: bool = False) -> bool: + if v is None: + return bool(default) + s = str(v).strip().lower() + if not s: + return bool(default) + return s in {"1", "true", "yes", "on"} def _require_permission(ctx: dict[str, Any], permission: str) -> None: @@ -188,6 +198,7 @@ def include_model_mgmt_routes( "role_ids": list(agent_role_ids()), "profile_secret": secret, "can_manage_llm_grants": _can_manage_llm_grants(ctx), + "chat_model_selector_visible": _truthy(store.get_setting(_CHAT_MODEL_SELECTOR_VISIBLE_KEY), default=True), # 便于核对「浏览器连的是哪台网关、网关读的是哪个库文件」 "db_path": db_path(), } @@ -294,6 +305,19 @@ def include_model_mgmt_routes( store.set_setting(_OPS_AI_BINDINGS_KEY, dump_agent_profile_bindings(cur)) return {"ok": True, "ops_ai_bindings": cur} + @mg.post("/chat-ui") + def api_models_set_chat_ui( + payload: dict[str, Any] | None = Body(default=None), + authorization: str | None = Header(default=None), + ) -> dict[str, Any]: + payload = payload or {} + store = SqliteStore(db_path()) + ctx = resolve_auth(store, authorization) + _require_permission(ctx, "admin:tenant:write") + visible = _truthy(payload.get("chat_model_selector_visible"), default=True) + store.set_setting(_CHAT_MODEL_SELECTOR_VISIBLE_KEY, "1" if visible else "0") + return {"ok": True, "chat_model_selector_visible": visible} + @mg.post("/profiles") def api_models_create_profile( payload: dict[str, Any] | None = Body(default=None), diff --git a/interfaces/admin/static/app.js b/interfaces/admin/static/app.js index 6b7add42..6b395031 100644 --- a/interfaces/admin/static/app.js +++ b/interfaces/admin/static/app.js @@ -322,6 +322,8 @@ const I18N = { "models.sectionBindings": "智能体与模型绑定", "models.sectionNew": "新建模型配置", "models.sectionApi": "API 与密钥", + "models.chatModelSelector": "聊天页模型选择器", + "models.chatModelSelectorHint": "控制 Chat 页底部的模型下拉是否显示。", "models.sectionEval": "Agent Evaluation", "models.pickModel": "选用配置", "models.agentBindingHelp": "可为不同角色指定模型配置;留空则跟随上方全局选用。", @@ -763,6 +765,8 @@ const I18N = { "models.sectionBindings": "Agent ↔ profile bindings", "models.sectionNew": "New profile", "models.sectionApi": "API & secret", + "models.chatModelSelector": "Chat model selector", + "models.chatModelSelectorHint": "Control whether the model dropdown is visible on the Chat page.", "models.sectionEval": "Agent Evaluation", "models.pickModel": "Profile", "models.agentBindingHelp": "Optional per-role profile; leave empty to use the global selection above.", @@ -3460,6 +3464,7 @@ async function renderApiGrants() { async function renderModels() { const canPickActive = hasPermission("admin:read"); const canConfigureBindings = canPickActive; + const canConfigureChatUi = hasPermission("admin:tenant:write"); const status = el("div", { class: "muted", text: "" }); const isAdminPool = String((authSession && authSession.username) || "").trim().toLowerCase() === "administrator"; // 与后端 _require_models_mutate 对齐:administrator 写全局池需 tenant:write;普通用户写自己的配置只需 admin:read @@ -3469,6 +3474,7 @@ async function renderModels() { text: isAdminPool && !canMutateProfiles ? t("models.readonly") : "", }); const activeSelect = el("select", { class: "input", disabled: !canPickActive }); + const chatModelSelectorVisibleCb = el("input", { type: "checkbox", disabled: !canConfigureChatUi }); const bindingWrap = el("div", {}); const opsAiSpecialistSelect = el("select", { class: "input", disabled: !canConfigureBindings }); const opsAiProfileSelect = el("select", { class: "input", disabled: !canConfigureBindings }); @@ -3882,6 +3888,8 @@ async function renderModels() { const aid = String(state.active_llm_profile_id || ""); if (profiles.some((p) => String(p.id) === aid)) activeSelect.value = aid; activeSelect.disabled = !canPickActive; + chatModelSelectorVisibleCb.disabled = !canConfigureChatUi; + chatModelSelectorVisibleCb.checked = state.chat_model_selector_visible !== false; opsAiSpecialistSelect.disabled = !canConfigureBindings; opsAiProfileSelect.disabled = !canConfigureBindings; @@ -3963,6 +3971,17 @@ async function renderModels() { status.textContent = String(e.message || e); } }); + chatModelSelectorVisibleCb.addEventListener("change", async () => { + if (!canConfigureChatUi) return; + try { + await apiPost("/admin/api/models/chat-ui", { + chat_model_selector_visible: !!chatModelSelectorVisibleCb.checked, + }); + await refresh(); + } catch (e) { + status.textContent = String(e.message || e); + } + }); opsAiSpecialistSelect.addEventListener("change", async () => { paintOpsAiBindings(); }); @@ -4206,6 +4225,8 @@ async function renderModels() { el("div", { class: "page-grid page-grid--two" }, [ renderSectionCard(t("models.sectionActive"), "", [ el("div", { class: "row" }, [el("label", { text: t("models.pickModel") }), activeSelect]), + el("div", { class: "row" }, [el("label", { text: t("models.chatModelSelector") }), chatModelSelectorVisibleCb]), + el("div", { class: "muted", text: t("models.chatModelSelectorHint") }), ], { id: "models-overview" }), renderSectionCard(t("models.sectionNew"), "", [ el("div", { class: "row" }, [el("label", { text: t("models.profileName") }), newName]), diff --git a/interfaces/admin/static/chat.js b/interfaces/admin/static/chat.js index d13b0943..32287306 100644 --- a/interfaces/admin/static/chat.js +++ b/interfaces/admin/static/chat.js @@ -2845,9 +2845,15 @@ async function renderChatUi() { }; await loadSpecialistCatalog(); 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; + }; const refreshActiveModelText = async () => { try { const ms = await apiGet("/admin/api/models"); + applyModelSelectorVisibility(ms); const profiles = Array.isArray(ms.profiles) ? ms.profiles : []; const aid = String(ms.active_llm_profile_id || ""); const p = profiles.find((x) => String(x.id || "") === aid) || null; @@ -2883,6 +2889,7 @@ async function renderChatUi() { modelSelect.value = String(modelSelect.options[0].value || ""); } } catch (_) { + applyModelSelectorVisibility(null); activeModelText.textContent = `${t("chat.activeModelLabel")}: -`; modelSelect.innerHTML = ""; modelSelect.appendChild(el("option", { value: "", text: "-" }));