class ChatUI { constructor() { this.messageContainer = document.getElementById("chat-messages"); this.input = document.getElementById("chat-input"); this.sendButton = document.getElementById("send-button"); this.promptButton = document.getElementById("prompt-button"); this.promptPanel = document.getElementById("prompt-panel"); // 确保AI服务已经初始化 if (!window.aiService) { throw new Error("AI Service not initialized"); } this.aiService = window.aiService; this.typingSpeed = 50; // 打字速度(毫秒/字符) this.setupEventListeners(); this.adjustInputHeight(); this.init(); // 添加新消息指示器 this.createNewMessageIndicator(); // 监听滚动事件 this.messageContainer.addEventListener( "scroll", this.handleScroll.bind(this) ); // 创建打断按钮 this.stopButton = document.createElement("button"); this.stopButton.className = "stop-button"; this.stopButton.innerHTML = ` 停止生成 `; // 将打断按钮添加到输入按钮区域 document.querySelector(".input-buttons").appendChild(this.stopButton); // 添加打断按钮事件监听 this.stopButton.addEventListener("click", () => this.handleStop()); this.isTyping = false; // 添加打字状态标记 this.loadingDiv = null; // 添加加载动画的引用 this.inputWrapper = document.querySelector(".input-wrapper"); } async init() { try { await this.aiService.init(); await this.checkApiKey(); } catch (error) { console.error("ChatUI initialization failed:", error); this.addMessage("初始化失败,请刷新页面重试。", "assistant"); } } setupEventListeners() { // 发送消息 this.sendButton.addEventListener("click", () => this.handleSend()); this.input.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); this.handleSend(); } }); // 自动调整输入框高度 this.input.addEventListener("input", () => this.adjustInputHeight()); // 快捷指令面板 this.promptButton.addEventListener("click", () => this.togglePromptPanel()); document.querySelector(".close-prompt").addEventListener("click", () => { this.promptPanel.classList.remove("show"); }); // 点击快捷指令 document.querySelectorAll(".prompt-item").forEach((item) => { item.addEventListener("click", () => { this.input.value = item.textContent; this.promptPanel.classList.remove("show"); this.adjustInputHeight(); this.input.focus(); }); }); // 点击外部关闭快捷指令面板 document.addEventListener("click", (e) => { if ( !this.promptPanel.contains(e.target) && !this.promptButton.contains(e.target) ) { this.promptPanel.classList.remove("show"); } }); } async handleSend() { const message = this.input.value.trim(); if (!message) return; // 处理命令 if (message.startsWith("/setapi ")) { const apiKey = message.substring(8).trim(); await this.aiService.setApiKey(apiKey); // 根据是否使用默认密钥显示不同消息 if (apiKey === CONFIG.AI_API.DEFAULT_API_KEY) { this.addMessage("已恢复使用默认API密钥", "assistant", false); } else { this.addMessage("自定义API密钥已设置", "assistant", false); } this.input.value = ""; return; } // 添加用户消息 this.addMessage(message, "user", false); // 更新上下文 this.aiService.updateContext(message, "user"); // 清空输入框 this.input.value = ""; this.adjustInputHeight(); try { this.stopButton.classList.add("show"); this.setInputState(true); // 禁用输入框 this.loadingDiv = this.createLoadingMessage(); const response = await this.aiService.sendMessage(message); if (this.loadingDiv) { this.loadingDiv.remove(); this.loadingDiv = null; } if (!response) return; this.isTyping = false; await this.addMessage(response, "assistant", true); this.stopButton.classList.remove("show"); this.setInputState(false); // 恢复输入框 this.aiService.updateContext(response, "assistant"); } catch (error) { if (this.loadingDiv) { this.loadingDiv.remove(); this.loadingDiv = null; } this.stopButton.classList.remove("show"); this.setInputState(false); // 恢复输入框 this.isTyping = false; if (error.message === "REQUEST_ABORTED") { return; } this.addMessage("抱歉,发生了一些错误,请稍后重试。", "assistant", false); console.error("AI response error:", error); } } /** * 添加消息到聊天界面 * @param {string} content 消息内容 * @param {string} type 消息类型(user/assistant) * @param {boolean} typing 是否使用打字效果 * @param {boolean} isInterrupted 是否是中断消息 */ async addMessage( content, type, typing = type === "assistant", isInterrupted = false ) { const messageDiv = document.createElement("div"); messageDiv.className = `message ${type}`; const messageContent = document.createElement("div"); messageContent.className = "message-content"; if (isInterrupted) { messageContent.classList.add("interrupted"); } const paragraph = document.createElement("p"); messageContent.appendChild(paragraph); // 创建操作栏(时间戳和复制按钮) const actionsDiv = document.createElement("div"); actionsDiv.className = "message-actions"; // 只为非中断的AI消息添加复制按钮 if (type === "assistant" && !isInterrupted) { const copyButton = this.createCopyButton(content); actionsDiv.appendChild(copyButton); } // 添加时间戳 const timestamp = document.createElement("div"); timestamp.className = "message-timestamp"; timestamp.textContent = this.formatTime(new Date()); actionsDiv.appendChild(timestamp); messageContent.appendChild(actionsDiv); messageDiv.appendChild(messageContent); this.messageContainer.appendChild(messageDiv); if (typing) { messageContent.classList.add("typing"); await this.typeMessage(paragraph, content); messageContent.classList.remove("typing"); } else { paragraph.innerHTML = this.formatMessage(content); } const { scrollTop, scrollHeight, clientHeight } = this.messageContainer; const wasAtBottom = scrollHeight - scrollTop - clientHeight < 100; if (wasAtBottom) { this.scrollToBottom(); } else { this.newMessageIndicator.classList.add("show"); } } /** * 实现打字机效果 * @param {HTMLElement} element 要添加文字的元素 * @param {string} text 要显示的文字 */ async typeMessage(element, text) { let index = 0; const rawText = text; const tempDiv = document.createElement("div"); this.isTyping = true; // 开始打字 return new Promise((resolve) => { const type = () => { // 检查是否被中断 if (!this.isTyping) { resolve(); return; } if (index < rawText.length) { const currentText = rawText.substring(0, index + 1); tempDiv.innerHTML = this.formatMessage(currentText); element.innerHTML = tempDiv.innerHTML; index++; this.scrollToBottom(); setTimeout(type, this.typingSpeed); } else { this.isTyping = false; // 打字结束 resolve(); } }; type(); }); } adjustInputHeight() { this.input.style.height = "auto"; this.input.style.height = Math.min(this.input.scrollHeight, 120) + "px"; } scrollToBottom() { this.messageContainer.scrollTop = this.messageContainer.scrollHeight; } togglePromptPanel() { this.promptPanel.classList.toggle("show"); } escapeHtml(html) { const div = document.createElement("div"); div.textContent = html; return div.innerHTML; } async checkApiKey() { // 只有当没有任何API密钥时才显示提示 if (!this.aiService.apiKey) { this.addMessage( "请先设置DeepSeek API密钥。输入格式:/setapi YOUR_API_KEY", "assistant" ); } } /** * 创建加载动画消息 */ createLoadingMessage() { const loadingDiv = document.createElement("div"); loadingDiv.className = "message assistant"; loadingDiv.innerHTML = `
`; this.messageContainer.appendChild(loadingDiv); this.scrollToBottom(); return loadingDiv; } /** * 格式化时间 */ formatTime(date) { const hours = date.getHours().toString().padStart(2, "0"); const minutes = date.getMinutes().toString().padStart(2, "0"); return `${hours}:${minutes}`; } createNewMessageIndicator() { this.newMessageIndicator = document.createElement("div"); this.newMessageIndicator.className = "new-messages-indicator"; this.newMessageIndicator.textContent = "新消息"; this.newMessageIndicator.addEventListener("click", () => { this.scrollToBottom(); }); document .querySelector(".chat-container") .appendChild(this.newMessageIndicator); } handleScroll() { const { scrollTop, scrollHeight, clientHeight } = this.messageContainer; const isNearBottom = scrollHeight - scrollTop - clientHeight < 100; if (isNearBottom) { this.newMessageIndicator.classList.remove("show"); } } /** * 格式化消息内容 * @param {string} text 原始文本 * @returns {string} 格式化后的HTML */ formatMessage(text) { if (!text) return ""; return ( text // 处理标题 (h1 ~ h6) .replace(/^#{1,6}\s+(.+)$/gm, (match, content) => { const level = match.trim().split("#").length - 1; return `${content.trim()}`; }) // 处理换行 .replace(/\n/g, "
") // 处理连续空格 .replace(/ {2,}/g, (match) => " ".repeat(match.length)) // 处理代码块 .replace( /```([\s\S]*?)```/g, (match, code) => `
${this.escapeHtml(code.trim())}
` ) // 处理行内代码 .replace( /`([^`]+)`/g, (match, code) => `${this.escapeHtml(code)}` ) // 处理粗体 .replace(/\*\*(.*?)\*\*/g, "$1") // 处理斜体 .replace(/\*(.*?)\*/g, "$1") // 处理链接 .replace( /\[([^\]]+)\]\(([^)]+)\)/g, '$1' ) // 处理无序列表 .replace(/^[*-]\s+(.+)$/gm, "
  • $1
  • ") .replace(/(
  • .*<\/li>)/gs, "") // 处理有序列表 .replace(/^\d+\.\s+(.+)$/gm, "
  • $1
  • ") .replace(/(
  • .*<\/li>)/gs, "
      $1
    ") // 处理分隔线 .replace(/^---+$/gm, "
    ") // 处理引用 .replace(/^>\s+(.+)$/gm, "
    $1
    ") ); } /** * 创建复制按钮 * @param {string} content 要复制的内容 * @returns {HTMLElement} 复制按钮元素 */ createCopyButton(content) { const button = document.createElement("button"); button.className = "copy-button"; button.innerHTML = ` 复制 `; button.addEventListener("click", async () => { try { // 直接使用传入的原始内容 await this.copyToClipboard(content); // 显示复制成功状态 button.classList.add("copied"); button.innerHTML = ` 已复制 `; // 2秒后恢复原始状态 setTimeout(() => { button.classList.remove("copied"); button.innerHTML = ` 复制 `; }, 2000); } catch (err) { console.error("Failed to copy text:", err); } }); return button; } /** * 复制文本到剪贴板 * @param {string} text 要复制的文本 */ async copyToClipboard(text) { try { // 通过postMessage发送复制请求到content script window.parent.postMessage( { type: "COPY_TO_CLIPBOARD", text: text, }, "*" ); return true; } catch (err) { console.error("Failed to copy text:", err); throw err; } } // 处理打断请求 async handleStop() { try { this.aiService.abortRequest(); if (this.isTyping) { this.isTyping = false; } if (this.loadingDiv) { this.loadingDiv.remove(); this.loadingDiv = null; } this.stopButton.classList.remove("show"); this.setInputState(false); // 恢复输入框状态 this.addMessage("用户手动停止生成", "assistant", false, true); } catch (error) { console.error("Failed to stop generation:", error); } } // 设置输入框状态 setInputState(isGenerating) { this.input.disabled = isGenerating; this.input.placeholder = isGenerating ? "回复生成中..." : "输入消息..."; if (isGenerating) { this.inputWrapper.classList.add("generating"); } else { this.inputWrapper.classList.remove("generating"); } } } // 等待DOM加载完成后再初始化 document.addEventListener("DOMContentLoaded", () => { try { const chatUI = new ChatUI(); } catch (error) { console.error("Failed to initialize ChatUI:", error); } });