239 lines
8.0 KiB
TypeScript
239 lines
8.0 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react'
|
|
|
|
import { ttsService, type TTSResponse } from '@/lib/api/services/tts'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select'
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
|
import { TTSRow } from './TTSRow'
|
|
import { RefreshCw, Search } from 'lucide-react'
|
|
|
|
export function TTSList() {
|
|
const [ttsHistory, setTTSHistory] = useState<TTSResponse[]>([])
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
const [sortBy, setSortBy] = useState('created_at')
|
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
|
|
const [limit, setLimit] = useState(50)
|
|
|
|
const fetchTTSHistory = useCallback(async () => {
|
|
try {
|
|
setIsLoading(true)
|
|
setError(null)
|
|
const data = await ttsService.getTTSHistory({ limit })
|
|
setTTSHistory(data.tts)
|
|
} catch (err) {
|
|
setError('Failed to load TTS history')
|
|
console.error('Failed to fetch TTS history:', err)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}, [limit])
|
|
|
|
useEffect(() => {
|
|
fetchTTSHistory()
|
|
}, [fetchTTSHistory])
|
|
|
|
// Listen for TTS generation events to refresh the list
|
|
useEffect(() => {
|
|
const handleTTSGenerated = () => {
|
|
fetchTTSHistory()
|
|
}
|
|
|
|
window.addEventListener('tts-generated', handleTTSGenerated)
|
|
return () => {
|
|
window.removeEventListener('tts-generated', handleTTSGenerated)
|
|
}
|
|
}, [fetchTTSHistory])
|
|
|
|
const filteredHistory = ttsHistory.filter((tts) =>
|
|
tts.text.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
tts.provider.toLowerCase().includes(searchQuery.toLowerCase())
|
|
)
|
|
|
|
const sortedHistory = [...filteredHistory].sort((a, b) => {
|
|
let aValue: any = a[sortBy as keyof TTSResponse]
|
|
let bValue: any = b[sortBy as keyof TTSResponse]
|
|
|
|
// Convert dates to timestamps for comparison
|
|
if (sortBy === 'created_at') {
|
|
aValue = new Date(aValue).getTime()
|
|
bValue = new Date(bValue).getTime()
|
|
}
|
|
|
|
if (sortOrder === 'asc') {
|
|
return aValue > bValue ? 1 : -1
|
|
} else {
|
|
return aValue < bValue ? 1 : -1
|
|
}
|
|
})
|
|
|
|
if (error) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="text-center text-muted-foreground">
|
|
Failed to load TTS history. Please try again.
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Search and filters */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
TTS History
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={fetchTTSHistory}
|
|
disabled={isLoading}
|
|
>
|
|
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
|
</Button>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex gap-4">
|
|
<div className="flex-1">
|
|
<Label htmlFor="search">Search</Label>
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
id="search"
|
|
placeholder="Search by text or provider..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-9"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="sortBy">Sort by</Label>
|
|
<Select value={sortBy} onValueChange={setSortBy}>
|
|
<SelectTrigger id="sortBy" className="w-40">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="created_at">Created</SelectItem>
|
|
<SelectItem value="text">Text</SelectItem>
|
|
<SelectItem value="provider">Provider</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="sortOrder">Order</Label>
|
|
<Select
|
|
value={sortOrder}
|
|
onValueChange={(value: 'asc' | 'desc') => setSortOrder(value)}
|
|
>
|
|
<SelectTrigger id="sortOrder" className="w-32">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="desc">Newest</SelectItem>
|
|
<SelectItem value="asc">Oldest</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="limit">Limit</Label>
|
|
<Select
|
|
value={limit.toString()}
|
|
onValueChange={(value) => setLimit(parseInt(value))}
|
|
>
|
|
<SelectTrigger id="limit" className="w-20">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="25">25</SelectItem>
|
|
<SelectItem value="50">50</SelectItem>
|
|
<SelectItem value="100">100</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Results */}
|
|
<div className="space-y-2">
|
|
{isLoading ? (
|
|
<div className="space-y-2">
|
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
<Card key={i}>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex-1 space-y-2">
|
|
{/* Badges row */}
|
|
<div className="flex items-center gap-2">
|
|
<Skeleton className="h-6 w-16" />
|
|
<Skeleton className="h-6 w-20" />
|
|
</div>
|
|
|
|
{/* Text content */}
|
|
<div className="space-y-1">
|
|
<Skeleton className="h-4 w-3/4" />
|
|
<div className="flex gap-1">
|
|
<Skeleton className="h-5 w-12" />
|
|
<Skeleton className="h-5 w-16" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Date and metadata */}
|
|
<div className="flex items-center gap-4">
|
|
<Skeleton className="h-3 w-32" />
|
|
<Skeleton className="h-3 w-20" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Play button */}
|
|
<div className="flex items-center gap-2">
|
|
<Skeleton className="h-8 w-8 rounded" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
) : sortedHistory.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="text-center text-muted-foreground">
|
|
{searchQuery
|
|
? 'No TTS generations match your search.'
|
|
: 'No TTS generations yet. Create your first one!'}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<>
|
|
<div className="flex items-center justify-between">
|
|
<div className="text-sm text-muted-foreground">
|
|
{sortedHistory.length} generation{sortedHistory.length !== 1 ? 's' : ''}
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{sortedHistory.map((tts) => (
|
|
<TTSRow key={tts.id} tts={tts} />
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
} |