剧本内容
diff --git a/frontend/src/pages/ProjectCreateEnhanced.tsx b/frontend/src/pages/ProjectCreateEnhanced.tsx
index 800a232..a60a5eb 100644
--- a/frontend/src/pages/ProjectCreateEnhanced.tsx
+++ b/frontend/src/pages/ProjectCreateEnhanced.tsx
@@ -1,26 +1,123 @@
/**
* 增强的项目创建页面
* 支持:上传剧本、AI 辅助生成、每个生成点独立配置 Skill
+ * 新增:一键生成所有内容、长剧本分段分析、异步任务、进度追踪
*/
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
+ Upload, Select, Space, Divider, Tag, Alert, Popover, Progress, Modal, Radio
} from 'antd'
import {
UploadOutlined, FileTextOutlined, RobotOutlined,
- CheckCircleOutlined, ArrowRightOutlined, SettingOutlined
+ CheckCircleOutlined, ArrowRightOutlined, SettingOutlined,
+ ThunderboltOutlined, CloseOutlined
} 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
-// Skill 选择器组件
+// 默认 Skill 配置(用于一键生成)
+const DEFAULT_SKILLS = {
+ worldSetting: ['world_builder'], // 世界观生成默认技能
+ characters: ['character_creator'], // 人物生成默认技能
+ outline: ['story_structurer'] // 大纲生成默认技能
+}
+
+// Skill 选择器和自定义提示词组件
+const SkillSelectorWithPrompt = ({
+ title,
+ icon,
+ skills,
+ selectedSkills,
+ onSkillsChange,
+ customPrompt,
+ onPromptChange,
+ description
+}: {
+ title: string
+ icon: React.ReactNode
+ skills: any[]
+ selectedSkills: string[]
+ onSkillsChange: (skills: string[]) => void
+ customPrompt: string
+ onPromptChange: (prompt: string) => void
+ description?: string
+}) => {
+ const content = (
+
+
+ {description || '选择用于此 AI 生成任务的技能'}
+
+
+ )
+
+ return (
+
+
+
+ )
+}
+
+// 保留原有的 Skill 选择器组件(用于向后兼容)
const SkillSelector = ({
title,
icon,
@@ -93,7 +190,7 @@ const SkillSelector = ({
export const ProjectCreateEnhanced = () => {
const navigate = useNavigate()
const createProject = useProjectStore(state => state.createProject)
- const { skills, fetchSkills } = useSkillStore()
+ const { skills, fetchSkills, loading: skillsLoading, error: skillsError } = useSkillStore()
const [currentStep, setCurrentStep] = useState(0)
const [loading, setLoading] = useState(false)
@@ -102,20 +199,49 @@ export const ProjectCreateEnhanced = () => {
const [aiGeneratingWorldSetting, setAiGeneratingWorldSetting] = useState(false)
const [aiAnalyzingScript, setAiAnalyzingScript] = useState(false)
const [aiAnalyzingWorldFromScript, setAiAnalyzingWorldFromScript] = useState(false)
+
+ // 一键生成相关状态
+ const [isGeneratingAll, setIsGeneratingAll] = useState(false)
+ const [hasGeneratedAll, setHasGeneratedAll] = useState(false)
+ const [generateProgress, setGenerateProgress] = useState({
+ current: 0,
+ total: 3,
+ currentStep: ''
+ })
+
const [form] = Form.useForm()
// 创作模式: 'upload' | 'create' | null
const [creationMode, setCreationMode] = useState<'upload' | 'create' | null>(null)
+ // 从头创作的子模式: 'text' | 'file' | null
+ const [createSubMode, setCreateSubMode] = useState<'text' | 'file' | null>(null)
+
// 上传的剧本内容
const [uploadedScript, setUploadedScript] = useState('')
+ // 直接输入的创作文字
+ const [directTextInput, setDirectTextInput] = useState('')
+
+ // 上传的灵感文件
+ const [inspirationFile, setInspirationFile] = useState
(null)
+ const [inspirationFileType, setInspirationFileType] = useState<'story' | 'character' | 'scene' | 'dialogue'>('story')
+
+ // 剧本预览展开状态
+ const [scriptExpanded, setScriptExpanded] = useState(false)
+
// 每个 AI 功能独立配置的 Skills
const [characterSkills, setCharacterSkills] = useState([])
const [outlineSkills, setOutlineSkills] = useState([])
const [worldSkills, setWorldSkills] = useState([])
const [analyzeSkills, setAnalyzeSkills] = useState([])
+ // 自定义提示词
+ const [characterPrompt, setCharacterPrompt] = useState('')
+ const [outlinePrompt, setOutlinePrompt] = useState('')
+ const [worldPrompt, setWorldPrompt] = useState('')
+ const [analyzePrompt, setAnalyzePrompt] = useState('')
+
// AI 辅助生成的内容
const [aiGeneratedContent, setAiGeneratedContent] = useState({
characters: '',
@@ -125,8 +251,25 @@ export const ProjectCreateEnhanced = () => {
// 项目级别的 Skills(用于后续创作流程)
const [projectSkills, setProjectSkills] = useState(['dialogue_writer_ancient', 'consistency_checker'])
+ // 异步任务状态
+ const [currentTaskId, setCurrentTaskId] = useState(null)
+ const [showTaskProgress, setShowTaskProgress] = useState(false)
+ const [taskResult, setTaskResult] = useState(null)
+ const [taskType, setTaskType] = useState('')
+
useEffect(() => {
- fetchSkills()
+ const loadSkills = async () => {
+ try {
+ console.log('正在加载 Skills...')
+ await fetchSkills()
+ console.log('Skills 加载完成:', skills.length)
+ } catch (error) {
+ console.error('加载 Skills 失败:', error)
+ message.warning('Skills 加载失败,部分功能可能不可用')
+ }
+ }
+
+ loadSkills()
}, [])
const steps = [
@@ -163,60 +306,100 @@ export const ProjectCreateEnhanced = () => {
const handleSelectCreateMode = () => {
setCreationMode('create')
setUploadedScript('')
+ // 重置子模式
+ setCreateSubMode(null)
+ setDirectTextInput('')
+ setInspirationFile(null)
+ }
+
+ // 选择从头创作的子模式
+ const handleSelectCreateSubMode = (mode: 'text' | 'file') => {
+ setCreateSubMode(mode)
+ // 切换子模式时保留已有内容,但清空另一种模式的内容
+ if (mode === 'text') {
+ setInspirationFile(null)
+ } else {
+ setDirectTextInput('')
+ }
}
// 清除创作方式选择
const handleClearCreationMode = () => {
setCreationMode(null)
+ setCreateSubMode(null)
setUploadedScript('')
+ setDirectTextInput('')
+ setInspirationFile(null)
+ }
+
+ // 处理灵感文件上传
+ const handleInspirationUpload = (file: any) => {
+ setInspirationFile(file)
+ message.success('灵感文件已选择')
+ return false // 阻止自动上传
}
// 验证步骤1并进入下一步
const handleNextToStep1 = async () => {
- // 验证项目名称必填(无论哪种模式都需要项目名称)
- const name = form.getFieldValue('name')
- if (!name || name.trim() === '') {
- message.error('请填写项目名称')
- return
- }
+ try {
+ // 使用 validateFields 确保表单值正确验证
+ const values = await form.validateFields(['name'])
- // 验证是否选择了创作方式
- if (!creationMode) {
- message.error('请选择创作方式(上传剧本或从头创作)')
- return
- }
+ // 验证项目名称必填(无论哪种模式都需要项目名称)
+ const name = values.name
+ if (!name || name.trim() === '') {
+ message.error('请填写项目名称')
+ return
+ }
- // 如果是上传剧本模式,验证已上传剧本
- if (creationMode === 'upload' && !uploadedScript) {
- message.error('请先上传剧本文件')
- return
- }
+ // 验证是否选择了创作方式
+ if (!creationMode) {
+ message.error('请选择创作方式(上传剧本或从头创作)')
+ return
+ }
- setCurrentStep(1)
+ // 如果是上传剧本模式,验证已上传剧本
+ if (creationMode === 'upload' && !uploadedScript) {
+ message.error('请先上传剧本文件')
+ return
+ }
+
+ // 如果是从头创作模式,验证子模式输入
+ if (creationMode === 'create') {
+ if (!createSubMode) {
+ message.error('请选择输入方式(直接输入文字或上传灵感文件)')
+ return
+ }
+ if (createSubMode === 'text' && !directTextInput.trim()) {
+ message.error('请输入创作文字内容')
+ return
+ }
+ if (createSubMode === 'file' && !inspirationFile) {
+ message.error('请上传灵感文件')
+ return
+ }
+ }
+
+ setCurrentStep(1)
+ } catch (error) {
+ // 表单验证失败
+ console.log('表单验证失败:', error)
+ }
}
// AI 生成人物设定
+ // 启动异步AI生成人物任务
const handleAIGenerateCharacters = async () => {
setAiGeneratingCharacters(true)
try {
- console.log('开始调用 AI 生成人物设定...')
-
- // 获取用户输入的项目信息
const projectName = form.getFieldValue('name') || '未命名项目'
const totalEpisodes = form.getFieldValue('totalEpisodes') || 30
-
- // 获取人物生成配置的技能信息
const selectedSkillsInfo = skills.filter(s => characterSkills.includes(s.id))
- const skillNames = selectedSkillsInfo.map(s => s.name).join('、')
- // 构建更详细的提示词
- const idea = `项目名称:${projectName},总集数:${totalEpisodes}集,古风权谋剧题材` +
- (skillNames ? `,使用技能风格:${skillNames}` : '')
+ const idea = `项目名称:${projectName},总集数:${totalEpisodes}集,古风权谋剧题材`
- console.log('使用的生成提示:', idea)
-
- // 调用后端 AI API 生成人物设定
- const data = await api.post('/ai-assistant/generate/characters', {
+ // 调用异步任务API
+ const response = await taskService.generateCharacters({
idea,
projectName,
totalEpisodes,
@@ -224,32 +407,65 @@ export const ProjectCreateEnhanced = () => {
id: s.id,
name: s.name,
behavior: s.behavior_guide
- }))
- }) as any
+ })),
+ customPrompt: characterPrompt || undefined
+ })
- console.log('AI 人物设定完整响应:', JSON.stringify(data, null, 2))
- // 后端返回: { success: true, characters: "...", usage: {...} }
- const characters = data?.characters || data?.result || data?.content || ''
- console.log('提取的人物设定:', characters?.substring(0, 100) + '...')
+ // 显示任务进度
+ setCurrentTaskId(response.taskId)
+ setTaskType('generate_characters')
+ setShowTaskProgress(true)
- if (!characters) {
- console.error('未能从响应中提取人物设定,响应数据:', data)
- message.error('AI 生成失败:未返回有效内容')
- return
- }
-
- // 同时更新状态和表单字段
- setAiGeneratedContent((prev: any) => ({ ...prev, characters }))
- form.setFieldsValue({ characters })
- message.success('AI 生成完成!')
+ message.info('AI 生成任务已启动,正在后台执行...')
} catch (error: any) {
- console.error('AI 生成失败,完整错误:', error)
- message.error(`AI 生成失败: ${error.message || '未知错误'}`)
+ console.error('创建任务失败:', error)
+ 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)
@@ -267,7 +483,8 @@ export const ProjectCreateEnhanced = () => {
id: s.id,
name: s.name,
behavior: s.behavior_guide
- }))
+ })),
+ customPrompt: analyzePrompt || undefined
}) as any
console.log('AI 剧本分析响应:', JSON.stringify(data, null, 2))
@@ -324,7 +541,8 @@ export const ProjectCreateEnhanced = () => {
id: s.id,
name: s.name,
behavior: s.behavior_guide
- }))
+ })),
+ customPrompt: worldPrompt || undefined
}) as any
console.log('AI 世界观分析响应:', JSON.stringify(data, null, 2))
@@ -355,42 +573,33 @@ export const ProjectCreateEnhanced = () => {
const handleAIGenerateWorldSetting = async () => {
setAiGeneratingWorldSetting(true)
try {
- console.log('开始调用 AI 生成世界观...')
-
- // 获取用户输入的项目信息
const projectName = form.getFieldValue('name') || '未命名项目'
-
- // 获取世界观生成配置的技能信息
const selectedSkillsInfo = skills.filter(s => worldSkills.includes(s.id))
- const skillNames = selectedSkillsInfo.map(s => s.name).join('、')
- const idea = `${projectName}世界观设定,架空朝代,皇权与相权之争,边关战事` +
- (skillNames ? `,使用技能风格:${skillNames}` : '')
+ const idea = `${projectName}世界观设定,架空朝代,皇权与相权之争,边关战事`
- const data = await api.post('/ai-assistant/generate/characters', {
+ // 调用异步任务API
+ const response = await taskService.generateWorld({
idea,
+ projectName,
+ genre: '古风',
skills: selectedSkillsInfo.map(s => ({
id: s.id,
name: s.name,
behavior: s.behavior_guide
- }))
- }) as any
+ })),
+ customPrompt: worldPrompt || undefined
+ })
- console.log('AI 世界观响应:', JSON.stringify(data, null, 2))
- const worldSetting = data?.characters || data?.result || data?.content || ''
- console.log('提取的世界观:', worldSetting?.substring(0, 100) + '...')
+ // 显示任务进度
+ setCurrentTaskId(response.taskId)
+ setTaskType('generate_world')
+ setShowTaskProgress(true)
- if (!worldSetting) {
- console.error('未能从响应中提取世界观,响应数据:', data)
- message.error('AI 生成失败:未返回有效内容')
- return
- }
-
- form.setFieldsValue({ worldSetting })
- message.success('AI 生成完成!')
+ message.info('AI 生成任务已启动,正在后台执行...')
} catch (error: any) {
- console.error('AI 生成失败,完整错误:', error)
- message.error(`AI 生成失败: ${error.message || '未知错误'}`)
+ console.error('创建任务失败:', error)
+ message.error(`创建任务失败: ${error.message || '未知错误'}`)
} finally {
setAiGeneratingWorldSetting(false)
}
@@ -400,24 +609,14 @@ export const ProjectCreateEnhanced = () => {
const handleAIGenerateOutline = async () => {
setAiGeneratingOutline(true)
try {
- console.log('开始调用 AI 生成大纲...')
-
- // 获取用户输入的项目信息
const projectName = form.getFieldValue('name') || '未命名项目'
const totalEpisodes = form.getFieldValue('totalEpisodes') || 30
-
- // 获取大纲生成配置的技能信息
const selectedSkillsInfo = skills.filter(s => outlineSkills.includes(s.id))
- const skillNames = selectedSkillsInfo.map(s => s.name).join('、')
- // 构建更详细的提示词
- const idea = `${projectName},古风权谋剧,边关将军与丞相之女的爱情故事,宫廷斗争` +
- (skillNames ? `,使用技能风格:${skillNames}` : '')
+ const idea = `${projectName},古风权谋剧,边关将军与丞相之女的爱情故事,宫廷斗争`
- console.log('使用的生成提示:', idea)
-
- // 调用后端 AI API 生成大纲
- const data = await api.post('/ai-assistant/generate/outline', {
+ // 调用异步任务API
+ const response = await taskService.generateOutline({
idea,
totalEpisodes,
genre: '古风',
@@ -426,36 +625,195 @@ export const ProjectCreateEnhanced = () => {
id: s.id,
name: s.name,
behavior: s.behavior_guide
- }))
- }) as any
+ })),
+ customPrompt: outlinePrompt || undefined
+ })
- console.log('AI 大纲完整响应:', JSON.stringify(data, null, 2))
- // 后端返回: { success: true, outline: "...", usage: {...} }
- const outline = data?.outline || data?.result || data?.content || ''
- console.log('提取的大纲:', outline?.substring(0, 100) + '...')
+ // 显示任务进度
+ setCurrentTaskId(response.taskId)
+ setTaskType('generate_outline')
+ setShowTaskProgress(true)
- if (!outline) {
- console.error('未能从响应中提取大纲,响应数据:', data)
- message.error('AI 生成失败:未返回有效内容')
- return
- }
-
- // 同时更新状态和表单字段
- setAiGeneratedContent((prev: any) => ({ ...prev, outline }))
- form.setFieldsValue({ overallOutline: outline })
- message.success('AI 生成完成!')
+ message.info('AI 生成任务已启动,正在后台执行...')
} catch (error: any) {
- console.error('AI 生成失败,完整错误:', error)
- message.error(`AI 生成失败: ${error.message || '未知错误'}`)
+ console.error('创建任务失败:', error)
+ message.error(`创建任务失败: ${error.message || '未知错误'}`)
} finally {
setAiGeneratingOutline(false)
}
}
+ // 一键生成所有内容
+ const handleGenerateAll = async () => {
+ setIsGeneratingAll(true)
+ setHasGeneratedAll(false)
+
+ try {
+ const projectName = form.getFieldValue('name') || '未命名项目'
+ const totalEpisodes = form.getFieldValue('totalEpisodes') || 30
+
+ // 步骤1: 生成/分析世界观(仅从头创作模式)
+ if (!uploadedScript) {
+ setGenerateProgress({ current: 1, total: 3, currentStep: '正在生成世界观设定...' })
+ await new Promise(resolve => setTimeout(resolve, 100)) // 让UI更新
+
+ try {
+ // 使用默认技能
+ const defaultWorldSkills = skills.filter(s => DEFAULT_SKILLS.worldSetting.includes(s.id))
+ const idea = `${projectName}世界观设定,架空朝代,皇权与相权之争,边关战事`
+
+ const data = await api.post('/ai-assistant/generate/world', {
+ idea,
+ projectName,
+ genre: '古风',
+ skills: defaultWorldSkills.map(s => ({
+ id: s.id,
+ name: s.name,
+ behavior: s.behavior_guide
+ }))
+ }) as any
+
+ const worldSetting = data?.worldSetting || data?.result || data?.content || ''
+ if (worldSetting) {
+ form.setFieldsValue({ worldSetting })
+ setAiGeneratedContent((prev: any) => ({ ...prev, worldSetting }))
+ }
+ } catch (error) {
+ console.error('生成世界观失败:', error)
+ message.warning('世界观生成失败,继续下一步...')
+ }
+ }
+
+ // 步骤2: 生成/分析人物设定
+ setGenerateProgress({ current: 2, total: 3, currentStep: uploadedScript ? '正在分析剧本中的人物...' : '正在生成人物设定...' })
+ await new Promise(resolve => setTimeout(resolve, 100))
+
+ try {
+ const defaultCharacterSkills = skills.filter(s => DEFAULT_SKILLS.characters.includes(s.id))
+
+ if (uploadedScript) {
+ // 上传剧本模式:分析剧本
+ const data = await api.post('/ai-assistant/parse/script', {
+ content: uploadedScript,
+ extractCharacters: true,
+ extractOutline: false,
+ skills: defaultCharacterSkills.map(s => ({
+ id: s.id,
+ name: s.name,
+ behavior: s.behavior_guide
+ }))
+ }) as any
+
+ const characters = data?.characters || []
+ if (characters.length > 0) {
+ const characterText = characters.map((c: any) =>
+ `【${c.name}】出场 ${c.lines} 次`
+ ).join('\n')
+ form.setFieldsValue({ characters: characterText })
+ setAiGeneratedContent((prev: any) => ({ ...prev, characters: characterText }))
+ }
+ } else {
+ // 从头创作模式:生成人物
+ const idea = `项目名称:${projectName},总集数:${totalEpisodes}集,古风权谋剧题材`
+ const data = await api.post('/ai-assistant/generate/characters', {
+ idea,
+ projectName,
+ totalEpisodes,
+ skills: defaultCharacterSkills.map(s => ({
+ id: s.id,
+ name: s.name,
+ behavior: s.behavior_guide
+ }))
+ }) as any
+
+ const characters = data?.characters || data?.result || data?.content || ''
+ if (characters) {
+ form.setFieldsValue({ characters })
+ setAiGeneratedContent((prev: any) => ({ ...prev, characters }))
+ }
+ }
+ } catch (error) {
+ console.error('生成/分析人物失败:', error)
+ message.warning('人物生成失败,继续下一步...')
+ }
+
+ // 步骤3: 生成/分析大纲
+ setGenerateProgress({ current: 3, total: 3, currentStep: uploadedScript ? '正在分析剧本大纲...' : '正在生成整体大纲...' })
+ await new Promise(resolve => setTimeout(resolve, 100))
+
+ try {
+ const defaultOutlineSkills = skills.filter(s => DEFAULT_SKILLS.outline.includes(s.id))
+
+ if (uploadedScript) {
+ // 上传剧本模式:分析剧本
+ const data = await api.post('/ai-assistant/parse/script', {
+ content: uploadedScript,
+ extractCharacters: false,
+ extractOutline: true,
+ skills: defaultOutlineSkills.map(s => ({
+ id: s.id,
+ name: s.name,
+ behavior: s.behavior_guide
+ }))
+ }) as any
+
+ const outline = data?.outline || data?.analysis || ''
+ if (outline) {
+ form.setFieldsValue({ overallOutline: outline })
+ setAiGeneratedContent((prev: any) => ({ ...prev, outline }))
+ }
+ } else {
+ // 从头创作模式:生成大纲
+ const idea = `${projectName},古风权谋剧,边关将军与丞相之女的爱情故事,宫廷斗争`
+ const data = await api.post('/ai-assistant/generate/outline', {
+ idea,
+ totalEpisodes,
+ genre: '古风',
+ projectName,
+ skills: defaultOutlineSkills.map(s => ({
+ id: s.id,
+ name: s.name,
+ behavior: s.behavior_guide
+ }))
+ }) as any
+
+ const outline = data?.outline || data?.result || data?.content || ''
+ if (outline) {
+ form.setFieldsValue({ overallOutline: outline })
+ setAiGeneratedContent((prev: any) => ({ ...prev, outline }))
+ }
+ }
+ } catch (error) {
+ console.error('生成/分析大纲失败:', error)
+ message.warning('大纲生成失败')
+ }
+
+ setHasGeneratedAll(true)
+ message.success('一键生成完成!您可以手动调整各模块内容,或单独重新生成某模块。')
+ } catch (error: any) {
+ console.error('一键生成失败:', error)
+ message.error(`一键生成失败: ${error.message || '未知错误'}`)
+ } finally {
+ setIsGeneratingAll(false)
+ setGenerateProgress({ current: 0, total: 3, currentStep: '' })
+ }
+ }
+
+ // 从步骤1快捷生成
+ const handleQuickGenerateAll = async () => {
+ setCurrentStep(1)
+ // 等待步骤渲染
+ setTimeout(() => {
+ handleGenerateAll()
+ }, 300)
+ }
+
const handleSubmit = async (values: any) => {
- // 项目名称已经在步骤1验证过,这里再检查一次
- if (!values.name || values.name.trim() === '') {
- message.error('请填写项目名称')
+ try {
+ // 确保表单通过验证
+ await form.validateFields()
+ } catch (error) {
+ message.error('请检查表单填写是否完整')
setCurrentStep(0)
return
}
@@ -505,7 +863,7 @@ export const ProjectCreateEnhanced = () => {
name: values.name,
totalEpisodes: values.totalEpisodes || 30,
globalContext: {
- worldSetting: values.worldSetting || values.characters || uploadedScript || '',
+ worldSetting: values.worldSetting || '',
overallOutline: values.overallOutline || '',
characterProfiles: {},
sceneSettings: {}
@@ -534,7 +892,19 @@ export const ProjectCreateEnhanced = () => {
return (
-
+ {/* Skills 加载状态 */}
+ {skillsError && (
+
+ )}
+
+
{steps.map((step, i) => (
@@ -615,23 +985,52 @@ export const ProjectCreateEnhanced = () => {
{/* 剧本内容预览 */}
{uploadedScript && (
-
剧本预览
+
+ 剧本预览
+ {uploadedScript.length > 500 && (
+
+ )}
+
- {uploadedScript.length > 500
- ? uploadedScript.substring(0, 500) + '\n\n... (更多内容)'
- : uploadedScript}
+ {uploadedScript}
+ {!scriptExpanded && uploadedScript.length > 500 && (
+
+
+
+ )}
)}
@@ -639,30 +1038,152 @@ export const ProjectCreateEnhanced = () => {
{/* 从头创作选项 */}
+ 从头创作
+ {creationMode !== 'create' && (
+
+ )}
+ {creationMode === 'create' && (
+
+ )}
+
+ }
style={{
- cursor: 'pointer',
border: creationMode === 'create' ? '2px solid #1677ff' : undefined,
backgroundColor: creationMode === 'create' ? '#f0f5ff' : undefined
}}
- onClick={handleSelectCreateMode}
>
-
+
让 AI 根据你的创意完整生成人物设定和剧情大纲
+
{creationMode === 'create' && (
-
- 已选择
-
+ <>
+
+
+ {/* 子模式选择 */}
+
+
+ 选择输入方式:
+
+
handleSelectCreateSubMode(e.target.value)}
+ style={{ width: '100%' }}
+ >
+
+
+
+
+ 直接输入创作文字
+
+
+
+
+
+ 上传灵感文件
+
+
+
+
+
+
+ {/* 直接输入文字 */}
+ {createSubMode === 'text' && (
+
+ )}
+
+ {/* 上传灵感文件 */}
+ {createSubMode === 'file' && (
+
+
+ 灵感类型:
+
+
+
setInspirationFile(null)}
+ >
+ } disabled={inspirationFile !== null}>
+ {inspirationFile ? '已选择文件' : '选择文件(支持 .txt, .md, .doc, .docx)'}
+
+
+ {inspirationFile && (
+
+ ✓ 已选择: {inspirationFile.name}
+
+ )}
+
+ )}
+
+ {createSubMode && (
+
+ {createSubMode === 'text' && '已选择:直接输入'}
+ {createSubMode === 'file' && '已选择:文件上传'}
+
+ )}
+ >
)}
-
+
{/* 已选择提示 */}
{creationMode && (
{
+ {/* 快捷操作:选择创作方式后显示 */}
+ {creationMode && (
+ }
+ >
+
+
+
+ }
+ onClick={handleQuickGenerateAll}
+ disabled={creationMode === 'create' && !createSubMode}
+ >
+ 立即AI生成所有内容
+
+
+
+
+
+ )}
+
{/* 统一的下一步按钮 */}
+
+
+ )}
+
+ {/* 生成进度显示 */}
+ {isGeneratingAll && (
+
+
+
+
+ {generateProgress.currentStep}
+
+
+
+ )}
+
{
title="AI 分析剧本"
extra={
- }
skills={skills}
selectedSkills={analyzeSkills}
onSkillsChange={setAnalyzeSkills}
+ customPrompt={analyzePrompt}
+ onPromptChange={setAnalyzePrompt}
description="选择用于剧本分析的技能,会影响提取效果"
/>
{
loading={aiAnalyzingScript}
onClick={handleAIAnalyzeScript}
>
- {aiAnalyzingScript ? '分析中...' : 'AI 分析'}
+ {aiAnalyzingScript ? '分析中...' : (form.getFieldValue('characters') ? 'AI 重新分析' : 'AI 分析')}
}
@@ -737,12 +1353,14 @@ export const ProjectCreateEnhanced = () => {
title="AI 分析世界观"
extra={
- }
skills={skills}
selectedSkills={worldSkills}
onSkillsChange={setWorldSkills}
+ customPrompt={worldPrompt}
+ onPromptChange={setWorldPrompt}
description="选择用于世界观分析的技能,会影响设定风格"
/>
{
loading={aiAnalyzingWorldFromScript}
onClick={handleAIAnalyzeWorldFromScript}
>
- {aiAnalyzingWorldFromScript ? '分析中...' : 'AI 分析'}
+ {aiAnalyzingWorldFromScript ? '分析中...' : (form.getFieldValue('worldSetting') ? 'AI 重新分析' : 'AI 分析')}
}
@@ -770,12 +1388,14 @@ export const ProjectCreateEnhanced = () => {
title="AI 生成人物设定"
extra={
- }
skills={skills}
selectedSkills={characterSkills}
onSkillsChange={setCharacterSkills}
+ customPrompt={characterPrompt}
+ onPromptChange={setCharacterPrompt}
description="选择用于生成人物设定的技能,会影响人物风格"
/>
{
loading={aiGeneratingCharacters}
onClick={handleAIGenerateCharacters}
>
- {aiGeneratingCharacters ? '生成中...' : 'AI 生成'}
+ {aiGeneratingCharacters ? '生成中...' : (form.getFieldValue('characters') ? 'AI 重新生成' : 'AI 生成')}
}
@@ -802,12 +1422,14 @@ export const ProjectCreateEnhanced = () => {
title={uploadedScript ? "AI 分析剧本大纲" : "AI 生成整体大纲"}
extra={
- }
skills={skills}
selectedSkills={outlineSkills}
onSkillsChange={setOutlineSkills}
+ customPrompt={outlinePrompt}
+ onPromptChange={setOutlinePrompt}
description="选择用于生成大纲的技能,会影响剧情结构"
/>
{
loading={aiGeneratingOutline}
onClick={handleAIGenerateOutline}
>
- {aiGeneratingOutline ? (uploadedScript ? '分析中...' : '生成中...') : (uploadedScript ? 'AI 分析' : 'AI 生成')}
+ {aiGeneratingOutline
+ ? (uploadedScript ? '分析中...' : '生成中...')
+ : (form.getFieldValue('overallOutline')
+ ? (uploadedScript ? 'AI 重新分析' : 'AI 重新生成')
+ : (uploadedScript ? 'AI 分析' : 'AI 生成'))}
}
@@ -834,12 +1460,14 @@ export const ProjectCreateEnhanced = () => {
title="世界观设定 (可选)"
extra={
- }
skills={skills}
selectedSkills={worldSkills}
onSkillsChange={setWorldSkills}
+ customPrompt={worldPrompt}
+ onPromptChange={setWorldPrompt}
description="选择用于生成世界观的技能,会影响设定风格"
/>
{
loading={aiGeneratingWorldSetting}
onClick={handleAIGenerateWorldSetting}
>
- {aiGeneratingWorldSetting ? '生成中...' : 'AI 生成'}
+ {aiGeneratingWorldSetting ? '生成中...' : (form.getFieldValue('worldSetting') ? 'AI 重新生成' : 'AI 生成')}
}
@@ -1000,6 +1628,25 @@ export const ProjectCreateEnhanced = () => {
)}
+
+ {/* 异步任务进度对话框 */}
+ }
+ >
+ {currentTaskId && (
+
+ )}
+
)
}
diff --git a/frontend/src/pages/ProjectCreateProgressive.tsx b/frontend/src/pages/ProjectCreateProgressive.tsx
new file mode 100644
index 0000000..ddd4409
--- /dev/null
+++ b/frontend/src/pages/ProjectCreateProgressive.tsx
@@ -0,0 +1,279 @@
+/**
+ * 简化项目创建页面
+ *
+ * 设计理念:
+ * 1. 收集项目基本信息(名称、集数)
+ * 2. 支持直接输入创作文字或上传灵感文件
+ * 3. 立即创建项目
+ * 4. 其他内容(AI辅助生成)在项目详情页完成
+ */
+import { useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { Form, Input, InputNumber, Button, Card, message, Space, Alert, Radio, Upload, Select } from 'antd'
+import { FileTextOutlined, PlusOutlined, UploadOutlined, BulbOutlined, EditOutlined } from '@ant-design/icons'
+import { useProjectStore } from '@/stores/projectStore'
+import { ProjectCreateRequest } from '@/services/projectService'
+import type { UploadFile } from 'antd'
+
+const { TextArea } = Input
+const { Option } = Select
+
+export const ProjectCreateProgressive = () => {
+ const navigate = useNavigate()
+ const createProject = useProjectStore(state => state.createProject)
+
+ // 项目状态
+ const [creating, setCreating] = useState(false)
+ const [creationMode, setCreationMode] = useState<'blank' | 'text' | 'file'>('blank')
+ const [textContent, setTextContent] = useState('')
+ const [uploadedFile, setUploadedFile] = useState(null)
+ const [inspirationType, setInspirationType] = useState<'story' | 'character' | 'scene' | 'dialogue'>('story')
+
+ const [form] = Form.useForm()
+
+ // 处理创作方式切换
+ const handleModeChange = (newMode: 'blank' | 'text' | 'file') => {
+ // 如果有内容且切换到不同模式,提示用户内容已保留
+ if (creationMode !== newMode && (textContent || uploadedFile)) {
+ const keptContent = creationMode === 'text' ? '文字内容' : '文件'
+ message.info(`切换到${newMode === 'blank' ? '从头创作' : newMode === 'text' ? '直接输入' : '文件上传'},${keptContent}已保留`)
+ }
+ setCreationMode(newMode)
+ }
+
+ // 创建项目
+ const handleCreateProject = async () => {
+ try {
+ await form.validateFields()
+ const name = form.getFieldValue('name')
+ const totalEpisodes = form.getFieldValue('totalEpisodes') || 30
+
+ // 验证创作内容
+ if (creationMode === 'text' && !textContent.trim()) {
+ message.error('请输入创作文字内容')
+ return
+ }
+ if (creationMode === 'file' && !uploadedFile) {
+ message.error('请上传灵感文件')
+ return
+ }
+
+ setCreating(true)
+
+ // 读取文件内容
+ let fileContent = ''
+ if (creationMode === 'file' && uploadedFile?.originFileObj) {
+ fileContent = await uploadedFile.originFileObj.text()
+ }
+
+ const projectData: ProjectCreateRequest = {
+ name: name.trim(),
+ totalEpisodes,
+ globalContext: {
+ worldSetting: '',
+ overallOutline: '',
+ characterProfiles: {},
+ sceneSettings: {},
+ // 根据创作模式保存内容
+ uploadedScript: creationMode === 'text' ? textContent : fileContent,
+ inspiration: creationMode === 'file' ? inspirationType : undefined
+ }
+ }
+
+ const project = await createProject(projectData)
+ message.success('项目创建成功!')
+
+ // 跳转到项目详情页
+ navigate(`/projects/${project.id}`)
+ } catch (error: any) {
+ if (error.errorFields) {
+ // 表单验证错误
+ return
+ }
+ console.error('创建项目失败:', error)
+ message.error(`创建失败: ${error.message || '未知错误'}`)
+ } finally {
+ setCreating(false)
+ }
+ }
+
+ return (
+
+ {/* 头部 */}
+
+
+
+
+
创建新项目
+
+
+
+
+
+ {/* 创建项目表单 */}
+
+
+
+
+
+
+
+
+
+ {/* 创作方式选择 */}
+
+ handleModeChange(e.target.value)}
+ style={{ width: '100%' }}
+ >
+
+
+
+
+ 从头创作(稍后在详情页补充内容)
+
+
+
+
+
+ 直接输入创作文字
+
+
+
+
+
+ 上传灵感文件
+
+
+
+
+
+
+ {/* 直接输入文字 */}
+ {creationMode === 'text' && (
+
+
+ )}
+
+ {/* 上传灵感文件 */}
+ {creationMode === 'file' && (
+ <>
+
+
+
+
+
+ {
+ setUploadedFile(file)
+ return false // 阻止自动上传
+ }}
+ onRemove={() => setUploadedFile(null)}
+ >
+ } disabled={uploadedFile !== null}>
+ {uploadedFile ? '已选择文件' : '选择文件(支持 .txt, .md, .doc, .docx)'}
+
+
+ {uploadedFile && (
+
+ ✓ 已选择: {uploadedFile.name}
+
+ )}
+
+
+ >
+ )}
+
+ {/* 显示已保存内容的提示 */}
+ {creationMode !== 'blank' && (textContent || uploadedFile) && (
+
+ )}
+
+
+
+ navigate('/projects')}>
+ 取消
+
+ }
+ loading={creating}
+ onClick={handleCreateProject}
+ >
+ 创建项目
+
+
+
+
+
+
+ )
+}
+
+export default ProjectCreateProgressive
diff --git a/frontend/src/pages/ProjectDetail.tsx b/frontend/src/pages/ProjectDetail.tsx
index d957bf8..c4f6495 100644
--- a/frontend/src/pages/ProjectDetail.tsx
+++ b/frontend/src/pages/ProjectDetail.tsx
@@ -1,24 +1,190 @@
/**
* 项目详情和执行页面
+ *
+ * 标签页结构:
+ * 1. 项目设置 - 编辑基本信息 + 创作方式选择 + 上传剧本/编辑灵感
+ * 2. 全局设定生成 - 根据项目设置进行分析或生成
+ * 3. 剧集创作 - 执行剧集创作任务
+ * 4. 记忆系统 - 故事记忆管理
+ * 5. 审核系统 - 内容质量审核
*/
import { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
-import { Card, Button, Descriptions, List, Tag, Space, Modal, message, Spin, Typography, Tabs } from 'antd'
-import { PlayCircleOutlined, CheckCircleOutlined, LoadingOutlined, ClockCircleOutlined, ScanOutlined, FileTextOutlined } from '@ant-design/icons'
+import { Card, Button, Descriptions, List, Tag, Space, Modal, message, Spin, Typography, Tabs, Form, Input, InputNumber, Upload, Alert, Popover, Select } from 'antd'
+import { ArrowLeftOutlined, PlayCircleOutlined, CheckCircleOutlined, LoadingOutlined, ClockCircleOutlined, ScanOutlined, FileTextOutlined, SettingOutlined, RobotOutlined, UploadOutlined, EditOutlined, SaveOutlined } from '@ant-design/icons'
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 { TabPane } = Tabs
+const { TextArea } = Input
+
+// Skill 选择器和自定义提示词组件
+const SkillSelectorWithPrompt = ({
+ title,
+ icon,
+ skills,
+ selectedSkills,
+ onSkillsChange,
+ customPrompt,
+ onPromptChange,
+ description
+}: {
+ title: string
+ icon: React.ReactNode
+ skills: any[]
+ selectedSkills: string[]
+ onSkillsChange: (skills: string[]) => void
+ customPrompt: string
+ onPromptChange: (prompt: string) => void
+ description?: string
+}) => {
+ const content = (
+
+
+ {description || '选择用于此 AI 生成任务的技能'}
+
+
+ )
+
+ return (
+
+ 0 || customPrompt) ? 'primary' : 'default'}
+ >
+ {selectedSkills.length > 0 || customPrompt ? `已配置` : '配置'}
+
+
+ )
+}
export const ProjectDetail = () => {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
- const { currentProject, episodes, loading, fetchProject, fetchEpisodes, executeEpisode } = useProjectStore()
+ const { currentProject, projects, episodes, loading, error, fetchProject, fetchEpisodes, executeEpisode, updateProject } = useProjectStore()
+ const { skills, fetchSkills } = useSkillStore()
const [executing, setExecuting] = useState(false)
const [currentEpisode, setCurrentEpisode] = useState(1)
const [selectedEpisode, setSelectedEpisode] = useState(null)
- const [activeTab, setActiveTab] = useState('episodes')
+ const [activeTab, setActiveTab] = useState('settings')
+
+ // 项目设置相关状态
+ const [settingsForm] = Form.useForm()
+ const [updatingSettings, setUpdatingSettings] = useState(false)
+ const [creationMode, setCreationMode] = useState<'script' | 'inspiration' | null>(null)
+ const [scriptContent, setScriptContent] = useState('')
+ const [inspirationContent, setInspirationContent] = useState('')
+ const [scriptEditing, setScriptEditing] = useState(false)
+ const [inspirationEditing, setInspirationEditing] = useState(false)
+
+ // 全局设定生成相关状态
+ const [globalForm] = Form.useForm()
+ const [generating, setGenerating] = useState(false)
+ const [currentTaskId, setCurrentTaskId] = useState(null)
+ const [showTaskProgress, setShowTaskProgress] = useState(false)
+ const [taskType, setTaskType] = useState(null)
+
+ // Skills 配置
+ const [worldSkills, setWorldSkills] = useState([])
+ const [characterSkills, setCharacterSkills] = useState([])
+ const [outlineSkills, setOutlineSkills] = useState([])
+
+ // 自定义提示词
+ const [worldPrompt, setWorldPrompt] = useState('')
+ const [characterPrompt, setCharacterPrompt] = useState('')
+ const [outlinePrompt, setOutlinePrompt] = useState('')
+
+ // 加载 Skills
+ useEffect(() => {
+ const loadSkills = async () => {
+ try {
+ await fetchSkills()
+ } catch (error) {
+ console.error('加载 Skills 失败:', error)
+ }
+ }
+ loadSkills()
+ }, [])
+
+ // 加载项目数据后初始化
+ useEffect(() => {
+ if (currentProject) {
+ // 初始化项目设置表单
+ settingsForm.setFieldsValue({
+ name: currentProject.name,
+ totalEpisodes: currentProject.totalEpisodes
+ })
+
+ // 确定创作方式
+ const hasScript = currentProject.globalContext?.uploadedScript && currentProject.globalContext.uploadedScript.length > 0
+ const hasInspiration = currentProject.globalContext?.inspiration && currentProject.globalContext.inspiration.length > 0
+
+ if (hasScript) {
+ setCreationMode('script')
+ setScriptContent(currentProject.globalContext.uploadedScript || '')
+ } else if (hasInspiration) {
+ setCreationMode('inspiration')
+ setInspirationContent(currentProject.globalContext.inspiration || '')
+ }
+
+ // 初始化全局设定表单
+ globalForm.setFieldsValue({
+ worldSetting: currentProject.globalContext?.worldSetting || '',
+ characters: currentProject.globalContext?.characterProfiles || '',
+ overallOutline: currentProject.globalContext?.overallOutline || ''
+ })
+ }
+ }, [currentProject])
useEffect(() => {
if (id) {
@@ -27,6 +193,250 @@ export const ProjectDetail = () => {
}
}, [id])
+ // 上传剧本文件
+ const handleScriptUpload = (file: any) => {
+ const reader = new FileReader()
+ reader.onload = (e) => {
+ const content = e.target?.result as string || ''
+ setScriptContent(content)
+ setCreationMode('script')
+ setScriptEditing(true)
+ message.success('剧本上传成功!您可以编辑内容后点击保存')
+ }
+ reader.readAsText(file)
+ return false
+ }
+
+ // 上传灵感文件
+ const handleInspirationUpload = (file: any) => {
+ const reader = new FileReader()
+ reader.onload = (e) => {
+ const content = e.target?.result as string || ''
+ setInspirationContent(content)
+ setCreationMode('inspiration')
+ setInspirationEditing(true)
+ message.success('灵感文件上传成功!您可以编辑内容后点击保存')
+ }
+ reader.readAsText(file)
+ return false
+ }
+
+ // 保存项目设置
+ const handleSaveSettings = async () => {
+ try {
+ await settingsForm.validateFields()
+
+ // 验证必须选择了创作方式并填写了内容
+ if (!creationMode) {
+ message.error('请选择创作方式并填写相应内容')
+ return
+ }
+
+ if (creationMode === 'script' && !scriptContent.trim()) {
+ message.error('请上传或填写剧本内容')
+ return
+ }
+
+ if (creationMode === 'inspiration' && !inspirationContent.trim()) {
+ message.error('请上传或填写灵感内容')
+ return
+ }
+
+ setUpdatingSettings(true)
+ const values = settingsForm.getFieldsValue()
+
+ await updateProject(id!, {
+ name: values.name,
+ totalEpisodes: values.totalEpisodes,
+ globalContext: {
+ ...currentProject?.globalContext,
+ uploadedScript: scriptContent,
+ inspiration: inspirationContent
+ }
+ })
+
+ message.success('项目设置已保存')
+ await fetchProject(id!)
+
+ // 自动跳转到全局设定生成标签页
+ setActiveTab('global-generation')
+ } catch (error: any) {
+ if (error.errorFields) {
+ return
+ }
+ message.error(`保存失败: ${error.message || '未知错误'}`)
+ } finally {
+ setUpdatingSettings(false)
+ }
+ }
+
+ // AI生成/分析世界观
+ const handleGenerateWorld = async () => {
+ if (!id) return
+
+ const projectName = currentProject?.name || '未命名项目'
+ const selectedSkillsInfo = skills.filter(s => worldSkills.includes(s.id))
+
+ // 根据创作方式决定是分析还是生成
+ const isAnalysis = creationMode === 'script'
+ const baseContent = isAnalysis ? scriptContent : inspirationContent
+ const idea = isAnalysis
+ ? `分析以下剧本,提取世界观设定:\n${baseContent?.substring(0, 2000)}`
+ : `项目名称:${projectName}\n创意灵感:\n${baseContent}`
+
+ try {
+ setGenerating(true)
+ const response = await taskService.generateWorld({
+ idea,
+ projectName,
+ genre: '古风',
+ skills: selectedSkillsInfo.map(s => ({
+ id: s.id,
+ name: s.name,
+ behavior: s.behavior_guide
+ })),
+ customPrompt: worldPrompt || undefined,
+ projectId: id
+ })
+
+ setCurrentTaskId(response.taskId)
+ setTaskType('generate_world')
+ setShowTaskProgress(true)
+ } catch (error: any) {
+ message.error(`创建任务失败: ${error.message || '未知错误'}`)
+ setGenerating(false)
+ }
+ }
+
+ // AI生成/分析人物设定
+ const handleGenerateCharacters = async () => {
+ if (!id) return
+
+ const projectName = currentProject?.name || '未命名项目'
+ const totalEpisodes = currentProject?.totalEpisodes || 30
+ const selectedSkillsInfo = skills.filter(s => characterSkills.includes(s.id))
+
+ const isAnalysis = creationMode === 'script'
+ const baseContent = isAnalysis ? scriptContent : inspirationContent
+ const idea = isAnalysis
+ ? `分析以下剧本,提取人物设定:\n${baseContent?.substring(0, 2000)}`
+ : `项目名称:${projectName},总集数:${totalEpisodes}集\n创意灵感:\n${baseContent}`
+
+ try {
+ setGenerating(true)
+ const response = await taskService.generateCharacters({
+ idea,
+ projectName,
+ totalEpisodes,
+ skills: selectedSkillsInfo.map(s => ({
+ id: s.id,
+ name: s.name,
+ behavior: s.behavior_guide
+ })),
+ customPrompt: characterPrompt || undefined,
+ projectId: id
+ })
+
+ setCurrentTaskId(response.taskId)
+ setTaskType('generate_characters')
+ setShowTaskProgress(true)
+ } catch (error: any) {
+ message.error(`创建任务失败: ${error.message || '未知错误'}`)
+ setGenerating(false)
+ }
+ }
+
+ // AI生成/分析大纲
+ const handleGenerateOutline = async () => {
+ if (!id) return
+
+ const projectName = currentProject?.name || '未命名项目'
+ const totalEpisodes = currentProject?.totalEpisodes || 30
+ const selectedSkillsInfo = skills.filter(s => outlineSkills.includes(s.id))
+
+ const isAnalysis = creationMode === 'script'
+ const baseContent = isAnalysis ? scriptContent : inspirationContent
+ const idea = isAnalysis
+ ? `分析以下剧本,提取整体大纲:\n${baseContent?.substring(0, 2000)}`
+ : `项目名称:${projectName},总集数:${totalEpisodes}集\n创意灵感:\n${baseContent}`
+
+ try {
+ setGenerating(true)
+ const response = await taskService.generateOutline({
+ idea,
+ totalEpisodes,
+ genre: '古风',
+ projectName,
+ skills: selectedSkillsInfo.map(s => ({
+ id: s.id,
+ name: s.name,
+ behavior: s.behavior_guide
+ })),
+ customPrompt: outlinePrompt || undefined,
+ projectId: id
+ })
+
+ setCurrentTaskId(response.taskId)
+ setTaskType('generate_outline')
+ setShowTaskProgress(true)
+ } catch (error: any) {
+ message.error(`创建任务失败: ${error.message || '未知错误'}`)
+ setGenerating(false)
+ }
+ }
+
+ // 任务完成回调
+ 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 handleTaskError = (error: string) => {
+ setShowTaskProgress(false)
+ setGenerating(false)
+ message.error(`任务执行失败: ${error}`)
+ }
+
+ // 保存全局设定
+ const handleSaveGlobalSettings = async () => {
+ try {
+ const worldSetting = globalForm.getFieldValue('worldSetting')
+ const characters = globalForm.getFieldValue('characters')
+ const overallOutline = globalForm.getFieldValue('overallOutline')
+
+ await updateProject(id!, {
+ globalContext: {
+ ...currentProject?.globalContext,
+ worldSetting: worldSetting || '',
+ overallOutline: overallOutline || '',
+ characterProfiles: characters ? {} : {},
+ sceneSettings: {}
+ }
+ })
+
+ message.success('全局设定已保存')
+ await fetchProject(id!)
+ } catch (error: any) {
+ message.error(`保存失败: ${error.message || '未知错误'}`)
+ }
+ }
+
+ // 执行剧集创作
const handleExecuteEpisode = async (epNum: number) => {
setExecuting(true)
try {
@@ -43,7 +453,6 @@ export const ProjectDetail = () => {
const handleExecuteBatch = async () => {
setExecuting(true)
try {
- // 依次执行 EP1-EP3
for (let i = 1; i <= 3; i++) {
await executeEpisode(id!, i)
message.success(`EP${i} 创作完成!`)
@@ -77,6 +486,35 @@ export const ProjectDetail = () => {
return texts[status] || status
}
+ // 检查是否可以进入全局设定生成
+ const canProceedToGlobalGeneration = creationMode &&
+ ((creationMode === 'script' && scriptContent.trim()) ||
+ (creationMode === 'inspiration' && inspirationContent.trim()))
+
+ // 检查全局设定是否完成
+ const globalSettingsCompleted = currentProject?.globalContext?.worldSetting?.trim() &&
+ currentProject?.globalContext?.overallOutline?.trim()
+
+ // 错误处理
+ if (error) {
+ return (
+
+
window.location.reload()}>
+ 刷新页面
+
+ }
+ />
+
+ )
+ }
+
+ // 加载中
if (loading || !currentProject) {
return (
@@ -85,14 +523,41 @@ export const ProjectDetail = () => {
)
}
+ const isAnalysisMode = creationMode === 'script'
+
return (
{/* 头部 */}
-
{currentProject.name}
-
navigate('/projects')}>返回列表
+
+ }
+ onClick={() => navigate('/projects')}
+ style={{
+ marginRight: '16px',
+ borderRadius: '8px',
+ height: '40px',
+ fontWeight: 600,
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
+ }}
+ onMouseEnter={(e) => {
+ e.currentTarget.style.transform = 'translateX(-4px)'
+ e.currentTarget.style.boxShadow = '0 4px 12px rgba(22, 119, 255, 0.15)'
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.transform = 'translateX(0)'
+ e.currentTarget.style.boxShadow = 'none'
+ }}
+ >
+ 返回项目列表
+
+
+ 共 {projects.length} 个项目
+
+
+
{currentProject.name}
{currentProject.totalEpisodes}
@@ -105,87 +570,425 @@ export const ProjectDetail = () => {
- {/* 全局设定 */}
-
-
- {currentProject.globalContext.worldSetting && (
-
- {currentProject.globalContext.worldSetting}
-
- )}
- {currentProject.globalContext.styleGuide && (
-
- {currentProject.globalContext.styleGuide}
-
- )}
- {currentProject.globalContext.overallOutline && (
-
-
- {currentProject.globalContext.overallOutline}
-
-
- )}
-
-
-
- {/* 执行控制和系统管理 */}
+ {/* 标签页 */}
-
-
- : }
- onClick={handleExecuteBatch}
- disabled={executing}
- >
- {executing ? '创作中...' : '开始创作 (EP1-EP3)'}
-
- 依次创作前3集,使用配置的 Skills
-
-
- {/* 剧集列表 */}
- (
- setSelectedEpisode(episode)}
- >
- 查看内容
-
- ) : null
- ]}
- >
-
- EP{episode.number}
- {episode.title && - {episode.title}}
-
- {getStatusText(episode.status)}
-
-
- }
- description={
-
- {episode.qualityScore && (
- 质量分数: {episode.qualityScore}
- )}
- {episode.issues.length > 0 && (
- 问题数: {episode.issues.length}
- )}
-
- }
- />
-
- )}
+ {/* 项目设置标签页 */}
+
+
+ 项目设置
+
+ }
+ key="settings"
+ >
+
+
+
+
+
+
+
+
+
+ {/* 创作方式选择 */}
+
+
+ {
+ setCreationMode('script')
+ setScriptEditing(true)
+ }}
+ >
+ e.stopPropagation()}>
+
+ 上传现有剧本,AI将分析并提取世界观、人物设定和大纲
+
+ {!scriptEditing ? (
+
+
+
+
+ 点击或拖拽文件到此区域上传剧本
+ 支持 .txt, .md, .docx 格式
+
+ ) : (
+
+
+
+ 剧本内容 ({scriptContent.length} 字符)
+ } onClick={() => setScriptEditing(true)}>
+ 编辑
+
+
+
+ {scriptEditing && (
+ <>
+
+ )}
+
+
+
+ {
+ setCreationMode('inspiration')
+ setInspirationEditing(true)
+ }}
+ >
+ e.stopPropagation()}>
+
+ 描述您的创意灵感,AI将根据灵感生成完整的世界观、人物设定和大纲
+
+ {!inspirationEditing ? (
+ <>
+
+
+
+
+
+
+
+ }>
+ 保存设置
+
+ {canProceedToGlobalGeneration && (
+ setActiveTab('global-generation')}>
+ 下一步:全局设定生成
+
+ )}
+
+
+
+ {/* 全局设定生成标签页 */}
+
+
+ 全局设定生成
+
+ }
+ key="global-generation"
+ >
+ {!canProceedToGlobalGeneration ? (
+ setActiveTab('settings')}>
+ 前往设置
+
+ }
+ />
+ ) : (
+
+
+
+
+ }
+ style={{ marginBottom: '16px' }}
+ >
+
+
+
+
+
+ {/* 人物设定 */}
+
+ }
+ skills={skills}
+ selectedSkills={characterSkills}
+ onSkillsChange={setCharacterSkills}
+ customPrompt={characterPrompt}
+ onPromptChange={setCharacterPrompt}
+ />
+ : }
+ onClick={handleGenerateCharacters}
+ disabled={generating}
+ >
+ {isAnalysisMode ? 'AI 分析' : 'AI 生成'}
+
+
+ }
+ style={{ marginBottom: '16px' }}
+ >
+
+
+
+
+
+ {/* 整体大纲 */}
+
+ }
+ skills={skills}
+ selectedSkills={outlineSkills}
+ onSkillsChange={setOutlineSkills}
+ customPrompt={outlinePrompt}
+ onPromptChange={setOutlinePrompt}
+ />
+ : }
+ onClick={handleGenerateOutline}
+ disabled={generating}
+ >
+ {isAnalysisMode ? 'AI 分析' : 'AI 生成'}
+
+
+ }
+ style={{ marginBottom: '16px' }}
+ >
+
+
+
+
+
+
+ }>
+ 保存全局设定
+
+
+
+
+ )}
+
+
+ {/* 剧集创作标签页 */}
+
+ {!globalSettingsCompleted ? (
+ setActiveTab('global-generation')}>
+ 前往全局设定生成
+
+ }
+ />
+ ) : (
+
+
+ : }
+ onClick={handleExecuteBatch}
+ disabled={executing}
+ >
+ {executing ? '创作中...' : '开始创作 (EP1-EP3)'}
+
+ 依次创作前3集,使用配置的 Skills
+
+
+ (
+ setSelectedEpisode(episode)}>
+ 查看内容
+
+ ) : null
+ ]}
+ >
+
+ EP{episode.number}
+ {episode.title && - {episode.title}}
+
+ {getStatusText(episode.status)}
+
+
+ }
+ description={
+
+ {episode.qualityScore && 质量分数: {episode.qualityScore}}
+ {episode.issues && episode.issues.length > 0 && 问题数: {episode.issues.length}}
+
+ }
+ />
+
+ )}
+ />
+
+ )}
+
+
+ {/* 记忆系统标签页 */}
@@ -203,6 +1006,7 @@ export const ProjectDetail = () => {
+ {/* 审核系统标签页 */}
@@ -215,12 +1019,8 @@ export const ProjectDetail = () => {
配置和执行内容质量审核,查看评分和问题详情。
- navigate(`/projects/${id}/review/config`)}>
- 配置审核
-
- navigate(`/projects/${id}/review/results`)}>
- 查看结果
-
+ navigate(`/projects/${id}/review/config`)}>配置审核
+ navigate(`/projects/${id}/review/results`)}>查看结果
@@ -259,6 +1059,25 @@ export const ProjectDetail = () => {
)}
+
+ {/* AI生成任务进度弹窗 */}
+
setShowTaskProgress(false)}
+ footer={null}
+ width={600}
+ closable={!generating}
+ >
+ {currentTaskId && (
+
+ )}
+
)
}
diff --git a/frontend/src/pages/ProjectList.tsx b/frontend/src/pages/ProjectList.tsx
index a3c1fae..89e583b 100644
--- a/frontend/src/pages/ProjectList.tsx
+++ b/frontend/src/pages/ProjectList.tsx
@@ -1,18 +1,223 @@
/**
- * 项目列表页面
+ * 项目列表页面 - 采用卡片网格布局
+ *
+ * 设计理念:
+ * - 不使用传统列表,而是卡片网格展示
+ * - 每个项目卡片显示项目状态和进度
+ * - 支持快速操作:继续编辑、查看详情、删除
*/
-import { useEffect } from 'react'
+import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
-import { Table, Button, Space, Tag, Card, message } from 'antd'
-import { PlusOutlined } from '@ant-design/icons'
+import { Button, Space, Tag, Card, message, Empty, Row, Col, Progress, Tooltip, Badge } from 'antd'
+import {
+ PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined,
+ ClockCircleOutlined, CheckCircleOutlined, LoadingOutlined,
+ FileTextOutlined, SettingOutlined
+} from '@ant-design/icons'
import { useProjectStore } from '@/stores/projectStore'
-import { type ColumnsType } from 'antd/es/table'
import { SeriesProject } from '@/services/projectService'
import dayjs from 'dayjs'
+import relativeTime from 'dayjs/plugin/relativeTime'
+import 'dayjs/locale/zh-cn'
+
+dayjs.extend(relativeTime)
+dayjs.locale('zh-cn')
+
+type ProjectStatus = 'draft' | 'in_progress' | 'completed'
+
+// 项目状态配置
+const STATUS_CONFIG: Record
= {
+ draft: {
+ text: '草稿',
+ color: 'default',
+ icon:
+ },
+ in_progress: {
+ text: '创作中',
+ color: 'processing',
+ icon:
+ },
+ completed: {
+ text: '已完成',
+ color: 'success',
+ icon:
+ }
+}
+
+// 计算项目完成度
+const calculateCompletion = (project: SeriesProject): number => {
+ let completed = 0
+ let total = 3
+
+ // 检查世界观设定
+ if (project.globalContext?.worldSetting) completed += 1
+
+ // 检查人物设定
+ if (project.globalContext?.characterProfiles &&
+ Object.keys(project.globalContext.characterProfiles).length > 0) {
+ completed += 1
+ }
+
+ // 检查大纲
+ if (project.globalContext?.overallOutline) completed += 1
+
+ // 检查剧集完成度
+ if (project.totalEpisodes && project.episodes) {
+ total = 3 + project.totalEpisodes
+ completed += project.episodes.length
+ }
+
+ return Math.min(100, Math.round((completed / total) * 100))
+}
+
+// 获取项目状态
+const getProjectStatus = (project: SeriesProject): ProjectStatus => {
+ const completion = calculateCompletion(project)
+
+ if (completion === 0) return 'draft'
+ if (completion >= 100) return 'completed'
+ return 'in_progress'
+}
+
+// 项目卡片组件
+const ProjectCard = ({ project, onEdit, onDelete, onView }: {
+ project: SeriesProject
+ onEdit: (id: string) => void
+ onDelete: (id: string) => void
+ onView: (id: string) => void
+}) => {
+ const status = getProjectStatus(project)
+ const statusConfig = STATUS_CONFIG[status]
+ const completion = calculateCompletion(project)
+
+ return (
+
+
+ {/* 项目标题 */}
+
+
+ {project.name}
+
+
+ }>
+ {project.totalEpisodes || 0} 集
+
+
+ {statusConfig.text}
+
+
+
+
+ {/* 项目描述/内容预览 */}
+
+ {project.globalContext?.overallOutline ? (
+
+ {project.globalContext.overallOutline}
+
+ ) : project.globalContext?.worldSetting ? (
+
+ {project.globalContext.worldSetting}
+
+ ) : (
+
+ 暂无内容描述
+
+ )}
+
+
+ {/* 进度条 */}
+
+
+ 完成度
+
+ {completion}%
+
+
+
+
+
+ {/* 时间信息 */}
+
+
+ {dayjs(project.createdAt).fromNow()}
+
+
+ {/* 操作按钮 */}
+
+ {status === 'draft' || status === 'in_progress' ? (
+ }
+ onClick={() => onEdit(project.id)}
+ style={{ flex: 1 }}
+ >
+ 继续编辑
+
+ ) : (
+ }
+ onClick={() => onView(project.id)}
+ style={{ flex: 1 }}
+ >
+ 查看详情
+
+ )}
+
+ }
+ onClick={() => onDelete(project.id)}
+ />
+
+
+
+
+ )
+}
export const ProjectList = () => {
const navigate = useNavigate()
const { projects, loading, fetchProjects, deleteProject } = useProjectStore()
+ const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
useEffect(() => {
fetchProjects()
@@ -27,79 +232,141 @@ export const ProjectList = () => {
}
}
- const columns: ColumnsType = [
- {
- title: '项目名称',
- dataIndex: 'name',
- key: 'name',
- },
- {
- title: '总集数',
- dataIndex: 'totalEpisodes',
- key: 'totalEpisodes',
- width: 100,
- },
- {
- title: '模式',
- dataIndex: 'mode',
- key: 'mode',
- width: 100,
- render: (mode: string) => {
- const modeMap: Record = {
- 'batch': { text: '分批次', color: 'blue' },
- 'auto': { text: '全自动', color: 'green' },
- 'step': { text: '逐步审核', color: 'orange' }
- }
- const config = modeMap[mode] || { text: mode, color: 'default' }
- return {config.text}
- }
- },
- {
- title: '创建时间',
- dataIndex: 'createdAt',
- key: 'createdAt',
- width: 180,
- render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm')
- },
- {
- title: '操作',
- key: 'actions',
- width: 200,
- render: (_, record) => (
-
- navigate(`/projects/${record.id}`)}>
- 查看详情
-
- handleDelete(record.id)}>
- 删除
-
-
- ),
- },
- ]
+ const handleContinueEdit = (id: string) => {
+ // 跳转到项目详情页继续编辑
+ navigate(`/projects/${id}`)
+ }
+
+ const handleView = (id: string) => {
+ navigate(`/projects/${id}`)
+ }
+
+ // 按状态分组项目
+ const draftProjects = projects.filter(p => getProjectStatus(p) === 'draft')
+ const inProgressProjects = projects.filter(p => getProjectStatus(p) === 'in_progress')
+ const completedProjects = projects.filter(p => getProjectStatus(p) === 'completed')
return (
-
+
+
我的项目
+
+ 共 {projects.length} 个项目
+ {inProgressProjects.length > 0 && ` · ${inProgressProjects.length} 个创作中`}
+ {completedProjects.length > 0 && ` · ${completedProjects.length} 个已完成`}
+
+
+
+ }
+ onClick={() => setViewMode(viewMode === 'grid' ? 'list' : 'grid')}
+ >
+ {viewMode === 'grid' ? '列表视图' : '网格视图'}
+
}
- onClick={() => navigate('/projects/new')}
+ onClick={() => navigate('/projects/progressive')}
>
创建新项目
- }
- >
-
-
+
+
+
+ {/* 项目列表 */}
+ {loading ? (
+
+ ) : projects.length === 0 ? (
+
+ 暂无项目
+ 创建您的第一个项目开始创作
+
+ }
+ >
+ }
+ onClick={() => navigate('/projects/progressive')}
+ >
+ 创建项目
+
+
+ ) : (
+
+ {/* 创作中的项目 */}
+ {inProgressProjects.length > 0 && (
+
+
+ 创作中 ({inProgressProjects.length})
+
+
+ {inProgressProjects.map(project => (
+
+
+
+ ))}
+
+
+ )}
+
+ {/* 草稿 */}
+ {draftProjects.length > 0 && (
+
+
+ 草稿 ({draftProjects.length})
+
+
+ {draftProjects.map(project => (
+
+
+
+ ))}
+
+
+ )}
+
+ {/* 已完成的项目 */}
+ {completedProjects.length > 0 && (
+
+
+ 已完成 ({completedProjects.length})
+
+
+ {completedProjects.map(project => (
+
+
+
+ ))}
+
+
+ )}
+
+ )}
)
}
diff --git a/frontend/src/pages/SkillManagement.tsx b/frontend/src/pages/SkillManagement.tsx
index d5de2ca..07fcceb 100644
--- a/frontend/src/pages/SkillManagement.tsx
+++ b/frontend/src/pages/SkillManagement.tsx
@@ -22,9 +22,12 @@ import {
Switch,
Empty,
Spin,
- Alert
+ Alert,
+ Progress,
+ List,
+ Typography
} from 'antd'
-import { SkillCreateWizard } from '@/components/SkillCreateWizard'
+import { SkillCreate } from '@/components/SkillCreate'
import {
PlusOutlined,
EyeOutlined,
@@ -39,11 +42,15 @@ import {
LockOutlined,
RobotOutlined,
EditFilled,
- ReloadOutlined
+ ReloadOutlined,
+ ClockCircleOutlined,
+ CloseCircleOutlined,
+ CheckOutlined
} from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
import { useSkillStore, Skill } from '@/stores/skillStore'
import { skillService, SkillDraft } from '@/services/skillService'
+import { taskService } from '@/services/taskService'
import { type ColumnsType, TablePaginationConfig } from 'antd/es/table'
import dayjs from 'dayjs'
@@ -99,6 +106,10 @@ export const SkillManagement = () => {
const [aiCategory, setAiCategory] = useState