ChatGPT Utils

Modifies the behavior of the chat interface on the OpenAI website

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

// ==UserScript==
// @name         ChatGPT Utils
// @description  Modifies the behavior of the chat interface on the OpenAI website
// @namespace    ChatGPTUtils
// @version      1.6.1
// @author       CriDos
// @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
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js
// @license      MIT
// ==/UserScript==

'use strict';

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

let debug = false;

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

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

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

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

        if (parentMsgMarkdown.isAutoTranslate) {
            continue;
        }
        parentMsgMarkdown.isAutoTranslate = true;

        setInterval(async () => {
            await translateNode(msgMarkdownNode);
        }, 500);
    }
}

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

    for (var i = 0; i < messages.length; i++) {
        const msgMarkdownNode = messages[i];
        const msgIcon = msgMarkdownNode.parentElement.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;";
            msgIcon.insertBefore(btn, msgIcon.firstChild);

            btn.addEventListener("click", async () => {
                await translateNode(msgMarkdownNode);
            });
        }
    }
}

async function translateNode(msgMarkdownNode) {
    const translateClassName = "translate-markdown";
    const parentMsgMarkdown = msgMarkdownNode.parentElement;

    const msgMarkdownContent = msgMarkdownNode.outerHTML;
    if (msgMarkdownNode.storeContent == msgMarkdownContent) {
        return;
    }
    msgMarkdownNode.storeContent = msgMarkdownContent;

    var translateNode = parentMsgMarkdown.querySelector(`.${translateClassName}`);
    if (translateNode == null) {
        translateNode = msgMarkdownNode.cloneNode(true);
        translateNode.classList.add(translateClassName);
        parentMsgMarkdown.insertBefore(translateNode, parentMsgMarkdown.firstChild);
    }

    var msgMarkdownClone = msgMarkdownNode.cloneNode(true);
    msgMarkdownClone.classList.add(translateClassName);
    const msgMarkdownCloneContent = msgMarkdownClone.outerHTML;
    msgMarkdownClone = null;

    const translatedContent = await translateHTML(msgMarkdownCloneContent, "auto", navigator.language)

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

    translateNode.outerHTML = before.concat(endTranslate, '</div>');
}

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

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;";
            msgIcon.insertBefore(btn, msgIcon.firstChild);

            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>');

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

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 translateText(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';

    let translateHTML = html;

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

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

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

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

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

    return translateHTML;
}

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

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

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

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

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

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

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

function removeSpaces(string) {
    const regex = /\[([^\[\]]*)\]/g;
    let result;

    while ((result = regex.exec(string)) !== null) {
        string = string.replace(result[1], result[1].replace(/\s/g, ''));
    }

    return string;
}