Refactor chapter preparation steps and enhance script processing features
This commit is contained in:
1 parent
8ff475175e
commit
de4c7c3baa
5 files changed
+236
-41
No files matched your search
+2
-3
@@ -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={
|
||||
|
||||
Reference in new issue
Block a user