From b7fa18b6d4fad2940ec91fc9c131863390807b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:44:04 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 261 ++++++++++++++++++++++++++++++++++++++ web/static/js/settings.js | 185 ++++++++++++++++++++++++++- web/templates/index.html | 16 +-- 3 files changed, 453 insertions(+), 9 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 6c912f95..7d5d0c12 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -5851,6 +5851,267 @@ header { box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.2); } +/* Model picker: input + custom dropdown */ +.model-pick-row { + display: flex; + gap: 8px; + align-items: stretch; + flex-wrap: wrap; +} + +.model-pick-input { + flex: 1 1 140px; + min-width: 0; + padding: 10px 12px; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 0.9375rem; + background: var(--bg-primary); + color: var(--text-primary); + transition: border-color 0.2s, box-shadow 0.2s; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; +} + +.model-pick-input:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1); +} + +.model-pick-input:hover { + border-color: rgba(0, 102, 255, 0.45); +} + +.model-pick-input.error { + border-color: var(--error-color); + box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.1); +} + +.model-pick-fetch-link { + flex: 0 0 auto; + align-self: center; + font-size: 0.8125rem; + color: var(--accent-color); + text-decoration: none; + cursor: pointer; + user-select: none; + white-space: nowrap; + padding: 4px 2px; + transition: color 0.15s ease, opacity 0.15s ease; +} + +.model-pick-fetch-link:hover { + color: var(--accent-hover); + text-decoration: underline; +} + +.model-pick-native { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.model-pick-dropdown { + position: relative; + flex: 0 0 auto; + min-width: 148px; + max-width: 220px; +} + +.model-pick-trigger { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; + height: 100%; + min-height: 42px; + box-sizing: border-box; + padding: 0 10px 0 12px; + border: 1px solid var(--border-color); + border-radius: 6px; + background: linear-gradient(180deg, var(--bg-primary) 0%, var(--bg-secondary) 100%); + color: var(--text-primary); + font-size: 0.8125rem; + line-height: 1.2; + cursor: pointer; + transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; +} + +.model-pick-trigger-label { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: left; + font-weight: 500; +} + +.model-pick-trigger-meta { + display: inline-flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +.model-pick-count { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 18px; + padding: 0 6px; + border-radius: 999px; + background: rgba(0, 102, 255, 0.1); + color: var(--accent-color); + font-size: 0.6875rem; + font-weight: 600; + line-height: 1; +} + +.model-pick-caret { + flex-shrink: 0; + width: 14px; + height: 14px; + color: var(--text-secondary); + transition: transform 0.2s ease, color 0.15s ease; +} + +.model-pick-dropdown.open .model-pick-caret { + transform: rotate(180deg); + color: var(--accent-color); +} + +.model-pick-trigger:hover:not(:disabled) { + border-color: rgba(0, 102, 255, 0.45); + background: var(--bg-primary); +} + +.model-pick-dropdown.open .model-pick-trigger { + border-color: var(--accent-color); + box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1); + background: var(--bg-primary); +} + +.model-pick-dropdown.is-disabled .model-pick-trigger { + opacity: 0.55; + cursor: not-allowed; +} + +.model-pick-menu { + display: none; + position: absolute; + top: calc(100% + 6px); + right: 0; + left: auto; + z-index: 2000; + min-width: 280px; + max-width: min(360px, 92vw); + max-height: 300px; + overflow: hidden; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 10px; + box-shadow: 0 12px 32px rgba(15, 23, 42, 0.14), 0 2px 8px rgba(15, 23, 42, 0.06); +} + +.model-pick-dropdown.open .model-pick-menu { + display: flex; + flex-direction: column; + animation: model-pick-menu-in 0.16s ease-out; +} + +@keyframes model-pick-menu-in { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.model-pick-menu-header { + flex: 0 0 auto; + padding: 10px 14px 8px; + border-bottom: 1px solid var(--border-color); + font-size: 0.75rem; + font-weight: 600; + color: var(--text-secondary); + letter-spacing: 0.02em; +} + +.model-pick-menu-list { + flex: 1 1 auto; + overflow-y: auto; + padding: 6px; + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.15) transparent; +} + +.model-pick-menu-list::-webkit-scrollbar { + width: 6px; +} + +.model-pick-menu-list::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.15); + border-radius: 999px; +} + +.model-pick-option { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border-radius: 6px; + font-size: 0.8125rem; + line-height: 1.3; + color: var(--text-primary); + cursor: pointer; + transition: background-color 0.12s ease, color 0.12s ease; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; +} + +.model-pick-option:hover { + background: var(--bg-secondary); +} + +.model-pick-option.is-selected { + background: rgba(0, 102, 255, 0.08); + color: var(--accent-color); + font-weight: 600; +} + +.model-pick-option-check { + flex: 0 0 14px; + width: 14px; + font-size: 0.75rem; + line-height: 1; + color: var(--accent-color); + opacity: 0; + text-align: center; +} + +.model-pick-option.is-selected .model-pick-option-check { + opacity: 1; +} + +.model-pick-option-label { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .batch-execute-now-label { display: inline-flex; align-items: center; diff --git a/web/static/js/settings.js b/web/static/js/settings.js index e387393a..b5d875df 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -1577,6 +1577,179 @@ function syncVisionFormEnabled() { } } +const modelPickSelectMap = {}; +let modelPickSelectDocListener = false; + +function modelPickT(key) { + return typeof window.t === 'function' ? window.t(key) : key; +} + +function closeAllModelPickDropdowns() { + Object.keys(modelPickSelectMap).forEach(function (id) { + modelPickSelectMap[id].wrapper.classList.remove('open'); + }); +} + +function syncModelPickDropdown(selectId) { + const reg = modelPickSelectMap[selectId]; + if (!reg) return; + const { select, dropdown, trigger, wrapper, menuList, countBadge } = reg; + const placeholder = modelPickT('settingsBasic.modelsListSelectPlaceholder'); + + menuList.innerHTML = ''; + let optionCount = 0; + Array.prototype.forEach.call(select.options, function (opt) { + if (!opt.value) return; + optionCount += 1; + const item = document.createElement('div'); + item.className = 'model-pick-option'; + item.setAttribute('role', 'option'); + item.setAttribute('data-value', opt.value); + if (opt.value === select.value) { + item.classList.add('is-selected'); + item.setAttribute('aria-selected', 'true'); + } + const check = document.createElement('span'); + check.className = 'model-pick-option-check'; + check.setAttribute('aria-hidden', 'true'); + check.textContent = '✓'; + const label = document.createElement('span'); + label.className = 'model-pick-option-label'; + label.textContent = opt.textContent; + item.appendChild(check); + item.appendChild(label); + menuList.appendChild(item); + }); + + const selectedOpt = select.selectedIndex >= 0 ? select.options[select.selectedIndex] : null; + const labelEl = trigger.querySelector('.model-pick-trigger-label'); + if (labelEl) { + labelEl.textContent = (selectedOpt && selectedOpt.value) ? selectedOpt.textContent : placeholder; + } + if (countBadge) { + countBadge.textContent = String(optionCount); + countBadge.style.display = optionCount > 0 ? '' : 'none'; + } + const header = wrapper.querySelector('.model-pick-menu-header'); + if (header) { + header.textContent = optionCount > 0 + ? placeholder + ' · ' + optionCount + : placeholder; + } + + trigger.disabled = !!select.disabled; + wrapper.classList.toggle('is-disabled', !!select.disabled); + wrapper.style.display = optionCount > 0 ? '' : 'none'; + select.style.display = 'none'; +} + +function enhanceModelPickSelect(selectId) { + const select = document.getElementById(selectId); + if (!select) return; + if (select.dataset.modelPickEnhanced === '1') { + syncModelPickDropdown(selectId); + return; + } + select.dataset.modelPickEnhanced = '1'; + select.classList.add('model-pick-native'); + select.tabIndex = -1; + select.setAttribute('aria-hidden', 'true'); + + const wrapper = document.createElement('div'); + wrapper.className = 'model-pick-dropdown'; + wrapper.style.display = 'none'; + + const trigger = document.createElement('button'); + trigger.type = 'button'; + trigger.className = 'model-pick-trigger'; + trigger.setAttribute('aria-haspopup', 'listbox'); + + const labelSpan = document.createElement('span'); + labelSpan.className = 'model-pick-trigger-label'; + labelSpan.textContent = modelPickT('settingsBasic.modelsListSelectPlaceholder'); + + const meta = document.createElement('span'); + meta.className = 'model-pick-trigger-meta'; + + const countBadge = document.createElement('span'); + countBadge.className = 'model-pick-count'; + countBadge.style.display = 'none'; + + const caret = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + caret.setAttribute('class', 'model-pick-caret'); + caret.setAttribute('viewBox', '0 0 16 16'); + caret.setAttribute('aria-hidden', 'true'); + caret.innerHTML = ''; + + meta.appendChild(countBadge); + meta.appendChild(caret); + trigger.appendChild(labelSpan); + trigger.appendChild(meta); + + const menu = document.createElement('div'); + menu.className = 'model-pick-menu'; + + const header = document.createElement('div'); + header.className = 'model-pick-menu-header'; + menu.appendChild(header); + + const menuList = document.createElement('div'); + menuList.className = 'model-pick-menu-list'; + menuList.setAttribute('role', 'listbox'); + menu.appendChild(menuList); + + const parent = select.parentNode; + const fetchLink = parent.querySelector('.model-pick-fetch-link'); + if (fetchLink) { + parent.insertBefore(wrapper, fetchLink); + } else { + parent.appendChild(wrapper); + } + wrapper.appendChild(trigger); + wrapper.appendChild(menu); + wrapper.appendChild(select); + + modelPickSelectMap[selectId] = { + wrapper, + trigger, + menu, + menuList, + countBadge, + select + }; + + if (!modelPickSelectDocListener) { + document.addEventListener('click', closeAllModelPickDropdowns); + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') closeAllModelPickDropdowns(); + }); + modelPickSelectDocListener = true; + } + + trigger.addEventListener('click', function (e) { + e.stopPropagation(); + if (select.disabled) return; + const open = wrapper.classList.contains('open'); + closeAllModelPickDropdowns(); + if (!open) wrapper.classList.add('open'); + }); + + menuList.addEventListener('click', function (e) { + const opt = e.target.closest('.model-pick-option'); + if (!opt) return; + const val = opt.getAttribute('data-value'); + if (val === null || val === '') return; + if (select.value !== val) { + select.value = val; + select.dispatchEvent(new Event('change', { bubbles: true })); + } + wrapper.classList.remove('open'); + syncModelPickDropdown(selectId); + }); + + syncModelPickDropdown(selectId); +} + function initModelListControls() { const providerEl = document.getElementById('openai-provider'); if (providerEl && !providerEl.dataset.modelListBound) { @@ -1605,6 +1778,7 @@ function bindModelSelect(scope) { const select = document.getElementById(selectId); if (!select || select.dataset.bound) return; select.dataset.bound = '1'; + enhanceModelPickSelect(selectId); select.addEventListener('change', function () { if (!select.value) return; const input = document.getElementById(inputId); @@ -1641,6 +1815,10 @@ function syncModelListFetchButtons() { } if (openaiSelect && isClaudeOpenai) { openaiSelect.style.display = 'none'; + const openaiWrap = modelPickSelectMap['openai-model-select']; + if (openaiWrap) openaiWrap.wrapper.style.display = 'none'; + } else if (openaiSelect && !isClaudeOpenai) { + syncModelPickDropdown('openai-model-select'); } if (openaiHint) { if (isClaudeOpenai) { @@ -1663,6 +1841,10 @@ function syncModelListFetchButtons() { } if (visionSelect && isClaudeVision) { visionSelect.style.display = 'none'; + const visionWrap = modelPickSelectMap['vision-model-select']; + if (visionWrap) visionWrap.wrapper.style.display = 'none'; + } else if (visionSelect && !isClaudeVision) { + syncModelPickDropdown('vision-model-select'); } if (visionHint) { if (isClaudeVision) { @@ -1705,7 +1887,8 @@ function populateModelSelect(scope, models, currentValue) { } else { select.value = ''; } - select.style.display = select.options.length > 1 ? '' : 'none'; + enhanceModelPickSelect(selectId); + syncModelPickDropdown(selectId); } async function fetchModelList(scope) { diff --git a/web/templates/index.html b/web/templates/index.html index 752cb5f3..594f788c 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -2413,12 +2413,12 @@
-
- - + - 获取列表 + 获取列表
@@ -2499,12 +2499,12 @@
-
- - + - 获取列表 + 获取列表