Spaces:
Sleeping
Sleeping
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Translator</title> | |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> | |
<style> | |
:root { | |
--primary-color: #F26522; /* Orange */ | |
--primary-hover: #E05B1F; /* Slightly darker orange for hover */ | |
--bg-color: #FFFFFF; /* White */ | |
--text-color: #78797D; /* Grey */ | |
--border-color: #E5E7EB; /* Light grey for borders */ | |
--success-color: #F26522; /* Use primary color for success */ | |
--error-color: #DC2626; /* Red for errors */ | |
--container-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
} | |
[data-theme='dark'] { | |
--bg-color: #1A1A1A; | |
--text-color: #E5E5E5; | |
--border-color: #333333; | |
--container-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2), 0 2px 4px -1px rgba(0, 0, 0, 0.1); | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
transition: background-color 0.3s ease, color 0.3s ease; | |
} | |
body { | |
font-family: 'Inter', sans-serif; | |
background-color: var(--bg-color); | |
color: var(--text-color); | |
line-height: 1.5; | |
min-height: 100vh; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
padding: 20px; | |
} | |
.container { | |
background-color: var(--bg-color); | |
padding: 2.5rem; | |
border-radius: 1.5rem; | |
box-shadow: var(--container-shadow); | |
width: 100%; | |
max-width: 800px; | |
transition: all 0.3s ease; | |
} | |
.container:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 6px 8px -1px rgba(0, 0, 0, 0.1), 0 4px 6px -1px rgba(0, 0, 0, 0.06); | |
} | |
h1 { | |
font-size: 2.25rem; | |
font-weight: 600; | |
color: var(--text-color); | |
margin-bottom: 2rem; | |
text-align: center; | |
letter-spacing: -0.5px; | |
} | |
.form-group { | |
margin-bottom: 2rem; | |
} | |
.input-container, .output-container { | |
position: relative; | |
margin-bottom: 1.5rem; | |
} | |
.label { | |
display: block; | |
font-size: 0.95rem; | |
font-weight: 500; | |
color: var(--text-color); | |
margin-bottom: 0.75rem; | |
letter-spacing: 0.2px; | |
} | |
textarea { | |
width: 100%; | |
padding: 1rem; | |
border: 2px solid var(--border-color); | |
border-radius: 0.75rem; | |
font-size: 1rem; | |
font-family: inherit; | |
min-height: 120px; | |
resize: vertical; | |
transition: all 0.2s ease; | |
background-color: var(--bg-color); | |
color: var(--text-color); | |
} | |
textarea:focus { | |
outline: none; | |
border-color: var(--primary-color); | |
box-shadow: 0 0 0 3px rgba(242, 101, 34, 0.1); | |
} | |
.theme-toggle { | |
position: fixed; | |
top: 20px; | |
right: 20px; | |
padding: 12px 24px; | |
background-color: var(--primary-color); | |
color: white; | |
border: none; | |
border-radius: 8px; | |
cursor: pointer; | |
font-size: 0.95rem; | |
font-weight: 500; | |
transition: all 0.3s ease; | |
z-index: 1000; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
} | |
.theme-toggle:hover { | |
background-color: var(--primary-hover); | |
transform: translateY(-2px); | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
} | |
.theme-toggle:active { | |
transform: translateY(0); | |
} | |
.button-container { | |
display: flex; | |
gap: 1rem; | |
margin-top: 1rem; | |
} | |
.translate-btn, .speak-btn { | |
padding: 0.75rem 1.5rem; | |
background-color: var(--primary-color); | |
color: white; | |
border: none; | |
border-radius: 8px; | |
cursor: pointer; | |
font-size: 0.95rem; | |
font-weight: 500; | |
transition: all 0.3s ease; | |
flex: 1; | |
} | |
.translate-btn:hover, .speak-btn:hover { | |
background-color: var(--primary-hover); | |
transform: translateY(-2px); | |
} | |
.translate-btn:active, .speak-btn:active { | |
transform: translateY(0); | |
} | |
.char-count { | |
position: absolute; | |
bottom: -1.5rem; | |
right: 0; | |
font-size: 0.85rem; | |
color: var(--text-color); | |
opacity: 0.8; | |
} | |
.chat-history { | |
margin-top: 2rem; | |
padding: 1.5rem; | |
border-radius: 1rem; | |
background-color: var(--bg-color); | |
border: 2px solid var(--border-color); | |
} | |
.chat-history h2 { | |
font-size: 1.25rem; | |
margin-bottom: 1rem; | |
color: var(--text-color); | |
} | |
.chat-item { | |
padding: 1rem; | |
margin-bottom: 1rem; | |
border-radius: 0.75rem; | |
background-color: rgba(242, 101, 34, 0.1); | |
border: 1px solid var(--border-color); | |
} | |
.chat-item:last-child { | |
margin-bottom: 0; | |
} | |
.translation-bubble { | |
margin-top: 1.5rem; | |
padding: 1rem; | |
border-radius: 1rem; | |
background-color: var(--bg-color); | |
border: 2px solid var(--border-color); | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
max-width: 600px; | |
margin-left: auto; | |
margin-right: auto; | |
} | |
.translation-bubble h2 { | |
font-size: 1.25rem; | |
margin-bottom: 0.5rem; | |
color: var(--text-color); | |
} | |
.translation-bubble p { | |
font-size: 1rem; | |
color: var(--text-color); | |
line-height: 1.5; | |
} | |
@media (max-width: 640px) { | |
.container { | |
padding: 1.5rem; | |
} | |
h1 { | |
font-size: 1.75rem; | |
} | |
.button-container { | |
flex-direction: column; | |
} | |
} | |
</style> | |
</head> | |
<body data-theme="light"> | |
<button class="theme-toggle">Switch to Dark Mode</button> | |
<div class="container"> | |
<h1>Translator</h1> | |
<div class="form-group"> | |
<div class="input-container"> | |
<label class="label" for="inputText">Original</label> | |
<textarea | |
id="inputText" | |
placeholder="Enter text to translate..." | |
maxlength="500" | |
oninput="updateCharCount(this)" | |
></textarea> | |
<div class="char-count">0/500</div> | |
</div> | |
<div class="direction-container"> | |
<label class="label" for="direction">Translation Direction</label> | |
<select id="direction"> | |
<option value="en-fi">English to Finnish</option> | |
<option value="fi-en">Finnish to English</option> | |
</select> | |
</div> | |
</div> | |
<div class="button-container"> | |
<button class="translate-btn" onclick="translateText()"> | |
Translate | |
</button> | |
<button class="speak-btn" onclick="speakText()"> | |
Speak Translation | |
</button> | |
</div> | |
<div class="error" id="errorMsg"></div> | |
<div class="translation-bubble"> | |
<h2>Translation</h2> | |
<p id="result"></p> | |
</div> | |
<div class="chat-history" id="chatHistory"> | |
<h2>Chat History</h2> | |
<ul class="chat-list" id="chatList"> | |
</ul> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
const themeToggleButton = document.querySelector('.theme-toggle'); | |
if (themeToggleButton) { | |
console.log('Theme toggle button found'); | |
themeToggleButton.addEventListener('click', () => { | |
console.log('Toggle theme button clicked'); | |
const body = document.body; | |
const currentTheme = body.getAttribute('data-theme'); | |
console.log('Current theme:', currentTheme); | |
const newTheme = currentTheme === 'light' ? 'dark' : 'light'; | |
console.log('Switching to theme:', newTheme); | |
body.setAttribute('data-theme', newTheme); | |
themeToggleButton.textContent = `Switch to ${newTheme === 'light' ? 'Dark' : 'Light'} Mode`; | |
}); | |
} else { | |
console.error('Theme toggle button not found'); | |
} | |
}); | |
function updateCharCount(textarea) { | |
const maxLength = textarea.getAttribute('maxlength'); | |
const currentLength = textarea.value.length; | |
const charCountElement = textarea.parentElement.querySelector('.char-count'); | |
charCountElement.textContent = `${currentLength}/${maxLength}`; | |
} | |
async function translateText() { | |
const inputText = document.getElementById('inputText').value; | |
const direction = document.getElementById('direction').value; | |
const translateBtn = document.querySelector('.translate-btn'); | |
const loading = document.querySelector('.loading'); | |
const result = document.getElementById('result'); | |
const resultContainer = document.querySelector('.translation-bubble'); | |
const errorMsg = document.getElementById('errorMsg'); | |
if (!inputText.trim()) { | |
errorMsg.textContent = 'Please enter some text to translate'; | |
errorMsg.style.display = 'block'; | |
return; | |
} | |
// Reset states | |
errorMsg.style.display = 'none'; | |
translateBtn.disabled = true; | |
result.textContent = ''; | |
resultContainer.classList.remove('show'); | |
try { | |
const response = await fetch('/translate', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ text: inputText, direction: direction }) | |
}); | |
const data = await response.json(); | |
if (response.ok) { | |
result.textContent = data.translation; | |
resultContainer.classList.add('show'); | |
// Update chat history | |
updateChatHistory(inputText, data.translation); | |
} else { | |
errorMsg.textContent = data.error || 'Failed to translate'; | |
errorMsg.style.display = 'block'; | |
} | |
} catch (error) { | |
console.error('Translation error:', error); | |
errorMsg.textContent = 'Failed to connect to the server'; | |
errorMsg.style.display = 'block'; | |
} finally { | |
translateBtn.disabled = false; | |
} | |
} | |
function updateChatHistory(input, output) { | |
const chatList = document.getElementById('chatList'); | |
const messageLi = document.createElement('li'); | |
messageLi.classList.add('chat-item'); | |
const originalDiv = document.createElement('div'); | |
originalDiv.classList.add('chat-original'); | |
originalDiv.textContent = `Original: ${input}`; | |
const translationDiv = document.createElement('div'); | |
translationDiv.classList.add('chat-translation'); | |
translationDiv.textContent = `Translation: ${output}`; | |
messageLi.appendChild(originalDiv); | |
messageLi.appendChild(translationDiv); | |
chatList.appendChild(messageLi); | |
chatList.scrollTop = chatList.scrollHeight; | |
} | |
function speakText() { | |
const text = document.getElementById('result').textContent; | |
if (!text) return; | |
const utterance = new SpeechSynthesisUtterance(text); | |
window.speechSynthesis.speak(utterance); | |
} | |
</script> | |
</body> | |
</html> | |