NAI Prompt Linker

Import prompts into NovelAI from other editor

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
            }
        };
    }
})();