Files
LocalAgent/ui/task_guide_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

604 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
任务引导视图组件
执行任务的引导式 UI - 简化版
"""
import tkinter as tk
from tkinter import scrolledtext, messagebox
from tkinter import ttk
from typing import Callable, Optional, List
from pathlib import Path
import re
class MarkdownText(tk.Text):
"""支持简单 Markdown 渲染的 Text 组件"""
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
self._setup_tags()
def _setup_tags(self):
"""设置 Markdown 样式标签"""
# 标题样式
self.tag_configure('h1', font=('Microsoft YaHei UI', 13, 'bold'), foreground='#ffd54f', spacing1=8, spacing3=4)
self.tag_configure('h2', font=('Microsoft YaHei UI', 11, 'bold'), foreground='#81c784', spacing1=6, spacing3=3)
self.tag_configure('h3', font=('Microsoft YaHei UI', 10, 'bold'), foreground='#4fc3f7', spacing1=4, spacing3=2)
# 列表样式
self.tag_configure('bullet', foreground='#ce93d8', lmargin1=15, lmargin2=30)
self.tag_configure('numbered', foreground='#ce93d8', lmargin1=15, lmargin2=30)
# 普通文本
self.tag_configure('normal', font=('Microsoft YaHei UI', 10), foreground='#d4d4d4')
def set_markdown(self, text: str):
"""设置 Markdown 内容并渲染"""
self.config(state=tk.NORMAL)
self.delete(1.0, tk.END)
lines = text.split('\n')
for line in lines:
self._render_line(line)
self.config(state=tk.DISABLED)
def _render_line(self, line: str):
"""渲染单行 Markdown"""
stripped = line.strip()
# 标题
if stripped.startswith('### '):
self.insert(tk.END, stripped[4:] + '\n', 'h3')
elif stripped.startswith('## '):
self.insert(tk.END, stripped[3:] + '\n', 'h2')
elif stripped.startswith('# '):
self.insert(tk.END, stripped[2:] + '\n', 'h1')
# 无序列表
elif stripped.startswith('- ') or stripped.startswith('* '):
self.insert(tk.END, '' + stripped[2:] + '\n', 'bullet')
# 有序列表
elif re.match(r'^\d+\.\s', stripped):
match = re.match(r'^(\d+\.)\s(.*)$', stripped)
if match:
self.insert(tk.END, match.group(1) + ' ' + match.group(2) + '\n', 'numbered')
# 普通文本
else:
self.insert(tk.END, line + '\n', 'normal')
class FileZone(tk.Frame):
"""简化的文件区域 - 仅显示打开文件夹按钮"""
def __init__(
self,
parent,
title: str,
target_dir: Path,
is_input: bool = True,
**kwargs
):
super().__init__(parent, **kwargs)
self.target_dir = target_dir
self.is_input = is_input
self.configure(bg='#2d2d2d')
# 确保目录存在
self.target_dir.mkdir(parents=True, exist_ok=True)
self._create_widgets(title)
def _create_widgets(self, title: str):
"""创建组件"""
# 打开文件夹按钮
color = '#4fc3f7' if self.is_input else '#81c784'
self.open_btn = tk.Button(
self,
text=f"📂 {title}",
font=('Microsoft YaHei UI', 10),
bg='#424242',
fg=color,
activebackground='#616161',
activeforeground=color,
relief=tk.FLAT,
padx=15,
pady=8,
cursor='hand2',
command=self._open_folder
)
self.open_btn.pack(fill=tk.X, padx=5, pady=5)
# 文件计数标签
self.count_label = tk.Label(
self,
text="0 个文件",
font=('Microsoft YaHei UI', 9),
fg='#888888',
bg='#2d2d2d'
)
self.count_label.pack(pady=(0, 5))
self._refresh_count()
def _open_folder(self):
"""打开目标文件夹"""
import os
os.startfile(str(self.target_dir))
def _refresh_count(self):
"""刷新文件计数"""
files = list(self.target_dir.glob('*'))
files = [f for f in files if f.is_file()]
self.count_label.config(text=f"{len(files)} 个文件")
def refresh(self):
"""刷新"""
self._refresh_count()
def get_files(self) -> List[Path]:
"""获取目录中的文件列表"""
files = list(self.target_dir.glob('*'))
return [f for f in files if f.is_file()]
class TaskGuideView:
"""
任务引导视图 - 简化版
"""
def __init__(
self,
parent: tk.Widget,
on_execute: Callable[[], None],
on_cancel: Callable[[], None],
workspace_path: Optional[Path] = None
):
self.parent = parent
self.on_execute = on_execute
self.on_cancel = on_cancel
if workspace_path:
self.workspace = workspace_path
else:
self.workspace = Path(__file__).parent.parent / "workspace"
self.input_dir = self.workspace / "input"
self.output_dir = self.workspace / "output"
self._create_widgets()
def _create_widgets(self):
"""创建 UI 组件"""
# 主框架 - 使用 Canvas 实现滚动
self.frame = tk.Frame(self.parent, bg='#1e1e1e')
# 标题
title_label = tk.Label(
self.frame,
text="执行任务确认",
font=('Microsoft YaHei UI', 14, 'bold'),
fg='#ffd54f',
bg='#1e1e1e'
)
title_label.pack(pady=(5, 10))
# 文件区域(横向排列)
file_section = tk.Frame(self.frame, bg='#1e1e1e')
file_section.pack(fill=tk.X, padx=10, pady=5)
# 输入文件区域
input_frame = tk.LabelFrame(
file_section,
text=" 📥 输入 ",
font=('Microsoft YaHei UI', 10, 'bold'),
fg='#4fc3f7',
bg='#1e1e1e',
relief=tk.GROOVE
)
input_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
self.input_zone = FileZone(
input_frame,
title="打开输入文件夹",
target_dir=self.input_dir,
is_input=True,
bg='#2d2d2d'
)
self.input_zone.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
# 箭头
arrow_label = tk.Label(
file_section,
text="",
font=('Microsoft YaHei UI', 16, 'bold'),
fg='#ffd54f',
bg='#1e1e1e'
)
arrow_label.pack(side=tk.LEFT, padx=5)
# 输出文件区域
output_frame = tk.LabelFrame(
file_section,
text=" 📤 输出 ",
font=('Microsoft YaHei UI', 10, 'bold'),
fg='#81c784',
bg='#1e1e1e',
relief=tk.GROOVE
)
output_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 0))
self.output_zone = FileZone(
output_frame,
title="打开输出文件夹",
target_dir=self.output_dir,
is_input=False,
bg='#2d2d2d'
)
self.output_zone.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
# 意图识别结果区域
self._create_intent_section()
# 执行计划区域Markdown
self._create_plan_section()
# 代码预览区域(可折叠)
self._create_code_section()
# 风险提示区域
self._create_risk_section()
# 按钮区域
self._create_button_section()
def _create_intent_section(self):
"""创建意图识别结果区域"""
section = tk.LabelFrame(
self.frame,
text=" 🎯 意图识别 ",
font=('Microsoft YaHei UI', 10, 'bold'),
fg='#81c784',
bg='#1e1e1e',
relief=tk.GROOVE
)
section.pack(fill=tk.X, padx=10, pady=3)
self.intent_label = tk.Label(
section,
text="",
font=('Microsoft YaHei UI', 9),
fg='#d4d4d4',
bg='#1e1e1e',
wraplength=650,
justify=tk.LEFT
)
self.intent_label.pack(padx=8, pady=5, anchor=tk.W)
def _create_plan_section(self):
"""创建执行计划区域(支持 Markdown"""
section = tk.LabelFrame(
self.frame,
text=" 📄 执行计划 ",
font=('Microsoft YaHei UI', 10, 'bold'),
fg='#ce93d8',
bg='#1e1e1e',
relief=tk.GROOVE
)
section.pack(fill=tk.BOTH, expand=True, padx=10, pady=3)
# 使用 Markdown 渲染的 Text
text_frame = tk.Frame(section, bg='#2d2d2d')
text_frame.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
self.plan_text = MarkdownText(
text_frame,
wrap=tk.WORD,
bg='#2d2d2d',
fg='#d4d4d4',
relief=tk.FLAT,
height=6,
padx=8,
pady=5
)
# 添加滚动条
scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=self.plan_text.yview)
self.plan_text.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.plan_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
def _create_code_section(self):
"""创建代码预览区域(可折叠)"""
# 折叠状态
self._code_expanded = False
# 外层框架
self.code_section = tk.LabelFrame(
self.frame,
text=" 💻 生成的代码 ",
font=('Microsoft YaHei UI', 10, 'bold'),
fg='#64b5f6',
bg='#1e1e1e',
relief=tk.GROOVE
)
self.code_section.pack(fill=tk.X, padx=10, pady=3)
# 展开/折叠按钮
self.toggle_code_btn = tk.Button(
self.code_section,
text="▶ 点击展开代码预览",
font=('Microsoft YaHei UI', 9),
bg='#2d2d2d',
fg='#64b5f6',
activebackground='#3d3d3d',
activeforeground='#64b5f6',
relief=tk.FLAT,
cursor='hand2',
command=self._toggle_code_view
)
self.toggle_code_btn.pack(fill=tk.X, padx=5, pady=5)
# 代码显示区域(初始隐藏)
self.code_frame = tk.Frame(self.code_section, bg='#1e1e1e')
# 代码文本框
self.code_text = tk.Text(
self.code_frame,
wrap=tk.NONE,
font=('Consolas', 10),
bg='#1e1e1e',
fg='#d4d4d4',
insertbackground='white',
relief=tk.FLAT,
height=12,
padx=8,
pady=5
)
# 配置代码高亮标签
self.code_text.tag_configure('keyword', foreground='#569cd6')
self.code_text.tag_configure('string', foreground='#ce9178')
self.code_text.tag_configure('comment', foreground='#6a9955')
self.code_text.tag_configure('function', foreground='#dcdcaa')
self.code_text.tag_configure('number', foreground='#b5cea8')
# 滚动条
code_scrollbar_y = ttk.Scrollbar(self.code_frame, orient=tk.VERTICAL, command=self.code_text.yview)
code_scrollbar_x = ttk.Scrollbar(self.code_frame, orient=tk.HORIZONTAL, command=self.code_text.xview)
self.code_text.configure(yscrollcommand=code_scrollbar_y.set, xscrollcommand=code_scrollbar_x.set)
code_scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y)
code_scrollbar_x.pack(side=tk.BOTTOM, fill=tk.X)
self.code_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 复制按钮
self.copy_code_btn = tk.Button(
self.code_frame,
text="📋 复制代码",
font=('Microsoft YaHei UI', 9),
bg='#424242',
fg='white',
activebackground='#616161',
activeforeground='white',
relief=tk.FLAT,
cursor='hand2',
command=self._copy_code
)
def _toggle_code_view(self):
"""切换代码预览的展开/折叠状态"""
self._code_expanded = not self._code_expanded
if self._code_expanded:
self.toggle_code_btn.config(text="▼ 点击折叠代码预览")
self.code_frame.pack(fill=tk.BOTH, expand=True, padx=3, pady=(0, 5))
self.copy_code_btn.pack(pady=5)
else:
self.toggle_code_btn.config(text="▶ 点击展开代码预览")
self.copy_code_btn.pack_forget()
self.code_frame.pack_forget()
def _copy_code(self):
"""复制代码到剪贴板"""
code = self.code_text.get(1.0, tk.END).strip()
self.frame.clipboard_clear()
self.frame.clipboard_append(code)
# 显示复制成功提示
original_text = self.copy_code_btn.cget('text')
self.copy_code_btn.config(text="✓ 已复制!")
self.frame.after(1500, lambda: self.copy_code_btn.config(text=original_text))
def _apply_syntax_highlight(self, code: str):
"""应用简单的语法高亮"""
import re
# 关键字
keywords = r'\b(import|from|def|class|if|else|elif|for|while|try|except|finally|with|as|return|yield|raise|pass|break|continue|and|or|not|in|is|None|True|False|lambda|global|nonlocal)\b'
# 字符串
strings = r'(\"\"\"[\s\S]*?\"\"\"|\'\'\'[\s\S]*?\'\'\'|\"[^\"]*\"|\'[^\']*\')'
# 注释
comments = r'(#.*$)'
# 函数调用
functions = r'\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\('
# 数字
numbers = r'\b(\d+\.?\d*)\b'
# 先插入纯文本
self.code_text.delete(1.0, tk.END)
self.code_text.insert(1.0, code)
# 应用高亮
for match in re.finditer(keywords, code, re.MULTILINE):
start = f"1.0+{match.start()}c"
end = f"1.0+{match.end()}c"
self.code_text.tag_add('keyword', start, end)
for match in re.finditer(strings, code, re.MULTILINE):
start = f"1.0+{match.start()}c"
end = f"1.0+{match.end()}c"
self.code_text.tag_add('string', start, end)
for match in re.finditer(comments, code, re.MULTILINE):
start = f"1.0+{match.start()}c"
end = f"1.0+{match.end()}c"
self.code_text.tag_add('comment', start, end)
for match in re.finditer(numbers, code, re.MULTILINE):
start = f"1.0+{match.start(1)}c"
end = f"1.0+{match.end(1)}c"
self.code_text.tag_add('number', start, end)
def _create_risk_section(self):
"""创建风险提示区域"""
section = tk.LabelFrame(
self.frame,
text=" ⚠️ 安全提示 ",
font=('Microsoft YaHei UI', 10, 'bold'),
fg='#ffb74d',
bg='#1e1e1e',
relief=tk.GROOVE
)
section.pack(fill=tk.X, padx=10, pady=3)
self.risk_label = tk.Label(
section,
text="• 所有操作仅在 workspace 目录内进行 • 原始文件不会被修改或删除 • 执行代码已通过安全检查",
font=('Microsoft YaHei UI', 9),
fg='#d4d4d4',
bg='#1e1e1e',
justify=tk.LEFT
)
self.risk_label.pack(padx=8, pady=5, anchor=tk.W)
def _create_button_section(self):
"""创建按钮区域"""
button_frame = tk.Frame(self.frame, bg='#1e1e1e')
button_frame.pack(fill=tk.X, padx=10, pady=10)
# 统一按钮样式
btn_font = ('Microsoft YaHei UI', 10)
btn_width = 12
btn_height = 1
# 刷新文件列表按钮
self.refresh_btn = tk.Button(
button_frame,
text="🔄 刷新",
font=btn_font,
width=btn_width,
height=btn_height,
bg='#424242',
fg='white',
activebackground='#616161',
activeforeground='white',
relief=tk.FLAT,
cursor='hand2',
command=self._refresh_all
)
self.refresh_btn.pack(side=tk.LEFT)
# 执行按钮
self.execute_btn = tk.Button(
button_frame,
text="🚀 开始执行",
font=('Microsoft YaHei UI', 10, 'bold'),
width=btn_width,
height=btn_height,
bg='#4caf50',
fg='white',
activebackground='#66bb6a',
activeforeground='white',
relief=tk.FLAT,
cursor='hand2',
command=self._on_execute_clicked
)
self.execute_btn.pack(side=tk.RIGHT)
# 取消按钮
self.cancel_btn = tk.Button(
button_frame,
text="取消",
font=btn_font,
width=btn_width,
height=btn_height,
bg='#616161',
fg='white',
activebackground='#757575',
activeforeground='white',
relief=tk.FLAT,
cursor='hand2',
command=self.on_cancel
)
self.cancel_btn.pack(side=tk.RIGHT, padx=(0, 10))
def _refresh_all(self):
"""刷新所有文件列表"""
self.input_zone.refresh()
self.output_zone.refresh()
def _on_execute_clicked(self):
"""执行按钮点击"""
# 刷新文件列表
self.input_zone.refresh()
# 检查 input 目录是否有文件
files = self.input_zone.get_files()
if not files:
result = messagebox.askyesno(
"确认执行",
"输入文件夹为空,确定要继续执行吗?",
icon='warning'
)
if not result:
return
self.on_execute()
def set_intent_result(self, reason: str, confidence: float):
"""设置意图识别结果"""
self.intent_label.config(
text=f"识别结果: 执行任务 (置信度: {confidence:.0%}) | 原因: {reason}"
)
def set_execution_plan(self, plan: str):
"""设置执行计划Markdown 格式)"""
self.plan_text.set_markdown(plan)
def set_code(self, code: str):
"""设置生成的代码"""
self.code_text.config(state=tk.NORMAL)
self._apply_syntax_highlight(code)
self.code_text.config(state=tk.DISABLED)
def set_risk_info(self, info: str):
"""设置风险提示"""
self.risk_label.config(text=info)
def set_buttons_enabled(self, enabled: bool):
"""设置按钮是否可用"""
state = tk.NORMAL if enabled else tk.DISABLED
self.execute_btn.config(state=state)
self.cancel_btn.config(state=state)
self.refresh_btn.config(state=state)
def refresh_output(self):
"""刷新输出文件列表"""
self.output_zone.refresh()
def show(self):
"""显示视图"""
self.frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self._refresh_all()
def hide(self):
"""隐藏视图"""
self.frame.pack_forget()
def get_frame(self) -> tk.Frame:
"""获取主框架"""
return self.frame