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
JSX
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;