first commit
This commit is contained in:
236
static/css/style.css
Normal file
236
static/css/style.css
Normal file
@@ -0,0 +1,236 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #7f8c8d;
|
||||
margin-bottom: 30px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.translation-panel {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.language-selector {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.select-container label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 0.9rem;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background-color: #f9f9f9;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#swapLanguages {
|
||||
background-color: #e8f4fc;
|
||||
border: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
#swapLanguages:hover {
|
||||
background-color: #d1e9f9;
|
||||
}
|
||||
|
||||
.text-panels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.text-panel {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
resize: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 15px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: background-color 0.3s, transform 0.1s;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.record-button, .speak-button {
|
||||
background-color: #e8f4fc;
|
||||
color: #3498db;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.record-button:hover, .speak-button:hover {
|
||||
background-color: #d1e9f9;
|
||||
}
|
||||
|
||||
.record-button.recording {
|
||||
background-color: #ffe9e9;
|
||||
color: #e74c3c;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.clear-button, .copy-button {
|
||||
background-color: #f5f5f5;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.clear-button:hover, .copy-button:hover {
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.primary-button:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
text-align: center;
|
||||
min-height: 24px;
|
||||
color: #7f8c8d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.status-message.error {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.status-message.success {
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
/* Animation for recording */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Media queries for responsiveness */
|
||||
@media (min-width: 768px) {
|
||||
.text-panels {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.translation-panel {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.record-button, .speak-button, .clear-button, .copy-button {
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
255
static/js/main.js
Normal file
255
static/js/main.js
Normal file
@@ -0,0 +1,255 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// DOM elements
|
||||
const sourceLanguage = document.getElementById('sourceLanguage');
|
||||
const targetLanguage = document.getElementById('targetLanguage');
|
||||
const swapButton = document.getElementById('swapLanguages');
|
||||
const sourceText = document.getElementById('sourceText');
|
||||
const translatedText = document.getElementById('translatedText');
|
||||
const recordSourceButton = document.getElementById('recordSource');
|
||||
const speakButton = document.getElementById('speak');
|
||||
const clearSourceButton = document.getElementById('clearSource');
|
||||
const copyTranslationButton = document.getElementById('copyTranslation');
|
||||
const translateButton = document.getElementById('translateButton');
|
||||
const statusMessage = document.getElementById('status');
|
||||
|
||||
// Audio recording variables
|
||||
let mediaRecorder;
|
||||
let audioChunks = [];
|
||||
let isRecording = false;
|
||||
|
||||
// Speech recognition setup
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
const recognition = SpeechRecognition ? new SpeechRecognition() : null;
|
||||
|
||||
if (recognition) {
|
||||
recognition.continuous = false;
|
||||
recognition.interimResults = false;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
swapButton.addEventListener('click', swapLanguages);
|
||||
translateButton.addEventListener('click', translateText);
|
||||
clearSourceButton.addEventListener('click', clearSource);
|
||||
copyTranslationButton.addEventListener('click', copyTranslation);
|
||||
|
||||
if (recognition) {
|
||||
recordSourceButton.addEventListener('click', toggleRecording);
|
||||
} else {
|
||||
recordSourceButton.textContent = "Speech API not supported";
|
||||
recordSourceButton.disabled = true;
|
||||
}
|
||||
|
||||
speakButton.addEventListener('click', speakTranslation);
|
||||
|
||||
// Functions (continued)
|
||||
function swapLanguages() {
|
||||
const tempLang = sourceLanguage.value;
|
||||
sourceLanguage.value = targetLanguage.value;
|
||||
targetLanguage.value = tempLang;
|
||||
|
||||
// Also swap the text if both fields have content
|
||||
if (sourceText.value && translatedText.value) {
|
||||
const tempText = sourceText.value;
|
||||
sourceText.value = translatedText.value;
|
||||
translatedText.value = tempText;
|
||||
}
|
||||
}
|
||||
|
||||
function clearSource() {
|
||||
sourceText.value = '';
|
||||
updateStatus('');
|
||||
}
|
||||
|
||||
function copyTranslation() {
|
||||
if (!translatedText.value) {
|
||||
updateStatus('Nothing to copy', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(translatedText.value)
|
||||
.then(() => {
|
||||
updateStatus('Copied to clipboard!', 'success');
|
||||
setTimeout(() => updateStatus(''), 2000);
|
||||
})
|
||||
.catch(err => {
|
||||
updateStatus('Failed to copy: ' + err, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
async function translateText() {
|
||||
const source = sourceText.value.trim();
|
||||
if (!source) {
|
||||
updateStatus('Please enter or speak some text to translate', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus('Translating...');
|
||||
translatedText.value = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/translate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sourceLanguage: sourceLanguage.value,
|
||||
targetLanguage: targetLanguage.value,
|
||||
text: source
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
translatedText.value = data.translation;
|
||||
updateStatus('Translation complete', 'success');
|
||||
setTimeout(() => updateStatus(''), 2000);
|
||||
} else {
|
||||
updateStatus(data.error || 'Translation failed', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
updateStatus('Network error: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRecording() {
|
||||
if (!recognition) {
|
||||
updateStatus('Speech recognition not supported in this browser', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRecording) {
|
||||
stopRecording();
|
||||
} else {
|
||||
startRecording();
|
||||
}
|
||||
}
|
||||
|
||||
function startRecording() {
|
||||
sourceText.value = '';
|
||||
updateStatus('Listening...');
|
||||
|
||||
recognition.lang = getLanguageCode(sourceLanguage.value);
|
||||
recognition.onresult = function(event) {
|
||||
const transcript = event.results[0][0].transcript;
|
||||
sourceText.value = transcript;
|
||||
updateStatus('Recording completed', 'success');
|
||||
setTimeout(() => updateStatus(''), 2000);
|
||||
};
|
||||
|
||||
recognition.onerror = function(event) {
|
||||
updateStatus('Error in speech recognition: ' + event.error, 'error');
|
||||
stopRecording();
|
||||
};
|
||||
|
||||
recognition.onend = function() {
|
||||
stopRecording();
|
||||
};
|
||||
|
||||
try {
|
||||
recognition.start();
|
||||
isRecording = true;
|
||||
recordSourceButton.classList.add('recording');
|
||||
recordSourceButton.querySelector('.button-text').textContent = 'Stop';
|
||||
} catch (error) {
|
||||
updateStatus('Failed to start recording: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function stopRecording() {
|
||||
if (isRecording) {
|
||||
try {
|
||||
recognition.stop();
|
||||
} catch (error) {
|
||||
console.error('Error stopping recognition:', error);
|
||||
}
|
||||
|
||||
isRecording = false;
|
||||
recordSourceButton.classList.remove('recording');
|
||||
recordSourceButton.querySelector('.button-text').textContent = 'Record';
|
||||
}
|
||||
}
|
||||
|
||||
function speakTranslation() {
|
||||
const text = translatedText.value.trim();
|
||||
if (!text) {
|
||||
updateStatus('No translation to speak', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the browser's speech synthesis API
|
||||
const speech = new SpeechSynthesisUtterance(text);
|
||||
speech.lang = getLanguageCode(targetLanguage.value);
|
||||
speech.volume = 1;
|
||||
speech.rate = 1;
|
||||
speech.pitch = 1;
|
||||
|
||||
speech.onstart = function() {
|
||||
updateStatus('Speaking...');
|
||||
speakButton.disabled = true;
|
||||
};
|
||||
|
||||
speech.onend = function() {
|
||||
updateStatus('');
|
||||
speakButton.disabled = false;
|
||||
};
|
||||
|
||||
speech.onerror = function(event) {
|
||||
updateStatus('Speech synthesis error: ' + event.error, 'error');
|
||||
speakButton.disabled = false;
|
||||
};
|
||||
|
||||
window.speechSynthesis.speak(speech);
|
||||
}
|
||||
|
||||
function getLanguageCode(language) {
|
||||
// Map language names to BCP 47 language tags for speech recognition/synthesis
|
||||
const languageMap = {
|
||||
"arabic": "ar-SA",
|
||||
"armenian": "hy-AM",
|
||||
"azerbaijani": "az-AZ",
|
||||
"english": "en-US",
|
||||
"french": "fr-FR",
|
||||
"georgian": "ka-GE",
|
||||
"kazakh": "kk-KZ",
|
||||
"mandarin": "zh-CN",
|
||||
"persian": "fa-IR",
|
||||
"portuguese": "pt-PT",
|
||||
"russian": "ru-RU",
|
||||
"turkish": "tr-TR",
|
||||
"uzbek": "uz-UZ"
|
||||
};
|
||||
|
||||
return languageMap[language] || 'en-US';
|
||||
}
|
||||
|
||||
function updateStatus(message, type = '') {
|
||||
statusMessage.textContent = message;
|
||||
statusMessage.className = 'status-message';
|
||||
|
||||
if (type) {
|
||||
statusMessage.classList.add(type);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for microphone and speech support when page loads
|
||||
function checkSupportedFeatures() {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
updateStatus('Microphone access is not supported in this browser', 'error');
|
||||
recordSourceButton.disabled = true;
|
||||
}
|
||||
|
||||
if (!window.SpeechRecognition && !window.webkitSpeechRecognition) {
|
||||
updateStatus('Speech recognition is not supported in this browser', 'error');
|
||||
recordSourceButton.disabled = true;
|
||||
}
|
||||
|
||||
if (!window.speechSynthesis) {
|
||||
updateStatus('Speech synthesis is not supported in this browser', 'error');
|
||||
speakButton.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
checkSupportedFeatures();
|
||||
});
|
Reference in New Issue
Block a user