diff --git a/app/page.tsx b/app/page.tsx index 02eb7fd..cb7909e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,17 @@ 'use client' import { useState, useEffect, useCallback, useRef } from 'react' -import { Mic, Volume2, VolumeX, Loader2, Trash2, AlertCircle, Play, Pause } from 'lucide-react' +import { + Mic, + Volume2, + VolumeX, + Loader2, + Trash2, + AlertCircle, + Play, + Pause, + Globe, +} from 'lucide-react' import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognition' import { useToast } from '@/hooks/use-toast' import { Header } from '@/components/header' @@ -9,6 +19,14 @@ import { Footer } from '@/components/footer' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Alert, AlertDescription } from '@/components/ui/alert' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import dynamic from 'next/dynamic' /** * ============================================================================= @@ -43,6 +61,40 @@ interface DebugState { apiCallCount: number } +/** Language configuration for speech recognition and synthesis */ +interface Language { + code: string + name: string + speechCode: string + flag: string +} + +/** Supported languages for speech recognition and synthesis */ +const SUPPORTED_LANGUAGES: Language[] = [ + { code: 'en-US', name: 'English (US)', speechCode: 'en-US', flag: '๐Ÿ‡บ๐Ÿ‡ธ' }, + { code: 'en-GB', name: 'English (UK)', speechCode: 'en-GB', flag: '๐Ÿ‡ฌ๐Ÿ‡ง' }, + { code: 'es-ES', name: 'Spanish (Spain)', speechCode: 'es-ES', flag: '๐Ÿ‡ช๐Ÿ‡ธ' }, + { code: 'es-MX', name: 'Spanish (Mexico)', speechCode: 'es-MX', flag: '๐Ÿ‡ฒ๐Ÿ‡ฝ' }, + { code: 'fr-FR', name: 'French (France)', speechCode: 'fr-FR', flag: '๐Ÿ‡ซ๐Ÿ‡ท' }, + { code: 'de-DE', name: 'German (Germany)', speechCode: 'de-DE', flag: '๐Ÿ‡ฉ๐Ÿ‡ช' }, + { code: 'it-IT', name: 'Italian (Italy)', speechCode: 'it-IT', flag: '๐Ÿ‡ฎ๐Ÿ‡น' }, + { code: 'pt-BR', name: 'Portuguese (Brazil)', speechCode: 'pt-BR', flag: '๐Ÿ‡ง๐Ÿ‡ท' }, + { code: 'pt-PT', name: 'Portuguese (Portugal)', speechCode: 'pt-PT', flag: '๐Ÿ‡ต๐Ÿ‡น' }, + { code: 'ru-RU', name: 'Russian (Russia)', speechCode: 'ru-RU', flag: '๐Ÿ‡ท๐Ÿ‡บ' }, + { code: 'ja-JP', name: 'Japanese (Japan)', speechCode: 'ja-JP', flag: '๐Ÿ‡ฏ๐Ÿ‡ต' }, + { code: 'ko-KR', name: 'Korean (South Korea)', speechCode: 'ko-KR', flag: '๐Ÿ‡ฐ๐Ÿ‡ท' }, + { code: 'zh-CN', name: 'Chinese (Simplified)', speechCode: 'zh-CN', flag: '๐Ÿ‡จ๐Ÿ‡ณ' }, + { code: 'zh-TW', name: 'Chinese (Traditional)', speechCode: 'zh-TW', flag: '๐Ÿ‡น๐Ÿ‡ผ' }, + { code: 'hi-IN', name: 'Hindi (India)', speechCode: 'hi-IN', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, + { code: 'bn-IN', name: 'Bengali (India)', speechCode: 'bn-IN', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, + // { code: 'bn-BD', name: 'Bengali (Bangladesh)', speechCode: 'bn-BD', flag: '๐Ÿ‡ง๐Ÿ‡ฉ' }, + { code: 'ar-SA', name: 'Arabic (Saudi Arabia)', speechCode: 'ar-SA', flag: '๐Ÿ‡ธ๐Ÿ‡ฆ' }, + { code: 'nl-NL', name: 'Dutch (Netherlands)', speechCode: 'nl-NL', flag: '๐Ÿ‡ณ๐Ÿ‡ฑ' }, + { code: 'sv-SE', name: 'Swedish (Sweden)', speechCode: 'sv-SE', flag: '๐Ÿ‡ธ๐Ÿ‡ช' }, + { code: 'da-DK', name: 'Danish (Denmark)', speechCode: 'da-DK', flag: '๐Ÿ‡ฉ๐Ÿ‡ฐ' }, + { code: 'no-NO', name: 'Norwegian (Norway)', speechCode: 'no-NO', flag: '๐Ÿ‡ณ๐Ÿ‡ด' }, +] + /** * ============================================================================= * MAIN COMPONENT @@ -56,7 +108,7 @@ interface DebugState { * - Real-time debugging and monitoring * - Automatic session management */ -export default function WebSpeechPage() { +function WebSpeechPageComponent() { // ============================================================================= // STATE MANAGEMENT // ============================================================================= @@ -83,6 +135,12 @@ export default function WebSpeechPage() { /** Error state for user feedback */ const [error, setError] = useState(null) + /** Selected language for speech recognition and synthesis */ + const [selectedLanguage, setSelectedLanguage] = useState(SUPPORTED_LANGUAGES[0]) + + /** Available voices for text-to-speech */ + const [availableVoices, setAvailableVoices] = useState([]) + // ============================================================================= // REFS FOR MANAGING ASYNC OPERATIONS // ============================================================================= @@ -212,7 +270,38 @@ export default function WebSpeechPage() { // ============================================================================= /** - * Convert text to speech using Web Speech API + * Find the best voice for the selected language + */ + const findBestVoice = useCallback( + (languageCode: string): SpeechSynthesisVoice | null => { + if (availableVoices.length === 0) return null + + // Try to find exact match first + let voice = availableVoices.find((v) => v.lang === languageCode) + + // If no exact match, try language without region (e.g., 'en' from 'en-US') + if (!voice) { + const baseLanguage = languageCode.split('-')[0] + voice = availableVoices.find((v) => v.lang.startsWith(baseLanguage)) + } + + // Prefer local voices over remote ones + if (voice && !voice.localService) { + const localVoice = availableVoices.find( + (v) => + (v.lang === languageCode || v.lang.startsWith(languageCode.split('-')[0])) && + v.localService + ) + if (localVoice) voice = localVoice + } + + return voice || null + }, + [availableVoices] + ) + + /** + * Convert text to speech using Web Speech API with language support * Automatically stops any currently playing speech * Provides user feedback through state updates and logging */ @@ -226,11 +315,23 @@ export default function WebSpeechPage() { // Stop any current speech to prevent overlap speechSynthesis.cancel() - addDebugLog('๐Ÿ”Š Starting text-to-speech') + addDebugLog(`๐Ÿ”Š Starting text-to-speech in ${selectedLanguage.name}`) // Create speech utterance const utterance = new SpeechSynthesisUtterance(text) + // Find and set the best voice for the selected language + const bestVoice = findBestVoice(selectedLanguage.speechCode) + if (bestVoice) { + utterance.voice = bestVoice + addDebugLog(`๐ŸŽค Using voice: ${bestVoice.name} (${bestVoice.lang})`) + } else { + addDebugLog(`โš ๏ธ No voice found for ${selectedLanguage.name}, using default`) + } + + // Set language + utterance.lang = selectedLanguage.speechCode + // Configure voice settings for better user experience utterance.rate = 0.9 // Slightly slower for clarity utterance.pitch = 1.0 // Natural pitch @@ -256,7 +357,7 @@ export default function WebSpeechPage() { // Start speaking speechSynthesis.speak(utterance) }, - [addDebugLog, updateSpeechState] + [addDebugLog, updateSpeechState, selectedLanguage, findBestVoice] ) /** @@ -409,9 +510,13 @@ export default function WebSpeechPage() { // Start speech recognition with proper configuration SpeechRecognition.startListening({ continuous: true, - language: 'en-US', + language: selectedLanguage.speechCode, }) + addDebugLog( + `๐ŸŒ Speech recognition started in ${selectedLanguage.name} (${selectedLanguage.speechCode})` + ) + // Start recording timer with auto-stop at maximum time intervalRef.current = setInterval(() => { setSpeechState((prev) => { @@ -448,6 +553,7 @@ export default function WebSpeechPage() { resetAllState, listening, speechState.hasProcessedCurrentSession, + selectedLanguage, ]) /** @@ -516,6 +622,19 @@ export default function WebSpeechPage() { addDebugLog('โœ… Microphone is available') } + // Load available voices for text-to-speech + const loadVoices = () => { + const voices = speechSynthesis.getVoices() + setAvailableVoices(voices) + addDebugLog(`๐ŸŽค Loaded ${voices.length} available voices`) + } + + // Load voices immediately if available + loadVoices() + + // Some browsers load voices asynchronously + speechSynthesis.onvoiceschanged = loadVoices + // Cleanup function to clear all timers on unmount return () => { addDebugLog('๐Ÿงน Component unmounting - cleaning up timers') @@ -706,14 +825,77 @@ export default function WebSpeechPage() { ) } + /** + * Language Selector Component + * Allows users to select the language for speech recognition and synthesis + */ + const LanguageSelector = () => ( +
+ + +
+ ) + /** * Control Buttons Component * Main action buttons with enhanced recording controls */ const ControlButtons = () => (
- {/* Main Start/Stop Controls */} + {/* Language Selector */}
+ + + {/* Reset Button */} + +
+ + {/* Main Start/Stop Controls */} +
{!listening ? ( ) : (
@@ -744,18 +926,6 @@ export default function WebSpeechPage() {
)} - - {/* Reset Button */} -
{/* Recording Duration Warning */} @@ -864,7 +1034,8 @@ export default function WebSpeechPage() { Voice Recording - Session #{speechState.sessionCount} โ€ข + Session #{speechState.sessionCount} โ€ข Language: {selectedLanguage.flag}{' '} + {selectedLanguage.name} โ€ข {speechState.isProcessingAI ? 'Processing...' : listening @@ -883,7 +1054,8 @@ export default function WebSpeechPage() { Real-time Transcription - Speech-to-text conversion โ€ข Automatically processes with AI when complete + Speech-to-text conversion in {selectedLanguage.flag} {selectedLanguage.name} โ€ข + Automatically processes with AI when complete @@ -903,7 +1075,8 @@ export default function WebSpeechPage() {
AI Response - OpenAI processes your speech and provides intelligent responses + OpenAI processes your speech and provides intelligent responses โ€ข Text-to-speech + in {selectedLanguage.flag} {selectedLanguage.name}
@@ -978,10 +1151,15 @@ export default function WebSpeechPage() {
  1. - Start: Click "Start Listening" to begin speech recognition + Language: Select your preferred language from the dropdown (20+ + languages supported)
  2. - Speak: Talk clearly into your microphone + Start: Click "Start Listening" to begin speech recognition in your + selected language +
  3. +
  4. + Speak: Talk clearly into your microphone in the selected language
  5. Auto-stop: Recognition stops after 3 seconds of silence @@ -990,13 +1168,19 @@ export default function WebSpeechPage() { Processing: Your speech is automatically sent to AI for processing
  6. - Response: Listen to the AI's spoken response or read the text + Response: Listen to the AI's spoken response in your selected + language or read the text
  7. - Reset: Use "Reset" to clear everything and start over + Language Switch: Change language anytime (current session will stop + automatically)
  8. - Debug: Monitor the debug console for troubleshooting + Reset: Use "Clear All" to reset everything and start over +
  9. +
  10. + Debug: Monitor the debug console for troubleshooting and voice + information
@@ -1006,3 +1190,21 @@ export default function WebSpeechPage() {
) } + +// Export as dynamic component to prevent SSR hydration errors +const WebSpeechPage = dynamic(() => Promise.resolve(WebSpeechPageComponent), { + ssr: false, + loading: () => ( +
+
+
+
+ +
+
+
+ ), +}) + +export default WebSpeechPage