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.
This commit is contained in:
Mimikko-zeus
2026-01-07 10:29:13 +08:00
parent 1ba5f0f7d6
commit 0a92355bfb
18 changed files with 2144 additions and 557 deletions

335
ui/history_view.py Normal file
View File

@@ -0,0 +1,335 @@
"""
历史记录视图组件
显示任务执行历史
"""
import tkinter as tk
from tkinter import ttk, messagebox
from typing import Callable, List, Optional
from pathlib import Path
from history.manager import TaskRecord, HistoryManager
class HistoryView:
"""
历史记录视图
显示任务执行历史列表,支持查看详情
"""
def __init__(
self,
parent: tk.Widget,
history_manager: HistoryManager,
on_back: Callable[[], None]
):
self.parent = parent
self.history = history_manager
self.on_back = on_back
self._selected_record: Optional[TaskRecord] = None
self._create_widgets()
def _create_widgets(self):
"""创建 UI 组件"""
self.frame = tk.Frame(self.parent, bg='#1e1e1e')
# 标题栏
title_frame = tk.Frame(self.frame, bg='#1e1e1e')
title_frame.pack(fill=tk.X, padx=10, pady=10)
# 返回按钮
back_btn = tk.Button(
title_frame,
text="← 返回",
font=('Microsoft YaHei UI', 10),
bg='#424242',
fg='white',
activebackground='#616161',
activeforeground='white',
relief=tk.FLAT,
padx=10,
cursor='hand2',
command=self.on_back
)
back_btn.pack(side=tk.LEFT)
# 标题
title_label = tk.Label(
title_frame,
text="📜 任务历史记录",
font=('Microsoft YaHei UI', 14, 'bold'),
fg='#ce93d8',
bg='#1e1e1e'
)
title_label.pack(side=tk.LEFT, padx=20)
# 统计信息
stats = self.history.get_stats()
stats_text = f"{stats['total']} 条 | 成功 {stats['success']} | 失败 {stats['failed']} | 成功率 {stats['success_rate']:.0%}"
stats_label = tk.Label(
title_frame,
text=stats_text,
font=('Microsoft YaHei UI', 9),
fg='#888888',
bg='#1e1e1e'
)
stats_label.pack(side=tk.RIGHT)
# 主内容区域(左右分栏)
content_frame = tk.Frame(self.frame, bg='#1e1e1e')
content_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
# 左侧:历史列表
list_frame = tk.LabelFrame(
content_frame,
text=" 任务列表 ",
font=('Microsoft YaHei UI', 10, 'bold'),
fg='#4fc3f7',
bg='#1e1e1e',
relief=tk.GROOVE
)
list_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
# 列表框
list_container = tk.Frame(list_frame, bg='#2d2d2d')
list_container.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
# 使用 Treeview 显示列表
columns = ('time', 'input', 'status', 'duration')
self.tree = ttk.Treeview(list_container, columns=columns, show='headings', height=15)
# 配置列
self.tree.heading('time', text='时间')
self.tree.heading('input', text='任务描述')
self.tree.heading('status', text='状态')
self.tree.heading('duration', text='耗时')
self.tree.column('time', width=120, minwidth=100)
self.tree.column('input', width=250, minwidth=150)
self.tree.column('status', width=60, minwidth=50)
self.tree.column('duration', width=70, minwidth=50)
# 滚动条
scrollbar = ttk.Scrollbar(list_container, orient=tk.VERTICAL, command=self.tree.yview)
self.tree.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 绑定选择事件
self.tree.bind('<<TreeviewSelect>>', self._on_select)
# 右侧:详情面板
detail_frame = tk.LabelFrame(
content_frame,
text=" 任务详情 ",
font=('Microsoft YaHei UI', 10, 'bold'),
fg='#81c784',
bg='#1e1e1e',
relief=tk.GROOVE
)
detail_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
# 详情文本框
detail_container = tk.Frame(detail_frame, bg='#2d2d2d')
detail_container.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
self.detail_text = tk.Text(
detail_container,
wrap=tk.WORD,
font=('Microsoft YaHei UI', 10),
bg='#2d2d2d',
fg='#d4d4d4',
relief=tk.FLAT,
padx=10,
pady=10,
state=tk.DISABLED
)
detail_scrollbar = ttk.Scrollbar(detail_container, orient=tk.VERTICAL, command=self.detail_text.yview)
self.detail_text.configure(yscrollcommand=detail_scrollbar.set)
detail_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.detail_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 配置详情文本样式
self.detail_text.tag_configure('title', font=('Microsoft YaHei UI', 11, 'bold'), foreground='#ffd54f')
self.detail_text.tag_configure('label', font=('Microsoft YaHei UI', 10, 'bold'), foreground='#4fc3f7')
self.detail_text.tag_configure('success', foreground='#81c784')
self.detail_text.tag_configure('error', foreground='#ef5350')
self.detail_text.tag_configure('code', font=('Consolas', 9), foreground='#ce93d8')
# 底部按钮
btn_frame = tk.Frame(self.frame, bg='#1e1e1e')
btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
# 打开日志按钮
self.open_log_btn = tk.Button(
btn_frame,
text="📄 打开日志",
font=('Microsoft YaHei UI', 10),
bg='#424242',
fg='white',
activebackground='#616161',
activeforeground='white',
relief=tk.FLAT,
padx=15,
cursor='hand2',
state=tk.DISABLED,
command=self._open_log
)
self.open_log_btn.pack(side=tk.LEFT)
# 清空历史按钮
clear_btn = tk.Button(
btn_frame,
text="🗑️ 清空历史",
font=('Microsoft YaHei UI', 10),
bg='#d32f2f',
fg='white',
activebackground='#f44336',
activeforeground='white',
relief=tk.FLAT,
padx=15,
cursor='hand2',
command=self._clear_history
)
clear_btn.pack(side=tk.RIGHT)
# 加载数据
self._load_data()
def _load_data(self):
"""加载历史数据到列表"""
# 清空现有数据
for item in self.tree.get_children():
self.tree.delete(item)
# 加载历史记录
records = self.history.get_all()
for record in records:
# 截断过长的输入
input_text = record.user_input
if len(input_text) > 30:
input_text = input_text[:30] + "..."
status = "✓ 成功" if record.success else "✗ 失败"
duration = f"{record.duration_ms}ms"
# 提取时间(只显示时分秒)
time_parts = record.timestamp.split(' ')
time_str = time_parts[1] if len(time_parts) > 1 else record.timestamp
date_str = time_parts[0] if len(time_parts) > 0 else ""
display_time = f"{date_str}\n{time_str}"
self.tree.insert('', tk.END, iid=record.task_id, values=(
record.timestamp,
input_text,
status,
duration
))
# 显示空状态提示
if not records:
self._show_detail("暂无历史记录\n\n执行任务后,记录将显示在这里。")
def _on_select(self, event):
"""选择记录事件"""
selection = self.tree.selection()
if not selection:
return
task_id = selection[0]
record = self.history.get_by_id(task_id)
if record:
self._selected_record = record
self._show_record_detail(record)
self.open_log_btn.config(state=tk.NORMAL)
def _show_record_detail(self, record: TaskRecord):
"""显示记录详情"""
self.detail_text.config(state=tk.NORMAL)
self.detail_text.delete(1.0, tk.END)
# 标题
self.detail_text.insert(tk.END, f"任务 ID: {record.task_id}\n", 'title')
self.detail_text.insert(tk.END, f"时间: {record.timestamp}\n\n")
# 用户输入
self.detail_text.insert(tk.END, "用户输入:\n", 'label')
self.detail_text.insert(tk.END, f"{record.user_input}\n\n")
# 执行状态
self.detail_text.insert(tk.END, "执行状态: ", 'label')
if record.success:
self.detail_text.insert(tk.END, "成功 ✓\n", 'success')
else:
self.detail_text.insert(tk.END, "失败 ✗\n", 'error')
self.detail_text.insert(tk.END, f"耗时: {record.duration_ms}ms\n\n")
# 执行计划
self.detail_text.insert(tk.END, "执行计划:\n", 'label')
plan_preview = record.execution_plan[:500] + "..." if len(record.execution_plan) > 500 else record.execution_plan
self.detail_text.insert(tk.END, f"{plan_preview}\n\n")
# 输出
if record.stdout:
self.detail_text.insert(tk.END, "输出:\n", 'label')
self.detail_text.insert(tk.END, f"{record.stdout}\n\n")
# 错误
if record.stderr:
self.detail_text.insert(tk.END, "错误:\n", 'label')
self.detail_text.insert(tk.END, f"{record.stderr}\n", 'error')
self.detail_text.config(state=tk.DISABLED)
def _show_detail(self, text: str):
"""显示详情文本"""
self.detail_text.config(state=tk.NORMAL)
self.detail_text.delete(1.0, tk.END)
self.detail_text.insert(tk.END, text)
self.detail_text.config(state=tk.DISABLED)
def _open_log(self):
"""打开日志文件"""
if self._selected_record and self._selected_record.log_path:
import os
log_path = Path(self._selected_record.log_path)
if log_path.exists():
os.startfile(str(log_path))
else:
messagebox.showwarning("提示", f"日志文件不存在:\n{log_path}")
def _clear_history(self):
"""清空历史记录"""
result = messagebox.askyesno(
"确认清空",
"确定要清空所有历史记录吗?\n此操作不可恢复。",
icon='warning'
)
if result:
self.history.clear()
self._load_data()
self._show_detail("历史记录已清空")
self.open_log_btn.config(state=tk.DISABLED)
def show(self):
"""显示视图"""
self._load_data() # 刷新数据
self.frame.pack(fill=tk.BOTH, expand=True)
def hide(self):
"""隐藏视图"""
self.frame.pack_forget()
def get_frame(self) -> tk.Frame:
"""获取主框架"""
return self.frame