diff --git a/backend/app/api/report.py b/backend/app/api/report.py index ce44db9..e05c73c 100644 --- a/backend/app/api/report.py +++ b/backend/app/api/report.py @@ -105,13 +105,18 @@ def generate_report(): "error": "缺少模拟需求描述" }), 400 + # 提前生成 report_id,以便立即返回给前端 + import uuid + report_id = f"report_{uuid.uuid4().hex[:12]}" + # 创建异步任务 task_manager = TaskManager() task_id = task_manager.create_task( task_type="report_generate", metadata={ "simulation_id": simulation_id, - "graph_id": graph_id + "graph_id": graph_id, + "report_id": report_id } ) @@ -140,8 +145,11 @@ def generate_report(): message=f"[{stage}] {message}" ) - # 生成报告 - report = agent.generate_report(progress_callback=progress_callback) + # 生成报告(传入预先生成的 report_id) + report = agent.generate_report( + progress_callback=progress_callback, + report_id=report_id + ) # 保存报告 ReportManager.save_report(report) @@ -170,6 +178,7 @@ def generate_report(): "success": True, "data": { "simulation_id": simulation_id, + "report_id": report_id, "task_id": task_id, "status": "generating", "message": "报告生成任务已启动,请通过 /api/report/generate/status 查询进度", @@ -739,6 +748,183 @@ def check_report_status(simulation_id: str): }), 500 +# ============== Agent 日志接口 ============== + +@report_bp.route('//agent-log', methods=['GET']) +def get_agent_log(report_id: str): + """ + 获取 Report Agent 的详细执行日志 + + 实时获取报告生成过程中的每一步动作,包括: + - 报告开始、规划开始/完成 + - 每个章节的开始、工具调用、LLM响应、完成 + - 报告完成或失败 + + Query参数: + from_line: 从第几行开始读取(可选,默认0,用于增量获取) + + 返回: + { + "success": true, + "data": { + "logs": [ + { + "timestamp": "2025-12-13T...", + "elapsed_seconds": 12.5, + "report_id": "report_xxxx", + "action": "tool_call", + "stage": "generating", + "section_title": "执行摘要", + "section_index": 1, + "details": { + "tool_name": "insight_forge", + "parameters": {...}, + ... + } + }, + ... + ], + "total_lines": 25, + "from_line": 0, + "has_more": false + } + } + """ + try: + from_line = request.args.get('from_line', 0, type=int) + + log_data = ReportManager.get_agent_log(report_id, from_line=from_line) + + return jsonify({ + "success": True, + "data": log_data + }) + + except Exception as e: + logger.error(f"获取Agent日志失败: {str(e)}") + return jsonify({ + "success": False, + "error": str(e), + "traceback": traceback.format_exc() + }), 500 + + +@report_bp.route('//agent-log/stream', methods=['GET']) +def stream_agent_log(report_id: str): + """ + 获取完整的 Agent 日志(一次性获取全部) + + 返回: + { + "success": true, + "data": { + "logs": [...], + "count": 25 + } + } + """ + try: + logs = ReportManager.get_agent_log_stream(report_id) + + return jsonify({ + "success": True, + "data": { + "logs": logs, + "count": len(logs) + } + }) + + except Exception as e: + logger.error(f"获取Agent日志失败: {str(e)}") + return jsonify({ + "success": False, + "error": str(e), + "traceback": traceback.format_exc() + }), 500 + + +# ============== 控制台日志接口 ============== + +@report_bp.route('//console-log', methods=['GET']) +def get_console_log(report_id: str): + """ + 获取 Report Agent 的控制台输出日志 + + 实时获取报告生成过程中的控制台输出(INFO、WARNING等), + 这与 agent-log 接口返回的结构化 JSON 日志不同, + 是纯文本格式的控制台风格日志。 + + Query参数: + from_line: 从第几行开始读取(可选,默认0,用于增量获取) + + 返回: + { + "success": true, + "data": { + "logs": [ + "[19:46:14] INFO: 搜索完成: 找到 15 条相关事实", + "[19:46:14] INFO: 图谱搜索: graph_id=xxx, query=...", + ... + ], + "total_lines": 100, + "from_line": 0, + "has_more": false + } + } + """ + try: + from_line = request.args.get('from_line', 0, type=int) + + log_data = ReportManager.get_console_log(report_id, from_line=from_line) + + return jsonify({ + "success": True, + "data": log_data + }) + + except Exception as e: + logger.error(f"获取控制台日志失败: {str(e)}") + return jsonify({ + "success": False, + "error": str(e), + "traceback": traceback.format_exc() + }), 500 + + +@report_bp.route('//console-log/stream', methods=['GET']) +def stream_console_log(report_id: str): + """ + 获取完整的控制台日志(一次性获取全部) + + 返回: + { + "success": true, + "data": { + "logs": [...], + "count": 100 + } + } + """ + try: + logs = ReportManager.get_console_log_stream(report_id) + + return jsonify({ + "success": True, + "data": { + "logs": logs, + "count": len(logs) + } + }) + + except Exception as e: + logger.error(f"获取控制台日志失败: {str(e)}") + return jsonify({ + "success": False, + "error": str(e), + "traceback": traceback.format_exc() + }), 500 + + # ============== 工具调用接口(供调试使用)============== @report_bp.route('/tools/search', methods=['POST']) diff --git a/backend/app/services/report_agent.py b/backend/app/services/report_agent.py index c968dc5..8dcb522 100644 --- a/backend/app/services/report_agent.py +++ b/backend/app/services/report_agent.py @@ -32,6 +32,336 @@ from .zep_tools import ( logger = get_logger('mirofish.report_agent') +class ReportLogger: + """ + Report Agent 详细日志记录器 + + 在报告文件夹中生成 agent_log.jsonl 文件,记录每一步详细动作。 + 每行是一个完整的 JSON 对象,包含时间戳、动作类型、详细内容等。 + """ + + def __init__(self, report_id: str): + """ + 初始化日志记录器 + + Args: + report_id: 报告ID,用于确定日志文件路径 + """ + self.report_id = report_id + self.log_file_path = os.path.join( + Config.UPLOAD_FOLDER, 'reports', report_id, 'agent_log.jsonl' + ) + self.start_time = datetime.now() + self._ensure_log_file() + + def _ensure_log_file(self): + """确保日志文件所在目录存在""" + log_dir = os.path.dirname(self.log_file_path) + os.makedirs(log_dir, exist_ok=True) + + def _get_elapsed_time(self) -> float: + """获取从开始到现在的耗时(秒)""" + return (datetime.now() - self.start_time).total_seconds() + + def log( + self, + action: str, + stage: str, + details: Dict[str, Any], + section_title: str = None, + section_index: int = None + ): + """ + 记录一条日志 + + Args: + action: 动作类型,如 'start', 'tool_call', 'llm_response', 'section_complete' 等 + stage: 当前阶段,如 'planning', 'generating', 'completed' + details: 详细内容字典,不截断 + section_title: 当前章节标题(可选) + section_index: 当前章节索引(可选) + """ + log_entry = { + "timestamp": datetime.now().isoformat(), + "elapsed_seconds": round(self._get_elapsed_time(), 2), + "report_id": self.report_id, + "action": action, + "stage": stage, + "section_title": section_title, + "section_index": section_index, + "details": details + } + + # 追加写入 JSONL 文件 + with open(self.log_file_path, 'a', encoding='utf-8') as f: + f.write(json.dumps(log_entry, ensure_ascii=False) + '\n') + + def log_start(self, simulation_id: str, graph_id: str, simulation_requirement: str): + """记录报告生成开始""" + self.log( + action="report_start", + stage="pending", + details={ + "simulation_id": simulation_id, + "graph_id": graph_id, + "simulation_requirement": simulation_requirement, + "message": "报告生成任务开始" + } + ) + + def log_planning_start(self): + """记录大纲规划开始""" + self.log( + action="planning_start", + stage="planning", + details={"message": "开始规划报告大纲"} + ) + + def log_planning_context(self, context: Dict[str, Any]): + """记录规划时获取的上下文信息""" + self.log( + action="planning_context", + stage="planning", + details={ + "message": "获取模拟上下文信息", + "context": context + } + ) + + def log_planning_complete(self, outline_dict: Dict[str, Any]): + """记录大纲规划完成""" + self.log( + action="planning_complete", + stage="planning", + details={ + "message": "大纲规划完成", + "outline": outline_dict + } + ) + + def log_section_start(self, section_title: str, section_index: int): + """记录章节生成开始""" + self.log( + action="section_start", + stage="generating", + section_title=section_title, + section_index=section_index, + details={"message": f"开始生成章节: {section_title}"} + ) + + def log_react_thought(self, section_title: str, section_index: int, iteration: int, thought: str): + """记录 ReACT 思考过程""" + self.log( + action="react_thought", + stage="generating", + section_title=section_title, + section_index=section_index, + details={ + "iteration": iteration, + "thought": thought, + "message": f"ReACT 第{iteration}轮思考" + } + ) + + def log_tool_call( + self, + section_title: str, + section_index: int, + tool_name: str, + parameters: Dict[str, Any], + iteration: int + ): + """记录工具调用""" + self.log( + action="tool_call", + stage="generating", + section_title=section_title, + section_index=section_index, + details={ + "iteration": iteration, + "tool_name": tool_name, + "parameters": parameters, + "message": f"调用工具: {tool_name}" + } + ) + + def log_tool_result( + self, + section_title: str, + section_index: int, + tool_name: str, + result: str, + iteration: int + ): + """记录工具调用结果(完整内容,不截断)""" + self.log( + action="tool_result", + stage="generating", + section_title=section_title, + section_index=section_index, + details={ + "iteration": iteration, + "tool_name": tool_name, + "result": result, # 完整结果,不截断 + "result_length": len(result), + "message": f"工具 {tool_name} 返回结果" + } + ) + + def log_llm_response( + self, + section_title: str, + section_index: int, + response: str, + iteration: int, + has_tool_calls: bool, + has_final_answer: bool + ): + """记录 LLM 响应(完整内容,不截断)""" + self.log( + action="llm_response", + stage="generating", + section_title=section_title, + section_index=section_index, + details={ + "iteration": iteration, + "response": response, # 完整响应,不截断 + "response_length": len(response), + "has_tool_calls": has_tool_calls, + "has_final_answer": has_final_answer, + "message": f"LLM 响应 (工具调用: {has_tool_calls}, 最终答案: {has_final_answer})" + } + ) + + def log_section_complete( + self, + section_title: str, + section_index: int, + content: str, + tool_calls_count: int + ): + """记录章节生成完成(完整内容,不截断)""" + self.log( + action="section_complete", + stage="generating", + section_title=section_title, + section_index=section_index, + details={ + "content": content, # 完整章节内容,不截断 + "content_length": len(content), + "tool_calls_count": tool_calls_count, + "message": f"章节 {section_title} 生成完成" + } + ) + + def log_report_complete(self, total_sections: int, total_time_seconds: float): + """记录报告生成完成""" + self.log( + action="report_complete", + stage="completed", + details={ + "total_sections": total_sections, + "total_time_seconds": round(total_time_seconds, 2), + "message": "报告生成完成" + } + ) + + def log_error(self, error_message: str, stage: str, section_title: str = None): + """记录错误""" + self.log( + action="error", + stage=stage, + section_title=section_title, + section_index=None, + details={ + "error": error_message, + "message": f"发生错误: {error_message}" + } + ) + + +class ReportConsoleLogger: + """ + Report Agent 控制台日志记录器 + + 将控制台风格的日志(INFO、WARNING等)写入报告文件夹中的 console_log.txt 文件。 + 这些日志与 agent_log.jsonl 不同,是纯文本格式的控制台输出。 + """ + + def __init__(self, report_id: str): + """ + 初始化控制台日志记录器 + + Args: + report_id: 报告ID,用于确定日志文件路径 + """ + self.report_id = report_id + self.log_file_path = os.path.join( + Config.UPLOAD_FOLDER, 'reports', report_id, 'console_log.txt' + ) + self._ensure_log_file() + self._file_handler = None + self._setup_file_handler() + + def _ensure_log_file(self): + """确保日志文件所在目录存在""" + log_dir = os.path.dirname(self.log_file_path) + os.makedirs(log_dir, exist_ok=True) + + def _setup_file_handler(self): + """设置文件处理器,将日志同时写入文件""" + import logging + + # 创建文件处理器 + self._file_handler = logging.FileHandler( + self.log_file_path, + mode='a', + encoding='utf-8' + ) + self._file_handler.setLevel(logging.INFO) + + # 使用与控制台相同的简洁格式 + formatter = logging.Formatter( + '[%(asctime)s] %(levelname)s: %(message)s', + datefmt='%H:%M:%S' + ) + self._file_handler.setFormatter(formatter) + + # 添加到 report_agent 相关的 logger + loggers_to_attach = [ + 'mirofish.report_agent', + 'mirofish.zep_tools', + ] + + for logger_name in loggers_to_attach: + target_logger = logging.getLogger(logger_name) + # 避免重复添加 + if self._file_handler not in target_logger.handlers: + target_logger.addHandler(self._file_handler) + + def close(self): + """关闭文件处理器并从 logger 中移除""" + import logging + + if self._file_handler: + loggers_to_detach = [ + 'mirofish.report_agent', + 'mirofish.zep_tools', + ] + + for logger_name in loggers_to_detach: + target_logger = logging.getLogger(logger_name) + if self._file_handler in target_logger.handlers: + target_logger.removeHandler(self._file_handler) + + self._file_handler.close() + self._file_handler = None + + def __del__(self): + """析构时确保关闭文件处理器""" + self.close() + + class ReportStatus(str, Enum): """报告状态""" PENDING = "pending" @@ -171,6 +501,11 @@ class ReportAgent: # 工具定义 self.tools = self._define_tools() + # 日志记录器(在 generate_report 中初始化) + self.report_logger: Optional[ReportLogger] = None + # 控制台日志记录器(在 generate_report 中初始化) + self.console_logger: Optional[ReportConsoleLogger] = None + logger.info(f"ReportAgent 初始化完成: graph_id={graph_id}, simulation_id={simulation_id}") def _define_tools(self) -> Dict[str, Dict[str, Any]]: @@ -579,7 +914,8 @@ class ReportAgent: section: ReportSection, outline: ReportOutline, previous_sections: List[str], - progress_callback: Optional[Callable] = None + progress_callback: Optional[Callable] = None, + section_index: int = 0 ) -> str: """ 使用ReACT模式生成单个章节内容 @@ -596,12 +932,17 @@ class ReportAgent: outline: 完整大纲 previous_sections: 之前章节的内容(用于保持连贯性) progress_callback: 进度回调 + section_index: 章节索引(用于日志记录) Returns: 章节内容(Markdown格式) """ logger.info(f"ReACT生成章节: {section.title}") + # 记录章节开始日志 + if self.report_logger: + self.report_logger.log_section_start(section.title, section_index) + # 构建系统prompt - 优化后强调工具使用和引用原文 # 确定当前章节的标题级别 section_level = 2 # 默认为二级标题(##) @@ -795,8 +1136,23 @@ class ReportAgent: logger.debug(f"LLM响应: {response[:200]}...") + # 检查是否有工具调用和最终答案 + has_tool_calls = bool(self._parse_tool_calls(response)) + has_final_answer = "Final Answer:" in response + + # 记录 LLM 响应日志 + if self.report_logger: + self.report_logger.log_llm_response( + section_title=section.title, + section_index=section_index, + response=response, + iteration=iteration + 1, + has_tool_calls=has_tool_calls, + has_final_answer=has_final_answer + ) + # 检查是否有最终答案 - if "Final Answer:" in response: + if has_final_answer: # 如果工具调用次数不足,提醒需要更多检索 if tool_calls_count < min_tool_calls: messages.append({"role": "assistant", "content": response}) @@ -816,6 +1172,16 @@ class ReportAgent: # 提取最终答案 final_answer = response.split("Final Answer:")[-1].strip() logger.info(f"章节 {section.title} 生成完成(工具调用: {tool_calls_count}次)") + + # 记录章节完成日志 + if self.report_logger: + self.report_logger.log_section_complete( + section_title=section.title, + section_index=section_index, + content=final_answer, + tool_calls_count=tool_calls_count + ) + return final_answer # 解析工具调用 @@ -854,11 +1220,32 @@ class ReportAgent: if tool_calls_count >= self.MAX_TOOL_CALLS_PER_SECTION: break + # 记录工具调用日志 + if self.report_logger: + self.report_logger.log_tool_call( + section_title=section.title, + section_index=section_index, + tool_name=call["name"], + parameters=call.get("parameters", {}), + iteration=iteration + 1 + ) + result = self._execute_tool( call["name"], call.get("parameters", {}), report_context=report_context ) + + # 记录工具结果日志 + if self.report_logger: + self.report_logger.log_tool_result( + section_title=section.title, + section_index=section_index, + tool_name=call["name"], + result=result, + iteration=iteration + 1 + ) + tool_results.append(f"═══ 工具 {call['name']} 返回 ═══\n{result}") tool_calls_count += 1 @@ -893,13 +1280,25 @@ class ReportAgent: ) if "Final Answer:" in response: - return response.split("Final Answer:")[-1].strip() + final_answer = response.split("Final Answer:")[-1].strip() + else: + final_answer = response - return response + # 记录章节完成日志 + if self.report_logger: + self.report_logger.log_section_complete( + section_title=section.title, + section_index=section_index, + content=final_answer, + tool_calls_count=tool_calls_count + ) + + return final_answer def generate_report( self, - progress_callback: Optional[Callable[[str, int, str], None]] = None + progress_callback: Optional[Callable[[str, int, str], None]] = None, + report_id: Optional[str] = None ) -> Report: """ 生成完整报告(分章节实时输出) @@ -917,13 +1316,17 @@ class ReportAgent: Args: progress_callback: 进度回调函数 (stage, progress, message) + report_id: 报告ID(可选,如果不传则自动生成) Returns: Report: 完整报告 """ import uuid - report_id = f"report_{uuid.uuid4().hex[:12]}" + # 如果没有传入 report_id,则自动生成 + if not report_id: + report_id = f"report_{uuid.uuid4().hex[:12]}" + start_time = datetime.now() report = Report( report_id=report_id, @@ -940,6 +1343,18 @@ class ReportAgent: try: # 初始化:创建报告文件夹并保存初始状态 ReportManager._ensure_report_folder(report_id) + + # 初始化日志记录器(结构化日志 agent_log.jsonl) + self.report_logger = ReportLogger(report_id) + self.report_logger.log_start( + simulation_id=self.simulation_id, + graph_id=self.graph_id, + simulation_requirement=self.simulation_requirement + ) + + # 初始化控制台日志记录器(console_log.txt) + self.console_logger = ReportConsoleLogger(report_id) + ReportManager.update_progress( report_id, "pending", 0, "初始化报告...", completed_sections=[] @@ -953,6 +1368,9 @@ class ReportAgent: completed_sections=[] ) + # 记录规划开始日志 + self.report_logger.log_planning_start() + if progress_callback: progress_callback("planning", 0, "开始规划报告大纲...") @@ -962,6 +1380,9 @@ class ReportAgent: ) report.outline = outline + # 记录规划完成日志 + self.report_logger.log_planning_complete(outline.to_dict()) + # 保存大纲到文件 ReportManager.save_outline(report_id, outline) ReportManager.update_progress( @@ -1007,7 +1428,8 @@ class ReportAgent: stage, base_progress + int(prog * 0.7 / total_sections), msg - ) if progress_callback else None + ) if progress_callback else None, + section_index=section_num ) section.content = section_content @@ -1037,7 +1459,8 @@ class ReportAgent: section=subsection, outline=outline, previous_sections=generated_sections, - progress_callback=None + progress_callback=None, + section_index=section_num * 100 + subsection_num # 子章节索引 ) subsection.content = subsection_content generated_sections.append(f"### {subsection.title}\n\n{subsection_content}") @@ -1077,6 +1500,16 @@ class ReportAgent: report.status = ReportStatus.COMPLETED report.completed_at = datetime.now().isoformat() + # 计算总耗时 + total_time_seconds = (datetime.now() - start_time).total_seconds() + + # 记录报告完成日志 + if self.report_logger: + self.report_logger.log_report_complete( + total_sections=total_sections, + total_time_seconds=total_time_seconds + ) + # 保存最终报告 ReportManager.save_report(report) ReportManager.update_progress( @@ -1088,6 +1521,12 @@ class ReportAgent: progress_callback("completed", 100, "报告生成完成") logger.info(f"报告生成完成: {report_id}") + + # 关闭控制台日志记录器 + if self.console_logger: + self.console_logger.close() + self.console_logger = None + return report except Exception as e: @@ -1095,6 +1534,10 @@ class ReportAgent: report.status = ReportStatus.FAILED report.error = str(e) + # 记录错误日志 + if self.report_logger: + self.report_logger.log_error(str(e), "failed") + # 保存失败状态 try: ReportManager.save_report(report) @@ -1105,6 +1548,11 @@ class ReportAgent: except Exception: pass # 忽略保存失败的错误 + # 关闭控制台日志记录器 + if self.console_logger: + self.console_logger.close() + self.console_logger = None + return report def chat( @@ -1342,6 +1790,139 @@ class ReportManager: """获取章节Markdown文件路径""" return os.path.join(cls._get_report_folder(report_id), f"section_{section_index:02d}.md") + @classmethod + def _get_agent_log_path(cls, report_id: str) -> str: + """获取 Agent 日志文件路径""" + return os.path.join(cls._get_report_folder(report_id), "agent_log.jsonl") + + @classmethod + def _get_console_log_path(cls, report_id: str) -> str: + """获取控制台日志文件路径""" + return os.path.join(cls._get_report_folder(report_id), "console_log.txt") + + @classmethod + def get_console_log(cls, report_id: str, from_line: int = 0) -> Dict[str, Any]: + """ + 获取控制台日志内容 + + 这是报告生成过程中的控制台输出日志(INFO、WARNING等), + 与 agent_log.jsonl 的结构化日志不同。 + + Args: + report_id: 报告ID + from_line: 从第几行开始读取(用于增量获取,0 表示从头开始) + + Returns: + { + "logs": [日志行列表], + "total_lines": 总行数, + "from_line": 起始行号, + "has_more": 是否还有更多日志 + } + """ + log_path = cls._get_console_log_path(report_id) + + if not os.path.exists(log_path): + return { + "logs": [], + "total_lines": 0, + "from_line": 0, + "has_more": False + } + + logs = [] + total_lines = 0 + + with open(log_path, 'r', encoding='utf-8') as f: + for i, line in enumerate(f): + total_lines = i + 1 + if i >= from_line: + # 保留原始日志行,去掉末尾换行符 + logs.append(line.rstrip('\n\r')) + + return { + "logs": logs, + "total_lines": total_lines, + "from_line": from_line, + "has_more": False # 已读取到末尾 + } + + @classmethod + def get_console_log_stream(cls, report_id: str) -> List[str]: + """ + 获取完整的控制台日志(一次性获取全部) + + Args: + report_id: 报告ID + + Returns: + 日志行列表 + """ + result = cls.get_console_log(report_id, from_line=0) + return result["logs"] + + @classmethod + def get_agent_log(cls, report_id: str, from_line: int = 0) -> Dict[str, Any]: + """ + 获取 Agent 日志内容 + + Args: + report_id: 报告ID + from_line: 从第几行开始读取(用于增量获取,0 表示从头开始) + + Returns: + { + "logs": [日志条目列表], + "total_lines": 总行数, + "from_line": 起始行号, + "has_more": 是否还有更多日志 + } + """ + log_path = cls._get_agent_log_path(report_id) + + if not os.path.exists(log_path): + return { + "logs": [], + "total_lines": 0, + "from_line": 0, + "has_more": False + } + + logs = [] + total_lines = 0 + + with open(log_path, 'r', encoding='utf-8') as f: + for i, line in enumerate(f): + total_lines = i + 1 + if i >= from_line: + try: + log_entry = json.loads(line.strip()) + logs.append(log_entry) + except json.JSONDecodeError: + # 跳过解析失败的行 + continue + + return { + "logs": logs, + "total_lines": total_lines, + "from_line": from_line, + "has_more": False # 已读取到末尾 + } + + @classmethod + def get_agent_log_stream(cls, report_id: str) -> List[Dict[str, Any]]: + """ + 获取完整的 Agent 日志(用于一次性获取全部) + + Args: + report_id: 报告ID + + Returns: + 日志条目列表 + """ + result = cls.get_agent_log(report_id, from_line=0) + return result["logs"] + @classmethod def save_outline(cls, report_id: str, outline: ReportOutline) -> None: """ diff --git a/frontend/src/components/GraphPanel.vue b/frontend/src/components/GraphPanel.vue index 95ea121..bc91448 100644 --- a/frontend/src/components/GraphPanel.vue +++ b/frontend/src/components/GraphPanel.vue @@ -30,6 +30,24 @@ {{ isSimulating ? 'GraphRAG长短期记忆实时更新中' : '实时更新中...' }} + +
+
+ + + + + +
+ 还有少量内容处理中,建议稍后手动刷新图谱 + +
+
@@ -235,6 +253,22 @@ const graphSvg = ref(null) const selectedItem = ref(null) const showEdgeLabels = ref(true) // 默认显示边标签 const expandedSelfLoops = ref(new Set()) // 展开的自环项 +const showSimulationFinishedHint = ref(false) // 模拟结束后的提示 +const wasSimulating = ref(false) // 追踪之前是否在模拟中 + +// 关闭模拟结束提示 +const dismissFinishedHint = () => { + showSimulationFinishedHint.value = false +} + +// 监听 isSimulating 变化,检测模拟结束 +watch(() => props.isSimulating, (newValue, oldValue) => { + if (wasSimulating.value && !newValue) { + // 从模拟中变为非模拟状态,显示结束提示 + showSimulationFinishedHint.value = true + } + wasSimulating.value = newValue +}, { immediate: true }) // 切换自环项展开/折叠状态 const toggleSelfLoop = (id) => { @@ -1196,6 +1230,50 @@ input:checked + .slider:before { 50% { opacity: 1; transform: scale(1.15); filter: drop-shadow(0 0 8px rgba(76, 175, 80, 0.6)); } } +/* 模拟结束后的提示样式 */ +.graph-building-hint.finished-hint { + background: rgba(0, 0, 0, 0.65); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.finished-hint .hint-icon-wrapper { + display: flex; + align-items: center; + justify-content: center; +} + +.finished-hint .hint-icon { + width: 18px; + height: 18px; + color: #FFF; +} + +.finished-hint .hint-text { + flex: 1; + white-space: nowrap; +} + +.hint-close-btn { + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + background: rgba(255, 255, 255, 0.2); + border: none; + border-radius: 50%; + cursor: pointer; + color: #FFF; + transition: all 0.2s; + margin-left: 8px; + flex-shrink: 0; +} + +.hint-close-btn:hover { + background: rgba(255, 255, 255, 0.35); + transform: scale(1.1); +} + /* Loading spinner */ .loading-spinner { width: 40px;