Initial commit

This commit is contained in:
Mimikko-zeus
2026-01-07 00:17:46 +08:00
commit 4b3286f546
49 changed files with 2492 additions and 0 deletions

2
ui/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# UI 模块

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

164
ui/chat_view.py Normal file
View File

@@ -0,0 +1,164 @@
"""
聊天视图组件
处理普通对话的 UI 展示
"""
import tkinter as tk
from tkinter import scrolledtext
from typing import Callable, Optional
class ChatView:
"""
聊天视图
包含:
- 消息显示区域
- 输入框
- 发送按钮
"""
def __init__(
self,
parent: tk.Widget,
on_send: Callable[[str], None]
):
"""
初始化聊天视图
Args:
parent: 父容器
on_send: 发送消息回调函数
"""
self.parent = parent
self.on_send = on_send
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_label = tk.Label(
self.frame,
text="LocalAgent - 本地 AI 助手",
font=('Microsoft YaHei UI', 16, 'bold'),
fg='#61dafb',
bg='#1e1e1e'
)
title_label.pack(pady=(0, 10))
# 消息显示区域
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))
# 输入区域框架
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')
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 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 get_frame(self) -> tk.Frame:
"""获取主框架"""
return self.frame

524
ui/task_guide_view.py Normal file
View 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