- Updated .env.example to include API key placeholder and configuration instructions. - Refactored main.py to support streaming responses from the LLM, improving user experience during chat interactions. - Enhanced LLMClient to include methods for streaming chat and collecting responses. - Modified safety review process to pass static analysis warnings to the LLM for better code safety evaluation. - Improved UI components in chat_view.py to handle streaming messages effectively.
220 lines
6.6 KiB
Python
220 lines
6.6 KiB
Python
"""
|
|
聊天视图组件
|
|
处理普通对话的 UI 展示 - 支持流式消息
|
|
"""
|
|
|
|
import tkinter as tk
|
|
from tkinter import scrolledtext
|
|
from typing import Callable, Optional
|
|
|
|
|
|
class ChatView:
|
|
"""
|
|
聊天视图
|
|
|
|
包含:
|
|
- 消息显示区域
|
|
- 输入框
|
|
- 发送按钮
|
|
- 流式消息支持
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
parent: tk.Widget,
|
|
on_send: Callable[[str], None]
|
|
):
|
|
"""
|
|
初始化聊天视图
|
|
|
|
Args:
|
|
parent: 父容器
|
|
on_send: 发送消息回调函数
|
|
"""
|
|
self.parent = parent
|
|
self.on_send = on_send
|
|
|
|
# 流式消息状态
|
|
self._stream_active = False
|
|
self._stream_tag = None
|
|
|
|
self._create_widgets()
|
|
|
|
def _create_widgets(self):
|
|
"""创建 UI 组件"""
|
|
# 主框架
|
|
self.frame = tk.Frame(self.parent, bg='#1e1e1e')
|
|
self.frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
|
|
# 标题
|
|
title_label = tk.Label(
|
|
self.frame,
|
|
text="LocalAgent - 本地 AI 助手",
|
|
font=('Microsoft YaHei UI', 16, 'bold'),
|
|
fg='#61dafb',
|
|
bg='#1e1e1e'
|
|
)
|
|
title_label.pack(pady=(0, 10))
|
|
|
|
# 消息显示区域
|
|
self.message_area = scrolledtext.ScrolledText(
|
|
self.frame,
|
|
wrap=tk.WORD,
|
|
font=('Microsoft YaHei UI', 11),
|
|
bg='#2d2d2d',
|
|
fg='#d4d4d4',
|
|
insertbackground='white',
|
|
relief=tk.FLAT,
|
|
padx=10,
|
|
pady=10,
|
|
state=tk.DISABLED
|
|
)
|
|
self.message_area.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
|
|
|
# 配置消息标签样式
|
|
self.message_area.tag_configure('user', foreground='#4fc3f7', font=('Microsoft YaHei UI', 11, 'bold'))
|
|
self.message_area.tag_configure('assistant', foreground='#81c784', font=('Microsoft YaHei UI', 11))
|
|
self.message_area.tag_configure('system', foreground='#ffb74d', font=('Microsoft YaHei UI', 10, 'italic'))
|
|
self.message_area.tag_configure('error', foreground='#ef5350', font=('Microsoft YaHei UI', 10))
|
|
self.message_area.tag_configure('streaming', foreground='#81c784', font=('Microsoft YaHei UI', 11))
|
|
|
|
# 输入区域框架
|
|
input_frame = tk.Frame(self.frame, bg='#1e1e1e')
|
|
input_frame.pack(fill=tk.X)
|
|
|
|
# 输入框
|
|
self.input_entry = tk.Entry(
|
|
input_frame,
|
|
font=('Microsoft YaHei UI', 12),
|
|
bg='#3c3c3c',
|
|
fg='#ffffff',
|
|
insertbackground='white',
|
|
relief=tk.FLAT
|
|
)
|
|
self.input_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=8, padx=(0, 10))
|
|
self.input_entry.bind('<Return>', self._on_enter_pressed)
|
|
|
|
# 发送按钮
|
|
self.send_button = tk.Button(
|
|
input_frame,
|
|
text="发送",
|
|
font=('Microsoft YaHei UI', 11, 'bold'),
|
|
bg='#0078d4',
|
|
fg='white',
|
|
activebackground='#106ebe',
|
|
activeforeground='white',
|
|
relief=tk.FLAT,
|
|
padx=20,
|
|
pady=5,
|
|
cursor='hand2',
|
|
command=self._on_send_clicked
|
|
)
|
|
self.send_button.pack(side=tk.RIGHT)
|
|
|
|
# 显示欢迎消息
|
|
welcome_msg = (
|
|
"欢迎使用 LocalAgent!\n"
|
|
"- 输入问题进行对话\n"
|
|
"- 输入文件处理需求(如\"复制文件\"、\"整理图片\")将触发执行模式"
|
|
)
|
|
self.add_message(welcome_msg, 'system')
|
|
|
|
def _on_enter_pressed(self, event):
|
|
"""回车键处理"""
|
|
self._on_send_clicked()
|
|
|
|
def _on_send_clicked(self):
|
|
"""发送按钮点击处理"""
|
|
text = self.input_entry.get().strip()
|
|
if text:
|
|
self.input_entry.delete(0, tk.END)
|
|
self.on_send(text)
|
|
|
|
def add_message(self, message: str, tag: str = 'assistant'):
|
|
"""
|
|
添加消息到显示区域
|
|
|
|
Args:
|
|
message: 消息内容
|
|
tag: 消息类型 (user/assistant/system/error)
|
|
"""
|
|
self.message_area.config(state=tk.NORMAL)
|
|
|
|
# 添加前缀
|
|
prefix_map = {
|
|
'user': '[你] ',
|
|
'assistant': '[助手] ',
|
|
'system': '[系统] ',
|
|
'error': '[错误] '
|
|
}
|
|
prefix = prefix_map.get(tag, '')
|
|
|
|
self.message_area.insert(tk.END, "\n" + prefix + message + "\n", tag)
|
|
self.message_area.see(tk.END)
|
|
self.message_area.config(state=tk.DISABLED)
|
|
|
|
def start_stream_message(self, tag: str = 'assistant'):
|
|
"""
|
|
开始流式消息
|
|
|
|
Args:
|
|
tag: 消息类型
|
|
"""
|
|
self._stream_active = True
|
|
self._stream_tag = tag
|
|
|
|
self.message_area.config(state=tk.NORMAL)
|
|
|
|
# 添加前缀
|
|
prefix_map = {
|
|
'user': '[你] ',
|
|
'assistant': '[助手] ',
|
|
'system': '[系统] ',
|
|
'error': '[错误] '
|
|
}
|
|
prefix = prefix_map.get(tag, '')
|
|
|
|
self.message_area.insert(tk.END, "\n" + prefix, tag)
|
|
self.message_area.see(tk.END)
|
|
# 保持 NORMAL 状态以便追加内容
|
|
|
|
def append_stream_chunk(self, chunk: str):
|
|
"""
|
|
追加流式消息片段
|
|
|
|
Args:
|
|
chunk: 消息片段
|
|
"""
|
|
if not self._stream_active:
|
|
return
|
|
|
|
self.message_area.insert(tk.END, chunk, self._stream_tag)
|
|
self.message_area.see(tk.END)
|
|
# 强制更新 UI
|
|
self.message_area.update_idletasks()
|
|
|
|
def end_stream_message(self):
|
|
"""结束流式消息"""
|
|
if self._stream_active:
|
|
self.message_area.insert(tk.END, "\n")
|
|
self.message_area.see(tk.END)
|
|
self.message_area.config(state=tk.DISABLED)
|
|
self._stream_active = False
|
|
self._stream_tag = None
|
|
|
|
def clear_messages(self):
|
|
"""清空消息区域"""
|
|
self.message_area.config(state=tk.NORMAL)
|
|
self.message_area.delete(1.0, tk.END)
|
|
self.message_area.config(state=tk.DISABLED)
|
|
|
|
def set_input_enabled(self, enabled: bool):
|
|
"""设置输入区域是否可用"""
|
|
state = tk.NORMAL if enabled else tk.DISABLED
|
|
self.input_entry.config(state=state)
|
|
self.send_button.config(state=state)
|
|
|
|
def get_frame(self) -> tk.Frame:
|
|
"""获取主框架"""
|
|
return self.frame
|