Files
ollama-traduttore/templates/index.html

588 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Traduttore con Ollama</title>
<style>
:root {
--color-white: rgba(255, 255, 255, 1);
--color-black: rgba(0, 0, 0, 1);
--color-cream-50: rgba(252, 252, 249, 1);
--color-gray-300: rgba(167, 169, 169, 1);
--color-slate-900: rgba(19, 52, 59, 1);
--color-teal-500: rgba(33, 128, 141, 1);
--color-teal-600: rgba(29, 116, 128, 1);
--color-teal-300: rgba(50, 184, 198, 1);
--color-background: var(--color-cream-50);
--color-surface: var(--color-white);
--color-text: var(--color-slate-900);
--color-text-secondary: var(--color-gray-300);
--color-primary: var(--color-teal-500);
--color-primary-hover: var(--color-teal-600);
--color-focus-ring: rgba(33, 128, 141, 0.4);
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--space-16: 16px;
--space-20: 20px;
--space-24: 24px;
--space-32: 32px;
--radius-base: 8px;
--radius-lg: 12px;
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.04), 0 2px 4px -1px rgba(0, 0, 0, 0.02);
--duration-normal: 250ms;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-family);
background: linear-gradient(135deg, #f5f5f5 0%, #e8f4f8 100%);
color: var(--color-text);
line-height: 1.6;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-20);
}
.container {
width: 100%;
max-width: 900px;
background: var(--color-surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
padding: var(--space-32);
}
.header {
text-align: center;
margin-bottom: var(--space-32);
}
.header h1 {
font-size: 32px;
font-weight: 600;
color: var(--color-slate-900);
margin-bottom: 8px;
}
.header p {
font-size: 14px;
color: var(--color-text-secondary);
}
.form-group {
margin-bottom: var(--space-24);
}
label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
color: var(--color-text);
}
textarea {
width: 100%;
min-height: 180px;
padding: 12px 16px;
font-size: 14px;
font-family: var(--font-family);
border: 2px solid #e0e0e0;
border-radius: var(--radius-base);
resize: vertical;
transition: border-color var(--duration-normal);
}
textarea:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px var(--color-focus-ring);
}
.char-count {
font-size: 12px;
color: var(--color-text-secondary);
margin-top: 4px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-20);
}
select {
width: 100%;
padding: 12px 16px;
font-size: 14px;
font-family: var(--font-family);
border: 2px solid #e0e0e0;
border-radius: var(--radius-base);
background: var(--color-surface);
color: var(--color-text);
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23134252' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
padding-right: 40px;
transition: border-color var(--duration-normal);
}
select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px var(--color-focus-ring);
}
.button-group {
display: flex;
gap: 12px;
justify-content: center;
}
button {
padding: 12px 24px;
font-size: 14px;
font-weight: 500;
border: none;
border-radius: var(--radius-base);
cursor: pointer;
transition: all var(--duration-normal);
}
.btn-primary {
background: var(--color-primary);
color: white;
flex: 1;
max-width: 300px;
}
.btn-primary:hover {
background: var(--color-primary-hover);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(33, 128, 141, 0.3);
}
.btn-primary:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
opacity: 0.6;
}
.btn-secondary {
background: #f0f0f0;
color: var(--color-text);
}
.btn-secondary:hover {
background: #e0e0e0;
}
.result-container {
margin-top: var(--space-32);
display: none;
}
.result-container.show {
display: block;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.result-box {
background: #f9f9f9;
border: 2px solid #e0e0e0;
border-radius: var(--radius-base);
padding: var(--space-20);
margin-bottom: var(--space-16);
}
.result-box h3 {
font-size: 14px;
font-weight: 600;
color: var(--color-text-secondary);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.result-box p {
font-size: 15px;
line-height: 1.7;
color: var(--color-text);
word-wrap: break-word;
white-space: pre-wrap;
}
.progress-container {
margin-top: var(--space-20);
display: none;
}
.progress-container.show {
display: block;
}
.progress-bar {
width: 100%;
height: 4px;
background: #e0e0e0;
border-radius: 2px;
overflow: hidden;
margin-bottom: 8px;
}
.progress-fill {
height: 100%;
background: var(--color-primary);
transition: width 0.3s ease;
width: 0%;
}
.progress-text {
font-size: 12px;
color: var(--color-text-secondary);
text-align: center;
}
.status-message {
font-size: 13px;
color: var(--color-text-secondary);
margin-top: 8px;
text-align: center;
min-height: 20px;
}
.status-message.error {
color: #d32f2f;
}
.status-message.success {
color: #388e3c;
}
.copy-btn {
background: var(--color-primary);
color: white;
padding: 8px 16px;
font-size: 13px;
border: none;
border-radius: var(--radius-base);
cursor: pointer;
margin-top: 12px;
transition: all var(--duration-normal);
}
.copy-btn:hover {
background: var(--color-primary-hover);
}
.copy-btn.copied {
background: #388e3c;
}
@media (max-width: 600px) {
.container {
padding: var(--space-20);
}
.form-row {
grid-template-columns: 1fr;
}
.button-group {
flex-direction: column;
}
.btn-primary {
max-width: none;
}
.header h1 {
font-size: 24px;
}
textarea {
min-height: 120px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🌍 Traduttore Ollama</h1>
<p>Traduci testi lunghi con modelli locali</p>
</div>
<form id="translatorForm">
<div class="form-group">
<label for="sourceText">Testo da tradurre</label>
<textarea id="sourceText" placeholder="Inserisci il testo che desideri tradurre (anche molto lungo)..." required></textarea>
<div class="char-count">
<span id="charCount">0</span> caratteri
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="targetLanguage">Lingua di destinazione</label>
<select id="targetLanguage" required>
<option value="">Seleziona una lingua</option>
<optgroup label="Principali / Molto comuni">
<option value="Chinese (Simplified)">Cinese (mandarino) semplificato</option>
<option value="Chinese (Traditional)">Cinese tradizionale</option>
<option value="Cantonese">Cantonese (dialetto)</option>
<option value="English">Inglese</option>
<option value="Spanish">Spagnolo</option>
<option value="French">Francese</option>
<option value="German">Tedesco</option>
<option value="Italian">Italiano</option>
<option value="Portuguese">Portoghese</option>
<option value="Japanese">Giapponese</option>
<option value="Korean">Coreano</option>
<option value="Russian">Russo</option>
<option value="Arabic">Arabo</option>
<option value="Turkish">Turco</option>
</optgroup>
<optgroup label="Europee e minori">
<option value="Dutch">Olandese</option>
<option value="Polish">Polacco</option>
<option value="Czech">Ceco</option>
<option value="Icelandic">Islandese</option>
<option value="Estonian">Estone</option>
<option value="Ukrainian">Ucraino</option>
</optgroup>
<optgroup label="Asia e Medio Oriente">
<option value="Vietnamese">Vietnamita</option>
<option value="Thai">Tailandese</option>
<option value="Malay">Malese</option>
<option value="Indonesian">Indonesiano</option>
<option value="Filipino">Filippino / Tagalog</option>
<option value="Burmese">Birmano (Myanmar)</option>
<option value="Khmer">Khmer / Cambogiano</option>
<option value="Persian">Persiano</option>
<option value="Hebrew">Ebraico</option>
<option value="Hindi">Hindi</option>
<option value="Marathi">Marathi</option>
<option value="Bengali">Bengali</option>
<option value="Tamil">Tamil</option>
<option value="Gujarati">Gujarati</option>
</optgroup>
</select>
</div>
<div class="form-group">
<label for="sourceLanguage">Lingua sorgente (opzionale)</label>
<select id="sourceLanguage">
<option value="">Autodetect</option>
<option value="English">Inglese</option>
<option value="Italian">Italiano</option>
<option value="Spanish">Spagnolo</option>
<option value="French">Francese</option>
<option value="German">Tedesco</option>
<option value="Chinese">Cinese</option>
<option value="Japanese">Giapponese</option>
<option value="Russian">Russo</option>
<option value="Arabic">Arabo</option>
</select>
</div>
</div>
<div class="button-group">
<button type="submit" class="btn-primary" id="translateBtn">
▶ Traduci
</button>
<button type="reset" class="btn-secondary">Pulisci</button>
</div>
<div class="status-message" id="statusMessage"></div>
</form>
<div class="result-container" id="resultContainer">
<div class="progress-container" id="progressContainer">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-text">
<span id="progressText">Elaborazione: 0%</span>
</div>
</div>
<div class="result-box" id="resultBox" style="display: none;">
<h3>Testo Tradotto</h3>
<p id="translatedText"></p>
<button type="button" class="copy-btn" id="copyBtn">📋 Copia</button>
</div>
</div>
</div>
<script>
const form = document.getElementById('translatorForm');
const sourceText = document.getElementById('sourceText');
const targetLanguage = document.getElementById('targetLanguage');
const translateBtn = document.getElementById('translateBtn');
const charCount = document.getElementById('charCount');
const statusMessage = document.getElementById('statusMessage');
const resultContainer = document.getElementById('resultContainer');
const progressContainer = document.getElementById('progressContainer');
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
const resultBox = document.getElementById('resultBox');
const translatedTextEl = document.getElementById('translatedText');
const copyBtn = document.getElementById('copyBtn');
// Update char count
sourceText.addEventListener('input', () => {
charCount.textContent = sourceText.value.length;
});
// Chunk text into tokens (simple space-based tokenization)
function chunkText(text, maxTokens = 500) {
const words = text.split(/\s+/).filter(w => w.length > 0);
const chunks = [];
let currentChunk = [];
for (const word of words) {
currentChunk.push(word);
if (currentChunk.length >= maxTokens) {
chunks.push(currentChunk.join(' '));
currentChunk = [];
}
}
if (currentChunk.length > 0) {
chunks.push(currentChunk.join(' '));
}
return chunks;
}
// Translate chunk via Ollama
async function translateChunk(text, targetLang) {
const prompt = `Translate the following text into ${targetLang}, output only the translation without extra explanation:\n\n${text}\n\nTranslation:`;
const response = await fetch('/api/translate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: prompt,
model: 'xieweicong95/HY-MT1.5-1.8B',
stream: false
})
});
if (!response.ok) {
throw new Error(`Server error: ${response.statusText}`);
}
const data = await response.json();
return data.translation || data.response || '';
}
// Handle form submission
form.addEventListener('submit', async (e) => {
e.preventDefault();
const text = sourceText.value.trim();
const target = targetLanguage.value;
if (!text) {
showStatus('Inserisci un testo da tradurre', 'error');
return;
}
if (!target) {
showStatus('Seleziona una lingua di destinazione', 'error');
return;
}
translateBtn.disabled = true;
resultContainer.classList.remove('show');
resultBox.style.display = 'none';
progressContainer.classList.add('show');
statusMessage.textContent = '';
try {
const chunks = chunkText(text, 500);
const translations = [];
for (let i = 0; i < chunks.length; i++) {
const progress = Math.round(((i + 1) / chunks.length) * 100);
progressFill.style.width = progress + '%';
progressText.textContent = `Elaborazione chunk ${i + 1} di ${chunks.length}...`;
try {
const translation = await translateChunk(chunks[i], target);
translations.push(translation.trim());
} catch (err) {
console.error(`Errore nel chunk ${i + 1}:`, err);
translations.push(`[ERRORE NEL CHUNK ${i + 1}]`);
}
// Small delay to avoid overwhelming the server
await new Promise(resolve => setTimeout(resolve, 100));
}
const fullTranslation = translations.join(' ').replace(/\s+/g, ' ').trim();
translatedTextEl.textContent = fullTranslation;
resultBox.style.display = 'block';
resultContainer.classList.add('show');
showStatus('Traduzione completata!', 'success');
} catch (error) {
console.error('Errore:', error);
showStatus('Errore durante la traduzione: ' + error.message, 'error');
resultContainer.classList.add('show');
} finally {
translateBtn.disabled = false;
progressContainer.classList.remove('show');
}
});
// Copy to clipboard
copyBtn.addEventListener('click', async () => {
const text = translatedTextEl.textContent;
try {
await navigator.clipboard.writeText(text);
copyBtn.textContent = '✓ Copiato!';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = '📋 Copia';
copyBtn.classList.remove('copied');
}, 2000);
} catch (err) {
showStatus('Errore nella copia', 'error');
}
});
function showStatus(message, type = 'info') {
statusMessage.textContent = message;
statusMessage.className = 'status-message ' + type;
}
</script>
</body>
</html>