Explorar o código

feat: 添加README文档并全面汉化代码注释

- 新增README.md,包含项目简介、功能说明、技术栈和快速开始指南
- 将后端所有Python文件的英文注释和文档字符串翻译为中文
- 添加路径检查脚本(check_paths.py)用于调试配置
- 前端添加markdown-it依赖以支持Markdown渲染
- 更新环境变量示例,优化默认配置(混合搜索、智能/快速模型分离)
- 修复播客脚本生成逻辑,确保正确解析JSON格式
- 优化音频输出目录处理,确保静态文件服务可用
- 增强任务规划提示词,避免空数组输出
- 改进流式研究的事件发射机制,确保任务列表正确传输
JJSun hai 5 meses
pai
achega
32012e2017
Modificáronse 26 ficheiros con 1474 adicións e 2194 borrados
  1. 0 1
      Co-creation-projects/JJason-DeepCastAgent/README.md
  2. 5 2
      Co-creation-projects/JJason-DeepCastAgent/backend/env.example
  3. 19 0
      Co-creation-projects/JJason-DeepCastAgent/backend/scripts/check_paths.py
  4. 2 0
      Co-creation-projects/JJason-DeepCastAgent/backend/scripts/test_agent_workflow.py
  5. 1 1
      Co-creation-projects/JJason-DeepCastAgent/backend/src/__init__.py
  6. 80 39
      Co-creation-projects/JJason-DeepCastAgent/backend/src/agent.py
  7. 85 80
      Co-creation-projects/JJason-DeepCastAgent/backend/src/config.py
  8. 37 12
      Co-creation-projects/JJason-DeepCastAgent/backend/src/main.py
  9. 12 12
      Co-creation-projects/JJason-DeepCastAgent/backend/src/models.py
  10. 5 3
      Co-creation-projects/JJason-DeepCastAgent/backend/src/prompts.py
  11. 1 1
      Co-creation-projects/JJason-DeepCastAgent/backend/src/services/__init__.py
  12. 31 9
      Co-creation-projects/JJason-DeepCastAgent/backend/src/services/audio_generator.py
  13. 11 11
      Co-creation-projects/JJason-DeepCastAgent/backend/src/services/audio_synthesizer.py
  14. 2 2
      Co-creation-projects/JJason-DeepCastAgent/backend/src/services/notes.py
  15. 24 8
      Co-creation-projects/JJason-DeepCastAgent/backend/src/services/planner.py
  16. 11 3
      Co-creation-projects/JJason-DeepCastAgent/backend/src/services/reporter.py
  17. 14 6
      Co-creation-projects/JJason-DeepCastAgent/backend/src/services/script_generator.py
  18. 24 4
      Co-creation-projects/JJason-DeepCastAgent/backend/src/services/search.py
  19. 9 5
      Co-creation-projects/JJason-DeepCastAgent/backend/src/services/summarizer.py
  20. 1 1
      Co-creation-projects/JJason-DeepCastAgent/backend/src/services/text_processing.py
  21. 42 12
      Co-creation-projects/JJason-DeepCastAgent/backend/src/services/tool_events.py
  22. 15 5
      Co-creation-projects/JJason-DeepCastAgent/backend/src/utils.py
  23. 1 1
      Co-creation-projects/JJason-DeepCastAgent/frontend/index.html
  24. 82 0
      Co-creation-projects/JJason-DeepCastAgent/frontend/package-lock.json
  25. 2 0
      Co-creation-projects/JJason-DeepCastAgent/frontend/package.json
  26. 958 1976
      Co-creation-projects/JJason-DeepCastAgent/frontend/src/App.vue

+ 0 - 1
Co-creation-projects/JJason-DeepCastAgent/backend/README.md → Co-creation-projects/JJason-DeepCastAgent/README.md

@@ -22,7 +22,6 @@ DeepCast 旨在解决信息获取的"枯燥"问题,将严肃的深度研究报
     - 语音: `ecnu-tts`
 - **搜索服务**: 
     - 混合搜索 (Hybrid Search): Tavily + SerpApi (Google)
-    - 备用方案: DuckDuckGo
 - **音频处理**: Pydub, FFmpeg
 
 ## 🚀 快速开始

+ 5 - 2
Co-creation-projects/JJason-DeepCastAgent/backend/env.example

@@ -1,12 +1,14 @@
 # 核心配置
 LOG_LEVEL=INFO
-SEARCH_API=tavily
+SEARCH_API=hybrid
 MAX_WEB_RESEARCH_LOOPS=3
 FETCH_FULL_PAGE=True
 
 # TEXT模型配置 (LLM)
 LLM_PROVIDER=custom
 LLM_MODEL_ID=ecnu-max
+SMART_LLM_MODEL=ecnu-reasoner
+FAST_LLM_MODEL=ecnu-plus
 LLM_API_KEY=your_ecnu_api_key_here
 LLM_BASE_URL=your_openai_api_key_here
 LLM_TIMEOUT=60
@@ -15,7 +17,8 @@ LLM_TIMEOUT=60
 TTS_API_KEY=your_ecnu_api_key_here
 TTS_BASE_URL=your_openai_api_key_here
 TTS_MODEL=ecnu-tts
-AUDIO_OUTPUT_DIR=./output/audio
+AUDIO_OUTPUT_DIR=output/audio
+NOTES_WORKSPACE=output/notes
 
 # FFmpeg 配置 (Windows 环境通常需要指定路径)
 FFMPEG_PATH=C:\ffmpeg\bin\ffmpeg.exe

+ 19 - 0
Co-creation-projects/JJason-DeepCastAgent/backend/scripts/check_paths.py

@@ -0,0 +1,19 @@
+import sys
+import os
+from pathlib import Path
+
+# 添加 src 到 sys.path
+sys.path.append(str(Path(__file__).parent.parent / "src"))
+
+from config import Configuration, BACKEND_ROOT
+
+def main():
+    print(f"BACKEND_ROOT: {BACKEND_ROOT}")
+    
+    config = Configuration.from_env()
+    
+    print(f"Audio Output Dir: {config.audio_output_dir}")
+    print(f"Notes Workspace: {config.notes_workspace}")
+
+if __name__ == "__main__":
+    main()

+ 2 - 0
Co-creation-projects/JJason-DeepCastAgent/backend/scripts/test_agent_workflow.py

@@ -62,6 +62,8 @@ def main():
     print("\nCurrent Configuration:")
     print(f"LLM Provider: {config.llm_provider}")
     print(f"Model: {config.resolved_model()}")
+    print(f"Smart Model: {config.smart_llm_model}")
+    print(f"Fast Model: {config.fast_llm_model}")
     print(f"Search API: {config.search_api}")
     print(f"TTS Enabled: {bool(config.tts_api_key)}")
     print(f"FFmpeg Path: {config.ffmpeg_path}")

+ 1 - 1
Co-creation-projects/JJason-DeepCastAgent/backend/src/__init__.py

@@ -1,4 +1,4 @@
-"""DeepCast - Automated Podcast Generation Agent powered by HelloAgents."""
+"""DeepCast - 由 HelloAgents 驱动的自动播客生成代理。"""
 
 __version__ = "0.0.1"
 

+ 80 - 39
Co-creation-projects/JJason-DeepCastAgent/backend/src/agent.py

@@ -1,4 +1,4 @@
-"""Orchestrator coordinating the deep research workflow."""
+"""协调深度研究工作流的编排器。"""
 
 from __future__ import annotations
 
@@ -34,12 +34,14 @@ logger = logging.getLogger(__name__)
 
 
 class DeepResearchAgent:
-    """Coordinator orchestrating TODO-based research workflow using HelloAgents."""
+    """使用 HelloAgents 协调基于 TODO 的研究工作流的协调器。"""
 
     def __init__(self, config: Configuration | None = None) -> None:
-        """Initialise the coordinator with configuration and shared tools."""
+        """使用配置和共享工具初始化协调器。"""
         self.config = config or Configuration.from_env()
-        self.llm = self._init_llm()
+        self.default_llm = self._init_llm(self.config.llm_model_id)
+        self.smart_llm = self._init_llm(self.config.smart_llm_model)
+        self.fast_llm = self._init_llm(self.config.fast_llm_model)
 
         self.note_tool = (
             NoteTool(workspace=self.config.notes_workspace)
@@ -61,19 +63,23 @@ class DeepResearchAgent:
         self.todo_agent = self._create_tool_aware_agent(
             name="研究规划专家",
             system_prompt=todo_planner_system_prompt.strip(),
+            llm=self.smart_llm,
         )
         self.report_agent = self._create_tool_aware_agent(
             name="报告撰写专家",
             system_prompt=report_writer_instructions.strip(),
+            llm=self.smart_llm,
         )
         self.script_agent = self._create_tool_aware_agent(
             name="脚本策划专家",
             system_prompt=script_writer_instructions.strip(),
+            llm=self.default_llm,
         )
 
         self._summarizer_factory: Callable[[], ToolAwareSimpleAgent] = lambda: self._create_tool_aware_agent(  # noqa: E501
             name="任务总结专家",
             system_prompt=task_summarizer_instructions.strip(),
+            llm=self.fast_llm,
         )
 
         self.planner = PlanningService(self.todo_agent, self.config)
@@ -86,13 +92,13 @@ class DeepResearchAgent:
         self._last_search_notices: list[str] = []
 
     # ------------------------------------------------------------------
-    # Public API
+    # 公共 API
     # ------------------------------------------------------------------
-    def _init_llm(self) -> HelloAgentsLLM:
-        """Instantiate HelloAgentsLLM following configuration preferences."""
+    def _init_llm(self, model_id_override: str | None = None) -> HelloAgentsLLM:
+        """根据配置偏好实例化 HelloAgentsLLM。"""
         llm_kwargs: dict[str, Any] = {"temperature": 0.0}
 
-        model_id = self.config.llm_model_id or self.config.local_llm
+        model_id = model_id_override or self.config.llm_model_id
         if model_id:
             llm_kwargs["model"] = model_id
 
@@ -100,29 +106,18 @@ class DeepResearchAgent:
         if provider:
             llm_kwargs["provider"] = provider
 
-        if provider == "ollama":
-            llm_kwargs["base_url"] = self.config.sanitized_ollama_url()
-            if self.config.llm_api_key:
-                llm_kwargs["api_key"] = self.config.llm_api_key
-            else:
-                llm_kwargs["api_key"] = "ollama"
-        elif provider == "lmstudio":
-            llm_kwargs["base_url"] = self.config.lmstudio_base_url
-            if self.config.llm_api_key:
-                llm_kwargs["api_key"] = self.config.llm_api_key
-        else:
-            if self.config.llm_base_url:
-                llm_kwargs["base_url"] = self.config.llm_base_url
-            if self.config.llm_api_key:
-                llm_kwargs["api_key"] = self.config.llm_api_key
+        if self.config.llm_base_url:
+            llm_kwargs["base_url"] = self.config.llm_base_url
+        if self.config.llm_api_key:
+            llm_kwargs["api_key"] = self.config.llm_api_key
 
         return HelloAgentsLLM(**llm_kwargs)
 
-    def _create_tool_aware_agent(self, *, name: str, system_prompt: str) -> ToolAwareSimpleAgent:
-        """Instantiate a ToolAwareSimpleAgent sharing tool registry and tracker."""
+    def _create_tool_aware_agent(self, *, name: str, system_prompt: str, llm: HelloAgentsLLM) -> ToolAwareSimpleAgent:
+        """实例化共享工具注册表和跟踪器的 ToolAwareSimpleAgent。"""
         return ToolAwareSimpleAgent(
             name=name,
-            llm=self.llm,
+            llm=llm,
             system_prompt=system_prompt,
             enable_tool_calling=self.tools_registry is not None,
             tool_registry=self.tools_registry,
@@ -130,12 +125,21 @@ class DeepResearchAgent:
         )
 
     def _set_tool_event_sink(self, sink: Callable[[dict[str, Any]], None] | None) -> None:
-        """Enable or disable immediate tool event callbacks."""
+        """启用或禁用立即工具事件回调。"""
         self._tool_event_sink_enabled = sink is not None
         self._tool_tracker.set_event_sink(sink)
 
     def run(self, topic: str) -> SummaryStateOutput:
-        """Execute the research workflow and return the final report."""
+        """
+        执行研究工作流并返回最终报告(同步模式)。
+        
+        此方法按顺序执行以下步骤:
+        1. 初始化状态和规划任务。
+        2. 串行执行每个任务(搜索 + 总结)。
+        3. 生成最终报告。
+        4. 生成播客脚本。
+        5. 生成音频文件并合成播客。
+        """
         state = SummaryState(research_topic=topic)
         state.todo_items = self.planner.plan_todo_list(state)
         self._drain_tool_events(state)
@@ -158,11 +162,11 @@ class DeepResearchAgent:
         self._drain_tool_events(state)
         state.podcast_script = script
 
-        # Generate audio for the script
+        # 为脚本生成音频
         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)
 
-        # Synthesize podcast
+        # 合成播客
         podcast_file = self.podcast_synthesizer.synthesize_podcast(audio_files, task_id)
         
         return SummaryStateOutput(
@@ -173,7 +177,17 @@ class DeepResearchAgent:
         )
 
     def run_stream(self, topic: str) -> Iterator[dict[str, Any]]:
-        """Execute the workflow yielding incremental progress events."""
+        """
+        执行研究工作流并产生增量进度事件(流式模式)。
+        
+        此方法使用多线程并行执行研究任务,并通过生成器实时返回进度。
+        主要步骤:
+        1. 初始化并规划任务。
+        2. 为每个任务启动一个工作线程进行并行处理。
+        3. 实时流式传输任务状态、搜索结果和部分总结。
+        4. 所有任务完成后,生成并流式传输最终报告。
+        5. 生成并流式传输播客脚本和音频合成进度。
+        """
         state = SummaryState(research_topic=topic)
         logger.debug("Starting streaming research: topic=%s", topic)
         yield {"type": "status", "message": "初始化研究流程"}
@@ -190,9 +204,13 @@ class DeepResearchAgent:
             task.stream_token = token
             channel_map[task.id] = {"step": index, "token": token}
 
+        # 确保在开始多线程任务前,显式发送 todo_list 事件
+        # 使用 list comprehension 确保 task 被正确序列化
+        serialized_tasks = [self._serialize_task(t) for t in state.todo_items]
+        logger.info(f"Emitting todo_list event with {len(serialized_tasks)} tasks")
         yield {
             "type": "todo_list",
-            "tasks": [self._serialize_task(t) for t in state.todo_items],
+            "tasks": serialized_tasks,
             "step": 0,
         }
 
@@ -311,7 +329,7 @@ class DeepResearchAgent:
         script = self.script_generator.generate_script(state)
         for event in self._drain_tool_events(state):
             yield event
-        state.podcast_script = PodcastScript(script=script)
+        state.podcast_script = script
         yield {
             "type": "podcast_script",
             "script": script,
@@ -336,7 +354,7 @@ class DeepResearchAgent:
         yield {"type": "done"}
 
     # ------------------------------------------------------------------
-    # Execution helpers
+    # 执行助手
     # ------------------------------------------------------------------
     def _execute_task(
         self,
@@ -346,7 +364,18 @@ class DeepResearchAgent:
         emit_stream: bool,
         step: int | None = None,
     ) -> Iterator[dict[str, Any]]:
-        """Run search + summarization for a single task."""
+        """
+        对单个任务运行搜索 + 总结逻辑。
+        
+        Args:
+            state: 全局研究状态。
+            task: 当前要执行的任务项。
+            emit_stream: 是否产生流式事件(True 用于 run_stream,False 用于 run)。
+            step: 当前步骤编号(仅用于流式事件)。
+            
+        Returns:
+            事件字典的迭代器(即使 emit_stream=False,也可能产生少量内部事件,通常被忽略)。
+        """
         task.status = "in_progress"
 
         search_result, notices, answer_text, backend = dispatch_search(
@@ -470,7 +499,7 @@ class DeepResearchAgent:
         *,
         step: int | None = None,
     ) -> list[dict[str, Any]]:
-        """Proxy to the shared tool call tracker."""
+        """共享工具调用跟踪器的代理。"""
         events = self._tool_tracker.drain(state, step=step)
         if self._tool_event_sink_enabled:
             return []
@@ -478,11 +507,11 @@ class DeepResearchAgent:
 
     @property
     def _tool_call_events(self) -> list[dict[str, Any]]:
-        """Expose recorded tool events for legacy integrations."""
+        """为旧版集成暴露记录的工具事件。"""
         return self._tool_tracker.as_dicts()
 
     def _serialize_task(self, task: TodoItem) -> dict[str, Any]:
-        """Convert task dataclass to serializable dict for frontend."""
+        """将任务数据类转换为前端可序列化的字典。"""
         return {
             "id": task.id,
             "title": task.title,
@@ -555,6 +584,18 @@ class DeepResearchAgent:
         return payload
 
     def _find_existing_report_note_id(self, state: SummaryState) -> str | None:
+        """
+        查找与研究主题相关的现有报告笔记 ID。
+        
+        此方法检查当前状态是否已关联报告笔记 ID。如果没有,它会遍历已记录的工具事件,
+        查找最近创建或更新的结论类型笔记,标题中包含研究主题的报告。
+        
+        Args:
+            state: 当前研究状态,包含研究主题和已记录的工具事件。
+            
+        Returns:
+            与研究主题相关的现有报告笔记 ID(如果存在),否则为 None。
+        """
         if state.report_note_id:
             return state.report_note_id
 
@@ -598,6 +639,6 @@ class DeepResearchAgent:
 
 
 def run_deep_research(topic: str, config: Configuration | None = None) -> SummaryStateOutput:
-    """Convenience function mirroring the class-based API."""
+    """镜像基于类的 API 的便捷函数。"""
     agent = DeepResearchAgent(config=config)
     return agent.run(topic)

+ 85 - 80
Co-creation-projects/JJason-DeepCastAgent/backend/src/config.py

@@ -1,148 +1,162 @@
 import os
 from enum import Enum
+from pathlib import Path
 from typing import Any, Optional
 
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, field_validator
 
+# Define backend root directory
+BACKEND_ROOT = Path(__file__).resolve().parent.parent
 
 class SearchAPI(Enum):
-    PERPLEXITY = "perplexity"
+    HYBRID = "hybrid"
     TAVILY = "tavily"
-    DUCKDUCKGO = "duckduckgo"
-    SEARXNG = "searxng"
-    ADVANCED = "advanced"
+    SERPAPI = "serpapi"
 
 
 class Configuration(BaseModel):
-    """Configuration options for the deep research assistant."""
+    """DeepCast Agent Configuration."""
 
     max_web_research_loops: int = Field(
         default=3,
         title="Research Depth",
-        description="Number of research iterations to perform",
-    )
-    local_llm: str = Field(
-        default="llama3.2",
-        title="Local Model Name",
-        description="Name of the locally hosted LLM (Ollama/LMStudio)",
+        description="Number of research iterations",
     )
     llm_provider: str = Field(
-        default="ollama",
-        title="LLM Provider",
-        description="Provider identifier (ollama, lmstudio, or custom)",
+        default="custom",
+        title="LLM 提供商",
+        description="提供商标识符 (custom)",
     )
     search_api: SearchAPI = Field(
-        default=SearchAPI.DUCKDUCKGO,
-        title="Search API",
-        description="Web search API to use",
+        default=SearchAPI.HYBRID,
+        title="搜索 API",
+        description="使用的网络搜索 API (hybrid, tavily, serpapi)",
     )
     enable_notes: bool = Field(
         default=True,
-        title="Enable Notes",
-        description="Whether to store task progress in NoteTool",
+        title="启用笔记",
+        description="是否在 NoteTool 中存储任务进度",
     )
     notes_workspace: str = Field(
-        default="./notes",
-        title="Notes Workspace",
-        description="Directory for NoteTool to persist task notes",
+        default=str(BACKEND_ROOT / "output" / "notes"),
+        title="笔记工作区",
+        description="NoteTool 持久化任务笔记的目录",
     )
     fetch_full_page: bool = Field(
         default=True,
-        title="Fetch Full Page",
-        description="Include the full page content in the search results",
-    )
-    ollama_base_url: str = Field(
-        default="http://localhost:11434",
-        title="Ollama Base URL",
-        description="Base URL for Ollama API (without /v1 suffix)",
-    )
-    lmstudio_base_url: str = Field(
-        default="http://localhost:1234/v1",
-        title="LMStudio Base URL",
-        description="Base URL for LMStudio OpenAI-compatible API",
+        title="获取完整页面",
+        description="在搜索结果中包含完整页面内容",
     )
     strip_thinking_tokens: bool = Field(
-        default=True,
-        title="Strip Thinking Tokens",
-        description="Whether to strip <think> tokens from model responses",
+        default=False,
+        title="移除思考 Token",
+        description="是否从模型响应中移除 <think> token",
     )
     use_tool_calling: bool = Field(
         default=False,
-        title="Use Tool Calling",
-        description="Use tool calling instead of JSON mode for structured output",
+        title="使用工具调用",
+        description="使用工具调用而非 JSON 模式进行结构化输出",
     )
     llm_api_key: Optional[str] = Field(
         default=None,
-        title="LLM API Key",
-        description="Optional API key when using custom OpenAI-compatible services",
+        title="LLM API 密钥",
+        description="使用自定义 OpenAI 兼容服务时的可选 API 密钥",
     )
     llm_base_url: Optional[str] = Field(
         default=None,
-        title="LLM Base URL",
-        description="Optional base URL when using custom OpenAI-compatible services",
+        title="LLM 基础 URL",
+        description="使用自定义 OpenAI 兼容服务时的可选基础 URL",
     )
     llm_model_id: Optional[str] = Field(
         default=None,
-        title="LLM Model ID",
-        description="Optional model identifier for custom OpenAI-compatible services",
+        title="LLM 模型 ID",
+        description="自定义 OpenAI 兼容服务的可选模型标识符",
+    )
+    smart_llm_model: Optional[str] = Field(
+        default="ecnu-reasoner",
+        title="Smart LLM Model",
+        description="Model ID for complex reasoning tasks (e.g. Planning, Reporting)",
+    )
+    fast_llm_model: Optional[str] = Field(
+        default="ecnu-plus",
+        title="Fast LLM Model",
+        description="Model ID for simple/fast tasks (e.g. Summarization)",
     )
     tts_api_key: Optional[str] = Field(
         default=None,
-        title="TTS API Key",
-        description="API key for TTS service",
+        title="TTS API 密钥",
+        description="TTS 服务的 API 密钥",
     )
     tts_base_url: str = Field(
         default="https://chat.ecnu.edu.cn/open/api/v1/audio/speech",
-        title="TTS Base URL",
-        description="Base URL for TTS API",
+        title="TTS 基础 URL",
+        description="TTS API 的基础 URL",
     )
     tts_model: str = Field(
         default="ecnu-tts",
-        title="TTS Model",
-        description="Model identifier for TTS service",
+        title="TTS 模型",
+        description="TTS 服务的模型标识符",
     )
     audio_output_dir: str = Field(
-        default="./output/audio",
-        title="Audio Output Directory",
-        description="Directory to save generated audio files",
+        default=str(BACKEND_ROOT / "output" / "audio"),
+        title="音频输出目录",
+        description="保存生成的音频文件的目录",
     )
     ffmpeg_path: Optional[str] = Field(
         default=None,
-        title="FFmpeg Path",
-        description="Path to ffmpeg executable",
+        title="FFmpeg 路径",
+        description="ffmpeg 可执行文件的路径",
     )
     tavily_api_key: Optional[str] = Field(
         default=None,
-        title="Tavily API Key",
-        description="API key for Tavily search",
+        title="Tavily API 密钥",
+        description="Tavily 搜索的 API 密钥",
     )
     serpapi_api_key: Optional[str] = Field(
         default=None,
-        title="SerpApi Key",
-        description="API key for SerpApi",
+        title="SerpApi 密钥",
+        description="SerpApi 的 API 密钥",
     )
 
+    @field_validator("notes_workspace", "audio_output_dir")
+    @classmethod
+    def resolve_path(cls, v: str) -> str:
+        """确保路径是绝对路径,如果是相对路径则基于 BACKEND_ROOT 解析。"""
+        if v is None:
+            return v
+        path = Path(v)
+        if not path.is_absolute():
+            return str(BACKEND_ROOT / path)
+        return v
+
     @classmethod
     def from_env(cls, overrides: Optional[dict[str, Any]] = None) -> "Configuration":
-        """Create a configuration object using environment variables and overrides."""
+        """
+        使用环境变量和覆盖项创建配置对象。
+        
+        Args:
+            overrides: 可选的配置覆盖字典。
+            
+        Returns:
+            初始化的配置对象。
+        """
 
         raw_values: dict[str, Any] = {}
 
-        # Load values from environment variables based on field names
+        # 基于字段名从环境变量加载值
         for field_name in cls.model_fields.keys():
             env_key = field_name.upper()
             if env_key in os.environ:
                 raw_values[field_name] = os.environ[env_key]
 
-        # Additional mappings for explicit env names
+        # 显式环境名称的额外映射
         env_aliases = {
-            "local_llm": os.getenv("LOCAL_LLM"),
             "llm_provider": os.getenv("LLM_PROVIDER"),
             "llm_api_key": os.getenv("LLM_API_KEY"),
             "llm_model_id": os.getenv("LLM_MODEL_ID"),
+            "smart_llm_model": os.getenv("SMART_LLM_MODEL"),
+            "fast_llm_model": os.getenv("FAST_LLM_MODEL"),
             "llm_base_url": os.getenv("LLM_BASE_URL"),
-            "lmstudio_base_url": os.getenv("LMSTUDIO_BASE_URL"),
-            "ollama_base_url": os.getenv("OLLAMA_BASE_URL"),
             "max_web_research_loops": os.getenv("MAX_WEB_RESEARCH_LOOPS"),
             "fetch_full_page": os.getenv("FETCH_FULL_PAGE"),
             "strip_thinking_tokens": os.getenv("STRIP_THINKING_TOKENS"),
@@ -159,11 +173,11 @@ class Configuration(BaseModel):
             "serpapi_api_key": os.getenv("SERPAPI_API_KEY"),
         }
 
-        # Handle NO_PROXY
+        # 处理 NO_PROXY
         no_proxy = os.getenv("NO_PROXY")
         if no_proxy:
             os.environ["NO_PROXY"] = no_proxy
-            # Also set lowercase for compatibility
+            # 同时设置为小写以兼容
             os.environ["no_proxy"] = no_proxy
 
         for key, value in env_aliases.items():
@@ -177,16 +191,7 @@ class Configuration(BaseModel):
 
         return cls(**raw_values)
 
-    def sanitized_ollama_url(self) -> str:
-        """Ensure Ollama base URL includes the /v1 suffix required by OpenAI clients."""
-
-        base = self.ollama_base_url.rstrip("/")
-        if not base.endswith("/v1"):
-            base = f"{base}/v1"
-        return base
-
     def resolved_model(self) -> Optional[str]:
-        """Best-effort resolution of the model identifier to use."""
-
-        return self.llm_model_id or self.local_llm
+        """尽力解析要使用的模型标识符。"""
 
+        return self.llm_model_id

+ 37 - 12
Co-creation-projects/JJason-DeepCastAgent/backend/src/main.py

@@ -1,14 +1,19 @@
-"""FastAPI entrypoint exposing the DeepResearchAgent via HTTP."""
+"""通过 HTTP 暴露 DeepResearchAgent 的 FastAPI 入口点。"""
 
 from __future__ import annotations
 
 import json
+import os
 import sys
 from typing import Any, Dict, Iterator, Optional
 
+# Ensure src directory is in sys.path for module imports
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
+
 from fastapi import FastAPI, HTTPException
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.responses import StreamingResponse
+from fastapi.staticfiles import StaticFiles
 from loguru import logger
 from pydantic import BaseModel, Field
 
@@ -34,37 +39,37 @@ logger.add(
 
 
 class ResearchRequest(BaseModel):
-    """Payload for triggering a research run."""
+    """触发研究运行的负载。"""
 
-    topic: str = Field(..., description="Research topic supplied by the user")
+    topic: str = Field(..., description="用户提供的研究主题")
     search_api: SearchAPI | None = Field(
         default=None,
-        description="Override the default search backend configured via env",
+        description="覆盖通过环境变量配置的默认搜索后端",
     )
 
 class PodcastScript(BaseModel):
-    """Model for podcast script content."""
-    script: str = Field(..., description="Generated podcast script content")
+    """播客脚本内容模型。"""
+    script: str = Field(..., description="生成的播客脚本内容")
 
 
 class ResearchResponse(BaseModel):
-    """HTTP response containing the generated report and structured tasks."""
+    """包含生成报告和结构化任务的 HTTP 响应。"""
 
     report_markdown: str = Field(
-        ..., description="Markdown-formatted research report including sections"
+        ..., description="Markdown 格式的研究报告,包含各个部分"
     )
     todo_items: list[dict[str, Any]] = Field(
         default_factory=list,
-        description="Structured TODO items with summaries and sources",
+        description="带有摘要和来源的结构化待办事项",
     )
     podcast_script: Optional[PodcastScript] = Field(
         default=None,
-        description="Generated podcast script content",
+        description="生成的播客脚本内容",
     )
 
 
 def _mask_secret(value: Optional[str], visible: int = 4) -> str:
-    """Mask sensitive tokens while keeping leading and trailing characters."""
+    """在保持前导和尾随字符的同时,掩盖敏感令牌。"""
     if not value:
         return "unset"
 
@@ -84,7 +89,7 @@ def _build_config(payload: ResearchRequest) -> Configuration:
 
 
 def create_app() -> FastAPI:
-    app = FastAPI(title="DeepCast - Automated Podcast Generation Agent")
+    app = FastAPI(title="DeepCast - 自动播客生成智能体")
 
     app.add_middleware(
         CORSMiddleware,
@@ -94,8 +99,18 @@ def create_app() -> FastAPI:
         allow_headers=["*"],
     )
 
+    # 确保输出目录存在
+    # 使用绝对路径,基于 backend 根目录
+    backend_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+    output_dir = os.path.join(backend_root, "output")
+    os.makedirs(output_dir, exist_ok=True)
+    
+    # 挂载静态文件目录,用于访问生成的音频文件
+    app.mount("/output", StaticFiles(directory=output_dir), name="output")
+
     @app.on_event("startup")
     def log_startup_configuration() -> None:
+        """记录启动时的关键配置参数。"""
         config = Configuration.from_env()
 
         if config.llm_provider == "ollama":
@@ -125,6 +140,11 @@ def create_app() -> FastAPI:
 
     @app.post("/research", response_model=ResearchResponse)
     def run_research(payload: ResearchRequest) -> ResearchResponse:
+        """
+        触发同步研究任务。
+        
+        执行完整的研究流程,并在 HTTP 响应中一次性返回所有结果。
+        """
         try:
             config = _build_config(payload)
             agent = DeepResearchAgent(config=config)
@@ -160,6 +180,11 @@ def create_app() -> FastAPI:
 
     @app.post("/research/stream")
     def stream_research(payload: ResearchRequest) -> StreamingResponse:
+        """
+        触发流式研究任务。
+        
+        通过 Server-Sent Events (SSE) 实时返回研究进度、日志和部分结果。
+        """
         try:
             config = _build_config(payload)
             agent = DeepResearchAgent(config=config)

+ 12 - 12
Co-creation-projects/JJason-DeepCastAgent/backend/src/models.py

@@ -1,4 +1,4 @@
-"""State models used by the deep research workflow."""
+"""状态模型,用于深度研究工作流。"""
 
 import operator
 from dataclasses import dataclass, field
@@ -26,27 +26,27 @@ class TodoItem:
 
 @dataclass(kw_only=True)
 class SummaryState:
-    research_topic: str = field(default=None)  # Report topic
-    search_query: str = field(default=None)  # Deprecated placeholder
+    research_topic: str = field(default=None)  # 研究主题
+    search_query: str = field(default=None)  # 已弃用的占位符
     web_research_results: Annotated[list, operator.add] = field(default_factory=list)
     sources_gathered: Annotated[list, operator.add] = field(default_factory=list)
-    research_loop_count: int = field(default=0)  # Research loop count
-    running_summary: str = field(default=None)  # Legacy summary field
-    todo_items: Annotated[list, operator.add] = field(default_factory=list)
-    structured_report: Optional[str] = field(default=None)
-    report_note_id: Optional[str] = field(default=None)
-    report_note_path: Optional[str] = field(default=None)
-    podcast_script: Optional[list] = field(default=None)
+    research_loop_count: int = field(default=0)  # 研究循环次数
+    running_summary: str = field(default=None)  # 传统摘要字段
+    todo_items: Annotated[list, operator.add] = field(default_factory=list)  # 待办任务项列表
+    structured_report: Optional[str] = field(default=None)  # 结构化报告(JSON 字符串)
+    report_note_id: Optional[str] = field(default=None)  # 报告笔记 ID
+    report_note_path: Optional[str] = field(default=None)  # 报告笔记路径
+    podcast_script: Optional[list] = field(default=None)  # 播客脚本(JSON 字符串)
 
 
 @dataclass(kw_only=True)
 class SummaryStateInput:
-    research_topic: str = field(default=None)  # Report topic
+    research_topic: str = field(default=None)  # 研究主题
 
 
 @dataclass(kw_only=True)
 class SummaryStateOutput:
-    running_summary: str = field(default=None)  # Backward-compatible文本
+    running_summary: str = field(default=None)  # 向后兼容的摘要文本
     report_markdown: Optional[str] = field(default=None)
     todo_items: List[TodoItem] = field(default_factory=list)
     podcast_script: Optional[list] = field(default=None)

+ 5 - 3
Co-creation-projects/JJason-DeepCastAgent/backend/src/prompts.py

@@ -1,7 +1,7 @@
 from datetime import datetime
 
 
-# Get current date in a readable format
+# 以可读格式获取当前日期
 def get_current_date():
     return datetime.now().strftime("%B %d, %Y")
 
@@ -56,7 +56,8 @@ todo_planner_instructions = """
 }}
 </FORMAT>
 
-如果主题信息不足以规划任务,请输出空数组:{{"tasks": []}}。必要时使用笔记工具记录你的思考过程。
+即使主题较为模糊或开放(例如“聊聊XX的未来”),也请不要放弃!请基于行业常识和通用研究框架(如:技术原理、应用场景、市场影响、潜在挑战、竞品对比等)主动构建 3-5 个合理的探索性任务。
+请确保输出的 JSON 格式正确,不要输出空数组。
 """
 
 
@@ -75,7 +76,8 @@ task_summarizer_instructions = """
 </NOTES>
 
 <FORMAT>
-- 使用 Markdown 输出;
+- **直接**输出 Markdown 格式的总结内容。
+- **严禁**包含任何自我陈述、规划或废话(如“我将...”、“基于...”)。
 - 以小节标题开头:"任务总结";
 - 关键发现使用有序或无序列表表达;
 - 若任务无有效结果,输出"暂无可用信息"。

+ 1 - 1
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/__init__.py

@@ -1,2 +1,2 @@
-"""Domain services for the deep researcher workflow."""
+"""深度研究工作流的领域服务。"""
 

+ 31 - 9
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/audio_generator.py

@@ -1,4 +1,4 @@
-"""Service for generating audio from text using TTS API."""
+"""使用 TTS API 从文本生成音频的服务。"""
 
 from __future__ import annotations
 
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
 
 
 class AudioGenerationService:
-    """Handles interaction with TTS service to generate audio files."""
+    """处理与 TTS 服务的交互以生成音频文件。"""
 
     def __init__(self, config: Configuration) -> None:
         self._config = config
@@ -23,7 +23,11 @@ class AudioGenerationService:
         self._ensure_output_dir()
 
     def _ensure_output_dir(self) -> None:
-        """Create output directory if it doesn't exist."""
+        """
+        如果输出目录不存在,则创建它。
+        
+        同时处理创建目录时的潜在权限错误。
+        """
         if not self._output_dir.exists():
             try:
                 self._output_dir.mkdir(parents=True, exist_ok=True)
@@ -33,14 +37,14 @@ class AudioGenerationService:
 
     def generate_audio(self, script: List[dict[str, str]], task_id: str = "default") -> List[str]:
         """
-        Generate audio files for a given script.
+        为给定的脚本生成音频文件。
         
         Args:
-            script: List of dialogue turns, e.g. [{"role": "Host", "content": "..."}, ...]
-            task_id: Unique identifier for the current task/session
+            script: 对话回合列表,例如 [{"role": "Host", "content": "..."}, ...]
+            task_id: 当前任务/会话的唯一标识符
             
         Returns:
-            List of paths to generated audio files
+            生成的音频文件的路径列表
         """
         # 检查FFmpeg路径是否配置
         if not self._config.ffmpeg_path:
@@ -76,7 +80,15 @@ class AudioGenerationService:
         return generated_files
 
     def _get_voice_for_role(self, role: str) -> str:
-        """Map role names to voice IDs."""
+        """
+        将角色名称映射到语音 ID。
+        
+        Args:
+            role: 角色名称(如 Host, Guest)。
+            
+        Returns:
+            对应的语音 ID(xiayu 或 liwa)。
+        """
         role_lower = role.lower()
         if "host" in role_lower or "xiayu" in role_lower:
             return "xiayu"
@@ -85,7 +97,17 @@ class AudioGenerationService:
         return "xiayu"
 
     def _call_tts_api(self, text: str, voice: str, output_path: Path) -> bool:
-        """Call the TTS API and save the audio file."""
+        """
+        调用 TTS API 并保存音频文件。
+        
+        Args:
+            text: 要转换的文本。
+            voice: 语音 ID。
+            output_path: 输出文件路径。
+            
+        Returns:
+            如果成功生成并保存,返回 True;否则返回 False。
+        """
         if output_path.exists():
             logger.debug("Audio file already exists: %s", output_path)
             return True

+ 11 - 11
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/audio_synthesizer.py

@@ -1,4 +1,4 @@
-"""Service for synthesizing audio segments into a single podcast file."""
+"""将音频片段合成为单个播客文件的服务。"""
 
 from __future__ import annotations
 
@@ -14,30 +14,30 @@ logger = logging.getLogger(__name__)
 
 
 class PodcastSynthesisService:
-    """Combines multiple audio segments into a final podcast file."""
+    """将多个音频片段组合成最终的播客文件。"""
 
     def __init__(self, config: Configuration) -> None:
         self._config = config
         self._output_dir = Path(config.audio_output_dir)
         
-        # Configure ffmpeg path if provided
+        # 如果提供了 ffmpeg 路径,则进行配置
         if config.ffmpeg_path:
             AudioSegment.converter = config.ffmpeg_path
             logger.info("Configured ffmpeg path: %s", config.ffmpeg_path)
         
-        # Ensure pydub/ffmpeg is available - assuming ffmpeg is installed on system
-        # If not, pydub might warn or fail, but we'll catch exceptions.
+        # 确保 pydub/ffmpeg 可用 - 假设 ffmpeg 已安装在系统中
+        # 如果没有,pydub 可能会发出警告或失败,但我们会捕获异常。
 
     def synthesize_podcast(self, audio_files: List[str], task_id: str = "default") -> str | None:
         """
-        Combine audio files into a single podcast MP3.
+        将音频文件组合成单个播客 MP3。
 
         Args:
-            audio_files: List of paths to input audio files in order.
-            task_id: Unique identifier for the output filename.
+            audio_files: 按顺序排列的输入音频文件路径列表。
+            task_id: 输出文件名的唯一标识符。
 
         Returns:
-            Path to the final podcast file, or None if failed.
+            最终播客文件的路径,如果失败则为 None。
         """
         if not audio_files:
             logger.warning("No audio files provided for synthesis.")
@@ -46,7 +46,7 @@ class PodcastSynthesisService:
         try:
             combined = AudioSegment.empty()
             
-            # Silence between segments (e.g. 500ms)
+            # 片段之间的静音(例如 500ms)
             silence = AudioSegment.silent(duration=500)
 
             valid_segments_count = 0
@@ -72,7 +72,7 @@ class PodcastSynthesisService:
             output_filename = f"podcast_{task_id}.mp3"
             output_path = self._output_dir / output_filename
             
-            # Export
+            # 导出
             logger.info("Exporting podcast to %s...", output_path)
             combined.export(output_path, format="mp3")
             

+ 2 - 2
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/notes.py

@@ -1,4 +1,4 @@
-"""Helpers for coordinating note tool usage instructions."""
+"""协调笔记工具使用说明的助手。"""
 
 from __future__ import annotations
 
@@ -8,7 +8,7 @@ from models import TodoItem
 
 
 def build_note_guidance(task: TodoItem) -> str:
-    """Generate note tool usage guidance for a specific task."""
+    """为特定任务生成笔记工具使用说明。"""
 
     tags_list = ["deep_research", f"task_{task.id}"]
     tags_literal = json.dumps(tags_list, ensure_ascii=False)

+ 24 - 8
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/planner.py

@@ -1,4 +1,4 @@
-"""Service responsible for converting the research topic into actionable tasks."""
+"""负责将研究主题转换为可操作任务的服务。"""
 
 from __future__ import annotations
 
@@ -22,14 +22,22 @@ TOOL_CALL_PATTERN = re.compile(
 )
 
 class PlanningService:
-    """Wraps the planner agent to produce structured TODO items."""
+    """包装规划器代理以生成结构化 TODO 项目。"""
 
     def __init__(self, planner_agent: ToolAwareSimpleAgent, config: Configuration) -> None:
         self._agent = planner_agent
         self._config = config
 
     def plan_todo_list(self, state: SummaryState) -> List[TodoItem]:
-        """Ask the planner agent to break the topic into actionable tasks."""
+        """
+        要求规划器代理将主题分解为可操作的任务。
+        
+        Args:
+            state: 当前研究状态,包含主题。
+            
+        Returns:
+            规划出的 TodoItem 列表。
+        """
 
         prompt = todo_planner_instructions.format(
             current_date=get_current_date(),
@@ -68,7 +76,11 @@ class PlanningService:
 
     @staticmethod
     def create_fallback_task(state: SummaryState) -> TodoItem:
-        """Create a minimal fallback task when planning failed."""
+        """
+        规划失败时创建一个最小的回退任务。
+        
+        当 LLM 无法生成有效的 JSON 任务列表时调用。
+        """
 
         return TodoItem(
             id=1,
@@ -78,10 +90,14 @@ class PlanningService:
         )
 
     # ------------------------------------------------------------------
-    # Parsing helpers
+    # 解析助手
     # ------------------------------------------------------------------
     def _extract_tasks(self, raw_response: str) -> List[dict[str, Any]]:
-        """Parse planner output into a list of task dictionaries."""
+        """
+        将规划器输出解析为任务字典列表。
+        
+        支持纯 JSON 格式或嵌入在工具调用中的 JSON。
+        """
 
         text = raw_response.strip()
         if self._config.strip_thinking_tokens:
@@ -111,7 +127,7 @@ class PlanningService:
         return tasks
 
     def _extract_json_payload(self, text: str) -> Optional[dict[str, Any] | list]:
-        """Try to locate and parse a JSON object or array from the text."""
+        """尝试从文本中定位并解析 JSON 对象或数组。"""
 
         start = text.find("{")
         end = text.rfind("}")
@@ -134,7 +150,7 @@ class PlanningService:
         return None
 
     def _extract_tool_payload(self, text: str) -> Optional[dict[str, Any]]:
-        """Parse the first TOOL_CALL expression in the output."""
+        """解析输出中的第一个 TOOL_CALL 表达式。"""
 
         match = TOOL_CALL_PATTERN.search(text)
         if not match:

+ 11 - 3
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/reporter.py

@@ -1,4 +1,4 @@
-"""Service that consolidates task results into the final report."""
+"""将任务结果整合为最终报告的服务。"""
 
 from __future__ import annotations
 
@@ -13,14 +13,22 @@ from services.text_processing import strip_tool_calls
 
 
 class ReportingService:
-    """Generates the final structured report."""
+    """生成最终的结构化报告。"""
 
     def __init__(self, report_agent: ToolAwareSimpleAgent, config: Configuration) -> None:
         self._agent = report_agent
         self._config = config
 
     def generate_report(self, state: SummaryState) -> str:
-        """Generate a structured report based on completed tasks."""
+        """
+        基于完成的任务生成结构化报告。
+        
+        Args:
+            state: 包含任务结果和笔记的研究状态。
+            
+        Returns:
+            Markdown 格式的报告文本。
+        """
 
         tasks_block = []
         for task in state.todo_items:

+ 14 - 6
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/script_generator.py

@@ -1,4 +1,4 @@
-"""Service that converts the research report into a podcast script."""
+"""将研究报告转换为播客脚本的服务。"""
 
 from __future__ import annotations
 
@@ -17,14 +17,22 @@ logger = logging.getLogger(__name__)
 
 
 class ScriptGenerationService:
-    """Generates a dialogue script from the research report."""
+    """从研究报告生成对话脚本。"""
 
     def __init__(self, script_agent: ToolAwareSimpleAgent, config: Configuration) -> None:
         self._agent = script_agent
         self._config = config
 
     def generate_script(self, state: SummaryState) -> List[dict[str, str]]:
-        """Generate a podcast script based on the structured report."""
+        """
+        基于结构化报告生成播客脚本。
+        
+        Args:
+            state: 包含结构化报告的研究状态。
+            
+        Returns:
+            对话脚本列表,每项包含 role 和 content。
+        """
 
         if not state.structured_report:
             logger.warning("No structured report available for script generation.")
@@ -40,13 +48,13 @@ class ScriptGenerationService:
         
         cleaned_response = response.strip()
         
-        # 1. Try to find markdown code block
+        # 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()
         else:
-            # 2. Try to find content between [ and ]
+            # 2. 尝试查找 [ 和 ] 之间的内容
             start = cleaned_response.find("[")
             end = cleaned_response.rfind("]")
             if start != -1 and end != -1 and end > start:
@@ -58,7 +66,7 @@ class ScriptGenerationService:
                 logger.error("Script generation output is not a list: %s", type(script))
                 return []
             
-            # Validate script format
+            # 验证脚本格式
             valid_script = []
             for item in script:
                 if isinstance(item, dict) and "role" in item and "content" in item:

+ 24 - 4
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/search.py

@@ -1,4 +1,4 @@
-"""Search dispatch helpers leveraging HelloAgents SearchTool."""
+"""利用 HelloAgents SearchTool 的搜索分发助手。"""
 
 from __future__ import annotations
 
@@ -21,7 +21,7 @@ _GLOBAL_SEARCH_TOOL = None
 
 
 def get_global_search_tool(config: Configuration) -> SearchTool:
-    """Lazy initialization of the global search tool with API keys."""
+    """使用 API 密钥延迟初始化全局搜索工具。"""
     global _GLOBAL_SEARCH_TOOL
     if _GLOBAL_SEARCH_TOOL is None:
         _GLOBAL_SEARCH_TOOL = SearchTool(
@@ -37,7 +37,17 @@ def dispatch_search(
     config: Configuration,
     loop_count: int,
 ) -> Tuple[dict[str, Any] | None, list[str], Optional[str], str]:
-    """Execute configured search backend and normalise response payload."""
+    """
+    执行配置的搜索后端并标准化响应负载。
+    
+    Args:
+        query: 搜索查询字符串。
+        config: 包含搜索 API 配置的对象。
+        loop_count: 当前研究循环计数(用于分页或深度控制)。
+        
+    Returns:
+        元组 (原始负载, 通知列表, 答案文本, 后端标签)。
+    """
 
     search_api = get_config_value(config.search_api)
     search_tool = get_global_search_tool(config)
@@ -95,7 +105,17 @@ def prepare_research_context(
     answer_text: Optional[str],
     config: Configuration,
 ) -> tuple[str, str]:
-    """Build structured context and source summary for downstream agents."""
+    """
+    为下游代理构建结构化上下文和来源摘要。
+    
+    Args:
+        search_result: 搜索后端返回的原始结果字典。
+        answer_text: 搜索后端直接生成的答案(如果有)。
+        config: 配置对象。
+        
+    Returns:
+        元组 (来源摘要列表, 详细上下文文本)。
+    """
 
     sources_summary = format_sources(search_result)
     context = deduplicate_and_format_sources(

+ 9 - 5
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/summarizer.py

@@ -1,4 +1,4 @@
-"""Task summarization utilities."""
+"""任务总结工具。"""
 
 from __future__ import annotations
 
@@ -15,7 +15,7 @@ from services.text_processing import strip_tool_calls
 
 
 class SummarizationService:
-    """Handles synchronous and streaming task summarization."""
+    """处理同步和流式任务总结。"""
 
     def __init__(
         self,
@@ -26,7 +26,7 @@ class SummarizationService:
         self._config = config
 
     def summarize_task(self, state: SummaryState, task: TodoItem, context: str) -> str:
-        """Generate a task-specific summary using the summarizer agent."""
+        """使用总结代理生成特定于任务的总结。"""
 
         prompt = self._build_prompt(state, task, context)
 
@@ -47,7 +47,7 @@ class SummarizationService:
     def stream_task_summary(
         self, state: SummaryState, task: TodoItem, context: str
     ) -> Tuple[Iterator[str], Callable[[], str]]:
-        """Stream the summary text for a task while collecting full output."""
+        """流式传输任务的总结文本,同时收集完整输出。"""
 
         prompt = self._build_prompt(state, task, context)
         remove_thinking = self._config.strip_thinking_tokens
@@ -57,6 +57,10 @@ class SummarizationService:
         agent = self._agent_factory()
 
         def flush_visible() -> Iterator[str]:
+            """
+            处理缓冲区,提取并 yield 所有不在 <think>...</think> 块中的可见文本。
+            如果遇到不完整的 <think> 标签,会暂停输出等待更多数据。
+            """
             nonlocal emit_index, raw_buffer
             while True:
                 start = raw_buffer.find("<think>", emit_index)
@@ -112,7 +116,7 @@ class SummarizationService:
         return generator(), get_summary
 
     def _build_prompt(self, state: SummaryState, task: TodoItem, context: str) -> str:
-        """Construct the summarization prompt shared by both modes."""
+        """构建两种模式共享的总结提示。"""
 
         return (
             f"任务主题:{state.research_topic}\n"

+ 1 - 1
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/text_processing.py

@@ -1,4 +1,4 @@
-"""Utility helpers for normalizing agent generated text."""
+"""用于标准化代理生成文本的实用助手。"""
 
 from __future__ import annotations
 

+ 42 - 12
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/tool_events.py

@@ -1,4 +1,4 @@
-"""Utility for collecting and exposing tool call events."""
+"""用于收集和暴露工具调用事件的实用程序。"""
 
 from __future__ import annotations
 
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
 
 @dataclass
 class ToolCallEvent:
-    """Internal representation of a tool call event."""
+    """工具调用事件的内部表示。"""
 
     id: int
     agent: str
@@ -29,7 +29,7 @@ class ToolCallEvent:
 
 
 class ToolCallTracker:
-    """Collects tool call events and converts them to SSE payloads."""
+    """收集工具调用事件并将其转换为 SSE 负载。"""
 
     def __init__(self, notes_workspace: Optional[str]) -> None:
         self._notes_workspace = notes_workspace
@@ -39,7 +39,12 @@ class ToolCallTracker:
         self._event_sink: Optional[Callable[[dict[str, Any]], None]] = None
 
     def record(self, payload: dict[str, Any]) -> None:
-        """记录模型工具调用情况,便于日志与前端展示。"""
+        """
+        记录模型工具调用情况,便于日志与前端展示。
+        
+        Args:
+            payload: 工具调用事件负载,包含工具名、参数和结果。
+        """
 
         agent_name = str(payload.get("agent_name") or "unknown")
         tool_name = str(payload.get("tool_name") or "unknown")
@@ -86,10 +91,22 @@ class ToolCallTracker:
             sink(self._build_payload(event, step=None))
 
     # ------------------------------------------------------------------
-    # Draining helpers
+    # 排放助手
     # ------------------------------------------------------------------
     def drain(self, state: SummaryState, *, step: Optional[int] = None) -> list[dict[str, Any]]:
-        """提取尚未消费的工具调用事件,并同步任务的 note_id。"""
+        """
+        提取尚未消费的工具调用事件,并同步任务的 note_id。
+        
+        此方法是线程安全的,会移除已提取的事件,避免重复处理。
+        同时会检查 note 工具的调用,更新任务状态中的 note_id。
+        
+        Args:
+            state: 当前研究状态。
+            step: 可选的步骤编号,附加到返回的事件中。
+            
+        Returns:
+            准备发送给前端的事件字典列表。
+        """
 
         with self._lock:
             if self._cursor >= len(self._events):
@@ -113,14 +130,22 @@ class ToolCallTracker:
         return payloads
 
     def reset(self) -> None:
-        """Clear recorded events."""
-
+        """
+        清除记录的工具调用事件。
+        
+        此方法是线程安全的,确保在多线程环境中安全调用。
+        """
         with self._lock:
             self._events.clear()
             self._cursor = 0
 
     def as_dicts(self) -> list[dict[str, Any]]:
-        """Expose a snapshot of raw events for backwards compatibility."""
+        """
+        暴露原始事件的快照以实现向后兼容性。
+        
+        Returns:
+            包含所有工具调用事件的字典列表。
+        """
 
         with self._lock:
             return [
@@ -138,7 +163,12 @@ class ToolCallTracker:
             ]
 
     def set_event_sink(self, sink: Optional[Callable[[dict[str, Any]], None]]) -> None:
-        """Register a callback for immediate tool event notifications."""
+        """
+        注册一个回调以获取即时工具事件通知。
+        
+        Args:
+            sink: 接收事件字典的回调函数。
+        """
 
         self._event_sink = sink
 
@@ -161,10 +191,10 @@ class ToolCallTracker:
         return payload
 
     # ------------------------------------------------------------------
-    # Internal helpers
+    # 内部助手
     # ------------------------------------------------------------------
     def _attach_note_to_task(self, tasks: list[TodoItem], task_id: int, note_id: str) -> None:
-        """Update matching TODO item with note metadata."""
+        """使用笔记元数据更新匹配的 TODO 项目。"""
 
         for task in tasks:
             if task.id != task_id:

+ 15 - 5
Co-creation-projects/JJason-DeepCastAgent/backend/src/utils.py

@@ -1,4 +1,4 @@
-"""Utility helpers shared across deep researcher services."""
+"""深层研究服务共享的实用助手。"""
 
 from __future__ import annotations
 
@@ -11,13 +11,13 @@ logger = logging.getLogger(__name__)
 
 
 def get_config_value(value: Any) -> str:
-    """Return configuration value as plain string."""
+    """以纯字符串形式返回配置值。"""
 
     return value if isinstance(value, str) else value.value
 
 
 def strip_thinking_tokens(text: str) -> str:
-    """Remove ``<think>`` sections from model responses."""
+    """移除模型响应中的 ``<think>`` 部分。"""
 
     while "<think>" in text and "</think>" in text:
         start = text.find("<think>")
@@ -32,7 +32,17 @@ def deduplicate_and_format_sources(
     *,
     fetch_full_page: bool = False,
 ) -> str:
-    """Format and deduplicate search results for downstream prompting."""
+    """
+    格式化并去重搜索结果以供下游提示使用。
+    
+    Args:
+        search_response: 原始搜索响应(字典或列表)。
+        max_tokens_per_source: 每个来源截取的最大 Token 数。
+        fetch_full_page: 是否尝试使用完整页面内容(如果可用)。
+        
+    Returns:
+        格式化后的上下文文本字符串。
+    """
 
     if isinstance(search_response, dict):
         sources_list = search_response.get("results", [])
@@ -71,7 +81,7 @@ def deduplicate_and_format_sources(
 
 
 def format_sources(search_results: Dict[str, Any] | None) -> str:
-    """Return bullet list summarising search sources."""
+    """返回总结搜索来源的项目符号列表。"""
 
     if not search_results:
         return ""

+ 1 - 1
Co-creation-projects/JJason-DeepCastAgent/frontend/index.html

@@ -3,7 +3,7 @@
   <head>
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>HelloAgents 深度研究助手</title>
+    <title>DeepCast 深度播客 Agent</title>
   </head>
   <body>
     <div id="app"></div>

+ 82 - 0
Co-creation-projects/JJason-DeepCastAgent/frontend/package-lock.json

@@ -8,7 +8,9 @@
       "name": "helloagents-deepresearch-frontend",
       "version": "0.1.0",
       "dependencies": {
+        "@types/markdown-it": "^14.1.2",
         "axios": "^1.7.9",
+        "markdown-it": "^14.1.0",
         "vue": "^3.5.13"
       },
       "devDependencies": {
@@ -828,12 +830,35 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/linkify-it": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+      "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+      "license": "MIT"
+    },
+    "node_modules/@types/markdown-it": {
+      "version": "14.1.2",
+      "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+      "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/linkify-it": "^5",
+        "@types/mdurl": "^2"
+      }
+    },
+    "node_modules/@types/mdurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+      "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+      "license": "MIT"
+    },
     "node_modules/@types/node": {
       "version": "22.18.12",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz",
       "integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "undici-types": "~6.21.0"
       }
@@ -1024,6 +1049,12 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "license": "Python-2.0"
+    },
     "node_modules/asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1400,6 +1431,15 @@
         "he": "bin/he"
       }
     },
+    "node_modules/linkify-it": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+      "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+      "license": "MIT",
+      "dependencies": {
+        "uc.micro": "^2.0.0"
+      }
+    },
     "node_modules/magic-string": {
       "version": "0.30.19",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
@@ -1409,6 +1449,23 @@
         "@jridgewell/sourcemap-codec": "^1.5.5"
       }
     },
+    "node_modules/markdown-it": {
+      "version": "14.1.0",
+      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
+      "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1",
+        "entities": "^4.4.0",
+        "linkify-it": "^5.0.0",
+        "mdurl": "^2.0.0",
+        "punycode.js": "^2.3.1",
+        "uc.micro": "^2.1.0"
+      },
+      "bin": {
+        "markdown-it": "bin/markdown-it.mjs"
+      }
+    },
     "node_modules/math-intrinsics": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1418,6 +1475,12 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/mdurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+      "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+      "license": "MIT"
+    },
     "node_modules/mime-db": {
       "version": "1.52.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -1499,6 +1562,7 @@
       "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "engines": {
         "node": ">=12"
       },
@@ -1540,6 +1604,15 @@
       "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
       "license": "MIT"
     },
+    "node_modules/punycode.js": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+      "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/rollup": {
       "version": "4.52.5",
       "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
@@ -1614,6 +1687,7 @@
       "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
       "devOptional": true,
       "license": "Apache-2.0",
+      "peer": true,
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -1622,6 +1696,12 @@
         "node": ">=14.17"
       }
     },
+    "node_modules/uc.micro": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+      "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+      "license": "MIT"
+    },
     "node_modules/undici-types": {
       "version": "6.21.0",
       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@@ -1635,6 +1715,7 @@
       "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "esbuild": "^0.25.0",
         "fdir": "^6.4.4",
@@ -1716,6 +1797,7 @@
       "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
       "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@vue/compiler-dom": "3.5.22",
         "@vue/compiler-sfc": "3.5.22",

+ 2 - 0
Co-creation-projects/JJason-DeepCastAgent/frontend/package.json

@@ -9,7 +9,9 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@types/markdown-it": "^14.1.2",
     "axios": "^1.7.9",
+    "markdown-it": "^14.1.0",
     "vue": "^3.5.13"
   },
   "devDependencies": {

+ 958 - 1976
Co-creation-projects/JJason-DeepCastAgent/frontend/src/App.vue

@@ -1,2304 +1,1286 @@
 <template>
-  <main class="app-shell" :class="{ expanded: isExpanded }">
-    <div class="aurora" aria-hidden="true">
-      <span></span>
-      <span></span>
-      <span></span>
-    </div>
-
-    <!-- 初始状态:居中输入卡片 -->
-    <div v-if="!isExpanded" class="layout layout-centered">
-      <section class="panel panel-form panel-centered">
-        <header class="panel-head">
-          <div class="logo">
-            <svg viewBox="0 0 24 24" aria-hidden="true">
-              <path
-                d="M12 2.5c-.7 0-1.4.2-2 .6L4.6 7C3.6 7.6 3 8.7 3 9.9v4.2c0 1.2.6 2.3 1.6 2.9l5.4 3.9c1.2.8 2.8.8 4 0l5.4-3.9c1-.7 1.6-1.7 1.6-2.9V9.9c0-1.2-.6-2.3-1.6-2.9L14 3.1a3.6 3.6 0 0 0-2-.6Z"
-              />
-            </svg>
-          </div>
-          <div>
-            <h1>深度研究助手</h1>
-            <p>结合多轮智能检索与总结,实时呈现洞见与引用。</p>
-          </div>
+  <div class="deepcast-container" :class="currentView">
+    <div class="background-gradient"></div>
+
+    <!-- 1. Setup View: 输入主题与配置 -->
+    <transition name="fade" mode="out-in">
+      <section v-if="currentView === 'setup'" class="view-setup" key="setup">
+        <header class="brand-header">
+          <div class="logo-icon">🎙️</div>
+          <h1>DeepCast</h1>
+          <p class="tagline">将深度研究转化为引人入胜的播客。</p>
         </header>
 
-        <form class="form" @submit.prevent="handleSubmit">
-          <label class="field">
-            <span>研究主题</span>
-            <textarea
-              v-model="form.topic"
-              placeholder="例如:探索多模态模型在 2025 年的关键突破"
-              rows="4"
+        <form @submit.prevent="startProduction" class="setup-form">
+          <div class="input-group">
+            <label>播客主题</label>
+            <textarea 
+              v-model="form.topic" 
+              placeholder="今天我们聊点什么?(例如:AI Agent 的未来)"
+              rows="3"
               required
+              @keydown.enter.prevent="startProduction"
             ></textarea>
-          </label>
-
-          <section class="options">
-            <label class="field option">
-              <span>搜索引擎</span>
-              <select v-model="form.searchApi">
-                <option value="">沿用后端配置</option>
-                <option
-                  v-for="option in searchOptions"
-                  :key="option"
-                  :value="option"
-                >
-                  {{ option }}
-                </option>
-              </select>
-            </label>
-          </section>
-
-          <div class="form-actions">
-            <button class="submit" type="submit" :disabled="loading">
-              <span class="submit-label">
-                <svg
-                  v-if="loading"
-                  class="spinner"
-                  viewBox="0 0 24 24"
-                  aria-hidden="true"
-                >
-                  <circle cx="12" cy="12" r="9" stroke-width="3" />
-                </svg>
-                {{ loading ? "研究进行中..." : "开始研究" }}
-              </span>
-            </button>
-            <button
-              v-if="loading"
-              type="button"
-              class="secondary-btn"
-              @click="cancelResearch"
-            >
-              取消研究
-            </button>
           </div>
-        </form>
 
-        <p v-if="error" class="error-chip">
-          <svg viewBox="0 0 20 20" aria-hidden="true">
-            <path
-              d="M10 3.2c-.3 0-.6.2-.8.5L3.4 15c-.4.7.1 1.6.8 1.6h11.6c.7 0 1.2-.9.8-1.6L10.8 3.7c-.2-.3-.5-.5-.8-.5Zm0 4.3c.4 0 .7.3.7.7v4c0 .4-.3.7-.7.7s-.7-.3-.7-.7V8.2c0-.4.3-.7.7-.7Zm0 6.6a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"
-            />
-          </svg>
-          {{ error }}
-        </p>
-        <p v-else-if="loading" class="hint muted">
-          正在收集线索与证据,实时进展见右侧区域。
-        </p>
-      </section>
-    </div>
-
-    <!-- 全屏状态:左右分栏布局 -->
-    <div v-else class="layout layout-fullscreen">
-      <!-- 左侧:研究信息 -->
-      <aside class="sidebar">
-        <div class="sidebar-header">
-          <button class="back-btn" @click="goBack" :disabled="loading">
-            <svg viewBox="0 0 24 24" width="20" height="20">
-              <path d="M19 12H5M12 19l-7-7 7-7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
-            </svg>
-            返回
+          <div class="settings-row">
+            <div class="setting-item">
+              <label>搜索引擎</label>
+              <div class="select-wrapper">
+                <select v-model="form.searchApi">
+                  <option value="hybrid">混合搜索 (Tavily + SerpApi)</option>
+                  <option value="tavily">仅 Tavily</option>
+                  <option value="serpapi">仅 SerpApi</option>
+                </select>
+                <span class="select-arrow">▼</span>
+              </div>
+            </div>
+          </div>
+
+          <button type="submit" class="cta-button" :disabled="!form.topic.trim()">
+            <span>开始制作播客</span>
+            <span class="icon">✨</span>
           </button>
-          <h2>🔍 深度研究助手</h2>
-        </div>
+        </form>
+      </section>
 
-        <div class="research-info">
-          <div class="info-item">
-            <label>研究主题</label>
-            <p class="topic-display">{{ form.topic }}</p>
+      <!-- 2. Production View: 制作进度监控 -->
+      <section v-else-if="currentView === 'producing'" class="view-production" key="production">
+        <div class="production-content">
+          <header class="production-header">
+            <h2>正在制作您的播客</h2>
+            <button class="cancel-btn" @click="cancelProduction">取消</button>
+          </header>
+
+          <div class="stage-monitor">
+            <div class="stage-step" :class="{ active: productionStage === 'research', completed: isStageCompleted('research') }">
+              <div class="step-icon">🔍</div>
+              <div class="step-label">深度研究</div>
+            </div>
+            <div class="stage-line"></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-step" :class="{ active: productionStage === 'audio', completed: isStageCompleted('audio') }">
+              <div class="step-icon">🎧</div>
+              <div class="step-label">音频合成</div>
+            </div>
           </div>
 
-          <div class="info-item" v-if="form.searchApi">
-            <label>搜索引擎</label>
-            <p>{{ form.searchApi }}</p>
+          <div class="terminal-log" v-if="logs.length > 0">
+            <div class="log-content" ref="logContainer">
+              <div v-for="(log, i) in logs" :key="i" class="log-entry">
+                <span class="log-time">{{ log.time }}</span>
+                <span class="log-msg">{{ log.message }}</span>
+              </div>
+            </div>
           </div>
 
-          <div class="info-item" v-if="totalTasks > 0">
-            <label>研究进度</label>
-            <div class="progress-bar">
-              <div class="progress-fill" :style="{ width: `${(completedTasks / totalTasks) * 100}%` }"></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>
             </div>
-            <p class="progress-text">{{ completedTasks }} / {{ totalTasks }} 任务完成</p>
           </div>
         </div>
+      </section>
 
-        <div class="sidebar-actions">
-          <button class="new-research-btn" @click="startNewResearch">
-            <svg viewBox="0 0 24 24" width="18" height="18">
-              <path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
-            </svg>
-            开始新研究
-          </button>
-        </div>
-      </aside>
-
-      <!-- 右侧:研究结果 -->
-      <section
-        class="panel panel-result"
-        v-if="todoTasks.length || reportMarkdown || progressLogs.length"
-      >
-        <header class="status-bar">
-          <div class="status-main">
-            <div class="status-chip" :class="{ active: loading }">
-              <span class="dot"></span>
-              {{ loading ? "研究进行中" : "研究流程完成" }}
-            </div>
-            <span class="status-meta">
-              任务进度:{{ completedTasks }} / {{ totalTasks || todoTasks.length || 1 }}
-              · 阶段记录 {{ progressLogs.length }} 条
-            </span>
-          </div>
-          <div class="status-controls">
-            <button class="secondary-btn" @click="logsCollapsed = !logsCollapsed">
-              {{ logsCollapsed ? "展开流程" : "收起流程" }}
+      <!-- 3. Player View: 播放器与脚本 -->
+      <section v-else-if="currentView === 'player'" class="view-player" key="player">
+        <div class="player-layout">
+          <!-- Left: Player Control -->
+          <div class="player-sidebar">
+            <button class="back-home-btn" @click="resetApp">
+              ← 制作新播客
             </button>
-          </div>
-        </header>
+            
+            <div class="album-art">
+              <div class="vinyl-record" :class="{ spinning: isPlaying }">
+                <div class="vinyl-label">DC</div>
+              </div>
+            </div>
 
-        <div class="timeline-wrapper" v-show="!logsCollapsed && progressLogs.length">
-          <transition-group name="timeline" tag="ul" class="timeline">
-            <li v-for="(log, index) in progressLogs" :key="`${log}-${index}`">
-              <span class="timeline-node"></span>
-              <p>{{ log }}</p>
-            </li>
-          </transition-group>
-        </div>
+            <div class="track-info">
+              <h3>{{ form.topic }}</h3>
+              <p>DeepCast 原创播客</p>
+            </div>
 
-        <div class="tasks-section" v-if="todoTasks.length">
-          <aside class="tasks-list">
-            <h3>任务清单</h3>
-            <ul>
-              <li
-                v-for="task in todoTasks"
-                :key="task.id"
-                :class="['task-item', { active: task.id === activeTaskId, completed: task.status === 'completed' }]"
-              >
-                <button
-                  type="button"
-                  class="task-button"
-                  @click="activeTaskId = task.id"
-                >
-                  <span class="task-title">{{ task.title }}</span>
-                  <span class="task-status" :class="task.status">
-                    {{ formatTaskStatus(task.status) }}
-                  </span>
+            <div class="audio-controls">
+              <audio 
+                ref="audioPlayer" 
+                :src="audioUrl" 
+                @timeupdate="onTimeUpdate"
+                @ended="isPlaying = false"
+                @play="isPlaying = true"
+                @pause="isPlaying = false"
+              ></audio>
+              
+              <div class="control-buttons">
+                <button class="play-btn" @click="togglePlay">
+                  {{ isPlaying ? '⏸' : '▶' }}
                 </button>
-                <p class="task-intent">{{ task.intent }}</p>
-              </li>
-            </ul>
-          </aside>
-
-          <article class="task-detail" v-if="currentTask">
-            <header class="task-header">
-              <div>
-                <h3>{{ currentTaskTitle || "当前任务" }}</h3>
-                <p class="muted" v-if="currentTaskIntent">
-                  {{ currentTaskIntent }}
-                </p>
+                <a :href="audioUrl" download class="download-btn" title="下载 MP3">
+                  ⬇
+                </a>
               </div>
-              <div class="task-chip-group">
-                <span class="task-label">查询:{{ currentTaskQuery || "" }}</span>
-                <span
-                  v-if="currentTaskNoteId"
-                  class="task-label note-chip"
-                  :title="currentTaskNoteId"
-                >
-                  笔记:{{ currentTaskNoteId }}
-                </span>
-                <span
-                  v-if="currentTaskNotePath"
-                  class="task-label note-chip path-chip"
-                  :title="currentTaskNotePath"
-                >
-                  <span class="path-label">路径:</span>
-                  <span class="path-text">{{ currentTaskNotePath }}</span>
-                  <button
-                    class="chip-action"
-                    type="button"
-                    @click="copyNotePath(currentTaskNotePath)"
-                  >
-                    复制
-                  </button>
-                </span>
+              
+              <div class="progress-bar-wrapper" @click="seekAudio">
+                <div class="progress-bar-bg">
+                  <div class="progress-bar-fill" :style="{ width: progressPercent + '%' }"></div>
+                </div>
+                <div class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</div>
               </div>
-            </header>
-
-            <section v-if="currentTask && currentTask.notices.length" class="task-notices">
-              <h4>系统提示</h4>
-              <ul>
-                <li v-for="(notice, idx) in currentTask.notices" :key="`${notice}-${idx}`">
-                  {{ notice }}
-                </li>
-              </ul>
-            </section>
-
-            <section
-              class="sources-block"
-              :class="{ 'block-highlight': sourcesHighlight }"
-            >
-              <h3>最新来源</h3>
-              <template v-if="currentTaskSources.length">
-                <ul class="sources-list">
-                  <li
-                    v-for="(item, index) in currentTaskSources"
-                    :key="`${item.title}-${index}`"
-                    class="source-item"
-                  >
-                    <a
-                      class="source-link"
-                      :href="item.url || '#'"
-                      target="_blank"
-                      rel="noopener noreferrer"
-                    >
-                      {{ item.title || item.url || `来源 ${index + 1}` }}
-                    </a>
-                    <div v-if="item.snippet || item.raw" class="source-tooltip">
-                      <p v-if="item.snippet">{{ item.snippet }}</p>
-                      <p v-if="item.raw" class="muted-text">{{ item.raw }}</p>
-                    </div>
-                  </li>
-                </ul>
-              </template>
-              <p v-else class="muted">暂无可用来源</p>
-            </section>
-
-            <section
-              class="summary-block"
-              :class="{ 'block-highlight': summaryHighlight }"
-            >
-              <h3>任务总结</h3>
-              <pre class="block-pre">{{ currentTaskSummary || "暂无可用信息" }}</pre>
-            </section>
-
-            <section
-              class="tools-block"
-              :class="{ 'block-highlight': toolHighlight }"
-              v-if="currentTaskToolCalls.length"
-            >
-              <h3>工具调用记录</h3>
-              <ul class="tool-list">
-                <li
-                  v-for="entry in currentTaskToolCalls"
-                  :key="`${entry.eventId}-${entry.timestamp}`"
-                  class="tool-entry"
-                >
-                  <div class="tool-entry-header">
-                    <span class="tool-entry-title">
-                      #{{ entry.eventId }} {{ entry.agent }} → {{ entry.tool }}
-                    </span>
-                    <span
-                      v-if="entry.noteId"
-                      class="tool-entry-note"
-                    >
-                      笔记:{{ entry.noteId }}
-                    </span>
-                  </div>
-                  <p v-if="entry.notePath" class="tool-entry-path">
-                    笔记路径:
-                    <button
-                      class="link-btn"
-                      type="button"
-                      @click="copyNotePath(entry.notePath)"
-                    >
-                      复制
-                    </button>
-                    <span class="path-text">{{ entry.notePath }}</span>
-                  </p>
-                  <p class="tool-subtitle">参数</p>
-                  <pre class="tool-pre">{{ formatToolParameters(entry.parameters) }}</pre>
-                  <template v-if="entry.result">
-                    <p class="tool-subtitle">执行结果</p>
-                    <pre class="tool-pre">{{ formatToolResult(entry.result) }}</pre>
-                  </template>
-                </li>
-              </ul>
-            </section>
-          </article>
-
-          <article class="task-detail" v-else>
-            <p class="muted">等待任务规划或执行结果。</p>
-          </article>
-        </div>
+            </div>
+
+            <div class="report-toggle">
+              <button @click="showReport = !showReport">
+                {{ showReport ? '隐藏深度研究报告' : '查看深度研究报告' }}
+              </button>
+            </div>
+          </div>
 
-        <div
-          v-if="reportMarkdown"
-          class="report-block"
-          :class="{ 'block-highlight': reportHighlight }"
-        >
-          <h3>最终报告</h3>
-          <pre class="block-pre">{{ reportMarkdown }}</pre>
+          <!-- Right: Script / Report -->
+          <div class="content-main">
+            <div v-if="!showReport" class="script-chat">
+              <div 
+                v-for="(line, idx) in podcastScript" 
+                :key="idx" 
+                class="chat-bubble"
+                :class="line.role.toLowerCase()"
+              >
+                <div class="avatar">{{ line.role[0] }}</div>
+                <div class="bubble-content">
+                  <div class="speaker-name">{{ line.role }}</div>
+                  <p>{{ line.content }}</p>
+                </div>
+              </div>
+            </div>
+
+            <div v-else class="markdown-report">
+              <div class="report-content" v-html="md.render(reportMarkdown)"></div>
+            </div>
+          </div>
         </div>
       </section>
-
-    </div>
-  </main>
+    </transition>
+  </div>
 </template>
 
 <script lang="ts" setup>
-import { computed, onBeforeUnmount, reactive, ref } from "vue";
-
-import {
-  runResearchStream,
-  type ResearchStreamEvent
-} from "./services/api";
-
-interface SourceItem {
-  title: string;
-  url: string;
-  snippet: string;
-  raw: string;
-}
-
-interface ToolCallLog {
-  eventId: number;
-  agent: string;
-  tool: string;
-  parameters: Record<string, unknown>;
-  result: string;
-  noteId: string | null;
-  notePath: string | null;
-  timestamp: number;
-}
-
-interface TodoTaskView {
-  id: number;
-  title: string;
-  intent: string;
-  query: string;
-  status: string;
-  summary: string;
-  sourcesSummary: string;
-  sourceItems: SourceItem[];
-  notices: string[];
-  noteId: string | null;
-  notePath: string | null;
-  toolCalls: ToolCallLog[];
+import { reactive, ref, computed, nextTick, watch } from "vue";
+import { runResearchStream, type ResearchStreamEvent } from "./services/api";
+
+// --- Types ---
+type ViewState = "setup" | "producing" | "player";
+type ProductionStage = "research" | "script" | "audio" | "done";
+
+interface LogEntry {
+  time: string;
+  message: string;
+}
+
+interface PodcastMessage {
+  role: string;
+  content: string;
 }
 
+// --- State ---
+const currentView = ref<ViewState>("setup");
+const productionStage = ref<ProductionStage>("research");
 const form = reactive({
   topic: "",
-  searchApi: ""
+  searchApi: "hybrid"
 });
 
-const loading = ref(false);
-const error = ref("");
-const progressLogs = ref<string[]>([]);
-const logsCollapsed = ref(false);
-const isExpanded = ref(false);
+const logs = ref<LogEntry[]>([]);
+const isPlaying = ref(false);
+const currentTime = ref(0);
+const duration = ref(0);
+const progressPercent = computed(() => (duration.value ? (currentTime.value / duration.value) * 100 : 0));
+const showReport = ref(false);
+
+// Research Progress State
+const totalTasks = ref(0);
+const completedTasks = ref(0);
+const todoList = ref<any[]>([]); // Store the full todo list
+const researchProgress = computed(() => {
+  if (totalTasks.value === 0) return "";
+  return `(${completedTasks.value}/${totalTasks.value})`;
+});
 
-const todoTasks = ref<TodoTaskView[]>([]);
-const activeTaskId = ref<number | null>(null);
+// Data
+const podcastScript = ref<PodcastMessage[]>([]);
 const reportMarkdown = ref("");
+const audioUrl = ref("");
+const currentTask = ref<any>(null); // 简化的任务状态
 
-const summaryHighlight = ref(false);
-const sourcesHighlight = ref(false);
-const reportHighlight = ref(false);
-const toolHighlight = ref(false);
-
-let currentController: AbortController | null = null;
+// Refs
+const audioPlayer = ref<HTMLAudioElement | null>(null);
+const logContainer = ref<HTMLElement | null>(null);
+let abortController: AbortController | null = null;
 
-const searchOptions = [
-  "advanced",
-  "duckduckgo",
-  "tavily",
-  "perplexity",
-  "searxng"
-];
+// --- Computed ---
 
-const TASK_STATUS_LABEL: Record<string, string> = {
-  pending: "待执行",
-  in_progress: "进行中",
-  completed: "已完成",
-  skipped: "已跳过"
-};
+// --- Methods ---
 
-function formatTaskStatus(status: string): string {
-  return TASK_STATUS_LABEL[status] ?? status;
+function isStageCompleted(stage: ProductionStage): boolean {
+  const stages: ProductionStage[] = ["research", "script", "audio", "done"];
+  return stages.indexOf(productionStage.value) > stages.indexOf(stage);
 }
 
-const totalTasks = computed(() => todoTasks.value.length);
-const completedTasks = computed(() =>
-  todoTasks.value.filter((task) => task.status === "completed").length
-);
-
-const currentTask = computed(() => {
-  if (activeTaskId.value !== null) {
-    return todoTasks.value.find((task) => task.id === activeTaskId.value) ?? null;
-  }
-  return todoTasks.value[0] ?? null;
-});
-
-const currentTaskSources = computed(() => currentTask.value?.sourceItems ?? []);
-const currentTaskSummary = computed(() => currentTask.value?.summary ?? "");
-const currentTaskTitle = computed(() => currentTask.value?.title ?? "");
-const currentTaskIntent = computed(() => currentTask.value?.intent ?? "");
-const currentTaskQuery = computed(() => currentTask.value?.query ?? "");
-const currentTaskNoteId = computed(() => currentTask.value?.noteId ?? "");
-const currentTaskNotePath = computed(() => currentTask.value?.notePath ?? "");
-const currentTaskToolCalls = computed(
-  () => currentTask.value?.toolCalls ?? []
-);
-
-const pulse = (flag: typeof summaryHighlight) => {
-  flag.value = false;
-  requestAnimationFrame(() => {
-    flag.value = true;
-    window.setTimeout(() => {
-      flag.value = false;
-    }, 1200);
-  });
-};
-
-function parseSources(raw: string): SourceItem[] {
-  if (!raw) {
-    return [];
-  }
-
-  const items: SourceItem[] = [];
-  const lines = raw.split("\n");
-
-  let current: SourceItem | null = null;
-  const truncate = (value: string, max = 360) => {
-    const trimmed = value.trim();
-    return trimmed.length > max ? `${trimmed.slice(0, max)}…` : trimmed;
-  };
-
-  const flush = () => {
-    if (!current) {
-      return;
-    }
-    const normalized: SourceItem = {
-      title: current.title?.trim() || "",
-      url: current.url?.trim() || "",
-      snippet: current.snippet ? truncate(current.snippet) : "",
-      raw: current.raw ? truncate(current.raw, 420) : ""
-    };
-
-    if (
-      normalized.title ||
-      normalized.url ||
-      normalized.snippet ||
-      normalized.raw
-    ) {
-      if (!normalized.title && normalized.url) {
-        normalized.title = normalized.url;
-      }
-      items.push(normalized);
-    }
-    current = null;
-  };
-
-  const ensureCurrent = () => {
-    if (!current) {
-      current = { title: "", url: "", snippet: "", raw: "" };
-    }
-  };
-
-  for (const line of lines) {
-    const trimmed = line.trim();
-    if (!trimmed) {
-      continue;
-    }
-
-    if (/^\*/.test(trimmed) && trimmed.includes(" : ")) {
-      flush();
-      const withoutBullet = trimmed.replace(/^\*\s*/, "");
-      const [titlePart, urlPart] = withoutBullet.split(" : ");
-      current = {
-        title: titlePart?.trim() || "",
-        url: urlPart?.trim() || "",
-        snippet: "",
-        raw: ""
-      };
-      continue;
-    }
-
-    if (/^(Source|信息来源)\s*:/.test(trimmed)) {
-      flush();
-      const [, titlePart = ""] = trimmed.split(/:\s*(.+)/);
-      current = {
-        title: titlePart.trim(),
-        url: "",
-        snippet: "",
-        raw: ""
-      };
-      continue;
-    }
-
-    if (/^URL\s*:/.test(trimmed)) {
-      ensureCurrent();
-      const [, urlPart = ""] = trimmed.split(/:\s*(.+)/);
-      current!.url = urlPart.trim();
-      continue;
-    }
-
-    if (
-      /^(Most relevant content from source|信息内容)\s*:/.test(trimmed)
-    ) {
-      ensureCurrent();
-      const [, contentPart = ""] = trimmed.split(/:\s*(.+)/);
-      current!.snippet = contentPart.trim();
-      continue;
-    }
-
-    if (
-      /^(Full source content limited to|信息内容限制为)\s*:/.test(trimmed)
-    ) {
-      ensureCurrent();
-      const [, rawPart = ""] = trimmed.split(/:\s*(.+)/);
-      current!.raw = rawPart.trim();
-      continue;
+function addLog(message: string) {
+  const time = new Date().toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
+  logs.value.push({ time, message });
+  nextTick(() => {
+    if (logContainer.value) {
+      logContainer.value.scrollTop = logContainer.value.scrollHeight;
     }
-
-    if (/^https?:\/\//.test(trimmed)) {
-      ensureCurrent();
-      if (!current!.url) {
-        current!.url = trimmed;
-        continue;
-      }
-    }
-
-    ensureCurrent();
-    current!.raw = current!.raw ? `${current!.raw}\n${trimmed}` : trimmed;
-  }
-
-  flush();
-  return items;
+  });
 }
 
-function extractOptionalString(value: unknown): string | null {
-  if (typeof value !== "string") {
-    return null;
-  }
-  const trimmed = value.trim();
-  return trimmed ? trimmed : null;
-}
+async function startProduction() {
+  if (!form.topic.trim()) return;
 
-function ensureRecord(value: unknown): Record<string, unknown> {
-  if (value && typeof value === "object" && !Array.isArray(value)) {
-    return value as Record<string, unknown>;
-  }
-  return {};
-}
+  currentView.value = "producing";
+  productionStage.value = "research";
+  logs.value = [];
+  podcastScript.value = [];
+  reportMarkdown.value = "";
+  audioUrl.value = "";
+  currentTask.value = null;
+  todoList.value = [];
+  totalTasks.value = 0;
+  completedTasks.value = 0;
 
-function applyNoteMetadata(
-  task: TodoTaskView,
-  payload: Record<string, unknown>
-): void {
-  const noteId = extractOptionalString(payload.note_id);
-  if (noteId) {
-    task.noteId = noteId;
-  }
-  const notePath = extractOptionalString(payload.note_path);
-  if (notePath) {
-    task.notePath = notePath;
-  }
-}
+  abortController = new AbortController();
+
+  addLog("🚀 启动 DeepCast 制作流程...");
+  addLog(`主题: ${form.topic}`);
 
-function formatToolParameters(parameters: Record<string, unknown>): string {
   try {
-    return JSON.stringify(parameters, null, 2);
-  } catch (error) {
-    console.warn("无法格式化工具参数", error, parameters);
-    return Object.entries(parameters)
-      .map(([key, value]) => `${key}: ${String(value)}`)
-      .join("\n");
+    await runResearchStream(
+      { topic: form.topic, search_api: form.searchApi },
+      handleStreamEvent,
+      { signal: abortController.signal }
+    );
+  } catch (err) {
+    if (err instanceof DOMException && err.name === "AbortError") {
+      addLog("🛑 制作已取消。");
+    } else {
+      addLog(`❌ 错误: ${err}`);
+      alert("制作失败,请查看日志。");
+    }
   }
 }
 
-function formatToolResult(result: string): string {
-  const trimmed = result.trim();
-  const limit = 900;
-  if (trimmed.length > limit) {
-    return `${trimmed.slice(0, limit)}…`;
+function handleStreamEvent(event: ResearchStreamEvent) {
+  // 1. Tool Calls (增加执行细节)
+  if (event.type === "tool_call") {
+    const payload = event as any;
+    const tool = payload.tool;
+    const agent = payload.agent || "Agent";
+    
+    // 解析具体操作
+    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} 正在记录关键信息`);
+      }
+    } else {
+      addLog(`🔧 ${agent} 调用了工具: ${tool}`);
+    }
+    return;
   }
-  return trimmed;
-}
 
-async function copyNotePath(path: string | null | undefined) {
-  if (!path) {
+  // 2. Sources (发现来源)
+  if (event.type === "sources") {
+    addLog("📚 发现新的信息来源,正在分析...");
     return;
   }
 
-  try {
-    await navigator.clipboard.writeText(path);
-    progressLogs.value.push(`已复制笔记路径:${path}`);
-  } catch (error) {
-    console.warn("无法直接复制到剪贴板", error);
-    window.prompt("复制以下笔记路径", path);
-    progressLogs.value.push("请手动复制笔记路径");
+  // 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";
   }
-}
 
-function resetWorkflowState() {
-  todoTasks.value = [];
-  activeTaskId.value = null;
-  reportMarkdown.value = "";
-  progressLogs.value = [];
-  summaryHighlight.value = false;
-  sourcesHighlight.value = false;
-  reportHighlight.value = false;
-  toolHighlight.value = false;
-  logsCollapsed.value = false;
-}
-
-function findTask(taskId: unknown): TodoTaskView | undefined {
-  const numeric =
-    typeof taskId === "number"
-      ? taskId
-      : typeof taskId === "string"
-      ? Number(taskId)
-      : NaN;
-  if (Number.isNaN(numeric)) {
-    return undefined;
+  // 3.5 Todo List (Total Tasks)
+  if (event.type === "todo_list") {
+    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
+      totalTasks.value = payload.tasks.length;
+      addLog(`📋 规划了 ${totalTasks.value} 个研究任务`);
+    } else {
+      console.warn("Received todo_list but tasks is empty or invalid", payload);
+    }
   }
-  return todoTasks.value.find((task) => task.id === numeric);
-}
 
-function upsertTaskMetadata(task: TodoTaskView, payload: Record<string, unknown>) {
-  if (typeof payload.title === "string" && payload.title.trim()) {
-    task.title = payload.title.trim();
-  }
-  if (typeof payload.intent === "string" && payload.intent.trim()) {
-    task.intent = payload.intent.trim();
+  // 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") {
+      completedTasks.value++;
+      addLog(`✅ 任务完成: ${payload.title}`);
+    } else if (payload.status === "skipped") {
+      completedTasks.value++;
+      addLog(`⏭️ 任务跳过: ${payload.title}`);
+    } else if (payload.status === "failed") {
+      completedTasks.value++;
+      addLog(`❌ 任务失败: ${payload.title}`);
+    }
   }
-  if (typeof payload.query === "string" && payload.query.trim()) {
-    task.query = payload.query.trim();
+  
+  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 = "";
+        }
+        
+        // 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
+      }
   }
-}
 
-const handleSubmit = async () => {
-  if (!form.topic.trim()) {
-    error.value = "请输入研究主题";
-    return;
+  // 5. Report Ready
+  if (event.type === "final_report") {
+    reportMarkdown.value = String(event.report);
+    addLog("📄 深度研究报告已生成。");
   }
 
-  if (currentController) {
-    currentController.abort();
-    currentController = null;
+  // 6. Script Ready
+  if (event.type === "podcast_script") {
+    const payload = event as any;
+    podcastScript.value = payload.script;
+    productionStage.value = "audio";
+    addLog("🎙️ 播客剧本创作完成。");
   }
 
-  loading.value = true;
-  error.value = "";
-  isExpanded.value = true;
-  resetWorkflowState();
-
-  const controller = new AbortController();
-  currentController = controller;
-
-  const payload = {
-    topic: form.topic.trim(),
-    search_api: form.searchApi || undefined
-  };
-
-  try {
-    await runResearchStream(
-      payload,
-      (event: ResearchStreamEvent) => {
-        if (event.type === "status") {
-          const message =
-            typeof event.message === "string" && event.message.trim()
-              ? event.message
-              : "流程状态更新";
-          progressLogs.value.push(message);
-
-          const payload = event as Record<string, unknown>;
-          const task = findTask(payload.task_id);
-          if (task && message) {
-            task.notices.push(message);
-            applyNoteMetadata(task, payload);
-          }
-          return;
-        }
-
-        if (event.type === "todo_list") {
-          const tasks = Array.isArray(event.tasks)
-            ? (event.tasks as Record<string, unknown>[])
-            : [];
-
-          todoTasks.value = tasks.map((item, index) => {
-            const rawId =
-              typeof item.id === "number"
-                ? item.id
-                : typeof item.id === "string"
-                ? Number(item.id)
-                : index + 1;
-            const id = Number.isFinite(rawId) ? Number(rawId) : index + 1;
-            const noteId =
-              typeof item.note_id === "string" && item.note_id.trim()
-                ? item.note_id.trim()
-                : null;
-            const notePath =
-              typeof item.note_path === "string" && item.note_path.trim()
-                ? item.note_path.trim()
-                : null;
-
-            return {
-              id,
-              title:
-                typeof item.title === "string" && item.title.trim()
-                  ? item.title.trim()
-                  : `任务${id}`,
-              intent:
-                typeof item.intent === "string" && item.intent.trim()
-                  ? item.intent.trim()
-                  : "探索与主题相关的关键信息",
-              query:
-                typeof item.query === "string" && item.query.trim()
-                  ? item.query.trim()
-                  : form.topic.trim(),
-              status:
-                typeof item.status === "string" && item.status.trim()
-                  ? item.status.trim()
-                  : "pending",
-              summary: "",
-              sourcesSummary: "",
-              sourceItems: [],
-              notices: [],
-              noteId,
-              notePath,
-              toolCalls: []
-            } as TodoTaskView;
-          });
-
-          if (todoTasks.value.length) {
-            activeTaskId.value = todoTasks.value[0].id;
-            progressLogs.value.push("已生成任务清单");
-          } else {
-            progressLogs.value.push("未生成任务清单,使用默认任务继续");
-          }
-          return;
-        }
-
-        if (event.type === "task_status") {
-          const payload = event as Record<string, unknown>;
-          const task = findTask(event.task_id);
-          if (!task) {
-            return;
-          }
-
-          upsertTaskMetadata(task, payload);
-          applyNoteMetadata(task, payload);
-          const status =
-            typeof event.status === "string" && event.status.trim()
-              ? event.status.trim()
-              : task.status;
-          task.status = status;
-
-          if (status === "in_progress") {
-            task.summary = "";
-            task.sourcesSummary = "";
-            task.sourceItems = [];
-            task.notices = [];
-            activeTaskId.value = task.id;
-            progressLogs.value.push(`开始执行任务:${task.title}`);
-          } else if (status === "completed") {
-            if (typeof event.summary === "string" && event.summary.trim()) {
-              task.summary = event.summary.trim();
-            }
-            if (
-              typeof event.sources_summary === "string" &&
-              event.sources_summary.trim()
-            ) {
-              task.sourcesSummary = event.sources_summary.trim();
-              task.sourceItems = parseSources(task.sourcesSummary);
-            }
-            progressLogs.value.push(`完成任务:${task.title}`);
-            if (activeTaskId.value === task.id) {
-              pulse(summaryHighlight);
-              pulse(sourcesHighlight);
-            }
-          } else if (status === "skipped") {
-            progressLogs.value.push(`任务跳过:${task.title}`);
-          }
-          return;
-        }
-
-        if (event.type === "sources") {
-          const payload = event as Record<string, unknown>;
-          const task = findTask(event.task_id);
-          if (!task) {
-            return;
-          }
-
-          const textCandidates = [
-            payload.latest_sources,
-            payload.sources_summary,
-            payload.raw_context
-          ];
-          const latestText = textCandidates
-            .map((value) => (typeof value === "string" ? value.trim() : ""))
-            .find((value) => value);
-
-          if (latestText) {
-            task.sourcesSummary = latestText;
-            task.sourceItems = parseSources(latestText);
-            if (activeTaskId.value === task.id) {
-              pulse(sourcesHighlight);
-            }
-            progressLogs.value.push(`已更新任务来源:${task.title}`);
-          }
-
-          if (typeof payload.backend === "string") {
-            progressLogs.value.push(
-              `当前使用搜索后端:${payload.backend}`
-            );
-          }
-
-          applyNoteMetadata(task, payload);
-
-          return;
-        }
-
-        if (event.type === "task_summary_chunk") {
-          const payload = event as Record<string, unknown>;
-          const task = findTask(event.task_id);
-          if (!task) {
-            return;
-          }
-          const chunk =
-            typeof event.content === "string" ? event.content : "";
-          task.summary += chunk;
-          applyNoteMetadata(task, payload);
-          if (activeTaskId.value === task.id) {
-            pulse(summaryHighlight);
-          }
-          return;
-        }
-
-        if (event.type === "tool_call") {
-          const payload = event as Record<string, unknown>;
-          const eventId =
-            typeof payload.event_id === "number"
-              ? payload.event_id
-              : Date.now();
-          const agent =
-            typeof payload.agent === "string" && payload.agent.trim()
-              ? payload.agent.trim()
-              : "Agent";
-          const tool =
-            typeof payload.tool === "string" && payload.tool.trim()
-              ? payload.tool.trim()
-              : "tool";
-          const parameters = ensureRecord(payload.parameters);
-          const result =
-            typeof payload.result === "string" ? payload.result : "";
-          const noteId = extractOptionalString(payload.note_id);
-          const notePath = extractOptionalString(payload.note_path);
-
-          const task = findTask(payload.task_id);
-          if (task) {
-            task.toolCalls.push({
-              eventId,
-              agent,
-              tool,
-              parameters,
-              result,
-              noteId,
-              notePath,
-              timestamp: Date.now()
-            });
-            if (noteId) {
-              task.noteId = noteId;
-            }
-            if (notePath) {
-              task.notePath = notePath;
-            }
-            const logSummary = noteId
-              ? `${agent} 调用了 ${tool}(任务 ${task.id},笔记 ${noteId})`
-              : `${agent} 调用了 ${tool}(任务 ${task.id})`;
-            progressLogs.value.push(logSummary);
-            if (activeTaskId.value === task.id) {
-              pulse(toolHighlight);
-            }
-          } else {
-            progressLogs.value.push(`${agent} 调用了 ${tool}`);
-          }
-          return;
-        }
-
-        if (event.type === "final_report") {
-          const report =
-            typeof event.report === "string" && event.report.trim()
-              ? event.report.trim()
-              : "";
-          reportMarkdown.value = report || "报告生成失败,未获得有效内容";
-          pulse(reportHighlight);
-          progressLogs.value.push("最终报告已生成");
-          return;
-        }
-
-        if (event.type === "error") {
-          const detail =
-            typeof event.detail === "string" && event.detail.trim()
-              ? event.detail
-              : "研究过程中发生错误";
-          error.value = detail;
-          progressLogs.value.push("研究失败,已停止流程");
-        }
-      },
-      { signal: controller.signal }
-    );
-
-    if (!reportMarkdown.value) {
-      reportMarkdown.value = "暂无生成的报告";
-    }
-  } catch (err) {
-    if (err instanceof DOMException && err.name === "AbortError") {
-      progressLogs.value.push("已取消当前研究任务");
-    } else {
-      error.value = err instanceof Error ? err.message : "请求失败";
-    }
-  } finally {
-    loading.value = false;
-    if (currentController === controller) {
-      currentController = null;
-    }
+  // 7. Audio Generation (Detail)
+  if (event.type === "audio_generated") {
+    const files = (event as any).files || [];
+    addLog(`🎵 已生成 ${files.length} 个音频片段。`);
   }
-};
 
-const cancelResearch = () => {
-  if (!loading.value || !currentController) {
-    return;
+  // 8. 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("🎉 播客制作完成!即将开始播放...");
+      productionStage.value = "done";
+      
+      // 延迟跳转到播放页
+      setTimeout(() => {
+        currentView.value = "player";
+      }, 1500);
+    }
   }
-  progressLogs.value.push("正在尝试取消当前研究任务…");
-  currentController.abort();
-};
+}
 
-const goBack = () => {
-  if (loading.value) {
-    return; // 研究进行中不允许返回
+function cancelProduction() {
+  if (abortController) {
+    abortController.abort();
+    abortController = null;
   }
-  isExpanded.value = false;
-};
+  currentView.value = "setup";
+}
 
-const startNewResearch = () => {
-  if (loading.value) {
-    cancelResearch();
-  }
-  resetWorkflowState();
-  isExpanded.value = false;
+function resetApp() {
+  currentView.value = "setup";
   form.topic = "";
-  form.searchApi = "";
-};
-
-onBeforeUnmount(() => {
-  if (currentController) {
-    currentController.abort();
-    currentController = null;
-  }
-});
-</script>
-
-
-<style scoped>
-.app-shell {
-  position: relative;
-  min-height: 100vh;
-  padding: 72px 24px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  background: radial-gradient(circle at 20% 20%, #f8fafc, #dbeafe 60%);
-  color: #1f2937;
-  overflow: hidden;
-  box-sizing: border-box;
-  transition: padding 0.4s ease;
+  isPlaying.value = false;
 }
 
-.app-shell.expanded {
-  padding: 0;
-  align-items: stretch;
+// Audio Controls
+function togglePlay() {
+  if (!audioPlayer.value) return;
+  if (isPlaying.value) {
+    audioPlayer.value.pause();
+  } else {
+    audioPlayer.value.play();
+  }
 }
 
-.aurora {
-  position: absolute;
-  inset: 0;
-  pointer-events: none;
-  opacity: 0.55;
+function onTimeUpdate() {
+  if (audioPlayer.value) {
+    currentTime.value = audioPlayer.value.currentTime;
+    duration.value = audioPlayer.value.duration || 0;
+  }
 }
 
-.aurora span {
-  position: absolute;
-  width: 45vw;
-  height: 45vw;
-  max-width: 520px;
-  max-height: 520px;
-  background: radial-gradient(circle, rgba(148, 197, 255, 0.35), transparent 60%);
-  filter: blur(90px);
-  animation: float 26s infinite linear;
+function seekAudio(e: MouseEvent) {
+  if (!audioPlayer.value || !duration.value) return;
+  const target = e.currentTarget as HTMLElement;
+  const rect = target.getBoundingClientRect();
+  const x = e.clientX - rect.left;
+  const percent = x / rect.width;
+  audioPlayer.value.currentTime = percent * duration.value;
 }
 
-.aurora span:nth-child(1) {
-  top: -20%;
-  left: -18%;
-  animation-delay: 0s;
+function formatTime(seconds: number) {
+  if (!seconds) return "0:00";
+  const m = Math.floor(seconds / 60);
+  const s = Math.floor(seconds % 60);
+  return `${m}:${s.toString().padStart(2, "0")}`;
 }
+</script>
 
-.aurora span:nth-child(2) {
-  bottom: -25%;
-  right: -20%;
-  background: radial-gradient(circle, rgba(166, 139, 255, 0.28), transparent 60%);
-  animation-delay: -9s;
+<style scoped>
+/* --- Global & Layout --- */
+::-webkit-scrollbar {
+  width: 8px;
+  height: 8px;
 }
-
-.aurora span:nth-child(3) {
-  top: 35%;
-  left: 45%;
-  background: radial-gradient(circle, rgba(164, 219, 216, 0.26), transparent 60%);
-  animation-delay: -16s;
+::-webkit-scrollbar-track {
+  background: rgba(255, 255, 255, 0.05);
 }
-
-.layout {
-  position: relative;
-  width: 100%;
-  display: flex;
-  gap: 24px;
-  z-index: 1;
-  transition: all 0.4s ease;
+::-webkit-scrollbar-thumb {
+  background: rgba(255, 255, 255, 0.2);
+  border-radius: 4px;
 }
-
-.layout-centered {
-  max-width: 600px;
-  justify-content: center;
-  align-items: center;
+::-webkit-scrollbar-thumb:hover {
+  background: rgba(255, 255, 255, 0.3);
 }
 
-.layout-fullscreen {
+.deepcast-container {
+  width: 100vw;
   height: 100vh;
-  max-width: 100%;
-  gap: 0;
-  align-items: stretch;
-}
-
-.panel {
-  position: relative;
-  flex: 1 1 360px;
-  padding: 24px;
-  border-radius: 20px;
-  background: rgba(255, 255, 255, 0.95);
-  border: 1px solid rgba(148, 163, 184, 0.18);
-  box-shadow: 0 24px 48px rgba(15, 23, 42, 0.12);
-  backdrop-filter: blur(8px);
   overflow: hidden;
+  font-family: 'Inter', system-ui, -apple-system, sans-serif;
+  color: #fff;
+  background: #0f172a;
+  position: relative;
 }
 
-.panel-form {
-  max-width: 420px;
-}
-
-.panel-centered {
-  width: 100%;
-  max-width: 600px;
-  padding: 40px;
-  box-shadow: 0 32px 64px rgba(15, 23, 42, 0.15);
-  transform: scale(1);
-  transition: transform 0.3s ease, box-shadow 0.3s ease;
-}
-
-.panel-centered:hover {
-  transform: scale(1.02);
-  box-shadow: 0 40px 80px rgba(15, 23, 42, 0.2);
-}
-
-.panel-result {
-  min-width: 360px;
-  flex: 2 1 420px;
-}
-
-.panel::before {
-  content: "";
+.background-gradient {
   position: absolute;
-  inset: 0;
-  background: linear-gradient(135deg, rgba(59, 130, 246, 0.12), rgba(125, 86, 255, 0.1));
-  opacity: 0;
-  transition: opacity 0.35s ease;
+  top: -50%;
+  left: -50%;
+  width: 200%;
+  height: 200%;
+  background: radial-gradient(circle at center, #1e293b 0%, #0f172a 60%, #000 100%);
   z-index: 0;
+  animation: pulseBg 20s infinite alternate;
 }
 
-.panel:hover::before {
-  opacity: 1;
+@keyframes pulseBg {
+  0% { transform: scale(1); }
+  100% { transform: scale(1.1); }
 }
 
-.panel > * {
+section {
   position: relative;
   z-index: 1;
-}
-
-.panel-form h1 {
-  margin: 0;
-  font-size: 26px;
-  letter-spacing: 0.01em;
-}
-
-.panel-form p {
-  margin: 4px 0 0;
-  color: #64748b;
-  font-size: 13px;
-}
-
-.panel-head {
-  display: flex;
-  align-items: center;
-  gap: 16px;
-  margin-bottom: 24px;
-}
-
-.logo {
-  width: 52px;
-  height: 52px;
-  display: grid;
-  place-items: center;
-  border-radius: 16px;
-  background: linear-gradient(135deg, #2563eb, #7c3aed);
-  box-shadow: 0 12px 28px rgba(59, 130, 246, 0.4);
-}
-
-.logo svg {
-  width: 28px;
-  height: 28px;
-  fill: #f8fafc;
-}
-
-.form {
-  display: flex;
-  flex-direction: column;
-  gap: 18px;
-}
-
-.field {
-  display: flex;
-  flex-direction: column;
-  gap: 10px;
-}
-
-.field span {
-  font-weight: 600;
-  color: #475569;
-}
-
-textarea,
-input,
-select {
-  padding: 14px 16px;
-  border-radius: 16px;
-  border: 1px solid rgba(148, 163, 184, 0.35);
-  background: rgba(255, 255, 255, 0.92);
-  color: #1f2937;
-  font-size: 14px;
-  transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
-}
-
-textarea:focus,
-input:focus,
-select:focus {
-  outline: none;
-  border-color: rgba(37, 99, 235, 0.65);
-  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
-  background: #ffffff;
-}
-
-.options {
-  display: flex;
-  gap: 16px;
-  flex-wrap: wrap;
-}
-
-.option {
-  flex: 1;
-  min-width: 140px;
-}
-
-.form-actions {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  flex-wrap: wrap;
-}
-
-.submit {
-  align-self: flex-start;
-  padding: 12px 24px;
-  border-radius: 16px;
-  border: none;
-  background: linear-gradient(135deg, #2563eb, #7c3aed);
-  color: #ffffff;
-  font-size: 15px;
-  font-weight: 600;
-  cursor: pointer;
-  transition: transform 0.2s, box-shadow 0.2s, opacity 0.2s;
-  display: inline-flex;
-  align-items: center;
-  gap: 10px;
-  position: relative;
-}
-
-.submit-label {
-  display: inline-flex;
-  align-items: center;
-  gap: 10px;
-}
-
-.submit .spinner {
-  width: 18px;
-  height: 18px;
-  fill: none;
-  stroke: rgba(255, 255, 255, 0.85);
-  stroke-linecap: round;
-  animation: spin 1s linear infinite;
-}
-
-.submit:disabled {
-  opacity: 0.7;
-  cursor: not-allowed;
-}
-
-.submit:not(:disabled):hover {
-  transform: translateY(-2px);
-  box-shadow: 0 12px 28px rgba(37, 99, 235, 0.28);
-}
-
-.secondary-btn {
-  padding: 10px 18px;
-  border-radius: 14px;
-  background: rgba(148, 163, 184, 0.12);
-  border: 1px solid rgba(148, 163, 184, 0.28);
-  color: #1f2937;
-  font-size: 14px;
-  font-weight: 500;
-  cursor: pointer;
-  transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
-}
-
-.secondary-btn:hover {
-  background: rgba(148, 163, 184, 0.2);
-  border-color: rgba(148, 163, 184, 0.35);
-  color: #0f172a;
-}
-
-.error-chip {
-  margin-top: 16px;
-  display: inline-flex;
-  align-items: center;
-  gap: 8px;
-  padding: 10px 14px;
-  background: rgba(248, 113, 113, 0.12);
-  border: 1px solid rgba(248, 113, 113, 0.35);
-  border-radius: 14px;
-  color: #b91c1c;
-  font-size: 14px;
-}
-
-.error-chip svg {
-  width: 18px;
-  height: 18px;
-  fill: currentColor;
-}
-
-.panel-result {
+  height: 100%;
+  width: 100%;
   display: flex;
   flex-direction: column;
-  gap: 18px;
 }
 
-.status-bar {
-  display: flex;
+/* --- Setup View --- */
+.view-setup {
   align-items: center;
-  justify-content: space-between;
-  gap: 12px;
-  flex-wrap: wrap;
-}
-
-.status-main {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  flex-wrap: wrap;
-}
-
-.status-controls {
-  display: flex;
-  gap: 8px;
-}
-
-.status-chip {
-  display: inline-flex;
-  align-items: center;
-  gap: 8px;
-  background: rgba(191, 219, 254, 0.28);
-  padding: 8px 14px;
-  border-radius: 999px;
-  font-size: 13px;
-  color: #1f2937;
-  border: 1px solid rgba(59, 130, 246, 0.35);
-  transition: background 0.3s ease, color 0.3s ease;
-}
-
-.status-chip.active {
-  background: rgba(129, 140, 248, 0.2);
-  border-color: rgba(129, 140, 248, 0.4);
-  color: #1e293b;
-}
-
-.status-chip .dot {
-  width: 8px;
-  height: 8px;
-  border-radius: 999px;
-  background: #2563eb;
-  box-shadow: 0 0 12px rgba(37, 99, 235, 0.45);
-  animation: pulse 1.8s ease-in-out infinite;
-}
-
-.status-meta {
-  color: #64748b;
-  font-size: 13px;
-}
-
-.timeline-wrapper {
-  margin-top: 12px;
-  max-height: 220px;
-  overflow-y: auto;
-  padding-right: 8px;
-  scrollbar-width: thin;
-  scrollbar-color: rgba(129, 140, 248, 0.45) rgba(226, 232, 240, 0.6);
-}
-
-.timeline-wrapper::-webkit-scrollbar {
-  width: 6px;
-}
-
-.timeline-wrapper::-webkit-scrollbar-track {
-  background: rgba(226, 232, 240, 0.6);
-  border-radius: 999px;
+  justify-content: center;
+  padding: 2rem;
 }
 
-.timeline-wrapper::-webkit-scrollbar-thumb {
-  background: linear-gradient(180deg, rgba(129, 140, 248, 0.8), rgba(59, 130, 246, 0.7));
-  border-radius: 999px;
+.brand-header {
+  text-align: center;
+  margin-bottom: 3rem;
 }
 
-.timeline-wrapper::-webkit-scrollbar-thumb:hover {
-  background: linear-gradient(180deg, rgba(99, 102, 241, 0.9), rgba(37, 99, 235, 0.8));
+.logo-icon {
+  font-size: 4rem;
+  margin-bottom: 1rem;
 }
 
-.timeline {
-  list-style: none;
-  padding: 0;
+h1 {
+  font-size: 3rem;
+  font-weight: 800;
+  letter-spacing: -1px;
+  background: linear-gradient(135deg, #60a5fa, #c084fc);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
   margin: 0;
-  display: flex;
-  flex-direction: column;
-  gap: 14px;
-  position: relative;
-  padding-left: 12px;
-}
-
-.timeline::before {
-  content: "";
-  position: absolute;
-  top: 8px;
-  bottom: 8px;
-  left: 0;
-  width: 2px;
-  background: linear-gradient(180deg, rgba(59, 130, 246, 0.35), rgba(129, 140, 248, 0.15));
-}
-
-.timeline li {
-  position: relative;
-  padding-left: 24px;
-  color: #1e293b;
-  font-size: 14px;
-  line-height: 1.5;
 }
 
-.timeline-node {
-  position: absolute;
-  left: -12px;
-  top: 6px;
-  width: 10px;
-  height: 10px;
-  border-radius: 999px;
-  background: linear-gradient(135deg, #38bdf8, #7c3aed);
-  box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.22);
-}
-
-.timeline-enter-active,
-.timeline-leave-active {
-  transition: all 0.35s ease, opacity 0.35s ease;
+.tagline {
+  color: #94a3b8;
+  font-size: 1.1rem;
+  margin-top: 0.5rem;
 }
 
-.timeline-enter-from,
-.timeline-leave-to {
-  opacity: 0;
-  transform: translateY(-6px);
-}
-
-.tasks-section {
-  display: grid;
-  grid-template-columns: 280px 1fr;
-  gap: 20px;
-  align-items: start;
-}
-
-@media (max-width: 960px) {
-  .tasks-section {
-    grid-template-columns: 1fr;
-  }
-}
-
-.tasks-list {
-  background: rgba(255, 255, 255, 0.92);
-  border: 1px solid rgba(148, 163, 184, 0.26);
-  border-radius: 18px;
-  padding: 18px;
-  display: flex;
-  flex-direction: column;
-  gap: 16px;
-  box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.4);
+.setup-form {
+  width: 100%;
+  max-width: 500px;
+  background: rgba(30, 41, 59, 0.5);
+  backdrop-filter: blur(10px);
+  padding: 2rem;
+  border-radius: 16px;
+  border: 1px solid rgba(255, 255, 255, 0.1);
+  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
 }
 
-.tasks-list h3 {
-  margin: 0;
-  font-size: 16px;
+.input-group label, .setting-item label {
+  display: block;
+  font-size: 0.875rem;
   font-weight: 600;
-  color: #1f2937;
+  color: #cbd5e1;
+  margin-bottom: 0.5rem;
 }
 
-.tasks-list ul {
-  list-style: none;
-  margin: 0;
-  padding: 0;
-  display: flex;
-  flex-direction: column;
-  gap: 12px;
+.input-group textarea {
+  width: 100%;
+  background: rgba(15, 23, 42, 0.6);
+  border: 1px solid rgba(255, 255, 255, 0.1);
+  color: #fff;
+  padding: 1rem;
+  border-radius: 8px;
+  resize: none;
+  font-size: 1rem;
+  transition: border-color 0.2s;
 }
 
-.task-item {
-  border-radius: 14px;
-  border: 1px solid transparent;
-  transition: border-color 0.2s ease, background 0.2s ease;
+.input-group textarea:focus {
+  outline: none;
+  border-color: #60a5fa;
 }
 
-.task-item.completed {
-  border-color: rgba(56, 189, 248, 0.35);
-  background: rgba(191, 219, 254, 0.28);
+.settings-row {
+  margin: 1.5rem 0;
 }
 
-.task-item.active {
-  border-color: rgba(129, 140, 248, 0.5);
-  background: rgba(224, 231, 255, 0.5);
+.select-wrapper {
+  position: relative;
 }
 
-.task-button {
+select {
   width: 100%;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  gap: 12px;
-  padding: 12px 14px 6px;
-  background: transparent;
-  border: none;
-  color: inherit;
+  appearance: none;
+  background: rgba(15, 23, 42, 0.6);
+  border: 1px solid rgba(255, 255, 255, 0.1);
+  color: #fff;
+  padding: 0.75rem 1rem;
+  border-radius: 8px;
+  font-size: 0.95rem;
   cursor: pointer;
-  text-align: left;
 }
 
-.task-title {
-  font-weight: 600;
-  font-size: 14px;
-  color: #1e293b;
-}
-
-.task-status {
-  display: inline-flex;
-  align-items: center;
-  justify-content: center;
-  padding: 4px 10px;
-  border-radius: 999px;
-  font-size: 12px;
-  font-weight: 500;
-  color: #1f2937;
-  background: rgba(148, 163, 184, 0.2);
-}
-
-.task-status.pending {
-  background: rgba(148, 163, 184, 0.18);
-  color: #475569;
-}
-
-.task-status.in_progress {
-  background: rgba(129, 140, 248, 0.24);
-  color: #312e81;
-}
-
-.task-status.completed {
-  background: rgba(34, 197, 94, 0.2);
-  color: #15803d;
-}
-
-.task-status.skipped {
-  background: rgba(248, 113, 113, 0.18);
-  color: #b91c1c;
-}
-
-.task-intent {
-  margin: 0;
-  padding: 0 14px 12px 14px;
-  font-size: 13px;
+.select-arrow {
+  position: absolute;
+  right: 1rem;
+  top: 50%;
+  transform: translateY(-50%);
   color: #64748b;
+  pointer-events: none;
+  font-size: 0.8rem;
 }
 
-.task-detail {
-  background: rgba(255, 255, 255, 0.94);
-  border: 1px solid rgba(148, 163, 184, 0.26);
-  border-radius: 18px;
-  padding: 22px;
-  display: flex;
-  flex-direction: column;
-  gap: 18px;
-  box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.5);
-}
-
-.task-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: flex-start;
-  flex-wrap: wrap;
-  gap: 12px;
-}
-
-.task-chip-group {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  flex-wrap: wrap;
-}
-
-.task-header h3 {
-  margin: 0;
-  font-size: 18px;
-  font-weight: 600;
-  color: #1f2937;
-}
-
-.task-header .muted {
-  margin: 6px 0 0;
-}
-
-.task-label {
-  padding: 6px 12px;
-  border-radius: 999px;
-  background: rgba(191, 219, 254, 0.32);
-  border: 1px solid rgba(59, 130, 246, 0.35);
-  font-size: 12px;
-  color: #1e3a8a;
-}
-
-.task-label.note-chip {
-  background: rgba(34, 197, 94, 0.2);
-  border-color: rgba(34, 197, 94, 0.35);
-  color: #15803d;
-}
-
-.task-label.path-chip {
-  display: inline-flex;
-  align-items: center;
-  gap: 6px;
-  max-width: 360px;
-  background: rgba(56, 189, 248, 0.2);
-  border-color: rgba(56, 189, 248, 0.35);
-  color: #0369a1;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.path-label {
-  font-weight: 500;
-}
-
-.path-text {
-  max-width: 220px;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.chip-action {
+.cta-button {
+  width: 100%;
+  padding: 1rem;
+  background: linear-gradient(135deg, #3b82f6, #8b5cf6);
   border: none;
-  background: rgba(56, 189, 248, 0.2);
-  color: #0369a1;
-  padding: 3px 8px;
-  border-radius: 10px;
-  font-size: 11px;
+  border-radius: 8px;
+  color: #fff;
+  font-size: 1.1rem;
+  font-weight: 600;
   cursor: pointer;
-  transition: background 0.2s ease, color 0.2s ease;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 0.5rem;
+  transition: transform 0.2s, opacity 0.2s;
 }
 
-.chip-action:hover {
-  background: rgba(14, 165, 233, 0.28);
-  color: #0f172a;
+.cta-button:hover:not(:disabled) {
+  transform: translateY(-2px);
+  opacity: 0.9;
 }
 
-.task-notices {
-  background: rgba(191, 219, 254, 0.28);
-  border: 1px solid rgba(96, 165, 250, 0.35);
-  border-radius: 16px;
-  padding: 14px 18px;
-  color: #1f2937;
+.cta-button:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
 }
 
-.task-notices h4 {
-  margin: 0 0 8px;
-  font-size: 14px;
-  font-weight: 600;
+/* --- Production View --- */
+.view-production {
+  overflow-y: auto;
+  width: 100%;
+  display: block;
 }
 
-.task-notices ul {
-  list-style: disc;
-  margin: 0 0 0 18px;
-  padding: 0;
+.production-content {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 4rem 2rem;
   display: flex;
   flex-direction: column;
-  gap: 6px;
+  align-items: center;
 }
 
-.task-notices li {
-  font-size: 13px;
+.todo-list-container {
+  width: 100%;
+  margin-top: 2rem;
+  border-top: 1px solid rgba(255, 255, 255, 0.1);
+  padding-top: 2rem;
 }
 
-.report-block {
-  background: rgba(255, 255, 255, 0.94);
-  border: 1px solid rgba(148, 163, 184, 0.26);
-  border-radius: 18px;
-  padding: 22px;
+.todo-list-container h3 {
+  margin-bottom: 1.5rem;
+  color: #e2e8f0;
+  font-size: 1.2rem;
   display: flex;
-  flex-direction: column;
-  gap: 12px;
+  align-items: center;
+  gap: 0.5rem;
 }
 
-.report-block h3 {
-  margin: 0;
-  font-size: 18px;
-  font-weight: 600;
-  color: #1f2937;
+.todo-items {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
 }
 
-.block-pre {
-  font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular,
-    Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
-  font-size: 13px;
-  line-height: 1.7;
-  white-space: pre-wrap;
-  word-break: break-word;
-  color: #1f2937;
-  background: rgba(248, 250, 252, 0.9);
-  padding: 16px;
-  border-radius: 14px;
-  border: 1px solid rgba(148, 163, 184, 0.35);
-  overflow: auto;
-  max-height: 420px;
-  scrollbar-width: thin;
-  scrollbar-color: rgba(129, 140, 248, 0.6) rgba(226, 232, 240, 0.7);
+.todo-item {
+  background: rgba(30, 41, 59, 0.5);
+  border: 1px solid rgba(255, 255, 255, 0.1);
+  border-radius: 12px;
+  padding: 1.5rem;
+  display: flex;
+  gap: 1rem;
+  transition: all 0.3s ease;
 }
 
-.block-pre::-webkit-scrollbar {
-  width: 6px;
+.todo-item:hover {
+  background: rgba(30, 41, 59, 0.8);
+  border-color: rgba(96, 165, 250, 0.3);
 }
 
-.block-pre::-webkit-scrollbar-track {
-  background: rgba(226, 232, 240, 0.7);
-  border-radius: 999px;
+.todo-item.in_progress {
+  border-color: #60a5fa;
+  box-shadow: 0 0 20px rgba(96, 165, 250, 0.1);
 }
 
-.block-pre::-webkit-scrollbar-thumb {
-  background: linear-gradient(180deg, rgba(99, 102, 241, 0.75), rgba(59, 130, 246, 0.65));
-  border-radius: 999px;
+.todo-item.completed {
+  border-color: rgba(16, 185, 129, 0.3);
 }
 
-.block-pre::-webkit-scrollbar-thumb:hover {
-  background: linear-gradient(180deg, rgba(79, 70, 229, 0.8), rgba(37, 99, 235, 0.75));
+.todo-item.failed {
+  border-color: rgba(239, 68, 68, 0.3);
 }
 
-.summary-block .block-pre,
-.sources-block .block-pre {
-  max-height: 360px;
+.task-status-icon {
+  font-size: 1.5rem;
+  padding-top: 0.2rem;
+  min-width: 2rem;
+  text-align: center;
 }
 
+.task-content {
+  flex: 1;
+  min-width: 0;
+}
 
-.tools-block {
-  position: relative;
-  margin-top: 16px;
-  padding: 20px;
-  border-radius: 18px;
-  background: rgba(255, 255, 255, 0.94);
-  border: 1px solid rgba(148, 163, 184, 0.18);
-  box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.4);
+.task-header {
   display: flex;
-  flex-direction: column;
-  gap: 12px;
+  justify-content: space-between;
+  align-items: baseline;
+  margin-bottom: 0.5rem;
+  flex-wrap: wrap;
+  gap: 0.5rem;
 }
 
-.tools-block h3 {
-  margin: 0;
-  font-size: 16px;
+.task-title {
   font-weight: 600;
-  color: #1f2937;
-  letter-spacing: 0.02em;
-}
-
-.tool-list {
-  list-style: none;
-  margin: 0;
-  padding: 0;
-  display: flex;
-  flex-direction: column;
-  gap: 12px;
+  color: #f1f5f9;
+  font-size: 1.05rem;
 }
 
-.tool-entry {
-  background: rgba(248, 250, 252, 0.95);
-  border: 1px solid rgba(148, 163, 184, 0.24);
-  border-radius: 14px;
-  padding: 14px;
-  display: flex;
-  flex-direction: column;
-  gap: 10px;
+.task-intent {
+  font-size: 0.8rem;
+  color: #94a3b8;
+  background: rgba(0, 0, 0, 0.2);
+  padding: 2px 8px;
+  border-radius: 4px;
 }
 
-.tool-entry-header {
+.production-header {
+  width: 100%;
   display: flex;
-  flex-wrap: wrap;
-  gap: 8px;
-  align-items: center;
   justify-content: space-between;
+  align-items: center;
+  margin-bottom: 3rem;
 }
 
-.tool-entry-title {
-  font-weight: 600;
-  color: #1f2937;
+.production-header h2 {
+  font-size: 1.5rem;
+  margin: 0;
 }
 
-.tool-entry-note {
-  font-size: 12px;
-  color: #0f766e;
+.cancel-btn {
+  background: transparent;
+  border: 1px solid rgba(239, 68, 68, 0.5);
+  color: #fca5a5;
+  padding: 0.5rem 1rem;
+  border-radius: 6px;
+  cursor: pointer;
 }
 
-.tool-entry-path {
-  margin: 0;
-  font-size: 12px;
+.stage-monitor {
   display: flex;
   align-items: center;
-  gap: 6px;
-  color: #2563eb;
+  width: 100%;
+  margin-bottom: 3rem;
 }
 
-.tool-subtitle {
-  margin: 0;
-  font-size: 13px;
-  color: #475569;
-  font-weight: 500;
+.stage-step {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 0.5rem;
+  opacity: 0.4;
+  transition: opacity 0.3s;
 }
 
-.tool-pre {
-  font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular,
-    Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
-  font-size: 12px;
-  line-height: 1.6;
-  white-space: pre-wrap;
-  word-break: break-word;
-  color: #1f2937;
-  background: rgba(248, 250, 252, 0.9);
-  padding: 12px;
-  border-radius: 12px;
-  border: 1px solid rgba(148, 163, 184, 0.28);
-  overflow: auto;
-  max-height: 260px;
-  scrollbar-width: thin;
-  scrollbar-color: rgba(129, 140, 248, 0.6) rgba(226, 232, 240, 0.7);
+.stage-step.active, .stage-step.completed {
+  opacity: 1;
 }
 
-.tool-pre::-webkit-scrollbar {
-  width: 6px;
+.step-icon {
+  width: 48px;
+  height: 48px;
+  background: #1e293b;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 1.5rem;
+  border: 2px solid transparent;
 }
 
-.tool-pre::-webkit-scrollbar-track {
-  background: rgba(226, 232, 240, 0.7);
+.stage-step.active .step-icon {
+  border-color: #60a5fa;
+  box-shadow: 0 0 15px rgba(96, 165, 250, 0.3);
+  animation: pulseIcon 1.5s infinite;
 }
 
-.tool-pre::-webkit-scrollbar-thumb {
-  background: rgba(99, 102, 241, 0.7);
-  border-radius: 10px;
+.stage-step.completed .step-icon {
+  background: #10b981;
+  color: #fff;
 }
 
-.link-btn {
-  background: none;
-  border: none;
-  color: #0369a1;
-  cursor: pointer;
-  padding: 0 4px;
-  font-size: 12px;
-  border-radius: 8px;
-  transition: color 0.2s ease, background 0.2s ease;
+@keyframes pulseIcon {
+  0% { transform: scale(1); }
+  50% { transform: scale(1.05); }
+  100% { transform: scale(1); }
 }
 
-.link-btn:hover {
-  color: #0ea5e9;
-  background: rgba(14, 165, 233, 0.16);
+.stage-line {
+  flex: 1;
+  height: 2px;
+  background: #334155;
+  margin: 0 1rem;
+  position: relative;
+  top: -14px;
 }
 
-
-.sources-block,
-.summary-block {
-  position: relative;
-  margin-top: 16px;
-  padding: 18px;
-  border-radius: 18px;
-  background: rgba(255, 255, 255, 0.94);
-  border: 1px solid rgba(148, 163, 184, 0.18);
-  box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.4);
+.terminal-log {
+  width: 100%;
+  background: #000;
+  border-radius: 8px;
+  padding: 1rem;
+  font-family: 'Fira Code', monospace;
+  font-size: 0.9rem;
+  height: 150px;
+  margin-bottom: 2rem;
+  border: 1px solid #333;
 }
 
-.sources-history {
-  margin-top: 16px;
+.log-content {
+  height: 100%;
+  overflow-y: auto;
   display: flex;
   flex-direction: column;
-  gap: 10px;
+  gap: 0.5rem;
 }
 
-.sources-history h4 {
-  margin: 0;
-  color: #1f2937;
-  font-size: 14px;
-  letter-spacing: 0.01em;
+.log-entry {
+  display: flex;
+  gap: 1rem;
 }
 
-.history-list {
-  display: flex;
-  flex-direction: column;
-  gap: 10px;
+.log-time {
+  color: #64748b;
 }
 
-.history-list details {
-  background: rgba(248, 250, 252, 0.95);
-  border: 1px solid rgba(148, 163, 184, 0.24);
-  border-radius: 14px;
-  padding: 12px 16px;
-  color: #1f2937;
-  transition: border-color 0.2s ease, background 0.2s ease;
+.log-msg {
+  color: #e2e8f0;
 }
 
-.history-list details[open] {
-  background: rgba(224, 231, 255, 0.55);
-  border-color: rgba(129, 140, 248, 0.4);
+.research-preview {
+  width: 100%;
+  background: rgba(30, 41, 59, 0.5);
+  border-radius: 8px;
+  padding: 1rem;
+  border-left: 4px solid #60a5fa;
 }
 
-.history-list summary {
-  cursor: pointer;
-  font-weight: 600;
-  outline: none;
-  list-style: none;
+.preview-header {
   display: flex;
+  gap: 0.5rem;
   align-items: center;
-  justify-content: space-between;
+  margin-bottom: 0.5rem;
 }
 
-.history-list summary::-webkit-details-marker {
-  display: none;
+.badge {
+  background: #2563eb;
+  font-size: 0.7rem;
+  padding: 2px 6px;
+  border-radius: 4px;
+  text-transform: uppercase;
 }
 
-.history-list summary::after {
-  content: "▾";
-  margin-left: 6px;
-  font-size: 12px;
-  opacity: 0.7;
-  transition: transform 0.2s ease;
+.task-title {
+  font-weight: 600;
 }
 
-.history-list details[open] summary::after {
-  transform: rotate(180deg);
+.preview-body {
+  color: #94a3b8;
+  font-size: 0.9rem;
+  line-height: 1.5;
 }
 
-.block-highlight {
-  animation: glow 1.2s ease;
+/* --- Player View --- */
+.view-player {
+  padding: 0;
+  overflow: hidden; /* Player view handles internal scrolling */
 }
 
-.sources-block h3,
-.summary-block h3 {
-  margin: 0 0 14px;
-  color: #1f2937;
-  letter-spacing: 0.02em;
+.player-layout {
+  display: flex;
+  height: 100%;
+  width: 100%;
 }
 
-.sources-list {
-  list-style: none;
-  margin: 0;
-  padding: 0;
+.player-sidebar {
+  width: 400px;
+  background: #0f172a;
+  border-right: 1px solid #1e293b;
+  padding: 2rem;
   display: flex;
   flex-direction: column;
-  gap: 10px;
+  align-items: center;
+  z-index: 2;
 }
 
-.source-item {
-  position: relative;
-  display: inline-flex;
-  flex-direction: column;
-  gap: 6px;
+.back-home-btn {
+  align-self: flex-start;
+  background: none;
+  border: none;
+  color: #64748b;
+  cursor: pointer;
+  margin-bottom: 2rem;
 }
 
-.source-link {
-  color: #2563eb;
-  text-decoration: none;
-  font-weight: 600;
-  letter-spacing: 0.01em;
-  transition: color 0.2s ease;
+.back-home-btn:hover {
+  color: #fff;
 }
 
-.source-link::after {
-  content: " ↗";
-  font-size: 12px;
-  opacity: 0.6;
+.album-art {
+  width: 260px;
+  height: 260px;
+  margin-bottom: 2rem;
+  position: relative;
 }
 
-.source-link:hover {
-  color: #0f172a;
+.vinyl-record {
+  width: 100%;
+  height: 100%;
+  background: radial-gradient(circle, #222 20%, #111 21%, #111 30%, #222 31%, #222 60%, #111 61%);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 10px 30px rgba(0,0,0,0.5);
+  border: 4px solid #333;
 }
 
-.source-tooltip {
-  display: none;
-  position: absolute;
-  bottom: calc(100% + 12px);
-  left: 50%;
-  transform: translateX(-50%);
-  background: rgba(255, 255, 255, 0.98);
-  color: #1f2937;
-  padding: 14px 16px;
-  border-radius: 16px;
-  box-shadow: 0 18px 32px rgba(15, 23, 42, 0.18);
-  width: min(420px, 90vw);
-  z-index: 20;
-  border: 1px solid rgba(148, 163, 184, 0.24);
+.vinyl-record.spinning {
+  animation: spin 5s linear infinite;
 }
 
-.source-tooltip::after {
-  content: "";
-  position: absolute;
-  top: 100%;
-  left: 50%;
-  transform: translateX(-50%);
-  border-width: 10px;
-  border-style: solid;
-  border-color: rgba(255, 255, 255, 0.98) transparent transparent transparent;
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
 }
 
-.source-tooltip::before {
-  content: "";
-  position: absolute;
-  bottom: -12px;
-  left: 50%;
-  transform: translateX(-50%);
-  border-width: 12px 10px 0 10px;
-  border-style: solid;
-  border-color: rgba(255, 255, 255, 0.98) transparent transparent transparent;
-  filter: drop-shadow(0 -2px 4px rgba(15, 23, 42, 0.12));
-}
-
-.source-tooltip p {
-  margin: 0 0 8px;
-  font-size: 13px;
-  line-height: 1.6;
+.vinyl-label {
+  width: 100px;
+  height: 100px;
+  background: #60a5fa;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-weight: bold;
+  font-size: 1.5rem;
+  color: #fff;
 }
 
-.source-tooltip p:last-child {
-  margin-bottom: 0;
+.track-info {
+  text-align: center;
+  margin-bottom: 2rem;
 }
 
-.muted-text {
-  color: #64748b;
+.track-info h3 {
+  font-size: 1.25rem;
+  margin-bottom: 0.5rem;
+  background: none;
+  -webkit-text-fill-color: initial;
+  color: #fff;
 }
 
-.source-item:hover .source-tooltip,
-.source-item:focus-within .source-tooltip {
-  display: block;
+.audio-controls {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  gap: 1.5rem;
 }
 
-.hint.muted {
-  color: #64748b;
+.control-buttons {
+  display: flex;
+  justify-content: center;
+  gap: 1.5rem;
+  align-items: center;
 }
 
-@keyframes float {
-  0% {
-    transform: translate3d(0, 0, 0) rotate(0deg);
-  }
-  50% {
-    transform: translate3d(10%, 6%, 0) rotate(3deg);
-  }
-  100% {
-    transform: translate3d(0, 0, 0) rotate(0deg);
-  }
+.play-btn {
+  width: 64px;
+  height: 64px;
+  border-radius: 50%;
+  background: #fff;
+  color: #0f172a;
+  border: none;
+  font-size: 1.5rem;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: transform 0.1s;
 }
 
-@keyframes spin {
-  to {
-    transform: rotate(360deg);
-  }
+.play-btn:active {
+  transform: scale(0.95);
 }
 
-@keyframes pulse {
-  0%,
-  100% {
-    transform: scale(1);
-    opacity: 1;
-  }
-  50% {
-    transform: scale(1.3);
-    opacity: 0.5;
-  }
+.download-btn {
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  background: rgba(255,255,255,0.1);
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  text-decoration: none;
+  font-size: 1.2rem;
 }
 
-@keyframes glow {
-  0% {
-    box-shadow: 0 0 0 rgba(59, 130, 246, 0.3);
-    border-color: rgba(59, 130, 246, 0.5);
-  }
-  100% {
-    box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.12);
-    border-color: rgba(148, 163, 184, 0.2);
-  }
+.progress-bar-wrapper {
+  cursor: pointer;
+  padding: 10px 0;
 }
 
-@media (max-width: 960px) {
-  .app-shell {
-    padding: 56px 16px;
-  }
-
-  .layout {
-    flex-direction: column;
-    align-items: stretch;
-  }
-
-  .panel {
-    padding: 22px;
-  }
-
-  .panel-form,
-  .panel-result {
-    max-width: none;
-  }
-
-  .status-bar {
-    flex-direction: column;
-    align-items: flex-start;
-  }
-
-  .status-main,
-  .status-controls {
-    width: 100%;
-  }
+.progress-bar-bg {
+  width: 100%;
+  height: 4px;
+  background: #334155;
+  border-radius: 2px;
+  position: relative;
+}
 
-  .status-controls {
-    justify-content: flex-start;
-  }
+.progress-bar-fill {
+  height: 100%;
+  background: #60a5fa;
+  border-radius: 2px;
 }
 
-@media (max-width: 600px) {
-  .options {
-    flex-direction: column;
-  }
+.time-display {
+  font-size: 0.75rem;
+  color: #64748b;
+  margin-top: 0.5rem;
+  text-align: right;
+}
 
-  .status-meta {
-    font-size: 12px;
-  }
+.report-toggle {
+  margin-top: auto;
+  width: 100%;
+  text-align: center;
+}
 
-  .panel-head {
-    flex-direction: column;
-    align-items: flex-start;
-  }
+.report-toggle button {
+  background: none;
+  border: 1px solid #334155;
+  color: #94a3b8;
+  padding: 0.5rem 1rem;
+  border-radius: 20px;
+  font-size: 0.8rem;
+  cursor: pointer;
+}
 
-  .panel-form h1 {
-    font-size: 24px;
-  }
+.content-main {
+  flex: 1;
+  background: #1e293b;
+  padding: 2rem;
+  overflow-y: auto;
 }
 
-/* 侧边栏样式 */
-.sidebar {
-  width: 400px;
-  min-width: 400px;
-  height: 100vh;
-  background: rgba(255, 255, 255, 0.98);
-  border-right: 1px solid rgba(148, 163, 184, 0.2);
-  padding: 32px 24px;
+/* Chat UI */
+.script-chat {
+  max-width: 800px;
+  margin: 0 auto;
   display: flex;
   flex-direction: column;
-  gap: 24px;
-  overflow-y: auto;
-  box-shadow: 4px 0 24px rgba(15, 23, 42, 0.08);
+  gap: 1.5rem;
 }
 
-.sidebar-header {
+.chat-bubble {
   display: flex;
-  flex-direction: column;
-  gap: 16px;
+  gap: 1rem;
 }
 
-.sidebar-header h2 {
-  font-size: 24px;
-  font-weight: 700;
-  margin: 0;
-  color: #1f2937;
+.chat-bubble.host {
+  flex-direction: row;
 }
 
-.back-btn {
+.chat-bubble.guest {
+  flex-direction: row-reverse;
+}
+
+.avatar {
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  background: #3b82f6;
+  color: #fff;
   display: flex;
   align-items: center;
-  gap: 8px;
-  padding: 10px 16px;
-  background: transparent;
-  border: 1px solid rgba(148, 163, 184, 0.3);
-  border-radius: 12px;
-  color: #64748b;
-  font-size: 14px;
-  font-weight: 500;
-  cursor: pointer;
-  transition: all 0.2s ease;
-  width: fit-content;
+  justify-content: center;
+  font-weight: bold;
+  flex-shrink: 0;
 }
 
-.back-btn:hover:not(:disabled) {
-  background: rgba(59, 130, 246, 0.1);
-  border-color: #3b82f6;
-  color: #3b82f6;
+.chat-bubble.guest .avatar {
+  background: #8b5cf6;
 }
 
-.back-btn:disabled {
-  opacity: 0.5;
-  cursor: not-allowed;
+.bubble-content {
+  background: #334155;
+  padding: 1rem;
+  border-radius: 12px;
+  border-top-left-radius: 2px;
+  max-width: 80%;
+  line-height: 1.6;
 }
 
-.research-info {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  gap: 20px;
+.chat-bubble.guest .bubble-content {
+  background: #475569;
+  border-radius: 12px;
+  border-top-right-radius: 2px;
 }
 
-.info-item {
-  display: flex;
-  flex-direction: column;
-  gap: 8px;
+.speaker-name {
+  font-size: 0.75rem;
+  color: #94a3b8;
+  margin-bottom: 0.25rem;
+  text-transform: uppercase;
+  font-weight: 600;
 }
 
-.info-item label {
-  font-size: 12px;
-  font-weight: 600;
-  text-transform: uppercase;
-  letter-spacing: 0.5px;
-  color: #64748b;
+.markdown-report {
+  max-width: 800px;
+  margin: 0 auto;
+  color: #e2e8f0;
+  line-height: 1.7;
 }
 
-.info-item p {
-  margin: 0;
-  font-size: 14px;
-  color: #1f2937;
+.task-summary {
+  font-size: 0.85rem;
+  color: #cbd5e1;
+  margin-top: 0.5rem;
+  padding-top: 0.5rem;
+  border-top: 1px dashed rgba(255, 255, 255, 0.1);
   line-height: 1.6;
 }
 
-.topic-display {
-  font-size: 16px !important;
+.task-summary :deep(h1),
+.task-summary :deep(h2),
+.task-summary :deep(h3),
+.task-summary :deep(h4) {
+  font-size: 0.95rem;
+  font-weight: 700;
+  margin-top: 0.8rem;
+  margin-bottom: 0.4rem;
+  color: #e2e8f0;
+}
+
+.task-summary :deep(p) {
+  margin-bottom: 0.6rem;
+}
+
+.task-summary :deep(ul),
+.task-summary :deep(ol) {
+  padding-left: 1.2rem;
+  margin-bottom: 0.6rem;
+}
+
+.task-summary :deep(li) {
+  margin-bottom: 0.3rem;
+}
+
+.task-summary :deep(strong) {
+  color: #60a5fa;
   font-weight: 600;
-  color: #0f172a !important;
-  padding: 12px;
-  background: rgba(59, 130, 246, 0.05);
-  border-radius: 8px;
-  border-left: 3px solid #3b82f6;
 }
 
-.progress-bar {
-  width: 100%;
-  height: 8px;
-  background: rgba(148, 163, 184, 0.2);
-  border-radius: 4px;
-  overflow: hidden;
+.task-summary :deep(code) {
+  background: rgba(0, 0, 0, 0.3);
+  padding: 2px 4px;
+  border-radius: 3px;
+  font-family: 'Fira Code', monospace;
+  font-size: 0.8em;
+  color: #f472b6;
 }
 
-.progress-fill {
-  height: 100%;
-  background: linear-gradient(90deg, #3b82f6, #8b5cf6);
-  border-radius: 4px;
-  transition: width 0.5s ease;
+.report-content {
+  line-height: 1.8;
 }
 
-.progress-text {
-  font-size: 13px !important;
-  color: #64748b !important;
-  font-weight: 500;
+.report-content :deep(h1) {
+  font-size: 1.8rem;
+  margin-bottom: 1.5rem;
+  color: #60a5fa;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+  padding-bottom: 0.5rem;
 }
 
-.sidebar-actions {
-  display: flex;
-  flex-direction: column;
-  gap: 12px;
-  padding-top: 16px;
-  border-top: 1px solid rgba(148, 163, 184, 0.2);
+.report-content :deep(h2) {
+  font-size: 1.4rem;
+  margin-top: 2rem;
+  margin-bottom: 1rem;
+  color: #c084fc;
 }
 
-.new-research-btn {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  gap: 8px;
-  padding: 14px 20px;
-  background: linear-gradient(135deg, #3b82f6, #8b5cf6);
-  border: none;
-  border-radius: 12px;
-  color: white;
-  font-size: 15px;
-  font-weight: 600;
-  cursor: pointer;
-  transition: all 0.3s ease;
-  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
+.report-content :deep(h3) {
+  font-size: 1.1rem;
+  margin-top: 1.5rem;
+  margin-bottom: 0.8rem;
+  color: #e2e8f0;
 }
 
-.new-research-btn:hover {
-  transform: translateY(-2px);
-  box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
+.report-content :deep(p) {
+  margin-bottom: 1rem;
+  color: #cbd5e1;
 }
 
-.new-research-btn:active {
-  transform: translateY(0);
+.report-content :deep(ul),
+.report-content :deep(ol) {
+  padding-left: 1.5rem;
+  margin-bottom: 1rem;
 }
 
-/* 全屏状态下的结果面板 */
-.layout-fullscreen .panel-result {
-  flex: 1;
-  height: 100vh;
-  border-radius: 0;
-  border: none;
-  overflow-y: auto;
-  max-width: none;
+.report-content :deep(li) {
+  margin-bottom: 0.5rem;
 }
 
-@media (max-width: 1024px) {
-  .sidebar {
-    width: 320px;
-    min-width: 320px;
-  }
+.report-content :deep(strong) {
+  color: #fff;
+  font-weight: 600;
 }
 
-@media (max-width: 768px) {
-  .layout-fullscreen {
-    flex-direction: column;
-  }
+.report-content :deep(blockquote) {
+  border-left: 4px solid #60a5fa;
+  padding-left: 1rem;
+  margin: 1rem 0;
+  color: #94a3b8;
+  font-style: italic;
+  background: rgba(255, 255, 255, 0.05);
+  padding: 0.5rem 1rem;
+  border-radius: 0 4px 4px 0;
+}
 
-  .sidebar {
-    width: 100%;
-    min-width: 100%;
-    height: auto;
-    max-height: 40vh;
-  }
+/* Transitions */
+.fade-enter-active, .fade-leave-active {
+  transition: opacity 0.3s ease;
+}
 
-  .layout-fullscreen .panel-result {
-    height: 60vh;
-  }
+.fade-enter-from, .fade-leave-to {
+  opacity: 0;
 }
 </style>