feat(audiobook): manage generating chapter indices in ProjectCard for improved chapter generation handling

This commit is contained in:
2026-03-10 20:27:27 +08:00
parent 2e005b0084
commit a517ce4ce7

View File

@@ -335,6 +335,7 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
const [isPolling, setIsPolling] = useState(false) const [isPolling, setIsPolling] = useState(false)
const [editingCharId, setEditingCharId] = useState<number | null>(null) const [editingCharId, setEditingCharId] = useState<number | null>(null)
const [editFields, setEditFields] = useState({ name: '', gender: '', description: '', instruct: '' }) const [editFields, setEditFields] = useState({ name: '', gender: '', description: '', instruct: '' })
const [generatingChapterIndices, setGeneratingChapterIndices] = useState<Set<number>>(new Set())
const [sequentialPlayingId, setSequentialPlayingId] = useState<number | null>(null) const [sequentialPlayingId, setSequentialPlayingId] = useState<number | null>(null)
const [charsCollapsed, setCharsCollapsed] = useState(false) const [charsCollapsed, setCharsCollapsed] = useState(false)
const [chaptersCollapsed, setChaptersCollapsed] = useState(false) const [chaptersCollapsed, setChaptersCollapsed] = useState(false)
@@ -382,11 +383,29 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
}, [isPolling, project.status, segments, hasParsingChapter]) }, [isPolling, project.status, segments, hasParsingChapter])
useEffect(() => { useEffect(() => {
const shouldPoll = isPolling || ['analyzing', 'generating'].includes(project.status) || hasParsingChapter if (generatingChapterIndices.size === 0) return
const done: number[] = []
generatingChapterIndices.forEach(chIdx => {
const chSegs = segments.filter(s => s.chapter_index === chIdx)
if (chSegs.length > 0 && chSegs.every(s => s.status === 'done' || s.status === 'error')) {
done.push(chIdx)
}
})
if (done.length > 0) {
setGeneratingChapterIndices(prev => {
const n = new Set(prev)
done.forEach(i => n.delete(i))
return n
})
}
}, [segments, generatingChapterIndices])
useEffect(() => {
const shouldPoll = isPolling || ['analyzing', 'generating'].includes(project.status) || hasParsingChapter || generatingChapterIndices.size > 0
if (!shouldPoll) return if (!shouldPoll) return
const id = setInterval(() => { onRefresh(); fetchSegments(); fetchDetail() }, 1500) const id = setInterval(() => { onRefresh(); fetchSegments(); fetchDetail() }, 1500)
return () => clearInterval(id) return () => clearInterval(id)
}, [isPolling, project.status, hasParsingChapter, onRefresh, fetchSegments, fetchDetail]) }, [isPolling, project.status, hasParsingChapter, generatingChapterIndices, onRefresh, fetchSegments, fetchDetail])
useEffect(() => { useEffect(() => {
if (!detail || segments.length === 0) return if (!detail || segments.length === 0) return
@@ -448,14 +467,22 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
const handleGenerate = async (chapterIndex?: number) => { const handleGenerate = async (chapterIndex?: number) => {
setLoadingAction(true) setLoadingAction(true)
setIsPolling(true) if (chapterIndex !== undefined) {
setGeneratingChapterIndices(prev => new Set([...prev, chapterIndex]))
} else {
setIsPolling(true)
}
try { try {
await audiobookApi.generate(project.id, chapterIndex) await audiobookApi.generate(project.id, chapterIndex)
toast.success(chapterIndex !== undefined ? `${chapterIndex + 1} 章生成已开始` : '全书生成已开始') toast.success(chapterIndex !== undefined ? `${chapterIndex + 1} 章生成已开始` : '全书生成已开始')
onRefresh() onRefresh()
fetchSegments() fetchSegments()
} catch (e: any) { } catch (e: any) {
setIsPolling(false) if (chapterIndex !== undefined) {
setGeneratingChapterIndices(prev => { const n = new Set(prev); n.delete(chapterIndex); return n })
} else {
setIsPolling(false)
}
toast.error(formatApiError(e)) toast.error(formatApiError(e))
} finally { } finally {
setLoadingAction(false) setLoadingAction(false)
@@ -465,10 +492,13 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
const handleProcessAll = async () => { const handleProcessAll = async () => {
if (!detail) return if (!detail) return
setLoadingAction(true) setLoadingAction(true)
const ready = detail.chapters.filter(c => c.status === 'ready')
if (ready.length > 0) {
setGeneratingChapterIndices(prev => new Set([...prev, ...ready.map(c => c.chapter_index)]))
}
setIsPolling(true) setIsPolling(true)
try { try {
const pending = detail.chapters.filter(c => c.status === 'pending' || c.status === 'error') const pending = detail.chapters.filter(c => c.status === 'pending' || c.status === 'error')
const ready = detail.chapters.filter(c => c.status === 'ready')
await Promise.all([ await Promise.all([
...pending.map(c => audiobookApi.parseChapter(project.id, c.id)), ...pending.map(c => audiobookApi.parseChapter(project.id, c.id)),
...ready.map(c => audiobookApi.generate(project.id, c.chapter_index)), ...ready.map(c => audiobookApi.generate(project.id, c.chapter_index)),
@@ -759,7 +789,7 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
<span></span> <span></span>
</div> </div>
)} )}
{ch.status === 'ready' && !chGenerating && !chAllDone && ( {ch.status === 'ready' && !chGenerating && !chAllDone && !generatingChapterIndices.has(ch.chapter_index) && (
<Button size="sm" variant="outline" className="h-6 text-xs px-2" disabled={loadingAction} onClick={() => { <Button size="sm" variant="outline" className="h-6 text-xs px-2" disabled={loadingAction} onClick={() => {
setExpandedChapters(prev => { const n = new Set(prev); n.add(ch.id); return n }) setExpandedChapters(prev => { const n = new Set(prev); n.add(ch.id); return n })
handleGenerate(ch.chapter_index) handleGenerate(ch.chapter_index)
@@ -767,7 +797,7 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
</Button> </Button>
)} )}
{ch.status === 'ready' && chGenerating && ( {ch.status === 'ready' && (chGenerating || generatingChapterIndices.has(ch.chapter_index)) && (
<div className="flex items-center gap-1 text-xs text-muted-foreground"> <div className="flex items-center gap-1 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" /> <Loader2 className="h-3 w-3 animate-spin" />
<span>{chDone}/{chTotal} </span> <span>{chDone}/{chTotal} </span>