ParaTranz-AI

ParaTranz文本替换和AI翻译功能拓展。

当前为 2025-05-15 提交的版本,查看 最新版本

// ==UserScript==
// @name         ParaTranz-AI
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  ParaTranz文本替换和AI翻译功能拓展。
// @author       HCPTangHY
// @license      WTFPL
// @match        https://paratranz.cn/*
// @icon         https://paratranz.cn/favicon.png
// @grant        none
// ==/UserScript==

// 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 = '';
            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">&times;</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() {
            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);
                        }
                    });
                    
                    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('.string-editor .tab.context-tab .context');
                if (!contextDiv) return '';
                const contextItems = Array.from(contextDiv.querySelectorAll('.context-item')).map(item => {
                    const textElement = item.querySelector('.text');
                    return textElement ? textElement.textContent.trim() : '';
                });
                return contextItems.filter(text => text).join('\n');
            });
            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);

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