Scrittura Assistita Trasparente Plus

Tono:
💡 Spazio di Scrittura: Clicca nel foglio bianco qui sotto per scrivere. TAB Accetta (da corsivo a tondo)  |  ESC Rifiuta (annulla intervento)
Generazione in corso...
"; const html = preHtml + clone.innerHTML + postHtml; // Specifichiamo MIME word const blob = new Blob(['\ufeff', html], { type: 'application/msword' }); // Forza download come .doc (Librerie come MS Word, LibreOffice, OpenOffice lo apriranno perfettamente // riconoscendo la struttura ed interpretando il foglio di stile locale) const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = "Documento_Assistito.doc"; link.click(); } // Settings Modal function openSettings() { document.getElementById('ai-provider').value = settings.provider; document.getElementById('gemini-api-key').value = settings.geminiKey; document.getElementById('ollama-url').value = settings.ollamaUrl; document.getElementById('ollama-model-hidden').value = settings.ollamaModel; document.getElementById('lmstudio-url').value = settings.lmstudioUrl; document.getElementById('lmstudio-model').value = settings.lmstudioModel; document.getElementById('altro-url').value = settings.altroUrl; document.getElementById('altro-key').value = settings.altroKey; document.getElementById('altro-model').value = settings.altroModel; document.getElementById('test-conn-result').innerText = ''; toggleProviderSettings(); if (settings.provider === 'ollama') fetchOllamaModels(); document.getElementById('settings-modal').style.display = 'flex'; } function closeSettings() { document.getElementById('settings-modal').style.display = 'none'; } function saveSettings() { settings.provider = document.getElementById('ai-provider').value; settings.geminiKey = document.getElementById('gemini-api-key').value; settings.ollamaUrl = document.getElementById('ollama-url').value; settings.ollamaModel = document.getElementById('ollama-model-hidden').value; settings.lmstudioUrl = document.getElementById('lmstudio-url').value; settings.lmstudioModel = document.getElementById('lmstudio-model').value; settings.altroUrl = document.getElementById('altro-url').value; settings.altroKey = document.getElementById('altro-key').value; settings.altroModel = document.getElementById('altro-model').value; localStorage.setItem('sat_provider', settings.provider); localStorage.setItem('sat_gemini_key', settings.geminiKey); localStorage.setItem('sat_ollama_url', settings.ollamaUrl); localStorage.setItem('sat_ollama_model', settings.ollamaModel); localStorage.setItem('sat_lmstudio_url', settings.lmstudioUrl); localStorage.setItem('sat_lmstudio_model', settings.lmstudioModel); localStorage.setItem('sat_altro_url', settings.altroUrl); localStorage.setItem('sat_altro_key', settings.altroKey); localStorage.setItem('sat_altro_model', settings.altroModel); closeSettings(); checkBackgroundConnection(); } function toggleProviderSettings() { const provider = document.getElementById('ai-provider').value; document.querySelectorAll('.provider-setting-block').forEach(el => el.style.display = 'none'); if (provider === 'gemini') document.getElementById('gemini-settings').style.display = 'block'; if (provider === 'ollama') { document.getElementById('ollama-settings').style.display = 'block'; fetchOllamaModels(); } if (provider === 'lmstudio') document.getElementById('lmstudio-settings').style.display = 'block'; if (provider === 'altro') document.getElementById('altro-settings').style.display = 'block'; document.getElementById('test-conn-result').innerText = ''; } async function fetchOllamaModels() { const url = document.getElementById('ollama-url').value; const select = document.getElementById('ollama-model-select'); select.innerHTML = ''; try { const res = await fetch(`${url}/api/tags`); if (!res.ok) throw new Error(); const data = await res.json(); select.innerHTML = ''; if (data.models && data.models.length > 0) { data.models.forEach(m => { const opt = document.createElement('option'); opt.value = m.name; opt.textContent = m.name; select.appendChild(opt); }); const current = document.getElementById('ollama-model-hidden').value; if (data.models.find(m => m.name === current)) { select.value = current; } else { select.value = data.models[0].name; document.getElementById('ollama-model-hidden').value = data.models[0].name; } } else { select.innerHTML = ''; document.getElementById('ollama-model-hidden').value = ''; } } catch (e) { select.innerHTML = ''; document.getElementById('ollama-model-hidden').value = ''; } } async function testConnection() { const provider = document.getElementById('ai-provider').value; const resEl = document.getElementById('test-conn-result'); resEl.style.color = '#1e293b'; resEl.innerText = 'Test in corso...'; try { let success = false; if (provider === 'gemini') { const key = document.getElementById('gemini-api-key').value; if (!key) throw new Error("Inserisci la API Key"); const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${key}`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: "Hello" }] }] }) }); if (response.ok) success = true; else { const e = await response.json(); throw new Error(e.error?.message || "Errore"); } } else if (provider === 'ollama') { const url = document.getElementById('ollama-url').value; const response = await fetch(`${url}/api/tags`); if (response.ok) success = true; else throw new Error(); } else if (provider === 'lmstudio' || provider === 'altro') { const url = provider === 'lmstudio' ? document.getElementById('lmstudio-url').value : document.getElementById('altro-url').value; const key = provider === 'altro' ? document.getElementById('altro-key').value : 'lm-studio'; const response = await fetch(`${url}/models`, { method: 'GET', headers: key ? { 'Authorization': `Bearer ${key}` } : {} }); if (response.ok) success = true; else throw new Error(); } if (success) { resEl.style.color = '#16a34a'; resEl.innerText = '✅ Accessibile'; setIndicatorState('online'); } } catch (e) { resEl.style.color = '#dc2626'; resEl.innerText = '❌ Non Raggiungibile'; setIndicatorState('offline'); } } async function checkBackgroundConnection() { try { let success = false; if (settings.provider === 'gemini') { if (!settings.geminiKey) throw new Error(); // Just basic key format check to avoid wasteful API calls on mount, // or we execute a tiny fast fetch if we explicitly want real verification. // To save usage, we assume online if key is present, let actual gen fail. if (settings.geminiKey.length > 10) success = true; else throw new Error(); } else if (settings.provider === 'ollama') { const response = await fetch(`${settings.ollamaUrl}/api/tags`).catch(() => null); if (response && response.ok) success = true; else throw new Error(); } else if (settings.provider === 'lmstudio' || settings.provider === 'altro') { const url = settings.provider === 'lmstudio' ? settings.lmstudioUrl : settings.altroUrl; const key = settings.provider === 'altro' ? settings.altroKey : 'lm-studio'; const response = await fetch(`${url}/models`, { method: 'GET', headers: key ? { 'Authorization': `Bearer ${key}` } : {} }).catch(() => null); if (response && response.ok) success = true; else throw new Error(); } setIndicatorState(success ? 'online' : 'offline'); } catch (e) { setIndicatorState('offline'); } } function setIndicatorState(state) { const ind = document.getElementById('conn-indicator'); let provName = settings.provider.toUpperCase(); if (state === 'online') { ind.style.backgroundColor = '#22c55e'; // green ind.title = `Connessione: Attiva (${provName})`; } else { ind.style.backgroundColor = '#ef4444'; // red ind.title = `Connessione: Problema Config o Non Raggiungibile (${provName})`; } } // Editor Interactions (TAB / ESC per interventi) editor.addEventListener('keydown', function (e) { // Find if cursor is inside or right after a pending intervention const sel = window.getSelection(); if (!sel.rangeCount) return; // Per facilitare l'uso, troviamo e gestiamo le zone "pending" ovunque con la tastiera, // specialmente se il cursore è dentro. let focusNode = sel.focusNode; let pendingSpan = null; // Check if inside a pending span while (focusNode && focusNode !== editor) { if (focusNode.nodeType === 1 && focusNode.classList.contains('ai-pending')) { pendingSpan = focusNode; break; } focusNode = focusNode.parentNode; } // Se non ne troviamo uno sotto cursore, potremmo prendere il "primo" nel documento per comodità, // ma meglio vincolarlo al focus del cursore if (!pendingSpan) { const nearest = editor.querySelector('.ai-pending'); if (nearest) { pendingSpan = nearest; } } if (pendingSpan) { if (e.key === 'Tab') { e.preventDefault(); // Accetta pendingSpan.classList.remove('ai-pending'); pendingSpan.classList.add('ai-accepted'); pendingSpan.removeAttribute('tabindex'); // Riposiziona cursore DOPO l'etichetta SOLO AD APPROVAZIONE AVVENUTA const range = document.createRange(); range.setStartAfter(pendingSpan); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } else if (e.key === 'Escape') { e.preventDefault(); // Rifiuta const originalText = pendingSpan.getAttribute('data-original') || ''; const textNode = document.createTextNode(originalText); pendingSpan.parentNode.replaceChild(textNode, pendingSpan); // Riposiziona cursore const range = document.createRange(); range.setStartAfter(textNode); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } } }); // AI Interaction Logic function showLoading(show) { document.getElementById('loading-overlay').style.display = show ? 'flex' : 'none'; } async function aiAction(actionType) { const tone = document.getElementById('tone-selector').value; const sel = window.getSelection(); if (!sel.rangeCount) return; const range = sel.getRangeAt(0); let selectedText = sel.toString().trim(); const fullText = editor.innerText; // Determine context if no selection let contextContext = ""; let originalData = selectedText; if (selectedText.length === 0) { if (actionType !== 'SUGGERIMENTO') { alert("Seleziona del testo per applicare questa funzione (es. Riscrivi, Sintetizza)."); return; } // Il Suggerimento usa il testo prima del cursore const preCaretRange = range.cloneRange(); preCaretRange.selectNodeContents(editor); preCaretRange.setEnd(range.endContainer, range.endOffset); let textBefore = preCaretRange.toString(); // Prendi le ultime 500 battute per contesto contextContext = textBefore.slice(-500); } // Costruisci il Prompt let prompt = ""; switch (actionType) { case "SUGGERIMENTO": prompt = `Sei un assistente alla scrittura. Basandoti sul testo precedente: "${contextContext}", continua la frase o suggerisci il prossimo paragrafo. Il tono deve essere ${tone}. Rispondi SOLO con il testo generato.`; break; case "RISCRITTURA": prompt = `Riscrivi il seguente testo: "${selectedText}". Modifica la struttura ma mantieni il significato. Il tono richiesto è ${tone}. Rispondi SOLO con il testo generato.`; break; case "PIANO": prompt = `Riscrivi il seguente testo usando il "linguaggio piano" (plain language): frasi brevi, sintassi semplice, vocabolario comune e comprensibile a tutti. Testo originale: "${selectedText}". Rispondi SOLO con il testo generato.`; break; case "SINTESI": prompt = `Sintetizza in modo conciso il seguente testo, mantenendo le informazioni chiave. Tono ${tone}. Testo originale: "${selectedText}". Rispondi SOLO con il testo generato.`; break; case "AMPLIAMENTO": prompt = `Amplia il seguente testo aggiungendo dettagli rilevanti, esempi o espandendo i concetti. Tono ${tone}. Testo originale: "${selectedText}". Rispondi SOLO con il testo generato.`; break; } let responseText = ""; showLoading(true); try { if (settings.provider === 'gemini') { if (!settings.geminiKey) throw new Error("Inserisci la API Key di Gemini nelle impostazioni."); responseText = await askGemini(prompt, settings.geminiKey); } else if (settings.provider === 'ollama') { responseText = await askOllama(prompt, settings.ollamaUrl, settings.ollamaModel); } else if (settings.provider === 'lmstudio') { responseText = await askOpenAICompatible(prompt, settings.lmstudioUrl, settings.lmstudioModel, 'lm-studio'); } else if (settings.provider === 'altro') { responseText = await askOpenAICompatible(prompt, settings.altroUrl, settings.altroModel, settings.altroKey); } // Pulizia minima della risposta responseText = responseText.replace(/^"|"$/g, '').trim(); // Build Final Text with Label const textWithLabel = `${responseText} [${actionType}]`; // Create Span Element Node const spanNode = document.createElement('span'); spanNode.className = 'ai-node ai-pending'; spanNode.style.color = 'red'; // Forza il colore inline cross-editor per export // Mantiene il testo originale (fuggiamo le virgolette in caso servisse) spanNode.setAttribute('data-label', `[${actionType}]`); spanNode.setAttribute('data-original', originalData); spanNode.innerText = textWithLabel; // Insert into Editor if (selectedText.length > 0) { range.deleteContents(); } range.insertNode(spanNode); // Move caret inside the span, at the very end // Manteniamo il cursore *all'interno* per facilitare l'accettazione TAB const newRange = document.createRange(); newRange.selectNodeContents(spanNode); newRange.collapse(false); // false = crolla alla fine (dentro il nodo) sel.removeAllRanges(); sel.addRange(newRange); } catch (error) { console.error(error); alert("Errore AI: " + error.message); } finally { showLoading(false); } } // --- API INTEGRATIONS --- async function askGemini(prompt, apiKey) { const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: { temperature: 0.7 } }) }); if (!response.ok) { const err = await response.json(); throw new Error(err.error?.message || "Errore Gemini API"); } const data = await response.json(); return data.candidates[0].content.parts[0].text; } async function askOllama(prompt, url, model) { const endpoint = `${url}/api/generate`; const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: model, prompt: prompt, stream: false }) }); if (!response.ok) { throw new Error("Impossibile connettersi ad Ollama. Verifica che sia avviato e le policy CORS."); } const data = await response.json(); return data.response; } async function askOpenAICompatible(prompt, url, model, apiKey) { const endpoint = `${url}/chat/completions`; const headers = { 'Content-Type': 'application/json' }; if (apiKey) { headers['Authorization'] = `Bearer ${apiKey}`; } const response = await fetch(endpoint, { method: 'POST', headers: headers, body: JSON.stringify({ model: model || "local-model", messages: [ { role: "system", content: "Sei un utile assistente che risponde sempre e solo con il testo richiesto, senza preamboli, senza spiegazioni, senza virgolette aggiuntive." }, { role: "user", content: prompt } ], temperature: 0.7 }) }); if (!response.ok) { throw new Error("Impossibile connettersi all'API. Verifica l'URL, la Key e che il servizio sia avviato."); } const data = await response.json(); return data.choices[0].message.content; }