UNPKG

leumas-private-shared

Version:

Private React JSX Package For Leumas Shared Components, Headers, Footers, Asides, Login Pages, API Key Manager and much more. Styles and everything reusable to avoid DRY code across all of our subdomains

275 lines (253 loc) 11.4 kB
import React, { useState, useEffect, useRef } from 'react'; import debounce from 'lodash.debounce'; import { Helmet } from 'react-helmet'; import translateText from './translateText'; import languagesMap from './languagesMap'; import { Container, Typography, Select, MenuItem, Button, Card, CardContent, Grid, IconButton, TextField, Paper, CircularProgress, Snackbar } from '@mui/material'; import VolumeUpIcon from '@mui/icons-material/VolumeUp'; import StopIcon from '@mui/icons-material/Stop'; import InfinityBackgroundComponent from '../Components/Backgrounds/Infinity'; import MuiAlert from '@mui/material/Alert'; const Alert = React.forwardRef((props, ref) => <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />); const ConversationTranslator = () => { const [text, setText] = useState(''); const [translations, setTranslations] = useState([]); const [isListening, setIsListening] = useState(false); const [mode, setMode] = useState('press-to-speak'); // 'press-to-speak' or 'always-listening' const [loading, setLoading] = useState(false); const [openSnackbar, setOpenSnackbar] = useState(false); const [isMyTurn, setIsMyTurn] = useState(true); const [inputLanguage, setInputLanguage] = useState('english'); const [outputLanguage, setOutputLanguage] = useState('spanish'); const recognitionRef = useRef(null); const utteranceRef = useRef(null); useEffect(() => { recognitionRef.current = new window.webkitSpeechRecognition(); recognitionRef.current.lang = languagesMap[inputLanguage.toLowerCase()]; recognitionRef.current.interimResults = true; recognitionRef.current.continuous = mode === 'always-listening'; recognitionRef.current.onresult = (event) => { const current = event.resultIndex; const transcript = event.results[current][0].transcript; setText(transcript); debouncedTranslateText(transcript); }; recognitionRef.current.onerror = (event) => { console.error('Speech recognition error', event.error); setOpenSnackbar(true); }; if (mode === 'always-listening' && isMyTurn) { recognitionRef.current.start(); } return () => { recognitionRef.current.stop(); }; }, [mode, isMyTurn, inputLanguage]); const handlePressToSpeak = () => { if (!isListening) { recognitionRef.current.start(); setIsListening(true); } else { recognitionRef.current.stop(); setIsListening(false); } }; const handleTranslation = async (text) => { setLoading(true); const targetLanguage = isMyTurn ? outputLanguage : inputLanguage; const translatedText = await translateText(text, targetLanguage); setLoading(false); if (translatedText) { setTranslations(prev => [ ...prev, { speaker: isMyTurn ? 'me' : 'them', original: text, translated: translatedText } ]); handleSpeak(translatedText); } }; const debouncedTranslateText = useRef(debounce((text) => { handleTranslation(text); }, 1000)).current; const handleSpeak = (text) => { if (utteranceRef.current) { window.speechSynthesis.cancel(); } utteranceRef.current = new SpeechSynthesisUtterance(text); utteranceRef.current.lang = languagesMap[isMyTurn ? outputLanguage.toLowerCase() : inputLanguage.toLowerCase()]; window.speechSynthesis.speak(utteranceRef.current); }; const handleStopSpeak = () => { if (utteranceRef.current) { window.speechSynthesis.cancel(); utteranceRef.current = null; } }; const handleTextChange = (e) => { const typedText = e.target.value; setText(typedText); debouncedTranslateText(typedText); }; const handleTurnChange = () => { setIsMyTurn(!isMyTurn); if (!isMyTurn) { recognitionRef.current.start(); } else { recognitionRef.current.stop(); } }; const handleCloseSnackbar = () => { setOpenSnackbar(false); }; const metaDescription = `Have a conversation with someone who speaks a different language using our real-time translation tool.`; return ( <> <Helmet> <title>Conversation Translator | Leumas Tech</title> <meta name="description" content={metaDescription} /> <meta name="keywords" content="translate, voice to text, conversation translator, real-time translation, speech recognition" /> <meta name="language" content={languagesMap[inputLanguage.toLowerCase()]} /> <link rel="canonical" href="https://yourwebsite.com/conversation-translator" /> <meta property="og:title" content="Conversation Translator | Leumas Tech" /> <meta property="og:description" content={metaDescription} /> <meta property="og:image" content="https://res.cloudinary.com/dx25lltre/image/upload/v1707175639/Leumas/LEUMAS_Tech_Logo_500x500_bvg8wu.png" /> <meta property="og:url" content="https://yourwebsite.com/conversation-translator" /> <meta property="og:type" content="website" /> <meta property="og:site_name" content="Leumas Tech" /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content="Conversation Translator | Leumas Tech" /> <meta name="twitter:description" content={metaDescription} /> <meta name="twitter:image" content="https://res.cloudinary.com/dx25lltre/image/upload/v1707175639/Leumas/LEUMAS_Tech_Logo_500x500_bvg8wu.png" /> <meta name="twitter:site" content="@LeumasTech" /> <link rel="icon" href="https://res.cloudinary.com/dx25lltre/image/upload/v1707175639/Leumas/LEUMAS_Tech_Logo_500x500_bvg8wu.png" /> </Helmet> <InfinityBackgroundComponent> <Container maxWidth="md" style={{ paddingTop: '20px', paddingBottom: '20px' }}> <Paper elevation={3} style={{ padding: '20px' }}> <Typography variant="h4" gutterBottom style={{ color: '#3f51b5', fontWeight: 'bold' }}>Conversation Translator</Typography> <Grid container spacing={2} alignItems="center"> <Grid item xs={12} sm={6}> <Typography variant="h6" style={{ color: '#3f51b5' }}>Select Mode:</Typography> <Select fullWidth value={mode} onChange={(e) => setMode(e.target.value)} > <MenuItem value="press-to-speak">Press to Speak</MenuItem> <MenuItem value="always-listening">Always Listening</MenuItem> <MenuItem value="type-to-translate">Type to Translate</MenuItem> </Select> </Grid> {mode === 'press-to-speak' && ( <Grid item xs={12} sm={6}> <Button variant="contained" color={isListening ? 'secondary' : 'primary'} onClick={handlePressToSpeak} style={{ marginTop: '24px' }} > {isListening ? 'Stop Listening' : 'Press to Speak'} </Button> </Grid> )} {mode === 'type-to-translate' && ( <Grid item xs={12} sm={6}> <TextField fullWidth label="Type text to translate" variant="outlined" value={text} onChange={handleTextChange} style={{ marginTop: '16px' }} /> </Grid> )} <Grid item xs={12} sm={6}> <Typography variant="h6" style={{ color: '#3f51b5' }}>Input Language:</Typography> <Select fullWidth value={inputLanguage} onChange={(e) => setInputLanguage(e.target.value)} > {Object.keys(languagesMap).map(lang => ( <MenuItem key={lang} value={lang}>{lang.charAt(0).toUpperCase() + lang.slice(1)}</MenuItem> ))} </Select> </Grid> <Grid item xs={12} sm={6}> <Typography variant="h6" style={{ color: '#3f51b5' }}>Output Language:</Typography> <Select fullWidth value={outputLanguage} onChange={(e) => setOutputLanguage(e.target.value)} > {Object.keys(languagesMap).map(lang => ( <MenuItem key={lang} value={lang}>{lang.charAt(0).toUpperCase() + lang.slice(1)}</MenuItem> ))} </Select> </Grid> <Grid item xs={12} sm={6}> <Button variant="contained" color="secondary" onClick={handleStopSpeak} style={{ marginTop: '24px' }} > <StopIcon /> Stop Speaking </Button> </Grid> <Grid item xs={12} sm={6}> <Button variant="contained" color="primary" onClick={handleTurnChange} style={{ marginTop: '24px' }} > {isMyTurn ? 'Their Turn to Speak' : 'My Turn to Speak'} </Button> </Grid> </Grid> {loading && <CircularProgress style={{ display: 'block', margin: '20px auto' }} />} <Typography variant="h6" gutterBottom style={{ marginTop: '20px', color: '#3f51b5' }}>Conversation</Typography> <div style={{ maxHeight: '400px', overflowY: 'auto', border: '1px solid #3f51b5', borderRadius: '4px', padding: '10px', backgroundColor: '#f5f5f5' }}> {translations.map((translation, index) => ( <Card key={index} style={{ marginBottom: '10px' }}> <CardContent> <Typography variant="body1"> <strong>{translation.speaker === 'me' ? 'Me' : 'Them'}:</strong> {translation.original} </Typography> <Typography variant="body1"> <strong>Translation ({translation.speaker === 'me' ? outputLanguage.charAt(0).toUpperCase() + outputLanguage.slice(1) : inputLanguage.charAt(0).toUpperCase() + inputLanguage.slice(1)}):</strong> {translation.translated} <IconButton onClick={() => handleSpeak(translation.translated)} aria-label="speak"> <VolumeUpIcon /> </IconButton> </Typography> </CardContent> </Card> ))} </div> </Paper> </Container> <Snackbar open={openSnackbar} autoHideDuration={6000} onClose={handleCloseSnackbar}> <Alert onClose={handleCloseSnackbar} severity="error"> Speech recognition error occurred. </Alert> </Snackbar> </InfinityBackgroundComponent> </> ); }; export default ConversationTranslator;