feat: Enhance narrator description and instructions in LLMService and Audiobook components

This commit is contained in:
2026-03-12 15:42:53 +08:00
parent 475df0c9ca
commit a1ee476e0f
3 changed files with 89 additions and 55 deletions

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Book, Plus, Trash2, RefreshCw, Download, ChevronDown, ChevronUp, Play, Square, Pencil, Check, X, Loader2, Zap, Settings2 } from 'lucide-react'
import { Book, Plus, Trash2, RefreshCw, Download, ChevronDown, ChevronUp, Play, Square, Pencil, Check, X, Loader2, Zap, Settings2, PanelLeftClose, PanelLeftOpen } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
@@ -334,6 +334,8 @@ function ProjectListSidebar({
onNew,
onLLM,
loading,
collapsed,
onToggle,
}: {
projects: AudiobookProject[]
selectedId: number | null
@@ -341,41 +343,55 @@ function ProjectListSidebar({
onNew: () => void
onLLM: () => void
loading: boolean
collapsed: boolean
onToggle: () => void
}) {
const { t } = useTranslation('audiobook')
return (
<div className="w-60 shrink-0 flex flex-col border-r overflow-hidden">
<div className="flex items-center justify-between px-3 py-2 border-b shrink-0">
<span className="text-sm font-semibold">{t('title')}</span>
<div className="flex items-center gap-0.5">
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={onLLM} title={t('llmConfig')}>
<Settings2 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>
</div>
</div>
<div className="flex-1 overflow-y-auto">
{loading ? (
<div className="px-3 py-4 text-xs text-muted-foreground">{t('loading')}</div>
) : projects.length === 0 ? (
<div className="px-3 py-4 text-xs text-muted-foreground">{t('noProjects')}</div>
) : (
projects.map(p => (
<button
key={p.id}
onClick={() => onSelect(p.id)}
className={`w-full text-left px-3 py-2 flex flex-col gap-0.5 hover:bg-muted/50 transition-colors border-b border-border/40 ${selectedId === p.id ? 'bg-muted' : ''}`}
>
<span className="text-sm font-medium truncate">{p.title}</span>
<Badge variant={(STATUS_COLORS[p.status] || 'secondary') as any} className="text-[10px] h-4 px-1 self-start">
{t(`status.${p.status}`, { defaultValue: p.status })}
</Badge>
</button>
))
<div className={`${collapsed ? 'w-10' : 'w-60'} shrink-0 flex flex-col bg-muted/30 overflow-hidden transition-all duration-200`}>
<div className="h-16 flex items-center shrink-0 px-2 gap-1">
{!collapsed && (
<div className="flex items-center gap-2 flex-1 min-w-0 ml-1">
<img src="/qwen.svg" alt="Qwen" className="h-5 w-5 shrink-0" />
<span className="text-sm font-semibold truncate">{t('title')}</span>
</div>
)}
<Button size="icon" variant="ghost" className="h-8 w-8 shrink-0" onClick={onToggle}>
{collapsed ? <PanelLeftOpen className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
</Button>
</div>
{!collapsed && (
<>
<div className="flex items-center justify-end px-2 pb-1 gap-0.5 shrink-0">
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={onLLM} title={t('llmConfig')}>
<Settings2 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>
</div>
<div className="flex-1 overflow-y-auto">
{loading ? (
<div className="px-3 py-4 text-xs text-muted-foreground">{t('loading')}</div>
) : projects.length === 0 ? (
<div className="px-3 py-4 text-xs text-muted-foreground">{t('noProjects')}</div>
) : (
projects.map(p => (
<button
key={p.id}
onClick={() => onSelect(p.id)}
className={`w-full text-left px-3 py-2 flex flex-col gap-0.5 hover:bg-muted/50 transition-colors border-b border-border/40 ${selectedId === p.id ? 'bg-muted' : ''}`}
>
<span className="text-sm font-medium truncate">{p.title}</span>
<Badge variant={(STATUS_COLORS[p.status] || 'secondary') as any} className="text-[10px] h-4 px-1 self-start">
{t(`status.${p.status}`, { defaultValue: p.status })}
</Badge>
</button>
))
)}
</div>
</>
)}
</div>
)
}
@@ -465,8 +481,8 @@ function CharactersPanel({
const charCount = detail?.characters.length ?? 0
return (
<div className="w-72 shrink-0 flex flex-col border-r overflow-hidden">
<div className="flex items-center justify-between px-3 py-2 border-b shrink-0">
<div className="w-72 shrink-0 flex flex-col border-r border-blue-500/20 bg-blue-500/5 overflow-hidden">
<div className="flex items-center justify-between px-3 py-2 border-b border-blue-500/20 shrink-0">
<span className="text-xs font-medium text-blue-400/80">
{t('projectCard.characters.title', { count: charCount })}
</span>
@@ -659,8 +675,8 @@ function ChaptersPanel({
const hasChapters = detail && detail.chapters.length > 0 && ['ready', 'generating', 'done'].includes(status)
return (
<div className="flex-1 flex flex-col overflow-hidden">
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between px-3 py-2 border-b shrink-0">
<div className="flex-1 flex flex-col bg-emerald-500/5 overflow-hidden">
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between px-3 py-2 border-b border-emerald-500/20 shrink-0">
<span className="text-xs font-medium text-emerald-400/80">
{t('projectCard.chapters.title', { count: detail?.chapters.length ?? 0 })}
</span>
@@ -818,6 +834,7 @@ export default function Audiobook() {
const [sequentialPlayingId, setSequentialPlayingId] = useState<number | null>(null)
const [showCreate, setShowCreate] = useState(false)
const [showLLM, setShowLLM] = useState(false)
const [sidebarOpen, setSidebarOpen] = useState(true)
const prevStatusRef = useRef<string>('')
const autoExpandedRef = useRef(new Set<string>())
@@ -1145,24 +1162,26 @@ export default function Audiobook() {
const isTurboMode = ['analyzing', 'parsing', 'processing'].includes(displayStatus)
return (
<div className="h-screen flex flex-col overflow-hidden">
<Navbar />
<div className="flex-1 flex overflow-hidden">
<ProjectListSidebar
projects={projects}
selectedId={selectedProjectId}
onSelect={(id) => {
if (id !== selectedProjectId) {
setSelectedProjectId(id)
setIsPolling(false)
setGeneratingChapterIndices(new Set())
}
}}
onNew={() => { setShowCreate(v => !v); setShowLLM(false) }}
onLLM={() => { setShowLLM(v => !v); setShowCreate(false) }}
loading={loading}
/>
<div className="flex-1 flex flex-col overflow-hidden">
<div className="h-screen overflow-hidden flex bg-background">
<ProjectListSidebar
projects={projects}
selectedId={selectedProjectId}
onSelect={(id) => {
if (id !== selectedProjectId) {
setSelectedProjectId(id)
setIsPolling(false)
setGeneratingChapterIndices(new Set())
}
}}
onNew={() => { setShowCreate(v => !v); setShowLLM(false) }}
onLLM={() => { setShowLLM(v => !v); setShowCreate(false) }}
loading={loading}
collapsed={!sidebarOpen}
onToggle={() => setSidebarOpen(v => !v)}
/>
<div className="flex-1 flex flex-col overflow-hidden bg-muted/30">
<Navbar />
<div className="flex-1 flex flex-col overflow-hidden bg-background rounded-tl-2xl">
{showLLM && (
<div className="shrink-0 border-b px-4 py-3">
<LLMConfigPanel onSaved={() => setShowLLM(false)} />