// ==UserScript== // @name Ollama Chat 助手 // @namespace http://tampermonkey.net/ // @version 0.1.0 // @description 一个基于 Ollama 的聊天助手,随时随地与您的本地大语言模型交流 // @author h7ml // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_getResourceText // @connect localhost // @connect * // @resource jquery https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js // @resource marked https://cdn.bootcdn.net/ajax/libs/marked/4.3.0/marked.min.js // @resource highlight https://cdn.bootcdn.net/ajax/libs/highlight.js/11.7.0/highlight.min.js // @resource highlightStyle https://cdn.bootcdn.net/ajax/libs/highlight.js/11.7.0/styles/github.min.css // @downloadURL none // ==/UserScript== (() => { // 检查jQuery是否已经存在 if (typeof jQuery === 'undefined') { try { const jqueryCode = GM_getResourceText('jquery'); eval(jqueryCode); initApp(); } catch (error) { console.error('加载jQuery失败:', error); const script = document.createElement('script'); script.textContent = GM_getResourceText('jquery'); document.head.appendChild(script); script.onload = initApp; } } else { initApp(); } function initApp() { 'use strict'; // 配置管理类 class ConfigManager { constructor() { this.DEFAULT_CONFIG = { url: 'http://localhost:11434/api/chat', model: 'llama2', useStream: false, // 默认不使用流式响应 params: { temperature: 0.7, top_p: 0.9, top_k: 40, num_ctx: 4096, repeat_penalty: 1.1 } }; this.DEFAULT_APP_SIZE = { width: 420, height: 620 }; } getConfig() { return GM_getValue('ollamaChatConfig', this.DEFAULT_CONFIG); } setConfig(config) { GM_setValue('ollamaChatConfig', config); } updateServerUrl(url) { const config = this.getConfig(); config.url = url; this.setConfig(config); return config; } updateUseStream(useStream) { const config = this.getConfig(); config.useStream = useStream; this.setConfig(config); return config; } getModelList() { return GM_getValue('ollamaModelList', []); } setModelList(list) { GM_setValue('ollamaModelList', list); } getAppPosition() { return GM_getValue('chatAppPosition', null); } setAppPosition(position) { GM_setValue('chatAppPosition', position); } getIconPosition() { return GM_getValue('chatIconPosition', null); } setIconPosition(position) { GM_setValue('chatIconPosition', position); } getAppSize() { return GM_getValue('chatAppSize', this.DEFAULT_APP_SIZE); } setAppSize(size) { GM_setValue('chatAppSize', size); } getAppMinimized() { return GM_getValue('chatAppMinimized', false); } setAppMinimized(minimized) { GM_setValue('chatAppMinimized', minimized); } getChatHistory() { return GM_getValue('chatHistory', []); } setChatHistory(history) { GM_setValue('chatHistory', history); } } // Ollama服务类 class OllamaService { constructor(configManager) { this.configManager = configManager; this.chatHistory = []; this.activeRequest = null; } async getModelList() { try { const config = this.configManager.getConfig(); // 从URL中提取基本URL,删除/api/chat部分 const baseUrl = config.url.replace(/\/api\/chat$/, ''); const listUrl = `${baseUrl}/api/tags`; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: listUrl, responseType: 'json', onload: function (response) { if (response.status >= 200 && response.status < 300) { const data = response.response; if (!data.models || !Array.isArray(data.models)) { console.error('获取到的模型数据格式不正确:', data); resolve([]); return; } // 提取模型名称并排序 const models = data.models.map(model => model.name).sort(); this.configManager.setModelList(models); resolve(models); } else { console.error('获取模型列表失败:', response.statusText); resolve([]); } }.bind(this), onerror: function (error) { console.error('获取模型列表异常:', error); resolve([]); } }); }); } catch (error) { console.error('获取模型列表异常:', error); return []; } } async sendChatMessage(message, messageCallback, completeCallback, errorCallback) { const config = this.configManager.getConfig(); const history = this.configManager.getChatHistory(); const messages = [...history]; messages.push({ role: "user", content: message, timestamp: Date.now() }); const requestData = { model: config.model, messages: messages, stream: config.useStream, options: config.params }; // 打印请求数据 console.log('请求数据:', JSON.stringify(requestData, null, 2)); // 如果存在以前的请求,尝试终止 if (this.activeRequest) { try { this.activeRequest.abort(); } catch (e) { console.error('终止上一次请求失败:', e); } this.activeRequest = null; } let streamResponse = { role: "assistant", content: "", timestamp: Date.now() }; let buffer = ''; this.activeRequest = GM_xmlhttpRequest({ method: 'POST', url: config.url, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(requestData), responseType: 'text', onloadstart: () => { console.log('请求开始 - 模式:', config.useStream ? '流式' : '非流式'); }, onprogress: (response) => { // 如果不是流式响应,不处理增量更新 if (!config.useStream) return; // 解码收到的数据并添加到缓冲区 const newText = response.responseText || ''; if (newText.length > buffer.length) { // 获取新增的文本 const newChunk = newText.substring(buffer.length); buffer = newText; // 处理新增的文本 const lines = newChunk.split('\n'); // 处理每一行 for (const line of lines) { if (!line.trim()) continue; try { const jsonData = JSON.parse(line); // 打印接收到的流式数据 console.log('收到流式数据片段:', JSON.stringify(jsonData)); // 处理不同的 Ollama API 响应格式 if (jsonData.message && typeof jsonData.message.content === 'string') { // 新版 Ollama API 使用 message.content 返回完整内容 streamResponse.content = jsonData.message.content; messageCallback(streamResponse.content); } else if (typeof jsonData.response === 'string') { // 旧版 Ollama API 使用 response 字段返回增量内容 streamResponse.content += jsonData.response; messageCallback(streamResponse.content); } else if (jsonData.content && typeof jsonData.content === 'string') { // 某些版本可能直接使用 content 字段 streamResponse.content += jsonData.content; messageCallback(streamResponse.content); } else if (jsonData.done === true || jsonData.done === false) { // 如果 API 返回了 done 标志但没有内容,忽略这个消息 continue; } else { // 尝试寻找任何可能包含文本的字段 const foundText = this.findContentInObject(jsonData); if (foundText) { streamResponse.content += foundText; messageCallback(streamResponse.content); } else { console.log('无法识别的响应格式:', jsonData); } } } catch (e) { // 可能是不完整的JSON,忽略解析错误 console.log('解析JSON出错:', e, '原始文本:', line); } } } }, onload: (response) => { // 打印完整响应 console.log('完整响应状态:', response.status, response.statusText); console.log('完整响应头:', response.responseHeaders); console.log('完整响应内容:', response.responseText); if (response.status >= 200 && response.status < 300) { // 非流式响应处理 if (!config.useStream) { try { const jsonResponse = JSON.parse(response.responseText); console.log('解析后的非流式响应:', JSON.stringify(jsonResponse, null, 2)); // 处理非流式响应的不同格式 let contentExtracted = false; // 标记是否已经提取内容 if (jsonResponse.message && typeof jsonResponse.message.content === 'string') { // 新版 Ollama API 使用 message.content console.log('检测到 message.content 字段:', jsonResponse.message.content); streamResponse.content = jsonResponse.message.content; contentExtracted = true; } else if (typeof jsonResponse.response === 'string') { // 旧版 Ollama API 使用 response 字段 console.log('检测到 response 字段:', jsonResponse.response); streamResponse.content = jsonResponse.response; contentExtracted = true; } else if (jsonResponse.content && typeof jsonResponse.content === 'string') { // 某些版本可能直接使用 content 字段 console.log('检测到 content 字段:', jsonResponse.content); streamResponse.content = jsonResponse.content; contentExtracted = true; } else { // 尝试寻找任何可能包含文本的字段 const foundText = this.findContentInObject(jsonResponse); if (foundText) { console.log('通过递归查找到文本内容:', foundText); streamResponse.content = foundText; contentExtracted = true; } else { console.error('无法识别的响应格式:', jsonResponse); streamResponse.content = '服务器返回了无法解析的数据。'; } } // 只调用一次消息回调 if (contentExtracted) { console.log('更新UI显示提取的内容:', streamResponse.content); messageCallback(streamResponse.content); } } catch (error) { console.error('解析响应失败:', error, '原始响应文本:', response.responseText); streamResponse.content = '解析响应失败: ' + error.message; messageCallback(streamResponse.content); } } // 添加到历史 history.push({ role: "user", content: message, timestamp: Date.now() }); history.push(streamResponse); console.log('聊天历史已更新,添加了用户消息和助手回复'); // 限制历史长度 const originalLength = history.length; while (JSON.stringify(history).length > 12000) { history.splice(0, 2); // 移除最旧的一轮对话 } if (originalLength !== history.length) { console.log(`历史记录过长,已移除 ${originalLength - history.length} 条记录`); } this.configManager.setChatHistory(history); completeCallback(streamResponse.content); } else { console.error('HTTP错误响应:', response.status, response.statusText, response.responseText); errorCallback(`HTTP 错误: ${response.status} ${response.statusText}`); } this.activeRequest = null; }, onerror: (error) => { console.error('发送消息错误:', error); errorCallback(error.message || '网络请求失败'); this.activeRequest = null; }, ontimeout: () => { console.error('请求超时'); errorCallback('请求超时'); this.activeRequest = null; }, onabort: () => { console.log('请求已取消'); this.activeRequest = null; } }); } // 递归查找对象中的文本内容 findContentInObject(obj) { if (!obj || typeof obj !== 'object') return null; // 直接检查常见的内容字段 const commonFields = ['content', 'text', 'message', 'response', 'answer', 'result']; for (const field of commonFields) { if (typeof obj[field] === 'string' && obj[field].trim()) { return obj[field]; } else if (obj[field] && typeof obj[field] === 'object') { // 如果字段是对象,递归检查 const nestedContent = this.findContentInObject(obj[field]); if (nestedContent) return nestedContent; } } // 检查所有其他字段 for (const key in obj) { if (typeof obj[key] === 'string' && obj[key].trim() && !['model', 'id', 'status', 'type', 'role'].includes(key)) { return obj[key]; } else if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) { const nestedContent = this.findContentInObject(obj[key]); if (nestedContent) return nestedContent; } } return null; } clearChatHistory() { this.configManager.setChatHistory([]); } } // UI管理类 class UIManager { constructor(configManager, ollamaService) { this.configManager = configManager; this.ollamaService = ollamaService; this.app = null; this.iconElement = null; this.elements = {}; this.isDragging = false; this.isIconDragging = false; this.isResizing = false; this.isMaximized = false; this.previousSize = {}; this.messageHandler = null; this.isGenerating = false; } async init() { await this.loadStyles(); this.createApp(); this.createIcon(); this.initializeElements(); this.bindEvents(); this.restoreState(); await this.fetchModels(); } async loadStyles() { const css = ` /* 基础样式 */ #ollama-chat-app { position: fixed; top: 20px; right: 20px; width: 400px; height: 600px; background: #ffffff; border-radius: 16px; box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18); display: flex; flex-direction: column; z-index: 999999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; resize: both; overflow: hidden; transition: box-shadow 0.3s ease; } #ollama-chat-app:hover { box-shadow: 0 8px 32px rgba(0, 0, 0, 0.24); } /* 调整大小把手 */ .resize-handle { position: absolute; width: 20px; height: 20px; transition: opacity 0.2s ease; opacity: 0.4; z-index: 10; } .resize-handle:hover { opacity: 1; } .resize-handle.right-bottom { bottom: 0; right: 0; cursor: nwse-resize; background: linear-gradient(135deg, transparent 50%, rgba(16, 163, 127, 0.6) 50%, rgba(16, 163, 127, 0.9) 100%); border-radius: 0 0 16px 0; } .resize-handle.left-bottom { bottom: 0; left: 0; cursor: nesw-resize; background: linear-gradient(225deg, transparent 50%, rgba(16, 163, 127, 0.6) 50%, rgba(16, 163, 127, 0.9) 100%); border-radius: 0 0 0 16px; } .resize-handle.left-top { top: 0; left: 0; cursor: nwse-resize; background: linear-gradient(315deg, transparent 50%, rgba(16, 163, 127, 0.6) 50%, rgba(16, 163, 127, 0.9) 100%); border-radius: 16px 0 0 0; } .resize-handle.right-top { top: 0; right: 0; cursor: nesw-resize; background: linear-gradient(45deg, transparent 50%, rgba(16, 163, 127, 0.6) 50%, rgba(16, 163, 127, 0.9) 100%); border-radius: 0 16px 0 0; } /* 消息样式 */ .message { margin: 12px; padding: 12px; border-radius: 12px; max-width: 85%; word-wrap: break-word; position: relative; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); transition: transform 0.2s ease, box-shadow 0.2s ease; } .message:hover { transform: translateY(-1px); box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15); } .message.user { background-color: #10a37f; color: white; margin-left: auto; } .message.assistant { background-color: #f5f5f7; color: #333; margin-right: auto; } .message-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 12px; } .role-name { font-weight: 500; } .message-time { opacity: 0.7; font-size: 11px; } .message.user .message-time { color: rgba(255, 255, 255, 0.8); } .message.assistant .message-time { color: rgba(0, 0, 0, 0.5); } .message-content { line-height: 1.5; } /* 聊天图标 */ #ollama-chat-icon { position: fixed; bottom: 20px; right: 20px; width: 56px; height: 56px; background: #10a37f; border-radius: 50%; display: none; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 12px rgba(16, 163, 127, 0.3); z-index: 999999; color: white; transition: transform 0.2s ease, box-shadow 0.2s ease; } #ollama-chat-icon:hover { transform: scale(1.05); box-shadow: 0 6px 16px rgba(16, 163, 127, 0.4); } #ollama-chat-icon:active { transform: scale(0.98); } /* 头部样式 */ #chat-header { padding: 16px 18px; border-bottom: 1px solid rgba(0, 0, 0, 0.08); display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; background: rgba(16, 163, 127, 0.05); } #chat-header h3 { margin: 0; font-size: 16px; font-weight: 600; color: #10a37f; display: flex; align-items: center; gap: 8px; } #header-actions { display: flex; gap: 10px; } .header-btn { background: none; border: none; padding: 8px; cursor: pointer; color: #8E8E93; border-radius: 8px; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; } .header-btn:hover { background: rgba(0, 0, 0, 0.05); color: #10a37f; } .header-btn:active { transform: scale(0.95); } /* 模型选择 */ #model-selector { padding: 12px 18px; border-bottom: 1px solid rgba(0, 0, 0, 0.08); display: flex; align-items: center; background: rgba(0, 0, 0, 0.02); } #model-select { flex: 1; padding: 10px 14px; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 10px; font-size: 14px; background-color: rgba(255, 255, 255, 0.9); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238E8E93' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; background-size: 16px; padding-right: 36px; -webkit-appearance: none; appearance: none; transition: all 0.2s ease; } #model-select:focus { outline: none; border-color: #10a37f; box-shadow: 0 0 0 3px rgba(16, 163, 127, 0.15); } /* 聊天内容区 */ #chat-messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 16px; scroll-behavior: smooth; background-color: #fafafa; } .message { display: flex; flex-direction: column; max-width: 85%; animation: fadeIn 0.3s ease; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .message-header { font-size: 12px; color: #8E8E93; margin-bottom: 4px; font-weight: 500; } .message-content { padding: 12px 16px; border-radius: 16px; position: relative; font-size: 14px; line-height: 1.5; } .user .message-content { background-color: #10a37f; color: white; border-top-right-radius: 4px; align-self: flex-end; } .assistant .message-content { background-color: #f1f1f1; color: #1D1D1F; border-top-left-radius: 4px; align-self: flex-start; } /* 代码块样式 */ .message-content pre { background: rgba(0, 0, 0, 0.1); padding: 14px; border-radius: 8px; overflow-x: auto; margin: 10px 0; border-left: 3px solid rgba(16, 163, 127, 0.5); } .assistant .message-content pre { background: rgba(0, 0, 0, 0.05); } .user .message-content pre { background: rgba(255, 255, 255, 0.1); border-left: 3px solid rgba(255, 255, 255, 0.3); } .message-content code { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 13px; } .message-content p { margin: 0 0 10px 0; } .message-content p:last-child { margin-bottom: 0; } /* 输入区域 */ #chat-input-container { padding: 16px 18px; border-top: 1px solid rgba(0, 0, 0, 0.08); background: rgba(255, 255, 255, 0.95); position: relative; display: flex; flex-direction: column; } .input-wrapper { display: flex; align-items: center; position: relative; background: rgba(255, 255, 255, 0.8); border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 16px; transition: all 0.2s ease; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); } .input-wrapper:focus-within { border-color: #10a37f; box-shadow: 0 0 0 3px rgba(16, 163, 127, 0.15); background-color: #fff; } #chat-input { flex: 1; min-height: 24px; max-height: 160px; padding: 14px 16px; border: none; border-radius: 16px; font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif; font-size: 14px; resize: none; overflow-y: auto; background-color: transparent; color: #1D1D1F; line-height: 1.5; } #chat-input:focus { outline: none; } #send-button { background: none; border: none; color: #10a37f; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; margin-right: 4px; border-radius: 50%; flex-shrink: 0; } #send-button:hover { background-color: rgba(16, 163, 127, 0.1); transform: scale(1.05); } #send-button:active { transform: scale(0.95); } #send-button:disabled { color: #C7C7CC; cursor: not-allowed; } /* 工具栏 */ #chat-toolbar { display: flex; justify-content: space-between; padding-top: 10px; font-size: 12px; } .toolbar-actions { display: flex; gap: 16px; color: #8E8E93; } .toolbar-btn { background: none; border: none; font-size: 12px; color: #8E8E93; cursor: pointer; padding: 0; display: flex; align-items: center; gap: 4px; transition: color 0.2s ease; } .toolbar-btn:hover { color: #10a37f; } #char-counter { color: #8E8E93; font-size: 12px; } .waiting-cursor { cursor: wait; } /* 打字机效果 */ @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } .typing-indicator::after { content: ''; width: 6px; height: 14px; display: inline-block; background-color: #1D1D1F; margin-left: 2px; animation: blink 1s infinite; vertical-align: text-bottom; } .user .typing-indicator::after { background-color: #fff; } /* 其他 */ .icon { width: 18px; height: 18px; } /* 加载动画 */ @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.2); } } .bounce-loader { display: flex; justify-content: center; gap: 4px; padding: 5px 0; } .bounce-loader > div { width: 6px; height: 6px; border-radius: 50%; background-color: rgba(0, 0, 0, 0.3); animation: pulse 1.5s infinite ease-in-out; } .bounce-loader > div:nth-child(2) { animation-delay: 0.2s; } .bounce-loader > div:nth-child(3) { animation-delay: 0.4s; } /* Markdown 样式 */ .markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4 { margin-top: 16px; margin-bottom: 10px; font-weight: 600; } .markdown-content h1 { font-size: 1.3rem; } .markdown-content h2 { font-size: 1.2rem; } .markdown-content h3 { font-size: 1.1rem; } .markdown-content ul, .markdown-content ol { padding-left: 20px; margin: 10px 0; } .markdown-content a { color: #10a37f; text-decoration: none; } .markdown-content a:hover { text-decoration: underline; } .markdown-content blockquote { border-left: 3px solid rgba(16, 163, 127, 0.5); padding-left: 12px; margin-left: 0; color: #555; font-style: italic; } /* 服务器配置样式 */ #server-config { padding: 12px 18px; border-bottom: 1px solid rgba(0, 0, 0, 0.08); background: rgba(16, 163, 127, 0.03); } .input-group { display: flex; align-items: center; gap: 10px; } #server-url { flex: 1; padding: 10px 14px; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 10px; font-size: 14px; transition: all 0.2s ease; } #server-url:focus { outline: none; border-color: #10a37f; box-shadow: 0 0 0 3px rgba(16, 163, 127, 0.15); } .save-btn { background: #10a37f; color: white; border: none; width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; } .save-btn:hover { background: #0c8e6e; transform: translateY(-1px); box-shadow: 0 4px 10px rgba(16, 163, 127, 0.3); } .save-btn:active { transform: translateY(0); } .form-tip { display: block; margin-top: 8px; font-size: 12px; color: #8E8E93; } .form-tip a { color: #10a37f; text-decoration: none; font-weight: 500; transition: color 0.2s ease; } .form-tip a:hover { color: #0c8e6e; text-decoration: underline; } /* 通知样式 */ #ollama-notification { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(100px); background: rgba(25, 25, 25, 0.9); color: white; padding: 12px 24px; border-radius: 12px; font-size: 14px; opacity: 0; transition: all 0.3s ease; z-index: 9999999; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } #ollama-notification.show { transform: translateX(-50%) translateY(0); opacity: 1; } /* 加载动画 */ .loading-spinner { display: inline-block; width: 18px; height: 18px; border: 2px solid rgba(255, 255, 255, 0.3); border-radius: 50%; border-top-color: white; animation: spin 1s ease-in-out infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* 配置面板样式 */ .config-panel { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #ffffff; z-index: 1000; display: none; flex-direction: column; border-radius: 16px; overflow: hidden; } .config-panel.show { display: flex; } .config-header { padding: 16px 18px; border-bottom: 1px solid rgba(0, 0, 0, 0.08); display: flex; justify-content: space-between; align-items: center; background: rgba(16, 163, 127, 0.05); } .config-header h3 { margin: 0; font-size: 16px; font-weight: 600; color: #10a37f; } .close-btn { background: none; border: none; padding: 8px; cursor: pointer; color: #8E8E93; border-radius: 8px; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; } .close-btn:hover { background: rgba(0, 0, 0, 0.05); color: #10a37f; } .config-content { flex: 1; overflow-y: auto; padding: 18px; background: #fafafa; } .config-group { margin-bottom: 24px; border-bottom: 1px solid rgba(0, 0, 0, 0.05); padding-bottom: 20px; } .config-group:last-child { border-bottom: none; margin-bottom: 0; } .config-group label { display: block; margin-bottom: 12px; font-size: 14px; color: #1D1D1F; font-weight: 600; } .config-group input[type="checkbox"] { margin-right: 10px; vertical-align: middle; width: 16px; height: 16px; accent-color: #10a37f; } .config-group input[type="range"] { width: 100%; margin: 8px 0; -webkit-appearance: none; height: 6px; background: #e0e0e0; border-radius: 3px; outline: none; } .config-group input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; background: #10a37f; border-radius: 50%; cursor: pointer; border: none; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); } .config-group input[type="range"]::-moz-range-thumb { width: 18px; height: 18px; background: #10a37f; border-radius: 50%; cursor: pointer; border: none; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); } .range-container { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; } .range-slider { flex: 1; margin-right: 14px; } .range-value { display: inline-block; min-width: 40px; text-align: center; font-size: 14px; color: #10a37f; font-weight: 500; background: rgba(16, 163, 127, 0.1); padding: 6px 10px; border-radius: 8px; } .config-footer { padding: 16px 18px; border-top: 1px solid rgba(0, 0, 0, 0.08); display: flex; justify-content: flex-end; background: rgba(255, 255, 255, 0.95); } .config-footer .save-btn { padding: 10px 18px; font-size: 14px; font-weight: 500; border-radius: 10px; width: auto; height: auto; transition: all 0.2s ease; } .config-footer .save-btn:hover { background: #0c8e6e; transform: translateY(-1px); box-shadow: 0 4px 10px rgba(16, 163, 127, 0.3); } `; GM_addStyle(css); } createApp() { this.app = document.createElement('div'); this.app.id = 'ollama-chat-app'; this.app.innerHTML = this.getAppHTML(); document.body.appendChild(this.app); // 添加调整大小把手 this.addResizeHandles(); } // 添加调整大小把手 addResizeHandles() { // 右下角把手 const rightBottomHandle = document.createElement('div'); rightBottomHandle.className = 'resize-handle right-bottom'; rightBottomHandle.addEventListener('mousedown', (e) => this.startResize(e, 'right-bottom')); this.app.appendChild(rightBottomHandle); // 左下角把手 const leftBottomHandle = document.createElement('div'); leftBottomHandle.className = 'resize-handle left-bottom'; leftBottomHandle.addEventListener('mousedown', (e) => this.startResize(e, 'left-bottom')); this.app.appendChild(leftBottomHandle); // 左上角把手 const leftTopHandle = document.createElement('div'); leftTopHandle.className = 'resize-handle left-top'; leftTopHandle.addEventListener('mousedown', (e) => this.startResize(e, 'left-top')); this.app.appendChild(leftTopHandle); // 右上角把手 const rightTopHandle = document.createElement('div'); rightTopHandle.className = 'resize-handle right-top'; rightTopHandle.addEventListener('mousedown', (e) => this.startResize(e, 'right-top')); this.app.appendChild(rightTopHandle); } createIcon() { this.iconElement = document.createElement('div'); this.iconElement.id = 'ollama-chat-icon'; this.iconElement.innerHTML = this.getIconHTML(); document.body.appendChild(this.iconElement); } initializeElements() { this.elements = { chatHeader: document.getElementById('chat-header'), chatMessages: document.getElementById('chat-messages'), chatInput: document.getElementById('chat-input'), sendButton: document.getElementById('send-button'), modelSelect: document.getElementById('model-select'), clearButton: document.getElementById('clear-chat'), charCounter: document.getElementById('char-counter'), toggleMinBtn: document.getElementById('toggle-min-btn'), toggleMaxBtn: document.getElementById('toggle-max-btn'), toggleConfigBtn: document.getElementById('toggle-config-btn'), configPanel: document.getElementById('config-panel'), closeConfigBtn: document.querySelector('.close-btn'), saveConfigBtn: document.getElementById('save-config'), useStreamCheckbox: document.getElementById('use-stream'), temperatureInput: document.getElementById('temperature'), topPInput: document.getElementById('top-p'), topKInput: document.getElementById('top-k'), numCtxInput: document.getElementById('num-ctx'), repeatPenaltyInput: document.getElementById('repeat-penalty'), serverUrl: document.getElementById('server-url') }; // 检查关键元素是否存在 const missingElements = []; for (const [key, element] of Object.entries(this.elements)) { if (!element) { missingElements.push(key); console.warn(`Element not found: ${key}`); } } if (missingElements.length > 0) { console.error('Missing elements:', missingElements.join(', ')); } } bindEvents() { try { console.log('正在绑定事件...'); // 拖拽事件 if (this.elements.chatHeader) { this.elements.chatHeader.addEventListener('mousedown', this.dragStart.bind(this)); console.log('已绑定头部拖拽事件'); } else { console.warn('未找到聊天头部元素'); } document.addEventListener('mousemove', this.drag.bind(this)); document.addEventListener('mouseup', this.dragEnd.bind(this)); // 聊天图标事件 if (this.iconElement) { this.iconElement.addEventListener('mousedown', this.iconDragStart.bind(this)); this.iconElement.addEventListener('click', this.toggleApp.bind(this)); console.log('已绑定图标事件'); } else { console.warn('未找到图标元素'); } // 窗口控制 if (this.elements.toggleMinBtn) { this.elements.toggleMinBtn.addEventListener('click', this.toggleMinimize.bind(this)); console.log('已绑定最小化按钮事件'); } else { console.warn('未找到最小化按钮'); } if (this.elements.toggleMaxBtn) { this.elements.toggleMaxBtn.addEventListener('click', this.toggleMaximize.bind(this)); console.log('已绑定最大化按钮事件'); } else { console.warn('未找到最大化按钮'); } if (this.elements.toggleConfigBtn) { this.elements.toggleConfigBtn.addEventListener('click', () => this.toggleConfigPanel()); console.log('已绑定配置按钮事件'); } else { console.warn('未找到配置按钮'); } // 发送消息 if (this.elements.sendButton && this.elements.chatInput) { this.elements.sendButton.addEventListener('click', this.handleSendMessage.bind(this)); this.elements.chatInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.handleSendMessage(); } }); this.elements.chatInput.addEventListener('input', this.updateCharCount.bind(this)); console.log('已绑定发送消息相关事件'); } else { console.warn('未找到发送按钮或输入框'); } // 输入框高度自适应 if (this.elements.chatInput) { this.elements.chatInput.addEventListener('input', function () { this.style.height = 'auto'; this.style.height = (this.scrollHeight < 160 ? Math.max(24, this.scrollHeight) : 160) + 'px'; }); console.log('已绑定输入框高度自适应事件'); } // 模型切换 if (this.elements.modelSelect) { this.elements.modelSelect.addEventListener('change', this.handleModelChange.bind(this)); console.log('已绑定模型选择事件'); } else { console.warn('未找到模型选择框'); } // 清空聊天 if (this.elements.clearButton) { this.elements.clearButton.addEventListener('click', this.clearChat.bind(this)); console.log('已绑定清空聊天事件'); } else { console.warn('未找到清空按钮'); } // 配置面板事件 if (this.elements.closeConfigBtn) { this.elements.closeConfigBtn.addEventListener('click', () => this.toggleConfigPanel()); console.log('已绑定关闭配置面板事件'); } else { console.warn('未找到关闭配置按钮'); } if (this.elements.saveConfigBtn) { this.elements.saveConfigBtn.addEventListener('click', () => this.saveConfig()); console.log('已绑定保存配置事件'); } else { console.warn('未找到保存配置按钮'); } // 配置值变化事件 const configInputs = { useStream: this.elements.useStreamCheckbox, temperature: this.elements.temperatureInput, topP: this.elements.topPInput, topK: this.elements.topKInput, numCtx: this.elements.numCtxInput, repeatPenalty: this.elements.repeatPenaltyInput }; for (const [name, element] of Object.entries(configInputs)) { if (element) { if (name === 'useStream') { element.addEventListener('change', () => this.updateConfig()); } else { element.addEventListener('input', (e) => this.updateRangeValue(e)); } console.log(`已绑定${name}配置项事件`); } else { console.warn(`未找到${name}配置项元素`); } } console.log('所有事件绑定完成'); } catch (error) { console.error('绑定事件时出错:', error); } } async fetchModels() { try { const models = await this.ollamaService.getModelList(); this.updateModelSelect(models); } catch (error) { console.error('获取模型列表失败:', error); } } updateModelSelect(models) { if (!models || models.length === 0) { // 如果没有获取到模型,使用默认选项 models = ['llama2', 'llama2:13b', 'mistral', 'mixtral']; } const selectEl = this.elements.modelSelect; selectEl.innerHTML = ''; const config = this.configManager.getConfig(); models.forEach(model => { const option = document.createElement('option'); option.value = model; option.textContent = model; selectEl.appendChild(option); }); // 设置当前选中的模型 if (models.includes(config.model)) { selectEl.value = config.model; } else if (models.length > 0) { selectEl.value = models[0]; this.handleModelChange(); } } handleModelChange() { const modelName = this.elements.modelSelect.value; const config = this.configManager.getConfig(); config.model = modelName; this.configManager.setConfig(config); } clearChat() { this.elements.chatMessages.innerHTML = ''; this.ollamaService.clearChatHistory(); } handleSendMessage() { if (this.isGenerating) return; const message = this.elements.chatInput.value.trim(); if (!message) return; this.isGenerating = true; this.elements.chatInput.value = ''; this.elements.chatInput.style.height = 'auto'; this.updateCharCount(); // 禁用发送按钮 this.elements.sendButton.disabled = true; document.body.classList.add('waiting-cursor'); // 添加用户消息 const userMessageId = this.addChatMessage(message, 'user'); // 添加机器人消息(先显示加载动画) const botMessageId = this.addChatMessage('', 'assistant', true); // 滚动到底部 this.scrollToBottom(); // 发送消息到Ollama this.ollamaService.sendChatMessage( message, // 消息流式更新回调 (content) => { // 更新机器人消息内容(打字机效果) this.updateChatMessage(botMessageId, content); this.scrollToBottom(); }, // 消息完成回调 (finalContent) => { // 完成时移除打字机效果 this.updateChatMessage(botMessageId, finalContent, false); this.isGenerating = false; this.elements.sendButton.disabled = false; document.body.classList.remove('waiting-cursor'); }, // 错误回调 (error) => { this.updateChatMessage(botMessageId, `出错了: ${error}`, false); this.isGenerating = false; this.elements.sendButton.disabled = false; document.body.classList.remove('waiting-cursor'); } ); } addChatMessage(content, role, isTyping = false) { const id = Date.now().toString(); const messageDiv = document.createElement('div'); messageDiv.id = `message-${id}`; messageDiv.className = `message ${role}`; const header = document.createElement('div'); header.className = 'message-header'; const time = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); header.innerHTML = ` ${role === 'user' ? '我' : 'Ollama'} ${time} `; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content markdown-content'; if (isTyping) { contentDiv.classList.add('typing-indicator'); } // 处理Markdown if (content) { contentDiv.innerHTML = this.markdownToHtml(content); } messageDiv.appendChild(header); messageDiv.appendChild(contentDiv); this.elements.chatMessages.appendChild(messageDiv); return id; } updateChatMessage(id, content, isTyping = true) { const contentDiv = document.querySelector(`#message-${id} .message-content`); if (!contentDiv) return; // 设置内容 contentDiv.innerHTML = this.markdownToHtml(content); // 更新打字机效果 if (isTyping) { contentDiv.classList.add('typing-indicator'); } else { contentDiv.classList.remove('typing-indicator'); } } markdownToHtml(text) { // 简单的Markdown转HTML return text // 代码块 .replace(/```(\w*)([\s\S]*?)```/g, '
$2
') // 行内代码 .replace(/`([^`]+)`/g, '$1') // 粗体 .replace(/\*\*([^*]+)\*\*/g, '$1') // 斜体 .replace(/\*([^*]+)\*/g, '$1') // 链接 .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') // 无序列表 .replace(/^\s*[-*+]\s+(.*)/gm, '
  • $1
  • ') // 段落 .replace(/^(?!<)(.+)$/gm, '

    $1

    '); } scrollToBottom() { this.elements.chatMessages.scrollTop = this.elements.chatMessages.scrollHeight; } updateCharCount() { const count = this.elements.chatInput.value.length; this.elements.charCounter.textContent = count > 0 ? `${count}` : ''; } // 拖拽相关方法 dragStart(e) { if (e.target.closest('button') || e.target.closest('select')) return; this.isDragging = true; this.initialX = e.clientX - this.app.offsetLeft; this.initialY = e.clientY - this.app.offsetTop; } drag(e) { if (this.isDragging) { e.preventDefault(); const currentX = Math.max(0, Math.min( e.clientX - this.initialX, window.innerWidth - this.app.offsetWidth )); const currentY = Math.max(0, Math.min( e.clientY - this.initialY, window.innerHeight - this.app.offsetHeight )); this.app.style.left = currentX + 'px'; this.app.style.top = currentY + 'px'; } } dragEnd() { if (this.isDragging) { this.isDragging = false; const position = { x: parseInt(this.app.style.left), y: parseInt(this.app.style.top) }; this.configManager.setAppPosition(position); } } // 图标拖拽相关方法 iconDragStart(e) { if (e.target.closest('button')) return; this.isIconDragging = true; this.iconInitialX = e.clientX - this.iconElement.offsetLeft; this.iconInitialY = e.clientY - this.iconElement.offsetTop; this.iconElement.style.cursor = 'grabbing'; } iconDrag(e) { if (this.isIconDragging) { e.preventDefault(); const currentX = Math.max(0, Math.min( e.clientX - this.iconInitialX, window.innerWidth - this.iconElement.offsetWidth )); const currentY = Math.max(0, Math.min( e.clientY - this.iconInitialY, window.innerHeight - this.iconElement.offsetHeight )); this.iconElement.style.left = currentX + 'px'; this.iconElement.style.top = currentY + 'px'; this.iconElement.style.right = 'auto'; } } iconDragEnd() { if (this.isIconDragging) { this.isIconDragging = false; this.iconElement.style.cursor = 'pointer'; const position = { x: parseInt(this.iconElement.style.left), y: parseInt(this.iconElement.style.top) }; this.configManager.setIconPosition(position); } } // 调整大小 startResize(e, direction) { e.preventDefault(); e.stopPropagation(); this.isResizing = true; this.resizeDirection = direction; this.initialWidth = this.app.offsetWidth; this.initialHeight = this.app.offsetHeight; this.initialX = e.clientX; this.initialY = e.clientY; this.initialLeft = this.app.offsetLeft; document.addEventListener('mousemove', this.resize.bind(this)); document.addEventListener('mouseup', this.stopResize.bind(this)); } resize(e) { if (!this.isResizing) return; const minWidth = 320; const minHeight = 400; if (this.resizeDirection === 'right-bottom') { // 右下角调整 - 只改变宽高 const newWidth = Math.max(minWidth, this.initialWidth + (e.clientX - this.initialX)); const newHeight = Math.max(minHeight, this.initialHeight + (e.clientY - this.initialY)); this.app.style.width = newWidth + 'px'; this.app.style.height = newHeight + 'px'; } else if (this.resizeDirection === 'left-bottom') { // 左下角调整 - 改变宽高和左侧位置 const widthDelta = this.initialX - e.clientX; const newWidth = Math.max(minWidth, this.initialWidth + widthDelta); const newHeight = Math.max(minHeight, this.initialHeight + (e.clientY - this.initialY)); // 只有当宽度有效时才调整左侧位置 if (newWidth >= minWidth) { this.app.style.left = (this.initialLeft - widthDelta) + 'px'; } this.app.style.width = newWidth + 'px'; this.app.style.height = newHeight + 'px'; } else if (this.resizeDirection === 'left-top') { // 左上角调整 - 改变宽高和左侧、顶部位置 const widthDelta = this.initialX - e.clientX; const heightDelta = this.initialY - e.clientY; const newWidth = Math.max(minWidth, this.initialWidth + widthDelta); const newHeight = Math.max(minHeight, this.initialHeight + heightDelta); // 只有当尺寸有效时才调整位置 if (newWidth >= minWidth) { this.app.style.left = (this.initialLeft - widthDelta) + 'px'; } if (newHeight >= minHeight) { this.app.style.top = (this.app.offsetTop - heightDelta) + 'px'; } this.app.style.width = newWidth + 'px'; this.app.style.height = newHeight + 'px'; } else if (this.resizeDirection === 'right-top') { // 右上角调整 - 改变宽高和顶部位置 const heightDelta = this.initialY - e.clientY; const newWidth = Math.max(minWidth, this.initialWidth + (e.clientX - this.initialX)); const newHeight = Math.max(minHeight, this.initialHeight + heightDelta); // 只有当高度有效时才调整顶部位置 if (newHeight >= minHeight) { this.app.style.top = (this.app.offsetTop - heightDelta) + 'px'; } this.app.style.width = newWidth + 'px'; this.app.style.height = newHeight + 'px'; } this.configManager.setAppSize({ width: this.app.offsetWidth, height: this.app.offsetHeight }); this.configManager.setAppPosition({ x: this.app.offsetLeft, y: this.app.offsetTop }); } stopResize() { this.isResizing = false; document.removeEventListener('mousemove', this.resize.bind(this)); document.removeEventListener('mouseup', this.stopResize.bind(this)); } // 窗口控制 toggleMaximize() { if (!this.isMaximized) { this.previousSize = { width: this.app.style.width, height: this.app.style.height, left: this.app.style.left, top: this.app.style.top }; this.app.style.width = '100%'; this.app.style.height = '100vh'; this.app.style.left = '0'; this.app.style.top = '0'; this.elements.toggleMaxBtn.innerHTML = ` `; } else { Object.assign(this.app.style, this.previousSize); this.elements.toggleMaxBtn.innerHTML = ` `; } this.isMaximized = !this.isMaximized; } toggleMinimize() { this.app.style.display = 'none'; this.iconElement.style.display = 'flex'; this.configManager.setAppMinimized(true); // 确保图标可见 this.iconElement.style.zIndex = '999999'; } toggleApp() { // 确保应用可见 this.app.style.display = 'flex'; this.app.style.zIndex = '999999'; this.iconElement.style.display = 'none'; this.configManager.setAppMinimized(false); // 恢复焦点 this.elements.chatInput.focus(); } restoreState() { // 恢复位置 const appPosition = this.configManager.getAppPosition(); if (appPosition) { this.app.style.left = appPosition.x + 'px'; this.app.style.top = appPosition.y + 'px'; this.app.style.right = 'auto'; this.app.style.bottom = 'auto'; } // 恢复尺寸 const appSize = this.configManager.getAppSize(); if (appSize) { this.app.style.width = appSize.width + 'px'; this.app.style.height = appSize.height + 'px'; } // 恢复最小化状态 const appMinimized = this.configManager.getAppMinimized(); if (appMinimized) { this.app.style.display = 'none'; this.iconElement.style.display = 'flex'; } else { this.app.style.display = 'flex'; this.iconElement.style.display = 'none'; } const iconPosition = this.configManager.getIconPosition(); if (iconPosition) { this.iconElement.style.left = iconPosition.x + 'px'; this.iconElement.style.top = iconPosition.y + 'px'; this.iconElement.style.right = 'auto'; this.iconElement.style.bottom = 'auto'; } // 加载服务器配置 this.loadServerConfig(); // 加载聊天历史 this.loadChatHistory(); } loadServerConfig() { // 加载服务器地址 const config = this.configManager.getConfig(); const serverUrlInput = document.getElementById('server-url'); if (serverUrlInput) { // 设置默认值 serverUrlInput.value = config.url || 'http://localhost:11434/api/chat'; serverUrlInput.placeholder = '例如:http://localhost:11434/api/chat'; } // 绑定保存URL按钮事件 const saveUrlBtn = document.getElementById('save-url'); if (saveUrlBtn) { saveUrlBtn.addEventListener('click', this.saveServerUrl.bind(this)); } } loadChatHistory() { const history = this.configManager.getChatHistory(); let lastUserMessageIndex = -1; // 查找历史中的最后一个用户消息 for (let i = history.length - 1; i >= 0; i--) { if (history[i].role === 'user') { lastUserMessageIndex = i; break; } } // 显示历史消息 for (let i = 0; i < history.length; i++) { // 只显示最后一轮对话 if (lastUserMessageIndex > -1 && i < lastUserMessageIndex - 1) continue; const msg = history[i]; const messageId = this.addChatMessage(msg.content, msg.role); // 如果有时间信息,更新消息时间 if (msg.timestamp) { const time = new Date(msg.timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); const timeElement = document.querySelector(`#message-${messageId} .message-time`); if (timeElement) { timeElement.textContent = time; } } } // 滚动到底部 this.scrollToBottom(); } getAppHTML() { return `

    Ollama 聊天助手

    没有Ollama服务?点击这里获取免费Ollama服务

    模型配置

    0.7
    0.9
    40
    4096
    1.1
    `; } getIconHTML() { return ` `; } saveServerUrl() { const urlInput = this.elements.serverUrl; if (!urlInput) return; let url = urlInput.value.trim(); if (!url) return; // 确保URL格式正确 if (!url.startsWith('http://') && !url.startsWith('https://')) { url = 'http://' + url; } // 确保URL以/api/chat结尾 if (!url.endsWith('/api/chat')) { url = url.replace(/\/*$/, '') + '/api/chat'; } // 显示加载状态 this.elements.saveUrlBtn.disabled = true; this.elements.serverUrl.disabled = true; // 保存动画 const originalContent = this.elements.saveUrlBtn.innerHTML; this.elements.saveUrlBtn.innerHTML = ''; // 更新URL this.configManager.updateServerUrl(url); urlInput.value = url; // 延迟一下,模拟服务器验证 setTimeout(() => { // 恢复按钮状态 this.elements.saveUrlBtn.disabled = false; this.elements.serverUrl.disabled = false; this.elements.saveUrlBtn.innerHTML = originalContent; // 提示保存成功 this.showNotification('服务器地址已保存'); // 重新获取模型列表 this.fetchModels(); }, 500); } showNotification(message, duration = 2000) { let notification = document.getElementById('ollama-notification'); if (!notification) { notification = document.createElement('div'); notification.id = 'ollama-notification'; document.body.appendChild(notification); } notification.textContent = message; notification.className = 'show'; setTimeout(() => { notification.className = ''; }, duration); } toggleConfigPanel() { try { console.log('正在切换配置面板...'); const configPanel = document.getElementById('config-panel'); if (!configPanel) { console.error('配置面板元素未找到'); return; } const isVisible = configPanel.classList.contains('show'); console.log('当前配置面板状态:', isVisible ? '显示' : '隐藏'); if (isVisible) { configPanel.classList.remove('show'); console.log('配置面板已隐藏'); } else { console.log('正在加载配置值...'); this.loadConfigValues(); configPanel.classList.add('show'); console.log('配置面板已显示'); } } catch (error) { console.error('切换配置面板时出错:', error); } } loadConfigValues() { const config = this.configManager.getConfig(); // 添加空值检查 if (!this.elements.useStreamCheckbox || !this.elements.temperatureInput || !this.elements.topPInput || !this.elements.topKInput || !this.elements.numCtxInput || !this.elements.repeatPenaltyInput) { console.error('配置面板元素未找到,无法加载配置值'); return; } // 设置流式响应选项 this.elements.useStreamCheckbox.checked = config.useStream; // 设置模型参数 if (this.elements.temperatureInput && this.elements.temperatureInput.nextElementSibling) { this.elements.temperatureInput.value = config.params.temperature; this.elements.temperatureInput.nextElementSibling.textContent = config.params.temperature; } if (this.elements.topPInput && this.elements.topPInput.nextElementSibling) { this.elements.topPInput.value = config.params.top_p; this.elements.topPInput.nextElementSibling.textContent = config.params.top_p; } if (this.elements.topKInput && this.elements.topKInput.nextElementSibling) { this.elements.topKInput.value = config.params.top_k; this.elements.topKInput.nextElementSibling.textContent = config.params.top_k; } if (this.elements.numCtxInput && this.elements.numCtxInput.nextElementSibling) { this.elements.numCtxInput.value = config.params.num_ctx; this.elements.numCtxInput.nextElementSibling.textContent = config.params.num_ctx; } if (this.elements.repeatPenaltyInput && this.elements.repeatPenaltyInput.nextElementSibling) { this.elements.repeatPenaltyInput.value = config.params.repeat_penalty; this.elements.repeatPenaltyInput.nextElementSibling.textContent = config.params.repeat_penalty; } } updateRangeValue(e) { try { const input = e.target; const container = input.closest('.range-container'); if (!container) { console.warn('未找到 range-container 元素'); return; } const valueDisplay = container.querySelector('.range-value'); if (!valueDisplay) { console.warn('未找到 range-value 元素'); return; } valueDisplay.textContent = input.value; console.log(`更新范围值: ${input.id} = ${input.value}`); } catch (error) { console.error('更新范围值时出错:', error); } } updateConfig() { const config = this.configManager.getConfig(); // 更新流式响应选项 config.useStream = this.elements.useStreamCheckbox.checked; // 更新模型参数 config.params.temperature = parseFloat(this.elements.temperatureInput.value); config.params.top_p = parseFloat(this.elements.topPInput.value); config.params.top_k = parseInt(this.elements.topKInput.value); config.params.num_ctx = parseInt(this.elements.numCtxInput.value); config.params.repeat_penalty = parseFloat(this.elements.repeatPenaltyInput.value); this.configManager.setConfig(config); } saveConfig() { this.updateConfig(); this.toggleConfigPanel(); this.showNotification('配置已保存'); } } // 初始化应用 const configManager = new ConfigManager(); const ollamaService = new OllamaService(configManager); const uiManager = new UIManager(configManager, ollamaService); uiManager.init(); } })();