push iniziale
This commit is contained in:
297
README.md
297
README.md
@@ -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.
|
||||
|
||||

|
||||
|
||||
## ✨ 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
133
index.html
Normal 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
193
main.js
Normal 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
4103
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
Normal file
41
package.json
Normal 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
7
preload.js
Normal 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
335
renderer.js
Normal 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
599
styles.css
Normal 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%);
|
||||
}
|
||||
Reference in New Issue
Block a user