ChatGPT Utils

Modifies the behavior of the chat interface on the OpenAI website

目前為 2022-12-23 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         ChatGPT Utils
// @description  Modifies the behavior of the chat interface on the OpenAI website
// @version      1.5.1
// @match        https://chat.openai.com/*
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// @license      MIT
// @namespace https://greasyfork.org/users/913219
// ==/UserScript==

console.log(`ChatGPT Utils initializing...`);

setInterval(() => {
    try {
        addTranslateButtons();
    } catch (error) {
        console.error(error);
    }

    try {
        findAndHookTextareaElement();
    } catch (error) {
        console.error(error);
    }
}, 100);

function addTranslateButtons() {
    var messages = document.querySelectorAll(".markdown.prose");

    for (var i = 0; i < messages.length; i++) {
        const msgMarkdown = messages[i];
        const parentMsgMarkdown = msgMarkdown.parentElement;
        const msgIcon = parentMsgMarkdown.parentElement.parentElement.previousElementSibling;

        if (!msgIcon.querySelector(".translate-button")) {
            var btn = document.createElement("button");
            btn.textContent = "Tr";
            btn.classList.add("translate-button");
            btn.style.cssText = "width: 30px; height: 30px;"; // margin-left: 8px; display: flex; flex-direction: column;
            msgIcon.insertBefore(btn, msgIcon.firstChild);

            // Add event listener for click event on translate button
            btn.addEventListener("click", () => {
                const translateClassName = "translate-markdown";

                var translateNode = parentMsgMarkdown.querySelector(`.${translateClassName}`);
                if (translateNode) {
                    parentMsgMarkdown.removeChild(translateNode);
                    delete parentMsgMarkdown.translateNode;
                }

                translateNode = msgMarkdown.cloneNode(true);
                translateNode.classList.add(translateClassName);

                const htmlContent = translateNode.outerHTML;
                parentMsgMarkdown.translateNode = translateNode;
                parentMsgMarkdown.insertBefore(translateNode, parentMsgMarkdown.firstChild);

                translateHTML(htmlContent, "auto", navigator.language).then(translatedContent => {
                    let index = translatedContent.lastIndexOf('</div>');
                    let before = translatedContent.slice(0, index);

                    const endTranslate = `<p>.......... конец_перевода ..........</p>`;
                    translatedContent = before.concat(endTranslate, '</div>');

                    //console.log("Translated content: " + translatedContent);

                    translateNode.outerHTML = translatedContent;
                });
            });
        }
    }
}

function findAndHookTextareaElement() {
    const targetElement = document.querySelector("textarea");
    if (targetElement === null) {
        return;
    }

    if (targetElement.isAddHookKeydownEvent === true) {
        return;
    }

    targetElement.isAddHookKeydownEvent = true;

    console.log(`Textarea element found. Adding keydown event listener.`);
    targetElement.addEventListener("keydown", async event => await handleSubmit(event, targetElement), true);
}

async function handleSubmit(event, targetElement) {
    console.log(`Keydown event detected: type - ${event.type}, key - ${event.key}`);

    if (event.shiftKey && event.key === "Enter") {
        return;
    }

    if (window.isActiveOnSubmit === true) {
        return;
    }

    if (event.key === "Enter") {
        window.isActiveOnSubmit = true;
        event.stopImmediatePropagation();

        const request = targetElement.value;
        targetElement.value = "";

        const translatedText = await translate(request, "ru", "en");

        targetElement.focus();
        targetElement.value = translatedText;
        const enterEvent = new KeyboardEvent("keydown", {
            bubbles: true,
            cancelable: true,
            key: "Enter",
            code: "Enter"
        });
        targetElement.dispatchEvent(enterEvent);

        window.isActiveOnSubmit = false;
    }
}

async function translateHTML(html, sLang, tLang) {
    const excludeTagRegex = /<(pre|code)[^>]*>([\s\S]*?)<\/(pre|code)>/g;
    const excludeTags = [];
    const excludePlaceholder = 'e0x';

    const replaceTagRegex = /<[^>]*>/g;
    const replaceTags = [];
    const replacePlaceholder = 'r0e';

    let translateHTML = html;

    let excludeTagsMatch;
    while (excludeTagsMatch = excludeTagRegex.exec(html)) {
        excludeTags.push(excludeTagsMatch[0]);
        translateHTML = translateHTML.replace(excludeTagsMatch[0], `[${excludePlaceholder}${excludeTags.length - 1}]`);
    }

    let replaceTagsMatch;
    while (replaceTagsMatch = replaceTagRegex.exec(html)) {
        replaceTags.push(replaceTagsMatch[0]);
        translateHTML = translateHTML.replace(replaceTagsMatch[0], `[${replacePlaceholder}${replaceTags.length - 1}]`);
    }

    console.log(`preTranslateHTML: ${html}`);

    translateHTML = await translate(translateHTML, sLang, tLang);
    translateHTML = removeSpaces(translateHTML);

    for (let i = 0; i < excludeTags.length; i++) {
        translateHTML = translateHTML.replace(`[${excludePlaceholder}${i}]`, excludeTags[i]);
    }

    for (let i = 0; i < replaceTags.length; i++) {
        translateHTML = translateHTML.replace(`[${replacePlaceholder}${i}]`, replaceTags[i]);
    }

    console.log(`postTranslateHTML: ${translateHTML}`);

    return translateHTML;
}

async function translate(text, sLang, tLang) {
    const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sLang}&tl=${tLang}&dt=t&q=${encodeURIComponent(text)}`;

    console.log(`preTranslate: ${text}`);

    try {
        const response = await doXHR(url);
        const responseText = JSON.parse(response.responseText);

        let postTranslate = "";
        responseText[0].forEach(part => {
            postTranslate += part[0];
        });

        console.log(`postTranslate: ${postTranslate}`);

        return postTranslate;
    } catch (error) {
        console.error(error);
    }
}

function removeSpaces(string) {
    // Use a regular expression to find [] blocks in the string
    const regex = /\[([^\[\]]*)\]/g;
    let result;

    // Keep searching for [] blocks until there are no more
    while ((result = regex.exec(string)) !== null) {
        // Replace all spaces within the block with an empty string
        string = string.replace(result[1], result[1].replace(/\s/g, ''));
    }

    return string;
}

async function doXHR(url) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", url);
        xhr.onload = () => resolve(xhr);
        xhr.onerror = () => reject(xhr.statusText);
        xhr.send();
    });
}