From d82455b079eb0727f32e1889b76763e50cb49e65 Mon Sep 17 00:00:00 2001 From: Metona Date: Sat, 4 Apr 2026 21:30:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A8=A1=E5=9E=8B=20vision=20=E8=83=BD?= =?UTF-8?q?=E5=8A=9B=E6=A3=80=E6=B5=8B=EF=BC=8C=E4=B8=8D=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=97=B6=E7=A6=81=E7=94=A8=E5=9B=BE=E7=89=87=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - model-bar.js: 新增 setVisionAvailable() 控制图片按钮状态 - model-bar.js: 导出 isVisionAvailable() 供其他组件查询 - input-area.js: 点击图片按钮时校验 vision 能力 - input-area.js: 发送消息时二次校验,防止绕过 - style.css: .icon-btn.disabled 灰显不可点击样式 --- css/style.css | 9 +++++++++ js/components/input-area.js | 16 ++++++++++++++-- js/components/model-bar.js | 30 +++++++++++++++++++++++++++++- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/css/style.css b/css/style.css index 8d90aa5..bec8a1f 100644 --- a/css/style.css +++ b/css/style.css @@ -226,6 +226,15 @@ body::before { box-shadow: 0 4px 18px rgba(0, 245, 212, 0.4); } +.icon-btn.disabled, +.icon-btn:disabled { + opacity: 0.3; + cursor: not-allowed; + transform: none; + box-shadow: none; + pointer-events: none; +} + .icon-btn:active { transform: scale(0.95); } diff --git a/js/components/input-area.js b/js/components/input-area.js index d462c54..da3ffbf 100644 --- a/js/components/input-area.js +++ b/js/components/input-area.js @@ -5,7 +5,7 @@ import { state, KEYS } from '../state.js'; import { fileToBase64, fileToText, detectLanguage, debounce, truncate, escapeHtml, formatSize } from '../utils.js'; -import { getSelectedModel, isThinkEnabled } from './model-bar.js'; +import { getSelectedModel, isThinkEnabled, isVisionAvailable } from './model-bar.js'; import { renderMessages, appendAssistantPlaceholder, appendSystemMessage, updateLastAssistantMessage, clearMessages, safeMarkdown @@ -98,7 +98,13 @@ export function initInputArea() { chatInputEl.addEventListener('input', autoResizeTextarea); // 图片上传 - document.querySelector('#btnAttachImg').addEventListener('click', () => fileInputEl.click()); + document.querySelector('#btnAttachImg').addEventListener('click', () => { + if (!isVisionAvailable()) { + showToast('当前模型不支持图片分析', 'warning'); + return; + } + fileInputEl.click(); + }); fileInputEl.addEventListener('change', (e) => { handleImageSelect(e.target.files); e.target.value = ''; @@ -290,6 +296,12 @@ export async function sendMessage() { return; } + // 有图片但模型不支持 vision → 阻止发送 + if (pendingImages.length > 0 && !isVisionAvailable()) { + appendSystemMessage('⚠️ 当前模型不支持图片分析,请移除图片后重试'); + return; + } + const currentSession = state.get(KEYS.CURRENT_SESSION); if (!currentSession) return; diff --git a/js/components/model-bar.js b/js/components/model-bar.js index b05749a..947d87a 100644 --- a/js/components/model-bar.js +++ b/js/components/model-bar.js @@ -6,7 +6,7 @@ import { state, KEYS } from '../state.js'; import { formatSize } from '../utils.js'; -let modelSelectEl, badgeThinkEl, badgeVisionEl, thinkCheckbox, thinkWrap; +let modelSelectEl, badgeThinkEl, badgeVisionEl, thinkCheckbox, thinkWrap, btnAttachImg; /** 缓存模型能力 { modelName: { think: bool, vision: bool } } */ const modelCapabilityCache = new Map(); @@ -17,6 +17,7 @@ export function initModelBar() { badgeVisionEl = document.querySelector('#badgeVision'); thinkCheckbox = document.querySelector('#toggleThink'); thinkWrap = document.querySelector('#thinkToggleWrap'); + btnAttachImg = document.querySelector('#btnAttachImg'); // 模型切换 → 检测能力 + 保存设置 modelSelectEl.addEventListener('change', async () => { @@ -26,6 +27,7 @@ export function initModelBar() { await checkModelCapability(model); } else { setThinkAvailable(false); + setVisionAvailable(false); badgeThinkEl.style.display = 'none'; badgeVisionEl.style.display = 'none'; } @@ -48,6 +50,7 @@ export async function loadModels() { if (!data.models || data.models.length === 0) { modelSelectEl.innerHTML = ''; badgeThinkEl.style.display = 'none'; badgeVisionEl.style.display = 'none'; + setThinkAvailable(false); setVisionAvailable(false); return; } @@ -77,6 +80,7 @@ export async function loadModels() { console.error('[ModelBar] 加载模型失败:', err); modelSelectEl.innerHTML = ''; badgeThinkEl.style.display = 'none'; badgeVisionEl.style.display = 'none'; + setThinkAvailable(false); setVisionAvailable(false); } } @@ -117,6 +121,7 @@ async function checkModelCapability(modelName) { */ function applyCapability(modelName, caps) { setThinkAvailable(caps.think); + setVisionAvailable(caps.vision); // 更新能力标签 badgeThinkEl.style.display = caps.think ? '' : 'none'; @@ -139,6 +144,29 @@ function setThinkAvailable(available) { } } +/** + * 设置图片上传按钮是否可用 + */ +function setVisionAvailable(available) { + if (!btnAttachImg) return; + if (available) { + btnAttachImg.disabled = false; + btnAttachImg.title = '上传图片'; + btnAttachImg.classList.remove('disabled'); + } else { + btnAttachImg.disabled = true; + btnAttachImg.title = '当前模型不支持图片分析'; + btnAttachImg.classList.add('disabled'); + } +} + +/** + * 当前选择的模型是否支持视觉分析 + */ +export function isVisionAvailable() { + return btnAttachImg && !btnAttachImg.disabled; +} + /** * 获取当前选择的模型 */