Refactor chapter preparation steps and enhance script processing features

This commit is contained in:
extreme committed 2026-03-27 16:27:28 +08:00
1 parent 8ff475175e
commit de4c7c3baa
5 files changed
+236 -41

No files matched your search

+2 -3
View File
@@ -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 = () => {
<Route path="projects/:projectId" element={<ProjectWorkbench />} />
<Route path="projects/:projectId/roles/:characterId/edit" element={<RoleDetailPage />} />
<Route path="projects/:projectId/chapters/:chapterId/prep" element={<ChapterPrepLayout />}>
<Route index element={<Navigate to="consistency" replace />} />
<Route path="consistency" element={<ConsistencyStep />} />
<Route index element={<Navigate to="divide" replace />} />
<Route path="consistency" element={<Navigate to="../divide" replace />} />
<Route path="divide" element={<DivideStep />} />
<Route path="extract" element={<ExtractProjectStep />} />
</Route>
@@ -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<EditorMode>('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<ScriptConsistencyCheckResult | null>(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<boolean> => {
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({
<Tag color="default">{paragraphCount} 段</Tag>
</div>
<Space size="small">
<Button size="small" type="primary" icon={<SaveOutlined />} loading={saving} onClick={() => void handleSave()}>
<Button size="small" type="primary" icon={<SaveOutlined />} loading={actionsLoading || saving} disabled={actionsLoading} onClick={() => void handleSave()}>
保存
</Button>
<Button
size="small"
icon={<ReloadOutlined />}
loading={actionsLoading}
disabled={actionsLoading}
onClick={() => void handleCheckConsistency()}
>
角色混淆检查
</Button>
<Button
size="small"
icon={<ThunderboltOutlined />}
loading={extracting}
loading={actionsLoading}
disabled={actionsLoading}
onClick={() => void handleSmartSimplify()}
>
智能精简
</Button>
{mode === 'condensed' ? (
<Button size="small" icon={<ReloadOutlined />} onClick={handleBackToRaw}>
<Button size="small" icon={<ReloadOutlined />} loading={actionsLoading} disabled={actionsLoading} onClick={handleBackToRaw}>
回到原文
</Button>
) : (
<Button
size="small"
icon={<ReloadOutlined />}
disabled={!condensedText.trim()}
loading={actionsLoading}
disabled={actionsLoading || !condensedText.trim()}
onClick={handleViewCondensed}
>
查看精简
@@ -204,6 +333,8 @@ export function ChapterRawTextEditorModal({
<Button
size="small"
icon={<DiffOutlined />}
loading={actionsLoading}
disabled={actionsLoading}
type={mode === 'compare' ? 'primary' : 'default'}
onClick={() => {
if (mode === 'compare') {
@@ -218,21 +349,26 @@ export function ChapterRawTextEditorModal({
>
对比模式
</Button>
<Button size="small" icon={<HistoryOutlined />} onClick={() => setHistoryOpen(true)}>
<Button size="small" icon={<HistoryOutlined />} loading={actionsLoading} disabled={actionsLoading} onClick={() => setHistoryOpen(true)}>
版本历史
</Button>
</Space>
</div>
}
open={open}
onCancel={onClose}
onCancel={() => {
handleRequestClose()
}}
width={900}
footer={
<Button type="primary" onClick={onClose}>
<Button type="primary" loading={actionsLoading} disabled={actionsLoading} onClick={handleRequestClose}>
关闭
</Button>
}
styles={{ body: { maxHeight: '70vh', overflow: 'auto', paddingTop: 12 } }}
styles={{
header: { paddingRight: 48 },
body: { maxHeight: '70vh', overflow: 'auto', paddingTop: 12 },
}}
>
{loading ? (
<div className="flex justify-center items-center py-10">
@@ -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' }}
/>
</div>
@@ -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' }}
/>
</div>
</div>
) : (
<Input.TextArea
value={editorText}
onChange={(e) => {
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' }}
/>
<div className="space-y-3">
<Input.TextArea
value={editorText}
onChange={(e) => {
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 ? (
<Card
size="small"
title="角色混淆检查结果"
extra={
<Space wrap>
<Tag color={consistencyResult.has_issues ? 'red' : 'green'}>
{consistencyResult.has_issues ? '发现问题' : '无问题'}
</Tag>
<Tag>issues:{consistencyIssues.length}</Tag>
<Button
size="small"
type="primary"
icon={<ThunderboltOutlined />}
loading={actionsLoading}
disabled={actionsLoading}
onClick={() => void handleOneClickOptimize()}
>
一键优化
</Button>
</Space>
}
>
{consistencyIssues.length === 0 ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="未发现角色混淆问题" />
) : (
<List
size="small"
dataSource={consistencyIssues}
renderItem={(it: any, idx) => (
<List.Item>
<div className="min-w-0">
<div className="font-medium">
{it?.issue_type ? `[${it.issue_type}] ` : ''}
Issue {idx + 1}
</div>
<div className="text-sm">{it?.description}</div>
{it?.character_candidates?.length ? (
<div className="text-xs text-gray-500 mt-1">候选角色:{it.character_candidates.join('、')}</div>
) : null}
{it?.suggestion ? (
<div className="text-xs text-gray-500 mt-1">建议:{it.suggestion}</div>
) : null}
{it?.affected_lines ? (
<div className="text-xs text-gray-400 mt-1">
影响范围:{it.affected_lines.start_line ?? '-'}–{it.affected_lines.end_line ?? '-'}
</div>
) : null}
</div>
</List.Item>
)}
/>
)}
{consistencyResult.summary ? (
<div className="text-xs text-gray-500 mt-2">{String(consistencyResult.summary)}</div>
) : null}
</Card>
) : null}
</div>
)}
</Modal>
@@ -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="请先点击右上角“编辑原文”粘贴/输入剧本,然后从“分镜提取”开始。"
/>
)}
@@ -159,7 +159,7 @@ export default function DivideStep() {
return (
<Card
title="Step 2 / 3:分镜提取(可编辑)"
title="Step 1 / 2:分镜提取(可编辑)"
style={{ flex: 1, minHeight: 0, overflow: 'auto' }}
bodyStyle={{ padding: 12 }}
extra={
@@ -167,7 +167,7 @@ export default function DivideStep() {
<Button type="primary" icon={<ThunderboltOutlined />} loading={loading} onClick={() => void runDivide()}>
开始分镜提取
</Button>
<Button icon={<PlusOutlined />} disabled={loading} onClick={addRow}>
<Button icon={<PlusOutlined />} disabled={loading || shots.length === 0} onClick={addRow}>
新增镜头
</Button>
<Button danger icon={<DeleteOutlined />} disabled={loading} onClick={deleteSelected}>
@@ -121,7 +121,7 @@ export default function ExtractProjectStep() {
return (
<Card
title="Step 3 / 3:项目级信息提取(最终输出)"
title="Step 2 / 2:项目级信息提取(最终输出)"
style={{ flex: 1, minHeight: 0, overflow: 'hidden' }}
bodyStyle={{ padding: 12, height: '100%', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}
extra={