mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 09:05:17 -07:00
Refactor ProfileForm for improved readability and maintainability
- Reorganized import statements for clarity. - Enhanced conditional checks for restoring saved files with improved formatting. - Streamlined draft saving logic by consolidating variable declarations. - Updated UI components for better structure and readability in the form layout.
This commit is contained in:
@@ -43,7 +43,7 @@ import { useSystemAudioCapture } from '@/lib/hooks/useSystemAudioCapture';
|
|||||||
import { useTranscription } from '@/lib/hooks/useTranscription';
|
import { useTranscription } from '@/lib/hooks/useTranscription';
|
||||||
import { isTauri } from '@/lib/tauri';
|
import { isTauri } from '@/lib/tauri';
|
||||||
import { formatAudioDuration, getAudioDuration } from '@/lib/utils/audio';
|
import { formatAudioDuration, getAudioDuration } from '@/lib/utils/audio';
|
||||||
import { useUIStore, type ProfileFormDraft } from '@/stores/uiStore';
|
import { type ProfileFormDraft, useUIStore } from '@/stores/uiStore';
|
||||||
import { AudioSampleRecording } from './AudioSampleRecording';
|
import { AudioSampleRecording } from './AudioSampleRecording';
|
||||||
import { AudioSampleSystem } from './AudioSampleSystem';
|
import { AudioSampleSystem } from './AudioSampleSystem';
|
||||||
import { AudioSampleUpload } from './AudioSampleUpload';
|
import { AudioSampleUpload } from './AudioSampleUpload';
|
||||||
@@ -265,11 +265,15 @@ export function ProfileForm() {
|
|||||||
});
|
});
|
||||||
setSampleMode(profileFormDraft.sampleMode);
|
setSampleMode(profileFormDraft.sampleMode);
|
||||||
// Restore the file if we have it saved
|
// Restore the file if we have it saved
|
||||||
if (profileFormDraft.sampleFileData && profileFormDraft.sampleFileName && profileFormDraft.sampleFileType) {
|
if (
|
||||||
|
profileFormDraft.sampleFileData &&
|
||||||
|
profileFormDraft.sampleFileName &&
|
||||||
|
profileFormDraft.sampleFileType
|
||||||
|
) {
|
||||||
const file = base64ToFile(
|
const file = base64ToFile(
|
||||||
profileFormDraft.sampleFileData,
|
profileFormDraft.sampleFileData,
|
||||||
profileFormDraft.sampleFileName,
|
profileFormDraft.sampleFileName,
|
||||||
profileFormDraft.sampleFileType
|
profileFormDraft.sampleFileType,
|
||||||
);
|
);
|
||||||
form.setValue('sampleFile', file);
|
form.setValue('sampleFile', file);
|
||||||
}
|
}
|
||||||
@@ -446,7 +450,8 @@ export function ProfileForm() {
|
|||||||
if (!newOpen && isCreating) {
|
if (!newOpen && isCreating) {
|
||||||
// Save draft when closing the create modal
|
// Save draft when closing the create modal
|
||||||
const values = form.getValues();
|
const values = form.getValues();
|
||||||
const hasContent = values.name || values.description || values.referenceText || values.sampleFile;
|
const hasContent =
|
||||||
|
values.name || values.description || values.referenceText || values.sampleFile;
|
||||||
|
|
||||||
if (hasContent) {
|
if (hasContent) {
|
||||||
const draft: ProfileFormDraft = {
|
const draft: ProfileFormDraft = {
|
||||||
@@ -491,135 +496,116 @@ export function ProfileForm() {
|
|||||||
<DialogContent className="max-w-none w-screen h-screen left-0 top-0 translate-x-0 translate-y-0 rounded-none p-6 overflow-y-auto">
|
<DialogContent className="max-w-none w-screen h-screen left-0 top-0 translate-x-0 translate-y-0 rounded-none p-6 overflow-y-auto">
|
||||||
<div className="max-w-5xl max-h-[85vh] mx-auto my-auto w-full flex flex-col">
|
<div className="max-w-5xl max-h-[85vh] mx-auto my-auto w-full flex flex-col">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-2xl">{editingProfileId ? 'Edit Voice' : 'Clone voice'}</DialogTitle>
|
<DialogTitle className="text-2xl">
|
||||||
<DialogDescription>
|
{editingProfileId ? 'Edit Voice' : 'Clone voice'}
|
||||||
{editingProfileId
|
</DialogTitle>
|
||||||
? 'Update your voice profile details and manage samples.'
|
<DialogDescription>
|
||||||
: 'Create a new voice profile with an audio sample to clone the voice.'}
|
{editingProfileId
|
||||||
</DialogDescription>
|
? 'Update your voice profile details and manage samples.'
|
||||||
{isCreating && profileFormDraft && (
|
: 'Create a new voice profile with an audio sample to clone the voice.'}
|
||||||
<div className="flex items-center gap-2 pt-2">
|
</DialogDescription>
|
||||||
<span className="text-xs text-muted-foreground">Draft restored</span>
|
{isCreating && profileFormDraft && (
|
||||||
<Button
|
<div className="flex items-center gap-2 pt-2">
|
||||||
type="button"
|
<span className="text-xs text-muted-foreground">Draft restored</span>
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
type="button"
|
||||||
className="h-6 px-2 text-xs text-muted-foreground"
|
variant="ghost"
|
||||||
onClick={() => {
|
size="sm"
|
||||||
setProfileFormDraft(null);
|
className="h-6 px-2 text-xs text-muted-foreground"
|
||||||
form.reset({
|
onClick={() => {
|
||||||
name: '',
|
setProfileFormDraft(null);
|
||||||
description: '',
|
form.reset({
|
||||||
language: 'en',
|
name: '',
|
||||||
sampleFile: undefined,
|
description: '',
|
||||||
referenceText: '',
|
language: 'en',
|
||||||
});
|
sampleFile: undefined,
|
||||||
setSampleMode('record');
|
referenceText: '',
|
||||||
}}
|
});
|
||||||
>
|
setSampleMode('record');
|
||||||
<X className="h-3 w-3 mr-1" />
|
}}
|
||||||
Discard
|
>
|
||||||
</Button>
|
<X className="h-3 w-3 mr-1" />
|
||||||
</div>
|
Discard
|
||||||
)}
|
</Button>
|
||||||
</DialogHeader>
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex-1 min-h-0 flex flex-col">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex-1 min-h-0 flex flex-col">
|
||||||
<div className="grid gap-6 grid-cols-2 flex-1 overflow-y-auto min-h-0">
|
<div className="grid gap-6 grid-cols-2 flex-1 overflow-y-auto min-h-0">
|
||||||
{/* Left column: Sample management */}
|
{/* Left column: Sample management */}
|
||||||
<div className="space-y-4 border-r pr-6">
|
<div className="space-y-4 border-r pr-6">
|
||||||
{isCreating ? (
|
{isCreating ? (
|
||||||
<>
|
<>
|
||||||
<Tabs
|
<Tabs
|
||||||
className="pt-4"
|
className="pt-4"
|
||||||
value={sampleMode}
|
value={sampleMode}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
const newMode = v as 'upload' | 'record' | 'system';
|
const newMode = v as 'upload' | 'record' | 'system';
|
||||||
// Cancel any active recordings when switching modes
|
// Cancel any active recordings when switching modes
|
||||||
if (isRecording && newMode !== 'record') {
|
if (isRecording && newMode !== 'record') {
|
||||||
cancelRecording();
|
cancelRecording();
|
||||||
}
|
}
|
||||||
if (isSystemRecording && newMode !== 'system') {
|
if (isSystemRecording && newMode !== 'system') {
|
||||||
cancelSystemRecording();
|
cancelSystemRecording();
|
||||||
}
|
}
|
||||||
setSampleMode(newMode);
|
setSampleMode(newMode);
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<TabsList
|
|
||||||
className={`grid w-full ${isTauri() && isSystemAudioSupported ? 'grid-cols-3' : 'grid-cols-2'}`}
|
|
||||||
>
|
>
|
||||||
<TabsTrigger value="upload" className="flex items-center gap-2">
|
<TabsList
|
||||||
<Upload className="h-4 w-4 shrink-0" />
|
className={`grid w-full ${isTauri() && isSystemAudioSupported ? 'grid-cols-3' : 'grid-cols-2'}`}
|
||||||
Upload
|
>
|
||||||
</TabsTrigger>
|
<TabsTrigger value="upload" className="flex items-center gap-2">
|
||||||
<TabsTrigger value="record" className="flex items-center gap-2">
|
<Upload className="h-4 w-4 shrink-0" />
|
||||||
<Mic className="h-4 w-4 shrink-0" />
|
Upload
|
||||||
Record
|
|
||||||
</TabsTrigger>
|
|
||||||
{isTauri() && isSystemAudioSupported && (
|
|
||||||
<TabsTrigger value="system" className="flex items-center gap-2">
|
|
||||||
<Monitor className="h-4 w-4 shrink-0" />
|
|
||||||
System Audio
|
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
)}
|
<TabsTrigger value="record" className="flex items-center gap-2">
|
||||||
</TabsList>
|
<Mic className="h-4 w-4 shrink-0" />
|
||||||
|
Record
|
||||||
<TabsContent value="upload" className="space-y-4">
|
</TabsTrigger>
|
||||||
<FormField
|
{isTauri() && isSystemAudioSupported && (
|
||||||
control={form.control}
|
<TabsTrigger value="system" className="flex items-center gap-2">
|
||||||
name="sampleFile"
|
<Monitor className="h-4 w-4 shrink-0" />
|
||||||
render={({ field: { onChange, name } }) => (
|
System Audio
|
||||||
<AudioSampleUpload
|
</TabsTrigger>
|
||||||
file={selectedFile}
|
|
||||||
onFileChange={onChange}
|
|
||||||
onTranscribe={handleTranscribe}
|
|
||||||
onPlayPause={handlePlayPause}
|
|
||||||
isPlaying={isPlaying}
|
|
||||||
isValidating={isValidatingAudio}
|
|
||||||
isTranscribing={transcribe.isPending}
|
|
||||||
isDisabled={
|
|
||||||
audioDuration !== null && audioDuration > MAX_AUDIO_DURATION_SECONDS
|
|
||||||
}
|
|
||||||
fieldName={name}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
</TabsList>
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="record" className="space-y-4">
|
<TabsContent value="upload" className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="sampleFile"
|
name="sampleFile"
|
||||||
render={() => (
|
render={({ field: { onChange, name } }) => (
|
||||||
<AudioSampleRecording
|
<AudioSampleUpload
|
||||||
file={selectedFile}
|
file={selectedFile}
|
||||||
isRecording={isRecording}
|
onFileChange={onChange}
|
||||||
duration={duration}
|
onTranscribe={handleTranscribe}
|
||||||
onStart={startRecording}
|
onPlayPause={handlePlayPause}
|
||||||
onStop={stopRecording}
|
isPlaying={isPlaying}
|
||||||
onCancel={handleCancelRecording}
|
isValidating={isValidatingAudio}
|
||||||
onTranscribe={handleTranscribe}
|
isTranscribing={transcribe.isPending}
|
||||||
onPlayPause={handlePlayPause}
|
isDisabled={
|
||||||
isPlaying={isPlaying}
|
audioDuration !== null &&
|
||||||
isTranscribing={transcribe.isPending}
|
audioDuration > MAX_AUDIO_DURATION_SECONDS
|
||||||
/>
|
}
|
||||||
)}
|
fieldName={name}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
)}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
{isTauri() && isSystemAudioSupported && (
|
<TabsContent value="record" className="space-y-4">
|
||||||
<TabsContent value="system" className="space-y-4">
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="sampleFile"
|
name="sampleFile"
|
||||||
render={() => (
|
render={() => (
|
||||||
<AudioSampleSystem
|
<AudioSampleRecording
|
||||||
file={selectedFile}
|
file={selectedFile}
|
||||||
isRecording={isSystemRecording}
|
isRecording={isRecording}
|
||||||
duration={systemDuration}
|
duration={duration}
|
||||||
onStart={startSystemRecording}
|
onStart={startRecording}
|
||||||
onStop={stopSystemRecording}
|
onStop={stopRecording}
|
||||||
onCancel={handleCancelRecording}
|
onCancel={handleCancelRecording}
|
||||||
onTranscribe={handleTranscribe}
|
onTranscribe={handleTranscribe}
|
||||||
onPlayPause={handlePlayPause}
|
onPlayPause={handlePlayPause}
|
||||||
@@ -629,111 +615,135 @@ export function ProfileForm() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<FormField
|
{isTauri() && isSystemAudioSupported && (
|
||||||
control={form.control}
|
<TabsContent value="system" className="space-y-4">
|
||||||
name="referenceText"
|
<FormField
|
||||||
render={({ field }) => (
|
control={form.control}
|
||||||
<FormItem>
|
name="sampleFile"
|
||||||
<FormLabel>Reference Text</FormLabel>
|
render={() => (
|
||||||
<FormControl>
|
<AudioSampleSystem
|
||||||
<Textarea
|
file={selectedFile}
|
||||||
placeholder="Enter the exact text spoken in the audio..."
|
isRecording={isSystemRecording}
|
||||||
className="min-h-[100px]"
|
duration={systemDuration}
|
||||||
{...field}
|
onStart={startSystemRecording}
|
||||||
|
onStop={stopSystemRecording}
|
||||||
|
onCancel={handleCancelRecording}
|
||||||
|
onTranscribe={handleTranscribe}
|
||||||
|
onPlayPause={handlePlayPause}
|
||||||
|
isPlaying={isPlaying}
|
||||||
|
isTranscribing={transcribe.isPending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</TabsContent>
|
||||||
<FormMessage />
|
)}
|
||||||
</FormItem>
|
</Tabs>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
// Show sample list when editing
|
|
||||||
editingProfileId && (
|
|
||||||
<div>
|
|
||||||
<SampleList profileId={editingProfileId} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right column: Profile info */}
|
<FormField
|
||||||
<div className="space-y-4">
|
control={form.control}
|
||||||
<FormField
|
name="referenceText"
|
||||||
control={form.control}
|
render={({ field }) => (
|
||||||
name="name"
|
<FormItem>
|
||||||
render={({ field }) => (
|
<FormLabel>Reference Text</FormLabel>
|
||||||
<FormItem>
|
<FormControl>
|
||||||
<FormLabel>Name</FormLabel>
|
<Textarea
|
||||||
<FormControl>
|
placeholder="Enter the exact text spoken in the audio..."
|
||||||
<Input placeholder="My Voice" {...field} />
|
className="min-h-[100px]"
|
||||||
</FormControl>
|
{...field}
|
||||||
<FormMessage />
|
/>
|
||||||
</FormItem>
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// Show sample list when editing
|
||||||
|
editingProfileId && (
|
||||||
|
<div>
|
||||||
|
<SampleList profileId={editingProfileId} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
<FormField
|
{/* Right column: Profile info */}
|
||||||
control={form.control}
|
<div className="space-y-4">
|
||||||
name="description"
|
<FormField
|
||||||
render={({ field }) => (
|
control={form.control}
|
||||||
<FormItem>
|
name="name"
|
||||||
<FormLabel>Description (Optional)</FormLabel>
|
render={({ field }) => (
|
||||||
<FormControl>
|
<FormItem>
|
||||||
<Textarea placeholder="Describe this voice..." {...field} />
|
<FormLabel>Name</FormLabel>
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="language"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Language</FormLabel>
|
|
||||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<Input placeholder="My Voice" {...field} />
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<FormMessage />
|
||||||
{LANGUAGE_OPTIONS.map((lang) => (
|
</FormItem>
|
||||||
<SelectItem key={lang.value} value={lang.value}>
|
)}
|
||||||
{lang.label}
|
/>
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2 justify-end mt-6 pt-4 border-t">
|
<FormField
|
||||||
<Button type="button" variant="outline" onClick={() => handleOpenChange(false)}>
|
control={form.control}
|
||||||
Cancel
|
name="description"
|
||||||
</Button>
|
render={({ field }) => (
|
||||||
<Button
|
<FormItem>
|
||||||
type="submit"
|
<FormLabel>Description (Optional)</FormLabel>
|
||||||
disabled={createProfile.isPending || updateProfile.isPending || addSample.isPending}
|
<FormControl>
|
||||||
>
|
<Textarea placeholder="Describe this voice..." {...field} />
|
||||||
{createProfile.isPending || updateProfile.isPending || addSample.isPending
|
</FormControl>
|
||||||
? 'Saving...'
|
<FormMessage />
|
||||||
: editingProfileId
|
</FormItem>
|
||||||
? 'Save Changes'
|
)}
|
||||||
: 'Create Profile'}
|
/>
|
||||||
</Button>
|
|
||||||
</div>
|
<FormField
|
||||||
</form>
|
control={form.control}
|
||||||
</Form>
|
name="language"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Language</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{LANGUAGE_OPTIONS.map((lang) => (
|
||||||
|
<SelectItem key={lang.value} value={lang.value}>
|
||||||
|
{lang.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 justify-end mt-6 pt-4 border-t">
|
||||||
|
<Button type="button" variant="outline" onClick={() => handleOpenChange(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={
|
||||||
|
createProfile.isPending || updateProfile.isPending || addSample.isPending
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{createProfile.isPending || updateProfile.isPending || addSample.isPending
|
||||||
|
? 'Saving...'
|
||||||
|
: editingProfileId
|
||||||
|
? 'Save Changes'
|
||||||
|
: 'Create Profile'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
Reference in New Issue
Block a user