diff --git a/static/js/settings.js b/static/js/settings.js index 6089717..be073ed 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -1,3 +1,317 @@ +/** + * 模型选择器类 - 处理模型选择下拉列表的所有交互 + */ +class ModelSelector { + /** + * 构造函数 + * @param {Object} options 配置选项 + * @param {Function} options.onChange 选择变更回调 + * @param {Object} options.models 模型定义对象 + * @param {Object} options.providers 提供商定义对象 + */ + constructor(options) { + this.options = options || {}; + this.models = this.options.models || {}; + this.providers = this.options.providers || {}; + this.onChange = this.options.onChange || (() => {}); + + // 元素引用 + this.container = document.getElementById('modelSelector'); + this.display = this.container?.querySelector('.model-display'); + this.currentNameEl = document.getElementById('currentModelName'); + this.currentProviderEl = document.getElementById('currentModelProvider'); + this.badgesContainer = document.getElementById('modelBadges'); + this.originalSelect = document.getElementById('modelSelect'); + + // 创建下拉面板和遮罩 + this.createDropdownElements(); + + // 当前选中的模型ID + this.selectedModelId = null; + + // 初始化 + this.initEvents(); + } + + /** + * 创建下拉面板和遮罩元素 + */ + createDropdownElements() { + // 创建遮罩层 + this.overlay = document.createElement('div'); + this.overlay.className = 'model-dropdown-overlay'; + document.body.appendChild(this.overlay); + + // 创建下拉面板 + this.dropdown = document.createElement('div'); + this.dropdown.className = 'model-dropdown-panel'; + document.body.appendChild(this.dropdown); + } + + /** + * 初始化事件监听 + */ + initEvents() { + if (!this.display) return; + + // 点击选择器显示下拉框 + this.display.addEventListener('click', (e) => { + e.stopPropagation(); + this.toggleDropdown(); + }); + + // 点击遮罩层关闭下拉框 + this.overlay.addEventListener('click', () => { + this.closeDropdown(); + }); + + // 监听原始select变化,保持同步 + if (this.originalSelect) { + this.originalSelect.addEventListener('change', () => { + const modelId = this.originalSelect.value; + if (modelId && modelId !== this.selectedModelId) { + this.selectModel(modelId, false); // 不触发onChange避免循环 + } + }); + } + + // 防止面板内部点击冒泡到遮罩 + this.dropdown.addEventListener('click', (e) => { + e.stopPropagation(); + }); + + console.log('模型选择器事件初始化完成'); + } + + /** + * 加载模型选项到下拉面板 + */ + loadModelOptions() { + if (!this.dropdown) return; + + // 清空下拉面板 + this.dropdown.innerHTML = ''; + + // 按提供商分组模型 + const groupedModels = {}; + Object.entries(this.models).forEach(([modelId, model]) => { + const providerId = model.provider; + if (!groupedModels[providerId]) { + groupedModels[providerId] = []; + } + groupedModels[providerId].push({ id: modelId, ...model }); + }); + + // 创建分组选项 + Object.entries(groupedModels).forEach(([providerId, models]) => { + const provider = this.providers[providerId] || { name: providerId }; + + // 创建分组容器 + const group = document.createElement('div'); + group.className = 'model-group'; + + // 创建分组标题 + const title = document.createElement('div'); + title.className = 'model-group-title'; + title.textContent = provider.name; + group.appendChild(title); + + // 添加该分组的模型选项 + models.sort((a, b) => a.name.localeCompare(b.name)) + .forEach(model => { + const option = document.createElement('div'); + option.className = 'model-option'; + option.dataset.modelId = model.id; + if (model.id === this.selectedModelId) { + option.classList.add('selected'); + } + + // 模型名称 - 移除版本显示 + const nameEl = document.createElement('div'); + nameEl.className = 'model-option-name'; + nameEl.textContent = model.name; + option.appendChild(nameEl); + + // 能力徽章 + if (model.supportsMultimodal || model.isReasoning) { + const badges = document.createElement('div'); + badges.className = 'model-option-badges'; + + if (model.supportsMultimodal) { + const badge = document.createElement('div'); + badge.className = 'model-option-badge'; + badge.title = '支持图像'; + badge.innerHTML = ''; + badges.appendChild(badge); + } + + if (model.isReasoning) { + const badge = document.createElement('div'); + badge.className = 'model-option-badge'; + badge.title = '支持深度推理'; + badge.innerHTML = ''; + badges.appendChild(badge); + } + + option.appendChild(badges); + } + + // 点击选项选择模型 + option.addEventListener('click', () => { + this.selectModel(model.id); + this.closeDropdown(); + }); + + group.appendChild(option); + }); + + // 只添加有模型的分组 + if (group.childElementCount > 1) { // > 1 因为包含标题 + this.dropdown.appendChild(group); + } + }); + + console.log('模型选项加载完成'); + } + + /** + * 打开下拉面板 + */ + openDropdown() { + if (!this.container || !this.dropdown || !this.overlay) return; + + // 加载模型选项 + this.loadModelOptions(); + + // 显示遮罩和下拉面板 + this.overlay.style.display = 'block'; + this.dropdown.style.display = 'block'; + + // 设置下拉面板位置 + const rect = this.display.getBoundingClientRect(); + this.dropdown.style.width = `${rect.width}px`; + this.dropdown.style.top = `${rect.bottom + 5}px`; + this.dropdown.style.left = `${rect.left}px`; + + // 延迟添加可见类以启用过渡效果 + setTimeout(() => { + this.dropdown.classList.add('visible'); + }, 10); + + // 添加打开状态类 + this.container.classList.add('open'); + + // 确保当前选中的选项可见 + setTimeout(() => { + const selectedOption = this.dropdown.querySelector('.model-option.selected'); + if (selectedOption) { + selectedOption.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + }, 100); + } + + /** + * 关闭下拉面板 + */ + closeDropdown() { + if (!this.container || !this.dropdown || !this.overlay) return; + + // 移除可见类 + this.dropdown.classList.remove('visible'); + + // 移除打开状态类 + this.container.classList.remove('open'); + + // 延迟隐藏元素,以便完成过渡动画 + setTimeout(() => { + this.overlay.style.display = 'none'; + this.dropdown.style.display = 'none'; + }, 200); + } + + /** + * 切换下拉面板显示状态 + */ + toggleDropdown() { + if (this.container.classList.contains('open')) { + this.closeDropdown(); + } else { + this.openDropdown(); + } + } + + /** + * 选择模型 + * @param {string} modelId 模型ID + * @param {boolean} triggerChange 是否触发onChange回调,默认true + */ + selectModel(modelId, triggerChange = true) { + const model = this.models[modelId]; + if (!model) return; + + // 更新选中的模型ID + this.selectedModelId = modelId; + + // 更新显示信息 + this.updateDisplayInfo(model); + + // 更新原始select + if (this.originalSelect && this.originalSelect.value !== modelId) { + this.originalSelect.value = modelId; + } + + // 触发变更回调 + if (triggerChange) { + this.onChange(modelId, model); + } + + console.log('已选择模型:', modelId); + } + + /** + * 更新显示信息 + * @param {Object} model 模型信息 + */ + updateDisplayInfo(model) { + if (!this.currentNameEl || !this.currentProviderEl || !this.badgesContainer) return; + + // 更新模型名称 - 不在这里显示版本号 + this.currentNameEl.textContent = model.name; + + // 更新提供商 + const provider = this.providers[model.provider] || { name: model.provider }; + this.currentProviderEl.textContent = provider.name; + + // 更新能力徽章 + this.badgesContainer.innerHTML = ''; + if (model.supportsMultimodal) { + const badge = document.createElement('div'); + badge.className = 'model-badge'; + badge.title = '支持图像'; + badge.innerHTML = ''; + this.badgesContainer.appendChild(badge); + } + + if (model.isReasoning) { + const badge = document.createElement('div'); + badge.className = 'model-badge'; + badge.title = '支持深度推理'; + badge.innerHTML = ''; + this.badgesContainer.appendChild(badge); + } + } + + /** + * 设置模型数据 + * @param {Object} models 模型定义对象 + * @param {Object} providers 提供商定义对象 + */ + setModelData(models, providers) { + this.models = models || {}; + this.providers = providers || {}; + } +} + class SettingsManager { constructor() { // 初始化属性 @@ -11,6 +325,9 @@ class SettingsManager { this.prompts = {}; this.currentPromptId = 'default'; + // 模型选择器对象 + this.modelSelector = null; + // 加载模型配置 this.isInitialized = false; this.initialize(); @@ -21,12 +338,12 @@ class SettingsManager { // 加载模型配置 await this.loadModelConfig(); - // 成功加载配置后,执行后续初始化 - this.updateModelOptions(); + // 成功加载配置后,执行后续初始化 + this.updateModelOptions(); await this.loadSettings(); await this.loadPrompts(); // 加载提示词配置 - this.setupEventListeners(); - this.updateUIBasedOnModelType(); + this.setupEventListeners(); + this.updateUIBasedOnModelType(); // 初始化可折叠内容逻辑 this.initCollapsibleContent(); @@ -37,246 +354,68 @@ class SettingsManager { this.highlightActivePreset(); } + // 初始化模型选择器 + this.initModelSelector(); + + // 添加到window对象,方便在控制台调试 + window.debugModelSelector = { + open: () => this.modelSelector?.openDropdown(), + close: () => this.modelSelector?.closeDropdown(), + toggle: () => this.modelSelector?.toggleDropdown(), + instance: this.modelSelector + }; + this.isInitialized = true; console.log('设置管理器初始化完成'); } catch (error) { console.error('初始化设置管理器失败:', error); - window.uiManager?.showToast('加载模型配置失败,使用默认配置', 'error'); - - // 使用默认配置作为备份 - this.setupDefaultModels(); - this.updateModelOptions(); + window.uiManager?.showToast('加载模型配置失败,使用默认配置', 'error'); + + // 使用默认配置作为备份 + this.setupDefaultModels(); + this.updateModelOptions(); await this.loadSettings(); await this.loadPrompts(); // 加载提示词配置 - this.setupEventListeners(); - this.updateUIBasedOnModelType(); - - // 初始化可折叠内容逻辑 - this.initCollapsibleContent(); + this.setupEventListeners(); + this.updateUIBasedOnModelType(); + + // 初始化可折叠内容逻辑 + this.initCollapsibleContent(); + + // 初始化模型选择器 + this.initModelSelector(); this.isInitialized = true; } } - - // 从配置文件加载模型定义 - async loadModelConfig() { - try { - // 使用API端点获取模型列表 - const response = await fetch('/api/models'); - if (!response.ok) { - throw new Error(`加载模型列表失败: ${response.status} ${response.statusText}`); - } - - // 获取模型列表 - const modelsList = await response.json(); - - // 获取提供商配置 - const configResponse = await fetch('/config/models.json'); - if (!configResponse.ok) { - throw new Error(`加载提供商配置失败: ${configResponse.status} ${configResponse.statusText}`); - } - - const config = await configResponse.json(); - - // 保存提供商定义 - this.providerDefinitions = config.providers || {}; - - // 处理模型定义 - this.modelDefinitions = {}; - - // 从API获取的模型列表创建模型定义 - modelsList.forEach(model => { - this.modelDefinitions[model.id] = { - name: model.display_name, - provider: this.getProviderIdByModel(model.id, config), - supportsMultimodal: model.is_multimodal, - isReasoning: model.is_reasoning, - apiKeyId: this.getApiKeyIdByModel(model.id, config), - description: model.description, - version: this.getVersionByModel(model.id, config) - }; - }); - - console.log('模型配置加载成功:', this.modelDefinitions); - } catch (error) { - console.error('加载模型配置时出错:', error); - throw error; - } - } - // 从配置中根据模型ID获取提供商ID - getProviderIdByModel(modelId, config) { - const modelConfig = config.models[modelId]; - return modelConfig ? modelConfig.provider : 'unknown'; - } - - // 从配置中根据模型ID获取API密钥ID - getApiKeyIdByModel(modelId, config) { - const modelConfig = config.models[modelId]; - if (!modelConfig) return null; - - const providerId = modelConfig.provider; - const provider = config.providers[providerId]; - return provider ? provider.api_key_id : null; - } - - // 从配置中根据模型ID获取版本信息 - getVersionByModel(modelId, config) { - const modelConfig = config.models[modelId]; - return modelConfig ? modelConfig.version : 'latest'; - } - - // 设置默认模型定义(当配置加载失败时使用) - setupDefaultModels() { - this.providerDefinitions = { - 'anthropic': { - name: 'Anthropic', - api_key_id: 'AnthropicApiKey' - }, - 'openai': { - name: 'OpenAI', - api_key_id: 'OpenaiApiKey' - }, - 'deepseek': { - name: 'DeepSeek', - api_key_id: 'DeepseekApiKey' - } - }; - - this.modelDefinitions = { - 'claude-3-7-sonnet-20250219': { - name: 'Claude 3.7 Sonnet', - provider: 'anthropic', - supportsMultimodal: true, - isReasoning: true, - apiKeyId: 'AnthropicApiKey', - version: '20250219' - }, - 'gpt-4o-2024-11-20': { - name: 'GPT-4o', - provider: 'openai', - supportsMultimodal: true, - isReasoning: false, - apiKeyId: 'OpenaiApiKey', - version: '2024-11-20' - }, - 'deepseek-reasoner': { - name: 'DeepSeek Reasoner', - provider: 'deepseek', - supportsMultimodal: false, - isReasoning: true, - apiKeyId: 'DeepseekApiKey', - version: 'latest' - } - }; - - console.log('使用默认模型配置'); - } - - initializeElements() { - // Settings panel elements - this.settingsPanel = document.getElementById('settingsPanel'); - this.modelSelect = document.getElementById('modelSelect'); - this.temperatureInput = document.getElementById('temperature'); - this.temperatureGroup = document.querySelector('.setting-group:has(#temperature)') || - document.querySelector('div.setting-group:has(input[id="temperature"])'); - this.systemPromptInput = document.getElementById('systemPrompt'); - this.promptDescriptionElement = document.getElementById('promptDescription'); - this.languageInput = document.getElementById('language'); - this.proxyEnabledInput = document.getElementById('proxyEnabled'); - this.proxyHostInput = document.getElementById('proxyHost'); - this.proxyPortInput = document.getElementById('proxyPort'); - this.proxySettings = document.getElementById('proxySettings'); - - // 提示词管理相关元素 - this.promptSelect = document.getElementById('promptSelect'); - this.savePromptBtn = document.getElementById('savePromptBtn'); - this.newPromptBtn = document.getElementById('newPromptBtn'); - this.deletePromptBtn = document.getElementById('deletePromptBtn'); - - // 提示词对话框元素 - this.promptDialog = document.getElementById('promptDialog'); - this.promptDialogOverlay = document.getElementById('promptDialogOverlay'); - this.promptIdInput = document.getElementById('promptId'); - this.promptNameInput = document.getElementById('promptName'); - this.promptContentInput = document.getElementById('promptContent'); - this.promptDescriptionInput = document.getElementById('promptDescriptionEdit'); - this.cancelPromptBtn = document.getElementById('cancelPromptBtn'); - this.confirmPromptBtn = document.getElementById('confirmPromptBtn'); - - // 最大Token设置元素 - 现在是输入框而不是滑块 - this.maxTokens = document.getElementById('maxTokens'); - this.maxTokensValue = document.getElementById('maxTokensValue'); - this.tokenPresets = document.querySelectorAll('.token-preset'); - - // 理性推理相关元素 - this.reasoningDepthSelect = document.getElementById('reasoningDepth'); - this.reasoningSettingGroup = document.querySelector('.reasoning-setting-group'); - this.thinkBudgetPercentInput = document.getElementById('thinkBudgetPercent'); - this.thinkBudgetPercentValue = document.getElementById('thinkBudgetPercentValue'); - this.thinkBudgetGroup = document.querySelector('.think-budget-group'); - - // Initialize Mathpix inputs - this.mathpixAppIdInput = document.getElementById('mathpixAppId'); - this.mathpixAppKeyInput = document.getElementById('mathpixAppKey'); - - // API Key elements - 所有的密钥输入框 - this.apiKeyInputs = { - 'AnthropicApiKey': document.getElementById('AnthropicApiKey'), - 'OpenaiApiKey': document.getElementById('OpenaiApiKey'), - 'DeepseekApiKey': document.getElementById('DeepseekApiKey'), - 'AlibabaApiKey': document.getElementById('AlibabaApiKey'), - 'mathpixAppId': this.mathpixAppIdInput, - 'mathpixAppKey': this.mathpixAppKeyInput - }; - - // API密钥状态显示相关元素 - this.apiKeysList = document.getElementById('apiKeysList'); - - // 防止API密钥区域的点击事件冒泡 - if (this.apiKeysList) { - this.apiKeysList.addEventListener('click', (e) => { - e.stopPropagation(); - }); - } - - // Settings toggle elements - this.settingsToggle = document.getElementById('settingsToggle'); - this.closeSettings = document.getElementById('closeSettings'); - - // 获取所有密钥输入组元素 - this.apiKeyGroups = document.querySelectorAll('.api-key-group'); - - // Initialize API key toggle buttons - document.querySelectorAll('.toggle-api-key').forEach(button => { - button.addEventListener('click', (e) => { - const input = e.currentTarget.closest('.input-group').querySelector('input'); - const type = input.type === 'password' ? 'text' : 'password'; - input.type = type; - const icon = e.currentTarget.querySelector('i'); - if (icon) { - icon.className = `fas fa-${type === 'password' ? 'eye' : 'eye-slash'}`; + // 初始化新的模型选择器 + initModelSelector() { + if (this.modelSelector) { + // 如果已存在,更新数据 + this.modelSelector.setModelData(this.modelDefinitions, this.providerDefinitions); + } else { + // 创建新实例 + this.modelSelector = new ModelSelector({ + models: this.modelDefinitions, + providers: this.providerDefinitions, + onChange: (modelId) => { + // 处理模型变更 + console.log('模型已变更:', modelId); + this.updateVisibleApiKey(modelId); + this.updateUIBasedOnModelType(); + this.updateModelVersionDisplay(modelId); + this.saveSettings(); } }); - }); + } - // 存储API密钥的对象 - this.apiKeyValues = { - 'AnthropicApiKey': '', - 'OpenaiApiKey': '', - 'DeepseekApiKey': '', - 'AlibabaApiKey': '', - 'MathpixAppId': '', - 'MathpixAppKey': '' - }; - - // 初始化密钥编辑功能 - this.initApiKeyEditFunctions(); - - this.reasoningOptions = document.querySelectorAll('.reasoning-option'); - this.thinkPresets = document.querySelectorAll('.think-preset'); + // 设置当前选择的模型 + if (this.modelSelect && this.modelSelect.value) { + this.modelSelector.selectModel(this.modelSelect.value, false); + } } - + // 更新模型选择下拉框 updateModelOptions() { // 清空现有选项 @@ -300,16 +439,12 @@ class SettingsManager { // 添加该提供商的模型选项 for (const [modelId, modelInfo] of providerModels) { + // 添加到原始select元素 const option = document.createElement('option'); option.value = modelId; - // 显示模型名称和版本(如果不是latest) - let displayName = modelInfo.name; - if (modelInfo.version && modelInfo.version !== 'latest') { - displayName += ` (${modelInfo.version})`; - } - - option.textContent = displayName; + // 只显示模型名称,不再显示版本号 + option.textContent = modelInfo.name; optgroup.appendChild(option); } @@ -319,35 +454,40 @@ class SettingsManager { } } } - + async loadSettings() { try { // 先从localStorage加载大部分设置 - const settings = JSON.parse(localStorage.getItem('aiSettings') || '{}'); - + const settings = JSON.parse(localStorage.getItem('aiSettings') || '{}'); + // 刷新API密钥状态(自动从服务器获取最新状态) await this.refreshApiKeyStatus(); console.log('已自动刷新API密钥状态'); // 加载其他设置 - // Load model selection - if (settings.model && this.modelExists(settings.model)) { - this.modelSelect.value = settings.model; - this.updateVisibleApiKey(settings.model); - } - - // Load max tokens setting - 现在直接设置输入框值 - if (settings.maxTokens) { - this.maxTokens.value = settings.maxTokens; - this.updateTokenValueDisplay(); - this.highlightActivePreset(); - } + // Load model selection + if (settings.model && this.modelExists(settings.model)) { + this.modelSelect.value = settings.model; + this.updateVisibleApiKey(settings.model); + + // 使用新的模型选择器更新UI + if (this.modelSelector) { + this.modelSelector.selectModel(settings.model, false); + } + } + + // Load max tokens setting - 现在直接设置输入框值 + if (settings.maxTokens) { + this.maxTokens.value = settings.maxTokens; + this.updateTokenValueDisplay(); + this.highlightActivePreset(); + } // Load reasoning depth & think budget settings if (settings.reasoningDepth) { this.reasoningDepthSelect.value = settings.reasoningDepth; - // 更新推理深度选项UI - this.updateReasoningOptionUI(settings.reasoningDepth); + // 更新推理深度选项UI + this.updateReasoningOptionUI(settings.reasoningDepth); } // 加载思考预算百分比 @@ -356,12 +496,12 @@ class SettingsManager { this.thinkBudgetPercentInput.value = thinkBudgetPercent; } - // 更新思考预算显示和滑块背景 + // 更新思考预算显示和滑块背景 this.updateThinkBudgetDisplay(); - this.updateThinkBudgetSliderBackground(); - this.highlightActiveThinkPreset(); + this.updateThinkBudgetSliderBackground(); + this.highlightActiveThinkPreset(); - // Load temperature setting + // Load temperature setting if (settings.temperature) { this.temperatureInput.value = settings.temperature; } @@ -408,22 +548,50 @@ class SettingsManager { // 更新模型版本显示 updateModelVersionDisplay(modelId) { + const modelVersionInfo = document.getElementById('modelVersionInfo'); const modelVersionText = document.getElementById('modelVersionText'); - if (!modelVersionText) return; + if (!modelVersionText || !modelVersionInfo) return; const model = this.modelDefinitions[modelId]; if (!model) { modelVersionText.textContent = '-'; + modelVersionInfo.classList.remove('has-version'); return; } // 显示版本信息(如果有) if (model.version && model.version !== 'latest') { + // 设置版本文本 modelVersionText.textContent = model.version; + // 添加具有版本的类 + modelVersionInfo.classList.add('has-version'); + + // 统一使用分支图标和紫色 + const icon = modelVersionInfo.querySelector('i'); + if (icon) { + icon.className = 'fas fa-code-branch'; + icon.title = `版本 ${model.version}`; + } + + // 移除所有特定版本类型的类 + modelVersionInfo.classList.remove('date-version', 'semantic-version'); } else if (model.version === 'latest') { - modelVersionText.textContent = '最新版'; + // 使用英文"latest"而不是中文 + modelVersionText.textContent = 'latest'; + modelVersionInfo.classList.add('has-version'); + + // 对latest版本也使用相同的分支图标 + const icon = modelVersionInfo.querySelector('i'); + if (icon) { + icon.className = 'fas fa-code-branch'; + icon.title = '最新版本'; + } + + // 移除所有特定版本类型的类 + modelVersionInfo.classList.remove('date-version', 'semantic-version'); } else { modelVersionText.textContent = '-'; + modelVersionInfo.classList.remove('has-version', 'date-version', 'semantic-version'); } } @@ -512,7 +680,7 @@ class SettingsManager { const settings = { apiKeys: this.apiKeyValues, // 保存到localStorage(向后兼容) model: this.modelSelect.value, - maxTokens: this.maxTokens.value, + maxTokens: this.maxTokens.value, reasoningDepth: this.reasoningDepthSelect?.value || 'standard', thinkBudgetPercent: this.thinkBudgetPercentInput?.value || '50', temperature: this.temperatureInput.value, @@ -707,9 +875,11 @@ class SettingsManager { if (this.maxTokens) { this.maxTokens.addEventListener('input', () => { this.updateTokenValueDisplay(); + this.updateTokenSliderBackground(); this.highlightActivePreset(); - }); - + this.saveSettings(); + }); + this.maxTokens.addEventListener('change', () => { this.saveSettings(); }); @@ -719,9 +889,9 @@ class SettingsManager { if (this.reasoningOptions && this.reasoningOptions.length > 0) { this.reasoningOptions.forEach(option => { option.addEventListener('click', (e) => { - // 阻止事件冒泡 - e.stopPropagation(); - + // 阻止事件冒泡 + e.stopPropagation(); + // 获取选择的值 const value = option.getAttribute('data-value'); @@ -741,8 +911,8 @@ class SettingsManager { const showThinkBudget = value === 'extended'; this.thinkBudgetGroup.style.display = showThinkBudget ? 'block' : 'none'; } - - this.saveSettings(); + + this.saveSettings(); }); }); } @@ -751,9 +921,9 @@ class SettingsManager { if (this.thinkPresets && this.thinkPresets.length > 0) { this.thinkPresets.forEach(preset => { preset.addEventListener('click', (e) => { - // 阻止事件冒泡 - e.stopPropagation(); - + // 阻止事件冒泡 + e.stopPropagation(); + // 获取预设值 const value = parseInt(preset.getAttribute('data-value')); @@ -769,7 +939,7 @@ class SettingsManager { // 更新预设按钮样式 this.highlightActiveThinkPreset(); - this.saveSettings(); + this.saveSettings(); }); }); } @@ -829,13 +999,17 @@ class SettingsManager { }); // Panel visibility + if (this.settingsToggle) { this.settingsToggle.addEventListener('click', () => { this.toggleSettingsPanel(); }); + } + if (this.closeSettings) { this.closeSettings.addEventListener('click', () => { this.closeSettingsPanel(); }); + } // 确保设置面板自身的点击不会干扰内部操作 if (this.settingsPanel) { @@ -887,8 +1061,13 @@ class SettingsManager { this.updateThinkBudgetSliderBackground(); }); } + + // 确保自定义模型选择器事件监听器被初始化 + if (this.modelSelectorDisplay && this.modelDropdown) { + this.initCustomSelectorEvents(); + } } - + // 更新思考预算显示 updateThinkBudgetDisplay() { if (this.thinkBudgetPercentInput && this.thinkBudgetPercentValue) { @@ -1172,20 +1351,20 @@ class SettingsManager { window.uiManager.showToast('密钥已保存', 'success'); } else { // 如果UIManager不可用,使用自己的方法作为备选 - this.createToast('密钥已保存', 'success'); + this.createToast('密钥已保存', 'success'); } } else { if (window.uiManager) { window.uiManager.showToast('保存密钥失败: ' + result.message, 'error'); - } else { - this.createToast('保存密钥失败: ' + result.message, 'error'); + } else { + this.createToast('保存密钥失败: ' + result.message, 'error'); } } } else { if (window.uiManager) { window.uiManager.showToast('无法连接到服务器', 'error'); - } else { - this.createToast('无法连接到服务器', 'error'); + } else { + this.createToast('无法连接到服务器', 'error'); } } } catch (error) { @@ -1193,7 +1372,7 @@ class SettingsManager { if (window.uiManager) { window.uiManager.showToast('保存密钥出错: ' + error.message, 'error'); } else { - this.createToast('保存密钥出错: ' + error.message, 'error'); + this.createToast('保存密钥出错: ' + error.message, 'error'); } } } @@ -1316,7 +1495,7 @@ class SettingsManager { // 更新提示词选择下拉框 if (this.promptSelect) { - this.updatePromptSelect(); + this.updatePromptSelect(); } // 如果有默认提示词,加载它 @@ -1603,11 +1782,11 @@ class SettingsManager { */ closePromptDialog() { if (this.promptDialog) { - this.promptDialog.classList.remove('active'); + this.promptDialog.classList.remove('active'); } if (this.promptDialogOverlay) { - this.promptDialogOverlay.classList.remove('active'); + this.promptDialogOverlay.classList.remove('active'); } } @@ -1661,6 +1840,225 @@ class SettingsManager { } }); } + + // 从配置文件加载模型定义 + async loadModelConfig() { + try { + // 使用API端点获取模型列表 + const response = await fetch('/api/models'); + if (!response.ok) { + throw new Error(`加载模型列表失败: ${response.status} ${response.statusText}`); + } + + // 获取模型列表 + const modelsList = await response.json(); + + // 获取提供商配置 + const configResponse = await fetch('/config/models.json'); + if (!configResponse.ok) { + throw new Error(`加载提供商配置失败: ${configResponse.status} ${configResponse.statusText}`); + } + + const config = await configResponse.json(); + + // 保存提供商定义 + this.providerDefinitions = config.providers || {}; + + // 处理模型定义 + this.modelDefinitions = {}; + + // 从API获取的模型列表创建模型定义 + modelsList.forEach(model => { + this.modelDefinitions[model.id] = { + name: model.display_name, + provider: this.getProviderIdByModel(model.id, config), + supportsMultimodal: model.is_multimodal, + isReasoning: model.is_reasoning, + apiKeyId: this.getApiKeyIdByModel(model.id, config), + description: model.description, + version: this.getVersionByModel(model.id, config) + }; + }); + + console.log('模型配置加载成功:', this.modelDefinitions); + } catch (error) { + console.error('加载模型配置时出错:', error); + throw error; + } + } + + // 从配置中根据模型ID获取提供商ID + getProviderIdByModel(modelId, config) { + const modelConfig = config.models[modelId]; + return modelConfig ? modelConfig.provider : 'unknown'; + } + + // 从配置中根据模型ID获取API密钥ID + getApiKeyIdByModel(modelId, config) { + const modelConfig = config.models[modelId]; + if (!modelConfig) return null; + + const providerId = modelConfig.provider; + const provider = config.providers[providerId]; + return provider ? provider.api_key_id : null; + } + + // 从配置中根据模型ID获取版本信息 + getVersionByModel(modelId, config) { + const modelConfig = config.models[modelId]; + return modelConfig ? modelConfig.version : 'latest'; + } + + // 设置默认模型定义(当配置加载失败时使用) + setupDefaultModels() { + this.providerDefinitions = { + 'anthropic': { + name: 'Anthropic', + api_key_id: 'AnthropicApiKey' + }, + 'openai': { + name: 'OpenAI', + api_key_id: 'OpenaiApiKey' + }, + 'deepseek': { + name: 'DeepSeek', + api_key_id: 'DeepseekApiKey' + } + }; + + this.modelDefinitions = { + 'claude-3-7-sonnet-20250219': { + name: 'Claude 3.7 Sonnet', + provider: 'anthropic', + supportsMultimodal: true, + isReasoning: true, + apiKeyId: 'AnthropicApiKey', + version: '20250219' + }, + 'gpt-4o-2024-11-20': { + name: 'GPT-4o', + provider: 'openai', + supportsMultimodal: true, + isReasoning: false, + apiKeyId: 'OpenaiApiKey', + version: '2024-11-20' + }, + 'deepseek-reasoner': { + name: 'DeepSeek Reasoner', + provider: 'deepseek', + supportsMultimodal: false, + isReasoning: true, + apiKeyId: 'DeepseekApiKey', + version: 'latest' + } + }; + + console.log('使用默认模型配置'); + } + + initializeElements() { + // Settings panel elements + this.settingsPanel = document.getElementById('settingsPanel'); + this.modelSelect = document.getElementById('modelSelect'); + this.temperatureInput = document.getElementById('temperature'); + this.temperatureGroup = document.querySelector('.setting-group:has(#temperature)') || + document.querySelector('div.setting-group:has(input[id="temperature"])'); + this.systemPromptInput = document.getElementById('systemPrompt'); + this.promptDescriptionElement = document.getElementById('promptDescription'); + this.languageInput = document.getElementById('language'); + this.proxyEnabledInput = document.getElementById('proxyEnabled'); + this.proxyHostInput = document.getElementById('proxyHost'); + this.proxyPortInput = document.getElementById('proxyPort'); + this.proxySettings = document.getElementById('proxySettings'); + + // 提示词管理相关元素 + this.promptSelect = document.getElementById('promptSelect'); + this.savePromptBtn = document.getElementById('savePromptBtn'); + this.newPromptBtn = document.getElementById('newPromptBtn'); + this.deletePromptBtn = document.getElementById('deletePromptBtn'); + + // 提示词对话框元素 + this.promptDialog = document.getElementById('promptDialog'); + this.promptDialogOverlay = document.getElementById('promptDialogOverlay'); + this.promptIdInput = document.getElementById('promptId'); + this.promptNameInput = document.getElementById('promptName'); + this.promptContentInput = document.getElementById('promptContent'); + this.promptDescriptionInput = document.getElementById('promptDescriptionEdit'); + this.cancelPromptBtn = document.getElementById('cancelPromptBtn'); + this.confirmPromptBtn = document.getElementById('confirmPromptBtn'); + + // 最大Token设置元素 - 现在是输入框而不是滑块 + this.maxTokens = document.getElementById('maxTokens'); + this.maxTokensValue = document.getElementById('maxTokensValue'); + this.tokenPresets = document.querySelectorAll('.token-preset'); + + // 理性推理相关元素 + this.reasoningDepthSelect = document.getElementById('reasoningDepth'); + this.reasoningSettingGroup = document.querySelector('.reasoning-setting-group'); + this.thinkBudgetPercentInput = document.getElementById('thinkBudgetPercent'); + this.thinkBudgetPercentValue = document.getElementById('thinkBudgetPercentValue'); + this.thinkBudgetGroup = document.querySelector('.think-budget-group'); + + // Initialize Mathpix inputs + this.mathpixAppIdInput = document.getElementById('mathpixAppId'); + this.mathpixAppKeyInput = document.getElementById('mathpixAppKey'); + + // API Key elements - 所有的密钥输入框 + this.apiKeyInputs = { + 'AnthropicApiKey': document.getElementById('AnthropicApiKey'), + 'OpenaiApiKey': document.getElementById('OpenaiApiKey'), + 'DeepseekApiKey': document.getElementById('DeepseekApiKey'), + 'AlibabaApiKey': document.getElementById('AlibabaApiKey'), + 'mathpixAppId': this.mathpixAppIdInput, + 'mathpixAppKey': this.mathpixAppKeyInput + }; + + // API密钥状态显示相关元素 + this.apiKeysList = document.getElementById('apiKeysList'); + + // 防止API密钥区域的点击事件冒泡 + if (this.apiKeysList) { + this.apiKeysList.addEventListener('click', (e) => { + e.stopPropagation(); + }); + } + + // Settings toggle elements + this.settingsToggle = document.getElementById('settingsToggle'); + this.closeSettings = document.getElementById('closeSettings'); + + // 获取所有密钥输入组元素 + this.apiKeyGroups = document.querySelectorAll('.api-key-group'); + + // Initialize API key toggle buttons + document.querySelectorAll('.toggle-api-key').forEach(button => { + button.addEventListener('click', (e) => { + const input = e.currentTarget.closest('.input-group').querySelector('input'); + const type = input.type === 'password' ? 'text' : 'password'; + input.type = type; + const icon = e.currentTarget.querySelector('i'); + if (icon) { + icon.className = `fas fa-${type === 'password' ? 'eye' : 'eye-slash'}`; + } + }); + }); + + // 存储API密钥的对象 + this.apiKeyValues = { + 'AnthropicApiKey': '', + 'OpenaiApiKey': '', + 'DeepseekApiKey': '', + 'AlibabaApiKey': '', + 'MathpixAppId': '', + 'MathpixAppKey': '' + }; + + // 初始化密钥编辑功能 + this.initApiKeyEditFunctions(); + + this.reasoningOptions = document.querySelectorAll('.reasoning-option'); + this.thinkPresets = document.querySelectorAll('.think-preset'); + } } // Export for use in other modules diff --git a/static/style.css b/static/style.css index 466277a..0aa30bb 100644 --- a/static/style.css +++ b/static/style.css @@ -8,7 +8,6 @@ --text-primary: #24292e; --text-secondary: #586069; --text-tertiary: #6a737d; - --text-muted: #6a737d; --text-success: #28a745; --text-warning: #f66a0a; --text-error: #d73a49; @@ -29,12 +28,6 @@ --screen-sm: 480px; --editor-padding: 0.5rem; - /* 用于模型选择下拉框的颜色变量 */ - --input-bg: #ffffff; - --dropdown-bg: #ffffff; - --hover-bg: #f6f8fa; - --badge-bg: #f1f1f1; - /* 其他颜色 */ --error-color: #e53935; --success-color: #4CAF50; @@ -56,7 +49,6 @@ --text-primary: #c9d1d9; --text-secondary: #8b949e; --text-tertiary: #6e7681; - --text-muted: #8b949e; --text-success: #3fb950; --text-warning: #d29922; --text-error: #f85149; @@ -72,12 +64,6 @@ --button-hover: #2a303b; --button-active: #252c36; - /* 用于模型选择下拉框的颜色变量 */ - --input-bg: #161b22; - --dropdown-bg: #161b22; - --hover-bg: #1f2937; - --badge-bg: #1f2428; - /* 其他颜色 */ --background: #0d1117; --text: #c9d1d9; @@ -1300,19 +1286,51 @@ textarea#systemPrompt { .model-version-info { display: flex; align-items: center; - gap: 0.5rem; - margin-top: 0.5rem; - font-size: 0.875rem; - color: var(--text-tertiary); - padding: 0.5rem 0.75rem; - background-color: rgba(0, 0, 0, 0.03); - border-radius: 0.5rem; - border: 1px dashed var(--border-color); + font-size: 0.75rem; + color: var(--text-secondary); + padding: 4px 0; + margin-top: 4px; + transition: all 0.2s ease; + gap: 4px; + opacity: 0.7; } .model-version-info i { - color: var(--info); - font-size: 1rem; + font-size: 0.7rem; + margin-right: 2px; + transition: color 0.2s ease; + color: #9C27B0; /* 统一使用紫色 */ +} + +.model-version-info.has-version { + opacity: 1; +} + +/* 悬停效果 */ +.model-version-info:hover { + color: var(--text); +} + +/* 暗色模式样式 */ +[data-theme="dark"] .model-version-info { + color: var(--text-tertiary); +} + +[data-theme="dark"] .model-version-info:hover { + color: var(--text-secondary); +} + +/* 移动端适配 */ +@media (max-width: 480px) { + .model-version-info { + font-size: 0.7rem; + padding: 3px 0; + margin-top: 3px; + } + + .model-version-info i { + font-size: 0.65rem; + } } /* API密钥设置区域的特殊样式 */ @@ -1702,7 +1720,7 @@ button:disabled { /* Select Styling */ .select-styled { appearance: none; - background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E"); + background-image: url("data:image/svg+xml;%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%236c757d' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.7rem top 50%; background-size: 0.65rem auto; @@ -1711,15 +1729,15 @@ button:disabled { } .select-styled:focus { - background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%234A6CF7%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E"); + background-image: url("data:image/svg+xml;%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%234A6CF7' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E"); } [data-theme="dark"] .select-styled { - background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23A0A0A0%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E"); + background-image: url("data:image/svg+xml;%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23A0A0A0' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E"); } [data-theme="dark"] .select-styled:focus { - background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%234A6CF7%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E"); + background-image: url("data:image/svg+xml;%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%234A6CF7' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E"); } /* 空状态提示样式 */ @@ -3152,8 +3170,6 @@ textarea:focus { .api-key-settings { --anim-order: 2; - position: relative; - z-index: 900; /* 确保低于模型选择下拉菜单 */ } .proxy-settings-section { @@ -3612,50 +3628,14 @@ textarea, /* 提示词描述样式 */ .prompt-description { - padding: 12px 15px; + padding: 10px 12px; background-color: rgba(var(--surface-rgb), 0.5); border: 1px solid var(--border-color); - border-radius: 8px; + border-radius: 6px; color: var(--text-secondary); font-size: 0.95rem; - line-height: 1.6; - margin: 5px 0 10px; - position: relative; - transition: all 0.3s ease; - max-height: 120px; - overflow-y: auto; - box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.02); -} - -.prompt-description:hover { - background-color: rgba(var(--surface-rgb), 0.7); - border-color: rgba(var(--primary-color-rgb), 0.3); - box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.03), 0 2px 8px rgba(var(--primary-color-rgb), 0.05); - transform: translateY(-1px); -} - -.prompt-description::before { - content: """; - position: absolute; - top: 0; - left: 5px; - font-size: 2rem; - line-height: 1; - color: rgba(var(--primary-color-rgb), 0.2); - pointer-events: none; - font-family: serif; -} - -.prompt-description::after { - content: """; - position: absolute; - bottom: -10px; - right: 5px; - font-size: 2rem; - line-height: 1; - color: rgba(var(--primary-color-rgb), 0.2); - pointer-events: none; - font-family: serif; + line-height: 1.5; + margin-bottom: 10px; } .prompt-description p { @@ -3665,127 +3645,10 @@ textarea, [data-theme="dark"] .prompt-description { background-color: rgba(255, 255, 255, 0.03); border-color: var(--border-color); - box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1); -} - -[data-theme="dark"] .prompt-description:hover { - background-color: rgba(255, 255, 255, 0.05); - border-color: rgba(var(--primary-color-rgb), 0.3); -} - -/* 系统提示词设置区域 */ -.prompt-settings { - position: relative; - border-left: 3px solid rgba(var(--primary-color-rgb), 0.5); - transition: all 0.3s ease; - padding: 16px; - margin-bottom: 15px; - background-color: rgba(var(--primary-color-rgb), 0.02); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); - border-radius: 0 8px 8px 0; -} - -.prompt-settings:hover { - border-left-color: var(--primary-color); - background-color: rgba(var(--primary-color-rgb), 0.04); - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); -} - -.prompt-settings h3 { - display: flex; - align-items: center; - gap: 8px; - color: var(--text-primary); - font-size: 1.1rem; -} - -.prompt-settings h3 i { - color: var(--primary-color); - font-size: 1.1em; -} - -.prompt-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; -} - -.prompt-header label { - display: flex; - align-items: center; - gap: 6px; - font-size: 0.95rem; -} - -.prompt-header label i { - color: var(--primary-color); - opacity: 0.9; -} - -.prompt-actions { - display: flex; - gap: 8px; - align-items: center; -} - -.prompt-actions select { - min-width: 180px; - padding: 6px 10px; - border-radius: 6px; - border: 1px solid var(--border-color); - background-color: var(--surface-alt); - color: var(--text-primary); - font-size: 0.9em; - transition: all 0.25s ease; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); - -webkit-appearance: none; - appearance: none; - background-image: url('data:image/svg+xml;utf8,'); - background-repeat: no-repeat; - background-position: right 10px center; - padding-right: 25px; -} - -.prompt-actions select:hover { - border-color: var(--primary-color); - box-shadow: 0 1px 5px rgba(var(--primary-color-rgb), 0.2); - transform: translateY(-1px); -} - -.prompt-actions select:focus { - outline: none; - border-color: var(--primary-color); - box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.15); -} - -[data-theme="dark"] .prompt-actions select { - background-image: url('data:image/svg+xml;utf8,'); -} - -[data-theme="dark"] .prompt-settings { - background-color: rgba(255, 255, 255, 0.01); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -} - -[data-theme="dark"] .prompt-settings:hover { - background-color: rgba(255, 255, 255, 0.03); - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); } /* 移动端提示词区域优化 */ @media (max-width: 768px) { - .prompt-settings { - padding: 15px; - margin-bottom: 12px; - border-radius: 0 6px 6px 0; - } - - .prompt-settings h3 { - font-size: 1rem; - margin-bottom: 12px; - } - .prompt-header { flex-direction: column; align-items: flex-start; @@ -3802,55 +3665,27 @@ textarea, min-width: 0; width: calc(100% - 110px); font-size: 0.85em; - padding: 5px 8px; - padding-right: 22px; - background-position: right 8px center; } .icon-btn { padding: 3px; } - + .prompt-description { - padding: 10px 12px; + padding: 8px 10px; font-size: 0.9rem; - max-height: 100px; - } - - .prompt-description::before, - .prompt-description::after { - font-size: 1.5rem; } } @media (max-width: 480px) { - .prompt-settings { - padding: 12px 10px; - border-left-width: 2px; - margin-bottom: 10px; - border-radius: 0 4px 4px 0; - } - - .prompt-settings h3 { - font-size: 0.95rem; - margin-bottom: 10px; - } - .prompt-actions { gap: 4px; } .prompt-actions select { width: calc(100% - 90px); - padding: 4px 6px; - padding-right: 20px; - background-position: right 6px center; - background-size: 10px 5px; - } - - .icon-btn { - font-size: 0.9em; - padding: 2px; + padding: 3px 6px; + font-size: 0.8em; } textarea#systemPrompt { @@ -3858,16 +3693,14 @@ textarea, font-size: 0.85rem; } - .prompt-description { - padding: 8px 10px; - font-size: 0.85rem; - max-height: 80px; - border-radius: 6px; + .icon-btn { + font-size: 0.9em; + padding: 2px; } - - .prompt-description::before, - .prompt-description::after { - font-size: 1.2rem; + + .prompt-description { + padding: 6px 8px; + font-size: 0.85rem; } } @@ -3879,75 +3712,1734 @@ textarea, padding: 4px; border-radius: 4px; transition: all 0.2s ease; - position: relative; - overflow: hidden; -} - -.icon-btn::before { - content: ""; - position: absolute; - top: 50%; - left: 50%; - width: 0; - height: 0; - background-color: rgba(var(--primary-color-rgb), 0.15); - border-radius: 50%; - transform: translate(-50%, -50%); - transition: width 0.3s, height 0.3s; - z-index: -1; -} - -.icon-btn:hover::before { - width: 200%; - height: 200%; } .icon-btn:hover { - color: var(--primary-color); - transform: translateY(-1px); + color: var(--text-color); + background-color: var(--hover-color); } .icon-btn:active { - transform: scale(0.95) translateY(0); + transform: scale(0.95); } -.icon-btn i { +/* 提示词对话框 */ +.prompt-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--surface); + border-radius: 8px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.5); + padding: 20px; + z-index: 1000; + width: 90%; + max-width: 500px; + display: none; + /* 确保背景完全不透明 */ + backdrop-filter: blur(5px); + border: 1px solid var(--border-color); +} + +/* 适配暗模式 */ +[data-theme="dark"] .prompt-dialog { + background-color: var(--surface); + border: 1px solid var(--border-color); + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.7); +} + +/* 移动端对话框优化 */ +@media (max-width: 480px) { + .prompt-dialog { + padding: 15px; + width: 95%; + } + + .prompt-dialog h3 { + font-size: 1.1rem; + padding-bottom: 8px; + margin-bottom: 12px; + } + + .prompt-dialog .form-group { + margin-bottom: 12px; + } + + .prompt-dialog label { + font-size: 0.9rem; + margin-bottom: 4px; + } + + .prompt-dialog input { + padding: 6px 8px; + font-size: 0.9rem; + } + + .prompt-dialog textarea { + min-height: 100px; + font-size: 0.9rem; + } + + .prompt-dialog .dialog-buttons { + margin-top: 15px; + } + + .prompt-dialog .dialog-buttons button { + padding: 6px 12px; + font-size: 0.9rem; + } +} + +.prompt-dialog.active { + display: block; +} + +.prompt-dialog h3 { + margin-top: 0; + border-bottom: 1px solid var(--border-color); + padding-bottom: 10px; + color: var(--text-primary); + font-weight: 600; +} + +[data-theme="dark"] .prompt-dialog h3 { + color: var(--text-primary); + border-bottom-color: var(--border-color); +} + +.prompt-dialog .form-group { + margin-bottom: 16px; +} + +.prompt-dialog label { + display: block; + margin-bottom: 6px; + font-weight: 500; +} + +.prompt-dialog input, +.prompt-dialog textarea { + width: 100%; + padding: 8px 10px; + border-radius: 4px; + border: 1px solid var(--border-color); + background-color: var(--surface-alt); + color: var(--text-primary); + font-size: 0.9rem; +} + +[data-theme="dark"] .prompt-dialog input, +[data-theme="dark"] .prompt-dialog textarea { + background-color: var(--input-background); + border: 1px solid var(--input-border); + color: var(--input-text); +} + +[data-theme="dark"] .prompt-dialog input:focus, +[data-theme="dark"] .prompt-dialog textarea:focus { + border-color: var(--input-focus-border); + box-shadow: 0 0 0 2px var(--input-focus-shadow); + outline: none; +} + +.prompt-dialog textarea { + min-height: 120px; + resize: vertical; +} + +.prompt-dialog .dialog-buttons { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; +} + +.prompt-dialog .dialog-buttons button { + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + border: none; + font-weight: 500; +} + +.prompt-dialog .dialog-buttons .cancel-btn { + background-color: var(--surface-alt); + color: var(--text-primary); + border: 1px solid var(--border-color); +} + +.prompt-dialog .dialog-buttons .save-btn { + background-color: var(--primary); + color: white; +} + +[data-theme="dark"] .prompt-dialog .dialog-buttons .cancel-btn { + background-color: var(--btn-secondary-bg); + color: var(--btn-secondary-text); + border: 1px solid var(--btn-secondary-border); +} + +[data-theme="dark"] .prompt-dialog .dialog-buttons .cancel-btn:hover { + background-color: var(--btn-secondary-hover-bg); +} + +[data-theme="dark"] .prompt-dialog .dialog-buttons .save-btn { + background-color: var(--primary); +} + +.dialog-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + z-index: 999; + display: none; + backdrop-filter: blur(2px); +} + +[data-theme="dark"] .dialog-overlay { + background-color: rgba(0, 0, 0, 0.8); +} + +.dialog-overlay.active { + display: block; +} + +/* 温度设置优化样式 */ +.temperature-control { + position: relative; + margin-top: 0.5rem; + margin-bottom: 1.2rem; +} + +.temperature-slider { + -webkit-appearance: none !important; + appearance: none !important; + width: 100%; + height: 10px; + border-radius: 10px; + background: linear-gradient(to right, + #3498db 0%, + #2ecc71 25%, + #f1c40f 50%, + #e67e22 75%, + #e74c3c 100%) !important; + outline: none; + box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); + margin: 0.8rem 0 1.2rem; + border: none; +} + +.temperature-slider::-webkit-slider-thumb { + -webkit-appearance: none !important; + appearance: none !important; + width: 22px; + height: 22px; + border-radius: 50%; + background: var(--surface); + border: 2px solid var(--primary); + cursor: pointer; + box-shadow: 0 2px 5px rgba(0,0,0,0.3); + transition: all 0.2s ease; + position: relative; + z-index: 2; +} + +.temperature-slider::-moz-range-track { + background: linear-gradient(to right, + #3498db 0%, + #2ecc71 25%, + #f1c40f 50%, + #e67e22 75%, + #e74c3c 100%) !important; + border-radius: 10px; + height: 10px; + border: none; +} + +.temperature-slider::-moz-range-thumb { + width: 22px; + height: 22px; + border-radius: 50%; + background: var(--surface); + border: 2px solid var(--primary); + cursor: pointer; + box-shadow: 0 2px 5px rgba(0,0,0,0.3); + transition: all 0.2s ease; + position: relative; + z-index: 2; +} + +.temperature-slider::-webkit-slider-thumb:hover, +.temperature-slider::-webkit-slider-thumb:active { + transform: scale(1.1); + box-shadow: 0 2px 5px rgba(0,0,0,0.3); +} + +.temperature-slider::-moz-range-thumb:hover, +.temperature-slider::-moz-range-thumb:active { + transform: scale(1.1); + box-shadow: 0 2px 5px rgba(0,0,0,0.3); +} + +.temperature-value { + display: none; /* 保留选择器但隐藏元素,防止JavaScript报错 */ +} + +[data-theme="dark"] .temperature-value { + display: none; +} + +@media (max-width: 480px) { + .temperature-slider { + height: 8px; + margin: 0.7rem 0 1rem; + } + + .temperature-slider::-webkit-slider-thumb { + width: 20px; + height: 20px; + } + + .temperature-slider::-moz-range-thumb { + width: 20px; + height: 20px; + } + + .temperature-value { + display: none; + } + + .temperature-markers { + font-size: 0.6rem; + margin-top: -0.6rem; + } + + .temperature-description { + font-size: 0.65rem; + margin-top: -0.3rem; + } +} + +.temperature-markers { + display: flex; + justify-content: space-between; + margin-top: -0.8rem; + margin-bottom: 0.2rem; + padding: 0 2px; + font-size: 0.65rem; + color: var(--text-tertiary); +} + +.temperature-description { + display: flex; + justify-content: space-between; + font-size: 0.7rem; + color: var(--text-tertiary); + margin-top: -0.5rem; +} + +.temperature-low { + text-align: left; +} + +.temperature-high { + text-align: right; +} + +.temperature-label { + display: flex; + align-items: center; + justify-content: flex-start; + margin-bottom: 0.2rem; +} + +.temperature-label i { + margin-right: 0.3rem; + color: var(--primary); +} + +/* 暗模式优化 */ +[data-theme="dark"] .temperature-slider { + background: linear-gradient(to right, + #3498db 0%, + #2ecc71 25%, + #f1c40f 50%, + #e67e22 75%, + #e74c3c 100%) !important; + box-shadow: inset 0 2px 4px rgba(0,0,0,0.4); +} + +[data-theme="dark"] .temperature-slider::-moz-range-track { + background: linear-gradient(to right, + #3498db 0%, + #2ecc71 25%, + #f1c40f 50%, + #e67e22 75%, + #e74c3c 100%) !important; + box-shadow: inset 0 2px 4px rgba(0,0,0,0.4); +} + +[data-theme="dark"] .temperature-slider::-webkit-slider-thumb { + background: var(--surface-alt); + border: 2px solid var(--primary); + box-shadow: 0 2px 5px rgba(0,0,0,0.5); +} + +[data-theme="dark"] .temperature-slider::-moz-range-thumb { + background: var(--surface-alt); + border: 2px solid var(--primary); + box-shadow: 0 2px 5px rgba(0,0,0,0.5); +} + +[data-theme="dark"] .temperature-value { + background-color: var(--surface); + box-shadow: 0 1px 3px rgba(0,0,0,0.3); +} + +/* Token控制区样式 */ +.token-control { + width: 100%; + display: flex; + flex-direction: column; + gap: 6px; + position: relative; +} + +.token-label { + display: flex; + align-items: center; + margin-bottom: 0.2rem; +} + +.token-slider-container { + display: flex; + align-items: center; + gap: 10px; + position: relative; + width: 100%; +} + +.token-slider { + flex: 1; + -webkit-appearance: none; + height: 6px; + border-radius: 4px; + background: linear-gradient(to right, + rgba(58, 134, 255, 0.8) 0%, + rgba(58, 134, 255, 0.8) 50%, + rgba(0, 0, 0, 0.1) 50%, + rgba(0, 0, 0, 0.1) 100%); + outline: none; + padding: 0; + margin: 0; + cursor: pointer; +} + +.token-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: white; + border: 2px solid #3a86ff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: all 0.2s ease; +} + +.token-slider::-moz-range-thumb { + width: 18px; + height: 18px; + border-radius: 50%; + background: white; + border: 2px solid #3a86ff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: all 0.2s ease; +} + +.token-slider::-webkit-slider-thumb:hover, +.token-slider::-webkit-slider-thumb:active { + transform: scale(1.2); + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3); +} + +.token-slider::-moz-range-thumb:hover, +.token-slider::-moz-range-thumb:active { + transform: scale(1.2); + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3); +} + +.token-value { + font-size: 0.85rem; + font-weight: 500; + opacity: 0.8; + background: rgba(0, 0, 0, 0.05); + padding: 2px 6px; + border-radius: 4px; + min-width: 50px; + text-align: center; + transition: all 0.2s ease; + flex-shrink: 0; +} + +[data-theme="dark"] .token-slider { + background: linear-gradient(to right, + rgba(72, 149, 239, 0.8) 0%, + rgba(72, 149, 239, 0.8) 50%, + rgba(255, 255, 255, 0.1) 50%, + rgba(255, 255, 255, 0.1) 100%); +} + +[data-theme="dark"] .token-slider::-webkit-slider-thumb { + background: #333; + border: 2px solid #4895ef; +} + +[data-theme="dark"] .token-slider::-moz-range-thumb { + background: #333; + border: 2px solid #4895ef; +} + +.token-markers { + display: flex; + justify-content: space-between; + margin-top: 2px; + font-size: 0.7rem; + color: rgba(0, 0, 0, 0.5); + padding: 0 2px; +} + +[data-theme="dark"] .token-markers { + color: rgba(255, 255, 255, 0.5); +} + +.token-presets { + display: flex; + justify-content: space-between; + gap: 8px; + margin-top: 8px; + flex-wrap: wrap; +} + +.token-preset { + flex: 1; + padding: 4px 8px; + font-size: 0.75rem; + border: none; + border-radius: 12px; + background: rgba(0, 0, 0, 0.05); + color: rgba(0, 0, 0, 0.7); + cursor: pointer; + transition: all 0.2s ease; + min-width: 0; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.token-preset:hover { + background: rgba(0, 0, 0, 0.1); + transform: translateY(-1px); +} + +.token-preset.active { + background: #3a86ff; + color: white; + font-weight: 500; +} + +[data-theme="dark"] .token-value, +[data-theme="dark"] .think-value-badge { + background: rgba(255, 255, 255, 0.1); +} + +[data-theme="dark"] .token-preset { + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.8); +} + +[data-theme="dark"] .token-preset:hover { + background: rgba(255, 255, 255, 0.15); +} + +[data-theme="dark"] .token-preset.active { + background: var(--primary-color); + color: #fff; + font-weight: 500; +} + +/* 思考预算控制组件样式 */ +.think-budget-control { + display: flex; + flex-direction: column; + gap: 6px; +} + +.think-budget-label { + display: flex; + align-items: center; + margin-bottom: 0.2rem; +} + +.think-slider-container { + display: flex; + align-items: center; + gap: 10px; + position: relative; + width: 100%; +} + +.think-slider { + flex: 1; + -webkit-appearance: none; + appearance: none; + height: 6px; + border-radius: 4px; + background: rgba(0, 0, 0, 0.1); + outline: none; + padding: 0; + margin: 0; + cursor: pointer; +} + +.think-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: white; + border: 2px solid var(--primary-color); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: all 0.2s ease; +} + +.think-slider::-moz-range-thumb { + width: 18px; + height: 18px; + border-radius: 50%; + background: white; + border: 2px solid var(--primary-color); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: all 0.2s ease; +} + +.think-slider::-webkit-slider-thumb:hover, +.think-slider::-webkit-slider-thumb:active { + transform: scale(1.2); + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3); +} + +.think-slider::-moz-range-thumb:hover, +.think-slider::-moz-range-thumb:active { + transform: scale(1.2); + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3); +} + +.think-value-badge { + font-size: 0.85rem; + font-weight: 500; + background: rgba(0, 0, 0, 0.05); + padding: 2px 8px; + border-radius: 12px; + min-width: 40px; + text-align: center; + color: var(--primary-color); + transition: all 0.2s ease; + flex-shrink: 0; +} + +.think-budget-markers { + display: flex; + justify-content: space-between; + margin-top: 2px; + font-size: 0.7rem; + color: rgba(0, 0, 0, 0.5); + padding: 0 2px; +} + +.think-budget-presets { + display: flex; + justify-content: space-around; + gap: 8px; + margin-top: 8px; +} + +.think-preset { + flex: 1; + padding: 4px 0; + font-size: 0.75rem; + border: none; + border-radius: 12px; + background: rgba(0, 0, 0, 0.05); + color: rgba(0, 0, 0, 0.7); + cursor: pointer; + transition: all 0.2s ease; +} + +.think-preset:hover { + background: rgba(0, 0, 0, 0.1); + transform: translateY(-1px); +} + +.think-preset.active { + background: var(--primary-color); + color: #333; + font-weight: 500; +} + +/* 移动端适配 */ +@media (max-width: 480px) { + .token-slider-container, + .think-slider-container { + gap: 8px; + } + + .token-value, + .think-value-badge { + font-size: 0.75rem; + min-width: 35px; + padding: 1px 4px; + } + + .token-presets { + gap: 4px; + } + + .token-preset { + padding: 3px 6px; + font-size: 0.7rem; + } + + .token-markers, + .think-budget-markers { + font-size: 0.65rem; + } + + .token-slider::-webkit-slider-thumb, + .think-slider::-webkit-slider-thumb { + width: 16px; + height: 16px; + } + + .token-slider::-moz-range-thumb, + .think-slider::-moz-range-thumb { + width: 16px; + height: 16px; + } +} + +/* 模型选择器增强 */ +.model-control { + width: 100%; + display: flex; + flex-direction: column; + gap: 10px; + position: relative; +} + +.model-selector-container { + position: relative; + width: 100%; +} + +.model-selector-display { + cursor: pointer; + background-color: var(--surface); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 10px 12px; + transition: all 0.2s ease; + user-select: none; +} + +.model-selector-display:hover { + border-color: var(--primary-color); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.selected-model { + display: flex; + align-items: center; + gap: 12px; +} + +.model-icon-container { + position: relative; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.2), rgba(var(--primary-rgb), 0.05)); + color: var(--primary-color); + border-radius: 8px; + font-size: 1.2rem; + flex-shrink: 0; +} + +.model-capabilities { + position: absolute; + bottom: -5px; + right: -5px; + display: flex; + gap: 2px; +} + +.model-capabilities i { + width: 16px; + height: 16px; + font-size: 0.6rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background-color: var(--surface); + border: 1px solid var(--border-color); + color: var(--text-tertiary); +} + +.model-capabilities i.model-multimodal { + background-color: rgba(52, 152, 219, 0.2); + color: #3498db; +} + +.model-capabilities i.model-reasoning { + background-color: rgba(155, 89, 182, 0.2); + color: #9b59b6; +} + +.model-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.model-name { + font-weight: 600; + font-size: 0.95rem; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.model-provider { + font-size: 0.75rem; + color: var(--text-tertiary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.selector-arrow { + color: var(--text-tertiary); + font-size: 0.9rem; + transition: transform 0.3s ease; +} + +.model-selector-display.active .selector-arrow { + transform: rotate(180deg); +} + +/* 强化模型选择下拉框样式 */ +.model-dropdown { + display: none !important; + position: absolute !important; + top: calc(100% + 5px) !important; + left: 0 !important; + width: 100% !important; + background: var(--surface) !important; /* 使用background代替background-color */ + border: 1px solid var(--border-color) !important; + border-radius: 8px !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2) !important; + z-index: 99999 !important; /* 提高z-index值 */ + max-height: 320px !important; + overflow-y: auto !important; + padding: 8px !important; + pointer-events: all !important; + opacity: 1 !important; + + /* 以下属性确保背景不透明 */ + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + background-clip: border-box !important; + transform: translateZ(0) !important; /* 启用硬件加速 */ + will-change: transform, opacity !important; /* 提升为合成层 */ + isolation: isolate !important; /* 创建新的层叠上下文 */ +} + +.model-dropdown.active { + display: block !important; + opacity: 1 !important; + visibility: visible !important; + pointer-events: all !important; + animation: dropdown-fade 0.2s ease !important; +} + +/* 修复模型选项的样式和交互 */ +.model-option { + pointer-events: all !important; + cursor: pointer !important; + position: relative !important; + z-index: 1 !important; + background-color: transparent !important; + transition: background-color 0.15s ease !important; + padding: 8px 12px !important; + border-radius: 6px !important; + overflow: hidden !important; + display: flex !important; + align-items: center !important; + gap: 12px !important; + margin-bottom: 2px !important; +} + +.model-option:hover { + background-color: rgba(var(--primary-rgb), 0.1) !important; + transform: translateX(2px) !important; +} + +.model-option:active { + transform: translateX(1px) scale(0.99) !important; + background-color: rgba(var(--primary-rgb), 0.15) !important; +} + +.model-option.selected { + background-color: rgba(var(--primary-rgb), 0.15) !important; + position: relative !important; +} + +.model-option.selected::after { + content: "\f00c" !important; + font-family: 'Font Awesome 5 Free' !important; + font-weight: 900 !important; + position: absolute !important; + right: 10px !important; + color: var(--primary-color) !important; + font-size: 0.8rem !important; +} + +/* 确保暗色模式下的配色正确 */ +[data-theme="dark"] .model-dropdown { + background: var(--surface-alt) !important; + border-color: var(--border-color-dark) !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5) !important; +} + +@keyframes dropdown-fade { + from { + opacity: 0; + transform: translateY(-5px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.model-option-group { + margin-bottom: 8px; +} + +.model-group-header { + padding: 6px 10px; + font-size: 0.75rem; + font-weight: 600; + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.03em; + border-bottom: 1px solid var(--border-color); + margin-bottom: 4px; +} + +.model-option { + padding: 8px 10px; + display: flex; + align-items: center; + gap: 12px; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.15s ease; +} + +.model-option:hover { + background-color: rgba(var(--primary-rgb), 0.05); +} + +.model-option.selected { + background-color: rgba(var(--primary-rgb), 0.1); + position: relative; +} + +.model-option.selected::after { + content: "\f00c"; + font-family: 'Font Awesome 5 Free'; + font-weight: 900; + position: absolute; + right: 10px; + color: var(--primary-color); + font-size: 0.8rem; +} + +.model-version-tag { + font-size: 0.7rem; + padding: 2px 6px; + background-color: rgba(var(--primary-rgb), 0.1); + color: var(--primary-color); + border-radius: 4px; + margin-left: 4px; +} + +/* 深色模式样式 */ +[data-theme="dark"] .model-selector-display { + background-color: var(--surface-alt); + border-color: var(--border-color-dark); +} + +[data-theme="dark"] .model-selector-display:hover { + border-color: var(--primary-color); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +[data-theme="dark"] .model-icon-container { + background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.3), rgba(var(--primary-rgb), 0.1)); +} + +[data-theme="dark"] .model-capabilities i { + background-color: var(--surface-alt); + border-color: var(--border-color-dark); +} + +[data-theme="dark"] .model-dropdown { + background-color: var(--surface-alt); + border-color: var(--border-color-dark); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +[data-theme="dark"] .model-option:hover { + background-color: rgba(var(--primary-rgb), 0.15); +} + +[data-theme="dark"] .model-option.selected { + background-color: rgba(var(--primary-rgb), 0.2); +} + +/* 移动端适配 */ +@media (max-width: 480px) { + .model-selector-display { + padding: 8px 10px; + } + + .model-icon-container { + width: 32px; + height: 32px; + font-size: 1rem; + } + + .model-capabilities i { + width: 14px; + height: 14px; + font-size: 0.55rem; + } + + .model-name { + font-size: 0.85rem; + } + + .model-provider { + font-size: 0.7rem; + } + + .model-dropdown { + max-height: 240px; + } + + .model-option { + padding: 6px 8px; + } + + .model-group-header { + padding: 4px 8px; + font-size: 0.7rem; + } +} + +/* 确保模型下拉框可见 - 强制覆盖 */ +.model-selector-container { + position: relative !important; + width: 100% !important; +} + +.model-dropdown.active { + display: block !important; + animation: dropdown-fade 0.2s ease !important; + opacity: 1 !important; + visibility: visible !important; +} + +.model-selector-display.active .selector-arrow { + transform: rotate(180deg) !important; +} + +/* 修复z-index问题 */ +#settingsPanel { + z-index: 100; +} + +.model-dropdown { + z-index: 1001 !important; +} + +/* 修复选择器指示箭头 */ +.model-selector-display .selector-arrow { + transition: transform 0.2s ease !important; +} + +.model-selector-display.active .selector-arrow { + transform: rotate(180deg) !important; +} + +/* 确保选择器有明显的交互感 */ +.model-selector-display { + cursor: pointer !important; + user-select: none !important; + transition: all 0.2s ease !important; +} + +.model-selector-display:hover { + border-color: var(--primary-color) !important; + box-shadow: 0 0 0 1px var(--primary-color) !important; +} + +/* 设置合适的z-index层级 */ +#settingsPanel { + z-index: 900; +} + +.model-dropdown { + z-index: 9999 !important; +} + +/* 模型选项互动效果增强 */ +.model-option { + transition: background-color 0.15s ease, transform 0.1s ease !important; +} + +.model-option:hover { + background-color: rgba(var(--primary-rgb), 0.1) !important; + transform: translateX(2px) !important; +} + +.model-option:active { + transform: scale(0.99) !important; +} + +.model-option.selected { + background-color: rgba(var(--primary-rgb), 0.2) !important; + position: relative !important; +} + +/* 补充模型下拉框分组样式 */ +.model-option-group { + pointer-events: all !important; + position: relative !important; + z-index: 2 !important; + background: inherit !important; + margin-bottom: 8px !important; +} + +.model-group-header { + padding: 6px 10px !important; + font-size: 0.75rem !important; + font-weight: 600 !important; + color: var(--text-tertiary) !important; + text-transform: uppercase !important; + letter-spacing: 0.03em !important; + border-bottom: 1px solid var(--border-color) !important; + margin-bottom: 4px !important; + pointer-events: none !important; + background: inherit !important; +} + +/* 添加强制不透明背景 */ +.model-dropdown { + background-color: #ffffff !important; + background: #ffffff !important; + box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.001) !important; /* 创建一个几乎不可见但能捕获点击的覆盖层 */ +} + +[data-theme="dark"] .model-dropdown { + background-color: #2a2a2a !important; + background: #2a2a2a !important; +} + +/* 确保选项组和头部也有不透明背景 */ +.model-option-group { + background-color: inherit !important; +} + +.model-group-header { + background-color: inherit !important; +} + +/* 下面是强制确保模态下拉框覆盖所有元素并正确显示 */ +.model-dropdown.active { + display: block !important; + opacity: 1 !important; + visibility: visible !important; + pointer-events: all !important; + z-index: 999999 !important; /* 超高的z-index确保在最上层 */ + position: absolute !important; + transform: translateZ(0) !important; /* 强制创建新的堆叠上下文 */ + backdrop-filter: blur(0) !important; /* 禁用任何背景滤镜效果 */ + -webkit-backdrop-filter: blur(0) !important; + background-blend-mode: normal !important; /* 禁用混合模式 */ +} + +/* 全新的模型选择器样式 */ +.model-selector { + position: relative; + width: 100%; + z-index: 10; +} + +.model-display { + display: flex; + align-items: center; + padding: 10px 12px; + border: 1px solid var(--border-color); + border-radius: 8px; + background-color: var(--input-bg); + cursor: pointer; + transition: all 0.2s ease; + gap: 10px; +} + +.model-display:hover { + border-color: var(--primary-color); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); +} + +.model-display-icon { + width: 36px; + height: 36px; + border-radius: 50%; + background-color: rgba(var(--primary-rgb), 0.1); + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; +} + +.model-display-info { + flex: 1; + display: flex; + flex-direction: column; +} + +.model-display-name { + font-weight: 500; + font-size: 0.95rem; + color: var(--text); +} + +.model-display-provider { + font-size: 0.8rem; + color: var(--text-secondary); +} + +.model-display-badges { + display: flex; + gap: 5px; +} + +.model-badge { + width: 20px; + height: 20px; + border-radius: 50%; + background-color: rgba(var(--primary-rgb), 0.1); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.7rem; + color: var(--primary-color); +} + +.model-selector-arrow { transition: transform 0.2s ease; } -.icon-btn:hover i { +.model-selector.open .model-selector-arrow { + transform: rotate(180deg); +} + +.model-dropdown-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: transparent; + z-index: 9999; + cursor: default; + display: none; +} + +.model-dropdown-panel { + position: absolute; + max-height: 300px; + overflow-y: auto; + background-color: var(--surface); + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); + z-index: 10000; + padding: 8px; + display: none; + opacity: 0; + transform: translateY(-10px); + transition: opacity 0.2s, transform 0.2s; +} + +.model-dropdown-panel.visible { + display: block; + opacity: 1; + transform: translateY(0); +} + +.model-group { + margin-bottom: 10px; +} + +.model-group-title { + padding: 5px 8px; + font-size: 0.75rem; + font-weight: 600; + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.03em; + border-bottom: 1px solid var(--border-color); + margin-bottom: 5px; +} + +.model-option { + display: flex; + align-items: center; + padding: 8px 10px; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.15s; + gap: 10px; +} + +.model-option:hover { + background-color: rgba(var(--primary-rgb), 0.1); +} + +.model-option.selected { + background-color: rgba(var(--primary-rgb), 0.15); +} + +.model-option-name { + flex: 1; + font-size: 0.9rem; +} + +.model-option-provider { + font-size: 0.75rem; + color: var(--text-secondary); +} + +.model-option-badges { + display: flex; + gap: 4px; +} + +.model-option-badge { + width: 16px; + height: 16px; + border-radius: 50%; + background-color: rgba(var(--primary-rgb), 0.1); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.6rem; + color: var(--primary-color); +} + +/* 暗色模式适配 */ +[data-theme="dark"] .model-display { + background-color: var(--surface-alt); + border-color: var(--border-color-dark); +} + +[data-theme="dark"] .model-display:hover { + border-color: var(--primary-color); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +[data-theme="dark"] .model-dropdown-panel { + background-color: var(--surface-alt); + border-color: var(--border-color-dark); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +/* 移动端适配 */ +@media (max-width: 480px) { + .model-display { + padding: 8px 10px; + } + + .model-display-icon { + width: 30px; + height: 30px; + font-size: 0.85rem; + } + + .model-display-name { + font-size: 0.85rem; + } + + .model-display-provider { + font-size: 0.7rem; + } + + .model-dropdown-panel { + max-height: 250px; + } + + .model-option { + padding: 6px 8px; + } + + .model-group-title { + font-size: 0.7rem; + padding: 4px 6px; + } +} + +/* 模型选择器容器 */ +.model-selector { + position: relative; + width: 100%; + z-index: 10; +} + +/* 模型显示区域 */ +.model-display { + display: flex; + align-items: center; + padding: 8px 10px; + border: 1px solid var(--border-color); + border-radius: 8px; + background-color: var(--input-bg); + cursor: pointer; + transition: all 0.2s ease; + gap: 8px; +} + +.model-display:hover { + border-color: var(--primary-color); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); +} + +/* 模型图标 */ +.model-display-icon { + width: 32px; + height: 32px; + border-radius: 50%; + background-color: rgba(var(--primary-rgb), 0.1); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.9rem; + flex-shrink: 0; + color: var(--primary-color); + transition: all 0.2s ease; +} + +/* 模型名称和提供商 */ +.model-display-info { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; /* 确保文本可以截断 */ +} + +.model-display-name { + font-weight: 500; + font-size: 0.9rem; + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.model-display-provider { + font-size: 0.75rem; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 模型能力徽章 */ +.model-display-badges { + display: flex; + gap: 4px; + margin-right: 2px; +} + +.model-badge { + width: 18px; + height: 18px; + border-radius: 50%; + background-color: rgba(var(--primary-rgb), 0.1); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.65rem; + color: var(--primary-color); + transition: transform 0.15s ease; +} + +.model-badge:hover { transform: scale(1.1); } -#savePromptBtn:hover i { - animation: pencil-move 0.5s ease; +/* 多模态能力特殊样式 */ +.model-badge i.fa-image { + color: #4CAF50; /* 绿色 */ } -#newPromptBtn:hover i { - animation: plus-rotate 0.5s ease; +/* 推理能力特殊样式 */ +.model-badge i.fa-brain { + color: #9C27B0; /* 紫色 */ } -#deletePromptBtn:hover i { - animation: trash-shake 0.5s ease; +/* 下拉箭头 */ +.model-selector-arrow { + transition: transform 0.2s ease; + font-size: 0.75rem; + color: var(--text-secondary); + width: 16px; + text-align: center; } -@keyframes pencil-move { - 0%, 100% { transform: translate(0, 0) rotate(0); } - 25% { transform: translate(-1px, -1px) rotate(-5deg); } - 50% { transform: translate(1px, -1px) rotate(5deg); } - 75% { transform: translate(0, 1px) rotate(0); } +.model-selector.open .model-selector-arrow { + transform: rotate(180deg); + color: var(--primary-color); } -@keyframes plus-rotate { - 0% { transform: rotate(0); } - 50% { transform: rotate(90deg) scale(1.1); } - 100% { transform: rotate(180deg) scale(1); } +/* 下拉面板覆盖层 */ +.model-dropdown-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: transparent; + z-index: 9999; + cursor: default; + display: none; } -@keyframes trash-shake { - 0%, 100% { transform: translate(0, 0) rotate(0); } - 20% { transform: translate(-2px, 0) rotate(-5deg); } - 40% { transform: translate(2px, 0) rotate(5deg); } - 60% { transform: translate(-2px, 0) rotate(-3deg); } - 80% { transform: translate(2px, 0) rotate(3deg); } +/* 下拉面板 */ +.model-dropdown-panel { + position: absolute; + max-height: 280px; + overflow-y: auto; + background-color: var(--surface); + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); + z-index: 10000; + padding: 6px; + display: none; + opacity: 0; + transform: translateY(-10px); + transition: opacity 0.2s, transform 0.2s; + scrollbar-width: thin; +} + +.model-dropdown-panel::-webkit-scrollbar { + width: 6px; +} + +.model-dropdown-panel::-webkit-scrollbar-thumb { + background-color: rgba(var(--primary-rgb), 0.2); + border-radius: 3px; +} + +.model-dropdown-panel::-webkit-scrollbar-thumb:hover { + background-color: rgba(var(--primary-rgb), 0.3); +} + +.model-dropdown-panel.visible { + display: block; + opacity: 1; + transform: translateY(0); +} + +/* 模型分组 */ +.model-group { + margin-bottom: 8px; +} + +.model-group:last-child { + margin-bottom: 0; +} + +.model-group-title { + padding: 4px 6px; + font-size: 0.7rem; + font-weight: 600; + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.03em; + border-bottom: 1px solid var(--border-color); + margin-bottom: 4px; +} + +/* 模型选项 */ +.model-option { + display: flex; + align-items: center; + padding: 6px 8px; + border-radius: 6px; + cursor: pointer; + transition: all 0.15s ease; + gap: 8px; +} + +.model-option:hover { + background-color: rgba(var(--primary-rgb), 0.08); + transform: translateX(2px); +} + +.model-option.selected { + background-color: rgba(var(--primary-rgb), 0.12); +} + +.model-option-name { + flex: 1; + font-size: 0.85rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.model-option-badges { + display: flex; + gap: 3px; +} + +.model-option-badge { + width: 16px; + height: 16px; + border-radius: 50%; + background-color: rgba(var(--primary-rgb), 0.08); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.6rem; +} + +/* 多模态能力特殊样式 */ +.model-option-badge i.fa-image { + color: #4CAF50; /* 绿色 */ +} + +/* 推理能力特殊样式 */ +.model-option-badge i.fa-brain { + color: #9C27B0; /* 紫色 */ +} + +/* 暗色模式适配 */ +[data-theme="dark"] .model-display { + background-color: var(--surface-alt); + border-color: var(--border-color-dark); +} + +[data-theme="dark"] .model-display:hover { + border-color: var(--primary-color); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +[data-theme="dark"] .model-display-icon { + background-color: rgba(var(--primary-rgb), 0.15); +} + +[data-theme="dark"] .model-badge { + background-color: rgba(var(--primary-rgb), 0.15); +} + +[data-theme="dark"] .model-dropdown-panel { + background-color: var(--surface-alt); + border-color: var(--border-color-dark); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +[data-theme="dark"] .model-option:hover { + background-color: rgba(var(--primary-rgb), 0.15); +} + +[data-theme="dark"] .model-option.selected { + background-color: rgba(var(--primary-rgb), 0.2); +} + +[data-theme="dark"] .model-option-badge { + background-color: rgba(var(--primary-rgb), 0.15); +} + +/* 移动端适配 */ +@media (max-width: 480px) { + .model-display { + padding: 6px 8px; + } + + .model-display-icon { + width: 28px; + height: 28px; + font-size: 0.8rem; + } + + .model-display-name { + font-size: 0.85rem; + } + + .model-display-provider { + font-size: 0.7rem; + } + + .model-badge { + width: 16px; + height: 16px; + font-size: 0.6rem; + } + + .model-dropdown-panel { + max-height: 240px; + padding: 5px; + } + + .model-option { + padding: 5px 7px; + } + + .model-group-title { + font-size: 0.65rem; + padding: 3px 5px; + } } diff --git a/templates/index.html b/templates/index.html index 16e7a18..5abf971 100644 --- a/templates/index.html +++ b/templates/index.html @@ -9,9 +9,10 @@ Snap Solver + + -