alibct 7 місяців тому
батько
коміт
7918868c31
11 змінених файлів з 1490 додано та 7 видалено
  1. 19 1
      .gitignore
  2. 68 0
      CHANGELOG.md
  3. 22 0
      README.md
  4. 582 0
      css/chat.css
  5. BIN
      images/icon32.png
  6. 155 0
      js/ai-service.js
  7. 511 0
      js/chat-ui.js
  8. 15 0
      js/config.template.js
  9. 26 0
      js/content.js
  10. 25 4
      manifest.json
  11. 67 2
      sidebar.html

+ 19 - 1
.gitignore

@@ -1,3 +1,21 @@
-node_modules/
+# 配置文件(包含敏感信息)
+js/config.js
+
+# 系统文件
 .DS_Store
+Thumbs.db
+
+# IDE文件
+.idea/
+.vscode/
+*.sublime-*
+
+# 依赖目录
+node_modules/
+
+# 构建输出
+dist/
+build/
+
+# 日志文件
 *.log 

+ 68 - 0
CHANGELOG.md

@@ -1,5 +1,73 @@
 # 更新日志
 
+## [0.0.4] - 2024-03-xx
+
+### 新增
+
+- 添加消息打断功能
+  - 支持中断 AI 回复生成
+  - 添加打断按钮和反馈
+  - 优化用户体验
+
+### 优化
+
+- 改进消息复制功能
+  - 通过 content script 实现跨域复制
+  - 添加错误处理和反馈
+  - 优化用户体验
+- 优化消息生成状态
+  - 禁用输入框和非必要按钮
+  - 添加状态提示和视觉反馈
+  - 改进交互体验
+- 完善扩展图标配置
+  - 添加多尺寸图标支持
+  - 优化显示效果
+
+### 修复
+
+- 修复扩展图标在管理页面不显示的问题
+- 修复消息复制功能的跨域问题
+- 修复按钮对齐和样式问题
+
+## [0.0.3] - 2024-03-xx
+
+### 新增
+
+- 集成 DeepSeek AI API
+  - 实现 API 通信
+  - 支持默认/自定义 API 密钥
+  - 添加 API 密钥管理命令
+- 优化聊天界面
+  - 添加打字机效果
+  - 实现加载动画
+  - 优化光标跟随
+  - 添加快捷指令面板
+- 添加消息打断功能
+  - 支持中断 AI 回复生成
+  - 添加打断按钮和反馈
+  - 优化用户体验
+
+### 优化
+
+- 改进消息显示效果
+  - 平滑的动画过渡
+  - 更自然的打字效果
+  - 优化消息布局
+- 完善 API 密钥管理
+  - 支持默认密钥
+  - 可选自定义密钥
+  - 密钥本地存储
+- 改进复制功能
+  - 通过 content script 实现跨域复制
+  - 添加错误处理和反馈
+  - 优化用户体验
+
+### 安全性
+
+- 添加配置文件模板
+- 分离敏感配置信息
+- 完善.gitignore 配置
+
 ## [0.0.2] - 2024-03-xx
 
 ### 新增

+ 22 - 0
README.md

@@ -1,7 +1,27 @@
 # 派维斯智能体助手
 
+## 版本
+
+当前版本:0.0.4
+
 这是一个 Chrome 浏览器扩展项目,用于辅助用户处理业务流程的智能体助手。
 
+## 功能特性
+
+### AI 智能助手
+
+- 智能对话:自然语言交互
+- 上下文感知:理解当前页面内容
+- 多轮对话:保持对话连贯性
+- 提示词系统:快速访问常用功能
+
+### 基础功能
+
+- 3:1 分屏布局
+- 页面自适应
+- 状态持久化
+- 平滑动画效果
+
 ## 安装说明
 
 1. 打开 Chrome 浏览器
@@ -12,6 +32,8 @@
 
 ## 开发说明
 
+### 项目结构
+
 项目结构:
 
 - manifest.json: 扩展配置文件

+ 582 - 0
css/chat.css

@@ -0,0 +1,582 @@
+.chat-container {
+  display: flex;
+  flex-direction: column;
+  height: calc(100vh - 60px); /* 减去header高度 */
+  background: #fcfcfd; /* 更浅的背景色,接近白色 */
+}
+
+.chat-messages {
+  flex: 1;
+  overflow-y: auto;
+  padding: 20px;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  scroll-behavior: smooth;
+  background: #fcfcfd; /* 与容器背景保持一致 */
+}
+
+.message {
+  display: flex;
+  align-items: flex-start;
+  max-width: 85%;
+  margin-bottom: 4px;
+}
+
+.message.user {
+  margin-left: auto;
+}
+
+.message-content {
+  position: relative;
+  padding: 12px 16px;
+  border-radius: 12px;
+  background: #fff;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
+  transition: all 0.3s ease;
+}
+
+.message.assistant .message-content {
+  background: #f0f4f8; /* AI消息背景色 */
+  border-top-left-radius: 4px;
+}
+
+.message.user .message-content {
+  background: #e8f4ff; /* 用户消息背景色 */
+  border-top-right-radius: 4px;
+}
+
+.message p {
+  margin: 0;
+  line-height: 1.5;
+  color: #333;
+}
+
+.chat-input-container {
+  padding: 16px;
+  background: #fff;
+  border-top: 1px solid #f0f0f0;
+}
+
+.input-wrapper {
+  position: relative;
+  display: flex;
+  gap: 8px;
+  background: #f0f4f8; /* 与AI消息框背景色保持一致 */
+  border-radius: 8px;
+  padding: 8px 12px;
+  transition: all 0.2s ease;
+}
+
+.input-wrapper:focus-within {
+  box-shadow: 0 0 0 1px #1a73e8;
+}
+
+#chat-input {
+  flex: 1;
+  border: none;
+  background: transparent;
+  resize: none;
+  padding: 8px;
+  font-size: 14px;
+  line-height: 1.5;
+  max-height: 120px;
+  outline: none;
+  color: #333;
+}
+
+.input-buttons {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.icon-button {
+  position: relative;
+  overflow: hidden;
+  background: none;
+  border: none;
+  padding: 6px;
+  cursor: pointer;
+  color: #666;
+  border-radius: 4px;
+  transition: all 0.2s;
+}
+
+.icon-button::after {
+  content: "";
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 100%;
+  height: 100%;
+  background: currentColor;
+  border-radius: 50%;
+  transform: translate(-50%, -50%) scale(0);
+  opacity: 0;
+  transition: all 0.2s ease;
+}
+
+.icon-button:hover::after {
+  transform: translate(-50%, -50%) scale(1.5);
+  opacity: 0.1;
+}
+
+/* 快捷指令面板 */
+.prompt-panel {
+  position: absolute;
+  bottom: 80px;
+  left: 16px;
+  right: 16px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  display: none;
+}
+
+.prompt-panel.show {
+  display: block;
+  animation: slideUp 0.2s ease-out;
+}
+
+.prompt-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px 16px;
+  border-bottom: 1px solid #eee;
+}
+
+.prompt-header h3 {
+  margin: 0;
+  font-size: 16px;
+  color: #333;
+}
+
+.close-prompt {
+  background: none;
+  border: none;
+  font-size: 20px;
+  color: #666;
+  cursor: pointer;
+  padding: 4px 8px;
+}
+
+.prompt-list {
+  padding: 8px;
+  max-height: 300px;
+  overflow-y: auto;
+}
+
+.prompt-item {
+  width: 100%;
+  padding: 12px;
+  text-align: left;
+  background: none;
+  border: none;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.prompt-item:hover {
+  background: #f5f5f5;
+}
+
+@keyframes slideUp {
+  from {
+    transform: translateY(20px);
+    opacity: 0;
+  }
+  to {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}
+
+/* 滚动条样式 */
+.chat-messages::-webkit-scrollbar {
+  width: 6px;
+}
+
+.chat-messages::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+.chat-messages::-webkit-scrollbar-thumb {
+  background: #ddd;
+  border-radius: 3px;
+}
+
+.chat-messages::-webkit-scrollbar-thumb:hover {
+  background: #ccc;
+}
+
+/* 打字动画相关样式 */
+.message-content.typing {
+  display: flex;
+  flex-direction: column;
+}
+
+.message-content.typing p {
+  display: inline;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+
+.message-content.typing p::after {
+  content: "|";
+  display: inline;
+  margin-left: 1px;
+  font-weight: bold;
+  animation: blink 1s infinite;
+  vertical-align: baseline;
+}
+
+@keyframes blink {
+  0%,
+  100% {
+    opacity: 1;
+  }
+  50% {
+    opacity: 0;
+  }
+}
+
+/* 加载动画样式 */
+.typing-indicator {
+  display: flex;
+  gap: 4px;
+  padding: 4px 8px;
+}
+
+.typing-indicator span {
+  width: 8px;
+  height: 8px;
+  background: #90a4ae;
+  border-radius: 50%;
+  animation: bounce 1.4s infinite ease-in-out;
+}
+
+.typing-indicator span:nth-child(1) {
+  animation-delay: -0.32s;
+}
+.typing-indicator span:nth-child(2) {
+  animation-delay: -0.16s;
+}
+
+@keyframes bounce {
+  0%,
+  80%,
+  100% {
+    transform: scale(0);
+  }
+  40% {
+    transform: scale(1);
+  }
+}
+
+/* 消息内容的过渡效果 */
+.message-content {
+  transition: opacity 0.3s ease;
+}
+
+.message-content.loading {
+  min-width: 60px;
+  min-height: 24px;
+}
+
+/* 消息时间戳样式 */
+.message-timestamp {
+  font-size: 12px;
+  color: #999;
+  line-height: 24px; /* 与按钮高度一致 */
+  margin: 0; /* 移除可能的外边距 */
+  padding: 0 2px; /* 添加少许水平内边距 */
+}
+
+/* 消息hover效果 */
+.message-content:hover {
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
+}
+
+/* 添加头像占位 */
+.message.assistant::before {
+  content: "";
+  width: 32px;
+  height: 32px;
+  min-width: 32px;
+  margin-right: 8px;
+  border-radius: 50%;
+  background: #fff url("../images/icon128.png") center/cover no-repeat;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  transition: all 0.3s ease;
+}
+
+/* 头像悬停效果 */
+.message.assistant:hover::before {
+  transform: scale(1.1);
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+}
+
+/* 消息分组 */
+.message + .message.assistant,
+.message + .message.user {
+  margin-top: 2px;
+}
+
+.message:not(:first-child) {
+  margin-top: 16px;
+}
+
+/* 添加新消息指示器 */
+.new-messages-indicator {
+  position: absolute;
+  bottom: 80px;
+  left: 50%;
+  transform: translateX(-50%);
+  background: rgba(33, 150, 243, 0.9);
+  color: #fff;
+  padding: 8px 16px;
+  border-radius: 20px;
+  font-size: 14px;
+  cursor: pointer;
+  opacity: 0;
+  transition: all 0.3s ease;
+  pointer-events: none;
+}
+
+.new-messages-indicator.show {
+  opacity: 1;
+  pointer-events: auto;
+}
+
+/* 消息格式化样式 */
+.message-content p {
+  margin: 0;
+  line-height: 1.5;
+}
+
+.message-content pre {
+  background: #f6f8fa;
+  padding: 12px;
+  border-radius: 6px;
+  overflow-x: auto;
+  margin: 8px 0;
+}
+
+.message-content code {
+  font-family: monospace;
+  background: #f6f8fa;
+  padding: 2px 4px;
+  border-radius: 4px;
+  font-size: 0.9em;
+}
+
+.message-content pre code {
+  padding: 0;
+  background: none;
+}
+
+.message-content a {
+  color: #1a73e8;
+  text-decoration: none;
+}
+
+.message-content a:hover {
+  text-decoration: underline;
+}
+
+.message-content strong {
+  font-weight: 600;
+}
+
+.message-content em {
+  font-style: italic;
+}
+
+/* 消息操作栏样式 */
+.message-actions {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center; /* 确保垂直居中对齐 */
+  gap: 8px;
+  margin-top: 4px;
+  min-height: 24px; /* 设置最小高度确保一致性 */
+}
+
+/* 复制按钮样式 */
+.copy-button {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  height: 24px; /* 固定高度 */
+  padding: 0 6px; /* 调整水平内边距 */
+  font-size: 12px;
+  color: #666;
+  background: none;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.copy-button:hover {
+  background: rgba(0, 0, 0, 0.05);
+  color: #1a73e8;
+}
+
+.copy-button svg {
+  width: 14px;
+  height: 14px;
+}
+
+/* 复制成功提示 */
+.copy-button.copied {
+  color: #00c853;
+  pointer-events: none;
+}
+
+/* Markdown样式 */
+.message-content h1,
+.message-content h2,
+.message-content h3,
+.message-content h4,
+.message-content h5,
+.message-content h6 {
+  margin: 16px 0 8px;
+  font-weight: 600;
+  line-height: 1.25;
+  color: #333;
+}
+
+.message-content h1 {
+  font-size: 1.5em;
+}
+.message-content h2 {
+  font-size: 1.3em;
+}
+.message-content h3 {
+  font-size: 1.2em;
+}
+.message-content h4 {
+  font-size: 1.1em;
+}
+.message-content h5 {
+  font-size: 1em;
+}
+.message-content h6 {
+  font-size: 0.9em;
+}
+
+.message-content ul,
+.message-content ol {
+  margin: 8px 0;
+  padding-left: 20px;
+}
+
+.message-content li {
+  margin: 4px 0;
+}
+
+.message-content blockquote {
+  margin: 8px 0;
+  padding: 8px 16px;
+  border-left: 4px solid #ddd;
+  background: rgba(0, 0, 0, 0.02);
+  color: #666;
+}
+
+.message-content hr {
+  margin: 16px 0;
+  border: none;
+  border-top: 1px solid #eee;
+}
+
+/* 调整代码块样式 */
+.message-content pre {
+  background: #f6f8fa;
+  padding: 16px;
+  border-radius: 6px;
+  overflow-x: auto;
+  margin: 8px 0;
+}
+
+.message-content pre code {
+  padding: 0;
+  background: none;
+  font-size: 0.9em;
+  line-height: 1.5;
+}
+
+/* 打断按钮样式 */
+.stop-button {
+  display: none; /* 默认隐藏 */
+  padding: 6px 12px;
+  color: #666;
+  background: none;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 12px;
+  transition: all 0.2s ease;
+}
+
+.stop-button.show {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.stop-button:hover {
+  color: #d32f2f;
+  border-color: #d32f2f;
+  background: rgba(211, 47, 47, 0.05);
+}
+
+.stop-button svg {
+  width: 14px;
+  height: 14px;
+}
+
+/* 中断消息样式 */
+.message.assistant .message-content.interrupted {
+  background: #fff1f0;
+  border-left: 4px solid #ff4d4f;
+  border-top-left-radius: 0;
+  padding-left: 12px;
+  color: #cf1322;
+}
+
+.message-content.interrupted .message-timestamp {
+  color: rgba(207, 19, 34, 0.6);
+}
+
+/* 输入框禁用状态样式 */
+#chat-input:disabled {
+  cursor: not-allowed;
+  color: #999;
+}
+
+.input-wrapper.generating {
+  background: #f5f5f5;
+}
+
+/* 修改:只禁用输入框和其他按钮 */
+.input-wrapper.generating #chat-input,
+.input-wrapper.generating .icon-button {
+  pointer-events: none;
+  opacity: 0.5; /* 添加透明度表示禁用状态 */
+}
+
+.input-wrapper.generating #chat-input::placeholder {
+  color: #999;
+  opacity: 0.8;
+}
+
+/* 确保只有停止按钮可以点击 */
+.input-wrapper.generating .stop-button {
+  pointer-events: auto;
+  opacity: 1; /* 保持完全不透明 */
+  cursor: pointer;
+}

BIN
images/icon32.png


+ 155 - 0
js/ai-service.js

@@ -0,0 +1,155 @@
+/**
+ * AI服务类
+ * 处理与AI模型的通信和响应
+ */
+class AIService {
+  constructor() {
+    this.apiEndpoint = CONFIG.AI_API.ENDPOINT;
+    this.model = CONFIG.AI_API.MODEL;
+    this.context = [];
+    // 使用默认API密钥
+    this.apiKey = CONFIG.AI_API.DEFAULT_API_KEY;
+    this.controller = null; // 用于中断请求的 AbortController
+  }
+
+  /**
+   * 初始化AI服务
+   */
+  async init() {
+    try {
+      // 尝试从storage中获取用户设置的API密钥
+      const result = await Utils.getStorageData("apiKey");
+      if (result?.apiKey) {
+        this.apiKey = result.apiKey;
+      }
+      console.log("AI Service initialized");
+    } catch (error) {
+      console.error("Failed to initialize AI service:", error);
+    }
+  }
+
+  /**
+   * 设置API密钥
+   * @param {string} apiKey
+   */
+  async setApiKey(apiKey) {
+    this.apiKey = apiKey;
+    await Utils.setStorageData("apiKey", { apiKey });
+  }
+
+  /**
+   * 发送消息到DeepSeek API
+   * @param {string} message 用户消息
+   * @returns {Promise<string>} AI响应
+   */
+  async sendMessage(message) {
+    try {
+      // 创建新的 AbortController
+      this.controller = new AbortController();
+
+      const response = await fetch(this.apiEndpoint, {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+          Authorization: `Bearer ${this.apiKey}`,
+        },
+        body: JSON.stringify({
+          model: this.model,
+          messages: this.formatMessages(message),
+          max_tokens: CONFIG.AI_API.MAX_TOKENS,
+          temperature: CONFIG.AI_API.TEMPERATURE,
+        }),
+        signal: this.controller.signal,
+      });
+
+      if (!response.ok) {
+        throw new Error(`API request failed: ${response.status}`);
+      }
+
+      const data = await response.json();
+      const aiResponse = data.choices[0]?.message?.content;
+
+      if (!aiResponse) {
+        throw new Error("无效的API响应");
+      }
+
+      return aiResponse;
+    } catch (error) {
+      if (error.name === "AbortError") {
+        throw new Error("REQUEST_ABORTED");
+      }
+      console.error("API call failed:", error);
+      throw error;
+    } finally {
+      this.controller = null;
+    }
+  }
+
+  /**
+   * 格式化消息历史
+   * @param {string} currentMessage 当前消息
+   * @returns {Array} 格式化后的消息数组
+   */
+  formatMessages(currentMessage) {
+    const messages = this.context.map((msg) => ({
+      role: msg.role,
+      content: msg.content,
+    }));
+
+    messages.push({
+      role: "user",
+      content: currentMessage,
+    });
+
+    return messages;
+  }
+
+  /**
+   * 更新对话上下文
+   * @param {string} message 新消息
+   * @param {string} role 消息角色(user/assistant)
+   */
+  updateContext(message, role) {
+    this.context.push({
+      role,
+      content: message,
+      timestamp: new Date().toISOString(),
+    });
+
+    // 保持上下文长度在合理范围内
+    if (this.context.length > 10) {
+      this.context = this.context.slice(-10);
+    }
+  }
+
+  /**
+   * 清除对话上下文
+   */
+  clearContext() {
+    this.context = [];
+  }
+
+  /**
+   * 获取当前对话上下文
+   * @returns {Array} 对话上下文数组
+   */
+  getContext() {
+    return this.context;
+  }
+
+  // 添加中断方法
+  abortRequest() {
+    if (this.controller) {
+      this.controller.abort();
+      this.controller = null;
+    }
+  }
+}
+
+// 确保在DOM加载完成后再创建实例
+document.addEventListener("DOMContentLoaded", () => {
+  // 只有在实例不存在时才创建
+  if (!window.aiService) {
+    window.aiService = new AIService();
+  }
+});

+ 511 - 0
js/chat-ui.js

@@ -0,0 +1,511 @@
+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 = `
+      <svg viewBox="0 0 24 24" fill="currentColor">
+        <path d="M6 6h12v12H6z"/>
+      </svg>
+      <span>停止生成</span>
+    `;
+
+    // 将打断按钮添加到输入按钮区域
+    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 = `
+      <div class="message-content loading">
+        <div class="typing-indicator">
+          <span></span>
+          <span></span>
+          <span></span>
+        </div>
+      </div>
+    `;
+    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 `<h${level}>${content.trim()}</h${level}>`;
+        })
+        // 处理换行
+        .replace(/\n/g, "<br>")
+        // 处理连续空格
+        .replace(/ {2,}/g, (match) => "&nbsp;".repeat(match.length))
+        // 处理代码块
+        .replace(
+          /```([\s\S]*?)```/g,
+          (match, code) =>
+            `<pre><code>${this.escapeHtml(code.trim())}</code></pre>`
+        )
+        // 处理行内代码
+        .replace(
+          /`([^`]+)`/g,
+          (match, code) => `<code>${this.escapeHtml(code)}</code>`
+        )
+        // 处理粗体
+        .replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
+        // 处理斜体
+        .replace(/\*(.*?)\*/g, "<em>$1</em>")
+        // 处理链接
+        .replace(
+          /\[([^\]]+)\]\(([^)]+)\)/g,
+          '<a href="$2" target="_blank">$1</a>'
+        )
+        // 处理无序列表
+        .replace(/^[*-]\s+(.+)$/gm, "<li>$1</li>")
+        .replace(/(<li>.*<\/li>)/gs, "<ul>$1</ul>")
+        // 处理有序列表
+        .replace(/^\d+\.\s+(.+)$/gm, "<li>$1</li>")
+        .replace(/(<li>.*<\/li>)/gs, "<ol>$1</ol>")
+        // 处理分隔线
+        .replace(/^---+$/gm, "<hr>")
+        // 处理引用
+        .replace(/^>\s+(.+)$/gm, "<blockquote>$1</blockquote>")
+    );
+  }
+
+  /**
+   * 创建复制按钮
+   * @param {string} content 要复制的内容
+   * @returns {HTMLElement} 复制按钮元素
+   */
+  createCopyButton(content) {
+    const button = document.createElement("button");
+    button.className = "copy-button";
+    button.innerHTML = `
+      <svg viewBox="0 0 24 24" fill="currentColor">
+        <path d="M16 1H4C2.9 1 2 1.9 2 3v14h2V3h12V1zm3 4H8C6.9 5 6 5.9 6 7v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
+      </svg>
+      <span>复制</span>
+    `;
+
+    button.addEventListener("click", async () => {
+      try {
+        // 直接使用传入的原始内容
+        await this.copyToClipboard(content);
+
+        // 显示复制成功状态
+        button.classList.add("copied");
+        button.innerHTML = `
+          <svg viewBox="0 0 24 24" fill="currentColor">
+            <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
+          </svg>
+          <span>已复制</span>
+        `;
+
+        // 2秒后恢复原始状态
+        setTimeout(() => {
+          button.classList.remove("copied");
+          button.innerHTML = `
+            <svg viewBox="0 0 24 24" fill="currentColor">
+              <path d="M16 1H4C2.9 1 2 1.9 2 3v14h2V3h12V1zm3 4H8C6.9 5 6 5.9 6 7v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
+            </svg>
+            <span>复制</span>
+          `;
+        }, 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);
+  }
+});

+ 15 - 0
js/config.template.js

@@ -0,0 +1,15 @@
+/**
+ * 配置文件模板
+ * 使用时复制为 config.js 并填入实际的API密钥
+ */
+const CONFIG = {
+  AI_API: {
+    ENDPOINT: "https://api.deepseek.com/v1/chat/completions",
+    MODEL: "deepseek-chat",
+    MAX_TOKENS: 2000,
+    TEMPERATURE: 0.7,
+    DEFAULT_API_KEY: "sk-3f91d3517b3648e8b4414f34de0696ea", // DeepSeek API密钥
+  },
+};
+
+Object.freeze(CONFIG);

+ 26 - 0
js/content.js

@@ -67,6 +67,32 @@ class SidebarManager {
         this.checkAndRestoreState();
       }
     }).observe(document, { subtree: true, childList: true });
+
+    // 监听来自sidebar的消息
+    window.addEventListener("message", async (event) => {
+      if (event.data.type === "COPY_TO_CLIPBOARD") {
+        try {
+          await navigator.clipboard.writeText(event.data.text);
+          // 可选:发送成功消息回sidebar
+          event.source.postMessage(
+            {
+              type: "COPY_SUCCESS",
+            },
+            "*"
+          );
+        } catch (err) {
+          console.error("Failed to copy text:", err);
+          // 可选:发送失败消息回sidebar
+          event.source.postMessage(
+            {
+              type: "COPY_ERROR",
+              error: err.message,
+            },
+            "*"
+          );
+        }
+      }
+    });
   }
 
   /**

+ 25 - 4
manifest.json

@@ -1,13 +1,28 @@
 {
   "manifest_version": 3,
   "name": "派维斯智能体助手",
-  "version": "0.0.2",
+  "version": "0.0.4",
   "description": "一个辅助用户处理业务流程的智能体助手",
   "author": "Paiwise Team",
-  "permissions": ["activeTab", "scripting", "storage", "tabs"],
+  "permissions": [
+    "activeTab",
+    "scripting",
+    "storage",
+    "tabs",
+    "notifications",
+    "clipboardWrite"
+  ],
+  "icons": {
+    "16": "images/icon16.png",
+    "32": "images/icon32.png",
+    "48": "images/icon48.png",
+    "128": "images/icon128.png"
+  },
   "action": {
+    "default_title": "派维斯智能体助手",
     "default_icon": {
       "16": "images/icon16.png",
+      "32": "images/icon32.png",
       "48": "images/icon48.png",
       "128": "images/icon128.png"
     }
@@ -19,12 +34,18 @@
     {
       "matches": ["<all_urls>"],
       "css": ["css/content.css"],
-      "js": ["js/utils.js", "js/content.js"]
+      "js": [
+        "js/config.js",
+        "js/utils.js",
+        "js/ai-service.js",
+        "js/chat-ui.js",
+        "js/content.js"
+      ]
     }
   ],
   "web_accessible_resources": [
     {
-      "resources": ["sidebar.html", "css/*", "js/*"],
+      "resources": ["sidebar.html", "css/*", "js/*", "templates/*"],
       "matches": ["<all_urls>"]
     }
   ]

+ 67 - 2
sidebar.html

@@ -4,15 +4,80 @@
     <meta charset="UTF-8" />
     <title>派维斯智能体助手</title>
     <link rel="stylesheet" href="css/sidebar.css" />
+    <link rel="stylesheet" href="css/chat.css" />
   </head>
   <body>
     <div class="sidebar-header">
       <h1>派维斯智能体助手</h1>
       <button id="close-sidebar" class="close-btn">×</button>
     </div>
-    <div class="sidebar-content">
-      <!-- 这里将放置助手的主要功能界面 -->
+
+    <div class="chat-container">
+      <!-- 聊天消息区域 -->
+      <div class="chat-messages" id="chat-messages">
+        <!-- 欢迎消息 -->
+        <div class="message assistant">
+          <div class="message-content">
+            <p>你好!我是派维斯智能助手。</p>
+            <p>使用说明:</p>
+            <ul>
+              <li>直接发送消息开始对话</li>
+              <li>使用快捷指令快速操作</li>
+              <li>可选:输入 /setapi YOUR_API_KEY 设置自定义API密钥</li>
+            </ul>
+          </div>
+        </div>
+      </div>
+
+      <!-- 输入区域 -->
+      <div class="chat-input-container">
+        <div class="input-wrapper">
+          <textarea
+            id="chat-input"
+            placeholder="输入消息..."
+            rows="1"
+            autofocus
+          ></textarea>
+          <div class="input-buttons">
+            <button id="prompt-button" class="icon-button" title="快捷指令">
+              <svg viewBox="0 0 24 24" width="20" height="20">
+                <path
+                  fill="currentColor"
+                  d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9L12 3zm6.82 6L12 12.72 5.18 9 12 5.28 18.82 9zM17 15.99l-5 2.73-5-2.73v-3.72L12 15l5-2.73v3.72z"
+                />
+              </svg>
+            </button>
+            <button id="send-button" class="icon-button" title="发送">
+              <svg viewBox="0 0 24 24" width="20" height="20">
+                <path
+                  fill="currentColor"
+                  d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"
+                />
+              </svg>
+            </button>
+          </div>
+        </div>
+      </div>
+
+      <!-- 快捷指令面板 -->
+      <div class="prompt-panel" id="prompt-panel">
+        <div class="prompt-header">
+          <h3>快捷指令</h3>
+          <button class="close-prompt">×</button>
+        </div>
+        <div class="prompt-list">
+          <button class="prompt-item">分析当前页面内容</button>
+          <button class="prompt-item">总结主要观点</button>
+          <button class="prompt-item">提取关键信息</button>
+          <button class="prompt-item">翻译选中内容</button>
+        </div>
+      </div>
     </div>
+
+    <script src="js/config.js"></script>
+    <script src="js/utils.js"></script>
+    <script src="js/ai-service.js"></script>
+    <script src="js/chat-ui.js"></script>
     <script src="js/sidebar.js"></script>
   </body>
 </html>