From cc0233d7b4f91b6eac58637c260f124e7c3aa38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Fri, 24 Jul 2026 17:05:44 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 104 ++++++++++++++++++++++++++--- web/static/i18n/en-US.json | 5 +- web/static/i18n/zh-CN.json | 5 +- web/static/js/settings.js | 132 +++++++++++++++++++++++++++++-------- web/templates/index.html | 6 +- 5 files changed, 212 insertions(+), 40 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 25306192..072ee1aa 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -4739,6 +4739,10 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { color: #10b981; } +.ai-channel-status-label.complete { + color: #3b82f6; +} + .ai-channel-status-label.testing { color: #3b82f6; } @@ -4760,6 +4764,10 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { background: #10b981; } +.ai-channel-status-dot.complete { + background: #3b82f6; +} + .ai-channel-status-dot.draft { background: #f59e0b; } @@ -4808,18 +4816,60 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { font-weight: 700; } -.ai-channel-editor-head h5 { +.ai-channel-editor-title { margin: 0; - color: var(--text-color, #2d3748); - font-size: 21px; - font-weight: 700; - line-height: 1.2; -} - -.ai-channel-editor-head p { - margin: 4px 0 0; color: var(--text-muted, #718096); font-size: 12px; + line-height: 1.5; +} + +.ai-channel-editor-meta { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; + min-width: 0; +} + +.ai-channel-editor-chip { + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border: 1px solid color-mix(in srgb, var(--border-color, #e2e8f0) 70%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--card-bg, #fff) 92%, var(--primary-color, #3182ce)); + color: var(--text-muted, #718096); + font-size: 12px; + font-weight: 600; + line-height: 1; + padding: 7px 10px; +} + +.ai-channel-editor-chip.default, +.ai-channel-editor-chip.complete { + border-color: rgba(49, 130, 206, 0.22); + background: rgba(49, 130, 206, 0.1); + color: var(--primary-color, #3182ce); +} + +.ai-channel-editor-chip.ready { + border-color: rgba(16, 185, 129, 0.22); + background: rgba(16, 185, 129, 0.1); + color: #059669; +} + +.ai-channel-editor-chip.testing { + border-color: rgba(214, 158, 46, 0.24); + background: rgba(214, 158, 46, 0.1); + color: #b7791f; +} + +.ai-channel-editor-chip.failed, +.ai-channel-editor-chip.draft { + border-color: rgba(229, 62, 62, 0.2); + background: rgba(229, 62, 62, 0.08); + color: var(--error-color, #e53e3e); } .ai-channel-editor-actions { @@ -5121,6 +5171,38 @@ html[data-theme="dark"] .ai-channel-editor-head { border-bottom-color: rgba(71, 85, 105, 0.34); } +html[data-theme="dark"] .ai-channel-editor-chip { + border-color: rgba(71, 85, 105, 0.46); + background: rgba(15, 23, 42, 0.7); + color: #94a3b8; +} + +html[data-theme="dark"] .ai-channel-editor-chip.default, +html[data-theme="dark"] .ai-channel-editor-chip.complete { + border-color: rgba(96, 165, 250, 0.34); + background: rgba(96, 165, 250, 0.12); + color: #93c5fd; +} + +html[data-theme="dark"] .ai-channel-editor-chip.ready { + border-color: rgba(52, 211, 153, 0.28); + background: rgba(52, 211, 153, 0.1); + color: #6ee7b7; +} + +html[data-theme="dark"] .ai-channel-editor-chip.testing { + border-color: rgba(251, 191, 36, 0.28); + background: rgba(251, 191, 36, 0.1); + color: #fbbf24; +} + +html[data-theme="dark"] .ai-channel-editor-chip.failed, +html[data-theme="dark"] .ai-channel-editor-chip.draft { + border-color: rgba(248, 113, 113, 0.28); + background: rgba(248, 113, 113, 0.1); + color: #fca5a5; +} + html[data-theme="dark"] .ai-channel-form-section, html[data-theme="dark"] .ai-channel-advanced-section { border-color: rgba(71, 85, 105, 0.34); @@ -5238,6 +5320,10 @@ html[data-theme="dark"] .ai-channel-editor-form select { align-items: stretch; } + .ai-channel-editor-meta { + justify-content: flex-start; + } + .ai-channel-switcher { align-items: stretch; flex-direction: column; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 38d580af..b98c1d85 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -2607,12 +2607,15 @@ "aiChannelSavedList": "Saved channels", "aiChannelListAria": "AI channel list", "aiChannelEditing": "Editing", + "aiChannelFormContext": "Editing the selected channel", + "aiChannelFormContextHint": "Saving the form updates this channel configuration.", "aiChannelBulkProbe": "Probe all", "aiChannelBulkProbeTitle": "Check whether all complete channels are available", "aiChannelSelectAria": "Select {name}", "aiChannelDefaultBadge": "Default", "aiChannelReady": "Ready", "aiChannelReadyWithLatency": "Ready{latency}", + "aiChannelComplete": "Complete", "aiChannelDraft": "Incomplete", "aiChannelDefaultMeta": "Default channel", "aiChannelCustomMeta": "Custom channel", @@ -2812,7 +2815,7 @@ "visionTimeout": "Timeout (seconds)", "visionTestFillRequired": "Enter vision model and ensure API Key is available (or reuse OpenAI)", "testConnection": "Test Connection", - "testFillRequired": "Please fill in API Key and Model first", + "testFillRequired": "Please fill in Base URL, API Key, and Model first", "testing": "Testing connection...", "testSuccess": "Connection successful", "testFailed": "Connection failed", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 36180344..e86e2deb 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -2595,12 +2595,15 @@ "aiChannelSavedList": "已保存通道", "aiChannelListAria": "AI 通道列表", "aiChannelEditing": "正在编辑", + "aiChannelFormContext": "编辑当前选择的通道", + "aiChannelFormContextHint": "表单保存后会更新该通道配置。", "aiChannelBulkProbe": "批量探活", "aiChannelBulkProbeTitle": "检测全部完整通道是否可用", "aiChannelSelectAria": "选择 {name}", "aiChannelDefaultBadge": "默认", "aiChannelReady": "可用", "aiChannelReadyWithLatency": "可用{latency}", + "aiChannelComplete": "配置完整", "aiChannelDraft": "待完善", "aiChannelDefaultMeta": "默认通道", "aiChannelCustomMeta": "自定义通道", @@ -2800,7 +2803,7 @@ "visionTimeout": "超时(秒)", "visionTestFillRequired": "请填写视觉模型,并确保 API Key 可用(可复用 OpenAI)", "testConnection": "测试连接", - "testFillRequired": "请先填写 API Key 和模型", + "testFillRequired": "请先填写 Base URL、API Key 和模型", "testing": "测试中...", "testSuccess": "连接成功", "testFailed": "连接失败", diff --git a/web/static/js/settings.js b/web/static/js/settings.js index 75f1f21e..eb6470da 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -2571,6 +2571,7 @@ function writeAIChannelToMainForm(id) { if (allowEl) allowEl.checked = r.allow_client_reasoning !== false; syncModelListFetchButtons(); syncAIChannelEditorPreview(); + syncConnectionTestResultForSelectedAIChannel(); } function displayAIChannelName(id, ch) { @@ -2618,8 +2619,10 @@ function updateAIChannelSelectOption(id) { delete opt.dataset.probeStatus; delete opt.dataset.probeMessage; } - select.value = channelId; - select.selectedIndex = opt.index; + if (channelId === selectedAIChannelId) { + select.value = channelId; + select.selectedIndex = opt.index; + } } if (typeof syncSettingsCustomSelect === 'function') { syncSettingsCustomSelect(select); @@ -2630,12 +2633,18 @@ function syncSelectedAIChannelUI() { updateAIChannelSelectOption(selectedAIChannelId); updateAIChannelEditorChrome(selectedAIChannelId); renderAIChannelList(); + syncConnectionTestResultForSelectedAIChannel(); } function syncAIChannelEditorPreview() { if (!currentConfig?.ai?.channels || !selectedAIChannelId || !currentConfig.ai.channels[selectedAIChannelId]) return; const id = normalizeAIChannelId(selectedAIChannelId); + const prev = currentConfig.ai.channels[id] || {}; const next = readAIChannelFromMainForm(id); + const connectionChanged = ['provider', 'base_url', 'api_key', 'model'].some((key) => String(prev[key] || '') !== String(next[key] || '')); + if (connectionChanged) { + delete aiChannelProbeResults[id]; + } currentConfig.ai.channels[id] = next; syncSelectedAIChannelUI(); } @@ -2644,6 +2653,7 @@ function bindAIChannelEditorPreviewSync() { const ids = [ 'ai-channel-name', 'openai-provider', + 'openai-api-key', 'openai-base-url', 'openai-model' ]; @@ -2739,9 +2749,9 @@ function renderAIChannelList(ids) { const displayName = displayAIChannelName(id, ch); const defaultBadge = isDefault ? `${escapeAIChannelHtml(settingsT('settingsBasic.aiChannelDefaultBadge', '默认'))}` : ''; let statusText = isComplete - ? settingsT('settingsBasic.aiChannelReady', '可用') + ? settingsT('settingsBasic.aiChannelComplete', '配置完整') : settingsT('settingsBasic.aiChannelDraft', '待完善'); - let statusClass = isComplete ? 'ready' : 'draft'; + let statusClass = isComplete ? 'complete' : 'draft'; if (probe) { statusText = probe.message || statusText; statusClass = probe.status || statusClass; @@ -2778,19 +2788,36 @@ function updateAIChannelEditorChrome(id) { const ai = ensureAIConfigShape(currentConfig || {}); const channelId = normalizeAIChannelId(id || ai.default_channel || 'default'); const ch = ai.channels[channelId] || {}; + const isDefault = channelId === ai.default_channel; + const isComplete = !validateSelectedAIChannelPayload(ch); + const probe = aiChannelProbeResults[channelId] || null; const title = document.getElementById('ai-channel-editor-title'); const meta = document.getElementById('ai-channel-editor-meta'); - if (title) title.textContent = displayAIChannelName(channelId, ch); + if (title) { + title.textContent = settingsT('settingsBasic.aiChannelFormContextHint', '表单保存后会更新该通道配置。'); + } if (meta) { - const parts = [ - channelId === ai.default_channel - ? settingsT('settingsBasic.aiChannelDefaultMeta', '默认通道') - : settingsT('settingsBasic.aiChannelCustomMeta', '自定义通道'), - ch.provider === 'claude' ? 'Claude' : settingsT('settingsBasic.aiChannelOpenAICompat', 'OpenAI 兼容'), - ch.model || settingsT('settingsBasic.aiChannelModelMissing', '未填写模型'), - channelHostLabel(ch.base_url) - ].filter(Boolean); - meta.textContent = parts.join(' / '); + const provider = ch.provider === 'claude' ? 'Claude' : settingsT('settingsBasic.aiChannelOpenAICompat', 'OpenAI 兼容'); + const statusText = probe?.message || (isComplete + ? settingsT('settingsBasic.aiChannelComplete', '配置完整') + : settingsT('settingsBasic.aiChannelDraft', '待完善')); + const statusClass = probe?.status || (isComplete ? 'complete' : 'draft'); + const chips = [ + { + label: isDefault + ? settingsT('settingsBasic.aiChannelDefaultMeta', '默认通道') + : settingsT('settingsBasic.aiChannelCustomMeta', '自定义通道'), + className: isDefault ? 'default' : '' + }, + { label: provider }, + { label: ch.model || settingsT('settingsBasic.aiChannelModelMissing', '未填写模型') }, + { label: channelHostLabel(ch.base_url) }, + { label: statusText, className: statusClass } + ].filter((chip) => chip.label); + meta.innerHTML = chips.map((chip) => { + const className = chip.className ? ` ${escapeAIChannelHtml(chip.className)}` : ''; + return `${escapeAIChannelHtml(chip.label)}`; + }).join(''); } } @@ -3536,10 +3563,10 @@ async function testHitlAuditModelConnection() { const resultEl = document.getElementById('test-hitl-audit-model-result'); const cfg = collectHitlAuditModelEffectiveConfig(); - if (!cfg.api_key || !cfg.model) { + if (!cfg.base_url || !cfg.api_key || !cfg.model) { if (resultEl) { resultEl.style.color = 'var(--danger-color, #e53e3e)'; - resultEl.textContent = typeof window.t === 'function' ? window.t('settingsBasic.testFillRequired') : '请先填写 API Key 和模型'; + resultEl.textContent = typeof window.t === 'function' ? window.t('settingsBasic.testFillRequired') : '请先填写 Base URL、API Key 和模型'; } return; } @@ -3604,6 +3631,29 @@ function setConnectionTestResult(resultEl, state, message, title) { } } +function syncConnectionTestResultForSelectedAIChannel() { + const resultEl = document.getElementById('test-openai-result'); + if (!resultEl) return; + const channelId = normalizeAIChannelId(selectedAIChannelId || currentConfig?.ai?.default_channel || 'default'); + const probe = aiChannelProbeResults[channelId]; + if (!probe) { + setConnectionTestResult(resultEl, '', ''); + return; + } + const state = probe.status === 'ready' + ? 'is-success' + : probe.status === 'failed' + ? 'is-error' + : 'is-muted'; + let message = probe.message || ''; + const fillRequiredMessage = settingsT('settingsBasic.testFillRequired', '请先填写 Base URL、API Key 和模型'); + const failedPrefix = (typeof window.t === 'function' ? window.t('settingsBasic.testFailed') : '连接失败') + ': '; + if (probe.status === 'failed' && message && message !== fillRequiredMessage && !message.startsWith(failedPrefix)) { + message = failedPrefix + message; + } + setConnectionTestResult(resultEl, state, message); +} + function showConnectionTestFailure(resultEl, errorText) { if (!resultEl) return; const formatted = formatConnectionTestError(errorText); @@ -3624,15 +3674,29 @@ async function testOpenAIConnection() { const baseUrl = document.getElementById('openai-base-url').value.trim(); const apiKey = document.getElementById('openai-api-key').value.trim(); const model = document.getElementById('openai-model').value.trim(); + const channelId = normalizeAIChannelId(selectedAIChannelId || currentConfig?.ai?.default_channel || 'default'); + const isTestingSameChannel = () => normalizeAIChannelId(selectedAIChannelId || currentConfig?.ai?.default_channel || 'default') === channelId; - if (!apiKey || !model) { - setConnectionTestResult(resultEl, 'is-error', typeof window.t === 'function' ? window.t('settingsBasic.testFillRequired') : '请先填写 API Key 和模型'); + if (!baseUrl || !apiKey || !model) { + const message = typeof window.t === 'function' ? window.t('settingsBasic.testFillRequired') : '请先填写 Base URL、API Key 和模型'; + aiChannelProbeResults[channelId] = { status: 'failed', message }; + if (isTestingSameChannel()) { + setConnectionTestResult(resultEl, 'is-error', message); + } + syncSelectedAIChannelUI(); return; } - btn.style.pointerEvents = 'none'; - btn.style.opacity = '0.5'; - setConnectionTestResult(resultEl, 'is-muted', typeof window.t === 'function' ? window.t('settingsBasic.testing') : '测试中...'); + if (btn) { + btn.style.pointerEvents = 'none'; + btn.style.opacity = '0.5'; + } + const testingMessage = settingsT('settingsBasic.testing', '测试中...'); + aiChannelProbeResults[channelId] = { status: 'testing', message: testingMessage }; + if (isTestingSameChannel()) { + setConnectionTestResult(resultEl, 'is-muted', testingMessage); + } + syncSelectedAIChannelUI(); try { const response = await apiFetch('/api/config/test-openai', { @@ -3651,15 +3715,31 @@ async function testOpenAIConnection() { if (result.success) { const latency = result.latency_ms ? ` (${result.latency_ms}ms)` : ''; const modelInfo = result.model ? ` [${result.model}]` : ''; - setConnectionTestResult(resultEl, 'is-success', (typeof window.t === 'function' ? window.t('settingsBasic.testSuccess') : '连接成功') + modelInfo + latency); + const message = (typeof window.t === 'function' ? window.t('settingsBasic.testSuccess') : '连接成功') + modelInfo + latency; + aiChannelProbeResults[channelId] = { status: 'ready', message }; + if (isTestingSameChannel()) { + setConnectionTestResult(resultEl, 'is-success', message); + } } else { - showConnectionTestFailure(resultEl, result.error || '未知错误'); + const message = formatConnectionTestError(result.error || '未知错误').message; + aiChannelProbeResults[channelId] = { status: 'failed', message }; + if (isTestingSameChannel()) { + showConnectionTestFailure(resultEl, message); + } } } catch (error) { - showConnectionTestFailure(resultEl, error.message || '测试出错'); + const message = formatConnectionTestError(error.message || '测试出错').message; + aiChannelProbeResults[channelId] = { status: 'failed', message }; + if (isTestingSameChannel()) { + showConnectionTestFailure(resultEl, message); + } } finally { - btn.style.pointerEvents = ''; - btn.style.opacity = ''; + updateAIChannelSelectOption(channelId); + syncSelectedAIChannelUI(); + if (btn) { + btn.style.pointerEvents = ''; + btn.style.opacity = ''; + } } } diff --git a/web/templates/index.html b/web/templates/index.html index b18adcea..8aca98be 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -3470,10 +3470,10 @@
-
+ 编辑当前选择的通道 +-