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

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 đặt, prompts, ảnh cache). Panel ở dưới, tag chính thủ công.

ของเมื่อวันที่ 10-05-2025 ดู เวอร์ชันล่าสุด

// ==UserScript==
// @name         Perchance Image Replacer (Vietnamese Version) - Multi-Key & Contextual Sub-Tags v1.7.4 (Modified)
// @namespace    http://tampermonkey.net/
// @version      1.7.5
// @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 đặt, prompts, ảnh cache). Panel ở 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;

    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: 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; }
            #perchance-prompt-menu-panel h2 { font-size: 1.6em; }
            #perchance-prompt-menu-panel label { font-size: 0.95em; }
            #perchance-prompt-menu-panel select, #perchance-prompt-menu-panel textarea, #perchance-prompt-menu-panel input[type="text"] {
                padding: 10px; font-size: 0.95em;
            }
            #perchance-prompt-menu-panel button { padding: 10px 15px; font-size: 0.9em; }
        `;
        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: [] };
    }

    // MODIFIED FUNCTION: User controls "((()))" manually
    function formatDetailedTag(mainTag, subTagsArray) {
        // User wants to manually control "((()))". This function will no longer add it automatically.
        // The subTagsArray might still be used for other logic by the script if needed,
        // but it won't be used for automatic visual formatting with "((()))" here.
        // If the user has manually typed "main tag(((sub, tags)))", parseDetailedTag will extract them.
        // When applying, we just return the mainTag as it is (which might already contain the user's "((()))").
        return mainTag;
    }


    // --- 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: currentMainTagText } = parseDetailedTag(currentEditingTagElement.dataset.currentValue); // Get current main tag text (might already have ((())) by user)
            const selectedSubTags = [];
            subTagModal.querySelectorAll('#subTagListContainer input[type="checkbox"]:checked').forEach(cb => {
                selectedSubTags.push(cb.value);
            });

            // Reconstruct the main tag with potentially new sub-tags, IF THE USER WANTS THIS BEHAVIOR.
            // For now, with the manual "((()))" change, we assume the user edits the mainTag text directly for "((()))"
            // So, we update the currentValue with the mainTagText (which they might have edited to include ((())) )
            // and the selectedSubTags are primarily for internal logic or if we decide to re-enable auto-formatting later.

            // If currentMainTagText already contains "((...))", we might want to replace its sub-tag part.
            // Or, if it doesn't, and selectedSubTags exist, we might want to add them.
            // For simplicity with the "manual ((()))" request, let's assume the user manages the "((()))" part entirely in the main tag text.
            // The modal's role is more to *select* which sub-tags are conceptually active.

            let newDetailedValue;
            const userTypedMainTag = currentMainTagText; // This is what was in the tag, potentially with user's ((()))

            // Option 1: If user wants the modal to *also* update the ((())) part based on checkboxes
            // This would override user's manual ((())) if they also check/uncheck in modal.
            // if (selectedSubTags.length > 0) {
            //     const mainTagPartOnly = userTypedMainTag.replace(/\(\(\(.*?\)\)\)\s*$/, '').trim(); // Remove existing ((())) if any
            //     newDetailedValue = `${mainTagPartOnly}(((${selectedSubTags.join(', ')})))`;
            // } else {
            //     newDetailedValue = userTypedMainTag.replace(/\(\(\(.*?\)\)\)\s*$/, '').trim(); // Remove existing ((())) if any
            // }

            // Option 2: The modal only *selects* sub-tags, but doesn't auto-format the main tag text with ((())).
            // The user is responsible for typing "main tag(((sub1, sub2)))" themselves if they want that format.
            // The `dataset.currentValue` should reflect what's visually shown.
            // The `parseDetailedTag` will still correctly parse it if the user *does* use the ((())) format.
            // For this option, we assume `currentMainTagText` is what the user wants to see.
            // The `selectedSubTags` are more for internal state or future use.
            // Let's stick to the idea that `formatDetailedTag` is now a no-op for `((()))`.
            // So, the `currentValue` should be the `mainTag` as it is, and `selectedSubTags` are just... selected.

            // The most straightforward interpretation of "manual ((()))" is that the text field IS the source of truth for ((()))
            // The checkboxes in the modal help manage the *list* of sub-tags, but don't force the ((())) format.
            // So, if a user types "tag A(((detail1)))", and then in modal unchecks detail1 and checks detail2,
            // what should happen?
            // A) "tag A" (modal clears ((())) because detail1 is gone)
            // B) "tag A(((detail2)))" (modal replaces ((())) content)
            // C) "tag A(((detail1)))" (modal does nothing to the text, only updates internal list of selectedSubTags)

            // Given the change to formatDetailedTag, let's make the modal update the text based on selected checkboxes.
            // This means if they uncheck all, the ((())) part will be removed. If they check some, it will be formed.
            // This seems like a reasonable middle ground. The formatDetailedTag change was to stop *automatic initial* addition.

            const mainTagPartOnly = currentMainTagText.replace(/\s*\(\(\(([^)]*)\)\)\)\s*$/,'').trim(); // Get the pure main tag, removing old sub-tags if any

            if (selectedSubTags.length > 0) {
                 newDetailedValue = `${mainTagPartOnly}(((${selectedSubTags.join(', ')})))`;
            } else {
                 newDetailedValue = mainTagPartOnly; // No sub-tags, so just the main part
            }


            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 = '';
        // When populating, we parse the current value to see what's already applied *structurally*
        const { mainTag: mainTagOnly, subTags: alreadyAppliedSubTags } = parseDetailedTag(mainTagElement.dataset.currentValue);

        subTagModal.querySelector('#editingMainTagText').textContent = mainTagOnly; // Show the clean main tag in modal title

        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); // This would strip ((())) for the title
        // subTagModal.querySelector('#editingMainTagText').textContent = mainTag;
        populateSubTagList(mainTagWrapperElement); // populateSubTagList now also sets the title
        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 = '';
                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';
        }
    }

    // --- 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';

                // If rawFullPromptText contains "((()))", parseDetailedTag will handle it.
                // Otherwise, it's just a list of comma-separated tags.
                // The detailedPromptsMap stores the *user-edited* versions, which might include "((()))".
                let tagsToProcess;
                if (detailedPromptsMap[rawFullPromptText]) {
                    tagsToProcess = detailedPromptsMap[rawFullPromptText]; // This is an array of strings
                } else {
                    // Initial processing: if the raw prompt itself has ((())), we need to parse it carefully
                    // Example: "tag1, tag2(((subA))), tag3"
                    // We can't just split by comma.
                    // For simplicity, assume initial tags from ###...### are comma separated main tags without ((()))
                    // If user wants initial ((())), they should edit it after it appears.
                    // Or, the regex for PROMPT_REGEX should be more complex to capture initial ((())).
                    // Current PROMPT_REGEX captures everything between ###...###.
                    // Let's assume rawFullPromptText is a string of tags, potentially with ((())) within it.
                    // We need to split it into individual "main tags" which might themselves contain ((())).

                    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; // Skip next two ((
                            currentTagBuffer += "((";
                        } else if (char === ')' && rawFullPromptText.substring(i, i+3) === ")))") {
                            inSubTagBlock = Math.max(0, inSubTagBlock - 1);
                            i += 2; // Skip next two ))
                            currentTagBuffer += "))";
                        } else if (char === ',' && inSubTagBlock === 0) {
                            individualRawTags.push(currentTagBuffer.slice(0, -1).trim()); // remove trailing comma
                            currentTagBuffer = "";
                        }
                    }
                    if (currentTagBuffer.trim()) {
                        individualRawTags.push(currentTagBuffer.trim());
                    }
                    tagsToProcess = individualRawTags.filter(tag => tag);
                }


                tagsToProcess.forEach(tagValue => { // tagValue here is a string, potentially like "mainTag" or "mainTag(((sub1, sub2)))"
                    const mainTagWrapper = targetDocument.createElement('span');
                    mainTagWrapper.className = 'perchance-main-tag-wrapper';
                    mainTagWrapper.dataset.currentValue = tagValue; // Store the full string

                    const tagTextSpan = targetDocument.createElement('span');
                    tagTextSpan.className = 'perchance-main-tag-text';
                    tagTextSpan.textContent = tagValue; // Display the full string
                    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';
        }
    }

    // --- 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.4_mod_bottom_panel_manual_tags", // 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ệ.");
                }

                // Xác nhận trước khi ghi đè
                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; // Reset file input
                    return;
                }

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

                // Nhập Prompt Presets
                if (importedData.promptPresets) {
                    savePromptPresets(importedData.promptPresets);
                    console.log("Prompt Presets đã được nhập.");
                }

                // Nhập Keyword-SubTag Map
                if (importedData.keywordSubTagMap) {
                    saveKeywordSubTagMap(importedData.keywordSubTagMap);
                    console.log("Keyword-SubTag Map đã được nhập.");
                }

                // Nhập User Keys
                if (importedData.userKeys && Array.isArray(importedData.userKeys)) {
                    saveUserKeys(importedData.userKeys); // Hàm này đã có sẵn logic quản lý
                    console.log("User Keys đã được nhập.");
                }

                // Nhập Detailed Prompts
                if (importedData.detailedPrompts) {
                    GM_setValue(DETAILED_PROMPTS_STORAGE, JSON.stringify(importedData.detailedPrompts));
                    console.log("Detailed Prompts đã được nhập.");
                }

                // Nhập và áp dụng Current Anime Style
                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.");
                    // Có thể thêm logic để tự động lưu nó vào preset "Mặc định" hoặc một preset mới nếu muốn
                }


                // Nhập Ảnh Cache (Xóa cũ, thêm mới)
                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ị.");

                // Làm mới các UI liên quan trong panel
                const panel = document.getElementById('perchance-prompt-menu-panel');
                if (panel && panel.style.display !== 'none') {
                    // Populate lại dropdown preset
                    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) { // Nếu không có preset, thử load style vừa nhập
                        positiveArea.value = ANIME_STYLE_DEFINITION.positive;
                        negativeArea.value = ANIME_STYLE_DEFINITION.negative;
                    }


                    // Populate lại keyword selector
                    const keywordSelector = panel.querySelector('#keywordSelector');
                    const keywordNameInput = panel.querySelector('#keywordNameInput');
                    const associatedSubTagsTextarea = panel.querySelector('#associatedSubTagsTextarea');
                    populateKeywordSelector(keywordSelector, getKeywordSubTagMap(), keywordNameInput, associatedSubTagsTextarea); // Truyền các element cần thiết
                }


            } 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; // Reset file input để có thể chọn lại cùng file
            }
        };
        reader.onerror = () => {
            if (statusDiv) statusDiv.textContent = `Lỗi đọc file: ${reader.error}`;
            alert(`Lỗi đọc file: ${reader.error}`);
            event.target.value = null; // Reset file input
        };
        reader.readAsText(file);
    }


    // --- Create Prompt Edit Menu Panel ---
    function createPromptEditMenuPanel() {
        const panel = document.createElement('div'); panel.id = 'perchance-prompt-menu-panel';
        // MODIFIED PANEL STYLE FOR BOTTOM PLACEMENT
        Object.assign(panel.style, {
            position: 'fixed',
            bottom: '0px', // Đặt ở dưới cùng
            left: '50%', // Căn giữa theo chiều ngang
            transform: 'translateX(-50%)', // Chỉ di chuyển theo trục X để căn giữa
            backgroundColor: '#2c2c2c',
            color: 'white',
            padding: '15px', // Giảm padding một chút cho gọn
            borderTop: '1px solid #555', // Chỉ giữ viền trên
            borderLeft: '1px solid #555',
            borderRight: '1px solid #555',
            borderBottom: 'none', // Không có viền dưới
            borderTopLeftRadius: '8px', // Bo tròn góc trên trái
            borderTopRightRadius: '8px', // Bo tròn góc trên phải
            borderBottomLeftRadius: '0px', // Góc dưới phẳng
            borderBottomRightRadius: '0px',
            zIndex: '10001',
            display: 'none',
            width: '95%', // Chiều rộng lớn hơn, phù hợp với mobile
            maxWidth: '700px', // Giới hạn chiều rộng tối đa trên desktop
            maxHeight: '60vh', // Giới hạn chiều cao, ví dụ 60% chiều cao màn hình
            overflowY: 'auto',
            boxShadow: '0 -5px 15px rgba(0,0,0,0.3)' // Đổ bóng lên trên
        });

        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><select id="prompt-preset-select-id" style="width: calc(100% - 125px); 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;">Xóa Style</button></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="width: 100%; height: 80px; background-color: #333; color: white; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;"></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="width: 100%; height: 80px; background-color: #333; color: white; border: 1px solid #555; border-radius: 4px; box-sizing: border-box;"></textarea></div>
            <div style="margin-bottom: 15px;"><input type="text" id="new-preset-name-id" placeholder="Tên Style cài đặt mới" style="width: calc(100% - 120px); 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;">Lưu Style</button></div>
            <div><button id="apply-prompts-btn" style="background-color: #2ecc71; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px;">Á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="2" placeholder="Ví dụ: blue eyes, long hair, smiling"></textarea>
                </div>
                <div class="keyword-actions">
                    <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>`;
        // Thêm phần quản lý dữ liệu (Xuất/Nhập)
        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>
                <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" style="display:none;">
                <div id="data-management-status" style="margin-top:10px; font-size:0.9em; color:#ccc;"></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()) { // Đổi tên và nhận currentMap
            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(); // Luôn lấy map mới nhất
            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); // Cập nhật dropdown
            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); // Cập nhật dropdown
                keywordNameInput.value = '';
                associatedSubTagsTextarea.value = '';
                originalLoadedKeywordGroupState = { keywords: [], subTags: [] };
                alert(`Đã xóa từ khóa "${keywordToDeleteFromDropdown}".`);
            }
        });

        // Gắn sự kiện cho nút Xuất/Nhập
        const exportBtn = panel.querySelector('#export-data-btn');
        const importFileInput = panel.querySelector('#import-data-file');
        exportBtn.addEventListener('click', exportData);
        importFileInput.addEventListener('change', importData);

        return panel;
    }
    // Cập nhật hàm populateKeywordSelector để nhận các element làm tham số
    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);
        });
        // Reset các trường input liên quan nếu có
        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'); // Example target, adjust if needed
        const menuButtonContainer = document.querySelector('#extensions_buttons_area') || document.body; // A more generic place if specific target not found

        if (document.getElementById('perchance_prompt_menu_toggle_button')) return; // Already added

        let promptMenuPanel = document.getElementById('perchance-prompt-menu-panel');
        if (!promptMenuPanel) { promptMenuPanel = createPromptEditMenuPanel(); }


        const toggleButton = document.createElement('button');
        toggleButton.id = 'perchance_prompt_menu_toggle_button';
        toggleButton.textContent = '⚙️ P.Styles';
        Object.assign(toggleButton.style, {
            position: 'fixed',
            bottom: '10px',
            right: '10px',
            zIndex: '10005', // Above the panel itself
            padding: '8px 12px',
            backgroundColor: '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '5px',
            cursor: 'pointer',
            boxShadow: '0 2px 5px rgba(0,0,0,0.2)'
        });
         toggleButton.addEventListener('click', (event) => {
            event.preventDefault();
            promptMenuPanel.style.display = promptMenuPanel.style.display === 'none' ? 'block' : 'none';
            if (promptMenuPanel.style.display === 'block') {
                // Khi mở panel, cập nhật các dropdown và text area
                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 { // Fallback to current ANIME_STYLE_DEFINITION if no presets
                    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);
                // Reset data management status
                const statusDiv = document.getElementById('data-management-status');
                if(statusDiv) statusDiv.textContent = '';
            }
        });

        // Add the button to a suitable place
        if (targetElement && targetElement.parentNode && targetElement.parentNode.id === 'extensions_menu_content') { // Specific SillyTavern menu
             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);
             newMenuItem.addEventListener('click', (event) => {
                event.preventDefault();
                toggleButton.click(); // Trigger the floating button's click
             });
             console.log("Đã thêm 'Perchance Styles & Tags' vào menu SillyTavern (sẽ mở panel nổi).");
             // Hide the floating button if integrated into ST menu
             toggleButton.style.display = 'none';
             document.body.appendChild(toggleButton); // Still add to body, just hidden
        } else {
            // Fallback: add floating button to body
            document.body.appendChild(toggleButton);
            console.log("Đã thêm nút nổi 'P.Styles' để mở panel cài đặt.");
        }


        // Attempt to integrate into SillyTavern menu if available, otherwise use floating button
        if (sillyTavernMenuIntegrationInterval) clearInterval(sillyTavernMenuIntegrationInterval);
        sillyTavernMenuIntegrationInterval = setInterval(() => {
            const stTargetElement = document.querySelector('#option_toggle_AN'); // SillyTavern specific
            const stMenuContent = document.querySelector('#extensions_menu_content'); // SillyTavern specific

            if (stTargetElement && stTargetElement.parentNode && stMenuContent && !document.getElementById('perchance_st_menu_item')) {
                clearInterval(sillyTavernMenuIntegrationInterval);
                sillyTavernMenuIntegrationInterval = null;

                const newMenuItem = document.createElement('a'); newMenuItem.id = 'perchance_st_menu_item';
                const icon = document.createElement('i'); icon.className = 'fa-lg fa-solid fa-image'; newMenuItem.appendChild(icon); // Changed icon
                const span = document.createElement('span'); span.textContent = ' Perchance Styles'; 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'; }

                // Insert after the "AI Network" toggle or a similar prominent menu item
                let insertAfterElement = stTargetElement;
                if (stMenuContent.contains(stTargetElement)) {
                    stTargetElement.parentNode.insertBefore(newMenuItem, stTargetElement.nextSibling);
                } else { // If #option_toggle_AN is not in #extensions_menu_content, append to #extensions_menu_content
                    stMenuContent.appendChild(newMenuItem);
                }


                newMenuItem.addEventListener('click', (event) => {
                    event.preventDefault();
                    promptMenuPanel.style.display = promptMenuPanel.style.display === 'none' ? 'block' : 'none';
                     if (promptMenuPanel.style.display === 'block') {
                        // Call updates when panel is shown
                        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 = '';
                    }
                });
                console.log("Đã tích hợp 'Perchance Styles' vào menu SillyTavern.");
                toggleButton.style.display = 'none'; // Hide floating button if ST integration worked
            } else if (!stMenuContent && !document.getElementById('perchance_st_menu_item')) {
                // If SillyTavern menu not found after a few tries, ensure floating button is visible
                // clearInterval(sillyTavernMenuIntegrationInterval); // Stop trying if ST not detected
                // sillyTavernMenuIntegrationInterval = null;
                // console.log("Không tìm thấy menu SillyTavern, sử dụng nút nổi.");
                // toggleButton.style.display = 'block';
            }
        }, 2000); // Check every 2 seconds for ST menu

        // Ensure floating button is added if interval doesn't find ST menu quickly
        setTimeout(() => {
            if (!document.getElementById('perchance_st_menu_item') && !document.body.contains(toggleButton)) {
                document.body.appendChild(toggleButton);
                console.log("Không tìm thấy menu SillyTavern sau một thời gian, đã thêm nút nổi.");
            } else if (document.getElementById('perchance_st_menu_item')) {
                 toggleButton.style.display = 'none'; // Ensure it's hidden if ST item was added
            }
        }, 6000); // After 6 seconds, make a final decision on the floating button
    }


    // --- Main Function ---
    async function main() {
        console.log("Perchance Image Replacer (VN) - Multi-Key & Contextual Sub-Tags v1.7.4 (Modified) - Xuất/Nhập Dữ Liệu, Panel dưới, Tag thủ cô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."); }

        // Create panel first, then try to integrate
        createPromptEditMenuPanel();
        integratePromptMenuIntoSillyTavern(); // This will now create/manage the floating button or ST menu item

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

})();