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

feat:为播客创作工作流新增播放器视图、制作视图、设置视图和终端日志组件

• 实现播放器视图,用于音频播放和报告展示。

• 创建制作视图,用于管理制作阶段和日志。

• 开发设置视图,用于收集用户输入的播客主题。

• 引入终端日志组件,以macOS风格终端界面显示日志。

• 通过毛玻璃面板效果和响应式设计增强用户界面。
JJSun 3 месяцев назад
Родитель
Сommit
60e91b77ad

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

@@ -33,4 +33,5 @@ NO_PROXY=chat.ecnu.edu.cn,api.longcat.chat,open.bigmodel.cn
 # 服务器配置
 HOST=0.0.0.0
 PORT=8000
-CORS_ORIGINS=http://localhost:5173,http://localhost:3000
+LOG_LEVEL=INFO
+CORS_ORIGINS=http://localhost:5173,http://localhost:5174,http://localhost:3000

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

@@ -127,10 +127,35 @@ class Configuration(BaseModel):
         description="SerpApi 的 API 密钥",
     )
     cors_origins: str = Field(
-        default="http://localhost:5173,http://localhost:3000",
+        default="http://localhost:5173,http://localhost:5174,http://localhost:3000",
         title="CORS 允许的源",
         description="逗号分隔的允许跨域请求的源列表",
     )
+    host: str = Field(
+        default="0.0.0.0",
+        title="服务器主机",
+        description="FastAPI 服务器监听的主机地址",
+    )
+    port: int = Field(
+        default=8000,
+        title="服务器端口",
+        description="FastAPI 服务器监听的端口",
+    )
+    log_level: str = Field(
+        default="INFO",
+        title="日志级别",
+        description="日志记录级别 (DEBUG, INFO, WARNING, ERROR)",
+    )
+    llm_timeout: int = Field(
+        default=60,
+        title="LLM 超时",
+        description="LLM 请求超时时间(秒)",
+    )
+    tts_timeout: int = Field(
+        default=300,
+        title="TTS 超时",
+        description="TTS 请求超时时间(秒)",
+    )
 
     @field_validator("notes_workspace", "audio_output_dir")
     @classmethod

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

@@ -267,6 +267,7 @@ def create_app() -> FastAPI:
                         if agent.is_cancelled():
                             logger.info("✅ 本次任务已取消(超时检测)")
                             yield 'data: {"type": "cancelled", "message": "研究任务已被用户取消"}\n\n'
+                            break
                         continue
 
                     # 哨兵:生成器已结束
@@ -306,10 +307,11 @@ app = create_app()
 if __name__ == "__main__":
     import uvicorn
 
+    _config = Configuration.from_env()
     uvicorn.run(
         "main:app",
-        host="0.0.0.0",
-        port=8000,
+        host=_config.host,
+        port=_config.port,
         reload=True,
-        log_level="info"
+        log_level=_config.log_level.lower(),
     )

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

@@ -164,7 +164,7 @@ class AudioGenerationService:
         try:
             logger.debug("Calling TTS API for voice %s: %s...", voice, text[:20])
             # Use configurable timeout if available; default to 300 seconds for robustness.
-            timeout = getattr(self._config, "tts_timeout", 300)
+            timeout = self._config.tts_timeout
             response = requests.post(
                 self._config.tts_base_url,
                 json=payload,

+ 12 - 7
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/search.py

@@ -3,6 +3,7 @@
 from __future__ import annotations
 
 import logging
+import threading
 from typing import Any
 
 from hello_agents.tools import SearchTool
@@ -17,18 +18,22 @@ from utils import (
 logger = logging.getLogger(__name__)
 
 MAX_TOKENS_PER_SOURCE = 2000
-_GLOBAL_SEARCH_TOOL = None
+_GLOBAL_SEARCH_TOOL: SearchTool | None = None
+_SEARCH_TOOL_LOCK = threading.Lock()
 
 
 def get_global_search_tool(config: Configuration) -> SearchTool:
-    """使用 API 密钥延迟初始化全局搜索工具。"""
+    """使用 API 密钥延迟初始化全局搜索工具(线程安全)。"""
     global _GLOBAL_SEARCH_TOOL
     if _GLOBAL_SEARCH_TOOL is None:
-        _GLOBAL_SEARCH_TOOL = SearchTool(
-            backend="hybrid",
-            tavily_key=config.tavily_api_key,
-            serpapi_key=config.serpapi_api_key,
-        )
+        with _SEARCH_TOOL_LOCK:
+            # 双重检查锁定,避免多线程重复创建
+            if _GLOBAL_SEARCH_TOOL is None:
+                _GLOBAL_SEARCH_TOOL = SearchTool(
+                    backend="hybrid",
+                    tavily_key=config.tavily_api_key,
+                    serpapi_key=config.serpapi_api_key,
+                )
     return _GLOBAL_SEARCH_TOOL
 
 

+ 54 - 491
Co-creation-projects/JJason-DeepCastAgent/frontend/src/App.vue

@@ -1,295 +1,63 @@
-<template>
+<template>
   <div class="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
-    
     <!-- View 1: Setup -->
-    <div v-if="currentView === 'setup'" 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>
-          
-        <div class="card glass-panel rounded-2xl">
-          <form @submit.prevent="startProduction" class="card-body p-8">
-            <div class="form-control mb-6">
-              <textarea 
-                v-model="form.topic" 
-                class="w-full textarea textarea-bordered h-32 text-lg leading-relaxed resize-none macos-input rounded-xl" 
-                placeholder="💡 请输入播客主题(例如:AI Agent 的发展趋势)"
-                required
-                @keydown.enter.prevent="startProduction"></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>
-            </div>
-
-            <button 
-              class="btn btn-lg w-full font-semibold rounded-xl macos-btn-primary border-0" 
-              :disabled="!form.topic.trim()">
-              ✨ 开始制作播客
-            </button>
-          </form>
-        </div>
-      </div>
-    </div>
+    <SetupView
+      v-if="currentView === 'setup'"
+      v-model:topic="form.topic"
+      @start="startProduction"
+    />
 
     <!-- View 2: Production -->
-    <div v-else-if="currentView === 'producing'" class="min-h-screen 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="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>
-          <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="downloadReport">
-              📄 下载研究报告
-            </button>
-            <button v-if="!podcastReady" class="btn btn-ghost btn-sm text-red-400 hover:bg-white/5" @click="cancelProduction">
-              取消制作
-            </button>
-          </div>
-        </div>
-      </div>
-
-      <!-- Main Content -->
-      <div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
-        
-        <!-- 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>
-                <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>
-                    </li>
-                  </ul>
-              </div>
-            </div>
-          </div>
-        </div>
-
-        <!-- Right Column: Logs & Output -->
-        <div class="lg:col-span-3 flex flex-col gap-4">
-          
-          <!-- macOS Style Terminal -->
-          <div class="macos-terminal rounded-xl shadow-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]">
-                <!-- 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>
-                <!-- Title -->
-                <div class="flex-1 text-center">
-                   <span class="text-[#9a9a9a] text-sm font-medium tracking-wide">deepcast — zsh — {{ logs.length }} lines</span>
-                </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);">
-                <!-- 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>
-                <!-- 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>
-                <!-- Waiting States -->
-                <div v-if="isWaiting && logs.length === 0" class="text-[#dcdcaa] text-center mt-8">
-                  <span class="inline-block animate-pulse">⏳ 正在初始化...</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>
-                  <span>处理中{{ waitingDots }}</span>
-                </div>
-             </div>
-          </div>
-
-          <!-- 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
-               </a>
-               <button class="btn glass text-white flex-1 btn-lg text-lg rounded-xl" @click="currentView = 'player'">
-                  🎧 进入播放器
-               </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>
-               <audio class="w-full opacity-90 hover:opacity-100 transition-opacity" :src="audioUrl" controls></audio>
-             </div>
-           </div>
-
-        </div>
-      </div>
-    </div>
-    </div>
+    <ProductionView
+      v-else-if="currentView === 'producing'"
+      ref="productionRef"
+      :logs="logs"
+      :is-waiting="isWaiting"
+      :waiting-dots="waitingDots"
+      :production-stage="productionStage"
+      :report-ready="reportReady"
+      :podcast-ready="podcastReady"
+      :audio-url="audioUrl"
+      @cancel="cancelProduction"
+      @download-report="downloadReport"
+      @go-player="currentView = 'player'"
+    />
 
     <!-- View 3: Player -->
-    <div v-else-if="currentView === 'player'" 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="downloadReport">下载</button>
-                </div>
-              </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="md.render(reportMarkdown)"></article>
-              </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>
-                   </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">{{ form.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 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="resetApp">
-                  🪄 制作新播客
-                </button>
-              </div>
-            </div>
-         </div>
-      </div>
-    </div>
-
+    <PlayerView
+      v-else-if="currentView === 'player'"
+      :topic="form.topic"
+      :audio-url="audioUrl"
+      :report-markdown="reportMarkdown"
+      @reset="resetApp"
+      @download-report="downloadReport"
+    />
   </div>
 </template>
 
 <script lang="ts" setup>
 import { reactive, ref, nextTick } from "vue";
 import { runResearchStream, cancelResearch, type ResearchStreamEvent } from "./services/api";
-import MarkdownIt from "markdown-it";
 
-// Markdown renderer
-const md = new MarkdownIt();
+import SetupView from "./components/SetupView.vue";
+import ProductionView from "./components/ProductionView.vue";
+import PlayerView from "./components/PlayerView.vue";
+import type { LogEntry } from "./components/TerminalLog.vue";
+import type { ProductionStage } from "./components/ProductionView.vue";
 
 // --- Types ---
 type ViewState = "setup" | "producing" | "player";
-type ProductionStage = "research" | "script" | "audio" | "done";
-
-interface LogEntry {
-  time: string;
-  message: string;
-}
 
 // --- State ---
 const currentView = ref<ViewState>("setup");
 const productionStage = ref<ProductionStage>("research");
-const form = reactive({
-  topic: ""
-});
+const form = reactive({ topic: "" });
 
 const logs = ref<LogEntry[]>([]);
-const isPlaying = ref(false);
 const reportReady = ref(false);
 const podcastReady = ref(false);
 
-const audioProgress = reactive({
-  current: 0,
-  total: 0,
-  role: ""
-});
-
+const audioProgress = reactive({ current: 0, total: 0, role: "" });
 const currentStatusMessage = ref("");
 const isWaiting = ref(false);
 const waitingDots = ref(".");
@@ -298,10 +66,10 @@ let waitingInterval: ReturnType<typeof setInterval> | null = null;
 const reportMarkdown = ref("");
 const audioUrl = ref("");
 
-const audioPlayer = ref<HTMLAudioElement | null>(null);
-const logContainer = ref<HTMLElement | null>(null);
 let abortController: AbortController | null = null;
 
+const productionRef = ref<InstanceType<typeof ProductionView> | null>(null);
+
 // --- Helpers ---
 
 function startWaitingAnimation() {
@@ -321,37 +89,11 @@ function stopWaitingAnimation() {
   }
 }
 
-function getLogClass(message: string): string {
-  // macOS Terminal style colors
-  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";
-}
-
-function getStepClass(step: ProductionStage) {
-  const stepsOrder = ["research", "script", "audio", "done"];
-  const currentIdx = stepsOrder.indexOf(productionStage.value);
-  const stepIdx = stepsOrder.indexOf(step);
-  
-  if (currentIdx > stepIdx) return "step-primary"; // Completed
-  if (currentIdx === stepIdx) return "step-primary font-bold"; // Active
-  return "";
-}
-
 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;
-    }
+    productionRef.value?.scrollTerminal();
   });
 }
 
@@ -360,7 +102,6 @@ function addLog(message: string) {
 async function startProduction() {
   if (!form.topic.trim()) return;
 
-  // Reset State
   currentView.value = "producing";
   productionStage.value = "research";
   logs.value = [];
@@ -399,15 +140,11 @@ async function startProduction() {
 function handleStreamEvent(event: ResearchStreamEvent) {
   console.log("Event:", event.type, event);
 
-  // 1. Log Event
   if (event.type === "log") {
     const msg = String((event as any).message || "");
-    // 去掉可能的颜色代码如果后端没去掉
     const cleanMsg = msg.replace(/\u001b\[\d+m/g, "");
     addLog(`INFO: ${cleanMsg}`);
-    
-    // 从日志中解析 TTS 进度 (作为备份机制)
-    // 格式: [TTS 6/13] ✓ Host 语音生成成功
+
     const ttsMatch = cleanMsg.match(/\[TTS (\d+)\/(\d+)\]/);
     if (ttsMatch) {
       audioProgress.current = parseInt(ttsMatch[1], 10);
@@ -417,29 +154,25 @@ function handleStreamEvent(event: ResearchStreamEvent) {
     return;
   }
 
-  // 2. Stage Change
   if (event.type === "stage_change") {
     const payload = event as any;
     const stage = payload.stage;
     const message = payload.message || "";
     currentStatusMessage.value = message;
-    
-    addLog(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
+
+    addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
     addLog(`📌 [STAGE] ${stage.toUpperCase()} - ${message}`);
-    addLog(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
-    
+    addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
+
     if (stage === "report") productionStage.value = "research";
     else if (stage === "script") productionStage.value = "script";
     else if (stage === "audio") productionStage.value = "audio";
-    // Backend distinguishes a separate "synthesis" stage for final audio stitching,
-    // but the UI groups it under the overall "audio" production stage for simplicity.
     else if (stage === "synthesis") productionStage.value = "audio";
   }
 
-  // 3. Task / Tool Updates (Simplified logging)
   if (event.type === "tool_call") {
     const p = event as any;
-    addLog(`🔧 [TOOL] ${p.tool} - ${p.agent || 'Agent'}`);
+    addLog(`🔧 [TOOL] ${p.tool} - ${p.agent || "Agent"}`);
   }
 
   if (event.type === "task_status") {
@@ -453,26 +186,23 @@ function handleStreamEvent(event: ResearchStreamEvent) {
     }
   }
 
-  // 4. Report Ready
   if (event.type === "final_report") {
     reportMarkdown.value = String((event as any).report);
     reportReady.value = true;
-    addLog(`📄 [REPORT] 报告已生成`);
+    addLog("📄 [REPORT] 报告已生成");
   }
 
-  // 5. Script Ready
   if (event.type === "podcast_script") {
     productionStage.value = "audio";
-    addLog(`🎙️ [SCRIPT] 剧本已生成`);
+    addLog("🎙️ [SCRIPT] 剧本已生成");
   }
 
-  // 6. Audio Progress
   if (event.type === "audio_start") {
     const p = event as any;
     audioProgress.total = p.total || 0;
     addLog(`🎵 [AUDIO] 开始生成音频, 共 ${audioProgress.total} 段`);
   }
-  
+
   if (event.type === "audio_progress") {
     const p = event as any;
     audioProgress.current = p.current;
@@ -480,7 +210,6 @@ function handleStreamEvent(event: ResearchStreamEvent) {
     currentStatusMessage.value = `生成音频: ${p.role} (${p.current}/${p.total})`;
   }
 
-  // 7. Podcast Ready
   if (event.type === "podcast_ready") {
     const p = event as any;
     const filename = String(p.file).split(/[\\/]/).pop();
@@ -495,16 +224,13 @@ function handleStreamEvent(event: ResearchStreamEvent) {
     }
   }
 
-  // 8. Done (Catch-all)
   if (event.type === "done") {
-    addLog(`✅ [DONE] 所有任务结束`);
+    addLog("✅ [DONE] 所有任务结束");
     stopWaitingAnimation();
     productionStage.value = "done";
-    
-    // 如果没有收到 podcast_ready 事件,尝试获取最新的音频文件
+
     if (!podcastReady.value && audioProgress.total > 0) {
       const baseUrl = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
-      // 尝试从后端获取最新的音频文件
       fetch(`${baseUrl}/api/audio/latest`)
         .then(res => res.json())
         .then(data => {
@@ -515,7 +241,7 @@ function handleStreamEvent(event: ResearchStreamEvent) {
             addLog(`🎉 [PODCAST] 找到音频文件: ${data.file}`);
           } else {
             currentStatusMessage.value = "任务完成(音频未生成)";
-            addLog(`⚠️ 未找到音频文件: ${data.error || '未知错误'}`);
+            addLog(`⚠️ 未找到音频文件: ${data.error || "未知错误"}`);
           }
         })
         .catch(err => {
@@ -533,7 +259,6 @@ function handleStreamEvent(event: ResearchStreamEvent) {
 function cancelProduction() {
   if (confirm("确定要取消制作吗?")) {
     addLog("🛑 用户请求取消制作...");
-    // 先通知后端停止,再断开 SSE 连接
     cancelResearch().then(() => {
       addLog("✅ 后端已接收取消请求");
     });
@@ -543,8 +268,7 @@ function cancelProduction() {
     }
     stopWaitingAnimation();
     productionStage.value = "done";
-    
-    // 给一点时间让状态重置
+
     setTimeout(() => {
       currentView.value = "setup";
       currentStatusMessage.value = "";
@@ -555,7 +279,6 @@ function cancelProduction() {
 function resetApp() {
   currentView.value = "setup";
   form.topic = "";
-  isPlaying.value = false;
   currentStatusMessage.value = "";
   reportReady.value = false;
   podcastReady.value = false;
@@ -565,174 +288,14 @@ function resetApp() {
 
 function downloadReport() {
   if (!reportMarkdown.value) return;
-  const blob = new Blob([reportMarkdown.value], { type: 'text/markdown;charset=utf-8' });
+  const blob = new Blob([reportMarkdown.value], { type: "text/markdown;charset=utf-8" });
   const url = URL.createObjectURL(blob);
-  const a = document.createElement('a');
+  const a = document.createElement("a");
   a.href = url;
-  a.download = `DeepCast深度研究报告.md`;
+  a.download = "DeepCast深度研究报告.md";
   document.body.appendChild(a);
   a.click();
   document.body.removeChild(a);
   URL.revokeObjectURL(url);
 }
 </script>
-
-<style scoped>
-/* macOS Terminal Styles */
-.macos-terminal {
-  background: #1e1e1e;
-  border: 1px solid #3d3d3d;
-  box-shadow: 
-    0 22px 70px 4px rgba(0, 0, 0, 0.56),
-    0 0 0 1px rgba(0, 0, 0, 0.3);
-}
-
-.macos-titlebar {
-  -webkit-app-region: drag;
-  user-select: none;
-}
-
-.terminal-content {
-  font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;
-  font-size: 13px;
-  line-height: 1.6;
-}
-
-/* Terminal Color Classes - VS Code Dark+ inspired */
-.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;
-}
-
-/* Blinking cursor animation */
-@keyframes blink {
-  0%, 50% { opacity: 1; }
-  51%, 100% { opacity: 0; }
-}
-
-.animate-blink {
-  animation: blink 1s step-end infinite;
-}
-
-/* Custom Scrollbar for log and report - macOS style */
-.custom-scrollbar::-webkit-scrollbar {
-  width: 8px;
-  height: 8px;
-}
-.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);
-}
-
-/* Hide scrollbar when not hovering (macOS behavior) */
-.terminal-content:not(:hover)::-webkit-scrollbar-thumb {
-  background: transparent;
-}
-
-/* Animation for spinning loader */
-@keyframes spin-slow {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}
-
-.animate-spin-slow {
-  animation: spin-slow 3s linear infinite;
-}
-
-/* macOS / Glassmorphism Design System */
-.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);
-}
-
-.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;
-}
-
-.macos-input:focus {
-  background: rgba(0, 0, 0, 0.4) !important;
-  border-color: #0A84FF !important; /* macOS Blue */
-  box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.2);
-  outline: none;
-}
-
-.macos-btn-primary {
-  background: linear-gradient(180deg, #0A84FF 0%, #007AFF 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);
-  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);
-}
-.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;
-}
-
-.nav-glass {
-  background: rgba(40, 40, 40, 0.85);
-  backdrop-filter: blur(20px);
-  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
-}
-</style>

+ 115 - 0
Co-creation-projects/JJason-DeepCastAgent/frontend/src/components/PlayerView.vue

@@ -0,0 +1,115 @@
+<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>
+          </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>
+        </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>
+              </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 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>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from "vue";
+import MarkdownIt from "markdown-it";
+
+const md = new MarkdownIt();
+
+const props = defineProps<{
+  topic: string;
+  audioUrl: string;
+  reportMarkdown: string;
+}>();
+
+defineEmits<{
+  reset: [];
+  downloadReport: [];
+}>();
+
+const isPlaying = ref(false);
+const audioPlayer = ref<HTMLAudioElement | null>(null);
+
+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);
+}
+
+.macos-btn-primary {
+  background: linear-gradient(180deg, #0A84FF 0%, #007AFF 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);
+  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);
+}
+.macos-btn-primary:active { transform: translateY(0.5px); filter: brightness(0.95); }
+
+.custom-scrollbar::-webkit-scrollbar { width: 8px; height: 8px; }
+.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); }
+</style>

+ 195 - 0
Co-creation-projects/JJason-DeepCastAgent/frontend/src/components/ProductionView.vue

@@ -0,0 +1,195 @@
+<template>
+  <div class="min-h-screen 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="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>
+          <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')">
+              📄 下载研究报告
+            </button>
+            <button v-if="!podcastReady" class="btn btn-ghost btn-sm text-red-400 hover:bg-white/5" @click="$emit('cancel')">
+              取消制作
+            </button>
+          </div>
+        </div>
+      </div>
+
+      <!-- Main Content -->
+      <div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
+
+        <!-- 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>
+                <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>
+                  </li>
+                </ul>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- Right Column: Logs & Output -->
+        <div class="lg:col-span-3 flex flex-col gap-4">
+
+          <!-- macOS Style Terminal -->
+          <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
+            </a>
+            <button class="btn glass text-white flex-1 btn-lg text-lg rounded-xl" @click="$emit('goPlayer')">
+              🎧 进入播放器
+            </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>
+              <audio class="w-full opacity-90 hover:opacity-100 transition-opacity" :src="audioUrl" controls></audio>
+            </div>
+          </div>
+
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+import TerminalLog from "./TerminalLog.vue";
+import type { LogEntry } from "./TerminalLog.vue";
+
+export type ProductionStage = "research" | "script" | "audio" | "done";
+
+const props = defineProps<{
+  logs: LogEntry[];
+  isWaiting: boolean;
+  waitingDots: string;
+  productionStage: ProductionStage;
+  reportReady: boolean;
+  podcastReady: boolean;
+  audioUrl: string;
+}>();
+
+defineEmits<{
+  cancel: [];
+  downloadReport: [];
+  goPlayer: [];
+}>();
+
+const terminalRef = ref<InstanceType<typeof TerminalLog> | null>(null);
+
+function scrollTerminal() {
+  terminalRef.value?.scrollToBottom();
+}
+
+defineExpose({ scrollTerminal });
+
+function getStepClass(step: ProductionStage) {
+  const stepsOrder: ProductionStage[] = ["research", "script", "audio", "done"];
+  const currentIdx = stepsOrder.indexOf(props.productionStage);
+  const stepIdx = stepsOrder.indexOf(step);
+
+  if (currentIdx > stepIdx) return "step-primary";
+  if (currentIdx === stepIdx) return "step-primary font-bold";
+  return "";
+}
+</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-glass {
+  background: rgba(40, 40, 40, 0.85);
+  backdrop-filter: blur(20px);
+  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.macos-btn-primary {
+  background: linear-gradient(180deg, #0A84FF 0%, #007AFF 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);
+  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);
+}
+.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; }
+
+@keyframes spin-slow {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+.animate-spin-slow { animation: spin-slow 3s linear infinite; }
+</style>

+ 84 - 0
Co-creation-projects/JJason-DeepCastAgent/frontend/src/components/SetupView.vue

@@ -0,0 +1,84 @@
+<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>
+
+      <div class="card glass-panel rounded-2xl">
+        <form @submit.prevent="$emit('start', topic)" class="card-body p-8">
+          <div class="form-control mb-6">
+            <textarea
+              v-model="topic"
+              class="w-full textarea textarea-bordered h-32 text-lg leading-relaxed resize-none macos-input rounded-xl"
+              placeholder="💡 请输入播客主题(例如:AI Agent 的发展趋势)"
+              required
+              @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>
+          </div>
+
+          <button
+            class="btn btn-lg w-full font-semibold rounded-xl macos-btn-primary border-0"
+            :disabled="!topic.trim()"
+          >
+            ✨ 开始制作播客
+          </button>
+        </form>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+
+const topic = defineModel<string>("topic", { required: true });
+
+defineEmits<{
+  start: [topic: string];
+}>();
+</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);
+}
+
+.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;
+}
+.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);
+  outline: none;
+}
+
+.macos-btn-primary {
+  background: linear-gradient(180deg, #0A84FF 0%, #007AFF 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);
+  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);
+}
+.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; }
+</style>

+ 129 - 0
Co-creation-projects/JJason-DeepCastAgent/frontend/src/components/TerminalLog.vue

@@ -0,0 +1,129 @@
+<template>
+  <div class="macos-terminal rounded-xl shadow-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]">
+      <!-- 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>
+      <!-- Title -->
+      <div class="flex-1 text-center">
+        <span class="text-[#9a9a9a] text-sm font-medium tracking-wide">deepcast — zsh — {{ logs.length }} lines</span>
+      </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);">
+      <!-- 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>
+      <!-- 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>
+      <!-- Waiting States -->
+      <div v-if="isWaiting && logs.length === 0" class="text-[#dcdcaa] text-center mt-8">
+        <span class="inline-block animate-pulse">⏳ 正在初始化...</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>
+        <span>处理中{{ waitingDots }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch, nextTick } from "vue";
+
+export interface LogEntry {
+  time: string;
+  message: string;
+}
+
+defineProps<{
+  logs: LogEntry[];
+  isWaiting: boolean;
+  waitingDots: string;
+}>();
+
+const logContainer = ref<HTMLElement | null>(null);
+
+/** 当 logs 变化时自动滚动到底部 */
+watch(
+  () => logContainer.value,
+  () => {},
+  { flush: "post" }
+);
+
+defineExpose({ scrollToBottom });
+
+function scrollToBottom() {
+  nextTick(() => {
+    if (logContainer.value) {
+      logContainer.value.scrollTop = logContainer.value.scrollHeight;
+    }
+  });
+}
+
+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";
+}
+</script>
+
+<style scoped>
+.macos-terminal {
+  background: #1e1e1e;
+  border: 1px solid #3d3d3d;
+  box-shadow:
+    0 22px 70px 4px rgba(0, 0, 0, 0.56),
+    0 0 0 1px rgba(0, 0, 0, 0.3);
+}
+
+.macos-titlebar {
+  -webkit-app-region: drag;
+  user-select: none;
+}
+
+.terminal-content {
+  font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;
+  font-size: 13px;
+  line-height: 1.6;
+}
+
+.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; }
+
+@keyframes blink {
+  0%, 50% { opacity: 1; }
+  51%, 100% { opacity: 0; }
+}
+.animate-blink { animation: blink 1s step-end infinite; }
+
+.custom-scrollbar::-webkit-scrollbar { width: 8px; height: 8px; }
+.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; }
+</style>