mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-18 22:00:46 +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;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
padding: 2px 4px 8px;
|
padding: 0 2px 6px;
|
||||||
}
|
}
|
||||||
.chat-composer-chip,
|
.chat-composer-chip,
|
||||||
.chat-composer-chip--select {
|
.chat-composer-chip--select {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
height: 30px;
|
height: 28px;
|
||||||
min-height: 30px;
|
min-height: 28px;
|
||||||
max-height: 30px;
|
max-height: 28px;
|
||||||
padding: 0 12px;
|
padding: 0 10px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid var(--ds-border, rgba(148, 163, 184, 0.28));
|
border: none;
|
||||||
background-color: var(--ds-surface-2, #152033);
|
background-color: transparent;
|
||||||
color: var(--ds-text, #e2e8f0);
|
color: var(--ds-text-muted, rgba(148, 163, 184, 0.92));
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
}
|
}
|
||||||
.chat-composer-chip--select {
|
.chat-composer-chip--select {
|
||||||
min-width: 148px;
|
min-width: 0;
|
||||||
max-width: 240px;
|
max-width: 220px;
|
||||||
padding-right: 28px;
|
width: auto;
|
||||||
background-color: var(--ds-surface-2, #152033);
|
padding-right: 22px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
background-color: transparent;
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(45deg, transparent 50%, var(--ds-text-muted, rgba(148, 163, 184, 0.85)) 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.85)) 50%, transparent 50%);
|
linear-gradient(135deg, var(--ds-text-muted, rgba(148, 163, 184, 0.7)) 50%, transparent 50%);
|
||||||
background-position:
|
background-position:
|
||||||
calc(100% - 14px) calc(50% - 2px),
|
calc(100% - 10px) calc(50% - 1.5px),
|
||||||
calc(100% - 9px) calc(50% - 2px);
|
calc(100% - 6px) calc(50% - 1.5px);
|
||||||
background-size: 5px 5px, 5px 5px;
|
background-size: 4px 4px, 4px 4px;
|
||||||
background-repeat: no-repeat;
|
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:hover,
|
||||||
.chat-composer-chip--select:hover {
|
.chat-composer-chip--select:hover {
|
||||||
background-color: var(--ds-menu-hover-bg, rgba(148, 163, 184, 0.16));
|
background-color: var(--ds-menu-hover-bg, rgba(148, 163, 184, 0.12));
|
||||||
border-color: var(--ds-accent-border, rgba(94, 179, 255, 0.35));
|
color: var(--ds-text, #e2e8f0);
|
||||||
}
|
}
|
||||||
.chat-composer-chip:focus-visible,
|
.chat-composer-chip:focus-visible,
|
||||||
.chat-composer-chip--select:focus-visible {
|
.chat-composer-chip--select:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--ds-accent-border-strong, rgba(94, 179, 255, 0.5));
|
background-color: var(--ds-accent-soft, rgba(94, 179, 255, 0.14));
|
||||||
box-shadow: 0 0 0 2px var(--ds-accent-soft, rgba(94, 179, 255, 0.2));
|
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 {
|
.chat-composer-shell {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
|
||||||
|
|
@ -3473,11 +3473,71 @@ async function renderChatUi() {
|
||||||
});
|
});
|
||||||
let globalMenuModeValue = String(localStorage.getItem(CHAT_USER_MENU_MODE_KEY) || MAIN_MODE_VALUE).toLowerCase();
|
let globalMenuModeValue = String(localStorage.getItem(CHAT_USER_MENU_MODE_KEY) || MAIN_MODE_VALUE).toLowerCase();
|
||||||
if (globalMenuModeValue === "comprehensive" || globalMenuModeValue === "main") globalMenuModeValue = MAIN_MODE_VALUE;
|
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",
|
class: "chat-composer-chip chat-composer-chip--select",
|
||||||
title: t("chat.activeModelLabel"),
|
title: t("chat.activeModelLabel"),
|
||||||
|
text: "-",
|
||||||
});
|
});
|
||||||
|
const modelSelectWrap = el("div", { class: "chat-model-select" }, [modelSelectBtn]);
|
||||||
let modelSelectNameToId = new Map();
|
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 normalizeExecutionMode = (v) => {
|
||||||
const raw = String(v || "").trim().toLowerCase();
|
const raw = String(v || "").trim().toLowerCase();
|
||||||
return raw === EXECUTION_MODE_PLAN ? EXECUTION_MODE_PLAN : EXECUTION_MODE_AGENT;
|
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 activeModelText = el("span", { class: "muted", text: `${t("chat.activeModelLabel")}: -` });
|
||||||
const applyModelSelectorVisibility = (modelsState) => {
|
const applyModelSelectorVisibility = (modelsState) => {
|
||||||
const show = !(modelsState && modelsState.chat_model_selector_visible === false);
|
const show = !(modelsState && modelsState.chat_model_selector_visible === false);
|
||||||
modelSelect.style.display = show ? "" : "none";
|
modelSelectWrap.style.display = show ? "" : "none";
|
||||||
modelSelect.disabled = !show;
|
modelSelectBtn.disabled = !show;
|
||||||
};
|
};
|
||||||
const refreshActiveModelText = async () => {
|
const refreshActiveModelText = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -3597,8 +3657,8 @@ async function renderChatUi() {
|
||||||
const p = profiles.find((x) => String(x.id || "") === aid) || null;
|
const p = profiles.find((x) => String(x.id || "") === aid) || null;
|
||||||
const modelName = String((p && p.model) || "").trim() || "-";
|
const modelName = String((p && p.model) || "").trim() || "-";
|
||||||
activeModelText.textContent = `${t("chat.activeModelLabel")}: ${modelName}`;
|
activeModelText.textContent = `${t("chat.activeModelLabel")}: ${modelName}`;
|
||||||
modelSelect.innerHTML = "";
|
|
||||||
modelSelectNameToId = new Map();
|
modelSelectNameToId = new Map();
|
||||||
|
modelSelectOptions = [];
|
||||||
const usedKeys = new Set();
|
const usedKeys = new Set();
|
||||||
profiles.forEach((row) => {
|
profiles.forEach((row) => {
|
||||||
const pid = String(row && row.id ? row.id : "");
|
const pid = String(row && row.id ? row.id : "");
|
||||||
|
|
@ -3613,38 +3673,30 @@ async function renderChatUi() {
|
||||||
}
|
}
|
||||||
usedKeys.add(key);
|
usedKeys.add(key);
|
||||||
modelSelectNameToId.set(key, pid);
|
modelSelectNameToId.set(key, pid);
|
||||||
const display = String(row.model || row.name || pid);
|
modelSelectOptions.push({
|
||||||
modelSelect.appendChild(el("option", { value: key, text: display }));
|
key,
|
||||||
|
id: pid,
|
||||||
|
label: String(row.model || row.name || pid),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
if (p) {
|
if (p) {
|
||||||
const rawName = String(p.name || "").trim();
|
|
||||||
const firstKey = Array.from(modelSelectNameToId.keys()).find((k) => modelSelectNameToId.get(k) === aid) || "";
|
const firstKey = Array.from(modelSelectNameToId.keys()).find((k) => modelSelectNameToId.get(k) === aid) || "";
|
||||||
const key = firstKey || rawName;
|
modelSelectedKey = firstKey || "";
|
||||||
if (key && Array.from(modelSelect.options).some((o) => String(o.value || "") === key)) {
|
} else if (modelSelectOptions.length > 0) {
|
||||||
modelSelect.value = key;
|
modelSelectedKey = String(modelSelectOptions[0].key || "");
|
||||||
}
|
} else {
|
||||||
} else if (modelSelect.options.length > 0) {
|
modelSelectedKey = "";
|
||||||
modelSelect.value = String(modelSelect.options[0].value || "");
|
|
||||||
}
|
}
|
||||||
|
syncModelSelectBtn();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
applyModelSelectorVisibility(null);
|
applyModelSelectorVisibility(null);
|
||||||
activeModelText.textContent = `${t("chat.activeModelLabel")}: -`;
|
activeModelText.textContent = `${t("chat.activeModelLabel")}: -`;
|
||||||
modelSelect.innerHTML = "";
|
modelSelectNameToId = new Map();
|
||||||
modelSelect.appendChild(el("option", { value: "", text: "-" }));
|
modelSelectOptions = [];
|
||||||
modelSelect.value = "";
|
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 () => {
|
const loadSessionModePreference = async () => {
|
||||||
if (!activeId) {
|
if (!activeId) {
|
||||||
setExecutionMode(currentExecutionMode, { persistLocal: true, saveSession: false });
|
setExecutionMode(currentExecutionMode, { persistLocal: true, saveSession: false });
|
||||||
|
|
@ -3781,7 +3833,7 @@ async function renderChatUi() {
|
||||||
});
|
});
|
||||||
const composerMetaBar = el("div", { class: "chat-composer-meta" }, [
|
const composerMetaBar = el("div", { class: "chat-composer-meta" }, [
|
||||||
execSelectWrap,
|
execSelectWrap,
|
||||||
modelSelect,
|
modelSelectWrap,
|
||||||
compressBtn,
|
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,
|
||||||
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select {
|
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select {
|
||||||
background-color: #ffffff;
|
background-color: transparent;
|
||||||
border-color: rgba(15, 23, 42, 0.16);
|
border: none;
|
||||||
color: #0f172a;
|
color: rgba(15, 23, 42, 0.62);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select {
|
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select {
|
||||||
background-color: #ffffff;
|
background-color: transparent;
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(45deg, transparent 50%, rgba(15, 23, 42, 0.55) 50%),
|
linear-gradient(45deg, transparent 50%, rgba(15, 23, 42, 0.45) 50%),
|
||||||
linear-gradient(135deg, rgba(15, 23, 42, 0.55) 50%, transparent 50%);
|
linear-gradient(135deg, rgba(15, 23, 42, 0.45) 50%, transparent 50%);
|
||||||
background-position:
|
background-position:
|
||||||
calc(100% - 14px) calc(50% - 2px),
|
calc(100% - 10px) calc(50% - 1.5px),
|
||||||
calc(100% - 9px) calc(50% - 2px);
|
calc(100% - 6px) calc(50% - 1.5px);
|
||||||
background-size: 5px 5px, 5px 5px;
|
background-size: 4px 4px, 4px 4px;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select option {
|
body.theme-ds-body[data-admin-theme="light"] .chat-model-select-pop {
|
||||||
background-color: #ffffff;
|
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;
|
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:hover,
|
||||||
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select:hover {
|
body.theme-ds-body[data-admin-theme="light"] .chat-composer-chip--select:hover {
|
||||||
background-color: #f8fafc;
|
background-color: rgba(15, 23, 42, 0.05);
|
||||||
border-color: rgba(37, 99, 235, 0.35);
|
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 {
|
body.theme-ds-body[data-admin-theme="light"] .chat-composer-shell {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue