From de4c7c3baaa1b9370c19e68dece9d1d6aefd5379 Mon Sep 17 00:00:00 2001 From: extreme Date: Fri, 27 Mar 2026 16:27:28 +0800 Subject: [PATCH] Refactor chapter preparation steps and enhance script processing features --- front/src/App.tsx | 5 +- .../components/ChapterRawTextEditorModal.tsx | 257 ++++++++++++++++-- .../chapter/prep/ChapterPrepLayout.tsx | 9 +- .../chapter/prep/steps/DivideStep.tsx | 4 +- .../chapter/prep/steps/ExtractProjectStep.tsx | 2 +- 5 files changed, 236 insertions(+), 41 deletions(-) diff --git a/front/src/App.tsx b/front/src/App.tsx index 3033c57..96633ca 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -19,7 +19,6 @@ import AgentManagement from './pages/aiStudio/agents/AgentManagement' import AgentEdit from './pages/aiStudio/agents/AgentEdit.tsx' import ModelManagement from './pages/aiStudio/models/ModelManagement' import ChapterPrepLayout from './pages/aiStudio/chapter/prep/ChapterPrepLayout' -import ConsistencyStep from './pages/aiStudio/chapter/prep/steps/ConsistencyStep' import DivideStep from './pages/aiStudio/chapter/prep/steps/DivideStep' import ExtractProjectStep from './pages/aiStudio/chapter/prep/steps/ExtractProjectStep' import { ChapterShotsPage } from './pages/aiStudio/shots/ChapterShotsPage' @@ -36,8 +35,8 @@ const App: React.FC = () => { } /> } /> }> - } /> - } /> + } /> + } /> } /> } /> diff --git a/front/src/pages/aiStudio/chapter/components/ChapterRawTextEditorModal.tsx b/front/src/pages/aiStudio/chapter/components/ChapterRawTextEditorModal.tsx index 4c667dc..24e784e 100644 --- a/front/src/pages/aiStudio/chapter/components/ChapterRawTextEditorModal.tsx +++ b/front/src/pages/aiStudio/chapter/components/ChapterRawTextEditorModal.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react' -import { Button, Collapse, Input, Modal, Space, Spin, Tag, message } from 'antd' +import { Button, Card, Collapse, Empty, Input, List, Modal, Space, Spin, Tag, message } from 'antd' import { DiffOutlined, FileTextOutlined, @@ -8,7 +8,8 @@ import { SaveOutlined, ThunderboltOutlined, } from '@ant-design/icons' -import { StudioChaptersService } from '../../../../services/generated' +import { ScriptProcessingService, StudioChaptersService } from '../../../../services/generated' +import type { ScriptConsistencyCheckResult } from '../../../../services/generated' type EditorMode = 'raw' | 'condensed' | 'compare' @@ -33,17 +34,27 @@ export function ChapterRawTextEditorModal({ const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) const [extracting, setExtracting] = useState(false) + const [checkingConsistency, setCheckingConsistency] = useState(false) + const [optimizingScript, setOptimizingScript] = useState(false) const [historyOpen, setHistoryOpen] = useState(false) const [mode, setMode] = useState('raw') const [rawText, setRawText] = useState('') const [condensedText, setCondensedText] = useState('') + const [savedRawText, setSavedRawText] = useState('') + const [savedCondensedText, setSavedCondensedText] = useState('') const [editorText, setEditorText] = useState('') const [compareRaw, setCompareRaw] = useState('') const [compareCondensed, setCompareCondensed] = useState('') + const [consistencyResult, setConsistencyResult] = useState(null) const plainWordCount = useMemo(() => editorText.trim().length, [editorText]) const paragraphCount = useMemo(() => editorText.split(/\n\s*\n/).filter((p) => p.trim()).length, [editorText]) + const actionsLoading = extracting || checkingConsistency || optimizingScript + const consistencyIssues = useMemo(() => { + const list = (consistencyResult?.issues as any[] | undefined) ?? [] + return Array.isArray(list) ? list : [] + }, [consistencyResult]) useEffect(() => { if (!open) return @@ -56,10 +67,13 @@ export function ChapterRawTextEditorModal({ const nextCondensed = data?.condensed_text ?? '' setRawText(nextRaw) setCondensedText(nextCondensed) + setSavedRawText(nextRaw) + setSavedCondensedText(nextCondensed) setMode('raw') setEditorText(nextRaw) setCompareRaw(nextRaw) setCompareCondensed(nextCondensed) + setConsistencyResult(null) }) .catch(() => { message.error('加载章节失败') @@ -74,9 +88,16 @@ export function ChapterRawTextEditorModal({ } setExtracting(true) try { - // TODO: 智能精简接口未接入,后续在此替换为真实接口调用,并将返回结果写入 condensedText - await new Promise((r) => setTimeout(r, 500)) - const simplified = '已精简' + const res = await ScriptProcessingService.simplifyScriptApiV1ScriptProcessingSimplifyScriptPost({ + requestBody: { + script_text: rawText, + }, + }) + const simplified = res.data?.simplified_script_text?.trim() ?? '' + if (!simplified) { + message.error('智能精简失败:未返回有效内容') + return + } setCondensedText(simplified) if (mode === 'compare') { // 对比模式下不切换编辑区:只更新右侧精简内容输入框 @@ -86,11 +107,75 @@ export function ChapterRawTextEditorModal({ setEditorText(simplified) } message.success('智能精简完成') + } catch { + message.error('智能精简失败') } finally { setExtracting(false) } } + const handleCheckConsistency = async () => { + const scriptText = rawText.trim() + if (!scriptText) { + message.warning('请先输入原文') + return + } + setCheckingConsistency(true) + try { + const res = await ScriptProcessingService.checkConsistencyApiV1ScriptProcessingCheckConsistencyPost({ + requestBody: { script_text: scriptText }, + }) + const data = res.data + if (!data) { + message.error(res.message || '一致性检查失败') + return + } + setConsistencyResult(data) + if (data.has_issues) message.warning(`发现 ${data.issues?.length ?? 0} 个角色混淆问题`) + else message.success('未发现角色混淆问题') + } catch (e: any) { + message.error(e?.message || '一致性检查失败') + } finally { + setCheckingConsistency(false) + } + } + + const handleOneClickOptimize = async () => { + const scriptText = rawText.trim() + if (!scriptText) { + message.warning('请先输入原文') + return + } + if (!consistencyResult) { + message.info('请先进行角色混淆检查') + return + } + setOptimizingScript(true) + try { + const res = await ScriptProcessingService.optimizeScriptApiV1ScriptProcessingOptimizeScriptPost({ + requestBody: { + script_text: scriptText, + consistency: consistencyResult as any, + }, + }) + const optimized = res.data?.optimized_script_text?.trim() ?? '' + if (!optimized) { + message.error(res.message || '一键优化失败') + return + } + // 优化后回写到主输入区(以原文视图承载) + setRawText(optimized) + setEditorText(optimized) + setMode('raw') + setCompareRaw(optimized) + message.success('一键优化完成') + } catch (e: any) { + message.error(e?.message || '一键优化失败') + } finally { + setOptimizingScript(false) + } + } + const handleBackToRaw = () => { setMode('raw') setEditorText(rawText) @@ -105,8 +190,8 @@ export function ChapterRawTextEditorModal({ setEditorText(condensedText) } - const handleSave = async () => { - if (!chapterId) return + const handleSave = async (): Promise => { + if (!chapterId) return false setSaving(true) try { if (mode === 'raw') { @@ -115,9 +200,10 @@ export function ChapterRawTextEditorModal({ requestBody: { raw_text: editorText }, }) setRawText(editorText) + setSavedRawText(editorText) onSaved?.({ rawText: editorText }) message.success('原文已保存') - return + return true } if (mode === 'condensed') { @@ -126,9 +212,10 @@ export function ChapterRawTextEditorModal({ requestBody: { condensed_text: editorText }, }) setCondensedText(editorText) + setSavedCondensedText(editorText) onSaved?.({ condensedText: editorText }) message.success('精简内容已保存') - return + return true } await StudioChaptersService.updateChapterApiV1StudioChaptersChapterIdPatch({ @@ -137,15 +224,46 @@ export function ChapterRawTextEditorModal({ }) setRawText(compareRaw) setCondensedText(compareCondensed) + setSavedRawText(compareRaw) + setSavedCondensedText(compareCondensed) onSaved?.({ rawText: compareRaw, condensedText: compareCondensed }) message.success('已保存') + return true } catch { message.error('保存失败') + return false } finally { setSaving(false) } } + const hasUnsavedChanges = useMemo(() => { + if (mode === 'compare') return compareRaw !== savedRawText || compareCondensed !== savedCondensedText + if (mode === 'raw') return editorText !== savedRawText + return editorText !== savedCondensedText + }, [compareCondensed, compareRaw, editorText, mode, savedCondensedText, savedRawText]) + + const handleRequestClose = () => { + if (actionsLoading) return + if (!hasUnsavedChanges) { + onClose() + return + } + Modal.confirm({ + title: '检测到未保存变更', + content: '文本输入区有未保存修改,关闭前请选择操作。', + okText: '保存', + cancelText: '忽略', + onOk: async () => { + const ok = await handleSave() + if (ok) onClose() + }, + onCancel: () => { + onClose() + }, + }) + } + const mockHistory: HistoryItem[] = useMemo( () => [ { @@ -176,26 +294,37 @@ export function ChapterRawTextEditorModal({ {paragraphCount} 段 - + {mode === 'condensed' ? ( - ) : ( - } open={open} - onCancel={onClose} + onCancel={() => { + handleRequestClose() + }} width={900} footer={ - } - styles={{ body: { maxHeight: '70vh', overflow: 'auto', paddingTop: 12 } }} + styles={{ + header: { paddingRight: 48 }, + body: { maxHeight: '70vh', overflow: 'auto', paddingTop: 12 }, + }} > {loading ? (
@@ -246,6 +382,7 @@ export function ChapterRawTextEditorModal({ value={compareRaw} onChange={(e) => setCompareRaw(e.target.value)} rows={14} + disabled={actionsLoading} style={{ resize: 'none', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace' }} />
@@ -255,23 +392,85 @@ export function ChapterRawTextEditorModal({ value={compareCondensed} onChange={(e) => setCompareCondensed(e.target.value)} rows={14} + disabled={actionsLoading} style={{ resize: 'none', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace' }} /> ) : ( - { - const v = e.target.value - setEditorText(v) - if (mode === 'raw') setRawText(v) - if (mode === 'condensed') setCondensedText(v) - }} - placeholder={mode === 'raw' ? '编辑章节原文…' : '编辑精简内容…'} - rows={16} - style={{ resize: 'none', background: '#fdfdfd' }} - /> +
+ { + const v = e.target.value + setEditorText(v) + if (mode === 'raw') setRawText(v) + if (mode === 'condensed') setCondensedText(v) + }} + disabled={actionsLoading} + placeholder={mode === 'raw' ? '编辑章节原文…' : '编辑精简内容…'} + rows={16} + style={{ resize: 'none', background: '#fdfdfd' }} + /> + {consistencyResult ? ( + + + {consistencyResult.has_issues ? '发现问题' : '无问题'} + + issues:{consistencyIssues.length} + + + } + > + {consistencyIssues.length === 0 ? ( + + ) : ( + ( + +
+
+ {it?.issue_type ? `[${it.issue_type}] ` : ''} + Issue {idx + 1} +
+
{it?.description}
+ {it?.character_candidates?.length ? ( +
候选角色:{it.character_candidates.join('、')}
+ ) : null} + {it?.suggestion ? ( +
建议:{it.suggestion}
+ ) : null} + {it?.affected_lines ? ( +
+ 影响范围:{it.affected_lines.start_line ?? '-'}–{it.affected_lines.end_line ?? '-'} +
+ ) : null} +
+
+ )} + /> + )} + {consistencyResult.summary ? ( +
{String(consistencyResult.summary)}
+ ) : null} +
+ ) : null} +
)} diff --git a/front/src/pages/aiStudio/chapter/prep/ChapterPrepLayout.tsx b/front/src/pages/aiStudio/chapter/prep/ChapterPrepLayout.tsx index 82df0de..66ceed3 100644 --- a/front/src/pages/aiStudio/chapter/prep/ChapterPrepLayout.tsx +++ b/front/src/pages/aiStudio/chapter/prep/ChapterPrepLayout.tsx @@ -9,7 +9,6 @@ import type { PrepFlowContext, StepKey } from './usePrepFlow' const { Header, Content } = Layout const STEP_ITEMS: { key: StepKey; title: string }[] = [ - { key: 'consistency', title: '角色混淆一致性检查' }, { key: 'divide', title: '分镜提取' }, { key: 'extract', title: '项目级信息提取' }, ] @@ -62,7 +61,7 @@ export default function ChapterPrepLayout() { }) }, [chapterId]) - const currentStepKey = getStepKeyFromPathname(location.pathname) ?? 'consistency' + const currentStepKey = getStepKeyFromPathname(location.pathname) ?? 'divide' const currentStepIndex = Math.max( 0, STEP_ITEMS.findIndex((x) => x.key === currentStepKey), @@ -132,14 +131,12 @@ export default function ChapterPrepLayout() { const nextStep = currentStepIndex < STEP_ITEMS.length - 1 ? STEP_ITEMS[currentStepIndex + 1] : null const isStepAllowed = (key: StepKey) => { - if (key === 'consistency') return true - if (key === 'divide') return !!workingScriptText.trim() && consistencyDone + if (key === 'divide') return !!workingScriptText.trim() if (key === 'extract') return !!scriptDivision && editableShots.length > 0 return false } const isCurrentStepCompleted = () => { - if (currentStepKey === 'consistency') return consistencyDone if (currentStepKey === 'divide') return !!scriptDivision && editableShots.length > 0 if (currentStepKey === 'extract') return !!extractionDraft return false @@ -225,7 +222,7 @@ export default function ChapterPrepLayout() { showIcon className="mb-3" message="当前章节没有原文内容" - description="请先点击右上角“编辑原文”粘贴/输入剧本,然后从“一致性检查”开始。" + description="请先点击右上角“编辑原文”粘贴/输入剧本,然后从“分镜提取”开始。" /> )} diff --git a/front/src/pages/aiStudio/chapter/prep/steps/DivideStep.tsx b/front/src/pages/aiStudio/chapter/prep/steps/DivideStep.tsx index e229856..e6ce112 100644 --- a/front/src/pages/aiStudio/chapter/prep/steps/DivideStep.tsx +++ b/front/src/pages/aiStudio/chapter/prep/steps/DivideStep.tsx @@ -159,7 +159,7 @@ export default function DivideStep() { return ( } loading={loading} onClick={() => void runDivide()}> 开始分镜提取 -