Files
LocalAgent/ui/chat_view.py
Mimikko-zeus 0a92355bfb feat: enhance LocalAgent configuration and UI components
- Updated .env.example to provide clearer configuration instructions and API key setup.
- Removed debug_env.py as it was no longer needed.
- Refactored main.py to streamline application initialization and workspace setup.
- Introduced a new HistoryManager for managing task execution history.
- Enhanced UI components in chat_view.py and task_guide_view.py to improve user interaction and code preview functionality.
- Added loading indicators and improved task history display in the UI.
- Implemented unit tests for history management and intent classification.
2026-01-07 10:29:13 +08:00

318 lines
9.6 KiB
Python

"""
聊天视图组件
处理普通对话的 UI 展示 - 支持流式消息和加载动画
"""
import tkinter as tk
from tkinter import scrolledtext
from typing import Callable, Optional
class LoadingIndicator:
"""加载动画指示器"""
FRAMES = ["", "", "", "", "", "", "", "", "", ""]
def __init__(self, parent: tk.Widget, text: str = "处理中"):
self.parent = parent
self.text = text
self.frame_index = 0
self.running = False
self.after_id = None
# 创建标签
self.label = tk.Label(
parent,
text="",
font=('Microsoft YaHei UI', 10),
fg='#ffd54f',
bg='#1e1e1e'
)
def start(self, text: str = None):
"""开始动画"""
if text:
self.text = text
self.running = True
self.label.pack(pady=5)
self._animate()
def stop(self):
"""停止动画"""
self.running = False
if self.after_id:
self.parent.after_cancel(self.after_id)
self.after_id = None
self.label.pack_forget()
def update_text(self, text: str):
"""更新提示文字"""
self.text = text
def _animate(self):
"""动画帧更新"""
if not self.running:
return
frame = self.FRAMES[self.frame_index]
self.label.config(text=f"{frame} {self.text}...")
self.frame_index = (self.frame_index + 1) % len(self.FRAMES)
self.after_id = self.parent.after(100, self._animate)
class ChatView:
"""
聊天视图
包含:
- 消息显示区域
- 输入框
- 发送按钮
- 流式消息支持
"""
def __init__(
self,
parent: tk.Widget,
on_send: Callable[[str], None],
on_show_history: Optional[Callable[[], None]] = None
):
"""
初始化聊天视图
Args:
parent: 父容器
on_send: 发送消息回调函数
on_show_history: 显示历史记录回调函数
"""
self.parent = parent
self.on_send = on_send
self.on_show_history = on_show_history
# 流式消息状态
self._stream_active = False
self._stream_tag = None
# 加载指示器
self.loading: Optional[LoadingIndicator] = 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_frame = tk.Frame(self.frame, bg='#1e1e1e')
title_frame.pack(fill=tk.X, pady=(0, 10))
# 标题
title_label = tk.Label(
title_frame,
text="LocalAgent - 本地 AI 助手",
font=('Microsoft YaHei UI', 16, 'bold'),
fg='#61dafb',
bg='#1e1e1e'
)
title_label.pack(side=tk.LEFT, expand=True)
# 历史记录按钮
if self.on_show_history:
self.history_btn = tk.Button(
title_frame,
text="📜 历史",
font=('Microsoft YaHei UI', 10),
bg='#424242',
fg='#ce93d8',
activebackground='#616161',
activeforeground='#ce93d8',
relief=tk.FLAT,
padx=10,
pady=3,
cursor='hand2',
command=self.on_show_history
)
self.history_btn.pack(side=tk.RIGHT)
# 消息显示区域
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')
# 创建加载指示器(放在消息区域下方)
self.loading = LoadingIndicator(self.frame)
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 show_loading(self, text: str = "处理中"):
"""显示加载动画"""
if self.loading:
self.loading.start(text)
def hide_loading(self):
"""隐藏加载动画"""
if self.loading:
self.loading.stop()
def update_loading_text(self, text: str):
"""更新加载提示文字"""
if self.loading:
self.loading.update_text(text)
def get_frame(self) -> tk.Frame:
"""获取主框架"""
return self.frame