Modifies the behavior of the chat interface on the OpenAI website
От
// ==UserScript==
// @name ChatGPT Utils
// @description Modifies the behavior of the chat interface on the OpenAI website
// @namespace ChatGPTUtils
// @version 1.7.1
// @author CriDos
// @match https://chat.openai.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=chat.openai.com
// @require https://cdnjs.cloudflare.com/ajax/libs/he/1.2.0/he.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js
// @grant GM_xmlhttpRequest
// @run-at document-end
// @license MIT
// ==/UserScript==
'use strict';
console.log(`ChatGPT Utils initializing...`);
let debug = true;
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);
});
}
}
}
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 === "F2") {
const request = targetElement.value;
targetElement.value = "";
const translatedText = await requestTranslate(request, "ru", "en", "text");
targetElement.focus();
targetElement.value = translatedText;
} else if (event.key === "Enter") {
window.isActiveOnSubmit = true;
event.stopImmediatePropagation();
const request = targetElement.value;
targetElement.value = "";
const translatedText = await requestTranslate(request, "ru", "en", "text");
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 translateNode(msgMarkdownNode) {
const translateClassName = "translate-markdown";
const parentMsgMarkdown = msgMarkdownNode.parentElement;
const msgMarkdownContent = $(msgMarkdownNode).html();
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 translatedContent = await translateHTML(msgMarkdownContent, "en", navigator.language)
translatedContent = translatedContent.replace(/<\/div>$/, '');
$(translateNode).html(translatedContent + `<p>.......... конец_перевода ..........</p></div>`);
}
async function translateHTML(html, sLang, tLang) {
const excludeTagRegex = /<(pre|code)[^>]*>([\s\S]*?)<\/(pre|code)>/g;
const excludeTags = [];
const excludePlaceholder = 'EXCLUDE_BLOCK';
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 requestTranslate(he.encode(translateHTML), sLang, tLang, "html");
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 requestTranslate(text, sLang, tLang, format) {
try {
if (debug) {
console.log(`preTranslate: ${text}`);
}
return await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url: "https://translate.google.com/translate_a/single",
data: `format=${format}&mode=1&client=gtx&sl=${sLang}&tl=${tLang}&dt=t&q=${encodeURIComponent(text)}`,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
// "Authorization": `Bearer ${API_KEY}`
},
onload: response => {
const translatedText = JSON.parse(response.responseText)[0]
.map(text => text[0])
.join(" ");
resolve(translatedText);
},
onerror: response => {
reject(response.statusText);
}
});
});
} catch (error) {
console.error(error);
}
}