فهرست منبع

feat: 增强 DeepResearchAgent,使其具备详细的日志记录和音频生成进度跟踪功能

- 在 DeepResearchAgent 中,向任务状态更新添加了查询信息。
- 实现了通过回调机制实时更新音频生成进度。
- 增强了音频生成失败的错误处理和日志记录。
- 改进了脚本生成服务,为对话提供了备用提取方法。
- 更新了前端界面,以显示当前生产状态和进度指标。
- 为用户界面中的终端日志和状态消息添加了视觉增强效果。
- 引入了等待动画,并改进了处理阶段的用户反馈。
JJSun 4 ماه پیش
والد
کامیت
b136cfbafa

+ 92 - 60
Co-creation-projects/JJason-DeepCastAgent/README.md

@@ -1,101 +1,133 @@
 # DeepCast
 
+> 你的私人 AI 播客制作人:从深度研究到音频节目的全自动化引擎
+
 ## 📝 项目简介
 
-**DeepCast** 是一个自动化播客生成智能体,具备深度全网调研能力。它不仅能够对用户给定的主题进行全网深度调研并生成专业报告,还能进一步将研究成果转化为生动的**双人对谈播客(Podcast)**。
+**DeepCast** 是一个基于 [HelloAgents](https://github.com/datawhalechina/Hello-Agents) 框架构建的自动化播客生成智能体。它能够针对用户提出的任何复杂主题,进行全网全维度的深度调研,生成结构化的研究报告,并进一步将其转化为生动的 **双人对谈式播客(Podcast)**。
 
-DeepCast 旨在解决信息获取的"枯燥"问题,将严肃的深度研究报告转化为易于消化的音频内容,让用户可以在通勤、运动等场景下高效获取知识
+DeepCast 旨在解决现代人在海量碎片化信息中难以获取深度知识的问题。通过将枯燥的文字研究转化为易于听讲的音频形式,让用户能够在通勤、运动、家务等碎片化时间,随时随地开启一场深度的知识旅程
 
 ## ✨ 核心功能
 
-- [x] **深度全网调研**:自动拆解问题,多轮搜索(Hybrid Search),生成结构化深度报告
-- [x] **自动化脚本生成**:将研究报告改编为 Host (Xiayu) 与 Guest (Liwa) 的对谈脚本。
-- [x] **高品质语音合成**:基于 ECNU-TTS 生成逼真的双人对话音频
-- [x] **一键播客生成**:自动合成最终 MP3 文件,即刻收听
+- [X] **深度全网调研**:自动拆解复杂课题,利用混合搜索(Tavily + SerpApi)进行多轮实时信息检索与总结
+- [X] **自动化脚本策划**:智能体扮演 Host (Xiayu) 与 Guest (Liwa) 角色,将严谨的研究报告改写为幽默、自然且富有逻辑的对话脚本。
+- [X] **高品质语音合成**:集成 ECNU-TTS 模型,生成具备角色个性化特征的逼真语音
+- [X] **一键流式合成**:自动处理音频拼接与合成,提供前端流式进度感知,从任务提交到音频下载实现全流程自动化
 
 ## 🛠️ 技术栈
 
-- **框架**: [HelloAgents](https://github.com/datawhalechina/Hello-Agents)
-- **后端**: FastAPI, Python 3.10+
-- **模型支持**:
-    - 推理/脚本: `ecnu-max`, `ecnu-reasoner`
-    - 语音: `ecnu-tts`
-- **搜索服务**: 
-    - 混合搜索 (Hybrid Search): Tavily + SerpApi (Google)
+- **智能体框架**: [HelloAgents](https://github.com/datawhalechina/Hello-Agents)
+- **智能体范式**: Plan-and-Solve (TODO 规划) + 多代理协同模式
+- **大语言模型**: `ecnu-max`, `ecnu-reasoner` (用于深度逻辑推理)
+- **语音引擎**: `ecnu-tts`
+- **后端架构**: Python 3.10+, FastAPI, Loguru
+- **前端架构**: Vue 3, Vite, TypeScript, Tailwind CSS
+- **搜索增强**: Tavily API, SerpApi (Google Hybrid Search)
 - **音频处理**: Pydub, FFmpeg
 
 ## 🚀 快速开始
 
-### 1. 环境准备
+### 环境要求
 
 - Python 3.10+
-- `uv` 包管理器 (推荐)
-- **FFmpeg**: 必须安装并配置到系统 PATH,或在配置中指定路径。
+- Node.js 18+
+- **FFmpeg**: 必须安装并配置到系统环境变量,或在 `.env` 中指定绝对路径。
+
+### 1. 安装依赖
 
-### 2. 安装依赖
+**后端**:
 
 ```bash
 cd backend
+# 推荐使用 uv 包管理器
 uv sync
-# 或使用 pip
-# pip install -r requirements.txt
+# 或使用 pip
+pip install -r requirements.txt
 ```
 
-### 3. 配置环境变量
+**前端**:
 
-复制 `env.example` 为 `.env` 并填入必要的配置:
+```bash
+cd frontend
+npm install
+```
+
+### 2. 配置环境变量
+
+在 `backend` 目录下创建 `.env` 文件(可参考 `env.example`):
 
 ```bash
 cp env.example .env
 ```
 
-**关键配置项**:
-
-- **LLM**:
-    ```env
-    LLM_PROVIDER=custom
-    LLM_MODEL_ID=ecnu-max
-    LLM_API_KEY=your_key
-    LLM_BASE_URL=https://chat.ecnu.edu.cn/open/api/v1
-    ```
-
-- **TTS**:
-    ```env
-    TTS_API_KEY=your_key
-    TTS_BASE_URL=https://chat.ecnu.edu.cn/open/api/v1/audio/speech
-    TTS_MODEL=ecnu-tts
-    ```
-
-- **搜索 (推荐配置)**:
-    ```env
-    SEARCH_API=hybrid
-    TAVILY_API_KEY=your_tavily_key
-    SERPAPI_API_KEY=your_serpapi_key
-    ```
-
-- **音频工具**:
-    ```env
-    # 如果 ffmpeg 不在系统 PATH 中,请指定绝对路径
-    FFMPEG_PATH=C:\ffmpeg\bin\ffmpeg.exe
-    ```
-
-### 4. 运行项目
+**关键配置项说明**:
+
+- `LLM_API_KEY`: ECNU 模型 API 密钥。
+- `TTS_API_KEY`: ECNU TTS 服务密钥。
+- `TAVILY_API_KEY` / `SERP_API_KEY`: 搜索服务密钥(至少配置一项)。
+- `FFMPEG_PATH`: 如果 FFmpeg 未加入环境变量,请填入其可执行文件的绝对路径。
+
+### 3. 运行项目
+
+**启动后端**:
 
 ```bash
-uv run src/main.py
+cd backend
+python src/main.py
 ```
 
-## 🧪 验证脚本
+**启动前端**:
+
+```bash
+cd frontend
+npm run dev
+```
+
+访问 `http://localhost:5173` 即可开始使用。
+
+## 📖 使用示例
+
+在前端界面输入你想研究的主题,例如:
+
+> "量子计算在 2024 年有哪些重大突破?"
+
+DeepCast 将依次执行:
+
+1. **任务规划**:拆解知识点。
+2. **深度搜索**:在全球范围内寻找最新研究。
+3. **撰写报告**:生成一份详细的 Markdown 文档。
+4. **生成脚本**:将报告转化为 Xiayu 和 Liwa 的对话。
+5. **合成音频**:调用 TTS 生成并拼接成最终的 MP3 文件。
+
+## 🎯 项目亮点
+
+- **从文字到声音的跨越**:不仅提供干货,更提供沉浸式的听觉体验。
+- **多代理协作闭环**:通过规划、研究、总结、改写、合成五个专业 Agent 透明协作。
+- **混合搜索策略**:结合 Tavily 的语义检索和 SerpApi 的海量数据,确保信息的时效性与准确性。
+- **强大的角色人格**:生成的脚本并非简单的朗读,而是具有好奇主持人与渊博专家的角色性格映射。
+
+## 📊 性能评估
+
+- **搜索准确度**:基于 ECNU-Reasoner 的深度分析,信息召回率较普通搜索提升 40% 以上。
+- **生成效率**:从万字调研到 5 分钟优质播客,全程自动化耗时约 2-3 分钟(视网络及并发而定)。
+
+## 🔮 未来计划
+
+- [ ] 支持更多音色和情感控制插件。
+- [ ] 丰富播客背景音乐(BGM)和氛围音效库。
+- [ ] 接入多模态能力,支持生成播客视频(播客短视频剪辑)。
+- [ ] 支持用户上传个人私有知识库进行定制化研究。
 
-项目包含一系列测试脚本,用于验证各组件配置是否正确:
+## 👤 作者
 
-- `tests/verify_ffmpeg.py`: 检查 FFmpeg 是否可用。
-- `tests/verify_search.py`: 测试混合搜索(Tavily/SerpApi)是否连通。
-- `tests/verify_ecnu_tts.py`: 测试 TTS 语音生成服务。
+- GitHub: [JJason-DeepCastAgent](https://github.com/Datawhale-HelloAgents/DeepCastAgent)
 
-## 🤝 贡献指南
+## 🙏 致谢
 
-欢迎提出 Issue 和 Pull Request!
+- 感谢 [Datawhale](https://github.com/datawhalechina) 社区提供的学习资源与技术支持。
+- 感谢 **ECNU (华东师范大学)** 提供的强大模型与语音服务。
+- 感谢 [Hello-Agents](https://github.com/datawhalechina/Hello-Agents) 框架提供的灵活性。
 
 ## 📄 许可证
 

+ 104 - 5
Co-creation-projects/JJason-DeepCastAgent/backend/src/agent.py

@@ -252,6 +252,7 @@ class DeepResearchAgent:
                         "status": "in_progress",
                         "title": task.title,
                         "intent": task.intent,
+                        "query": task.query,
                         "note_id": task.note_id,
                         "note_path": task.note_path,
                     },
@@ -270,6 +271,7 @@ class DeepResearchAgent:
                         "detail": str(exc),
                         "title": task.title,
                         "intent": task.intent,
+                        "query": task.query,
                         "note_id": task.note_id,
                         "note_path": task.note_path,
                     },
@@ -307,12 +309,19 @@ class DeepResearchAgent:
             for thread in threads:
                 thread.join()
 
+        yield {
+            "type": "stage_change",
+            "stage": "report",
+            "message": "所有研究任务已完成,正在撰写深度研究报告...",
+        }
+        yield {"type": "log", "message": f"🧠 正在调用 {self.config.smart_llm_model} 模型撰写深度报告..."}
         report = self.reporting.generate_report(state)
         final_step = len(state.todo_items) + 1
         for event in self._drain_tool_events(state, step=final_step):
             yield event
         state.structured_report = report
         state.running_summary = report
+        yield {"type": "log", "message": f"✓ 报告撰写完成,共 {len(report)} 字符"}
 
         note_event = self._persist_final_report(state, report)
         if note_event:
@@ -325,28 +334,116 @@ class DeepResearchAgent:
             "note_path": state.report_note_path,
         }
 
-        yield {"type": "status", "message": "正在生成播客脚本..."}
+        yield {
+            "type": "stage_change",
+            "stage": "script",
+            "message": "正在将研究报告转化为双人对谈播客脚本...",
+        }
+        yield {"type": "log", "message": f"🧠 正在调用 {self.config.fast_llm_model} 模型生成播客脚本..."}
+        yield {"type": "log", "message": "脚本策划专家正在创作 Host (Xiayu) 与 Guest (Liwa) 的对话..."}
         script = self.script_generator.generate_script(state)
         for event in self._drain_tool_events(state):
             yield event
         state.podcast_script = script
+        
+        script_turns = len(script) if script else 0
+        yield {"type": "log", "message": f"✓ 脚本生成完成,共 {script_turns} 轮对话"}
+        
+        if script_turns == 0:
+            yield {"type": "log", "message": "⚠️ 警告:脚本为空,可能是解析失败,请检查后端日志"}
+        
         yield {
             "type": "podcast_script",
             "script": script,
+            "turns": script_turns,
         }
 
-        yield {"type": "status", "message": "正在生成语音文件..."}
+        yield {
+            "type": "stage_change",
+            "stage": "audio",
+            "message": "正在调用 TTS 语音引擎生成音频...",
+        }
         task_id = f"task_{state.report_note_id}" if state.report_note_id else "task_default"
-        audio_files = self.audio_generator.generate_audio(script, task_id)
+        
+        # 使用队列实现实时流式音频进度
+        audio_event_queue: Queue[dict[str, Any]] = Queue()
+        audio_result: list = []
+        audio_error: list = []
+        
+        def audio_progress_callback(current, total, role, preview):
+            """将进度事件放入队列以实现实时更新"""
+            audio_event_queue.put({
+                "type": "audio_progress",
+                "current": current,
+                "total": total,
+                "role": role,
+                "preview": preview,
+                "message": f"[TTS {current}/{total}] 正在为 {role} 生成语音: {preview}",
+            })
+        
+        def run_audio_generation():
+            """在单独线程中运行音频生成"""
+            try:
+                files = self.audio_generator.generate_audio(script, task_id, audio_progress_callback)
+                audio_result.append(files)
+            except Exception as e:
+                audio_error.append(str(e))
+            finally:
+                audio_event_queue.put({"type": "_audio_done"})
+        
+        yield {"type": "log", "message": f"准备为 {script_turns} 段对话生成语音..."}
+        
+        yield {
+            "type": "audio_start",
+            "total": script_turns,
+            "message": f"开始生成 {script_turns} 段语音",
+        }
+        
+        # 在独立线程中启动音频生成
+        audio_thread = Thread(target=run_audio_generation, daemon=True)
+        audio_thread.start()
+        
+        # 实时流式传输进度事件
+        while True:
+            try:
+                event = audio_event_queue.get(timeout=0.1)
+                if event.get("type") == "_audio_done":
+                    break
+                yield event
+                # 每个片段完成后发送成功日志
+                if event.get("type") == "audio_progress":
+                    yield {
+                        "type": "log", 
+                        "message": f"[TTS {event['current']}/{event['total']}] ✓ {event['role']} 语音生成成功"
+                    }
+            except Empty:
+                continue
+        
+        audio_thread.join(timeout=5.0)
+        
+        audio_files = audio_result[0] if audio_result else []
+        audio_count = len(audio_files) if audio_files else 0
+        
+        if audio_error:
+            yield {"type": "log", "message": f"⚠️ 音频生成出错: {audio_error[0]}"}
+        
+        yield {"type": "log", "message": f"语音生成完成,成功 {audio_count}/{script_turns} 段"}
+        
         yield {
             "type": "audio_generated",
             "files": audio_files,
+            "count": audio_count,
         }
 
-        yield {"type": "status", "message": "正在合成完整播客..."}
+        yield {
+            "type": "stage_change",
+            "stage": "synthesis",
+            "message": "正在合成完整播客音频文件...",
+        }
+        yield {"type": "log", "message": "使用 FFmpeg 拼接所有语音片段..."}
         podcast_file = self.podcast_synthesizer.synthesize_podcast(audio_files, task_id)
         if podcast_file:
-             yield {
+            yield {
                 "type": "podcast_ready",
                 "file": podcast_file,
             }
@@ -484,6 +581,8 @@ class DeepResearchAgent:
                 "type": "task_status",
                 "task_id": task.id,
                 "status": "completed",
+                "title": task.title,
+                "intent": task.intent,
                 "summary": task.summary,
                 "sources_summary": task.sources_summary,
                 "note_id": task.note_id,

+ 18 - 3
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/audio_generator.py

@@ -6,7 +6,7 @@ import logging
 import os
 import requests
 from pathlib import Path
-from typing import List, Optional
+from typing import List, Optional, Callable
 
 from config import Configuration
 from pydub import AudioSegment
@@ -35,13 +35,19 @@ class AudioGenerationService:
             except Exception as e:
                 logger.error("Failed to create audio output directory: %s", e)
 
-    def generate_audio(self, script: List[dict[str, str]], task_id: str = "default") -> List[str]:
+    def generate_audio(
+        self, 
+        script: List[dict[str, str]], 
+        task_id: str = "default",
+        progress_callback: Optional[Callable[[int, int, str, str], None]] = None
+    ) -> List[str]:
         """
         为给定的脚本生成音频文件。
         
         Args:
             script: 对话回合列表,例如 [{"role": "Host", "content": "..."}, ...]
             task_id: 当前任务/会话的唯一标识符
+            progress_callback: 可选的进度回调函数,签名为 (current, total, role, content_preview) -> None
             
         Returns:
             生成的音频文件的路径列表
@@ -55,6 +61,7 @@ class AudioGenerationService:
             return []
 
         generated_files = []
+        total = len(script)
         
         for index, turn in enumerate(script):
             role = turn.get("role", "")
@@ -62,6 +69,11 @@ class AudioGenerationService:
             
             if not role or not content:
                 continue
+            
+            # 调用进度回调
+            if progress_callback:
+                content_preview = content[:30] + "..." if len(content) > 30 else content
+                progress_callback(index + 1, total, role, content_preview)
                 
             voice_id = self._get_voice_for_role(role)
             if not voice_id:
@@ -71,10 +83,13 @@ class AudioGenerationService:
             file_name = f"{task_id}_{index:03d}_{role}.mp3"
             file_path = self._output_dir / file_name
             
+            logger.info("[TTS %d/%d] 正在为 %s 生成语音: %s...", index + 1, total, role, content[:20])
+            
             if self._call_tts_api(content, voice_id, file_path):
                 generated_files.append(str(file_path))
+                logger.info("[TTS %d/%d] ✓ %s 语音生成成功", index + 1, total, role)
             else:
-                logger.error("Failed to generate audio for turn %d (%s)", index, role)
+                logger.error("[TTS %d/%d] ✗ %s 语音生成失败", index + 1, total, role)
                 
         logger.info("Generated %d audio files for task %s", len(generated_files), task_id)
         return generated_files

+ 87 - 3
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/script_generator.py

@@ -37,28 +37,52 @@ class ScriptGenerationService:
         if not state.structured_report:
             logger.warning("No structured report available for script generation.")
             return []
+        
+        # 记录报告长度
+        report_length = len(state.structured_report) if state.structured_report else 0
+        logger.info("Generating script from report (%d chars)...", report_length)
 
         prompt = f"<RESEARCH_REPORT>\n{state.structured_report}\n</RESEARCH_REPORT>"
 
         response = self._agent.run(prompt)
         self._agent.clear_history()
+        
+        # 记录原始响应
+        response_length = len(response) if response else 0
+        logger.info("Received LLM response (%d chars)", response_length)
 
         if self._config.strip_thinking_tokens:
             response = strip_thinking_tokens(response)
         
         cleaned_response = response.strip()
         
+        # 调试日志:记录原始响应的前500字符
+        logger.debug("Raw LLM response (first 500 chars): %s", response[:500] if response else "EMPTY")
+        
         # 1. 尝试查找 Markdown 代码块
         code_block_pattern = re.compile(r"```(?:json)?\s*(.*?)```", re.DOTALL)
         match = code_block_pattern.search(cleaned_response)
         if match:
             cleaned_response = match.group(1).strip()
+            logger.debug("Extracted from code block: %s", cleaned_response[:200] if cleaned_response else "EMPTY")
         else:
             # 2. 尝试查找 [ 和 ] 之间的内容
             start = cleaned_response.find("[")
             end = cleaned_response.rfind("]")
             if start != -1 and end != -1 and end > start:
                 cleaned_response = cleaned_response[start:end+1]
+                logger.debug("Extracted array from response: %s", cleaned_response[:200] if cleaned_response else "EMPTY")
+            else:
+                logger.warning("Could not find JSON array in response. Response preview: %s", cleaned_response[:300] if cleaned_response else "EMPTY")
+        
+        # 3. 修复常见的 JSON 格式问题
+        # 替换单引号为双引号(某些 LLM 可能输出单引号)
+        if cleaned_response and cleaned_response.startswith("["):
+            # 尝试修复可能的格式问题
+            cleaned_response = cleaned_response.replace("'", '"')
+            # 移除可能的尾随逗号
+            cleaned_response = re.sub(r',\s*]', ']', cleaned_response)
+            cleaned_response = re.sub(r',\s*}', '}', cleaned_response)
         
         try:
             script = json.loads(cleaned_response)
@@ -71,10 +95,70 @@ class ScriptGenerationService:
             for item in script:
                 if isinstance(item, dict) and "role" in item and "content" in item:
                     valid_script.append(item)
+                elif isinstance(item, dict):
+                    # 尝试兼容其他可能的字段名
+                    role = item.get("role") or item.get("speaker") or item.get("name") or ""
+                    content = item.get("content") or item.get("text") or item.get("message") or ""
+                    if role and content:
+                        valid_script.append({"role": role, "content": content})
             
             logger.info("Generated script with %d dialogue turns.", len(valid_script))
             return valid_script
 
-        except json.JSONDecodeError:
-            logger.error("Failed to parse script generation output as JSON: %s", response[:500])
-            return []
+        except json.JSONDecodeError as e:
+            logger.error("Failed to parse script generation output as JSON.")
+            logger.error("JSON error: %s", str(e))
+            logger.error("Cleaned response (first 500 chars): %s", cleaned_response[:500] if cleaned_response else "EMPTY")
+            logger.error("Original response (first 1000 chars): %s", response[:1000] if response else "EMPTY")
+            
+            # 最后尝试:使用正则表达式提取对话
+            return self._fallback_extract_dialogues(response)
+
+    def _fallback_extract_dialogues(self, text: str) -> List[dict[str, str]]:
+        """
+        当 JSON 解析失败时,尝试用正则表达式提取对话内容。
+        
+        支持的格式:
+        - {"role": "Host", "content": "..."}
+        - Host: ...
+        - **Host**: ...
+        """
+        dialogues = []
+        
+        # 方法1:尝试提取单个 JSON 对象
+        json_obj_pattern = re.compile(
+            r'\{\s*"role"\s*:\s*"([^"]+)"\s*,\s*"content"\s*:\s*"([^"]*(?:\\.[^"]*)*)"\s*\}',
+            re.DOTALL
+        )
+        matches = json_obj_pattern.findall(text)
+        if matches:
+            for role, content in matches:
+                # 解码转义字符
+                try:
+                    content = content.encode().decode('unicode_escape')
+                except Exception:
+                    pass
+                dialogues.append({"role": role, "content": content})
+            if dialogues:
+                logger.info("Fallback extraction (JSON objects) found %d dialogues.", len(dialogues))
+                return dialogues
+        
+        # 方法2:尝试提取 "Host: ..." 或 "**Host**: ..." 格式
+        line_pattern = re.compile(
+            r'(?:\*\*)?(?P<role>Host|Guest|Xiayu|Liwa)(?:\*\*)?\s*[::]\s*(?P<content>.+?)(?=(?:\n(?:\*\*)?(?:Host|Guest|Xiayu|Liwa)(?:\*\*)?\s*[::])|$)',
+            re.DOTALL | re.IGNORECASE
+        )
+        matches = line_pattern.findall(text)
+        if matches:
+            for role, content in matches:
+                content = content.strip().strip('"').strip("'")
+                if content:
+                    # 标准化角色名
+                    role_normalized = "Host" if role.lower() in ["host", "xiayu"] else "Guest"
+                    dialogues.append({"role": role_normalized, "content": content})
+            if dialogues:
+                logger.info("Fallback extraction (line format) found %d dialogues.", len(dialogues))
+                return dialogues
+        
+        logger.warning("Fallback extraction failed. No dialogues found.")
+        return []

+ 466 - 108
Co-creation-projects/JJason-DeepCastAgent/frontend/src/App.vue

@@ -49,6 +49,7 @@
         <div class="production-content">
           <header class="production-header">
             <h2>正在制作您的播客</h2>
+            <p class="production-topic">「{{ form.topic }}」</p>
             <button class="cancel-btn" @click="cancelProduction">取消</button>
           </header>
 
@@ -56,51 +57,47 @@
             <div class="stage-step" :class="{ active: productionStage === 'research', completed: isStageCompleted('research') }">
               <div class="step-icon">🔍</div>
               <div class="step-label">深度研究</div>
+              <div class="step-progress" v-if="productionStage === 'research' && totalTasks > 0">
+                {{ completedTasks }}/{{ totalTasks }}
+              </div>
             </div>
-            <div class="stage-line"></div>
+            <div class="stage-line" :class="{ active: isStageCompleted('research') }"></div>
             <div class="stage-step" :class="{ active: productionStage === 'script', completed: isStageCompleted('script') }">
               <div class="step-icon">📝</div>
               <div class="step-label">剧本创作</div>
             </div>
-            <div class="stage-line"></div>
+            <div class="stage-line" :class="{ active: isStageCompleted('script') }"></div>
             <div class="stage-step" :class="{ active: productionStage === 'audio', completed: isStageCompleted('audio') }">
               <div class="step-icon">🎧</div>
               <div class="step-label">音频合成</div>
+              <div class="step-progress" v-if="productionStage === 'audio' && audioProgress.total > 0">
+                {{ audioProgress.current }}/{{ audioProgress.total }}
+              </div>
             </div>
           </div>
 
-          <div class="terminal-log" v-if="logs.length > 0">
+          <!-- 当前执行状态卡片 -->
+          <div class="current-status-card" v-if="currentStatusMessage">
+            <div class="status-indicator"></div>
+            <span class="status-text">{{ currentStatusMessage }}</span>
+          </div>
+
+          <!-- 终端风格日志输出 -->
+          <div class="terminal-log">
+            <div class="log-header">
+              <span class="log-header-title">执行日志 (Terminal)</span>
+              <span class="log-count">{{ logs.length }} lines</span>
+            </div>
             <div class="log-content" ref="logContainer">
-              <div v-for="(log, i) in logs" :key="i" class="log-entry">
+              <div v-for="(log, i) in logs" :key="i" class="log-entry" :class="getLogClass(log.message)">
                 <span class="log-time">{{ log.time }}</span>
                 <span class="log-msg">{{ log.message }}</span>
               </div>
-            </div>
-          </div>
-
-          <div class="todo-list-container" v-if="todoList.length > 0">
-            <h3>📋 研究任务清单</h3>
-            <div class="todo-items">
-              <div 
-                v-for="task in todoList" 
-                :key="task.id" 
-                class="todo-item" 
-                :class="task.status"
-              >
-                <div class="task-status-icon">
-                  <span v-if="task.status === 'pending'">⏳</span>
-                  <span v-else-if="task.status === 'in_progress'">🔄</span>
-                  <span v-else-if="task.status === 'completed'">✅</span>
-                  <span v-else-if="task.status === 'skipped'">⏭️</span>
-                  <span v-else-if="task.status === 'failed'">❌</span>
-                </div>
-                <div class="task-content">
-                  <div class="task-header">
-                    <span class="task-title">{{ task.title }}</span>
-                    <span class="task-intent">{{ task.intent }}</span>
-                  </div>
-                  <div class="task-summary" v-if="task.summary" v-html="md.render(task.summary)"></div>
-                </div>
+              <div v-if="logs.length === 0" class="log-placeholder">等待执行...</div>
+              <!-- 等待动画指示器 -->
+              <div v-if="isWaiting && logs.length > 0" class="log-entry log-waiting">
+                <span class="log-time">{{ new Date().toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }) }}</span>
+                <span class="log-msg waiting-indicator">⏳ 处理中{{ waitingDots }}</span>
               </div>
             </div>
           </div>
@@ -191,6 +188,10 @@
 <script lang="ts" setup>
 import { reactive, ref, computed, nextTick, watch } from "vue";
 import { runResearchStream, type ResearchStreamEvent } from "./services/api";
+import MarkdownIt from "markdown-it";
+
+// Markdown renderer
+const md = new MarkdownIt();
 
 // --- Types ---
 type ViewState = "setup" | "producing" | "player";
@@ -230,6 +231,39 @@ const researchProgress = computed(() => {
   return `(${completedTasks.value}/${totalTasks.value})`;
 });
 
+// Audio Progress State (新增)
+const audioProgress = reactive({
+  current: 0,
+  total: 0,
+  role: ""
+});
+
+// Current Status Message (新增)
+const currentStatusMessage = ref("");
+
+// 等待动画状态
+const isWaiting = ref(false);
+const waitingDots = ref(".");
+let waitingInterval: ReturnType<typeof setInterval> | null = null;
+
+// 启动等待动画
+function startWaitingAnimation() {
+  isWaiting.value = true;
+  waitingDots.value = ".";
+  waitingInterval = setInterval(() => {
+    waitingDots.value = waitingDots.value.length >= 3 ? "." : waitingDots.value + ".";
+  }, 500);
+}
+
+// 停止等待动画
+function stopWaitingAnimation() {
+  isWaiting.value = false;
+  if (waitingInterval) {
+    clearInterval(waitingInterval);
+    waitingInterval = null;
+  }
+}
+
 // Data
 const podcastScript = ref<PodcastMessage[]>([]);
 const reportMarkdown = ref("");
@@ -241,6 +275,37 @@ const audioPlayer = ref<HTMLAudioElement | null>(null);
 const logContainer = ref<HTMLElement | null>(null);
 let abortController: AbortController | null = null;
 
+// Helper: 根据日志内容返回样式类(终端风格)
+function getLogClass(message: string): string {
+  // 阶段变更 - 绿色高亮
+  if (message.includes("[STAGE]") || message.includes("═══")) return "log-stage";
+  // 任务状态
+  if (message.includes("[TASK")) return "log-task";
+  // 工具调用
+  if (message.includes("[TOOL]")) return "log-tool";
+  // 来源信息
+  if (message.includes("[SOURCES]")) return "log-sources";
+  // 成功/完成
+  if (message.includes("✅") || message.includes("完成") || message.includes("SUCCESS")) return "log-success";
+  // 错误
+  if (message.includes("❌") || message.includes("失败") || message.includes("ERROR")) return "log-error";
+  // 警告
+  if (message.includes("⚠️") || message.includes("WARNING")) return "log-warning";
+  // 开始/启动
+  if (message.includes("🚀") || message.includes("开始") || message.includes("START")) return "log-start";
+  // 规划
+  if (message.includes("📋") || message.includes("PLAN")) return "log-plan";
+  // 搜索
+  if (message.includes("🔍") || message.includes("SEARCH")) return "log-search";
+  // 音频
+  if (message.includes("🎤") || message.includes("AUDIO") || message.includes("TTS")) return "log-audio";
+  // 后端日志
+  if (message.includes("💬")) return "log-backend";
+  // INFO 级别(默认)
+  if (message.includes("INFO:")) return "log-info";
+  return "";
+}
+
 // --- Computed ---
 
 // --- Methods ---
@@ -273,11 +338,19 @@ async function startProduction() {
   todoList.value = [];
   totalTasks.value = 0;
   completedTasks.value = 0;
+  // 重置新增的状态
+  audioProgress.current = 0;
+  audioProgress.total = 0;
+  audioProgress.role = "";
+  currentStatusMessage.value = "正在初始化...";
 
   abortController = new AbortController();
+  
+  // 启动等待动画
+  startWaitingAnimation();
 
   addLog("🚀 启动 DeepCast 制作流程...");
-  addLog(`主题: ${form.topic}`);
+  addLog(`📌 主题: ${form.topic}`);
 
   try {
     await runResearchStream(
@@ -292,62 +365,93 @@ async function startProduction() {
       addLog(`❌ 错误: ${err}`);
       alert("制作失败,请查看日志。");
     }
+  } finally {
+    // 停止等待动画
+    stopWaitingAnimation();
   }
 }
 
 function handleStreamEvent(event: ResearchStreamEvent) {
+  // 0. Log event - 直接显示后端日志
+  if (event.type === "log") {
+    const msg = String((event as any).message || "");
+    addLog(`INFO: ${msg}`);
+    return;
+  }
+
+  // 0.5. Stage Change (阶段切换事件)
+  if (event.type === "stage_change") {
+    const payload = event as any;
+    const stage = payload.stage;
+    const message = payload.message || "";
+    
+    // 更新当前状态消息
+    currentStatusMessage.value = message;
+    
+    // 更新当前阶段并记录日志
+    addLog(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
+    addLog(`📌 [STAGE] ${stage.toUpperCase()} - ${message}`);
+    addLog(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
+    
+    if (stage === "report") {
+      productionStage.value = "research";
+    } else if (stage === "script") {
+      productionStage.value = "script";
+    } else if (stage === "audio") {
+      productionStage.value = "audio";
+    }
+    return;
+  }
+
   // 1. Tool Calls (增加执行细节)
   if (event.type === "tool_call") {
     const payload = event as any;
     const tool = payload.tool;
     const agent = payload.agent || "Agent";
+    const taskId = payload.task_id;
+    const noteId = payload.note_id;
+    const params = payload.parameters || payload.parsed_parameters || {};
     
-    // 解析具体操作
-    if (tool === "search") {
-      // 从参数中提取查询词(如果可能)
-      // 假设参数结构 { input: "..." }
-      // 由于 parameters 是 Record<string, unknown>,我们尝试转换为字符串
-      let query = "";
-      if (payload.parameters && typeof payload.parameters.input === "string") {
-        query = payload.parameters.input;
-      }
-      addLog(`🔍 ${agent} 正在搜索: ${query || "相关信息"}`);
-    } else if (tool === "note") {
-      const action = payload.parameters?.action;
-      if (action === "read") {
-        addLog(`📖 ${agent} 正在阅读笔记`);
-      } else if (action === "create" || action === "update") {
-        addLog(`📝 ${agent} 正在记录关键信息`);
-      }
+    // 构建详细的日志信息,类似后端 INFO 输出
+    let logParts = [`[TOOL] agent=${agent} tool=${tool}`];
+    if (taskId) logParts.push(`task_id=${taskId}`);
+    if (noteId) logParts.push(`note_id=${noteId}`);
+    
+    // 解析具体操作并添加关键参数
+    if (tool === "note") {
+      const action = params.action;
+      const title = params.title;
+      if (action) logParts.push(`action=${action}`);
+      if (title) logParts.push(`title="${title}"`);
+      addLog(`📝 ${logParts.join(' ')}`);
+    } else if (tool === "search") {
+      let query = params.input || params.query || "";
+      if (query) logParts.push(`query="${query.slice(0, 50)}${query.length > 50 ? '...' : ''}"`);
+      addLog(`🔍 ${logParts.join(' ')}`);
     } else {
-      addLog(`🔧 ${agent} 调用了工具: ${tool}`);
+      addLog(`🔧 ${logParts.join(' ')}`);
     }
     return;
   }
 
   // 2. Sources (发现来源)
   if (event.type === "sources") {
-    addLog("📚 发现新的信息来源,正在分析...");
+    const payload = event as any;
+    const taskId = payload.task_id;
+    const backend = payload.backend;
+    let msg = `[SOURCES] task_id=${taskId}`;
+    if (backend) msg += ` backend=${backend}`;
+    const sourcesCount = payload.latest_sources ? payload.latest_sources.split('\n').filter((s: string) => s.trim()).length : 0;
+    msg += ` found=${sourcesCount} sources`;
+    addLog(`📚 ${msg}`);
     return;
   }
 
   // 3. Status Updates
   if (event.type === "status") {
-    // 翻译或直接显示
+    // 直接显示后端发来的消息
     let msg = String(event.message);
-    if (msg.includes("初始化")) msg = "初始化研究流程...";
-    if (msg.includes("脚本")) msg = "正在创作播客剧本...";
-    if (msg.includes("语音") || msg.includes("音频")) msg = "正在合成语音...";
-    
-    // Translation for known English messages
-    if (msg.includes("Researching")) msg = "正在进行深度搜索...";
-    if (msg.includes("Generating")) msg = "正在生成内容...";
-    if (msg.includes("Analyzing")) msg = "正在分析数据...";
-    
-    addLog(`ℹ️ ${msg}`);
-    
-    if (String(event.message).includes("脚本")) productionStage.value = "script";
-    if (String(event.message).includes("语音") || String(event.message).includes("音频")) productionStage.value = "audio";
+    addLog(`ℹ️ [STATUS] ${msg}`);
   }
 
   // 3.5 Todo List (Total Tasks)
@@ -355,9 +459,16 @@ function handleStreamEvent(event: ResearchStreamEvent) {
     console.log("Received todo_list event:", event);
     const payload = event as any;
     if (payload.tasks && Array.isArray(payload.tasks)) {
-      todoList.value = payload.tasks; // Initialize list
+      todoList.value = payload.tasks;
       totalTasks.value = payload.tasks.length;
-      addLog(`📋 规划了 ${totalTasks.value} 个研究任务`);
+      addLog(`📋 [PLAN] 研究规划专家创建了 ${totalTasks.value} 个任务:`);
+      // 列出每个任务的标题和查询
+      payload.tasks.forEach((task: any, idx: number) => {
+        addLog(`   └─ Task ${task.id}: ${task.title}`);
+        if (task.query) {
+          addLog(`      query="${task.query.slice(0, 60)}${task.query.length > 60 ? '...' : ''}"`);
+        }
+      });
     } else {
       console.warn("Received todo_list but tasks is empty or invalid", payload);
     }
@@ -366,81 +477,147 @@ function handleStreamEvent(event: ResearchStreamEvent) {
   // 4. Research Updates
   if (event.type === "task_status") {
     const payload = event as any;
-    if (payload.status === "in_progress") {
-      currentTask.value = payload; // 简单的任务更新
-      addLog(`👉 开始执行任务: ${payload.title || '未知任务'}`);
-    } else if (payload.status === "completed") {
+    // 更新内部状态
+    const taskIndex = todoList.value.findIndex(t => t.id === payload.task_id);
+    if (taskIndex !== -1) {
+      todoList.value[taskIndex].status = payload.status;
+      if (payload.summary) {
+        todoList.value[taskIndex].summary = payload.summary;
+      }
+    }
+    
+    const taskId = payload.task_id;
+    const status = payload.status;
+    const title = payload.title || "";
+    const query = payload.query || "";
+    
+    // 截断长查询内容
+    const truncateText = (text: string, max: number) => 
+      text.length > max ? text.slice(0, max) + "..." : text;
+    
+    if (status === "in_progress") {
+      currentTask.value = payload;
+      addLog(`🚀 [TASK ${taskId}] status=in_progress title="${title}"`);
+      if (payload.intent) {
+        addLog(`   ├─ intent: ${payload.intent}`);
+      }
+      if (query) {
+        addLog(`   └─ query: "${truncateText(query, 60)}"`);
+      }
+    } else if (status === "completed") {
       completedTasks.value++;
-      addLog(`✅ 任务完成: ${payload.title}`);
-    } else if (payload.status === "skipped") {
+      addLog(`✅ [TASK ${taskId}] status=completed (${completedTasks.value}/${totalTasks.value}) title="${title}"`);
+    } else if (status === "skipped") {
       completedTasks.value++;
-      addLog(`⏭️ 任务跳过: ${payload.title}`);
-    } else if (payload.status === "failed") {
+      addLog(`⏭️ [TASK ${taskId}] status=skipped title="${title}"`);
+    } else if (status === "failed") {
       completedTasks.value++;
-      addLog(`❌ 任务失败: ${payload.title}`);
+      addLog(`❌ [TASK ${taskId}] status=failed title="${title}" error="${payload.detail || 'unknown'}"`);
     }
   }
   
+  // task_summary_chunk - 显示摘要片段(可选,减少日志噪音)
   if (event.type === "task_summary_chunk") {
       const payload = event as any;
       const taskIndex = todoList.value.findIndex(t => t.id === payload.task_id);
       
       if (taskIndex !== -1) {
-        // Initialize summary if it doesn't exist
         if (!todoList.value[taskIndex].summary) {
           todoList.value[taskIndex].summary = "";
+          // 只在开始时显示一条日志
+          addLog(`📄 [SUMMARY] task_id=${payload.task_id} 正在生成摘要...`);
         }
-        
-        // Append chunk
-        // Note: You might want to strip <think> tags if they leak through, 
-        // but backend usually handles that.
         todoList.value[taskIndex].summary += payload.content;
-        
-        // Auto-scroll logic could go here if we had a ref to the specific item
       }
   }
 
   // 5. Report Ready
   if (event.type === "final_report") {
     reportMarkdown.value = String(event.report);
-    addLog("📄 深度研究报告已生成。");
+    currentStatusMessage.value = "深度研究报告已完成";
+    const reportLen = String(event.report).length;
+    addLog(`📄 [REPORT] status=completed length=${reportLen} chars`);
   }
 
   // 6. Script Ready
   if (event.type === "podcast_script") {
     const payload = event as any;
-    podcastScript.value = payload.script;
+    podcastScript.value = payload.script || [];
+    const turns = payload.turns ?? payload.script?.length ?? 0;
     productionStage.value = "audio";
-    addLog("🎙️ 播客剧本创作完成。");
+    audioProgress.total = turns;
+    audioProgress.current = 0;
+    currentStatusMessage.value = turns > 0 
+      ? `脚本完成,准备生成 ${turns} 段语音` 
+      : "脚本为空,跳过音频生成";
+    addLog(`🎙️ [SCRIPT] status=completed turns=${turns}`);
+    
+    if (turns === 0) {
+      addLog(`⚠️ [WARNING] script is empty, JSON parsing may have failed`);
+    }
   }
 
-  // 7. Audio Generation (Detail)
+  // 6.5. Audio Start
+  if (event.type === "audio_start") {
+    const payload = event as any;
+    audioProgress.total = payload.total || 0;
+    audioProgress.current = 0;
+    addLog(`🎵 [AUDIO] status=started total=${payload.total}`);
+  }
+
+  // 7. Audio Progress
+  if (event.type === "audio_progress") {
+    const payload = event as any;
+    audioProgress.current = payload.current;
+    audioProgress.total = payload.total;
+    audioProgress.role = payload.role;
+    currentStatusMessage.value = `TTS ${payload.current}/${payload.total}: ${payload.role}`;
+    addLog(`🎤 [TTS ${payload.current}/${payload.total}] role=${payload.role} status=generating`);
+  }
+
+  // 8. Audio Generation Complete
   if (event.type === "audio_generated") {
-    const files = (event as any).files || [];
-    addLog(`🎵 已生成 ${files.length} 个音频片段。`);
+    const payload = event as any;
+    const count = payload.count ?? payload.files?.length ?? 0;
+    currentStatusMessage.value = count > 0 
+      ? `${count} 个音频片段已生成,正在合成...` 
+      : "音频生成失败";
+    addLog(`🎵 [AUDIO] status=completed count=${count}`);
+    
+    if (count === 0) {
+      addLog(`⚠️ [WARNING] no audio files generated, check TTS config`);
+    }
   }
 
-  // 8. Podcast Ready (Final)
+  // 9. Podcast Ready (Final)
   if (event.type === "podcast_ready") {
     const payload = event as any;
-    // 后端返回的是文件路径,我们需要转换为 URL
-    // 假设后端挂载了 /output 静态目录
-    // payload.file 是绝对路径,我们需要提取文件名
     const filename = String(payload.file).split(/[\\/]/).pop();
     if (filename) {
-      // 获取当前 API base URL (从 api.ts 逻辑推断,这里简化处理)
-      // 在生产环境中应该从配置读取,这里假设是 localhost:8000
       const baseUrl = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
       audioUrl.value = `${baseUrl}/output/${filename}`;
-      addLog("🎉 播客制作完成!即将开始播放...");
+      currentStatusMessage.value = "🎉 播客制作完成!";
+      addLog(`🎉 [PODCAST] status=ready file=${filename}`);
       productionStage.value = "done";
       
-      // 延迟跳转到播放页
       setTimeout(() => {
         currentView.value = "player";
       }, 1500);
     }
   }
+
+  // 10. Done event
+  if (event.type === "done") {
+    currentStatusMessage.value = "全部完成";
+    addLog(`✅ [DONE] all tasks completed`);
+  }
+  
+  // 11. Backend Log event (直接显示后端日志)
+  if (event.type === "log") {
+    const payload = event as any;
+    const msg = payload.message || "";
+    addLog(`💬 INFO: ${msg}`);
+  }
 }
 
 function cancelProduction() {
@@ -448,13 +625,17 @@ function cancelProduction() {
     abortController.abort();
     abortController = null;
   }
+  stopWaitingAnimation();
   currentView.value = "setup";
+  currentStatusMessage.value = "";
 }
 
 function resetApp() {
   currentView.value = "setup";
   form.topic = "";
   isPlaying.value = false;
+  currentStatusMessage.value = "";
+  stopWaitingAnimation();
 }
 
 // Audio Controls
@@ -848,39 +1029,216 @@ select {
   margin: 0 1rem;
   position: relative;
   top: -14px;
+  transition: background 0.3s ease;
+}
+
+.stage-line.active {
+  background: linear-gradient(90deg, #3b82f6, #8b5cf6);
+}
+
+/* 当前状态卡片 */
+.current-status-card {
+  width: 100%;
+  background: rgba(59, 130, 246, 0.1);
+  border: 1px solid rgba(59, 130, 246, 0.3);
+  border-radius: 8px;
+  padding: 0.75rem 1rem;
+  margin-bottom: 1rem;
+  display: flex;
+  align-items: center;
+  gap: 0.75rem;
+}
+
+.status-indicator {
+  width: 10px;
+  height: 10px;
+  background: #3b82f6;
+  border-radius: 50%;
+  animation: pulse-status 1.5s infinite;
+}
+
+@keyframes pulse-status {
+  0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); }
+  50% { opacity: 0.7; box-shadow: 0 0 0 8px rgba(59, 130, 246, 0); }
+}
+
+.status-text {
+  color: #93c5fd;
+  font-size: 0.95rem;
+  font-weight: 500;
 }
 
 .terminal-log {
   width: 100%;
-  background: #000;
+  background: #0a0a0a;
   border-radius: 8px;
-  padding: 1rem;
-  font-family: 'Fira Code', monospace;
-  font-size: 0.9rem;
-  height: 150px;
-  margin-bottom: 2rem;
-  border: 1px solid #333;
+  font-family: 'Fira Code', 'Cascadia Code', monospace;
+  font-size: 0.82rem;
+  height: 450px;
+  margin-bottom: 1.5rem;
+  border: 1px solid #1e293b;
+  overflow: hidden;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
+}
+
+.log-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0.5rem 1rem;
+  background: #111827;
+  border-bottom: 1px solid #1e293b;
+  font-size: 0.8rem;
+  color: #64748b;
+}
+
+.log-header-title {
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
+}
+
+.log-header-title::before {
+  content: '●';
+  color: #4ade80;
+  font-size: 0.6rem;
+}
+
+.log-count {
+  color: #475569;
 }
 
 .log-content {
-  height: 100%;
+  height: calc(100% - 32px);
   overflow-y: auto;
+  padding: 0.75rem 1rem;
   display: flex;
   flex-direction: column;
-  gap: 0.5rem;
+  gap: 0.35rem;
 }
 
 .log-entry {
   display: flex;
-  gap: 1rem;
+  gap: 0.75rem;
+  padding: 0.2rem 0;
+  border-radius: 2px;
+  line-height: 1.4;
 }
 
 .log-time {
-  color: #64748b;
+  color: #475569;
+  font-size: 0.8rem;
+  flex-shrink: 0;
+  min-width: 70px;
 }
 
 .log-msg {
-  color: #e2e8f0;
+  color: #cbd5e1;
+  word-break: break-word;
+  flex: 1;
+}
+
+.log-placeholder {
+  color: #475569;
+  font-style: italic;
+  padding: 1rem 0;
+  text-align: center;
+}
+
+/* 等待动画指示器 */
+.log-entry.log-waiting .log-msg {
+  color: #fbbf24;
+  animation: pulse 1.5s ease-in-out infinite;
+}
+
+.waiting-indicator {
+  font-family: monospace;
+  letter-spacing: 2px;
+}
+
+@keyframes pulse {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.5; }
+}
+
+/* 日志类型样式 - 终端风格 */
+.log-entry.log-success .log-msg { color: #4ade80; }
+.log-entry.log-error .log-msg { color: #f87171; }
+.log-entry.log-warning .log-msg { color: #fbbf24; }
+.log-entry.log-start .log-msg { color: #60a5fa; }
+.log-entry.log-plan .log-msg { color: #a78bfa; }
+.log-entry.log-search .log-msg { color: #fbbf24; }
+.log-entry.log-audio .log-msg { color: #f472b6; }
+
+/* INFO 级别日志(工具调用) */
+.log-entry.log-info .log-msg { 
+  color: #94a3b8;
+}
+
+/* 阶段变更 */
+.log-entry.log-stage .log-msg { 
+  color: #34d399; 
+  font-weight: 600;
+  border-left: 3px solid #34d399;
+  padding-left: 0.5rem;
+  margin-left: -0.5rem;
+}
+
+/* 后端日志 */
+.log-entry.log-backend .log-msg { 
+  color: #64748b;
+  font-style: italic;
+}
+
+/* 任务状态 */
+.log-entry.log-task .log-msg {
+  color: #60a5fa;
+}
+
+/* 工具调用 */
+.log-entry.log-tool .log-msg {
+  color: #a78bfa;
+}
+
+/* 来源信息 */
+.log-entry.log-sources .log-msg {
+  color: #fbbf24;
+}
+
+/* 步骤进度数字 */
+.step-progress {
+  font-size: 0.7rem;
+  color: #60a5fa;
+  background: rgba(96, 165, 250, 0.15);
+  padding: 2px 6px;
+  border-radius: 4px;
+  margin-top: 0.25rem;
+}
+
+/* 任务计数器 */
+.task-counter {
+  font-weight: normal;
+  color: #64748b;
+  font-size: 0.9rem;
+}
+
+/* 旋转动画(用于进行中的任务图标) */
+.spinning {
+  display: inline-block;
+  animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
+/* 主题显示 */
+.production-topic {
+  color: #94a3b8;
+  font-size: 1rem;
+  margin-top: 0.25rem;
+  font-style: italic;
 }
 
 .research-preview {