NovelAI 快捷键权重调整

通过Ctrl+↑/↓快速调整输入光标所在的Tag权重。支持所有Prompt输入框。

// ==UserScript==
// @name         NovelAI 快捷键权重调整
// @namespace    https://novelai.net
// @match        https://novelai.net/image
// @icon         https://novelai.net/_next/static/media/goose_blue.1580a990.svg
// @license      MIT
// @version      1.5
// @author       Takoro
// @description  通过Ctrl+↑/↓快速调整输入光标所在的Tag权重。支持所有Prompt输入框。
// ==/UserScript==

(function() {
    'use strict';

    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 node = range.startContainer;
        const offset = range.startOffset;

        const fullText = inputElement.textContent || '';

        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 {
            globalOffset = offset;
        }

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

        if (fullText[start] === ',' || fullText[start] === '\n') start++;
        if (fullText[end - 1] === ',') end--;

        const tagText = fullText.slice(start, end).trim();
        return tagText ? { tagText, start, end, fullText } : null;
    }

    // 权重解析函数
    function parseWeight(text) {
        const cleanText = text.replace(/:{2,}/g, '::').replace(/:+$/, '');
        const weightMatch = cleanText.match(/^(-?[\d.]+)::(.+?)(?:::|$)/);

        if (weightMatch) {
            const weight = parseFloat(weightMatch[1]);
            return {
                weight: isNaN(weight) ? 1.0 : weight,
                tag: weightMatch[2].trim()
            };
        }

        return { weight: 1.0, tag: text.trim() };
    }

    function adjustWeight(text, direction) {
        const { weight, tag } = parseWeight(text);
        let newWeight = weight + (direction * 0.1);
        newWeight = Math.round(newWeight * 10) / 10;

        if (Math.abs(newWeight - 1.0) < 0.001) {
            return tag;
        }

        return `${newWeight}::${tag}::`;
    }

    function modifyInputText(inputElement, newText, start, end, fullText) {
        const newContent = fullText.slice(0, start) + newText + fullText.slice(end);

        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();
        newRange.setStart(inputElement.firstChild, start);
        newRange.setEnd(inputElement.firstChild, start + newText.length);

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

        const inputEvent = new Event('input', { bubbles: true });
        inputElement.dispatchEvent(inputEvent);
    }

    function handleKeydown(event) {
        const inputElement = getActiveInputElement();
        if (!inputElement) return;

        if (!event.ctrlKey || (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')) return;

        event.preventDefault();
        event.stopPropagation();

        const tagInfo = getSelectedTagInfo(inputElement);
        if (!tagInfo) return;

        const direction = (event.key === 'ArrowUp') ? 1 : -1;
        const newText = adjustWeight(tagInfo.tagText, direction);

        modifyInputText(inputElement, newText, tagInfo.start, tagInfo.end, tagInfo.fullText);
    }

    function init() {
        const checkInterval = setInterval(() => {
            const inputElement = getActiveInputElement();
            if (inputElement) {
                clearInterval(checkInterval);
                document.addEventListener('keydown', handleKeydown, true);
            }
        }, 500);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。