mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 00:55:14 -07:00
Refactor VoiceProfiles components and remove ProfileDetail
- Removed the ProfileDetail component to streamline the ProfileCard functionality. - Updated ProfileCard to eliminate the detail view and associated state management. - Enhanced ProfileForm to manage audio samples more effectively, including improved UI for sample management. - Adjusted SampleList to ensure proper button types for better accessibility.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { Download, Edit, Eye, Mic, Trash2 } from 'lucide-react';
|
import { Download, Edit, Mic, Trash2 } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -16,14 +16,12 @@ import type { VoiceProfileResponse } from '@/lib/api/types';
|
|||||||
import { useDeleteProfile, useExportProfile } from '@/lib/hooks/useProfiles';
|
import { useDeleteProfile, useExportProfile } from '@/lib/hooks/useProfiles';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
import { useUIStore } from '@/stores/uiStore';
|
import { useUIStore } from '@/stores/uiStore';
|
||||||
import { ProfileDetail } from './ProfileDetail';
|
|
||||||
|
|
||||||
interface ProfileCardProps {
|
interface ProfileCardProps {
|
||||||
profile: VoiceProfileResponse;
|
profile: VoiceProfileResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProfileCard({ profile }: ProfileCardProps) {
|
export function ProfileCard({ profile }: ProfileCardProps) {
|
||||||
const [detailOpen, setDetailOpen] = useState(false);
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const deleteProfile = useDeleteProfile();
|
const deleteProfile = useDeleteProfile();
|
||||||
const exportProfile = useExportProfile();
|
const exportProfile = useExportProfile();
|
||||||
@@ -85,14 +83,6 @@ export function ProfileCard({ profile }: ProfileCardProps) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-0.5 justify-end items-end mt-auto">
|
<div className="flex gap-0.5 justify-end items-end mt-auto">
|
||||||
<CircleButton
|
|
||||||
icon={Eye}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setDetailOpen(true);
|
|
||||||
}}
|
|
||||||
aria-label="View details"
|
|
||||||
/>
|
|
||||||
<CircleButton
|
<CircleButton
|
||||||
icon={Download}
|
icon={Download}
|
||||||
onClick={handleExport}
|
onClick={handleExport}
|
||||||
@@ -117,8 +107,6 @@ export function ProfileCard({ profile }: ProfileCardProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<ProfileDetail profileId={profile.id} open={detailOpen} onOpenChange={setDetailOpen} />
|
|
||||||
|
|
||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import { useProfile } from '@/lib/hooks/useProfiles';
|
|
||||||
import { formatDate } from '@/lib/utils/format';
|
|
||||||
import { SampleList } from './SampleList';
|
|
||||||
|
|
||||||
interface ProfileDetailProps {
|
|
||||||
profileId: string;
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProfileDetail({ profileId, open, onOpenChange }: ProfileDetailProps) {
|
|
||||||
const { data: profile, isLoading } = useProfile(profileId);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent>
|
|
||||||
<div className="text-muted-foreground">Loading profile...</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!profile) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{profile.name}</DialogTitle>
|
|
||||||
<DialogDescription>Manage samples and view profile details</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{profile.description && (
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-medium mb-1">Description</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">{profile.description}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Badge variant="outline">{profile.language}</Badge>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
Created {formatDate(profile.created_at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t pt-4">
|
|
||||||
<SampleList profileId={profileId} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -47,6 +47,7 @@ import { 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';
|
||||||
|
import { SampleList } from './SampleList';
|
||||||
|
|
||||||
// Helper function to get audio duration from File
|
// Helper function to get audio duration from File
|
||||||
async function getAudioDuration(file: File & { recordedDuration?: number }): Promise<number> {
|
async function getAudioDuration(file: File & { recordedDuration?: number }): Promise<number> {
|
||||||
@@ -325,7 +326,7 @@ export function ProfileForm() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
toast({
|
toast({
|
||||||
title: 'Profile updated',
|
title: 'Voice updated',
|
||||||
description: `"${data.name}" has been updated successfully.`,
|
description: `"${data.name}" has been updated successfully.`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -446,17 +447,17 @@ export function ProfileForm() {
|
|||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className="max-w-4xl">
|
<DialogContent className="max-w-4xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{editingProfileId ? 'Edit Profile' : 'Create Voice Profile'}</DialogTitle>
|
<DialogTitle>{editingProfileId ? 'Edit Voice' : 'Create Voice Profile'}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{editingProfileId
|
{editingProfileId
|
||||||
? 'Update your voice profile details.'
|
? 'Update your voice profile details and manage samples.'
|
||||||
: 'Create a new voice profile with an audio sample to clone the voice.'}
|
: 'Create a new voice profile with an audio sample to clone the voice.'}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
<div className={`grid gap-6 ${isCreating ? 'grid-cols-2' : 'grid-cols-1'}`}>
|
<div className="grid gap-6 grid-cols-2">
|
||||||
{/* Left column: Profile info */}
|
{/* Left column: Profile info */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
@@ -513,15 +514,16 @@ export function ProfileForm() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right column: Sample upload section - only show when creating */}
|
{/* Right column: Sample management */}
|
||||||
{isCreating && (
|
<div className="space-y-4 border-l pl-6">
|
||||||
<div className="space-y-4 border-l pl-6">
|
{isCreating ? (
|
||||||
<div>
|
<>
|
||||||
<h3 className="text-sm font-medium mb-2">Add Sample</h3>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
<h3 className="text-sm font-medium mb-2">Add Sample</h3>
|
||||||
Provide an audio sample to clone the voice. You can add more samples later.
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
</p>
|
Provide an audio sample to clone the voice. You can add more samples later.
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
value={sampleMode}
|
value={sampleMode}
|
||||||
@@ -623,25 +625,33 @@ export function ProfileForm() {
|
|||||||
)}
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="referenceText"
|
name="referenceText"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Reference Text</FormLabel>
|
<FormLabel>Reference Text</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Enter the exact text spoken in the audio..."
|
placeholder="Enter the exact text spoken in the audio..."
|
||||||
className="min-h-[100px]"
|
className="min-h-[100px]"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
)}
|
) : (
|
||||||
|
// Show sample list when editing
|
||||||
|
editingProfileId && (
|
||||||
|
<div>
|
||||||
|
<SampleList profileId={editingProfileId} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 justify-end mt-6 pt-4 border-t">
|
<div className="flex gap-2 justify-end mt-6 pt-4 border-t">
|
||||||
@@ -655,7 +665,7 @@ export function ProfileForm() {
|
|||||||
{createProfile.isPending || updateProfile.isPending || addSample.isPending
|
{createProfile.isPending || updateProfile.isPending || addSample.isPending
|
||||||
? 'Saving...'
|
? 'Saving...'
|
||||||
: editingProfileId
|
: editingProfileId
|
||||||
? 'Update Profile'
|
? 'Save Changes'
|
||||||
: 'Create Profile'}
|
: 'Create Profile'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function SampleList({ profileId }: SampleListProps) {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-lg font-semibold">Audio Samples</h3>
|
<h3 className="text-lg font-semibold">Audio Samples</h3>
|
||||||
<Button size="sm" onClick={() => setUploadOpen(true)}>
|
<Button type="button" size="sm" onClick={() => setUploadOpen(true)}>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Add Sample
|
Add Sample
|
||||||
</Button>
|
</Button>
|
||||||
@@ -60,6 +60,7 @@ export function SampleList({ profileId }: SampleListProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handlePlay(sample.reference_text, sample.id)}
|
onClick={() => handlePlay(sample.reference_text, sample.id)}
|
||||||
@@ -69,6 +70,7 @@ export function SampleList({ profileId }: SampleListProps) {
|
|||||||
Play
|
Play
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleDelete(sample.id)}
|
onClick={() => handleDelete(sample.id)}
|
||||||
|
|||||||
Reference in New Issue
Block a user