From 9327af602115b8b17e7855aa4189377da0805472 Mon Sep 17 00:00:00 2001 From: hjjjj <1311711287@qq.com> Date: Wed, 28 Jan 2026 18:33:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96=E5=89=A7=E9=9B=86?= =?UTF-8?q?=E5=88=9B=E4=BD=9C=E5=B9=B3=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/ai_assistant.py | 76 +++ backend/app/api/v1/projects.py | 90 ++- backend/app/api/v1/websocket.py | 306 ++++++++- .../app/core/agent_runtime/director_agent.py | 25 +- .../app/core/agent_runtime/director_tools.py | 85 ++- .../app/core/agents/series_creation_agent.py | 239 +++++++- backend/app/core/execution/batch_executor.py | 2 +- backend/data/episodes.json | 144 ++++- backend/data/messages.json | 40 ++ backend/data/projects.json | 58 +- backend/requirements.txt | 2 + frontend/src/App.tsx | 7 +- .../src/components/Workspace/ContextPanel.tsx | 7 +- .../components/Workspace/DirectorInbox.tsx | 255 +++++--- .../components/Workspace/EpisodeSidebar.tsx | 49 +- .../src/components/Workspace/SmartCanvas.tsx | 486 ++++++++++++--- frontend/src/pages/ProjectDetail.tsx | 162 ++++- frontend/src/pages/ProjectList.tsx | 2 +- frontend/src/pages/ProjectWorkspace.tsx | 580 ++++++++++++++++-- frontend/src/pages/ReviewConfig.tsx | 28 +- frontend/src/pages/ReviewResults.tsx | 31 +- frontend/src/pages/SkillManagement.tsx | 3 +- frontend/src/services/projectService.ts | 2 + frontend/src/stores/projectStore.ts | 7 + test.md | 17 +- 25 files changed, 2291 insertions(+), 412 deletions(-) diff --git a/backend/app/api/v1/ai_assistant.py b/backend/app/api/v1/ai_assistant.py index 72d67e8..68dbe02 100644 --- a/backend/app/api/v1/ai_assistant.py +++ b/backend/app/api/v1/ai_assistant.py @@ -892,3 +892,79 @@ async def get_available_skills(): except Exception as e: logger.error(f"获取 Skills 失败: {str(e)}") raise HTTPException(status_code=500, detail=f"获取失败: {str(e)}") + + +class OptimizeEpisodeRequest(BaseModel): + """优化剧集请求""" + projectId: str + episodeNumber: int + content: str + skills: Optional[List[SkillInfo]] = None + customPrompt: Optional[str] = None + + +@router.post("/optimize-episode") +async def optimize_episode(request: OptimizeEpisodeRequest): + """ + AI 辅助优化剧集内容 + + 支持融入 Skills 的行为指导 + 支持自定义提示词 + """ + try: + glm_client = get_glm_client() + skill_manager = get_skill_manager() + + # 构建增强的 System Prompt(融入 Skills) + base_role = """你是专业的剧集创作优化专家,擅长改进和提升剧集内容质量。 +你能识别剧情中的问题并提出改进建议,使内容更加引人入胜、逻辑严密、人物鲜明。""" + system_prompt = await build_enhanced_system_prompt( + base_role=base_role, + skills=request.skills, + skill_manager=skill_manager + ) + + # 构建用户提示 + custom_requirements = "" + if request.customPrompt: + custom_requirements = f"\n【用户自定义要求】\n{request.customPrompt}\n" + + user_prompt = f"""请优化以下剧集内容: + +项目 ID: {request.projectId} +集数: EP{request.episodeNumber} +{custom_requirements} +【剧集内容】 +{request.content[:8000]} + +【优化要求】 +1. 保持原有的故事结构和情节走向 +2. 改进对话,使其更符合人物性格 +3. 增强场景描写的画面感 +4. 优化叙事节奏 +5. 严格遵守上面【应用技能指导】中的优化要求 + +请直接输出优化后的剧集内容,不要添加任何解释或说明。 +""" + + logger.info(f"优化剧集 EP{request.episodeNumber},使用 {len(request.skills) if request.skills else 0} 个 Skills") + + response = await glm_client.chat( + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt} + ], + temperature=0.7 + ) + + optimized_content = response["choices"][0]["message"]["content"] + + return { + "success": True, + "optimizedContent": optimized_content, + "usage": response.get("usage") + } + + except Exception as e: + logger.error(f"优化剧集失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"优化失败: {str(e)}") diff --git a/backend/app/api/v1/projects.py b/backend/app/api/v1/projects.py index 1d1ab5c..1c7b1f4 100644 --- a/backend/app/api/v1/projects.py +++ b/backend/app/api/v1/projects.py @@ -49,7 +49,7 @@ async def create_project(project_data: SeriesProjectCreate): id=str(uuid.uuid4()), projectId=project.id, number=episode_num, - title=f"第{episode_num}集", + title=f"第{episode_num}集内容创作", status="pending", content="" # 初始化为空白 ) @@ -139,7 +139,7 @@ async def list_episodes(project_id: str): id=str(uuid.uuid4()), projectId=project_id, number=episode_num, - title=f"第{episode_num}集", + title=f"第{episode_num}集内容创作", status="pending", content="" # 初始化为空白,避免触发前端生成大纲按钮 ) @@ -207,14 +207,16 @@ async def update_episode(project_id: str, episode_number: int, update_data: dict @router.post("/{project_id}/execute", response_model=EpisodeExecuteResponse) async def execute_episode( project_id: str, - request: EpisodeExecuteRequest + request: EpisodeExecuteRequest, + background_tasks: BackgroundTasks ): """ - 执行单集创作 + 执行单集创作(已弃用,建议通过 WebSocket 使用 DirectorAgent) - 这是核心功能端点,调用 Agent 执行完整的创作流程 + 注意:此端点现在会立即返回,创作在后台执行。 + 推荐通过 WebSocket 连接到 /ws/projects/{project_id}/execute 并发送消息。 """ - # 获取项目 + # 获取项目验证存在 project = await project_repo.get(project_id) if not project: raise HTTPException( @@ -222,56 +224,44 @@ async def execute_episode( detail=f"项目不存在: {project_id}" ) + logger.info(f"接收创作请求(后台执行模式): 项目 {project_id}, EP{request.episodeNumber}") + + # 立即返回响应,创作在后台执行 + background_tasks.add_task(_execute_episode_in_background, project_id, request.episodeNumber, request.title) + + return EpisodeExecuteResponse( + episode=Episode( + projectId=project_id, + number=request.episodeNumber, + status="writing", + title=request.title or f"第{request.episodeNumber}集" + ), + success=True, + message=f"EP{request.episodeNumber} 已开始在后台创作" + ) + + +async def _execute_episode_in_background( + project_id: str, + episode_number: int, + title: str +): + """后台执行剧集创作的辅助函数""" try: - # 获取 Agent - agent = get_series_agent() + from app.api.v1.websocket import _execute_episode_creation - # 执行创作 - logger.info(f"开始执行创作: 项目 {project_id}, EP{request.episodeNumber}") - - # 先获取现有剧集记录,避免重复创建 - existing_episodes = await episode_repo.list_by_project(project_id) - episode_record = next((ep for ep in existing_episodes if ep.number == request.episodeNumber), None) + logger.info(f"后台创作任务开始: EP{episode_number}") - episode = await agent.execute_episode( - project=project, - episode_number=request.episodeNumber, - title=request.title - ) - - # 保持原有的 ID 如果记录已存在 - if episode_record: - episode.id = episode_record.id - episode.projectId = project_id - await episode_repo.update(episode) - logger.info(f"更新现有剧集记录: {episode.id} - EP{request.episodeNumber}") - else: - episode.projectId = project_id - await episode_repo.create(episode) - logger.info(f"创建新剧集记录: {episode.id} - EP{request.episodeNumber}") - - # 更新项目记忆 - await project_repo.update(project_id, { - "memory": project.memory.dict() - }) - - return EpisodeExecuteResponse( - episode=episode, - success=True, - message=f"EP{request.episodeNumber} 创作完成" + # 调用 WebSocket 的创作函数(它已经处理了后台执行) + await _execute_episode_creation( + project_id=project_id, + episode_number=episode_number, + analyze_previous_memory=True ) + logger.info(f"后台创作任务完成: EP{episode_number}") except Exception as e: - logger.error(f"执行创作失败: {str(e)}") - return EpisodeExecuteResponse( - episode=Episode( - projectId=project_id, - number=request.episodeNumber, - status="needs-review" - ), - success=False, - message=f"创作失败: {str(e)}" - ) + logger.error(f"后台创作任务失败: {str(e)}", exc_info=True) # ============================================ diff --git a/backend/app/api/v1/websocket.py b/backend/app/api/v1/websocket.py index e24b8e2..bd816d4 100644 --- a/backend/app/api/v1/websocket.py +++ b/backend/app/api/v1/websocket.py @@ -525,6 +525,61 @@ async def _handle_client_message( }) +async def _sync_context_states( + project_id: str, + episode_number: int, + memory: Any +): + """同步上下文状态到前端""" + try: + # 提取角色状态作为上下文状态 + context_states = [] + + # 添加时间状态 + context_states.append({ + "type": "time", + "value": f"EP{episode_number} 完成后" + }) + + # 添加角色状态 + character_states = getattr(memory, 'characterStates', {}) + if isinstance(character_states, dict): + for char_name, states in character_states.items(): + if states and isinstance(states, list) and len(states) > 0: + latest_state = states[-1] + if isinstance(latest_state, dict): + state_value = latest_state.get('state', f"{char_name}状态") + else: + state_value = getattr(latest_state, 'state', f"{char_name}状态") + + context_states.append({ + "type": "character", + "value": f"{char_name}: {state_value}", + "character": char_name, + "state": state_value + }) + + # 添加待收线数量 + pending_threads = getattr(memory, 'pendingThreads', []) + if pending_threads: + context_states.append({ + "type": "pending_threads", + "value": f"待收线: {len(pending_threads)} 条" + }) + + # 广播上下文更新 + await manager.send_to_project(project_id, { + "type": "context_update", + "states": context_states, + "episode_number": episode_number + }) + + logger.info(f"已同步上下文状态到项目 {project_id}, {len(context_states)} 个状态") + + except Exception as e: + logger.error(f"同步上下文状态失败: {str(e)}") + + async def _handle_tool_call(project_id: str, event: Dict[str, Any]): """ 处理工具调用,转换为特定的 WebSocket 消息 @@ -660,7 +715,7 @@ async def _handle_tool_call(project_id: str, event: Dict[str, Any]): if episode_number: try: from app.db.repositories import episode_repo, project_repo - from app.core.memory.memory_manager import MemoryManager + from app.core.memory.memory_manager import get_memory_manager episodes = await episode_repo.list_by_project(project_id) episode = next((ep for ep in episodes if ep.number == episode_number), None) @@ -676,9 +731,12 @@ async def _handle_tool_call(project_id: str, event: Dict[str, Any]): try: project = await project_repo.get(project_id) if project and episode.content: - memory_manager = MemoryManager() + memory_manager = get_memory_manager() await memory_manager.update_memory_from_episode(project, episode) logger.info(f"Updated memory after saving episode {episode_number}") + + # 同步上下文状态到前端 + await _sync_context_states(project_id, episode_number, project.memory) except Exception as memory_error: logger.warning(f"Failed to update memory for episode {episode_number}: {memory_error}") @@ -711,7 +769,7 @@ async def _handle_tool_call(project_id: str, event: Dict[str, Any]): if episode_number: try: from app.db.repositories import episode_repo, project_repo - from app.core.memory.memory_manager import MemoryManager + from app.core.memory.memory_manager import get_memory_manager episodes = await episode_repo.list_by_project(project_id) episode = next((ep for ep in episodes if ep.number == episode_number), None) @@ -729,9 +787,12 @@ async def _handle_tool_call(project_id: str, event: Dict[str, Any]): try: project = await project_repo.get(project_id) if project: - memory_manager = MemoryManager() + memory_manager = get_memory_manager() await memory_manager.update_memory_from_episode(project, episode) logger.info(f"Updated memory after updating episode {episode_number}") + + # 同步上下文状态到前端 + await _sync_context_states(project_id, episode_number, project.memory) except Exception as memory_error: logger.warning(f"Failed to update memory for episode {episode_number}: {memory_error}") @@ -758,6 +819,226 @@ async def _handle_tool_call(project_id: str, event: Dict[str, Any]): "episodeTitle": title }) + elif name == "create_episode": + # 处理剧集创作请求 + episode_number = args.get("episode_number") + analyze_previous_memory = args.get("analyze_previous_memory", True) + + if episode_number: + # 使用 ensure_future 确保任务在后台执行,即使 WebSocket 断开也能继续 + asyncio.ensure_future(_execute_episode_creation( + project_id, episode_number, analyze_previous_memory + )) + logger.info(f"已启动后台创作任务: EP{episode_number}") + +# ============================================ +# 辅助函数 - 用于从其他模块发送消息 +# ============================================ + + +# 全局后台任务跟踪 +_background_tasks: Dict[str, asyncio.Task] = {} + + +async def _execute_episode_creation( + project_id: str, + episode_number: int, + analyze_previous_memory: bool +): + """ + 异步执行剧集创作 + + 这个函数在后台执行,不会阻塞 WebSocket 连接。 + 即使 WebSocket 断开,任务也会继续执行并保存到数据库。 + + 它会: + 1. 分析上一集的记忆(如果需要) + 2. 执行剧集创作 + 3. 通过 WebSocket 发送进度更新(如果连接存在) + 4. 将创作内容发送到画布(如果连接存在) + 5. 无论如何都保存到数据库 + """ + task_key = f"{project_id}_{episode_number}" + + try: + from app.db.repositories import project_repo, episode_repo + from app.core.agents.series_creation_agent import get_series_agent + from app.core.memory.memory_manager import get_memory_manager + + # 注册到后台任务字典 + current_task = asyncio.current_task() + if current_task: + _background_tasks[task_key] = current_task + + logger.info(f"开始后台创作任务: {task_key}") + + # 辅助函数:安全发送 WebSocket 消息(忽略连接错误) + async def safe_send(message_type: str, data: dict = None): + try: + if data is None: + data = {} + await manager.send_to_project(project_id, { + "type": message_type, + "data": data + }) + except Exception as e: + # WebSocket 可能已断开,忽略错误继续执行 + logger.debug(f"WebSocket 发送失败(可能已断开): {e}") + + # 获取项目 + project = await project_repo.get(project_id) + if not project: + await safe_send("error", {"message": f"项目不存在: {project_id}"}) + return + + # 更新计划状态 - 开始 + await safe_send("plan_update", { + "plan": [ + f"分析 EP{episode_number - 1 if episode_number > 1 else 'N/A'} 的记忆系统" if analyze_previous_memory and episode_number > 1 else "跳过记忆分析(首集)", + f"生成 EP{episode_number} 大纲", + f"创作 EP{episode_number} 对话内容", + f"执行质量审核", + f"更新记忆系统" + ], + "status": "planning", + "current_step_index": 0 + }) + + # 步骤 1: 分析上一集记忆(如果需要) + if analyze_previous_memory and episode_number > 1: + await safe_send("plan_update", { + "plan": [ + f"分析 EP{episode_number - 1} 的记忆系统", + f"生成 EP{episode_number} 大纲", + f"创作 EP{episode_number} 对话内容", + f"执行质量审核", + f"更新记忆系统" + ], + "status": "planning", + "current_step_index": 0 + }) + + # 获取上一集内容 + prev_episodes = await episode_repo.list_by_project(project_id) + prev_episode = next((ep for ep in prev_episodes if ep.number == episode_number - 1), None) + + if prev_episode and prev_episode.content: + await safe_send("text", {"content": f"\n\n--- 正在分析 EP{episode_number - 1} 的记忆系统 ---\n"}) + + # 使用 MemoryManager 更新记忆 + try: + memory_manager = get_memory_manager() + await memory_manager.update_memory_from_episode(project, prev_episode) + logger.info(f"EP{episode_number - 1} 记忆已分析并注入到 EP{episode_number}") + except Exception as e: + logger.warning(f"分析 EP{episode_number - 1} 记忆失败: {e}") + + # 步骤 2-5: 执行剧集创作 + agent = get_series_agent() + + await safe_send("plan_update", { + "plan": [ + f"分析 EP{episode_number - 1} 的记忆系统" if analyze_previous_memory and episode_number > 1 else "跳过记忆分析", + f"生成 EP{episode_number} 大纲", + f"创作 EP{episode_number} 对话内容", + f"执行质量审核", + f"更新记忆系统" + ], + "status": "writing", + "current_step_index": 1 + }) + + await safe_send("text", {"content": f"\n\n--- 开始创作 EP{episode_number} ---\n"}) + + # 执行创作 + episode = await agent.execute_episode( + project=project, + episode_number=episode_number, + title=f"第{episode_number}集" + ) + + # 检查是否创作成功(错误处理) + if episode.status == "needs-review" and not episode.content: + # 创作失败,没有内容 + await safe_send("error", { + "message": f"EP{episode_number} 创作失败", + "episode_number": episode_number + }) + await safe_send("text", {"content": f"\n\n❌ EP{episode_number} 创作失败。请检查错误日志并重试。\n"}) + logger.error(f"EP{episode_number} 创作失败,无内容生成") + return + + # 创作成功,保存到数据库(无论 WebSocket 是否连接) + existing_episodes = await episode_repo.list_by_project(project_id) + episode_record = next((ep for ep in existing_episodes if ep.number == episode_number), None) + + if episode_record: + episode.id = episode_record.id + episode.projectId = project_id + await episode_repo.update(episode) + logger.info(f"更新现有剧集记录: {episode.id}") + else: + episode.projectId = project_id + await episode_repo.create(episode) + logger.info(f"创建新剧集记录: {episode.id}") + + # 发送内容到画布 + if episode.content: + await safe_send("canvas_update", {"content": episode.content}) + + # 更新记忆 + await project_repo.update(project_id, { + "memory": project.memory.dict() + }) + + # 完成消息 + await safe_send("plan_update", { + "plan": [ + f"分析 EP{episode_number - 1} 的记忆系统" if analyze_previous_memory and episode_number > 1 else "跳过记忆分析", + f"生成 EP{episode_number} 大纲", + f"创作 EP{episode_number} 对话内容", + f"执行质量审核", + f"更新记忆系统" + ], + "status": "idle", + "current_step_index": 4 + }) + + await safe_send("text", {"content": f"\n\n✅ EP{episode_number} 创作完成!质量分数: {episode.qualityScore or 0}\n"}) + + # 广播更新 + await safe_send("episode_updated", { + "number": episode_number, + "title": episode.title, + "status": episode.status + }) + + logger.info(f"EP{episode_number} 后台创作完成,已保存到数据库") + + except Exception as e: + logger.error(f"执行剧集创作失败: {str(e)}", exc_info=True) + try: + await manager.send_to_project(project_id, { + "type": "error", + "data": { + "message": f"EP{episode_number} 创作失败: {str(e)}", + "episode_number": episode_number + } + }) + await manager.send_to_project(project_id, { + "type": "text", + "data": {"content": f"\n\n❌ EP{episode_number} 创作失败: {str(e)}\n"} + }) + except Exception: + # WebSocket 可能已断开,忽略错误 + pass + finally: + # 清理任务跟踪 + if task_key in _background_tasks: + del _background_tasks[task_key] + logger.info(f"后台创作任务结束: {task_key}") + + # ============================================ # 辅助函数 - 用于从其他模块发送消息 # ============================================ @@ -860,6 +1141,20 @@ async def broadcast_batch_complete( await manager.send_to_batch(batch_id, message) +async def broadcast_to_project( + project_id: str, + message_type: str, + data: Dict[str, Any] +): + """向项目的所有连接广播消息""" + message = { + "type": message_type, + "data": data, + "timestamp": datetime.now().isoformat() + } + await manager.send_to_project(project_id, message) + + # 导出连接管理器和辅助函数 __all__ = [ "manager", @@ -867,5 +1162,6 @@ __all__ = [ "broadcast_episode_complete", "broadcast_batch_progress", "broadcast_error", - "broadcast_batch_complete" + "broadcast_batch_complete", + "broadcast_to_project" ] diff --git a/backend/app/core/agent_runtime/director_agent.py b/backend/app/core/agent_runtime/director_agent.py index 2b46981..e167ade 100644 --- a/backend/app/core/agent_runtime/director_agent.py +++ b/backend/app/core/agent_runtime/director_agent.py @@ -76,7 +76,14 @@ The User is the Director. Your goal is to help the Director create high-quality - When receiving a high-level goal (e.g., "Write all scripts"), break it down into episodes and steps. - Use `update_plan(steps=[...], current_step=0, status='planning')`. -2. **Execution & Writing** +2. **Episode Creation (IMPORTANT)** + - When the Director says "开始创作第X集" or "开始创作 EP X" or similar: + - Use `create_episode(episode_number=X)` tool to start the creation process + - This tool will automatically: analyze previous memory, generate outline, write dialogue, review, and update memory + - The creation runs in the background and updates will be sent via WebSocket + - The Director will see progress in the plan panel and final content on the canvas + +3. **Execution & Writing** - Use `list_episodes` to see the current progress of the project. - Use `focus_episode` to navigate between episodes. This will update the user's view. - **Canvas Focus**: The main canvas is for the **actual script/dialogue content**. Do NOT put outlines on the canvas unless specifically asked. @@ -85,12 +92,22 @@ The User is the Director. Your goal is to help the Director create high-quality - Use `save_episode` (for completion) or `update_episode` (for partial updates) to persist content to the database. - Update your plan status as you progress: `update_plan(..., status='writing')`. -3. **Review & Approval** +4. **AI Editing & Optimization (CRITICAL)** + - When the Director asks you to "optimize", "improve", "edit", or "revise" content, you MUST: + 1. Process the content to improve it based on the request + 2. Use `write_to_canvas(content="IMPROVED_FULL_CONTENT", append=false)` to replace the canvas content + 3. Do NOT just output text - you must use the tool to update the canvas + - When asked to "优化", "改进", "编辑", or "修改" content, same rules apply + - Example: If asked to optimize Episode 1, call `write_to_canvas(content="", append=false)` + - Always use append=false when doing full content replacement + - This is the ONLY way to properly update the canvas for the user to see + +5. **Review & Approval** - NEVER mark a major deliverable as "Final" without Director approval. - Use `add_inbox_task` to submit drafts or questions to the Director. - Example: `add_inbox_task(title="Review Episode 1 Outline", type="review", ...)` -4. **Context & Memory** +6. **Context & Memory** - If the story state changes (e.g., location change, character injury), use `update_context`. - If you spot consistency issues, use `add_annotation` on the canvas. @@ -98,6 +115,8 @@ The User is the Director. Your goal is to help the Director create high-quality - Be professional, creative, and concise. - Use the Director's language (Chinese/English) matching their input. - When you are working, keep the plan updated. +- CRITICAL: When asked to edit/optimize content, ALWAYS use `write_to_canvas` tool with append=false to replace content, don't just output text. +- CRITICAL: When asked to create episodes, use `create_episode` tool which runs in background and doesn't block the interface. """ # 添加项目上下文(如果有) diff --git a/backend/app/core/agent_runtime/director_tools.py b/backend/app/core/agent_runtime/director_tools.py index 201ab9b..b9fe97e 100644 --- a/backend/app/core/agent_runtime/director_tools.py +++ b/backend/app/core/agent_runtime/director_tools.py @@ -585,6 +585,88 @@ def update_episode( return f"✓ Episode {episode_number} queued for update." +# ============================================ +# 附加工具:需要数据库访问 +# ============================================ + +@tool +def create_episode( + episode_number: int, + analyze_previous_memory: bool = True, + runtime: ToolRuntime[SkillAgentContext] = None +) -> str: + """ + Create a new episode with full workflow including memory analysis. + + This tool orchestrates the complete episode creation process: + 1. Analyzes previous episode's memory (if analyze_previous_memory=True) + 2. Creates execution plan + 3. Generates outline + 4. Writes dialogue/script + 5. Performs quality review + 6. Updates memory system + + Args: + episode_number: The episode number to create (e.g., 1, 2, 3...) + analyze_previous_memory: Whether to analyze previous episode's memory first (default: True) + + Returns: + Status message with creation details + + Example: + create_episode(episode_number=2, analyze_previous_memory=True) + """ + if runtime is None: + return "Error: runtime context not available" + + project_id = runtime.context.project_id if runtime.context else None + if not project_id: + return "Error: No project ID in context" + + # 更新当前剧集焦点 + runtime.context.active_episode_number = episode_number + + # 存储剧集创作请求到状态中,由 WebSocket 处理器异步执行 + if "episodes_to_create" not in runtime.state: + runtime.state["episodes_to_create"] = [] + + creation_request = { + "episode_number": episode_number, + "analyze_previous_memory": analyze_previous_memory, + "project_id": project_id, + "status": "pending" + } + runtime.state["episodes_to_create"].append(creation_request) + + # 更新执行计划 + plan_steps = [ + f"分析 EP{episode_number - 1 if episode_number > 1 else 'N/A'} 的记忆系统" if analyze_previous_memory and episode_number > 1 else "跳过记忆分析(首集)", + f"生成 EP{episode_number} 大纲", + f"创作 EP{episode_number} 对话内容", + f"执行质量审核", + f"更新记忆系统" + ] + + # 存储 plan 到状态中 + runtime.state[PLAN_STATE_KEY] = { + "steps": plan_steps, + "current_step_index": 0, + "status": "planning" + } + + return f"""✓ Episode creation started for EP{episode_number} + +Steps: +1. 分析上一集记忆: {'是' if analyze_previous_memory and episode_number > 1 else '否(首集)'} +2. 生成大纲 +3. 创作对话内容 +4. 质量审核 +5. 更新记忆系统 + +The creation process is running in the background. +Progress will be updated automatically.""" + + # 导出工具列表 DIRECTOR_TOOLS = [ update_plan, @@ -597,5 +679,6 @@ DIRECTOR_TOOLS = [ request_review, focus_episode, list_episodes, - update_episode + update_episode, + create_episode # 新增剧集创作工具 ] diff --git a/backend/app/core/agents/series_creation_agent.py b/backend/app/core/agents/series_creation_agent.py index e4e40cf..77f90be 100644 --- a/backend/app/core/agents/series_creation_agent.py +++ b/backend/app/core/agents/series_creation_agent.py @@ -54,7 +54,7 @@ class SeriesCreationAgent: episode = Episode( projectId=project.id, number=episode_number, - title=title or (existing_episode.title if existing_episode else f"第{episode_number}集"), + title=title or (existing_episode.title if existing_episode else f"第{episode_number}集内容创作"), status="writing" ) if existing_episode: @@ -90,12 +90,13 @@ class SeriesCreationAgent: episode.content = content # ============================================ - # 阶段 4: 一致性审核 + # 阶段 4: 质量分析和审核 # ============================================ - logger.info(f"EP{episode_number} - 阶段 4: 一致性审核") - review = await self._review_consistency(project, episode_number, content) + logger.info(f"EP{episode_number} - 阶段 4: 质量分析和审核") + review = await self._review_episode_full(project, episode_number, content) episode.qualityScore = review.get("score", 0) episode.issues = review.get("issues", []) + episode.reviewResult = review.get("reviewResult") # ============================================ # 阶段 5: 更新记忆系统 @@ -111,7 +112,8 @@ class SeriesCreationAgent: except Exception as e: logger.error(f"EP{episode_number} 创作失败: {str(e)}", exc_info=True) episode.status = "needs-review" - episode.content = f"第{episode_number}集内容创作失败: {str(e)}" + # 不将错误信息写入 content,避免错误信息被填入画布 + episode.content = episode.content or "" # 保持现有内容或为空 episode.issues = [ EpisodeIssue( type="execution_error", @@ -191,7 +193,7 @@ class SeriesCreationAgent: except Exception as e: logger.error(f"大纲生成失败: {str(e)}") - return f"第{episode_number}集大纲:本集讲述..." + return "" # 返回空字符串,避免错误信息被当作内容 async def _write_dialogue( self, @@ -258,7 +260,7 @@ class SeriesCreationAgent: except Exception as e: logger.error(f"对话创作失败: {str(e)}") - return f"第{episode_number}集内容创作失败: {str(e)}" + return "" # 返回空字符串,避免错误信息被当作内容 async def _review_consistency( self, @@ -266,54 +268,160 @@ class SeriesCreationAgent: episode_number: int, content: str ) -> Dict[str, Any]: - """审核一致性""" + """审核一致性(保留兼容)""" + return await self._review_episode_full(project, episode_number, content) + + async def _review_episode_full( + self, + project: SeriesProject, + episode_number: int, + content: str + ) -> Dict[str, Any]: + """完整剧集审核,包括多维度分析和审核卡片推送""" try: - from app.core.memory.memory_manager import get_memory_manager - from app.models.memory import EnhancedMemory + from app.core.review.review_manager import get_review_manager + from app.models.review import ReviewConfig, DimensionConfig, DimensionType - memory_manager = get_memory_manager() + review_manager = get_review_manager() - # 转换为 EnhancedMemory - enhanced_memory = memory_manager._convert_to_enhanced_memory(project.memory) - - # 使用 MemoryManager 检查一致性 - issues = await memory_manager.check_consistency( - episode_content=content, - episode_number=episode_number, - memory=enhanced_memory + # 构建审核配置 + review_config = ReviewConfig( + enabled_review_skills=["consistency_checker"], + overall_strictness=0.7, + pass_threshold=75.0 ) - # 计算质量分数 - high_issues = [i for i in issues if i.severity.value == "high"] - medium_issues = [i for i in issues if i.severity.value == "medium"] + # 添加默认维度 + for dim_type in [ + DimensionType.consistency, + DimensionType.quality, + DimensionType.dialogue, + DimensionType.character, + DimensionType.plot + ]: + review_config.dimension_settings[dim_type] = DimensionConfig( + enabled=True, + strictness=0.7, + weight=1.0 + ) - score = 100 - (len(high_issues) * 15) - (len(medium_issues) * 5) - score = max(0, min(100, score)) + # 创建 Episode 对象用于审核 + episode = Episode( + projectId=project.id, + number=episode_number, + title=f"第{episode_number}集内容创作", + status="completed", + content=content, + completedAt=datetime.now() + ) - # 将问题转换为 EpisodeIssue 格式 + # 执行完整审核 + review_result = await review_manager.review_episode( + project=project, + episode=episode, + config=review_config + ) + + # 转换为 EpisodeIssue 格式 episode_issues = [ EpisodeIssue( - type=issue.type, + type=issue.type.value, description=issue.description, severity=issue.severity.value, suggestion=issue.suggestion ) - for issue in issues + for issue in review_result.issues ] logger.info( - f"一致性审核完成: 分数={score}, " - f"问题={len(issues)} (高={len(high_issues)}, 中={len(medium_issues)})" + f"完整审核完成: 总分={review_result.overall_score:.1f}, " + f"通过={'是' if review_result.passed else '否'}, " + f"问题数={len(review_result.issues)}" ) + # 推送审核卡片到前端(通过 WebSocket) + await self._push_review_card(project.id, episode_number, review_result) + return { - "score": score, - "issues": episode_issues + "score": review_result.overall_score, + "issues": episode_issues, + "reviewResult": { + "dimensionScores": [ + { + "dimension": ds.dimension.value, + "score": ds.score, + "passed": ds.passed, + "issueCount": ds.issue_count + } + for ds in review_result.dimension_scores + ], + "passed": review_result.passed, + "highSeverityCount": review_result.high_severity_count, + "mediumSeverityCount": review_result.medium_severity_count + } } except Exception as e: - logger.error(f"一致性审核失败: {str(e)}") - return {"score": 80, "issues": []} + logger.error(f"完整审核失败: {str(e)}") + import traceback + traceback.print_exc() + return {"score": 80, "issues": [], "reviewResult": None} + + async def _push_review_card( + self, + project_id: str, + episode_number: int, + review_result: Any + ): + """推送审核卡片到前端""" + try: + from app.api.v1.websocket import broadcast_to_project + + # 构建审核卡片数据 + card_data = { + "type": "review_card", + "episode_number": episode_number, + "overall_score": review_result.overall_score, + "passed": review_result.passed, + "issue_count": len(review_result.issues), + "high_severity_count": review_result.high_severity_count, + "medium_severity_count": review_result.medium_severity_count, + "dimension_scores": [ + { + "dimension": ds.dimension.value, + "score": ds.score, + "passed": ds.passed + } + for ds in review_result.dimension_scores + ], + "issues": [ + { + "type": issue.type.value, + "dimension": issue.dimension.value, + "severity": issue.severity.value, + "description": issue.description, + "suggestion": issue.suggestion, + "location": { + "episode": issue.location.episode, + "scene": issue.location.scene + } if issue.location.scene else None + } + for issue in review_result.issues[:10] # 最多显示10个问题 + ], + "timestamp": datetime.now().isoformat() + } + + # 广播到项目 + await broadcast_to_project( + project_id=project_id, + message_type="review_complete", + data=card_data + ) + + logger.info(f"已推送审核卡片到项目 {project_id}, EP{episode_number}") + + except Exception as e: + logger.error(f"推送审核卡片失败: {str(e)}") async def _update_memory( self, @@ -321,7 +429,7 @@ class SeriesCreationAgent: episode_number: int, content: str ): - """更新记忆系统""" + """更新记忆系统并同步上下文到前端""" from app.core.memory.memory_manager import get_memory_manager # 创建 Episode 对象 @@ -346,8 +454,71 @@ class SeriesCreationAgent: f"一致性问题={result.consistency_issues_found}" ) + # 同步上下文状态到前端(通过 WebSocket) + await self._sync_context_states(project.id, episode_number, project.memory) + return result + async def _sync_context_states( + self, + project_id: str, + episode_number: int, + memory: Any + ): + """同步上下文状态到前端""" + try: + from app.api.v1.websocket import broadcast_to_project + + # 提取角色状态作为上下文状态 + context_states = [] + + # 添加时间状态 + context_states.append({ + "type": "time", + "value": f"EP{episode_number} 完成后" + }) + + # 添加角色状态 + character_states = getattr(memory, 'characterStates', {}) + if isinstance(character_states, dict): + for char_name, states in character_states.items(): + if states and isinstance(states, list) and len(states) > 0: + latest_state = states[-1] + if isinstance(latest_state, dict): + state_value = latest_state.get('state', f"{char_name}状态") + else: + state_value = getattr(latest_state, 'state', f"{char_name}状态") + + context_states.append({ + "type": "character", + "value": f"{char_name}: {state_value}", + "character": char_name, + "state": state_value + }) + + # 添加待收线数量 + pending_threads = getattr(memory, 'pendingThreads', []) + if pending_threads: + context_states.append({ + "type": "pending_threads", + "value": f"待收线: {len(pending_threads)} 条" + }) + + # 广播上下文更新 + await broadcast_to_project( + project_id=project_id, + message_type="context_update", + data={ + "states": context_states, + "episode_number": episode_number + } + ) + + logger.info(f"已同步上下文状态到项目 {project_id}, {len(context_states)} 个状态") + + except Exception as e: + logger.error(f"同步上下文状态失败: {str(e)}") + def _build_context( self, project: SeriesProject, diff --git a/backend/app/core/execution/batch_executor.py b/backend/app/core/execution/batch_executor.py index 1fd64e6..943eb65 100644 --- a/backend/app/core/execution/batch_executor.py +++ b/backend/app/core/execution/batch_executor.py @@ -272,7 +272,7 @@ class BatchExecutor: episode = await self.series_agent.execute_episode( project=project, episode_number=episode_number, - title=f"第{episode_number}集" + title=f"第{episode_number}集内容创作" ) # 2. 更新记忆系统 diff --git a/backend/data/episodes.json b/backend/data/episodes.json index 70b05d2..85289ef 100644 --- a/backend/data/episodes.json +++ b/backend/data/episodes.json @@ -3,7 +3,7 @@ "id": "f3cc5c29-9fdd-4403-b48b-0cec9a126bf7", "projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c", "number": 1, - "title": "第1集", + "title": "11", "status": "completed", "structure": { "episodeNumber": 1, @@ -11,7 +11,7 @@ "keyEvents": [] }, "outline": "第1集大纲:本集讲述...", - "content": "第1集内容创作失败: 'str' object has no attribute 'items'", + "content": "方便复旦百年奋斗", "summary": null, "qualityScore": 100.0, "retryCount": 0, @@ -24,35 +24,135 @@ "id": "5969c386-9615-461f-ad4a-12b0161020d7", "projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c", "number": 2, - "title": "第2集", - "status": "pending", - "structure": null, - "outline": null, - "content": null, + "title": "", + "status": "completed", + "structure": { + "episodeNumber": 2, + "scenes": [], + "keyEvents": [] + }, + "outline": "第2集大纲:本集讲述...", + "content": "第2集内容创作失败: 'str' object has no attribute 'items'", "summary": null, - "qualityScore": null, + "qualityScore": 98.6, "retryCount": 0, - "issues": [], - "reviewResult": null, - "createdAt": "2026-01-28T10:28:53.840823", - "completedAt": null + "issues": [ + { + "type": "custom", + "description": "The provided episode content is an error message, not a script. Cannot perform consistency check.", + "severity": "medium", + "suggestion": "Please fix the content generation error and provide valid script text for EP2." + } + ], + "reviewResult": { + "dimensionScores": [ + { + "dimension": "consistency", + "score": 93.0, + "passed": true, + "issueCount": 1 + }, + { + "dimension": "quality", + "score": 100.0, + "passed": true, + "issueCount": 0 + }, + { + "dimension": "dialogue", + "score": 100.0, + "passed": true, + "issueCount": 0 + }, + { + "dimension": "character", + "score": 100.0, + "passed": true, + "issueCount": 0 + }, + { + "dimension": "plot", + "score": 100.0, + "passed": true, + "issueCount": 0 + } + ], + "passed": true, + "highSeverityCount": 0, + "mediumSeverityCount": 1 + }, + "createdAt": "2026-01-28T14:54:46.743471", + "completedAt": "2026-01-28T15:00:53.777508" }, "75a7de41-4768-450a-889b-783f818893f2": { "id": "75a7de41-4768-450a-889b-783f818893f2", "projectId": "8f969272-4ece-49e7-8ca1-4877cc62c57c", "number": 3, - "title": "第3集", - "status": "pending", - "structure": null, - "outline": null, - "content": null, + "title": "内容创作", + "status": "completed", + "structure": { + "episodeNumber": 3, + "scenes": [], + "keyEvents": [] + }, + "outline": "# 第3集大纲:苏园深会\n\n## 场景一:苏府大门外(日)\n- 楚云飞驾车来到苏府门前,欲归还之前的车马\n- 苏瑾月借机与楚云飞对话,探听他的真实身份\n- 两人言语交锋,苏瑾月察觉到楚云飞不简单,但也看到他的义气\n\n## 场景二:苏府花园(午后)\n- 林墨轩来访苏府,向苏老爷提亲\n- 苏老爷欣喜,但苏瑾月巧妙拖延,提出\"试婚\"考验\n- 林墨轩表面答应,心中却开始盘算如何对付潜在的竞争者\n\n## 场景三:街边茶寮(黄昏)\n- 燕无痕出现在江南,寻找旧友楚云飞的线索\n- 无意中听到江湖传言,得知苏家与楚云飞有过交集\n- 燕无痕决定暗中观察,看看其中是否有阴谋\n\n## 场景四:苏瑾月闺房(夜)\n- 苏瑾月独自沉思,分析目前形势\n- 三个男人:楚云飞神秘而有义气,林墨轩儒雅却心机深,燕无痕未知\n- 决定以\"试婚\"为名,逐步揭开三人的真面目\n\n## 关键剧情点\n1. 楚云飞与苏瑾月的对话埋下情感线索\n2. 林墨轩提亲,矛盾正式展开\n3. 燕无痕登场,江湖线开启\n4. 苏瑾月的\"试婚\"计划为后续剧情奠定基础", + "content": "", "summary": null, - "qualityScore": null, + "qualityScore": 94.4, "retryCount": 0, - "issues": [], - "reviewResult": null, - "createdAt": "2026-01-28T10:28:53.843865", - "completedAt": null + "issues": [ + { + "type": "custom", + "description": "EP3集的剧集内容部分为空,无法进行一致性审核", + "severity": "high", + "suggestion": "请提供EP3集的实际内容,包括场景描述、对话、角色行为等详细信息" + }, + { + "type": "custom", + "description": "没有提供剧集内容进行审核", + "severity": "high", + "suggestion": "请提供需要审核的剧集内容" + } + ], + "reviewResult": { + "dimensionScores": [ + { + "dimension": "consistency", + "score": 86.0, + "passed": true, + "issueCount": 1 + }, + { + "dimension": "quality", + "score": 86.0, + "passed": true, + "issueCount": 1 + }, + { + "dimension": "dialogue", + "score": 100.0, + "passed": true, + "issueCount": 0 + }, + { + "dimension": "character", + "score": 100.0, + "passed": true, + "issueCount": 0 + }, + { + "dimension": "plot", + "score": 100.0, + "passed": true, + "issueCount": 0 + } + ], + "passed": true, + "highSeverityCount": 2, + "mediumSeverityCount": 0 + }, + "createdAt": "2026-01-28T16:51:40.626838", + "completedAt": "2026-01-28T16:52:55.787498" }, "57a6aeb2-bcf0-4eb0-935a-542ccd36b7b4": { "id": "57a6aeb2-bcf0-4eb0-935a-542ccd36b7b4", diff --git a/backend/data/messages.json b/backend/data/messages.json index 2cbce01..eb17c8d 100644 --- a/backend/data/messages.json +++ b/backend/data/messages.json @@ -34,6 +34,46 @@ "role": "user", "content": "开始生成大纲", "timestamp": "2026-01-28T10:19:32.449321" + }, + { + "role": "user", + "content": "开始创作第三集", + "timestamp": "2026-01-28T15:17:45.959233" + }, + { + "role": "user", + "content": "开始生成第3集大纲", + "timestamp": "2026-01-28T16:47:54.452228" + }, + { + "role": "agent", + "content": "\n我来帮你生成第3集的大纲。首先让我查看一下当前项目的进度。\n\n我来为第3集制定创作计划并生成大纲。\n\n现在让我创建第3集。根据项目协议,当你说\"开始创作第X集\"时,我应该使用 `create_episode` 工具来自动完成整个创建过程。\n\n系统正在后台自动创建第3集,包括大纲、内容创作和质量审核。根据项目协议,这种创作过程会自动通过WebSocket更新进度,最终第3集的内容将显示在画布上。\n\n让我继续关注创作进度:", + "timestamp": "2026-01-28T16:49:00.614398" + }, + { + "role": "user", + "content": "开始生成第3集大纲", + "timestamp": "2026-01-28T17:46:38.247766" + }, + { + "role": "agent", + "content": "我来开始生成第3集的大纲。首先让我创建一个执行计划。现在让我基于全局设定来创作第3集的大纲。## ✅ 第3集大纲已生成\n\n我已经完成了第3集《意外相遇》的大纲创作,大纲已呈现在画布上。\n\n**本集核心情节:**\n- 苏瑾月被林墨轩商议婚事之事烦扰\n- 苏老爷催促婚事,女儿巧妙应对拖延时间\n- 前往寺庙途中马车\"意外\"故障\n- 楚云飞路过出手相助,两人初次相遇\n- 楚云飞修好马车但收钱时展现江湖本色\n- 林墨轩对楚云飞起疑\n- 苏瑾月内心开始动摇,对这位江湖车夫产生好奇\n\n**创作亮点:**\n1. ✨ 体现了苏瑾月的机智与临危不乱\n2. ✨ 建立了楚云飞的侠义形象,暗示其真实身份\n3. ✨ 埋下苏瑾月与楚云飞情感发展的种子\n4. ✨ 强化林墨轩的反派色彩\n\n**已提交审阅:** 我已将大纲提交到您的收件箱,请您审阅并反馈是否需要调整。\n\n您可以:\n- 批准大纲 → 我将开始创作正式剧本\n- 要求修改 → 告诉我具体的修改意见\n- 重新创作 → 我会从新的角度重新设计\n\n期待您的反馈!", + "timestamp": "2026-01-28T17:48:50.985473" + }, + { + "role": "user", + "content": "批准大纲", + "timestamp": "2026-01-28T17:49:11.764238" + }, + { + "role": "agent", + "content": "我先查看一下项目的当前状态,看看有什么大纲需要批准。我看到项目中前三集已完成,后续剧集待创作。不过我需要确认一下:您要批准的是哪个大纲呢?\n\n目前可能的情况:\n1. **全局大纲**(项目上下文中的Overall Outline目前显示为\"11\",似乎不完整)\n2. **某个特定剧集的大纲**\n\n您是指:\n- 如果是全局大纲,我可以帮您完善并提交审批\n- 如果是某个待定剧集的大纲,请告诉我是第几集,我可以立即开始创作并生成大纲供您审批\n\n请指明具体要批准的内容,我会立即为您处理!", + "timestamp": "2026-01-28T17:49:37.624535" + }, + { + "role": "user", + "content": "开始生成第3集大纲", + "timestamp": "2026-01-28T18:30:44.338922" } ] } \ No newline at end of file diff --git a/backend/data/projects.json b/backend/data/projects.json index 8d5776d..57645d6 100644 --- a/backend/data/projects.json +++ b/backend/data/projects.json @@ -17,8 +17,60 @@ }, "memory": { "eventTimeline": [], - "pendingThreads": [], - "foreshadowing": [], + "pendingThreads": [ + { + "id": "881661a2-7372-41ce-9f7a-ed77efe5d92c", + "description": "这个创作失败的原因是什么?是技术故障还是人为因素?", + "introduced_at": 2, + "importance": "high", + "resolved": false, + "resolved_at": null, + "reminder_episode": 3, + "status": "pending", + "characters_involved": [], + "notes": "", + "created_at": "2026-01-28T16:49:57.524646", + "updated_at": "2026-01-28T16:49:57.524646" + }, + { + "id": "015785d6-6f16-4a2f-8260-a8c3515a41c2", + "description": "这个错误是否会影响后续剧情发展?是否会成为解开某个谜题的关键?", + "introduced_at": 2, + "importance": "medium", + "resolved": false, + "resolved_at": null, + "reminder_episode": 4, + "status": "pending", + "characters_involved": [], + "notes": "", + "created_at": "2026-01-28T16:49:57.524646", + "updated_at": "2026-01-28T16:49:57.524646" + } + ], + "foreshadowing": [ + { + "id": "e16f93c7-cc24-47b7-9c58-5fc84fba4409", + "description": "第2集内容创作失败的错误信息可能暗示剧集制作过程中存在技术问题或人为错误", + "introduced_at": 2, + "payoff_at": null, + "type": "plot", + "importance": "medium", + "is_payed_off": false, + "related_events": [], + "notes": "" + }, + { + "id": "8b43d627-9107-430b-aa0f-6d1052ad1511", + "description": "'str' object has no attribute 'items'这个技术错误可能是一个元叙事元素,暗示剧集本身可能存在问题", + "introduced_at": 2, + "payoff_at": null, + "type": "plot", + "importance": "medium", + "is_payed_off": false, + "related_events": [], + "notes": "" + } + ], "characterStates": {} }, "totalEpisodes": 30, @@ -28,6 +80,6 @@ "autoRetryConfig": null, "reviewConfig": null, "createdAt": "2026-01-27T16:22:58.755260", - "updatedAt": "2026-01-28T10:49:27.517654" + "updatedAt": "2026-01-28T16:51:40.612879" } } \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 7fb57a9..32b489d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -50,3 +50,5 @@ pytest-asyncio==0.21.1 black==23.12.0 flake8==6.1.0 mypy==1.7.1 +langchain-community>=0.3.0 +python-multipart==0.0.6 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1195d81..e37dd1b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -35,7 +35,12 @@ function App() { }, }} > - +
void; onNavigateToSettings?: () => void; } @@ -29,6 +30,7 @@ export const ContextPanel: React.FC = ({ loading, activeStates = [], memoryItems = [], + width = 350, onNavigateToSettings }) => { const [activeTab, setActiveTab] = useState('world'); @@ -49,13 +51,14 @@ export const ContextPanel: React.FC = ({ return (
diff --git a/frontend/src/components/Workspace/DirectorInbox.tsx b/frontend/src/components/Workspace/DirectorInbox.tsx index cb44601..3659eb1 100644 --- a/frontend/src/components/Workspace/DirectorInbox.tsx +++ b/frontend/src/components/Workspace/DirectorInbox.tsx @@ -1,13 +1,14 @@ import React, { useState, useEffect, useRef } from 'react'; import { Layout, Input, List, Avatar, Button, Card, Tag, Badge, Tooltip, Divider } from 'antd'; -import { - SendOutlined, - RobotOutlined, - UserOutlined, - CheckCircleOutlined, +import { + SendOutlined, + RobotOutlined, + UserOutlined, + CheckCircleOutlined, CloseCircleOutlined, ExclamationCircleOutlined, - LoadingOutlined + LoadingOutlined, + StopOutlined } from '@ant-design/icons'; const { Sider } = Layout; @@ -15,30 +16,37 @@ const { TextArea } = Input; export interface InboxItem { id: string; - type: 'review' | 'decision' | 'notification'; + type: 'review' | 'decision' | 'notification' | 'review_card'; title: string; description: string; status: 'pending' | 'approved' | 'rejected' | 'ignored'; timestamp: number; options?: string[]; + reviewData?: any; } interface DirectorInboxProps { onSendMessage: (message: string) => void; + onStopAgent?: () => void; onInboxAction?: (itemId: string, action: 'approve' | 'reject') => void; + onNavigateToReview?: (episodeNum: number) => void; agentStatus: 'idle' | 'planning' | 'writing' | 'reviewing'; agentPlan?: string[]; inboxItems?: InboxItem[]; chatHistory?: {role: 'user' | 'agent', content: string}[]; + width?: number; } export const DirectorInbox: React.FC = ({ onSendMessage, + onStopAgent, onInboxAction, + onNavigateToReview, agentStatus, agentPlan = [], inboxItems = [], - chatHistory = [] + chatHistory = [], + width = 400 }) => { const [inputValue, setInputValue] = useState(''); // Use local state for immediate feedback, but sync with props if provided @@ -85,104 +93,151 @@ export const DirectorInbox: React.FC = ({ } }; + const handleInboxItemClick = (item: InboxItem) => { + if (item.type === 'review_card' && item.reviewData) { + if (onNavigateToReview) { + onNavigateToReview(item.reviewData.episode_number); + } + } + }; + return ( - - {/* Agent 状态与计划 */} -
-
- } style={{ backgroundColor: '#1890ff', marginRight: '8px' }} /> -
-
AI Director Agent
- - {agentStatus !== 'idle' && } - {getStatusText(agentStatus)} - -
-
- - {Array.isArray(agentPlan) && agentPlan.length > 0 && ( - -
    - {agentPlan.map((step, idx) => ( -
  • {step}
  • - ))} -
-
- )} -
- - {/* 导演信箱 (Inbox) */} -
- 待处理任务 (Inbox) - - {Array.isArray(inboxItems) && inboxItems.map(item => ( -