NovelAI Image Generator Enhancements

Enhancements for NovelAI image generator: A1111->NovelAI syntax button, Reference Strength slider patch, and more.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         NovelAI Image Generator Enhancements
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  Enhancements for NovelAI image generator: A1111->NovelAI syntax button, Reference Strength slider patch, and more.
// @author       OpenAI
// @match        https://novelai.net/image*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    console.log('[NovelAI Enhancer] Script loaded');

    // --- A1111 to NovelAI Syntax Button ---
    function a1111ToNovelAI(text) {
        text = text.replace(/\(([^:()]+):([0-9.]+)\)/g, '$2::$1::');
        text = text.replace(/\\\(/g, '(').replace(/\\\)/g, ')');
        text = text.replace(/_/g, ' ');
        return text;
    }

    // --- Updated Syntax Button Insertion (robust, no compiled class) ---
    function insertSyntaxButton() {
        // Find the prompt input box (anchor)
        const promptBox = document.querySelector('.prompt-input-box-prompt');
        if (!promptBox) return;
        // Get its previous sibling (the container with the buttons)
        const container = promptBox.previousElementSibling;
        if (!container) return;
        // Find the settings row: flex row with gap: 10px and align-items: center
        const settingsRow = Array.from(container.querySelectorAll('div[style*="flex-direction: row"]')).find(div =>
            div.style.gap === '10px' && div.style.alignItems === 'center'
        );
        if (!settingsRow) return;
        // Find the settings icon container robustly
        const settingsIconDiv = Array.from(settingsRow.children).find(child => {
            if (!child.querySelector) return false;
            const btn = child.querySelector('button');
            if (!btn) return false;
            const iconDiv = btn.querySelector('div');
            if (!iconDiv) return false;
            const style = window.getComputedStyle(iconDiv);
            return style.height === '16px' && style.width === '16px' && iconDiv.className.includes('htrggD');
        });
        if (!settingsIconDiv) return;
        // Avoid double-inserting
        if (settingsRow.querySelector('.syntax-convert-btn')) return;

        const btn = document.createElement('button');
        btn.className = 'syntax-convert-btn';
        btn.style.height = '32px';
        btn.style.width = '32px';
        btn.style.display = 'flex';
        btn.style.alignItems = 'center';
        btn.style.justifyContent = 'center';
        btn.style.padding = '0';
        btn.style.marginRight = '5px';
        btn.style.background = 'transparent';
        btn.style.border = 'none';
        btn.innerHTML = `
            <i class="fa-solid fa-right-left" style="color: #ebdab2; font-size: 16px; background-color: none;"></i>
        `;
        btn.title = 'Convert from A1111 to NovelAI syntax';
        btn.onclick = function() {
            document.querySelectorAll('.ProseMirror').forEach(editor => {
                const ps = Array.from(editor.querySelectorAll('p'));
                ps.forEach(p => {
                    if (!p.textContent.trim()) return;
                    p.textContent = a1111ToNovelAI(p.textContent);
                });
            });
        };
        btn.style.cursor = 'pointer';
        settingsRow.insertBefore(btn, settingsIconDiv); // Insert before the settings icon
        // Inject Font Awesome if needed
        if (!document.getElementById('fa-cdn')) {
            const link = document.createElement('link');
            link.id = 'fa-cdn';
            link.rel = 'stylesheet';
            link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css';
            document.head.appendChild(link);
        }
        // Add tooltip CSS if not present
        if (!document.getElementById('syntax-tooltip-style')) {
            const style = document.createElement('style');
            style.id = 'syntax-tooltip-style';
            style.textContent = `
            .syntax-tooltip-popper {
                background: rgb(60, 56, 54);
                color: rgb(235, 218, 178);
                font-size: 14.4px;
                font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
                font-weight: 400;
                line-height: 21.6px;
                max-width: 300px;
                padding: 8px;
                border: 2px solid rgb(60, 56, 54);
                border-radius: 1px;
                box-sizing: border-box;
                text-align: center;
                word-break: break-word;
                z-index: 9999;
                pointer-events: none;
                opacity: 1;
                transition: none;
            }
            .syntax-tooltip-fade {
                opacity: 0;
                transition: opacity 0.2s ease-in-out;
            }
            .syntax-tooltip-fade.syntax-tooltip-visible {
                opacity: 1;
            }
            .syntax-tooltip-popper p {
                margin: 5px;
            }
            .syntax-tooltip-arrow {
                position: absolute;
                left: 50%;
                bottom: -8px;
                transform: translateX(-50%);
                width: 0;
                height: 0;
                pointer-events: none;
            }
            .syntax-tooltip-arrow-border {
                position: absolute;
                left: 50%;
                transform: translateX(-50%);
                border-left: 9px solid transparent;
                border-right: 9px solid transparent;
                border-top: 9px solid rgb(60,56,54);
                bottom: 0;
                width: 0;
                height: 0;
            }
            .syntax-tooltip-arrow-bg {
                position: absolute;
                left: 50%;
                transform: translateX(-50%);
                border-left: 8px solid transparent;
                border-right: 8px solid transparent;
                border-top: 8px solid #222;
                bottom: 1px;
                width: 0;
                height: 0;
            }
            `;
            document.head.appendChild(style);
        }
        // Add beautiful tooltip on hover (like settings button)
        btn.addEventListener('mouseenter', function() {
            // Remove any existing tooltip
            let old = document.getElementById('syntax-tooltip');
            if (old) old.remove();
            // Create tooltip
            const tooltip = document.createElement('div');
            tooltip.className = 'syntax-tooltip-popper syntax-tooltip-fade';
            tooltip.id = 'syntax-tooltip';
            tooltip.style.position = 'fixed';
            tooltip.style.visibility = 'hidden';
            tooltip.innerHTML = '<p>Convert A1111 prompt to NovelAI syntax</p>' +
                '<div class="syntax-tooltip-arrow"><div class="syntax-tooltip-arrow-border" style="border-top-color: rgb(60,56,54);"></div><div class="syntax-tooltip-arrow-bg" style="border-top-color: rgb(60,56,54);"></div></div>';
            document.body.appendChild(tooltip);
            // Position above the inner icon div
            const icon = btn.querySelector('div');
            const rect = icon ? icon.getBoundingClientRect() : btn.getBoundingClientRect();
            const tooltipHeight = tooltip.offsetHeight;
            tooltip.style.left = `${rect.left + window.scrollX + rect.width/2 - tooltip.offsetWidth/2}px`;
            tooltip.style.top = `${rect.top + window.scrollY - tooltipHeight - 8}px`;
            tooltip.style.visibility = 'visible';
            setTimeout(() => { tooltip.classList.add('syntax-tooltip-visible'); }, 10);
        });
        btn.addEventListener('mouseleave', function() {
            let old = document.getElementById('syntax-tooltip');
            if (old) old.remove();
        });
    }

    function observeSyntaxButton() {
        const observer = new MutationObserver(() => {
            insertSyntaxButton();
        });
        observer.observe(document.body, { childList: true, subtree: true });
        insertSyntaxButton();
    }

    // --- Reference Strength Slider Patch ---
    // New: Find the Reference Strength sliders by traversing from the 'Normalize Reference Strength Values' label
    function findRefStrengthSliders() {
        // Find the span with the exact text 'Normalize Reference Strength Values'
        const labelSpan = Array.from(document.querySelectorAll('span')).find(
            span => span.textContent.trim() === 'Normalize Reference Strength Values'
        );
        if (!labelSpan) return [];
        // Go up to the label, then to the container div
        let container = labelSpan.closest('div');
        // Go up to the nearest parent that contains both the label and the sliders (likely 2-3 levels up)
        for (let i = 0; i < 4 && container; ++i) {
            // Heuristic: look for a container with at least one input[type="range"]
            if (container.querySelector('input[type="range"]')) break;
            container = container.parentElement;
        }
        if (!container) return [];
        // Find all input[type="range"] inside this container
        return Array.from(container.querySelectorAll('input[type="range"]'));
    }

    function patchRefStrengthSlider(slider) {
        slider.min   = -1;
        slider.max   =  1;
        slider.step  =  0.01;
        let val = parseFloat(slider.value);
        if (val > slider.max) slider.value = slider.max;
        if (val < slider.min) slider.value = slider.min;
    }

    function patchAllRefStrengthSliders() {
        findRefStrengthSliders().forEach(slider => patchRefStrengthSlider(slider));
    }

    function observeRefStrengthSliders() {
        new MutationObserver(muts => {
            muts.forEach(m => {
                m.addedNodes.forEach(node => {
                    if (node.nodeType !== 1) return;
                    // If the node is a slider in the right container, patch it
                    if (node.matches && node.matches('input[type="range"]')) {
                        if (findRefStrengthSliders().includes(node)) patchRefStrengthSlider(node);
                    }
                    // Or if it contains sliders, patch them
                    node.querySelectorAll && node.querySelectorAll('input[type="range"]').forEach(slider => {
                        if (findRefStrengthSliders().includes(slider)) patchRefStrengthSlider(slider);
                    });
                });
            });
        }).observe(document.body, { childList: true, subtree: true });
        patchAllRefStrengthSliders();
    }

    // --- Smart Weight-Enhancer Module (Global Delegation Debug) ---
    function setupWeightEnhancer() {
        console.log('[NovelAI Enhancer] setupWeightEnhancer called');
        try {
            // Regex for weight::tag
            const WEIGHT_TAG_REGEX = /^(\d+(?:\.\d+)?)::([^:]+)$/;
            // Helper: is intensity span
            function isIntensitySpan(span) {
                if (!span || span.nodeType !== 1) return false;
                return Array.from(span.classList).some(cls => cls.includes('intensity-color-'));
            }
            // Helper: is mid-intensity '::' span
            function isMidIntensityColonSpan(span) {
                return span && span.nodeType === 1 && Array.from(span.classList).some(cls => cls === 'mid-intensity-color-20') && span.textContent === '::';
            }
            const proseMirrors = document.querySelectorAll('.ProseMirror');
            console.log('[NovelAI Enhancer] Found .ProseMirror elements:', proseMirrors);
            let tooltip = null;
            let current = null;
            function cleanup() {
                if (tooltip) { tooltip.remove(); tooltip = null; }
                current = null;
            }
            document.body.addEventListener('mouseover', function(e) {
                try {
                    let proseMirror = e.target.closest && e.target.closest('.ProseMirror');
                    if (!proseMirror) return;
                    cleanup();
                    let node = e.target;
                    if (node.nodeType === 1 && node.tagName === 'SPAN') {
                        let text = node.textContent;
                        let match = text.match(/^(\d+(?:\.\d+)?)(::)(.*)$/);
                        if (match) {
                            let weight = match[1];
                            let after = match[3];
                            let tag = after;
                            let tagEndNode = node;
                            if (!after) {
                                let p = node.closest ? node.closest('p') : node.parentElement.closest('p');
                                if (p) {
                                    let nodes = Array.from(p.childNodes);
                                    let idx = nodes.indexOf(node);
                                    let j = idx + 1;
                                    while (j < nodes.length && nodes[j].nodeType === 3 && !nodes[j].textContent.trim()) ++j;
                                    if (j < nodes.length && nodes[j].nodeType === 3) {
                                        tag = nodes[j].textContent;
                                        tagEndNode = nodes[j];
                                    }
                                }
                            }
                            // Show tooltip
                            tooltip = document.createElement('div');
                            tooltip.className = 'syntax-tooltip-popper';
                            tooltip.id = 'syntax-tooltip';
                            tooltip.style.position = 'fixed';
                            tooltip.style.visibility = 'hidden';
                            tooltip.style.background = '#222';
                            tooltip.style.border = '2px solid rgb(60, 56, 54)';
                            tooltip.style.color = 'rgb(235, 218, 178)';
                            tooltip.innerHTML = `<span>Weight: ${weight}</span><div class="syntax-tooltip-arrow"><div class="syntax-tooltip-arrow-border"></div><div class="syntax-tooltip-arrow-bg"></div></div>`;
                            document.body.appendChild(tooltip);
                            let rect = node.getBoundingClientRect();
                            tooltip.style.left = `${rect.left + window.scrollX + rect.width/2 - tooltip.offsetWidth/2}px`;
                            tooltip.style.top = `${rect.top + window.scrollY - tooltip.offsetHeight - 8}px`;
                            tooltip.style.visibility = 'visible';
                            current = { node, tagEndNode, weight, tag, match, isSplit: !after };
                            return;
                        }
                    } else if (node.nodeType === 3 && (node.parentElement && (node.parentElement.tagName === 'SPAN' || node.parentElement.tagName === 'P'))) {
                        let text = node.textContent.trim();
                        if (!text) return;
                        let match = text.match(/^(\d+(?:\.\d+)?)(::)(.*)$/);
                        if (match) {
                            let weight = match[1];
                            let after = match[3];
                            let tag = after;
                            let tagEndNode = node;
                            if (!after) {
                                let p = node.parentElement.closest('p');
                                if (p) {
                                    let nodes = Array.from(p.childNodes);
                                    let idx = nodes.indexOf(node);
                                    let j = idx + 1;
                                    while (j < nodes.length && nodes[j].nodeType === 3 && !nodes[j].textContent.trim()) ++j;
                                    if (j < nodes.length && nodes[j].nodeType === 3) {
                                        tag = nodes[j].textContent;
                                        tagEndNode = nodes[j];
                                    }
                                }
                            }
                            // Show tooltip
                            tooltip = document.createElement('div');
                            tooltip.className = 'syntax-tooltip-popper';
                            tooltip.id = 'syntax-tooltip';
                            tooltip.style.position = 'fixed';
                            tooltip.style.visibility = 'hidden';
                            tooltip.style.background = '#222';
                            tooltip.style.border = '2px solid rgb(60, 56, 54)';
                            tooltip.style.color = 'rgb(235, 218, 178)';
                            tooltip.innerHTML = `<span>Weight: ${weight}</span><div class="syntax-tooltip-arrow"><div class="syntax-tooltip-arrow-border"></div><div class="syntax-tooltip-arrow-bg"></div></div>`;
                            document.body.appendChild(tooltip);
                            let rect = node.parentElement.getBoundingClientRect();
                            tooltip.style.left = `${rect.left + window.scrollX + rect.width/2 - tooltip.offsetWidth/2}px`;
                            tooltip.style.top = `${rect.top + window.scrollY - tooltip.offsetHeight - 8}px`;
                            tooltip.style.visibility = 'visible';
                            current = { node, tagEndNode, weight, tag, match, isSplit: !after };
                            return;
                        }
                    }
                    cleanup();
                } catch (err) {
                    // Fail silently in production
                }
            });
            document.body.addEventListener('mousemove', function(e) {
                try {
                    if (!current) return;
                    let over = (e.target === current.node || e.target === current.tagEndNode);
                    if (!over) cleanup();
                } catch (err) {
                    console.error('[NovelAI Enhancer] Error in mousemove handler:', err);
                }
            });
            document.body.addEventListener('mouseleave', cleanup);
            document.body.addEventListener('wheel', function(e) {
                try {
                    if (!current) return;
                    e.preventDefault();
                    let delta = e.shiftKey ? 0.01 : 0.05;
                    if (e.ctrlKey) delta = 1; // Snap to whole numbers
                    if (e.deltaY > 0) delta = -delta;
                    let newWeight;
                    if (e.ctrlKey) {
                        // Snap to nearest whole number
                        newWeight = Math.max(0, Math.round(parseFloat(current.weight) + delta));
                    } else {
                        newWeight = Math.max(0, Math.round((parseFloat(current.weight) + delta) * 100) / 100);
                    }
                    if (newWeight === parseFloat(current.weight)) return;
                    // Update the node(s)
                    if (!current.isSplit) {
                        // Simple case: update textContent
                        let newText = current.node.textContent.replace(/^\d+(?:\.\d+)?/, newWeight.toFixed(2).replace(/\.00$/, ''));
                        current.node.textContent = newText;
                    } else {
                        // Split case: update number:: in node, tag in tagEndNode
                        current.node.textContent = newWeight.toFixed(2).replace(/\.00$/, '') + '::';
                    }
                    if (tooltip) { tooltip.remove(); tooltip = null; }
                    current = null;
                } catch (err) {
                    console.error('[NovelAI Enhancer] Error in wheel handler:', err);
                }
            }, { passive: false });
        } catch (err) {
            console.error('[NovelAI Enhancer] Error in setupWeightEnhancer:', err);
        }
    }

    // --- Main ---
    function main() {
        observeSyntaxButton();
        observeRefStrengthSliders();
        setupWeightEnhancer();
    }

    main();
})();