Novelai Prompt Helper / Novelai 提示词增强

Tag suggestions and weight shortcuts for NovelAI prompts. 通过Ctrl+↑/↓快速调整输入光标所在的Tag权重,Ctrl+←/→移动Tag位置。支持{}[]||等特殊表达式。自动格式化Prompt。

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Novelai Prompt Helper / Novelai 提示词增强
// @namespace    https://novelai.net
// @version      1.2.3
// @description  Tag suggestions and weight shortcuts for NovelAI prompts. 通过Ctrl+↑/↓快速调整输入光标所在的Tag权重,Ctrl+←/→移动Tag位置。支持{}[]||等特殊表达式。自动格式化Prompt。
// @author       Takoro
// @match        https://novelai.net/image
// @match        https://novelai.github.io/image
// @icon         https://www.google.com/s2/favicons?sz=64&domain=novelai.net
// @license      MIT
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_openInTab
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @connect      danbooru.donmai.us
// @connect      raw.githubusercontent.com
// @history      1.2.3 (2026-02-05) 添加候选词收藏/排除功能(右键候选词操作);收藏词优先匹配并置顶显示;添加NAI重命名Tag关联(如v→peace sign);修复权重组内尾部带数字的tag被NAI错误解析;表情联想支持;格式化自动替换中文逗号
// @history      1.2.2 (2026-01-28) 修复开启保持选中后连续调整带权重tag时无法继续调整的问题
// @history      1.2.1 (2026-01-26) 添加[]减权符号支持;完善{} []||等特殊表达式的识别和处理
// @history      1.2 (2026-01-26) 添加{}加权和||...||随机组支持;支持在特殊表达式内移动和调整权重
// @history      1.1.5 原始版本
// ==/UserScript==
(function () {
    'use strict';
    if (typeof window.__NAIWeightAdjusting !== 'boolean') {
        window.__NAIWeightAdjusting = false;
    }
    if (typeof window.__NAIWeightAdjustingUntil !== 'number') {
        window.__NAIWeightAdjustingUntil = 0;
    }
    const LOCALE_KEY = 'nai_prompt_helper_locale_v1';
    const AUTOFORMAT_KEY = 'nai_prompt_helper_autoformat_enabled_v1';
    const KEEP_SELECTION_KEY = 'nai_prompt_helper_keep_selection_v1';
    const FAVORITES_KEY = 'nai_prompt_helper_favorites_v1';
    const EXCLUDED_KEY = 'nai_prompt_helper_excluded_v1';
    const LOCALE_MESSAGES = {
        zh: {
            infoTitle: 'Did you mean...?',
            hints: [
                '按住 Ctrl + 左键点击标签可以打开对应的 Wiki 页面。',
                '紫色标签代表系列/IP,绿色标签代表角色。',
                '标签后面的数字表示相关作品数量。',
                '可以尝试使用 Tab、Enter 或方向键选择候选项。',
                '右键点击标签可以收藏或排除。',
                'Takoro'
            ],
            fallbackHint: 'Danbooru API 连接失败,已切换到本地缓存。',
            apiTimeoutLog: '[NAI Prompt Helper] 网络请求超时,改用本地缓存。',
            logReady: '[NAI Prompt Helper] 标签联想模块就绪。',
            menuSwitchLabel: '切换到英文界面',
            menuEnableFormat: '启用自动格式化',
            menuDisableFormat: '禁用自动格式化',
            menuEnableKeepSelection: '启用调整后保持选中',
            menuDisableKeepSelection: '禁用调整后保持选中',
            contextMenuFavorite: '收藏',
            contextMenuExclude: '排除',
            contextMenuRemoveFavorite: '取消收藏',
            contextMenuRemoveExclude: '取消排除',
        },
        en: {
            infoTitle: 'Did you mean...?',
            hints: [
                'Ctrl + Click a tag to open its Danbooru wiki page.',
                'Purple tags are series/IP names; green tags are characters.',
                'Numbers after a tag show total post counts.',
                'Use Tab, Enter, or arrow keys to pick a suggestion.',
                'Right-click a tag to favorite or exclude it.',
                'Takoro'
            ],
            fallbackHint: 'Danbooru API error, switching to cached suggestions.',
            apiTimeoutLog: '[NAI Prompt Helper] API timeout, using cached suggestions.',
            logReady: '[NAI Prompt Helper] Tag suggestions ready.',
            menuSwitchLabel: 'Switch to Chinese UI',
            menuEnableFormat: 'Enable Auto-Formatting',
            menuDisableFormat: 'Disable Auto-Formatting',
            menuEnableKeepSelection: 'Enable Keep Selection After Adjustment',
            menuDisableKeepSelection: 'Disable Keep Selection After Adjustment',
            contextMenuFavorite: 'Favorite',
            contextMenuExclude: 'Exclude',
            contextMenuRemoveFavorite: 'Remove Favorite',
            contextMenuRemoveExclude: 'Remove Exclude',
        }
    };
    const SUPPORTED_LOCALES = Object.keys(LOCALE_MESSAGES);
    function detectInitialLocale() {
        try {
            if (typeof GM_getValue === 'function') {
                const stored = GM_getValue(LOCALE_KEY);
                if (stored && SUPPORTED_LOCALES.includes(stored)) {
                    return stored;
                }
            }
        } catch (error) { }
        const nav = (navigator.language || navigator.userLanguage || '').toLowerCase();
        if (nav.startsWith('zh')) { return 'zh'; }
        return 'en';
    }
    let currentLocale = detectInitialLocale();
    let autoFormatEnabled = true;
    let keepSelectionAfterAdjustment = true;
    let favoriteTags = new Set();
    let excludedTags = new Set();
    try {
        if (typeof GM_getValue === 'function') {
            const storedValue = GM_getValue(AUTOFORMAT_KEY);
            if (typeof storedValue === 'boolean') {
                autoFormatEnabled = storedValue;
            } else {
                GM_setValue(AUTOFORMAT_KEY, true);
            }
            const storedKeepSelection = GM_getValue(KEEP_SELECTION_KEY);
            if (typeof storedKeepSelection === 'boolean') {
                keepSelectionAfterAdjustment = storedKeepSelection;
            } else {
                GM_setValue(KEEP_SELECTION_KEY, true);
            }
            const storedFavorites = GM_getValue(FAVORITES_KEY);
            if (Array.isArray(storedFavorites)) {
                favoriteTags = new Set(storedFavorites);
            }
            const storedExcluded = GM_getValue(EXCLUDED_KEY);
            if (Array.isArray(storedExcluded)) {
                excludedTags = new Set(storedExcluded);
            }
        }
    } catch (e) { }
    function saveFavorites() {
        try { if (typeof GM_setValue === 'function') GM_setValue(FAVORITES_KEY, Array.from(favoriteTags)); } catch (e) { }
    }
    function saveExcluded() {
        try { if (typeof GM_setValue === 'function') GM_setValue(EXCLUDED_KEY, Array.from(excludedTags)); } catch (e) { }
    }
    function toggleFavorite(tagName) {
        if (favoriteTags.has(tagName)) {
            favoriteTags.delete(tagName);
        } else {
            favoriteTags.add(tagName);
            excludedTags.delete(tagName);
            saveExcluded();
        }
        saveFavorites();
    }
    function toggleExcluded(tagName) {
        if (excludedTags.has(tagName)) {
            excludedTags.delete(tagName);
        } else {
            excludedTags.add(tagName);
            favoriteTags.delete(tagName);
            saveFavorites();
        }
        saveExcluded();
    }
    let registeredMenuIds = [];
    const isChineseLocale = currentLocale === 'zh';
    function t(key) {
        const bundle = LOCALE_MESSAGES[currentLocale] || LOCALE_MESSAGES.en;
        return bundle[key] ?? LOCALE_MESSAGES.en[key] ?? key;
    }
    function getHints() {
        const bundle = LOCALE_MESSAGES[currentLocale] || LOCALE_MESSAGES.en;
        return bundle.hints || [];
    }
    function clearLocaleMenus() {
        if (typeof GM_unregisterMenuCommand !== 'function') return;
        registeredMenuIds.forEach(id => {
            try { GM_unregisterMenuCommand(id); } catch (error) { }
        });
        registeredMenuIds = [];
    }
    function registerMenus() {
        if (typeof GM_registerMenuCommand !== 'function') return;
        clearLocaleMenus();
        const otherLocale = currentLocale === 'zh' ? 'en' : 'zh';
        const localeLabel = LOCALE_MESSAGES[currentLocale].menuSwitchLabel;
        try {
            const localeCmdId = GM_registerMenuCommand(localeLabel, () => {
                try {
                    if (typeof GM_setValue === 'function') {
                        GM_setValue(LOCALE_KEY, otherLocale);
                    }
                } catch (error) { }
                window.location.reload();
            });
            if (typeof localeCmdId !== 'undefined') {
                registeredMenuIds.push(localeCmdId);
            }
        } catch (error) { }
        const formatLabel = autoFormatEnabled ? t('menuDisableFormat') : t('menuEnableFormat');
        try {
            const formatCmdId = GM_registerMenuCommand(formatLabel, () => {
                try {
                    if (typeof GM_setValue === 'function') {
                        GM_setValue(AUTOFORMAT_KEY, !autoFormatEnabled);
                    }
                } catch (error) { }
                window.location.reload();
            });
            if (typeof formatCmdId !== 'undefined') {
                registeredMenuIds.push(formatCmdId);
            }
        } catch (error) { }
        const keepSelectionLabel = keepSelectionAfterAdjustment ? t('menuDisableKeepSelection') : t('menuEnableKeepSelection');
        try {
            const keepSelectionCmdId = GM_registerMenuCommand(keepSelectionLabel, () => {
                try {
                    if (typeof GM_setValue === 'function') {
                        GM_setValue(KEEP_SELECTION_KEY, !keepSelectionAfterAdjustment);
                    }
                } catch (error) { }
                window.location.reload();
            });
            if (typeof keepSelectionCmdId !== 'undefined') {
                registeredMenuIds.push(keepSelectionCmdId);
            }
        } catch (error) { }
    }
    function formatPromptText(text) {
        // 中文逗号替换为英文逗号
        text = text.replace(/,/g, ',');
        const structure = WeightShortcuts.parsePromptStructure(text);
        WeightShortcuts.normalizeTree(structure);
        return WeightShortcuts.serializeTree(structure);
    }
    registerMenus();
    const TagAssist = (() => {
        const MAX_SUGGESTIONS = 10;
        const DEBOUNCE_DELAY = 400;
        const DEBOUNCE_DELAY_EMOTICON = 1200;
        const API_BASE_URL = 'https://danbooru.donmai.us';
        const TRANSLATION_URL = 'https://raw.githubusercontent.com/Yellow-Rush/zh_CN-Tags/main/danbooru.csv';
        const CACHE_KEY = 'danbooru_translations_cache';
        const CACHE_DURATION_MS = 7 * 24 * 60 * 60 * 1000;
        const TITLE_COLOR = '#e3dccc';
        const SHORT_QUERY_PREFIX_LENGTH = 2;
        const ALLOWED_CHARS_REGEX = /^[a-zA-Z\d_\-\s'\^=@()\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FF]+$/;
        const BREAK_CHARS = ',{}[]:.';
        let popup = null;
        let selectedIndex = -1;
        let currentMatches = [];
        let isPopupActive = false;
        let isKeyboardNavigation = false;
        let apiAbortController = null;
        let lastKnownRange = null;
        let translations = new Map();
        let contextMenu = null;
        let lastQuery = '';
        let lastInput = null;
        const STAR_SVG = '<svg viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>';
        const CROSS_SVG = '<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>';
        // NAI tag aliases: original Danbooru name -> NAI name
        const NAI_TAG_ALIASES = {
            'v': 'peace sign',
            'double_v': 'double peace',
            '|_|': 'bar eyes',
            '\\||/': 'open \\m/',
            ':|': 'neutral face',
            ';|': 'neutral face',
            '<|>_<|>': 'neco-arc eyes',
            'eyepatch_bikini': 'square bikini',
            'tachi-e': 'character image',
            'vivian_banshee': 'vivian (zenless zone zero)'
        };
        // Reverse mapping: NAI name -> original Danbooru name(s)
        const NAI_ALIAS_REVERSE = {};
        for (const [original, nai] of Object.entries(NAI_TAG_ALIASES)) {
            const naiKey = nai.toLowerCase().replace(/\s+/g, '_');
            if (!NAI_ALIAS_REVERSE[naiKey]) NAI_ALIAS_REVERSE[naiKey] = [];
            NAI_ALIAS_REVERSE[naiKey].push(original);
        }
        GM_addStyle(`
                .autocomplete-container { position: absolute; background: #0e0f21; border: 1px solid #3B3B52; border-radius: 8px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); max-height: 450px; display: flex; flex-direction: column; z-index: 100000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; color: #EAEAEB; min-width: 350px; max-width: 500px; }
                .suggestion-info-display { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: #1a1c30; border-bottom: 1px solid #3B3B52; font-size: 13.5px; line-height: 1.6; min-height: 17px; font-weight: bold; }
                .info-hint { color: #8a8a9e; font-weight: normal; font-size: 12px; margin-left: 10px; white-space: nowrap; }
                .info-hint-error { color: #ff7b7b; font-weight: bold; font-size: 12px; margin-left: 10px; white-space: nowrap; }
                .info-title { color: ${TITLE_COLOR}; }
                .suggestion-scroll-wrapper { padding: 7px; overflow-y: auto; }
                .suggestion-flex { display: flex; flex-wrap: wrap; gap: 6px; }
                .suggestion-item { display: flex; align-items: center; justify-content: space-between; padding: 4px 10px; background: #22253f; border: 1px solid transparent; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; font-size: 14px; height: 26px; white-space: nowrap; flex-shrink: 0; }
                .suggestion-item:hover, .suggestion-item.selected { background: #34395f; border-color: #F5F3C2; }
                .suggestion-item:hover .suggestion-divider, .suggestion-item.selected .suggestion-divider { background: #8a8a9e; }
                .suggestion-text { overflow: hidden; text-overflow: ellipsis; color: #EAEAEB; }
                .suggestion-count { color: #8a8a9e; margin-left: 12px; font-size: 12px; }
                .suggestion-item[data-category='4'] .suggestion-text { color: #a6f3a6; }
                .suggestion-item[data-category='3'] .suggestion-text { color: #d6bcf5; }
                .suggestion-alias { color: #8a8a9e; margin-left: 4px; }
                .suggestion-icon { display: flex; align-items: center; margin-right: 6px; flex-shrink: 0; }
                .suggestion-icon svg { width: 12px; height: 12px; }
                .suggestion-icon.fav-icon svg { fill: #ffd700; }
                .suggestion-icon.excl-icon svg { fill: #ff6b6b; }
                .suggestion-divider { width: 1px; height: 14px; background: #3B3B52; margin: 0 6px; flex-shrink: 0; }
                .tag-context-menu { position: fixed; background: #1a1c30; border: 1px solid #3B3B52; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); z-index: 100001; min-width: 100px; padding: 2px 0; }
                .tag-context-menu-item { display: flex; align-items: center; padding: 5px 10px; cursor: pointer; font-size: 13px; color: #EAEAEB; transition: background 0.15s ease; }
                .tag-context-menu-item:first-child { border-bottom: 1px solid #3B3B52; }
                .tag-context-menu-item:hover { background: #34395f; }
                .tag-context-menu-item svg { width: 12px; height: 12px; margin-right: 6px; }
                .tag-context-menu-item.fav-item svg { fill: #ffd700; }
                .tag-context-menu-item.excl-item svg { fill: #ff6b6b; }
            `);
        const normalizeQuery = (query) => query.trim().replace(/\s+/g, '_');
        function hideContextMenu() {
            if (contextMenu) {
                contextMenu.remove();
                contextMenu = null;
            }
        }
        function showContextMenu(tagName, x, y) {
            hideContextMenu();
            contextMenu = document.createElement('div');
            contextMenu.className = 'tag-context-menu';
            const isFavorite = favoriteTags.has(tagName);
            const isExcluded = excludedTags.has(tagName);
            const favItem = document.createElement('div');
            favItem.className = 'tag-context-menu-item fav-item';
            favItem.innerHTML = `${STAR_SVG}<span>${isFavorite ? t('contextMenuRemoveFavorite') : t('contextMenuFavorite')}</span>`;
            favItem.addEventListener('mousedown', (e) => {
                e.preventDefault();
                e.stopPropagation();
                toggleFavorite(tagName);
                hideContextMenu();
                if (lastQuery && lastInput && currentMatches.length > 0) {
                    updatePopup(lastInput, currentMatches, null, lastQuery);
                }
            });
            const exclItem = document.createElement('div');
            exclItem.className = 'tag-context-menu-item excl-item';
            exclItem.innerHTML = `${CROSS_SVG}<span>${isExcluded ? t('contextMenuRemoveExclude') : t('contextMenuExclude')}</span>`;
            exclItem.addEventListener('mousedown', (e) => {
                e.preventDefault();
                e.stopPropagation();
                toggleExcluded(tagName);
                hideContextMenu();
                if (lastQuery && lastInput && currentMatches.length > 0) {
                    updatePopup(lastInput, currentMatches, null, lastQuery);
                }
            });
            contextMenu.appendChild(favItem);
            contextMenu.appendChild(exclItem);
            contextMenu.style.left = `${x}px`;
            contextMenu.style.top = `${y}px`;
            document.body.appendChild(contextMenu);
        }
        function sortMatchesByPriority(matches, query) {
            const normalizedQuery = normalizeQuery(query.toLowerCase());
            return matches.sort((a, b) => {
                const aFav = favoriteTags.has(a.en);
                const bFav = favoriteTags.has(b.en);
                const aExcl = excludedTags.has(a.en);
                const bExcl = excludedTags.has(b.en);
                // Favorites first
                if (aFav && !bFav) return -1;
                if (!aFav && bFav) return 1;
                // Excluded last
                if (aExcl && !bExcl) return 1;
                if (!aExcl && bExcl) return -1;
                // For favorites, check if query matches the start
                if (aFav && bFav) {
                    const aStart = a.en.toLowerCase().startsWith(normalizedQuery);
                    const bStart = b.en.toLowerCase().startsWith(normalizedQuery);
                    if (aStart && !bStart) return -1;
                    if (!aStart && bStart) return 1;
                }
                return 0;
            });
        }
        function mergeMatchedFavorites(matches, query) {
            if (!favoriteTags.size) return matches;
            const queryLower = normalizeQuery(query).toLowerCase();
            if (queryLower.length < 3) return matches;
            const querySpaced = query.toLowerCase().trim().replace(/\s+/g, ' ');
            const matchNames = new Set(matches.map(m => m.en));
            const additionalFavorites = [];
            for (const favName of favoriteTags) {
                if (matchNames.has(favName)) continue;
                const enLower = favName.toLowerCase();
                const enSpaced = enLower.replace(/_/g, ' ');
                if (enLower.startsWith(queryLower) || enSpaced.startsWith(querySpaced) ||
                    enLower.includes(queryLower) || enSpaced.includes(querySpaced)) {
                    additionalFavorites.push({ en: favName, count: undefined, category: 0 });
                }
            }
            return [...additionalFavorites, ...matches];
        }
        function findNaiAliasMatches(query) {
            const queryLower = query.toLowerCase().trim();
            const queryNorm = queryLower.replace(/\s+/g, '_');
            const results = [];
            // Search by NAI name (reverse lookup)
            for (const [naiKey, originals] of Object.entries(NAI_ALIAS_REVERSE)) {
                const naiSpaced = naiKey.replace(/_/g, ' ');
                if (naiKey.startsWith(queryNorm) || naiSpaced.startsWith(queryLower) ||
                    naiKey.includes(queryNorm) || naiSpaced.includes(queryLower)) {
                    for (const original of originals) {
                        results.push({ en: original, count: undefined, category: 0, isAliasMatch: true });
                    }
                }
            }
            // Search by original name
            for (const [original, nai] of Object.entries(NAI_TAG_ALIASES)) {
                const origLower = original.toLowerCase();
                const origSpaced = origLower.replace(/_/g, ' ');
                if (origLower.startsWith(queryNorm) || origSpaced.startsWith(queryLower) ||
                    origLower.includes(queryNorm) || origSpaced.includes(queryLower)) {
                    if (!results.some(r => r.en === original)) {
                        results.push({ en: original, count: undefined, category: 0, isAliasMatch: true });
                    }
                }
            }
            return results;
        }
        function mergeNaiAliasMatches(matches, query) {
            const aliasMatches = findNaiAliasMatches(query);
            if (!aliasMatches.length) return matches;
            const matchNames = new Set(matches.map(m => m.en));
            const additional = aliasMatches.filter(a => !matchNames.has(a.en));
            return [...additional, ...matches];
        }
        function shouldSkipWeightSuggestions(textBeforeCursor, delimiterIndex) {
            if (delimiterIndex < 0 || textBeforeCursor[delimiterIndex] !== ':') return false;
            if (textBeforeCursor.substring(delimiterIndex - 1, delimiterIndex + 1) === '::') { return false; }
            let i = delimiterIndex - 1;
            if (i >= 0 && textBeforeCursor[i] === ':') {
                i--;
                while (i >= 0 && /\s/.test(textBeforeCursor[i])) i--;
                if (i >= 0 && /[\d.]/.test(textBeforeCursor[i])) {
                    let end = i;
                    while (i >= 0 && /[\d.]/.test(textBeforeCursor[i])) i--;
                    const numericPart = textBeforeCursor.substring(i + 1, end + 1);
                    if (numericPart && /^\d+(?:\.\d+)?$/.test(numericPart)) {
                        const precedingChar = i >= 0 ? textBeforeCursor[i] : '';
                        if (i < 0 || BREAK_CHARS.includes(precedingChar) || /\s/.test(precedingChar)) { return true; }
                    }
                }
                return false;
            }
            while (i >= 0 && /\s/.test(textBeforeCursor[i])) i--;
            if (i < 0) return false;
            let end = i;
            while (i >= 0 && /[\d.]/.test(textBeforeCursor[i])) i--;
            const numericPart = textBeforeCursor.substring(i + 1, end + 1);
            if (!numericPart || !/^\d+(?:\.\d+)?$/.test(numericPart)) return false;
            const precedingChar = i >= 0 ? textBeforeCursor[i] : '';
            if (i >= 0 && !BREAK_CHARS.includes(precedingChar) && !/\s/.test(precedingChar)) return false;
            return true;
        }
        function getClonedSelectionRange() {
            const selection = window.getSelection();
            if (!selection || !selection.rangeCount) return null;
            return selection.getRangeAt(0).cloneRange();
        }
        function loadTranslations() {
            if (!isChineseLocale) { translations = new Map(); return; }
            const cachedData = GM_getValue(CACHE_KEY);
            if (cachedData && cachedData.timestamp && (Date.now() - cachedData.timestamp < CACHE_DURATION_MS)) {
                translations = new Map(cachedData.translations); return;
            }
            GM_xmlhttpRequest({
                method: "GET", url: TRANSLATION_URL,
                onload: function (response) {
                    if (response.status === 200) {
                        const lines = response.responseText.split('\n').filter(line => line.trim());
                        lines.forEach(line => {
                            const columns = line.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);
                            if (columns.length >= 2) {
                                const en = (columns[0] || '').trim().replace(/^"|"$/g, '');
                                const zh = (columns[1] || '').trim().replace(/^"|"$/g, '');
                                if (en && zh) { translations.set(en, zh); }
                            }
                        });
                        GM_setValue(CACHE_KEY, { translations: Array.from(translations.entries()), timestamp: Date.now() });
                    }
                },
            });
        }
        function openWikiPage(tagName) { if (!tagName) return; GM_openInTab(`${API_BASE_URL}/wiki_pages/show_or_new?title=${tagName}`, { active: true }); }
        function getRandomHint() {
            const hints = getHints();
            if (!hints.length || Math.random() < 0.5) { return ""; }
            return hints[Math.floor(Math.random() * hints.length)];
        }
        function searchLocalSuggestions(query, input) {
            if (!isChineseLocale || !translations.size) return;
            const queryLower = query.toLowerCase();
            const rankedItems = [];
            for (const [en, zh] of translations.entries()) {
                const zhLower = zh.toLowerCase();
                let score = 0;
                if (zhLower.startsWith(queryLower)) score = 1;
                else if (zhLower.includes(queryLower)) score = 2;
                if (score > 0) { rankedItems.push({ score: score, data: { en: en, count: undefined, category: 0 } }); }
            }
            let finalMatches = rankedItems.sort((a, b) => a.score - b.score).map(item => item.data).slice(0, MAX_SUGGESTIONS);
            finalMatches = mergeMatchedFavorites(finalMatches, query);
            finalMatches = mergeNaiAliasMatches(finalMatches, query);
            updatePopup(input, finalMatches, null, query);
        }
        function searchLocalFallback(query, input) {
            if (!isChineseLocale || !translations.size) { hidePopup(); return; }
            const queryLower = query.toLowerCase(), normalizedQuery = normalizeQuery(queryLower), spacedQuery = queryLower.replace(/\s+/g, ' ');
            if (!normalizedQuery) { hidePopup(); return; }
            const rankedItems = [];
            for (const en of translations.keys()) {
                const enLower = en.toLowerCase();
                let score = 0;
                const enNormalized = normalizeQuery(enLower), enSpaced = enLower.replace(/_/g, ' ');
                if (enNormalized.startsWith(normalizedQuery) || enSpaced.startsWith(spacedQuery)) score = 1;
                else if (enNormalized.includes(normalizedQuery) || enSpaced.includes(spacedQuery)) score = 2;
                if (score > 0) { rankedItems.push({ score: score, data: { en: en, count: undefined, category: 0 } }); }
            }
            const finalMatches = rankedItems.sort((a, b) => a.score - b.score).map(item => item.data).slice(0, MAX_SUGGESTIONS);
            if (finalMatches.length > 0) { updatePopup(input, mergeNaiAliasMatches(mergeMatchedFavorites(finalMatches, query), query), t('fallbackHint'), query); } else { hidePopup(); }
        }
        function fetchSuggestions(query, input) {
            const normalizedQuery = normalizeQuery(query);
            if (!normalizedQuery) { hidePopup(); return; }
            if (apiAbortController) apiAbortController.abort();
            apiAbortController = new AbortController();
            const searchPattern = normalizedQuery.length <= SHORT_QUERY_PREFIX_LENGTH ? `${normalizedQuery}*` : `*${normalizedQuery}*`;
            const params = new URLSearchParams({ 'search[name_matches]': searchPattern, 'search[order]': 'count', 'limit': MAX_SUGGESTIONS, 'search[hide_empty]': 'true' });
            GM_xmlhttpRequest({
                method: "GET",
                url: `${API_BASE_URL}/tags.json?${params.toString()}`,
                signal: apiAbortController.signal,
                timeout: 1500,
                headers: {
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36",
                    "Referer": "https://danbooru.donmai.us/"
                },

                ontimeout: function () {
                    console.error(`[NAI Prompt Helper] API 请求超时 (ontimeout)。(Timeout: 1500ms). URL: ${API_BASE_URL}/tags.json?${params.toString()}`);
                    searchLocalFallback(query, input);
                },

                onload: function (response) {
                    if (response.status === 200) {
                        let matches = JSON.parse(response.responseText).map(tag => ({ en: tag.name, count: tag.post_count, category: tag.category }));
                        matches = mergeMatchedFavorites(matches, query);
                        matches = mergeNaiAliasMatches(matches, query);
                        updatePopup(input, matches, null, query);
                    } else {
                        console.error(`[NAI Prompt Helper] API 错误: 收到 HTTP 状态 ${response.status} ${response.statusText}`);
                        console.warn(`[NAI Prompt Helper] 失败的URL: ${response.finalUrl}`);
                        searchLocalFallback(query, input);
                    }
                },

                onerror: (error) => {
                    console.error('[NAI Prompt Helper] API 请求失败 (onerror)。 详细信息:', error);
                    if (error.readyState !== 0) { // 忽略 abort 信号
                        searchLocalFallback(query, input);
                    }
                }
            });
        }
        function fetchEmoticonSuggestions(query, input) {
            // 先获取 NAI 别名匹配
            const aliasMatches = findNaiAliasMatches(query);
            // 同时尝试 Danbooru API
            const normalizedQuery = normalizeQuery(query);
            if (!normalizedQuery) {
                if (aliasMatches.length > 0) {
                    updatePopup(input, aliasMatches, null, query);
                } else { hidePopup(); }
                return;
            }
            if (apiAbortController) apiAbortController.abort();
            apiAbortController = new AbortController();
            const searchPattern = `${normalizedQuery}*`;
            const params = new URLSearchParams({ 'search[name_matches]': searchPattern, 'search[order]': 'count', 'limit': MAX_SUGGESTIONS, 'search[hide_empty]': 'true' });
            GM_xmlhttpRequest({
                method: "GET",
                url: `${API_BASE_URL}/tags.json?${params.toString()}`,
                signal: apiAbortController.signal,
                timeout: 1500,
                headers: {
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36",
                    "Referer": "https://danbooru.donmai.us/"
                },
                ontimeout: function () {
                    // API 超时,只显示别名匹配
                    if (aliasMatches.length > 0) {
                        updatePopup(input, aliasMatches, null, query);
                    } else { hidePopup(); }
                },
                onload: function (response) {
                    if (response.status === 200) {
                        let matches = JSON.parse(response.responseText).map(tag => ({ en: tag.name, count: tag.post_count, category: tag.category }));
                        // 合并 API 结果和别名匹配
                        const matchNames = new Set(matches.map(m => m.en));
                        const uniqueAliases = aliasMatches.filter(a => !matchNames.has(a.en));
                        matches = [...uniqueAliases, ...matches];
                        matches = mergeMatchedFavorites(matches, query);
                        if (matches.length > 0) {
                            updatePopup(input, matches, null, query);
                        } else { hidePopup(); }
                    } else {
                        // API 错误,只显示别名匹配
                        if (aliasMatches.length > 0) {
                            updatePopup(input, aliasMatches, null, query);
                        } else { hidePopup(); }
                    }
                },
                onerror: () => {
                    // API 错误,只显示别名匹配
                    if (aliasMatches.length > 0) {
                        updatePopup(input, aliasMatches, null, query);
                    } else { hidePopup(); }
                }
            });
        }
        function updatePopup(input, matches, overrideHint = null, query = '') {
            createPopup();
            hideContextMenu();
            lastInput = input;
            lastQuery = query;
            // Sort matches by favorites/excluded priority
            if (query) {
                matches = sortMatchesByPriority([...matches], query);
            }
            let hintHTML = overrideHint ? `<span class="info-hint-error">${overrideHint}</span>` : (getRandomHint() ? `<span class="info-hint">${getRandomHint()}</span>` : '');
            popup.innerHTML = `<div class="suggestion-info-display"><span class="info-title">${t('infoTitle')}</span>${hintHTML}</div><div class="suggestion-scroll-wrapper"><div class="suggestion-flex"></div></div>`;
            const flexContainer = popup.querySelector('.suggestion-flex');
            currentMatches = matches;
            if (matches.length === 0) { hidePopup(); return; }
            matches.forEach((tag, index) => {
                const item = document.createElement('div');
                item.className = 'suggestion-item';
                item.dataset.category = tag.category || '0';
                const naiAlias = NAI_TAG_ALIASES[tag.en];
                const enText = tag.en.replace(/_/g, ' ');
                const zhText = translations.get(tag.en);
                let displayHTML;
                if (naiAlias) {
                    const naiText = naiAlias.replace(/_/g, ' ');
                    displayHTML = zhText
                        ? `<span class="suggestion-text">${enText}</span><span class="suggestion-divider"></span><span class="suggestion-text">${naiText} (${zhText})</span>`
                        : `<span class="suggestion-text">${enText}</span><span class="suggestion-divider"></span><span class="suggestion-text">${naiText}</span>`;
                } else {
                    const displayText = zhText ? `${enText} (${zhText})` : enText;
                    displayHTML = `<span class="suggestion-text">${displayText}</span>`;
                }
                // Store the name to insert (NAI version if available)
                tag.insertName = naiAlias || tag.en;
                let countHTML = '';
                if (tag.count !== undefined && tag.count !== null) {
                    const countText = tag.count > 1000 ? `${(tag.count / 1000).toFixed(1)}k` : tag.count;
                    countHTML = `<span class="suggestion-count">${countText}</span>`;
                }
                // Add icon for favorite/excluded tags
                let iconHTML = '';
                if (favoriteTags.has(tag.en)) {
                    iconHTML = `<span class="suggestion-icon fav-icon">${STAR_SVG}</span>`;
                } else if (excludedTags.has(tag.en)) {
                    iconHTML = `<span class="suggestion-icon excl-icon">${CROSS_SVG}</span>`;
                }
                item.innerHTML = `${iconHTML}${displayHTML}${countHTML}`;
                item.addEventListener('mouseover', () => {
                    isKeyboardNavigation = false;
                    if (selectedIndex !== index) { selectedIndex = index; updateSelectionUI(); }
                });
                item.addEventListener('mousedown', (e) => {
                    if (e.button !== 0) return;
                    e.preventDefault();
                    selectedIndex = index;
                    updateSelectionUI();
                    if (e.ctrlKey) { openWikiPage(tag.en); }
                    else {
                        const currentInput = getActiveInputElement();
                        if (currentInput) {
                            const rangeToRestore = lastKnownRange ? lastKnownRange.cloneRange() : getClonedSelectionRange();
                            currentInput.focus();
                            requestAnimationFrame(() => insertTag(currentInput, tag.insertName, rangeToRestore));
                        }
                    }
                });
                item.addEventListener('contextmenu', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    // Save selection before showing context menu to prevent cursor loss
                    const currentInput = getActiveInputElement();
                    if (currentInput) {
                        const range = getClonedSelectionRange();
                        if (range && currentInput.contains(range.commonAncestorContainer)) {
                            lastKnownRange = range;
                        }
                    }
                    showContextMenu(tag.en, e.clientX, e.clientY);
                });
                flexContainer.appendChild(item);
            });
            positionPopup(input);
            popup.style.display = 'flex';
            isPopupActive = true;
            if (matches.length > 0) { selectedIndex = 0; updateSelectionUI(); }
        }

        function findNodeAndOffsetFromGlobal(root, globalOffset) {
            const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
            let accumulatedLength = 0;
            let lastTextNode = null;
            let currentNode;
            while (currentNode = treeWalker.nextNode()) {
                lastTextNode = currentNode;
                const nodeLength = currentNode.textContent.length;
                if (accumulatedLength + nodeLength >= globalOffset) {
                    return { node: currentNode, offset: globalOffset - accumulatedLength };
                }
                accumulatedLength += nodeLength;
            }
            if (lastTextNode) {
                return { node: lastTextNode, offset: lastTextNode.textContent.length };
            }
            return { node: root, offset: 0 };
        }

        function insertTag(input, tag, rangeToRestore) {
            const cleanTag = tag.replace(/_/g, ' ');
            const workingRange = rangeToRestore ? rangeToRestore.cloneRange() : getClonedSelectionRange();
            if (!workingRange) return;

            const tempRange = document.createRange();
            tempRange.setStart(input, 0);
            tempRange.setEnd(workingRange.startContainer, workingRange.startOffset);
            const globalCursorPos = tempRange.toString().length;
            const fullText = input.textContent;

            if (autoFormatEnabled) {

                let start = globalCursorPos;
                while (start > 0 && !BREAK_CHARS.includes(fullText[start - 1])) { start--; }
                let end = globalCursorPos;
                while (end < fullText.length && !BREAK_CHARS.includes(fullText[end])) { end++; }

                let textBefore = fullText.substring(0, start);
                const textAfter = fullText.substring(end);

                // 只对以分隔符开头的标签检查重叠(如 :> 这类表情符号)
                if (cleanTag.length > 0 && BREAK_CHARS.includes(cleanTag[0])) {
                    for (let overlap = Math.min(textBefore.length, cleanTag.length); overlap > 0; overlap--) {
                        if (textBefore.endsWith(cleanTag.substring(0, overlap))) {
                            textBefore = textBefore.substring(0, textBefore.length - overlap);
                            break;
                        }
                    }
                }

                const newFullText = textBefore + cleanTag + textAfter;

                let formattedText = formatPromptText(newFullText);
                formattedText = formattedText.trim();

                if (formattedText.length > 0 && !formattedText.endsWith(',')) {
                    formattedText += ', ';
                } else if (formattedText.endsWith(',')) {
                    formattedText += ' ';
                }

                // 计算插入位置之前有多少个同名 tag
                let countBefore = 0;
                let searchPos = 0;
                while ((searchPos = textBefore.indexOf(cleanTag, searchPos)) !== -1) {
                    // 确保是完整的 tag(前后是边界字符或字符串边界)
                    const charBefore = searchPos > 0 ? textBefore[searchPos - 1] : ',';
                    const charAfter = searchPos + cleanTag.length < textBefore.length ? textBefore[searchPos + cleanTag.length] : ',';
                    if ((BREAK_CHARS.includes(charBefore) || /\s/.test(charBefore)) &&
                        (BREAK_CHARS.includes(charAfter) || /\s/.test(charAfter))) {
                        countBefore++;
                    }
                    searchPos += cleanTag.length;
                }

                // 在格式化后的文本中找到第 countBefore+1 个同名 tag
                let newCursorPos = -1;
                let foundIndex = -1;
                searchPos = 0;
                for (let i = 0; i <= countBefore; i++) {
                    foundIndex = formattedText.indexOf(cleanTag, searchPos);
                    if (foundIndex === -1) break;
                    searchPos = foundIndex + cleanTag.length;
                }

                if (foundIndex !== -1) {
                    newCursorPos = foundIndex + cleanTag.length + 2;
                } else {
                    newCursorPos = start + cleanTag.length + 2;
                }

                if (newCursorPos >= 2 && formattedText.substring(newCursorPos - 2, newCursorPos) === '::') {
                    newCursorPos -= 2;
                }

                newCursorPos = Math.min(newCursorPos, formattedText.length);
                WeightShortcuts.updateInputContent(input, formattedText, newCursorPos, newCursorPos);

            } else {
                try {
                    if (!input.contains(workingRange.startContainer)) return;

                    let start = globalCursorPos;
                    while (start > 0 && !BREAK_CHARS.includes(fullText[start - 1])) { start--; }
                    let end = globalCursorPos;
                    while (end < fullText.length && !BREAK_CHARS.includes(fullText[end])) { end++; }

                    const { node: startNode, offset: startOffset } = findNodeAndOffsetFromGlobal(input, start);
                    const { node: endNode, offset: endOffset } = findNodeAndOffsetFromGlobal(input, end);

                    const replacementRange = document.createRange();
                    replacementRange.setStart(startNode, startOffset);
                    replacementRange.setEnd(endNode, endOffset);

                    replacementRange.deleteContents();

                    let textToInsert = cleanTag;
                    const textBefore = fullText.substring(0, start).trimEnd();
                    let prefix = '';
                    if (textBefore.length > 0) {
                        if (textBefore.endsWith(',')) {
                            prefix = ' ';
                        } else {
                            prefix = ', ';
                        }
                    }
                    textToInsert = prefix + textToInsert + ', ';

                    const newTextNode = document.createTextNode(textToInsert);
                    replacementRange.insertNode(newTextNode);

                    replacementRange.setStartAfter(newTextNode);
                    replacementRange.collapse(true);

                    const selection = window.getSelection();
                    selection.removeAllRanges();
                    selection.addRange(replacementRange);

                    const inputEvent = new Event('input', { bubbles: true, composed: true });
                    input.dispatchEvent(inputEvent);
                } catch (error) {
                    console.error("[NAI Prompt Helper] An error occurred during surgical insertTag:", error);
                }
            }

            window.__NAIWeightAdjustingUntil = Date.now() + 500;
            hidePopup();
        }
        function createPopup() { if (!popup) { popup = document.createElement('div'); popup.className = 'autocomplete-container'; document.body.appendChild(popup); } }
        function hidePopup() { if (popup) popup.style.display = 'none'; hideContextMenu(); if (apiAbortController) apiAbortController.abort(); selectedIndex = -1; isPopupActive = false; currentMatches = []; isKeyboardNavigation = false; }
        let debounceTimeout = null;
        function cancelPendingSuggestions() { if (debounceTimeout) { clearTimeout(debounceTimeout); debounceTimeout = null; } if (apiAbortController) apiAbortController.abort(); }
        function getActiveInputElement() { const selection = window.getSelection(); if (!selection.rangeCount) return null; const node = selection.focusNode; const pElement = node.nodeType === 3 ? node.parentElement.closest('p') : node.closest('p'); if (pElement && (pElement.closest('[class*="prompt-input"]') || pElement.closest('[class*="character-prompt-input"]'))) { return pElement; } return null; }
        function positionPopup(input) { let rect; const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0).cloneRange(); if (range.collapsed) { const tempSpan = document.createElement('span'); tempSpan.appendChild(document.createTextNode('\u200b')); range.insertNode(tempSpan); rect = tempSpan.getBoundingClientRect(); tempSpan.remove(); } else { rect = range.getBoundingClientRect(); } } if (!rect || (rect.width === 0 && rect.height === 0)) { rect = input.getBoundingClientRect(); } popup.style.top = `${rect.bottom + window.scrollY + 5}px`; popup.style.left = `${rect.left + window.scrollX}px`; }
        function updateSelectionUI() { const items = popup.querySelectorAll('.suggestion-item'); items.forEach((item, index) => item.classList.toggle('selected', index === selectedIndex)); const selectedEl = popup.querySelector('.selected'); if (selectedEl) selectedEl.scrollIntoView({ block: 'nearest' }); }
        function saveSelection() {
            const selection = window.getSelection();
            const activeInput = getActiveInputElement();
            if (activeInput && selection.rangeCount > 0) {
                const range = selection.getRangeAt(0);
                if (activeInput.contains(range.commonAncestorContainer)) { lastKnownRange = range.cloneRange(); }
            }
        }
        function handleInput() {
            cancelPendingSuggestions();
            const input = getActiveInputElement(); if (!input) { hidePopup(); return; }
            const textBeforeCursor = (() => { const sel = window.getSelection(); if (!sel.rangeCount) return ''; const range = sel.getRangeAt(0).cloneRange(); const parent = sel.focusNode.parentElement; if (!parent) return ''; range.selectNodeContents(parent); range.setEnd(sel.focusNode, sel.focusOffset); return range.toString(); })();
            if (textBeforeCursor.endsWith(',')) { hidePopup(); return; }

            // 检测表情符号模式:以 : 或 ; 开头,可能后跟 _ < > 等符号(不含 |,NAI会转换)
            const emoticonMatch = textBeforeCursor.match(/(?:^|[,\s\n\r{[\|])([;:][_<>\-\\\/;:]*?)$/);
            if (emoticonMatch) {
                const emoticonQuery = emoticonMatch[1];
                debounceTimeout = setTimeout(() => {
                    const now = Date.now();
                    if (window.__NAIWeightAdjusting || now < window.__NAIWeightAdjustingUntil) { hidePopup(); return; }
                    // 表情符号查询:同时搜索 NAI 别名和 Danbooru API
                    fetchEmoticonSuggestions(emoticonQuery, input);
                }, DEBOUNCE_DELAY_EMOTICON);
                return;
            }

            debounceTimeout = setTimeout(() => {
                const now = Date.now();
                if (window.__NAIWeightAdjusting || now < window.__NAIWeightAdjustingUntil) { hidePopup(); return; }
                const lastDelimiterIndex = Math.max(textBeforeCursor.lastIndexOf(','), textBeforeCursor.lastIndexOf(':'), textBeforeCursor.lastIndexOf('['), textBeforeCursor.lastIndexOf('{'), textBeforeCursor.lastIndexOf('|'));
                const currentQuery = textBeforeCursor.substring(lastDelimiterIndex + 1).trim();
                if (shouldSkipWeightSuggestions(textBeforeCursor, lastDelimiterIndex)) { hidePopup(); return; }
                if (currentQuery.length < 1) { hidePopup(); return; }
                if (!ALLOWED_CHARS_REGEX.test(currentQuery)) { hidePopup(); return; }
                if (/^\d*\.?\d*$/.test(currentQuery)) { hidePopup(); return; }
                if (/[\u4e00-\u9fa5]/.test(currentQuery)) { searchLocalSuggestions(currentQuery, input); } else { fetchSuggestions(currentQuery, input); }
            }, DEBOUNCE_DELAY);
        }
        function handleKeydown(e) {
            if (e.ctrlKey) { return; }
            if (e.key === ',') { cancelPendingSuggestions(); hidePopup(); return; }
            const keyMap = { 'ArrowDown': 1, 'ArrowRight': 1, 'ArrowUp': -1, 'ArrowLeft': -1 };
            if (keyMap[e.key] !== undefined) { cancelPendingSuggestions(); }
            if (!isPopupActive) return;
            if (keyMap[e.key] !== undefined) {
                e.preventDefault();
                isKeyboardNavigation = true;
                selectedIndex = (selectedIndex + keyMap[e.key] + currentMatches.length) % currentMatches.length;
                updateSelectionUI();
            } else if (e.key === 'Enter' || e.key === 'Tab') {
                e.preventDefault(); e.stopPropagation();
                if (selectedIndex >= 0 && currentMatches[selectedIndex]) {
                    const input = getActiveInputElement();
                    if (input) {
                        const rangeForInsert = getClonedSelectionRange();
                        insertTag(input, currentMatches[selectedIndex].insertName, rangeForInsert);
                    }
                } else { hidePopup(); }
            } else if (e.key === 'Escape') { e.preventDefault(); hidePopup(); }
            else { isKeyboardNavigation = false; }
        }
        function handleClickOutside(e) {
            if (contextMenu && !contextMenu.contains(e.target)) { hideContextMenu(); }
            const input = getActiveInputElement();
            const clickedInPopup = popup && popup.contains(e.target);
            const clickedInInput = input && input.contains(e.target);
            const clickedInContext = contextMenu && contextMenu.contains(e.target);
            if (!clickedInPopup && !clickedInInput && !clickedInContext) { cancelPendingSuggestions(); }
            if (isPopupActive && popup && !clickedInPopup && !clickedInInput && !clickedInContext) { hidePopup(); }
        }
        function init() {
            loadTranslations();
            document.addEventListener('input', handleInput);
            document.addEventListener('keydown', handleKeydown, true);
            document.addEventListener('mousedown', handleClickOutside);
            document.addEventListener('keyup', saveSelection);
            document.addEventListener('mouseup', saveSelection);
            document.addEventListener('selectionchange', saveSelection);
        }
        return { init };
    })();
    const WeightShortcuts = (() => {
        function getActiveInputElement() {
            const selection = window.getSelection();
            if (!selection.rangeCount) return null;
            const node = selection.focusNode;
            const pElement = node.nodeType === 3 ? node.parentElement.closest('p') : node.closest('p');
            if (pElement && (pElement.closest('.prompt-input-box-prompt') || pElement.closest('.prompt-input-box-base-prompt') || pElement.closest('.prompt-input-box-negative-prompt') || pElement.closest('.prompt-input-box-undesired-content') || pElement.closest('[class*="character-prompt-input"]'))) {
                return pElement;
            }
            return null;
        }
        function getSelectedTagInfo(inputElement) {
            if (!inputElement) return null;
            const selection = window.getSelection();
            if (!selection.rangeCount) return null;
            const range = selection.getRangeAt(0);
            const fullText = inputElement.textContent || '';

            // 辅助函数:计算节点在 inputElement 中的全局文本偏移量
            function computeGlobalOffset(node, offset) {
                let globalOffset = 0;
                if (node.nodeType === 3) {
                    // 文本节点:遍历找到该节点前的所有文本长度
                    const treeWalker = document.createTreeWalker(inputElement, NodeFilter.SHOW_TEXT);
                    let currentNode;
                    while ((currentNode = treeWalker.nextNode())) {
                        if (currentNode === node) break;
                        globalOffset += currentNode.length;
                    }
                    globalOffset += offset;
                } else if (node === inputElement || inputElement.contains(node)) {
                    // 元素节点:offset 是子节点索引,计算前面子节点的文本长度
                    const children = node.childNodes;
                    for (let i = 0; i < offset && i < children.length; i++) {
                        globalOffset += children[i].textContent?.length || 0;
                    }
                    // 如果 node 不是 inputElement,还需要加上 node 之前的文本长度
                    if (node !== inputElement) {
                        const treeWalker = document.createTreeWalker(inputElement, NodeFilter.SHOW_ALL);
                        let currentNode;
                        let foundNode = false;
                        let textBeforeNode = 0;
                        while ((currentNode = treeWalker.nextNode())) {
                            if (currentNode === node) { foundNode = true; break; }
                            if (currentNode.nodeType === 3) { textBeforeNode += currentNode.length; }
                        }
                        if (foundNode) globalOffset += textBeforeNode;
                    }
                } else {
                    globalOffset = offset;
                }
                return Math.max(0, Math.min(globalOffset, fullText.length));
            }

            // 计算选区开始和结束位置
            const globalStartOffset = computeGlobalOffset(range.startContainer, range.startOffset);
            const globalEndOffset = computeGlobalOffset(range.endContainer, range.endOffset);

            // 使用选区中间位置,更容易命中标签
            const globalOffset = Math.floor((globalStartOffset + globalEndOffset) / 2);

            const BOUNDARY_CHARS = new Set([',', '\n', '{', '}', '[', ']', '|']);

            let start = globalOffset;
            while (start > 0 && !BOUNDARY_CHARS.has(fullText[start - 1])) { start--; }
            let end = globalOffset;
            while (end < fullText.length && !BOUNDARY_CHARS.has(fullText[end])) { end++; }

            if (BOUNDARY_CHARS.has(fullText[start])) start++;
            if (end > 0 && BOUNDARY_CHARS.has(fullText[end - 1])) end--;

            const tagText = fullText.slice(start, end).trim();
            return tagText ? { tagText, start, end, fullText, cursorOffset: globalOffset } : null;
        }
        function updateInputContent(inputElement, newContent, selectionStart, selectionEnd, keepSelection = false) {
            window.__NAIWeightAdjusting = true;
            window.__NAIWeightAdjustingUntil = Date.now() + 500;
            try {
                if (inputElement.childNodes.length === 1 && inputElement.firstChild.nodeType === 3) {
                    inputElement.firstChild.textContent = newContent;
                } else {
                    const newTextNode = document.createTextNode(newContent);
                    inputElement.innerHTML = '';
                    inputElement.appendChild(newTextNode);
                }
                const newRange = document.createRange();
                const textLength = inputElement.firstChild.textContent.length;
                if (keepSelection) {
                    const safeStart = Math.max(0, Math.min(textLength, selectionStart));
                    const safeEnd = Math.max(0, Math.min(textLength, selectionEnd));
                    newRange.setStart(inputElement.firstChild, safeStart);
                    newRange.setEnd(inputElement.firstChild, safeEnd);
                } else {
                    const safeIndex = Math.max(0, Math.min(textLength, selectionEnd));
                    newRange.setStart(inputElement.firstChild, safeIndex);
                    newRange.setEnd(inputElement.firstChild, safeIndex);
                }
                const selection = window.getSelection();
                selection.removeAllRanges();
                selection.addRange(newRange);
                const inputEvent = new Event('input', { bubbles: true });
                inputElement.dispatchEvent(inputEvent);
            } finally { window.__NAIWeightAdjusting = false; }
        }
        function parsePromptStructure(text) {
            return parseSequence(text, 0, false, 0).items;
        }
        function parseSequence(text, startIndex, stopAtClose, depth = 0) {
            const MAX_DEPTH = 20;
            if (depth > MAX_DEPTH) {
                return { items: [], index: text.length };
            }
            const items = [];
            let i = startIndex;
            const readSeparator = (input, index, shouldStopAtClose) => {
                let cursor = index; let separator = '';
                while (cursor < input.length) {
                    if (shouldStopAtClose && input.startsWith('::', cursor)) { break; }
                    const ch = input[cursor];
                    if (ch === ',') { separator += ch; cursor++; while (cursor < input.length && input[cursor] === ' ') { separator += input[cursor]; cursor++; } }
                    else if (ch === '\n' || ch === '\r') { separator += ch; cursor++; }
                    else if (ch === ' ' || ch === '\t') { separator += ch; cursor++; }
                    else { break; }
                }
                return { separator, nextIndex: cursor };
            };
            while (i < text.length) {
                if (stopAtClose && text.startsWith('::', i)) { return { items, index: i + 2, closeStart: i, closeEnd: i + 2 }; }
                while (i < text.length && (text[i] === ' ' || text[i] === '\t')) { i++; }
                if (stopAtClose && text.startsWith('::', i)) { return { items, index: i + 2, closeStart: i, closeEnd: i + 2 }; }
                if (i >= text.length) { break; }

                // 检查 || ... || 随机组
                if (text.startsWith('||', i)) {
                    const randomStart = i;
                    i += 2;
                    let randomEnd = i;
                    while (randomEnd < text.length && !text.startsWith('||', randomEnd)) {
                        randomEnd++;
                    }
                    if (randomEnd < text.length) {
                        const randomContent = text.slice(i, randomEnd);
                        const childResult = parseSequence(randomContent, 0, false, depth + 1);
                        const randomNode = { type: 'random', content: randomContent, start: randomStart, contentStart: i, contentEnd: randomEnd, end: randomEnd + 2, children: childResult.items };
                        i = randomEnd + 2;
                        const separatorInfo = readSeparator(text, i, stopAtClose);
                        i = separatorInfo.nextIndex;
                        items.push({ node: randomNode, separator: separatorInfo.separator });
                        continue;
                    }
                }

                // 检查 { ... } 大括号权重
                if (text[i] === '{') {
                    const braceStart = i;
                    i += 1;
                    let braceDepth = 1;
                    let braceEnd = i;
                    while (braceEnd < text.length && braceDepth > 0) {
                        if (text[braceEnd] === '{') braceDepth++;
                        else if (text[braceEnd] === '}') braceDepth--;
                        if (braceDepth > 0) braceEnd++;
                    }
                    if (braceDepth === 0) {
                        const braceContent = text.slice(i, braceEnd);
                        const childResult = parseSequence(braceContent, 0, false, depth + 1);
                        const braceNode = { type: 'brace', content: braceContent, start: braceStart, contentStart: i, contentEnd: braceEnd, end: braceEnd + 1, children: childResult.items };
                        i = braceEnd + 1;
                        const separatorInfo = readSeparator(text, i, stopAtClose);
                        i = separatorInfo.nextIndex;
                        items.push({ node: braceNode, separator: separatorInfo.separator });
                        continue;
                    }
                }

                // 检查 [ ... ] 方括号减权
                if (text[i] === '[') {
                    const bracketStart = i;
                    i += 1;
                    let bracketDepth = 1;
                    let bracketEnd = i;
                    while (bracketEnd < text.length && bracketDepth > 0) {
                        if (text[bracketEnd] === '[') bracketDepth++;
                        else if (text[bracketEnd] === ']') bracketDepth--;
                        if (bracketDepth > 0) bracketEnd++;
                    }
                    if (bracketDepth === 0) {
                        const bracketContent = text.slice(i, bracketEnd);
                        const childResult = parseSequence(bracketContent, 0, false, depth + 1);
                        const bracketNode = { type: 'bracket', content: bracketContent, start: bracketStart, contentStart: i, contentEnd: bracketEnd, end: bracketEnd + 1, children: childResult.items };
                        i = bracketEnd + 1;
                        const separatorInfo = readSeparator(text, i, stopAtClose);
                        i = separatorInfo.nextIndex;
                        items.push({ node: bracketNode, separator: separatorInfo.separator });
                        continue;
                    }
                }

                const doubleColonMatch = text.slice(i).match(/^(-?\d+(?:\.\d+)?)(::)(?!:)/);
                if (doubleColonMatch) {
                    const groupStart = i;
                    const weightString = doubleColonMatch[1];
                    const parsedWeight = parseFloat(weightString);
                    i += doubleColonMatch[0].length;
                    const childResult = parseSequence(text, i, true, depth + 1);
                    const groupNode = { type: 'group', weight: isNaN(parsedWeight) ? 1.0 : parsedWeight, format: 'doubleColon', start: groupStart, weightStart: groupStart, weightEnd: groupStart + weightString.length, prefixEnd: i, closeStart: childResult.closeStart ?? childResult.index, closeEnd: childResult.closeEnd ?? childResult.index, children: childResult.items };
                    i = childResult.index;
                    const separatorInfo = readSeparator(text, i, stopAtClose);
                    i = separatorInfo.nextIndex;
                    items.push({ node: groupNode, separator: separatorInfo.separator });
                    continue;
                }
                const groupMatch = text.slice(i).match(/^(-?\d+(?:\.\d+)?)(\s*):(?!:)/);
                if (groupMatch) {
                    const groupStart = i;
                    const weightString = groupMatch[1];
                    const parsedWeight = parseFloat(weightString);
                    i += groupMatch[0].length;
                    const childResult = parseSequence(text, i, true, depth + 1);
                    const groupNode = { type: 'group', weight: isNaN(parsedWeight) ? 1.0 : parsedWeight, format: 'singleColon', start: groupStart, weightStart: groupStart, weightEnd: groupStart + weightString.length, prefixEnd: i, closeStart: childResult.closeStart ?? childResult.index, closeEnd: childResult.closeEnd ?? childResult.index, children: childResult.items };
                    i = childResult.index;
                    const separatorInfo = readSeparator(text, i, stopAtClose);
                    i = separatorInfo.nextIndex;
                    items.push({ node: groupNode, separator: separatorInfo.separator });
                    continue;
                }
                const tagRawStart = i;
                while (i < text.length && text[i] !== ',' && text[i] !== '\n' && text[i] !== '\r' && text[i] !== '{' && text[i] !== '}' && text[i] !== '[' && text[i] !== ']' && text[i] !== '|') {
                    if (stopAtClose && text.startsWith('::', i)) { break; }
                    if (i === tagRawStart) {
                        const aheadMatch = text.slice(i).match(/^(-?\d+(?:\.\d+)?)(\s*):(?!:)/);
                        if (aheadMatch) { break; }
                    }
                    i++;
                }
                const rawSegment = text.slice(tagRawStart, i);
                if (rawSegment.trim()) {
                    let leadingSpaces = 0;
                    while (leadingSpaces < rawSegment.length && /\s/.test(rawSegment[leadingSpaces])) { leadingSpaces++; }
                    let trailingSpaces = 0;
                    while (trailingSpaces < rawSegment.length - leadingSpaces && /\s/.test(rawSegment[rawSegment.length - 1 - trailingSpaces])) { trailingSpaces++; }
                    const contentStart = tagRawStart + leadingSpaces;
                    const contentEnd = i - trailingSpaces;
                    const tagText = text.slice(contentStart, contentEnd);
                    const tagNode = { type: 'tag', text: tagText, start: contentStart, end: contentEnd };
                    const separatorInfo = readSeparator(text, i, stopAtClose);
                    i = separatorInfo.nextIndex;
                    items.push({ node: tagNode, separator: separatorInfo.separator });
                } else {
                    const separatorInfo = readSeparator(text, i, stopAtClose);
                    i = separatorInfo.nextIndex;
                    if (items.length) { items[items.length - 1].separator += separatorInfo.separator; }
                    if (i === tagRawStart && i < text.length) { i++; }
                }
            }
            return { items, index: i };
        }
        function normalizeSequence(sequence) {
            for (let i = 0; i < sequence.length; i++) {
                sequence[i].separator = i < sequence.length - 1 ? ', ' : '';
                if (sequence[i].node.type === 'group') { normalizeSequence(sequence[i].node.children); }
            }
        }
        function normalizeTree(sequence) { normalizeSequence(sequence); }
        function formatWeightValue(value) {
            const rounded = Math.round(value * 20) / 20;
            let str = rounded.toFixed(2);
            return str.replace(/\.?0+$/, '');
        }
        function serializeTree(sequence) {
            const state = { output: '', offset: 0 };
            serializeSequence(sequence, state);
            return state.output;
        }
        function serializeSequence(sequence, state) {
            for (let i = 0; i < sequence.length; i++) {
                serializeNode(sequence[i].node, state);
                const sep = sequence[i].separator || '';
                state.output += sep;
                state.offset += sep.length;
            }
        }
        function serializeNode(node, state) {
            if (node.type === 'tag') {
                node.serializeStart = state.offset;
                node.serializeEnd = state.offset + node.text.length;
                state.output += node.text;
                state.offset += node.text.length;
            } else if (node.type === 'brace') {
                node.serializeStart = state.offset;
                state.output += '{';
                state.offset += 1;
                state.output += node.content;
                state.offset += node.content.length;
                state.output += '}';
                state.offset += 1;
                node.serializeEnd = state.offset;
            } else if (node.type === 'bracket') {
                node.serializeStart = state.offset;
                state.output += '[';
                state.offset += 1;
                state.output += node.content;
                state.offset += node.content.length;
                state.output += ']';
                state.offset += 1;
                node.serializeEnd = state.offset;
            } else if (node.type === 'random') {
                node.serializeStart = state.offset;
                state.output += '||';
                state.offset += 2;
                state.output += node.content;
                state.offset += node.content.length;
                state.output += '||';
                state.offset += 2;
                node.serializeEnd = state.offset;
            } else if (node.type === 'group') {
                node.serializeStart = state.offset;
                const weightStr = formatWeightValue(node.weight);
                const format = node.format || 'singleColon';
                if (format === 'doubleColon') {
                    state.output += `${weightStr}::`;
                    state.offset += weightStr.length + 2;
                } else {
                    state.output += `${weightStr}:`;
                    state.offset += weightStr.length + 1;
                }
                const childrenStart = state.output.length;
                serializeSequence(node.children, state);
                // Fix: if content inside group ends with digits (as part of a tag name),
                // NAI would misinterpret trailing digits + :: as a weight prefix.
                // Insert a comma to prevent this, e.g. "style1999" -> "style1999," before "::"
                const childrenContent = state.output.slice(childrenStart);
                if (/\d$/.test(childrenContent) && /[a-zA-Z_]/.test(childrenContent)) {
                    state.output += ',';
                    state.offset += 1;
                }
                state.output += '::';
                state.offset += 2;
                node.serializeEnd = state.offset;
            }
        }
        function findFirstTagInSequence(sequence, ancestors = [], depth = 0) {
            const MAX_DEPTH = 20;
            if (depth > MAX_DEPTH) return null;
            for (let i = 0; i < sequence.length; i++) {
                const item = sequence[i];
                const node = item.node;
                if (node.type === 'tag') {
                    return { item, sequence, index: i, ancestors };
                } else if (node.type === 'group') {
                    const result = findFirstTagInSequence(node.children,
                        ancestors.concat({ groupNode: node, entry: item, parentSequence: sequence, index: i }), depth + 1);
                    if (result) return result;
                } else if (node.type === 'brace') {
                    const result = findFirstTagInSequence(node.children,
                        ancestors.concat({ braceNode: node, entry: item, parentSequence: sequence, index: i }), depth + 1);
                    if (result) return result;
                } else if (node.type === 'bracket') {
                    const result = findFirstTagInSequence(node.children,
                        ancestors.concat({ bracketNode: node, entry: item, parentSequence: sequence, index: i }), depth + 1);
                    if (result) return result;
                } else if (node.type === 'random') {
                    const result = findFirstTagInSequence(node.children,
                        ancestors.concat({ randomNode: node, entry: item, parentSequence: sequence, index: i }), depth + 1);
                    if (result) return result;
                }
            }
            return null;
        }
        function findTagNodeByOffset(sequence, offset, ancestors = [], depth = 0) {
            const MAX_DEPTH = 20;
            if (depth > MAX_DEPTH) {
                return null;
            }
            for (let i = 0; i < sequence.length; i++) {
                const item = sequence[i];
                const node = item.node;
                if (node.type === 'tag') {
                    if (node.start <= offset && offset <= node.end) { return { item, sequence, index: i, ancestors }; }
                } else if (node.type === 'brace') {
                    if (node.start <= offset && offset <= node.end) {
                        const childOffset = offset - node.contentStart;
                        const result = findTagNodeByOffset(node.children, childOffset, ancestors.concat({ braceNode: node, entry: item, parentSequence: sequence, index: i }), depth + 1);
                        if (result) return result;
                    }
                } else if (node.type === 'bracket') {
                    if (node.start <= offset && offset <= node.end) {
                        const childOffset = offset - node.contentStart;
                        const result = findTagNodeByOffset(node.children, childOffset, ancestors.concat({ bracketNode: node, entry: item, parentSequence: sequence, index: i }), depth + 1);
                        if (result) return result;
                    }
                } else if (node.type === 'random') {
                    if (node.start <= offset && offset <= node.end) {
                        const childOffset = offset - node.contentStart;
                        const result = findTagNodeByOffset(node.children, childOffset, ancestors.concat({ randomNode: node, entry: item, parentSequence: sequence, index: i }), depth + 1);
                        if (result) return result;
                    }
                } else if (node.type === 'group') {
                    const groupEnd = node.closeEnd ?? node.closeStart ?? Infinity;
                    const newAncestors = ancestors.concat({ groupNode: node, entry: item, parentSequence: sequence, index: i });
                    // 先尝试精确匹配
                    const result = findTagNodeByOffset(node.children, offset, newAncestors, depth + 1);
                    if (result) { return result; }
                    // 如果在 group 范围内但没有精确匹配,返回第一个 tag
                    if (node.start <= offset && offset <= groupEnd) {
                        const firstTag = findFirstTagInSequence(node.children, newAncestors, depth + 1);
                        if (firstTag) return firstTag;
                    }
                }
            }
            return null;
        }
        function removeGroupWrapper(groupInfo) {
            const { parentSequence, index, groupNode } = groupInfo;
            const children = groupNode.children;
            parentSequence.splice(index, 1, ...children);
        }
        function wrapTagWithGroup(target, newWeight) {
            const { sequence, index, item } = target;
            const tagNode = item.node, originalSeparator = item.separator || '';
            sequence.splice(index, 1);
            const groupNode = { type: 'group', weight: newWeight, format: 'doubleColon', children: [{ node: tagNode, separator: '' }] };
            sequence.splice(index, 0, { node: groupNode, separator: originalSeparator });
            return groupNode;
        }
        function adjustWeightForTag(target, direction) {
            const step = 0.05;
            const tagNode = target.item.node;
            const ancestors = target.ancestors;
            // 找到最近的 group 祖先(忽略 brace 和 random)
            let groupAncestorIndex = -1;
            for (let i = ancestors.length - 1; i >= 0; i--) {
                if (ancestors[i].groupNode) {
                    groupAncestorIndex = i;
                    break;
                }
            }
            if (groupAncestorIndex >= 0) {
                const outerGroupInfo = ancestors[groupAncestorIndex];
                const groupNode = outerGroupInfo.groupNode;
                let newWeight = groupNode.weight + (direction * step);
                newWeight = Math.round(newWeight * 20) / 20;
                if (Math.abs(newWeight - 1.0) < 0.001) {
                    removeGroupWrapper(outerGroupInfo);
                    return { tagNode, wrapperNode: null };
                }
                else {
                    groupNode.weight = newWeight;
                    return { tagNode, wrapperNode: groupNode };
                }
            }
            let newWeight = 1.0 + (direction * step);
            newWeight = Math.round(newWeight * 20) / 20;
            if (Math.abs(newWeight - 1.0) < 0.001) { return { tagNode, wrapperNode: null }; }
            const newGroupNode = wrapTagWithGroup(target, newWeight);
            return { tagNode, wrapperNode: newGroupNode };
        }
        function resolveMovementContext(target) {
            let { sequence, index, item, ancestors } = target;
            let movedAsGroup = false;
            if (ancestors.length) {
                const immediate = ancestors[ancestors.length - 1];
                const immediateNode = immediate.groupNode || immediate.braceNode || immediate.bracketNode || immediate.randomNode;
                if (immediateNode && immediateNode.children && immediateNode.children.length === 1) {
                    movedAsGroup = true;
                    sequence = immediate.parentSequence;
                    index = immediate.index;
                    item = immediate.entry;
                    ancestors = ancestors.slice(0, -1);
                }
            }
            return { sequence, index, item, ancestors, movedAsGroup };
        }
        function moveTagWithinStructure(target, direction) {
            const context = resolveMovementContext(target);
            const { sequence, index, item, ancestors } = context;
            const isOriginalWeighted = target.ancestors.length > 0;
            if (direction === -1) {
                if (index > 0) {
                    const prevItem = sequence[index - 1];
                    if (!isOriginalWeighted && prevItem.node.type === 'group') {
                        sequence.splice(index, 1);
                        item.separator = '';
                        prevItem.node.children.push(item);
                        return true;
                    }
                    sequence[index - 1] = item;
                    sequence[index] = prevItem;
                    return true;
                }
                if (!ancestors.length) { return false; }
                const parentContext = ancestors[ancestors.length - 1];
                const { parentSequence, index: parentIndex } = parentContext;
                sequence.splice(index, 1);
                if (sequence.length === 0) {
                    parentSequence.splice(parentIndex, 1);
                    parentSequence.splice(parentIndex, 0, item);
                } else { parentSequence.splice(parentIndex, 0, item); }
                return true;
            }
            if (direction === 1) {
                if (index < sequence.length - 1) {
                    const nextItem = sequence[index + 1];
                    if (!isOriginalWeighted && nextItem.node.type === 'group') {
                        sequence.splice(index, 1);
                        item.separator = '';
                        nextItem.node.children.unshift(item);
                        return true;
                    }
                    sequence[index + 1] = item;
                    sequence[index] = nextItem;
                    return true;
                }
                if (!ancestors.length) { return false; }
                const parentContext = ancestors[ancestors.length - 1];
                const { parentSequence, index: parentIndex } = parentContext;
                sequence.splice(index, 1);
                if (sequence.length === 0) {
                    parentSequence.splice(parentIndex, 1);
                    parentSequence.splice(parentIndex, 0, item);
                } else { parentSequence.splice(parentIndex + 1, 0, item); }
                return true;
            }
            return false;
        }
        function handleKeydown(event) {
            const inputElement = getActiveInputElement();
            if (!inputElement || !event.ctrlKey) return;
            if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
                event.preventDefault(); event.stopPropagation();
                const tagInfo = getSelectedTagInfo(inputElement); if (!tagInfo) return;
                const direction = (event.key === 'ArrowUp') ? 1 : -1;
                const leadingWhitespace = tagInfo.fullText.slice(tagInfo.start, tagInfo.end).match(/^\s*/)[0];
                const structure = parsePromptStructure(tagInfo.fullText);
                const offsetForSearch = tagInfo.cursorOffset ?? (tagInfo.start + leadingWhitespace.length);
                const target = findTagNodeByOffset(structure, offsetForSearch); if (!target) { return; }
                const { tagNode, wrapperNode } = adjustWeightForTag(target, direction);
                normalizeTree(structure);
                const serialized = serializeTree(structure);
                let selectionStart, selectionEnd;
                if (keepSelectionAfterAdjustment) {
                    if (wrapperNode) {
                        selectionStart = wrapperNode.serializeStart;
                        selectionEnd = wrapperNode.serializeEnd;
                    } else {
                        selectionStart = tagNode.serializeStart ?? (tagInfo.start + leadingWhitespace.length);
                        selectionEnd = tagNode.serializeEnd ?? (selectionStart + tagNode.text.length);
                    }
                } else {
                    selectionStart = tagNode.serializeEnd ?? (tagInfo.start + leadingWhitespace.length + tagNode.text.length);
                    selectionEnd = selectionStart;
                }
                updateInputContent(inputElement, serialized, selectionStart, selectionEnd, keepSelectionAfterAdjustment);
            } else if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
                event.preventDefault(); event.stopPropagation();
                const tagInfo = getSelectedTagInfo(inputElement); if (!tagInfo) return;
                const direction = (event.key === 'ArrowLeft') ? -1 : 1;
                const leadingWhitespace = tagInfo.fullText.slice(tagInfo.start, tagInfo.end).match(/^\s*/)[0];
                const structure = parsePromptStructure(tagInfo.fullText);
                const offsetForSearch = tagInfo.cursorOffset ?? (tagInfo.start + leadingWhitespace.length);
                const target = findTagNodeByOffset(structure, offsetForSearch); if (!target) { return; }
                const tagNode = target.item.node;
                if (!moveTagWithinStructure(target, direction)) { return; }
                normalizeTree(structure);
                const serialized = serializeTree(structure);
                let selectionStart, selectionEnd;
                if (keepSelectionAfterAdjustment) {
                    selectionStart = tagNode.serializeStart ?? (tagInfo.start + leadingWhitespace.length);
                    selectionEnd = tagNode.serializeEnd ?? (selectionStart + tagNode.text.length);
                } else {
                    selectionStart = tagNode.serializeEnd ?? (tagInfo.start + leadingWhitespace.length + tagNode.text.length);
                    selectionEnd = selectionStart;
                }
                updateInputContent(inputElement, serialized, selectionStart, selectionEnd, keepSelectionAfterAdjustment);
            }
        }
        function init() {
            const checkInterval = setInterval(() => {
                const inputElement = getActiveInputElement();
                if (inputElement) { clearInterval(checkInterval); document.addEventListener('keydown', handleKeydown, true); }
            }, 500);
        }
        return { init, parsePromptStructure, normalizeTree, serializeTree, updateInputContent };
    })();
    function init() {
        TagAssist.init();
        WeightShortcuts.init();
    }
    if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); }
    else { init(); }
})();