diff --git a/src/ui/ai_chat_panel.py b/src/ui/ai_chat_panel.py new file mode 100644 index 0000000..cc0eb68 --- /dev/null +++ b/src/ui/ai_chat_panel.py @@ -0,0 +1,381 @@ +# ai_chat_panel.py - AI对话面板组件 +import json +import os +import requests +import threading +from datetime import datetime +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLineEdit, + QPushButton, QLabel, QScrollArea, QFrame, QMessageBox +) +from PyQt5.QtCore import Qt, pyqtSignal, QThread, QTimer, QSize, pyqtSlot +from PyQt5.QtGui import QFont, QColor, QTextCursor, QIcon, QPixmap +from PyQt5.QtGui import QTextDocument, QTextCharFormat + +class AIChatPanel(QWidget): + """AI对话面板""" + + # 信号定义 - 用于线程安全的UI更新 + update_chat_display = pyqtSignal(str) # 更新聊天显示信号 + + def __init__(self, parent=None): + super().__init__(parent) + self.api_key = "" + self.conversation_history = [] + self.current_streaming_content = "" + self.is_streaming = False + self.streaming_thread = None + self.streaming_timer = None + + # 加载API密钥 + self.load_api_key() + + self.init_ui() + + # 连接信号到槽 + self.update_chat_display.connect(self.on_update_chat_display) + + def load_api_key(self): + """从本地文件加载API密钥""" + config_file = os.path.join( + os.path.dirname(__file__), + "..", "..", "resources", "config", "deepseek_api.json" + ) + + try: + if os.path.exists(config_file): + with open(config_file, 'r', encoding='utf-8') as f: + config = json.load(f) + self.api_key = config.get('api_key', '') + except Exception as e: + print(f"加载API密钥失败: {e}") + + def init_ui(self): + """初始化UI""" + main_layout = QVBoxLayout() + main_layout.setContentsMargins(8, 8, 8, 8) + main_layout.setSpacing(8) + + # 标题栏 + header_layout = QHBoxLayout() + header_label = QLabel("AI 助手") + header_font = QFont() + header_font.setBold(True) + header_font.setPointSize(11) + header_label.setFont(header_font) + header_layout.addWidget(header_label) + header_layout.addStretch() + + # 清空历史按钮 + clear_btn = QPushButton("清空") + clear_btn.setMaximumWidth(50) + clear_btn.setFont(QFont("微软雅黑", 9)) + clear_btn.clicked.connect(self.clear_history) + header_layout.addWidget(clear_btn) + + main_layout.addLayout(header_layout) + + # 分割线 + line = QFrame() + line.setFrameShape(QFrame.HLine) + line.setStyleSheet("color: #d0d0d0;") + main_layout.addWidget(line) + + # 对话显示区域 + self.chat_display = QTextEdit() + self.chat_display.setReadOnly(True) + self.chat_display.setStyleSheet(""" + QTextEdit { + background-color: #ffffff; + border: 1px solid #d0d0d0; + border-radius: 4px; + font-family: 'Segoe UI', '微软雅黑', sans-serif; + font-size: 10pt; + padding: 8px; + color: #333333; + } + """) + self.chat_display.setMinimumHeight(400) + main_layout.addWidget(self.chat_display) + + # 输入区域 + input_layout = QVBoxLayout() + input_layout.setSpacing(4) + + # 输入框 + self.input_field = QLineEdit() + self.input_field.setPlaceholderText("输入您的问题或请求...") + self.input_field.setStyleSheet(""" + QLineEdit { + background-color: #f9f9f9; + border: 1px solid #d0d0d0; + border-radius: 4px; + font-family: '微软雅黑', sans-serif; + font-size: 10pt; + padding: 6px; + color: #333333; + } + QLineEdit:focus { + border: 1px solid #0078d4; + background-color: #ffffff; + } + """) + self.input_field.returnPressed.connect(self.send_user_message) + input_layout.addWidget(self.input_field) + + # 按钮区域 + button_layout = QHBoxLayout() + button_layout.setSpacing(4) + + # 发送按钮 + self.send_btn = QPushButton("发送") + self.send_btn.setFont(QFont("微软雅黑", 9)) + self.send_btn.setStyleSheet(""" + QPushButton { + background-color: #0078d4; + color: white; + border: none; + border-radius: 4px; + padding: 6px 16px; + font-weight: bold; + } + QPushButton:hover { + background-color: #0063b1; + } + QPushButton:pressed { + background-color: #005a9e; + } + """) + self.send_btn.clicked.connect(self.send_user_message) + button_layout.addStretch() + button_layout.addWidget(self.send_btn) + + input_layout.addLayout(button_layout) + main_layout.addLayout(input_layout) + + self.setLayout(main_layout) + + # 设置背景颜色 + self.setStyleSheet(""" + AIChatPanel { + background-color: #f5f5f5; + border-left: 1px solid #d0d0d0; + } + """) + + def send_user_message(self): + """发送用户消息""" + if not self.api_key: + QMessageBox.warning(self, "警告", "请先配置DeepSeek API密钥") + return + + message = self.input_field.text().strip() + if not message: + return + + # 清空输入框 + self.input_field.clear() + + # 禁用发送按钮 + self.send_btn.setEnabled(False) + self.input_field.setEnabled(False) + + # 显示用户消息 + self.add_message_to_display("用户", message) + + # 添加用户消息到对话历史 + self.conversation_history.append({"sender": "用户", "message": message}) + + # 显示AI正在思考 + self.add_message_to_display("AI助手", "正在思考...") + self.conversation_history.append({"sender": "AI助手", "message": ""}) + + # 在新线程中调用API + self.streaming_thread = threading.Thread( + target=self.call_deepseek_api_stream, + args=(message,) + ) + self.streaming_thread.daemon = True + self.streaming_thread.start() + + # 启动定时器更新显示 + self.streaming_timer = QTimer() + self.streaming_timer.timeout.connect(self.update_streaming_display) + self.streaming_timer.start(100) # 每100毫秒更新一次显示 + + def add_message_to_display(self, sender, message): + """添加消息到显示区域""" + cursor = self.chat_display.textCursor() + cursor.movePosition(QTextCursor.End) + self.chat_display.setTextCursor(cursor) + + # 设置格式 + char_format = QTextCharFormat() + char_format.setFont(QFont("微软雅黑", 10)) + + if sender == "用户": + char_format.setForeground(QColor("#0078d4")) + char_format.setFontWeight(70) + prefix = "你: " + else: # AI助手 + char_format.setForeground(QColor("#333333")) + prefix = "AI: " + + # 插入时间戳 + timestamp_format = QTextCharFormat() + timestamp_format.setForeground(QColor("#999999")) + timestamp_format.setFont(QFont("微软雅黑", 8)) + cursor.insertText(f"\n[{datetime.now().strftime('%H:%M:%S')}] ", timestamp_format) + + # 插入前缀 + cursor.insertText(prefix, char_format) + + # 插入消息 + cursor.insertText(message, char_format) + + # 自动滚动到底部 + self.chat_display.verticalScrollBar().setValue( + self.chat_display.verticalScrollBar().maximum() + ) + + def update_streaming_display(self): + """更新流式显示""" + if self.is_streaming and self.current_streaming_content: + # 重新显示所有对话 + self.rebuild_chat_display() + self.chat_display.verticalScrollBar().setValue( + self.chat_display.verticalScrollBar().maximum() + ) + + def rebuild_chat_display(self): + """重新构建聊天显示""" + self.chat_display.clear() + cursor = self.chat_display.textCursor() + + for msg in self.conversation_history: + sender = msg["sender"] + message = msg["message"] + + # 设置格式 + char_format = QTextCharFormat() + char_format.setFont(QFont("微软雅黑", 10)) + + if sender == "用户": + char_format.setForeground(QColor("#0078d4")) + char_format.setFontWeight(70) + prefix = "你: " + else: + char_format.setForeground(QColor("#333333")) + prefix = "AI: " + + # 插入分隔符 + if self.chat_display.toPlainText(): + cursor.insertText("\n") + + # 插入时间戳 + timestamp_format = QTextCharFormat() + timestamp_format.setForeground(QColor("#999999")) + timestamp_format.setFont(QFont("微软雅黑", 8)) + cursor.insertText(f"[{datetime.now().strftime('%H:%M:%S')}] ", timestamp_format) + + # 插入前缀 + cursor.insertText(prefix, char_format) + + # 插入消息 + cursor.insertText(message, char_format) + + def call_deepseek_api_stream(self, message): + """调用DeepSeek API(流式版本)""" + url = "https://api.deepseek.com/v1/chat/completions" + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.api_key}" + } + + messages = [{"role": "user", "content": message}] + + data = { + "model": "deepseek-chat", + "messages": messages, + "stream": True, + "temperature": 0.7, + "max_tokens": 2000 + } + + self.is_streaming = True + self.current_streaming_content = "" + + try: + response = requests.post(url, headers=headers, json=data, stream=True, timeout=30) + + if response.status_code == 200: + for line in response.iter_lines(): + if line: + line = line.decode('utf-8') + if line.startswith('data: '): + data_str = line[6:] + if data_str == '[DONE]': + break + + try: + data_obj = json.loads(data_str) + if 'choices' in data_obj and len(data_obj['choices']) > 0: + delta = data_obj['choices'][0].get('delta', {}) + if 'content' in delta: + content = delta['content'] + self.current_streaming_content += content + except json.JSONDecodeError: + pass + else: + error_msg = f"API调用失败: {response.status_code}" + self.current_streaming_content = error_msg + + except requests.exceptions.Timeout: + self.current_streaming_content = "请求超时,请重试" + except Exception as e: + self.current_streaming_content = f"错误: {str(e)}" + + finally: + self.is_streaming = False + # 停止定时器 + if self.streaming_timer: + self.streaming_timer.stop() + + # 最后更新一次显示,使用信号在主线程中进行 + self.update_chat_display.emit(self.current_streaming_content) + + @pyqtSlot(str) + def on_update_chat_display(self, content): + """在主线程中更新聊天显示""" + # 更新最后一条AI消息 + if len(self.conversation_history) > 0: + self.conversation_history[-1]["message"] = content + + # 重新构建显示 + self.rebuild_chat_display() + + # 自动滚动到底部 + self.chat_display.verticalScrollBar().setValue( + self.chat_display.verticalScrollBar().maximum() + ) + + # 重新启用输入 + self.send_btn.setEnabled(True) + self.input_field.setEnabled(True) + self.input_field.setFocus() + + def clear_history(self): + """清空聊天历史""" + reply = QMessageBox.question( + self, + "确认", + "确定要清空聊天历史吗?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes: + self.conversation_history = [] + self.chat_display.clear() + self.input_field.clear() diff --git a/src/word_main_window.py b/src/word_main_window.py index bdf3bbc..819f300 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -22,6 +22,7 @@ from ui.calendar_widget import CalendarWidget from ui.weather_floating_widget import WeatherFloatingWidget from ui.quote_floating_widget import QuoteFloatingWidget from ui.calendar_floating_widget import CalendarFloatingWidget +from ui.ai_chat_panel import AIChatPanel # 导入主题管理器 from ui.theme_manager import theme_manager @@ -828,8 +829,12 @@ class WordStyleMainWindow(QMainWindow): def create_document_area(self, main_layout): - """创建文档编辑区域""" + """创建文档编辑区域和AI对话面板""" + # 创建水平分割器 + splitter = QSplitter(Qt.Horizontal) + + # ========== 左侧:文档编辑区域 ========== # 创建滚动区域 from PyQt5.QtWidgets import QScrollArea @@ -906,7 +911,22 @@ class WordStyleMainWindow(QMainWindow): document_container.setLayout(document_layout) self.scroll_area.setWidget(document_container) - main_layout.addWidget(self.scroll_area) + + # ========== 右侧:AI对话面板 ========== + self.ai_chat_panel = AIChatPanel() + self.ai_chat_panel.setMinimumWidth(320) + + # 添加左右两部分到分割器 + splitter.addWidget(self.scroll_area) + splitter.addWidget(self.ai_chat_panel) + + # 设置分割器大小比例(文档区:对话区 = 70:30) + splitter.setSizes([700, 300]) + splitter.setStretchFactor(0, 2) # 文档区可伸缩 + splitter.setStretchFactor(1, 1) # 对话区可伸缩 + + # 添加分割器到主布局 + main_layout.addWidget(splitter) def init_network_services(self): """初始化网络服务""" @@ -920,6 +940,7 @@ class WordStyleMainWindow(QMainWindow): self.quote_thread.quote_fetched.connect(self.update_quote_display) self.quote_thread.start() + def init_typing_logic(self): """初始化打字逻辑""" # 使用默认内容初始化打字逻辑