Sleazy Fork is available in English.

NovelAI Mod

Extention for NovelAI Image Generator. Fast Suggestion, Chant, Save in JPEG, Get aspect ratio.

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name         NovelAI Mod
// @namespace    https://www6.notion.site/dc99953d5f04405c893fba95dace0722
// @version      18
// @description  Extention for NovelAI Image Generator. Fast Suggestion, Chant, Save in JPEG, Get aspect ratio.
// @author       SenY
// @match        https://novelai.net/image
// @icon         https://www.google.com/s2/favicons?sz=64&domain=novelai.net
// @grant        none
// @license      MIT

// ==/UserScript==

// 定数と設定
const SUGGESTION_LIMIT = 500;
const colors = {
    "-1": ["red", "maroon"],
    "0": ["lightblue", "dodgerblue"],
    "1": ["gold", "goldenrod"],
    "3": ["violet", "darkorchid"],
    "4": ["lightgreen", "darkgreen"],
    "5": ["tomato", "darksalmon"],
    "6": ["red", "maroon"],
    "7": ["whitesmoke", "black"],
    "8": ["seagreen", "darkseagreen"]
};

// グローバル変数
let lastTyped = new Date();
let suggested_at = new Date();
let seedButton;
let chants = [];
let allTags = [];
let chantURL;

// ユーティリティ関数
const gcd = (a, b) => b == 0 ? a : gcd(b, a % b);
const getAspect = whArray => whArray.map(x => x / gcd(whArray[0], whArray[1]));

const aspectList = (wa, ha, maxpixel = 1024 * 1024) => {
    let aspect = wa / ha;
    let limit = 16384;
    let steps = 64;
    let ws = Array.from({ length: (limit - steps) / steps + 1 }, (_, i) => (i + 1) * steps);
    let hs = ws.slice();
    return ws.flatMap(w => hs.map(h => w / h === aspect && w * h <= maxpixel ? [w, h] : null)).filter(Boolean);
};

const getChantURL = force => {
    if (force === true) {
        localStorage.removeItem("chantURL");
    }
    let url = localStorage.getItem("chantURL") || prompt("Input your chants json url.\nThe URL must be a Cors-enabled server (e.g., gist.github.com).", "https://gist.githubusercontent.com/vkff5833/989808aadebf8648831955cdf2a7b3e3/raw/yuuri.json");
    if (url) localStorage.setItem("chantURL", url);
    return url;
};

// 新しい関数: 現在表示されている画像のDataURLを取得
const getImageDataURL = async (mode) => {
    try {
        if (mode === "current") {
            const imgs = Array.from(document.querySelectorAll('img[src]')).filter(x => x.offsetParent);
            const url = imgs.reduce((max, img) => img.height > max.height ? img : max).src || document.querySelector('img[src]')?.src;
            if (!url) return null;
            const response = await fetch(url);
            const blob = await response.blob();
            return await new Promise(resolve => {
                let reader = new FileReader();
                reader.onload = () => resolve(reader.result);
                reader.readAsDataURL(blob);
            });
        } else if (mode === "uploaded") {
            const uploadInput = document.getElementById('naimodUploadedImage');
            const previewImage = document.getElementById('naimodPreviewImage');
            if (uploadInput && uploadInput.files[0]) {
                return await new Promise(resolve => {
                    let reader = new FileReader();
                    reader.onload = (e) => resolve(e.target.result);
                    reader.readAsDataURL(uploadInput.files[0]);
                });
            } else if (previewImage && previewImage.src) {
                return previewImage.src;
            }
            return null;
        }
    } catch (error) {
        console.error("Error getting image DataURL:", error);
        return null;
    }
};

// メイン機能関数
async function ImprovementTags(tags, imageDataURL) {
    console.debug("ImprovementTags function called with tags:", tags);

    // タグの_を半角スペースに置換
    tags = tags.replace(/_/g, " ");

    const apiKey = document.getElementById('geminiApiKey').value;
    if (!apiKey) {
        alert("Gemini API Keyが設定されていません。設定画面でAPIキーを入力してください。");
        return false;
    }
    console.debug("API Key found:", apiKey.substring(0, 5) + "...");

    const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`;
    console.debug("ENDPOINT:", ENDPOINT);

    let prompt = `以下に示すのはdanbooruのタグを用いて画像を生成するNovelAIの機能で、添付の画像を生成する為に用意したタグの一覧です。

${tags}

- 実際の画像の内容と列挙されたタグ一覧の、比較検討とブラッシュアップを行ってください。
- 不要と判断したタグを列挙してください。例えば顔しか映っていない画像なのにスカートや靴下に関する言及がある場合や、顔が映らない構図なのに瞳の色や表情に言及がある場合、黒髪のキャラしか居ないのにblonde hairと記述されるなど明らかに的外れなタグが含まれている場合などは、それらを除去する必要があります。(should remove)
- それとは逆に、新たに付与するべきであると考えられるタグがある場合は列挙してください。(should add)
- また、客観的に画像から読み取って付与するべきと判断したタグ以外に、追加することでより表現が豊かになると考えられるタグがあれば、それも提案してください。例えば文脈上付与した方がよりリアリティが増すと思われるアクセサリや小物を示すタグ、より効果的な表現を得る為に付与するのが好ましいと思われるエフェクトのタグなどです。
これらの内容は元々の画像には含まれていない要素であるべきです。(may add)
- 固有名詞と思われる認識不能なタグに関しては無視してください。
- また、NovelAI系ではない自然文のプロンプトで画像生成をする場合に用いるプロンプトも別途提案してください。実際の画像や上記のshouldRemove,shouldAdd,mayAddの内容を踏襲し、2000文字程度の自然言語の英文で表現してください。

返信はJSON形式で以下のスキーマに従ってください。

{
"shouldRemove": [string]
"shouldAdd": [string]
"mayAdd": [string],
"naturalLanguagePrompt": [string]
}
`;
    console.debug("Prompt prepared:", prompt);

    const payload = {
        method: 'POST',
        headers: {},
        body: JSON.stringify({
            contents: [
                {
                    parts: [
                        { text: prompt },
                        { inline_data: { mime_type: "image/jpeg", data: imageDataURL.split(',')[1] } }
                    ]
                }
            ],
            "generationConfig": {
                "temperature": 1.0,
                "max_output_tokens": 4096
            },
            safetySettings: [
                {
                    "category": "HARM_CATEGORY_HATE_SPEECH",
                    "threshold": "OFF"
                },
                {
                    "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
                    "threshold": "OFF"
                },
                {
                    "category": "HARM_CATEGORY_HARASSMENT",
                    "threshold": "OFF"
                },
                {
                    "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
                    "threshold": "OFF"
                }
            ]
        }),
        mode: 'cors'
    };
    console.debug("Payload prepared:", payload);

    let result;
    try {
        console.debug("Sending fetch request to Gemini API");
        const response = await fetch(ENDPOINT, payload);
        console.debug("Response received:", response);
        const data = await response.json();
        console.debug("Response data:", data);
        result = data.candidates[0].content.parts[0].text;
        result = result.replace('```json\n', '').replace('\n```', '');
        result = JSON.parse(result);
        // 結果のタグも_を半角スペースに置換
        result.shouldRemove = result.shouldRemove.map(tag => tag.replace(/_/g, " "));
        result.shouldAdd = result.shouldAdd.map(tag => tag.replace(/_/g, " "));
        result.mayAdd = result.mayAdd.map(tag => tag.replace(/_/g, " "));
        console.debug("Parsed result:", result);
    } catch (error) {
        console.error('エラー:', error);
        alert("AI改善の処理中にエラーが発生しました。");
        return false;
    }
    return result || false;
}

const saveJpeg = async () => {
    let seed = Array.from(seedButton.querySelectorAll("span")).find(x => x.textContent.match(/^[0-9]*$/))?.textContent;
    let filename = `${seed}.jpg`;

    try {
        const dataURI = await getImageDataURL("current");
        if (!dataURI) {
            throw new Error("Failed to get image DataURL");
        }

        let image = new Image();
        image.src = dataURI;
        await new Promise(resolve => { image.onload = resolve; });

        let canvas = document.createElement("canvas");
        canvas.width = image.width;
        canvas.height = image.height;
        canvas.getContext("2d").drawImage(image, 0, 0);

        let JPEG = canvas.toDataURL("image/jpeg", document.getElementById("jpegQuality").value - 0);
        let link = document.createElement('a');
        link.href = JPEG;
        link.download = filename;
        link.click();
    } catch (error) {
        console.error("Error saving JPEG:", error);
    }
};

const changePixel = (w, h) => {
    document.querySelectorAll('input[type="number"][step="64"]').forEach((x, i) => {
        x.value = [w, h][i];
        x._valueTracker = '';
        x.dispatchEvent(new Event('input', { bubbles: true }));
    });
};

// ProseMirrorエディタのヘルパー関数
const getEditorContent = () => {
    const editor = document.querySelector(".ProseMirror");
    return editor ? editor.textContent : "";
};

const setEditorContent = (content, targetEditor = null) => {
    const editor = targetEditor || document.querySelector(".ProseMirror");
    if (editor) {
        // 既存の内容をクリア
        editor.innerHTML = "";
        // 新しい段落を作成
        const p = document.createElement("p");
        p.textContent = content;
        editor.appendChild(p);
        // 入力イベントを発火
        editor.dispatchEvent(new Event('input', { bubbles: true }));
    }
};

// getTargetTag関数の更新
const getTargetTag = (editor = null) => {
    editor = editor || document.querySelector(".ProseMirror");
    if (editor) {
        const selection = window.getSelection();
        const content = editor.textContent;
        const cursorPosition = selection.focusOffset;
        
        const oldTags = content.split(",").map(x => x.trim());
        const beforeCursor = content.slice(0, cursorPosition);
        const beforeTags = beforeCursor.split(",").map(x => x.trim());
        const targetTag = beforeTags[beforeTags.length - 2];
        
        return oldTags[oldTags.indexOf(targetTag) + 1]?.trim();
    }
    return null;
};

// Append関数の更新
const Append = (key, value, targetEditor = null) => {
    const editor = targetEditor || document.querySelector(".ProseMirror");
    if (editor) {
        const content = editor.textContent;
        const oldTags = content.split(",").map(x => x.trim());
        const targetTag = getTargetTag(editor);
        
        let newTags = key ? chants.find(x => x.name === key.trim())?.content.split(",").map(x => x.trim()) :
            value ? [value.trim()] : [];

        if (newTags?.length) {
            let tags = oldTags.flatMap(tag =>
                tag === targetTag ? (key ? [tag, ...newTags] : newTags) : [tag]
            );
            if (!targetTag) tags = [...tags, ...newTags];
            tags = [...new Set(tags.filter(Boolean))];

            setEditorContent(tags.join(", ") + ", ");
        }
    }
};

// Crease関数の更新
const Crease = value => {
    const getTagPower = tag => tag.split("").filter(x => x === "{").length - tag.split("").filter(x => x === "[").length;
    const editor = document.querySelector(".ProseMirror");
    
    if (editor) {
        const content = editor.textContent;
        const oldTags = content.split(",").map(x => x.trim());
        const targetTag = getTargetTag();

        if (targetTag) {
            let tags = oldTags.map(tag => {
                if (tag === targetTag) {
                    let tagPower = getTagPower(targetTag) + value;
                    tag = tag.replace(/[\{\}\[\]]/g, "");
                    return tagPower > 0 ? '{'.repeat(tagPower) + tag + '}'.repeat(tagPower) :
                        tagPower < 0 ? '['.repeat(-tagPower) + tag + ']'.repeat(-tagPower) : tag;
                }
                return tag;
            }).filter(Boolean);

            setEditorContent(tags.join(", ") + ", ");
        }
    }
};

const Suggest = (targetEditor = null) => {
    console.debug("Suggest function called");
    const editor = targetEditor || document.querySelector(".ProseMirror");
    if (editor) {
        let targetTag = getTargetTag(editor)?.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
        // 最も近い suggestionField を探す
        let suggestionField = editor.closest(".relative")?.querySelector("#suggestionField") || 
                            document.getElementById("suggestionField");
        
        if (suggestionField) {
            suggestionField.textContent = "";

            if (targetTag) {
                let suggestions = allTags.filter(x =>
                    x.name.search(targetTag.replace(/_/g, " ").replace(/[\{\}\[\]\\]/g, "")) >= 0 ||
                    x.terms.some(y => y.search(targetTag.replace(/_/g, " ").replace(/[\{\}\[\]\\]/g, "")) >= 0)
                ).slice(0, 20);

                let done = new Set();
                suggestions.forEach(tag => {
                    if (!done.has(tag.name)) {
                        let button = createButton(
                            `${tag.name} (${tag.coumt > 1000 ? `${(Math.round(tag.coumt / 100) / 10)}k` : tag.coumt})`, 
                            colors[tag.category][1], 
                            () => Append(null, tag.name, editor)
                        );
                        button.title = tag.terms.filter(Boolean).join(", ");
                        suggestionField.appendChild(button);

                        if (editor.textContent.split(",").map(y => y.trim().replace(/_/g, " ").replace(/[\{\}\[\]\\]/g, "")).includes(tag.name.replace(/_/g, " "))) {
                            button.style.opacity = 0.5;
                        }
                        done.add(tag.name);
                    }
                });
            }
        }
    }
};

const createTagButton = (tag, color, isEnabled) => {
    let button = document.createElement('button');
    button.textContent = tag;
    button.className = "naimod-tag-button";
    button.style.backgroundColor = color;
    button.dataset.enabled = isEnabled ? 'true' : 'false';

    const updateButtonStyle = () => {
        if (button.dataset.enabled === 'true') {
            button.style.opacity = '1';
            button.style.textDecoration = 'none';
        } else {
            button.style.opacity = '0.5';
            button.style.textDecoration = 'line-through';
        }
    };

    updateButtonStyle();

    button.onclick = () => {
        button.dataset.enabled = button.dataset.enabled === 'true' ? 'false' : 'true';
        updateButtonStyle();
    };

    return button;
};

const isDirectorToolEditor = () => {
    // ディレクターツールの特徴的なテキスト"背景の除去"が存在するか確認
    const root = document.getElementById("__next");
    return root?.textContent.includes("背景の除去") || false;
};

const Build = async () => {
    console.debug("Build function called");
    // スタイルの定義を更新
    if (!document.getElementById('naimod-styles')) {
        const style = document.createElement('style');
        style.id = 'naimod-styles';
        style.textContent = `
            #imageSource {
                color: black;
            }
            .naimod-modal {
                display: none;
                position: fixed;
                z-index: 1000;
                left: 0;
                top: 0;
                width: 100%;
                height: 100%;
                background-color: rgba(0,0,0,0.4);
                overflow: auto;
            }
            .naimod-settings-content {
                padding: 1rem;
                border: 1px solid rgb(204, 204, 204);
                border-radius: 10px;
                width: 90%;
                max-width: 600px;
                position: absolute;
                left: 50%;
                top: 50%;
                transform: translate(-50%, -50%);
                background-color: rgba(0, 0, 64, 0.9);
                color: #fff;
                box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            }
            .naimod-settings-title {
                margin-bottom: 10px;
                border-bottom: 2px solid #fff;
                padding-bottom: 5px;
            }
            .naimod-input {
                width: 100%;
                margin-bottom: 10px;
                padding: 5px;
            }
            .naimod-two-pane {
                display: flex;
                flex-direction: column;
                gap: 10px;
                max-height: 50vh;
                overflow-y: auto;
            }
            .naimod-pane-container {
                display: flex;
                flex-direction: column;
            }
            .naimod-pane {
                border: 1px solid #ccc;
                padding: 10px;
                background-color: rgba(255, 255, 255, 0.1);
                max-height: 20vh;
                overflow-y: auto;
            }
            .naimod-button {
                margin: 2px;
                padding: 5px;
                border: none;
                border-radius: 3px;
                cursor: pointer;
            }
            .naimod-tag-button {
                color: white;
                margin: 2px;
                padding: 5px;
                border: none;
                border-radius: 3px;
                cursor: pointer;
            }
            .naimod-accordion {
                background-color: rgba(255, 255, 255, 0.1);
                color: #fff;
                cursor: pointer;
                padding: 10px;
                width: 100%;
                text-align: left;
                border: none;
                outline: none;
                transition: 0.4s;
                display: flex;
                justify-content: space-between;
                align-items: center;
            }
            .naimod-panel {
                padding: 0 10px;
                background-color: rgba(255, 255, 255, 0.05);
                max-height: 0;
                overflow: hidden;
                transition: max-height 0.2s ease-out;
            }
            .naimod-image-source-container {
                margin-bottom: 10px;
            }
            .naimod-upload-container {
                margin-top: 10px;
                width: 200px;
                height: 200px;
                border: 2px dashed #ccc;
                border-radius: 5px;
                display: none;
                justify-content: center;
                align-items: center;
                cursor: pointer;
            }
            .naimod-preview-image {
                max-width: 100%;
                max-height: 100%;
                display: none;
            }
            .naimod-natural-language-container {
                margin-top: 20px;
                padding: 10px;
                border: 1px solid #ccc;
                border-radius: 5px;
                background-color: rgba(255, 255, 255, 0.1);
            }
            .naimod-natural-language-title {
                margin-top: 0;
                margin-bottom: 10px;
            }
            .naimod-natural-language-content {
                white-space: pre-wrap;
                margin: 0;
                max-height: 20vh;
                overflow-y: auto;
            }
            .naimod-close-button-container {
                display: flex;
                justify-content: flex-end;
                margin-top: 10px;
            }
            @media (min-width: 768px) {
                .naimod-two-pane {
                    flex-direction: row;
                }
                .naimod-pane-container {
                    flex: 1;
                }
            }
            .accordion-arrow {
                font-size: 12px;
                transition: transform 0.3s ease;
            }
            .naimod-accordion.active .accordion-arrow {
                transform: rotate(180deg);
            }
        `;
        document.head.appendChild(style);
    }

    let modal = document.getElementById("naimod-modal");
    if (!modal) {
        modal = document.createElement("div");
        modal.id = "naimod-modal";
        modal.className = "naimod-modal";
        modal.setAttribute("role", "dialog");
        modal.setAttribute("aria-modal", "true");
        document.body.appendChild(modal);

        let settingsContent = document.createElement("div");
        settingsContent.className = "naimod-settings-content";

        let settingsTitle = document.createElement("h2");
        settingsTitle.textContent = "Settings";
        settingsTitle.className = "naimod-settings-title";

        // アコーディオンボタンのスタイルを更新
        let apiKeyAccordion = document.createElement("button");
        apiKeyAccordion.className = "naimod-accordion";
        apiKeyAccordion.innerHTML = "API Settings <span class='accordion-arrow'>▼</span>"; // 矢印を追加

        let apiKeyPanel = document.createElement("div");
        apiKeyPanel.className = "naimod-panel";

        let apiKeyInput = document.createElement("input");
        apiKeyInput.type = "text";
        apiKeyInput.id = "geminiApiKey";
        apiKeyInput.className = "naimod-input";
        apiKeyInput.placeholder = "Enter Gemini API Key";
        apiKeyInput.value = localStorage.getItem("geminiApiKey") || "";
        apiKeyInput.addEventListener("change", () => {
            localStorage.setItem("geminiApiKey", apiKeyInput.value);
        });

        let apiKeyLabel = document.createElement("label");
        apiKeyLabel.htmlFor = "geminiApiKey";
        apiKeyLabel.textContent = "Gemini API Key:";
        apiKeyLabel.style.display = "block";
        apiKeyLabel.style.marginBottom = "5px";

        apiKeyPanel.appendChild(apiKeyLabel);
        apiKeyPanel.appendChild(apiKeyInput);

        let resetButton = createButton("Reset Chants URL", "red", () => {
            chantURL = getChantURL(true);
            Build();
            modal.style.display = "none";
        });
        resetButton.className = "naimod-button";
        resetButton.style.marginTop = "10px";

        apiKeyPanel.appendChild(resetButton);

        // アコーディオンの動作を更新
        apiKeyAccordion.addEventListener("click", function () {
            this.classList.toggle("active");
            let panel = this.nextElementSibling;
            let arrow = this.querySelector('.accordion-arrow');
            if (panel.style.maxHeight) {
                panel.style.maxHeight = null;
                arrow.textContent = '▼'; // 閉じた状態
            } else {
                panel.style.maxHeight = panel.scrollHeight + "px";
                arrow.textContent = '▲'; // 開いた状態
            }
        });

        // 二段パネルの構造を変更
        let twoPane = document.createElement("div");
        twoPane.className = "naimod-two-pane";

        let leftContainer = document.createElement("div");
        leftContainer.className = "naimod-pane-container";

        let rightContainer = document.createElement("div");
        rightContainer.className = "naimod-pane-container";

        let leftPane = document.createElement("div");
        leftPane.className = "naimod-pane";

        let rightPane = document.createElement("div");
        rightPane.className = "naimod-pane right-pane";

        let imageSourceContainer = document.createElement("div");
        imageSourceContainer.className = "naimod-image-source-container";

        let imageSourceLabel = document.createElement("label");
        imageSourceLabel.textContent = "Image Source: ";

        let imageSourceSelect = document.createElement("select");
        imageSourceSelect.id = "imageSource";
        ["current", "uploaded"].forEach(source => {
            let option = document.createElement("option");
            option.value = source;
            option.textContent = source.charAt(0).toUpperCase() + source.slice(1);
            imageSourceSelect.appendChild(option);
        });

        let uploadContainer = document.createElement("div");
        uploadContainer.className = "naimod-upload-container";
        uploadContainer.textContent = "Drag & Drop or Click to Upload";

        let uploadInput = document.createElement("input");
        uploadInput.type = "file";
        uploadInput.id = "naimodUploadedImage";
        uploadInput.accept = "image/*";
        uploadInput.style.display = "none";

        // previewImage の作成を削除(handleImageUpload 内で必要に応じて作成されます)

        const handleImageUpload = (file) => {
            if (file && file.type.startsWith('image/')) {
                const reader = new FileReader();
                reader.onload = (e) => {
                    const uploadContainer = document.querySelector('.naimod-upload-container');
                    if (uploadContainer) {
                        let previewImage = document.getElementById('naimodPreviewImage');
                        if (!previewImage) {
                            previewImage = document.createElement('img');
                            previewImage.id = 'naimodPreviewImage';
                            previewImage.className = 'naimod-preview-image';
                        }
                        previewImage.src = e.target.result;
                        previewImage.style.display = "block";
                        uploadContainer.textContent = "";
                        uploadContainer.appendChild(previewImage);
                        console.debug("Image uploaded successfully:", previewImage.src.substring(0, 50) + "...");
                    } else {
                        console.error("Upload container not found");
                    }
                };
                reader.onerror = (error) => {
                    console.error("Error reading file:", error);
                    alert("画像の読み込み中にエラーが発生しました。");
                };
                reader.readAsDataURL(file);
            } else {
                console.error("Invalid file type:", file ? file.type : "No file");
                alert("有効な画像ファイルを選択してください。");
            }
        };

        uploadInput.addEventListener("change", (e) => {
            if (e.target.files && e.target.files[0]) {
                handleImageUpload(e.target.files[0]);
            } else {
                console.error("No file selected");
                alert("ファイルが選択されていません。");
            }
        });

        uploadContainer.addEventListener("click", () => {
            uploadInput.click();
        });

        uploadContainer.addEventListener("dragover", (e) => {
            e.preventDefault();
            uploadContainer.style.borderColor = "#000";
        });

        uploadContainer.addEventListener("dragleave", () => {
            uploadContainer.style.borderColor = "#ccc";
        });

        uploadContainer.addEventListener("drop", (e) => {
            e.preventDefault();
            uploadContainer.style.borderColor = "#ccc";
            if (e.dataTransfer.files && e.dataTransfer.files[0]) {
                handleImageUpload(e.dataTransfer.files[0]);
            } else {
                console.error("No file dropped");
                alert("ファイルのドロップに失敗しました。");
            }
        });

        // クリップボードからの画像貼り付け
        document.addEventListener("paste", (e) => {
            if (imageSourceSelect.value === "uploaded") {
                const items = e.clipboardData.items;
                for (let i = 0; i < items.length; i++) {
                    if (items[i].type.indexOf("image") !== -1) {
                        const blob = items[i].getAsFile();
                        handleImageUpload(blob);
                        break;
                    }
                }
            }
        });

        imageSourceSelect.addEventListener("change", () => {
            uploadContainer.style.display = imageSourceSelect.value === "uploaded" ? "flex" : "none";
        });

        // 初期状態を設定
        imageSourceSelect.value = "current";
        uploadContainer.style.display = "none";

        imageSourceContainer.appendChild(imageSourceLabel);
        imageSourceContainer.appendChild(imageSourceSelect);
        imageSourceContainer.appendChild(uploadContainer);

        // suggestButton の onclick イベントを修正
        let suggestButton = createButton("Suggest AI Improvements", "blue", async () => {
            let textarea = document.querySelector("textarea[placeholder]");
            if (textarea) {
                let tags = textarea.value.replace(/_/g, " "); // _を半角スペースに置換

                rightPane.innerHTML = "<div>Loading AI suggestions...</div>";
                suggestButton.disabled = true;
                suggestButton.textContent = "Processing...";

                let imageSource = document.getElementById("imageSource").value;
                console.debug("Selected image source:", imageSource); // デバッグ用
                let imageDataURL = await getImageDataURL(imageSource);

                if (!imageDataURL) {
                    console.error("Failed to get image DataURL"); // デバッグ用
                    rightPane.innerHTML = "<div>Failed to get image. Please check your image source.</div>";
                    suggestButton.disabled = false;
                    suggestButton.textContent = "Suggest AI Improvements";
                    return;
                }

                console.debug("Image DataURL obtained:", imageDataURL.substring(0, 50) + "..."); // デバッグ用

                let result = await ImprovementTags(tags, imageDataURL);

                suggestButton.disabled = false;
                suggestButton.textContent = "Suggest AI Improvements";

                if (result) {
                    let originalTags = tags.split(',').map(t => t.trim()).filter(x => x);

                    rightPane.innerHTML = '';

                    // 元のタグを黒色のボタンで表示(shouldRemoveに含まれるものは赤色)
                    let originalTagsDiv = document.createElement('div');
                    originalTagsDiv.style.marginBottom = '10px';
                    originalTags.forEach(tag => {
                        let color = result.shouldRemove.includes(tag) ? 'red' : 'black';
                        let isEnabled = !result.shouldRemove.includes(tag);
                        originalTagsDiv.appendChild(createTagButton(tag, color, isEnabled));
                    });
                    rightPane.appendChild(originalTagsDiv);

                    // shouldAddを緑色のボタンで表示
                    if (result.shouldAdd.length > 0) {
                        let shouldAddDiv = document.createElement('div');
                        shouldAddDiv.style.marginBottom = '10px';
                        result.shouldAdd.forEach(tag => {
                            shouldAddDiv.appendChild(createTagButton(tag.replace(/_/g, " "), 'green', true));
                        });
                        rightPane.appendChild(shouldAddDiv);
                    }

                    // mayAddを青色のボタンで表示
                    if (result.mayAdd.length > 0) {
                        let mayAddDiv = document.createElement('div');
                        mayAddDiv.style.marginBottom = '10px';
                        result.mayAdd.forEach(tag => {
                            mayAddDiv.appendChild(createTagButton(tag.replace(/_/g, " "), 'blue', false));
                        });
                        rightPane.appendChild(mayAddDiv);
                    }

                    // naturalLanguagePrompt を更新
                    let naturalLanguagePromptElement = document.getElementById("naturalLanguagePrompt");
                    if (naturalLanguagePromptElement && result.naturalLanguagePrompt) {
                        naturalLanguagePromptElement.textContent = result.naturalLanguagePrompt;
                    } else {
                        naturalLanguagePromptElement.textContent = "No natural language prompt available.";
                    }
                } else {
                    rightPane.innerHTML = "<div>Failed to get AI suggestions.</div>";
                    document.getElementById("naturalLanguagePrompt").textContent = "Failed to get natural language prompt.";
                }

                updateLeftPane();
            }
        });
        suggestButton.style.marginBottom = "10px";

        let applyButton = createButton("Apply Suggestions", "green", () => {
            let textarea = document.querySelector("textarea[placeholder]");
            if (textarea) {
                let enabledTags = Array.from(rightPane.querySelectorAll('button[data-enabled="true"]'))
                    .map(button => button.textContent.replace(/_/g, " "));
                textarea.value = enabledTags.join(", ");
                textarea._valueTracker = '';
                textarea.dispatchEvent(new Event('input', { bubbles: true }));
            }
        });
        applyButton.style.marginBottom = "10px";

        leftContainer.appendChild(suggestButton);
        leftContainer.appendChild(leftPane);

        rightContainer.appendChild(applyButton);
        rightContainer.appendChild(rightPane);

        twoPane.appendChild(leftContainer);
        twoPane.appendChild(rightContainer);

        // naturalLanguagePrompt を表示するための新しい要素を追加
        let naturalLanguageContainer = document.createElement("div");
        naturalLanguageContainer.className = "naimod-natural-language-container";

        let naturalLanguageTitle = document.createElement("h3");
        naturalLanguageTitle.className = "naimod-natural-language-title";
        naturalLanguageTitle.textContent = "Natural Language Prompt";

        let naturalLanguageContent = document.createElement("p");
        naturalLanguageContent.id = "naturalLanguagePrompt";
        naturalLanguageContent.className = "naimod-natural-language-content";
        naturalLanguageContent.textContent = "-";

        naturalLanguageContainer.appendChild(naturalLanguageTitle);
        naturalLanguageContainer.appendChild(naturalLanguageContent);

        let closeButtonContainer = document.createElement("div");
        closeButtonContainer.className = "naimod-close-button-container";

        let closeButton = createButton("Close", "grey", () => modal.style.display = "none");
        closeButton.style.padding = "5px 15px";

        closeButtonContainer.appendChild(closeButton);

        settingsContent.appendChild(settingsTitle);
        settingsContent.appendChild(apiKeyAccordion);
        settingsContent.appendChild(apiKeyPanel);
        settingsContent.appendChild(imageSourceContainer);
        settingsContent.appendChild(twoPane);
        settingsContent.appendChild(naturalLanguageContainer); // 新しく追加
        settingsContent.appendChild(closeButtonContainer);

        // テキストエリアの内容を左ペインに表示する関数
        function updateLeftPane() {
            let textarea = document.querySelector("textarea[placeholder]");
            if (textarea) {
                let tags = textarea.value.split(",").map(x => x.trim().replace(/_/g, " ")).filter(x => x);
                leftPane.innerHTML = '';
                tags.forEach(tag => {
                    let button = document.createElement('button');
                    button.textContent = tag;
                    button.style.backgroundColor = 'black';
                    button.style.color = 'white';
                    button.style.margin = '2px';
                    button.style.padding = '5px';
                    button.style.border = 'none';
                    button.style.borderRadius = '3px';
                    button.style.cursor = 'default'; // カーソルをデフォルトに設定
                    button.disabled = true; // ボタンを無効化
                    leftPane.appendChild(button);
                });
            }
        }

        // テキストエリアの内容が変更されたときに左ペインを更新
        document.addEventListener("input", function (e) {
            if (e.target.tagName.toLowerCase() === "textarea") {
                updateLeftPane();
            }
        });

        // モーダルが表示されたときに左ペインを更新
        modal.addEventListener("show", updateLeftPane);

        // 初期表示時に左ペインを更新
        updateLeftPane();

        modal.appendChild(settingsContent);

        // モーダルの外側をクリックしたときに閉じる
        modal.onclick = (event) => {
            if (event.target === modal) {
                modal.style.display = "none";
            }
        };
    }

    let ui = document.getElementById("NAIModUi") || (() => {
        let el = document.createElement("div");
        el.id = "NAIModUi";
        el.style.padding = "0.5rem";
        return el;
    })();
    ui.innerHTML = '';

    // DirectorTool内かどうかを判定
    const isDirectorTool = isDirectorToolEditor();

    if (!isDirectorTool) {
        // 通常のUI(DirectorTool以外)
        let hr = document.createElement("hr");
        hr.style.color = "grey";
        hr.style.borderWidth = "2px";

        let [optionField, chantsField, suggestionField] = ['option', 'chants', 'suggestion'].map(id => {
            let el = document.createElement("div");
            el.id = `${id}Field`;
            return el;
        });

        [optionField, hr.cloneNode(), chantsField, hr.cloneNode(), suggestionField].forEach(el => ui.appendChild(el));

        let openModalButton = createButton("Open Settings", "blue", () => {
            modal.style.display = "block";
        });
        optionField.appendChild(openModalButton);

        let clearButton = createButton("Clear Suggestion", "red", () => {
            document.getElementById("suggestionField").textContent = "";
        });
        optionField.appendChild(clearButton);

        // Chantsボタンを追加
        try {
            const response = await fetch(chantURL);
            chants = await response.json();
            chants.forEach(chant => {
                let button = createButton(chant.name, colors[chant.color][1], () => Append(chant.name, null));
                chantsField.appendChild(button);
            });
        } catch (error) {
            console.error("Error fetching chants:", error);
        }
    } else {
        // DirectorTool用の簡素化されたUI
        let suggestionField = document.createElement("div");
        suggestionField.id = "suggestionField";
        ui.appendChild(suggestionField);
    }

    putUI(ui);

    // seedButtonの更新(必要に応じて)
    seedButton = Array.from(document.querySelectorAll('span[style]'))
        .find(x => x.textContent.trim().match(/^[0-9]*(seed|シード)/i))?.closest("button");

    if (seedButton) {
        [jpegButton, jpegQuality].forEach(el => {
            seedButton.classList.forEach(x => el.classList.add(x));
            if (!document.getElementById(el.id)) {
                seedButton.parentNode.insertBefore(el, seedButton);
            }
        });

    }

    let widthInput = document.querySelector('input[type="number"][step="64"]');
    if (widthInput) {
        let displayAspect = document.querySelector("#displayAspect") || createDisplayAspect();
        widthInput.parentNode.appendChild(displayAspect);
        updateAspectInputs();
    }
};

const putUI = ui => {
    const editor = document.querySelector(".ProseMirror");
    if (editor) {
        editor.parentElement.appendChild(ui);
    }
    seedButton = Array.from(document.querySelectorAll('span[style]'))
        .find(x => x.textContent.trim().match(/^[0-9]*(seed|シード)/i))?.closest("button");

    if (seedButton && !document.getElementById("jpegButton")) {
        let jpegButton = createButton("JPEG", "", saveJpeg);
        jpegButton.id = "jpegButton";
        let jpegQuality = document.createElement("div");
        let input = document.createElement("input");
        Object.assign(input, {
            value: 0.85,
            id: "jpegQuality",
            type: "number",
            step: "0.01",
            max: "1",
            min: "0",
            style: "maxWidth: 3rem"
        });
        jpegQuality.appendChild(input);

        [jpegButton, jpegQuality].forEach(el => {
            seedButton.classList.forEach(x => el.classList.add(x));
            seedButton.parentNode.insertBefore(el, seedButton);
        });
    }

    let widthInput = document.querySelector('input[type="number"][step="64"]');
    if (widthInput) {
        let displayAspect = document.querySelector("#displayAspect") || createDisplayAspect();
        widthInput.parentNode.appendChild(displayAspect);
        updateAspectInputs();
    }
};

const createDisplayAspect = () => {
    let displayAspect = document.createElement("div");
    displayAspect.id = "displayAspect";
    let container = document.createElement("div");
    container.style.maxWidth = "130px";
    container.style.display = "flex";
    let [widthAspect, heightAspect] = ['width', 'height'].map(type => {
        let input = document.createElement("input");
        input.type = "number";
        return input;
    });
    let largeCheck = document.createElement("input");
    largeCheck.type = "checkbox";
    largeCheck.title = "Check to large size.";
    let submitButton = document.createElement("input");
    submitButton.type = "submit";
    submitButton.value = "Aspect => Pixel";

    [widthAspect, heightAspect].forEach(input => {
        document.querySelector('input[type="number"][step="64"]').classList.forEach(x => input.classList.add(x));
    });

    [widthAspect, heightAspect, largeCheck].forEach(el => container.appendChild(el));
    [container, submitButton].forEach(el => displayAspect.appendChild(el));

    submitButton.addEventListener("click", () => {
        let wa = widthAspect.value;
        let ha = heightAspect.value;
        let maxpixel = largeCheck.checked ? 1664 * 1664 : 1024 * 1024;
        let as = aspectList(wa, ha, maxpixel);
        if (as.length) {
            changePixel(...as[as.length - 1]);
        }
    });

    return displayAspect;
};

const updateAspectInputs = () => {
    let displayAspect = document.querySelector("#displayAspect");
    if (displayAspect) {
        let [widthAspect, heightAspect] = displayAspect.querySelectorAll("input");
        let Aspect = getAspect(Array.from(document.querySelectorAll('input[type="number"][step="64"]')).map(x => x.value));
        [widthAspect.value, heightAspect.value] = Aspect;
    }
};

// ヘルパー関数
const createButton = (text, color, onClick) => {
    let button = document.createElement("button");
    button.textContent = text;
    button.style.color = color;
    button.addEventListener("click", onClick);
    return button;
};

// イベントリスナー
document.addEventListener("blur", event => {
    if (event.target.tagName.toLowerCase() === 'button') {
        setTimeout(() => {
            Build();
        }, 1000);
    }
}, true);

document.addEventListener("keydown", e => {
    if (e.ctrlKey && e.code === "ArrowUp") {
        e.preventDefault();
        Crease(1);
    }
    if (e.ctrlKey && e.code === "ArrowDown") {
        e.preventDefault();
        Crease(-1);
    }
});

// ProseMirrorエディタの監視
const observeEditor = (editor) => {
    if (editor) {
        editor.addEventListener("input", e => {
            const now = new Date();
            lastTyped = now;
            if (now - suggested_at > SUGGESTION_LIMIT) {
                Suggest(editor);
                suggested_at = now;
            } else {
                setTimeout(() => {
                    if (lastTyped === now) {
                        Suggest(editor);
                        suggested_at = new Date();
                    }
                }, 1000);
            }
        });

        editor.addEventListener("click", e => {
            const now = new Date();
            if (now - suggested_at > SUGGESTION_LIMIT / 2) {
                Suggest(editor);
                suggested_at = now;
            } else {
                setTimeout(() => {
                    if (lastTyped === now) {
                        Suggest(editor);
                        suggested_at = new Date();
                    }
                }, 1000);
            }
        });
    }
};

// DOM変更の監視
const observeDOM = () => {
    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            mutation.addedNodes.forEach((node) => {
                if (node.nodeType === 1) { // ELEMENT_NODE
                    const editors = node.getElementsByClassName("ProseMirror");
                    Array.from(editors).forEach(editor => {
                        if (!editor.hasAttribute('data-naimod-observed')) {
                            observeEditor(editor);
                            editor.setAttribute('data-naimod-observed', 'true');
                        }
                    });
                }
            });
        });
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
};

// タグデータの取得と処理を最適化
fetch("https://gist.githubusercontent.com/vkff5833/275ccf8fa51c2c4ba767e2fb9c653f9a/raw/danbooru.json")
    .then(response => response.json())
    .then(data => {
        allTags = data;
        return fetch("https://gist.githubusercontent.com/vkff5833/275ccf8fa51c2c4ba767e2fb9c653f9a/raw/danbooru_wiki.slim.json");
    })
    .then(response => response.json())
    .then(wikiPages => {
        // wikiPagesをマップに変換して検索を高速化
        const wikiMap = new Map(wikiPages.map(page => [page.name, page]));
        
        allTags = allTags.map(x => {
            const wikiPage = wikiMap.get(x.name);
            if (wikiPage) {
                // Set操作を1回だけ実行
                const allTerms = new Set(x.terms);
                wikiPage.otherNames.forEach(name => allTerms.add(name));
                x.terms = Array.from(allTerms);
            }
            return x;
        });
    });

// 初期化処理を最適化
let init = setInterval(() => {
    const editors = document.getElementsByClassName("ProseMirror");
    if (editors.length > 0) {
        clearInterval(init);
        chantURL = getChantURL(null);
        
        // エディタの監視を一括で設定
        const observedEditors = new Set();
        Array.from(editors).forEach(editor => {
            if (!observedEditors.has(editor)) {
                observeEditor(editor);
                editor.setAttribute('data-naimod-observed', 'true');
                observedEditors.add(editor);
            }
        });
        
        Build(); // UIの構築をここで行う
        observeDOM();
    }
}, 100);