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:
106
ui/chat_view.py
106
ui/chat_view.py
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
聊天视图组件
|
||||
处理普通对话的 UI 展示 - 支持流式消息
|
||||
处理普通对话的 UI 展示 - 支持流式消息和加载动画
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
@@ -8,6 +8,58 @@ 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:
|
||||
"""
|
||||
聊天视图
|
||||
@@ -22,7 +74,8 @@ class ChatView:
|
||||
def __init__(
|
||||
self,
|
||||
parent: tk.Widget,
|
||||
on_send: Callable[[str], None]
|
||||
on_send: Callable[[str], None],
|
||||
on_show_history: Optional[Callable[[], None]] = None
|
||||
):
|
||||
"""
|
||||
初始化聊天视图
|
||||
@@ -30,14 +83,19 @@ class ChatView:
|
||||
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):
|
||||
@@ -46,15 +104,37 @@ class ChatView:
|
||||
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(
|
||||
self.frame,
|
||||
title_frame,
|
||||
text="LocalAgent - 本地 AI 助手",
|
||||
font=('Microsoft YaHei UI', 16, 'bold'),
|
||||
fg='#61dafb',
|
||||
bg='#1e1e1e'
|
||||
)
|
||||
title_label.pack(pady=(0, 10))
|
||||
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(
|
||||
@@ -118,6 +198,9 @@ class ChatView:
|
||||
"- 输入文件处理需求(如\"复制文件\"、\"整理图片\")将触发执行模式"
|
||||
)
|
||||
self.add_message(welcome_msg, 'system')
|
||||
|
||||
# 创建加载指示器(放在消息区域下方)
|
||||
self.loading = LoadingIndicator(self.frame)
|
||||
|
||||
def _on_enter_pressed(self, event):
|
||||
"""回车键处理"""
|
||||
@@ -214,6 +297,21 @@ class ChatView:
|
||||
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
|
||||
|
||||
335
ui/history_view.py
Normal file
335
ui/history_view.py
Normal 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
|
||||
|
||||
@@ -243,6 +243,9 @@ class TaskGuideView:
|
||||
# 执行计划区域(Markdown)
|
||||
self._create_plan_section()
|
||||
|
||||
# 代码预览区域(可折叠)
|
||||
self._create_code_section()
|
||||
|
||||
# 风险提示区域
|
||||
self._create_risk_section()
|
||||
|
||||
@@ -306,6 +309,148 @@ class TaskGuideView:
|
||||
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(
|
||||
@@ -423,6 +568,12 @@ class TaskGuideView:
|
||||
"""设置执行计划(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)
|
||||
|
||||
Reference in New Issue
Block a user