mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 04:40:45 +08:00
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 <cursoragent@cursor.com>
This commit is contained in:
parent
61d2b87a30
commit
6628cd1220
3 changed files with 52 additions and 0 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
|
|
|
|||
|
|
@ -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: "-" }));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue