Add IconLabel component and integrate it into forms for improved accessibility and UI consistency

This commit is contained in:
2026-01-26 17:11:03 +08:00
parent 23e72f80e5
commit 8f7b6ec773
6 changed files with 506 additions and 178 deletions

View File

@@ -0,0 +1,26 @@
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import type { ElementType } from 'react'
interface IconLabelProps {
icon: ElementType
tooltip: string
required?: boolean
}
export function IconLabel({ icon: Icon, tooltip, required = false }: IconLabelProps) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1 cursor-help">
<Icon className="h-4 w-4 text-muted-foreground" />
{required && <span className="text-destructive">*</span>}
</div>
</TooltipTrigger>
<TooltipContent>
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}

View File

@@ -21,7 +21,7 @@ const PresetSelectorInner = <T extends Preset>({ presets, onSelect }: PresetSele
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => onSelect(preset)} onClick={() => onSelect(preset)}
className="text-xs md:text-sm px-2 h-6 md:h-7" className="text-xs md:text-sm px-2 py-0.5 h-5"
> >
{preset.label} {preset.label}
</Button> </Button>
@@ -34,19 +34,19 @@ const PresetSelectorInner = <T extends Preset>({ presets, onSelect }: PresetSele
} }
return ( return (
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-1.5 mt-1">
<div className="flex flex-wrap gap-1 flex-1"> <div className="flex flex-wrap gap-1 flex-1">
{presetButtons} {presetButtons}
</div> </div>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="icon"
onClick={handleRandomSelect} onClick={handleRandomSelect}
className="h-6 md:h-7 px-2 flex-shrink-0" className="h-6 w-6 flex-shrink-0"
title="随机选择" title="随机选择"
> >
<Shuffle className="h-3.5 w-3.5" /> <Shuffle className="h-3 w-3" />
</Button> </Button>
</div> </div>
) )

View File

@@ -6,10 +6,12 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from '@/components/ui/dialog'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { ChevronDown } from 'lucide-react' import { Globe2, User, Type, Sparkles, Play, Settings } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { IconLabel } from '@/components/IconLabel'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { ttsApi, jobApi } from '@/lib/api' import { ttsApi, jobApi } from '@/lib/api'
import { useJobPolling } from '@/hooks/useJobPolling' import { useJobPolling } from '@/hooks/useJobPolling'
import { LoadingState } from '@/components/LoadingState' import { LoadingState } from '@/components/LoadingState'
@@ -42,6 +44,13 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
const [speakers, setSpeakers] = useState<Speaker[]>([]) const [speakers, setSpeakers] = useState<Speaker[]>([])
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [advancedOpen, setAdvancedOpen] = useState(false) const [advancedOpen, setAdvancedOpen] = useState(false)
const [tempAdvancedParams, setTempAdvancedParams] = useState({
max_new_tokens: 2048,
temperature: 0.3,
top_k: 20,
top_p: 0.7,
repetition_penalty: 1.05
})
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling() const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
@@ -116,9 +125,9 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
}, [currentJob?.id, currentJob?.audio_url]) }, [currentJob?.id, currentJob?.audio_url])
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-3"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-2">
<div className="space-y-1"> <div className="space-y-0.5">
<Label htmlFor="language"></Label> <IconLabel icon={Globe2} tooltip="语言" required />
<Select <Select
value={watch('language')} value={watch('language')}
onValueChange={(value: string) => setValue('language', value)} onValueChange={(value: string) => setValue('language', value)}
@@ -139,8 +148,8 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
)} )}
</div> </div>
<div className="space-y-1"> <div className="space-y-0.5">
<Label htmlFor="speaker"></Label> <IconLabel icon={User} tooltip="发音人" required />
<Select <Select
value={watch('speaker')} value={watch('speaker')}
onValueChange={(value: string) => setValue('speaker', value)} onValueChange={(value: string) => setValue('speaker', value)}
@@ -161,8 +170,8 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
)} )}
</div> </div>
<div className="space-y-1"> <div className="space-y-0.5">
<Label htmlFor="text"></Label> <IconLabel icon={Type} tooltip="合成文本" required />
<Textarea <Textarea
{...register('text')} {...register('text')}
placeholder="输入要合成的文本..." placeholder="输入要合成的文本..."
@@ -173,8 +182,8 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
)} )}
</div> </div>
<div className="space-y-1"> <div className="space-y-0.5">
<Label htmlFor="instruct"></Label> <IconLabel icon={Sparkles} tooltip="情绪指导(可选)" />
<Textarea <Textarea
{...register('instruct')} {...register('instruct')}
placeholder="例如:温柔体贴,语速平缓,充满关怀" placeholder="例如:温柔体贴,语速平缓,充满关怀"
@@ -194,68 +203,175 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
)} )}
</div> </div>
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}> <Dialog open={advancedOpen} onOpenChange={(open) => {
<CollapsibleTrigger asChild> if (open) {
<Button type="button" variant="ghost" className="w-full py-1.5"> setTempAdvancedParams({
max_new_tokens: watch('max_new_tokens'),
temperature: watch('temperature'),
top_k: watch('top_k'),
top_p: watch('top_p'),
repetition_penalty: watch('repetition_penalty')
})
}
setAdvancedOpen(open)
}}>
<DialogTrigger asChild>
<Button type="button" variant="outline" className="w-full">
<Settings className="mr-2 h-4 w-4" />
<ChevronDown className="ml-2 h-4 w-4" />
</Button> </Button>
</CollapsibleTrigger> </DialogTrigger>
<CollapsibleContent className="space-y-2 pt-2"> <DialogContent className="sm:max-w-[500px]">
<ParamInput <DialogHeader>
name="max_new_tokens" <DialogTitle></DialogTitle>
label={ADVANCED_PARAMS_INFO.max_new_tokens.label} </DialogHeader>
description={ADVANCED_PARAMS_INFO.max_new_tokens.description} <div className="space-y-4 py-4">
tooltip={ADVANCED_PARAMS_INFO.max_new_tokens.tooltip} <div className="space-y-2">
register={register} <Label htmlFor="dialog-max_new_tokens">
{ADVANCED_PARAMS_INFO.max_new_tokens.label}
</Label>
<Input
id="dialog-max_new_tokens"
type="number"
min={1} min={1}
max={10000} max={10000}
value={tempAdvancedParams.max_new_tokens}
onChange={(e) => setTempAdvancedParams({
...tempAdvancedParams,
max_new_tokens: parseInt(e.target.value) || 2048
})}
/> />
<ParamInput <p className="text-sm text-muted-foreground">
name="temperature" {ADVANCED_PARAMS_INFO.max_new_tokens.description}
label={ADVANCED_PARAMS_INFO.temperature.label} </p>
description={ADVANCED_PARAMS_INFO.temperature.description} </div>
tooltip={ADVANCED_PARAMS_INFO.temperature.tooltip} <div className="space-y-2">
register={register} <Label htmlFor="dialog-temperature">
step={0.1} {ADVANCED_PARAMS_INFO.temperature.label}
</Label>
<Input
id="dialog-temperature"
type="number"
min={0} min={0}
max={2} max={2}
step={0.1}
value={tempAdvancedParams.temperature}
onChange={(e) => setTempAdvancedParams({
...tempAdvancedParams,
temperature: parseFloat(e.target.value) || 0.3
})}
/> />
<ParamInput <p className="text-sm text-muted-foreground">
name="top_k" {ADVANCED_PARAMS_INFO.temperature.description}
label={ADVANCED_PARAMS_INFO.top_k.label} </p>
description={ADVANCED_PARAMS_INFO.top_k.description} </div>
tooltip={ADVANCED_PARAMS_INFO.top_k.tooltip} <div className="space-y-2">
register={register} <Label htmlFor="dialog-top_k">
{ADVANCED_PARAMS_INFO.top_k.label}
</Label>
<Input
id="dialog-top_k"
type="number"
min={1} min={1}
max={100} max={100}
value={tempAdvancedParams.top_k}
onChange={(e) => setTempAdvancedParams({
...tempAdvancedParams,
top_k: parseInt(e.target.value) || 20
})}
/> />
<ParamInput <p className="text-sm text-muted-foreground">
name="top_p" {ADVANCED_PARAMS_INFO.top_k.description}
label={ADVANCED_PARAMS_INFO.top_p.label} </p>
description={ADVANCED_PARAMS_INFO.top_p.description} </div>
tooltip={ADVANCED_PARAMS_INFO.top_p.tooltip} <div className="space-y-2">
register={register} <Label htmlFor="dialog-top_p">
step={0.1} {ADVANCED_PARAMS_INFO.top_p.label}
</Label>
<Input
id="dialog-top_p"
type="number"
min={0} min={0}
max={1} max={1}
step={0.1}
value={tempAdvancedParams.top_p}
onChange={(e) => setTempAdvancedParams({
...tempAdvancedParams,
top_p: parseFloat(e.target.value) || 0.7
})}
/> />
<ParamInput <p className="text-sm text-muted-foreground">
name="repetition_penalty" {ADVANCED_PARAMS_INFO.top_p.description}
label={ADVANCED_PARAMS_INFO.repetition_penalty.label} </p>
description={ADVANCED_PARAMS_INFO.repetition_penalty.description} </div>
tooltip={ADVANCED_PARAMS_INFO.repetition_penalty.tooltip} <div className="space-y-2">
register={register} <Label htmlFor="dialog-repetition_penalty">
step={0.01} {ADVANCED_PARAMS_INFO.repetition_penalty.label}
</Label>
<Input
id="dialog-repetition_penalty"
type="number"
min={0} min={0}
max={2} max={2}
step={0.01}
value={tempAdvancedParams.repetition_penalty}
onChange={(e) => setTempAdvancedParams({
...tempAdvancedParams,
repetition_penalty: parseFloat(e.target.value) || 1.05
})}
/> />
</CollapsibleContent> <p className="text-sm text-muted-foreground">
</Collapsible> {ADVANCED_PARAMS_INFO.repetition_penalty.description}
</p>
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => {
setTempAdvancedParams({
max_new_tokens: watch('max_new_tokens'),
temperature: watch('temperature'),
top_k: watch('top_k'),
top_p: watch('top_p'),
repetition_penalty: watch('repetition_penalty')
})
setAdvancedOpen(false)
}}
>
</Button>
<Button
type="button"
onClick={() => {
setValue('max_new_tokens', tempAdvancedParams.max_new_tokens)
setValue('temperature', tempAdvancedParams.temperature)
setValue('top_k', tempAdvancedParams.top_k)
setValue('top_p', tempAdvancedParams.top_p)
setValue('repetition_penalty', tempAdvancedParams.repetition_penalty)
setAdvancedOpen(false)
}}
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button type="submit" className="w-full" disabled={isLoading || isPolling}> <Button type="submit" className="w-full" disabled={isLoading || isPolling}>
<Play className="mr-2 h-4 w-4" />
{isLoading ? '创建中...' : '生成语音'} {isLoading ? '创建中...' : '生成语音'}
</Button> </Button>
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{isPolling && <LoadingState elapsedTime={elapsedTime} />} {isPolling && <LoadingState elapsedTime={elapsedTime} />}

View File

@@ -6,11 +6,13 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from '@/components/ui/dialog'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { ChevronDown } from 'lucide-react' import { Settings, Globe2, Type, Play, FileText, Mic, Zap, Database } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { IconLabel } from '@/components/IconLabel'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { ttsApi, jobApi } from '@/lib/api' import { ttsApi, jobApi } from '@/lib/api'
import { useJobPolling } from '@/hooks/useJobPolling' import { useJobPolling } from '@/hooks/useJobPolling'
import { LoadingState } from '@/components/LoadingState' import { LoadingState } from '@/components/LoadingState'
@@ -41,6 +43,9 @@ function VoiceCloneForm() {
const [languages, setLanguages] = useState<Language[]>([]) const [languages, setLanguages] = useState<Language[]>([])
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [advancedOpen, setAdvancedOpen] = useState(false) const [advancedOpen, setAdvancedOpen] = useState(false)
const [tempAdvancedParams, setTempAdvancedParams] = useState({
max_new_tokens: 2048
})
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling() const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
@@ -101,9 +106,9 @@ function VoiceCloneForm() {
}, [currentJob?.id, currentJob?.audio_url]) }, [currentJob?.id, currentJob?.audio_url])
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-3"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-2">
<div className="space-y-1"> <div className="space-y-0.5">
<Label htmlFor="ref_text">稿</Label> <IconLabel icon={FileText} tooltip="参考文稿(可选)" />
<Textarea <Textarea
{...register('ref_text')} {...register('ref_text')}
placeholder="参考音频对应的文本..." placeholder="参考音频对应的文本..."
@@ -118,8 +123,8 @@ function VoiceCloneForm() {
)} )}
</div> </div>
<div className="space-y-1"> <div className="space-y-0.5">
<Label htmlFor="ref_audio"></Label> <IconLabel icon={Mic} tooltip="参考音频" required />
<Controller <Controller
name="ref_audio" name="ref_audio"
control={control} control={control}
@@ -133,8 +138,8 @@ function VoiceCloneForm() {
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-0.5">
<Label htmlFor="language"></Label> <IconLabel icon={Globe2} tooltip="语言(可选)" />
<Select <Select
value={watch('language')} value={watch('language')}
onValueChange={(value: string) => setValue('language', value)} onValueChange={(value: string) => setValue('language', value)}
@@ -152,8 +157,8 @@ function VoiceCloneForm() {
</Select> </Select>
</div> </div>
<div className="space-y-1"> <div className="space-y-0.5">
<Label htmlFor="text"></Label> <IconLabel icon={Type} tooltip="合成文本" required />
<Textarea <Textarea
{...register('text')} {...register('text')}
placeholder="输入要合成的文本..." placeholder="输入要合成的文本..."
@@ -170,6 +175,7 @@ function VoiceCloneForm() {
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Zap className="h-4 w-4 text-muted-foreground" />
<Controller <Controller
name="x_vector_only_mode" name="x_vector_only_mode"
control={control} control={control}
@@ -187,6 +193,7 @@ function VoiceCloneForm() {
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Database className="h-4 w-4 text-muted-foreground" />
<Controller <Controller
name="use_cache" name="use_cache"
control={control} control={control}
@@ -204,29 +211,82 @@ function VoiceCloneForm() {
</div> </div>
</div> </div>
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}> <Dialog open={advancedOpen} onOpenChange={(open) => {
<CollapsibleTrigger asChild> if (open) {
<Button type="button" variant="ghost" className="w-full py-1.5"> setTempAdvancedParams({
max_new_tokens: watch('max_new_tokens')
})
}
setAdvancedOpen(open)
}}>
<DialogTrigger asChild>
<Button type="button" variant="outline" className="w-full">
<Settings className="mr-2 h-4 w-4" />
<ChevronDown className="ml-2 h-4 w-4" />
</Button> </Button>
</CollapsibleTrigger> </DialogTrigger>
<CollapsibleContent className="space-y-2 pt-2"> <DialogContent className="sm:max-w-[500px]">
<ParamInput <DialogHeader>
name="max_new_tokens" <DialogTitle></DialogTitle>
label={ADVANCED_PARAMS_INFO.max_new_tokens.label} </DialogHeader>
description={ADVANCED_PARAMS_INFO.max_new_tokens.description} <div className="space-y-4 py-4">
tooltip={ADVANCED_PARAMS_INFO.max_new_tokens.tooltip} <div className="space-y-2">
register={register} <Label htmlFor="dialog-max_new_tokens">
{ADVANCED_PARAMS_INFO.max_new_tokens.label}
</Label>
<Input
id="dialog-max_new_tokens"
type="number"
min={1} min={1}
max={10000} max={10000}
value={tempAdvancedParams.max_new_tokens}
onChange={(e) => setTempAdvancedParams({
...tempAdvancedParams,
max_new_tokens: parseInt(e.target.value) || 2048
})}
/> />
</CollapsibleContent> <p className="text-sm text-muted-foreground">
</Collapsible> {ADVANCED_PARAMS_INFO.max_new_tokens.description}
</p>
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => {
setTempAdvancedParams({ max_new_tokens: watch('max_new_tokens') })
setAdvancedOpen(false)
}}
>
</Button>
<Button
type="button"
onClick={() => {
setValue('max_new_tokens', tempAdvancedParams.max_new_tokens)
setAdvancedOpen(false)
}}
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button type="submit" className="w-full" disabled={isLoading || isPolling}> <Button type="submit" className="w-full" disabled={isLoading || isPolling}>
<Play className="mr-2 h-4 w-4" />
{isLoading ? '创建中...' : '生成语音'} {isLoading ? '创建中...' : '生成语音'}
</Button> </Button>
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{isPolling && <LoadingState elapsedTime={elapsedTime} />} {isPolling && <LoadingState elapsedTime={elapsedTime} />}

View File

@@ -6,10 +6,12 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from '@/components/ui/dialog'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { ChevronDown } from 'lucide-react' import { Settings, Globe2, Type, Play, Palette } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { IconLabel } from '@/components/IconLabel'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { ttsApi, jobApi } from '@/lib/api' import { ttsApi, jobApi } from '@/lib/api'
import { useJobPolling } from '@/hooks/useJobPolling' import { useJobPolling } from '@/hooks/useJobPolling'
import { LoadingState } from '@/components/LoadingState' import { LoadingState } from '@/components/LoadingState'
@@ -40,6 +42,13 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
const [languages, setLanguages] = useState<Language[]>([]) const [languages, setLanguages] = useState<Language[]>([])
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [advancedOpen, setAdvancedOpen] = useState(false) const [advancedOpen, setAdvancedOpen] = useState(false)
const [tempAdvancedParams, setTempAdvancedParams] = useState({
max_new_tokens: 2048,
temperature: 0.3,
top_k: 20,
top_p: 0.7,
repetition_penalty: 1.05
})
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling() const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
@@ -107,9 +116,9 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
}, [currentJob?.id, currentJob?.audio_url]) }, [currentJob?.id, currentJob?.audio_url])
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-3"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-2">
<div className="space-y-1"> <div className="space-y-0.5">
<Label htmlFor="language"></Label> <IconLabel icon={Globe2} tooltip="语言" required />
<Select <Select
value={watch('language')} value={watch('language')}
onValueChange={(value: string) => setValue('language', value)} onValueChange={(value: string) => setValue('language', value)}
@@ -130,8 +139,8 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
)} )}
</div> </div>
<div className="space-y-1"> <div className="space-y-0.5">
<Label htmlFor="text"></Label> <IconLabel icon={Type} tooltip="合成文本" required />
<Textarea <Textarea
{...register('text')} {...register('text')}
placeholder="输入要合成的文本..." placeholder="输入要合成的文本..."
@@ -142,8 +151,8 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
)} )}
</div> </div>
<div className="space-y-1"> <div className="space-y-0.5">
<Label htmlFor="instruct"></Label> <IconLabel icon={Palette} tooltip="音色描述" required />
<Textarea <Textarea
{...register('instruct')} {...register('instruct')}
placeholder="例如:成熟男性,低沉磁性,充满权威感" placeholder="例如:成熟男性,低沉磁性,充满权威感"
@@ -163,68 +172,175 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
)} )}
</div> </div>
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}> <Dialog open={advancedOpen} onOpenChange={(open) => {
<CollapsibleTrigger asChild> if (open) {
<Button type="button" variant="ghost" className="w-full py-1.5"> setTempAdvancedParams({
max_new_tokens: watch('max_new_tokens'),
temperature: watch('temperature'),
top_k: watch('top_k'),
top_p: watch('top_p'),
repetition_penalty: watch('repetition_penalty')
})
}
setAdvancedOpen(open)
}}>
<DialogTrigger asChild>
<Button type="button" variant="outline" className="w-full">
<Settings className="mr-2 h-4 w-4" />
<ChevronDown className="ml-2 h-4 w-4" />
</Button> </Button>
</CollapsibleTrigger> </DialogTrigger>
<CollapsibleContent className="space-y-2 pt-2"> <DialogContent className="sm:max-w-[500px]">
<ParamInput <DialogHeader>
name="max_new_tokens" <DialogTitle></DialogTitle>
label={ADVANCED_PARAMS_INFO.max_new_tokens.label} </DialogHeader>
description={ADVANCED_PARAMS_INFO.max_new_tokens.description} <div className="space-y-4 py-4">
tooltip={ADVANCED_PARAMS_INFO.max_new_tokens.tooltip} <div className="space-y-2">
register={register} <Label htmlFor="dialog-max_new_tokens">
{ADVANCED_PARAMS_INFO.max_new_tokens.label}
</Label>
<Input
id="dialog-max_new_tokens"
type="number"
min={1} min={1}
max={10000} max={10000}
value={tempAdvancedParams.max_new_tokens}
onChange={(e) => setTempAdvancedParams({
...tempAdvancedParams,
max_new_tokens: parseInt(e.target.value) || 2048
})}
/> />
<ParamInput <p className="text-sm text-muted-foreground">
name="temperature" {ADVANCED_PARAMS_INFO.max_new_tokens.description}
label={ADVANCED_PARAMS_INFO.temperature.label} </p>
description={ADVANCED_PARAMS_INFO.temperature.description} </div>
tooltip={ADVANCED_PARAMS_INFO.temperature.tooltip} <div className="space-y-2">
register={register} <Label htmlFor="dialog-temperature">
step={0.1} {ADVANCED_PARAMS_INFO.temperature.label}
</Label>
<Input
id="dialog-temperature"
type="number"
min={0} min={0}
max={2} max={2}
step={0.1}
value={tempAdvancedParams.temperature}
onChange={(e) => setTempAdvancedParams({
...tempAdvancedParams,
temperature: parseFloat(e.target.value) || 0.3
})}
/> />
<ParamInput <p className="text-sm text-muted-foreground">
name="top_k" {ADVANCED_PARAMS_INFO.temperature.description}
label={ADVANCED_PARAMS_INFO.top_k.label} </p>
description={ADVANCED_PARAMS_INFO.top_k.description} </div>
tooltip={ADVANCED_PARAMS_INFO.top_k.tooltip} <div className="space-y-2">
register={register} <Label htmlFor="dialog-top_k">
{ADVANCED_PARAMS_INFO.top_k.label}
</Label>
<Input
id="dialog-top_k"
type="number"
min={1} min={1}
max={100} max={100}
value={tempAdvancedParams.top_k}
onChange={(e) => setTempAdvancedParams({
...tempAdvancedParams,
top_k: parseInt(e.target.value) || 20
})}
/> />
<ParamInput <p className="text-sm text-muted-foreground">
name="top_p" {ADVANCED_PARAMS_INFO.top_k.description}
label={ADVANCED_PARAMS_INFO.top_p.label} </p>
description={ADVANCED_PARAMS_INFO.top_p.description} </div>
tooltip={ADVANCED_PARAMS_INFO.top_p.tooltip} <div className="space-y-2">
register={register} <Label htmlFor="dialog-top_p">
step={0.1} {ADVANCED_PARAMS_INFO.top_p.label}
</Label>
<Input
id="dialog-top_p"
type="number"
min={0} min={0}
max={1} max={1}
step={0.1}
value={tempAdvancedParams.top_p}
onChange={(e) => setTempAdvancedParams({
...tempAdvancedParams,
top_p: parseFloat(e.target.value) || 0.7
})}
/> />
<ParamInput <p className="text-sm text-muted-foreground">
name="repetition_penalty" {ADVANCED_PARAMS_INFO.top_p.description}
label={ADVANCED_PARAMS_INFO.repetition_penalty.label} </p>
description={ADVANCED_PARAMS_INFO.repetition_penalty.description} </div>
tooltip={ADVANCED_PARAMS_INFO.repetition_penalty.tooltip} <div className="space-y-2">
register={register} <Label htmlFor="dialog-repetition_penalty">
step={0.01} {ADVANCED_PARAMS_INFO.repetition_penalty.label}
</Label>
<Input
id="dialog-repetition_penalty"
type="number"
min={0} min={0}
max={2} max={2}
step={0.01}
value={tempAdvancedParams.repetition_penalty}
onChange={(e) => setTempAdvancedParams({
...tempAdvancedParams,
repetition_penalty: parseFloat(e.target.value) || 1.05
})}
/> />
</CollapsibleContent> <p className="text-sm text-muted-foreground">
</Collapsible> {ADVANCED_PARAMS_INFO.repetition_penalty.description}
</p>
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => {
setTempAdvancedParams({
max_new_tokens: watch('max_new_tokens'),
temperature: watch('temperature'),
top_k: watch('top_k'),
top_p: watch('top_p'),
repetition_penalty: watch('repetition_penalty')
})
setAdvancedOpen(false)
}}
>
</Button>
<Button
type="button"
onClick={() => {
setValue('max_new_tokens', tempAdvancedParams.max_new_tokens)
setValue('temperature', tempAdvancedParams.temperature)
setValue('top_k', tempAdvancedParams.top_k)
setValue('top_p', tempAdvancedParams.top_p)
setValue('repetition_penalty', tempAdvancedParams.repetition_penalty)
setAdvancedOpen(false)
}}
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button type="submit" className="w-full" disabled={isLoading || isPolling}> <Button type="submit" className="w-full" disabled={isLoading || isPolling}>
<Play className="mr-2 h-4 w-4" />
{isLoading ? '创建中...' : '生成语音'} {isLoading ? '创建中...' : '生成语音'}
</Button> </Button>
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{isPolling && <LoadingState elapsedTime={elapsedTime} />} {isPolling && <LoadingState elapsedTime={elapsedTime} />}

View File

@@ -2,6 +2,7 @@ import { useState, useRef, lazy, Suspense } from 'react'
import { Navbar } from '@/components/Navbar' import { Navbar } from '@/components/Navbar'
import { Card, CardContent, CardHeader } from '@/components/ui/card' import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { User, Palette, Copy } from 'lucide-react'
import type { CustomVoiceFormHandle } from '@/components/tts/CustomVoiceForm' import type { CustomVoiceFormHandle } from '@/components/tts/CustomVoiceForm'
import type { VoiceDesignFormHandle } from '@/components/tts/VoiceDesignForm' import type { VoiceDesignFormHandle } from '@/components/tts/VoiceDesignForm'
import { HistorySidebar } from '@/components/HistorySidebar' import { HistorySidebar } from '@/components/HistorySidebar'
@@ -63,10 +64,19 @@ function Home() {
<Card> <Card>
<CardHeader> <CardHeader>
<Tabs value={currentTab} onValueChange={setCurrentTab}> <Tabs value={currentTab} onValueChange={setCurrentTab}>
<TabsList className="grid w-full grid-cols-3"> <TabsList className="grid w-full grid-cols-3 h-9">
<TabsTrigger value="custom-voice"></TabsTrigger> <TabsTrigger value="custom-voice">
<TabsTrigger value="voice-design"></TabsTrigger> <User className="h-4 w-4 md:mr-2" />
<TabsTrigger value="voice-clone"></TabsTrigger> <span className="hidden md:inline"></span>
</TabsTrigger>
<TabsTrigger value="voice-design">
<Palette className="h-4 w-4 md:mr-2" />
<span className="hidden md:inline"></span>
</TabsTrigger>
<TabsTrigger value="voice-clone">
<Copy className="h-4 w-4 md:mr-2" />
<span className="hidden md:inline"></span>
</TabsTrigger>
</TabsList> </TabsList>
</Tabs> </Tabs>
</CardHeader> </CardHeader>