@@ -10,7 +10,7 @@ import { Progress } from '@/components/ui/progress'
import { Dialog , DialogContent , DialogHeader , DialogTitle } from '@/components/ui/dialog'
import { Navbar } from '@/components/Navbar'
import { AudioPlayer } from '@/components/AudioPlayer'
import { audiobookApi , type AudiobookProject , type AudiobookProjectDetail , type AudiobookCharacter , type AudiobookSegment } from '@/lib/api/audiobook'
import { audiobookApi , type AudiobookProject , type AudiobookProjectDetail , type AudiobookCharacter , type AudiobookSegment , type ScriptGenerationRequest } from '@/lib/api/audiobook'
import apiClient , { formatApiError , adminApi } from '@/lib/api'
import { useAuth } from '@/contexts/AuthContext'
@@ -352,11 +352,119 @@ function CreateProjectDialog({ open, onClose, onCreated }: { open: boolean; onCl
)
}
const GENRE_OPTIONS = [ '玄幻' , '武侠' , '仙侠' , '现代言情' , '都市' , '悬疑' , '科幻' , '历史' , '恐怖' ]
function AIScriptDialog ( { open , onClose , onCreated } : { open : boolean ; onClose : ( ) = > void ; onCreated : ( ) = > void } ) {
const [ title , setTitle ] = useState ( '' )
const [ genre , setGenre ] = useState ( '玄幻' )
const [ subgenre , setSubgenre ] = useState ( '' )
const [ premise , setPremise ] = useState ( '' )
const [ style , setStyle ] = useState ( '' )
const [ numCharacters , setNumCharacters ] = useState ( 5 )
const [ numChapters , setNumChapters ] = useState ( 8 )
const [ loading , setLoading ] = useState ( false )
const reset = ( ) = > {
setTitle ( '' ) ; setGenre ( '玄幻' ) ; setSubgenre ( '' ) ; setPremise ( '' ) ; setStyle ( '' )
setNumCharacters ( 5 ) ; setNumChapters ( 8 )
}
const handleCreate = async ( ) = > {
if ( ! title ) { toast . error ( '请输入作品标题' ) ; return }
if ( ! premise ) { toast . error ( '请输入故事简介' ) ; return }
setLoading ( true )
try {
await audiobookApi . createAIScript ( {
title ,
genre ,
subgenre ,
premise ,
style ,
num_characters : numCharacters ,
num_chapters : numChapters ,
} as ScriptGenerationRequest )
toast . success ( 'AI剧本生成任务已创建' )
reset ( )
onCreated ( )
onClose ( )
} catch ( e : any ) {
toast . error ( formatApiError ( e ) )
} finally {
setLoading ( false )
}
}
return (
< Dialog open = { open } onOpenChange = { v = > { if ( ! v ) { reset ( ) ; onClose ( ) } } } >
< DialogContent className = "sm:max-w-lg" >
< DialogHeader >
< DialogTitle > AI 生 成 剧 本 < / DialogTitle >
< / DialogHeader >
< div className = "space-y-3 pt-1" >
< Input placeholder = "作品标题" value = { title } onChange = { e = > setTitle ( e . target . value ) } / >
< div className = "flex gap-2" >
< select
className = "flex-1 h-9 rounded-md border border-input bg-background px-3 py-1 text-sm"
value = { genre }
onChange = { e = > setGenre ( e . target . value ) }
>
{ GENRE_OPTIONS . map ( g = > < option key = { g } value = { g } > { g } < / option > ) }
< / select >
< Input
className = "flex-1"
placeholder = "子类型(可选,如:升级流)"
value = { subgenre }
onChange = { e = > setSubgenre ( e . target . value ) }
/ >
< / div >
< Textarea
placeholder = "故事简介(描述世界观、主角、核心冲突等)"
rows = { 4 }
value = { premise }
onChange = { e = > setPremise ( e . target . value ) }
/ >
< Input placeholder = "写作风格(可选,如:热血、轻松幽默、黑暗沉郁)" value = { style } onChange = { e = > setStyle ( e . target . value ) } / >
< div className = "flex gap-3" >
< label className = "flex-1 flex flex-col gap-1 text-sm" >
< span className = "text-muted-foreground text-xs" > 角 色 数 量 ( 2 - 10 ) < / span >
< Input
type = "number"
min = { 2 }
max = { 10 }
value = { numCharacters }
onChange = { e = > setNumCharacters ( Math . min ( 10 , Math . max ( 2 , Number ( e . target . value ) ) ) ) }
/ >
< / label >
< label className = "flex-1 flex flex-col gap-1 text-sm" >
< span className = "text-muted-foreground text-xs" > 章 节 数 量 ( 2 - 30 ) < / span >
< Input
type = "number"
min = { 2 }
max = { 30 }
value = { numChapters }
onChange = { e = > setNumChapters ( Math . min ( 30 , Math . max ( 2 , Number ( e . target . value ) ) ) ) }
/ >
< / label >
< / div >
< div className = "flex justify-end gap-2 pt-1" >
< Button size = "sm" variant = "outline" onClick = { ( ) = > { reset ( ) ; onClose ( ) } } disabled = { loading } > 取 消 < / Button >
< Button size = "sm" onClick = { handleCreate } disabled = { loading } >
{ loading ? < Loader2 className = "h-3 w-3 animate-spin mr-1" / > : null }
{ loading ? '创建中...' : '生成剧本' }
< / Button >
< / div >
< / div >
< / DialogContent >
< / Dialog >
)
}
function ProjectListSidebar ( {
projects ,
selectedId ,
onSelect ,
onNew ,
onAIScript ,
onLLM ,
loading ,
collapsed ,
@@ -367,6 +475,7 @@ function ProjectListSidebar({
selectedId : number | null
onSelect : ( id : number ) = > void
onNew : ( ) = > void
onAIScript : ( ) = > void
onLLM : ( ) = > void
loading : boolean
collapsed : boolean
@@ -395,6 +504,9 @@ function ProjectListSidebar({
< Settings2 className = "h-4 w-4" / >
< / Button >
) }
< Button size = "icon" variant = "ghost" className = "h-7 w-7" onClick = { onAIScript } title = "AI 生成剧本" >
< Zap className = "h-4 w-4" / >
< / Button >
< Button size = "icon" variant = "ghost" className = "h-7 w-7" onClick = { onNew } title = { t ( 'newProject' ) } >
< Plus className = "h-4 w-4" / >
< / Button >
@@ -698,7 +810,11 @@ function CharactersPanel({
onClick = { onConfirm }
disabled = { loadingAction || editingCharId !== null }
>
{ loadingAction ? t ( 'projectCard.confirm.loading' ) : t ( 'projectCard.confirm.button' ) }
{ loadingAction
? t ( 'projectCard.confirm.loading' )
: project . source_type === 'ai_generated'
? t ( 'projectCard.confirm.generateScript' , '确认角色并生成剧本' )
: t ( 'projectCard.confirm.button' ) }
< / Button >
< / div >
) }
@@ -1100,6 +1216,7 @@ export default function Audiobook() {
const [ generatingChapterIndices , setGeneratingChapterIndices ] = useState < Set < number > > ( new Set ( ) )
const [ sequentialPlayingId , setSequentialPlayingId ] = useState < number | null > ( null )
const [ showCreate , setShowCreate ] = useState ( false )
const [ showAIScript , setShowAIScript ] = useState ( false )
const [ showLLM , setShowLLM ] = useState ( false )
const [ sidebarOpen , setSidebarOpen ] = useState ( true )
const [ charactersCollapsed , setCharactersCollapsed ] = useState ( false )
@@ -1465,8 +1582,9 @@ export default function Audiobook() {
setGeneratingChapterIndices ( new Set ( ) )
}
} }
onNew = { ( ) = > { setShowCreate ( v = > ! v ) ; setShowLLM ( false ) } }
onLLM = { ( ) = > { setShowLLM ( v = > ! v ) ; setShowCreate ( false ) } }
onNew = { ( ) = > { setShowCreate ( v = > ! v ) ; setShowLLM ( false ) ; setShowAIScript ( false ) } }
onAIScript = { ( ) = > { setShowAIScript ( v = > ! v ) ; setShowCreate ( false ) ; setShowLLM ( false ) } }
onLLM = { ( ) = > { setShowLLM ( v = > ! v ) ; setShowCreate ( false ) ; setShowAIScript ( false ) } }
loading = { loading }
collapsed = { ! sidebarOpen }
onToggle = { ( ) = > setSidebarOpen ( v = > ! v ) }
@@ -1477,6 +1595,7 @@ export default function Audiobook() {
< div className = "flex-1 flex flex-col overflow-hidden bg-background rounded-tl-2xl" >
< LLMConfigDialog open = { showLLM } onClose = { ( ) = > setShowLLM ( false ) } / >
< CreateProjectDialog open = { showCreate } onClose = { ( ) = > setShowCreate ( false ) } onCreated = { fetchProjects } / >
< AIScriptDialog open = { showAIScript } onClose = { ( ) = > setShowAIScript ( false ) } onCreated = { ( ) = > { fetchProjects ( ) ; setShowAIScript ( false ) } } / >
{ ! selectedProject ? (
< EmptyState / >
) : (