import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { Sparkles, Hash, FileText } from 'lucide-react' import { useLLMStore } from '../../stores/llm-store' import { useWorkflowStore } from '../../stores/workflow-store' import { useProjectStore } from '../../stores/project-store' import { createConfigGenerationWorkflow } from '../../services/workflows/architecture-workflow' import { confirm } from '../ui/Confirm' import { toast } from '../ui/Toast' import { Dialog, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, } from '../ui/Dialog' import { Button } from '../ui/Button' import { Input } from '../ui/Input' import { Textarea } from '../ui/Textarea' import type { NovelConfig } from '../../shared/ipc-channels' interface Props { isOpen: boolean onClose: () => void /** 生成完成后回调(传入 AI 生成的配置) */ onGenerated: (config: Partial) => void } /** AI 生成配置对话框 — 用户输入脑洞,AI 自动生成所有配置字段 */ export default function GenerateConfigDialog({ isOpen, onClose, onGenerated }: Props) { const { t } = useTranslation('dialogs') const defaultModelId = useLLMStore(s => s.defaultModelId) // ✅ 用 getState() 获取 action,不订阅 workflow store 的 globalLogs 高频更新 const addLog = useWorkflowStore.getState().addLog const startWorkflow = useWorkflowStore.getState().startWorkflow const currentProject = useProjectStore(s => s.currentProject) const updateNovelConfig = useProjectStore(s => s.updateNovelConfig) const [idea, setIdea] = useState('') // 控制当外部 Confirm 弹窗显示时,阻止本 Dialog 因为"点击外部"而意外关闭 const [confirming, setConfirming] = useState(false) // 直接从 Store 读取规模参数 — 单一数据源,无需 local state 镜像 const totalChapters = currentProject?.novelConfig.totalChapters ?? 100 const wordsPerChapter = currentProject?.novelConfig.wordsPerChapter ?? 3000 /** 修改总章数:直接写 Store,允许清空(失焦时由 Input 组件全局兜底) */ const handleTotalChaptersChange = (val: string) => { if (val === '') { updateNovelConfig({ totalChapters: '' as unknown as number }) return } const n = parseInt(val, 10) if (isNaN(n)) return updateNovelConfig({ totalChapters: n }) } /** 修改每章字数:直接写 Store,允许清空(失焦时由 Input 组件全局兜底) */ const handleWordsPerChapterChange = (val: string) => { if (val === '') { updateNovelConfig({ wordsPerChapter: '' as unknown as number }) return } const n = parseInt(val, 10) if (isNaN(n)) return updateNovelConfig({ wordsPerChapter: n }) } const [isSubmitting, setIsSubmitting] = useState(false) const isSubmittingRef = useRef(false) const handleGenerate = async () => { if (!idea.trim() || isSubmittingRef.current) return if (!defaultModelId) { addLog('error', t('generateConfig.needModel')) return } isSubmittingRef.current = true setIsSubmitting(true) try { // 检测已填写的核心字段,提示用户确认覆盖 const cfg = currentProject?.novelConfig const filledFields: string[] = [] if (cfg?.coreOutline?.trim()) filledFields.push(t('generateConfig.fields.outline')) if (cfg?.worldSetting?.trim()) filledFields.push(t('generateConfig.fields.worldbuilding')) if (cfg?.goldenFinger?.trim()) filledFields.push(t('generateConfig.fields.goldenFinger')) if (cfg?.protagonistProfile?.trim()) filledFields.push(t('generateConfig.fields.protagonist')) if (cfg?.globalGuidance?.trim()) filledFields.push(t('generateConfig.fields.writingRequirements')) if (cfg?.subGenre?.trim()) filledFields.push(t('generateConfig.fields.subGenre')) if (filledFields.length > 0) { setConfirming(true) const fieldList = filledFields.map(f => `• ${f}`).join('\n') const ok = await confirm( t('generateConfig.overwriteConfirm', { fields: fieldList }), { title: t('generateConfig.configExists'), confirmText: t('common.override', { ns: 'common' }), cancelText: t('common.cancel', { ns: 'common' }) } ) setConfirming(false) if (!ok) return } // 覆盖确认通过后,立即关闭弹窗 onClose() toast.info(t('generateConfig.generating')) addLog('info', t('generateConfig.generatingLog', { chapters: totalChapters, words: wordsPerChapter })) // 后台执行 LLM 调用(由 WorkflowEngine 接管并显示全局状态面板) startWorkflow( createConfigGenerationWorkflow({ idea, totalChapters: totalChapters || 100, wordsPerChapter: wordsPerChapter || 3000, onGenerated, }) ) } finally { isSubmittingRef.current = false setIsSubmitting(false) } } const handleOpenChange = (open: boolean) => { // 确认弹框期间不允许关闭 if (!open && !confirming) onClose() } // 每次打开时预填当前项目的核心大纲 const defaultIdea = currentProject?.novelConfig.coreOutline || '' return ( { // 当全局 Confirm 弹窗弹出时,点击 Confirm (由于渲染在 Body) // 会被 Radix 误认为是 Interact Outside。因此此时屏蔽关闭事件 if (confirming) e.preventDefault() }} > {t('generateConfig.title')} {t('generateConfig.description')}
{/* 规模参数(联动小说配置)*/}
{/* 标题行:左侧「规模参数」,右侧「全书约 x 字」 */}
{t('generateConfig.scaleParams')} {t('generateConfig.totalApprox')}{' '} {((Number(totalChapters) || 0) * (Number(wordsPerChapter) || 0)).toLocaleString()} {' '} {t('generateConfig.wordsUnit')}
{/* 两列输入框 */}
{/* 总章数 */}
handleTotalChaptersChange(e.target.value)} onBlur={() => { if (!totalChapters || totalChapters < 1) { updateNovelConfig({ totalChapters: 100 }) } }} />
{/* 每章字数 */}
handleWordsPerChapterChange(e.target.value)} onBlur={() => { if (!wordsPerChapter || wordsPerChapter < 100) { updateNovelConfig({ wordsPerChapter: 3000 }) } }} />
{/* 创作脑洞输入框 */}