feat: add Text to Speech (TTS) functionality with provider selection, generation, and history management
- Implemented CreateTTSDialog for generating TTS from user input. - Added TTSHeader for search, sorting, and creation controls. - Created TTSList to display TTS history with filtering and sorting capabilities. - Developed TTSLoadingStates for handling loading and error states. - Introduced TTSRow for individual TTS entries with play and delete options. - Built TTSTable for structured display of TTS history. - Integrated TTS service API for generating and managing TTS data. - Added TTSPage to encapsulate the TTS feature with pagination and state management.
This commit is contained in:
177
src/components/tts/TTSTable.tsx
Normal file
177
src/components/tts/TTSTable.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { format } from 'date-fns'
|
||||
import { CheckCircle, Clock, Loader, Mic, Trash2, Volume2 } from 'lucide-react'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { type TTSResponse, ttsService } from '@/lib/api/services/tts'
|
||||
import { soundsService } from '@/lib/api/services/sounds'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface TTSTableProps {
|
||||
ttsHistory: TTSResponse[]
|
||||
onTTSDeleted?: (ttsId: number) => void
|
||||
}
|
||||
|
||||
export function TTSTable({ ttsHistory, onTTSDeleted }: TTSTableProps) {
|
||||
const handlePlaySound = async (tts: TTSResponse) => {
|
||||
if (!tts.sound_id) {
|
||||
toast.error('This TTS is still being processed.')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await soundsService.playSound(tts.sound_id)
|
||||
} catch (error) {
|
||||
toast.error('Failed to play the sound.')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteTTS = async (tts: TTSResponse) => {
|
||||
if (!confirm(`Are you sure you want to delete this TTS generation?\n\n"${tts.text}"\n\nThis will also delete the associated sound file and cannot be undone.`)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await ttsService.deleteTTS(tts.id)
|
||||
toast.success('TTS generation deleted successfully')
|
||||
onTTSDeleted?.(tts.id)
|
||||
} catch (error) {
|
||||
toast.error('Failed to delete TTS generation')
|
||||
}
|
||||
}
|
||||
|
||||
const getProviderColor = (provider: string) => {
|
||||
const colors = {
|
||||
gtts: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
|
||||
}
|
||||
return colors[provider as keyof typeof colors] || 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300'
|
||||
}
|
||||
|
||||
const getStatusBadge = (tts: TTSResponse) => {
|
||||
const isCompleted = tts.sound_id !== null
|
||||
|
||||
if (isCompleted) {
|
||||
return (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
<CheckCircle className="h-3 w-3" />
|
||||
Complete
|
||||
</Badge>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Badge variant="outline" className="gap-1">
|
||||
<Loader className="h-3 w-3 animate-spin" />
|
||||
Processing
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Text</TableHead>
|
||||
<TableHead>Provider</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Options</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Sound ID</TableHead>
|
||||
<TableHead className="w-[120px]">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{ttsHistory.map((tts) => (
|
||||
<TableRow key={tts.id}>
|
||||
<TableCell className="max-w-md">
|
||||
<div className="truncate font-medium">
|
||||
"{tts.text}"
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={getProviderColor(tts.provider)}>
|
||||
<Mic className="mr-1 h-3 w-3" />
|
||||
{tts.provider.toUpperCase()}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{getStatusBadge(tts)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{Object.keys(tts.options).length > 0 ? (
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{Object.entries(tts.options).map(([key, value]) => (
|
||||
<Badge key={key} variant="outline" className="text-xs">
|
||||
{key}: {String(value)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">None</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||
<Clock className="h-3 w-3" />
|
||||
{format(new Date(tts.created_at), 'MMM dd, yyyy HH:mm')}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{tts.sound_id ? (
|
||||
<span className="text-sm font-mono">{tts.sound_id}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handlePlaySound(tts)}
|
||||
disabled={!tts.sound_id}
|
||||
>
|
||||
<Volume2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{!tts.sound_id ? 'Sound is being processed' : 'Play sound'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteTTS(tts)}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Delete TTS generation
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user