push iniziale

This commit is contained in:
2026-03-06 19:26:36 +01:00
parent 29fb5571a4
commit 6794b96648
8 changed files with 5706 additions and 2 deletions

297
README.md
View File

@@ -1,3 +1,296 @@
# Traduttore
# Traduttore AI 🌍
Applicazione desktop che sfrutta l'AI per tradurre testi dall'italiano all'inglese e viceversa. Configurata per modelli ollama in locale.
Un'applicazione desktop elegante e moderna per tradurre testi dall'inglese all'italiano e viceversa utilizzando modelli AI locali tramite Ollama. L'app rileva automaticamente la lingua del testo inserito e fornisce la traduzione istantanea.
![Screenshot](screenshot.png)
## ✨ Caratteristiche
- 🎨 **Design moderno** con gradienti colorati e interfaccia intuitiva
- 🔄 **Rilevamento automatico** della lingua (inglese/italiano)
- 🤖 **Integrazione con Ollama** per traduzioni locali e private
-**Traduzione istantanea** con un solo click
- 📋 **Menu contestuale** con Copia/Incolla (tasto destro)
- ⚙️ **Configurazione flessibile** di modello e URL Ollama
- 🖥️ **Multi-piattaforma** (Linux e Windows)
- 🎯 **Rilevamento automatico** direzione traduzione (EN→IT o IT→EN)
## 📋 Requisiti
- **Node.js** v18 o superiore
- **npm** o **yarn**
- **Ollama** installato e in esecuzione
- Almeno un modello LLM scaricato in Ollama (consigliati: llama3, mistral, qwen3:8b)
## 🚀 Installazione
### 1. Clona il repository
```bash
git clone https://github.com/tuousername/traduttore-ai.git
cd Traduttore
```
### 2. Installa le dipendenze
```bash
npm install
```
### 3. Installa Ollama
#### Linux
```bash
# Installazione automatica
curl -fsSL https://ollama.com/install.sh | sh
# Oppure scarica manualmente da:
# https://ollama.com/download
```
#### Windows
1. Scarica l'installer da: https://ollama.com/download/windows
2. Esegui il file `.exe` e segui la procedura guidata
3. Ollama si avvierà automaticamente
### 4. Scarica un modello
```bash
# Modello consigliato (buon bilanciamento qualità/velocità)
ollama pull llama3
# Oppure per traduzioni più accurate
ollama pull qwen3:8b
# Per computer meno potenti
ollama pull mistral
```
### 5. Verifica che Ollama sia in esecuzione
```bash
# Linux - controlla se il servizio è attivo
systemctl status ollama
# Oppure prova a fare una richiesta
curl http://localhost:11434/api/tags
```
## 💻 Utilizzo
### Modalità Sviluppo
Per avviare l'applicazione in modalità sviluppo con hot-reload:
```bash
npm start
```
### Build per Distribuzione
#### Linux (AppImage)
```bash
npm run build:linux
```
Il file eseguibile verrà creato in:
- `dist/Traduttore AI-x.x.x.AppImage`
Per eseguire:
```bash
chmod +x "dist/Traduttore AI-x.x.x.AppImage"
./"dist/Traduttore AI-x.x.x.AppImage"
```
#### Windows (Installer .exe)
```bash
npm run build:win
```
Il file installer verrà creato in:
- `dist/Traduttore AI Setup x.x.x.exe`
Installa l'applicazione eseguendo il file `.exe`.
#### Build completa (tutte le piattaforme)
```bash
npm run build
```
## 🎯 Come Usare
### Primo Avvio
1. **Avvia l'app** con `npm start` (dev) o esegui il file buildato
2. **Configura Ollama**: clicca sull'icona ⚙️ in alto a destra
3. **Inserisci l'URL**: di default è `http://localhost:11434`
4. **Clicca "Aggiorna"** per caricare i modelli disponibili
5. **Seleziona un modello** dalla lista
6. **Clicca "Salva"**
### Tradurre un Testo
1. **Scrivi o incolla** il testo nell'area a sinistra
2. L'app rileverà **automaticamente** la lingua (🇮🇹 Italiano o 🇬🇧 Inglese)
3. Clicca il pulsante **"Traduci"** o premi `Ctrl+Enter`
4. La traduzione apparirà nell'area a destra
### Scorciatoie da Tastiera
| Tasto | Azione |
|-------|--------|
| `Ctrl+Enter` | Traduci il testo |
| `Ctrl+,` | Apri impostazioni |
| `Ctrl+Q` | Esci dall'applicazione |
| `Esc` | Chiudi modale impostazioni |
| `Ctrl+C` | Copia testo selezionato |
| `Ctrl+V` | Incolla testo |
| `Ctrl+X` | Taglia testo selezionato |
| `Ctrl+A` | Seleziona tutto il testo |
### Menu Contestuale (Tasto Destro)
Clicca con il tasto destro su qualsiasi textarea per:
- Taglia
- Copia
- Incolla
- Seleziona tutto
- Elimina
## 🛠️ Risoluzione Problemi
### "Errore: fetch failed" o "ECONNREFUSED"
**Problema**: L'app non riesce a connettersi a Ollama
**Soluzioni**:
1. Verifica che Ollama sia in esecuzione:
```bash
curl http://localhost:11434/api/tags
```
2. Se non risponde, avvia Ollama:
```bash
ollama serve
```
3. Verifica che la porta non sia occupata:
```bash
# Linux
netstat -tulpn | grep 11434
# Windows
netstat -ano | findstr 11434
```
### "Nessun modello trovato"
**Problema**: Ollama non ha modelli installati
**Soluzione**:
```bash
ollama pull llama3
# oppure
ollama pull qwen3:8b
```
### Traduzione lenta o non risponde
**Possibili cause**:
- Modello troppo pesante per il tuo hardware
- Memoria RAM insufficiente
- GPU non utilizzata (se disponibile)
**Soluzioni**:
- Usa un modello più leggero: `ollama pull mistral` o `ollama pull llama3:8b`
- Chiudi altre applicazioni per liberare RAM
- Verifica che Ollama stia usando la GPU: `ollama ps`
### Errore su Windows: "Ollama not found"
**Soluzione**:
1. Verifica che Ollama sia installato correttamente
2. Aggiungi Ollama al PATH di sistema:
- Pannello di Controllo → Sistema → Impostazioni Avanzate → Variabili d'ambiente
- Aggiungi il percorso di installazione di Ollama (es: `C:\Users\<username>\AppData\Local\Programs\Ollama`)
### Problemi con IPv6 (Linux)
Se vedi errori come `connect ECONNREFUSED ::1:11434`, l'app ora gestisce automaticamente questo problema sostituendo `localhost` con `127.0.0.1`.
## 📁 Struttura del Progetto
```
Traduttore/
├── main.js # Processo principale Electron
├── preload.js # Bridge sicuro tra processi
├── index.html # Interfaccia utente
├── renderer.js # Logica applicazione
├── styles.css # Stili e design
├── package.json # Configurazione npm
└── README.md # Questo file
```
## 🔧 Configurazione Avanzata
### Cambiare la porta di Ollama
Se vuoi usare una porta diversa per Ollama:
**Linux**:
```bash
OLLAMA_HOST=0.0.0.0:8080 ollama serve
```
**Windows**:
```cmd
set OLLAMA_HOST=0.0.0.0:8080
ollama serve
```
Poi nell'app, imposta l'URL: `http://localhost:8080`
### Modelli Consigliati
| Modello | Dimensione | Qualità | Velocità | Uso |
|---------|-----------|---------|----------|-----|
| llama3 | 4.7 GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Uso generale |
| qwen3:8b | 4.9 GB | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | Traduzioni accurate |
| mistral | 4.1 GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Buon compromesso |
| llama3:8b | 4.7 GB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Versione ridotta |
## 🤝 Contribuire
Se vuoi contribuire al progetto:
1. Fork il repository
2. Crea un branch: `git checkout -b feature/nuova-funzionalita`
3. Committa le modifiche: `git commit -am 'Aggiunta nuova funzionalità'`
4. Push al branch: `git push origin feature/nuova-funzionalita`
5. Apri una Pull Request
## 📝 Changelog
### v1.0.0
- ✨ Rilascio iniziale
- 🎨 Design moderno con gradienti
- 🤖 Integrazione Ollama
- 🔄 Rilevamento automatico lingua
- 📋 Menu contestuale tasto destro
- 🖥️ Supporto Linux e Windows
## 📄 Licenza
MIT License - vedi file [LICENSE](LICENSE) per i dettagli.
---
Fatto con ❤️ e ☕ per la comunità open source
**Autore**: Tu
**Versione**: 1.0.0

133
index.html Normal file
View File

@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Traduttore AI</title>
<link rel="stylesheet" href="styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet">
</head>
<body>
<div class="app-container">
<header class="app-header">
<div class="logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M2 12h20"/>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
</svg>
<h1>Traduttore AI</h1>
</div>
<button class="settings-btn" id="settingsBtn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l-.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
</button>
</header>
<div class="language-indicator">
<span id="detectedLang">Rilevamento automatico...</span>
</div>
<main class="translation-area">
<div class="input-section">
<div class="textarea-container">
<textarea
id="inputText"
placeholder="Inserisci il testo da tradurre..."
spellcheck="false"
></textarea>
<div class="textarea-actions">
<button class="action-btn" id="clearBtn" title="Cancella">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/>
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
</button>
<span class="char-count" id="charCount">0 caratteri</span>
</div>
</div>
</div>
<div class="output-section">
<div class="textarea-container">
<textarea
id="outputText"
placeholder="La traduzione apparirà qui..."
readonly
spellcheck="false"
></textarea>
<div class="textarea-actions">
<button class="action-btn" id="copyBtn" title="Copia">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
</button>
<div class="status-indicator" id="statusIndicator"></div>
</div>
</div>
</div>
</main>
<div class="translate-section">
<button class="translate-btn" id="translateBtn">
<span class="btn-text">Traduci</span>
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14"/>
<path d="M12 5l7 7-7 7"/>
</svg>
</button>
</div>
</div>
<!-- Modal Impostazioni -->
<div class="modal" id="settingsModal">
<div class="modal-content">
<div class="modal-header">
<h2>Impostazioni</h2>
<button class="close-btn" id="closeModal">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="modal-body">
<div class="setting-item">
<label for="baseUrl">Ollama Base URL</label>
<input type="text" id="baseUrl" value="http://localhost:11434" placeholder="http://localhost:11434">
<span class="help-text">Lascia vuoto per usare localhost:11434</span>
</div>
<div class="setting-item">
<label for="modelSelect">Modello</label>
<select id="modelSelect">
<option value="">Carica modelli...</option>
</select>
<button class="refresh-btn" id="refreshModels">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<polyline points="1 20 1 14 7 14"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>
Aggiorna
</button>
<span class="help-text">Seleziona il modello per la traduzione</span>
</div>
</div>
<div class="modal-footer">
<button class="save-btn" id="saveSettings">Salva</button>
</div>
</div>
</div>
<!-- Toast Notification -->
<div class="toast" id="toast"></div>
<script src="renderer.js"></script>
</body>
</html>

193
main.js Normal file
View File

@@ -0,0 +1,193 @@
const { app, BrowserWindow, ipcMain, dialog, Menu } = require('electron');
const path = require('path');
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 900,
minHeight: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
},
titleBarStyle: 'hiddenInset',
show: false
});
mainWindow.loadFile('index.html');
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
mainWindow.on('closed', () => {
mainWindow = null;
});
// Menu contestuale (tasto destro)
mainWindow.webContents.on('context-menu', (event, params) => {
const contextMenu = Menu.buildFromTemplate([
{
label: 'Taglia',
accelerator: 'CmdOrCtrl+X',
role: 'cut',
enabled: params.editFlags.canCut
},
{
label: 'Copia',
accelerator: 'CmdOrCtrl+C',
role: 'copy',
enabled: params.editFlags.canCopy
},
{
label: 'Incolla',
accelerator: 'CmdOrCtrl+V',
role: 'paste',
enabled: params.editFlags.canPaste
},
{
label: 'Seleziona tutto',
accelerator: 'CmdOrCtrl+A',
role: 'selectAll',
enabled: params.editFlags.canSelectAll
},
{ type: 'separator' },
{
label: 'Elimina',
role: 'delete',
enabled: params.editFlags.canDelete
}
]);
contextMenu.popup(mainWindow);
});
createMenu();
}
function createMenu() {
const template = [
{
label: 'File',
submenu: [
{
label: 'Esci',
accelerator: 'CmdOrCtrl+Q',
click: () => {
app.quit();
}
}
]
},
{
label: 'Impostazioni',
submenu: [
{
label: 'Configura Modello e URL',
accelerator: 'CmdOrCtrl+,',
click: () => {
mainWindow.webContents.send('open-settings');
}
}
]
},
{
label: 'Aiuto',
submenu: [
{
label: 'Informazioni',
click: () => {
dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'Informazioni',
message: 'Traduttore AI',
detail: 'Applicazione desktop per traduzioni con modelli AI locali tramite Ollama.'
});
}
}
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
ipcMain.handle('translate-text', async (event, { text, direction, model, baseUrl }) => {
try {
const sourceLang = direction === 'en-it' ? 'inglese' : 'italiano';
const targetLang = direction === 'en-it' ? 'italiano' : 'inglese';
// Usa 127.0.0.1 invece di localhost per evitare problemi IPv6
const cleanBaseUrl = baseUrl.replace('localhost', '127.0.0.1');
const response = await fetch(`${cleanBaseUrl}/api/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: model,
prompt: `Traduci il seguente testo da ${sourceLang} a ${targetLang}. Rispondi SOLO con la traduzione, senza spiegazioni o commenti aggiuntivi:\n\n${text}`,
stream: false
})
});
if (!response.ok) {
throw new Error(`Errore HTTP: ${response.status}`);
}
const data = await response.json();
return { success: true, translation: data.response };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle('get-models', async (event, baseUrl) => {
try {
// Assicurati che l'URL sia pulito e sostituisci localhost con 127.0.0.1 per evitare problemi IPv6
let cleanUrl = baseUrl.trim().replace(/\/$/, '');
cleanUrl = cleanUrl.replace('localhost', '127.0.0.1');
const fullUrl = `${cleanUrl}/api/tags`;
console.log('Richiesta modelli da:', fullUrl);
const response = await fetch(fullUrl, {
method: 'GET',
headers: {
'Accept': 'application/json',
}
});
console.log('Risposta status:', response.status);
if (!response.ok) {
throw new Error(`Errore HTTP: ${response.status} - ${response.statusText}`);
}
const data = await response.json();
console.log('Modelli trovati:', data.models?.length || 0);
return { success: true, models: data.models || [] };
} catch (error) {
console.error('Errore in get-models:', error);
return { success: false, error: error.message };
}
});

4103
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

41
package.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "traduttore-ai",
"version": "1.0.0",
"description": "Applicazione desktop per traduzioni con AI locale",
"main": "main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder",
"build:win": "electron-builder --win",
"build:linux": "electron-builder --linux"
},
"author": "Tu",
"license": "MIT",
"devDependencies": {
"electron": "^28.0.0",
"electron-builder": "^24.9.1"
},
"build": {
"appId": "com.tuo.traduttore",
"productName": "Traduttore AI",
"directories": {
"output": "dist"
},
"files": [
"main.js",
"preload.js",
"index.html",
"renderer.js",
"styles.css",
"node_modules/**/*"
],
"win": {
"target": "nsis",
"icon": "icon.ico"
},
"linux": {
"target": "AppImage",
"icon": "icon.png"
}
}
}

7
preload.js Normal file
View File

@@ -0,0 +1,7 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
translateText: (data) => ipcRenderer.invoke('translate-text', data),
getModels: (baseUrl) => ipcRenderer.invoke('get-models', baseUrl),
onOpenSettings: (callback) => ipcRenderer.on('open-settings', callback)
});

335
renderer.js Normal file
View File

@@ -0,0 +1,335 @@
// Elementi DOM
const inputText = document.getElementById('inputText');
const outputText = document.getElementById('outputText');
const translateBtn = document.getElementById('translateBtn');
const detectedLang = document.getElementById('detectedLang');
const clearBtn = document.getElementById('clearBtn');
const copyBtn = document.getElementById('copyBtn');
const charCount = document.getElementById('charCount');
const statusIndicator = document.getElementById('statusIndicator');
const settingsBtn = document.getElementById('settingsBtn');
const settingsModal = document.getElementById('settingsModal');
const closeModal = document.getElementById('closeModal');
const baseUrlInput = document.getElementById('baseUrl');
const modelSelect = document.getElementById('modelSelect');
const refreshModels = document.getElementById('refreshModels');
const saveSettings = document.getElementById('saveSettings');
const toast = document.getElementById('toast');
// Stato applicazione
let isTranslating = false;
let currentDetectedLang = null;
// Carica impostazioni salvate
function loadSettings() {
const savedBaseUrl = localStorage.getItem('ollamaBaseUrl') || 'http://localhost:11434';
const savedModel = localStorage.getItem('ollamaModel') || '';
baseUrlInput.value = savedBaseUrl;
if (savedModel) {
const existingOption = Array.from(modelSelect.options).find(opt => opt.value === savedModel);
if (!existingOption) {
const option = document.createElement('option');
option.value = savedModel;
option.textContent = savedModel;
modelSelect.appendChild(option);
}
modelSelect.value = savedModel;
}
}
// Salva impostazioni
function saveSettingsData() {
const baseUrl = baseUrlInput.value.trim() || 'http://localhost:11434';
const model = modelSelect.value;
localStorage.setItem('ollamaBaseUrl', baseUrl);
localStorage.setItem('ollamaModel', model);
closeSettingsModal();
showToast('Impostazioni salvate!');
}
// Carica modelli disponibili
async function loadAvailableModels() {
const baseUrl = baseUrlInput.value.trim() || 'http://localhost:11434';
console.log('DEBUG - Tentativo di caricare modelli da:', baseUrl);
console.log('DEBUG - electronAPI disponibile?', !!window.electronAPI);
console.log('DEBUG - getModels funzione?', typeof window.electronAPI?.getModels);
refreshModels.disabled = true;
refreshModels.innerHTML = `
<svg class="spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<polyline points="1 20 1 14 7 14"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>
Caricamento...
`;
try {
const cleanBaseUrl = baseUrl.trim();
console.log('Tentativo connessione a:', cleanBaseUrl);
const result = await window.electronAPI.getModels(cleanBaseUrl);
if (result.success) {
modelSelect.innerHTML = '<option value="">Seleziona un modello...</option>';
result.models.forEach(model => {
const option = document.createElement('option');
option.value = model.name;
option.textContent = model.name;
modelSelect.appendChild(option);
});
const savedModel = localStorage.getItem('ollamaModel');
if (savedModel && result.models.find(m => m.name === savedModel)) {
modelSelect.value = savedModel;
}
showToast(`${result.models.length} modelli trovati!`);
} else {
showToast('Errore: ' + result.error, 'error');
}
} catch (error) {
console.error('Errore completo:', error);
showToast('Errore di connessione: ' + (error.message || 'Verifica che Ollama sia in esecuzione'), 'error');
} finally {
refreshModels.disabled = false;
refreshModels.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<polyline points="1 20 1 14 7 14"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>
Aggiorna
`;
}
}
// Rileva la lingua del testo
function detectLanguage(text) {
if (!text.trim()) {
currentDetectedLang = null;
detectedLang.textContent = 'Rilevamento automatico...';
detectedLang.className = '';
return null;
}
// Caratteri italiani comuni
const italianChars = /[àèéìòùÀÈÉÌÒÙ]/;
const italianWords = /\b(ciao|grazie|buongiorno|come|sono|per|con|su|tra|fra|nel|del|degli|della)\b/gi;
// Caratteri inglesi comuni (rari in italiano)
const englishChars = /[wkyjWQX]/;
const englishWords = /\b(the|and|for|are|with|you|that|have|this|from|they|we|say|her|she|or|an|will|my|one|all|would|there|their|what|so|up|out|if|about|who|get|which|go|me|when|make|can|like|time|no|just|him|know|take|people|into|year|your|good|some|could|them|see|other|than|then|now|look|only|come|its|over|think|also|back|after|use|two|how|our|work|first|well|way|even|new|want|because|any|these|give|day|most|us)\b/gi;
const hasItalianChars = italianChars.test(text);
const hasItalianWords = (text.match(italianWords) || []).length;
const hasEnglishChars = englishChars.test(text);
const hasEnglishWords = (text.match(englishWords) || []).length;
// Logica di rilevamento
let detected = null;
if (hasItalianChars) {
detected = 'it';
} else if (hasEnglishChars && !hasItalianWords) {
detected = 'en';
} else if (hasItalianWords > hasEnglishWords) {
detected = 'it';
} else if (hasEnglishWords > italianWords) {
detected = 'en';
} else {
// Fallback: verifica la distribuzione delle vocali
const vowels = text.toLowerCase().match(/[aeiou]/g) || [];
const yCount = (text.match(/y/gi) || []).length;
if (yCount > 0) {
detected = 'en';
} else {
detected = 'it'; // default
}
}
currentDetectedLang = detected;
updateLanguageIndicator();
return detected;
}
// Aggiorna l'indicatore di lingua
function updateLanguageIndicator() {
if (currentDetectedLang === 'it') {
detectedLang.innerHTML = '🇮🇹 Italiano → 🇬🇧 Inglese';
detectedLang.className = 'detected-it';
} else if (currentDetectedLang === 'en') {
detectedLang.innerHTML = '🇬🇧 Inglese → 🇮🇹 Italiano';
detectedLang.className = 'detected-en';
} else {
detectedLang.textContent = 'Rilevamento automatico...';
detectedLang.className = '';
}
}
// Traduci testo
async function translateText() {
const text = inputText.value.trim();
if (!text) {
showToast('Inserisci del testo da tradurre', 'warning');
return;
}
const baseUrl = localStorage.getItem('ollamaBaseUrl') || 'http://localhost:11434';
const model = localStorage.getItem('ollamaModel');
if (!model) {
showToast('Configura un modello nelle impostazioni', 'warning');
openSettingsModal();
return;
}
// Rileva la lingua se non già rilevata
const detected = currentDetectedLang || detectLanguage(text);
if (!detected) {
showToast('Impossibile rilevare la lingua', 'warning');
return;
}
const direction = detected === 'en' ? 'en-it' : 'it-en';
isTranslating = true;
translateBtn.disabled = true;
translateBtn.innerHTML = `
<span class="spinner"></span>
<span class="btn-text">Traduzione...</span>
`;
statusIndicator.className = 'status-indicator translating';
try {
const result = await window.electronAPI.translateText({
text,
direction,
model,
baseUrl
});
if (result.success) {
outputText.value = result.translation.trim();
statusIndicator.className = 'status-indicator success';
showToast('Traduzione completata!', 'success');
} else {
outputText.value = '';
statusIndicator.className = 'status-indicator error';
showToast('Errore: ' + result.error, 'error');
}
} catch (error) {
outputText.value = '';
statusIndicator.className = 'status-indicator error';
showToast('Errore: ' + error.message, 'error');
} finally {
isTranslating = false;
translateBtn.disabled = false;
translateBtn.innerHTML = `
<span class="btn-text">Traduci</span>
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14"/>
<path d="M12 5l7 7-7 7"/>
</svg>
`;
}
}
// Aggiorna conteggio caratteri
function updateCharCount() {
const count = inputText.value.length;
charCount.textContent = `${count} caratter${count === 1 ? 'e' : 'i'}`;
}
// Copia negli appunti
async function copyToClipboard() {
const text = outputText.value;
if (!text) return;
try {
await navigator.clipboard.writeText(text);
showToast('Testo copiato!', 'success');
} catch (err) {
outputText.select();
document.execCommand('copy');
showToast('Testo copiato!', 'success');
}
}
// Pulisci input
function clearInput() {
inputText.value = '';
outputText.value = '';
updateCharCount();
statusIndicator.className = 'status-indicator';
currentDetectedLang = null;
detectedLang.textContent = 'Rilevamento automatico...';
detectedLang.className = '';
inputText.focus();
}
// Mostra toast
function showToast(message, type = 'info') {
toast.textContent = message;
toast.className = `toast show ${type}`;
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// Gestione modale
function openSettingsModal() {
settingsModal.classList.add('show');
loadAvailableModels();
}
function closeSettingsModal() {
settingsModal.classList.remove('show');
}
// Event Listeners
translateBtn.addEventListener('click', translateText);
inputText.addEventListener('input', () => {
updateCharCount();
detectLanguage(inputText.value);
});
clearBtn.addEventListener('click', clearInput);
copyBtn.addEventListener('click', copyToClipboard);
settingsBtn.addEventListener('click', openSettingsModal);
closeModal.addEventListener('click', closeSettingsModal);
refreshModels.addEventListener('click', loadAvailableModels);
saveSettings.addEventListener('click', saveSettingsData);
// Chiudi modale con ESC
settingsModal.addEventListener('click', (e) => {
if (e.target === settingsModal) {
closeSettingsModal();
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeSettingsModal();
}
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
translateText();
}
});
// Ascolta richiesta apertura impostazioni dal menu
window.electronAPI.onOpenSettings((event) => {
openSettingsModal();
});
// Inizializzazione
document.addEventListener('DOMContentLoaded', () => {
loadSettings();
updateCharCount();
});

599
styles.css Normal file
View File

@@ -0,0 +1,599 @@
/* Variabili colori - Design allegro e moderno */
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
--success-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
--warning-gradient: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
--bg-primary: #f8fafc;
--bg-secondary: #ffffff;
--bg-tertiary: #f1f5f9;
--text-primary: #1e293b;
--text-secondary: #64748b;
--text-tertiary: #94a3b8;
--border-color: #e2e8f0;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 24px;
--transition-fast: 150ms ease;
--transition-normal: 250ms ease;
--transition-slow: 350ms ease;
}
/* Reset e base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
}
/* Container principale */
.app-container {
height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-primary);
}
/* Header */
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
box-shadow: var(--shadow-sm);
position: relative;
z-index: 10;
}
.logo {
display: flex;
align-items: center;
gap: 12px;
}
.logo svg {
width: 36px;
height: 36px;
color: #667eea;
background: linear-gradient(135deg, #667eea20 0%, #764ba220 100%);
padding: 6px;
border-radius: var(--radius-md);
}
.logo h1 {
font-size: 1.5rem;
font-weight: 800;
background: var(--primary-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.settings-btn {
width: 44px;
height: 44px;
border: none;
background: var(--bg-tertiary);
border-radius: var(--radius-md);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition-normal);
}
.settings-btn:hover {
background: var(--primary-gradient);
color: white;
transform: rotate(30deg);
}
.settings-btn svg {
width: 22px;
height: 22px;
}
/* Indicatore lingua automatica */
.language-indicator {
display: flex;
align-items: center;
justify-content: center;
padding: 16px 24px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.language-indicator span {
font-size: 1rem;
font-weight: 600;
color: var(--text-secondary);
padding: 8px 20px;
background: var(--bg-tertiary);
border-radius: var(--radius-lg);
transition: all var(--transition-normal);
}
.language-indicator span.detected-it {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
color: white;
box-shadow: var(--shadow-md);
}
.language-indicator span.detected-en {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: var(--shadow-md);
}
/* Area traduzione */
.translation-area {
flex: 1;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
padding: 24px;
overflow: hidden;
}
.input-section,
.output-section {
display: flex;
flex-direction: column;
min-height: 0;
}
.textarea-container {
flex: 1;
display: flex;
flex-direction: column;
background: var(--bg-secondary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
overflow: hidden;
transition: box-shadow var(--transition-normal);
}
.textarea-container:focus-within {
box-shadow: var(--shadow-xl);
}
textarea {
flex: 1;
width: 100%;
padding: 20px;
border: none;
background: transparent;
font-family: inherit;
font-size: 1.1rem;
line-height: 1.7;
color: var(--text-primary);
resize: none;
outline: none;
}
textarea::placeholder {
color: var(--text-tertiary);
}
#outputText {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
}
.textarea-actions {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
border-top: 1px solid var(--border-color);
background: var(--bg-tertiary);
}
.action-btn {
width: 36px;
height: 36px;
border: none;
background: transparent;
border-radius: var(--radius-md);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: all var(--transition-fast);
}
.action-btn:hover {
background: var(--bg-secondary);
color: #667eea;
transform: scale(1.1);
}
.action-btn svg {
width: 20px;
height: 20px;
}
.char-count {
font-size: 0.875rem;
color: var(--text-tertiary);
font-weight: 500;
}
/* Status indicator */
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--text-tertiary);
transition: all var(--transition-normal);
}
.status-indicator.translating {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
animation: pulse 1.5s infinite;
}
.status-indicator.success {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.status-indicator.error {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.2);
opacity: 0.7;
}
}
/* Pulsante traduci */
.translate-section {
padding: 0 24px 24px;
display: flex;
justify-content: center;
}
.translate-btn {
padding: 16px 48px;
border: none;
background: var(--primary-gradient);
border-radius: var(--radius-xl);
color: white;
font-family: inherit;
font-size: 1.1rem;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
transition: all var(--transition-normal);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.translate-btn:hover:not(:disabled) {
transform: translateY(-3px) scale(1.02);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
}
.translate-btn:active:not(:disabled) {
transform: translateY(-1px);
}
.translate-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.btn-icon {
width: 20px;
height: 20px;
transition: transform var(--transition-normal);
}
.translate-btn:hover:not(:disabled) .btn-icon {
transform: translateX(4px);
}
.spinner {
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
opacity: 0;
transition: opacity var(--transition-normal);
}
.modal.show {
display: flex;
opacity: 1;
}
.modal-content {
background: var(--bg-secondary);
border-radius: var(--radius-xl);
width: 90%;
max-width: 500px;
box-shadow: var(--shadow-xl);
transform: scale(0.95);
transition: transform var(--transition-normal);
}
.modal.show .modal-content {
transform: scale(1);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px;
border-bottom: 1px solid var(--border-color);
}
.modal-header h2 {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-primary);
}
.close-btn {
width: 36px;
height: 36px;
border: none;
background: transparent;
border-radius: var(--radius-md);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: all var(--transition-fast);
}
.close-btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.close-btn svg {
width: 20px;
height: 20px;
}
.modal-body {
padding: 24px;
}
.setting-item {
margin-bottom: 20px;
}
.setting-item:last-child {
margin-bottom: 0;
}
.setting-item label {
display: block;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
font-size: 0.95rem;
}
.setting-item input,
.setting-item select {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
font-family: inherit;
font-size: 1rem;
color: var(--text-primary);
background: var(--bg-primary);
transition: all var(--transition-fast);
}
.setting-item input:focus,
.setting-item select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.help-text {
display: block;
font-size: 0.875rem;
color: var(--text-tertiary);
margin-top: 6px;
}
.refresh-btn {
margin-top: 10px;
padding: 10px 16px;
border: 2px solid var(--border-color);
background: var(--bg-primary);
border-radius: var(--radius-md);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-family: inherit;
font-size: 0.9rem;
font-weight: 600;
color: var(--text-secondary);
transition: all var(--transition-fast);
}
.refresh-btn:hover:not(:disabled) {
border-color: #667eea;
color: #667eea;
}
.refresh-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.refresh-btn svg {
width: 18px;
height: 18px;
}
.refresh-btn svg.spin {
animation: spin 1s linear infinite;
}
.modal-footer {
padding: 20px 24px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
}
.save-btn {
padding: 12px 32px;
border: none;
background: var(--primary-gradient);
border-radius: var(--radius-lg);
color: white;
font-family: inherit;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
transition: all var(--transition-fast);
}
.save-btn:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
/* Toast Notification */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(100px);
padding: 16px 32px;
background: var(--bg-secondary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
font-weight: 600;
z-index: 2000;
opacity: 0;
transition: all var(--transition-normal);
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
.toast.success {
background: var(--success-gradient);
color: white;
}
.toast.error {
background: var(--secondary-gradient);
color: white;
}
.toast.warning {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
color: white;
}
/* Responsive */
@media (max-width: 768px) {
.translation-area {
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
.language-selector {
flex-wrap: wrap;
gap: 12px;
}
.lang-group {
flex: 1;
justify-content: center;
}
.swap-btn {
order: 1;
width: 40px;
height: 40px;
}
}
/* Scrollbar personalizzata */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-tertiary);
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}