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:
Jamie Pine
2026-01-26 22:28:29 -08:00
parent 892f363e3a
commit 5f58c4dc3d
4 changed files with 47 additions and 113 deletions
@@ -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,9 +514,10 @@ 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> <div>
<h3 className="text-sm font-medium mb-2">Add Sample</h3> <h3 className="text-sm font-medium mb-2">Add Sample</h3>
<p className="text-sm text-muted-foreground mb-4"> <p className="text-sm text-muted-foreground mb-4">
@@ -640,9 +642,17 @@ export function ProfileForm() {
</FormItem> </FormItem>
)} )}
/> />
</>
) : (
// Show sample list when editing
editingProfileId && (
<div>
<SampleList profileId={editingProfileId} />
</div> </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">
<Button type="button" variant="outline" onClick={() => handleOpenChange(false)}> <Button type="button" variant="outline" onClick={() => handleOpenChange(false)}>
@@ -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)}