feat: 重新设计模型选择栏与 Think 模式
模型选择栏: - 去掉原生 select 样式,自定义 appearance:none + 箭头图标 - 模型列表显示参数大小 (如 qwen2.5 · 7B) - 选中模型后展示能力标签 (🧠 Think / 👁️ Vision) Think 模式: - 从模型栏移至输入区域,紧邻发送按钮 - 使用灯泡图标按钮替代原来的 toggle 开关 - 默认 disabled,切换模型时调用 /api/show 检测 capabilities - capabilities 包含 'thinking' 时才启用,否则禁用并灰显 - 模型能力结果缓存,避免重复请求
This commit is contained in:
+107
-16
@@ -1,61 +1,152 @@
|
||||
/**
|
||||
* ModelBar - 模型选择栏组件
|
||||
* 包含模型下拉选择、Think 能力检测、模型信息展示
|
||||
*/
|
||||
|
||||
import { state, KEYS } from '../state.js';
|
||||
import { formatSize } from '../utils.js';
|
||||
import { showToast } from './toast.js';
|
||||
|
||||
let modelSelectEl, toggleThinkEl;
|
||||
let modelSelectEl, modelInfoBadgeEl, thinkCheckbox, thinkWrap;
|
||||
|
||||
/** 缓存模型能力 { modelName: { think: bool, vision: bool } } */
|
||||
const modelCapabilityCache = new Map();
|
||||
|
||||
export function initModelBar() {
|
||||
modelSelectEl = document.querySelector('#modelSelect');
|
||||
toggleThinkEl = document.querySelector('#toggleThink');
|
||||
modelInfoBadgeEl = document.querySelector('#modelInfoBadge');
|
||||
thinkCheckbox = document.querySelector('#toggleThink');
|
||||
thinkWrap = document.querySelector('#thinkToggleWrap');
|
||||
|
||||
// 模型选择变化 → 保存到设置
|
||||
// 模型切换 → 检测能力 + 保存设置
|
||||
modelSelectEl.addEventListener('change', async () => {
|
||||
const db = state.get(KEYS.DB);
|
||||
if (db) await db.saveSetting('selectedModel', modelSelectEl.value);
|
||||
const model = modelSelectEl.value;
|
||||
if (db()) await db().saveSetting('selectedModel', model);
|
||||
if (model) {
|
||||
await checkModelCapability(model);
|
||||
} else {
|
||||
setThinkAvailable(false);
|
||||
setModelBadge('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function db() { return state.get(KEYS.DB); }
|
||||
function api() { return state.get(KEYS.API); }
|
||||
|
||||
/**
|
||||
* 加载模型列表
|
||||
*/
|
||||
export async function loadModels() {
|
||||
const api = state.get(KEYS.API);
|
||||
const db = state.get(KEYS.DB);
|
||||
const currentSession = state.get(KEYS.CURRENT_SESSION);
|
||||
|
||||
if (!api) return;
|
||||
if (!api()) return;
|
||||
|
||||
try {
|
||||
const data = await api.listModels();
|
||||
const data = await api().listModels();
|
||||
modelSelectEl.innerHTML = '';
|
||||
|
||||
if (!data.models || data.models.length === 0) {
|
||||
modelSelectEl.innerHTML = '<option value="">未安装任何模型</option>';
|
||||
setModelBadge('');
|
||||
return;
|
||||
}
|
||||
|
||||
data.models.forEach(m => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = m.name;
|
||||
opt.textContent = `${m.name} (${formatSize(m.size)})`;
|
||||
const paramSize = m.details?.parameter_size || '';
|
||||
const family = m.details?.family || '';
|
||||
const label = paramSize ? `${m.name} · ${paramSize}` : m.name;
|
||||
opt.textContent = label;
|
||||
modelSelectEl.appendChild(opt);
|
||||
});
|
||||
|
||||
// 恢复上次选择的模型
|
||||
if (currentSession && currentSession.model) {
|
||||
const currentSession = state.get(KEYS.CURRENT_SESSION);
|
||||
if (currentSession?.model) {
|
||||
modelSelectEl.value = currentSession.model;
|
||||
}
|
||||
|
||||
if (db) await db.saveSetting('selectedModel', modelSelectEl.value);
|
||||
if (db()) await db().saveSetting('selectedModel', modelSelectEl.value);
|
||||
|
||||
// 检测当前选中模型的能力
|
||||
if (modelSelectEl.value) {
|
||||
await checkModelCapability(modelSelectEl.value);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[ModelBar] 加载模型失败:', err);
|
||||
modelSelectEl.innerHTML = '<option value="">加载失败 - 检查连接</option>';
|
||||
setModelBadge('');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 /api/show 检测模型能力
|
||||
* @param {string} modelName
|
||||
*/
|
||||
async function checkModelCapability(modelName) {
|
||||
// 命中缓存
|
||||
if (modelCapabilityCache.has(modelName)) {
|
||||
applyCapability(modelName, modelCapabilityCache.get(modelName));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!api()) return;
|
||||
|
||||
try {
|
||||
const info = await api().showModel(modelName);
|
||||
const capabilities = info.capabilities || [];
|
||||
const caps = {
|
||||
think: capabilities.includes('thinking'),
|
||||
vision: capabilities.includes('vision'),
|
||||
completion: capabilities.includes('completion'),
|
||||
};
|
||||
modelCapabilityCache.set(modelName, caps);
|
||||
applyCapability(modelName, caps);
|
||||
} catch (err) {
|
||||
console.warn('[ModelBar] 获取模型能力失败:', err);
|
||||
// 降级:不支持 think
|
||||
const fallback = { think: false, vision: false, completion: true };
|
||||
modelCapabilityCache.set(modelName, fallback);
|
||||
applyCapability(modelName, fallback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据能力设置 UI 状态
|
||||
*/
|
||||
function applyCapability(modelName, caps) {
|
||||
setThinkAvailable(caps.think);
|
||||
|
||||
// 更新信息标签
|
||||
const tags = [];
|
||||
if (caps.think) tags.push('🧠 Think');
|
||||
if (caps.vision) tags.push('👁️ Vision');
|
||||
setModelBadge(tags.join(' '));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Think 开关是否可用
|
||||
*/
|
||||
function setThinkAvailable(available) {
|
||||
if (available) {
|
||||
thinkCheckbox.disabled = false;
|
||||
thinkWrap.title = 'Think 模式:启用深度推理';
|
||||
thinkWrap.classList.remove('unavailable');
|
||||
} else {
|
||||
thinkCheckbox.checked = false;
|
||||
thinkCheckbox.disabled = true;
|
||||
thinkWrap.title = '当前模型不支持 Think 模式';
|
||||
thinkWrap.classList.add('unavailable');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新模型信息标签
|
||||
*/
|
||||
function setModelBadge(text) {
|
||||
modelInfoBadgeEl.textContent = text;
|
||||
modelInfoBadgeEl.style.display = text ? '' : 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选择的模型
|
||||
*/
|
||||
@@ -74,5 +165,5 @@ export function setSelectedModel(modelName) {
|
||||
* Think 模式是否开启
|
||||
*/
|
||||
export function isThinkEnabled() {
|
||||
return toggleThinkEl.checked;
|
||||
return thinkCheckbox.checked && !thinkCheckbox.disabled;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user