// ==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 // @downloadURL https://update.greasyfork.icu/scripts/488168/ChatGPT%E8%BD%BB%E5%B0%8F%E8%AF%B4%E5%88%86%E6%AE%B5%E7%BF%BB%E8%AF%91.user.js // @updateURL https://update.greasyfork.icu/scripts/488168/ChatGPT%E8%BD%BB%E5%B0%8F%E8%AF%B4%E5%88%86%E6%AE%B5%E7%BF%BB%E8%AF%91.meta.js // ==/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 = "