feat: add continue script functionality for AI-generated audiobook projects

This commit is contained in:
2026-03-13 11:59:37 +08:00
parent 7129047c3f
commit 7644584c39
11 changed files with 355 additions and 7 deletions

View File

@@ -699,6 +699,47 @@ function AIScriptDialog({ open, onClose, onCreated }: { open: boolean; onClose:
)
}
function ContinueScriptDialog({ open, onClose, onConfirm }: { open: boolean; onClose: () => void; onConfirm: (n: number) => Promise<void> }) {
const { t } = useTranslation('audiobook')
const [count, setCount] = useState(4)
const [loading, setLoading] = useState(false)
const handleSubmit = async () => {
setLoading(true)
try {
await onConfirm(count)
} finally {
setLoading(false)
}
}
return (
<Dialog open={open} onOpenChange={v => { if (!v) onClose() }}>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>{t('projectCard.continueScriptDialog.title')}</DialogTitle>
</DialogHeader>
<div className="space-y-3 py-2">
<label className="flex flex-col gap-1 text-sm">
<span className="text-muted-foreground text-xs">{t('projectCard.continueScriptDialog.label')}</span>
<Input
type="number" min={1} max={20} value={count}
onChange={e => setCount(Math.min(20, Math.max(1, Number(e.target.value))))}
/>
</label>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button size="sm" variant="ghost" onClick={onClose} disabled={loading}>{t('projectCard.segments.cancel')}</Button>
<Button size="sm" onClick={handleSubmit} disabled={loading}>
{loading ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : null}
{loading ? t('projectCard.continueScriptDialog.starting') : t('projectCard.continueScriptDialog.start')}
</Button>
</div>
</DialogContent>
</Dialog>
)
}
function ProjectListSidebar({
projects,
selectedId,
@@ -1079,6 +1120,7 @@ function ChaptersPanel({
onParseAll,
onGenerateAll,
onProcessAll,
onContinueScript,
onDownload,
onSequentialPlayingChange,
onUpdateSegment,
@@ -1097,6 +1139,7 @@ function ChaptersPanel({
onParseAll: () => void
onGenerateAll: () => void
onProcessAll: () => void
onContinueScript?: () => void
onDownload: (chapterIndex?: number) => void
onSequentialPlayingChange: (id: number | null) => void
onUpdateSegment: (segmentId: number, data: { text: string; emo_text?: string | null; emo_alpha?: number | null }) => Promise<void>
@@ -1227,6 +1270,12 @@ function ChaptersPanel({
{loadingAction ? <Loader2 className="h-3 w-3 animate-spin" /> : t('projectCard.chapters.processAll')}
</Button>
)}
{isAIMode && onContinueScript && (
<Button size="xs" variant="outline" disabled={loadingAction} onClick={onContinueScript}>
<Plus className="h-3 w-3 mr-1" />
{t('projectCard.chapters.continueScript')}
</Button>
)}
</div>
)}
</div>
@@ -1464,6 +1513,7 @@ export default function Audiobook() {
const [sequentialPlayingId, setSequentialPlayingId] = useState<number | null>(null)
const [showCreate, setShowCreate] = useState(false)
const [showAIScript, setShowAIScript] = useState(false)
const [showContinueScript, setShowContinueScript] = useState(false)
const [showLLM, setShowLLM] = useState(false)
const [sidebarOpen, setSidebarOpen] = useState(true)
const [charactersCollapsed, setCharactersCollapsed] = useState(false)
@@ -1718,6 +1768,24 @@ export default function Audiobook() {
}
}
const handleContinueScript = async (additionalChapters: number) => {
if (!selectedProject) return
setLoadingAction(true)
setIsPolling(true)
try {
await audiobookApi.continueScript(selectedProject.id, additionalChapters)
toast.success(t('projectCard.chapters.continueScriptStarted'))
setShowContinueScript(false)
fetchProjects()
fetchDetail()
} catch (e: any) {
setIsPolling(false)
toast.error(formatApiError(e))
} finally {
setLoadingAction(false)
}
}
const handleCancelBatch = async () => {
if (!selectedProject) return
try {
@@ -1843,6 +1911,7 @@ export default function Audiobook() {
<LLMConfigDialog open={showLLM} onClose={() => setShowLLM(false)} />
<CreateProjectDialog open={showCreate} onClose={() => setShowCreate(false)} onCreated={fetchProjects} />
<AIScriptDialog open={showAIScript} onClose={() => setShowAIScript(false)} onCreated={() => { fetchProjects(); setShowAIScript(false) }} />
<ContinueScriptDialog open={showContinueScript} onClose={() => setShowContinueScript(false)} onConfirm={handleContinueScript} />
{!selectedProject ? (
<EmptyState />
) : (
@@ -1889,9 +1958,9 @@ export default function Audiobook() {
</div>
)}
{status === 'analyzing' && (
{(status === 'analyzing' || (status === 'generating' && selectedProject.source_type === 'ai_generated')) && (
<div className="shrink-0 mx-4 mt-2">
<LogStream projectId={selectedProject.id} active={status === 'analyzing'} />
<LogStream projectId={selectedProject.id} active={true} />
</div>
)}
@@ -1981,6 +2050,7 @@ export default function Audiobook() {
onParseAll={handleParseAll}
onGenerateAll={handleGenerateAll}
onProcessAll={handleProcessAll}
onContinueScript={selectedProject.source_type === 'ai_generated' ? () => setShowContinueScript(true) : undefined}
onDownload={handleDownload}
onSequentialPlayingChange={setSequentialPlayingId}
onUpdateSegment={handleUpdateSegment}