Просмотр исходного кода

feat: enhance SetupView and TerminalLog components with new styles and features

- Updated SetupView.vue with a new layout, background decorations, and improved form elements.
- Enhanced TerminalLog.vue with a new terminal window design, improved title bar, and refined log entry styles.
- Added global styles for scrollbar and selection color in style.css.
- Improved accessibility and user experience with better button states and input handling.
JJSun 3 месяцев назад
Родитель
Сommit
355ec2c026

+ 1 - 1
Co-creation-projects/JJason-DeepCastAgent/.github/copilot-instructions.md

@@ -69,7 +69,7 @@ self.todo_agent = self._create_tool_aware_agent(
 | Role | Voice ID | Character |
 |------|----------|-----------|
 | Host (夏雨) | `xiayu` | Curious, humorous, audience proxy |
-| Guest (李) | `liwa` | Knowledgeable expert |
+| Guest (李) | `liwa` | Knowledgeable expert |
 
 Voice mapping in [backend/src/services/audio_generator.py](backend/src/services/audio_generator.py) `_get_voice_for_role()`
 

+ 2 - 1
Co-creation-projects/JJason-DeepCastAgent/.vscode/settings.json

@@ -1,5 +1,6 @@
 {
     "python.analysis.extraPaths": [
         "./backend/src"
-    ]
+    ],
+    "python.analysis.autoImportCompletions": true
 }

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

@@ -147,7 +147,7 @@ cd frontend
 npm run dev
 ```
 
-访问 `http://localhost:5173` 即可开始使用。
+访问 `http://localhost:5174` 即可开始使用。
 
 ## 📖 使用示例
 

+ 97 - 85
Co-creation-projects/JJason-DeepCastAgent/backend/src/agent.py

@@ -32,11 +32,6 @@ from services.tool_events import ToolCallTracker
 logger = logging.getLogger(__name__)
 
 
-class CancelledException(Exception):
-    """研究任务被用户取消时抛出的异常。"""
-    pass
-
-
 class DeepResearchAgent:
     """使用 HelloAgents 协调基于 TODO 的研究工作流的协调器。"""
 
@@ -187,7 +182,7 @@ class DeepResearchAgent:
     def run_stream(self, topic: str) -> Iterator[dict[str, Any]]:
         """
         执行研究工作流并产生增量进度事件(流式模式)。
-        
+
         此方法使用多线程并行执行研究任务,并通过生成器实时返回进度。
         主要步骤:
         1. 初始化并规划任务。
@@ -195,22 +190,57 @@ class DeepResearchAgent:
         3. 实时流式传输任务状态、搜索结果和部分总结。
         4. 所有任务完成后,生成并流式传输最终报告。
         5. 生成并流式传输播客脚本和音频合成进度。
-        
+
         支持通过 cancel() 方法取消执行。
         """
         # 重置取消状态
         self._cancel_event.clear()
-        
+
         state = SummaryState(research_topic=topic)
         logger.debug("Starting streaming research: topic=%s", topic)
         yield {"type": "status", "message": "初始化研究流程"}
 
-        # 检查取消
         if self.is_cancelled():
             yield {"type": "cancelled", "message": "研究任务已取消"}
             return
 
+        # Phase 1: 规划 + 并行研究
+        yield from self._stream_research_phase(state)
+        if self.is_cancelled():
+            yield {"type": "cancelled", "message": "研究任务已取消"}
+            return
+
+        # Phase 2: 报告生成
+        yield from self._stream_report_phase(state)
+        if self.is_cancelled():
+            yield {"type": "cancelled", "message": "研究任务已取消"}
+            return
+
+        # Phase 3: 播客脚本
+        script_turns = yield from self._stream_script_phase(state)
+        if self.is_cancelled():
+            yield {"type": "cancelled", "message": "研究任务已取消"}
+            return
+        if script_turns == 0:
+            yield {"type": "done"}
+            return
+
+        # Phase 4: 音频生成 + 合成
+        yield from self._stream_audio_phase(state, script_turns)
+
+        yield {"type": "done"}
+
+    # ------------------------------------------------------------------
+    # 流式阶段方法
+    # ------------------------------------------------------------------
+
+    def _stream_research_phase(self, state: SummaryState) -> Iterator[dict[str, Any]]:
+        """Phase 1: 规划任务并行执行搜索 + 总结。"""
+        if self.is_cancelled():
+            return
         state.todo_items = self.planner.plan_todo_list(state)
+        if self.is_cancelled():
+            return
         for event in self._drain_tool_events(state, step=0):
             yield event
         if not state.todo_items:
@@ -222,8 +252,6 @@ 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 {
@@ -245,7 +273,6 @@ class DeepResearchAgent:
             if task is not None:
                 target_task_id = task.id
                 payload["task_id"] = task.id
-
             channel = channel_map.get(target_task_id) if target_task_id is not None else None
             if channel:
                 payload.setdefault("step", channel["step"])
@@ -254,20 +281,15 @@ class DeepResearchAgent:
                 payload["step"] = step_override
             event_queue.put(payload)
 
-        def tool_event_sink(event: dict[str, Any]) -> None:
-            enqueue(event)
-
-        self._set_tool_event_sink(tool_event_sink)
+        self._set_tool_event_sink(lambda ev: enqueue(ev))
 
         threads: list[Thread] = []
 
         def worker(task: TodoItem, step: int) -> None:
             try:
-                # 检查取消状态
                 if self.is_cancelled():
                     enqueue({"type": "__task_done__", "task_id": task.id})
                     return
-                    
                 enqueue(
                     {
                         "type": "task_status",
@@ -281,16 +303,15 @@ class DeepResearchAgent:
                     },
                     task=task,
                 )
-
                 for event in self._execute_task(state, task, emit_stream=True, step=step):
-                    # 在每个事件之后检查取消
                     if self.is_cancelled():
                         break
                     enqueue(event, task=task)
-            except CancelledException:
-                logger.info("Task %s cancelled", task.id)
-            except Exception as exc:  # pragma: no cover - defensive guardrail
-                logger.exception("Task execution failed", exc_info=exc)
+            except Exception as exc:
+                if self.is_cancelled():
+                    logger.info("Task %s cancelled", task.id)
+                else:
+                    logger.exception("Task execution failed", exc_info=exc)
                 enqueue(
                     {
                         "type": "task_status",
@@ -319,17 +340,14 @@ class DeepResearchAgent:
 
         try:
             while finished_workers < active_workers:
-                # 使用带超时的 get 以便定期检查取消状态
                 try:
                     event = event_queue.get(timeout=0.5)
                 except Empty:
-                    # 检查是否取消
                     if self.is_cancelled():
                         logger.info("Research cancelled during task execution")
                         yield {"type": "cancelled", "message": "研究任务已取消"}
                         return
                     continue
-                    
                 if event.get("type") == "__task_done__":
                     finished_workers += 1
                     continue
@@ -347,18 +365,20 @@ class DeepResearchAgent:
             for thread in threads:
                 thread.join(timeout=1.0)
 
-        # 检查取消
-        if self.is_cancelled():
-            yield {"type": "cancelled", "message": "研究任务已取消"}
-            return
-
+    def _stream_report_phase(self, state: SummaryState) -> Iterator[dict[str, Any]]:
+        """Phase 2: 生成深度研究报告。"""
         yield {
             "type": "stage_change",
             "stage": "report",
             "message": "所有研究任务已完成,正在撰写深度研究报告...",
         }
         yield {"type": "log", "message": f"🧠 正在调用 {self.config.smart_llm_model} 模型撰写深度报告..."}
+
+        if self.is_cancelled():
+            return
         report = self.reporting.generate_report(state)
+        if self.is_cancelled():
+            return
         final_step = len(state.todo_items) + 1
         for event in self._drain_tool_events(state, step=final_step):
             yield event
@@ -366,9 +386,7 @@ class DeepResearchAgent:
         state.running_summary = report
         yield {"type": "log", "message": f"✓ 报告撰写完成,共 {len(report)} 字符"}
 
-        # 检查取消
         if self.is_cancelled():
-            yield {"type": "cancelled", "message": "研究任务已取消"}
             return
 
         note_event = self._persist_final_report(state, report)
@@ -382,11 +400,13 @@ class DeepResearchAgent:
             "note_path": state.report_note_path,
         }
 
-        # 检查取消
-        if self.is_cancelled():
-            yield {"type": "cancelled", "message": "研究任务已取消"}
-            return
+    def _stream_script_phase(self, state: SummaryState) -> Iterator[dict[str, Any] | int]:
+        """
+        Phase 3: 将报告转化为播客脚本。
 
+        Yields 流式事件,最终 return 脚本轮次数 (int)。
+        调用方通过 ``script_turns = yield from self._stream_script_phase(state)`` 获取。
+        """
         yield {
             "type": "stage_change",
             "stage": "script",
@@ -394,49 +414,51 @@ class DeepResearchAgent:
         }
         yield {"type": "log", "message": f"🧠 正在调用 {self.config.fast_llm_model} 模型生成播客脚本..."}
         yield {"type": "log", "message": "脚本策划专家正在创作 Host (Xiayu) 与 Guest (Liwa) 的对话..."}
+
+        if self.is_cancelled():
+            return
         script = self.script_generator.generate_script(state)
+        if self.is_cancelled():
+            return
         for event in self._drain_tool_events(state):
             yield event
         state.podcast_script = script
-        
+
         script_turns = len(script) if script else 0
         yield {"type": "log", "message": f"✓ 脚本生成完成,共 {script_turns} 轮对话"}
-        
         yield {
             "type": "podcast_script",
             "script": script,
             "turns": script_turns,
         }
-        
-        # 脚本为空时跳过音频生成
+
         if script_turns == 0:
             yield {"type": "log", "message": "⚠️ 警告:脚本为空,跳过音频生成"}
-            yield {"type": "done"}
-            return
 
-        # 检查取消
-        if self.is_cancelled():
-            yield {"type": "cancelled", "message": "研究任务已取消"}
-            return
+        return script_turns  # type: ignore[return-value]
+
+    def _stream_audio_phase(self, state: SummaryState, script_turns: int) -> Iterator[dict[str, Any]]:
+        """Phase 4: TTS 音频生成 + FFmpeg 合成。"""
+        script = state.podcast_script
 
         yield {
             "type": "stage_change",
             "stage": "audio",
             "message": "正在调用 TTS 语音引擎生成音频...",
         }
+
         task_id = f"task_{state.report_note_id}" if state.report_note_id else "task_default"
-        
+
         # 使用队列实现实时流式音频进度
         audio_event_queue: Queue[dict[str, Any]] = Queue()
         audio_result: list = []
         audio_error: list = []
-        cancel_audio = Event()  # 用于取消音频生成的信号
-        
-        def audio_progress_callback(current, total, role, preview):
-            """将进度事件放入队列以实现实时更新"""
-            # 检查是否应该取消
+        cancel_audio = Event()
+
+        def audio_progress_callback(current: int, total: int, role: str, preview: str) -> bool:
+            """将进度事件放入队列以实现实时更新。"""
             if self.is_cancelled() or cancel_audio.is_set():
-                return False  # 返回 False 表示应该停止
+                return False
             audio_event_queue.put({
                 "type": "audio_progress",
                 "current": current,
@@ -445,10 +467,10 @@ class DeepResearchAgent:
                 "preview": preview,
                 "message": f"[TTS {current}/{total}] ✓ {role} 语音生成成功",
             })
-            return True  # 返回 True 表示继续
-        
-        def run_audio_generation():
-            """在单独线程中运行音频生成"""
+            return True
+
+        def run_audio_generation() -> None:
+            """在单独线程中运行音频生成"""
             try:
                 files = self.audio_generator.generate_audio(
                     script, task_id, audio_progress_callback,
@@ -460,87 +482,77 @@ class DeepResearchAgent:
                     audio_error.append(str(e))
             finally:
                 audio_event_queue.put({"type": "_audio_done"})
-        
+
         yield {"type": "log", "message": f"准备为 {script_turns} 段对话生成语音..."}
-        
         yield {
             "type": "audio_start",
             "total": script_turns,
             "message": f"开始生成 {script_turns} 段语音",
         }
-        
-        # 在独立线程中启动音频生成
+
         audio_thread = Thread(target=run_audio_generation, daemon=True)
         audio_thread.start()
-        
+
         # 实时流式传输进度事件
         while True:
-            # 检查取消
             if self.is_cancelled():
-                cancel_audio.set()  # 通知音频生成线程停止
+                cancel_audio.set()
                 yield {"type": "cancelled", "message": "研究任务已取消"}
                 audio_thread.join(timeout=2.0)
                 return
-                
             try:
                 event = audio_event_queue.get(timeout=0.1)
                 if event.get("type") == "_audio_done":
                     break
                 yield event
-                # 每个片段完成后发送成功日志
                 if event.get("type") == "audio_progress":
                     yield {
-                        "type": "log", 
-                        "message": f"[TTS {event['current']}/{event['total']}] ✓ {event['role']} 语音已完成"
+                        "type": "log",
+                        "message": f"[TTS {event['current']}/{event['total']}] ✓ {event['role']} 语音已完成",
                     }
             except Empty:
                 continue
-        
+
         audio_thread.join(timeout=5.0)
-        
-        # 检查取消
+
         if self.is_cancelled():
             yield {"type": "cancelled", "message": "研究任务已取消"}
             return
-        
+
         audio_files = audio_result[0] if audio_result else []
         audio_count = len(audio_files) if audio_files else 0
-        
+
         if audio_error:
             yield {"type": "log", "message": f"⚠️ 音频生成出错: {audio_error[0]}"}
-        
+
         yield {"type": "log", "message": f"语音生成完成,成功 {audio_count}/{script_turns} 段"}
-        
         yield {
             "type": "audio_generated",
             "files": audio_files,
             "count": audio_count,
         }
 
+        # 合成播客
         yield {
             "type": "stage_change",
             "stage": "synthesis",
             "message": "正在合成完整播客音频文件...",
         }
 
-        # 检查取消
         if self.is_cancelled():
             yield {"type": "cancelled", "message": "研究任务已取消"}
             return
 
         yield {"type": "log", "message": "使用 FFmpeg 拼接所有语音片段..."}
-        podcast_file = self.podcast_synthesizer.synthesize_podcast(audio_files, task_id, cancel_check=self.is_cancelled)
+        podcast_file = self.podcast_synthesizer.synthesize_podcast(
+            audio_files, task_id, cancel_check=self.is_cancelled,
+        )
         if podcast_file:
-            yield {
-                "type": "podcast_ready",
-                "file": podcast_file,
-            }
+            yield {"type": "podcast_ready", "file": podcast_file}
             yield {"type": "log", "message": f"🎉 播客文件生成成功: {podcast_file}"}
         else:
             yield {"type": "log", "message": "⚠️ 播客合成失败,请检查 FFmpeg 配置"}
 
-        yield {"type": "done"}
-
     # ------------------------------------------------------------------
     # 执行助手
     # ------------------------------------------------------------------

+ 38 - 29
Co-creation-projects/JJason-DeepCastAgent/backend/src/main.py

@@ -3,9 +3,12 @@
 from __future__ import annotations
 
 import asyncio
+import concurrent.futures
+import glob
 import json
 import os
 import sys
+from contextlib import asynccontextmanager
 from typing import Any
 
 # Ensure src directory is in sys.path for module imports
@@ -73,40 +76,19 @@ def _build_config(payload: ResearchRequest) -> Configuration:
 
 def create_app() -> FastAPI:
     """创建并配置 FastAPI 应用实例。"""
-    app = FastAPI(title="DeepCast - 自动播客生成智能体")
 
     # 当前活跃的研究 agent 引用,用于支持取消操作
     _active_agent: dict[str, DeepResearchAgent | None] = {"current": None}
 
-    # 从配置读取 CORS 允许的源,避免生产环境使用通配符
-    _startup_config = Configuration.from_env()
-    _allowed_origins = [
-        origin.strip()
-        for origin in _startup_config.cors_origins.split(",")
-        if origin.strip()
-    ]
-    app.add_middleware(
-        CORSMiddleware,
-        allow_origins=_allowed_origins,
-        allow_credentials=True,
-        allow_methods=["*"],
-        allow_headers=["*"],
-    )
-
-    # 确保输出目录存在
-    # 使用绝对路径,基于 backend 根目录
+    # 确保输出目录存在(使用绝对路径,基于 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:
-        """记录启动时的关键配置参数。"""
+    @asynccontextmanager
+    async def lifespan(app: FastAPI):
+        """应用生命周期管理:启动时记录配置,关闭时清理资源。"""
         config = Configuration.from_env()
-
         logger.info(
             "DeepResearch configuration loaded: provider=%s model=%s base_url=%s search_api=%s "
             "max_loops=%s fetch_full_page=%s tool_calling=%s strip_thinking=%s api_key=%s",
@@ -120,6 +102,29 @@ def create_app() -> FastAPI:
             config.strip_thinking_tokens,
             _mask_secret(config.llm_api_key),
         )
+        yield  # 应用运行中
+        # 关闭时清理
+        _active_agent["current"] = None
+
+    app = FastAPI(title="DeepCast - 自动播客生成智能体", lifespan=lifespan)
+
+    # 从配置读取 CORS 允许的源,避免生产环境使用通配符
+    _startup_config = Configuration.from_env()
+    _allowed_origins = [
+        origin.strip()
+        for origin in _startup_config.cors_origins.split(",")
+        if origin.strip()
+    ]
+    app.add_middleware(
+        CORSMiddleware,
+        allow_origins=_allowed_origins,
+        allow_credentials=True,
+        allow_methods=["*"],
+        allow_headers=["*"],
+    )
+
+    # 挂载静态文件目录,用于访问生成的音频文件
+    app.mount("/output", StaticFiles(directory=output_dir), name="output")
 
     @app.get("/healthz")
     def health_check() -> dict[str, str]:
@@ -128,7 +133,6 @@ def create_app() -> FastAPI:
     @app.get("/api/audio/latest")
     def get_latest_audio() -> dict[str, Any]:
         """获取最新生成的音频文件。"""
-        import glob
         audio_dir = os.path.join(output_dir, "audio")
         if not os.path.exists(audio_dir):
             return {"file": None, "error": "音频目录不存在"}
@@ -222,8 +226,6 @@ def create_app() -> FastAPI:
             raise HTTPException(status_code=400, detail=str(exc)) from exc
 
         async def event_iterator():
-            import concurrent.futures
-
             loop = asyncio.get_event_loop()
             # 用 asyncio.Queue 桥接同步生成器和异步循环
             # 生成器在单一后台线程中完整运行,避免并发调用 next() 破坏生成器状态
@@ -234,6 +236,9 @@ def create_app() -> FastAPI:
                 """在后台线程中完整运行生成器,将事件逐一推入异步队列。"""
                 try:
                     for event in agent.run_stream(payload.topic):
+                        if agent.is_cancelled():
+                            logger.info("Generator stopped: cancel detected")
+                            break
                         loop.call_soon_threadsafe(event_queue.put_nowait, event)
                 except Exception as exc:
                     logger.exception("Generator raised exception")
@@ -280,12 +285,16 @@ def create_app() -> FastAPI:
                     if event.get("type") in ("done", "cancelled", "error"):
                         break
             finally:
+                # 确保取消信号被设置 —— 这是取消机制的核心:
+                # 前端 abort SSE 后 monitor_task 可能还未检测到断连就被 cancel,
+                # 而 /research/cancel API 到达时 _active_agent 可能已被置 None。
+                # 因此必须在此处显式调用 cancel() 确保后台线程能感知取消。
+                agent.cancel()
                 monitor_task.cancel()
                 try:
                     await monitor_task
                 except asyncio.CancelledError:
                     pass
-                # 不等待后台线程(daemon),立即返回响应
                 executor.shutdown(wait=False)
                 _active_agent["current"] = None
 

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

@@ -1,8 +1,6 @@
 """状态模型,用于深度研究工作流。"""
 
-import operator
 from dataclasses import dataclass, field
-from typing import Annotated
 
 
 @dataclass(kw_only=True)
@@ -30,11 +28,11 @@ class SummaryState:
     """
 
     research_topic: str | None = field(default=None)  # 研究主题
-    web_research_results: Annotated[list, operator.add] = field(default_factory=list)
-    sources_gathered: Annotated[list, operator.add] = field(default_factory=list)
+    web_research_results: list = field(default_factory=list)
+    sources_gathered: list = field(default_factory=list)
     research_loop_count: int = field(default=0)  # 研究循环次数
     running_summary: str | None = field(default=None)  # 传统摘要字段
-    todo_items: Annotated[list, operator.add] = field(default_factory=list)  # 待办任务项列表
+    todo_items: list = field(default_factory=list)  # 待办任务项列表
     structured_report: str | None = field(default=None)  # 结构化报告(JSON 字符串)
     report_note_id: str | None = field(default=None)  # 报告笔记 ID
     report_note_path: str | None = field(default=None)  # 报告笔记路径

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

@@ -3,6 +3,7 @@
 from __future__ import annotations
 
 import logging
+from collections.abc import Callable
 from pathlib import Path
 
 from pydub import AudioSegment
@@ -33,7 +34,7 @@ class PodcastSynthesisService:
         # 确保 pydub/ffmpeg 可用 - 假设 ffmpeg 已安装在系统中
         # 如果没有,pydub 可能会发出警告或失败,但我们会捕获异常。
 
-    def synthesize_podcast(self, audio_files: list[str], task_id: str = "default", cancel_check: callable = None) -> str | None:
+    def synthesize_podcast(self, audio_files: list[str], task_id: str = "default", cancel_check: Callable[[], bool] | None = None) -> str | None:
         """
         将音频文件组合成单个播客 MP3。
 

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

@@ -4,6 +4,7 @@ from __future__ import annotations
 
 import json
 import logging
+import re
 
 from openai import OpenAI
 
@@ -141,8 +142,6 @@ class ScriptGenerationService:
         Returns:
             解析后的列表,失败返回 None
         """
-        import re
-        
         # 1. 直接尝试解析
         try:
             return json.loads(content)
@@ -195,8 +194,6 @@ class ScriptGenerationService:
     
     def _fix_json_issues(self, json_str: str) -> str:
         """尝试修复常见的 JSON 格式问题。"""
-        import re
-        
         fixed = json_str
         
         # 替换中文引号为英文引号
@@ -222,8 +219,6 @@ class ScriptGenerationService:
         
         当整体解析失败时,尝试提取每个 {role, content} 对象。
         """
-        import re
-        
         results = []
         
         # 匹配 {"role": "...", "content": "..."} 模式

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

@@ -10,13 +10,12 @@
   },
   "dependencies": {
     "@tailwindcss/vite": "^4.1.18",
-    "@types/markdown-it": "^14.1.2",
-    "axios": "^1.7.9",
     "markdown-it": "^14.1.0",
     "tailwindcss": "^4.1.18",
     "vue": "^3.5.13"
   },
   "devDependencies": {
+    "@types/markdown-it": "^14.1.2",
     "@types/node": "^22.10.5",
     "@vitejs/plugin-vue": "^5.2.1",
     "daisyui": "^5.5.14",

+ 61 - 10
Co-creation-projects/JJason-DeepCastAgent/frontend/src/App.vue

@@ -1,5 +1,5 @@
-<template>
-  <div class="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
+<template>
+  <div class="app-root min-h-screen">
     <!-- View 1: Setup -->
     <SetupView
       v-if="currentView === 'setup'"
@@ -15,6 +15,7 @@
       :is-waiting="isWaiting"
       :waiting-dots="waitingDots"
       :production-stage="productionStage"
+      :progress-percent="progressPercent"
       :report-ready="reportReady"
       :podcast-ready="podcastReady"
       :audio-url="audioUrl"
@@ -58,6 +59,8 @@ const reportReady = ref(false);
 const podcastReady = ref(false);
 
 const audioProgress = reactive({ current: 0, total: 0, role: "" });
+const taskProgress = reactive({ completed: 0, total: 0 });
+const progressPercent = ref(0);
 const currentStatusMessage = ref("");
 const isWaiting = ref(false);
 const waitingDots = ref(".");
@@ -109,6 +112,9 @@ async function startProduction() {
   audioUrl.value = "";
   audioProgress.current = 0;
   audioProgress.total = 0;
+  taskProgress.completed = 0;
+  taskProgress.total = 0;
+  progressPercent.value = 2;
   currentStatusMessage.value = "正在初始化...";
   reportReady.value = false;
   podcastReady.value = false;
@@ -164,10 +170,19 @@ function handleStreamEvent(event: ResearchStreamEvent) {
     addLog(`📌 [STAGE] ${stage.toUpperCase()} - ${message}`);
     addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
 
-    if (stage === "report") productionStage.value = "research";
-    else if (stage === "script") productionStage.value = "script";
-    else if (stage === "audio") productionStage.value = "audio";
-    else if (stage === "synthesis") productionStage.value = "audio";
+    if (stage === "report") {
+      productionStage.value = "research";
+      progressPercent.value = 40;
+    } else if (stage === "script") {
+      productionStage.value = "script";
+      progressPercent.value = 55;
+    } else if (stage === "audio") {
+      productionStage.value = "audio";
+      progressPercent.value = 70;
+    } else if (stage === "synthesis") {
+      productionStage.value = "audio";
+      progressPercent.value = 95;
+    }
   }
 
   if (event.type === "tool_call") {
@@ -175,9 +190,20 @@ function handleStreamEvent(event: ResearchStreamEvent) {
     addLog(`🔧 [TOOL] ${p.tool} - ${p.agent || "Agent"}`);
   }
 
+  if (event.type === "todo_list") {
+    const p = event as any;
+    const tasks = p.tasks || [];
+    taskProgress.total = tasks.length;
+    taskProgress.completed = 0;
+  }
+
   if (event.type === "task_status") {
     const p = event as any;
     if (p.status === "completed") {
+      taskProgress.completed++;
+      if (taskProgress.total > 0) {
+        progressPercent.value = Math.round((taskProgress.completed / taskProgress.total) * 40);
+      }
       addLog(`✅ [TASK ${p.task_id}] ${p.title}`);
     } else if (p.status === "in_progress") {
       addLog(`🚀 [TASK ${p.task_id}] ${p.title} (In Progress)`);
@@ -208,6 +234,9 @@ function handleStreamEvent(event: ResearchStreamEvent) {
     audioProgress.current = p.current;
     audioProgress.total = p.total;
     currentStatusMessage.value = `生成音频: ${p.role} (${p.current}/${p.total})`;
+    if (p.total > 0) {
+      progressPercent.value = 70 + Math.round((p.current / p.total) * 25);
+    }
   }
 
   if (event.type === "podcast_ready") {
@@ -218,16 +247,27 @@ function handleStreamEvent(event: ResearchStreamEvent) {
       audioUrl.value = `${baseUrl}/output/audio/${filename}`;
       podcastReady.value = true;
       productionStage.value = "done";
+      progressPercent.value = 100;
       currentStatusMessage.value = "🎉 播客制作完成!";
       stopWaitingAnimation();
       addLog(`🎉 [PODCAST] 制作完成: ${filename}`);
     }
   }
 
+  if (event.type === "cancelled") {
+    const msg = (event as any).message || "研究任务已取消";
+    addLog(`🛑 [CANCELLED] ${msg}`);
+    stopWaitingAnimation();
+    productionStage.value = "cancelled";
+    currentStatusMessage.value = "任务已取消";
+    return;
+  }
+
   if (event.type === "done") {
     addLog("✅ [DONE] 所有任务结束");
     stopWaitingAnimation();
     productionStage.value = "done";
+    progressPercent.value = 100;
 
     if (!podcastReady.value && audioProgress.total > 0) {
       const baseUrl = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
@@ -259,15 +299,19 @@ function handleStreamEvent(event: ResearchStreamEvent) {
 function cancelProduction() {
   if (confirm("确定要取消制作吗?")) {
     addLog("🛑 用户请求取消制作...");
-    cancelResearch().then(() => {
-      addLog("✅ 后端已接收取消请求");
-    });
+
+    // 1. 立即中断 SSE 连接 — 后端 monitor_disconnect 会自动检测并设置 cancel_event
     if (abortController) {
       abortController.abort();
       abortController = null;
     }
+
+    // 2. 显式调用 cancel API 作为后备(防止 disconnect 检测延迟)
+    cancelResearch().catch(() => {});
+
     stopWaitingAnimation();
-    productionStage.value = "done";
+    productionStage.value = "cancelled";
+    addLog("🛑 已取消制作");
 
     setTimeout(() => {
       currentView.value = "setup";
@@ -299,3 +343,10 @@ function downloadReport() {
   URL.revokeObjectURL(url);
 }
 </script>
+
+<style scoped>
+.app-root {
+  background: linear-gradient(145deg, #0c0e14 0%, #111420 30%, #0e1018 60%, #0a0c12 100%);
+  background-attachment: fixed;
+}
+</style>

+ 297 - 63
Co-creation-projects/JJason-DeepCastAgent/frontend/src/components/PlayerView.vue

@@ -1,60 +1,107 @@
 <template>
-  <div class="hero min-h-screen">
-    <div class="hero-content flex-col lg:flex-row-reverse gap-8 w-full max-w-6xl items-start">
-      <!-- Right: Report -->
-      <div class="card glass-panel shadow-2xl flex-1 h-[70vh] w-full lg:w-3/5 overflow-hidden rounded-2xl border border-white/10">
-        <div class="card-body p-0 flex flex-col h-full bg-black/20">
-          <div class="p-6 border-b border-white/10 sticky top-0 z-10 bg-black/40 backdrop-blur-md">
-            <div class="flex items-center justify-between">
-              <h2 class="card-title text-white">📄 研究报告</h2>
-              <button class="btn btn-xs btn-ghost text-white/50" @click="$emit('downloadReport')">下载</button>
+  <div class="min-h-screen p-4 md:p-6">
+    <div class="max-w-6xl mx-auto">
+      <!-- Navbar -->
+      <div class="player-nav rounded-2xl mb-6 px-6 py-3.5">
+        <div class="flex items-center justify-between">
+          <div class="flex items-center gap-3">
+            <div class="w-9 h-9 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-blue-500/20">
+              <span class="text-lg">🎙️</span>
             </div>
+            <span class="text-xl font-bold text-white tracking-tight">DeepCast</span>
           </div>
-          <div class="overflow-y-auto p-8 custom-scrollbar flex-1 text-gray-200">
-            <article class="prose prose-sm prose-invert max-w-none" v-html="renderedReport"></article>
+          <div class="flex items-center gap-2">
+            <button class="player-nav-btn text-blue-300" @click="$emit('downloadReport')" aria-label="下载研究报告">
+              <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
+              报告
+            </button>
+            <button class="player-nav-btn text-gray-400" @click="$emit('reset')" aria-label="制作新播客">
+              <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
+              新播客
+            </button>
           </div>
         </div>
       </div>
 
-      <!-- Left: Player -->
-      <div class="card glass-panel shadow-2xl flex-shrink-0 w-full lg:w-2/5 text-center h-auto rounded-2xl border border-white/10">
-        <figure class="px-10 pt-12 pb-4">
-          <div class="avatar placeholder">
-            <div class="bg-black/40 text-white rounded-full w-48 h-48 ring-4 ring-white/10 shadow-[0_0_50px_rgba(0,0,0,0.5)] flex items-center justify-center relative overflow-hidden backdrop-blur-md">
-              <!-- Vinyl Animation -->
-              <div class="absolute inset-0 border-[2px] border-white/5 rounded-full" style="margin: 2px"></div>
-              <div class="absolute inset-0 border-[2px] border-white/5 rounded-full" style="margin: 10px"></div>
-              <div class="absolute inset-0 border-[2px] border-white/5 rounded-full" style="margin: 20px"></div>
-              <div class="absolute inset-0 border-[10px] border-black/60 rounded-full opacity-40" :class="{ 'animate-spin': isPlaying }" style="animation-duration: 4s;"></div>
-              <!-- Center Label -->
-              <div class="z-10 w-16 h-16 rounded-full bg-gradient-to-tr from-blue-500 to-purple-500 shadow-inner flex items-center justify-center">
-                <span class="text-xl font-bold text-white">DC</span>
+      <!-- Content -->
+      <div class="grid grid-cols-1 lg:grid-cols-5 gap-5 items-start">
+
+        <!-- Left: Player Card -->
+        <div class="lg:col-span-2">
+          <div class="player-card rounded-2xl overflow-hidden">
+            <!-- Album Art Area -->
+            <div class="player-art-area">
+              <div class="player-art-bg"></div>
+              <div class="relative z-10 flex flex-col items-center py-10">
+                <!-- Vinyl disc -->
+                <div class="player-vinyl" :class="{ 'player-vinyl--spinning': isPlaying }">
+                  <div class="player-vinyl-ring player-vinyl-ring-1"></div>
+                  <div class="player-vinyl-ring player-vinyl-ring-2"></div>
+                  <div class="player-vinyl-ring player-vinyl-ring-3"></div>
+                  <div class="player-vinyl-groove"></div>
+                  <!-- Center label -->
+                  <div class="player-vinyl-label">
+                    <span class="text-lg font-bold text-white tracking-tight">DC</span>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <!-- Info & Controls -->
+            <div class="p-6">
+              <div class="text-center mb-5">
+                <h2 class="text-xl font-bold text-white leading-tight mb-1.5">{{ topic }}</h2>
+                <p class="text-xs font-medium text-gray-500 tracking-[0.2em] uppercase">DeepCast Original</p>
+              </div>
+
+              <!-- Audio Player -->
+              <div class="player-audio-wrap mb-5">
+                <audio
+                  ref="audioPlayer"
+                  :src="audioUrl"
+                  controls
+                  class="w-full"
+                  @play="isPlaying = true"
+                  @pause="isPlaying = false"
+                ></audio>
+              </div>
+
+              <!-- Action Buttons -->
+              <div class="flex flex-col gap-2.5">
+                <a :href="audioUrl" download class="player-btn-primary" aria-label="下载播客 MP3 文件">
+                  <svg class="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
+                  下载 MP3
+                </a>
+                <button class="player-btn-ghost" @click="$emit('reset')" aria-label="制作新播客">
+                  <svg class="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
+                  制作新播客
+                </button>
               </div>
             </div>
           </div>
-        </figure>
-        <div class="card-body items-center text-center pt-2">
-          <h2 class="card-title text-2xl text-white font-bold drop-shadow-md">{{ topic }}</h2>
-          <p class="text-blue-200/60 text-sm font-medium tracking-widest uppercase mb-6">DeepCast Original</p>
-
-          <div class="w-full bg-black/30 rounded-xl p-4 border border-white/5 shadow-inner">
-            <audio
-              ref="audioPlayer"
-              :src="audioUrl"
-              controls
-              class="w-full"
-              @play="isPlaying = true"
-              @pause="isPlaying = false"
-            ></audio>
-          </div>
+        </div>
 
-          <div class="card-actions mt-8 w-full gap-3 flex-col">
-            <a :href="audioUrl" download class="btn macos-btn-primary w-full border-0 rounded-xl text-lg h-12">
-              ⬇️ 下载 MP3
-            </a>
-            <button class="btn btn-ghost text-white/50 hover:text-white w-full" @click="$emit('reset')">
-              🪄 制作新播客
-            </button>
+        <!-- Right: Report -->
+        <div class="lg:col-span-3">
+          <div class="report-card rounded-2xl overflow-hidden" style="height: 75vh;">
+            <!-- Report Header (macOS-style) -->
+            <div class="report-header">
+              <div class="flex items-center gap-2 mr-4">
+                <div class="w-3 h-3 rounded-full bg-[#ff5f57]"></div>
+                <div class="w-3 h-3 rounded-full bg-[#febc2e]"></div>
+                <div class="w-3 h-3 rounded-full bg-[#28c840]"></div>
+              </div>
+              <div class="flex-1 text-center">
+                <span class="text-sm font-medium text-gray-400 tracking-wide">研究报告</span>
+              </div>
+              <button class="report-download-btn" @click="$emit('downloadReport')" aria-label="下载研究报告">
+                <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
+              </button>
+            </div>
+            <!-- Report Content -->
+            <div class="report-content custom-scrollbar" style="height: calc(100% - 48px);">
+              <article class="prose prose-sm prose-invert max-w-none report-prose" v-html="renderedReport"></article>
+            </div>
           </div>
         </div>
       </div>
@@ -86,30 +133,217 @@ const renderedReport = computed(() => md.render(props.reportMarkdown));
 </script>
 
 <style scoped>
-.glass-panel {
-  background: rgba(30, 30, 30, 0.7);
-  backdrop-filter: blur(25px);
-  -webkit-backdrop-filter: blur(25px);
-  border: 1px solid rgba(255, 255, 255, 0.08);
-  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
+/* ── Nav ── */
+.player-nav {
+  background: rgba(30, 32, 38, 0.9);
+  backdrop-filter: blur(20px);
+  -webkit-backdrop-filter: blur(20px);
+  border: 1px solid rgba(255, 255, 255, 0.06);
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
+}
+.player-nav-btn {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 14px;
+  border-radius: 8px;
+  font-size: 13px;
+  font-weight: 500;
+  background: rgba(255, 255, 255, 0.04);
+  border: 1px solid rgba(255, 255, 255, 0.06);
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+.player-nav-btn:hover {
+  background: rgba(255, 255, 255, 0.08);
+  border-color: rgba(255, 255, 255, 0.1);
+}
+
+/* ── Player Card ── */
+.player-card {
+  background: rgba(22, 24, 30, 0.85);
+  backdrop-filter: blur(30px);
+  -webkit-backdrop-filter: blur(30px);
+  border: 1px solid rgba(255, 255, 255, 0.06);
+  box-shadow:
+    0 25px 60px rgba(0, 0, 0, 0.4),
+    inset 0 1px 0 rgba(255, 255, 255, 0.05);
 }
 
-.macos-btn-primary {
-  background: linear-gradient(180deg, #0A84FF 0%, #007AFF 100%);
+/* ── Album Art Area ── */
+.player-art-area {
+  position: relative;
+  overflow: hidden;
+  background: linear-gradient(135deg, rgba(15, 23, 42, 0.8), rgba(30, 20, 50, 0.6));
+}
+.player-art-bg {
+  position: absolute;
+  inset: 0;
+  background:
+    radial-gradient(ellipse at 30% 50%, rgba(59, 130, 246, 0.15), transparent 60%),
+    radial-gradient(ellipse at 70% 30%, rgba(139, 92, 246, 0.12), transparent 50%);
+}
+
+/* ── Vinyl Disc ── */
+.player-vinyl {
+  width: 180px;
+  height: 180px;
+  border-radius: 50%;
+  background: radial-gradient(circle, #1a1a2e 0%, #0d0d1a 100%);
+  box-shadow:
+    0 0 0 3px rgba(255, 255, 255, 0.05),
+    0 10px 40px rgba(0, 0, 0, 0.5);
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: transform 0.5s ease;
+}
+.player-vinyl--spinning {
+  animation: vinyl-spin 4s linear infinite;
+}
+.player-vinyl-ring {
+  position: absolute;
+  inset: 0;
+  border-radius: 50%;
+  border: 1px solid rgba(255, 255, 255, 0.04);
+}
+.player-vinyl-ring-1 { margin: 8px; }
+.player-vinyl-ring-2 { margin: 22px; }
+.player-vinyl-ring-3 { margin: 38px; }
+.player-vinyl-groove {
+  position: absolute;
+  inset: 5px;
+  border-radius: 50%;
+  background: repeating-radial-gradient(
+    circle at center,
+    transparent 0px,
+    transparent 3px,
+    rgba(255, 255, 255, 0.02) 3px,
+    rgba(255, 255, 255, 0.02) 4px
+  );
+}
+.player-vinyl-label {
+  width: 56px;
+  height: 56px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #3b82f6, #8b5cf6);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 2;
+  box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.2);
+}
+
+/* ── Audio Wrap ── */
+.player-audio-wrap {
+  background: rgba(0, 0, 0, 0.2);
+  border-radius: 12px;
+  padding: 12px;
+  border: 1px solid rgba(255, 255, 255, 0.04);
+}
+
+/* ── Buttons ── */
+.player-btn-primary {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 12px;
+  border-radius: 12px;
+  font-size: 15px;
+  font-weight: 600;
   color: white;
+  text-decoration: none;
+  background: linear-gradient(135deg, #3b82f6 0%, #6366f1 50%, #8b5cf6 100%);
   border: 1px solid rgba(255, 255, 255, 0.1);
-  box-shadow: 0 1px 2px rgba(0,0,0,0.2), inset 0 1px 1px rgba(255,255,255,0.2);
-  transition: all 0.2s;
+  box-shadow: 0 4px 14px rgba(59, 130, 246, 0.25), inset 0 1px 1px rgba(255, 255, 255, 0.15);
+  cursor: pointer;
+  transition: all 0.2s ease;
 }
-.macos-btn-primary:hover {
+.player-btn-primary:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 8px 25px rgba(59, 130, 246, 0.3), inset 0 1px 1px rgba(255, 255, 255, 0.15);
   filter: brightness(1.05);
-  transform: translateY(-0.5px);
-  box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3), inset 0 1px 1px rgba(255,255,255,0.2);
 }
-.macos-btn-primary:active { transform: translateY(0.5px); filter: brightness(0.95); }
+.player-btn-ghost {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 10px;
+  border-radius: 10px;
+  font-size: 14px;
+  font-weight: 500;
+  color: #9ca3af;
+  background: transparent;
+  border: 1px solid rgba(255, 255, 255, 0.06);
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+.player-btn-ghost:hover {
+  color: white;
+  background: rgba(255, 255, 255, 0.05);
+  border-color: rgba(255, 255, 255, 0.1);
+}
 
-.custom-scrollbar::-webkit-scrollbar { width: 8px; height: 8px; }
+/* ── Report Card ── */
+.report-card {
+  background: rgba(22, 24, 30, 0.85);
+  backdrop-filter: blur(30px);
+  -webkit-backdrop-filter: blur(30px);
+  border: 1px solid rgba(255, 255, 255, 0.06);
+  box-shadow:
+    0 25px 60px rgba(0, 0, 0, 0.4),
+    inset 0 1px 0 rgba(255, 255, 255, 0.05);
+}
+.report-header {
+  display: flex;
+  align-items: center;
+  padding: 12px 16px;
+  background: linear-gradient(180deg, rgba(50, 52, 58, 0.9), rgba(38, 40, 46, 0.9));
+  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
+  user-select: none;
+}
+.report-download-btn {
+  width: 28px;
+  height: 28px;
+  border-radius: 6px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #6b7280;
+  background: transparent;
+  border: none;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+.report-download-btn:hover {
+  color: #d1d5db;
+  background: rgba(255, 255, 255, 0.06);
+}
+.report-content {
+  overflow-y: auto;
+  padding: 28px 32px;
+  color: #d1d5db;
+}
+.report-prose :deep(h1) { color: #f3f4f6; font-size: 1.5em; margin-top: 1em; }
+.report-prose :deep(h2) { color: #e5e7eb; font-size: 1.25em; margin-top: 0.8em; border-bottom: 1px solid rgba(255,255,255,0.06); padding-bottom: 0.3em; }
+.report-prose :deep(h3) { color: #d1d5db; font-size: 1.1em; }
+.report-prose :deep(a) { color: #60a5fa; text-decoration: none; }
+.report-prose :deep(a:hover) { text-decoration: underline; }
+.report-prose :deep(code) { background: rgba(255,255,255,0.06); padding: 2px 6px; border-radius: 4px; font-size: 0.85em; }
+.report-prose :deep(blockquote) { border-left: 3px solid #3b82f6; padding-left: 1em; color: #9ca3af; }
+
+/* ── Scrollbar ── */
+.custom-scrollbar::-webkit-scrollbar { width: 6px; }
 .custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
-.custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); border-radius: 4px; }
-.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.25); }
+.custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 3px; }
+.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); }
+
+/* ── Animation ── */
+@keyframes vinyl-spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
 </style>

+ 399 - 95
Co-creation-projects/JJason-DeepCastAgent/frontend/src/components/ProductionView.vue

@@ -1,82 +1,121 @@
 <template>
-  <div class="min-h-screen p-6">
+  <div class="min-h-screen p-4 md:p-6">
     <div class="max-w-7xl mx-auto">
       <!-- Navbar / Header -->
-      <div class="nav-glass rounded-xl shadow-lg mb-6 px-6 py-4">
+      <div class="nav-glass rounded-2xl shadow-lg mb-6 px-6 py-3.5">
         <div class="flex items-center justify-between gap-4">
           <div class="flex items-center gap-3">
-            <span class="text-3xl filter drop-shadow-md">🎙️</span>
-            <span class="text-2xl font-bold text-white tracking-tight">DeepCast</span>
+            <div class="w-9 h-9 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-blue-500/20">
+              <span class="text-lg">🎙️</span>
+            </div>
+            <span class="text-xl font-bold text-white tracking-tight">DeepCast</span>
           </div>
-          <div class="flex items-center gap-3">
-            <button v-if="reportReady" class="btn btn-ghost btn-sm text-blue-300 hover:bg-white/5" @click="$emit('downloadReport')">
-              📄 下载研究报告
+          <div class="flex items-center gap-2">
+            <button v-if="reportReady" class="nav-action-btn text-blue-300" @click="$emit('downloadReport')" aria-label="下载研究报告">
+              <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
+              研究报告
             </button>
-            <button v-if="!podcastReady" class="btn btn-ghost btn-sm text-red-400 hover:bg-white/5" @click="$emit('cancel')">
-              取消制作
+            <button v-if="!podcastReady" class="nav-action-btn text-red-400" @click="$emit('cancel')" aria-label="取消制作">
+              <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
+              取消
             </button>
           </div>
         </div>
       </div>
 
       <!-- Main Content -->
-      <div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
+      <div class="grid grid-cols-1 lg:grid-cols-4 gap-5">
 
         <!-- Left Column: Progress Steps -->
         <div class="lg:col-span-1">
-          <div class="card glass-panel h-[500px] rounded-xl">
-            <div class="card-body p-6 relative overflow-hidden">
-              <!-- Decorative element -->
-              <div class="absolute top-0 right-0 -mr-8 -mt-8 w-40 h-40 bg-blue-500/20 rounded-full blur-3xl"></div>
-              <div class="absolute bottom-0 left-0 -ml-8 -mb-8 w-40 h-40 bg-purple-500/20 rounded-full blur-3xl"></div>
-
-              <h2 class="text-xl font-bold text-white mb-8 flex items-center justify-center gap-3 z-10">
-                <div class="p-2 bg-white/10 rounded-lg backdrop-blur-md shadow-inner border border-white/5">
-                  <span v-if="productionStage === 'done'" class="text-2xl">✅</span>
-                  <span v-else class="text-3xl animate-spin-slow inline-block">🔄</span>
+          <div class="pipeline-card rounded-2xl h-[500px]">
+            <!-- Top progress bar -->
+            <div class="pipeline-progress-bar">
+              <div class="pipeline-progress-fill" :style="{ width: progress + '%' }"></div>
+            </div>
+
+            <div class="p-5 h-full flex flex-col relative overflow-hidden">
+              <!-- Ambient glow -->
+              <div class="ambient-glow ambient-glow-1"></div>
+              <div class="ambient-glow ambient-glow-2"></div>
+
+              <!-- Header -->
+              <div class="flex items-center gap-3 mb-2 z-10 relative">
+                <div class="pipeline-icon-badge">
+                  <span v-if="productionStage === 'done'" class="text-lg">✅</span>
+                  <svg v-else class="w-5 h-5 text-blue-400 animate-spin-slow" fill="none" viewBox="0 0 24 24">
+                    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3"/>
+                    <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
+                  </svg>
                 </div>
-                <span class="tracking-wide">制作流程</span>
-              </h2>
-
-              <div class="flex-1 w-full flex justify-center pl-4">
-                <ul class="steps steps-vertical font-medium w-full h-full justify-evenly">
-                  <li class="step gap-3" :class="getStepClass('research')">
-                    <div class="flex flex-col text-left py-2 min-w-[120px]">
-                      <div class="flex items-center gap-2">
-                        <span class="text-xl filter drop-shadow" :class="{ 'animate-bounce': productionStage === 'research' }">🔍</span>
-                        <span class="font-bold text-gray-200">深度研究</span>
-                      </div>
-                      <span class="text-xs text-gray-400 font-normal ml-8 mt-1">网络搜索 & 信息聚合</span>
-                    </div>
-                  </li>
-                  <li class="step gap-3" :class="getStepClass('script')">
-                    <div class="flex flex-col text-left py-2 min-w-[120px]">
-                      <div class="flex items-center gap-2">
-                        <span class="text-xl filter drop-shadow" :class="{ 'animate-bounce': productionStage === 'script' }">✍️</span>
-                        <span class="font-bold text-gray-200">剧本创作</span>
-                      </div>
-                      <span class="text-xs text-gray-400 font-normal ml-8 mt-1">生成对话 & 角色分配</span>
-                    </div>
-                  </li>
-                  <li class="step gap-3" :class="getStepClass('audio')">
-                    <div class="flex flex-col text-left py-2 min-w-[120px]">
-                      <div class="flex items-center gap-2">
-                        <span class="text-xl filter drop-shadow" :class="{ 'animate-bounce': productionStage === 'audio' }">🎵</span>
-                        <span class="font-bold text-gray-200">音频合成</span>
-                      </div>
-                      <span class="text-xs text-gray-400 font-normal ml-8 mt-1">TTS 语音生成 & 拼接</span>
-                    </div>
-                  </li>
-                  <li class="step gap-3" :class="{ 'step-primary': podcastReady || productionStage === 'done' }">
-                    <div class="flex flex-col text-left py-2 min-w-[120px]">
-                      <div class="flex items-center gap-2">
-                        <span class="text-xl filter drop-shadow" :class="{ 'animate-pulse': podcastReady }">🎉</span>
-                        <span class="font-bold text-gray-200">完成</span>
-                      </div>
-                      <span class="text-xs text-gray-400 font-normal ml-8 mt-1">播放 & 下载播客</span>
+                <div>
+                  <h2 class="text-base font-bold text-white leading-tight">制作流程</h2>
+                  <p class="text-[11px] text-gray-500 mt-0.5">{{ stageLabel }}</p>
+                </div>
+                <div class="ml-auto">
+                  <span class="text-xs font-mono font-semibold text-blue-400/80">{{ progress }}%</span>
+                </div>
+              </div>
+
+              <!-- Divider -->
+              <div class="h-px bg-gradient-to-r from-transparent via-white/10 to-transparent my-3"></div>
+
+              <!-- Timeline Steps -->
+              <div class="flex-1 flex flex-col justify-center gap-1 z-10 relative">
+                <div v-for="(step, idx) in pipelineSteps" :key="step.id"
+                  class="pipeline-step group"
+                  :class="{
+                    'pipeline-step--completed': isStepCompleted(step.id),
+                    'pipeline-step--active': isStepActive(step.id),
+                    'pipeline-step--pending': isStepPending(step.id),
+                  }">
+                  <!-- Connector line -->
+                  <div v-if="idx < pipelineSteps.length - 1" class="pipeline-connector"
+                    :class="{
+                      'pipeline-connector--completed': isStepCompleted(step.id),
+                      'pipeline-connector--active': isStepActive(step.id),
+                    }"></div>
+
+                  <!-- Step indicator -->
+                  <div class="pipeline-indicator">
+                    <!-- Completed -->
+                    <svg v-if="isStepCompleted(step.id)" class="w-3.5 h-3.5 text-white" fill="currentColor" viewBox="0 0 20 20">
+                      <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
+                    </svg>
+                    <!-- Active pulse -->
+                    <div v-else-if="isStepActive(step.id)" class="pipeline-pulse"></div>
+                    <!-- Pending dot -->
+                    <div v-else class="w-2 h-2 rounded-full bg-gray-600"></div>
+                  </div>
+
+                  <!-- Step content -->
+                  <div class="pipeline-step-content">
+                    <div class="flex items-center gap-2">
+                      <span class="text-base" :class="{ 'animate-float': isStepActive(step.id) }">{{ step.icon }}</span>
+                      <span class="text-sm font-semibold" :class="isStepActive(step.id) ? 'text-white' : isStepCompleted(step.id) ? 'text-gray-300' : 'text-gray-500'">{{ step.label }}</span>
                     </div>
-                  </li>
-                </ul>
+                    <p class="text-[11px] mt-0.5 ml-7" :class="isStepActive(step.id) ? 'text-gray-400' : 'text-gray-600'">{{ step.desc }}</p>
+                  </div>
+                </div>
+              </div>
+
+              <!-- Bottom status chip -->
+              <div class="z-10 relative mt-3">
+                <div class="pipeline-status-chip" :class="{
+                  'pipeline-status-chip--done': productionStage === 'done',
+                  'pipeline-status-chip--cancelled': isCancelled
+                }">
+                  <span class="inline-block w-1.5 h-1.5 rounded-full mr-2" :class="
+                    productionStage === 'done' ? 'bg-emerald-400' :
+                    isCancelled ? 'bg-red-400' :
+                    'bg-blue-400 animate-pulse'
+                  "></span>
+                  <span class="text-[11px] font-medium">{{
+                    productionStage === 'done' ? '制作完成' :
+                    isCancelled ? '已取消' :
+                    '正在处理...'
+                  }}</span>
+                </div>
               </div>
             </div>
           </div>
@@ -89,23 +128,27 @@
           <TerminalLog ref="terminalRef" :logs="logs" :is-waiting="isWaiting" :waiting-dots="waitingDots" />
 
           <!-- Result Actions -->
-          <div v-if="podcastReady" class="flex gap-4">
-            <a :href="audioUrl" download class="btn macos-btn-primary flex-1 btn-lg text-lg rounded-xl border-0">
-              ⬇️ 下载 MP3
+          <div v-if="podcastReady" class="flex gap-3">
+            <a :href="audioUrl" download class="btn macos-btn-primary flex-1 btn-lg text-base rounded-xl border-0 gap-2">
+              <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
+              下载 MP3
             </a>
-            <button class="btn glass text-white flex-1 btn-lg text-lg rounded-xl" @click="$emit('goPlayer')">
-              🎧 进入播放器
+            <button class="btn result-btn-secondary flex-1 btn-lg text-base rounded-xl gap-2" @click="$emit('goPlayer')">
+              <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"/></svg>
+              进入播放器
             </button>
           </div>
 
           <!-- Inline Player -->
-          <div v-if="podcastReady" class="card glass-panel rounded-xl mt-2">
-            <div class="card-body p-4">
-              <div class="flex items-center gap-3 mb-2">
-                <span class="text-xl">🎧</span>
-                <h3 class="text-sm font-bold text-gray-200">快速试听</h3>
+          <div v-if="podcastReady" class="player-inline-card rounded-xl">
+            <div class="p-4">
+              <div class="flex items-center gap-2.5 mb-3">
+                <div class="w-7 h-7 rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shadow-md">
+                  <svg class="w-3.5 h-3.5 text-white" fill="currentColor" viewBox="0 0 20 20"><path d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v9.114A4.369 4.369 0 005 14c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V7.82l8-1.6v5.894A4.37 4.37 0 0015 12c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V3z"/></svg>
+                </div>
+                <h3 class="text-sm font-semibold text-gray-200">快速试听</h3>
               </div>
-              <audio class="w-full opacity-90 hover:opacity-100 transition-opacity" :src="audioUrl" controls></audio>
+              <audio class="w-full audio-player" :src="audioUrl" controls></audio>
             </div>
           </div>
 
@@ -116,17 +159,34 @@
 </template>
 
 <script lang="ts" setup>
-import { ref } from "vue";
+import { ref, computed, toRef } from "vue";
 import TerminalLog from "./TerminalLog.vue";
 import type { LogEntry } from "./TerminalLog.vue";
 
-export type ProductionStage = "research" | "script" | "audio" | "done";
+export type ProductionStage = "research" | "script" | "audio" | "done" | "cancelled";
+
+interface PipelineStep {
+  id: ProductionStage;
+  icon: string;
+  label: string;
+  desc: string;
+}
+
+const pipelineSteps: PipelineStep[] = [
+  { id: "research", icon: "🔍", label: "深度研究", desc: "网络搜索 & 信息聚合" },
+  { id: "script", icon: "✍️", label: "剧本创作", desc: "生成对话 & 角色分配" },
+  { id: "audio", icon: "🎵", label: "音频合成", desc: "TTS 语音生成 & 拼接" },
+  { id: "done", icon: "🎉", label: "制作完成", desc: "播放 & 下载播客" },
+];
+
+const stepsOrder: ProductionStage[] = ["research", "script", "audio", "done"];
 
 const props = defineProps<{
   logs: LogEntry[];
   isWaiting: boolean;
   waitingDots: string;
   productionStage: ProductionStage;
+  progressPercent: number;
   reportReady: boolean;
   podcastReady: boolean;
   audioUrl: string;
@@ -146,50 +206,294 @@ function scrollTerminal() {
 
 defineExpose({ scrollTerminal });
 
-function getStepClass(step: ProductionStage) {
-  const stepsOrder: ProductionStage[] = ["research", "script", "audio", "done"];
-  const currentIdx = stepsOrder.indexOf(props.productionStage);
-  const stepIdx = stepsOrder.indexOf(step);
+const progress = toRef(props, 'progressPercent');
+const currentIdx = computed(() => stepsOrder.indexOf(props.productionStage));
+
+const isCancelled = computed(() => props.productionStage === 'cancelled');
+
+const stageLabel = computed(() => {
+  const labels: Record<ProductionStage, string> = {
+    research: "正在进行深度研究...",
+    script: "正在创作剧本...",
+    audio: "正在合成音频...",
+    done: "播客制作完成!",
+    cancelled: "已取消制作",
+  };
+  return labels[props.productionStage] || "";
+});
+
+function isStepCompleted(stepId: ProductionStage) {
+  return currentIdx.value > stepsOrder.indexOf(stepId);
+}
+
+function isStepActive(stepId: ProductionStage) {
+  return currentIdx.value === stepsOrder.indexOf(stepId);
+}
 
-  if (currentIdx > stepIdx) return "step-primary";
-  if (currentIdx === stepIdx) return "step-primary font-bold";
-  return "";
+function isStepPending(stepId: ProductionStage) {
+  return currentIdx.value < stepsOrder.indexOf(stepId);
 }
 </script>
 
 <style scoped>
-.glass-panel {
-  background: rgba(30, 30, 30, 0.7);
-  backdrop-filter: blur(25px);
-  -webkit-backdrop-filter: blur(25px);
+/* ── Pipeline Card ── */
+.pipeline-card {
+  background: rgba(22, 24, 30, 0.85);
+  backdrop-filter: blur(30px);
+  -webkit-backdrop-filter: blur(30px);
+  border: 1px solid rgba(255, 255, 255, 0.06);
+  box-shadow:
+    0 20px 50px rgba(0, 0, 0, 0.4),
+    inset 0 1px 0 rgba(255, 255, 255, 0.05);
+  position: relative;
+  overflow: hidden;
+}
+
+/* ── Top Progress Bar ── */
+.pipeline-progress-bar {
+  height: 3px;
+  background: rgba(255, 255, 255, 0.05);
+  position: relative;
+  overflow: hidden;
+}
+.pipeline-progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #3b82f6, #8b5cf6, #06b6d4);
+  background-size: 200% 100%;
+  animation: shimmer 2s ease-in-out infinite;
+  transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
+  border-radius: 0 2px 2px 0;
+}
+
+/* ── Ambient Glow ── */
+.ambient-glow {
+  position: absolute;
+  border-radius: 50%;
+  filter: blur(60px);
+  pointer-events: none;
+  opacity: 0.4;
+}
+.ambient-glow-1 {
+  top: -30px;
+  right: -30px;
+  width: 120px;
+  height: 120px;
+  background: radial-gradient(circle, rgba(59, 130, 246, 0.3), transparent 70%);
+}
+.ambient-glow-2 {
+  bottom: -20px;
+  left: -20px;
+  width: 100px;
+  height: 100px;
+  background: radial-gradient(circle, rgba(139, 92, 246, 0.25), transparent 70%);
+}
+
+/* ── Header Icon Badge ── */
+.pipeline-icon-badge {
+  width: 36px;
+  height: 36px;
+  border-radius: 10px;
+  background: rgba(255, 255, 255, 0.06);
   border: 1px solid rgba(255, 255, 255, 0.08);
-  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
 }
 
+/* ── Pipeline Step ── */
+.pipeline-step {
+  display: flex;
+  align-items: flex-start;
+  gap: 12px;
+  padding: 10px 8px;
+  border-radius: 10px;
+  position: relative;
+  transition: all 0.3s ease;
+}
+.pipeline-step--active {
+  background: rgba(59, 130, 246, 0.08);
+  border: 1px solid rgba(59, 130, 246, 0.15);
+  margin: 0 -4px;
+  padding: 10px 12px;
+}
+.pipeline-step--completed {
+  opacity: 0.85;
+}
+
+/* ── Connector Line ── */
+.pipeline-connector {
+  position: absolute;
+  left: 21px;
+  top: 38px;
+  width: 2px;
+  height: calc(100% - 10px);
+  background: rgba(255, 255, 255, 0.06);
+  border-radius: 1px;
+  z-index: 1;
+}
+.pipeline-connector--completed {
+  background: linear-gradient(180deg, #3b82f6, #8b5cf6);
+}
+.pipeline-connector--active {
+  background: linear-gradient(180deg, #3b82f6 0%, rgba(59, 130, 246, 0.15) 100%);
+}
+
+/* ── Step Indicator ── */
+.pipeline-indicator {
+  width: 26px;
+  height: 26px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  position: relative;
+  z-index: 2;
+  margin-top: 1px;
+}
+.pipeline-step--completed .pipeline-indicator {
+  background: linear-gradient(135deg, #3b82f6, #8b5cf6);
+  box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
+}
+.pipeline-step--active .pipeline-indicator {
+  background: rgba(59, 130, 246, 0.15);
+  border: 2px solid #3b82f6;
+  box-shadow: 0 0 12px rgba(59, 130, 246, 0.25);
+}
+.pipeline-step--pending .pipeline-indicator {
+  background: rgba(255, 255, 255, 0.04);
+  border: 1.5px solid rgba(255, 255, 255, 0.1);
+}
+
+/* ── Active Pulse Dot ── */
+.pipeline-pulse {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: #3b82f6;
+  animation: pulse-ring 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+  box-shadow: 0 0 6px rgba(59, 130, 246, 0.6);
+}
+
+/* ── Step Content ── */
+.pipeline-step-content {
+  flex: 1;
+  min-width: 0;
+}
+
+/* ── Status Chip ── */
+.pipeline-status-chip {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 6px 12px;
+  border-radius: 8px;
+  background: rgba(59, 130, 246, 0.08);
+  border: 1px solid rgba(59, 130, 246, 0.12);
+  color: #93c5fd;
+}
+.pipeline-status-chip--done {
+  background: rgba(16, 185, 129, 0.08);
+  border-color: rgba(16, 185, 129, 0.15);
+  color: #6ee7b7;
+}
+.pipeline-status-chip--cancelled {
+  background: rgba(239, 68, 68, 0.08);
+  border-color: rgba(239, 68, 68, 0.15);
+  color: #fca5a5;
+}
+
+/* ── Navbar ── */
 .nav-glass {
-  background: rgba(40, 40, 40, 0.85);
+  background: rgba(30, 32, 38, 0.9);
   backdrop-filter: blur(20px);
-  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+  -webkit-backdrop-filter: blur(20px);
+  border: 1px solid rgba(255, 255, 255, 0.06);
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
+}
+.nav-action-btn {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 14px;
+  border-radius: 8px;
+  font-size: 13px;
+  font-weight: 500;
+  background: rgba(255, 255, 255, 0.04);
+  border: 1px solid rgba(255, 255, 255, 0.06);
+  transition: all 0.2s ease;
+  cursor: pointer;
+}
+.nav-action-btn:hover {
+  background: rgba(255, 255, 255, 0.08);
+  border-color: rgba(255, 255, 255, 0.1);
 }
 
+/* ── Result Buttons ── */
 .macos-btn-primary {
-  background: linear-gradient(180deg, #0A84FF 0%, #007AFF 100%);
+  background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
   color: white;
   border: 1px solid rgba(255, 255, 255, 0.1);
-  box-shadow: 0 1px 2px rgba(0,0,0,0.2), inset 0 1px 1px rgba(255,255,255,0.2);
+  box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3), inset 0 1px 1px rgba(255, 255, 255, 0.15);
   transition: all 0.2s;
 }
 .macos-btn-primary:hover {
-  filter: brightness(1.05);
-  transform: translateY(-0.5px);
-  box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3), inset 0 1px 1px rgba(255,255,255,0.2);
+  filter: brightness(1.08);
+  transform: translateY(-1px);
+  box-shadow: 0 6px 20px rgba(37, 99, 235, 0.35), inset 0 1px 1px rgba(255, 255, 255, 0.15);
 }
 .macos-btn-primary:active { transform: translateY(0.5px); filter: brightness(0.95); }
-.macos-btn-primary:disabled { opacity: 0.5; filter: grayscale(0.5); transform: none; cursor: not-allowed; }
 
+.result-btn-secondary {
+  background: rgba(255, 255, 255, 0.06);
+  color: white;
+  border: 1px solid rgba(255, 255, 255, 0.1);
+  backdrop-filter: blur(10px);
+  transition: all 0.2s;
+}
+.result-btn-secondary:hover {
+  background: rgba(255, 255, 255, 0.1);
+  border-color: rgba(255, 255, 255, 0.15);
+  transform: translateY(-1px);
+}
+
+/* ── Inline Player ── */
+.player-inline-card {
+  background: rgba(22, 24, 30, 0.7);
+  backdrop-filter: blur(20px);
+  border: 1px solid rgba(255, 255, 255, 0.06);
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
+}
+.audio-player {
+  opacity: 0.9;
+  border-radius: 8px;
+  transition: opacity 0.2s;
+}
+.audio-player:hover {
+  opacity: 1;
+}
+
+/* ── Animations ── */
 @keyframes spin-slow {
   from { transform: rotate(0deg); }
   to { transform: rotate(360deg); }
 }
-.animate-spin-slow { animation: spin-slow 3s linear infinite; }
+.animate-spin-slow { animation: spin-slow 2s linear infinite; }
+
+@keyframes pulse-ring {
+  0%, 100% { transform: scale(1); opacity: 1; }
+  50% { transform: scale(1.3); opacity: 0.7; }
+}
+
+@keyframes float {
+  0%, 100% { transform: translateY(0px); }
+  50% { transform: translateY(-3px); }
+}
+.animate-float { animation: float 2s ease-in-out infinite; }
+
+@keyframes shimmer {
+  0% { background-position: 200% 0; }
+  100% { background-position: -200% 0; }
+}
 </style>

+ 173 - 39
Co-creation-projects/JJason-DeepCastAgent/frontend/src/components/SetupView.vue

@@ -1,36 +1,72 @@
 <template>
-  <div class="min-h-screen flex items-center justify-center p-6">
-    <div class="w-full max-w-xl">
-      <div class="text-center mb-12">
-        <div class="text-6xl mb-6">🎙️</div>
-        <h1 class="text-6xl font-bold mb-4 text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-indigo-400 to-purple-500">DeepCast</h1>
-        <p class="text-xl text-gray-400">进行深度研究并转化为引人入胜的播客</p>
+  <div class="min-h-screen flex items-center justify-center p-6 relative overflow-hidden">
+    <!-- Background decorations -->
+    <div class="setup-bg-orb setup-bg-orb-1"></div>
+    <div class="setup-bg-orb setup-bg-orb-2"></div>
+    <div class="setup-bg-orb setup-bg-orb-3"></div>
+    <div class="setup-bg-grid"></div>
+
+    <div class="w-full max-w-xl relative z-10">
+      <!-- Brand -->
+      <div class="text-center mb-10">
+        <div class="inline-flex items-center justify-center w-20 h-20 rounded-[22px] bg-gradient-to-br from-blue-500 via-indigo-500 to-purple-600 shadow-2xl shadow-blue-500/20 mb-6 ring-1 ring-white/10">
+          <span class="text-4xl">🎙️</span>
+        </div>
+        <h1 class="text-5xl font-bold mb-3 text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-indigo-400 to-purple-400 tracking-tight">DeepCast</h1>
+        <p class="text-base text-gray-400 font-light">进行深度研究并转化为引人入胜的播客</p>
       </div>
 
-      <div class="card glass-panel rounded-2xl">
-        <form @submit.prevent="$emit('start', topic)" class="card-body p-8">
-          <div class="form-control mb-6">
+      <!-- Main Card -->
+      <div class="setup-card rounded-2xl">
+        <form @submit.prevent="$emit('start', topic)" class="p-7">
+          <!-- Input area -->
+          <div class="mb-5">
+            <label for="topic-input" class="block text-sm font-medium text-gray-300 mb-2.5 flex items-center gap-2">
+              <svg class="w-4 h-4 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
+              播客主题
+            </label>
             <textarea
+              id="topic-input"
               v-model="topic"
-              class="w-full textarea textarea-bordered h-32 text-lg leading-relaxed resize-none macos-input rounded-xl"
-              placeholder="💡 请输入播客主题(例如:AI Agent 的发展趋势)"
+              class="setup-textarea"
+              placeholder="请输入播客主题(例如:AI Agent 的发展趋势)"
               required
+              rows="4"
+              aria-label="播客主题输入"
               @keydown.enter.prevent="$emit('start', topic)"
             ></textarea>
           </div>
 
-          <div class="alert bg-blue-500/10 border border-blue-500/20 mb-8 rounded-xl">
-            <span class="text-sm text-blue-300 font-medium">🔍 使用混合搜索引擎 (Tavily + SerpApi)</span>
+          <!-- Feature badges -->
+          <div class="flex flex-wrap gap-2 mb-6">
+            <div class="setup-badge">
+              <svg class="w-3.5 h-3.5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
+              <span>混合搜索引擎</span>
+            </div>
+            <div class="setup-badge">
+              <svg class="w-3.5 h-3.5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
+              <span>深度 AI 研究</span>
+            </div>
+            <div class="setup-badge">
+              <svg class="w-3.5 h-3.5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"/></svg>
+              <span>自然语音合成</span>
+            </div>
           </div>
 
+          <!-- Submit button -->
           <button
-            class="btn btn-lg w-full font-semibold rounded-xl macos-btn-primary border-0"
+            class="setup-btn w-full"
             :disabled="!topic.trim()"
+            aria-label="开始制作播客"
           >
-            ✨ 开始制作播客
+            <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
+            开始制作播客
           </button>
         </form>
       </div>
+
+      <!-- Footer hint -->
+      <p class="text-center text-xs text-gray-600 mt-5">基于 HelloAgents 框架 · 自动化深度研究 → 播客生成</p>
     </div>
   </div>
 </template>
@@ -46,39 +82,137 @@ defineEmits<{
 </script>
 
 <style scoped>
-.glass-panel {
-  background: rgba(30, 30, 30, 0.7);
-  backdrop-filter: blur(25px);
-  -webkit-backdrop-filter: blur(25px);
-  border: 1px solid rgba(255, 255, 255, 0.08);
-  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
+/* ── Background Decorations ── */
+.setup-bg-orb {
+  position: absolute;
+  border-radius: 50%;
+  filter: blur(80px);
+  pointer-events: none;
+  opacity: 0.35;
+}
+.setup-bg-orb-1 {
+  top: 10%;
+  left: 15%;
+  width: 300px;
+  height: 300px;
+  background: radial-gradient(circle, rgba(59, 130, 246, 0.4), transparent 70%);
+  animation: float-slow 8s ease-in-out infinite;
+}
+.setup-bg-orb-2 {
+  bottom: 15%;
+  right: 10%;
+  width: 250px;
+  height: 250px;
+  background: radial-gradient(circle, rgba(139, 92, 246, 0.35), transparent 70%);
+  animation: float-slow 10s ease-in-out infinite reverse;
+}
+.setup-bg-orb-3 {
+  top: 50%;
+  left: 60%;
+  width: 200px;
+  height: 200px;
+  background: radial-gradient(circle, rgba(6, 182, 212, 0.25), transparent 70%);
+  animation: float-slow 12s ease-in-out infinite 2s;
+}
+.setup-bg-grid {
+  position: absolute;
+  inset: 0;
+  background-image:
+    linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
+  background-size: 60px 60px;
+  pointer-events: none;
 }
 
-.macos-input {
-  background: rgba(0, 0, 0, 0.2) !important;
-  border: 1px solid rgba(255, 255, 255, 0.1) !important;
-  color: #fff !important;
-  transition: all 0.3s ease;
+/* ── Main Card ── */
+.setup-card {
+  background: rgba(22, 24, 30, 0.8);
+  backdrop-filter: blur(30px);
+  -webkit-backdrop-filter: blur(30px);
+  border: 1px solid rgba(255, 255, 255, 0.06);
+  box-shadow:
+    0 25px 60px rgba(0, 0, 0, 0.4),
+    inset 0 1px 0 rgba(255, 255, 255, 0.05);
 }
-.macos-input:focus {
-  background: rgba(0, 0, 0, 0.4) !important;
-  border-color: #0A84FF !important;
-  box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.2);
+
+/* ── Textarea ── */
+.setup-textarea {
+  width: 100%;
+  min-height: 110px;
+  padding: 14px 16px;
+  border-radius: 12px;
+  font-size: 15px;
+  line-height: 1.6;
+  resize: none;
+  color: #e5e7eb;
+  background: rgba(0, 0, 0, 0.25);
+  border: 1px solid rgba(255, 255, 255, 0.08);
+  transition: all 0.25s ease;
   outline: none;
+  font-family: inherit;
+}
+.setup-textarea::placeholder {
+  color: rgba(156, 163, 175, 0.5);
+}
+.setup-textarea:focus {
+  background: rgba(0, 0, 0, 0.35);
+  border-color: rgba(59, 130, 246, 0.5);
+  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12), 0 2px 8px rgba(0, 0, 0, 0.2);
+}
+
+/* ── Feature Badge ── */
+.setup-badge {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 5px 12px;
+  border-radius: 8px;
+  font-size: 12px;
+  font-weight: 500;
+  color: #9ca3af;
+  background: rgba(255, 255, 255, 0.04);
+  border: 1px solid rgba(255, 255, 255, 0.06);
 }
 
-.macos-btn-primary {
-  background: linear-gradient(180deg, #0A84FF 0%, #007AFF 100%);
+/* ── Submit Button ── */
+.setup-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 14px 24px;
+  border-radius: 12px;
+  font-size: 16px;
+  font-weight: 600;
   color: white;
+  background: linear-gradient(135deg, #3b82f6 0%, #6366f1 50%, #8b5cf6 100%);
   border: 1px solid rgba(255, 255, 255, 0.1);
-  box-shadow: 0 1px 2px rgba(0,0,0,0.2), inset 0 1px 1px rgba(255,255,255,0.2);
-  transition: all 0.2s;
+  box-shadow:
+    0 4px 14px rgba(59, 130, 246, 0.3),
+    inset 0 1px 1px rgba(255, 255, 255, 0.15);
+  cursor: pointer;
+  transition: all 0.25s ease;
 }
-.macos-btn-primary:hover {
+.setup-btn:hover:not(:disabled) {
+  transform: translateY(-1px);
+  box-shadow:
+    0 8px 25px rgba(59, 130, 246, 0.35),
+    inset 0 1px 1px rgba(255, 255, 255, 0.15);
   filter: brightness(1.05);
-  transform: translateY(-0.5px);
-  box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3), inset 0 1px 1px rgba(255,255,255,0.2);
 }
-.macos-btn-primary:active { transform: translateY(0.5px); filter: brightness(0.95); }
-.macos-btn-primary:disabled { opacity: 0.5; filter: grayscale(0.5); transform: none; cursor: not-allowed; }
+.setup-btn:active:not(:disabled) {
+  transform: translateY(0.5px);
+  filter: brightness(0.95);
+}
+.setup-btn:disabled {
+  opacity: 0.4;
+  cursor: not-allowed;
+  filter: grayscale(0.4);
+}
+
+/* ── Animations ── */
+@keyframes float-slow {
+  0%, 100% { transform: translate(0, 0); }
+  50% { transform: translate(20px, -15px); }
+}
 </style>

+ 181 - 55
Co-creation-projects/JJason-DeepCastAgent/frontend/src/components/TerminalLog.vue

@@ -1,37 +1,50 @@
 <template>
-  <div class="macos-terminal rounded-xl shadow-2xl overflow-hidden" style="height: 500px;">
+  <div class="term-window rounded-2xl overflow-hidden" style="height: 500px;">
     <!-- macOS Title Bar -->
-    <div class="macos-titlebar bg-gradient-to-b from-[#3d3d3d] to-[#2d2d2d] px-4 py-3 flex items-center shrink-0 border-b border-[#1a1a1a]">
+    <div class="term-titlebar">
       <!-- Traffic Lights -->
-      <div class="flex items-center gap-2 mr-4">
-        <div class="w-3 h-3 rounded-full bg-[#ff5f57] shadow-inner hover:brightness-110 cursor-pointer transition-all" title="关闭"></div>
-        <div class="w-3 h-3 rounded-full bg-[#febc2e] shadow-inner hover:brightness-110 cursor-pointer transition-all" title="最小化"></div>
-        <div class="w-3 h-3 rounded-full bg-[#28c840] shadow-inner hover:brightness-110 cursor-pointer transition-all" title="最大化"></div>
+      <div class="flex items-center gap-[7px] mr-4">
+        <div class="term-dot term-dot-red" title="关闭">
+          <svg class="term-dot-icon" viewBox="0 0 12 12"><path d="M3.5 3.5l5 5M8.5 3.5l-5 5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
+        </div>
+        <div class="term-dot term-dot-yellow" title="最小化">
+          <svg class="term-dot-icon" viewBox="0 0 12 12"><path d="M2.5 6h7" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
+        </div>
+        <div class="term-dot term-dot-green" title="最大化">
+          <svg class="term-dot-icon" viewBox="0 0 12 12"><path d="M3.5 8.5V5a1 1 0 011-1H8M8.5 3.5V7a1 1 0 01-1 1H4" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/></svg>
+        </div>
       </div>
       <!-- Title -->
       <div class="flex-1 text-center">
-        <span class="text-[#9a9a9a] text-sm font-medium tracking-wide">deepcast — zsh — {{ logs.length }} lines</span>
+        <div class="inline-flex items-center gap-2 px-3 py-0.5 rounded-md bg-white/[0.03]">
+          <svg class="w-3 h-3 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
+          <span class="text-[13px] font-medium text-gray-500 tracking-wide">deepcast — {{ logs.length }} lines</span>
+        </div>
       </div>
       <!-- Placeholder for symmetry -->
       <div class="w-16"></div>
     </div>
     <!-- Terminal Content -->
-    <div class="bg-[#1e1e1e] overflow-y-auto p-4 flex-1 font-mono text-sm custom-scrollbar terminal-content" ref="logContainer" style="height: calc(100% - 44px);">
+    <div class="term-body custom-scrollbar" ref="logContainer" style="height: calc(100% - 48px);">
       <!-- Welcome Message -->
-      <div v-if="logs.length === 0 && !isWaiting" class="text-[#6a9955] mb-2">
-        <span class="text-[#569cd6]">deepcast</span><span class="text-[#d4d4d4]">@</span><span class="text-[#4ec9b0]">studio</span> <span class="text-[#d4d4d4]">~</span> <span class="text-[#dcdcaa]">ready</span>
+      <div v-if="logs.length === 0 && !isWaiting" class="term-welcome">
+        <span class="term-prompt-user">deepcast</span><span class="text-gray-500">@</span><span class="term-prompt-host">studio</span>
+        <span class="text-gray-500 mx-1">~</span>
+        <span class="term-prompt-cmd">ready</span>
+        <span class="term-cursor"></span>
       </div>
       <!-- Log Entries -->
-      <div v-for="(log, i) in logs" :key="i" class="mb-1 leading-relaxed" :class="getLogClass(log.message)">
-        <span class="text-[#6a6a6a] mr-2 text-xs select-none">[{{ log.time }}]</span>
-        <span class="terminal-text">{{ log.message }}</span>
+      <div v-for="(log, i) in logs" :key="i" class="term-line" :class="getLogClass(log.message)">
+        <span class="term-time">[{{ log.time }}]</span>
+        <span class="term-text">{{ log.message }}</span>
       </div>
       <!-- Waiting States -->
-      <div v-if="isWaiting && logs.length === 0" class="text-[#dcdcaa] text-center mt-8">
-        <span class="inline-block animate-pulse">⏳ 正在初始化...</span>
+      <div v-if="isWaiting && logs.length === 0" class="term-waiting-init">
+        <div class="term-spinner"></div>
+        <span>正在初始化...</span>
       </div>
-      <div v-else-if="isWaiting" class="text-[#dcdcaa] mt-2 flex items-center gap-2">
-        <span class="inline-block w-2 h-4 bg-[#569cd6] animate-blink"></span>
+      <div v-else-if="isWaiting" class="term-waiting">
+        <span class="term-cursor"></span>
         <span>处理中{{ waitingDots }}</span>
       </div>
     </div>
@@ -54,7 +67,6 @@ defineProps<{
 
 const logContainer = ref<HTMLElement | null>(null);
 
-/** 当 logs 变化时自动滚动到底部 */
 watch(
   () => logContainer.value,
   () => {},
@@ -72,58 +84,172 @@ function scrollToBottom() {
 }
 
 function getLogClass(message: string): string {
-  if (message.includes("[STAGE]")) return "terminal-stage";
-  if (message.includes("[TASK")) return "terminal-info";
-  if (message.includes("[TOOL]")) return "terminal-tool";
-  if (message.includes("[SOURCES]")) return "terminal-warning";
-  if (message.includes("✅") || message.includes("status=completed")) return "terminal-success";
-  if (message.includes("❌") || message.includes("ERROR") || message.includes("failed")) return "terminal-error";
-  if (message.includes("⚠️") || message.includes("WARNING")) return "terminal-warning";
-  if (message.includes("INFO:")) return "terminal-muted";
-  if (message.includes("━")) return "terminal-divider";
-  return "terminal-default";
+  if (message.includes("[STAGE]")) return "term-line--stage";
+  if (message.includes("[TASK")) return "term-line--info";
+  if (message.includes("[TOOL]")) return "term-line--tool";
+  if (message.includes("[SOURCES]")) return "term-line--warning";
+  if (message.includes("✅") || message.includes("status=completed")) return "term-line--success";
+  if (message.includes("❌") || message.includes("ERROR") || message.includes("failed")) return "term-line--error";
+  if (message.includes("⚠️") || message.includes("WARNING")) return "term-line--warning";
+  if (message.includes("INFO:")) return "term-line--muted";
+  if (message.includes("━")) return "term-line--divider";
+  return "term-line--default";
 }
 </script>
 
 <style scoped>
-.macos-terminal {
-  background: #1e1e1e;
-  border: 1px solid #3d3d3d;
+/* ── Terminal Window ── */
+.term-window {
+  background: #161618;
+  border: 1px solid rgba(255, 255, 255, 0.08);
   box-shadow:
-    0 22px 70px 4px rgba(0, 0, 0, 0.56),
-    0 0 0 1px rgba(0, 0, 0, 0.3);
+    0 25px 60px rgba(0, 0, 0, 0.5),
+    0 0 0 1px rgba(0, 0, 0, 0.3),
+    inset 0 1px 0 rgba(255, 255, 255, 0.04);
 }
 
-.macos-titlebar {
-  -webkit-app-region: drag;
+/* ── Title Bar ── */
+.term-titlebar {
+  display: flex;
+  align-items: center;
+  padding: 12px 16px;
+  background: linear-gradient(180deg, rgba(50, 52, 58, 0.95), rgba(38, 40, 46, 0.95));
+  border-bottom: 1px solid rgba(0, 0, 0, 0.4);
   user-select: none;
+  -webkit-app-region: drag;
 }
 
-.terminal-content {
-  font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;
+/* ── Traffic Light Dots ── */
+.term-dot {
+  width: 12px;
+  height: 12px;
+  border-radius: 50%;
+  cursor: pointer;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: filter 0.15s;
+}
+.term-dot:hover { filter: brightness(1.15); }
+.term-dot-icon {
+  width: 8px;
+  height: 8px;
+  color: rgba(0, 0, 0, 0.5);
+  opacity: 0;
+  transition: opacity 0.15s;
+}
+.term-titlebar:hover .term-dot-icon { opacity: 1; }
+.term-dot-red {
+  background: #ff5f57;
+  box-shadow: inset 0 -1px 1px rgba(0,0,0,0.15), 0 1px 2px rgba(255,95,87,0.2);
+}
+.term-dot-yellow {
+  background: #febc2e;
+  box-shadow: inset 0 -1px 1px rgba(0,0,0,0.15), 0 1px 2px rgba(254,188,46,0.2);
+}
+.term-dot-green {
+  background: #28c840;
+  box-shadow: inset 0 -1px 1px rgba(0,0,0,0.15), 0 1px 2px rgba(40,200,64,0.2);
+}
+
+/* ── Terminal Body ── */
+.term-body {
+  padding: 16px 18px;
+  overflow-y: auto;
+  font-family: 'SF Mono', 'Monaco', 'Menlo', 'Cascadia Code', 'Consolas', monospace;
   font-size: 13px;
-  line-height: 1.6;
+  line-height: 1.7;
+  color: #d4d4d8;
+  background:
+    linear-gradient(180deg, rgba(22, 22, 24, 1) 0%, rgba(18, 18, 20, 1) 100%);
 }
 
-.terminal-stage { color: #569cd6; font-weight: 600; padding-bottom: 2px; margin-bottom: 2px; }
-.terminal-info { color: #4fc1ff; }
-.terminal-tool { color: #c586c0; }
-.terminal-success { color: #4ec9b0; }
-.terminal-error { color: #f14c4c; }
-.terminal-warning { color: #dcdcaa; }
-.terminal-muted { color: #6a9955; }
-.terminal-divider { color: #3d3d3d; opacity: 0.8; }
-.terminal-default { color: #d4d4d4; }
+/* ── Welcome Prompt ── */
+.term-welcome { margin-bottom: 4px; }
+.term-prompt-user { color: #60a5fa; font-weight: 600; }
+.term-prompt-host { color: #34d399; }
+.term-prompt-cmd { color: #fbbf24; }
 
-@keyframes blink {
-  0%, 50% { opacity: 1; }
-  51%, 100% { opacity: 0; }
+/* ── Cursor ── */
+.term-cursor {
+  display: inline-block;
+  width: 8px;
+  height: 16px;
+  background: #60a5fa;
+  margin-left: 4px;
+  vertical-align: text-bottom;
+  animation: cursor-blink 1s step-end infinite;
 }
-.animate-blink { animation: blink 1s step-end infinite; }
 
-.custom-scrollbar::-webkit-scrollbar { width: 8px; height: 8px; }
+/* ── Log Lines ── */
+.term-line {
+  margin-bottom: 2px;
+  padding: 1px 0;
+  transition: background 0.15s;
+}
+.term-line:hover {
+  background: rgba(255, 255, 255, 0.02);
+  border-radius: 4px;
+}
+.term-time {
+  color: rgba(113, 113, 122, 0.7);
+  font-size: 11px;
+  margin-right: 10px;
+  user-select: none;
+  font-variant-numeric: tabular-nums;
+}
+
+/* ── Log Colors ── */
+.term-line--stage { color: #60a5fa; font-weight: 600; }
+.term-line--stage .term-time { color: rgba(96, 165, 250, 0.5); }
+.term-line--info { color: #67e8f9; }
+.term-line--tool { color: #c084fc; }
+.term-line--success { color: #34d399; }
+.term-line--error { color: #f87171; }
+.term-line--warning { color: #fbbf24; }
+.term-line--muted { color: #6b7280; }
+.term-line--divider { color: rgba(63, 63, 70, 0.6); font-size: 12px; letter-spacing: -1px; }
+.term-line--default { color: #d4d4d8; }
+
+/* ── Waiting States ── */
+.term-waiting-init {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 10px;
+  color: #fbbf24;
+  padding-top: 32px;
+}
+.term-waiting {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  color: #fbbf24;
+  margin-top: 6px;
+}
+.term-spinner {
+  width: 14px;
+  height: 14px;
+  border: 2px solid rgba(251, 191, 36, 0.3);
+  border-top-color: #fbbf24;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+}
+
+/* ── Scrollbar ── */
+.custom-scrollbar::-webkit-scrollbar { width: 6px; }
 .custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
-.custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); border-radius: 4px; }
-.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.25); }
-.terminal-content:not(:hover)::-webkit-scrollbar-thumb { background: transparent; }
+.custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.08); border-radius: 3px; }
+.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.15); }
+.term-body:not(:hover)::-webkit-scrollbar-thumb { background: transparent; }
+
+/* ── Animations ── */
+@keyframes cursor-blink {
+  0%, 50% { opacity: 1; }
+  51%, 100% { opacity: 0; }
+}
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
 </style>

+ 38 - 0
Co-creation-projects/JJason-DeepCastAgent/frontend/src/style.css

@@ -1,2 +1,40 @@
 @import "tailwindcss";
 @plugin "daisyui";
+
+/* ── Global Base ── */
+html {
+  color-scheme: dark;
+}
+
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+/* ── Global Scrollbar ── */
+::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+::-webkit-scrollbar-track {
+  background: transparent;
+}
+::-webkit-scrollbar-thumb {
+  background: rgba(255, 255, 255, 0.08);
+  border-radius: 3px;
+}
+::-webkit-scrollbar-thumb:hover {
+  background: rgba(255, 255, 255, 0.15);
+}
+
+/* ── Selection Color ── */
+::selection {
+  background: rgba(59, 130, 246, 0.3);
+  color: #fff;
+}
+
+/* ── Audio element styling ── */
+audio::-webkit-media-controls-panel {
+  background: rgba(30, 30, 30, 0.8);
+}