ChatGPT Utils

Modifies the behavior of the chat interface on the OpenAI website

2022-12-23 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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://greatest.deepsurf.us/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();
    });
}