class ChatUI { constructor() { this.messageContainer = document.getElementById("chat-messages"); this.input = document.getElementById("chat-input"); this.sendButton = document.getElementById("send-button"); this.stopButton = document.getElementById("stop-button"); this.promptButton = document.getElementById("prompt-button"); this.promptPanel = document.getElementById("prompt-panel"); this.uploadButton = document.getElementById("upload-button"); this.fileInput = document.getElementById("file-input"); this.ContentInputs = [] this.iframeInfo = null this.excelData = []; // 确保AI服务已经初始化 if (!window.aiService) { throw new Error("AI Service not initialized"); } this.aiService = window.aiService; this.typingSpeed = 50; this.inputWrapper = document.querySelector(".input-wrapper"); // 先添加过渡动画类 this.input.classList.add("input-transition"); // 设置初始高度 this.input.style.height = "40px"; // 获取总结按钮 this.summarizeButton = document.querySelector(".summarize-button"); // 支持的Excel文件类型 this.excelTypes = { xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", xls: "application/vnd.ms-excel", csv: "text/csv", }; // 初始化其他组件 this.setupEventListeners(); this.init(); // 使用微任务确保DOM更新后再调整高度 Promise.resolve().then(() => { // 第一帧:等待过渡动画类生效 requestAnimationFrame(() => { // 第二帧:执行高度调整 requestAnimationFrame(() => { this.adjustInputHeight(); // 触发一次输入框focus以确保正确的高度 this.input.focus(); }); }); }); // 添加新消息指示器 this.createNewMessageIndicator(); // 监听滚动事件 this.messageContainer.addEventListener( "scroll", this.handleScroll.bind(this) ); // 创建打断按钮 // 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.initPageInfoCard(); } 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()); // 添加focus事件监听,确保在获得焦点时高度正确 this.input.addEventListener("focus", () => { requestAnimationFrame(() => 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", () => { // 获取文本内容并处理格式 const text = item.textContent .trim() // 移除首尾空白 .replace(/\s+/g, " ") // 将多个空白字符替换为单个空格 .replace(/[\n\r]+/g, ""); // 移除所有换行符 // 设置到输入框 this.input.value = text; 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"); } }); // 添加总结按钮点击事件 if (this.summarizeButton) { this.summarizeButton.addEventListener("click", () => { this.handleSummarize(); }); } // 工具箱面板 const toolsButton = document.getElementById("tools-button"); const toolsPanel = document.getElementById("tools-panel"); const closeTools = document.querySelector(".close-tools"); toolsButton.addEventListener("click", () => { toolsPanel.classList.toggle("show"); // 关闭其他面板 this.promptPanel.classList.remove("show"); }); closeTools.addEventListener("click", () => { toolsPanel.classList.remove("show"); }); // 点击外部关闭工具箱面板 document.addEventListener("click", (e) => { if (!toolsPanel.contains(e.target) && !toolsButton.contains(e.target)) { toolsPanel.classList.remove("show"); } }); // 上传按钮点击事件 this.uploadButton.addEventListener("click", async () => { this.iframeInfo = await new Promise((resolve) => { // 创建一次性消息监听器 const messageHandler = (event) => { if (event.data.type === "PAGE_ANALYSIS_RESULT") { window.removeEventListener("message", messageHandler); resolve(event.data.pageInfo); } }; console.log(this.iframeInfo); // 添加消息监听 window.addEventListener("message", messageHandler); // 发送分析请求到父页面 window.parent.postMessage({ type: "ANALYZE_PAGE" }, "*"); console.log(2222285); }); console.log(this.iframeInfo); this.fileInput.click(); }); // 文件选择事件 this.fileInput.addEventListener("change", (event) => { console.log(event); const files = event.target.files; if (files.length > 0) { this.handleFileUpload(files); } // 清空文件输入框,确保同一文件可以重复上传 event.target.value = ""; }); } 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 = '' console.log(this.aiService.currentExcelData); // 如果存在Excel数据,添加上下文提示 let prompt = message; if (this.aiService.currentExcelData) { const { fileName, headers, totalRows, rows } = this.aiService.currentExcelData; prompt = `请记住之前我们正在讨论的Excel文件: - 文件名:${fileName} - 列标题:${headers.join(", ")} - 列内容:${rows.join(", ")} - 总行数:${totalRows} 基于这个Excel文件的内容,请回答以下问题: ${this.iframeInfo}`; } const response = await this.aiService.sendMessage(prompt); 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); console.log(content); console.log(Object.prototype.toString.call(content)); if (Object.prototype.toString.call(content) === "[object Object]") { actionsDiv.appendChild(this.createFillButton(paragraph)); } } // 添加时间戳 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"); if (typeof content !== 'string') { const signal = content.controller.signal; try { const iterator = content.iterator(); for await (const chunk of iterator) { if (chunk) { const decodedChunk = chunk.choices[0].delta.content; if (decodedChunk) { paragraph.innerHTML += decodedChunk; } } } } catch (error) { if (signal.aborted) { console.log("Stream reading aborted"); } else { console.error("Error reading stream:", error); } } } else { 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"); } return paragraph } /** * 实现打字机效果 * @param {HTMLElement} element 要添加文字的元素 * @param {string} text 要显示的文字 */ async typeMessage(element, text) { return element.innerHTML = 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() { const scrollPos = this.input.scrollTop; // 获取当前高度 const currentHeight = this.input.style.height; // 重置高度 this.input.style.height = "auto"; // 计算新高度 const newHeight = Math.min(this.input.scrollHeight, 120); // 如果高度有变化才设置 if (currentHeight !== `${newHeight}px`) { this.input.style.height = `${newHeight}px`; } // 恢复滚动位置 this.input.scrollTop = scrollPos; } 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
    ") ); } createFillButton(paragraph) { const button = document.createElement("button"); button.className = "fill-button"; button.innerHTML = ` 填充 `; button.addEventListener("click", () => { window.parent.postMessage({ type: "HANDLE_FILL_INPUT", data: { formData: JSON.parse(paragraph.innerHTML.split('json')[1].split('```')[0]) //根据不同返回修改res } }, "*"); }); return button; } /** * 创建复制按钮 * @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"); this.stopButton.classList.add("show"); } else { this.inputWrapper.classList.remove("generating"); // this.stopButton.classList.remove("show"); // AI回复完成后,自动聚焦到输入框 this.input.focus(); } } /** * 初始化页面信息卡片 */ initPageInfoCard() { // 从父窗口获取页面信息 window.addEventListener("message", (event) => { if (event.data.type === "PAGE_INFO") { const pageInfo = event.data.pageInfo; this.iframeInfo = event.data.pageInfo.iframe this.ContentInputs = pageInfo.inputs // 更新卡片内容 const favicon = document.querySelector(".page-favicon"); const title = document.querySelector(".page-title"); // 设置网站图标 favicon.src = pageInfo.favicon; favicon.onerror = () => { // 如果图标加载失败,使用默认图标 favicon.src = chrome.runtime.getURL("images/icon16.png"); }; // 设置页面标题 title.textContent = pageInfo.title; } }); } /** * 获取页面favicon * @returns {string} favicon URL */ getPageFavicon() { // 尝试获取页面favicon const iconLink = document.querySelector('link[rel*="icon"]'); if (iconLink) return iconLink.href; // 如果没有找到,返回网站根目录的favicon.ico const url = new URL(window.location.href); return `${url.protocol}//${url.hostname}/favicon.ico`; } /** * 处理总结请求 */ async handleSummarize() { try { // 显示加载状态 this.setInputState(true); this.loadingDiv = '' // 通过postMessage请求页面分析结果 this.iframeInfo = await new Promise((resolve) => { // 创建一次性消息监听器 const messageHandler = (event) => { if (event.data.type === "PAGE_ANALYSIS_RESULT") { window.removeEventListener("message", messageHandler); resolve(event.data.pageInfo); } }; // 添加消息监听 window.addEventListener("message", messageHandler); // 发送分析请求到父页面 window.parent.postMessage({ type: "ANALYZE_PAGE" }, "*"); }); // 构建提示词 const prompt = this.aiService.getSummaryPrompt(pageInfo); // 发送分析请求 const response = await this.aiService.sendMessage(prompt); // 显示分析结果 if (this.loadingDiv) { this.loadingDiv.remove(); this.loadingDiv = null; } await this.addMessage(response, "assistant", false); // 将总结内容添加到上下文 this.aiService.updateContext(prompt, "user"); this.aiService.updateContext(response, "assistant"); // 保存页面信息到上下文 this.aiService.setPageInfo(pageInfo); this.setInputState(false); } catch (error) { console.error("Failed to summarize page:", error); this.addMessage( "抱歉,页面总结过程中出现错误,请稍后重试。", "assistant", false ); this.setInputState(false); } } /** * 处理文件上传 * @param {FileList} files 上传的文件列表 */ async handleFileUpload(files) { console.log(files); try { for (const file of files) { const extension = file.name.split(".").pop().toLowerCase(); // 显示文件上传消息 this.addMessage(`已上传文件:${file.name}`, "user", false); if (this.excelTypes[extension]) { try { this.setInputState(true); this.loadingDiv = this.createLoadingMessage(); // 读取Excel文件 const data = await this.readExcelFile(file); // 保存Excel数据到AI服务 this.aiService.setExcelData({ fileName: file.name, headers: data[0], rows: data.slice(1), totalRows: data.length - 1, }); // 构建Excel理解提示词 const prompt = this.buildExcelUnderstandingPrompt(data, file.name); // 调用AI服务理解数据 const response = await this.aiService.sendMessage(prompt); console.log(response); if (this.loadingDiv) { this.loadingDiv.remove(); this.loadingDiv = null; } 0 && window.parent.postMessage({ type: "HANDLE_FILL_INPUT", data: { excelData: this.excelData, formData: JSON.parse(response.split('json')[1].split('```')[0]) //根据不同返回修改res } }, "*"); // 显示AI的理解结果 this.addMessage(response, "assistant", true); } catch (error) { console.error("Excel processing error:", error); this.addMessage( "Excel文件处理过程中出现错误,请重试。", "assistant", false ); } finally { this.setInputState(false); } } } } catch (error) { console.error("File upload error:", error); this.addMessage("文件上传过程中出现错误,请重试。", "assistant", false); } } /** * 构建Excel理解提示词 */ buildExcelUnderstandingPrompt(data, fileName) { if (!data || data.length < 2) { return "这是一个空的Excel文件,请检查文件内容。"; } const headers = data[0]; const rows = data.slice(1); const sampleRows = rows.slice(0, 2); console.log(); data[0].forEach((header, i) => { if (!this.excelData[header]) this.excelData[header] = [] this.excelData[header].push(data[1][i]) }) window.parent.postMessage({ type: "EXCEL_DATA", data: { excelData: this.excelData, } }, "*"); return `我将向你展示一个通过SheetJS库读取的Excel文件内容和一个form表单。请帮我理解这些数据: 文件名:${fileName} 列标题:${headers.join(", ")} 表单内容: ${this.iframeInfo} 要求: 1. 请分析表单中实际可操作的表单项, 2. 并根据实际可操作的表单项的所有信息与上传excel文件中的列标题进行匹配,生成表单项与excel文件中列标题的数组,并使用findBy告诉我通过表单项的什么字段信息匹配到的,使用findByValue告诉我匹配到的表单项字段值,使用excelColumn字段告诉我excel文件中列标题的值。在一个字段内返回 3. 并去除没有匹配到的表单项和excel文件中没有匹配到的列, 4. 通过type字段告诉我输入项的类型 5. 如果表单项有label标签,同时返回label,通过label字段告诉我label元素的文本 5. 仅返回数组,不要返回任何其他内容。` } /** * 读取Excel文件 * @param {File} file * @returns {Promise} */ async readExcelFile(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { try { const data = new Uint8Array(e.target.result); const workbook = XLSX.read(data, { type: "array", cellDates: true, cellNF: false, cellText: false, }); // 获取第一个工作表 const firstSheet = workbook.Sheets[workbook.SheetNames[0]]; // 转换为JSON数据 const jsonData = XLSX.utils.sheet_to_json(firstSheet, { header: 1, raw: true, defval: "", }); resolve(jsonData); } catch (error) { reject(error); } }; reader.onerror = () => reject(reader.error); reader.readAsArrayBuffer(file); }); } } // 数据行数:${rows.length} // 示例数据(前2行): // ${sampleRows // .map((row, index) => { // return `第${index + 1}行: ${row // .map((cell, i) => `${headers[i]}=${cell}`) // .join(", ")}`; // }) // .join("\n")} // 等待DOM加载完成后再初始化 document.addEventListener("DOMContentLoaded", () => { try { window.chatUI = new ChatUI(); } catch (error) { console.error("Failed to initialize ChatUI:", error); } });