ParaTranz-AI

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

// ==UserScript==
// @name         ParaTranz-AI
// @namespace    http://tampermonkey.net/
// @version      1.4.2
// @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==

const PARATRANZ_AI_TOAST_STYLES = `
/* Toast Notifications */
#toast-container-paratranz-ai {
    position: fixed;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 10000;
    display: flex;
    flex-direction: column-reverse;
    align-items: center;
    pointer-events: none; /* Allow clicks to pass through the container */
}

.toast-message {
    padding: 10px 20px;
    margin-top: 10px;
    border-radius: 5px;
    color: white;
    box-shadow: 0 2px 10px rgba(0,0,0,0.2);
    opacity: 0;
    transform: translateY(20px);
    transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
    min-width: 250px;
    max-width: 80vw;
    text-align: center;
    pointer-events: all; /* Individual toasts should be interactive if needed */
}

.toast-message.show {
    opacity: 1;
    transform: translateY(0);
}

.toast-message.toast-success { background-color: #28a745; }
.toast-message.toast-error   { background-color: #dc3545; }
.toast-message.toast-warning { background-color: #ffc107; color: black; }
.toast-message.toast-info    { background-color: #17a2b8; }
`;
GM_addStyle(GM_getResourceText("css") + PARATRANZ_AI_TOAST_STYLES);

// fork from HeliumOctahelide https://greasyfork.org/zh-CN/scripts/503063-paratranz-tools
(function() {
    'use strict';

    // Helper function for Toast Notifications
    function showToast(message, type = 'info', duration = 3000) {
        let toastContainer = document.getElementById('toast-container-paratranz-ai');
        if (!toastContainer) {
            toastContainer = document.createElement('div');
            toastContainer.id = 'toast-container-paratranz-ai';
            document.body.appendChild(toastContainer);
        }

        const toast = document.createElement('div');
        toast.className = `toast-message toast-${type}`;
        toast.textContent = message;

        toastContainer.appendChild(toast);

        // Animate in
        requestAnimationFrame(() => {
            toast.classList.add('show');
        });

        // Auto-dismiss
        setTimeout(() => {
            toast.classList.remove('show');
            toast.addEventListener('transitionend', () => {
                if (toast.parentElement) { // Check if still attached
                    toast.remove();
                }
                if (toastContainer && !toastContainer.hasChildNodes()) {
                    // Check if toastContainer is still in the DOM before removing
                    if (toastContainer.parentElement) {
                        toastContainer.remove();
                    }
                }
            }, { once: true });
        }, duration);
    }

    // 基类定义
    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();
                        showToast('替换规则导入成功!', 'success');
                    } else {
                        showToast('导入的文件格式不正确。', 'error');
                    }
                } catch (error) {
                    console.error('Error importing rules:', error);
                    showToast('导入失败,文件可能已损坏或格式不正确。', 'error');
                }
            };
            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 modal-lg" role="document"> <!-- Added modal-lg -->
                        <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-3" style="max-height: 80vh; overflow-y: auto;"> <!-- Increased max-height, added p-3 -->
                                <form id="translationConfigForm">
                                    <div class="form-row">
                                        <div class="form-group col-md-7">
                                            <label for="apiConfigSelect">API 配置</label>
                                            <select class="custom-select" id="apiConfigSelect">
                                                <option value="" selected>选择或新建配置...</option>
                                            </select>
                                        </div>
                                        <div class="form-group col-md-5 d-flex align-items-end">
                                            <button type="button" class="btn btn-success mr-2 w-100" id="saveApiConfigButton" title="保存或更新当前填写的配置"><i class="fas fa-save"></i> 保存</button>
                                            <button type="button" class="btn btn-info mr-2 w-100" id="newApiConfigButton" title="清空表单以新建配置"><i class="fas fa-plus-circle"></i> 新建</button>
                                            <button type="button" class="btn btn-danger w-100" id="deleteApiConfigButton" title="删除下拉框中选中的配置"><i class="fas fa-trash-alt"></i> 删除</button>
                                        </div>
                                    </div>
                                    <hr>
                                    <p><strong>当前配置详情:</strong></p>
                                    <div class="form-row">
                                        <div class="form-group col-md-6">
                                            <label for="apiConfigName">配置名称</label>
                                            <input type="text" class="form-control" id="apiConfigName" placeholder="为此配置命名 (例如 My OpenAI)">
                                        </div>
                                        <div class="form-group col-md-6">
                                            <label for="apiKey">API Key</label>
                                            <input type="text" class="form-control" id="apiKey" placeholder="Enter API key">
                                        </div>
                                    </div>
                                    <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-row">
                                        <div class="form-group col-md-8">
                                            <label for="model">Model</label>
                                            <div class="input-group">
                                                <input type="text" class="form-control" id="model" placeholder="Enter model (e.g., gpt-4o-mini)" list="modelDatalist">
                                                <datalist id="modelDatalist"></datalist>
                                                <div class="input-group-append">
                                                    <button class="btn btn-outline-secondary" type="button" id="fetchModelsButton" title="Fetch Models from API">
                                                        <i class="fas fa-sync-alt"></i>
                                                    </button>
                                                </div>
                                            </div>
                                        </div>
                                        <div class="form-group col-md-4">
                                            <label for="temperature">Temperature</label>
                                            <input type="number" step="0.1" class="form-control" id="temperature" placeholder="e.g., 0.7">
                                        </div>
                                    </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. 可用变量: {{original}}, {{context}}, {{terms}}"></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>自动化选项</label>
                                        <div class="d-flex">
                                            <div class="custom-control custom-switch mr-3">
                                                <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 apiConfigSelect = document.getElementById('apiConfigSelect');
            const saveApiConfigButton = document.getElementById('saveApiConfigButton');
            const newApiConfigButton = document.getElementById('newApiConfigButton');
            const deleteApiConfigButton = document.getElementById('deleteApiConfigButton');
            const apiConfigNameInput = document.getElementById('apiConfigName');
            const baseUrlInput = document.getElementById('baseUrl');
            const apiKeyInput = document.getElementById('apiKey');
            const modelSelect = document.getElementById('model'); // This is now an input text field
            const fetchModelsButton = document.getElementById('fetchModelsButton');
            const promptInput = document.getElementById('prompt');
            const temperatureInput = document.getElementById('temperature');
            const autoTranslateToggle = document.getElementById('autoTranslateToggle');
            const autoPasteToggle = document.getElementById('autoPasteToggle');
            const promptLibrarySelect = document.getElementById('promptLibrarySelect');
            const saveToPromptLibraryButton = document.getElementById('saveToPromptLibraryButton');
            const deleteFromPromptLibraryButton = document.getElementById('deleteFromPromptLibraryButton');

            // API Config related functions are now defined in IIFE scope

            function updateActiveConfigField(fieldName, value) {
                const activeConfigName = getCurrentApiConfigName();
                if (activeConfigName) {
                    let configs = getApiConfigurations();
                    const activeConfigIndex = configs.findIndex(c => c.name === activeConfigName);
                    if (activeConfigIndex > -1) {
                        configs[activeConfigIndex][fieldName] = value;
                        saveApiConfigurations(configs);
                        // console.log(`Field '${fieldName}' for active config '${activeConfigName}' updated to '${value}' and saved.`);
                    }
                }
            }
            
            function updateFullUrlPreview(baseUrl) {
                const fullUrlPreview = document.getElementById('fullUrlPreview');
                if (baseUrl) {
                    const fullUrl = `${baseUrl}${baseUrl.endsWith('/') ? '' : '/'}chat/completions`;
                    fullUrlPreview.textContent = `完整URL: ${fullUrl}`;
                } else {
                    fullUrlPreview.textContent = '';
                }
            }

            function populateApiConfigSelect() {
                const configs = getApiConfigurations();
                const currentConfigName = getCurrentApiConfigName();
                apiConfigSelect.innerHTML = '<option value="">选择或新建配置...</option>'; // Changed placeholder
                configs.forEach(config => {
                    const option = document.createElement('option');
                    option.value = config.name;
                    option.textContent = config.name;
                    if (config.name === currentConfigName) {
                        option.selected = true;
                    }
                    apiConfigSelect.appendChild(option);
                });
            }

            function clearConfigForm() {
                apiConfigNameInput.value = '';
                baseUrlInput.value = '';
                apiKeyInput.value = '';
                // Optionally reset model, prompt, temp, toggles to defaults or leave them
                // modelSelect.value = 'gpt-4o-mini';
                // promptInput.value = '';
                // temperatureInput.value = '';
                // autoTranslateToggle.checked = false;
                // autoPasteToggle.checked = false;
                updateFullUrlPreview('');
                apiConfigSelect.value = ""; // Reset dropdown to placeholder
            }
            
            function loadConfigToUI(configName) {
                const configs = getApiConfigurations();
                const config = configs.find(c => c.name === configName);
                if (config) {
                    apiConfigNameInput.value = config.name;
                    baseUrlInput.value = config.baseUrl;
                    apiKeyInput.value = config.apiKey;
                    modelSelect.value = config.model || localStorage.getItem('model') || 'gpt-4o-mini';
                    promptInput.value = config.prompt || localStorage.getItem('prompt') || '';
                    temperatureInput.value = config.temperature || localStorage.getItem('temperature') || '';
                    autoTranslateToggle.checked = config.autoTranslateEnabled !== undefined ? config.autoTranslateEnabled : (localStorage.getItem('autoTranslateEnabled') === 'true');
                    autoPasteToggle.checked = config.autoPasteEnabled !== undefined ? config.autoPasteEnabled : (localStorage.getItem('autoPasteEnabled') === 'true');
                    setCurrentApiConfigName(config.name);
                    apiConfigSelect.value = config.name; // Ensure dropdown reflects loaded config
                } else {
                    clearConfigForm(); // Clear form if no specific config is loaded (e.g., "Select or create new")
                }
                updateFullUrlPreview(baseUrlInput.value);
            }
            
            // Initial load
            populateApiConfigSelect();
            const activeConfigName = getCurrentApiConfigName();
            if (activeConfigName) {
                loadConfigToUI(activeConfigName);
            } else { 
                // Try to migrate old settings if no new config is active
                const oldBaseUrl = localStorage.getItem('baseUrl'); // Check for old individual settings
                const oldApiKey = localStorage.getItem('apiKey');
                if (oldBaseUrl && oldApiKey && !getApiConfigurations().length) { // Migrate only if no new configs exist
                    const defaultConfigName = "默认迁移配置";
                    const newConfig = {
                        name: defaultConfigName,
                        baseUrl: oldBaseUrl,
                        apiKey: oldApiKey,
                        model: localStorage.getItem('model') || 'gpt-4o-mini',
                        prompt: localStorage.getItem('prompt') || '',
                        temperature: localStorage.getItem('temperature') || '',
                        autoTranslateEnabled: localStorage.getItem('autoTranslateEnabled') === 'true',
                        autoPasteEnabled: localStorage.getItem('autoPasteEnabled') === 'true'
                    };
                    let configs = getApiConfigurations();
                    configs.push(newConfig);
                    saveApiConfigurations(configs);
                    setCurrentApiConfigName(defaultConfigName);
                    populateApiConfigSelect();
                    loadConfigToUI(defaultConfigName);
                    // Optionally remove old keys after successful migration
                    // localStorage.removeItem('baseUrl'); localStorage.removeItem('apiKey');
                } else {
                    // If no active config and no old settings to migrate, or if configs already exist, load general settings.
                    modelSelect.value = localStorage.getItem('model') || 'gpt-4o-mini';
                    promptInput.value = localStorage.getItem('prompt') || '';
                    temperatureInput.value = localStorage.getItem('temperature') || '';
                    autoTranslateToggle.checked = localStorage.getItem('autoTranslateEnabled') === 'true';
                    autoPasteToggle.checked = localStorage.getItem('autoPasteEnabled') === 'true';
                    clearConfigForm(); // Start with a clean slate for API specific parts if no config selected
                }
            }

            apiConfigSelect.addEventListener('change', function() {
                if (this.value) {
                    loadConfigToUI(this.value);
                } else {
                    clearConfigForm();
                    // User selected "Select or create new...", so we clear the form for a new entry.
                    // Do not clear currentApiConfigName here, as they might just be viewing.
                }
            });
            
            newApiConfigButton.addEventListener('click', function() {
                clearConfigForm();
                apiConfigNameInput.focus();
            });

            saveApiConfigButton.addEventListener('click', function() {
                const name = apiConfigNameInput.value.trim();
                const baseUrl = baseUrlInput.value.trim();
                const apiKey = apiKeyInput.value.trim();

                if (!name || !baseUrl || !apiKey) {
                    showToast('配置名称、Base URL 和 API Key 不能为空。', 'error');
                    return;
                }

                let configs = getApiConfigurations();
                const existingConfigIndex = configs.findIndex(c => c.name === name);

                const currentConfigData = {
                    name,
                    baseUrl,
                    apiKey,
                    model: modelSelect.value,
                    prompt: promptInput.value,
                    temperature: temperatureInput.value,
                    autoTranslateEnabled: autoTranslateToggle.checked,
                    autoPasteEnabled: autoPasteToggle.checked
                };

                if (existingConfigIndex > -1) {
                    configs[existingConfigIndex] = currentConfigData; // Update existing
                } else {
                    configs.push(currentConfigData); // Add new
                }
                saveApiConfigurations(configs);
                setCurrentApiConfigName(name); // Set this as the active config
                populateApiConfigSelect(); // Refresh dropdown
                apiConfigSelect.value = name; // Ensure the saved/updated config is selected
                showToast(`API 配置 "${name}" 已保存!`, 'success');
            });

            deleteApiConfigButton.addEventListener('click', function() {
                const selectedNameToDelete = apiConfigSelect.value; // The config selected in dropdown
                if (!selectedNameToDelete) {
                    showToast('请先从下拉列表中选择一个要删除的配置。', 'error');
                    return;
                }
                if (!confirm(`确定要删除配置 "${selectedNameToDelete}" 吗?`)) {
                    return;
                }

                let configs = getApiConfigurations();
                configs = configs.filter(c => c.name !== selectedNameToDelete);
                saveApiConfigurations(configs);

                // If the deleted config was the currently active one, clear the form and active status
                if (getCurrentApiConfigName() === selectedNameToDelete) {
                    setCurrentApiConfigName(''); 
                    clearConfigForm();
                }
                populateApiConfigSelect(); // Refresh dropdown
                showToast(`API 配置 "${selectedNameToDelete}" 已删除!`, 'success');
                 // If there are other configs, load the first one or leave blank
                if (getApiConfigurations().length > 0) {
                    const firstConfigName = getApiConfigurations()[0].name;
                    loadConfigToUI(firstConfigName);
                    apiConfigSelect.value = firstConfigName;
                } else {
                    clearConfigForm(); // No configs left, clear form
                }
            });
            
            // Event listeners for general (non-API-config specific) fields
            // Event listeners for general (non-API-config specific) fields
            // These save to general localStorage and also update the active API config if one is selected.
            baseUrlInput.addEventListener('input', () => {
                updateFullUrlPreview(baseUrlInput.value);
                // Base URL and API Key are core to a config, usually not changed outside explicit save.
            });
            // apiKeyInput does not have a live update to avoid frequent writes of sensitive data.

            document.getElementById('openaiButton').addEventListener('click', () => {
                baseUrlInput.value = 'https://api.openai.com/v1';
                updateFullUrlPreview(baseUrlInput.value);
            });
            document.getElementById('deepseekButton').addEventListener('click', () => {
                baseUrlInput.value = 'https://api.deepseek.com';
                updateFullUrlPreview(baseUrlInput.value);
            });
            
            fetchModelsButton.addEventListener('click', async () => {
                await this.fetchModelsAndUpdateDatalist();
            });

            modelSelect.addEventListener('input', () => { // modelSelect is the input field
                localStorage.setItem('model', modelSelect.value);
                updateActiveConfigField('model', modelSelect.value);
            });
            promptInput.addEventListener('input', () => {
                localStorage.setItem('prompt', promptInput.value);
                updateActiveConfigField('prompt', promptInput.value);
            });
            temperatureInput.addEventListener('input', () => {
                const tempValue = temperatureInput.value;
                localStorage.setItem('temperature', tempValue);
                updateActiveConfigField('temperature', tempValue);
            });
            autoTranslateToggle.addEventListener('change', () => {
                localStorage.setItem('autoTranslateEnabled', autoTranslateToggle.checked);
                updateActiveConfigField('autoTranslateEnabled', autoTranslateToggle.checked);
            });
            autoPasteToggle.addEventListener('change', () => {
                localStorage.setItem('autoPasteEnabled', autoPasteToggle.checked);
                updateActiveConfigField('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); // Keep for fallback if no config selected
                    updateActiveConfigField('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;
                        showToast('Prompt 已保存到库中。', 'success');
                    } else {
                        showToast('此 Prompt 已存在于库中。', 'warning');
                    }
                } else {
                    showToast('Prompt 内容不能为空。', 'error');
                }
            });

            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', '');
                        }
                        showToast('选定的 Prompt 已从库中删除。', 'success');
                    }
                } else {
                    showToast('请先从库中选择一个 Prompt 进行删除。', 'error');
                }
            });

            populatePromptLibrarySelect();

            // Sync promptLibrarySelect with the initial promptInput value
            const initialPromptValue = promptInput.value;
            if (initialPromptValue) {
                const library = getPromptLibrary();
                if (library.includes(initialPromptValue)) {
                    promptLibrarySelect.value = initialPromptValue;
                } else {
                    promptLibrarySelect.value = ""; // If not in library, keep placeholder
                }
            } else {
                 promptLibrarySelect.value = ""; // Default to placeholder if no initial prompt
            }
            // Removed duplicated listeners for temperature and autoTranslateToggle here,
            // as they are already defined above with updateActiveConfigField logic.

            this.setupTranslation();
        }

        async fetchModelsAndUpdateDatalist() {
            const modelDatalist = document.getElementById('modelDatalist');
            const fetchModelsButton = document.getElementById('fetchModelsButton');
            const originalButtonHtml = fetchModelsButton.innerHTML;
            fetchModelsButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
            fetchModelsButton.disabled = true;

            let API_SECRET_KEY = '';
            let BASE_URL = '';

            const currentConfigName = getCurrentApiConfigName();
            let activeConfig = null;

            if (currentConfigName) {
                const configs = getApiConfigurations();
                activeConfig = configs.find(c => c.name === currentConfigName);
            }

            if (activeConfig) {
                BASE_URL = activeConfig.baseUrl;
                API_SECRET_KEY = activeConfig.apiKey;
            } else {
                // Fallback to general localStorage if no active config (less ideal)
                BASE_URL = localStorage.getItem('baseUrl');
                API_SECRET_KEY = localStorage.getItem('apiKey');
            }

            if (!BASE_URL || !API_SECRET_KEY) {
                showToast('请先配置并选择一个有效的 API 配置 (包含 Base URL 和 API Key)。', 'error', 5000);
                fetchModelsButton.innerHTML = originalButtonHtml;
                fetchModelsButton.disabled = false;
                return;
            }

            // Construct the models API URL (OpenAI standard is /models)
            const modelsUrl = `${BASE_URL}${BASE_URL.endsWith('/') ? '' : '/'}models`;

            try {
                const response = await fetch(modelsUrl, {
                    method: 'GET',
                    headers: {
                        'Authorization': `Bearer ${API_SECRET_KEY}`
                    }
                });

                if (!response.ok) {
                    const errorData = await response.text();
                    console.error('Error fetching models:', response.status, errorData);
                    showToast(`获取模型列表失败: ${response.status} - ${errorData.substring(0,100)}`, 'error', 5000);
                    return;
                }

                const data = await response.json();
                if (data && data.data && Array.isArray(data.data)) {
                    modelDatalist.innerHTML = ''; // Clear existing options
                    data.data.forEach(model => {
                        if (model.id) {
                            const option = document.createElement('option');
                            option.value = model.id;
                            modelDatalist.appendChild(option);
                        }
                    });
                    showToast('模型列表已更新。', 'success');
                } else {
                    console.warn('Unexpected models API response structure:', data);
                    showToast('获取模型列表成功,但响应数据格式不符合预期。', 'warning', 4000);
                }
            } catch (error) {
                console.error('Failed to fetch models:', error);
                showToast(`获取模型列表时发生网络错误: ${error.message}`, 'error', 5000);
            } finally {
                fetchModelsButton.innerHTML = originalButtonHtml;
                fetchModelsButton.disabled = false;
            }
        }

        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: "user",
            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;
        }
    }
    // Define API config utility functions in IIFE scope
    const API_CONFIGURATIONS_KEY = 'apiConfigurations';
    const CURRENT_API_CONFIG_NAME_KEY = 'currentApiConfigName';

    function getApiConfigurations() {
        return JSON.parse(localStorage.getItem(API_CONFIGURATIONS_KEY)) || [];
    }

    function saveApiConfigurations(configs) {
        localStorage.setItem(API_CONFIGURATIONS_KEY, JSON.stringify(configs));
    }

    function getCurrentApiConfigName() {
        return localStorage.getItem(CURRENT_API_CONFIG_NAME_KEY);
    }

    function setCurrentApiConfigName(name) {
        localStorage.setItem(CURRENT_API_CONFIG_NAME_KEY, name);
    }

    async function translateText(query, model, prompt, temperature) {
        let API_SECRET_KEY = '';
        let BASE_URL = '';
        
        const currentConfigName = getCurrentApiConfigName();
        let activeConfig = null;

        if (currentConfigName) {
            const configs = getApiConfigurations();
            activeConfig = configs.find(c => c.name === currentConfigName);
        }

        if (activeConfig) {
            BASE_URL = activeConfig.baseUrl;
            API_SECRET_KEY = activeConfig.apiKey;
            model = activeConfig.model || localStorage.getItem('model') || 'gpt-4o-mini'; // Fallback to general localStorage then default
            prompt = activeConfig.prompt || localStorage.getItem('prompt') || ''; 
            temperature = activeConfig.temperature !== undefined && activeConfig.temperature !== '' ? parseFloat(activeConfig.temperature) : (localStorage.getItem('temperature') !== null ? parseFloat(localStorage.getItem('temperature')) : 0);
        } else {
            // If no active config, try to use general localStorage settings as a last resort for key/URL
            // This case should ideally be handled by prompting user to select/create a config
            console.warn("No active API configuration selected. Translation might fail or use stale settings.");
            BASE_URL = localStorage.getItem('baseUrl_fallback_for_translate') || ''; // Example of a dedicated fallback key
            API_SECRET_KEY = localStorage.getItem('apiKey_fallback_for_translate') || '';
            // For other params, use general localStorage or defaults
            model = localStorage.getItem('model') || 'gpt-4o-mini';
            prompt = localStorage.getItem('prompt') || '';
            temperature = localStorage.getItem('temperature') !== null ? parseFloat(localStorage.getItem('temperature')) : 0;
        }

        if (!BASE_URL || !API_SECRET_KEY) {
            console.error("API Base URL or Key is missing. Please configure an API setting.");
            return "API Base URL 或 Key 未配置。请在翻译配置中设置。";
        }
        
        if (!prompt) { // Default prompt if still empty after all fallbacks
            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.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">&times;</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 d-flex justify-content-between align-items-center">
                                            <span>当前翻译</span>
                                            <button class="btn btn-sm btn-primary" id="editTranslationButton">编辑</button>
                                        </div>
                                        <div class="diff-content" id="translationDiffContent" style="display: block;"></div>
                                        <textarea class="form-control" id="translationEditor" style="display: none; height: 100%; width: 100%; border: none; resize: none; font-family: monospace;" placeholder="在此编辑翻译内容..."></textarea>
                                    </div>
                                </div>
                            </div>
                            <div class="modal-footer">
                                <button type="button" class="btn btn-secondary" id="closeDiffModalButton">关闭</button>
                                <button type="button" class="btn btn-primary" id="saveTranslationButton" style="display: none;">保存</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; /* Light green for whole line add */
                }
                .diff-line.diff-removed {
                    background-color: #ffeef0; /* Light red for whole line remove */
                }
                .diff-line.diff-common {
                    background-color: #ffffff;
                }
                .diff-line.diff-placeholder,
                .diff-line.diff-modified-old, /* Placeholder for original side of a modification */
                .diff-line.diff-added-extra { /* Placeholder for translation side of a modification where original has fewer lines */
                    background-color: #f0f0f0; /* Grey for placeholders */
                }
                .copy-action-button { /* Unified class for action buttons */
                    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-action-button:hover {
                    background-color: #e0e0e0;
                }
                .diff-header {
                    font-weight: bold;
                    position: sticky;
                    top: 0;
                    z-index: 1;
                    background-color: #f8f9fa; /* Ensure header bg covers scrolling content */
                }
                /* Intra-line diff styles */
                .diff-intraline-added {
                    background-color: #acf2bd; /* More prominent green for intra-line additions */
                    /* text-decoration: underline; */
                }
                .diff-intraline-removed {
                    background-color: #fdb8c0; /* More prominent red for intra-line deletions */
                    text-decoration: line-through;
                }
            `;
            document.head.appendChild(style);

            document.getElementById('closeDiffModal').addEventListener('click', this.closeModal.bind(this));
            document.getElementById('closeDiffModalButton').addEventListener('click', this.closeModal.bind(this));
            document.getElementById('editTranslationButton').addEventListener('click', this.toggleEditMode.bind(this));
            document.getElementById('saveTranslationButton').addEventListener('click', this.saveTranslation.bind(this));
        }

        toggleEditMode() {
            const translationContent = document.getElementById('translationDiffContent');
            const translationEditor = document.getElementById('translationEditor');
            const editButton = document.getElementById('editTranslationButton');
            const saveButton = document.getElementById('saveTranslationButton');

            if (translationContent.style.display === 'block') {
                translationContent.style.display = 'none';
                translationEditor.style.display = 'block';
                editButton.textContent = '取消编辑';
                saveButton.style.display = 'inline-block';
                translationEditor.value = document.querySelector('textarea.translation.form-control')?.value || '';
                translationEditor.focus();
            } else {
                translationContent.style.display = 'block';
                translationEditor.style.display = 'none';
                editButton.textContent = '编辑';
                saveButton.style.display = 'none';
            }
        }

        saveTranslation() {
            const translationEditor = document.getElementById('translationEditor');
            const textarea = document.querySelector('textarea.translation.form-control');
            if (textarea) {
                textarea.value = translationEditor.value;
                simulateInputChange(textarea, textarea.value); // Ensure change is registered by React/Vue if applicable
                this.toggleEditMode(); // Switch back to diff view
                this.generateDiff(); // Regenerate diff with new translation
            }
        }

        show() {
            const modal = document.getElementById(this.modalId);
            modal.style.display = 'block';
            this.generateDiff();
        }

        closeModal() {
            document.getElementById(this.modalId).style.display = 'none';
        }

        // Helper to split lines, handling trailing newline consistently and removing CR
        splitIntoLines(text) {
            if (text === null || text === undefined) return [];
            if (text === '') return ['']; // An empty text is one empty line for diffing purposes
            let lines = text.split('\n');
            // If the text ends with a newline, split will produce an empty string at the end.
            // jsdiff's diffLines handles this by considering the newline as part of the last line's value or as a separate token.
            // For our rendering, we want to represent each line distinctly.
            // If text is "a\nb\n", split gives ["a", "b", ""]. We want ["a", "b"].
            // If text is "a\nb", split gives ["a", "b"]. We want ["a", "b"].
            // If text is "\n", split gives ["", ""]. We want [""] for one empty line.
            if (text.endsWith('\n') && lines.length > 0) {
                 lines.pop(); // Remove the empty string caused by a trailing newline
            }
            return lines.map(l => l.replace(/\r$/, '')); // Remove CR if present for consistency
        }


        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 lineDiffResult = this.diffLib.diffLines(originalText, translationText, { newlineIsToken: false, ignoreWhitespace: false });

            let origDisplayLineNum = 1;
            let transDisplayLineNum = 1;
            let currentTranslationLineIndexForAction = 0;

            for (let i = 0; i < lineDiffResult.length; i++) {
                const part = lineDiffResult[i];
                const nextPart = (i + 1 < lineDiffResult.length) ? lineDiffResult[i + 1] : null;

                let linesInPart = this.splitIntoLines(part.value);

                if (part.removed) {
                    if (nextPart && nextPart.added) { // This is a modification block
                        let linesInNextPart = this.splitIntoLines(nextPart.value);
                        const maxLines = Math.max(linesInPart.length, linesInNextPart.length);

                        for (let j = 0; j < maxLines; j++) {
                            const removedLine = j < linesInPart.length ? linesInPart[j] : null;
                            const addedLine = j < linesInNextPart.length ? linesInNextPart[j] : null;

                            if (removedLine !== null) {
                                this.appendLine(originalContent, origDisplayLineNum++, removedLine, 'diff-removed', removedLine, currentTranslationLineIndexForAction, true, 'original', addedLine, 'replace'); // Action: replace for modified lines
                            } else {
                                this.appendLine(originalContent, '-', '', 'diff-placeholder diff-added-extra', null, null, false, 'original', null);
                            }

                            if (addedLine !== null) {
                                this.appendLine(translationContent, transDisplayLineNum++, addedLine, 'diff-added', addedLine, currentTranslationLineIndexForAction, true, 'translation', removedLine);
                            } else {
                                this.appendLine(translationContent, '-', '', 'diff-placeholder diff-modified-old', null, null, false, 'translation', null);
                            }
                            currentTranslationLineIndexForAction++;
                        }
                        i++; // Skip nextPart as it's processed
                    } else { // Pure removal
                        linesInPart.forEach(lineText => {
                            this.appendLine(originalContent, origDisplayLineNum++, lineText, 'diff-removed', lineText, currentTranslationLineIndexForAction, true, 'original', '', 'insert'); // Action: insert for removed lines
                            this.appendLine(translationContent, '-', '', 'diff-placeholder diff-removed', null, null, false, 'translation', null);
                            // currentTranslationLineIndexForAction does not advance for placeholders on translation side if original is removed
                        });
                    }
                } else if (part.added) { // Pure addition (modification handled above)
                    linesInPart.forEach(lineText => {
                        this.appendLine(originalContent, '-', '', 'diff-placeholder diff-added', null, null, false, 'original', null, 'insert'); // Or 'replace' if that makes more sense for placeholder context
                        this.appendLine(translationContent, transDisplayLineNum++, lineText, 'diff-added', lineText, currentTranslationLineIndexForAction, true, 'translation', '');
                        currentTranslationLineIndexForAction++;
                    });
                } else { // Common part
                    linesInPart.forEach(lineText => {
                        this.appendLine(originalContent, origDisplayLineNum++, lineText, 'diff-common', lineText, currentTranslationLineIndexForAction, true, 'original', lineText, 'replace'); // Action: replace for common lines
                        this.appendLine(translationContent, transDisplayLineNum++, lineText, 'diff-common', lineText, currentTranslationLineIndexForAction, true, 'translation', lineText, 'replace');
                        currentTranslationLineIndexForAction++;
                    });
                }
            }
        }

        appendLine(container, lineNumber, text, diffClass, lineTextForAction = null, translationLineIndexForAction = null, showActionButton = false, side = 'original', otherTextForIntralineDiff = null, actionType = 'replace') { // Added actionType, default to '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 === null || (text === '' && diffClass.includes('placeholder'))) {
                contentSpan.innerHTML = '&nbsp;';
            } else if (this.diffLib && otherTextForIntralineDiff !== null && (diffClass.includes('diff-removed') || diffClass.includes('diff-added') || diffClass.includes('diff-common'))) {
                let oldContentForWordDiff, newContentForWordDiff;

                if (diffClass.includes('diff-removed')) { // Displaying on original side, text is old
                    oldContentForWordDiff = text;
                    newContentForWordDiff = otherTextForIntralineDiff || '';
                } else if (diffClass.includes('diff-added')) { // Displaying on translation side, text is new
                    oldContentForWordDiff = otherTextForIntralineDiff || '';
                    newContentForWordDiff = text;
                } else { // Common line
                    oldContentForWordDiff = text;
                    newContentForWordDiff = text; // or otherTextForIntralineDiff, they are the same
                }

                const wordDiff = this.diffLib.diffWordsWithSpace(oldContentForWordDiff, newContentForWordDiff);
                wordDiff.forEach(part => {
                    const span = document.createElement('span');
                    if (part.added) {
                        // Style as added if we are on the side that displays the "new" content of the pair
                        if (diffClass.includes('diff-added') || (diffClass.includes('diff-removed') && side === 'original')) {
                             span.className = 'diff-intraline-added';
                        }
                    } else if (part.removed) {
                        // Style as removed if we are on the side that displays the "old" content of the pair
                         if (diffClass.includes('diff-removed') || (diffClass.includes('diff-added') && side === 'translation')) {
                            span.className = 'diff-intraline-removed';
                        }
                    }
                    span.textContent = part.value;
                    contentSpan.appendChild(span);
                });

            } else {
                contentSpan.textContent = text;
            }
            lineDiv.appendChild(contentSpan);

            if (showActionButton && lineTextForAction !== null && translationLineIndexForAction !== null && !diffClass.includes('placeholder')) {
                const actionButton = document.createElement('button');
                actionButton.className = `btn btn-link p-0 ml-2 copy-action-button`;
                let buttonTitle = '';
                let buttonIconClass = '';

                if (side === 'original') {
                    buttonIconClass = 'fas fa-arrow-right';
                    if (actionType === 'replace') {
                        buttonTitle = '使用此原文行覆盖译文对应行';
                    } else { // actionType === 'insert'
                        buttonTitle = '将此原文行插入到译文对应位置';
                    }
                }
                // Add logic for buttons on translation side if needed later

                if (buttonIconClass && !diffClass.includes('diff-common')) { // <--- 修改点在这里
                    actionButton.innerHTML = `<i class="${buttonIconClass}"></i>`;
                    actionButton.title = buttonTitle;
                    actionButton.addEventListener('click', () => {
                        const textarea = document.querySelector('textarea.translation.form-control');
                        if (!textarea) return;
                        let lines = textarea.value.split('\n');
                        const targetIndex = Math.max(0, translationLineIndexForAction);

                        while (lines.length <= targetIndex) {
                            lines.push('');
                        }
                        if (actionType === 'replace') {
                            // 确保目标索引在数组范围内,如果超出则扩展数组
                            while (lines.length <= targetIndex) {
                                lines.push('');
                            }
                            lines[targetIndex] = lineTextForAction;
                        } else { // actionType === 'insert'
                            const effectiveTargetIndex = Math.min(lines.length, targetIndex);
                            lines.splice(effectiveTargetIndex, 0, lineTextForAction);
                        }

                        textarea.value = lines.join('\n');
                        simulateInputChange(textarea, textarea.value);
                        requestAnimationFrame(() => this.generateDiff());
                    });
                    lineDiv.appendChild(actionButton);
                }
            }
            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);
            });
        }
    );

    // AI 对话框类
    class AIChatDialog {
        constructor() {
            this.fabId = 'ai-chat-fab';
            this.dialogId = 'ai-chat-dialog';
            this.messagesContainerId = 'ai-chat-messages';
            this.inputAreaId = 'ai-chat-input';
            this.sendButtonId = 'ai-chat-send';
            this.closeButtonId = 'ai-chat-close';
            this.clearHistoryButtonId = 'ai-chat-clear-history'; // New ID for clear button
            this.isDragging = false;
            this.dragStartX = 0;
            this.dragStartY = 0;
            this.dialogX = 0;
            this.dialogY = 0;
            this.sendContextToggleId = 'ai-chat-send-context-toggle';
            this.localStorageKeySendContext = 'aiChatSendContextEnabled';
            this.aiChatModelInputId = 'aiChatModelInput';
            this.aiChatModelDatalistId = 'aiChatModelDatalist';
            this.fetchAiChatModelsButtonId = 'fetchAiChatModelsButton';
            this.localStorageKeyAiChatModel = 'aiChatModelName'; // New key for AI chat model
            this.init();
        }

        init() {
            this.addStyles();
            this.insertFab();
            // Dialog is inserted only when FAB is clicked for the first time
        }

        addStyles() {
            const css = `
                #${this.fabId} {
                    position: fixed;
                    bottom: 20px;
                    right: 20px;
                    width: 50px;
                    height: 50px;
                    background-color: #007bff;
                    color: white;
                    border-radius: 50%;
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    font-size: 24px;
                    cursor: pointer;
                    box-shadow: 0 2px 10px rgba(0,0,0,0.2);
                    z-index: 9998; /* Below dialog */
                    transition: background-color 0.3s ease;
                }
                #${this.fabId}:hover {
                    background-color: #0056b3;
                }
                #${this.dialogId} {
                    position: fixed;
                    bottom: 80px; /* Position above FAB */
                    right: 20px;
                    width: 380px; /* Increased width */
                    height: 450px;
                    background-color: white;
                    border: 1px solid #ccc;
                    border-radius: 8px;
                    box-shadow: 0 5px 15px rgba(0,0,0,0.3);
                    display: none; /* Hidden by default */
                    flex-direction: column;
                    z-index: 9999;
                    overflow: hidden; /* Prevent content spill */
                }
                #${this.dialogId} .ai-chat-header {
                    padding: 10px 15px;
                    background-color: #f8f9fa;
                    border-bottom: 1px solid #dee2e6;
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    cursor: move; /* Make header draggable */
                }
                #${this.dialogId} .ai-chat-header h5 {
                    margin: 0;
                    font-size: 1rem;
                    flex-grow: 1; /* Allow title to take space */
                }
                #${this.dialogId} .ai-chat-header .header-buttons {
                    display: flex;
                    align-items: center;
                }
                #${this.dialogId} .ai-chat-header .btn-icon { /* Style for icon buttons */
                    background: none;
                    border: none;
                    font-size: 1.2rem; /* Adjust icon size */
                    opacity: 0.6;
                    cursor: pointer;
                    padding: 5px;
                    margin-left: 8px;
                }
                #${this.dialogId} .ai-chat-header .btn-icon:hover {
                    opacity: 1;
                }
                #${this.messagesContainerId} {
                    flex-grow: 1;
                    overflow-y: auto;
                    padding: 15px;
                    background-color: #f0f0f0; /* Light grey background for messages */
                }
                #${this.messagesContainerId} .message {
                    margin-bottom: 10px;
                    padding: 8px 12px;
                    border-radius: 15px;
                    max-width: 80%;
                    word-wrap: break-word;
                }
                #${this.messagesContainerId} .message.user {
                    background-color: #007bff;
                    color: white;
                    margin-left: auto;
                    border-bottom-right-radius: 5px;
                }
                #${this.messagesContainerId} .message.ai {
                    background-color: #e9ecef;
                    color: #333;
                    margin-right: auto;
                    border-bottom-left-radius: 5px;
                }
                 #${this.messagesContainerId} .message.error {
                    background-color: #f8d7da;
                    color: #721c24;
                    margin-right: auto;
                    border-bottom-left-radius: 5px;
                    font-style: italic;
                }
                #${this.dialogId} .ai-chat-input-area {
                    display: flex;
                    align-items: flex-start; /* Align items to the start for multi-line textarea */
                    padding: 10px;
                    border-top: 1px solid #dee2e6;
                    background-color: #f8f9fa;
                }
                #${this.inputAreaId} {
                    flex-grow: 1;
                    margin-right: 8px; /* Reduced margin */
                    resize: none; /* Prevent manual resize */
                    min-height: 40px; /* Ensure it's at least one line */
                    max-height: 120px; /* Limit max height for textarea */
                    overflow-y: auto; /* Allow scroll if content exceeds max-height */
                    line-height: 1.5; /* Adjust line height for better readability */
                }
                #${this.sendButtonId} {
                    height: 40px; /* Keep button height consistent */
                    min-width: 65px; /* Ensure button has enough space for "发送" */
                    padding-left: 12px; 
                    padding-right: 12px;
                    align-self: flex-end; /* Align button to bottom if textarea grows */
                }
                .ai-chat-options {
                    padding: 5px 10px;
                    background-color: #f8f9fa;
                    border-bottom: 1px solid #dee2e6;
                    font-size: 0.85rem;
                }
                .ai-chat-options .custom-control-label {
                    font-weight: normal;
                }
            `;
            GM_addStyle(css);
        }

        insertFab() {
            if (document.getElementById(this.fabId)) return;
            const fab = document.createElement('div');
            fab.id = this.fabId;
            fab.innerHTML = '<i class="fas fa-robot"></i>'; // Example icon
            fab.title = 'AI 助手';
            fab.addEventListener('click', () => this.toggleDialog());
            document.body.appendChild(fab);
        }

        insertDialog() {
            if (document.getElementById(this.dialogId)) return;

            const dialog = document.createElement('div');
            dialog.id = this.dialogId;
            dialog.innerHTML = `
                <div class="ai-chat-header">
                    <h5>AI 助手</h5>
                    <div class="header-buttons">
                        <button type="button" class="btn-icon" id="${this.clearHistoryButtonId}" title="清空聊天记录">
                            <i class="fas fa-trash-alt"></i>
                        </button>
                        <button type="button" class="btn-icon close" id="${this.closeButtonId}" aria-label="Close" title="关闭对话框">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
                </div>
                <div id="${this.messagesContainerId}">
                    <div class="message ai">你好!有什么可以帮你的吗?</div>
                </div>
                <div class="ai-chat-options">
                    <div class="custom-control custom-switch custom-control-sm">
                        <input type="checkbox" class="custom-control-input" id="${this.sendContextToggleId}">
                        <label class="custom-control-label" for="${this.sendContextToggleId}">发送页面上下文给AI</label>
                    </div>
                </div>
                <div class="ai-chat-options" style="border-top: 1px solid #dee2e6; padding-top: 8px; margin-top: 5px;"> <!-- Model selection for AI Chat -->
                    <div class="form-group mb-1">
                        <label for="${this.aiChatModelInputId}" style="font-size: 0.85rem; margin-bottom: .2rem;">AI 模型:</label>
                        <div class="input-group input-group-sm">
                            <input type="text" class="form-control form-control-sm" id="${this.aiChatModelInputId}" placeholder="默认 (gpt-4o-mini)" list="${this.aiChatModelDatalistId}">
                            <datalist id="${this.aiChatModelDatalistId}"></datalist>
                            <div class="input-group-append">
                                <button class="btn btn-outline-secondary btn-sm" type="button" id="${this.fetchAiChatModelsButtonId}" title="获取模型列表">
                                    <i class="fas fa-sync-alt"></i>
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="ai-chat-input-area">
                    <textarea id="${this.inputAreaId}" class="form-control" placeholder="输入消息..."></textarea>
                    <button id="${this.sendButtonId}" class="btn btn-primary">发送</button>
                </div>
            `;
            document.body.appendChild(dialog);

            // Add event listeners
            document.getElementById(this.closeButtonId).addEventListener('click', () => this.toggleDialog(false));
            document.getElementById(this.clearHistoryButtonId).addEventListener('click', () => this.clearChatHistory());
            document.getElementById(this.sendButtonId).addEventListener('click', () => this.sendMessage());
            
            const sendContextToggle = document.getElementById(this.sendContextToggleId);
            const aiChatModelInput = document.getElementById(this.aiChatModelInputId);
            const fetchAiChatModelsButton = document.getElementById(this.fetchAiChatModelsButtonId);

            // Load saved preference for sending context
            const savedSendContextPreference = localStorage.getItem(this.localStorageKeySendContext);
            if (savedSendContextPreference === 'true') {
                sendContextToggle.checked = true;
            } else if (savedSendContextPreference === 'false') {
                sendContextToggle.checked = false;
            } else {
                sendContextToggle.checked = true; // Default to true if not set
                localStorage.setItem(this.localStorageKeySendContext, 'true');
            }

            sendContextToggle.addEventListener('change', (e) => {
                localStorage.setItem(this.localStorageKeySendContext, e.target.checked);
            });

            // AI Chat Model preferences
            let initialAiChatModel = localStorage.getItem(this.localStorageKeyAiChatModel);
            if (!initialAiChatModel) {
                // If no specific AI chat model is saved, try to use the model from the current translation config
                const currentTranslationConfigName = getCurrentApiConfigName();
                if (currentTranslationConfigName) {
                    const configs = getApiConfigurations();
                    const activeTranslationConfig = configs.find(c => c.name === currentTranslationConfigName);
                    if (activeTranslationConfig && activeTranslationConfig.model) {
                        initialAiChatModel = activeTranslationConfig.model;
                        // Save this inherited model as the current AI chat model
                        localStorage.setItem(this.localStorageKeyAiChatModel, initialAiChatModel);
                    }
                }
            }
            aiChatModelInput.value = initialAiChatModel || ''; // Fallback to empty if no model found

            aiChatModelInput.addEventListener('input', () => {
                localStorage.setItem(this.localStorageKeyAiChatModel, aiChatModelInput.value);
            });
            fetchAiChatModelsButton.addEventListener('click', async () => {
                await this.fetchModelsAndUpdateDatalistForChat();
            });

            document.getElementById(this.inputAreaId).addEventListener('keypress', (e) => {
                if (e.key === 'Enter' && !e.shiftKey) {
                    e.preventDefault(); // Prevent newline
                    this.sendMessage();
                }
            });
             // Auto-resize textarea
            const textarea = document.getElementById(this.inputAreaId);
            textarea.addEventListener('input', () => {
                // Auto-resize textarea based on content, up to max-height
                textarea.style.height = 'auto'; // Reset height to shrink if text is deleted
                let scrollHeight = textarea.scrollHeight;
                const maxHeight = parseInt(window.getComputedStyle(textarea).maxHeight, 10);
                if (maxHeight && scrollHeight > maxHeight) {
                    textarea.style.height = maxHeight + 'px';
                    textarea.style.overflowY = 'auto';
                } else {
                    textarea.style.height = scrollHeight + 'px';
                    textarea.style.overflowY = 'hidden';
                }
            });

            // Make dialog draggable
            const header = dialog.querySelector('.ai-chat-header');
            header.addEventListener('mousedown', (e) => {
                this.isDragging = true;
                this.dragStartX = e.clientX - dialog.offsetLeft;
                this.dragStartY = e.clientY - dialog.offsetTop;
                header.style.cursor = 'grabbing'; // Change cursor while dragging
                // Prevent text selection during drag
                document.body.style.userSelect = 'none';
            });

            document.addEventListener('mousemove', (e) => {
                if (!this.isDragging) return;
                const newX = e.clientX - this.dragStartX;
                const newY = e.clientY - this.dragStartY;

                // Keep dialog within viewport boundaries (optional)
                const maxX = window.innerWidth - dialog.offsetWidth;
                const maxY = window.innerHeight - dialog.offsetHeight;

                dialog.style.left = Math.max(0, Math.min(newX, maxX)) + 'px';
                dialog.style.top = Math.max(0, Math.min(newY, maxY)) + 'px';
                // Update position relative to bottom/right if needed, but left/top is simpler for dragging
                dialog.style.bottom = 'auto';
                dialog.style.right = 'auto';
            });

            document.addEventListener('mouseup', () => {
                if (this.isDragging) {
                    this.isDragging = false;
                    header.style.cursor = 'move';
                    document.body.style.userSelect = ''; // Restore text selection
                }
            });
        }

        toggleDialog(forceShow = null) {
            if (!document.getElementById(this.dialogId)) {
                this.insertDialog(); // Create dialog on first open
            }
            const dialog = document.getElementById(this.dialogId);
            const shouldShow = forceShow !== null ? forceShow : dialog.style.display === 'none';

            if (shouldShow) {
                dialog.style.display = 'flex';
                // Focus input when opened
                setTimeout(() => document.getElementById(this.inputAreaId)?.focus(), 0);
            } else {
                dialog.style.display = 'none';
            }
        }

        displayMessage(text, sender = 'ai', isError = false) {
            const messagesContainer = document.getElementById(this.messagesContainerId);
            if (!messagesContainer) return;

            const messageDiv = document.createElement('div');
            messageDiv.classList.add('message', sender);
            if (isError) {
                messageDiv.classList.add('error');
            }
            
            if (sender === 'ai' && !isError) {
                messageDiv.innerHTML = text.replace(/\n/g, '<br>'); // Initial text or full text if not streaming
            } else {
                messageDiv.textContent = text;
            }
            
            messagesContainer.appendChild(messageDiv);
            messagesContainer.scrollTop = messagesContainer.scrollHeight;
            return messageDiv; // Return the created message element for potential stream updates
        }

        updateAIMessage(messageElement, chunk) {
            if (!messageElement) return;
            // Append new chunk, converting newlines.
            // For proper Markdown streaming, this would need to be more sophisticated,
            // potentially re-rendering the whole Markdown on each chunk or using a lib that supports streaming.
            messageElement.innerHTML += chunk.replace(/\n/g, '<br>');
            const messagesContainer = document.getElementById(this.messagesContainerId);
            if (messagesContainer) {
                messagesContainer.scrollTop = messagesContainer.scrollHeight;
            }
        }
        
        clearChatHistory() {
            const messagesContainer = document.getElementById(this.messagesContainerId);
            if (messagesContainer) {
                messagesContainer.innerHTML = ''; // Clear all messages
                this.displayMessage('你好!有什么可以帮你的吗?', 'ai'); // Display initial greeting
            }
        }

        async sendMessage() {
            const inputArea = document.getElementById(this.inputAreaId);
            const sendButton = document.getElementById(this.sendButtonId);
            const messageText = inputArea.value.trim();

            if (!messageText) return;

            this.displayMessage(messageText, 'user');
            inputArea.value = '';
            // Reset textarea height after sending
            inputArea.style.height = 'auto'; 
            inputArea.style.height = (inputArea.scrollHeight < 40 ? 40 : inputArea.scrollHeight) + 'px';
            if (parseInt(inputArea.style.height) > parseInt(window.getComputedStyle(inputArea).maxHeight)) {
                 inputArea.style.height = window.getComputedStyle(inputArea).maxHeight;
                 inputArea.style.overflowY = 'auto';
            } else {
                 inputArea.style.overflowY = 'hidden';
            }
            inputArea.disabled = true;
            sendButton.disabled = true;
            
            // Display "Thinking..." and get the message element
            let aiMessageElement = this.displayMessage('思考中...', 'ai'); 
            const messagesContainerElement = document.getElementById(this.messagesContainerId);

            try {
                // Call chatWithAI, now potentially streaming
                await this.chatWithAI(messageText, (chunk) => {
                    if (aiMessageElement && aiMessageElement.textContent === '思考中...') {
                        // Replace "Thinking..." with the first chunk
                        aiMessageElement.innerHTML = chunk.replace(/\n/g, '<br>');
                    } else if (aiMessageElement) {
                        // Append subsequent chunks
                        this.updateAIMessage(aiMessageElement, chunk);
                    }
                });

                // If the "Thinking..." message is still there (e.g. stream was empty or very fast non-streamed error)
                // This case should ideally be handled by the streaming logic itself replacing "Thinking..."
                // For non-streaming success, chatWithAI would have to call the onChunk callback once.
                // If chatWithAI throws an error before any chunk, the catch block handles it.

            } catch (error) {
                if (aiMessageElement && messagesContainerElement) { // Ensure element exists
                    // If "Thinking..." is still shown, replace it with error. Otherwise, display a new error message.
                    if (aiMessageElement.textContent === '思考中...') {
                         aiMessageElement.classList.add('error');
                         aiMessageElement.innerHTML = `抱歉,与 AI 通信时出错: ${error.message}`.replace(/\n/g, '<br>');
                    } else {
                        this.displayMessage(`抱歉,与 AI 通信时出错: ${error.message}`, 'ai', true);
                    }
                } else { // Fallback if aiMessageElement somehow isn't there
                     this.displayMessage(`抱歉,与 AI 通信时出错: ${error.message}`, 'ai', true);
                }
                console.error('AI Chat Error:', error);
                this.displayMessage(`抱歉,与 AI 通信时出错: ${error.message}`, 'ai', true);
            } finally {
                inputArea.disabled = false;
                sendButton.disabled = false;
                inputArea.focus();
            }
        }

        // Modified chat function to support streaming
        async fetchModelsAndUpdateDatalistForChat() {
            const modelDatalist = document.getElementById(this.aiChatModelDatalistId);
            const fetchButton = document.getElementById(this.fetchAiChatModelsButtonId);
            const originalButtonHtml = fetchButton.innerHTML;
            fetchButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
            fetchButton.disabled = true;

            let API_SECRET_KEY = '';
            let BASE_URL = '';
            const currentConfigName = getCurrentApiConfigName();
            let activeConfig = null;

            if (currentConfigName) {
                const configs = getApiConfigurations();
                activeConfig = configs.find(c => c.name === currentConfigName);
            }

            if (activeConfig) {
                BASE_URL = activeConfig.baseUrl;
                API_SECRET_KEY = activeConfig.apiKey;
            } else {
                showToast('请先在“机器翻译”配置中选择一个有效的 API 配置。', 'error', 5000);
                fetchButton.innerHTML = originalButtonHtml;
                fetchButton.disabled = false;
                return;
            }

            if (!BASE_URL || !API_SECRET_KEY) {
                showToast('当前选中的 API 配置缺少 Base URL 或 API Key。', 'error', 5000);
                fetchButton.innerHTML = originalButtonHtml;
                fetchButton.disabled = false;
                return;
            }

            const modelsUrl = `${BASE_URL}${BASE_URL.endsWith('/') ? '' : '/'}models`;

            try {
                const response = await fetch(modelsUrl, {
                    method: 'GET',
                    headers: { 'Authorization': `Bearer ${API_SECRET_KEY}` }
                });
                if (!response.ok) {
                    const errorData = await response.text();
                    showToast(`为AI助手获取模型列表失败: ${response.status} - ${errorData.substring(0,100)}`, 'error', 5000);
                    return;
                }
                const data = await response.json();
                if (data && data.data && Array.isArray(data.data)) {
                    modelDatalist.innerHTML = ''; // Clear existing options
                    data.data.forEach(model => {
                        if (model.id) {
                            const option = document.createElement('option');
                            option.value = model.id;
                            modelDatalist.appendChild(option);
                        }
                    });
                    showToast('AI助手模型列表已更新。', 'success');
                } else {
                    showToast('AI助手模型列表响应数据格式不符合预期。', 'warning', 4000);
                }
            } catch (error) {
                showToast(`为AI助手获取模型列表时发生网络错误: ${error.message}`, 'error', 5000);
            } finally {
                fetchButton.innerHTML = originalButtonHtml;
                fetchButton.disabled = false;
            }
        }

        async chatWithAI(userMessage, onChunkReceived) {
            let API_SECRET_KEY = '';
            let BASE_URL = '';
            
            const currentConfigName = getCurrentApiConfigName(); // This is the translation config
            let activeTranslationConfig = null;
            if (currentConfigName) {
                const configs = getApiConfigurations();
                activeTranslationConfig = configs.find(c => c.name === currentConfigName);
            }

            // Get AI Chat specific model.
            // Priority: 1. localStorageKeyAiChatModel, 2. activeTranslationConfig.model, 3. 'gpt-4o-mini'
            let model = localStorage.getItem(this.localStorageKeyAiChatModel);
            if (!model && activeTranslationConfig && activeTranslationConfig.model) {
                model = activeTranslationConfig.model;
            }
            if (!model) {
                model = 'gpt-4o-mini'; // Ultimate fallback
            }
            
            let temperature = 0.7; // Default temperature for chat
            let systemPrompt = `你是一个在 Paratranz 翻译平台工作的 AI 助手。请根据用户的问题,结合当前条目的原文、上下文、术语等信息(如果提供),提供翻译建议、解释或回答相关问题。请保持回答简洁明了。`;

            if (activeTranslationConfig) {
                BASE_URL = activeTranslationConfig.baseUrl;
                API_SECRET_KEY = activeTranslationConfig.apiKey;
                temperature = (activeTranslationConfig.temperature !== undefined && activeTranslationConfig.temperature !== '') 
                              ? parseFloat(activeTranslationConfig.temperature) 
                              : temperature;
            } else {
                console.warn("AI Chat: No active API configuration selected for API credentials. Chat might fail.");
                // Attempt to use fallback keys if absolutely necessary, but ideally user should configure
                BASE_URL = localStorage.getItem('baseUrl_fallback_for_translate') || ''; 
                API_SECRET_KEY = localStorage.getItem('apiKey_fallback_for_translate') || '';
            }

            if (!BASE_URL || !API_SECRET_KEY) {
                throw new Error("API Base URL 或 Key 未配置。请在“机器翻译”配置中设置。");
            }

            // --- Context Gathering (Optional but Recommended) ---
            let contextInfo = "";
            const shouldSendContext = localStorage.getItem(this.localStorageKeySendContext) === 'true';

            if (shouldSendContext) {
                try {
                    const originalDiv = document.querySelector('.original.well');
                    if (originalDiv) contextInfo += `当前原文 (Original Text):\n${originalDiv.innerText.trim()}\n\n`;

                    const currentTranslationTextarea = document.querySelector('textarea.translation.form-control');
                    if (currentTranslationTextarea && currentTranslationTextarea.value.trim()) {
                        contextInfo += `当前翻译 (Current Translation):\n${currentTranslationTextarea.value.trim()}\n\n`;
                    }

                    const contextNoteDiv = document.querySelector('.context .well');
                    if (contextNoteDiv) contextInfo += `上下文注释 (Context Note):\n${contextNoteDiv.innerText.trim()}\n\n`;

                    const terms = await getTermsData(); // Reuse existing function
                    if (terms.length > 0) {
                        contextInfo += `相关术语 (Terms):\n${terms.map(t => `${t.source} -> ${t.target}${t.note ? ` (${t.note})` : ''}`).join('\n')}\n\n`;
                    }
                } catch (e) {
                    console.warn("AI Chat: Error gathering context:", e);
                }
            }
            // --- End Context Gathering ---

            const messages = [
                { role: "system", content: systemPrompt }
            ];

            if (contextInfo) {
                messages.push({ role: "user", content: `请参考以下上下文信息:\n${contextInfo}我的问题是:\n${userMessage}` });
            } else {
                messages.push({ role: "user", content: userMessage });
            }

            const requestBody = { model, temperature, messages, stream: true }; // Enable streaming

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

            if (!response.ok) {
                let errorData;
                try { errorData = await response.json(); } catch (e) { /* ignore parsing error for non-json errors */ }
                console.error('AI Chat API Error:', errorData || response.statusText);
                throw new Error(`API 请求失败: ${response.status} - ${errorData?.error?.message || errorData?.message || response.statusText}`);
            }

            if (!response.body) {
                throw new Error('ReadableStream not available in response.');
            }

            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let buffer = '';

            try {
                while (true) {
                    const { done, value } = await reader.read();
                    if (done) break;

                    buffer += decoder.decode(value, { stream: true });
                    
                    let eolIndex;
                    while ((eolIndex = buffer.indexOf('\n')) >= 0) {
                        const line = buffer.substring(0, eolIndex).trim();
                        buffer = buffer.substring(eolIndex + 1);

                        if (line.startsWith('data: ')) {
                            const jsonData = line.substring(6);
                            if (jsonData === '[DONE]') {
                                console.log("Stream finished.");
                                return; // Stream ended
                            }
                            try {
                                const parsed = JSON.parse(jsonData);
                                if (parsed.choices && parsed.choices[0]?.delta?.content) {
                                    onChunkReceived(parsed.choices[0].delta.content);
                                }
                            } catch (e) {
                                console.error('Error parsing stream JSON:', e, jsonData);
                            }
                        }
                    }
                }
                // Process any remaining buffer content if necessary (though for SSE, lines usually end with \n)
                if (buffer.trim().startsWith('data: ')) {
                     const jsonData = buffer.trim().substring(6);
                     if (jsonData !== '[DONE]') {
                        try {
                            const parsed = JSON.parse(jsonData);
                            if (parsed.choices && parsed.choices[0]?.delta?.content) {
                                onChunkReceived(parsed.choices[0].delta.content);
                            }
                        } catch (e) {
                            console.error('Error parsing final buffer JSON:', e, jsonData);
                        }
                     }
                }


            } catch (error) {
                console.error('Error reading stream:', error);
                throw new Error(`读取流时出错: ${error.message}`);
            } finally {
                reader.releaseLock();
            }
        }
    }

    // --- Initialization ---
    const aiChatDialog = new AIChatDialog(); // Initialize AI Chat Dialog

})();