添加AI对话面板组件并更新文档编辑区域布局

pull/117/head
Maziang 3 months ago
parent 75dbeb24e9
commit 60be0fda50

@ -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()

@ -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):
"""初始化打字逻辑"""
# 使用默认内容初始化打字逻辑

Loading…
Cancel
Save