Initial commit
This commit is contained in:
524
ui/task_guide_view.py
Normal file
524
ui/task_guide_view.py
Normal file
@@ -0,0 +1,524 @@
|
||||
"""
|
||||
任务引导视图组件
|
||||
执行任务的引导式 UI - 支持文件拖拽和 Markdown 渲染
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import scrolledtext, messagebox
|
||||
from tkinter import ttk
|
||||
from typing import Callable, Optional, List
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
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', 14, 'bold'), foreground='#ffd54f', spacing1=10, spacing3=5)
|
||||
self.tag_configure('h2', font=('Microsoft YaHei UI', 12, 'bold'), foreground='#81c784', spacing1=8, spacing3=4)
|
||||
self.tag_configure('h3', font=('Microsoft YaHei UI', 11, 'bold'), foreground='#4fc3f7', spacing1=6, spacing3=3)
|
||||
|
||||
# 列表样式
|
||||
self.tag_configure('bullet', foreground='#ce93d8', lmargin1=20, lmargin2=35)
|
||||
self.tag_configure('numbered', foreground='#ce93d8', lmargin1=20, lmargin2=35)
|
||||
|
||||
# 代码样式
|
||||
self.tag_configure('code', font=('Consolas', 10), background='#3c3c3c', foreground='#f8f8f2')
|
||||
|
||||
# 粗体和斜体
|
||||
self.tag_configure('bold', font=('Microsoft YaHei UI', 10, 'bold'))
|
||||
self.tag_configure('italic', font=('Microsoft YaHei UI', 10, 'italic'))
|
||||
|
||||
# 普通文本
|
||||
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._render_inline(line + '\n')
|
||||
|
||||
def _render_inline(self, text: str):
|
||||
"""渲染行内 Markdown(粗体、斜体、代码)"""
|
||||
# 简化处理:直接插入普通文本
|
||||
# 完整实现需要更复杂的解析
|
||||
self.insert(tk.END, text, 'normal')
|
||||
|
||||
|
||||
class DropZone(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', relief=tk.GROOVE, bd=2)
|
||||
|
||||
# 确保目录存在
|
||||
self.target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self._create_widgets(title)
|
||||
self._setup_drag_drop()
|
||||
self._refresh_file_list()
|
||||
|
||||
def _create_widgets(self, title: str):
|
||||
"""创建组件"""
|
||||
# 标题
|
||||
title_frame = tk.Frame(self, bg='#2d2d2d')
|
||||
title_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||
|
||||
tk.Label(
|
||||
title_frame,
|
||||
text=title,
|
||||
font=('Microsoft YaHei UI', 11, 'bold'),
|
||||
fg='#4fc3f7' if self.is_input else '#81c784',
|
||||
bg='#2d2d2d'
|
||||
).pack(side=tk.LEFT)
|
||||
|
||||
# 打开文件夹按钮
|
||||
open_btn = tk.Button(
|
||||
title_frame,
|
||||
text="📂",
|
||||
font=('Microsoft YaHei UI', 10),
|
||||
bg='#424242',
|
||||
fg='white',
|
||||
relief=tk.FLAT,
|
||||
cursor='hand2',
|
||||
command=self._open_folder
|
||||
)
|
||||
open_btn.pack(side=tk.RIGHT)
|
||||
|
||||
# 刷新按钮
|
||||
refresh_btn = tk.Button(
|
||||
title_frame,
|
||||
text="🔄",
|
||||
font=('Microsoft YaHei UI', 10),
|
||||
bg='#424242',
|
||||
fg='white',
|
||||
relief=tk.FLAT,
|
||||
cursor='hand2',
|
||||
command=self._refresh_file_list
|
||||
)
|
||||
refresh_btn.pack(side=tk.RIGHT, padx=(0, 5))
|
||||
|
||||
# 拖拽提示区域
|
||||
self.drop_label = tk.Label(
|
||||
self,
|
||||
text="将文件拖拽到此处\n或点击 📂 打开文件夹",
|
||||
font=('Microsoft YaHei UI', 10),
|
||||
fg='#888888',
|
||||
bg='#3c3c3c',
|
||||
relief=tk.SUNKEN,
|
||||
padx=20,
|
||||
pady=15
|
||||
)
|
||||
self.drop_label.pack(fill=tk.X, padx=5, pady=5)
|
||||
|
||||
# 文件列表
|
||||
self.file_listbox = tk.Listbox(
|
||||
self,
|
||||
font=('Microsoft YaHei UI', 9),
|
||||
bg='#2d2d2d',
|
||||
fg='#d4d4d4',
|
||||
selectbackground='#0078d4',
|
||||
relief=tk.FLAT,
|
||||
height=4
|
||||
)
|
||||
self.file_listbox.pack(fill=tk.BOTH, expand=True, 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))
|
||||
|
||||
def _setup_drag_drop(self):
|
||||
"""设置拖拽功能(Windows 需要 windnd 库,这里用简化方案)"""
|
||||
# 由于 Tkinter 原生不支持文件拖拽,使用点击打开文件夹的方式
|
||||
self.drop_label.bind('<Button-1>', lambda e: self._open_folder())
|
||||
|
||||
def _open_folder(self):
|
||||
"""打开目标文件夹"""
|
||||
import os
|
||||
os.startfile(str(self.target_dir))
|
||||
|
||||
def _refresh_file_list(self):
|
||||
"""刷新文件列表"""
|
||||
self.file_listbox.delete(0, tk.END)
|
||||
|
||||
files = list(self.target_dir.glob('*'))
|
||||
files = [f for f in files if f.is_file()]
|
||||
|
||||
for f in files:
|
||||
self.file_listbox.insert(tk.END, f.name)
|
||||
|
||||
self.count_label.config(text=f"{len(files)} 个文件")
|
||||
|
||||
def get_files(self) -> List[Path]:
|
||||
"""获取目录中的文件列表"""
|
||||
files = list(self.target_dir.glob('*'))
|
||||
return [f for f in files if f.is_file()]
|
||||
|
||||
def clear_files(self):
|
||||
"""清空目录中的文件"""
|
||||
for f in self.target_dir.glob('*'):
|
||||
if f.is_file():
|
||||
f.unlink()
|
||||
self._refresh_file_list()
|
||||
|
||||
|
||||
class TaskGuideView:
|
||||
"""
|
||||
任务引导视图
|
||||
|
||||
小白引导式界面,包含:
|
||||
- 意图识别结果
|
||||
- 文件拖拽区域(输入/输出)
|
||||
- 执行计划展示(Markdown 渲染)
|
||||
- 风险提示
|
||||
- 执行按钮
|
||||
"""
|
||||
|
||||
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 组件"""
|
||||
# 主框架
|
||||
self.frame = tk.Frame(self.parent, bg='#1e1e1e')
|
||||
|
||||
# 标题
|
||||
title_label = tk.Label(
|
||||
self.frame,
|
||||
text="执行任务确认",
|
||||
font=('Microsoft YaHei UI', 16, 'bold'),
|
||||
fg='#ffd54f',
|
||||
bg='#1e1e1e'
|
||||
)
|
||||
title_label.pack(pady=(10, 15))
|
||||
|
||||
# 上半部分:文件区域
|
||||
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', 11, '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 = DropZone(
|
||||
input_frame,
|
||||
title="待处理文件",
|
||||
target_dir=self.input_dir,
|
||||
is_input=True,
|
||||
bg='#2d2d2d'
|
||||
)
|
||||
self.input_zone.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
# 箭头
|
||||
arrow_frame = tk.Frame(file_section, bg='#1e1e1e')
|
||||
arrow_frame.pack(side=tk.LEFT, padx=10)
|
||||
tk.Label(
|
||||
arrow_frame,
|
||||
text="➡️",
|
||||
font=('Microsoft YaHei UI', 20),
|
||||
fg='#ffd54f',
|
||||
bg='#1e1e1e'
|
||||
).pack(pady=30)
|
||||
|
||||
# 输出文件区域
|
||||
output_frame = tk.LabelFrame(
|
||||
file_section,
|
||||
text=" 📤 输出文件 ",
|
||||
font=('Microsoft YaHei UI', 11, '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 = DropZone(
|
||||
output_frame,
|
||||
title="处理结果",
|
||||
target_dir=self.output_dir,
|
||||
is_input=False,
|
||||
bg='#2d2d2d'
|
||||
)
|
||||
self.output_zone.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
# 意图识别结果区域
|
||||
self._create_intent_section()
|
||||
|
||||
# 执行计划区域(Markdown)
|
||||
self._create_plan_section()
|
||||
|
||||
# 风险提示区域
|
||||
self._create_risk_section()
|
||||
|
||||
# 按钮区域
|
||||
self._create_button_section()
|
||||
|
||||
def _create_intent_section(self):
|
||||
"""创建意图识别结果区域"""
|
||||
section = tk.LabelFrame(
|
||||
self.frame,
|
||||
text=" 🎯 意图识别 ",
|
||||
font=('Microsoft YaHei UI', 11, 'bold'),
|
||||
fg='#81c784',
|
||||
bg='#1e1e1e',
|
||||
relief=tk.GROOVE
|
||||
)
|
||||
section.pack(fill=tk.X, padx=10, pady=5)
|
||||
|
||||
self.intent_label = tk.Label(
|
||||
section,
|
||||
text="",
|
||||
font=('Microsoft YaHei UI', 10),
|
||||
fg='#d4d4d4',
|
||||
bg='#1e1e1e',
|
||||
wraplength=650,
|
||||
justify=tk.LEFT
|
||||
)
|
||||
self.intent_label.pack(padx=10, pady=8, anchor=tk.W)
|
||||
|
||||
def _create_plan_section(self):
|
||||
"""创建执行计划区域(支持 Markdown)"""
|
||||
section = tk.LabelFrame(
|
||||
self.frame,
|
||||
text=" 📄 执行计划 ",
|
||||
font=('Microsoft YaHei UI', 11, 'bold'),
|
||||
fg='#ce93d8',
|
||||
bg='#1e1e1e',
|
||||
relief=tk.GROOVE
|
||||
)
|
||||
section.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||||
|
||||
# 使用 Markdown 渲染的 Text
|
||||
self.plan_text = MarkdownText(
|
||||
section,
|
||||
wrap=tk.WORD,
|
||||
bg='#2d2d2d',
|
||||
fg='#d4d4d4',
|
||||
relief=tk.FLAT,
|
||||
height=8,
|
||||
padx=10,
|
||||
pady=10
|
||||
)
|
||||
|
||||
# 添加滚动条
|
||||
scrollbar = ttk.Scrollbar(section, 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(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
def _create_risk_section(self):
|
||||
"""创建风险提示区域"""
|
||||
section = tk.LabelFrame(
|
||||
self.frame,
|
||||
text=" ⚠️ 安全提示 ",
|
||||
font=('Microsoft YaHei UI', 11, 'bold'),
|
||||
fg='#ffb74d',
|
||||
bg='#1e1e1e',
|
||||
relief=tk.GROOVE
|
||||
)
|
||||
section.pack(fill=tk.X, padx=10, pady=5)
|
||||
|
||||
self.risk_label = tk.Label(
|
||||
section,
|
||||
text="• 所有操作仅在 workspace 目录内进行\n• 原始文件不会被修改或删除\n• 执行代码已通过安全检查",
|
||||
font=('Microsoft YaHei UI', 10),
|
||||
fg='#d4d4d4',
|
||||
bg='#1e1e1e',
|
||||
justify=tk.LEFT
|
||||
)
|
||||
self.risk_label.pack(padx=10, pady=8, 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=15)
|
||||
|
||||
# 刷新文件列表按钮
|
||||
self.refresh_btn = tk.Button(
|
||||
button_frame,
|
||||
text="🔄 刷新文件",
|
||||
font=('Microsoft YaHei UI', 10),
|
||||
bg='#424242',
|
||||
fg='white',
|
||||
activebackground='#616161',
|
||||
activeforeground='white',
|
||||
relief=tk.FLAT,
|
||||
padx=15,
|
||||
pady=5,
|
||||
cursor='hand2',
|
||||
command=self._refresh_all
|
||||
)
|
||||
self.refresh_btn.pack(side=tk.LEFT, padx=(0, 10))
|
||||
|
||||
# 取消按钮
|
||||
self.cancel_btn = tk.Button(
|
||||
button_frame,
|
||||
text="取消",
|
||||
font=('Microsoft YaHei UI', 11),
|
||||
bg='#616161',
|
||||
fg='white',
|
||||
activebackground='#757575',
|
||||
activeforeground='white',
|
||||
relief=tk.FLAT,
|
||||
padx=20,
|
||||
pady=5,
|
||||
cursor='hand2',
|
||||
command=self.on_cancel
|
||||
)
|
||||
self.cancel_btn.pack(side=tk.RIGHT, padx=(10, 0))
|
||||
|
||||
# 执行按钮
|
||||
self.execute_btn = tk.Button(
|
||||
button_frame,
|
||||
text="🚀 开始执行",
|
||||
font=('Microsoft YaHei UI', 12, 'bold'),
|
||||
bg='#4caf50',
|
||||
fg='white',
|
||||
activebackground='#66bb6a',
|
||||
activeforeground='white',
|
||||
relief=tk.FLAT,
|
||||
padx=30,
|
||||
pady=8,
|
||||
cursor='hand2',
|
||||
command=self._on_execute_clicked
|
||||
)
|
||||
self.execute_btn.pack(side=tk.RIGHT)
|
||||
|
||||
def _refresh_all(self):
|
||||
"""刷新所有文件列表"""
|
||||
self.input_zone._refresh_file_list()
|
||||
self.output_zone._refresh_file_list()
|
||||
|
||||
def _on_execute_clicked(self):
|
||||
"""执行按钮点击"""
|
||||
# 刷新文件列表
|
||||
self.input_zone._refresh_file_list()
|
||||
|
||||
# 检查 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%})\n原因: {reason}"
|
||||
)
|
||||
|
||||
def set_execution_plan(self, plan: str):
|
||||
"""设置执行计划(Markdown 格式)"""
|
||||
self.plan_text.set_markdown(plan)
|
||||
|
||||
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)
|
||||
|
||||
def refresh_output(self):
|
||||
"""刷新输出文件列表"""
|
||||
self.output_zone._refresh_file_list()
|
||||
|
||||
def show(self):
|
||||
"""显示视图"""
|
||||
self.frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
self._refresh_all()
|
||||
|
||||
def hide(self):
|
||||
"""隐藏视图"""
|
||||
self.frame.pack_forget()
|
||||
|
||||
def get_frame(self) -> tk.Frame:
|
||||
"""获取主框架"""
|
||||
return self.frame
|
||||
Reference in New Issue
Block a user