Initial commit: Ollama Translator Web App

This commit is contained in:
2026-01-09 18:06:45 +01:00
commit 0460984618
5 changed files with 995 additions and 0 deletions

29
.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
# Ambiente virtuale
venv/
ENV/
env/
# File Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
dist/
build/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# File di log
*.log

231
ReadMe.MD Normal file
View File

@@ -0,0 +1,231 @@
# 🌍 Traduttore Ollama
Un'applicazione web moderna per tradurre testi lunghi utilizzando modelli locali di Ollama. Supporta oltre 30 lingue e utilizza chunking intelligente per gestire testi di qualsiasi lunghezza.
## 📋 Caratteristiche
- ✅ **Supporto multi-lingua**: 30+ lingue (cinese, inglese, spagnolo, francese, tedesco, italiano, giapponese, coreano, russo, arabo, e molte altre)
- ✅ **Gestione testi lunghi**: Chunking automatico a 500 token per elaborare documenti estesi
- ✅ **UI moderna e responsiva**: Design elegante con gradient background e animazioni fluide
- ✅ **Barra di progresso**: Visualizzazione in tempo reale dell'elaborazione dei chunk
- ✅ **Ottimizzazione modello**: Parametri `top_p=0.6` e `repeat_penalty=1.1` per miglior qualità
- ✅ **Copia negli appunti**: Pulsante per copiare la traduzione con un click
- ✅ **Gestione errori robusta**: Messaggi di errore chiari e dettagliati
- ✅ **Health check**: Verificare lo stato di Ollama e i modelli disponibili
## 🚀 Installazione Rapida
### Prerequisiti
- **Python 3.8+**
- **Ollama** in esecuzione su `192.168.0.170:11434` con il modello `xieweicong95/HY-MT1.5-1.8B` scaricato
### Step 1: Clona o crea la directory del progetto
mkdir ollama-traduttore
cd ollama-traduttore
### Step 2: Crea l'ambiente virtuale
**Su Windows (CMD o PowerShell):**
python -m venv venv
venv\Scripts\activate
**Su Linux/Mac:**
python3 -m venv venv
source venv/bin/activate
### Step 3: Installa le dipendenze
pip install -r requirements.txt
### Step 4: Assicurati che Ollama sia pronto
# Verifica che Ollama sia in esecuzione
ollama list
# Se il modello non è presente, scaricalo
ollama pull xieweicong95/HY-MT1.5-1.8B
# Avvia Ollama (se non è già running)
ollama serve
### Step 5: Avvia l'applicazione
python app.py
L'applicazione sarà disponibile su: **`http://localhost:5000`**
## 📁 Struttura del Progetto
ollama-traduttore/
├── app.py # Backend Flask
├── requirements.txt # Dipendenze Python
├── templates/
│ └── index.html # Frontend HTML/CSS/JS
└── venv/ # Ambiente virtuale
## 🔧 Configurazione
### Cambiare l'indirizzo di Ollama
Se Ollama è su un indirizzo/porta diverso, modifica `app.py`:
OLLAMA_BASE_URL = "http://192.168.0.170:11434" # Cambia questo
### Cambiare il modello
Per usare un modello diverso, modifica in `app.py`:
OLLAMA_MODEL = "xieweicong95/HY-MT1.5-1.8B" # Cambia questo
### Regolare i parametri di traduzione
I parametri di qualità sono configurabili in `app.py`:
OLLAMA_PARAMS = {
"top_p": 0.6, # Diversità del sampling (0.0-1.0)
"repeat_penalty": 1.1 # Penalità per ripetizioni (1.0-2.0)
}
## 📖 Come Usare
1. **Incolla il testo** da tradurre nella textarea
2. **Seleziona la lingua di destinazione** dal dropdown
3. **Opzionalmente**, specifica la lingua sorgente (default: autodetect)
4. **Clicca "Traduci"** e attendi il completamento
5. **Copia la traduzione** con il pulsante "Copia" o selezionala manualmente
### Lingue Supportate
#### Principali / Molto comuni
- Cinese (mandarino) semplificato
- Cinese tradizionale
- Cantonese (dialetto)
- Inglese
- Spagnolo
- Francese
- Tedesco
- Italiano
- Portoghese
- Giapponese
- Coreano
- Russo
- Arabo
- Turco
#### Europee e minori
- Olandese
- Polacco
- Ceco
- Islandese
- Estone
- Ucraino
#### Asia e Medio Oriente
- Vietnamita
- Tailandese
- Malese
- Indonesiano
- Filippino / Tagalog
- Birmano (Myanmar)
- Khmer / Cambogiano
- Persiano
- Ebraico
- Hindi
- Marathi
- Bengali
- Tamil
- Gujarati
## 🔌 API Endpoints
### POST `/api/translate`
Traduce un testo usando Ollama.
**Request:**
{
"prompt": "Translate the following text into [Language]...",
"model": "xieweicong95/HY-MT1.5-1.8B",
"stream": false
}
**Response:**
{
"success": true,
"translation": "Testo tradotto...",
"response": "Testo tradotto...",
"model": "xieweicong95/HY-MT1.5-1.8B"
}
### GET `/api/health`
Verifica lo stato di Ollama e i modelli disponibili.
**Response:**
{
"status": "healthy",
"ollama_url": "http://192.168.0.170:11434",
"available_models": ["xieweicong95/HY-MT1.5-1.8B", "..."],
"target_model": "xieweicong95/HY-MT1.5-1.8B",
"model_available": true
}
## 🛠️ Troubleshooting
### Errore: "Impossibile connettersi a Ollama"
1. Verifica che Ollama sia in esecuzione: `ollama serve`
2. Controlla l'indirizzo IP e la porta in `app.py`
3. Assicurati che il firewall non blocchi la connessione
### Errore: "Modello non trovato"
Scarica il modello:
ollama pull xieweicong95/HY-MT1.5-1.8B
### Timeout della richiesta
Aumenta il timeout in `app.py`:
timeout=300 # Aumenta da 300 secondi (5 minuti)
### Traduzione lenta
- Riduci il numero di chunk aumentando `maxTokens` in `index.html`
- Aumenta le risorse assegnate a Ollama
- Considera l'uso di un modello più piccolo
## 📊 Performance
- **Tempo di risposta**: Dipende dal modello e dalla lunghezza del testo
- **Limite chunk**: 500 token (configurabile)
- **Timeout**: 5 minuti per chunk
- **RAM consigliata**: Almeno 8GB per il modello HY-MT1.5-1.8B
## 🔐 Sicurezza
- L'applicazione è accessibile solo in locale per impostazione predefinita
- I dati non vengono salvati, solo elaborati in tempo reale
- Usa HTTPS in produzione
## 📝 Licenza
Questo progetto utilizza Ollama (Open Source) e Flask (BSD License).
## 🤝 Contributi
Suggerimenti e miglioramenti sono benvenuti!
## 📧 Supporto
Per problemi o domande:
1. Verifica il section Troubleshooting
2. Controlla i log di Flask per errori
3. Assicurati che Ollama sia configurato correttamente
---
**Versione**: 1.0.0
**Ultima modifica**: Gennaio 2026
**Modello**: xieweicong95/HY-MT1.5-1.8B

146
app.py Normal file
View File

@@ -0,0 +1,146 @@
from flask import Flask, render_template, request, jsonify
import requests
import json
import os
app = Flask(__name__)
# Configurazione Ollama
OLLAMA_BASE_URL = "http://192.168.0.170:11434"
OLLAMA_MODEL = "xieweicong95/HY-MT1.5-1.8B"
OLLAMA_PARAMS = {
"top_p": 0.6,
"repeat_penalty": 1.1
}
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/translate', methods=['POST'])
def translate():
"""
Endpoint per tradurre testo usando Ollama.
Accetta JSON con:
- prompt: il prompt da inviare a Ollama
- model: il modello da usare
- stream: boolean per streaming (default: False)
"""
try:
data = request.get_json()
if not data or 'prompt' not in data:
return jsonify({'error': 'Prompt non fornito'}), 400
prompt = data.get('prompt')
model = data.get('model', OLLAMA_MODEL)
stream = data.get('stream', False)
# Costruisci il payload per Ollama
payload = {
"model": model,
"prompt": prompt,
"stream": stream,
**OLLAMA_PARAMS # Aggiungi i parametri top_p e repeat_penalty
}
# Invia la richiesta a Ollama
response = requests.post(
f"{OLLAMA_BASE_URL}/api/generate",
json=payload,
timeout=300 # Timeout di 5 minuti per testi lunghi
)
if response.status_code != 200:
return jsonify({
'error': f'Errore Ollama: {response.status_code}',
'details': response.text
}), response.status_code
# Se non è streaming, la risposta è JSON diretto
if not stream:
result = response.json()
translation = result.get('response', '').strip()
# Pulisci la traduzione da eventuali prefissi
if translation.startswith('Translation:'):
translation = translation.replace('Translation:', '', 1).strip()
return jsonify({
'success': True,
'translation': translation,
'response': translation,
'model': model
})
else:
# Se è streaming, concatena le risposte
full_response = ""
for line in response.iter_lines():
if line:
chunk = json.loads(line)
full_response += chunk.get('response', '')
translation = full_response.strip()
if translation.startswith('Translation:'):
translation = translation.replace('Translation:', '', 1).strip()
return jsonify({
'success': True,
'translation': translation,
'response': translation,
'model': model
})
except requests.exceptions.Timeout:
return jsonify({
'error': 'Timeout della richiesta',
'details': 'La richiesta a Ollama ha impiegato troppo tempo'
}), 408
except requests.exceptions.ConnectionError:
return jsonify({
'error': 'Errore di connessione',
'details': f'Impossibile connettersi a Ollama su {OLLAMA_BASE_URL}'
}), 503
except Exception as e:
return jsonify({
'error': 'Errore interno del server',
'details': str(e)
}), 500
@app.route('/api/health', methods=['GET'])
def health():
"""Controlla lo stato di Ollama"""
try:
response = requests.get(
f"{OLLAMA_BASE_URL}/api/tags",
timeout=5
)
if response.status_code == 200:
models = response.json().get('models', [])
model_names = [m.get('name') for m in models]
return jsonify({
'status': 'healthy',
'ollama_url': OLLAMA_BASE_URL,
'available_models': model_names,
'target_model': OLLAMA_MODEL,
'model_available': OLLAMA_MODEL in model_names
})
else:
return jsonify({
'status': 'unhealthy',
'error': f'Ollama ha risposto con status {response.status_code}'
}), 503
except Exception as e:
return jsonify({
'status': 'error',
'error': str(e),
'ollama_url': OLLAMA_BASE_URL
}), 503
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
Flask==3.0.0
requests==2.31.0

587
templates/index.html Normal file
View 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>