diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 99e8df4..eb556ed 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -8,7 +8,12 @@ "Bash(npx tsc:*)", "Bash(findstr:*)", "Bash(tree:*)", - "Bash(npx eslint:*)" + "Bash(npx eslint:*)", + "Bash(curl:*)", + "Bash(node:*)", + "Bash(netstat:*)", + "Bash(tail:*)", + "Bash(tasklist:*)" ] } } diff --git a/backend/app/api/v1/ai_async.py b/backend/app/api/v1/ai_async.py index 8b1d45b..afae2a4 100644 --- a/backend/app/api/v1/ai_async.py +++ b/backend/app/api/v1/ai_async.py @@ -103,7 +103,7 @@ async def execute_generate_characters( {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt} ], - temperature=0.7 + temperature=0.9 ) task_manager.update_task_progress( @@ -188,7 +188,7 @@ async def execute_generate_outline( {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt} ], - temperature=0.7 + temperature=0.9 ) task_manager.update_task_progress( @@ -268,7 +268,7 @@ async def execute_generate_world( {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt} ], - temperature=0.7 + temperature=0.9 ) task_manager.update_task_progress( diff --git a/backend/app/models/project.py b/backend/app/models/project.py index 203c23c..def70bb 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -56,6 +56,8 @@ class GlobalContext(BaseModel): sceneSettings: Dict[str, SceneSetting] = Field(default_factory=dict) overallOutline: str = "" styleGuide: str = "" + uploadedScript: str = "" # 上传的剧本内容 + inspiration: str = "" # 创意灵感内容 class Memory(BaseModel): diff --git a/backend/app/models/task.py b/backend/app/models/task.py index 9378e3f..cd56bec 100644 --- a/backend/app/models/task.py +++ b/backend/app/models/task.py @@ -71,7 +71,7 @@ class TaskCreateRequest(BaseModel): class TaskResponse(BaseModel): """任务响应""" - id: str + id: str = Field(serialization_alias="taskId") type: TaskType status: TaskStatus progress: TaskProgress diff --git a/frontend/src/components/TaskProgressTracker.tsx b/frontend/src/components/TaskProgressTracker.tsx index 98a3cc5..7a6bf01 100644 --- a/frontend/src/components/TaskProgressTracker.tsx +++ b/frontend/src/components/TaskProgressTracker.tsx @@ -106,7 +106,7 @@ export const TaskProgressTracker = ({ clearTimeout(pollTimer) } } - }, [taskId, pollInterval, onComplete, onError]) + }, [taskId, pollInterval]) // 获取状态图标 const getStatusIcon = (status: string) => { @@ -284,7 +284,7 @@ export const StageProgressTracker = ({ clearTimeout(pollTimer) } } - }, [taskId, pollInterval, onComplete, onError]) + }, [taskId, pollInterval]) // 获取当前阶段索引 const getCurrentStageIndex = () => { diff --git a/frontend/src/pages/ProjectCreateEnhanced.tsx b/frontend/src/pages/ProjectCreateEnhanced.tsx index 488a93b..971ac6d 100644 --- a/frontend/src/pages/ProjectCreateEnhanced.tsx +++ b/frontend/src/pages/ProjectCreateEnhanced.tsx @@ -7,19 +7,18 @@ import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { Form, Input, InputNumber, Button, Card, Steps, message, - Upload, Select, Space, Divider, Tag, Alert, Popover, Progress, Modal, Radio + Upload, Select, Space, Divider, Tag, Alert, Popover, Progress, Radio } from 'antd' import { UploadOutlined, FileTextOutlined, RobotOutlined, CheckCircleOutlined, ArrowRightOutlined, SettingOutlined, - ThunderboltOutlined, CloseOutlined + ThunderboltOutlined, EditOutlined } from '@ant-design/icons' import { useProjectStore } from '@/stores/projectStore' import { useSkillStore } from '@/stores/skillStore' import { api } from '@/services' import { ProjectCreateRequest } from '@/services/projectService' import { taskService } from '@/services/taskService' -import { TaskProgressTracker, TaskResultDisplay } from '@/components/TaskProgressTracker' const { TextArea } = Input const { Step } = Steps @@ -253,8 +252,6 @@ export const ProjectCreateEnhanced = () => { // 异步任务状态 const [currentTaskId, setCurrentTaskId] = useState(null) - const [showTaskProgress, setShowTaskProgress] = useState(false) - const [taskResult, setTaskResult] = useState(null) const [taskType, setTaskType] = useState('') useEffect(() => { @@ -378,9 +375,13 @@ export const ProjectCreateEnhanced = () => { } // AI 生成人物设定 - // 启动异步AI生成人物任务 const handleAIGenerateCharacters = async () => { + // 防止连续触发 + if (aiGeneratingCharacters) return + setAiGeneratingCharacters(true) + message.loading('AI 正在生成人物设定...', 0) + try { const projectName = form.getFieldValue('name') || '未命名项目' const totalEpisodes = form.getFieldValue('totalEpisodes') || 30 @@ -401,67 +402,92 @@ export const ProjectCreateEnhanced = () => { customPrompt: characterPrompt || undefined }) - // 显示任务进度 + // 设置任务ID和类型,用于后续查询结果 setCurrentTaskId(response.taskId) setTaskType('generate_characters') - setShowTaskProgress(true) - message.info('AI 生成任务已启动,正在后台执行...') + // 轮询任务结果 + let attempts = 0 + const maxAttempts = 60 // 最多等待60秒 + + const pollResult = async (): Promise => { + attempts++ + + try { + const result = await api.get(`/tasks/${response.taskId}`) + + if (result.status === 'completed') { + return result.result + } else if (result.status === 'failed') { + throw new Error(result.error || '任务执行失败') + } else if (attempts >= maxAttempts) { + throw new Error('任务执行超时') + } else { + // 继续轮询 + await new Promise(resolve => setTimeout(resolve, 1000)) + return pollResult() + } + } catch (error: any) { + throw error + } + } + + const result = await pollResult() + + // 处理结果 + let characterText = '' + + if (Array.isArray(result?.characters)) { + characterText = result.characters.map((c: any) => { + if (typeof c === 'string') return c + if (c.name) { + return c.lines ? `【${c.name}】出场 ${c.lines} 次` : `【${c.name}】` + } + return JSON.stringify(c) + }).join('\n') + } else if (typeof result?.characters === 'string') { + characterText = result.characters + } else if (typeof result === 'string') { + characterText = result + } else if (Array.isArray(result)) { + characterText = result.map((c: any) => { + if (typeof c === 'string') return c + if (c.name) { + return c.lines ? `【${c.name}】出场 ${c.lines} 次` : `【${c.name}】` + } + return JSON.stringify(c) + }).join('\n') + } + + if (characterText) { + setAiGeneratedContent((prev: any) => ({ ...prev, characters: characterText })) + form.setFieldsValue({ characters: characterText }) + + // 关闭loading并显示成功提示 + message.destroy() + message.success('人物设定生成完成!', 1.5) + } else { + message.destroy() + message.warning('生成完成,但未返回有效内容') + } } catch (error: any) { - console.error('创建任务失败:', error) - message.error(`创建任务失败: ${error.message || '未知错误'}`) + console.error('AI 生成人物设定失败:', error) + message.destroy() + message.error(`生成失败: ${error.message || '未知错误'}`) } finally { setAiGeneratingCharacters(false) } } - // 任务完成回调 - const handleTaskComplete = (result: any) => { - setShowTaskProgress(false) - - if (taskType === 'generate_characters') { - const characters = result?.characters || result - if (characters) { - setAiGeneratedContent((prev: any) => ({ ...prev, characters })) - form.setFieldsValue({ characters }) - message.success('人物设定生成完成!') - } - } else if (taskType === 'generate_outline') { - const outline = result?.outline || result - if (outline) { - setAiGeneratedContent((prev: any) => ({ ...prev, outline })) - form.setFieldsValue({ overallOutline: outline }) - message.success('剧情大纲生成完成!') - } - } else if (taskType === 'generate_world') { - const worldSetting = result?.worldSetting || result - if (worldSetting) { - setAiGeneratedContent((prev: any) => ({ ...prev, worldSetting })) - form.setFieldsValue({ worldSetting }) - message.success('世界观设定生成完成!') - } - } - - setTaskResult(result) - } - - // 任务失败回调 - const handleTaskError = (error: string) => { - setShowTaskProgress(false) - message.error(`任务执行失败: ${error}`) - } - - // 关闭任务进度对话框 - const handleCloseTaskProgress = () => { - setShowTaskProgress(false) - } - // AI 分析上传的剧本 const handleAIAnalyzeScript = async () => { - setAiAnalyzingScript(true) - try { - console.log('开始调用 AI 分析剧本...') + // 防止连续触发 + if (aiAnalyzingScript) return + setAiAnalyzingScript(true) + message.loading('AI 正在分析剧本...', 0) + + try { // 获取剧本分析配置的技能信息 const selectedSkillsInfo = skills.filter(s => analyzeSkills.includes(s.id)) @@ -477,8 +503,6 @@ export const ProjectCreateEnhanced = () => { customPrompt: analyzePrompt || undefined }) as any - console.log('AI 剧本分析响应:', JSON.stringify(data, null, 2)) - // 提取分析结果 const characters = data?.characters || [] const outline = data?.outline || '' @@ -492,21 +516,23 @@ export const ProjectCreateEnhanced = () => { ).join('\n') } - console.log('分析结果 - 人物:', characterText, '概要:', summary) - // 更新表单字段 if (characterText) { form.setFieldsValue({ characters: characterText }) setAiGeneratedContent((prev: any) => ({ ...prev, characters: characterText })) } if (outline) { - form.setFieldsValue({ overallOutline: outline }) - setAiGeneratedContent((prev: any) => ({ ...prev, outline })) + const outlineText = typeof outline === 'string' ? outline : JSON.stringify(outline, null, 2) + form.setFieldsValue({ overallOutline: outlineText }) + setAiGeneratedContent((prev: any) => ({ ...prev, outline: outlineText })) } - message.success(`AI 分析完成!${summary}`) + // 关闭loading并显示成功提示 + message.destroy() + message.success(`AI 分析完成!${summary}`, 1.5) } catch (error: any) { console.error('AI 分析失败,完整错误:', error) + message.destroy() message.error(`AI 分析失败: ${error.message || '未知错误'}`) } finally { setAiAnalyzingScript(false) @@ -515,10 +541,13 @@ export const ProjectCreateEnhanced = () => { // AI 从剧本分析世界观 const handleAIAnalyzeWorldFromScript = async () => { - setAiAnalyzingWorldFromScript(true) - try { - console.log('开始调用 AI 分析世界观...') + // 防止连续触发 + if (aiAnalyzingWorldFromScript) return + setAiAnalyzingWorldFromScript(true) + message.loading('AI 正在分析世界观...', 0) + + try { // 获取世界观生成配置的技能信息 const selectedSkillsInfo = skills.filter(s => worldSkills.includes(s.id)) @@ -535,13 +564,10 @@ export const ProjectCreateEnhanced = () => { customPrompt: worldPrompt || undefined }) as any - console.log('AI 世界观分析响应:', JSON.stringify(data, null, 2)) - const worldSetting = data?.worldSetting || data?.result || data?.content || '' - console.log('提取的世界观:', worldSetting?.substring(0, 100) + '...') if (!worldSetting) { - console.error('未能从响应中提取世界观,响应数据:', data) + message.destroy() message.error('AI 分析失败:未返回有效内容') return } @@ -550,9 +576,12 @@ export const ProjectCreateEnhanced = () => { form.setFieldsValue({ worldSetting }) setAiGeneratedContent((prev: any) => ({ ...prev, worldSetting })) - message.success('AI 世界观分析完成!') + // 关闭loading并显示成功提示 + message.destroy() + message.success('AI 世界观分析完成!', 1.5) } catch (error: any) { console.error('AI 世界观分析失败,完整错误:', error) + message.destroy() message.error(`AI 分析失败: ${error.message || '未知错误'}`) } finally { setAiAnalyzingWorldFromScript(false) @@ -561,7 +590,12 @@ export const ProjectCreateEnhanced = () => { // AI 生成世界观 const handleAIGenerateWorldSetting = async () => { + // 防止连续触发 + if (aiGeneratingWorldSetting) return + setAiGeneratingWorldSetting(true) + message.loading('AI 正在生成世界观设定...', 0) + try { const projectName = form.getFieldValue('name') || '未命名项目' const selectedSkillsInfo = skills.filter(s => worldSkills.includes(s.id)) @@ -581,15 +615,57 @@ export const ProjectCreateEnhanced = () => { customPrompt: worldPrompt || undefined }) - // 显示任务进度 + // 设置任务ID和类型,用于后续查询结果 setCurrentTaskId(response.taskId) setTaskType('generate_world') - setShowTaskProgress(true) - message.info('AI 生成任务已启动,正在后台执行...') + // 轮询任务结果 + let attempts = 0 + const maxAttempts = 60 // 最多等待60秒 + + const pollResult = async (): Promise => { + attempts++ + + try { + const result = await api.get(`/tasks/${response.taskId}`) + + if (result.status === 'completed') { + return result.result + } else if (result.status === 'failed') { + throw new Error(result.error || '任务执行失败') + } else if (attempts >= maxAttempts) { + throw new Error('任务执行超时') + } else { + // 继续轮询 + await new Promise(resolve => setTimeout(resolve, 1000)) + return pollResult() + } + } catch (error: any) { + throw error + } + } + + const result = await pollResult() + + // 处理结果 + const worldSetting = result?.worldSetting || result + const worldText = typeof worldSetting === 'string' ? worldSetting : JSON.stringify(worldSetting, null, 2) + + if (worldText) { + setAiGeneratedContent((prev: any) => ({ ...prev, worldSetting: worldText })) + form.setFieldsValue({ worldSetting: worldText }) + + // 关闭loading并显示成功提示 + message.destroy() + message.success('世界观设定生成完成!', 1.5) + } else { + message.destroy() + message.warning('生成完成,但未返回有效内容') + } } catch (error: any) { - console.error('创建任务失败:', error) - message.error(`创建任务失败: ${error.message || '未知错误'}`) + console.error('AI 生成世界观设定失败:', error) + message.destroy() + message.error(`生成失败: ${error.message || '未知错误'}`) } finally { setAiGeneratingWorldSetting(false) } @@ -597,7 +673,12 @@ export const ProjectCreateEnhanced = () => { // AI 生成大纲 const handleAIGenerateOutline = async () => { + // 防止连续触发 + if (aiGeneratingOutline) return + setAiGeneratingOutline(true) + message.loading('AI 正在生成剧情大纲...', 0) + try { const projectName = form.getFieldValue('name') || '未命名项目' const totalEpisodes = form.getFieldValue('totalEpisodes') || 30 @@ -619,15 +700,57 @@ export const ProjectCreateEnhanced = () => { customPrompt: outlinePrompt || undefined }) - // 显示任务进度 + // 设置任务ID和类型,用于后续查询结果 setCurrentTaskId(response.taskId) setTaskType('generate_outline') - setShowTaskProgress(true) - message.info('AI 生成任务已启动,正在后台执行...') + // 轮询任务结果 + let attempts = 0 + const maxAttempts = 60 // 最多等待60秒 + + const pollResult = async (): Promise => { + attempts++ + + try { + const result = await api.get(`/tasks/${response.taskId}`) + + if (result.status === 'completed') { + return result.result + } else if (result.status === 'failed') { + throw new Error(result.error || '任务执行失败') + } else if (attempts >= maxAttempts) { + throw new Error('任务执行超时') + } else { + // 继续轮询 + await new Promise(resolve => setTimeout(resolve, 1000)) + return pollResult() + } + } catch (error: any) { + throw error + } + } + + const result = await pollResult() + + // 处理结果 + const outline = result?.outline || result + const outlineText = typeof outline === 'string' ? outline : JSON.stringify(outline, null, 2) + + if (outlineText) { + setAiGeneratedContent((prev: any) => ({ ...prev, outline: outlineText })) + form.setFieldsValue({ overallOutline: outlineText }) + + // 关闭loading并显示成功提示 + message.destroy() + message.success('剧情大纲生成完成!', 1.5) + } else { + message.destroy() + message.warning('生成完成,但未返回有效内容') + } } catch (error: any) { - console.error('创建任务失败:', error) - message.error(`创建任务失败: ${error.message || '未知错误'}`) + console.error('AI 生成剧情大纲失败:', error) + message.destroy() + message.error(`生成失败: ${error.message || '未知错误'}`) } finally { setAiGeneratingOutline(false) } @@ -665,8 +788,9 @@ export const ProjectCreateEnhanced = () => { const worldSetting = data?.worldSetting || data?.result || data?.content || '' if (worldSetting) { - form.setFieldsValue({ worldSetting }) - setAiGeneratedContent((prev: any) => ({ ...prev, worldSetting })) + const worldText = typeof worldSetting === 'string' ? worldSetting : JSON.stringify(worldSetting, null, 2) + form.setFieldsValue({ worldSetting: worldText }) + setAiGeneratedContent((prev: any) => ({ ...prev, worldSetting: worldText })) } } catch (error) { console.error('生成世界观失败:', error) @@ -718,8 +842,22 @@ export const ProjectCreateEnhanced = () => { const characters = data?.characters || data?.result || data?.content || '' if (characters) { - form.setFieldsValue({ characters }) - setAiGeneratedContent((prev: any) => ({ ...prev, characters })) + let characterText = '' + if (typeof characters === 'string') { + characterText = characters + } else if (Array.isArray(characters)) { + characterText = characters.map((c: any) => { + if (typeof c === 'string') return c + if (c.name) { + return c.lines ? `【${c.name}】出场 ${c.lines} 次` : `【${c.name}】` + } + return JSON.stringify(c) + }).join('\n') + } else { + characterText = JSON.stringify(characters, null, 2) + } + form.setFieldsValue({ characters: characterText }) + setAiGeneratedContent((prev: any) => ({ ...prev, characters: characterText })) } } } catch (error) { @@ -749,8 +887,9 @@ export const ProjectCreateEnhanced = () => { const outline = data?.outline || data?.analysis || '' if (outline) { - form.setFieldsValue({ overallOutline: outline }) - setAiGeneratedContent((prev: any) => ({ ...prev, outline })) + const outlineText = typeof outline === 'string' ? outline : JSON.stringify(outline, null, 2) + form.setFieldsValue({ overallOutline: outlineText }) + setAiGeneratedContent((prev: any) => ({ ...prev, outline: outlineText })) } } else { // 从头创作模式:生成大纲 @@ -769,8 +908,9 @@ export const ProjectCreateEnhanced = () => { const outline = data?.outline || data?.result || data?.content || '' if (outline) { - form.setFieldsValue({ overallOutline: outline }) - setAiGeneratedContent((prev: any) => ({ ...prev, outline })) + const outlineText = typeof outline === 'string' ? outline : JSON.stringify(outline, null, 2) + form.setFieldsValue({ overallOutline: outlineText }) + setAiGeneratedContent((prev: any) => ({ ...prev, outline: outlineText })) } } } catch (error) { @@ -849,12 +989,29 @@ export const ProjectCreateEnhanced = () => { } } + // 处理灵感内容(如果是文件上传,需要读取内容) + let inspirationContent = directTextInput || '' + if (createSubMode === 'file' && inspirationFile) { + // 灵感文件内容已经在 handleInspirationUpload 中读取 + // 这里使用 originFileObj 来读取 + const reader = new FileReader() + const fileContent = await new Promise((resolve) => { + reader.onload = (e) => resolve(e.target?.result as string || '') + reader.readAsText(inspirationFile.originFileObj || inspirationFile) + }) + inspirationContent = fileContent + } + const projectData: ProjectCreateRequest = { name: values.name, totalEpisodes: values.totalEpisodes || 30, globalContext: { worldSetting: values.worldSetting || '', overallOutline: values.overallOutline || '', + // 保存创作内容 + uploadedScript: uploadedScript || '', // 上传的剧本 + inspiration: inspirationContent, // 灵感内容(直接输入或文件) + styleGuide: values.characters || '', // 人物设定 characterProfiles: {}, sceneSettings: {} }, @@ -1589,7 +1746,11 @@ export const ProjectCreateEnhanced = () => { maxHeight: '200px', overflowY: 'auto' }}> - {aiGeneratedContent.characters || '未设置'} + {typeof aiGeneratedContent.characters === 'string' + ? aiGeneratedContent.characters + : aiGeneratedContent.characters + ? JSON.stringify(aiGeneratedContent.characters, null, 2) + : '未设置'} @@ -1602,7 +1763,11 @@ export const ProjectCreateEnhanced = () => { maxHeight: '200px', overflowY: 'auto' }}> - {aiGeneratedContent.outline || '未设置'} + {typeof aiGeneratedContent.outline === 'string' + ? aiGeneratedContent.outline + : aiGeneratedContent.outline + ? JSON.stringify(aiGeneratedContent.outline, null, 2) + : '未设置'} @@ -1639,25 +1804,6 @@ export const ProjectCreateEnhanced = () => { )} - - {/* 异步任务进度对话框 */} - } - > - {currentTaskId && ( - - )} - ) } diff --git a/frontend/src/pages/ProjectDetail.tsx b/frontend/src/pages/ProjectDetail.tsx index c4f6495..ce0e9ce 100644 --- a/frontend/src/pages/ProjectDetail.tsx +++ b/frontend/src/pages/ProjectDetail.tsx @@ -16,9 +16,8 @@ import { useProjectStore } from '@/stores/projectStore' import { useSkillStore } from '@/stores/skillStore' import { Episode } from '@/services/projectService' import { taskService } from '@/services/taskService' -import { TaskProgressTracker } from '@/components/TaskProgressTracker' -const { TextParagraph } = Typography +const { Paragraph } = Typography const { TabPane } = Tabs const { TextArea } = Input @@ -129,10 +128,9 @@ export const ProjectDetail = () => { // 全局设定生成相关状态 const [globalForm] = Form.useForm() - const [generating, setGenerating] = useState(false) - const [currentTaskId, setCurrentTaskId] = useState(null) - const [showTaskProgress, setShowTaskProgress] = useState(false) - const [taskType, setTaskType] = useState(null) + const [generatingWorld, setGeneratingWorld] = useState(false) + const [generatingCharacters, setGeneratingCharacters] = useState(false) + const [generatingOutline, setGeneratingOutline] = useState(false) // Skills 配置 const [worldSkills, setWorldSkills] = useState([]) @@ -180,7 +178,7 @@ export const ProjectDetail = () => { // 初始化全局设定表单 globalForm.setFieldsValue({ worldSetting: currentProject.globalContext?.worldSetting || '', - characters: currentProject.globalContext?.characterProfiles || '', + characters: currentProject.globalContext?.styleGuide || '', overallOutline: currentProject.globalContext?.overallOutline || '' }) } @@ -273,19 +271,29 @@ export const ProjectDetail = () => { // AI生成/分析世界观 const handleGenerateWorld = async () => { if (!id) return + if (generatingWorld) return // 防止重复点击 const projectName = currentProject?.name || '未命名项目' const selectedSkillsInfo = skills.filter(s => worldSkills.includes(s.id)) - // 根据创作方式决定是分析还是生成 + // 根据创作方式决定是分析还是生成(从项目数据中实时获取最新内容) + const projectScript = currentProject?.globalContext?.uploadedScript || '' + const projectInspiration = currentProject?.globalContext?.inspiration || '' + + // 如果有编辑中的内容,优先使用;否则使用项目中保存的内容 + const finalScript = scriptContent || projectScript + const finalInspiration = inspirationContent || projectInspiration + const isAnalysis = creationMode === 'script' - const baseContent = isAnalysis ? scriptContent : inspirationContent + const baseContent = isAnalysis ? finalScript : finalInspiration const idea = isAnalysis ? `分析以下剧本,提取世界观设定:\n${baseContent?.substring(0, 2000)}` : `项目名称:${projectName}\n创意灵感:\n${baseContent}` + const hideMessage = message.loading(isAnalysis ? '正在分析剧本...' : '正在生成世界观设定...', 0) + try { - setGenerating(true) + setGeneratingWorld(true) const response = await taskService.generateWorld({ idea, projectName, @@ -299,31 +307,58 @@ export const ProjectDetail = () => { projectId: id }) - setCurrentTaskId(response.taskId) - setTaskType('generate_world') - setShowTaskProgress(true) + // 轮询任务直到完成 + const result = await taskService.pollTask(response.taskId) + + const worldSetting = result?.worldSetting || result + const worldText = typeof worldSetting === 'string' ? worldSetting : JSON.stringify(worldSetting, null, 2) + globalForm.setFieldsValue({ worldSetting: worldText }) + + hideMessage() + message.success(isAnalysis ? '世界观分析完成!' : '世界观设定生成完成!') } catch (error: any) { - message.error(`创建任务失败: ${error.message || '未知错误'}`) - setGenerating(false) + hideMessage() + message.error(`生成失败: ${error.message || '未知错误'}`) + } finally { + setGeneratingWorld(false) } } // AI生成/分析人物设定 const handleGenerateCharacters = async () => { if (!id) return + if (generatingCharacters) return // 防止重复点击 const projectName = currentProject?.name || '未命名项目' const totalEpisodes = currentProject?.totalEpisodes || 30 const selectedSkillsInfo = skills.filter(s => characterSkills.includes(s.id)) + // 获取已生成的世界观设定,保证一致性 + const worldSetting = globalForm.getFieldValue('worldSetting') || '' + + // 从项目数据中实时获取最新内容 + const projectScript = currentProject?.globalContext?.uploadedScript || '' + const projectInspiration = currentProject?.globalContext?.inspiration || '' + + // 如果有编辑中的内容,优先使用;否则使用项目中保存的内容 + const finalScript = scriptContent || projectScript + const finalInspiration = inspirationContent || projectInspiration + const isAnalysis = creationMode === 'script' - const baseContent = isAnalysis ? scriptContent : inspirationContent - const idea = isAnalysis + const baseContent = isAnalysis ? finalScript : finalInspiration + let idea = isAnalysis ? `分析以下剧本,提取人物设定:\n${baseContent?.substring(0, 2000)}` : `项目名称:${projectName},总集数:${totalEpisodes}集\n创意灵感:\n${baseContent}` + // 如果已有世界观设定,将其作为上下文 + if (worldSetting && !isAnalysis) { + idea += `\n\n【世界观设定】\n${worldSetting}` + } + + const hideMessage = message.loading(isAnalysis ? '正在分析人物设定...' : '正在生成人物设定...', 0) + try { - setGenerating(true) + setGeneratingCharacters(true) const response = await taskService.generateCharacters({ idea, projectName, @@ -337,31 +372,77 @@ export const ProjectDetail = () => { projectId: id }) - setCurrentTaskId(response.taskId) - setTaskType('generate_characters') - setShowTaskProgress(true) + // 轮询任务直到完成 + const result = await taskService.pollTask(response.taskId) + + const characters = result?.characters || result + let characterText = '' + if (typeof characters === 'string') { + characterText = characters + } else if (Array.isArray(characters)) { + characterText = characters.map((c: any) => { + if (typeof c === 'string') return c + if (c.name) { + return c.lines ? `【${c.name}】出场 ${c.lines} 次` : `【${c.name}】` + } + return JSON.stringify(c) + }).join('\n') + } else { + characterText = JSON.stringify(characters, null, 2) + } + globalForm.setFieldsValue({ characters: characterText }) + + hideMessage() + message.success(isAnalysis ? '人物设定分析完成!' : '人物设定生成完成!') } catch (error: any) { - message.error(`创建任务失败: ${error.message || '未知错误'}`) - setGenerating(false) + hideMessage() + message.error(`生成失败: ${error.message || '未知错误'}`) + } finally { + setGeneratingCharacters(false) } } // AI生成/分析大纲 const handleGenerateOutline = async () => { if (!id) return + if (generatingOutline) return // 防止重复点击 const projectName = currentProject?.name || '未命名项目' const totalEpisodes = currentProject?.totalEpisodes || 30 const selectedSkillsInfo = skills.filter(s => outlineSkills.includes(s.id)) + // 获取已生成的世界观设定和人物设定,保证一致性 + const worldSetting = globalForm.getFieldValue('worldSetting') || '' + const characters = globalForm.getFieldValue('characters') || '' + + // 从项目数据中实时获取最新内容 + const projectScript = currentProject?.globalContext?.uploadedScript || '' + const projectInspiration = currentProject?.globalContext?.inspiration || '' + + // 如果有编辑中的内容,优先使用;否则使用项目中保存的内容 + const finalScript = scriptContent || projectScript + const finalInspiration = inspirationContent || projectInspiration + const isAnalysis = creationMode === 'script' - const baseContent = isAnalysis ? scriptContent : inspirationContent - const idea = isAnalysis + const baseContent = isAnalysis ? finalScript : finalInspiration + let idea = isAnalysis ? `分析以下剧本,提取整体大纲:\n${baseContent?.substring(0, 2000)}` : `项目名称:${projectName},总集数:${totalEpisodes}集\n创意灵感:\n${baseContent}` + // 如果已有世界观设定和人物设定,将其作为上下文 + if (!isAnalysis) { + if (worldSetting) { + idea += `\n\n【世界观设定】\n${worldSetting}` + } + if (characters) { + idea += `\n\n【人物设定】\n${characters}` + } + } + + const hideMessage = message.loading(isAnalysis ? '正在分析整体大纲...' : '正在生成整体大纲...', 0) + try { - setGenerating(true) + setGeneratingOutline(true) const response = await taskService.generateOutline({ idea, totalEpisodes, @@ -376,40 +457,21 @@ export const ProjectDetail = () => { projectId: id }) - setCurrentTaskId(response.taskId) - setTaskType('generate_outline') - setShowTaskProgress(true) - } catch (error: any) { - message.error(`创建任务失败: ${error.message || '未知错误'}`) - setGenerating(false) - } - } + // 轮询任务直到完成 + const result = await taskService.pollTask(response.taskId) - // 任务完成回调 - const handleTaskComplete = (result: any) => { - setShowTaskProgress(false) - setGenerating(false) - - if (taskType === 'generate_world') { - const worldSetting = result?.worldSetting || result - globalForm.setFieldsValue({ worldSetting }) - message.success('世界观设定生成完成!') - } else if (taskType === 'generate_characters') { - const characters = result?.characters || result - globalForm.setFieldsValue({ characters }) - message.success('人物设定生成完成!') - } else if (taskType === 'generate_outline') { const outline = result?.outline || result - globalForm.setFieldsValue({ overallOutline: outline }) - message.success('大纲生成完成!') - } - } + const outlineText = typeof outline === 'string' ? outline : JSON.stringify(outline, null, 2) + globalForm.setFieldsValue({ overallOutline: outlineText }) - // 任务失败回调 - const handleTaskError = (error: string) => { - setShowTaskProgress(false) - setGenerating(false) - message.error(`任务执行失败: ${error}`) + hideMessage() + message.success(isAnalysis ? '整体大纲分析完成!' : '整体大纲生成完成!') + } catch (error: any) { + hideMessage() + message.error(`生成失败: ${error.message || '未知错误'}`) + } finally { + setGeneratingOutline(false) + } } // 保存全局设定 @@ -424,8 +486,9 @@ export const ProjectDetail = () => { ...currentProject?.globalContext, worldSetting: worldSetting || '', overallOutline: overallOutline || '', - characterProfiles: characters ? {} : {}, - sceneSettings: {} + styleGuide: characters || '', + characterProfiles: currentProject?.globalContext?.characterProfiles || {}, + sceneSettings: currentProject?.globalContext?.sceneSettings || {} } }) @@ -812,9 +875,9 @@ export const ProjectDetail = () => { @@ -852,9 +915,9 @@ export const ProjectDetail = () => { @@ -892,9 +955,9 @@ export const ProjectDetail = () => { @@ -1059,25 +1122,6 @@ export const ProjectDetail = () => { )} - - {/* AI生成任务进度弹窗 */} - setShowTaskProgress(false)} - footer={null} - width={600} - closable={!generating} - > - {currentTaskId && ( - - )} - ) } diff --git a/frontend/src/stores/projectStore.ts b/frontend/src/stores/projectStore.ts index d03f8cb..fb5c156 100644 --- a/frontend/src/stores/projectStore.ts +++ b/frontend/src/stores/projectStore.ts @@ -9,6 +9,7 @@ interface ProjectStore { currentProject: SeriesProject | null episodes: Episode[] loading: boolean + loadingEpisodes: boolean error: string | null // Actions @@ -27,6 +28,7 @@ export const useProjectStore = create((set, get) => ({ currentProject: null, episodes: [], loading: false, + loadingEpisodes: false, error: null, fetchProjects: async () => { @@ -93,12 +95,12 @@ export const useProjectStore = create((set, get) => ({ }, fetchEpisodes: async (projectId: string) => { - set({ loading: true, error: null }) + set({ loadingEpisodes: true, error: null }) try { const episodes = await projectService.listEpisodes(projectId) - set({ episodes, loading: false }) + set({ episodes, loadingEpisodes: false }) } catch (error) { - set({ error: (error as Error).message, loading: false }) + set({ error: (error as Error).message, loadingEpisodes: false }) } },