-
Notifications
You must be signed in to change notification settings - Fork 4
/
chat_window.py
430 lines (351 loc) · 16.3 KB
/
chat_window.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
from PySide6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QTextEdit,
QPushButton, QHBoxLayout, QCheckBox, QLabel, QListWidget, QListWidgetItem, QSizePolicy, QSizeGrip)
from PySide6.QtCore import Qt, QEvent,QTimer, QSize
from PySide6.QtGui import QTextOption, QIcon
import asyncio
class MessageItem(QWidget):
def __init__(self, role: str, content: str):
super().__init__()
self.role = role
self.content = content
self.label = None
self.init_ui()
def init_ui(self):
layout = QHBoxLayout()
layout.setContentsMargins(5, 2, 5, 2)
layout.setSpacing(5)
# 使用 QTextEdit 替代 QLabel
self.label = QTextEdit()
self.label.setReadOnly(True)
self.label.setText(self.content)
# 设置自动换行
self.label.setWordWrapMode(QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere)
# 隐藏滚动条
self.label.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.label.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# 修改宽度计算逻辑,使其基于父窗口宽度
parent_width = self.parent().width() if self.parent() else 550
max_width = int(parent_width * 0.85) # 使用父窗口宽度的85%
content_length = max(len(line) for line in self.content.split('\n'))
width = min(max_width, max(200, content_length * 20)) # 增加每个字符的宽度
self.label.setFixedWidth(width)
# 计算实际需要的高度
document = self.label.document()
document.setTextWidth(width - 20) # 考虑padding
# 使用documentLayout来获取更准确的文本高度
text_height = document.documentLayout().documentSize().height()
padding = 20 # 上下各10px的padding
total_height = int(text_height + padding)
# 设置最小高度
total_height = max(40, total_height) # 确保至少40px高
self.label.setFixedHeight(total_height)
if self.role == 'user':
layout.addStretch()
layout.addWidget(self.label)
self.label.setStyleSheet("""
QTextEdit {
background-color: #DCF8C6;
padding: 10px;
border-radius: 10px;
border: none;
font-size: 13px;
color: #000000;
selection-background-color: #B5E2A8;
line-height: 1.5;
}
""")
else:
layout.addWidget(self.label)
layout.addStretch()
self.label.setStyleSheet("""
QTextEdit {
background-color: #E3F2FD;
padding: 10px;
border-radius: 10px;
border: none;
font-size: 13px;
color: #000000;
selection-background-color: #BBDEFB;
line-height: 1.5;
}
""")
self.setLayout(layout)
# 为整个widget设置合适的高度,添加额外空间以防止文本被截断
self.setFixedHeight(total_height + 10)
def update_content(self, content):
"""更新消息内容"""
self.content = content
self.label.setText(content)
width = self.label.width()
document = self.label.document()
document.setTextWidth(width - 20)
# 使用documentLayout获取准确的文本高度
text_height = document.documentLayout().documentSize().height()
padding = 20
total_height = int(text_height + padding)
total_height = max(40, total_height) # 确保最小高度
self.label.setFixedHeight(total_height)
self.setFixedHeight(total_height + 10)
def resizeEvent(self, event):
"""处理大小调整事件"""
super().resizeEvent(event)
# 重新计算宽度和高度
parent_width = self.parent().width() if self.parent() else 550
max_width = int(parent_width * 0.85)
content_length = max(len(line) for line in self.content.split('\n'))
width = min(max_width, max(200, content_length * 20))
self.label.setFixedWidth(width)
# 重新计算高度
document = self.label.document()
document.setTextWidth(width - 20)
text_height = document.documentLayout().documentSize().height()
padding = 20
total_height = int(text_height + padding)
total_height = max(40, total_height)
self.label.setFixedHeight(total_height)
self.setFixedHeight(total_height + 10)
class ChatWindow(QMainWindow):
def __init__(self, ai_client):
super().__init__()
self.ai_client = ai_client
self.messages = [] # 存储对话历史
self.should_stop = False
# 设置窗口属性 - 移除标题栏并保持置顶
self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint | Qt.WindowMaximizeButtonHint)
self.setAttribute(Qt.WA_TranslucentBackground)
# 创建中心部件并设置对象名称
central_widget = QWidget()
central_widget.setObjectName("centralWidget")
self.setCentralWidget(central_widget)
self.setWindowIcon(QIcon(r"icons\logo.ico"))
# 从 styles.py 导入样式
from styles import CHAT_WINDOW_STYLE
self.setStyleSheet(CHAT_WINDOW_STYLE)
layout = QVBoxLayout(central_widget)
layout.setContentsMargins(5, 5, 5, 5) # 减小边距
layout.setSpacing(5) # 减小间距
# 创建顶部布局(包含关闭按钮)
top_layout = QHBoxLayout()
top_layout.setSpacing(0)
# 添加标题
title_label = QLabel("连续对话")
top_layout.addWidget(title_label)
# 添加弹性空间
top_layout.addStretch()
# 添加关闭按钮
close_button = QPushButton("×")
close_button.clicked.connect(self.hide)
top_layout.addWidget(close_button)
layout.addLayout(top_layout)
# 创建聊天历史显示区域(使用 QListWidget)
self.chat_history = QListWidget()
self.chat_history.setStyleSheet("""
QListWidget {
background-color: transparent;
border: none;
padding: 5px;
}
QListWidget::item {
padding: 0px;
margin: 2px 0px;
}
""")
self.chat_history.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.chat_history.setVerticalScrollMode(QListWidget.ScrollPerPixel)
self.chat_history.setSpacing(2) # 减小项目间距
layout.addWidget(self.chat_history)
# 创建输入框
self.input_text = QTextEdit()
self.input_text.setPlaceholderText("在此输入消息...")
self.input_text.setMaximumHeight(100)
self.input_text.installEventFilter(self)
layout.addWidget(self.input_text)
# 创建底部控制栏
bottom_layout = QHBoxLayout()
bottom_layout.setContentsMargins(10, 5, 10, 5) # 添加边距
bottom_layout.setSpacing(10) # 增加间距
# 添加过滤 markdown 的复选框
self.filter_markdown = QCheckBox("过滤 Markdown") # 缩短文本
self.filter_markdown.setFixedHeight(25) # 设置固定高度
bottom_layout.addWidget(self.filter_markdown)
# 添加流模式的复选框
self.stream_mode = QCheckBox("流模式") # 缩短文本
self.stream_mode.setChecked(True)
self.stream_mode.setFixedHeight(25) # 设置固定高度
bottom_layout.addWidget(self.stream_mode)
# 添加清空按钮
clear_button = QPushButton("清空") # 缩短文本
clear_button.setFixedHeight(25) # 设置固定高度
clear_button.clicked.connect(self.clear_chat)
bottom_layout.addWidget(clear_button)
# 添加发送按钮
self.send_button = QPushButton("发送")
self.send_button.setFixedHeight(25) # 设置固定高度
self.send_button.clicked.connect(
lambda: asyncio.get_event_loop().create_task(self.send_message())
)
bottom_layout.addWidget(self.send_button)
# 设置底部布局的固定高度
bottom_widget = QWidget()
bottom_widget.setLayout(bottom_layout)
bottom_widget.setFixedHeight(35) # 设置固定高度
layout.addWidget(bottom_widget)
# 创建一个大小调整手柄
self.size_grip = QSizeGrip(self)
self.size_grip.setFixedSize(20, 20)
# 设置初始位置
self.updateSizeGripPos()
# 设置窗口大小
self.resize(600, 550)
# 允许窗口调整大小
self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint | Qt.WindowMaximizeButtonHint)
# 设置最小窗口大小
self.setMinimumSize(400, 300)
def updateSizeGripPos(self):
"""更新大小调整手柄的位置"""
self.size_grip.move(
self.width() - self.size_grip.width(),
self.height() - self.size_grip.height()
)
def resizeEvent(self, event):
"""处理窗口大小调整事件"""
super().resizeEvent(event)
# 更新大小调整手柄位置
self.updateSizeGripPos()
# 遍历所有消息项并更新它们的大小
for i in range(self.chat_history.count()):
item = self.chat_history.item(i)
widget = self.chat_history.itemWidget(item)
if widget:
widget.resizeEvent(event)
item.setSizeHint(widget.sizeHint())
def append_message(self, role: str, content: str):
"""添加消息到聊天历史,使用自定义的 MessageItem"""
self.messages.append({"role": role, "content": content})
message_item = MessageItem(role, content)
list_item = QListWidgetItem()
# 获取消息项的实际大小
size = message_item.sizeHint()
# 确保高度足够
size.setHeight(message_item.height()+15) # 添加一点间距
list_item.setSizeHint(size)
self.chat_history.addItem(list_item)
self.chat_history.setItemWidget(list_item, message_item)
# 滚动到底部
self.chat_history.scrollToBottom()
# 延迟再次滚动,确保完全显示
QTimer.singleShot(100, self.chat_history.scrollToBottom)
def mousePressEvent(self, event):
# 添加右下角调整大小的功能
if event.position().x() > self.width() - 20 and event.position().y() > self.height() - 20:
self.resizing = True
self.resize_start_pos = event.position().toPoint()
self.resize_start_size = self.size()
else:
self.resizing = False
if event.button() == Qt.LeftButton:
self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
event.accept()
def mouseMoveEvent(self, event):
if self.resizing:
# 处理窗口大小调整
delta = event.position().toPoint() - self.resize_start_pos
new_size = self.resize_start_size + QSize(delta.x(), delta.y())
self.resize(new_size)
elif event.buttons() == Qt.LeftButton:
# 处理窗口拖动
self.move(event.globalPosition().toPoint() - self.drag_position)
event.accept()
def mouseReleaseEvent(self, event):
self.resizing = False
event.accept()
def eventFilter(self, obj, event):
if obj is self.input_text and event.type() == QEvent.Type.KeyPress:
if event.key() == Qt.Key_Return and event.modifiers() == Qt.NoModifier:
asyncio.get_event_loop().create_task(self.send_message())
return True
elif event.key() == Qt.Key_Return and event.modifiers() == Qt.ShiftModifier:
cursor = self.input_text.textCursor()
cursor.insertText('\n')
return True
return super().eventFilter(obj, event)
async def send_message(self):
"""发送消息并获取回复"""
user_input = self.input_text.toPlainText().strip()
if not user_input:
return
# 保存完整的用户输入
full_input = user_input
# 清空输入框
self.input_text.clear()
# 添加用户消息
self.append_message("user", full_input)
try:
messages = [{"role": "system", "content": "请直接回答问题,不要使用 markdown 格式。"}]
messages.extend(self.messages)
if self.stream_mode.isChecked():
response_text = ""
message_widget = None
first_chunk = True
async for text in self.ai_client.get_response_stream(full_input, stream=True, messages=messages):
if self.should_stop:
break
if self.filter_markdown.isChecked():
from utils import remove_markdown
text = remove_markdown(text)
response_text += text
if first_chunk:
self.append_message("assistant", response_text)
last_item = self.chat_history.item(self.chat_history.count() - 1)
message_widget = self.chat_history.itemWidget(last_item)
first_chunk = False
else:
message_widget.update_content(response_text)
self.chat_history.scrollToBottom()
await asyncio.sleep(0.01)
# 流式输出结束后,重新计算并更新消息高度
if message_widget:
# 重新计算高度
width = message_widget.label.width()
document = message_widget.label.document()
document.setTextWidth(width - 20)
# 获取最终的文本高度
layout = document.documentLayout()
size = layout.documentSize()
# 正确触发 documentSizeChanged 信号
layout.documentSizeChanged.emit(size)
# 获取最终的文本高度
text_height = size.height()
padding = 20
total_height = int(text_height + padding)
total_height = max(40, total_height)
# 更新高度
message_widget.label.setFixedHeight(total_height)
message_widget.setFixedHeight(total_height + 10)
# 更新 QListWidgetItem 的大小
last_item.setSizeHint(message_widget.sizeHint())
# 确保滚动到底部
self.chat_history.scrollToBottom()
# 更新消息历史
if len(self.messages) > 0 and self.messages[-1]['role'] == 'assistant':
self.messages[-1]['content'] = response_text
else:
self.messages.append({"role": "assistant", "content": response_text})
else:
response = await self.ai_client.get_response(full_input, messages=messages)
if self.filter_markdown.isChecked():
from utils import remove_markdown
response = remove_markdown(response)
self.append_message("assistant", response)
self.messages.append({"role": "assistant", "content": response})
except Exception as e:
error_msg = f"错误: {str(e)}"
self.append_message("assistant", error_msg)
def clear_chat(self):
"""清空聊天历史"""
self.messages.clear()
self.chat_history.clear()
def closeEvent(self, event):
"""重写关闭事件,使窗口关闭时只隐藏而不退出程序"""
event.ignore() # 忽略原始的关闭事件
self.hide() # 只隐藏窗口