From 7faf2d38abcf6504e49fe2535620904f8975033d Mon Sep 17 00:00:00 2001 From: JSC Date: Sun, 21 Sep 2025 15:20:12 +0200 Subject: [PATCH] feat: add language selection combobox and update TTS dialog for improved language handling --- src/components/player/Player.tsx | 1 - src/components/tts/CreateTTSDialog.tsx | 56 ++++-------- src/components/tts/TTSList.tsx | 3 +- src/components/tts/TTSTable.tsx | 2 +- src/contexts/SocketContext.tsx | 4 +- src/lib/api/services/tts.ts | 5 ++ src/lib/constants/gtts-languages.ts | 115 +++++++++++++++++++++++++ 7 files changed, 141 insertions(+), 45 deletions(-) create mode 100644 src/lib/constants/gtts-languages.ts diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx index 5fde65b..8d1ceb2 100644 --- a/src/components/player/Player.tsx +++ b/src/components/player/Player.tsx @@ -19,7 +19,6 @@ import { import { soundsService } from '@/lib/api/services/sounds' import { PLAYER_EVENTS, playerEvents } from '@/lib/events' import { cn } from '@/lib/utils' -import { formatDuration } from '@/utils/format-duration' import { ArrowRight, ArrowRightToLine, diff --git a/src/components/tts/CreateTTSDialog.tsx b/src/components/tts/CreateTTSDialog.tsx index 0f02f78..053f320 100644 --- a/src/components/tts/CreateTTSDialog.tsx +++ b/src/components/tts/CreateTTSDialog.tsx @@ -15,6 +15,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' +import { Combobox, type ComboboxOption } from '@/components/ui/combobox' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { Switch } from '@/components/ui/switch' @@ -24,12 +25,12 @@ import { Loader2, Mic } from 'lucide-react' import { ttsService, type TTSProvider } from '@/lib/api/services/tts' import { TTS_EVENTS, ttsEvents } from '@/lib/events' import { toast } from 'sonner' +import { getSortedLanguages, getLanguageDisplayName } from '@/lib/constants/gtts-languages' interface FormData { text: string provider: string language?: string - tld?: string slow?: boolean } @@ -49,10 +50,16 @@ export function CreateTTSDialog({ open, onOpenChange }: CreateTTSDialogProps) { text: '', provider: 'gtts', language: 'en', - tld: 'com', slow: false, }) + // Prepare language options for combobox + const languageOptions: ComboboxOption[] = getSortedLanguages().map((lang) => ({ + value: lang.code, + label: getLanguageDisplayName(lang), + searchValue: `${lang.name} ${lang.region || ''} ${lang.code}`.toLowerCase() + })) + // Load providers when dialog opens useEffect(() => { if (open && !providers) { @@ -74,13 +81,13 @@ export function CreateTTSDialog({ open, onOpenChange }: CreateTTSDialogProps) { const generateTTS = async (data: FormData) => { try { setIsGenerating(true) - const { text, provider, language, tld, slow } = data + const { text, provider, language, slow } = data const response = await ttsService.generateTTS({ text, provider, options: { ...(language && { lang: language }), - ...(tld && { tld }), + tld: 'com', // Always use .com TLD ...(slow !== undefined && { slow }), }, }) @@ -134,7 +141,6 @@ export function CreateTTSDialog({ open, onOpenChange }: CreateTTSDialogProps) { text: '', provider: 'gtts', language: 'en', - tld: 'com', slow: false, }) setFormErrors({}) @@ -220,42 +226,16 @@ export function CreateTTSDialog({ open, onOpenChange }: CreateTTSDialogProps) { <>
- -
- -
- - + options={languageOptions} + placeholder="Select language..." + searchPlaceholder="Search languages..." + emptyMessage="No language found." + />

- Different domains may have different voice characteristics + Choose from {languageOptions.length} supported languages including regional variants

diff --git a/src/components/tts/TTSList.tsx b/src/components/tts/TTSList.tsx index e2a5214..813a994 100644 --- a/src/components/tts/TTSList.tsx +++ b/src/components/tts/TTSList.tsx @@ -2,7 +2,6 @@ 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 { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' @@ -31,7 +30,7 @@ export function TTSList() { setIsLoading(true) setError(null) const data = await ttsService.getTTSHistory({ limit }) - setTTSHistory(data) + setTTSHistory(data.tts) } catch (err) { setError('Failed to load TTS history') console.error('Failed to fetch TTS history:', err) diff --git a/src/components/tts/TTSTable.tsx b/src/components/tts/TTSTable.tsx index cf41ed3..317c607 100644 --- a/src/components/tts/TTSTable.tsx +++ b/src/components/tts/TTSTable.tsx @@ -1,4 +1,4 @@ -import { AlertCircle, Calendar, CheckCircle, Clock, Loader, Mic, Trash2, Volume2, XCircle } from 'lucide-react' +import { Calendar, CheckCircle, Clock, Loader, Mic, Trash2, Volume2, XCircle } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' diff --git a/src/contexts/SocketContext.tsx b/src/contexts/SocketContext.tsx index a20ae59..1b84097 100644 --- a/src/contexts/SocketContext.tsx +++ b/src/contexts/SocketContext.tsx @@ -162,8 +162,6 @@ 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) @@ -173,7 +171,7 @@ export function SocketProvider({ children }: SocketProviderProps) { }) newSocket.on('tts_failed', data => { - const { tts_id, error } = data + const { error } = data // Emit local event for other components to listen to ttsEvents.emit(TTS_EVENTS.TTS_FAILED, data) diff --git a/src/lib/api/services/tts.ts b/src/lib/api/services/tts.ts index 66e7e85..ec80455 100644 --- a/src/lib/api/services/tts.ts +++ b/src/lib/api/services/tts.ts @@ -98,6 +98,11 @@ export const ttsService = { bValue = new Date(bValue as string).getTime() } + // Handle null values + if (aValue === null && bValue === null) return 0 + if (aValue === null) return 1 + if (bValue === null) return -1 + const comparison = aValue > bValue ? 1 : -1 return params.sort_order === 'asc' ? comparison : -comparison }) diff --git a/src/lib/constants/gtts-languages.ts b/src/lib/constants/gtts-languages.ts new file mode 100644 index 0000000..ef30f24 --- /dev/null +++ b/src/lib/constants/gtts-languages.ts @@ -0,0 +1,115 @@ +export interface LanguageOption { + code: string + name: string + region?: string +} + +export const GTTS_LANGUAGES: LanguageOption[] = [ + { code: 'af', name: 'Afrikaans' }, + { code: 'ar', name: 'Arabic' }, + { code: 'bg', name: 'Bulgarian' }, + { code: 'bn', name: 'Bengali' }, + { code: 'bs', name: 'Bosnian' }, + { code: 'ca', name: 'Catalan' }, + { code: 'cs', name: 'Czech' }, + { code: 'cy', name: 'Welsh' }, + { code: 'da', name: 'Danish' }, + { code: 'de', name: 'German' }, + { code: 'el', name: 'Greek' }, + { code: 'en', name: 'English', region: 'United States' }, + { code: 'en-au', name: 'English', region: 'Australia' }, + { code: 'en-ca', name: 'English', region: 'Canada' }, + { code: 'en-gb', name: 'English', region: 'UK' }, + { code: 'en-ie', name: 'English', region: 'Ireland' }, + { code: 'en-in', name: 'English', region: 'India' }, + { code: 'en-ng', name: 'English', region: 'Nigeria' }, + { code: 'en-nz', name: 'English', region: 'New Zealand' }, + { code: 'en-ph', name: 'English', region: 'Philippines' }, + { code: 'en-za', name: 'English', region: 'South Africa' }, + { code: 'en-tz', name: 'English', region: 'Tanzania' }, + { code: 'en-uk', name: 'English', region: 'United Kingdom' }, + { code: 'en-us', name: 'English', region: 'United States' }, + { code: 'eo', name: 'Esperanto' }, + { code: 'es', name: 'Spanish', region: 'Spain' }, + { code: 'es-es', name: 'Spanish', region: 'Spain' }, + { code: 'es-mx', name: 'Spanish', region: 'Mexico' }, + { code: 'es-us', name: 'Spanish', region: 'United States' }, + { code: 'et', name: 'Estonian' }, + { code: 'eu', name: 'Basque' }, + { code: 'fa', name: 'Persian' }, + { code: 'fi', name: 'Finnish' }, + { code: 'fr', name: 'French', region: 'France' }, + { code: 'fr-ca', name: 'French', region: 'Canada' }, + { code: 'fr-fr', name: 'French', region: 'France' }, + { code: 'ga', name: 'Irish' }, + { code: 'gu', name: 'Gujarati' }, + { code: 'he', name: 'Hebrew' }, + { code: 'hi', name: 'Hindi' }, + { code: 'hr', name: 'Croatian' }, + { code: 'hu', name: 'Hungarian' }, + { code: 'hy', name: 'Armenian' }, + { code: 'id', name: 'Indonesian' }, + { code: 'is', name: 'Icelandic' }, + { code: 'it', name: 'Italian' }, + { code: 'ja', name: 'Japanese' }, + { code: 'jw', name: 'Javanese' }, + { code: 'ka', name: 'Georgian' }, + { code: 'kk', name: 'Kazakh' }, + { code: 'km', name: 'Khmer' }, + { code: 'kn', name: 'Kannada' }, + { code: 'ko', name: 'Korean' }, + { code: 'la', name: 'Latin' }, + { code: 'lv', name: 'Latvian' }, + { code: 'mk', name: 'Macedonian' }, + { code: 'ml', name: 'Malayalam' }, + { code: 'mr', name: 'Marathi' }, + { code: 'ms', name: 'Malay' }, + { code: 'mt', name: 'Maltese' }, + { code: 'my', name: 'Myanmar (Burmese)' }, + { code: 'ne', name: 'Nepali' }, + { code: 'nl', name: 'Dutch' }, + { code: 'no', name: 'Norwegian' }, + { code: 'pa', name: 'Punjabi' }, + { code: 'pl', name: 'Polish' }, + { code: 'pt', name: 'Portuguese', region: 'Brazil' }, + { code: 'pt-br', name: 'Portuguese', region: 'Brazil' }, + { code: 'pt-pt', name: 'Portuguese', region: 'Portugal' }, + { code: 'ro', name: 'Romanian' }, + { code: 'ru', name: 'Russian' }, + { code: 'si', name: 'Sinhala' }, + { code: 'sk', name: 'Slovak' }, + { code: 'sl', name: 'Slovenian' }, + { code: 'sq', name: 'Albanian' }, + { code: 'sr', name: 'Serbian' }, + { code: 'su', name: 'Sundanese' }, + { code: 'sv', name: 'Swedish' }, + { code: 'sw', name: 'Swahili' }, + { code: 'ta', name: 'Tamil' }, + { code: 'te', name: 'Telugu' }, + { code: 'th', name: 'Thai' }, + { code: 'tl', name: 'Filipino' }, + { code: 'tr', name: 'Turkish' }, + { code: 'uk', name: 'Ukrainian' }, + { code: 'ur', name: 'Urdu' }, + { code: 'vi', name: 'Vietnamese' }, + { code: 'yo', name: 'Yoruba' }, + { code: 'zh', name: 'Chinese (Mandarin)' }, + { code: 'zh-cn', name: 'Chinese', region: 'China' }, + { code: 'zh-tw', name: 'Chinese', region: 'Taiwan' }, + { code: 'zu', name: 'Zulu' } +] + +export function getLanguageDisplayName(lang: LanguageOption): string { + if (lang.region) { + return `${lang.name} (${lang.region}) - ${lang.code}` + } + return `${lang.name} - ${lang.code}` +} + +export function getSortedLanguages(): LanguageOption[] { + return [...GTTS_LANGUAGES].sort((a, b) => { + const aDisplay = getLanguageDisplayName(a) + const bDisplay = getLanguageDisplayName(b) + return aDisplay.localeCompare(bDisplay) + }) +} \ No newline at end of file