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

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ụ (tích hợp ST gốc). Giao diện nền tối. Panel dọc ở dưới, tag chính thủ công.

目前為 2025-05-10 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Perchance Image Replacer (Vietnamese Version) - Multi-Key & Contextual Sub-Tags v1.7.4 (User Mod)
// @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ụ (tích hợp ST gốc). Giao diện nền tối. Panel dọc ở dưới, tag chính thủ cô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;
    let db;
    let activeRequests = new Set();
    let mutationObserver = null;
    let sillyTavernMenuIntegrationInterval = null; // Restored for original ST integration logic

    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; }
            #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); 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; box-sizing: border-box; } /* Added box-sizing */
            #perchance-prompt-menu-panel h2 { font-size: 1.5em; margin-bottom:15px; } /* Adjusted heading */
            #perchance-prompt-menu-panel label { font-size: 0.9em; } /* Adjusted label */
            #perchance-prompt-menu-panel select, #perchance-prompt-menu-panel textarea, #perchance-prompt-menu-panel input[type="text"] {
                padding: 8px; font-size: 0.9em; /* Adjusted padding & font */
            }
            #perchance-prompt-menu-panel button { padding: 8px 12px; font-size: 0.9em; } /* Adjusted button */
            #perchance-prompt-menu-panel textarea { height: 60px; } /* Reduced textarea height for compact panel */
        `;
        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);
                            count++;
                            if (count === imagesArray.length) resolve();
                        };
                    });
                } else {
                    resolve();
                }
            };
            clearRequest.onerror = (event) => { console.error("Lỗi xóa ảnh cache cũ:", event.target.error); reject(event.target.error); };
        });
    }

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

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

    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.'))
            });
        });
    }

    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) {
        // User wants to manually control "((()))". This function will no longer add it automatically.
        return mainTag;
    }

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

    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: currentMainTagTextFromDataset } = parseDetailedTag(currentEditingTagElement.dataset.currentValue);
            const selectedSubTags = [];
            subTagModal.querySelectorAll('#subTagListContainer input[type="checkbox"]:checked').forEach(cb => {
                selectedSubTags.push(cb.value);
            });

            const mainTagPartOnly = currentMainTagTextFromDataset.replace(/\s*\(\(\(([^)]*)\)\)\)\s*$/,'').trim();
            let newDetailedValue;
            if (selectedSubTags.length > 0) {
                 newDetailedValue = `${mainTagPartOnly}(((${selectedSubTags.join(', ')})))`;
            } else {
                 newDetailedValue = mainTagPartOnly;
            }

            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 = {} }
                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);
        subTagModal.querySelector('#editingMainTagText').textContent = mainTagOnly;
        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;
        populateSubTagList(mainTagWrapperElement);
        subTagModal.style.display = 'block';
    }

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

    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 = '';
                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);
                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";
                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';
        }
    }

    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';
                let tagsToProcess;
                if (detailedPromptsMap[rawFullPromptText]) {
                    tagsToProcess = detailedPromptsMap[rawFullPromptText];
                } else {
                    const individualRawTags = []; let currentTagBuffer = ""; let inSubTagBlock = 0;
                    for (let i = 0; i < rawFullPromptText.length; i++) {
                        const char = rawFullPromptText[i]; currentTagBuffer += char;
                        if (char === '(' && rawFullPromptText.substring(i, i+3) === "(((") { inSubTagBlock++; i += 2; currentTagBuffer += "(("; }
                        else if (char === ')' && rawFullPromptText.substring(i, i+3) === ")))") { inSubTagBlock = Math.max(0, inSubTagBlock - 1); i += 2; currentTagBuffer += "))"; }
                        else if (char === ',' && inSubTagBlock === 0) { individualRawTags.push(currentTagBuffer.slice(0, -1).trim()); currentTagBuffer = ""; }
                    }
                    if (currentTagBuffer.trim()) { individualRawTags.push(currentTagBuffer.trim()); }
                    tagsToProcess = individualRawTags.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();
                    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);
                })();
                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';
        }
    }

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

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

    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;
            const cachedImages = await getAllCachedImages();
            const dataToExport = {
                version: "1.7.4_user_mod_st_menu_vertical_panel",
                exportedAt: new Date().toISOString(),
                promptPresets, keywordSubTagMap, userKeys, detailedPrompts, currentAnimeStyle, 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, prompt, ả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);
    }

    function createPromptEditMenuPanel() {
        const panel = document.createElement('div'); panel.id = 'perchance-prompt-menu-panel';
        // MODIFIED PANEL STYLE FOR VERTICAL RECTANGLE AT BOTTOM
        Object.assign(panel.style, {
            position: 'fixed',
            bottom: '10px', // Slightly off the bottom edge
            left: '50%',
            transform: 'translateX(-50%)',
            backgroundColor: '#2c2c2c',
            color: 'white',
            padding: '15px', // Adjusted padding
            border: '1px solid #555',
            borderRadius: '8px',
            zIndex: '10001',
            display: 'none',
            width: '360px',                 // Narrower width for vertical rectangle "phone-like"
            maxWidth: 'calc(100vw - 20px)', // Ensure it fits on very narrow screens (viewport width minus 20px for margins)
            height: 'auto',                 // Auto height based on content
            maxHeight: '75vh',              // Tall, like a phone screen (75% of viewport height)
            overflowY: 'auto',
            boxShadow: '0 0 15px rgba(0,0,0,0.5)', // Adjusted shadow for better visibility
            boxSizing: 'border-box'         // Ensure padding and border are included in width/height
        });

        let htmlContent = `
            <h2 style="margin-top:0; border-bottom: 1px solid #555; padding-bottom:10px;">Chỉnh sửa Prompt Styles & Quản lý</h2>
            <div style="margin-bottom: 10px;"><label for="prompt-preset-select-id" style="display:block; margin-bottom:5px;">Preset:</label><select id="prompt-preset-select-id" style="width: calc(100% - 105px); background-color: #333; color: white; border: 1px solid #555; border-radius: 4px; margin-right:5px;"></select><button id="delete-prompt-preset-btn" style="background-color: #e74c3c; color: white; border: none; border-radius: 4px; cursor: pointer;padding: 8px 10px;">Xóa</button></div>
            <div style="margin-bottom: 10px;"><label for="positive-prompt-area-id" style="display:block; margin-bottom:5px;">Positive:</label><textarea id="positive-prompt-area-id" style="width: 100%; background-color: #333; color: white; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;"></textarea></div>
            <div style="margin-bottom: 15px;"><label for="negative-prompt-area-id" style="display:block; margin-bottom:5px;">Negative:</label><textarea id="negative-prompt-area-id" style="width: 100%; background-color: #333; color: white; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;"></textarea></div>
            <div style="margin-bottom: 10px;"><input type="text" id="new-preset-name-id" placeholder="Tên Style mới" style="width: calc(100% - 100px); background-color: #333; color: white; border: 1px solid #555; border-radius: 4px; margin-right:5px; box-sizing: border-box;"><button id="save-prompt-preset-btn" style="background-color: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;padding: 8px 10px;">Lưu</button></div>
            <div style="margin-bottom:15px; display:flex; justify-content:space-between;"><button id="apply-prompts-btn" style="background-color: #2ecc71; color: white; border: none; border-radius: 4px; cursor: pointer;">Áp dụng 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>
            <div class="keyword-subtag-manager">
                <h4>Quản lý Tag Phụ Gợi ý</h4>
                <div class="keyword-list-area" style="margin-bottom:5px;">
                    <label for="keywordSelector" style="font-size:0.85em;">Chọn Từ khóa (sửa nhóm):</label>
                    <select id="keywordSelector"></select>
                </div>
                <div class="keyword-edit-area">
                    <label for="keywordNameInput" style="font-size:0.85em;">Tên Từ khóa (vd: girl, co gai):</label>
                    <input type="text" id="keywordNameInput" placeholder="girl, co gai, nu...">
                    <label for="associatedSubTagsTextarea" style="font-size:0.85em;">Tag Phụ Gợi ý (vd: blue eyes, long hair):</label>
                    <textarea id="associatedSubTagsTextarea" rows="2" placeholder="blue eyes, long hair"></textarea>
                </div>
                <div class="keyword-actions" style="display:flex; justify-content:space-between;">
                    <button id="saveKeywordMappingBtn">Lưu Từ khóa</button>
                    <button id="deleteKeywordMappingBtn" class="delete">Xóa Từ khóa</button>
                </div>
            </div>
            <div class="data-management-section">
                <h4>Quản lý Dữ Liệu Script</h4>
                <p style="font-size:0.8em; color:#bbb; margin-top:-5px; margin-bottom:8px;">Xuất/Nhập tất cả cài đặt, prompts, từ khóa, tag phụ, user keys và ảnh cache (JSON).</p>
                <div style="display:flex; justify-content:space-around;">
                     <button id="export-data-btn" style="background-color: #5cb85c; color: white;">Xuất Data</button>
                     <label for="import-data-file" class="file-input-label" style="background-color: #f0ad4e; color:white; margin-right:0;">Nhập Data</label>
                     <input type="file" id="import-data-file" accept=".json" style="display:none;">
                </div>
                <div id="data-management-status" style="margin-top:8px; font-size:0.85em; color:#ccc; min-height:1.2em;"></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 ---</option>'; // Shortened
            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', () => { loadKeywordDetails(keywordSelector.value); });
        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ệ."); 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 gợi ý cho: ${newlyInputKeywords.join(', ')}.`);
        });
        deleteKeywordMappingBtn.addEventListener('click', () => {
            const keywordToDeleteFromDropdown = keywordSelector.value;
            let currentKeywordMap = getKeywordSubTagMap();
            if (!keywordToDeleteFromDropdown || !currentKeywordMap[keywordToDeleteFromDropdown]) { alert("Chọn từ khóa từ danh sách để xóa."); return; }
            if (confirm(`Xóa từ khóa "${keywordToDeleteFromDropdown}" và gợi ý liên quan?`)) {
                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 ---</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: [] };
    }

    // Restored original SillyTavern menu integration logic
    function integratePromptMenuIntoSillyTavern() {
        const targetElement = document.querySelector('#option_toggle_AN'); // Standard ST target
        if (targetElement && targetElement.parentNode && !document.getElementById('perchance_prompt_menu_toggle')) {
            // The original script had this line, implying an interval might call this function.
            // We keep it for faithfulness to the original structure.
            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'; // Original icon
            newMenuItem.appendChild(icon);
            const span = document.createElement('span');
            span.textContent = ' Perchance Styles & Tags'; // Original text
            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');
            // Panel should already be created by main(), but this is a fallback.
            if (!promptMenuPanel) {
                promptMenuPanel = createPromptEditMenuPanel();
            }

            newMenuItem.addEventListener('click', (event) => {
                event.preventDefault();
                if (!promptMenuPanel) promptMenuPanel = document.getElementById('perchance-prompt-menu-panel'); // Ensure panel reference

                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')) {
            // This log indicates the target wasn't found AND no interval is active (or it's the first call of an intended interval)
            // console.log("Chưa tìm thấy #option_toggle_AN của SillyTavern để tích hợp menu.");
        }
    }

    async function main() {
        console.log("Perchance Image Replacer (VN) - User Mod - ST Menu, Vertical Panel");
        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 once here
        integratePromptMenuIntoSillyTavern(); // Attempt to integrate into SillyTavern menu once on load

        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或关注我们的公众号极客氢云获取最新地址