mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 03:30:48 +08:00
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 <cursoragent@cursor.com>
This commit is contained in:
parent
746570dfc9
commit
2a52349668
3 changed files with 164 additions and 67 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue