{"repo": "asowerp/2026-practice-of-programming-in-python-and-cpp", "files": {"ui/task3_widget.py": {"b9055e875c52beba21a8e483cb9bd8b5b65ddb8b": {"t": 1786068483.86647, "n": 21682, "h": "d341779693e241b4", "c": "from __future__ import annotations\n\nfrom PySide6.QtCore import QObject, QThread, Qt, Signal, Slot\nfrom PySide6.QtGui import QFont\nfrom PySide6.QtWidgets import (\n QComboBox,\n QFormLayout,\n QGroupBox,\n QHBoxLayout,\n QLabel,\n QLineEdit,\n QPlainTextEdit,\n QPushButton,\n QSplitter,\n QTextEdit,\n QVBoxLayout,\n QWidget,\n)\n\nfrom engine.ai_log_assistant import AIDebugResult, build_model_config, explain_log_mismatch, get_model_names\nfrom engine.class_manager import ClassManager\nfrom engine.task3_log_helper import LogComparison, build_log_text, compare_logs, filter_events, summarize_bundle\nfrom engine.warcraft_engine import STANDARD_STAGE_DEFINITIONS, SimulationBundle\nfrom ui.block_workspace import BlockProgramEditor, BlockSpec, FieldSpec\nfrom ui.timeline_controller import TimelineController\nfrom ui.timeline_panel import TimelinePanel\n\n\nclass AIAnalysisWorker(QObject):\n progress = Signal(str)\n finished = Signal(object)\n\n def __init__(self, config, comparison: LogComparison, expected_text: str, actual_text: str) -> None:\n super().__init__()\n self.config = config\n self.comparison = comparison\n self.expected_text = expected_text\n self.actual_text = actual_text\n\n @Slot()\n def run(self) -> None:\n try:\n result = explain_log_mismatch(\n self.config,\n self.comparison,\n expected_text=self.expected_text,\n actual_text=self.actual_text,\n progress=self.progress.emit,\n )\n except Exception as exc:\n result = AIDebugResult(False, f\"AI 分析线程异常: {exc}\")\n self.finished.emit(result)\n\n\nclass Task3Widget(QWidget):\n def __init__(self, manager: ClassManager | None = None) -> None:\n super().__init__()\n self.manager = manager or ClassManager.get_instance()\n self.imported_bundle: SimulationBundle | None = None\n self.last_comparison: LogComparison | None = None\n self.timeline_controller: TimelineController | None = None\n self.ai_thread: QThread | None = None\n self.ai_worker: AIAnalysisWorker | None = None\n self._build_ui()\n\n def _build_ui(self) -> None:\n main_layout = QVBoxLayout(self)\n main_layout.setContentsMargins(16, 16, 16, 16)\n main_layout.setSpacing(12)\n\n title = QLabel(\"Task3:完整日志生成与对拍\")\n title_font = QFont()\n title_font.setPointSize(16)\n title_font.setBold(True)\n title.setFont(title_font)\n\n subtitle = QLabel(\n \"Task3 消费 Task2 导出的整局事件流。可以生成标准日志、筛选局部事件、对比学生输出,\"\n \"并在提供 API Key 后让 AI 辅助定位格式或逻辑错误。\"\n )\n subtitle.setWordWrap(True)\n subtitle.setStyleSheet(\"color: #94A3B8; font-size: 13px;\")\n main_layout.addWidget(title)\n main_layout.addWidget(subtitle)\n\n action_row = QHBoxLayout()\n self.example_btn = QPushButton(\"加载推荐积木\")\n self.run_btn = QPushButton(\"执行日志脚本\")\n self.run_btn.setObjectName(\"btnPrimary\")\n self.clear_btn = QPushButton(\"清空差异视图\")\n self.ai_btn = QPushButton(\"AI 分析差异\")\n self.example_btn.setToolTip(\"自动填入导入、生成日志、对拍输出的推荐流程。\")\n self.run_btn.setToolTip(\"按左侧积木顺序生成标准日志并对拍你的输出。\")\n self.clear_btn.setToolTip(\"清空右侧日志、差异和 AI 建议,不删除左侧积木脚本。\")\n self.ai_btn.setToolTip(\"在存在差异时,用所选模型辅助定位原因。\")\n self.example_btn.clicked.connect(self.load_example_script)\n self.run_btn.clicked.connect(self.run_script)\n self.clear_btn.clicked.connect(self.clear_outputs)\n self.ai_btn.clicked.connect(self.run_ai_debug)\n action_row.addWidget(self.example_btn)\n action_row.addWidget(self.run_btn)\n action_row.addWidget(self.clear_btn)\n action_row.addWidget(self.ai_btn)\n action_row.addStretch(1)\n main_layout.addLayout(action_row)\n\n splitter = QSplitter(Qt.Orientation.Horizontal)\n splitter.setHandleWidth(1)\n self.block_editor = BlockProgramEditor(self._build_block_specs())\n splitter.addWidget(self.block_editor)\n splitter.addWidget(self._build_result_panel())\n splitter.setSizes([600, 780])\n splitter.setToolTip(\"拖动中间的分隔线可以调整左右面板宽度\")\n main_layout.addWidget(splitter)\n self._refresh_ai_button_state()\n\n def _build_result_panel(self) -> QWidget:\n container = QWidget()\n layout = QVBoxLayout(container)\n\n self.import_status_label = QLabel(\"尚未接收 Task2 导出的整局事件流。\")\n self.import_status_label.setStyleSheet(\"font-weight: 700; color: #4F46E5;\")\n layout.addWidget(self.import_status_label)\n\n self.summary_label = QLabel(\"导入 Task2 结果后,再用左侧积木生成标准日志并对拍。\")\n self.summary_label.setStyleSheet(\"font-weight: 700; color: #4F46E5;\")\n layout.addWidget(self.summary_label)\n\n right_splitter = QSplitter(Qt.Orientation.Horizontal)\n right_splitter.setHandleWidth(1)\n\n left_widget = QWidget()\n left_layout = QVBoxLayout(left_widget)\n left_layout.setContentsMargins(0, 0, 0, 0)\n\n timeline_group = QGroupBox(\"导入事件时间轴\")\n timeline_layout = QVBoxLayout(timeline_group)\n self.timeline_panel = TimelinePanel(\n \"Task3 对拍时间轴\",\n \"从 Task2 导入事件后,这里会显示同一条事件流,便于按时间定位日志差异。\",\n )\n self.timeline_controller = TimelineController(self.timeline_panel)\n self.timeline_controller.eventSelected.connect(self.focus_timeline_event)\n timeline_layout.addWidget(self.timeline_panel)\n left_layout.addWidget(timeline_group, 1)\n\n ai_group = QGroupBox(\"AI 调错助手\")\n ai_layout = QFormLayout(ai_group)\n self.ai_model_combo = QComboBox()\n self.ai_model_combo.addItems(get_model_names())\n self.ai_key_edit = QLineEdit()\n self.ai_key_edit.setEchoMode(QLineEdit.EchoMode.Password)\n self.ai_key_edit.setPlaceholderText(\"填写 API Key,仅用于本次请求,不会保存\")\n self.ai_model_override_edit = QLineEdit()\n self.ai_model_override_edit.setPlaceholderText(\"可选:覆盖模型名,例如 deepseek-v4-flash 或 deepseek-v4-pro\")\n self.ai_base_url_edit = QLineEdit()\n self.ai_base_url_edit.setPlaceholderText(\"可选:OpenAI 兼容 base URL,例如 https://api.deepseek.com\")\n ai_layout.addRow(\"模型\", self.ai_model_combo)\n ai_layout.addRow(\"API Key\", self.ai_key_edit)\n ai_layout.addRow(\"模型名\", self.ai_model_override_edit)\n ai_layout.addRow(\"接口地址\", self.ai_base_url_edit)\n left_layout.addWidget(ai_group)\n\n process_group = QGroupBox(\"脚本执行日志\")\n process_layout = QVBoxLayout(process_group)\n self.process_output = QPlainTextEdit()\n self.process_output.setReadOnly(True)\n self.process_output.setPlaceholderText(\"执行日志脚本后,这里会显示每个积木的处理结果。\")\n process_layout.addWidget(self.process_output)\n left_layout.addWidget(process_group)\n\n right_widget = QWidget()\n right_layout = QVBoxLayout(right_widget)\n right_layout.setContentsMargins(0, 0, 0, 0)\n\n text_row = QHBoxLayout()\n\n expected_group = QGroupBox(\"当前标准日志\")\n expected_layout = QVBoxLayout(expected_group)\n self.expected_output = QPlainTextEdit()\n self.expected_output.setReadOnly(True)\n self.expected_output.setPlaceholderText(\"生成标准日志后会显示在这里。\")\n expected_layout.addWidget(self.expected_output)\n\n actual_group = QGroupBox(\"你的输出\")\n actual_layout = QVBoxLayout(actual_group)\n self.actual_output = QPlainTextEdit()\n self.actual_output.setPlaceholderText(\"把你程序的输出粘贴到这里,再执行“对比我的输出”。\")\n actual_layout.addWidget(self.actual_output)\n\n text_row.addWidget(expected_group)\n text_row.addWidget(actual_group)\n right_layout.addLayout(text_row, 1)\n\n diff_group = QGroupBox(\"差异高亮\")\n diff_layout = QVBoxLayout(diff_group)\n self.diff_view = QTextEdit()\n self.diff_view.setReadOnly(True)\n self.diff_view.setPlaceholderText(\"执行“对比我的输出”后,这里会显示逐行差异。\")\n diff_layout.addWidget(self.diff_view)\n right_layout.addWidget(diff_group, 1)\n\n ai_result_group = QGroupBox(\"AI 调错建议\")\n ai_result_layout = QVBoxLayout(ai_result_group)\n self.ai_output = QPlainTextEdit()\n self.ai_output.setReadOnly(True)\n self.ai_output.setPlaceholderText(\"先执行日志脚本并产生差异,再点击“AI 分析差异”。\")\n ai_result_layout.addWidget(self.ai_output)\n right_layout.addWidget(ai_result_group, 1)\n\n right_splitter.addWidget(left_widget)\n right_splitter.addWidget(right_widget)\n right_splitter.setSizes([320, 460])\n right_splitter.setToolTip(\"拖动分隔线可调整右侧两栏宽度\")\n layout.addWidget(right_splitter, 1)\n\n return container\n\n def _build_block_specs(self) -> list[BlockSpec]:\n stage_keys = [definition.key for definition in STANDARD_STAGE_DEFINITIONS]\n return [\n BlockSpec(\n key=\"use_imported_bundle\",\n title=\"使用 Task2 导入结果\",\n color=\"#2563EB\",\n description=\"读取 Task2 导出的整局事件流、Case 参数和阶段日程。\",\n ),\n BlockSpec(\n key=\"inspect_bundle\",\n title=\"查看当前导入摘要\",\n color=\"#0891B2\",\n description=\"查看当前是标准题面模式还是自定义教学模式,并预览前几条事件。\",\n ),\n BlockSpec(\n key=\"filter_hour\",\n title=\"按小时筛选\",\n color=\"#16A34A\",\n description=\"只保留某个小时内的事件,用来单独查看局部输出。\",\n fields=[\n FieldSpec(\"hour\", \"小时\", \"int\", 0, minimum=0, maximum=100, width=140),\n ],\n ),\n BlockSpec(\n key=\"filter_stage\",\n title=\"按阶段筛选\",\n color=\"#22C55E\",\n description=\"只保留某类阶段,例如 spawn / march / battle。\",\n fields=[\n FieldSpec(\"stage_key\", \"阶段\", \"combo\", stage_keys[0], stage_keys, editable=False, width=170),\n ],\n ),\n BlockSpec(\n key=\"filter_keyword\",\n title=\"按关键词筛选\",\n color=\"#65A30D\",\n description=\"按关键词过滤事件描述,例如 city 1、dragon、headquarter。\",\n fields=[\n FieldSpec(\"keyword\", \"关键词\", \"text\", \"city 1\", width=170, placeholder=\"例如 city 1\"),\n ],\n ),\n BlockSpec(\n key=\"generate_log\",\n title=\"生成当前日志\",\n color=\"#EA580C\",\n description=\"把当前筛选后的事件序列转换成标准文本。\",\n ),\n BlockSpec(\n key=\"compare_output\",\n title=\"对比我的输出\",\n color=\"#DC2626\",\n description=\"将当前生成的日志与右侧“你的输出”逐行比较。\",\n ),\n BlockSpec(\n key=\"reset_filters\",\n title=\"清空筛选链\",\n color=\"#475569\",\n description=\"恢复为导入时的完整事件流。\",\n ),\n ]\n\n def load_example_script(self) -> None:\n self.block_editor.set_script([\n {\"key\": \"use_imported_bundle\", \"fields\": {}},\n {\"key\": \"inspect_bundle\", \"fields\": {}},\n {\"key\": \"generate_log\", \"fields\": {}},\n {\"key\": \"compare_output\", \"fields\": {}},\n ])\n self.summary_label.setText(\"已加载推荐积木。下一步先从 Task2 导入事件,再点击“执行日志脚本”。\")\n self.summary_label.setStyleSheet(\"font-weight: 700; color: #4F46E5;\")\n\n def clear_outputs(self) -> None:\n self.process_output.clear()\n self.expected_output.clear()\n self.diff_view.clear()\n self.ai_output.clear()\n self.last_comparison = None\n if self.timeline_controller is not None and self.imported_bundle is None:\n self.timeline_controller.set_events([])\n self.summary_label.setText(\"输出已清空。导入结果和左侧脚本仍保留,可继续对拍。\")\n self.summary_label.setStyleSheet(\"font-weight: 700; color: #4F46E5;\")\n self._refresh_ai_button_state()\n\n def load_simulation_bundle(self, bundle: object) -> None:\n if not isinstance(bundle, SimulationBundle):\n self.imported_bundle = None\n self.import_status_label.setText(\"收到的数据不是合法的 Task2 模拟结果。\")\n self.import_status_label.setStyleSheet(\"font-weight: 700; color: #DC2626;\")\n if self.timeline_controller is not None:\n self.timeline_controller.set_events([])\n return\n self.imported_bundle = bundle\n self.import_status_label.setText(\n f\"已接收 Task2 导出的 {len(bundle.events)} 条事件,当前模式:{bundle.mode_label}。\"\n )\n self.import_status_label.setStyleSheet(\"font-weight: 700; color: #4F46E5;\")\n self.process_output.setPlainText(summarize_bundle(bundle))\n if self.timeline_controller is not None:\n self.timeline_controller.load_simulation_bundle(bundle)\n self.summary_label.setText(\"Task2 事件已导入。下一步加载推荐积木或执行已有日志脚本。\")\n self.summary_label.setStyleSheet(\"font-weight: 700; color: #4F46E5;\")\n self._refresh_ai_button_state()\n\n def focus_timeline_event(self, index: int, event: object) -> None:\n event_time = getattr(event, \"display_time\", \"--:--\")\n stage_key = getattr(event, \"stage_key\", \"unknown\")\n description = getattr(event, \"description\", str(event))\n self.summary_label.setText(f\"时间轴已定位到事件 #{index + 1}:{event_time} / {stage_key}。\")\n self.summary_label.setStyleSheet(\"font-weight: 700; color: #4F46E5;\")\n if self.expected_output.toPlainText().strip():\n matching_line = event.to_log_line() if hasattr(event, \"to_log_line\") else description\n self.diff_view.setHtml(\n \"
{matching_line}\"\n \"