NAI Prompt Linker

Import prompts into NovelAI from other editor

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         NAI Prompt Linker
// @namespace    https://github.com/cpuopt/NAI-Prompt-Linker
// @version      1.0.10
// @description  Import prompts into NovelAI from other editor
// @author       cpufan
// @license      GPL-3.0 License
// @include      *://*:7860/*
// @include      *://*:17860/*
// @match        https://novelai.net/image
// @match        https://tags.novelai.dev/
// @match        https://thereisnospon.github.io/NovelAiTag/
// @match        https://wolfchen.top/tag/
// @match        https://tools.miku.ac/novelai_tag/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=novelai.net
// @grant        GM_setValue
// @grant        GM_addValueChangeListener
// @run-at       document-end
// @supportURL   https://github.com/cpuopt/NAI-Prompt-Linker/issues
// ==/UserScript==

(function () {
    "use strict";

    if (window.location.href.startsWith("https://novelai.net/image")) {
        console.debug("NAI端加载成功");
        var opInterval = 200;

        /**
         * @description 向输入框内覆写内容
         * @param {object} area
         * @param {string} text
         */
        var insertText = (area, text) => {
            area.focus();
            document.execCommand("selectAll");
            document.execCommand("delete");
            document.execCommand("insertText", false, text);
            area.blur();
        };

        /**
         * @description 点击生成按钮
         * @returns
         */
        var click_generate = () => {
            return new Promise((resolve) => {
                setTimeout(() => {
                    document.evaluate("//span[contains(text(),'Generate ') and contains(text(),' Image')]/..", document.body, null, 9, null).singleNodeValue.click();
                    resolve();
                }, opInterval);
            });
        };

        /**
         * @description 向正向提示词输入框内覆写内容
         * @param {string} prompt
         * @returns
         */
        var insertPrompt = (prompt) => {
            return new Promise((resolve) => {
                setTimeout(() => {
                    let Prompt_button = document.evaluate("//button[text()='Prompt']", document.body, null, 9, null).singleNodeValue;
                    if (Prompt_button != null) {
                        Prompt_button.click();
                    }
                    setTimeout(() => {
                        insertText(document.querySelector("textarea[placeholder='Write your prompt here. Use tags to sculpt your outputs.']"), prompt);
                    }, opInterval);
                    resolve();
                }, opInterval);
            });
        };

        /**
         * @description 向反向提示词输入框内覆写内容
         * @param {string} uprompt
         * @returns
         */
        var insertUndesiredContent = (uprompt) => {
            return new Promise((resolve) => {
                setTimeout(() => {
                    let UndesiredContent_button = document.evaluate("//button[text()='Undesired Content']", document.body, null, 9, null).singleNodeValue;
                    if (UndesiredContent_button != null) {
                        UndesiredContent_button.click();
                    }
                    setTimeout(() => {
                        insertText(document.querySelector("textarea[placeholder='Write what you want removed from the generation.']"), uprompt);
                    }, opInterval);
                    resolve();
                }, opInterval);
            });
        };

        /**
         *
         * @param {string} prompt
         * @param {string} uprompt
         * @param {boolean} generate 是否直接点击生成
         */
        async function insert(prompt, uprompt, generate) {
            if (uprompt != null) await insertUndesiredContent(uprompt);
            if (prompt != null) await insertPrompt(prompt);
            if (generate == true) {
                await click_generate();
            }
        }

        let NAI_save = GM_addValueChangeListener("->NAI", function (key, oldValue, newValue, remote) {
            console.debug(key + ":\n" + oldValue + "=>" + newValue);
            if (newValue != null) {
                let title = document.title;
                document.title = "NAI Accepted!";
                console.debug(newValue.msg, newValue.time, newValue.prompt.prompt, newValue.prompt.uprompt);
                insert(newValue.prompt.prompt, newValue.prompt.uprompt, newValue.generate);

                GM_setValue("NAI->", { time: newValue.time });
                setTimeout(() => {
                    document.title = title;
                }, 2000);
            }
        });
    } else {
        var pluginCanvas;
        var sendPrompt;
        window.onload = function () {
            //菜单初始化
            pluginCanvas = new Canvas();
            sendPrompt = new SendPrompt();
        };

        class SendPrompt {
            button;
            last_time;
            constructor() {
                this.button = document.createElement("button");
                this.button.className = "plugin-button-blue";
                this.button.id = "SendPromptButton";
                this.button.innerText = "发送Prompt到NAI";

                // this.button.innerHTML = `
                // <svg xmlns="http://www.w3.org/2000/svg" style="fill:white" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24" width="20" height="20"><path d="m20,0H4C1.794,0,0,1.794,0,4v12c0,2.206,1.794,4,4,4h2.923l3.748,3.156c.382.34.862.509,1.338.509.467,0,.931-.163,1.292-.485l3.847-3.18h2.852c2.206,0,4-1.794,4-4V4c0-2.206-1.794-4-4-4Zm-3,13c0,.553-.448,1-1,1s-1-.447-1-1v-3.56l-5.294,5.269c-.195.194-.451.291-.706.291-.257,0-.513-.099-.709-.295-.39-.392-.388-1.024.003-1.414l5.317-5.291h-3.611c-.552,0-1-.447-1-1s.448-1,1-1h4c1.654,0,3,1.346,3,3v4Z"/></svg>
                // `
                this.button.setAttribute("onclick", `window.sendPrompt2NAI()`);

                const canvas = document.querySelector("#plugin-canvas");
                canvas.appendChild(this.button);

                let NAI_send = GM_addValueChangeListener("NAI->", function (key, oldValue, newValue, remote) {
                    console.debug(newValue);
                    if (newValue.time == sendPrompt.last_time) {
                        sendPrompt.button.className = "plugin-button-white";
                        sendPrompt.button.innerText = "发送成功";

                        setTimeout(() => {
                            sendPrompt.button.className = "plugin-button-blue";
                            sendPrompt.button.innerText = "发送Prompt到NAI";
                        }, 2000);
                    }
                });
            }
            /**
             * sd webui and prompt all in one app
             */
            send_Sd2promptAllinOne() {
                let prompt = document.querySelector("#txt2img_prompt > label > textarea").value;
                let uprompt = document.querySelector("#txt2img_neg_prompt > label > textarea").value;
                this.last_time = Number(Date.now());
                let msg = { msg: "", prompt: { prompt: prompt, uprompt: uprompt }, generate: false, time: this.last_time };
                console.debug("setValue", msg);
                GM_setValue("->NAI", msg);
            }

            /**
             * https://tags.novelai.dev/
             */
            send_DanbooruTagShop() {
                document.querySelector("#app > section > section > aside.el-aside.container-full-height.left-bordered.cart-aside > div.cart-container > div.btn-block.mb-bottom > button").click();

                setTimeout(() => {
                    let prompt = document.querySelector("div.tag-positive > div.el-textarea > textarea").value;
                    let uprompt = document.querySelector("div.tag-negative > div.el-textarea > textarea").value;
                    this.last_time = Number(Date.now());
                    let msg = { msg: "", prompt: { prompt: prompt, uprompt: uprompt }, generate: false, time: this.last_time };
                    console.debug("setValue", msg);
                    GM_setValue("->NAI", msg);
                    document.querySelector("div.cart-container > div> div > div > footer > span > button").click();
                }, opInterval);
            }
            /**
             * https://thereisnospon.github.io/NovelAiTag/
             */
            send_Novel_AI_Tag() {
                let prompt = document.getElementById("textarea_pos").value;
                let uprompt = document.getElementById("textarea_neg").value;
                this.last_time = Number(Date.now());
                let msg = { msg: "", prompt: { prompt: prompt, uprompt: uprompt }, generate: false, time: this.last_time };
                console.debug("setValue", msg);
                GM_setValue("->NAI", msg);
            }
            /**
             * https://wolfchen.top/tag/
             */
            send_NovelAITagGenerator() {
                let prompt = document.getElementById("tagarea").value + document.getElementById("tagareb").value;
                let uprompt = document.getElementById("tagarea2").value;
                this.last_time = Number(Date.now());
                let msg = { msg: "", prompt: { prompt: prompt, uprompt: uprompt }, generate: false, time: this.last_time };
                console.debug("setValue", msg);
                GM_setValue("->NAI", msg);
            }

            /**
             * https://tools.miku.ac/novelai_tag/
             */
            send_MikuToolsNaiTag() {
                let prompt = document.querySelector("div.nya-copy > pre > span").textContent;
                this.last_time = Number(Date.now());
                let msg = { msg: "", prompt: { prompt: prompt, uprompt: null }, generate: false, time: this.last_time };
                console.debug("setValue", msg);
                GM_setValue("->NAI", msg);
            }
        }

        /**
         * 插件菜单类
         */
        class Canvas {
            constructor() {
                var styles = document.createElement("style");
                document.head.appendChild(styles);
                styles.innerHTML = `
#plugin-canvas {z-index:99999;min-width: max-content;position: fixed;background-color: #ffffff;height: 75px;border-radius: 10px;border-inline: solid 2px rgb(162, 218, 255)}
.plugin-button-blue {display: block;color: rgb(255, 255, 255);background-color: rgb(0, 150, 250);font-size: 14px;font-weight: bold;margin: 16px;padding-left: 20px;padding-right: 20px;height: 42px;border: none;border-radius: 100000px;transition: background-color 0.6s;}
.plugin-button-white {display: block;color: rgb(71,71,71);background-color: rgb(245,245,245);font-size: 14px;font-weight: bold;margin: 16px;padding-left: 20px;padding-right: 20px;height: 42px;border: none;border-radius: 100000px;transition: background-color 0.6s;}
.plugin-button-blue:hover {background-color: rgb(0,114,240);cursor: pointer;}
.plugin-button-white:hover {background-color: rgb(235,235,235);cursor: pointer;}
.hideCanvas {padding: 0;height: 60px;width: 15px;position: absolute;left: -15px;top: 6px;border: solid 2px rgb(162, 218, 255);background-color: rgb(255, 255, 255);border-radius: 10px 0px 0px 10px;transition: background-color 0.6s;cursor: pointer;}
.hideCanvas:hover{background-color: rgb(162, 218, 255);}
.showCanvas {padding: 0;height: 60px;width: 15px;position: absolute;left: -15px;top: 6px;border: solid 2px rgb(162, 218, 255);background-color: rgb(255, 255, 255);border-radius: 10px 0px 0px 10px;transition: background-color 0.6s;cursor: pointer;}
.showCanvas:hover{background-color: rgb(162, 218, 255);}
        `;
                let canvas = document.createElement("div");
                canvas.id = "plugin-canvas";
                canvas.style.right = "0";
                canvas.style.top = "5px";

                let canvasButton = document.createElement("button");
                var offsetX, offsetY;
                var isDragging = false;

                canvasButton.addEventListener("mousedown", function (e) {
                    offsetX = e.clientX - canvas.offsetLeft;
                    offsetY = e.clientY - canvas.offsetTop;
                    isDragging = true;
                });

                document.addEventListener("mousemove", function (e) {
                    if (isDragging) {
                        canvas.style.right = "auto";
                        canvas.style.left = e.clientX - offsetX + "px";
                        canvas.style.top = e.clientY - offsetY + "px";
                    }
                });

                document.addEventListener("mouseup", function (e) {
                    isDragging = false;
                });

                canvas.appendChild(canvasButton);
                canvasButton.id = "canvasButton";
                // canvasButton.setAttribute('onclick', `window.hideCanvas()`)
                canvasButton.className = "hideCanvas";
                canvasButton.innerHTML = '<svg style="margin-left:-2px" width="16" height="16" fill="rgb(0,150,250)" class="bi bi-caret-right-fill" viewBox="0 0 16 16"><path d="m12.14 8.753-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z"/></svg>';

                document.body.appendChild(canvas);
            }
            /**
             * 显示插件菜单
             */
            show() {
                document.querySelector("#plugin-canvas").style.right = "0px";

                document.querySelector("#canvasButton").className = "hideCanvas";
                document.querySelector("#canvasButton").innerHTML = `<svg style="margin-left:-2px" width="16" height="16" fill="rgb(0,150,250)" class="bi bi-caret-right-fill" viewBox="0 0 16 16">
        <path d="m12.14 8.753-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z"/>
      </svg>`;
                document.querySelector("#canvasButton").setAttribute("onclick", `window.hideCanvas()`);
            }
            /**
             * 隐藏插件菜单
             */
            hide() {
                document.querySelector("#plugin-canvas").style.right = "-171.6px";

                document.querySelector("#canvasButton").className = "showCanvas";
                document.querySelector("#canvasButton").innerHTML = `<svg style="margin-left:-2px" width="16" height="16" fill="rgb(0,150,250)" class="bi bi-caret-left-fill" viewBox="0 0 16 16">
            <path d="m3.86 8.753 5.482 4.796c.646.566 1.658.106 1.658-.753V3.204a1 1 0 0 0-1.659-.753l-5.48 4.796a1 1 0 0 0 0 1.506z"/>
          </svg>`;
                document.querySelector("#canvasButton").setAttribute("onclick", `window.showCanvas()`);
            }
        }

        // 显示插件菜单
        unsafeWindow.showCanvas = function () {
            pluginCanvas.show();
            GM_setValue("CanvasState", "S");
        };
        // 隐藏插件菜单
        unsafeWindow.hideCanvas = function () {
            pluginCanvas.hide();
            GM_setValue("CanvasState", "H");
        };

        // 发送Prompt到NAI
        unsafeWindow.sendPrompt2NAI = function () {
            let url = window.location.href;
            console.log(url);
            if (/(http|https):\/\/(localhost|127\.0\.0\.1):(7860|17860)\\*/.test(url)) {
                sendPrompt.send_Sd2promptAllinOne();
            } else if (/https:\/\/tags.novelai.dev/.test(url)) {
                sendPrompt.send_DanbooruTagShop();
            } else if (/https:\/\/thereisnospon.github.io\/NovelAiTag/.test(url)) {
                sendPrompt.send_Novel_AI_Tag();
            } else if (/https:\/\/wolfchen.top\/tag/.test(url)) {
                sendPrompt.send_NovelAITagGenerator();
            } else if (/https:\/\/tools.miku.ac\/novelai_tag/.test(url)) {
                sendPrompt.send_MikuToolsNaiTag();
            }
        };
    }
})();