import { useState, useEffect, useRef } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { Steps, Form, Input, InputNumber, Select, Button, message, Card, Spin, Row, Col, Typography, Modal, Space, Divider, Popconfirm } from 'antd'; import { RocketOutlined, GlobalOutlined, TeamOutlined, FileTextOutlined, LoadingOutlined, ArrowLeftOutlined, HomeOutlined, UserOutlined, EditOutlined, RedoOutlined } from '@ant-design/icons'; import { wizardStreamApi, characterApi, projectApi } from '../services/api'; import type { WorldBuildingResponse, Character, WizardBasicInfo, ApiError, CharacterUpdate, GenerateOutlineRequest, ProjectWizardUpdate } from '../types'; import { characterGridConfig } from '../components/CardStyles'; import { CharacterCard } from '../components/CharacterCard'; import { SSELoadingOverlay } from '../components/SSELoadingOverlay'; const { Step } = Steps; const { TextArea } = Input; const { Title, Paragraph, Text } = Typography; export default function ProjectWizardNew() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const [form] = Form.useForm(); const [characterForm] = Form.useForm(); const [worldForm] = Form.useForm(); const [current, setCurrent] = useState(0); const [loading, setLoading] = useState(false); const [isResumingWizard, setIsResumingWizard] = useState(false); const [isEditingWorld, setIsEditingWorld] = useState(false); const [isRegeneratingWorld, setIsRegeneratingWorld] = useState(false); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); useEffect(() => { const handleResize = () => { setIsMobile(window.innerWidth <= 768); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // SSE流式进度状态 const [progress, setProgress] = useState(0); const [progressMessage, setProgressMessage] = useState(''); // 步骤数据 const [basicInfo, setBasicInfo] = useState({ title: '', description: '', theme: '', genre: ['玄幻'], chapter_count: 30, narrative_perspective: '第三人称', character_count: 3, target_words: 100000, }); const [worldBuilding, setWorldBuilding] = useState(null); const [characters, setCharacters] = useState([]); const [projectId, setProjectId] = useState(''); const [requiredCharacterCount, setRequiredCharacterCount] = useState(5); // 角色编辑 const [isCharacterModalOpen, setIsCharacterModalOpen] = useState(false); const [editingCharacter, setEditingCharacter] = useState(null); const [modalType, setModalType] = useState<'character' | 'organization'>('character'); // 异步任务 const pollingIntervalRef = useRef(null); const hasRestoredRef = useRef(false); // 自动保存进度(当步骤改变时) useEffect(() => { const saveProgress = async () => { if (projectId && current > 0 && current < 4 && !isResumingWizard) { try { await projectApi.updateProject(projectId, { wizard_step: current } as ProjectWizardUpdate); } catch (error) { console.error('保存进度失败:', error); } } }; saveProgress(); }, [current, projectId, isResumingWizard]); // 恢复向导逻辑 useEffect(() => { const resumeWizard = async () => { const projectId = searchParams.get('projectId'); const step = searchParams.get('step'); // 防止重复执行 if (projectId && step && !hasRestoredRef.current) { hasRestoredRef.current = true; setIsResumingWizard(true); try { // 加载项目数据 const project = await projectApi.getProject(projectId); setProjectId(projectId); // 恢复基本信息 const restoredBasicInfo = { title: project.title, description: project.description || '', theme: project.theme || '', genre: project.genre ? project.genre.split('、') : [], chapter_count: project.chapter_count || 30, narrative_perspective: project.narrative_perspective || '第三人称', character_count: project.character_count || 3, target_words: project.target_words || 100000, }; setBasicInfo(restoredBasicInfo); // 恢复世界观 if (project.world_time_period) { const restoredWorldBuilding = { project_id: projectId, time_period: project.world_time_period, location: project.world_location || '', atmosphere: project.world_atmosphere || '', rules: project.world_rules || '', }; setWorldBuilding(restoredWorldBuilding); } // 恢复角色数据 if (parseInt(step) >= 2) { const response = await characterApi.getCharacters(projectId); console.log('恢复向导 - API响应:', response); // 处理可能的分页格式或直接数组格式 const chars = Array.isArray(response) ? response : ((response as unknown as { items: Character[] }).items || []); console.log('恢复向导 - 解析后的角色数组:', chars); setCharacters(chars); setRequiredCharacterCount(project.character_count || 5); } // 设置当前步骤 const currentStep = parseInt(step); setCurrent(currentStep); message.info('已恢复上次的创建进度'); } catch (error) { const apiError = error as ApiError; message.error('恢复向导失败:' + (apiError.response?.data?.detail || apiError.message || '未知错误')); } finally { setIsResumingWizard(false); } } }; resumeWizard(); }, [searchParams]); // 步骤定义 const steps = [ { title: '基本信息', icon: , description: '书名和主题' }, { title: '世界构建', icon: , description: 'AI生成世界观' }, { title: '角色设定', icon: , description: '生成和编辑角色' }, { title: '大纲生成', icon: , description: '生成章节大纲' }, { title: '完成创建', icon: , description: '创建项目' }, ]; // 第一步:基本信息 (使用SSE流式API) const handleBasicInfo = async (values: WizardBasicInfo) => { try { setLoading(true); setBasicInfo(values); setProgress(0); setProgressMessage('准备生成世界观...'); // 使用SSE流式API生成世界构建并创建项目 const result = await wizardStreamApi.generateWorldBuildingStream( { title: values.title, description: values.description, theme: values.theme, genre: Array.isArray(values.genre) ? values.genre.join('、') : values.genre, narrative_perspective: values.narrative_perspective, target_words: values.target_words, chapter_count: values.chapter_count, character_count: values.character_count, }, { onProgress: (msg, prog) => { setProgress(prog); setProgressMessage(msg); console.log(`进度 ${prog}%: ${msg}`); }, onResult: (data) => { console.log('世界观生成完成:', data); setProjectId(data.project_id); setWorldBuilding(data); }, onError: (error) => { message.error('生成世界观失败:' + error); }, onComplete: () => { message.success('世界观生成成功,项目已创建!'); setCurrent(1); setLoading(false); setProgress(0); setProgressMessage(''); } } ); // 如果result中有数据也要处理 if (result && result.project_id) { setProjectId(result.project_id); setWorldBuilding(result); } } catch (error) { const apiError = error as ApiError; message.error('生成世界观失败:' + (apiError.response?.data?.detail || apiError.message || '未知错误')); setLoading(false); setProgress(0); setProgressMessage(''); } }; // 第二步:确认世界构建,进入角色设定 (使用SSE流式API) const handleWorldBuildingNext = async (values: Partial & { character_count?: number }) => { try { setLoading(true); setRequiredCharacterCount(values.character_count || 5); setProgress(0); setProgressMessage('准备生成角色...'); // 用于存储生成的角色数量 let generatedCount = 0; // 使用SSE流式API生成角色 const result = await wizardStreamApi.generateCharactersStream( { project_id: projectId, count: values.character_count || 5, world_context: { time_period: worldBuilding?.time_period || '', location: worldBuilding?.location || '', atmosphere: worldBuilding?.atmosphere || '', rules: worldBuilding?.rules || '', }, theme: basicInfo.theme, genre: Array.isArray(basicInfo.genre) ? basicInfo.genre.join('、') : basicInfo.genre, }, { onProgress: (msg, prog) => { setProgress(prog); setProgressMessage(msg); console.log(`进度 ${prog}%: ${msg}`); }, onResult: (data) => { console.log('角色生成完成:', data); const generatedChars = data.characters || []; generatedCount = generatedChars.length; setCharacters(generatedChars); }, onError: (error) => { message.error('生成角色失败:' + error); }, onComplete: () => { message.success(`成功生成${generatedCount}个角色!`); setCurrent(2); setLoading(false); setProgress(0); setProgressMessage(''); } } ); // 如果result中有数据也要处理 if (result && result.characters) { setCharacters(result.characters); } } catch (error) { const apiError = error as ApiError; message.error('生成角色失败:' + (apiError.response?.data?.detail || apiError.message || '未知错误')); setLoading(false); setProgress(0); setProgressMessage(''); } }; // 打开角色编辑对话框 const handleEditCharacter = (character?: Character) => { if (character) { setEditingCharacter(character); characterForm.setFieldsValue(character); setModalType(character.is_organization ? 'organization' : 'character'); } else { setEditingCharacter(null); characterForm.resetFields(); } setIsCharacterModalOpen(true); }; // 保存角色(仅用于编辑) const handleSaveCharacter = async (values: CharacterUpdate) => { try { if (editingCharacter) { // 更新现有角色 await characterApi.updateCharacter(editingCharacter.id, values); const updatedChars = characters.map(c => c.id === editingCharacter.id ? { ...c, ...values } : c ); setCharacters(updatedChars); message.success('角色更新成功'); setIsCharacterModalOpen(false); characterForm.resetFields(); } } catch (error) { const apiError = error as ApiError; message.error('保存角色失败:' + (apiError.response?.data?.detail || apiError.message || '未知错误')); } }; // 删除角色 const handleDeleteCharacter = async (id: string) => { try { await characterApi.deleteCharacter(id); setCharacters(characters.filter(c => c.id !== id)); message.success('角色删除成功'); } catch (error) { const apiError = error as ApiError; message.error('删除角色失败:' + (apiError.response?.data?.detail || apiError.message || '未知错误')); } }; // 第三步:确认角色,进入大纲生成 const handleCharactersConfirm = async () => { if (characters.length !== requiredCharacterCount) { message.error(`请保持角色数量为${requiredCharacterCount}个`); return; } try { setLoading(true); // 更新向导步骤 await projectApi.updateProject(projectId, { wizard_step: 3 } as ProjectWizardUpdate); setCurrent(3); } catch (error) { const apiError = error as ApiError; message.error('更新进度失败:' + (apiError.response?.data?.detail || apiError.message || '未知错误')); } finally { setLoading(false); } }; // 第四步:生成大纲 (使用SSE流式API) const handleGenerateOutline = async (values: Partial) => { try { setLoading(true); setProgress(0); setProgressMessage('准备生成大纲...'); // 使用SSE流式API生成大纲 await wizardStreamApi.generateCompleteOutlineStream( { project_id: projectId, chapter_count: values.chapter_count || 20, narrative_perspective: values.narrative_perspective || '第三人称', target_words: values.target_words, }, { onProgress: (msg, prog) => { setProgress(prog); setProgressMessage(msg); console.log(`进度 ${prog}%: ${msg}`); }, onResult: (data) => { console.log('大纲生成完成:', data); }, onError: (error) => { message.error('生成大纲失败:' + error); }, onComplete: () => { message.success('大纲生成成功!'); setCurrent(4); setLoading(false); setProgress(0); setProgressMessage(''); } } ); } catch (error) { const apiError = error as ApiError; message.error('生成大纲失败:' + (apiError.response?.data?.detail || apiError.message || '未知错误')); setLoading(false); setProgress(0); setProgressMessage(''); } }; // 更新世界观 (使用SSE流式API) const handleUpdateWorldBuilding = async (values: Partial) => { try { setLoading(true); setProgress(0); setProgressMessage('准备更新世界观...'); const result = await wizardStreamApi.updateWorldBuildingStream( projectId, values, { onProgress: (msg, prog) => { setProgress(prog); setProgressMessage(msg); }, onResult: (data) => { setWorldBuilding(data); }, onError: (error) => { message.error('更新世界观失败:' + error); }, onComplete: () => { setIsEditingWorld(false); message.success('世界观更新成功!'); setLoading(false); setProgress(0); setProgressMessage(''); } } ); if (result) { setWorldBuilding(result); } } catch (error) { const apiError = error as ApiError; message.error('更新世界观失败:' + (apiError.response?.data?.detail || apiError.message || '未知错误')); setLoading(false); setProgress(0); setProgressMessage(''); } }; // 重新生成世界观 (使用SSE流式API) const handleRegenerateWorldBuilding = async () => { try { setIsRegeneratingWorld(true); setProgress(0); setProgressMessage('准备重新生成世界观...'); const result = await wizardStreamApi.regenerateWorldBuildingStream( projectId, {}, { onProgress: (msg, prog) => { setProgress(prog); setProgressMessage(msg); }, onResult: (data) => { setWorldBuilding(data); }, onError: (error) => { message.error('重新生成世界观失败:' + error); }, onComplete: () => { message.success('世界观重新生成成功!'); setIsRegeneratingWorld(false); setProgress(0); setProgressMessage(''); } } ); if (result) { setWorldBuilding(result); } } catch (error) { const apiError = error as ApiError; message.error('重新生成世界观失败:' + (apiError.response?.data?.detail || apiError.message || '未知错误')); setIsRegeneratingWorld(false); setProgress(0); setProgressMessage(''); } }; // 上一步 const handlePrev = async () => { // 如果在第一步之后返回,需要确认是否清理数据 if (current === 1 && projectId) { Modal.confirm({ title: '确认返回', content: '返回上一步将清除已创建的项目和世界观数据,是否继续?', okText: '确认', cancelText: '取消', centered: true, ...(isMobile && { style: { top: 'auto' } }), onOk: async () => { try { setLoading(true); setProgress(0); setProgressMessage('准备清理数据...'); await wizardStreamApi.cleanupWizardDataStream( projectId, { onProgress: (msg, prog) => { setProgress(prog); setProgressMessage(msg); }, onResult: (data) => { console.log('清理完成:', data); }, onError: (error) => { message.error('清理数据失败:' + error); }, onComplete: () => { setProjectId(''); setWorldBuilding(null); setCurrent(0); message.success('已清理数据'); setLoading(false); setProgress(0); setProgressMessage(''); } } ); } catch (error) { const apiError = error as ApiError; message.error('清理数据失败:' + (apiError.response?.data?.detail || apiError.message || '未知错误')); setLoading(false); setProgress(0); setProgressMessage(''); } }, }); } else if (current === 2 && projectId) { // 从角色设定返回到世界构建 Modal.confirm({ title: '确认返回', content: '返回上一步将清除已生成的角色数据,是否继续?', okText: '确认', cancelText: '取消', centered: true, ...(isMobile && { style: { top: 'auto' } }), onOk: async () => { try { setLoading(true); // 只删除角色,保留项目和世界观 const safeCharacters = Array.isArray(characters) ? characters : []; for (const char of safeCharacters) { await characterApi.deleteCharacter(char.id); } setCharacters([]); setCurrent(1); message.success('已清理角色数据'); } catch (error) { const apiError = error as ApiError; message.error('清理角色数据失败:' + (apiError.response?.data?.detail || apiError.message || '未知错误')); } finally { setLoading(false); } }, }); } else if (current === 3 && projectId) { // 从大纲生成返回到角色设定,需要加载角色数据 try { setLoading(true); const response = await characterApi.getCharacters(projectId); console.log('返回上一步 - API响应:', response); // 处理可能的分页格式或直接数组格式 const chars = Array.isArray(response) ? response : ((response as unknown as { items: Character[] }).items || []); console.log('返回上一步 - 解析后的角色数组:', chars); setCharacters(chars); setCurrent(2); message.success(`已加载${chars.length}个角色`); } catch (error) { const apiError = error as ApiError; console.error('加载角色数据失败:', error); message.error('加载角色数据失败:' + (apiError.response?.data?.detail || apiError.message || '未知错误')); } finally { setLoading(false); } } else { setCurrent(current - 1); } }; // 清理定时器 useEffect(() => { const intervalId = pollingIntervalRef.current; return () => { if (intervalId) { clearInterval(intervalId); } }; }, []); // 渲染第一步:基本信息 const renderBasicInfo = () => (