kiến thức Dùng AI để dịch mọi thứ

  • Người tạo chủ đề Người tạo chủ đề ChuSyThang21
  • Ngày bắt đầu Ngày bắt đầu
Mã:
// ==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';

    // Cấu hình API
    const API_CONFIG = {
        providers: {
            gemini: {
                url: (apiKey) => `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite-preview-02-05:generateContent?key=${apiKey}`,
                headers: {
                    'Content-Type': 'application/json'
                },
                body: (prompt) => ({
                    contents: [{
                        role: "user",
                        parts: [{ text: prompt }]
                    }],
                    generationConfig: { temperature: 0.7 }
                }),
                responseParser: (response) => {
                    if (!response.candidates || response.candidates.length === 0) {
                        throw new Error('Gemini API: No candidates in response');
                    }
                    if (!response.candidates[0].content?.parts?.[0]?.text) {
                        throw new Error('Gemini API: Invalid response format');
                    }
                    return response.candidates[0].content.parts[0].text;
                }
            },
            openai: {
                url: () => 'https://api.groq.com/openai/v1/chat/completions',
                headers: (apiKey) => ({
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${apiKey}`
                }),
                body: (prompt) => ({
                    model: "llama-3.3-70b-versatile",
                    messages: [{ role: "user", content: prompt }],
                    temperature: 0.7
                }),
                responseParser: (response) => response.choices?.[0]?.message?.content
            }
        },
        currentProvider: 'gemini',
        //apiKey: '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', // OpenAI completion api
        apiKey: 'AIzaSCqCxFH32-luLxrdPH9p5FEmxk', // Gemini
        maxRetries: 3,
        retryDelay: 1000,
        rateLimit: {
            maxRequests: 5,
            perMilliseconds: 10000
        }
    };

    // Biến trạng thái
    let isTranslating = false;
    let requestQueue = [];
    let requestCount = 0;
    let lastRequestTime = 0;

     // Add CSS for the draggable popup
    GM_addStyle(`
        .draggable {
          cursor: move;
        }
    `);

    // Hàm gọi API Gemini để dịch văn bản
     async function translateText(text, targetElement, isAdvanced = false, displaySimple = false) {
        // Thêm vào hàng đợi và xử lý tuần tự
        return new Promise((resolve) => {
            requestQueue.push(async () => {
                try {
                    if (isTranslating) return;
                    isTranslating = true;

                    // Kiểm tra rate limiting
                    const now = Date.now();
                    if (now - lastRequestTime < API_CONFIG.rateLimit.perMilliseconds) {
                        if (requestCount >= API_CONFIG.rateLimit.maxRequests) {
                            const delay = API_CONFIG.rateLimit.perMilliseconds - (now - lastRequestTime);
                            await new Promise(res => setTimeout(res, delay));
                            requestCount = 0;
                        }
                    } else {
                        requestCount = 0;
                        lastRequestTime = now;
                    }

                    // Tạo prompt
                    //let prompt = `Translate to Vietnamese naturally: "${text}"`;
                    let prompt = `Translate "${text}" to Vietnamese with:
                                  - Strict adherence to original context and nuances
                                  - Natural fluency as spoken by native speakers
                                  - No added explanations/interpretations
                                  - 1:1 structure preservation for terms/proper nouns
                                  Output only the translation without quotation marks`;
                    if (isAdvanced) {
                        prompt = `Translate and analyze keywords: "${text}"`;
                    }

                    const provider = API_CONFIG.providers[API_CONFIG.currentProvider];
                    let translatedText = '';
                    let attempts = 0;

                    // Retry logic với exponential backoff
                    while (attempts < API_CONFIG.maxRetries) {
                        try {
                            translatedText = await new Promise((resolve, reject) => {
                                GM_xmlhttpRequest({
                                    method: 'POST',
                                    url: provider.url(API_CONFIG.apiKey),
                                    headers: typeof provider.headers === 'function' ? provider.headers(API_CONFIG.apiKey) : provider.headers,
                                    data: JSON.stringify(provider.body(prompt)),
                                    onload: function(response) {
                                        if (response.status >= 200 && response.status < 300) {
                                            const result = JSON.parse(response.responseText);
                                            const text = provider.responseParser(result);
                                            text ? resolve(text) : reject('Invalid response format');
                                        } else if (response.status === 429) {
                                            reject('Rate limit exceeded');
                                        } else {
                                            reject(`API Error: ${response.status}`);
                                        }
                                    },
                                    onerror: function(error) {
                                        reject(`Connection error: ${error}`);
                                    }
                                });
                            });

                            requestCount++;
                            break; // Thoát vòng lặp nếu thành công
                        } catch (error) {
                            attempts++;
                            if (attempts >= API_CONFIG.maxRetries) throw error;
                            await new Promise(res =>
                                setTimeout(res, API_CONFIG.retryDelay * Math.pow(2, attempts))
                            );
                        }
                    }

                    // Hiển thị kết quả
                    if (isAdvanced) {
                        displayPopup(translatedText, text);
                    } else if (displaySimple) {
                        displaySimplePopup(text, translatedText);
                    } else {
                        showTranslationBelow(targetElement, translatedText);
                    }
                } catch (error) {
                    console.error('Translation failed:', error);
                    const errorMessage = error instanceof Error ? error.message : String(error);
                    showErrorBelow(targetElement,
                        errorMessage.includes('Rate limit') ? 'Vui lòng chờ giữa các lần dịch' :
                        errorMessage.includes('Gemini API') ? 'Lỗi Gemini: ' + errorMessage :
                        errorMessage.includes('API Key') ? 'Lỗi xác thực API' :
                        'Lỗi dịch thuật: ' + errorMessage);
                } finally {
                    isTranslating = false;
                    requestQueue.shift();
                    if (requestQueue.length > 0) requestQueue[0]();
                }
            });

            if (!isTranslating && requestQueue.length === 1) {
                requestQueue[0]();
            }
        });
    }


      // 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);
    }


    // Cache các bản dịch
    const translationCache = new Map();
    const CACHE_EXPIRATION = 300000; // 5 phút

    // Hàm thêm vào cache
    function addToCache(original, translated) {
        translationCache.set(original, {
            text: translated,
            timestamp: Date.now()
        });
    }

    // Hàm kiểm tra cache
    function checkCache(text) {
        const entry = translationCache.get(text);
        if (entry && Date.now() - entry.timestamp < CACHE_EXPIRATION) {
            return entry.text;
        }
        translationCache.delete(text);
        return null;
    }

    // Hàm hiển thị lỗ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 + Q hoặc Cmd + Q, Alt + Q 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 === 'q') {
             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 === 'q') {
           event.preventDefault();
            translateText(selectedText, targetElement, true); // Dịch nâng cao khi ấn Alt + Q
        }
        else if (event.altKey && event.key === 't') {
            event.preventDefault();
            translateText(selectedText, targetElement, false, true); // Dịch nhanh popup khi ấn Alt + T
        }
    });


})();
em mới chỉnh lại prompt với sử dụng model gemini-2.0-flash-lite-preview-02-05 cho nó mạnh, nhanh. H bác chỉ thay API gemini vào test lại thử xem
ngon lành r bác
API bị 429 do prompt của bác thớt quá dài, mà sử dụng bản free nên sẽ bị 429.
Muốn prompt dài mà ko bị 429 thì solution là tạo nhiều apiKey của Germini (cỡ 20 cái) rồi cho random get apiKey (hoặc thuật toán không trùng trong thời gian nào khác) ngay đoạn tạo provider.url(API_CONFIG.apiKey) => nếu retry sẽ lấy apiKey khác theo code của bác @VooDanh
hóng code của bác
 
mình dùng rất ngon,,, dịch quá tuyệt nhưng đôi khi báo lỗi Lỗi API: 429 . Mình có gg thì kêu là do "vượt quá giới hạn tần suất yêu cầu của API" nhưng lâu lâu mình mới dùng chứ k phải dùng liên tục. API là acc gg của mình mới tạo dùng cho cái này
mình cũng bị lỗi này
 
bác cho em hỏi dùng cái này dịch được mấy đoạn văn phong 18+ không hay vẫn bị giới hạn như mấy con chatgpt web vậy
  • xài của mấy pháp sư được cái chắc không bị block mấy cái woke hay 18+, chứ dùng chatGPT hay gemini nó hay block safety lắm.
  • Nhưng dùng cho việc chơi chơi thôi chứ có dữ liệu gì thì cũng run.
 
Mã:
// ==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';

    // Cấu hình API
    const API_CONFIG = {
        providers: {
            gemini: {
                url: (apiKey) => `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite-preview-02-05:generateContent?key=${apiKey}`,
                headers: {
                    'Content-Type': 'application/json'
                },
                body: (prompt) => ({
                    contents: [{
                        role: "user",
                        parts: [{ text: prompt }]
                    }],
                    generationConfig: { temperature: 0.7 }
                }),
                responseParser: (response) => {
                    if (!response.candidates || response.candidates.length === 0) {
                        throw new Error('Gemini API: No candidates in response');
                    }
                    if (!response.candidates[0].content?.parts?.[0]?.text) {
                        throw new Error('Gemini API: Invalid response format');
                    }
                    return response.candidates[0].content.parts[0].text;
                }
            },
            openai: {
                url: () => 'https://api.groq.com/openai/v1/chat/completions',
                headers: (apiKey) => ({
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${apiKey}`
                }),
                body: (prompt) => ({
                    model: "llama-3.3-70b-versatile",
                    messages: [{ role: "user", content: prompt }],
                    temperature: 0.7
                }),
                responseParser: (response) => response.choices?.[0]?.message?.content
            }
        },
        currentProvider: 'gemini',
        //apiKey: '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', // OpenAI completion api
        apiKey: 'AIzaSCqCxFH32-luLxrdPH9p5FEmxk', // Gemini
        maxRetries: 3,
        retryDelay: 1000,
        rateLimit: {
            maxRequests: 5,
            perMilliseconds: 10000
        }
    };

    // Biến trạng thái
    let isTranslating = false;
    let requestQueue = [];
    let requestCount = 0;
    let lastRequestTime = 0;

     // Add CSS for the draggable popup
    GM_addStyle(`
        .draggable {
          cursor: move;
        }
    `);

    // Hàm gọi API Gemini để dịch văn bản
     async function translateText(text, targetElement, isAdvanced = false, displaySimple = false) {
        // Thêm vào hàng đợi và xử lý tuần tự
        return new Promise((resolve) => {
            requestQueue.push(async () => {
                try {
                    if (isTranslating) return;
                    isTranslating = true;

                    // Kiểm tra rate limiting
                    const now = Date.now();
                    if (now - lastRequestTime < API_CONFIG.rateLimit.perMilliseconds) {
                        if (requestCount >= API_CONFIG.rateLimit.maxRequests) {
                            const delay = API_CONFIG.rateLimit.perMilliseconds - (now - lastRequestTime);
                            await new Promise(res => setTimeout(res, delay));
                            requestCount = 0;
                        }
                    } else {
                        requestCount = 0;
                        lastRequestTime = now;
                    }

                    // Tạo prompt
                    //let prompt = `Translate to Vietnamese naturally: "${text}"`;
                    let prompt = `Translate "${text}" to Vietnamese with:
                                  - Strict adherence to original context and nuances
                                  - Natural fluency as spoken by native speakers
                                  - No added explanations/interpretations
                                  - 1:1 structure preservation for terms/proper nouns
                                  Output only the translation without quotation marks`;
                    if (isAdvanced) {
                        prompt = `Translate and analyze keywords: "${text}"`;
                    }

                    const provider = API_CONFIG.providers[API_CONFIG.currentProvider];
                    let translatedText = '';
                    let attempts = 0;

                    // Retry logic với exponential backoff
                    while (attempts < API_CONFIG.maxRetries) {
                        try {
                            translatedText = await new Promise((resolve, reject) => {
                                GM_xmlhttpRequest({
                                    method: 'POST',
                                    url: provider.url(API_CONFIG.apiKey),
                                    headers: typeof provider.headers === 'function' ? provider.headers(API_CONFIG.apiKey) : provider.headers,
                                    data: JSON.stringify(provider.body(prompt)),
                                    onload: function(response) {
                                        if (response.status >= 200 && response.status < 300) {
                                            const result = JSON.parse(response.responseText);
                                            const text = provider.responseParser(result);
                                            text ? resolve(text) : reject('Invalid response format');
                                        } else if (response.status === 429) {
                                            reject('Rate limit exceeded');
                                        } else {
                                            reject(`API Error: ${response.status}`);
                                        }
                                    },
                                    onerror: function(error) {
                                        reject(`Connection error: ${error}`);
                                    }
                                });
                            });

                            requestCount++;
                            break; // Thoát vòng lặp nếu thành công
                        } catch (error) {
                            attempts++;
                            if (attempts >= API_CONFIG.maxRetries) throw error;
                            await new Promise(res =>
                                setTimeout(res, API_CONFIG.retryDelay * Math.pow(2, attempts))
                            );
                        }
                    }

                    // Hiển thị kết quả
                    if (isAdvanced) {
                        displayPopup(translatedText, text);
                    } else if (displaySimple) {
                        displaySimplePopup(text, translatedText);
                    } else {
                        showTranslationBelow(targetElement, translatedText);
                    }
                } catch (error) {
                    console.error('Translation failed:', error);
                    const errorMessage = error instanceof Error ? error.message : String(error);
                    showErrorBelow(targetElement,
                        errorMessage.includes('Rate limit') ? 'Vui lòng chờ giữa các lần dịch' :
                        errorMessage.includes('Gemini API') ? 'Lỗi Gemini: ' + errorMessage :
                        errorMessage.includes('API Key') ? 'Lỗi xác thực API' :
                        'Lỗi dịch thuật: ' + errorMessage);
                } finally {
                    isTranslating = false;
                    requestQueue.shift();
                    if (requestQueue.length > 0) requestQueue[0]();
                }
            });

            if (!isTranslating && requestQueue.length === 1) {
                requestQueue[0]();
            }
        });
    }


      // 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);
    }


    // Cache các bản dịch
    const translationCache = new Map();
    const CACHE_EXPIRATION = 300000; // 5 phút

    // Hàm thêm vào cache
    function addToCache(original, translated) {
        translationCache.set(original, {
            text: translated,
            timestamp: Date.now()
        });
    }

    // Hàm kiểm tra cache
    function checkCache(text) {
        const entry = translationCache.get(text);
        if (entry && Date.now() - entry.timestamp < CACHE_EXPIRATION) {
            return entry.text;
        }
        translationCache.delete(text);
        return null;
    }

    // Hàm hiển thị lỗ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 + Q hoặc Cmd + Q, Alt + Q 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 === 'q') {
             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 === 'q') {
           event.preventDefault();
            translateText(selectedText, targetElement, true); // Dịch nâng cao khi ấn Alt + Q
        }
        else if (event.altKey && event.key === 't') {
            event.preventDefault();
            translateText(selectedText, targetElement, false, true); // Dịch nhanh popup khi ấn Alt + T
        }
    });


})();
em mới chỉnh lại prompt với sử dụng model gemini-2.0-flash-lite-preview-02-05 cho nó mạnh, nhanh. H bác chỉ thay API gemini vào test lại thử xem
bác thử alt+q chưa, sao của em nó giải nghĩa keywords nhưng không dịch mà vẫn ngôn ngữ đó.

edit: em tự chỉnh lại rồi
 
Sửa lần cuối:
Bọn gemini dịch truyện bố đời quá.
]
Chương Tám

"Thằng khốn, nhấc cái mông thối của mày lên. Tao đéo có hơi đâu mà vác hết đống đá chết tiệt này một mình!"

Troy, vừa rên rỉ, vừa lảo đảo về phía chỗ trú, vác theo một tảng đá lớn, nặng trịch. Mất ngủ, hắn đã dậy sau khi những người khác rời đi và bắt đầu xây một hàng rào thô sơ quanh nơi trú ẩn để ngăn nước tràn vào nếu cơn bão trở nên tồi tệ. Stuart lẽo đẽo theo sau, quay phim lại những nỗ lực của Troy, trong khi Stefan nằm ườn bên cạnh hố lửa, mắt nhắm nghiền.

"Nói tao nghe, Troy. Mày có thực sự cần phải điểm xuyết từng câu nói của mày bằng một từ chửi thề không?"

"Đéo sai. Đấy là cách tao nói. Muốn làm gì nhau à? Tao có xúc phạm đến cái màng nhĩ mỏng manh chết tiệt của mày không?"

"Tao chắc mẹ mày tự hào lắm."

"Này, đừng có lôi mẹ tao vào, thằng chó."

"Làm ơn im lặng." Stefan cau mày, xua tay. "Mày không thấy tao đang bận à?"

Vật lộn với gánh nặng, Troy dừng lại, chỉnh lại tư thế cầm tảng đá.

"Ồ, phải," hắn khịt mũi. "Bận cái gì?"

"Tao đang suy nghĩ. Thỉnh thoảng mày nên thử đi. Nó giải phóng vãi cả ***."

"Tao đang suy nghĩ. Suy nghĩ về việc đá cái chân chết tiệt của tao vào mông mày."

Stefan lật úp người xuống. Đất và lá bám đầy lưng hắn. Hắn chống cằm lên tay, mỉm cười với Troy.

"Láo xược thế. Đấy có phải là cách mày nói chuyện với người duy nhất trên hòn đảo này có thể kiếm cho mày một điếu thuốc không?"

"Mày có à?"

"Không, tao bỏ thuốc nhiều năm rồi—thói quen kinh tởm. Nhưng tao biết có người có."

"Đừng có xạo *** với tao, thằng khốn."

"Tao thề. Có người trên đảo này có thuốc lá."

"Ai? Đám quay phim à? Chúng nó không được phép cho bọn tao cái gì cả. Tao thử rồi. Lũ chó chết còn quay phim cảnh tao van xin một điếu."

"Tao muốn xem đoạn phim đó đấy. Nhưng để trả lời câu hỏi của mày, không, không phải đám quay phim. Một trong những người cùng chơi của chúng ta mang thuốc lá làm vật dụng xa xỉ, và họ đã dùng lửa trại để châm thuốc khi mọi người khác đang ngủ. Họ giới hạn chỉ hút một điếu mỗi ngày, để bao thuốc dùng được lâu."

"Cút mẹ mày đi."

"Tao nói nghiêm túc."

"Ai?"

Stefan ngập ngừng. "Thằng mọi đen. Raul."

Troy đánh rơi tảng đá. Nó rơi thịch xuống đất, suýt nữa thì đè vào ngón chân hắn, lăn vài mét rồi dừng lại, tựa vào một thân cây. Hắn không để ý. Sự chú ý của hắn dồn hết vào Stefan.

"Mày có nhận ra mày vừa nói gì trên truyền hình quốc gia không? Mày quên mất là mày đang bị quay phim à?"

"Thì sao?" Stefan nhún vai. "Tao không ở đây để giành lấy trái tim và khối óc của nước Mỹ. Tao ở đây để chơi một trò chơi. Và dù sao thì đài truyền hình cũng sẽ cắt phần đó đi."

"Mày đúng là một thằng khốn nạn, thằng chó ạ." Stefan mỉm cười. "Sao lại thế?"

"Không chỉ là một thằng lười biếng, mày còn là một thằng phân biệt chủng tộc. Tao cứ tưởng mày với Raul thân thiết, nhưng hóa ra, mày chỉ lợi dụng nó, phải không? Tao cá là nó sẽ không thích nếu nghe mày gọi nó như thế. Bạn bè chó má gì mày."

"Nhảm nhí. Tao không ở đây để kết bạn, Troy. Tao ở đây để thắng một trò chơi."

"Ừ? Rồi xem Raul nói gì về chuyện đó khi nó quay lại. Mày sẽ đéo thắng được cái gì khi mọi người quay lưng lại với mày. Thằng khốn nạn."

Stefan phớt lờ lời đe dọa. "Raul mang theo một bao thuốc lá làm vật dụng xa xỉ của mình. Nó không nghĩ ai trong số những người khác biết về điều đó, nhưng tao biết. Chắc chắn là nó sẽ không cho mày một điếu, chứ đừng nói đến việc thừa nhận nó có thuốc. Thỏa thuận với tao, và tao sẽ kiếm cho mày một điếu."

"Thỏa thuận kiểu gì?"

"Mày phải thề là sẽ không bỏ phiếu chống lại tao, nếu mày có cơ hội."

"Làm sao tao có thể? Tất cả chúng ta đều biết mày và lũ chó săn chết tiệt của mày đang nhắm vào tao trong cuộc bỏ phiếu trục xuất tiếp theo."

"Có lẽ." Stefan ngừng lại. "Hoặc có lẽ đó chỉ là những gì bọn tao muốn mọi người tin, để bọn tao có thể bắt ai đó mất cảnh giác và trục xuất họ. Xét cho cùng, mày cũng chẳng phải là mối đe dọa lớn lao gì."

Cau mày, Troy quay đi, đá xuống đất. Rồi hắn quay ngoắt lại về phía Stefan, nắm đấm siết chặt. Stefan bình tĩnh đứng dậy. Stuart lia máy quay lại gần hơn, cố gắng tránh đường.

"Mày biết không," Troy nhổ nước bọt, chỉ một ngón tay cáu bẩn vào Stefan, "ở Seattle, bọn tao gặp những thằng như mày ở tiệm suốt, thằng khốn ạ. Chúng nó mang chiếc BMW của mình đến để thay dầu và mong đợi nó được thực hiện trong vòng năm phút chết tiệt. Muốn bọn tao bỏ bất cứ việc gì đang làm và chỉ tập trung vào xe của chúng nó."

"Thực ra, tao lái Lexus. Kỹ thuật trên những chiếc BMW gần đây bị đánh giá quá cao."

"Mày sai rồi. Và đó đéo phải là vấn đề!"

"Vậy thì, làm ơn nói rõ vấn đề của mày đi."

"Vài tháng trước, một thằng như mày đến với một miếng đệm đầu xi lanh bị thổi. Một miếng đệm đầu xi lanh chết tiệt bị thổi. Hắn không nghĩ đó là vấn đề. Hắn muốn tao sửa nó ngay lập tức, và khi tao nói tao không thể—rằng hắn cần một miếng đệm đầu xi lanh mới hoàn toàn—thằng ngu chết tiệt này trở nên hợm hĩnh với tao. Hắn khăng khăng rằng nó có thể được sửa mà không cần cái đó. Nói rằng tất cả những gì hắn cần là một sự điều chỉnh chết tiệt. Thằng khốn biết cái đéo gì về động cơ. Khi tao nói lại với hắn rằng không có cách nào cả, thằng khốn, hắn muốn biết tại sao không. Mày có biết tao nói gì với hắn không?"

"Điều gì đó sâu sắc, tao chắc chắn."

"Đéo sai. Tao nói với hắn, 'thằng khốn chết tiệt đó đã hỏng mẹ nó rồi.'"

"Và vấn đề của mày là gì?"

"Mày cũng vậy, thằng khốn lái Lexus." Nụ cười của Stefan chao đảo, rồi trở lại. "Đó có phải là một lời đe dọa không?"

"Nó là thế đấy."

Nụ cười của Stefan biến mất. Mặt hắn đỏ bừng. Hắn đứng lên từ từ và tiến một bước về phía Troy. Gã thợ máy không lùi bước.

"Mày muốn gì?" Troy nắm chặt tay thành nắm đấm. "Lên đi, con đĩ."

"Tao sẽ làm." Stefan nhích lại gần hơn. "Mày cần phải được dạy một số phép tắc, thằng lùn bẩn thỉu."

"Không phải bởi mày, thằng đầu ****. Và không phải hôm nay."

"Ồ, phải không? Mày nghĩ vậy sao? Vậy thì mày nhầm rồi, bạn của tao. Tao vượt trội hơn mày trong một trận chiến trí tuệ, và tao chắc chắn cũng có thể đánh bại mày trong một trận đấu thể chất. Giờ học bắt đầu rồi. Hãy coi đây là bài học đầu tiên của mày."

Ngáp dài, Troy chỉnh lại mũ. "Mày muốn nói cả ngày, hay là chúng ta sẽ đánh nhau?"

Hai người đàn ông tiến lại gần nhau trong gang tấc. Stefan trừng mắt, ưỡn ngực. Troy cười toe toét. Stuart nín thở.

"Đồ con ***."

"Tao sẽ rất vui lòng xóa cái nụ cười đó khỏi khuôn mặt xấu xí của mày."

Stuart trụ chân và phóng to hơn, lo lắng cố gắng tránh đường cho nắm đấm của cả hai người. Một cuộc chiến sắp xảy ra. Hắn liếm môi chờ đợi. Không một lần ý nghĩ can thiệp lướt qua tâm trí hắn. Đây là vàng ròng về rating.

Stefan nghiêng người về phía trước, mũi gần như chạm vào mũi Troy. "Tao sẽ tận hưởng điều này."

"Sao cũng được."

"Chúng ta sẽ xem mày cảm thấy thế nào khi tao hất cái mũ đó ra khỏi đầu mày."

Troy nhún vai. "Làm hoặc im mẹ mồm đi. Sắp hết ngày rồi, thằng khốn."

Cây cỏ ở rìa trại xào xạc, và Raul, Pauline, Jeff, Becka và Jerry xuất hiện từ khu rừng. Mọi người trừ Pauline đều đang ôm củi khô.

"Chúng ta có củi rồi. Chắc là đủ để qua đêm, nếu cơn bão không..." Raul im bặt, nhìn Stefan và Troy bối rối.

Stuart lia máy quay sang để bắt lấy biểu cảm của cả nhóm và sau đó lùi lại, cố gắng bắt trọn cả nhóm, cùng với Stefan và Troy.

"Chuyện gì đang xảy ra vậy?" Jeff hỏi. "Mọi thứ ổn chứ?"

Stefan liếc nhìn những người khác. Troy vẫn tiếp tục nhìn chằm chằm vào hắn, không chớp mắt.

"Không có gì," Stefan nói. "Troy và tao chỉ đang thảo luận về cơn bão sắp tới. Có đúng không, Troy?"

"Mày nói gì cũng đúng, thằng đầu ****. Mày nói gì cũng đúng." Cười khẩy, hắn lắc đầu và quay sang những người khác. "Có cần giúp xếp đống củi đó không?"

"Chắc chắn rồi," Jeff nói. "Cứ tự nhiên."

Stefan nghiêng người về phía trước và thì thầm vào tai Troy, "Mày nên nhớ những gì tao nói, nếu mày muốn điếu thuốc đó."

Phớt lờ hắn, Troy đỡ lấy gánh nặng của Becka và mang củi đến hố lửa.

"Cảm ơn." Vai cô trĩu xuống.

"Không có gì."

Stefan tuyên bố, "Tao đi tè phát." "Nghĩa là gì?" Raul hỏi. "Như cách bọn Mỹ các người nói, tao phải đi đái." Hắn biến mất vào bụi cây xung quanh trại.

"Tao tưởng mày đang ngủ trưa," Jerry nói với Troy. "Thay đổi ý định à?"

Troy bắt chước giọng Anh. "Và bỏ lỡ cuộc trò chuyện chết tiệt đầy thú vị này với người bạn cùng chơi của tao sao? Không đời nào. Chào nhé, anh bạn."

"Đó là giọng giả tệ nhất mà tao từng nghe," Pauline trêu chọc. "Mày nghe như Dick Van Dyke trong Mary Poppins."

Troy nháy mắt với cô. "Chà, có lẽ khi chương trình này kết thúc, tao sẽ kiếm một công việc quét ống khói chết tiệt."

"Nếu mày thắng, tao sẽ cho mày quét ống khói của tao."

Becka và Jerry liếc nhìn nhau. Becka đảo mắt.

"Cẩn thận với những gì mày ước," Troy nói đùa. "Mày có thể đạt được nó đấy."

Stefan quay lại từ bụi cây và ngồi xuống quanh hố lửa. Pauline ngồi cạnh hắn. Becka chọn một chỗ ngồi ở phía bên kia hố lửa. Troy đổ gục xuống bên cạnh cô, thở dài. Raul, Jeff và Jerry dỡ củi của họ xuống và sau đó tham gia cùng họ. Stuart lảng vảng ở phía sau, quay phim.

"Chà," Becka thì thầm với Troy, "tao không biết chuyện gì đang xảy ra, nhưng mày có vẻ đang có tâm trạng tốt hơn."

Troy ngước nhìn bầu trời tối sầm. "Tao có thể nói gì đây? Tao thích thời tiết này. Nó phù hợp với tâm trạng chết tiệt của tao."

Raul gật đầu về phía khu rừng. "Ước gì Richard và Sal quay lại với bữa tối. Tao đói meo rồi."

"Tao chắc là họ sẽ sớm quay lại thôi," Jerry nói. "Hy vọng, Ryan, Shonette và Roberta cũng sẽ không về muộn."

Vẫn đang quay phim, Stuart nhận thấy rằng cả nhóm một lần nữa đã quên mất Matthew. Mặc dù hắn không nói to, hắn hy vọng Mark và Jesse sẽ sớm quay lại. Không còn nghi ngờ gì nữa, Ivan sẽ tác động đến vị trí của họ. Hắn không thích ý tưởng họ ở ngoài rừng khi cơn bão ập đến. Với thời tiết ngày càng xấu đi, các cuộc phỏng vấn với Stefan và Roberta sẽ phải dời lại, nhưng không sao, miễn là mọi người đều an toàn và đầy đủ.
Raul định nói, nhưng tiếng sấm rền vang ở phía xa và hắn im bặt. Khu rừng tối sầm lại. Gió tăng lên, rít qua những hàng cây.
"Ôi chết tiệt," Troy chửi thề. "Tao chưa xây xong hàng rào.
[/SPOILER
 
bác thử alt+q chưa, sao của em nó giải nghĩa keywords nhưng không dịch mà vẫn ngôn ngữ đó.

edit: em tự chỉnh lại rồi
mình chỉnh rồi thêm luôn nút biểu tượng nhỏ để dịch trên cả Pc lẫn Moblie (vẫn hỗ trợ phím tắt).
1. select text rồi nhấn nút dịch sẽ tương đương: Ctrl+t
2. select text rồi giữ nút dịch (trên 500ms) sẽ tương đương phím tắt Alt+q

ai cần thì thêm nhé.

JavaScript:
// ==UserScript==
// @name         Gemini AI Inline Translator (Popup)
// @namespace    Violentmonkey Scripts
// @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       Voodanh, king1x32
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  "use strict";

  // Cấu hình API
  const API_CONFIG = {
    providers: {
      gemini: {
        url: (apiKey) =>
          `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite-preview-02-05:generateContent?key=${apiKey}`,
        headers: {
          "Content-Type": "application/json",
        },
        body: (prompt) => ({
          contents: [
            {
              role: "user",
              parts: [{ text: prompt }],
            },
          ],
          generationConfig: { temperature: 0.7 },
        }),
        responseParser: (response) => {
          if (!response.candidates || response.candidates.length === 0) {
            throw new Error("Gemini API: No candidates in response");
          }
          if (!response.candidates[0].content?.parts?.[0]?.text) {
            throw new Error("Gemini API: Invalid response format");
          }
          return response.candidates[0].content.parts[0].text;
        },
      },
      openai: {
        url: () => "https://api.groq.com/openai/v1/chat/completions",
        headers: (apiKey) => ({
          "Content-Type": "application/json",
          Authorization: `Bearer ${apiKey}`,
        }),
        body: (prompt) => ({
          model: "llama-3.3-70b-versatile",
          messages: [{ role: "user", content: prompt }],
          temperature: 0.7,
        }),
        responseParser: (response) => response.choices?.[0]?.message?.content,
      },
    },
    currentProvider: "gemini",
    apiKey: "AIzaSCqCxFH32-luLxrdPH9p5FEmxk", // Gemini
    maxRetries: 3,
    retryDelay: 1000,
    rateLimit: {
      maxRequests: 5,
      perMilliseconds: 10000,
    },
  };

  // Biến trạng thái
  let isTranslating = false;
  let requestQueue = [];
  let requestCount = 0;
  let lastRequestTime = 0;

  // Add CSS for the draggable popup
  GM_addStyle(`
        .draggable {
          cursor: move;
        }
    `);

  // Hàm gọi API Gemini để dịch văn bản
  async function translateText(
    text,
    targetElement,
    isAdvanced = false,
    displaySimple = false,
  ) {
    // Thêm vào hàng đợi và xử lý tuần tự
    return new Promise((resolve) => {
      requestQueue.push(async () => {
        try {
          if (isTranslating) return;
          isTranslating = true;

          // Kiểm tra rate limiting
          const now = Date.now();
          if (now - lastRequestTime < API_CONFIG.rateLimit.perMilliseconds) {
            if (requestCount >= API_CONFIG.rateLimit.maxRequests) {
              const delay =
                API_CONFIG.rateLimit.perMilliseconds - (now - lastRequestTime);
              await new Promise((res) => setTimeout(res, delay));
              requestCount = 0;
            }
          } else {
            requestCount = 0;
            lastRequestTime = now;
          }

          // Tạo prompt
          let prompt = `Cho bạn đoạn văn bản: "${text}".
Hãy dịch đoạn văn bản đó thành Tiếng Việt (Vietnamese) với các điều kiện sau:
                                  - Tuân thủ chặt chẽ bối cảnh và sắc thái ban đầu.
                                  - Sự lưu loát tự nhiên như người bản xứ.
                                  - Không có thêm giải thích/diễn giải.
                                  - Bảo toàn thuật ngữ 1:1 cho các thuật ngữ/danh từ riêng.
                                  Chỉ in ra bản dịch mà không có dấu ngoặc kép.`;
          if (isAdvanced) {
            prompt = `Dịch và phân tích từ khóa: "${text}"`;
          }

          const provider = API_CONFIG.providers[API_CONFIG.currentProvider];
          let translatedText = "";
          let attempts = 0;

          // Retry logic với exponential backoff
          while (attempts < API_CONFIG.maxRetries) {
            try {
              translatedText = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                  method: "POST",
                  url: provider.url(API_CONFIG.apiKey),
                  headers:
                    typeof provider.headers === "function"
                      ? provider.headers(API_CONFIG.apiKey)
                      : provider.headers,
                  data: JSON.stringify(provider.body(prompt)),
                  onload: function (response) {
                    if (response.status >= 200 && response.status < 300) {
                      const result = JSON.parse(response.responseText);
                      const text = provider.responseParser(result);
                      text ? resolve(text) : reject("Invalid response format");
                    } else if (response.status === 429) {
                      reject("Rate limit exceeded");
                    } else {
                      reject(`API Error: ${response.status}`);
                    }
                  },
                  onerror: function (error) {
                    reject(`Connection error: ${error}`);
                  },
                });
              });

              requestCount++;
              break; // Thoát vòng lặp nếu thành công
            } catch (error) {
              attempts++;
              if (attempts >= API_CONFIG.maxRetries) throw error;
              await new Promise((res) =>
                setTimeout(res, API_CONFIG.retryDelay * Math.pow(2, attempts)),
              );
            }
          }

          // Hiển thị kết quả
          if (isAdvanced) {
            displayPopup(translatedText, text);
          } else if (displaySimple) {
            displaySimplePopup(text, translatedText);
          } else {
            showTranslationBelow(targetElement, translatedText);
          }
        } catch (error) {
          console.error("Translation failed:", error);
          const errorMessage =
            error instanceof Error ? error.message : String(error);
          showErrorBelow(
            targetElement,
            errorMessage.includes("Rate limit")
              ? "Vui lòng chờ giữa các lần dịch"
              : errorMessage.includes("Gemini API")
                ? "Lỗi Gemini: " + errorMessage
                : errorMessage.includes("API Key")
                  ? "Lỗi xác thực API"
                  : "Lỗi dịch thuật: " + errorMessage,
          );
        } finally {
          isTranslating = false;
          requestQueue.shift();
          if (requestQueue.length > 0) requestQueue[0]();
        }
      });

      if (!isTranslating && requestQueue.length === 1) {
        requestQueue[0]();
      }
    });
  }

  // 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 = "2147483647";
    summaryDiv.style.width = "90vw"; // Responsive width
    summaryDiv.style.maxHeight = "80vh"; // Responsive 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.overflowY = "auto"; // Enable vertical scrolling

    // 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 = "2147483647";
    simplePopupDiv.style.width = "90vw"; // Responsive width for simple popup
    simplePopupDiv.style.maxHeight = "80vh"; // Responsive 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.overflowY = "auto"; // Enable vertical scrolling if needed

    // 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: calc(100% - 50px); padding-right: 10px;">${translatedText}</div>`; // Increased font size, added scroll and padding
    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);
  }

  // Cache các bản dịch
  const translationCache = new Map();
  const CACHE_EXPIRATION = 300000; // 5 phút

  // Hàm thêm vào cache
  function addToCache(original, translated) {
    translationCache.set(original, {
      text: translated,
      timestamp: Date.now(),
    });
  }

  // Hàm kiểm tra cache
  function checkCache(text) {
    const entry = translationCache.get(text);
    if (entry && Date.now() - entry.timestamp < CACHE_EXPIRATION) {
      return entry.text;
    }
    translationCache.delete(text);
    return null;
  }

  // Hàm hiển thị lỗ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;
    }
  }

  // Biến lưu trữ nút dịch hiện tại
  let currentTranslateButton = null;
  let touchStartTime = 0;
  let longPressTimeout;

  // Hàm tạo nút dịch
  function createTranslateButton(selection) {
      if (currentTranslateButton) {
          currentTranslateButton.remove();
      }

      const buttonDich = document.createElement("button");
      buttonDich.textContent = "Dịch";
      buttonDich.style.position = "fixed";
      buttonDich.style.backgroundColor = "#007BFF";
      buttonDich.style.color = "#fff";
      buttonDich.style.border = "none";
      buttonDich.style.borderRadius = "3px";
      buttonDich.style.padding = "5px 10px";
      buttonDich.style.cursor = "pointer";
      buttonDich.style.zIndex = "2147483647";
      buttonDich.style.fontSize = "14px";

      const rect = selection.getRangeAt(0).getBoundingClientRect();
      const buttonHeight = 30;
      buttonDich.style.top = `${rect.bottom + buttonHeight + window.scrollY}px`;
      buttonDich.style.left = `${rect.left + window.scrollX}px`;

      let pressTimer;
      let isLongPress = false;

      // Xử lý sự kiện bắt đầu nhấn
      const handlePressStart = (e) => {
          isLongPress = false;
          pressTimer = setTimeout(() => {
              isLongPress = true;
              const selectedText = selection.toString().trim();
              const targetElement = selection.anchorNode.parentElement;
              translateText(selectedText, targetElement, true);  // Dịch nâng cao khi giữ nút dịch
              buttonDich.remove();
              currentTranslateButton = null;
          }, 500); // 500ms để xác định là giữ nút
      };

      // Xử lý sự kiện kết thúc nhấn
      const handlePressEnd = (e) => {
          clearTimeout(pressTimer);
          if (!isLongPress) {
              const selectedText = selection.toString().trim();
              const targetElement = selection.anchorNode.parentElement;
              translateText(selectedText, targetElement, false, true); // Dịch nhanh popup khi nhấn nút dịch
              buttonDich.remove();
              currentTranslateButton = null;
          }
      };

      // Xử lý sự kiện di chuyển ra khỏi nút
      const handlePressCancel = () => {
          clearTimeout(pressTimer);
      };

      // Thêm các event listener cho desktop
      buttonDich.addEventListener("mousedown", handlePressStart);
      buttonDich.addEventListener("mouseup", handlePressEnd);
      buttonDich.addEventListener("mouseleave", handlePressCancel);

      // Thêm các event listener cho mobile
      buttonDich.addEventListener("touchstart", (e) => {
          e.preventDefault(); // Ngăn chặn các hành vi mặc định
          handlePressStart(e);
      });
      buttonDich.addEventListener("touchend", (e) => {
          e.preventDefault();
          handlePressEnd(e);
      });
      buttonDich.addEventListener("touchcancel", handlePressCancel);

      document.body.appendChild(buttonDich);
      currentTranslateButton = buttonDich;
  }

  // Xử lý sự kiện chọn văn bản
  function handleTextSelection(event) {
    // Đợi một chút để selection được cập nhật
    setTimeout(() => {
      const selection = window.getSelection();
      const selectedText = selection.toString().trim();

      if (selectedText.length > 0) {
        createTranslateButton(selection);
      } else if (currentTranslateButton) {
        currentTranslateButton.remove();
        currentTranslateButton = null;
      }
    }, 100);
  }

  // Xử lý sự kiện touch trên mobile
  document.addEventListener("touchstart", (e) => {
    touchStartTime = Date.now();
    longPressTimeout = setTimeout(() => {
      handleTextSelection(e);
    }, 500); // Đợi 500ms để xác định là long press
  });

  document.addEventListener("touchend", (e) => {
    clearTimeout(longPressTimeout);
    if (Date.now() - touchStartTime < 500) {
      // Nếu không phải long press, xử lý như click thường
      handleTextSelection(e);
    }
  });

  // Xử lý sự kiện mouse trên desktop
  document.addEventListener("mouseup", handleTextSelection);

  // Thêm style để đảm bảo nút dịch hiển thị đúng trên mobile
  GM_addStyle(`
    @media (max-width: 768px) {
        buttonDich {
            touch-action: manipulation;
            -webkit-tap-highlight-color: transparent;
            user-select: none;
        }
    }
`);

  // Lắng nghe sự kiện phím tắt (Ctrl + Q hoặc Cmd + Q, Alt + Q 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 === "q") {
      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 === "q") {
      event.preventDefault();
      translateText(selectedText, targetElement, true); // Dịch nâng cao khi ấn Alt + Q
    } else if (event.altKey && event.key === "t") {
      event.preventDefault();
      translateText(selectedText, targetElement, false, true); // Dịch nhanh popup khi ấn Alt + T
    }
  });
})();

Screenshot_2025-02-16-21-22-19-060_org.mozilla.fenix.jpg
Screenshot_2025-02-16-21-23-33-494_org.mozilla.fenix.jpg
Kazam_screenshot_00002.png
Kazam_screenshot_00003.png
 
Sửa lần cuối:
mình chỉnh rồi thêm luôn nút biểu tượng nhỏ để dịch trên cả Pc lẫn Moblie (vẫn hỗ trợ phím tắt).
1. select text rồi nhấn nút dịch sẽ tương đương: Ctrl+t
2. select text rồi giữ nút dịch (trên 500ms) sẽ tương đương phím tắt Alt+q

ai cần thì thêm nhé.

JavaScript:
// ==UserScript==
// @name         Gemini AI Inline Translator (Popup)
// @namespace    Violentmonkey Scripts
// @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       Voodanh, king1x32
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  "use strict";

  // Cấu hình API
  const API_CONFIG = {
    providers: {
      gemini: {
        url: (apiKey) =>
          `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite-preview-02-05:generateContent?key=${apiKey}`,
        headers: {
          "Content-Type": "application/json",
        },
        body: (prompt) => ({
          contents: [
            {
              role: "user",
              parts: [{ text: prompt }],
            },
          ],
          generationConfig: { temperature: 0.7 },
        }),
        responseParser: (response) => {
          if (!response.candidates || response.candidates.length === 0) {
            throw new Error("Gemini API: No candidates in response");
          }
          if (!response.candidates[0].content?.parts?.[0]?.text) {
            throw new Error("Gemini API: Invalid response format");
          }
          return response.candidates[0].content.parts[0].text;
        },
      },
      openai: {
        url: () => "https://api.groq.com/openai/v1/chat/completions",
        headers: (apiKey) => ({
          "Content-Type": "application/json",
          Authorization: `Bearer ${apiKey}`,
        }),
        body: (prompt) => ({
          model: "llama-3.3-70b-versatile",
          messages: [{ role: "user", content: prompt }],
          temperature: 0.7,
        }),
        responseParser: (response) => response.choices?.[0]?.message?.content,
      },
    },
    currentProvider: "gemini",
    apiKey: "AIzaSCqCxFH32-luLxrdPH9p5FEmxk", // Gemini
    maxRetries: 3,
    retryDelay: 1000,
    rateLimit: {
      maxRequests: 5,
      perMilliseconds: 10000,
    },
  };

  // Biến trạng thái
  let isTranslating = false;
  let requestQueue = [];
  let requestCount = 0;
  let lastRequestTime = 0;

  // Add CSS for the draggable popup
  GM_addStyle(`
        .draggable {
          cursor: move;
        }
    `);

  // Hàm gọi API Gemini để dịch văn bản
  async function translateText(
    text,
    targetElement,
    isAdvanced = false,
    displaySimple = false,
  ) {
    // Thêm vào hàng đợi và xử lý tuần tự
    return new Promise((resolve) => {
      requestQueue.push(async () => {
        try {
          if (isTranslating) return;
          isTranslating = true;

          // Kiểm tra rate limiting
          const now = Date.now();
          if (now - lastRequestTime < API_CONFIG.rateLimit.perMilliseconds) {
            if (requestCount >= API_CONFIG.rateLimit.maxRequests) {
              const delay =
                API_CONFIG.rateLimit.perMilliseconds - (now - lastRequestTime);
              await new Promise((res) => setTimeout(res, delay));
              requestCount = 0;
            }
          } else {
            requestCount = 0;
            lastRequestTime = now;
          }

          // Tạo prompt
          let prompt = `Cho bạn đoạn văn bản: "${text}".
Hãy dịch đoạn văn bản đó thành Tiếng Việt (Vietnamese) với các điều kiện sau:
                                  - Tuân thủ chặt chẽ bối cảnh và sắc thái ban đầu.
                                  - Sự lưu loát tự nhiên như người bản xứ.
                                  - Không có thêm giải thích/diễn giải.
                                  - Bảo toàn thuật ngữ 1:1 cho các thuật ngữ/danh từ riêng.
                                  Chỉ in ra bản dịch mà không có dấu ngoặc kép.`;
          if (isAdvanced) {
            prompt = `Dịch và phân tích từ khóa: "${text}"`;
          }

          const provider = API_CONFIG.providers[API_CONFIG.currentProvider];
          let translatedText = "";
          let attempts = 0;

          // Retry logic với exponential backoff
          while (attempts < API_CONFIG.maxRetries) {
            try {
              translatedText = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                  method: "POST",
                  url: provider.url(API_CONFIG.apiKey),
                  headers:
                    typeof provider.headers === "function"
                      ? provider.headers(API_CONFIG.apiKey)
                      : provider.headers,
                  data: JSON.stringify(provider.body(prompt)),
                  onload: function (response) {
                    if (response.status >= 200 && response.status < 300) {
                      const result = JSON.parse(response.responseText);
                      const text = provider.responseParser(result);
                      text ? resolve(text) : reject("Invalid response format");
                    } else if (response.status === 429) {
                      reject("Rate limit exceeded");
                    } else {
                      reject(`API Error: ${response.status}`);
                    }
                  },
                  onerror: function (error) {
                    reject(`Connection error: ${error}`);
                  },
                });
              });

              requestCount++;
              break; // Thoát vòng lặp nếu thành công
            } catch (error) {
              attempts++;
              if (attempts >= API_CONFIG.maxRetries) throw error;
              await new Promise((res) =>
                setTimeout(res, API_CONFIG.retryDelay * Math.pow(2, attempts)),
              );
            }
          }

          // Hiển thị kết quả
          if (isAdvanced) {
            displayPopup(translatedText, text);
          } else if (displaySimple) {
            displaySimplePopup(text, translatedText);
          } else {
            showTranslationBelow(targetElement, translatedText);
          }
        } catch (error) {
          console.error("Translation failed:", error);
          const errorMessage =
            error instanceof Error ? error.message : String(error);
          showErrorBelow(
            targetElement,
            errorMessage.includes("Rate limit")
              ? "Vui lòng chờ giữa các lần dịch"
              : errorMessage.includes("Gemini API")
                ? "Lỗi Gemini: " + errorMessage
                : errorMessage.includes("API Key")
                  ? "Lỗi xác thực API"
                  : "Lỗi dịch thuật: " + errorMessage,
          );
        } finally {
          isTranslating = false;
          requestQueue.shift();
          if (requestQueue.length > 0) requestQueue[0]();
        }
      });

      if (!isTranslating && requestQueue.length === 1) {
        requestQueue[0]();
      }
    });
  }

  // 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 = "2147483647";
    summaryDiv.style.width = "90vw"; // Responsive width
    summaryDiv.style.maxHeight = "80vh"; // Responsive 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.overflowY = "auto"; // Enable vertical scrolling

    // 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 = "2147483647";
    simplePopupDiv.style.width = "90vw"; // Responsive width for simple popup
    simplePopupDiv.style.maxHeight = "80vh"; // Responsive 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.overflowY = "auto"; // Enable vertical scrolling if needed

    // 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: calc(100% - 50px); padding-right: 10px;">${translatedText}</div>`; // Increased font size, added scroll and padding
    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);
  }

  // Cache các bản dịch
  const translationCache = new Map();
  const CACHE_EXPIRATION = 300000; // 5 phút

  // Hàm thêm vào cache
  function addToCache(original, translated) {
    translationCache.set(original, {
      text: translated,
      timestamp: Date.now(),
    });
  }

  // Hàm kiểm tra cache
  function checkCache(text) {
    const entry = translationCache.get(text);
    if (entry && Date.now() - entry.timestamp < CACHE_EXPIRATION) {
      return entry.text;
    }
    translationCache.delete(text);
    return null;
  }

  // Hàm hiển thị lỗ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;
    }
  }

  // Biến lưu trữ nút dịch hiện tại
  let currentTranslateButton = null;
  let touchStartTime = 0;
  let longPressTimeout;

  // Hàm tạo nút dịch
  function createTranslateButton(selection) {
      if (currentTranslateButton) {
          currentTranslateButton.remove();
      }

      const buttonDich = document.createElement("button");
      buttonDich.textContent = "Dịch";
      buttonDich.style.position = "fixed";
      buttonDich.style.backgroundColor = "#007BFF";
      buttonDich.style.color = "#fff";
      buttonDich.style.border = "none";
      buttonDich.style.borderRadius = "3px";
      buttonDich.style.padding = "5px 10px";
      buttonDich.style.cursor = "pointer";
      buttonDich.style.zIndex = "2147483647";
      buttonDich.style.fontSize = "14px";

      const rect = selection.getRangeAt(0).getBoundingClientRect();
      const buttonHeight = 30;
      buttonDich.style.top = `${rect.bottom + buttonHeight + window.scrollY}px`;
      buttonDich.style.left = `${rect.left + window.scrollX}px`;

      let pressTimer;
      let isLongPress = false;

      // Xử lý sự kiện bắt đầu nhấn
      const handlePressStart = (e) => {
          isLongPress = false;
          pressTimer = setTimeout(() => {
              isLongPress = true;
              const selectedText = selection.toString().trim();
              const targetElement = selection.anchorNode.parentElement;
              translateText(selectedText, targetElement, true);  // Dịch nâng cao khi giữ nút dịch
              buttonDich.remove();
              currentTranslateButton = null;
          }, 500); // 500ms để xác định là giữ nút
      };

      // Xử lý sự kiện kết thúc nhấn
      const handlePressEnd = (e) => {
          clearTimeout(pressTimer);
          if (!isLongPress) {
              const selectedText = selection.toString().trim();
              const targetElement = selection.anchorNode.parentElement;
              translateText(selectedText, targetElement, false, true); // Dịch nhanh popup khi nhấn nút dịch
              buttonDich.remove();
              currentTranslateButton = null;
          }
      };

      // Xử lý sự kiện di chuyển ra khỏi nút
      const handlePressCancel = () => {
          clearTimeout(pressTimer);
      };

      // Thêm các event listener cho desktop
      buttonDich.addEventListener("mousedown", handlePressStart);
      buttonDich.addEventListener("mouseup", handlePressEnd);
      buttonDich.addEventListener("mouseleave", handlePressCancel);

      // Thêm các event listener cho mobile
      buttonDich.addEventListener("touchstart", (e) => {
          e.preventDefault(); // Ngăn chặn các hành vi mặc định
          handlePressStart(e);
      });
      buttonDich.addEventListener("touchend", (e) => {
          e.preventDefault();
          handlePressEnd(e);
      });
      buttonDich.addEventListener("touchcancel", handlePressCancel);

      document.body.appendChild(buttonDich);
      currentTranslateButton = buttonDich;
  }

  // Xử lý sự kiện chọn văn bản
  function handleTextSelection(event) {
    // Đợi một chút để selection được cập nhật
    setTimeout(() => {
      const selection = window.getSelection();
      const selectedText = selection.toString().trim();

      if (selectedText.length > 0) {
        createTranslateButton(selection);
      } else if (currentTranslateButton) {
        currentTranslateButton.remove();
        currentTranslateButton = null;
      }
    }, 100);
  }

  // Xử lý sự kiện touch trên mobile
  document.addEventListener("touchstart", (e) => {
    touchStartTime = Date.now();
    longPressTimeout = setTimeout(() => {
      handleTextSelection(e);
    }, 500); // Đợi 500ms để xác định là long press
  });

  document.addEventListener("touchend", (e) => {
    clearTimeout(longPressTimeout);
    if (Date.now() - touchStartTime < 500) {
      // Nếu không phải long press, xử lý như click thường
      handleTextSelection(e);
    }
  });

  // Xử lý sự kiện mouse trên desktop
  document.addEventListener("mouseup", handleTextSelection);

  // Thêm style để đảm bảo nút dịch hiển thị đúng trên mobile
  GM_addStyle(`
    @media (max-width: 768px) {
        buttonDich {
            touch-action: manipulation;
            -webkit-tap-highlight-color: transparent;
            user-select: none;
        }
    }
`);

  // Lắng nghe sự kiện phím tắt (Ctrl + Q hoặc Cmd + Q, Alt + Q 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 === "q") {
      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 === "q") {
      event.preventDefault();
      translateText(selectedText, targetElement, true); // Dịch nâng cao khi ấn Alt + Q
    } else if (event.altKey && event.key === "t") {
      event.preventDefault();
      translateText(selectedText, targetElement, false, true); // Dịch nhanh popup khi ấn Alt + T
    }
  });
})();

bác làm cho phần pop-up nó fit theo content thay vì mặc định hiển thị full được không bác ?
 
bác làm cho phần pop-up nó fit theo content thay vì mặc định hiển thị full được không bác ?
bro thử code này xem
JavaScript:
// ==UserScript==
// @name         Gemini AI Inline Translator (Popup)
// @namespace    Violentmonkey Scripts
// @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       Voodanh, king1x32
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  "use strict";

  // Cấu hình API
  const API_CONFIG = {
    providers: {
      gemini: {
        url: (apiKey) =>
          `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite-preview-02-05:generateContent?key=${apiKey}`,
        headers: {
          "Content-Type": "application/json",
        },
        body: (prompt) => ({
          contents: [
            {
              role: "user",
              parts: [{ text: prompt }],
            },
          ],
          generationConfig: { temperature: 0.7 },
        }),
        responseParser: (response) => {
          if (!response.candidates || response.candidates.length === 0) {
            throw new Error("Gemini API: No candidates in response");
          }
          if (!response.candidates[0].content?.parts?.[0]?.text) {
            throw new Error("Gemini API: Invalid response format");
          }
          return response.candidates[0].content.parts[0].text;
        },
      },
      openai: {
        url: () => "https://api.groq.com/openai/v1/chat/completions",
        headers: (apiKey) => ({
          "Content-Type": "application/json",
          Authorization: `Bearer ${apiKey}`,
        }),
        body: (prompt) => ({
          model: "llama-3.3-70b-versatile",
          messages: [{ role: "user", content: prompt }],
          temperature: 0.7,
        }),
        responseParser: (response) => response.choices?.[0]?.message?.content,
      },
    },
    currentProvider: "gemini",
    apiKey: "xxxxxxxx", // Gemini
    maxRetries: 3,
    retryDelay: 1000,
    rateLimit: {
      maxRequests: 5,
      perMilliseconds: 10000,
    },
  };

  // Biến trạng thái
  let isTranslating = false;
  let requestQueue = [];
  let requestCount = 0;
  let lastRequestTime = 0;

  // Add CSS for the draggable popup
  GM_addStyle(`
        .draggable {
          cursor: move;
        }
    `);

  // Hàm gọi API Gemini để dịch văn bản
  async function translateText(
    text,
    targetElement,
    isAdvanced = false,
    displaySimple = false,
  ) {
    // Thêm vào hàng đợi và xử lý tuần tự
    return new Promise((resolve) => {
      requestQueue.push(async () => {
        try {
          if (isTranslating) return;
          isTranslating = true;

          // Kiểm tra rate limiting
          const now = Date.now();
          if (now - lastRequestTime < API_CONFIG.rateLimit.perMilliseconds) {
            if (requestCount >= API_CONFIG.rateLimit.maxRequests) {
              const delay =
                API_CONFIG.rateLimit.perMilliseconds - (now - lastRequestTime);
              await new Promise((res) => setTimeout(res, delay));
              requestCount = 0;
            }
          } else {
            requestCount = 0;
            lastRequestTime = now;
          }

          // Tạo prompt
          let prompt = `Cho bạn đoạn văn bản: "${text}".
Hãy dịch đoạn văn bản đó thành Tiếng Việt (Vietnamese) với các điều kiện sau:
                                  - Tuân thủ chặt chẽ bối cảnh và sắc thái ban đầu.
                                  - Sự lưu loát tự nhiên như người bản xứ.
                                  - Không có thêm giải thích/diễn giải.
                                  - Bảo toàn thuật ngữ 1:1 cho các thuật ngữ/danh từ riêng.
                                  Chỉ in ra bản dịch mà không có dấu ngoặc kép.`;
          if (isAdvanced) {
            prompt = `Dịch và phân tích từ khóa: "${text}"`;
          }

          const provider = API_CONFIG.providers[API_CONFIG.currentProvider];
          let translatedText = "";
          let attempts = 0;

          // Retry logic với exponential backoff
          while (attempts < API_CONFIG.maxRetries) {
            try {
              translatedText = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                  method: "POST",
                  url: provider.url(API_CONFIG.apiKey),
                  headers:
                    typeof provider.headers === "function"
                      ? provider.headers(API_CONFIG.apiKey)
                      : provider.headers,
                  data: JSON.stringify(provider.body(prompt)),
                  onload: function (response) {
                    if (response.status >= 200 && response.status < 300) {
                      const result = JSON.parse(response.responseText);
                      const text = provider.responseParser(result);
                      text ? resolve(text) : reject("Invalid response format");
                    } else if (response.status === 429) {
                      reject("Rate limit exceeded");
                    } else {
                      reject(`API Error: ${response.status}`);
                    }
                  },
                  onerror: function (error) {
                    reject(`Connection error: ${error}`);
                  },
                });
              });

              requestCount++;
              break; // Thoát vòng lặp nếu thành công
            } catch (error) {
              attempts++;
              if (attempts >= API_CONFIG.maxRetries) throw error;
              await new Promise((res) =>
                setTimeout(res, API_CONFIG.retryDelay * Math.pow(2, attempts)),
              );
            }
          }

          // Hiển thị kết quả
          if (isAdvanced) {
            displayPopup(translatedText, text);
          } else if (displaySimple) {
            displaySimplePopup(text, translatedText);
          } else {
            showTranslationBelow(targetElement, translatedText);
          }
        } catch (error) {
          console.error("Translation failed:", error);
          const errorMessage =
            error instanceof Error ? error.message : String(error);
          showErrorBelow(
            targetElement,
            errorMessage.includes("Rate limit")
              ? "Vui lòng chờ giữa các lần dịch"
              : errorMessage.includes("Gemini API")
                ? "Lỗi Gemini: " + errorMessage
                : errorMessage.includes("API Key")
                  ? "Lỗi xác thực API"
                  : "Lỗi dịch thuật: " + errorMessage,
          );
        } finally {
          isTranslating = false;
          requestQueue.shift();
          if (requestQueue.length > 0) requestQueue[0]();
        }
      });

      if (!isTranslating && requestQueue.length === 1) {
        requestQueue[0]();
      }
    });
  }

  // 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 = "2147483647";
    summaryDiv.style.width = "auto";
    summaryDiv.style.maxWidth = "90vw";
    summaryDiv.style.minWidth = "300px";
    summaryDiv.style.maxHeight = "80vh";
    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.overflowY = "auto"; // Enable vertical scrolling

    // 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 = "2147483647";
    simplePopupDiv.style.width = "auto";
    simplePopupDiv.style.maxWidth = "90vw";
    simplePopupDiv.style.minWidth = "300px";
    simplePopupDiv.style.maxHeight = "80vh"; // Responsive 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.overflowY = "auto"; // Enable vertical scrolling if needed

    // 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: calc(100% - 50px); padding-right: 10px;">${translatedText}</div>`; // Increased font size, added scroll and padding
    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);
  }

  // Cache các bản dịch
  const translationCache = new Map();
  const CACHE_EXPIRATION = 300000; // 5 phút

  // Hàm thêm vào cache
  function addToCache(original, translated) {
    translationCache.set(original, {
      text: translated,
      timestamp: Date.now(),
    });
  }

  // Hàm kiểm tra cache
  function checkCache(text) {
    const entry = translationCache.get(text);
    if (entry && Date.now() - entry.timestamp < CACHE_EXPIRATION) {
      return entry.text;
    }
    translationCache.delete(text);
    return null;
  }

  // Hàm hiển thị lỗ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;
    }
  }

  // Biến lưu trữ nút dịch hiện tại
  let currentTranslateButton = null;
  let touchStartTime = 0;
  let longPressTimeout;

  // Hàm tạo nút dịch
  function createTranslateButton(selection) {
      if (currentTranslateButton) {
          currentTranslateButton.remove();
      }

      const buttonDich = document.createElement("button");
      buttonDich.textContent = "Dịch";
      buttonDich.style.position = "fixed";
      buttonDich.style.backgroundColor = "#007BFF";
      buttonDich.style.color = "#fff";
      buttonDich.style.border = "none";
      buttonDich.style.borderRadius = "3px";
      buttonDich.style.padding = "5px 10px";
      buttonDich.style.cursor = "pointer";
      buttonDich.style.zIndex = "2147483647";
      buttonDich.style.fontSize = "14px";

      const rect = selection.getRangeAt(0).getBoundingClientRect();
      const buttonHeight = 30;
      buttonDich.style.top = `${rect.bottom + buttonHeight + window.scrollY}px`;
      buttonDich.style.left = `${rect.left + window.scrollX}px`;

      let pressTimer;
      let isLongPress = false;

      // Xử lý sự kiện bắt đầu nhấn
      const handlePressStart = (e) => {
          isLongPress = false;
          pressTimer = setTimeout(() => {
              isLongPress = true;
              const selectedText = selection.toString().trim();
              const targetElement = selection.anchorNode.parentElement;
              translateText(selectedText, targetElement, true);  // Dịch nâng cao khi giữ nút dịch
              buttonDich.remove();
              currentTranslateButton = null;
          }, 500); // 500ms để xác định là giữ nút
      };

      // Xử lý sự kiện kết thúc nhấn
      const handlePressEnd = (e) => {
          clearTimeout(pressTimer);
          if (!isLongPress) {
              const selectedText = selection.toString().trim();
              const targetElement = selection.anchorNode.parentElement;
              translateText(selectedText, targetElement, false, true); // Dịch nhanh popup khi nhấn nút dịch
              buttonDich.remove();
              currentTranslateButton = null;
          }
      };

      // Xử lý sự kiện di chuyển ra khỏi nút
      const handlePressCancel = () => {
          clearTimeout(pressTimer);
      };

      // Thêm các event listener cho desktop
      buttonDich.addEventListener("mousedown", handlePressStart);
      buttonDich.addEventListener("mouseup", handlePressEnd);
      buttonDich.addEventListener("mouseleave", handlePressCancel);

      // Thêm các event listener cho mobile
      buttonDich.addEventListener("touchstart", (e) => {
          e.preventDefault(); // Ngăn chặn các hành vi mặc định
          handlePressStart(e);
      });
      buttonDich.addEventListener("touchend", (e) => {
          e.preventDefault();
          handlePressEnd(e);
      });
      buttonDich.addEventListener("touchcancel", handlePressCancel);

      document.body.appendChild(buttonDich);
      currentTranslateButton = buttonDich;
  }

  // Xử lý sự kiện chọn văn bản
  function handleTextSelection(event) {
    // Đợi một chút để selection được cập nhật
    setTimeout(() => {
      const selection = window.getSelection();
      const selectedText = selection.toString().trim();

      if (selectedText.length > 0) {
        createTranslateButton(selection);
      } else if (currentTranslateButton) {
        currentTranslateButton.remove();
        currentTranslateButton = null;
      }
    }, 100);
  }

  // Xử lý sự kiện touch trên mobile
  document.addEventListener("touchstart", (e) => {
    touchStartTime = Date.now();
    longPressTimeout = setTimeout(() => {
      handleTextSelection(e);
    }, 500); // Đợi 500ms để xác định là long press
  });

  document.addEventListener("touchend", (e) => {
    clearTimeout(longPressTimeout);
    if (Date.now() - touchStartTime < 500) {
      // Nếu không phải long press, xử lý như click thường
      handleTextSelection(e);
    }
  });

  // Xử lý sự kiện mouse trên desktop
  document.addEventListener("mouseup", handleTextSelection);

  // Thêm style để đảm bảo nút dịch hiển thị đúng trên mobile
  GM_addStyle(`
    @media (max-width: 768px) {
        buttonDich {
            touch-action: manipulation;
            -webkit-tap-highlight-color: transparent;
            user-select: none;
        }
    }
`);

  // Lắng nghe sự kiện phím tắt (Ctrl + Q hoặc Cmd + Q, Alt + Q 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 === "q") {
      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 === "q") {
      event.preventDefault();
      translateText(selectedText, targetElement, true); // Dịch nâng cao khi ấn Alt + Q
    } else if (event.altKey && event.key === "t") {
      event.preventDefault();
      translateText(selectedText, targetElement, false, true); // Dịch nhanh popup khi ấn Alt + T
    }
  });
})();
 
Hôm bữa mình có lên bài tìm app dịch giống Qtranslate nhưng không thấy ổn cái nào.
Do đó mình nhờ đến AI để nó giúp tìm giải pháp.
Đầu tiên, mọi người tải Violenmonkey về vì mình sẽ tạo scripts để dùng API của AI Studio để dịch (free 100%)
Tiếp đó mọi người dùng đoạn scripts bến dưới add vào, thay API và địa chỉ API rồi sau đó sử dụng.
*Lưu ý: Mình không biết gì về code, chỉ lên ý tưởng để AI làm và đến giờ dùng thì thấy ổn nên nếu hỏi chuyên môn code thì mình ko biết TL. MN có thể thêm ý tưởng vào các AI rồi bắt nó sửa cho mình.
Mọi người có thể lấy API tại đây hoặc gõ vào gg AI Studio (CỦa google nên ko lo virus j nhé)
"Sign in - Google Accounts (https://aistudio.google.com/apikey)"
Mình tạo ra 3 nút phím tắt:
Xem tệp đính kèm 2913915 (CTRL + m : Tạo 1 bản dịch bên dưới bản tiếng anh)
Xem tệp đính kèm 2913916 (ALT + m : Cái này mụcđíchđể học tiếng anh, sẽ tạo popup dịch, xácđịnh keywords , coi nhưđể học từ mới)
Cách dùng scripts này để dịch file pdf (theo đoạn bôi đen, không dịch được cả file 1 lần)
Xem tệp đính kèm 2913919 ( ALT + t :Giống CTRL m nhưng tạo popup)
Tuy nhiên, với các file pdf như mình dùng edge sẽ không cho dùng extention nên mọi người dùng link này để mở file pdf, sẽ cho phép dùng extension.
"PDF.js viewer (https://pdf.translatewebpages.org/pdf.js/web/viewer)"
Mã:
// ==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
        }
    });


})();
dùng model nào dịch ngon nhất vậy fen?
 
mình thấy ko khác nhiều nên dùng bản flash cho nhanh ra kết quả. Thím đổi sang bản 2.0 như của bác trên kia cx đc, mình cx đổi sang model của bác ấy r
flash 2.0 có 1 đống như này, cụ thể là cái nào zị fen?
gemini-2.0-flash-exp
gemini-2.0-flash
gemini-2.0-flash-001
gemini-2.0-flash-lite-preview
gemini-2.0-flash-lite-preview-02-05
gemini-2.0-flash-thinking-exp-01-21
gemini-2.0-flash-thinking-exp
gemini-2.0-flash-thinking-exp-1219
 
flash 2.0 có 1 đống như này, cụ thể là cái nào zị fen?
gemini-2.0-flash-exp
gemini-2.0-flash
gemini-2.0-flash-001
gemini-2.0-flash-lite-preview
gemini-2.0-flash-lite-preview-02-05
gemini-2.0-flash-thinking-exp-01-21
gemini-2.0-flash-thinking-exp
gemini-2.0-flash-thinking-exp-1219
thím test thấy cái nào hợp, ra kq nhanh thì dùng. Bản thinking phải nghĩ, suy luận nên chắc sẽ ra kq chậm hơn
 
Sao phải khổ thế, dùng cái này là xong mà Easydict nhé
Cái này không có Mac dùng...chê
 

Thống kê chủ đề

Ngày tạo
ChuSyThang21,
Người trả lời cuối
Fioren,
Trả lời
204
Lượt xem
28.628
Quay lại
Lên đầu trang