Initial commit: Ollama Translator Web App
This commit is contained in:
587
templates/index.html
Normal file
587
templates/index.html
Normal file
@@ -0,0 +1,587 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user