ChatGPT Utils

Modifies the behavior of the chat interface on the OpenAI website

当前为 2022-12-23 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ChatGPT Utils
// @description  Modifies the behavior of the chat interface on the OpenAI website
// @version      1.5.2
// @namespace    ChatGPTUtils
// @match        https://chat.openai.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=chat.openai.com
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// @license      MIT
// ==/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();
    });
}