import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Button, Card, Form, Input, message, Spin, Space, Alert, Modal, Descriptions, Tag, Select, Radio, InputNumber, Row, Col, Divider } from 'antd'; import { ArrowLeftOutlined, RobotOutlined, CheckOutlined, DownloadOutlined, EditOutlined, UnorderedListOutlined, EyeOutlined } from '@ant-design/icons'; import { storage } from '../utils/indexedDB'; import { Novel, NovelGenerationParams, ChapterOutline } from '../types'; import { useOllama } from '../contexts/OllamaContext'; import { NovelSettingManager } from '../utils/novelSettingManager'; const { TextArea } = Input; const { Option } = Select; // 小说题材选项 const NOVEL_GENRES = [ '穿越', '都市', '修仙', '武侠', '玄幻', '科幻', '言情', '历史', '游戏', '灵异', '军事', '悬疑', '其他' ]; // 字数选项 const WORD_COUNT_OPTIONS = [ { label: '5万字 (短篇)', value: 50000 }, { label: '10万字 (中篇)', value: 100000 }, { label: '20万字 (长篇)', value: 200000 }, { label: '30万字 (超长篇)', value: 300000 }, { label: '50万字 (史诗篇)', value: 500000 }, { label: '100万字 (巨著)', value: 1000000 } ]; const NovelGenerate: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [novel, setNovel] = useState(null); const [loading, setLoading] = useState(false); const [generating, setGenerating] = useState(false); const [generatedSettings, setGeneratedSettings] = useState(null); const [previewVisible, setPreviewVisible] = useState(false); const [editModalVisible, setEditModalVisible] = useState(false); const [editForm] = Form.useForm(); const [form] = Form.useForm(); const [chapterListVisible, setChapterListVisible] = useState(false); const [editingChapter, setEditingChapter] = useState(null); const [chapterEditModalVisible, setChapterEditModalVisible] = useState(false); const [chapterEditForm] = Form.useForm(); const [viewingChapter, setViewingChapter] = useState(null); const [chapterViewModalVisible, setChapterViewModalVisible] = useState(false); const [forceUpdate, setForceUpdate] = useState(0); // 添加强制更新状态 const { chat } = useOllama(); // 辅助函数:生成模块名称 const getModuleName = (index: number): string => { const moduleNames = ['起始阶段', '发展阶段', '转折阶段', '高潮阶段', '解决阶段', '结局阶段']; return moduleNames[index % moduleNames.length]; }; // 辅助函数:生成模块描述 const getModuleDescription = (index: number): string => { const descriptions = [ '开篇设定,引入主要冲突和人物', '情节推进,矛盾升级,人物成长', '故事转折,新的挑战和机遇', '核心冲突爆发,达到最高潮', '矛盾逐步解决,铺垫结局', '收尾总结,主题升华' ]; return descriptions[index % descriptions.length]; }; // 辅助函数:生成章节范围 const getChapterRange = (moduleIndex: number, totalChapters: number, totalModules: number): string => { const chaptersPerModule = Math.ceil(totalChapters / totalModules); const start = moduleIndex * chaptersPerModule + 1; const end = Math.min((moduleIndex + 1) * chaptersPerModule, totalChapters); return `${start}-${end}`; }; useEffect(() => { if (id) { loadNovel(); } }, [id]); const loadNovel = async () => { if (!id) return; try { setLoading(true); const novels = await storage.getNovels(); const currentNovel = novels.find((n: Novel) => n.id === id); if (!currentNovel) { message.error('小说不存在'); navigate('/novels'); return; } setNovel(currentNovel); if (currentNovel.generatedSettings) { setGeneratedSettings(currentNovel.generatedSettings); } form.setFieldsValue({ title: currentNovel.title, genre: currentNovel.genre === '未分类' ? undefined : currentNovel.genre, targetWordCount: currentNovel.generatedSettings?.targetWordCount || 50000, customRequirements: '' }); } catch (error) { message.error('加载数据失败'); } finally { setLoading(false); } }; const handleGenerateSettings = async () => { if (!novel) return; try { const values = await form.validateFields(); setGenerating(true); const params: NovelGenerationParams = { title: values.title, genre: values.genre || '未分类', customRequirements: values.customRequirements }; // 计算章节数量和每章字数(每章900-1200字,平均1050字) const avgWordsPerChapter = 1050; // 平均每章字数 const targetWordCount = params.targetWordCount || 50000; const chapterCount = Math.round(targetWordCount / avgWordsPerChapter); // 5万字约48章 const wordsPerChapterMin = 900; // 最少字数 const wordsPerChapterMax = 1200; // 最多字数 const moduleCount = params.preferredModuleCount || Math.max(3, Math.round(chapterCount / 15)); // 每15章左右一个模块 // 只生成基础设定,不包含任何章节规划 message.info({ content: '正在生成基础设定...', duration: 2 }); const basePrompt = `请为小说《${params.title}》${params.genre ? `(${params.genre}题材)` : ''}生成基础创作设定。 ## 📏 篇幅要求 - **总目标字数**:${targetWordCount.toLocaleString()}字 - **章节数量**:${chapterCount}章 - **每章字数**:${wordsPerChapterMin}-${wordsPerChapterMax}字(平均${avgWordsPerChapter}字) - **模块划分**:${moduleCount}个模块 ${params.customRequirements ? `## 🎯 特殊要求\n${params.customRequirements}\n\n` : ''} 请按照以下格式生成基础设定: ## 📖 故事大纲 简要描述整个故事的核心情节和主题(150字以内) ## 🏗️ 情节结构 按模块划分整个故事的结构:${Array.from({length: moduleCount}, (_, i) => ` - 模块${i + 1}:${getModuleName(i)}(第${getChapterRange(i, chapterCount, moduleCount)}章)- ${getModuleDescription(i)}`).join('')} ## 👥 人物设定 主要人物的详细设定,包括: - 主角:姓名、性格、背景、目标、能力特长(150字内) - 配角:重要配角设定(至少3个,每个100字内) - 反派:对立角色设定(100字内) 请开始生成基础设定:`; const baseResponse = await chat(basePrompt, `小说《${novel.title}》的基础设定生成`); // 解析基础设定 const baseSettings = parseBaseSettings(baseResponse); // 生成初始的章节规划结构(完全空的章节列表) const initialSettings = { storyOutline: baseSettings.storyOutline, plotStructure: baseSettings.plotStructure, characters: baseSettings.characters, targetWordCount: targetWordCount, chapterCount: chapterCount, moduleCount: moduleCount, chapters: [] as any[], // 完全空的章节列表 chapterOutline: '' }; setGeneratedSettings(initialSettings); message.success('基础设定生成成功!现在可以开始手动生成章节标题和细纲。'); } catch (error: any) { message.error(error.message || 'AI生成失败,请重试'); } finally { setGenerating(false); } }; // 手动生成单个章节规划 const handleGenerateSingleChapter = async (chapterNumber: number) => { if (!novel || !generatedSettings) return; try { const params = { targetWordCount: generatedSettings.targetWordCount, chapterCount: generatedSettings.chapterCount, moduleCount: generatedSettings.moduleCount, avgWordsPerChapter: Math.round(generatedSettings.targetWordCount / generatedSettings.chapterCount), wordsPerChapterMin: 900, wordsPerChapterMax: 1200 }; // 计算当前章节应该属于哪个模块 const moduleNumber = Math.min(Math.ceil(chapterNumber / Math.ceil(params.chapterCount / params.moduleCount)), params.moduleCount); message.info({ content: `正在生成第${chapterNumber}章的标题和细纲...`, duration: 2 }); // 使用函数式状态更新获取最新的chapters const currentChapters = generatedSettings.chapters || []; const chapterPrompt = `基于以下小说设定,为第${chapterNumber}章生成详细规划。 ## 小说基础设定 ### 📖 故事大纲 ${generatedSettings.storyOutline} ### 🏗️ 情节结构 ${generatedSettings.plotStructure} ### 👥 人物设定 ${generatedSettings.characters} ## 章节规划要求 **当前章节**:第${chapterNumber}章(共${params.chapterCount}章) **所属模块**:模块${moduleNumber} **每章字数**:${params.wordsPerChapterMin}-${params.wordsPerChapterMax}字 ${currentChapters.length > 0 ? `## 前面章节概览\n\n${currentChapters.slice(-3).map((ch: any) => `第${ch.chapterNumber}章:${ch.title}\n细纲:${ch.outline.substring(0, 50)}...` ).join('\n\n')}\n\n` : ''} 请按照以下格式生成第${chapterNumber}章的完整规划: 第${chapterNumber}章:章节标题 - 模块${moduleNumber} 细纲:本章的详细细纲描述,包括主要情节、人物发展、冲突转折等(100-200字)。 预计字数:1050字(范围900-1200字) ⚠️ **重要提醒**: 1. 章节细纲必须控制在100-200字之间,既要详细又不能过长 2. 要与前面章节自然衔接${chapterNumber > 1 ? '(这是后续章节的延续)' : '(这是开篇章节)'} 3. 严格按照格式生成,确保能正确解析 请开始生成第${chapterNumber}章的详细规划:`; const chapterResponse = await chat(chapterPrompt, `小说《${novel.title}》第${chapterNumber}章规划`); // 解析单个章节 const parsedChapter = parseSingleChapter(chapterResponse, chapterNumber, moduleNumber, params); // 检查解析是否成功 if (!parsedChapter || !parsedChapter.title || !parsedChapter.outline) { throw new Error('章节解析失败,请重试'); } // 使用函数式状态更新,确保基于最新状态 setGeneratedSettings((prevSettings: any) => { const currentChapters = prevSettings.chapters || []; // 添加到章节列表 - 深拷贝确保引用正确 const updatedChapters = currentChapters.map((ch: any) => ({ ...ch })); // 替换或添加章节 const existingIndex = updatedChapters.findIndex((ch: any) => ch.chapterNumber === chapterNumber); if (existingIndex >= 0) { updatedChapters[existingIndex] = { ...parsedChapter }; } else { updatedChapters.push({ ...parsedChapter }); } // 按章节号排序 updatedChapters.sort((a: any, b: any) => a.chapterNumber - b.chapterNumber); // 返回全新的设置对象,确保React检测到变化 const newSettings = { ...prevSettings, chapters: updatedChapters, // 添加一个时间戳确保对象总是新的 lastUpdated: Date.now() }; // 强制触发UI更新 setTimeout(() => { setForceUpdate(prev => prev + 1); }, 50); return newSettings; }); message.success(`第${chapterNumber}章的标题和细纲生成成功!`); } catch (error: any) { message.error(error.message || '章节生成失败,请重试'); } }; // 批量生成多个章节规划 const handleGenerateBatchChapters = async (startChapter: number, endChapter: number) => { if (!novel || !generatedSettings) return; try { setGenerating(true); for (let chapterNum = startChapter; chapterNum <= endChapter; chapterNum++) { // 生成单个章节 await handleGenerateSingleChapter(chapterNum); // 等待更长时间,确保状态更新和界面渲染完成 await new Promise(resolve => setTimeout(resolve, 300)); } message.success(`第${startChapter}-${endChapter}章的标题和细纲批量生成成功!`); } catch (error: any) { message.error(error.message || '批量生成失败,请重试'); } finally { setGenerating(false); } }; // 解析单个章节 const parseSingleChapter = (response: string, chapterNumber: number, moduleNumber: number, metadata: any) => { const lines = response.split('\n'); let chapter: any = { chapterNumber: chapterNumber, title: `第${chapterNumber}章`, moduleNumber: moduleNumber, outline: '', estimatedWords: metadata.avgWordsPerChapter }; let foundChapter = false; let inOutline = false; lines.forEach(line => { const trimmedLine = line.trim(); // 匹配章节标题行:第1章:意外穿越 - 模块1 const chapterMatch = trimmedLine.match(/第(\d+)章[::]\s*(.+?)\s*[-—]\s*模块(\d+)/); if (chapterMatch) { const matchedChapterNum = parseInt(chapterMatch[1]); if (matchedChapterNum === chapterNumber) { chapter.title = chapterMatch[2].trim(); chapter.moduleNumber = parseInt(chapterMatch[3]); foundChapter = true; } } else if (trimmedLine.includes('细纲:')) { // 开始收集细纲内容 const outlineStart = trimmedLine.indexOf('细纲:') + 3; chapter.outline = trimmedLine.substring(outlineStart).trim(); inOutline = true; } else if (trimmedLine.includes('预计字数:')) { // 解析字数 const wordMatch = trimmedLine.match(/(\d+)/); if (wordMatch) { chapter.estimatedWords = parseInt(wordMatch[1]); } inOutline = false; // 细纲结束 } else if (inOutline && trimmedLine && !trimmedLine.includes('第') && !trimmedLine.includes('模块')) { // 继续收集细纲内容 if (chapter.outline) { chapter.outline += ' ' + trimmedLine; } else { chapter.outline = trimmedLine; } } }); // 如果没有找到章节标题,使用默认值 if (!foundChapter) { console.warn(`章节${chapterNumber}解析失败,使用默认值`); chapter.title = `第${chapterNumber}章`; } // 确保细纲不为空 if (!chapter.outline) { chapter.outline = `第${chapterNumber}章的详细细纲(待补充)`; } return chapter; }; const parseBaseSettings = (response: string) => { const settings: any = { storyOutline: '', plotStructure: '', characters: '' }; const lines = response.split('\n'); let currentSection = ''; let content = ''; lines.forEach(line => { if (line.startsWith('## ')) { if (currentSection && content) { switch(currentSection) { case '📖 故事大纲': case '故事大纲': settings.storyOutline = content.trim(); break; case '🏗️ 情节结构': case '情节结构': settings.plotStructure = content.trim(); break; case '👥 人物设定': case '人物设定': settings.characters = content.trim(); break; } } currentSection = line.replace('## ', '').trim(); content = ''; } else if (line.trim()) { content += line + '\n'; } }); // 处理最后一个section if (currentSection && content) { if (currentSection.includes('故事大纲')) { settings.storyOutline = content.trim(); } else if (currentSection.includes('情节结构')) { settings.plotStructure = content.trim(); } else if (currentSection.includes('人物设定')) { settings.characters = content.trim(); } } return settings; }; const parseChapterBatch = (response: string, startChapter: number, metadata: any) => { const chapters: any[] = []; const lines = response.split('\n'); let currentChapter: any = null; let expectedChapterNum = startChapter; lines.forEach(line => { // 匹配章节标题行:第1章:意外穿越 - 模块1 const chapterMatch = line.match(/第(\d+)章[::]\s*(.+?)\s*[-—]\s*模块(\d+)/); if (chapterMatch) { if (currentChapter && currentChapter.chapterNumber) { chapters.push(currentChapter); } currentChapter = { chapterNumber: parseInt(chapterMatch[1]), title: chapterMatch[2].trim(), moduleNumber: parseInt(chapterMatch[3]), outline: '', estimatedWords: metadata.avgWordsPerChapter }; expectedChapterNum++; } else if (line.includes('细纲:') && currentChapter) { currentChapter.outline = line.replace('细纲:', '').trim(); } else if (line.includes('预计字数:') && currentChapter) { const wordMatch = line.match(/(\d+)/); if (wordMatch) { currentChapter.estimatedWords = parseInt(wordMatch[1]); } } else if (line.trim() && currentChapter && !line.includes('第') && !line.includes('模块')) { if (currentChapter.outline) { currentChapter.outline += ' ' + line.trim(); } else { currentChapter.outline = line.trim(); } } }); if (currentChapter && currentChapter.chapterNumber) { chapters.push(currentChapter); } return chapters; }; const handleConfirmSettings = async () => { if (!novel || !generatedSettings || !id) return; try { // 确保保存所有章节信息,包括未生成的章节 const completeSettings = { ...generatedSettings, // 确保章节数组包含所有章节的占位符 chapters: Array.from({ length: generatedSettings.chapterCount }, (_, i) => { const chapterNum = i + 1; const existingChapter = generatedSettings.chapters?.find((ch: any) => ch.chapterNumber === chapterNum); // 如果该章节已存在,使用现有数据 if (existingChapter) { return existingChapter; } // 如果该章节不存在,创建占位符 return { chapterNumber: chapterNum, title: `第${chapterNum}章(待生成标题)`, moduleNumber: Math.min(Math.ceil(chapterNum / Math.ceil(generatedSettings.chapterCount / generatedSettings.moduleCount)), generatedSettings.moduleCount), outline: `第${chapterNum}章的细纲(待生成)`, estimatedWords: Math.round(generatedSettings.targetWordCount / generatedSettings.chapterCount), status: 'pending' }; }) }; // 生成并保存设置文件 const settingFilePath = await NovelSettingManager.generateAndSaveSettingFile( novel.title, completeSettings ); // 更新小说记录 await storage.updateNovel(id, { generatedSettings: completeSettings, settingFilePath, outline: completeSettings.storyOutline || novel.outline, targetWordCount: completeSettings.targetWordCount }); message.success('设定已保存并生成了配置文件,现在可以开始创作章节!'); // 更新本地状态 setGeneratedSettings(completeSettings); // 重新加载数据 await loadNovel(); } catch (error: any) { message.error(error.message || '保存失败,请重试'); } }; const handleEditSettings = () => { if (generatedSettings) { editForm.setFieldsValue({ storyOutline: generatedSettings.storyOutline, plotStructure: generatedSettings.plotStructure, characters: generatedSettings.characters, targetWordCount: generatedSettings.targetWordCount, chapterCount: generatedSettings.chapterCount, chapterOutline: generatedSettings.chapterOutline }); setEditModalVisible(true); } }; const handleSaveEdit = async () => { try { const values = await editForm.validateFields(); if (!novel?.generatedSettings) return; // 更新generatedSettings const updatedSettings = { ...novel.generatedSettings, ...values }; // 保存到数据库 await storage.updateNovel(novel.id, { generatedSettings: updatedSettings }); // 更新本地状态 setNovel({ ...novel, generatedSettings: updatedSettings }); setGeneratedSettings(updatedSettings); setEditModalVisible(false); message.success('设定更新成功'); } catch (error) { message.error('保存失败'); } }; const handleViewChapter = (chapter: ChapterOutline) => { setViewingChapter(chapter); setChapterViewModalVisible(true); }; const handleEditChapter = (chapter: ChapterOutline) => { setEditingChapter(chapter); chapterEditForm.setFieldsValue({ title: chapter.title, moduleNumber: chapter.moduleNumber, outline: chapter.outline, estimatedWords: chapter.estimatedWords }); setChapterEditModalVisible(true); }; const handleSaveChapterEdit = async () => { try { const values = await chapterEditForm.validateFields(); if (!generatedSettings || !editingChapter) return; // 更新章节信息 const updatedChapters = generatedSettings.chapters.map((chapter: any) => chapter.chapterNumber === editingChapter.chapterNumber ? { ...chapter, ...values } : chapter ); // 更新generatedSettings const updatedSettings = { ...generatedSettings, chapters: updatedChapters }; setGeneratedSettings(updatedSettings); // 如果已保存设定,同时更新到数据库 if (novel?.settingFilePath) { await storage.updateNovel(novel.id, { generatedSettings: updatedSettings }); } setChapterEditModalVisible(false); chapterEditForm.resetFields(); message.success('章节标题和细纲更新成功'); } catch (error) { message.error('保存失败'); } }; const handleRegenerate = () => { setPreviewVisible(false); setGeneratedSettings(null); }; if (loading) { return (
); } if (!novel) { return (
); } return (