""" 聊天视图组件 处理普通对话的 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('', 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