您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ParaTranz文本替换和AI翻译功能拓展。
当前为
// ==UserScript== // @name ParaTranz-AI // @namespace http://tampermonkey.net/ // @version 1.3.1 // @description ParaTranz文本替换和AI翻译功能拓展。 // @author HCPTangHY // @license WTFPL // @match https://paratranz.cn/* // @icon https://paratranz.cn/favicon.png // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/diff.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/bundles/js/diff2html-ui.min.js // @resource css https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css // @grant GM_getResourceURL // @grant GM_getResourceText // @grant GM_addStyle // ==/UserScript== GM_addStyle(GM_getResourceText("css")); // fork from HeliumOctahelide https://greasyfork.org/zh-CN/scripts/503063-paratranz-tools (function() { 'use strict'; // 基类定义 class BaseComponent { constructor(selector) { this.selector = selector; this.init(); } init() { this.checkExistence(); } checkExistence() { const element = document.querySelector(this.selector); if (!element) { this.insert(); } setTimeout(() => this.checkExistence(), 1000); } insert() { // 留空,子类实现具体插入逻辑 } } // 按钮类定义,继承自BaseComponent class Button extends BaseComponent { constructor(selector, toolbarSelector, htmlContent, callback) { super(selector); this.toolbarSelector = toolbarSelector; this.htmlContent = htmlContent; this.callback = callback; } insert() { const toolbar = document.querySelector(this.toolbarSelector); if (!toolbar) { console.log(`Toolbar not found: ${this.toolbarSelector}`); return; } if (toolbar && !document.querySelector(this.selector)) { const button = document.createElement('button'); button.className = this.selector.split('.').join(' '); button.innerHTML = this.htmlContent; button.type = 'button'; button.addEventListener('click', this.callback); toolbar.insertAdjacentElement('afterbegin', button); console.log(`Button inserted: ${this.selector}`); } } } // 手风琴类定义,继承自BaseComponent class Accordion extends BaseComponent { constructor(selector, parentSelector) { super(selector); this.parentSelector = parentSelector; } insert() { const parentElement = document.querySelector(this.parentSelector); if (!parentElement) { console.log(`Parent element not found: ${this.parentSelector}`); return; } if (parentElement && !document.querySelector(this.selector)) { const accordionHTML = ` <div class="accordion" id="accordionExample"></div> <hr> `; parentElement.insertAdjacentHTML('afterbegin', accordionHTML); } } addCard(card) { card.insert(); } } // 卡片类定义,继承自BaseComponent class Card extends BaseComponent { constructor(selector, parentSelector, headingId, title, contentHTML) { super(selector); this.parentSelector = parentSelector; this.headingId = headingId; this.title = title; this.contentHTML = contentHTML; } insert() { const parentElement = document.querySelector(this.parentSelector); if (!parentElement) { console.log(`Parent element not found: ${this.parentSelector}`); return; } if (parentElement && !document.querySelector(this.selector)) { const cardHTML = ` <div class="card m-0"> <div class="card-header p-0" id="${this.headingId}"> <h2 class="mb-0"> <button class="btn btn-link" type="button" aria-expanded="false" aria-controls="${this.selector.substring(1)}"> ${this.title} </button> </h2> </div> <div id="${this.selector.substring(1)}" class="collapse" aria-labelledby="${this.headingId}" data-parent="#accordionExample" style="max-height: 70vh; overflow-y: auto;"> <div class="card-body"> ${this.contentHTML} </div> </div> </div> `; parentElement.insertAdjacentHTML('beforeend', cardHTML); const toggleButton = document.querySelector(`#${this.headingId} button`); const collapseDiv = document.querySelector(this.selector); toggleButton.addEventListener('click', function() { if (collapseDiv.style.maxHeight === '0px' || !collapseDiv.style.maxHeight) { collapseDiv.style.display = 'block'; requestAnimationFrame(() => { collapseDiv.style.maxHeight = collapseDiv.scrollHeight + 'px'; }); toggleButton.setAttribute('aria-expanded', 'true'); } else { collapseDiv.style.maxHeight = '0px'; toggleButton.setAttribute('aria-expanded', 'false'); collapseDiv.addEventListener('transitionend', () => { if (collapseDiv.style.maxHeight === '0px') { collapseDiv.style.display = 'none'; } }, { once: true }); } }); collapseDiv.style.maxHeight = '0px'; collapseDiv.style.overflow = 'hidden'; collapseDiv.style.transition = 'max-height 0.3s ease'; } } } // 定义具体的文本替换管理卡片 class StringReplaceCard extends Card { constructor(parentSelector) { const headingId = 'headingOne'; const contentHTML = ` <div id="manageReplacePage"> <div id="replaceListContainer"></div> <div class="replace-item mb-3 p-2" style="border: 1px solid #ccc; border-radius: 8px;"> <input type="text" placeholder="查找文本" id="newFindText" class="form-control mb-2"/> <input type="text" placeholder="替换为" id="newReplacementText" class="form-control mb-2"/> <button class="btn btn-secondary" id="addReplaceRuleButton"> <i class="far fa-plus-circle"></i> 添加替换规则 </button> </div> <div class="mt-3"> <button class="btn btn-primary" id="exportReplaceRulesButton">导出替换规则</button> <input type="file" id="importReplaceRuleInput" class="d-none"/> <button class="btn btn-primary" id="importReplaceRuleButton">导入替换规则</button> </div> </div> `; super('#collapseOne', parentSelector, headingId, '文本替换', contentHTML); } insert() { super.insert(); if (!document.querySelector('#collapseOne')) { return; } document.getElementById('addReplaceRuleButton').addEventListener('click', this.addReplaceRule); document.getElementById('exportReplaceRulesButton').addEventListener('click', this.exportReplaceRules); document.getElementById('importReplaceRuleButton').addEventListener('click', () => { document.getElementById('importReplaceRuleInput').click(); }); document.getElementById('importReplaceRuleInput').addEventListener('change', this.importReplaceRules); this.loadReplaceList(); } addReplaceRule = () => { const findText = document.getElementById('newFindText').value; const replacementText = document.getElementById('newReplacementText').value; if (findText) { const replaceList = JSON.parse(localStorage.getItem('replaceList')) || []; replaceList.push({ findText, replacementText, disabled: false }); localStorage.setItem('replaceList', JSON.stringify(replaceList)); this.loadReplaceList(); document.getElementById('newFindText').value = ''; document.getElementById('newReplacementText').value = ''; } }; updateRuleText(index, type, value) { const replaceList = JSON.parse(localStorage.getItem('replaceList')) || []; if (replaceList[index]) { if (type === 'findText') { replaceList[index].findText = value; } else if (type === 'replacementText') { replaceList[index].replacementText = value; } localStorage.setItem('replaceList', JSON.stringify(replaceList)); } } loadReplaceList() { const replaceList = JSON.parse(localStorage.getItem('replaceList')) || []; const replaceListDiv = document.getElementById('replaceListContainer'); replaceListDiv.innerHTML = ''; // Add scrollbar when rules are too many replaceListDiv.style.maxHeight = '40vh'; // Adjust as needed replaceListDiv.style.overflowY = 'auto'; replaceList.forEach((rule, index) => { const ruleDiv = document.createElement('div'); ruleDiv.className = 'replace-item mb-3 p-2'; ruleDiv.style.border = '1px solid #ccc'; ruleDiv.style.borderRadius = '8px'; ruleDiv.style.transition = 'transform 0.3s'; ruleDiv.style.backgroundColor = rule.disabled ? '#f2dede' : '#fff'; const inputsDiv = document.createElement('div'); inputsDiv.className = 'mb-2'; const findInput = document.createElement('input'); findInput.type = 'text'; findInput.className = 'form-control mb-1'; findInput.value = rule.findText; findInput.placeholder = '查找文本'; findInput.dataset.index = index; findInput.addEventListener('change', (event) => this.updateRuleText(index, 'findText', event.target.value)); inputsDiv.appendChild(findInput); const replInput = document.createElement('input'); replInput.type = 'text'; replInput.className = 'form-control'; replInput.value = rule.replacementText; replInput.placeholder = '替换为'; replInput.dataset.index = index; replInput.addEventListener('change', (event) => this.updateRuleText(index, 'replacementText', event.target.value)); inputsDiv.appendChild(replInput); ruleDiv.appendChild(inputsDiv); const buttonsDiv = document.createElement('div'); buttonsDiv.className = 'd-flex justify-content-between'; const leftButtonGroup = document.createElement('div'); leftButtonGroup.className = 'btn-group'; leftButtonGroup.setAttribute('role', 'group'); const moveUpButton = this.createButton('上移', 'fas fa-arrow-up', () => this.moveReplaceRule(index, -1)); const moveDownButton = this.createButton('下移', 'fas fa-arrow-down', () => this.moveReplaceRule(index, 1)); const toggleButton = this.createButton('禁用/启用', rule.disabled ? 'fas fa-toggle-off' : 'fas fa-toggle-on', () => this.toggleReplaceRule(index)); const applyButton = this.createButton('应用此规则', 'fas fa-play', () => this.applySingleReplaceRule(index)); leftButtonGroup.append(moveUpButton, moveDownButton, toggleButton, applyButton); const rightButtonGroup = document.createElement('div'); rightButtonGroup.className = 'btn-group'; rightButtonGroup.setAttribute('role', 'group'); const deleteButton = this.createButton('删除', 'far fa-trash-alt', () => this.deleteReplaceRule(index), 'btn-danger'); rightButtonGroup.appendChild(deleteButton); buttonsDiv.append(leftButtonGroup, rightButtonGroup); ruleDiv.appendChild(buttonsDiv); replaceListDiv.appendChild(ruleDiv); }); replaceListDiv.style.display = 'none'; replaceListDiv.offsetHeight; replaceListDiv.style.display = ''; } createButton(title, iconClass, onClick, btnClass = 'btn-secondary') { const button = document.createElement('button'); button.className = `btn ${btnClass}`; button.title = title; button.innerHTML = `<i class="${iconClass}"></i>`; button.addEventListener('click', onClick); return button; } deleteReplaceRule(index) { const replaceList = JSON.parse(localStorage.getItem('replaceList')) || []; replaceList.splice(index, 1); localStorage.setItem('replaceList', JSON.stringify(replaceList)); this.loadReplaceList(); } toggleReplaceRule(index) { const replaceList = JSON.parse(localStorage.getItem('replaceList')) || []; replaceList[index].disabled = !replaceList[index].disabled; localStorage.setItem('replaceList', JSON.stringify(replaceList)); this.loadReplaceList(); } applySingleReplaceRule(index) { const replaceList = JSON.parse(localStorage.getItem('replaceList')) || []; const rule = replaceList[index]; if (rule.disabled || !rule.findText) return; const textareas = document.querySelectorAll('textarea.translation.form-control'); textareas.forEach(textarea => { let text = textarea.value; text = text.replaceAll(rule.findText, rule.replacementText); this.simulateInputChange(textarea, text); }); } moveReplaceRule(index, direction) { const replaceList = JSON.parse(localStorage.getItem('replaceList')) || []; const newIndex = index + direction; if (newIndex >= 0 && newIndex < replaceList.length) { const [movedItem] = replaceList.splice(index, 1); replaceList.splice(newIndex, 0, movedItem); localStorage.setItem('replaceList', JSON.stringify(replaceList)); this.loadReplaceListWithAnimation(index, newIndex); } } loadReplaceListWithAnimation(oldIndex, newIndex) { const replaceListDiv = document.getElementById('replaceListContainer'); const items = replaceListDiv.querySelectorAll('.replace-item'); if (items[oldIndex] && items[newIndex]) { items[oldIndex].style.transform = `translateY(${(newIndex - oldIndex) * 100}%)`; items[newIndex].style.transform = `translateY(${(oldIndex - newIndex) * 100}%)`; } setTimeout(() => { this.loadReplaceList(); }, 300); } simulateInputChange(element, newValue) { const inputEvent = new Event('input', { bubbles: true }); const originalValue = element.value; element.value = newValue; const tracker = element._valueTracker; if (tracker) { tracker.setValue(originalValue); } element.dispatchEvent(inputEvent); } exportReplaceRules() { const replaceList = JSON.parse(localStorage.getItem('replaceList')) || []; const json = JSON.stringify(replaceList, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'replaceList.json'; a.click(); URL.revokeObjectURL(url); } importReplaceRules(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = e => { try { const content = e.target.result; const importedList = JSON.parse(content); if (Array.isArray(importedList) && importedList.every(item => typeof item.findText === 'string' && typeof item.replacementText === 'string')) { localStorage.setItem('replaceList', JSON.stringify(importedList)); this.loadReplaceList(); } else { alert('导入的文件格式不正确。'); } } catch (error) { console.error('Error importing rules:', error); alert('导入失败,文件可能已损坏或格式不正确。'); } }; reader.readAsText(file); event.target.value = null; } } // 定义具体的机器翻译卡片 class MachineTranslationCard extends Card { constructor(parentSelector) { const headingId = 'headingTwo'; const contentHTML = ` <button class="btn btn-primary" id="openTranslationConfigButton">配置翻译</button> <div class="mt-3"> <div class="d-flex"> <textarea id="originalText" class="form-control" style="width: 100%; height: 25vh;"></textarea> <div class="d-flex flex-column ml-2"> <button class="btn btn-secondary mb-2" id="copyOriginalButton"> <i class="fas fa-copy"></i> </button> <button class="btn btn-secondary" id="translateButton"> <i class="fas fa-globe"></i> </button> </div> </div> <div class="d-flex mt-2"> <textarea id="translatedText" class="form-control" style="width: 100%; height: 25vh;"></textarea> <div class="d-flex flex-column ml-2"> <button class="btn btn-secondary mb-2" id="pasteTranslationButton"> <i class="fas fa-arrow-alt-left"></i> </button> <button class="btn btn-secondary" id="copyTranslationButton"> <i class="fas fa-copy"></i> </button> </div> </div> </div> <!-- Translation Configuration Modal --> <div class="modal" id="translationConfigModal" tabindex="-1" role="dialog" style="display: none;"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header py-2"> <h5 class="modal-title">翻译配置</h5> <button type="button" class="close" id="closeTranslationConfigModal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body p-2" style="max-height: 70vh; overflow-y: auto;"> <form id="translationConfigForm"> <div class="form-group"> <label for="baseUrl">Base URL</label> <div class="input-group"> <input type="text" class="form-control" id="baseUrl" placeholder="Enter base URL"> <div class="input-group-append"> <button class="btn btn-outline-secondary" type="button" title="OpenAI API" id="openaiButton"> <img src="https://paratranz.cn/media/f2014e0647283fcff54e3a8f4edaa488.png!webp160" style="width: 16px; height: 16px;"> </button> <button class="btn btn-outline-secondary" type="button" title="DeepSeek API" id="deepseekButton"> <img src="https://paratranz.cn/media/0bfd294f99b9141e3432c0ffbf3d8e78.png!webp160" style="width: 16px; height: 16px;"> </button> </div> </div> <small id="fullUrlPreview" class="form-text text-muted mt-1" style="word-break: break-all;"></small> </div> <div class="form-group"> <label for="apiKey">API Key</label> <input type="text" class="form-control" id="apiKey" placeholder="Enter API key"> </div> <div class="form-group"> <label for="model">Model</label> <input type="text" class="form-control" id="model" placeholder="Enter model"> </div> <div class="form-group"> <label for="prompt">Prompt</label> <textarea class="form-control" id="prompt" rows="3" placeholder="Enter prompt or use default prompt"></textarea> </div> <div class="form-group"> <label for="promptLibrarySelect">Prompt 库</label> <div class="input-group"> <select class="custom-select" id="promptLibrarySelect"> <option value="" selected>从库中选择或管理...</option> </select> <div class="input-group-append"> <button class="btn btn-outline-secondary" type="button" id="saveToPromptLibraryButton" title="保存当前Prompt到库"><i class="fas fa-save"></i></button> <button class="btn btn-outline-danger" type="button" id="deleteFromPromptLibraryButton" title="从库中删除选定Prompt"><i class="fas fa-trash-alt"></i></button> </div> </div> </div> <div class="form-group"> <label for="temperature">Temperature</label> <input type="number" step="0.1" class="form-control" id="temperature" placeholder="Enter temperature"> </div> <div class="form-group"> <label>自动化选项</label> <div class="d-flex justify-content-between"> <div class="custom-control custom-switch mr-2"> <input type="checkbox" class="custom-control-input" id="autoTranslateToggle"> <label class="custom-control-label" for="autoTranslateToggle">自动翻译</label> </div> <div class="custom-control custom-switch"> <input type="checkbox" class="custom-control-input" id="autoPasteToggle"> <label class="custom-control-label" for="autoPasteToggle">自动粘贴</label> </div> </div> <small class="form-text text-muted">自动翻译:进入新条目时自动翻译 / 自动粘贴:翻译完成后自动填充到翻译框</small> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" id="closeTranslationConfigModalButton">关闭</button> </div> </div> </div> </div> `; super('#collapseTwo', parentSelector, headingId, '机器翻译', contentHTML); } insert() { super.insert(); if (!document.querySelector('#collapseTwo')) { return; } const translationConfigModal = document.getElementById('translationConfigModal'); document.getElementById('openTranslationConfigButton').addEventListener('click', function() { translationConfigModal.style.display = 'block'; }); function closeModal() { translationConfigModal.style.display = 'none'; } document.getElementById('closeTranslationConfigModal').addEventListener('click', closeModal); document.getElementById('closeTranslationConfigModalButton').addEventListener('click', closeModal); const baseUrlInput = document.getElementById('baseUrl'); const apiKeyInput = document.getElementById('apiKey'); const modelSelect = document.getElementById('model'); const promptInput = document.getElementById('prompt'); const temperatureInput = document.getElementById('temperature'); const autoTranslateToggle = document.getElementById('autoTranslateToggle'); const promptLibrarySelect = document.getElementById('promptLibrarySelect'); const saveToPromptLibraryButton = document.getElementById('saveToPromptLibraryButton'); const deleteFromPromptLibraryButton = document.getElementById('deleteFromPromptLibraryButton'); baseUrlInput.value = localStorage.getItem('baseUrl') || ''; apiKeyInput.value = localStorage.getItem('apiKey') || ''; modelSelect.value = localStorage.getItem('model') || 'gpt-4o-mini'; promptInput.value = localStorage.getItem('prompt') || ''; temperatureInput.value = localStorage.getItem('temperature') || ''; autoTranslateToggle.checked = localStorage.getItem('autoTranslateEnabled') === 'true'; baseUrlInput.addEventListener('input', () => { const value = baseUrlInput.value; localStorage.setItem('baseUrl', value); updateFullUrlPreview(value); }); document.getElementById('openaiButton').addEventListener('click', () => { baseUrlInput.value = 'https://api.openai.com/v1'; localStorage.setItem('baseUrl', baseUrlInput.value); updateFullUrlPreview(baseUrlInput.value); }); document.getElementById('deepseekButton').addEventListener('click', () => { baseUrlInput.value = 'https://api.deepseek.com'; localStorage.setItem('baseUrl', baseUrlInput.value); updateFullUrlPreview(baseUrlInput.value); }); function updateFullUrlPreview(baseUrl) { const fullUrlPreview = document.getElementById('fullUrlPreview'); if (baseUrl) { const fullUrl = `${baseUrl}${baseUrl.endsWith('/') ? '' : '/'}chat/completions`; fullUrlPreview.textContent = `完整URL: ${fullUrl}`; } else { fullUrlPreview.textContent = ''; } } updateFullUrlPreview(baseUrlInput.value); apiKeyInput.addEventListener('input', () => localStorage.setItem('apiKey', apiKeyInput.value)); modelSelect.addEventListener('input', () => localStorage.setItem('model', modelSelect.value)); promptInput.addEventListener('input', () => localStorage.setItem('prompt', promptInput.value)); temperatureInput.addEventListener('input', () => localStorage.setItem('temperature', temperatureInput.value)); autoTranslateToggle.addEventListener('change', () => localStorage.setItem('autoTranslateEnabled', autoTranslateToggle.checked)); const autoPasteToggle = document.getElementById('autoPasteToggle'); autoPasteToggle.checked = localStorage.getItem('autoPasteEnabled') === 'true'; autoPasteToggle.addEventListener('change', () => localStorage.setItem('autoPasteEnabled', autoPasteToggle.checked)); const PROMPT_LIBRARY_KEY = 'promptLibrary'; function getPromptLibrary() { return JSON.parse(localStorage.getItem(PROMPT_LIBRARY_KEY)) || []; } function savePromptLibrary(library) { localStorage.setItem(PROMPT_LIBRARY_KEY, JSON.stringify(library)); } function populatePromptLibrarySelect() { const library = getPromptLibrary(); promptLibrarySelect.innerHTML = '<option value="" selected>从库中选择或管理...</option>'; library.forEach((promptText) => { const option = document.createElement('option'); option.value = promptText; option.textContent = promptText.substring(0, 50) + (promptText.length > 50 ? '...' : ''); option.dataset.fulltext = promptText; promptLibrarySelect.appendChild(option); }); } promptLibrarySelect.addEventListener('change', function() { if (this.value) { promptInput.value = this.value; localStorage.setItem('prompt', this.value); } }); saveToPromptLibraryButton.addEventListener('click', function() { const currentPrompt = promptInput.value.trim(); if (currentPrompt) { let library = getPromptLibrary(); if (!library.includes(currentPrompt)) { library.push(currentPrompt); savePromptLibrary(library); populatePromptLibrarySelect(); promptLibrarySelect.value = currentPrompt; } else { alert('此 Prompt 已存在于库中。'); } } else { alert('Prompt 内容不能为空。'); } }); deleteFromPromptLibraryButton.addEventListener('click', function() { const selectedPromptValue = promptLibrarySelect.value; if (selectedPromptValue) { let library = getPromptLibrary(); const indexToRemove = library.indexOf(selectedPromptValue); if (indexToRemove > -1) { library.splice(indexToRemove, 1); savePromptLibrary(library); populatePromptLibrarySelect(); if (promptInput.value === selectedPromptValue) { promptInput.value = ''; localStorage.setItem('prompt', ''); } } } else { alert('请先从库中选择一个 Prompt 进行删除。'); } }); populatePromptLibrarySelect(); temperatureInput.addEventListener('input', function() { localStorage.setItem('temperature', temperatureInput.value); }); autoTranslateToggle.addEventListener('change', function() { localStorage.setItem('autoTranslateEnabled', autoTranslateToggle.checked); }); this.setupTranslation(); } setupTranslation() { function removeThoughtProcessContent(text) { if (typeof text !== 'string') return text; // 移除XML风格的思考标签 let cleanedText = text.replace(/<thought>[\s\S]*?<\/thought>/gi, ''); cleanedText = cleanedText.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');cleanedText = cleanedText.replace(/<think>[\s\S]*?<\/think>/gi, ''); cleanedText = cleanedText.replace(/<reasoning>[\s\S]*?<\/reasoning>/gi, ''); // 移除Markdown风格的思考标签 cleanedText = cleanedText.replace(/\[THOUGHT\][\s\S]*?\[\/THOUGHT\]/gi, ''); cleanedText = cleanedText.replace(/\[REASONING\][\s\S]*?\[\/REASONING\]/gi, ''); // 移除以特定关键词开头的思考过程 cleanedText = cleanedText.replace(/^(思考过程:|思考:|Thought process:|Thought:|Thinking:|Reasoning:)[\s\S]*?(\n|$)/gim, ''); // 移除常见的工具交互XML标签 cleanedText = cleanedText.replace(/<tool_code>[\s\S]*?<\/tool_code>/gi, ''); cleanedText = cleanedText.replace(/<tool_code_executing>[\s\S]*?<\/tool_code_executing>/gi, ''); cleanedText = cleanedText.replace(/<tool_code_completed>[\s\S]*?<\/tool_code_completed>/gi, ''); cleanedText = cleanedText.replace(/<tool_code_error>[\s\S]*?<\/tool_code_error>/gi, ''); cleanedText = cleanedText.replace(/<tool_code_output>[\s\S]*?<\/tool_code_output>/gi, ''); cleanedText = cleanedText.replace(/<tool_code_execution_succeeded>[\s\S]*?<\/tool_code_execution_succeeded>/gi, ''); cleanedText = cleanedText.replace(/<tool_code_execution_failed>[\s\S]*?<\/tool_code_execution_failed>/gi, ''); // 移除 SEARCH/REPLACE 块标记 cleanedText = cleanedText.replace(/<<<<<<< SEARCH[\s\S]*?>>>>>>> REPLACE/gi, ''); // 清理多余的空行,并将多个连续空行合并为一个 cleanedText = cleanedText.replace(/\n\s*\n/g, '\n'); // 移除首尾空白字符 (包括换行符) cleanedText = cleanedText.trim(); return cleanedText; } const translationCache = {}; const translationsInProgress = {}; async function getCurrentStringId() { const pathParts = window.location.pathname.split('/'); let stringId = null; const stringsIndex = pathParts.indexOf('strings'); if (stringsIndex !== -1 && pathParts.length > stringsIndex + 1) { const idFromPath = pathParts[stringsIndex + 1]; if (!isNaN(parseInt(idFromPath, 10))) { stringId = idFromPath; } } if (!stringId) { const copyLinkButton = document.querySelector('.string-editor a.float-right.no-select[href*="/strings?id="]'); if (copyLinkButton) { const href = copyLinkButton.getAttribute('href'); const urlParams = new URLSearchParams(href.split('?')[1]); stringId = urlParams.get('id'); } else { const settingsLink = document.querySelector('.string-editor .tab.context-tab a[href*="/settings/strings?id="]'); if (settingsLink) { const href = settingsLink.getAttribute('href'); const urlParams = new URLSearchParams(href.split('?')[1]); stringId = urlParams.get('id'); } } } return stringId && !isNaN(parseInt(stringId, 10)) ? stringId : null; } function updateTranslationUI(text, modelName, stringIdForUI) { document.getElementById('translatedText').value = text; if (localStorage.getItem('autoPasteEnabled') === 'true') { const targetTextarea = document.querySelector('textarea.translation.form-control'); // 修复:仅当翻译框为空时才自动粘贴 if (targetTextarea && targetTextarea.value.trim() === '') { simulateInputChange(targetTextarea, text); } } let translationMemoryDiv = document.querySelector('.translation-memory'); let mtListContainer; if (!translationMemoryDiv) { const tabs = document.querySelector('.sidebar-right .tabs'); if (!tabs) { console.error('找不到.sidebar-right .tabs元素'); return; } translationMemoryDiv = document.createElement('div'); translationMemoryDiv.className = 'translation-memory'; translationMemoryDiv.style.display = 'block'; const header = document.createElement('header'); header.className = 'mb-3'; const headerContent = document.createElement('div'); headerContent.className = 'row medium align-items-center'; headerContent.innerHTML = ` <div class="col-auto"> <button title="Ctrl + Shift + F" type="button" class="btn btn-secondary btn-sm"> <i class="far fa-search"></i> 搜索历史翻译 </button> </div> <div class="col text-right"> <span class="text-muted">共 0 条建议</span> <button type="button" class="btn btn-secondary btn-sm"><i class="far fa-cog fa-fw"></i></button> </div>`; header.appendChild(headerContent); translationMemoryDiv.appendChild(header); mtListContainer = document.createElement('div'); mtListContainer.className = 'list mt-list'; translationMemoryDiv.appendChild(mtListContainer); tabs.insertBefore(translationMemoryDiv, tabs.firstChild); } else { mtListContainer = translationMemoryDiv.querySelector('.list.mt-list'); if (!mtListContainer) { mtListContainer = document.createElement('div'); mtListContainer.className = 'list mt-list'; const header = translationMemoryDiv.querySelector('header'); if (header) header.insertAdjacentElement('afterend', mtListContainer); else translationMemoryDiv.appendChild(mtListContainer); } } const existingAiReferences = mtListContainer.querySelectorAll('.mt-reference.paratranz-ai-reference'); existingAiReferences.forEach(ref => ref.remove()); if (mtListContainer) { const newReferenceDiv = document.createElement('div'); newReferenceDiv.className = 'mt-reference paratranz-ai-reference'; newReferenceDiv.dataset.stringId = stringIdForUI; const header = document.createElement('header'); header.className = 'medium mb-2 text-muted'; const icon = document.createElement('i'); icon.className = 'far fa-language'; header.appendChild(icon); header.appendChild(document.createTextNode(' 机器翻译参考')); newReferenceDiv.appendChild(header); const bodyRow = document.createElement('div'); bodyRow.className = 'row align-items-center'; const colAuto = document.createElement('div'); colAuto.className = 'col-auto pr-0'; const copyButton = document.createElement('button'); copyButton.title = '复制当前文本至翻译框'; copyButton.type = 'button'; copyButton.className = 'btn btn-link'; const copyIcon = document.createElement('i'); copyIcon.className = 'far fa-clone'; copyButton.appendChild(copyIcon); copyButton.addEventListener('click', function() { simulateInputChange(document.querySelector('textarea.translation.form-control'), text); }); colAuto.appendChild(copyButton); bodyRow.appendChild(colAuto); const colText = document.createElement('div'); colText.className = 'col'; const translationSpan = document.createElement('span'); translationSpan.className = 'translation notranslate'; translationSpan.textContent = text; colText.appendChild(translationSpan); bodyRow.appendChild(colText); newReferenceDiv.appendChild(bodyRow); const footer = document.createElement('footer'); footer.className = 'medium mt-2 text-muted'; const leftText = document.createElement('span'); leftText.textContent = 'Paratranz-AI'; const rightText = document.createElement('div'); rightText.className = 'float-right'; rightText.textContent = modelName || 'N/A'; footer.appendChild(leftText); footer.appendChild(rightText); newReferenceDiv.appendChild(footer); mtListContainer.prepend(newReferenceDiv); } } async function processTranslationRequest(stringIdToProcess, textToTranslate) { const translateButtonElement = document.getElementById('translateButton'); if (!stringIdToProcess) { console.warn('processTranslationRequest called with no stringId.'); return; } if (translationsInProgress[stringIdToProcess]) { console.log(`Translation for ${stringIdToProcess} is already in progress. Ignoring new request.`); return; } translationsInProgress[stringIdToProcess] = true; if (translateButtonElement) translateButtonElement.disabled = true; document.getElementById('translatedText').value = '翻译中...'; let translatedTextOutput = ''; try { console.log(`Processing translation for stringId ${stringIdToProcess}:`, textToTranslate); const model = localStorage.getItem('model') || 'gpt-4o-mini'; const promptStr = localStorage.getItem('prompt') || `You are a translator, you will translate all the message I send to you.\n\nSource Language: en\nTarget Language: zh-cn\n\nOutput result and thought with zh-cn, and keep the result pure text\nwithout any markdown syntax and any thought or references.\n\nInstructions:\n - Accuracy: Ensure the translation accurately conveys the original meaning.\n - Context: Adapt to cultural nuances and specific context to avoid misinterpretation.\n - Tone: Match the tone (formal, informal, technical) of the source text.\n - Grammar: Use correct grammar and sentence structure in the target language.\n - Readability: Ensure the translation is clear and easy to understand.\n - Keep Tags: Maintain the original tags intact, do not translate tags themselves!\n - Keep or remove the spaces around the tags based on the language manners (in CJK, usually the spaces will be removed).\n\nTags are matching the following regular expressions (one per line):\n/{\w+}/\n/%[ds]?\d/\n/\\s#\d{1,2}/\n/<[^>]+?>/\n/%{\d}[a-z]/\n/@[a-zA-Z.]+?@/`; const temperature = parseFloat(localStorage.getItem('temperature')) || 0; translatedTextOutput = await translateText(textToTranslate, model, promptStr, temperature); const replaceList = JSON.parse(localStorage.getItem('replaceList')) || []; replaceList.forEach(rule => { if (!rule.disabled && rule.findText) { translatedTextOutput = translatedTextOutput.replaceAll(rule.findText, rule.replacementText); } }); // 新增:去除思维链内容 translatedTextOutput = removeThoughtProcessContent(translatedTextOutput); translationCache[stringIdToProcess] = translatedTextOutput; const currentPageId = await getCurrentStringId(); if (currentPageId === stringIdToProcess) { updateTranslationUI(translatedTextOutput, model, stringIdToProcess); } else { console.log(`Translated stringId ${stringIdToProcess}, but page is now ${currentPageId}. Reference UI not updated for ${stringIdToProcess}.`); document.getElementById('translatedText').value = translatedTextOutput; } } catch (error) { console.error(`Error during translation processing for stringId ${stringIdToProcess}:`, error); const translatedTextArea = document.getElementById('translatedText'); if (translatedTextArea) { translatedTextArea.value = `翻译出错 (ID: ${stringIdToProcess}): ${error.message}`; } } finally { delete translationsInProgress[stringIdToProcess]; if (translateButtonElement) translateButtonElement.disabled = false; console.log(`Translation processing for stringId ${stringIdToProcess} finished, flags reset.`); } } async function updateOriginalTextAndTranslateIfNeeded() { const currentStringId = await getCurrentStringId(); if (!currentStringId) { return; } const originalDiv = document.querySelector('.original.well'); if (originalDiv) { const originalText = originalDiv.innerText; document.getElementById('originalText').value = originalText; const existingAiReference = document.querySelector('.mt-reference.paratranz-ai-reference'); if (translationCache[currentStringId]) { console.log(`Using cached translation for stringId: ${currentStringId}`); const model = localStorage.getItem('model') || 'gpt-4o-mini'; if (existingAiReference && existingAiReference.dataset.stringId !== currentStringId) { existingAiReference.remove(); } updateTranslationUI(translationCache[currentStringId], model, currentStringId); return; } else { if (existingAiReference) { existingAiReference.remove(); } } if (localStorage.getItem('autoTranslateEnabled') === 'true' && originalText.trim() !== '' && !translationsInProgress[currentStringId]) { console.log(`Auto-translating for stringId: ${currentStringId}`); await processTranslationRequest(currentStringId, originalText); } else if (translationsInProgress[currentStringId]) { console.log(`Translation already in progress for stringId: ${currentStringId} (checked in updateOriginalText)`); } } } let debounceTimer = null; const observer = new MutationObserver(async () => { if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(async () => { console.log('Observer triggered, updating original text and checking translation.'); await updateOriginalTextAndTranslateIfNeeded(); }, 200); }); const config = { childList: true, subtree: true, characterData: true }; const originalDivTarget = document.querySelector('.original.well'); if (originalDivTarget) { observer.observe(originalDivTarget, config); updateOriginalTextAndTranslateIfNeeded(); } else { console.warn("Original text container (.original.well) not found at observer setup."); } document.getElementById('copyOriginalButton').addEventListener('click', async () => { await updateOriginalTextAndTranslateIfNeeded(); }); document.getElementById('translateButton').addEventListener('click', async function() { const currentStringId = await getCurrentStringId(); const originalText = document.getElementById('originalText').value; if (!currentStringId) { console.error('Cannot translate: No valid stringId found for manual trigger.'); return; } await processTranslationRequest(currentStringId, originalText); }); document.getElementById('copyTranslationButton').addEventListener('click', function() { const translatedText = document.getElementById('translatedText').value; navigator.clipboard.writeText(translatedText).then(() => { console.log('Translated text copied to clipboard'); }).catch(err => { console.error('Failed to copy text: ', err); }); }); document.getElementById('pasteTranslationButton').addEventListener('click', function() { const translatedText = document.getElementById('translatedText').value; simulateInputChange(document.querySelector('textarea.translation.form-control'), translatedText); }); } } // 获取术语表数据 (异步) async function getTermsData() { const terms = []; const pathParts = window.location.pathname.split('/'); let projectId = null; let stringId = null; const projectIndex = pathParts.indexOf('projects'); if (projectIndex !== -1 && pathParts.length > projectIndex + 1) { projectId = pathParts[projectIndex + 1]; } const stringsIndex = pathParts.indexOf('strings'); if (stringsIndex !== -1 && pathParts.length > stringsIndex + 1) { const idFromPath = pathParts[stringsIndex + 1]; if (!isNaN(parseInt(idFromPath, 10))) { stringId = idFromPath; } } if (!stringId) { const copyLinkButton = document.querySelector('.string-editor a.float-right.no-select[href*="/strings?id="]'); if (copyLinkButton) { const href = copyLinkButton.getAttribute('href'); const urlParams = new URLSearchParams(href.split('?')[1]); const idFromHref = urlParams.get('id'); if (idFromHref && !isNaN(parseInt(idFromHref, 10))) { stringId = idFromHref; // console.log(`从页面 context-tab 的“复制链接”按钮获取到 stringId: ${stringId}`); const hrefPathParts = new URL(href, window.location.origin).pathname.split('/'); const projectIdx = hrefPathParts.indexOf('projects'); if (projectIdx !== -1 && hrefPathParts.length > projectIdx + 1) { const pidFromHref = hrefPathParts[projectIdx + 1]; if (pidFromHref && projectId !== pidFromHref) { // console.log(`从“复制链接”的 href 中更新 projectId 从 ${projectId} 到 ${pidFromHref}`); projectId = pidFromHref; } } } } else { const settingsLink = document.querySelector('.string-editor .tab.context-tab a[href*="/settings/strings?id="]'); if (settingsLink) { const href = settingsLink.getAttribute('href'); const urlParams = new URLSearchParams(href.split('?')[1]); const idFromHref = urlParams.get('id'); if (idFromHref && !isNaN(parseInt(idFromHref, 10))) { stringId = idFromHref; // console.log(`从页面 context-tab 的“设置”链接获取到 stringId: ${stringId}`); } } } } if (!projectId) { console.warn('无法从 URL 中解析项目 ID。URL:', window.location.pathname); return terms; } if (!stringId || isNaN(parseInt(stringId, 10))) { // console.warn(`无法从 URL 或页面元素中解析有效的字符串 ID "${stringId}",跳过术语获取。`); return terms; } const apiUrl = `https://paratranz.cn/api/projects/${projectId}/strings/${stringId}/terms`; try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时 const response = await fetch(apiUrl, { signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { console.error(`获取术语 API 失败: ${response.status} ${response.statusText}`); return terms; } const apiResult = await response.json(); apiResult.forEach(item => { if (item.term && item.translation) { terms.push({ source: item.term, target: item.translation, note: item.note || '' }); } }); // console.log(`通过 API 获取到 ${terms.length} 条术语。`); } catch (error) { if (error.name === 'AbortError') { console.error('获取术语 API 超时。'); } else { console.error('调用术语 API 时发生错误:', error); } } return terms; } async function buildTermsSystemMessageWithRetry() { let terms = await getTermsData(); if (!terms.length) { // console.log('第一次通过 API 获取术语表失败或为空,等待100ms后重试...'); await new Promise(resolve => setTimeout(resolve, 100)); terms = await getTermsData(); if (!terms.length) { // console.log('第二次通过 API 获取术语表仍然失败或为空。'); return null; } // console.log(`第二次尝试通过 API 获取到 ${terms.length} 条术语。`); } else { // console.log(`第一次尝试通过 API 获取到 ${terms.length} 条术语。`); } const termsContext = terms.map(term => { let termString = `${term.source} → ${term.target}`; if (term.note) { termString += ` (备注(辅助思考不要出现在译文中):${term.note})`; } return termString; }).join('\n'); return { role: "system", content: `翻译时请参考以下术语表:\n${termsContext}` }; } class PromptTagProcessor { constructor() { this.tagProcessors = new Map(); this.setupDefaultTags(); } setupDefaultTags() { this.registerTag('original', (text) => text); this.registerTag('context', async () => { const contextDiv = document.querySelector('.context .well'); if (!contextDiv) return ''; return contextDiv.innerText.trim(); }); this.registerTag('terms', async () => { const terms = await getTermsData(); if (!terms.length) return ''; return terms.map(term => { let termString = `${term.source} → ${term.target}`; if (term.note) termString += ` (${term.note})`; return termString; }).join('\n'); }); } registerTag(tagName, processor) { this.tagProcessors.set(tagName, processor); } async processPrompt(prompt, originalText) { let processedPrompt = prompt; for (const [tagName, processor] of this.tagProcessors) { const tagPattern = new RegExp(`{{${tagName}}}`, 'g'); if (tagPattern.test(processedPrompt)) { let replacement; try { replacement = (tagName === 'original') ? originalText : await processor(); processedPrompt = processedPrompt.replace(tagPattern, replacement || ''); // console.log(`替换标签 {{${tagName}}} 成功`); } catch (error) { console.error(`处理标签 {{${tagName}}} 时出错:`, error); } } } // console.log('处理后的prompt:', processedPrompt); return processedPrompt; } } async function translateText(query, model, prompt, temperature) { const API_SECRET_KEY = localStorage.getItem('apiKey'); const BASE_URL = localStorage.getItem('baseUrl'); if (!prompt) { prompt = "You are a professional translator focusing on translating Magic: The Gathering cards from English to Chinese. You are given a card's original text in English. Translate it into Chinese."; } const tagProcessor = new PromptTagProcessor(); const processedPrompt = await tagProcessor.processPrompt(prompt, query); const messages = [{ role: "system", content: processedPrompt }]; // console.log('准备获取术语表信息...'); const termsMessage = await buildTermsSystemMessageWithRetry(); if (termsMessage && termsMessage.content) { // console.log('成功获取术语表信息,添加到请求中。'); messages.push(termsMessage); } else { // console.log('未获取到术语表信息或术语表为空,翻译请求将不包含术语表。'); } messages.push({ role: "user", content: query }); const requestBody = { model, temperature, messages }; try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 25000); // 25秒超时 const response = await fetch(`${BASE_URL}${BASE_URL.endsWith('/') ? '' : '/'}chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_SECRET_KEY}` }, body: JSON.stringify(requestBody), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { let errorData; try { errorData = await response.json(); } catch (e) { /* ignore */ } console.error('API Error:', errorData || response.statusText); return `API 翻译失败: ${response.status} - ${errorData?.error?.message || errorData?.message || response.statusText}`; } const data = await response.json(); if (data.choices && data.choices[0]?.message?.content) { return data.choices[0].message.content; } else { console.error('Invalid API response structure:', data); return '翻译失败: API响应格式无效'; } } catch (error) { if (error.name === 'AbortError') { console.error('API translation request timed out.'); return '翻译请求超时。'; } console.error('Translation Fetch/Network Error:', error); return `翻译请求失败: ${error.message || error.toString()}`; } } function simulateInputChange(element, newValue) { if (element.value.trim() !== '') { // return; // Allowing overwrite now based on typical user expectation for paste } const inputEvent = new Event('input', { bubbles: true }); const originalValue = element.value; element.value = newValue; const tracker = element._valueTracker; if (tracker) tracker.setValue(originalValue); element.dispatchEvent(inputEvent); } const accordion = new Accordion('#accordionExample', '.sidebar-right'); const stringReplaceCard = new StringReplaceCard('#accordionExample'); const machineTranslationCard = new MachineTranslationCard('#accordionExample'); accordion.addCard(stringReplaceCard); accordion.addCard(machineTranslationCard); // Diff对比模态框类 class DiffModal { constructor() { this.modalId = 'diffModal'; this.diffLib = null; // this.diff2htmlLib = null; // No longer using Diff2HtmlUI directly in the modal this.initModal(); this.initDiffLibraries(); } initDiffLibraries() { if (typeof Diff !== 'undefined') { this.diffLib = Diff; console.log('jsdiff library initialized successfully'); } else { console.error('jsdiff library is not available'); } } initModal() { if (document.getElementById(this.modalId)) return; const modalHTML = ` <div class="modal" id="${this.modalId}" tabindex="-1" role="dialog" style="display: none;"> <div class="modal-dialog modal-xl" role="document"> <div class="modal-content"> <div class="modal-header py-2"> <h5 class="modal-title">文本对比</h5> <button type="button" class="close" id="closeDiffModal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body p-0" style="height: 70vh;"> <div class="diff-container d-flex h-100"> <div class="diff-original w-50 border-right" style="overflow-y: auto;"> <div class="diff-header bg-light p-2">原文</div> <div class="diff-content" id="originalDiffContent"></div> </div> <div class="diff-translation w-50" style="overflow-y: auto;"> <div class="diff-header bg-light p-2">当前翻译</div> <div class="diff-content" id="translationDiffContent"></div> </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" id="closeDiffModalButton">关闭</button> </div> </div> </div> </div> `; document.body.insertAdjacentHTML('beforeend', modalHTML); const style = document.createElement('style'); style.textContent = ` .diff-line { display: flex; padding: 2px 5px; font-family: monospace; line-height: 1.4; } .diff-line-number { min-width: 35px; color: #999; text-align: right; padding-right: 10px; user-select: none; font-size: 0.9em; } .diff-line-content { flex: 1; white-space: pre-wrap; word-break: break-word; padding-left: 5px; } .diff-line.diff-added { background-color: #e6ffed; } .diff-line.diff-removed { background-color: #ffeef0; } .copy-to-right { cursor: pointer; margin-left: 8px; padding: 0 4px; font-size: 0.9em; line-height: 1; border: 1px solid #ccc; border-radius: 3px; background-color: #f0f0f0; } .copy-to-right:hover { background-color: #e0e0e0; } .diff-header { font-weight: bold; position: sticky; top: 0; z-index: 1; background-color: #f8f9fa; /* Ensure header has background */ } .diff-placeholder { /* Style for placeholder lines */ color: #ccc; } `; document.head.appendChild(style); document.getElementById('closeDiffModal').addEventListener('click', this.closeModal.bind(this)); document.getElementById('closeDiffModalButton').addEventListener('click', this.closeModal.bind(this)); // We call generateDiff explicitly in show() } show() { const modal = document.getElementById(this.modalId); modal.style.display = 'block'; this.generateDiff(); } closeModal() { document.getElementById(this.modalId).style.display = 'none'; } generateDiff() { const originalText = document.querySelector('.original.well')?.innerText || ''; const translationText = document.querySelector('textarea.translation.form-control')?.value || ''; const originalContent = document.getElementById('originalDiffContent'); const translationContent = document.getElementById('translationDiffContent'); originalContent.innerHTML = ''; translationContent.innerHTML = ''; if (!this.diffLib) { console.error('Diff library (jsdiff) not loaded.'); originalContent.innerHTML = '<p>差异库未加载</p>'; return; } const diffResult = this.diffLib.diffLines(originalText, translationText, { newlineIsToken: true }); let origDisplayLineNum = 1; let transDisplayLineNum = 1; let currentTranslationFileIndex = 0; // 0-based index in the translationLines array for (let i = 0; i < diffResult.length; i++) { const part = diffResult[i]; let linesInPart = part.value.split('\n'); // If the part ends with a newline, split will produce an extra empty string. // We want to preserve actual empty lines but remove the one from a trailing newline. if (part.value.endsWith('\n') && linesInPart.length > 0) { linesInPart.pop(); } // Handle a part that is just a single newline (becomes one empty string line) if (part.value === '\n' && linesInPart.length === 1 && linesInPart[0] === '') { // This is correct, represents one empty line. } else if (linesInPart.length === 0 && part.value.length > 0) { // Should not happen if split correctly, but as a fallback for non-newline part linesInPart = [part.value]; } if (part.removed) { let actionType = 'insert'; // Default for pure delete // Check if this 'removed' part is followed by an 'added' part, indicating a "change" if (i + 1 < diffResult.length && diffResult[i+1].added) { actionType = 'replace'; } linesInPart.forEach(lineText => { this.appendLine(originalContent, origDisplayLineNum++, lineText, 'diff-removed', lineText, currentTranslationFileIndex, true, actionType); this.appendLine(translationContent, '-', '', 'diff-placeholder'); // currentTranslationFileIndex does NOT advance for the original's removed line itself, // but the button will target this index. If it's a replace, the corresponding 'added' line will use this index. }); } else if (part.added) { linesInPart.forEach(lineText => { this.appendLine(translationContent, transDisplayLineNum++, lineText, 'diff-added'); this.appendLine(originalContent, '-', '', 'diff-placeholder'); currentTranslationFileIndex++; }); } else { // Common lines linesInPart.forEach(lineText => { this.appendLine(originalContent, origDisplayLineNum++, lineText, ''); this.appendLine(translationContent, transDisplayLineNum++, lineText, ''); currentTranslationFileIndex++; }); } } } appendLine(container, lineNumber, text, diffClass, originalLineTextForCopy = null, translationLineIndexForCopy = null, showCopyButton = false, actionType = 'replace') { const lineDiv = document.createElement('div'); lineDiv.className = `diff-line ${diffClass || ''}`; const numberSpan = document.createElement('span'); numberSpan.className = 'diff-line-number'; numberSpan.textContent = lineNumber; lineDiv.appendChild(numberSpan); const contentSpan = document.createElement('span'); contentSpan.className = 'diff-line-content'; if (text === '' && diffClass && diffClass.includes('placeholder')) { contentSpan.innerHTML = ' '; // Ensure empty placeholders take up space } else { contentSpan.textContent = text; } lineDiv.appendChild(contentSpan); if (showCopyButton && originalLineTextForCopy !== null && translationLineIndexForCopy !== null) { const copyButton = document.createElement('button'); copyButton.className = 'btn btn-link p-0 ml-2 copy-to-right'; copyButton.innerHTML = '<i class="fas fa-arrow-right"></i>'; copyButton.title = actionType === 'replace' ? '替换右侧对应行' : '在此位置插入'; copyButton.addEventListener('click', () => { const textarea = document.querySelector('textarea.translation.form-control'); if (!textarea) return; let lines = textarea.value.split('\n'); const targetIndex = Math.max(0, translationLineIndexForCopy); if (actionType === 'replace') { while (lines.length <= targetIndex) { lines.push(''); } lines[targetIndex] = originalLineTextForCopy; } else { // 'insert' while (lines.length < targetIndex) { lines.push(''); } lines.splice(targetIndex, 0, originalLineTextForCopy); } textarea.value = lines.join('\n'); simulateInputChange(textarea, textarea.value); requestAnimationFrame(() => { this.generateDiff(); }); }); lineDiv.appendChild(copyButton); } container.appendChild(lineDiv); } } // 添加对比按钮 const diffButton = new Button( '.btn.btn-secondary.show-diff-button', '.toolbar .right .btn-group', '<i class="fas fa-file-alt"></i> 对比文本', function() { new DiffModal().show(); } ); const runAllReplacementsButton = new Button( '.btn.btn-secondary.apply-all-rules-button', '.toolbar .right .btn-group', '<i class="fas fa-cogs"></i> 应用全部替换', function() { const replaceList = JSON.parse(localStorage.getItem('replaceList')) || []; const textareas = document.querySelectorAll('textarea.translation.form-control'); textareas.forEach(textarea => { let text = textarea.value; replaceList.forEach(rule => { if (!rule.disabled && rule.findText) { text = text.replaceAll(rule.findText, rule.replacementText); } }); simulateInputChange(textarea, text); }); } ); })();