Perchance Image Replacer (Vietnamese Version) - Multi-Key & Contextual Sub-Tags v1.7.5

Tìm prompt, hiển thị nút "Tạo". Giữ lại ảnh cũ, thêm điều hướng ảnh. Lưu tag phụ khi tải lại trang. Cache IndexedDB, quản lý userKey. Menu chỉnh sửa prompt style & tag phụ. Giao diện nền tối. Thêm chức năng Xuất/Nhập dữ liệu. Cải thiện hiển thị panel trên di động.

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

// ==UserScript==
// @name         Perchance Image Replacer (Vietnamese Version) - Multi-Key & Contextual Sub-Tags v1.7.5
// @namespace    http://tampermonkey.net/
// @version      1.7.6
// @description  Tìm prompt, hiển thị nút "Tạo". Giữ lại ảnh cũ, thêm điều hướng ảnh. Lưu tag phụ khi tải lại trang. Cache IndexedDB, quản lý userKey. Menu chỉnh sửa prompt style & tag phụ. Giao diện nền tối. Thêm chức năng Xuất/Nhập dữ liệu. Cải thiện hiển thị panel trên di động.
// @author       Dựa trên ý tưởng của bạn & Gemini & Claude
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @require      https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
// @connect      image-generation.perchance.org
// @connect      perchance.org
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const PROMPT_REGEX = /(?:image|img)###([^#]+)###/gi;
    const CREATE_URL = 'https://image-generation.perchance.org/api/generate';
    const DOWNLOAD_URL = 'https://image-generation.perchance.org/api/downloadTemporaryImage';
    const VERIFY_KEY_URL = 'https://image-generation.perchance.org/api/checkUserVerificationStatus';
    const KEY_FETCH_PAGE = 'https://perchance.org/ai-text-to-image-generator';
    const PERCHANCE_IFRAME_PANEL_PATH_FALLBACK = 'ai-image-generator-panel';

    const USER_KEYS_STORAGE = 'perchanceUserKeys_v4_multi';
    const MAX_STORED_KEYS = 5;
    const PROMPT_PRESETS_STORAGE = 'perchancePromptPresets_v1';
    const KEYWORD_SUBTAG_MAP_STORAGE = 'perchanceKeywordSubTagMap_v1';
    const DETAILED_PROMPTS_STORAGE = 'perchanceDetailedPrompts_v1'; // For persisting detailed tags
    const SCRIPT_SETTINGS_STORAGE = 'perchanceScriptSettings_v1'; // For general script settings if needed in future

    let ANIME_STYLE_DEFINITION = {
        positive: '(sharp_focus, pale_male, An 8k hyper-anime, HDR photo, highly detailed, split-complementary color palette, UHD, hyperrealistic, casting shadow style, vivid, clear shadows and highlights, intense, enhanced contrast, best quality, masterpiece, well-structured, vibrant colors, dynamic lighting, high resolution, sharp focus, crisp details, smooth texture, clean lines, highly polished, accurate proportions, age-appropriate features, natural skin tones, anime natural facial features, realistic age portrayal, subtle age details, refined character depth, soft, natural lighting, gentle, expressive shadows, nuanced facial contours, detailed, lifelike textures, artistically integrated age signs when applicable, flexible racial/spesies features.) NSFW, Uncensored Hentai Artwork, Ecchi, Explicit Scenes, Adult Content, Explicit Sexual Acts, Visual Hentai',
        negative: '(low quality, worst quality:1.3), text, watermark, signature, title, overused, generic, cliché, unoriginal, bland, uninspired, conventional, inconsistent style, mismatched elements, clashing colors, disjointed composition, uneven proportions, incoherent design, unbalanced layout, disorganized appearance, randomized elements, asymmetrical features, overused, predictable, wrong sex, wrong gender, wrong species, wrong age, misleading gender presentation, incorrect intimacy positioning, unnatural intimacy, distorted features, harsh lighting, overly saturated colors, cluttered background, unrealistic proportions, lack of detail, exaggerated expressions, pixelated or low-quality image, anatomical inaccuracies, mutations, deformities, disfigurements, grotesque elements, unnatural body proportions, blurred, jpeg artifacts, cropped, cut-off, flat shading, unnatural line integration with background, stiff poses, unnatural skin textures, overexposed areas, underexposed areas, excessive noise, artificial-looking shadows, disproportionate hands, large or creepy fingers and toes, deformities in close-up shots'
    };
    let DEFAULT_NEGATIVE_PROMPT_BASE = '';

    let subTagModal = null;
    let currentEditingTagElement = null;
    let originalLoadedKeywordGroupState = { keywords: [], subTags: [] };

    const DB_NAME = 'perchanceImageCacheDB_multi_v2';
    const DB_VERSION = 1;
    const IMAGE_STORE_NAME = 'images';
    let db;
    let activeRequests = new Set();
    let mutationObserver = null;
    let sillyTavernMenuIntegrationInterval = null;

    function addGlobalStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .perchance-prompt-container {
                display: inline-block; vertical-align: middle; margin: 0 2px;
                padding: 8px; border: 1px solid #4f4f4f;
                border-radius: 5px; background-color: #3a3a3a;
                color: #f0f0f0; font-family: sans-serif;
            }
            .perchance-main-tags-area { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
            .perchance-main-tag-wrapper {
                display: flex; align-items: center; background-color: #4a4a4a;
                color: #e0e0e0; padding: 4px 8px; border-radius: 4px; font-size: 0.9em;
            }
            .perchance-main-tag-text { margin-right: 5px; }
            .edit-subtags-btn { background-color: #4CAF50; color: white; border: none; border-radius: 3px; padding: 2px 6px; font-size: 0.85em; cursor: pointer; margin-left: 3px; }
            .edit-subtags-btn:hover { background-color: #45a049; }
            .perchance-generate-btn { padding: 4px 10px; font-size: 13px; line-height: 1.4; cursor: pointer; border: 1px solid #0056b3; border-radius: 4px; background-color: #007bff; color: white; margin-right: 4px; }
            .perchance-generate-btn:hover { background-color: #0069d9; }
            .perchance-generate-btn:disabled { background-color: #555; color: #aaa; border-color: #444; cursor: not-allowed; }
            .perchance-image-placeholder { margin-top: 8px; min-height: 20px; text-align: left; }
            .perchance-image-placeholder img { max-width: 100%; display: block; border-radius: 4px; margin-top: 8px; }
            .perchance-image-placeholder img:not(.active-gallery-image) { display: none; } /* Hide non-active gallery images */
            .perchance-image-placeholder hr { border: none; border-top: 1px solid #555; margin: 10px 0; }

            .perchance-image-nav-controls { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 5px; }
            .perchance-image-nav-controls button { background-color: #555; color: white; border: 1px solid #777; padding: 3px 8px; border-radius: 3px; cursor: pointer; font-size: 0.9em; }
            .perchance-image-nav-controls button:disabled { background-color: #333; color: #666; cursor: not-allowed; }
            .perchance-image-nav-counter { font-size: 0.9em; color: #ccc; }


            .placeholder-status-info { font-size:0.85em; color: #bbbbbb; padding: 2px 0; }
            .placeholder-error { color:#ff9999; border:1px solid #cc3333; background-color: #4d0000; padding:4px 6px; margin:0; font-size:0.85em; border-radius: 3px; }
            .placeholder-warning { color:#ffd799; border:1px solid #cc8800; background-color: #4d3300; padding:4px 6px; margin:0; font-size:0.85em; border-radius: 3px; }

            #subTagModal {
                position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
                background-color: #3a3a3a; color: white; padding: 20px;
                border: 1px solid #666; border-radius: 8px; z-index: 10002;
                display: none; width: 90%; max-width: 600px; max-height: 85vh;
                box-shadow: 0 8px 20px rgba(0,0,0,0.6); font-family: sans-serif;
                box-sizing: border-box; overflow-y: auto;
            }
            #subTagModal h3 { margin-top: 0; border-bottom: 1px solid #555; padding-bottom:10px; font-size: 1.3em; }
            #subTagModal p { font-size:0.9em; color:#ccc; margin-top:-5px; margin-bottom:15px; }
            #subTagListContainer {
                display: flex; flex-wrap: wrap; gap: 8px;
                max-height: calc(60vh - 50px); /* Consider adjusting if modal content changes significantly */
                overflow-y: auto; padding: 10px; background-color: #2c2c2c;
                border-radius: 4px; margin-bottom: 15px;
            }
            .subtag-item { display: flex; align-items: center; background-color: #4a4a4a; padding: 6px 10px; border-radius: 4px; cursor: pointer; font-size: 0.95em; }
            .subtag-item:hover { background-color: #5a5a5a; }
            .subtag-item input[type="checkbox"] { margin-right: 8px; transform: scale(1.1); }
            #subTagModalButtons { text-align: right; margin-top: 20px; }
            #subTagModalButtons button { padding: 10px 18px; border: none; border-radius: 4px; cursor: pointer; margin-left: 10px; font-size: 0.95em; }
            #applySubTagsBtn { background-color: #28a745; color: white; }
            #applySubTagsBtn:hover { background-color: #218838; }
            #closeSubTagModalBtn { background-color: #6c757d; color: white; }
            #closeSubTagModalBtn:hover { background-color: #5a6268; }

            .keyword-subtag-manager, .data-management-section { border-top: 1px solid #555; margin-top: 20px; padding-top: 15px; }
            .keyword-subtag-manager h4, .data-management-section h4 { margin-top: 0; margin-bottom: 10px; font-size: 1.2em; }
            .keyword-list-area { margin-bottom: 10px; }
            .keyword-list-area label, .associated-tags-area label, .keyword-edit-area label, .data-management-section label { display: block; margin-bottom: 6px; font-size: 0.95em; }
            #keywordSelector, #keywordNameInput, #associatedSubTagsTextarea { width: 100%; padding: 10px; background-color: #333; color: white; border: 1px solid #555; border-radius: 4px; box-sizing: border-box; margin-bottom:12px; font-size: 0.95em; }
            .keyword-actions button, .data-management-section button { padding: 10px 15px; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right:8px; font-size: 0.9em; }
            .keyword-actions button#saveKeywordMappingBtn { background-color: #3498db; }
            .keyword-actions button#saveKeywordMappingBtn:hover { background-color: #2980b9; }
            .keyword-actions button.delete { background-color: #e74c3c; }
            .keyword-actions button.delete:hover { background-color: #c0392b; }
            .data-management-section input[type="file"] { display: none; } /* Hide default file input */
            .data-management-section .file-input-label { background-color: #5bc0de; color: white; padding: 10px 15px; border-radius: 4px; cursor: pointer; display: inline-block; margin-right: 8px; font-size: 0.9em; }
            .data-management-section .file-input-label:hover { background-color: #31b0d5; }
            #data-management-status { margin-top:10px; font-size:0.9em; color:#ccc; }

            #perchance-prompt-menu-panel {
                font-family: sans-serif;
                position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
                background-color: #2c2c2c; color: white; padding: 20px; /* Adjusted padding slightly */
                border: 1px solid #555; border-radius: 8px; z-index: 10001;
                display: none; width: 90%; max-width: 700px; /* Responsive width */
                max-height: 90vh; overflow-y: auto;
                box-shadow: 0 5px 15px rgba(0,0,0,0.5);
                box-sizing: border-box; /* Added for better width calculation */
            }
            #perchance-prompt-menu-panel h2 { font-size: 1.4em; /* Adjusted for smaller screens */ }
            #perchance-prompt-menu-panel label { font-size: 0.9em; /* Adjusted for smaller screens */ }
            #perchance-prompt-menu-panel select,
            #perchance-prompt-menu-panel textarea,
            #perchance-prompt-menu-panel input[type="text"] {
                padding: 8px; font-size: 0.9em; /* Adjusted for smaller screens */
                width: 100%; box-sizing: border-box; /* Ensure full width within padding */
            }
            #perchance-prompt-menu-panel .button-group { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom:10px } /* For button groups */
            #perchance-prompt-menu-panel .button-group button { flex-grow: 1; } /* Allow buttons to grow */

            #perchance-prompt-menu-panel button { padding: 8px 12px; font-size: 0.9em; /* Adjusted for smaller screens */ }
            /* Specific adjustments for side-by-side elements if needed via media queries */
            @media (max-width: 480px) {
                 #perchance-prompt-menu-panel select,
                 #perchance-prompt-menu-panel input[type="text"] {
                    margin-bottom: 8px; /* Add space when stacked */
                 }
                 #perchance-prompt-menu-panel .button-group {
                    flex-direction: column; /* Stack buttons on very small screens */
                 }
                 #perchance-prompt-menu-panel div[style*="margin-bottom: 15px;"] > select,
                 #perchance-prompt-menu-panel div[style*="margin-bottom: 15px;"] > input[type="text"] {
                     width: 100% !important; /* Override inline style for full width */
                     margin-right: 0 !important;
                 }
                 #perchance-prompt-menu-panel div[style*="margin-bottom: 15px;"] > button {
                     width: 100%;
                     margin-top: 5px;
                 }
            }
        `;
        document.head.appendChild(style);
    }

    // --- User Key Management ---
    function getStoredUserKeys() {
        const keysJson = GM_getValue(USER_KEYS_STORAGE, JSON.stringify([]));
        try { const keys = JSON.parse(keysJson); return Array.isArray(keys) ? keys.filter(key => typeof key === 'string' && /^[a-f0-9]{64}$/.test(key)) : []; }
        catch (e) { console.error("Lỗi JSON UserKeys:", e); return []; }
    }
    function saveUserKeys(keys) {
        const uniqueKeys = [...new Set(keys)].filter(key => typeof key === 'string' && /^[a-f0-9]{64}$/.test(key));
        if (uniqueKeys.length > MAX_STORED_KEYS) { uniqueKeys.splice(0, uniqueKeys.length - MAX_STORED_KEYS); }
        GM_setValue(USER_KEYS_STORAGE, JSON.stringify(uniqueKeys)); return uniqueKeys;
    }
    function addAndSaveUserKey(newKey) {
        if (!newKey || typeof newKey !== 'string' || !/^[a-f0-9]{64}$/.test(newKey)) { console.warn("Key không hợp lệ:", newKey); return getStoredUserKeys(); }
        let keys = getStoredUserKeys(); const existingIndex = keys.indexOf(newKey);
        if (existingIndex > -1) { keys.splice(existingIndex, 1); }
        keys.push(newKey); return saveUserKeys(keys);
    }
    function removeAndSaveUserKey(keyToRemove) {
        let keys = getStoredUserKeys(); const index = keys.indexOf(keyToRemove);
        if (index > -1) { keys.splice(index, 1); keys = saveUserKeys(keys); console.log(`Đã xóa key ...${keyToRemove.slice(-6)}.`); }
        return keys;
    }

    // --- IndexedDB Management ---
    async function initDB() {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(DB_NAME, DB_VERSION);
            request.onerror = (event) => { console.error("Lỗi IndexedDB:", event.target.error); reject(event.target.error); };
            request.onsuccess = (event) => { db = event.target.result; console.log("IndexedDB đã mở."); resolve(db); };
            request.onupgradeneeded = (event) => {
                const store = event.target.result.createObjectStore(IMAGE_STORE_NAME, { keyPath: 'promptKey' });
                store.createIndex('timestamp', 'timestamp', { unique: false });
            };
        });
    }
    async function storeImage(promptKey, base64Image) {
        if (!db) { console.error("DB chưa sẵn sàng."); return; }
        return new Promise((resolve, reject) => {
            const transaction = db.transaction([IMAGE_STORE_NAME], 'readwrite');
            const store = transaction.objectStore(IMAGE_STORE_NAME);
            const request = store.put({ promptKey: promptKey, image: base64Image, timestamp: Date.now() });
            request.onsuccess = () => resolve();
            request.onerror = (event) => { console.error(`Lỗi lưu ảnh ${promptKey.substring(0,10)}...:`, event.target.error); reject(event.target.error); };
        });
    }
    async function getCachedImage(promptKey) {
        if (!db) { console.error("DB chưa sẵn sàng."); return null; }
        return new Promise((resolve, reject) => {
            const transaction = db.transaction([IMAGE_STORE_NAME], 'readonly');
            const store = transaction.objectStore(IMAGE_STORE_NAME);
            const request = store.get(promptKey);
            request.onsuccess = (event) => resolve(event.target.result ? event.target.result.image : null);
            request.onerror = (event) => { console.error(`Lỗi lấy cache ${promptKey.substring(0,10)}...:`, event.target.error); reject(event.target.error); };
        });
    }
    async function getAllCachedImages() {
        if (!db) { console.error("DB chưa sẵn sàng."); return []; }
        return new Promise((resolve, reject) => {
            const transaction = db.transaction([IMAGE_STORE_NAME], 'readonly');
            const store = transaction.objectStore(IMAGE_STORE_NAME);
            const request = store.getAll();
            request.onsuccess = (event) => resolve(event.target.result || []);
            request.onerror = (event) => { console.error("Lỗi lấy tất cả ảnh cache:", event.target.error); reject(event.target.error); };
        });
    }
    async function clearAndStoreImages(imagesArray) {
        if (!db) { console.error("DB chưa sẵn sàng."); return; }
        return new Promise((resolve, reject) => {
            const transaction = db.transaction([IMAGE_STORE_NAME], 'readwrite');
            const store = transaction.objectStore(IMAGE_STORE_NAME);
            const clearRequest = store.clear();
            clearRequest.onsuccess = () => {
                if (imagesArray && imagesArray.length > 0) {
                    let count = 0;
                    imagesArray.forEach(imgData => {
                        const putRequest = store.put(imgData);
                        putRequest.onsuccess = () => {
                            count++;
                            if (count === imagesArray.length) resolve();
                        };
                        putRequest.onerror = (event) => {
                            console.error(`Lỗi ghi ảnh ${imgData.promptKey ? imgData.promptKey.substring(0,10) : 'UNKNOWN'}... vào DB:`, event.target.error);
                            // Tiếp tục với các ảnh khác
                            count++;
                            if (count === imagesArray.length) resolve(); // Vẫn resolve để hoàn tất quá trình nhập
                        };
                    });
                } else {
                    resolve(); // Không có ảnh để nhập
                }
            };
            clearRequest.onerror = (event) => { console.error("Lỗi xóa ảnh cache cũ:", event.target.error); reject(event.target.error); };
        });
    }


    // --- Blob to Base64 ---
    function blobToBase64(blob) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onloadend = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob);
        });
    }

    // --- Automatic User Key Fetching ---
    async function fetchUserKeyAutomatically(showMessages = false) {
        if (showMessages) console.log(`Đang thử tự động lấy userKey từ ${KEY_FETCH_PAGE}...`);
        try {
            const mainPageResponse = await new Promise((resolve, reject) => GM_xmlhttpRequest({ method: "GET", url: KEY_FETCH_PAGE, onload: resolve, onerror: () => reject(new Error('Lỗi mạng (trang chính).')), ontimeout: () => reject(new Error('Timeout (trang chính).')) }));
            if (mainPageResponse.status !== 200) throw new Error(`Lỗi tải trang lấy key: ${mainPageResponse.status}`);
            const parser = new DOMParser(); const mainDoc = parser.parseFromString(mainPageResponse.responseText, "text/html");
            let iframeSrc; const mainIframe = mainDoc.querySelector('iframe#main');
            if (mainIframe && mainIframe.getAttribute('src')) { iframeSrc = new URL(mainIframe.getAttribute('src'), KEY_FETCH_PAGE).href; }
            else { const panelIframe = mainDoc.querySelector(`iframe[src*="${PERCHANCE_IFRAME_PANEL_PATH_FALLBACK}"]`); iframeSrc = panelIframe ? new URL(panelIframe.getAttribute('src'), KEY_FETCH_PAGE).href : new URL(PERCHANCE_IFRAME_PANEL_PATH_FALLBACK, new URL(KEY_FETCH_PAGE).origin).href; }
            if (!iframeSrc) throw new Error('Không thể xác định URL iframe chứa userKey.');
            const iframeResponse = await new Promise((resolve, reject) => GM_xmlhttpRequest({ method: "GET", url: iframeSrc, onload: resolve, onerror: () => reject(new Error(`Lỗi mạng (iframe ${iframeSrc}).`)), ontimeout: () => reject(new Error(`Timeout (iframe ${iframeSrc}).`)) }));
            if (iframeResponse.status !== 200) throw new Error(`Lỗi tải iframe (${iframeSrc}): ${iframeResponse.status}`);
            const iframeContent = iframeResponse.responseText; const keyRegex = /userKey(?:["']?:["']?|\s*=\s*['"]?)([a-f0-9]{64})['"]?/gi;
            let regexMatch; const potentialKeys = new Set();
            while ((regexMatch = keyRegex.exec(iframeContent)) !== null) potentialKeys.add(regexMatch[1]);
            if (potentialKeys.size === 0 && showMessages) console.warn(`Không tìm thấy userKey nào trong iframe từ ${iframeSrc}.`);
            for (const potentialKey of potentialKeys) {
                const verificationParams = new URLSearchParams({ 'userKey': potentialKey, '__cacheBust': Math.random().toString() });
                try {
                    const verificationResponseText = await new Promise((resolve, reject) => GM_xmlhttpRequest({ method: "GET", url: `${VERIFY_KEY_URL}?${verificationParams.toString()}`, onload: (r) => r.status === 200 ? resolve(r.responseText) : reject(new Error(`Trạng thái ${r.status}`)), onerror: () => reject(new Error('Lỗi mạng (xác minh).')), ontimeout: () => reject(new Error('Timeout (xác minh).')) }));
                    if (verificationResponseText && verificationResponseText.includes('verified') && !verificationResponseText.includes('not_verified')) {
                        if (showMessages) console.log(`UserKey tự động: ...${potentialKey.slice(-6)} (Đã xác minh từ ${iframeSrc})`);
                        addAndSaveUserKey(potentialKey); return potentialKey;
                    } else if (showMessages) console.log(`Key ...${potentialKey.slice(-6)} xác minh thất bại. Resp:`, verificationResponseText.substring(0,100));
                } catch (verifyError) { if (showMessages) console.warn(`Lỗi xác minh key ...${potentialKey.slice(-6)}: ${verifyError.message}`); }
            }
            if (showMessages) console.warn(`Không tìm thấy userKey hợp lệ tự động từ ${iframeSrc}.`); return null;
        } catch (error) { if (showMessages) console.error("Lỗi tự động lấy userKey:", error.message); return null; }
    }
    async function promptForUserKey(message) {
        const userKeyInput = prompt(message + `\n\n(Mẹo: Truy cập ${KEY_FETCH_PAGE}, mở Developer Tools (F12) > Network tab, thử tạo một hành động nào đó trên trang. Tìm request có 'userKey=xxx...' trong URL hoặc payload, copy phần giá trị 64 ký tự của userKey đó.)`);
        if (userKeyInput) {
            const trimmedKey = userKeyInput.trim();
            if (/^[a-f0-9]{64}$/.test(trimmedKey)) { addAndSaveUserKey(trimmedKey); console.log(`Đã lưu userKey: ...${trimmedKey.slice(-6)}`); return trimmedKey; }
            else { alert("Định dạng userKey không đúng."); }
        } return null;
    }

    // --- API Calls ---
    async function generateImageApi(promptText, negativePromptText, userKeyToUse, resolution = '512x768', guidanceScale = '7') {
        const createParams = new URLSearchParams({ 'prompt': promptText, 'negativePrompt': negativePromptText, 'userKey': userKeyToUse, '__cache_bust': Math.random().toString(), 'seed': '-1', 'resolution': resolution, 'guidanceScale': guidanceScale.toString(), 'channel': 'ai-text-to-image-generators', 'subChannel': 'public', 'requestId': Math.random().toString() });
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET", url: `${CREATE_URL}?${createParams.toString()}`, headers: { "User-Agent": "Mozilla/5.0", "Accept": "application/json, text/plain, */*", "Referer": "https://perchance.org/" },
                onload: async function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.status === 'success' && data.imageId) resolve(data.imageId);
                        else if (data.status === 'invalid_key' || (data.error && data.error.includes('invalid_key'))) { console.error(`UserKey ...${userKeyToUse.slice(-6)} không hợp lệ.`); reject(new Error('INVALID_KEY_API_ERROR')); }
                        else reject(new Error(data.message || data.error || 'Lỗi API Perchance.'));
                    } catch (e) { reject(new Error(response.responseText.includes("<!doctype html>") ? 'API Perchance trả về HTML.' : `Lỗi phân tích JSON: ${e.message}`)); }
                },
                onerror: (err) => { console.error("GM_xmlhttpRequest error:", err); reject(new Error('Lỗi mạng khi tạo ảnh.')); },
                ontimeout: () => reject(new Error('Timeout tạo ảnh.'))
            });
        });
    }
    async function downloadImageApi(imageId) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET", url: `${DOWNLOAD_URL}?imageId=${imageId}`, responseType: 'blob', headers: { "User-Agent": "Mozilla/5.0", "Accept": "image/webp,image/jpeg,image/png,*/*", "Referer": "https://perchance.org/" },
                onload: (r) => (r.status === 200 && r.response) ? resolve(r.response) : reject(new Error(`Lỗi tải ảnh: ${r.status}`)),
                onerror: (err) => { console.error("GM_xmlhttpRequest error:", err); reject(new Error('Lỗi mạng khi tải ảnh.')); },
                ontimeout: () => reject(new Error('Timeout tải ảnh.'))
            });
        });
    }

    // --- Tag Parsing and Formatting ---
    function parseDetailedTag(detailedValue) {
        const match = detailedValue.match(/^([^(]+)(?:\(\(\(([^)]+)\)\)\))?$/);
        if (match) {
            const mainTag = match[1].trim();
            const subTagsString = match[2];
            const subTags = subTagsString ? subTagsString.split(',').map(s => s.trim()).filter(s => s) : [];
            return { mainTag, subTags };
        }
        return { mainTag: detailedValue.trim(), subTags: [] };
    }
    function formatDetailedTag(mainTag, subTagsArray) {
        if (subTagsArray.length === 0) return mainTag;
        return `${mainTag}(((${subTagsArray.join(', ')})))`;
    }

    // --- Keyword-SubTag Mappings Management ---
    function getKeywordSubTagMap() {
        const mapJson = GM_getValue(KEYWORD_SUBTAG_MAP_STORAGE, JSON.stringify({}));
        try {
            const map = JSON.parse(mapJson);
            for (const key in map) {
                if (Array.isArray(map[key])) {
                    map[key] = map[key].map(String).filter(s => s.trim() !== "");
                } else { delete map[key]; }
            }
            return map;
        }
        catch (e) { console.error("Error parsing KeywordSubTagMap:", e); return {}; }
    }
    function saveKeywordSubTagMap(map) {
        GM_setValue(KEYWORD_SUBTAG_MAP_STORAGE, JSON.stringify(map));
    }

    // --- Sub-Tag Modal ---
    function createSubTagModal() {
        if (document.getElementById('subTagModal')) return;
        subTagModal = document.createElement('div'); subTagModal.id = 'subTagModal';
        let modalHTML = `
            <h3>Chỉnh sửa Tag Phụ cho "<span id="editingMainTagText"></span>"</h3>
            <p>Các tag phụ được gợi ý theo từ khóa (nếu có) hoặc đã áp dụng trước đó. Chỉ những tag đã áp dụng mới được chọn sẵn.</p>
            <div id="subTagListContainer"></div>
            <div id="subTagModalButtons">
                <button id="applySubTagsBtn">Áp dụng</button>
                <button id="closeSubTagModalBtn">Đóng</button>
            </div>`;
        subTagModal.innerHTML = modalHTML; document.body.appendChild(subTagModal);

        const applyBtn = subTagModal.querySelector('#applySubTagsBtn');
        const closeBtn = subTagModal.querySelector('#closeSubTagModalBtn');

        applyBtn.addEventListener('click', () => {
            if (!currentEditingTagElement) return;
            const { mainTag } = parseDetailedTag(currentEditingTagElement.dataset.currentValue);
            const selectedSubTags = [];
            subTagModal.querySelectorAll('#subTagListContainer input[type="checkbox"]:checked').forEach(cb => {
                selectedSubTags.push(cb.value);
            });
            const newDetailedValue = formatDetailedTag(mainTag, selectedSubTags);
            currentEditingTagElement.dataset.currentValue = newDetailedValue;
            currentEditingTagElement.querySelector('.perchance-main-tag-text').textContent = newDetailedValue;

            const promptWrapper = currentEditingTagElement.closest('.perchance-prompt-container');
            if (promptWrapper && promptWrapper.dataset.rawPrompt) {
                const originalRawPrompt = promptWrapper.dataset.rawPrompt;
                const allCurrentDetailedValues = [];
                promptWrapper.querySelectorAll('.perchance-main-tag-wrapper').forEach(tagEl => {
                    allCurrentDetailedValues.push(tagEl.dataset.currentValue);
                });
                let detailedPromptsMap = GM_getValue(DETAILED_PROMPTS_STORAGE, {});
                try { detailedPromptsMap = JSON.parse(detailedPromptsMap) } catch(e) { detailedPromptsMap = {} } // Ensure it's an object
                detailedPromptsMap[originalRawPrompt] = allCurrentDetailedValues;
                GM_setValue(DETAILED_PROMPTS_STORAGE, JSON.stringify(detailedPromptsMap));
            }
            subTagModal.style.display = 'none';
        });
        closeBtn.addEventListener('click', () => { subTagModal.style.display = 'none'; });
    }

    function populateSubTagList(mainTagElement) {
        const listContainer = subTagModal.querySelector('#subTagListContainer');
        listContainer.innerHTML = '';
        const { mainTag: mainTagOnly, subTags: alreadyAppliedSubTags } = parseDetailedTag(mainTagElement.dataset.currentValue);
        let suggestedByKeywords = [];
        const keywordMap = getKeywordSubTagMap();
        for (const keyword in keywordMap) {
            if (mainTagOnly.toLowerCase().includes(keyword.toLowerCase())) {
                keywordMap[keyword].forEach(suggestedTag => {
                    if (!suggestedByKeywords.includes(suggestedTag)) { suggestedByKeywords.push(suggestedTag); }
                });
            }
        }
        const allDisplayableSubTags = [...new Set([...alreadyAppliedSubTags, ...suggestedByKeywords])].sort();
        if (allDisplayableSubTags.length === 0) {
            listContainer.innerHTML = '<p style="color:#aaa; font-style:italic; text-align:center; padding: 10px 0;">Không có tag phụ nào được áp dụng hoặc được gợi ý.</p>';
            return;
        }
        allDisplayableSubTags.forEach(subTag => {
            const item = document.createElement('label'); item.className = 'subtag-item';
            const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = subTag;
            if (alreadyAppliedSubTags.includes(subTag)) { checkbox.checked = true; }
            item.appendChild(checkbox); item.appendChild(document.createTextNode(` ${subTag}`));
            listContainer.appendChild(item);
        });
    }

    function showSubTagModal(mainTagWrapperElement) {
        if (!subTagModal) createSubTagModal();
        currentEditingTagElement = mainTagWrapperElement;
        const { mainTag } = parseDetailedTag(mainTagWrapperElement.dataset.currentValue);
        subTagModal.querySelector('#editingMainTagText').textContent = mainTag;
        populateSubTagList(mainTagWrapperElement);
        subTagModal.style.display = 'block';
    }

    // --- Image Navigation Functions ---
    function showImageAtIndex(imagePlaceholder, newIndex) {
        const galleryImages = Array.from(imagePlaceholder.querySelectorAll('img'));
        const navControls = imagePlaceholder.querySelector('.perchance-image-nav-controls');
        if (!galleryImages.length || !navControls) return;

        const currentIdx = parseInt(newIndex, 10);
        imagePlaceholder.dataset.currentImageIndex = currentIdx.toString();

        galleryImages.forEach((img, idx) => {
            img.style.display = (idx === currentIdx) ? 'block' : 'none';
            if (idx === currentIdx) img.classList.add('active-gallery-image');
            else img.classList.remove('active-gallery-image');
        });

        const counterSpan = navControls.querySelector('.perchance-image-nav-counter');
        const prevBtn = navControls.querySelector('.prev-image-btn');
        const nextBtn = navControls.querySelector('.next-image-btn');

        if (counterSpan) counterSpan.textContent = `${currentIdx + 1} / ${galleryImages.length}`;
        if (prevBtn) prevBtn.disabled = (currentIdx === 0);
        if (nextBtn) nextBtn.disabled = (currentIdx === galleryImages.length - 1);
    }

    function updateImageNavigation(imagePlaceholder) {
        let navControls = imagePlaceholder.querySelector('.perchance-image-nav-controls');
        const galleryImages = Array.from(imagePlaceholder.querySelectorAll('img'));

        if (navControls) navControls.remove();

        if (galleryImages.length <= 1) {
            galleryImages.forEach(img => {
                img.style.display = 'block';
                img.classList.add('active-gallery-image');
            });
            return;
        }

        navControls = document.createElement('div');
        navControls.className = 'perchance-image-nav-controls';

        const prevBtn = document.createElement('button');
        prevBtn.textContent = 'Trước';
        prevBtn.className = 'prev-image-btn';
        prevBtn.onclick = () => {
            let currentIndex = parseInt(imagePlaceholder.dataset.currentImageIndex || "0", 10);
            if (currentIndex > 0) showImageAtIndex(imagePlaceholder, currentIndex - 1);
        };

        const counterSpan = document.createElement('span');
        counterSpan.className = 'perchance-image-nav-counter';

        const nextBtn = document.createElement('button');
        nextBtn.textContent = 'Sau';
        nextBtn.className = 'next-image-btn';
        nextBtn.onclick = () => {
            let currentIndex = parseInt(imagePlaceholder.dataset.currentImageIndex || "0", 10);
            if (currentIndex < galleryImages.length - 1) showImageAtIndex(imagePlaceholder, currentIndex + 1);
        };

        navControls.appendChild(prevBtn);
        navControls.appendChild(counterSpan);
        navControls.appendChild(nextBtn);
        imagePlaceholder.appendChild(navControls);

        const initialIndex = parseInt(imagePlaceholder.dataset.currentImageIndex || "0", 10);
        showImageAtIndex(imagePlaceholder, Math.min(initialIndex, galleryImages.length - 1));
    }


    // --- Handle Generate Click ---
    async function handleGenerateClick(event) {
        const button = event.target;
        const promptWrapper = button.closest('.perchance-prompt-container');
        const imagePlaceholder = promptWrapper ? promptWrapper.querySelector('.perchance-image-placeholder') : null;
        if (!promptWrapper || !imagePlaceholder) { console.error("Thiếu thông tin."); return; }

        const mainTagElements = promptWrapper.querySelectorAll('.perchance-main-tag-wrapper');
        let detailedMainTags = [];
        mainTagElements.forEach(tagEl => { detailedMainTags.push(tagEl.dataset.currentValue); });
        const basePromptFromTags = detailedMainTags.join(', ');
        const finalDetailedPromptForHashing = `${basePromptFromTags}, ${ANIME_STYLE_DEFINITION.positive}`;
        const detailedPromptKey = CryptoJS.MD5(finalDetailedPromptForHashing + ANIME_STYLE_DEFINITION.negative).toString();

        const action = button.dataset.action || 'generate';
        button.disabled = true; activeRequests.add(detailedPromptKey);

        if (action === 'generate') {
            const cachedImage = await getCachedImage(detailedPromptKey);
            if (cachedImage) {
                imagePlaceholder.innerHTML = ''; // Clear previous content before adding cached image
                const img = document.createElement('img');
                img.src = cachedImage; img.alt = `${basePromptFromTags.substring(0,30)}... (cache)`; img.title = img.alt;
                img.style.border = "2px solid #00bcd4";
                imagePlaceholder.appendChild(img);
                updateImageNavigation(imagePlaceholder); // Update nav after adding the image
                button.textContent = 'Tạo lại'; button.dataset.action = 'regenerate';
                button.disabled = false; activeRequests.delete(detailedPromptKey);
                console.log(`Ảnh "${basePromptFromTags.substring(0,30)}..." từ cache.`); return;
            }
        }

        const statusMessages = imagePlaceholder.querySelectorAll('span.placeholder-status-info, p.placeholder-error, p.placeholder-warning');
        statusMessages.forEach(msg => msg.remove());
        const preparingMsg = document.createElement('span');
        preparingMsg.className = 'placeholder-status-info';
        preparingMsg.textContent = 'Đang chuẩn bị...';
        imagePlaceholder.insertBefore(preparingMsg, imagePlaceholder.firstChild);


        let currentKeysInStorage = getStoredUserKeys(); let keysAttemptedInThisRun = new Set(); let newKeyFetchedInThisRun = false;

        while (true) {
            let keyToTry = null;
            for (let i = currentKeysInStorage.length - 1; i >= 0; i--) {
                const storedKey = currentKeysInStorage[i];
                if (!keysAttemptedInThisRun.has(storedKey)) { keyToTry = storedKey; break; }
            }
            if (!keyToTry && !newKeyFetchedInThisRun) {
                newKeyFetchedInThisRun = true;
                preparingMsg.textContent = 'Đang lấy key mới...';
                const autoKey = await fetchUserKeyAutomatically(true);
                if (autoKey && !keysAttemptedInThisRun.has(autoKey)) keyToTry = autoKey;
                if (!keyToTry) {
                    const promptedKey = await promptForUserKey("Hết key hoặc lấy key tự động thất bại. Vui lòng nhập Perchance userKey:");
                    if (promptedKey && !keysAttemptedInThisRun.has(promptedKey)) keyToTry = promptedKey;
                }
                currentKeysInStorage = getStoredUserKeys();
            }
            if (!keyToTry) {
                preparingMsg.remove();
                const errorMsg = document.createElement('p'); errorMsg.className = 'placeholder-error';
                errorMsg.textContent = 'Hết key hoặc không lấy được key mới.';
                imagePlaceholder.insertBefore(errorMsg, imagePlaceholder.firstChild);
                button.textContent = 'Hết key! Tạo lại'; button.dataset.action = 'generate'; break;
            }
            keysAttemptedInThisRun.add(keyToTry); button.textContent = `Tạo... (${keysAttemptedInThisRun.size})`;
            preparingMsg.textContent = `Đang thử key ...${keyToTry.slice(-6)}`;
            try {
                const finalPrompt = `${basePromptFromTags}, ${ANIME_STYLE_DEFINITION.positive}`;
                const finalNegativePrompt = `${ANIME_STYLE_DEFINITION.negative}${DEFAULT_NEGATIVE_PROMPT_BASE ? ', ' + DEFAULT_NEGATIVE_PROMPT_BASE : ''}`;
                const imageId = await generateImageApi(finalPrompt, finalNegativePrompt, keyToTry);
                const imageBlob = await downloadImageApi(imageId); const base64Image = await blobToBase64(imageBlob);
                await storeImage(detailedPromptKey, base64Image);

                preparingMsg.remove();

                const newImg = document.createElement('img');
                newImg.src = base64Image;
                newImg.alt = `${basePromptFromTags.substring(0,30)}... (mới)`; newImg.title = newImg.alt;
                newImg.style.border = "2px solid #4caf50";

                const firstChildIsImageOrHr = imagePlaceholder.firstChild && (imagePlaceholder.firstChild.tagName === 'IMG' || imagePlaceholder.firstChild.tagName === 'HR');
                if (firstChildIsImageOrHr) {
                    const spacer = document.createElement('hr');
                    imagePlaceholder.insertBefore(spacer, imagePlaceholder.firstChild);
                }
                imagePlaceholder.insertBefore(newImg, imagePlaceholder.firstChild);
                imagePlaceholder.dataset.currentImageIndex = "0"; // Reset to first image
                updateImageNavigation(imagePlaceholder);

                button.textContent = 'Tạo lại'; button.dataset.action = 'regenerate';
                addAndSaveUserKey(keyToTry); console.log(`Ảnh "${basePromptFromTags.substring(0,30)}..." tạo OK với key ...${keyToTry.slice(-6)}.`); break;
            } catch (error) {
                if (error.message === 'INVALID_KEY_API_ERROR') {
                    console.warn(`Key ...${keyToTry.slice(-6)} không hợp lệ. Xóa.`); removeAndSaveUserKey(keyToTry); currentKeysInStorage = getStoredUserKeys();
                    preparingMsg.textContent = `Key ...${keyToTry.slice(-6)} không hợp lệ. Thử key khác...`;
                } else {
                    console.error(`Lỗi tạo ảnh key ...${keyToTry.slice(-6)}:`, error);
                    preparingMsg.remove();
                    const errorMsg = document.createElement('p'); errorMsg.className = 'placeholder-error';
                    errorMsg.textContent = `Lỗi: ${error.message}.`;
                    imagePlaceholder.insertBefore(errorMsg, imagePlaceholder.firstChild);
                    button.textContent = 'Lỗi! Tạo lại'; button.dataset.action = 'generate'; break;
                }
            }
        }
        activeRequests.delete(detailedPromptKey); button.disabled = false;
        if (!imagePlaceholder.querySelector('img.active-gallery-image') && (button.textContent.startsWith('Tạo...') || button.textContent.startsWith('Hết key!'))) {
            if (!imagePlaceholder.querySelector('.placeholder-error') && !imagePlaceholder.querySelector('.placeholder-warning')) {
                 const warnMsg = document.createElement('p'); warnMsg.className = 'placeholder-warning';
                 warnMsg.textContent = 'Không thể hoàn tất yêu cầu.';
                 if (preparingMsg && preparingMsg.parentNode) preparingMsg.remove();
                 imagePlaceholder.insertBefore(warnMsg, imagePlaceholder.firstChild);
            }
            if (button.textContent.startsWith('Tạo...')) button.textContent = 'Thất bại! Tạo lại';
            button.dataset.action = 'generate';
        }
    }

    // --- Process Node ---
    function processNode(node, targetDocument) {
        if (node.nodeType === Node.TEXT_NODE) {
            let match; let lastIndex = 0; const textContent = node.nodeValue; const parent = node.parentNode;
            if (!parent || parent.closest('textarea, script, style, input, button, .perchance-prompt-container, [data-perchance-processed-parent="true"], #perchance-prompt-menu-panel, #subTagModal') || parent.isContentEditable) { return; }
            const fragment = targetDocument.createDocumentFragment(); let replaced = false; PROMPT_REGEX.lastIndex = 0;

            let detailedPromptsMap = GM_getValue(DETAILED_PROMPTS_STORAGE, "{}");
            try { detailedPromptsMap = JSON.parse(detailedPromptsMap); } catch(e) { detailedPromptsMap = {}; }


            while ((match = PROMPT_REGEX.exec(textContent)) !== null) {
                replaced = true; const rawFullPromptText = match[1].trim();
                if (match.index > lastIndex) { fragment.appendChild(targetDocument.createTextNode(textContent.substring(lastIndex, match.index))); }

                const promptWrapper = targetDocument.createElement('span');
                promptWrapper.className = 'perchance-prompt-container';
                promptWrapper.dataset.rawPrompt = rawFullPromptText;

                const mainTagsArea = targetDocument.createElement('div');
                mainTagsArea.className = 'perchance-main-tags-area';

                const tagsToProcess = detailedPromptsMap[rawFullPromptText] || rawFullPromptText.split(',').map(tag => tag.trim()).filter(tag => tag);

                tagsToProcess.forEach(tagValue => {
                    const mainTagWrapper = targetDocument.createElement('span');
                    mainTagWrapper.className = 'perchance-main-tag-wrapper';
                    mainTagWrapper.dataset.currentValue = tagValue;
                    const tagTextSpan = targetDocument.createElement('span');
                    tagTextSpan.className = 'perchance-main-tag-text';
                    tagTextSpan.textContent = tagValue;
                    mainTagWrapper.appendChild(tagTextSpan);
                    const editBtn = targetDocument.createElement('button');
                    editBtn.className = 'edit-subtags-btn'; editBtn.textContent = '✎'; editBtn.title = 'Thêm/Sửa chi tiết tag';
                    editBtn.onclick = () => showSubTagModal(mainTagWrapper);
                    mainTagWrapper.appendChild(editBtn); mainTagsArea.appendChild(mainTagWrapper);
                });
                promptWrapper.appendChild(mainTagsArea);
                const generateButton = targetDocument.createElement('button');
                generateButton.textContent = 'Tạo'; generateButton.className = 'perchance-generate-btn';
                generateButton.dataset.action = 'generate'; generateButton.addEventListener('click', handleGenerateClick);
                promptWrapper.appendChild(generateButton);
                const imagePlaceholder = targetDocument.createElement('div');
                imagePlaceholder.className = 'perchance-image-placeholder';
                promptWrapper.appendChild(imagePlaceholder);

                (async () => {
                    const currentDetailedTags = [];
                    mainTagsArea.querySelectorAll('.perchance-main-tag-wrapper').forEach(tw => currentDetailedTags.push(tw.dataset.currentValue));
                    const currentBasePrompt = currentDetailedTags.join(', ');
                    const currentPromptKey = CryptoJS.MD5(`${currentBasePrompt}, ${ANIME_STYLE_DEFINITION.positive}${ANIME_STYLE_DEFINITION.negative}`).toString(); // Ensure consistent hashing as in handleGenerateClick
                    const cachedImgSrc = await getCachedImage(currentPromptKey);
                    if (cachedImgSrc) {
                        const img = document.createElement('img');
                        img.src = cachedImgSrc;
                        img.alt = `${currentBasePrompt.substring(0,30)}... (cache)`;
                        img.title = img.alt;
                        img.style.border = "2px solid #00bcd4";
                        imagePlaceholder.appendChild(img);
                        updateImageNavigation(imagePlaceholder); // Update nav after adding cached image
                    } else {
                         updateImageNavigation(imagePlaceholder); // Still call to ensure nav controls are removed if no images
                    }
                })();


                fragment.appendChild(promptWrapper); lastIndex = PROMPT_REGEX.lastIndex;
            }
            if (replaced) {
                if (lastIndex < textContent.length) { fragment.appendChild(targetDocument.createTextNode(textContent.substring(lastIndex))); }
                parent.replaceChild(fragment, node);
                if (parent.nodeType === Node.ELEMENT_NODE) { parent.dataset.perchanceProcessedParent = 'true'; }
            }
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'IFRAME', 'CANVAS', 'INPUT', 'BUTTON', 'A'].includes(node.tagName.toUpperCase()) ||
                node.isContentEditable || node.closest('.perchance-prompt-container, #perchance-prompt-menu-panel, #subTagModal') || node.dataset.perchanceProcessed === 'true') { return; }
            Array.from(node.childNodes).forEach(child => processNode(child, targetDocument));
            node.dataset.perchanceProcessed = 'true';
        }
    }

    // --- Setup Observer ---
    function setupObserver(targetDoc) {
        if (mutationObserver) mutationObserver.disconnect();
        mutationObserver = new MutationObserver(async (mutationsList) => {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    mutation.addedNodes.forEach(newNode => {
                        if ((newNode.nodeType === Node.ELEMENT_NODE && (newNode.dataset.perchanceProcessed === 'true' || newNode.id === 'perchance-prompt-menu-panel' || newNode.id === 'subTagModal')) ||
                            (newNode.nodeType === Node.TEXT_NODE && newNode.parentNode && newNode.parentNode.dataset.perchanceProcessedParent === 'true')) return;
                        processNode(newNode, targetDoc);
                    });
                } else if (mutation.type === 'characterData') {
                     if (mutation.target.parentNode && mutation.target.parentNode.dataset.perchanceProcessedParent !== 'true' && !mutation.target.parentNode.closest('#perchance-prompt-menu-panel, #subTagModal')) {
                        processNode(mutation.target.parentNode, targetDoc);
                     }
                }
            }
        });
        mutationObserver.observe(targetDoc.body, { childList: true, subtree: true, characterData: true });
        console.log("MutationObserver đã thiết lập cho:", targetDoc.location.href || "document chính");
    }

    // --- Prompt Style Preset Management ---
    function getPromptPresets() {
        const presetsJson = GM_getValue(PROMPT_PRESETS_STORAGE, JSON.stringify({ "Mặc định": ANIME_STYLE_DEFINITION }));
        try { return JSON.parse(presetsJson); }
        catch (e) { console.error("Lỗi JSON PromptPresets:", e); return { "Mặc định": ANIME_STYLE_DEFINITION }; }
    }
    function savePromptPresets(presets) { GM_setValue(PROMPT_PRESETS_STORAGE, JSON.stringify(presets)); }
    function populatePresetDropdown(selectElement, presets) {
        selectElement.innerHTML = '';
        for (const presetName in presets) {
            const option = document.createElement('option'); option.value = presetName; option.textContent = presetName; selectElement.appendChild(option);
        }
    }
    function loadPresetToTextareas(presetName, presets, positiveArea, negativeArea) {
        if (presets[presetName]) { positiveArea.value = presets[presetName].positive; negativeArea.value = presets[presetName].negative; }
    }
    function applyPresetToScript(presetName, presets) {
        if (presets[presetName]) {
            ANIME_STYLE_DEFINITION.positive = presets[presetName].positive; ANIME_STYLE_DEFINITION.negative = presets[presetName].negative;
            alert(`Đã áp dụng style prompt: "${presetName}"`); console.log(`Đã áp dụng style: "${presetName}"`, ANIME_STYLE_DEFINITION);
        }
    }

    // --- Data Export/Import ---
    async function exportData() {
        const statusDiv = document.getElementById('data-management-status');
        if (statusDiv) statusDiv.textContent = 'Đang thu thập dữ liệu để xuất...';

        try {
            const promptPresets = getPromptPresets();
            const keywordSubTagMap = getKeywordSubTagMap();
            const userKeys = getStoredUserKeys();
            let detailedPrompts = GM_getValue(DETAILED_PROMPTS_STORAGE, "{}");
            try { detailedPrompts = JSON.parse(detailedPrompts); } catch (e) { detailedPrompts = {}; }
            const currentAnimeStyle = ANIME_STYLE_DEFINITION; // Lấy style đang được áp dụng
            const cachedImages = await getAllCachedImages();

            const dataToExport = {
                version: "1.7.5", // Phiên bản script để dễ dàng quản lý sau này
                exportedAt: new Date().toISOString(),
                promptPresets,
                keywordSubTagMap,
                userKeys,
                detailedPrompts,
                currentAnimeStyle, // Lưu cả style hiện tại
                cachedImages
            };

            const jsonData = JSON.stringify(dataToExport, null, 2);
            const blob = new Blob([jsonData], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
            a.download = `perchance_image_replacer_backup_${timestamp}.json`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
            if (statusDiv) statusDiv.textContent = 'Xuất dữ liệu thành công! File đã được tải về.';
            console.log("Dữ liệu đã xuất:", dataToExport);
        } catch (error) {
            console.error("Lỗi khi xuất dữ liệu:", error);
            if (statusDiv) statusDiv.textContent = `Lỗi xuất dữ liệu: ${error.message}`;
            alert(`Lỗi khi xuất dữ liệu: ${error.message}`);
        }
    }

    async function importData(event) {
        const statusDiv = document.getElementById('data-management-status');
        const file = event.target.files[0];
        if (!file) {
            if (statusDiv) statusDiv.textContent = 'Không có file nào được chọn.';
            return;
        }
        if (statusDiv) statusDiv.textContent = `Đang đọc file ${file.name}...`;

        const reader = new FileReader();
        reader.onload = async (e) => {
            try {
                const jsonData = e.target.result;
                const importedData = JSON.parse(jsonData);

                if (!importedData || typeof importedData !== 'object') {
                    throw new Error("Định dạng file không hợp lệ.");
                }

                if (!confirm("Bạn có chắc chắn muốn nhập dữ liệu này? Dữ liệu hiện tại (bao gồm cài đặt, prompts, ảnh cache) sẽ bị ghi đè.")) {
                    if (statusDiv) statusDiv.textContent = 'Đã hủy thao tác nhập.';
                    event.target.value = null;
                    return;
                }

                if (statusDiv) statusDiv.textContent = 'Đang nhập dữ liệu... Vui lòng đợi.';

                if (importedData.promptPresets) {
                    savePromptPresets(importedData.promptPresets);
                    console.log("Prompt Presets đã được nhập.");
                }
                if (importedData.keywordSubTagMap) {
                    saveKeywordSubTagMap(importedData.keywordSubTagMap);
                    console.log("Keyword-SubTag Map đã được nhập.");
                }
                if (importedData.userKeys && Array.isArray(importedData.userKeys)) {
                    saveUserKeys(importedData.userKeys);
                    console.log("User Keys đã được nhập.");
                }
                if (importedData.detailedPrompts) {
                    GM_setValue(DETAILED_PROMPTS_STORAGE, JSON.stringify(importedData.detailedPrompts));
                    console.log("Detailed Prompts đã được nhập.");
                }
                if (importedData.currentAnimeStyle && importedData.currentAnimeStyle.positive && importedData.currentAnimeStyle.negative) {
                    ANIME_STYLE_DEFINITION.positive = importedData.currentAnimeStyle.positive;
                    ANIME_STYLE_DEFINITION.negative = importedData.currentAnimeStyle.negative;
                    console.log("Current Anime Style đã được nhập và áp dụng.");
                }
                if (importedData.cachedImages && Array.isArray(importedData.cachedImages)) {
                    await clearAndStoreImages(importedData.cachedImages);
                    console.log(`${importedData.cachedImages.length} ảnh cache đã được nhập.`);
                }

                if (statusDiv) statusDiv.textContent = 'Nhập dữ liệu thành công! Vui lòng làm mới các panel hoặc trang nếu cần.';
                alert("Nhập dữ liệu thành công! Một số thay đổi có thể cần làm mới trang hoặc mở lại panel cài đặt để hiển thị.");

                const panel = document.getElementById('perchance-prompt-menu-panel');
                if (panel && panel.style.display !== 'none') {
                    const presetSelect = panel.querySelector('#prompt-preset-select-id');
                    const positiveArea = panel.querySelector('#positive-prompt-area-id');
                    const negativeArea = panel.querySelector('#negative-prompt-area-id');
                    const currentPresets = getPromptPresets();
                    populatePresetDropdown(presetSelect, currentPresets);
                    if (presetSelect.options.length > 0) {
                        loadPresetToTextareas(presetSelect.value, currentPresets, positiveArea, negativeArea);
                    } else if (importedData.currentAnimeStyle) {
                        positiveArea.value = ANIME_STYLE_DEFINITION.positive;
                        negativeArea.value = ANIME_STYLE_DEFINITION.negative;
                    }
                    const keywordSelector = panel.querySelector('#keywordSelector');
                    const keywordNameInput = panel.querySelector('#keywordNameInput');
                    const associatedSubTagsTextarea = panel.querySelector('#associatedSubTagsTextarea');
                    populateKeywordSelector(keywordSelector, getKeywordSubTagMap(), keywordNameInput, associatedSubTagsTextarea);
                }
            } catch (error) {
                console.error("Lỗi khi nhập dữ liệu:", error);
                if (statusDiv) statusDiv.textContent = `Lỗi nhập dữ liệu: ${error.message}`;
                alert(`Lỗi khi nhập dữ liệu: ${error.message}`);
            } finally {
                event.target.value = null;
            }
        };
        reader.onerror = () => {
            if (statusDiv) statusDiv.textContent = `Lỗi đọc file: ${reader.error}`;
            alert(`Lỗi đọc file: ${reader.error}`);
            event.target.value = null;
        };
        reader.readAsText(file);
    }


    // --- Create Prompt Edit Menu Panel ---
    function createPromptEditMenuPanel() {
        const panel = document.createElement('div'); panel.id = 'perchance-prompt-menu-panel';
        // Styles are now primarily handled by addGlobalStyles

        let htmlContent = `
            <h2 style="margin-top:0; border-bottom: 1px solid #555; padding-bottom:10px;">Chỉnh sửa Prompt Styles & Quản lý (Perchance)</h2>

            <div style="margin-bottom: 15px;">
                <label for="prompt-preset-select-id" style="display:block; margin-bottom:5px;">Cài đặt Style trước (Preset):</label>
                <div class="button-group"> <select id="prompt-preset-select-id" style="flex-grow:1; margin-right:5px;"></select>
                    <button id="delete-prompt-preset-btn" style="background-color: #e74c3c; color: white; border: none; border-radius: 4px; cursor: pointer;">Xóa Style</button>
                </div>
            </div>

            <div style="margin-bottom: 15px;">
                <label for="positive-prompt-area-id" style="display:block; margin-bottom:5px;">Prompt Tích cực (Positive):</label>
                <textarea id="positive-prompt-area-id" style="height: 100px;"></textarea>
            </div>

            <div style="margin-bottom: 20px;">
                <label for="negative-prompt-area-id" style="display:block; margin-bottom:5px;">Prompt Tiêu cực (Negative):</label>
                <textarea id="negative-prompt-area-id" style="height: 100px;"></textarea>
            </div>

            <div style="margin-bottom: 15px;" class="button-group"> <input type="text" id="new-preset-name-id" placeholder="Tên Style cài đặt mới" style="flex-grow:1; margin-right:5px;">
                <button id="save-prompt-preset-btn" style="background-color: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">Lưu Style</button>
            </div>

            <div class="button-group"> <button id="apply-prompts-btn" style="background-color: #2ecc71; color: white; border: none; border-radius: 4px; cursor: pointer;">Áp dụng Style vào Script</button>
                <button id="close-prompt-menu-btn" style="background-color: #7f8c8d; color: white; border: none; border-radius: 4px; cursor: pointer;">Đóng</button>
            </div>`;

        htmlContent += `
            <div class="keyword-subtag-manager">
                <h4>Quản lý Tag Phụ Gợi ý theo Từ Khóa</h4>
                <div class="keyword-list-area">
                    <label for="keywordSelector">Chọn Từ khóa Hiện có (để sửa nhóm):</label>
                    <select id="keywordSelector"></select>
                </div>
                <div class="keyword-edit-area">
                    <label for="keywordNameInput">Tên Từ khóa (nhập một hoặc nhiều từ khóa cách nhau bởi dấu phẩy, ví dụ: girl, co gai, nu):</label>
                    <input type="text" id="keywordNameInput" placeholder="girl, co gai, nu...">
                    <label for="associatedSubTagsTextarea">Các Tag Phụ Gợi ý (cách nhau bởi dấu phẩy):</label>
                    <textarea id="associatedSubTagsTextarea" rows="3" placeholder="Ví dụ: blue eyes, long hair, smiling"></textarea>
                </div>
                <div class="keyword-actions button-group">
                    <button id="saveKeywordMappingBtn">Lưu Từ khóa & Tag phụ Gợi ý</button>
                    <button id="deleteKeywordMappingBtn" class="delete">Xóa Từ khóa đang chọn (từ dropdown)</button>
                </div>
            </div>`;

        htmlContent += `
            <div class="data-management-section">
                <h4>Quản lý Dữ liệu Script</h4>
                <p style="font-size:0.85em; color:#bbb; margin-top:-5px; margin-bottom:10px;">Xuất tất cả cài đặt, prompt styles, từ khóa, tag phụ, user keys và ảnh đã cache ra file JSON. Nhập từ file JSON để khôi phục.</p>
                <div class="button-group">
                    <button id="export-data-btn" style="background-color: #5cb85c; color: white;">Xuất Dữ Liệu</button>
                    <label for="import-data-file" class="file-input-label" style="background-color: #f0ad4e; color:white;">Chọn File để Nhập</label>
                    <input type="file" id="import-data-file" accept=".json">
                </div>
                <div id="data-management-status"></div>
            </div>`;

        panel.innerHTML = htmlContent; document.body.appendChild(panel);

        const presetSelect = panel.querySelector('#prompt-preset-select-id'); const positiveArea = panel.querySelector('#positive-prompt-area-id'); const negativeArea = panel.querySelector('#negative-prompt-area-id'); const newPresetNameInput = panel.querySelector('#new-preset-name-id'); const saveBtn = panel.querySelector('#save-prompt-preset-btn'); const deleteBtn = panel.querySelector('#delete-prompt-preset-btn'); const applyBtn = panel.querySelector('#apply-prompts-btn'); const closeBtn = panel.querySelector('#close-prompt-menu-btn');
        let currentPresets = getPromptPresets(); populatePresetDropdown(presetSelect, currentPresets);
        if (presetSelect.options.length > 0) { loadPresetToTextareas(presetSelect.value, currentPresets, positiveArea, negativeArea); } else { positiveArea.value = ANIME_STYLE_DEFINITION.positive; negativeArea.value = ANIME_STYLE_DEFINITION.negative; }
        presetSelect.addEventListener('change', () => loadPresetToTextareas(presetSelect.value, currentPresets, positiveArea, negativeArea));
        saveBtn.addEventListener('click', () => {
            const presetNameToSave = newPresetNameInput.value.trim() || presetSelect.value; if (!presetNameToSave) { alert("Nhập tên style mới hoặc chọn style có sẵn."); return; }
            currentPresets[presetNameToSave] = { positive: positiveArea.value, negative: negativeArea.value };
            savePromptPresets(currentPresets); populatePresetDropdown(presetSelect, currentPresets); presetSelect.value = presetNameToSave; newPresetNameInput.value = ''; alert(`Đã lưu style: "${presetNameToSave}"`);
        });
        deleteBtn.addEventListener('click', () => {
            const selectedPreset = presetSelect.value; if (!selectedPreset) { alert("Chọn style để xóa."); return; } if (selectedPreset === "Mặc định") { alert("Không thể xóa style 'Mặc định'."); return; }
            if (confirm(`Xóa preset style "${selectedPreset}"?`)) {
                delete currentPresets[selectedPreset]; savePromptPresets(currentPresets); populatePresetDropdown(presetSelect, currentPresets);
                if (presetSelect.options.length > 0) { loadPresetToTextareas(presetSelect.value, currentPresets, positiveArea, negativeArea); } else { positiveArea.value = ''; negativeArea.value = ''; }
                alert(`Đã xóa style: "${selectedPreset}"`);
            }
        });
        applyBtn.addEventListener('click', () => {
            const selectedPreset = presetSelect.value;
            if (selectedPreset && currentPresets[selectedPreset]) { applyPresetToScript(selectedPreset, currentPresets); }
            else { ANIME_STYLE_DEFINITION.positive = positiveArea.value; ANIME_STYLE_DEFINITION.negative = negativeArea.value; alert("Đã áp dụng style từ textareas."); console.log("Áp dụng style từ textareas:", ANIME_STYLE_DEFINITION); }
        });
        closeBtn.addEventListener('click', () => { panel.style.display = 'none'; });

        const keywordSelector = panel.querySelector('#keywordSelector');
        const keywordNameInput = panel.querySelector('#keywordNameInput');
        const associatedSubTagsTextarea = panel.querySelector('#associatedSubTagsTextarea');
        const saveKeywordMappingBtn = panel.querySelector('#saveKeywordMappingBtn');
        const deleteKeywordMappingBtn = panel.querySelector('#deleteKeywordMappingBtn');

        function populateKeywordSelectorAndUpdate(currentMap = getKeywordSubTagMap()) {
            keywordSelector.innerHTML = '<option value="">--- Chọn Từ khóa (để sửa nhóm) ---</option>';
            Object.keys(currentMap).sort().forEach(kw => {
                const option = document.createElement('option'); option.value = kw; option.textContent = kw;
                keywordSelector.appendChild(option);
            });
        }
        populateKeywordSelectorAndUpdate();

        function loadKeywordDetails(selectedKeyword) {
            let currentKeywordMap = getKeywordSubTagMap();
            if (selectedKeyword && currentKeywordMap[selectedKeyword]) {
                const subTagsOfSelected = currentKeywordMap[selectedKeyword];
                let groupKeywords = [selectedKeyword];
                for (const otherKeyword in currentKeywordMap) {
                    if (otherKeyword !== selectedKeyword) {
                        if (JSON.stringify(currentKeywordMap[otherKeyword]) === JSON.stringify(subTagsOfSelected)) {
                            groupKeywords.push(otherKeyword);
                        }
                    }
                }
                keywordNameInput.value = groupKeywords.sort().join(', ');
                associatedSubTagsTextarea.value = subTagsOfSelected.join(', ');
                originalLoadedKeywordGroupState = { keywords: [...groupKeywords], subTags: [...subTagsOfSelected] };
            } else {
                keywordNameInput.value = selectedKeyword || '';
                associatedSubTagsTextarea.value = '';
                originalLoadedKeywordGroupState = { keywords: selectedKeyword ? [selectedKeyword] : [], subTags: [] };
            }
        }

        keywordSelector.addEventListener('change', () => {
            const selectedKeyword = keywordSelector.value;
            loadKeywordDetails(selectedKeyword);
        });

        saveKeywordMappingBtn.addEventListener('click', () => {
            const keywordInputString = keywordNameInput.value.trim().toLowerCase();
            if (!keywordInputString) { alert("Tên Từ khóa không được để trống."); return; }
            const newlyInputKeywords = keywordInputString.split(',').map(kw => kw.trim()).filter(kw => kw);
            if (newlyInputKeywords.length === 0) { alert("Tên Từ khóa không hợp lệ sau khi xử lý."); return; }
            const subTagsString = associatedSubTagsTextarea.value.trim();
            const newSubTagsArray = subTagsString ? subTagsString.split(',').map(s => s.trim()).filter(s => s) : [];
            let currentKeywordMap = getKeywordSubTagMap();
            newlyInputKeywords.forEach(kw => { currentKeywordMap[kw] = newSubTagsArray; });
            saveKeywordSubTagMap(currentKeywordMap);
            populateKeywordSelectorAndUpdate(currentKeywordMap);
            keywordNameInput.value = '';
            associatedSubTagsTextarea.value = '';
            originalLoadedKeywordGroupState = { keywords: [], subTags: [] };
            alert(`Đã lưu cài đặt gợi ý cho từ khóa: ${newlyInputKeywords.join(', ')}.`);
        });

        deleteKeywordMappingBtn.addEventListener('click', () => {
            const keywordToDeleteFromDropdown = keywordSelector.value;
            let currentKeywordMap = getKeywordSubTagMap();
            if (!keywordToDeleteFromDropdown || !currentKeywordMap[keywordToDeleteFromDropdown]) {
                alert("Vui lòng chọn một từ khóa hợp lệ từ danh sách để xóa."); return;
            }
            if (confirm(`Bạn có chắc chắn muốn xóa từ khóa "${keywordToDeleteFromDropdown}" và các tag phụ gợi ý liên quan không?`)) {
                delete currentKeywordMap[keywordToDeleteFromDropdown];
                saveKeywordSubTagMap(currentKeywordMap);
                populateKeywordSelectorAndUpdate(currentKeywordMap);
                keywordNameInput.value = '';
                associatedSubTagsTextarea.value = '';
                originalLoadedKeywordGroupState = { keywords: [], subTags: [] };
                alert(`Đã xóa từ khóa "${keywordToDeleteFromDropdown}".`);
            }
        });

        const exportBtn = panel.querySelector('#export-data-btn');
        const importFileInput = panel.querySelector('#import-data-file');
        exportBtn.addEventListener('click', exportData);
        importFileInput.addEventListener('change', importData);

        return panel;
    }

    function populateKeywordSelector(selectorElement, currentMap, nameInputElement, textareaElement) {
        selectorElement.innerHTML = '<option value="">--- Chọn Từ khóa (để sửa nhóm) ---</option>';
        Object.keys(currentMap).sort().forEach(kw => {
            const option = document.createElement('option'); option.value = kw; option.textContent = kw;
            selectorElement.appendChild(option);
        });
        if (nameInputElement) nameInputElement.value = '';
        if (textareaElement) textareaElement.value = '';
        originalLoadedKeywordGroupState = { keywords: [], subTags: [] };
    }


    // --- Integrate Prompt Menu Into SillyTavern ---
    function integratePromptMenuIntoSillyTavern() {
        const targetElement = document.querySelector('#option_toggle_AN');
        if (targetElement && targetElement.parentNode && !document.getElementById('perchance_prompt_menu_toggle')) {
            if (sillyTavernMenuIntegrationInterval) { clearInterval(sillyTavernMenuIntegrationInterval); sillyTavernMenuIntegrationInterval = null; }
            const newMenuItem = document.createElement('a'); newMenuItem.id = 'perchance_prompt_menu_toggle';
            const icon = document.createElement('i'); icon.className = 'fa-lg fa-solid fa-pen-to-square'; newMenuItem.appendChild(icon);
            const span = document.createElement('span'); span.textContent = ' Perchance Styles & Tags'; newMenuItem.appendChild(span);
            Object.assign(newMenuItem.style, { display: 'block', padding: '10px 15px', cursor: 'pointer', textDecoration: 'none', color: 'var(--text_color)' });
            newMenuItem.onmouseover = function() { this.style.backgroundColor = 'var(--hover_color)'; }
            newMenuItem.onmouseout = function() { this.style.backgroundColor = 'transparent'; }
            targetElement.parentNode.insertBefore(newMenuItem, targetElement.nextSibling);
            console.log("Đã thêm 'Perchance Styles & Tags' vào menu SillyTavern.");
            let promptMenuPanel = document.getElementById('perchance-prompt-menu-panel');
            if (!promptMenuPanel) { promptMenuPanel = createPromptEditMenuPanel(); }

            newMenuItem.addEventListener('click', (event) => {
                event.preventDefault();
                promptMenuPanel.style.display = promptMenuPanel.style.display === 'none' ? 'block' : 'none';
                if (promptMenuPanel.style.display === 'block') {
                    const presetSelect = promptMenuPanel.querySelector('#prompt-preset-select-id');
                    const positiveArea = promptMenuPanel.querySelector('#positive-prompt-area-id');
                    const negativeArea = promptMenuPanel.querySelector('#negative-prompt-area-id');
                    const currentPresets = getPromptPresets();
                    populatePresetDropdown(presetSelect, currentPresets);
                    if (presetSelect.value && currentPresets[presetSelect.value]) {
                        loadPresetToTextareas(presetSelect.value, currentPresets, positiveArea, negativeArea);
                    } else if (currentPresets["Mặc định"]) {
                        presetSelect.value = "Mặc định";
                        loadPresetToTextareas("Mặc định", currentPresets, positiveArea, negativeArea);
                    } else {
                        positiveArea.value = ANIME_STYLE_DEFINITION.positive;
                        negativeArea.value = ANIME_STYLE_DEFINITION.negative;
                    }

                    const keywordSelectorElement = promptMenuPanel.querySelector('#keywordSelector');
                    const keywordNameInputElement = promptMenuPanel.querySelector('#keywordNameInput');
                    const associatedSubTagsTextareaElement = promptMenuPanel.querySelector('#associatedSubTagsTextarea');
                    populateKeywordSelector(keywordSelectorElement, getKeywordSubTagMap(), keywordNameInputElement, associatedSubTagsTextareaElement);
                    const statusDiv = document.getElementById('data-management-status');
                    if(statusDiv) statusDiv.textContent = '';
                }
            });
        } else if (!sillyTavernMenuIntegrationInterval && !document.getElementById('perchance_prompt_menu_toggle')) {
            // console.log("Chưa tìm thấy #option_toggle_AN của SillyTavern...");
        }
    }

    // --- Main Function ---
    async function main() {
        console.log("Perchance Image Replacer (VN) - Multi-Key & Contextual Sub-Tags v1.7.5 - Xuất/Nhập Dữ Liệu & UI Di động");
        addGlobalStyles(); createSubTagModal();
        try { await initDB(); } catch (error) { /* Logged */ }
        const initialPresets = getPromptPresets();
        if (initialPresets["Mặc định"]) { ANIME_STYLE_DEFINITION.positive = initialPresets["Mặc định"].positive; ANIME_STYLE_DEFINITION.negative = initialPresets["Mặc định"].negative; console.log("Đã tải prompt style 'Mặc định'."); }
        else { initialPresets["Mặc định"] = { positive: ANIME_STYLE_DEFINITION.positive, negative: ANIME_STYLE_DEFINITION.negative }; savePromptPresets(initialPresets); console.log("Đã lưu prompt style mặc định."); }
        const initialKeys = getStoredUserKeys();
        if (initialKeys.length > 0) { console.log(`Tìm thấy ${initialKeys.length} userKey. Key gần nhất: ...${initialKeys[initialKeys.length - 1].slice(-6)}`); }
        else { console.log("Không có userKey. Sẽ thử lấy tự động/hỏi khi cần."); }
        createPromptEditMenuPanel(); // Panel is created but hidden by default
        integratePromptMenuIntoSillyTavern(); // For SillyTavern integration
        sillyTavernMenuIntegrationInterval = setInterval(integratePromptMenuIntoSillyTavern, 3000); // Periodically check for SillyTavern menu

        const outputIframe = document.querySelector('iframe#outputIframeEl');
        if (outputIframe) {
            const handleIframeLoad = () => {
                if (outputIframe.contentDocument && outputIframe.contentDocument.body) {
                    try { console.log("iframe#outputIframeEl tải xong. Xử lý node..."); processNode(outputIframe.contentDocument.body, outputIframe.contentDocument); setupObserver(outputIframe.contentDocument); }
                    catch(e) { console.error("Lỗi iframe:", e, ". Fallback."); processNode(document.body, document); setupObserver(document); }
                } else { console.error("Không truy cập được contentDocument.body iframe. Fallback."); processNode(document.body, document); setupObserver(document); }
            };
            if (outputIframe.contentDocument && outputIframe.contentDocument.readyState === 'complete') { handleIframeLoad(); }
            else { console.log("Chờ iframe#outputIframeEl tải..."); outputIframe.addEventListener('load', handleIframeLoad, { once: true }); }
        } else {
            console.warn("Không tìm thấy iframe#outputIframeEl. Xử lý trên document.body.");
            processNode(document.body, document); setupObserver(document);
        }
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(main, 500); }
    else { window.addEventListener('load', () => setTimeout(main, 500)); }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址