// ==UserScript==
// @name Gemini AI Inline Translator (Popup)
// @namespace http://tampermonkey.net/
// @version 2.8
// @description Dịch văn bản bôi đen bằng Google Gemini API, có phím tắt, sửa lỗi ký tự, đảm bảo chỉ dịch một lần và có nút đóng. Cải thiện chất lượng dịch. Hỗ trợ phân tích từ vựng (Alt + M) hiển thị trong popup và dịch nhanh (Alt + T).
// @author Your Name
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// ==/UserScript==
(function () {
'use strict';
// API Key của bạn
const API_KEY = 'THAY API KEY VÀO ĐÂY NH MN'; // Thay thế bằng API Key của bạn
const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${API_KEY}`;
// Biến để theo dõi trạng thái dịch
let isTranslating = false;
// Add CSS for the draggable popup
GM_addStyle(`
.draggable {
cursor: move;
}
`);
// Hàm gọi API Gemini để dịch văn bản
function translateText(text, targetElement, isAdvanced = false, displaySimple = false) {
if (isTranslating) return; // Ngăn chặn dịch trùng lặp
isTranslating = true;
// Prompt cải tiến để dịch tự nhiên hơn, không word-by-word
let prompt = `Translate the following text to Vietnamese in a natural and fluent way, avoiding word-by-word translation. Focus on conveying the meaning and intent of the original text, rather than a literal conversion: "${text}"`;
if (isAdvanced) {
prompt = `Translate the following text to Vietnamese in a natural and fluent way, avoiding word-by-word translation. After that, analyze the following text and select 2-5 relevant keywords. For each keyword, provide the definition from the Oxford Dictionary, along with its part of speech, a Vietnamese translation of the definition, synonyms (separated by commas), antonyms (separated by commas). Select an example sentence for each keyword from the original text and provide a Vietnamese translation of that example sentence. Format the output as follows: <br><b>KEYWORD</b>: Keyword<br> + Định nghĩa:<br>Definition from Oxford dictionary<br> - Bản dịch định nghĩa: vietnamese definition <br> + Loại từ: Part of speech <br> + Từ đồng nghĩa: synonym1, synonym2 <br> + Từ trái nghĩa: antonym1, antonym2 <br> + Ví dụ: example sentence from original text<br> - Bản dịch ví dụ: example sentence translation <br> Text to analyze: "${text}"`;
}
const requestBody = {
contents: [
{
role: "user",
parts: [
{
text: prompt,
},
],
},
],
generationConfig: {
temperature: 0.7,
}
};
GM_xmlhttpRequest({
method: 'POST',
url: API_URL,
headers: {
'Content-Type': 'application/json',
},
data: JSON.stringify(requestBody),
onload: function (response) {
if (response.status >= 200 && response.status < 300) {
const result = JSON.parse(response.responseText);
if (result.candidates && result.candidates[0].content.parts[0].text) {
const translatedText = result.candidates[0].content.parts[0].text;
if(isAdvanced) {
displayPopup(translatedText, text); // Show in Popup for Alt+M
} else if (displaySimple) {
displaySimplePopup(text, translatedText); // Show simple Popup for Alt+T
}
else {
showTranslationBelow(targetElement, translatedText); // Show Inline for Ctrl+M
}
} else {
console.error('Lỗi dịch thuật:', result);
showErrorBelow(targetElement, "Lỗi dịch thuật");
}
} else {
console.error('Lỗi API:', response.status, response.statusText);
showErrorBelow(targetElement, "Lỗi API: " + response.status);
}
isTranslating = false; // Đặt lại trạng thái dịch
},
onerror: function (error) {
console.error('Lỗi kết nối API:', error);
showErrorBelow(targetElement, "Lỗi kết nối");
isTranslating = false; // Đặt lại trạng thái dịch
},
});
}
// Hàm hiển thị bản dịch ngay bên dưới đoạn văn bản được bôi đen
function showTranslationBelow(targetElement, translatedText) {
// Tìm đoạn văn cuối cùng được bôi đen
const selection = window.getSelection();
const lastSelectedNode = selection.focusNode;
let lastSelectedParagraph = lastSelectedNode.parentElement;
// Đảm bảo phần tử cuối cùng là một đoạn văn
while (lastSelectedParagraph && lastSelectedParagraph.tagName !== 'P') {
lastSelectedParagraph = lastSelectedParagraph.parentElement;
}
// Nếu không tìm thấy, sử dụng targetElement (đoạn đầu tiên)
if (!lastSelectedParagraph) {
lastSelectedParagraph = targetElement;
}
// Kiểm tra xem đã có bản dịch nào được hiển thị chưa
if (lastSelectedParagraph.nextElementSibling && lastSelectedParagraph.nextElementSibling.classList.contains('translation-div')) {
return; // Nếu đã có bản dịch, không hiển thị thêm
}
const translationDiv = document.createElement('div');
translationDiv.classList.add('translation-div'); // Thêm class để nhận diện
translationDiv.style.marginTop = '10px';
translationDiv.style.padding = '10px';
translationDiv.style.backgroundColor = '#f0f0f0';
translationDiv.style.borderLeft = '3px solid #4CAF50';
translationDiv.style.color = '#333';
translationDiv.style.position = 'relative'; // Thêm thuộc tính position: relative
// Thiết lập font chữ và cỡ chữ
translationDiv.style.fontFamily = 'SF Pro Rounded, sans-serif';
translationDiv.style.fontSize = '16px'; // Cỡ chữ 16px (có thể điều chỉnh từ 15-17px)
translationDiv.textContent = `Dịch: ${translatedText}`;
// Thêm nút đóng (nút "x")
const closeButton = document.createElement('span');
closeButton.textContent = 'x';
closeButton.style.position = 'absolute';
closeButton.style.top = '5px';
closeButton.style.right = '5px';
closeButton.style.cursor = 'pointer';
closeButton.style.color = '#999';
closeButton.style.fontSize = '14px';
closeButton.style.fontWeight = 'bold';
closeButton.addEventListener('click', function () {
translationDiv.remove();
});
translationDiv.appendChild(closeButton);
lastSelectedParagraph.parentNode.insertBefore(translationDiv, lastSelectedParagraph.nextSibling);
}
// Function to display the summary on the webpage with dynamic width and scrollable content
function displayPopup(translatedText, originalText) {
const summaryDiv = document.createElement('div');
summaryDiv.classList.add('draggable');
summaryDiv.style.position = 'fixed';
summaryDiv.style.top = '50%';
summaryDiv.style.left = '50%';
summaryDiv.style.transform = 'translate(-50%, -50%)';
summaryDiv.style.backgroundColor = '#fff';
summaryDiv.style.border = '1px solid #ccc';
summaryDiv.style.padding = '20px';
summaryDiv.style.zIndex = '10000';
summaryDiv.style.width = '700px'; // Wider width
summaryDiv.style.maxHeight = '500px'; // Lower max height
summaryDiv.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.1)';
summaryDiv.style.borderRadius = '15px';
summaryDiv.style.fontFamily = 'SF Pro Rounded, Arial, sans-serif';
summaryDiv.style.fontSize = '16px'; // Font size 16px
summaryDiv.style.display = 'flex';
summaryDiv.style.flexDirection = 'column';
summaryDiv.style.overflow = 'hidden';
// Add summary section with scrollable content
const summarySection = document.createElement('div');
const cleanedSummary = translatedText.replace(/(\*\*)(.*?)\1/g, '<b>$2</b>'); // Remove ** markers
const formattedSummary = cleanedSummary.split('<br>').map(line => {
if (line.startsWith('<b>KEYWORD</b>:')) {
return `<h4 style="margin-bottom: 5px;">${line}</h4>`;
} else if (line.startsWith('+ Định nghĩa:')) {
const parts = line.split(':');
if (parts.length > 1) {
const definition = parts.slice(1).join(':').trim();
const definitionParts = definition.split('<br> - Bản dịch định nghĩa:');
if (definitionParts.length === 2) {
return `<p style="margin-left: 20px; margin-bottom: 5px; white-space: pre-wrap; word-wrap: break-word; text-align: justify;">+ Định nghĩa:<br>${definitionParts[0].trim()}</p><p style="margin-left: 20px; margin-bottom: 10px; white-space: pre-wrap; word-wrap: break-word; text-align: justify;"> - Bản dịch định nghĩa: ${definitionParts[1].trim()}</p>`;
}
}
return `<p style="margin-left: 20px; margin-bottom: 10px; white-space: pre-wrap; word-wrap: break-word; text-align: justify;">${line}</p>`;
} else if (line.startsWith('+ Ví dụ:')) {
const parts = line.split(':');
if (parts.length > 1) {
const example = parts.slice(1).join(':').trim();
const exampleParts = example.split('<br> - Bản dịch ví dụ:');
if (exampleParts.length === 2) {
// Replace example with a sentence from originalText if available
const keywordMatch = line.match(/<b>KEYWORD<\/b>:\s*(\w+)/i);
let exampleFromText = null;
if (keywordMatch)
{
const keyword = keywordMatch[1];
const regex = new RegExp(`\\b${keyword}\\b`, 'i');
const sentences = originalText.split(/[.?!]/).filter(sentence => regex.test(sentence));
if (sentences.length > 0)
exampleFromText = sentences[0].trim() + '.';
}
const displayExample = exampleFromText ? exampleFromText : exampleParts[0].trim();
return `<p style="margin-left: 20px; margin-bottom: 5px; white-space: pre-wrap; word-wrap: break-word; text-align: justify;">+ Ví dụ: ${displayExample}</p><p style="margin-left: 20px; margin-bottom: 10px; white-space: pre-wrap; word-wrap: break-word; text-align: justify;"> - Bản dịch ví dụ: ${exampleParts[1].trim()}</p>`;
}
}
return `<p style="margin-left: 20px; margin-bottom: 10px; white-space: pre-wrap; word-wrap: break-word; text-align: justify;">${line}</p>`;
} else {
return `<p style="margin-left: 20px; margin-bottom: 10px; white-space: pre-wrap; word-wrap: break-word; text-align: justify;">${line}</p>`;
}
}).join('');
summarySection.innerHTML = `<h3 style="color: #333;">Dịch</h3><div style="overflow-y: auto; max-height: 400px; color: #555; font-size: 16px;">${formattedSummary}</div>`; // Font size 16px and lower max-height
summaryDiv.appendChild(summarySection);
// Add close button
const closeButton = document.createElement('button');
closeButton.innerText = 'Đóng';
closeButton.style.marginTop = '10px';
closeButton.style.padding = '8px 16px';
closeButton.style.backgroundColor = '#ff4444';
closeButton.style.color = '#fff';
closeButton.style.border = 'none';
closeButton.style.borderRadius = '4px';
closeButton.style.cursor = 'pointer';
closeButton.onclick = () => summaryDiv.remove();
summaryDiv.appendChild(closeButton);
makeDraggable(summaryDiv);
document.body.appendChild(summaryDiv);
}
// Function to display the simple translation popup
function displaySimplePopup(originalText, translatedText) {
const simplePopupDiv = document.createElement('div');
simplePopupDiv.classList.add('draggable');
simplePopupDiv.style.position = 'fixed';
simplePopupDiv.style.top = '50%';
simplePopupDiv.style.left = '50%';
simplePopupDiv.style.transform = 'translate(-50%, -50%)';
simplePopupDiv.style.backgroundColor = '#fff';
simplePopupDiv.style.border = '1px solid #ccc';
simplePopupDiv.style.padding = '20px';
simplePopupDiv.style.zIndex = '10000';
simplePopupDiv.style.width = '400px'; // Wider width for simple popup
simplePopupDiv.style.maxHeight = '400px'; // Lower max height for simple popup
simplePopupDiv.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.1)';
simplePopupDiv.style.borderRadius = '15px';
simplePopupDiv.style.fontFamily = 'SF Pro Rounded, Arial, sans-serif';
simplePopupDiv.style.fontSize = '16px'; // Font size 16px
simplePopupDiv.style.display = 'flex';
simplePopupDiv.style.flexDirection = 'column';
simplePopupDiv.style.overflow = 'hidden'; // Keep overflow hidden for container, content will scroll
// Translated Text Section (Only translation, no original text)
const translatedTextSection = document.createElement('div');
translatedTextSection.innerHTML = `<div style="white-space: pre-wrap; word-wrap: break-word; text-align: justify; font-size: 16px; overflow-y: auto; max-height: 350px; padding-right: 10px;">${translatedText}</div>`; // Increased font size, added scroll and padding, lower max-height
simplePopupDiv.appendChild(translatedTextSection);
// Close Button
const closeButton = document.createElement('button');
closeButton.innerText = 'Đóng';
closeButton.style.marginTop = '10px';
closeButton.style.padding = '8px 16px';
closeButton.style.backgroundColor = '#ff4444';
closeButton.style.color = '#fff';
closeButton.style.border = 'none';
closeButton.style.borderRadius = '4px';
closeButton.style.cursor = 'pointer';
closeButton.onclick = () => simplePopupDiv.remove();
simplePopupDiv.appendChild(closeButton);
makeDraggable(simplePopupDiv);
document.body.appendChild(simplePopupDiv);
}
// Hàm hiển thị lỗi bên dưới
function showErrorBelow(targetElement, errorMessage) {
// Tìm đoạn văn cuối cùng được bôi đen (tương tự như trên)
const selection = window.getSelection();
const lastSelectedNode = selection.focusNode;
let lastSelectedParagraph = lastSelectedNode.parentElement;
while (lastSelectedParagraph && lastSelectedParagraph.tagName !== 'P') {
lastSelectedParagraph = lastSelectedParagraph.parentElement;
}
if (!lastSelectedParagraph) {
lastSelectedParagraph = targetElement;
}
if (lastSelectedParagraph.nextElementSibling && lastSelectedParagraph.nextElementSibling.classList.contains('translation-div')) {
return;
}
const errorDiv = document.createElement('div');
errorDiv.classList.add('translation-div');
errorDiv.style.marginTop = '10px';
errorDiv.style.padding = '10px';
errorDiv.style.backgroundColor = '#fdd';
errorDiv.style.borderLeft = '3px solid #faa';
errorDiv.style.color = '#a00';
errorDiv.style.fontFamily = 'SF Pro Rounded Medium, sans-serif';
errorDiv.style.fontSize = '16px';
errorDiv.style.position = 'relative'; // Thêm thuộc tính position: relative
errorDiv.textContent = errorMessage;
// Thêm nút đóng (nút "x")
const closeButton = document.createElement('span');
closeButton.textContent = 'x';
closeButton.style.position = 'absolute';
closeButton.style.top = '5px';
closeButton.style.right = '5px';
closeButton.style.cursor = 'pointer';
closeButton.style.color = '#999';
closeButton.style.fontSize = '14px';
closeButton.style.fontWeight = 'bold';
closeButton.addEventListener('click', function () {
errorDiv.remove();
});
errorDiv.appendChild(closeButton);
lastSelectedParagraph.parentNode.insertBefore(errorDiv, lastSelectedParagraph.nextSibling);
}
// Function to make the summary popup draggable
function makeDraggable(element) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
element.onmousedown = dragMouseDown;
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
// get the mouse cursor position at startup
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// calculate the new cursor position:
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// set the element's new position:
element.style.top = (element.offsetTop - pos2) + "px";
element.style.left = (element.offsetLeft - pos1) + "px";
}
function closeDragElement() {
// stop moving when mouse button is released:
document.onmouseup = null;
document.onmousemove = null;
}
}
// Lắng nghe sự kiện phím tắt (Ctrl + M hoặc Cmd + M, Alt + M và Alt + T)
document.addEventListener('keydown', function (event) {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (!selectedText) return;
const targetElement = selection.anchorNode.parentElement;
if ((event.ctrlKey || event.metaKey) && event.key === 'm') {
event.preventDefault(); // Ngăn chặn hành động mặc định của trình duyệt
translateText(selectedText, targetElement);
}
else if (event.altKey && event.key === 'm') {
event.preventDefault();
translateText(selectedText, targetElement, true); // Dịch nâng cao khi ấn Alt + M
}
else if (event.altKey && event.key === 't') {
event.preventDefault();
translateText(selectedText, targetElement, false, true); // Dịch nhanh popup khi ấn Alt + T
}
});
})();