feat: implement TTS event handling for creation, completion, and failure in TTSPage and SocketContext

This commit is contained in:
JSC
2025-09-21 14:39:15 +02:00
parent 75b52caf85
commit 92846c6d3a
6 changed files with 107 additions and 29 deletions

View File

@@ -22,6 +22,7 @@ import { Badge } from '@/components/ui/badge'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Loader2, Mic } from 'lucide-react' import { Loader2, Mic } from 'lucide-react'
import { ttsService, type TTSProvider } from '@/lib/api/services/tts' import { ttsService, type TTSProvider } from '@/lib/api/services/tts'
import { TTS_EVENTS, ttsEvents } from '@/lib/events'
import { toast } from 'sonner' import { toast } from 'sonner'
interface FormData { interface FormData {
@@ -88,8 +89,8 @@ export function CreateTTSDialog({ open, onOpenChange }: CreateTTSDialogProps) {
onOpenChange(false) onOpenChange(false)
handleReset() handleReset()
// Trigger refresh of parent list if needed // Emit event for new TTS created
window.dispatchEvent(new CustomEvent('tts-generated')) ttsEvents.emit(TTS_EVENTS.TTS_CREATED, response.tts)
} catch (error: any) { } catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to generate TTS') toast.error(error.response?.data?.detail || 'Failed to generate TTS')
} finally { } finally {

View File

@@ -1,4 +1,4 @@
import { Calendar, CheckCircle, Loader, Mic, Trash2, Volume2 } from 'lucide-react' import { AlertCircle, Calendar, CheckCircle, Clock, Loader, Mic, Trash2, Volume2, XCircle } from 'lucide-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'
@@ -57,22 +57,36 @@ export function TTSTable({ ttsHistory, onTTSDeleted }: TTSTableProps) {
} }
const getStatusBadge = (tts: TTSResponse) => { const getStatusBadge = (tts: TTSResponse) => {
const isCompleted = tts.sound_id !== null switch (tts.status) {
case 'completed':
if (isCompleted) { return (
return ( <Badge variant="secondary" className="gap-1">
<Badge variant="secondary" className="gap-1"> <CheckCircle className="h-3 w-3" />
<CheckCircle className="h-3 w-3" /> Completed
Complete </Badge>
</Badge> )
) case 'processing':
} else { return (
return ( <Badge variant="outline" className="gap-1">
<Badge variant="outline" className="gap-1"> <Loader className="h-3 w-3 animate-spin" />
<Loader className="h-3 w-3 animate-spin" /> Processing
Processing </Badge>
</Badge> )
) case 'failed':
return (
<Badge variant="destructive" className="gap-1">
<XCircle className="h-3 w-3" />
Failed
</Badge>
)
case 'pending':
default:
return (
<Badge variant="outline" className="gap-1">
<Clock className="h-3 w-3" />
Pending
</Badge>
)
} }
} }
@@ -83,8 +97,8 @@ export function TTSTable({ ttsHistory, onTTSDeleted }: TTSTableProps) {
<TableRow> <TableRow>
<TableHead>Text</TableHead> <TableHead>Text</TableHead>
<TableHead>Provider</TableHead> <TableHead>Provider</TableHead>
<TableHead>Status</TableHead>
<TableHead>Options</TableHead> <TableHead>Options</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead> <TableHead>Created</TableHead>
<TableHead className="w-[120px]">Actions</TableHead> <TableHead className="w-[120px]">Actions</TableHead>
</TableRow> </TableRow>
@@ -103,9 +117,6 @@ export function TTSTable({ ttsHistory, onTTSDeleted }: TTSTableProps) {
{tts.provider.toUpperCase()} {tts.provider.toUpperCase()}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell>
{getStatusBadge(tts)}
</TableCell>
<TableCell> <TableCell>
{Object.keys(tts.options).length > 0 ? ( {Object.keys(tts.options).length > 0 ? (
<div className="flex gap-1 flex-wrap"> <div className="flex gap-1 flex-wrap">
@@ -119,6 +130,19 @@ export function TTSTable({ ttsHistory, onTTSDeleted }: TTSTableProps) {
<span className="text-muted-foreground text-sm">None</span> <span className="text-muted-foreground text-sm">None</span>
)} )}
</TableCell> </TableCell>
<TableCell>
<div className="space-y-1">
{getStatusBadge(tts)}
{tts.error && (
<div
className="text-xs text-destructive max-w-48 truncate"
title={tts.error}
>
{tts.error}
</div>
)}
</div>
</TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-1 text-sm text-muted-foreground"> <div className="flex items-center gap-1 text-sm text-muted-foreground">
<Calendar className="h-3 w-3" /> <Calendar className="h-3 w-3" />

View File

@@ -12,11 +12,13 @@ import {
EXTRACTION_EVENTS, EXTRACTION_EVENTS,
PLAYER_EVENTS, PLAYER_EVENTS,
SOUND_EVENTS, SOUND_EVENTS,
TTS_EVENTS,
USER_EVENTS, USER_EVENTS,
authEvents, authEvents,
extractionEvents, extractionEvents,
playerEvents, playerEvents,
soundEvents, soundEvents,
ttsEvents,
userEvents, userEvents,
} from '../lib/events' } from '../lib/events'
import { extractionsService } from '../lib/api/services/extractions' import { extractionsService } from '../lib/api/services/extractions'
@@ -158,6 +160,30 @@ export function SocketProvider({ children }: SocketProviderProps) {
} }
}) })
// Listen for TTS status updates
newSocket.on('tts_completed', data => {
const { tts_id, sound_id } = data
// Emit local event for other components to listen to
ttsEvents.emit(TTS_EVENTS.TTS_COMPLETED, data)
toast.success('TTS generation completed', {
duration: 3000,
})
})
newSocket.on('tts_failed', data => {
const { tts_id, error } = data
// Emit local event for other components to listen to
ttsEvents.emit(TTS_EVENTS.TTS_FAILED, data)
toast.error('TTS generation failed', {
description: error,
duration: 5000,
})
})
return newSocket return newSocket
}, [user, fetchAndShowOngoingExtractions]) }, [user, fetchAndShowOngoingExtractions])

View File

@@ -11,6 +11,8 @@ export interface TTSResponse {
text: string text: string
provider: string provider: string
options: Record<string, any> options: Record<string, any>
status: string
error: string | null
sound_id: number | null sound_id: number | null
user_id: number user_id: number
created_at: string created_at: string

View File

@@ -37,6 +37,7 @@ export const playerEvents = new EventEmitter()
export const soundEvents = new EventEmitter() export const soundEvents = new EventEmitter()
export const userEvents = new EventEmitter() export const userEvents = new EventEmitter()
export const extractionEvents = new EventEmitter() export const extractionEvents = new EventEmitter()
export const ttsEvents = new EventEmitter()
// Auth event types // Auth event types
export const AUTH_EVENTS = { export const AUTH_EVENTS = {
@@ -69,3 +70,11 @@ export const EXTRACTION_EVENTS = {
EXTRACTION_COMPLETED: 'extraction_completed', EXTRACTION_COMPLETED: 'extraction_completed',
EXTRACTION_FAILED: 'extraction_failed', EXTRACTION_FAILED: 'extraction_failed',
} as const } as const
// TTS event types
export const TTS_EVENTS = {
TTS_STATUS_UPDATED: 'tts_status_updated',
TTS_CREATED: 'tts_created',
TTS_COMPLETED: 'tts_completed',
TTS_FAILED: 'tts_failed',
} as const

View File

@@ -14,6 +14,7 @@ import {
type TTSSortOrder, type TTSSortOrder,
ttsService, ttsService,
} from '@/lib/api/services/tts' } from '@/lib/api/services/tts'
import { TTS_EVENTS, ttsEvents } from '@/lib/events'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
@@ -82,15 +83,30 @@ export function TTSPage() {
} }
}, [debouncedSearchQuery, sortBy, sortOrder, pageSize]) }, [debouncedSearchQuery, sortBy, sortOrder, pageSize])
// Listen for TTS generation events to refresh the list // Listen for TTS events to refresh the list
useEffect(() => { useEffect(() => {
const handleTTSGenerated = () => { const handleTTSCompleted = () => {
fetchTTSHistory() fetchTTSHistory()
} }
window.addEventListener('tts-generated', handleTTSGenerated) const handleTTSFailed = () => {
fetchTTSHistory()
}
const handleTTSCreated = () => {
fetchTTSHistory()
}
// Subscribe to TTS events
ttsEvents.on(TTS_EVENTS.TTS_COMPLETED, handleTTSCompleted)
ttsEvents.on(TTS_EVENTS.TTS_FAILED, handleTTSFailed)
ttsEvents.on(TTS_EVENTS.TTS_CREATED, handleTTSCreated)
return () => { return () => {
window.removeEventListener('tts-generated', handleTTSGenerated) // Cleanup event listeners
ttsEvents.off(TTS_EVENTS.TTS_COMPLETED, handleTTSCompleted)
ttsEvents.off(TTS_EVENTS.TTS_FAILED, handleTTSFailed)
ttsEvents.off(TTS_EVENTS.TTS_CREATED, handleTTSCreated)
} }
}, [fetchTTSHistory]) }, [fetchTTSHistory])