// ==UserScript==
// @name ChatGPT轻小说分段翻译
// @namespace http://tampermonkey.net/
// @version 0.15
// @description 上传长文本TXT, 并分段翻译成简体中文。
// @author root
// @match https://chat.openai.com/*
// @match https://chatgpt.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=openai.com
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Initialize default values
let num = 15;
let shouldStop = false;
let prefixText = "";
// CSS styles for the elements
const styles = {
button:
"background-color: #FAE69E; font-weight: bold; width:100px; height:30px; color: #927201; padding: 5px; border: none; border-radius: 5px; margin: 5px; font-size: 14px; cursor: pointer; transition: all 0.3s ease;",
greenButton:
"background-color: #19C37D; font-weight: bold; width:100px; height:30px; color: white; padding: 5px; border: none; border-radius: 5px; margin: 5px; font-size: 14px; cursor: pointer; transition: all 0.3s ease;",
stopButton:
"background-color: #dc3545; font-weight: bold; width:100px; height:30px; color: white; padding: 5px; border: none; border-radius: 5px; margin: 5px; font-size: 14px; cursor: pointer; transition: all 0.3s ease;",
aboutButton:
"background-color: #FAE69E; font-weight: bold; width:100px; height:30px; color: #927201; padding: 5px; border: none; border-radius: 5px; margin: 5px; font-size: 14px; cursor: pointer; transition: all 0.3s ease;",
progress:
"width: 70%; height: 15px; background-color: #d9d9e3; border-radius: 15px; margin-top: 10px; overflow: hidden; margin: 0 auto; box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);",
progressBar:
"height: 100%; background: linear-gradient(90deg, #19C37D, #1B98E0); width: 0%; transition: width 0.5s ease-in-out; border-radius: 15px;",
input:
"margin: 5px; width: 120px; height: 30px; padding: 5px; font-size: 14px; border: 1px solid #ccc; border-radius: 5px;",
};
// Function to create an input
const createInput = (placeholderText) => {
const input = document.createElement("input");
input.type = "number";
input.placeholder = placeholderText;
input.style = styles.input;
return input;
};
// Function to create a file input
const createFileInput = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".txt,.js,.py,.html,.css,.json,.csv";
input.style = styles.input;
return input;
};
// Function to create a button
const createButton = (text, style) => {
const button = document.createElement("button");
button.style = style;
button.textContent = text;
// Add hover style
button.onmouseover = function () {
this.style.border = "2px solid rgba(0, 0, 0, 0.5)"; // shallow black border on hover
};
button.onmouseout = function () {
this.style.border = "none"; // remove border when mouse leaves
};
return button;
};
// Function to create a button wrapper (div)
const createButtonWrapper = (
submitButton,
aboutButton,
input1,
input2,
prefixButton
) => {
const div = document.createElement("div");
div.classList.add("buttonWrapper");
div.appendChild(input1);
div.appendChild(input2);
div.appendChild(prefixButton);
div.appendChild(submitButton);
// div.appendChild(aboutButton);
div.setAttribute("data-inserted", "true");
return div;
};
// Function to create a progress element
const createProgress = () => {
const progress = document.createElement("div");
progress.style = styles.progress;
const progressBar = document.createElement("div");
progressBar.style = styles.progressBar;
const progressAnimation = document.createElement("style");
progressAnimation.type = "text/css";
progressAnimation.innerHTML = `
@keyframes gradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.animated-gradient {
background-size: 200% 200%;
animation: gradient 5s ease infinite;
}
`;
document.head.appendChild(progressAnimation);
progressBar.classList.add("animated-gradient");
progress.appendChild(progressBar);
return { progress, progressBar };
};
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Async function to submit conversation
async function submitConversation(text, part, filename, delay) {
const textarea = document.querySelector("textarea[tabindex='0']");
const inputEvent = new Event("input", {
bubbles: true,
cancelable: true,
});
const enterKeyEvent = new KeyboardEvent("keydown", {
bubbles: true,
cancelable: true,
keyCode: 13,
});
textarea.value = `Part ${part} of ${filename}: \n\n ${prefixText} ${text}`;
textarea.dispatchEvent(inputEvent);
await sleep(delay);
textarea.dispatchEvent(enterKeyEvent);
}
// Function to check if ChatGPT is ready
async function isChatGptReady() {
let chatgptReady = false;
while (!chatgptReady) {
await new Promise((resolve) => setTimeout(resolve, 1000));
//chatgptReady = !document.querySelector(".text-2xl > span:not(.invisible)");
//const newElement = document.querySelector('[data-testid="send-button"]');
const newElement = document.querySelector('button.mb-1.me-1');
if (newElement) {
const isDisabled = newElement.hasAttribute("disabled");
if (isDisabled) {
chatgptReady = true;
}
}
}
return chatgptReady;
}
// Function to split text into sentences
function splitIntoSentences(text) {
// Use regular expression to split text by sentence
return text.match(/[^。!?”.!?]+[。!?”.!?]+/g);
}
// Function to group sentences
function groupSentences(sentences, groupSize) {
const groups = [];
for (let i = 0; i < sentences.length; i += groupSize) {
groups.push(sentences.slice(i, i + groupSize).join(""));
}
return groups;
}
const initPlugin = () => {
// Check if the element exists and insert the elements into the DOM
const checkExist = setInterval(function () {
const parentElement = document.querySelector(
".px-2.py-2"
);
if (
parentElement !== null &&
!parentElement.getAttribute("data-inserted")
) {
console.log("Element exists!");
// 添加一个标记,表示已经插入了插件
parentElement.setAttribute("data-inserted", "true");
// Create elements
prefixText = "将以下内容翻译成简体中文:";
const submitButton = createButton("上传文件", styles.button);
const aboutButton = createButton("关于", styles.aboutButton);
const numInput = createInput("单次句子数量");
const delayInput = createInput("延迟时间(秒)");
const prefixButton = createButton("前置提示词", styles.button);
const buttonWrapper = createButtonWrapper(
submitButton,
aboutButton,
numInput,
delayInput,
prefixButton
);
const { progress, progressBar } = createProgress();
parentElement.insertBefore(buttonWrapper, parentElement.firstChild);
parentElement.insertBefore(progress, parentElement.firstChild);
clearInterval(checkExist);
// Handle prefix button click
prefixButton.addEventListener("click", () => {
const prefixInput = createFileInput();
const fileChangeListener = async () => {
if (!prefixInput.files.length) {
// add this check
prefixInput.removeEventListener("change", fileChangeListener);
return;
}
const file = prefixInput.files[0];
const reader = new FileReader();
reader.readAsText(file);
reader.onload = () => {
prefixText = reader.result;
prefixButton.style = styles.greenButton;
};
};
prefixInput.addEventListener("change", fileChangeListener);
prefixInput.click();
});
submitButton.addEventListener("click", async () => {
// If a file is uploading, stop it
if (submitButton.textContent === "停止") {
shouldStop = true;
progressBar.style.width = "0%";
submitButton.textContent = "上传文件"; // change the button text back to 'Upload file'
submitButton.style = styles.button; // change the button color back to blue
return;
}
// Get values from inputs and update variables
num = numInput.value ? parseInt(numInput.value) : num;
const delay = delayInput.value
? parseInt(delayInput.value) * 1000
: 1000;
const input = createFileInput();
const fileChangeListener = async () => {
if (!input.files.length) {
// add this check
input.removeEventListener("change", fileChangeListener);
return;
}
const file = input.files[0];
const reader = new FileReader();
reader.readAsText(file);
reader.onload = async () => {
// Change the button to a stop button after file read is completed
submitButton.textContent = "停止"; // change the button text to 'Stop'
submitButton.style = styles.stopButton; // change the button color to red
const sentences = splitIntoSentences(reader.result);
const chunks = groupSentences(sentences, num);
for (let i = 0; i < chunks.length; i++) {
if (shouldStop) {
shouldStop = false;
break;
}
if (!shouldStop) {
await submitConversation(chunks[i], i + 1, file.name, delay);
}
progressBar.style.width = `${((i + 1) / chunks.length) * 100}%`;
if (!shouldStop) {
await isChatGptReady();
}
if (shouldStop) {
shouldStop = false;
break;
}
}
progressBar.style.backgroundColor = "#19C37D";
// After file uploading is finished or stopped
submitButton.textContent = "上传文件"; // change the button text back to 'Upload file'
submitButton.style = styles.button; // change the button color back to blue
progressBar.style.width = "0%";
};
};
input.addEventListener("change", fileChangeListener);
input.click();
});
// aboutButton click
aboutButton.addEventListener("click", () => {
// 生成一个半透明的背景
const overlay = document.createElement("div");
overlay.style = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
`;
// 生成一个带有毛玻璃效果的窗口
const modal = document.createElement("div");
modal.style = `
width: 50%;
padding: 20px;
background: rgba(225, 225, 225, 0.7);
border-radius: 10px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
border: 1px solid rgba(255, 255, 255, 0.18);
text-align: center;
`;
// 添加帮助文字
const text = document.createElement("p");
text.innerHTML =
"<div>【ChatGPT自动上传插件】</div>" +
'<div style="text-align: left;"><br>1.选择参数:设定单次发送句子的数量和延迟时间。<br>' +
"2.设定提示词(可选):点击“前置提示词”按钮,选择一个文件作为固定提示词。<br>" +
"3.上传文件:点击“上传文件”按钮,选择一个文本。<br>" +
"4.停止上传:在文件上传过程中,随时可以点击“停止”按钮来停止上传。<br>" +
"5.查看进度:屏幕上的进度条显示上传的进度。<br><br>" +
"【注意】上传文件只支持TXT格式,上传文件后会稍微卡顿几秒。<br><br>" +
"作者:老陆(微信:laolu2045)<br>" +
"想加入AI群一起学习或有其他建议请联系我。</div>";
modal.appendChild(text);
// 添加关闭按钮
const closeButton = document.createElement("button");
closeButton.textContent = "关闭";
closeButton.style = `
display: block;
margin-top: 20px;
margin-left: auto;
margin-right: auto;
padding: 10px 20px;
border: none;
border-radius: 5px;
background: linear-gradient(90deg, rgba(235, 52, 52, 1) 0%, rgba(236, 116, 116, 1) 100%);
box-shadow: 0px 3px 15px rgba(0,0,0,0.2);
color: white;
cursor: pointer;
transition: background 0.5s;
`;
closeButton.addEventListener("mouseover", () => {
closeButton.style.background = "rgba(236, 116, 116, 1)";
});
closeButton.addEventListener("mouseout", () => {
closeButton.style.background =
"linear-gradient(90deg, rgba(235, 52, 52, 1) 0%, rgba(236, 116, 116, 1) 100%)";
});
closeButton.addEventListener("click", () => {
document.body.removeChild(overlay);
});
modal.appendChild(closeButton);
// 将窗口添加到背景上,然后将背景添加到文档上
overlay.appendChild(modal);
document.body.appendChild(overlay);
});
}
}, 100);
};
// Initialize MutationObserver
const targetNode = document.body;
const config = { childList: true, subtree: true };
const callback = function (mutationsList, observer) {
for (const mutation of mutationsList) {
if (mutation.type === "childList") {
const targetElement = document.querySelector(
".pb-3.pt-2"
);
if (
targetElement !== null &&
!targetElement.querySelector(".buttonWrapper[data-inserted='true']")
) {
initPlugin();
}
}
}
};
// 使用MutationObserver来监听DOM变化
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
// 检查是否有新节点被添加
if (mutation.addedNodes.length) {
initPlugin();
}
});
});
// 选项配置
const observerConfig = {
attributes: true,
childList: true,
characterData: true,
subtree: true,
};
// 监听document.body的变化
observer.observe(document.body, observerConfig);
})();