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:
185
src/pages/TTSPage.tsx
Normal file
185
src/pages/TTSPage.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
import { AppPagination } from '@/components/AppPagination'
|
||||
import { CreateTTSDialog } from '@/components/tts/CreateTTSDialog'
|
||||
import { TTSHeader } from '@/components/tts/TTSHeader'
|
||||
import {
|
||||
TTSEmpty,
|
||||
TTSError,
|
||||
TTSLoading,
|
||||
} from '@/components/tts/TTSLoadingStates'
|
||||
import { TTSTable } from '@/components/tts/TTSTable'
|
||||
import {
|
||||
type TTSResponse,
|
||||
type TTSSortField,
|
||||
type TTSSortOrder,
|
||||
ttsService,
|
||||
} from '@/lib/api/services/tts'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export function TTSPage() {
|
||||
const [ttsHistory, setTTSHistory] = useState<TTSResponse[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Search and sorting state
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [sortBy, setSortBy] = useState<TTSSortField>('created_at')
|
||||
const [sortOrder, setSortOrder] = useState<TTSSortOrder>('desc')
|
||||
|
||||
// Pagination state
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const [totalCount, setTotalCount] = useState(0)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
|
||||
// Create TTS dialog state
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
|
||||
// Debounce search query
|
||||
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedSearchQuery(searchQuery)
|
||||
}, 300)
|
||||
|
||||
return () => clearTimeout(handler)
|
||||
}, [searchQuery])
|
||||
|
||||
const fetchTTSHistory = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const response = await ttsService.getTTSHistory({
|
||||
search: debouncedSearchQuery.trim() || undefined,
|
||||
sort_by: sortBy,
|
||||
sort_order: sortOrder,
|
||||
page: currentPage,
|
||||
limit: pageSize,
|
||||
})
|
||||
setTTSHistory(response.tts)
|
||||
setTotalPages(response.total_pages)
|
||||
setTotalCount(response.total)
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : 'Failed to fetch TTS history'
|
||||
setError(errorMessage)
|
||||
toast.error(errorMessage)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [debouncedSearchQuery, sortBy, sortOrder, currentPage, pageSize])
|
||||
|
||||
useEffect(() => {
|
||||
fetchTTSHistory()
|
||||
}, [fetchTTSHistory])
|
||||
|
||||
// Reset to page 1 when filters change
|
||||
useEffect(() => {
|
||||
if (currentPage !== 1) {
|
||||
setCurrentPage(1)
|
||||
}
|
||||
}, [debouncedSearchQuery, sortBy, sortOrder, pageSize])
|
||||
|
||||
// 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 handlePageChange = (page: number) => {
|
||||
setCurrentPage(page)
|
||||
}
|
||||
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
setPageSize(size)
|
||||
setCurrentPage(1) // Reset to first page when changing page size
|
||||
}
|
||||
|
||||
const handleTTSDeleted = (ttsId: number) => {
|
||||
// Remove the deleted TTS from the current list
|
||||
setTTSHistory(prev => prev.filter(tts => tts.id !== ttsId))
|
||||
|
||||
// Update total count
|
||||
setTotalCount(prev => prev - 1)
|
||||
|
||||
// If current page is now empty and not the first page, go to previous page
|
||||
const remainingOnCurrentPage = ttsHistory.length - 1
|
||||
if (remainingOnCurrentPage === 0 && currentPage > 1) {
|
||||
setCurrentPage(currentPage - 1)
|
||||
}
|
||||
|
||||
// Refresh the full list to ensure accuracy
|
||||
fetchTTSHistory()
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading) {
|
||||
return <TTSLoading />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <TTSError error={error} onRetry={fetchTTSHistory} />
|
||||
}
|
||||
|
||||
if (!ttsHistory || ttsHistory.length === 0) {
|
||||
return <TTSEmpty searchQuery={searchQuery} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<TTSTable
|
||||
ttsHistory={ttsHistory}
|
||||
onTTSDeleted={handleTTSDeleted}
|
||||
/>
|
||||
<AppPagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
totalCount={totalCount}
|
||||
pageSize={pageSize}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
itemName="TTS generations"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
breadcrumb={{
|
||||
items: [{ label: 'Dashboard', href: '/' }, { label: 'Text to Speech' }],
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
||||
<TTSHeader
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
sortBy={sortBy}
|
||||
onSortByChange={setSortBy}
|
||||
sortOrder={sortOrder}
|
||||
onSortOrderChange={setSortOrder}
|
||||
onRefresh={fetchTTSHistory}
|
||||
onCreateClick={() => setShowCreateDialog(true)}
|
||||
loading={loading}
|
||||
error={error}
|
||||
ttsCount={totalCount}
|
||||
/>
|
||||
|
||||
<CreateTTSDialog
|
||||
open={showCreateDialog}
|
||||
onOpenChange={setShowCreateDialog}
|
||||
/>
|
||||
|
||||
{renderContent()}
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user