NovelAI Prompt Composer and Tag Manager

Enhances NovelAI image generation with a prompt composer. Allows saving, categorizing, and quickly toggling common prompt elements.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         NovelAI Prompt Composer and Tag Manager
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Enhances NovelAI image generation with a prompt composer. Allows saving, categorizing, and quickly toggling common prompt elements.
// @author       ManuMonkey
// @license      MIT
// @match        http*://novelai.net/image
// @require      http://code.jquery.com/jquery-latest.js
// ==/UserScript==

/*
User Guide:
1. Click the "Compose Prompt" button to open the Prompt Composer.
2. Manage your categories and tags.
3. Use checkboxes to toggle tags on/off.
4. Click "Generate Prompt" to create your prompt.
*/

const promptComposerCSS = `
.prompt-composer-modal {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background-color: #f9f9f9;
    padding: 20px;
    border: 2px solid #333;
    border-radius: 10px;
    z-index: 9999999999;
    width: 600px;
    height: 80vh;
    display: flex;
    flex-direction: column;
    color: #333;
}

/* Fix header and footer positions */
.prompt-composer-title,
.button-container,
.bottom-buttons-container {
    flex-shrink: 0;
}

/* Make middle content one scrollable block */
.modal-content {
    flex: 1;
    overflow-y: auto;
    min-height: 0;
}

/* Remove individual scroll areas */
.character-list,
.character-panel {
    max-height: none;
    overflow: visible;
}

/* Ensure bottom buttons stay visible */
.bottom-buttons-container {
    margin-top: 20px;
    padding-top: 20px;
    background-color: #f9f9f9;
    border-top: 1px solid #eee;
}

@media screen and (min-width: 1200px) {
    .prompt-composer-modal {
        width: 1000px;
    }
}

.prompt-composer-title {
    text-align: center;
    margin-bottom: 20px;
    color: #333;
}

.button-container {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-bottom: 20px;
    justify-content: flex-start;
}

.prompt-set-controls {
    display: flex;
    gap: 10px;
    margin-left: auto; /* Push to the right */
    flex-wrap: wrap;
}

.toggle-button {
    position: relative;
    width: 32px;  /* Make buttons square */
    height: 32px;
    padding: 5px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.toggle-button::before {
    content: attr(data-full-text);
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    background-color: #333;
    color: white;
    padding: 5px 10px;
    border-radius: 5px;
    font-size: 14px;
    white-space: nowrap;
    visibility: hidden;
    opacity: 0;
    transition: opacity 0.2s;
    z-index: 10000;
}

.toggle-button:hover::before {
    visibility: visible;
    opacity: 1;
}

.toggle-button:active {
    background-color: #444;
}

.category-container {
    margin-bottom: 15px;
}

.category-label {
    font-weight: bold;
    color: #333;
    margin-right: auto; /* Push gender selector to the right */
    white-space: nowrap; /* Prevent character label from wrapping */
}

.checkbox-container {
    width: 100%; /* Take full width of section */
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.checkbox-label {
    color: #333;
    display: flex;
    align-items: center;
    white-space: nowrap;
    margin-right: 10px;
}

.checkbox-input {
    margin-right: 5px;
}

.weight-display {
    font-size: 0.8em;
    color: #666;
}

.weight-control {
    display: none;
    align-items: center;
    margin-left: 5px;
}

.weight-input {
    width: 40px;
    margin: 0 5px;
    padding:2px 0px 2px 9px;
}

.weight-button {
    padding: 0 5px;
}

.delete-button {
    margin-left: 5px;
    padding: 0px 5px;
    border-radius: 3px;
    border: none;
    cursor: pointer;
    background-color: #ff4d4d;
    color: #fff;
    display: none;
}

.add-tag-container {
    display: none;
    margin-top: 10px;
    align-items: center;
}

.add-tag-input {
    flex-grow: 1;
    padding: 5px;
    border-radius: 5px;
    border: 1px solid #333;
    margin-right: 10px;
}

.add-tag-button {
    padding: 5px 10px;
    border-radius: 5px;
    border: none;
    cursor: pointer;
    background-color: #333;
    color: #fff;
    white-space: nowrap;
}

.tag-textarea {
    width: 100%;
    margin-top: 10px;
    border-radius: 5px;
    padding: 5px;
}

.generate-button, .close-button {
    color: #333;
    background-color: rgb(108, 245, 74);
    font-size: medium;
    padding: 10px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    width: 48%; /* Make buttons take up almost half the width each */
}

.generate-button:hover, .close-button:hover {
    /* background-color: #87e43b; */
}

.close-button {
    background-color: #ccc;
}

.compose-prompt-button {
    color: #333;
    background-color: rgb(246, 245, 244);
    font-size: small;
    padding: 5px 10px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    margin-bottom: 10px;
}

.compose-prompt-button:hover {
    background-color: #e6e6e6;
}

.add-category-container {
    display: flex;
    margin-bottom: 15px;
    align-items: center;
}

.add-category-input {
    flex-grow: 1;
    padding: 5px;
    border-radius: 5px;
    border: 1px solid #333;
    margin-right: 10px;
}

.add-category-button {
    padding: 5px 10px;
    border-radius: 5px;
    border: none;
    cursor: pointer;
    background-color: #333;
    color: #fff;
    white-space: nowrap;
}

.category-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 10px;
}

.category-management-buttons {
    display: flex;
    align-items: center;
    margin-left: 10px;
}

.delete-category-button {
    background-color: #ff4d4d;
    color: white;
    border: none;
    border-radius: 5px;
    padding: 5px 10px;
    font-size: 14px;
    cursor: pointer;
    margin-left: 10px;
}

.delete-category-button:hover {
    background-color: #ff3333;
}

.delete-category-button:active {
    background-color: #e60000;
}

.move-category-button {
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 50%;
    width: 24px;
    height: 24px;
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
    margin-right: 5px;
}

.move-category-button:hover {
    background-color: #45a049;
}

.move-category-button {
    width: auto;
    height: auto;
    border-radius: 5px;
    padding: 5px 8px;
}

.category-management {
    margin-bottom: 15px;
    border-top: 1px solid #ccc;
    padding-top: 15px;
}

.bottom-buttons-container {
    display: flex;
    justify-content: space-between;
    margin-top: 20px;
}

@media screen and (min-width: 1200px) {
    .prompt-composer-modal {
        width: 1000px; /* Increased width for larger screens */
    }

    .categories-container {
        display: flex;
        flex-wrap: wrap;
        justify-content: space-between;
    }

    .category-container {
        width: 48%; /* Slightly less than 50% to account for margins */
    }
}

.categories-container {
}

.character-label {
    font-weight: bold;
    color: #333;
    margin-right: auto; /* This pushes the delete button to the right */
}

.character-header {
    display: flex;
    align-items: center;
    padding: 5px 0;
    margin-bottom: 10px;
    border-bottom: 1px solid #ccc;
}

.header-content {
    display: flex;
    align-items: center;
    gap: 15px;
    flex: 1;
}

.gender-selector {
    display: flex;
    gap: 12px;
    margin-left: auto;
    margin-right: 15px;
    white-space: nowrap; /* Prevent wrapping */
    flex-shrink: 0; /* Prevent the gender selector from shrinking */
}

.gender-selector label {
    display: flex;
    align-items: center;
    gap: 4px;
    color: #333;
    cursor: pointer;
    white-space: nowrap; /* Ensure labels don't wrap */
}

.tag-section h5 {
    color: #333;
    margin-top: 8px;
    font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
}

.character-manager-container h4 {
    color: #333;
    font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
}

.character-list {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 20px;
    margin-bottom: 20px;
    align-items: stretch; /* Make all panels stretch to match the tallest */
}

.character-panel {
    background-color: #f5f5f5;
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 15px;
    display: flex;
    flex-direction: column;
    width: 100%; /* Ensure panel takes full width of its grid cell */
}

.tag-sections {
    flex: 1; /* Allow tag sections to grow */
    overflow-y: auto;
    width: 100%; /* Take full width of panel */
}

.tag-section {
    width: 100%; /* Take full width of container */
}

.character-manager-container {
    margin-top: 5px;
}

.add-character-button {
    width: 100%;
    padding: 10px;
    margin-top: 10px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}

.add-character-button:hover {
    background-color: #45a049;
}

.character-panel .infotext-input {
    margin-left: 8px;
    padding: 2px 4px;
    border: 1px solid #ccc;
    border-radius: 3px;
    font-size: 12px;
    width: 150px;
}

.character-panel .checkbox-label {
    display: flex;
    align-items: center;
    padding: 2px 0;
}

.category-header {
    color: #333;
    margin-top: 8px;
    font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
}

.character-actions {
    display: flex;
    gap: 8px;
    margin-left: auto;
}

.save-character-button {
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 3px;
    padding: 2px 8px;
    cursor: pointer;
    font-size: 12px;
}

.load-character-select {
    max-width: 120px;
    padding: 2px 4px;
    border-radius: 3px;
    border: 1px solid #ccc;
    font-size: 12px;
    color: #333;
}

.delete-saved-button {
    background-color: #ff4d4d;
    color: white;
    border: none;
    border-radius: 3px;
    padding: 2px 8px;
    cursor: pointer;
    font-size: 12px;
}

.delete-saved-button:hover {
    background-color: #ff3333;
}

.load-character-container {
    display: flex;
    gap: 4px;
    align-items: center;
}

.load-prompt-container {
    display: flex;
    align-items: center;
    gap: 5px;
}

.load-prompt-select {
    width: auto;  /* Override the width for the select */
    min-width: 80px;
}

.delete-prompt-button {
    padding: 5px 8px;
    border-radius: 5px;
    border: none;
    cursor: pointer;
    background-color: #ff4d4d;
    color: #fff;
}

.delete-prompt-button:hover {
    background-color: #ff3333;
}

@media screen and (max-width: 800px) {
    .button-container {
        flex-direction: column;
        align-items: stretch;
    }
    
    .prompt-set-controls {
        margin-left: 0;
        margin-top: 10px;
        width: 100%;
    }

    .toggle-button {
        width: 100%;
        height: auto;
    }
    
    .toggle-button::before {
        display: none;  /* Hide tooltips on mobile */
    }
}
`;

(function () {
    'use strict';

    // Function to inject CSS
    function injectCSS() {
        const style = document.createElement('style');
        style.textContent = promptComposerCSS;
        document.head.appendChild(style);
    }

    // Call the injectCSS function immediately
    injectCSS();

    let tagArea = null;
    let promptComposerData = JSON.parse(localStorage.getItem('promptComposerData')) || {
        categories: [
            { name: "Artists", tags: [], pretext: "artist:" },
            { name: "Persons", tags: [] },
            { name: "Locations", tags: [] },
            { name: "Expressions", tags: [] }
        ],
        customTags: {},
        savedCharacters: {} // Initialize savedCharacters as an empty object
    };

    let savedCharacters = JSON.parse(localStorage.getItem('promptComposerCharacters')) || {};
    console.log('Initial load of savedCharacters:', savedCharacters);
    let savedPromptSets = JSON.parse(localStorage.getItem('promptComposerSets')) || {};

    function saveToLocalStorage() {
        localStorage.setItem('promptComposerData', JSON.stringify(promptComposerData));
    }

    let modalDiv; // Declare modalDiv in the global scope
    let categoriesContainer;

    let isV4 = false;

    function detectVersion() {
        // Look for "Add Character" button which is unique to V4
        const addCharacterBtn = Array.from(document.querySelectorAll('button')).find(btn => 
            btn.textContent.trim() === 'Add Character'
        );
        isV4 = !!addCharacterBtn;
        console.log("Detected version:", isV4 ? "V4" : "V3");
    }

    function getPromptFields() {
        const allFields = document.querySelectorAll('.ProseMirror p');
        // Only use the first half of the fields since the latter half are duplicates
        const uniqueFields = Array.from(allFields).slice(0, Math.floor(allFields.length / 2));
        
        return {
            generalField: uniqueFields[0], // First field is always general prompt
            characterFields: uniqueFields.slice(1).filter(field => field.textContent.trim() !== '') // Only get non-empty character fields
        };
    }

    function getCategoryTags() {
        const tagCategories = {
            'Artists': [],
            'Activities': [],
            'Participants': [],
            'Body Features': [],
            'Clothing': [],
            'Emotions': [],
            'Backgrounds': []
        };

        // Collect tags from existing categories that match our desired categories
        promptComposerData.categories.forEach(category => {
            if (tagCategories.hasOwnProperty(category.name)) {
                tagCategories[category.name] = category.tags.map(tag => tag.name);
            }
        });

        return tagCategories;
    }

    // Add this function at the module level (before addCharacter)
    function updateCharacterPrompt(panel, characterNum) {
        // Get checked checkbox tags
        const checkedTags = panel.find('.checkbox-input:checked').map(function() {
            return $(this).attr('data-tag');
        }).get();

        // Get custom traits that don't have checkboxes
        const customTraits = panel.find('.custom-traits-input').val()
            .split(',')
            .map(t => t.trim())
            .filter(t => t && !checkedTags.includes(t)); // Filter out traits that have checkboxes

        const traits = [...checkedTags, ...customTraits];
        
        if (traits.length > 0) {
            const charPrompt = traits.join(', ');
            
            // Update the corresponding prompt field
            const { characterFields } = getPromptFields();
            if (characterFields[characterNum]) {
                characterFields[characterNum].innerHTML = charPrompt;
            }
        }
    }

    function addCharacter(characterList, characterData = null, fieldIndex = null) {
        const characterNum = characterList.children().length;
        if (characterNum >= 4) return;
        
        const characterPanel = $('<div></div>').addClass('character-panel');
        
        // Character header with label, gender selector, and delete button
        const headerDiv = $('<div></div>').addClass('character-header');
        const headerContent = $('<div></div>').addClass('header-content');
        
        const characterLabel = $('<span></span>')
            .addClass('character-label')
            .text(`Character ${characterNum + 1}`);

        // Create gender selector
        const genderSelector = $('<div></div>').addClass('gender-selector');
        
        ['girl', 'boy'].forEach(gender => {
            const label = $('<label></label>');
            const checkbox = $('<input type="checkbox">')
                .addClass('checkbox-input gender-checkbox')
                .attr('data-tag', gender)
                .prop('checked', characterData?.tags?.includes(gender) || false)
                .on('change', function() {
                    if (this.checked) {
                        genderSelector.find('.gender-checkbox').not(this).prop('checked', false);
                    }
                    updateCharacterPrompt(characterPanel, characterNum);
                });
                
            label.append(checkbox, gender);
            genderSelector.append(label);
        });

        // Create save/load controls
        const characterActions = $('<div></div>').addClass('character-actions');
        
        const saveBtn = $('<button>Save</button>')
            .addClass('save-character-button')
            .click(() => {
                const name = prompt('Enter a name for this character:');
                if (name) {
                    const characterState = {
                        gender: characterPanel.find('.gender-checkbox:checked').attr('data-tag') || null,
                        tags: characterPanel.find('.checkbox-input:checked:not(.gender-checkbox)').map(function() {
                            return $(this).attr('data-tag');
                        }).get(),
                        customTraits: characterPanel.find('.custom-traits-input').val()
                    };
                    
                    console.log('Saving character:', name, characterState);
                    savedCharacters[name] = characterState;
                    localStorage.setItem('promptComposerCharacters', JSON.stringify(savedCharacters));
                    console.log('Updated savedCharacters:', savedCharacters);
                    updateLoadSelects();
                }
            });

        const loadContainer = $('<div></div>').addClass('load-character-container');
        
        const loadSelect = $('<select></select>')
            .addClass('load-character-select')
            .append('<option value="">Load char...</option>');

        // Add this right after creating loadSelect
        Object.entries(savedCharacters).forEach(([name, data]) => {
            loadSelect.append(new Option(name, name));
        });

        const deleteSavedBtn = $('<button>×</button>')
            .addClass('delete-saved-button')
            .hide()
            .click(() => {
                const selectedName = loadSelect.val();
                if (selectedName && confirm(`Delete saved character "${selectedName}"?`)) {
                    delete savedCharacters[selectedName];
                    localStorage.setItem('promptComposerCharacters', JSON.stringify(savedCharacters));
                    updateLoadSelects();
                }
            });

        loadSelect.on('change', function() {
            const selectedName = $(this).val();
            deleteSavedBtn.toggle(!!selectedName);
            
            if (selectedName) {
                const savedChar = savedCharacters[selectedName];
                
                characterPanel.find('.checkbox-input').prop('checked', false);
                
                if (savedChar.gender) {
                    characterPanel.find(`.gender-checkbox[data-tag="${savedChar.gender}"]`).prop('checked', true);
                }
                
                savedChar.tags.forEach(tag => {
                    characterPanel.find(`.checkbox-input[data-tag="${tag}"]`).prop('checked', true);
                });
                
                characterPanel.find('.custom-traits-input').val(savedChar.customTraits || '');
                
                updateCharacterPrompt(characterPanel, characterNum);
            }
        });

        loadContainer.append(loadSelect, deleteSavedBtn);
        characterActions.append(saveBtn, loadContainer);
        headerContent.append(characterLabel, genderSelector, characterActions);
        headerDiv.append(headerContent);

        const deleteCharacterBtn = $('<button>×</button>')
            .addClass('delete-character-button')
            .click(() => {
                if (confirm('Delete this character?')) {
                    characterPanel.remove();
                    $('.character-panel').each((idx, panel) => {
                        $(panel).find('.character-label').text(`Character ${idx + 1}`);
                        updateCharacterPrompt($(panel), idx);
                    });
                }
            });

        headerDiv.append(deleteCharacterBtn);

        // Tag sections from existing categories
        const tagCategories = getCategoryTags();
        const tagSection = $('<div></div>').addClass('tag-sections');

        Object.entries(tagCategories).forEach(([category, tags]) => {
            if (tags.length > 0) {
                const section = $('<div></div>').addClass('tag-section');
                const title = $('<h5 class="category-header"></h5>').text(category);
                const checkboxContainer = $('<div></div>').addClass('checkbox-container');
                
                tags.forEach(tag => {
                    const checkboxLabel = $('<label></label>').addClass('checkbox-label');
                    const checkbox = $('<input type="checkbox">')
                        .addClass('checkbox-input')
                        .attr('data-tag', tag)
                        .prop('checked', characterData?.tags?.includes(tag) || false)
                        .on('change', () => updateCharacterPrompt(characterPanel, characterNum));

                    // Add infotext input (initially hidden)
                    const infotextInput = $('<input type="text">')
                        .addClass('infotext-input')
                        .attr('placeholder', 'Infotext...')
                        .val(tag.infotext || '')
                        .hide();

                    infotextInput.on('input', function() {
                        const tagObj = promptComposerData.categories
                            .find(cat => cat.name === category)?.tags
                            .find(t => t.name === tag);
                        if (tagObj) {
                            tagObj.infotext = $(this).val();
                            checkboxLabel.attr('title', $(this).val());
                            saveToLocalStorage();
                        }
                    });

                    // Set initial tooltip if infotext exists
                    const tagObj = promptComposerData.categories
                        .find(cat => cat.name === category)?.tags
                        .find(t => t.name === tag);
                    if (tagObj?.infotext) {
                        checkboxLabel.attr('title', tagObj.infotext);
                        infotextInput.val(tagObj.infotext);
                    }
                        
                    checkboxLabel.append(checkbox, tag, infotextInput);
                    checkboxContainer.append(checkboxLabel);
                });
                
                section.append(title, checkboxContainer);
                tagSection.append(section);
            }
        });

        // Custom traits input for non-checkbox tags
        const customTraitsInput = $('<textarea></textarea>')
            .addClass('custom-traits-input')
            .attr('placeholder', 'Add custom traits (comma-separated)...')
            .on('input', function() {
                updateCharacterPrompt(characterPanel, characterNum);
            });

        characterPanel.append(
            headerDiv,
            tagSection,
            $('<div>Custom Traits:</div>').addClass('category-header'),
            customTraitsInput
        );

        characterList.append(characterPanel);
        updateCharacterPrompt(characterPanel, characterNum);
    }

    function createCharacterManager() {
        const container = $('<div></div>').addClass('character-manager-container');
        const characterList = $('<div></div>').addClass('character-list');
        
        // Get existing character fields
        const { characterFields } = getPromptFields();

        // Initialize the character loader dropdowns
        updateLoadSelects();

        // Load existing characters first
        characterFields.forEach((field, index) => {
            const existingTags = field.textContent.split(',').map(t => t.trim()).filter(t => t);
            addCharacter(characterList, { tags: existingTags }, index);
        });

        // Add character button (up to 4)
        const addCharacterBtn = $('<button>Add Character</button>')
            .addClass('add-character-button')
            .click(() => {
                if (characterList.children().length < 4) {
                    addCharacter(characterList, null, null);
                }
            });
        
        container.append(characterList, addCharacterBtn);

        return container;
    }

    function renderCategories(editMode = false) {
        categoriesContainer.empty();
        promptComposerData.categories.forEach((category, index) => {
            createCategorySection(category, index, categoriesContainer);
        });
        if (editMode) {
            $('.move-category-button, .delete-category-button').toggle();
        }
    }

    function openPromptComposer() {
        console.log("openPromptComposer called");
        try {
            detectVersion();
            
            if ($('#promptComposerModal').length) {
                $('#promptComposerModal').remove();
                return;
            }

            modalDiv = $('<div id="promptComposerModal"></div>').addClass('prompt-composer-modal');
            const title = $('<h3>Prompt Composer</h3>').addClass('prompt-composer-title');
            modalDiv.append(title);

            const buttonContainer = $('<div></div>').addClass('button-container');
            const editButton = $('<button>E</button>')
                .addClass('toggle-button')
                .attr('data-full-text', 'Edit Tags')
                .click(() => {
                    $('.add-tag-container, .delete-button').toggle();
                    $('.add-tag-container:visible').css('display', 'flex');
                });
            const weightToggleButton = $('<button>W</button>')
                .addClass('toggle-button')
                .attr('data-full-text', 'Toggle Weights')
                .click(() => {
                    const weightsVisible = !$('.weight-control').first().is(':visible');
                    $('.weight-control').toggle(weightsVisible);
                    $('.weight-display').each(function () {
                        const label = $(this).closest('label');
                        const weightControl = label.find('.weight-control');
                        const tag = {
                            weight: parseFloat(weightControl.find('input[type="number"]').val())
                        };
                        updateWeightDisplay(tag, $(this), weightControl);
                    });
                });
            const categoryManagementButton = $('<button>C</button>')
                .addClass('toggle-button')
                .attr('data-full-text', 'Manage Categories')
                .click(() => {
                    $('.category-management').toggle();
                    $('.move-category-button, .delete-category-button').toggle();
                });
            const infotextToggleButton = $('<button>I</button>')
                .addClass('toggle-button')
                .attr('data-full-text', 'Manage Infotexts')
                .click(() => {
                    $('.infotext-input').toggle();
                });

            // Add prompt set management buttons
            const savePromptButton = $('<button>S</button>')
                .addClass('toggle-button')
                .attr('data-full-text', 'Save Prompt Set')
                .click(saveCurrentPromptSet);

            const loadContainer = $('<div></div>').addClass('load-prompt-container');
            const deletePromptButton = $('<button>×</button>')
                .addClass('delete-prompt-button toggle-button')
                .attr('data-full-text', 'Delete Prompt Set')
                .hide()
                .click(() => {
                    const selectedName = loadSelect.val();
                    if (selectedName) {
                        deletePromptSet(selectedName);
                        loadSelect.val('');
                        deletePromptButton.hide();
                    }
                });

            const loadSelect = $('<select></select>')
                .addClass('load-prompt-select toggle-button')
                .append('<option value="">Load...</option>')
                .on('change', function() {
                    const selectedName = $(this).val();
                    deletePromptButton.toggle(!!selectedName);
                    if (selectedName) {
                        loadPromptSet(selectedName);
                    }
                });

            loadContainer.append(loadSelect, deletePromptButton);

            // Create container for prompt set controls
            const promptSetControls = $('<div></div>').addClass('prompt-set-controls');

            // Add the regular buttons
            buttonContainer.append(
                editButton, 
                weightToggleButton, 
                categoryManagementButton, 
                infotextToggleButton
            );

            // Add the prompt set controls to their container
            promptSetControls.append(
                savePromptButton,
                loadContainer
            );

            // Add the prompt set controls to the main button container
            buttonContainer.append(promptSetControls);

            modalDiv.append(buttonContainer);

            // Create scrollable content container
            const modalContent = $('<div></div>').addClass('modal-content');
            
            // Add category management (initially hidden)
            const categoryManagement = $('<div></div>').addClass('category-management').hide();
            const addCategoryContainer = $('<div></div>').addClass('add-category-container');
            const addCategoryInput = $('<input type="text">').addClass('add-category-input')
                .attr('placeholder', 'Add new category...');
            const addCategoryButton = $('<button>Add Category</button>').addClass('add-category-button')
                .click(() => {
                    const newCategory = addCategoryInput.val().trim();
                    if (newCategory && !promptComposerData.categories.some(cat => cat.name === newCategory)) {
                        promptComposerData.categories.push({ name: newCategory, tags: [] });
                        saveToLocalStorage();
                        addCategoryInput.val('');
                        renderCategories(true);
                    }
                });
            addCategoryContainer.append(addCategoryInput, addCategoryButton);
            categoryManagement.append(addCategoryContainer);

            // Create a container for categories
            categoriesContainer = $('<div></div>').addClass('categories-container');

            // Add all the middle content to modalContent
            modalContent.append(categoryManagement);
            modalContent.append(categoriesContainer);

            // Render the categories
            renderCategories();

            if (isV4) {
                const characterManager = createCharacterManager();
                modalContent.append(characterManager);
            }

            // Create bottom buttons container
            const bottomButtonsContainer = $('<div></div>').addClass('bottom-buttons-container');

            const generateButton = $('<button>Generate Prompt</button>').addClass('generate-button')
                .click(() => {
                    const { generalField, characterFields } = getPromptFields();
                    
                    // Update general prompt field first
                    const generalPrompt = generatePrompt();
                    if (generalField) {
                        generalField.innerHTML = generalPrompt;
                    }
                    
                    saveToLocalStorage();
                    modalDiv.remove();
                });

            const cancelButton = $('<button>Close</button>').addClass('close-button')
                .click(() => {
                    saveToLocalStorage();
                    modalDiv.remove();
                });

            bottomButtonsContainer.append(generateButton, cancelButton);

            // Add the content container to the modal
            modalDiv.append(modalContent);

            // Add bottom buttons last
            modalDiv.append(bottomButtonsContainer);

            $('body').append(modalDiv);

            // Adjust layout based on screen size
            adjustLayout();

            $(window).on('resize', adjustLayout);

            console.log("Modal appended to body");

            // Add this line after creating the modal
            updatePromptSetSelect();
        } catch (error) {
            console.error("Error in openPromptComposer:", error);
        }
    }

    function adjustLayout() {
        if ($(window).width() >= 1600) {
            $('.category-container').css('width', '32%');
            modalDiv.css('width', '1500px');
        }
        else if ($(window).width() >= 1200) {
            $('.category-container').css('width', '48%');
            modalDiv.css('width', '1000px');
        } else {
            $('.category-container').css('width', '100%');
            modalDiv.css('width', '600px');
        }
    }

    function createCategorySection(category, index, modalDiv) {
        const categoryContainer = $('<div></div>').addClass('category-container');
        const categoryHeader = $('<div></div>').addClass('category-header');
        const label = $('<label></label>').text(category.name + ':').addClass('category-label');

        const deleteCategoryButton = $('<button>Delete Category</button>')
            .addClass('delete-category-button')
            .hide() // Initially hide the delete button
            .click(() => {
                if (confirm(`Are you sure you want to delete the category "${category.name}" and all its tags?`)) {
                    promptComposerData.categories.splice(index, 1);
                    saveToLocalStorage();
                    renderCategories(true);
                }
            });

        const moveCategoryUpButton = $('<button>↑</button>').addClass('move-category-button')
            .hide() // Initially hide the move up button
            .click(() => {
                if (index > 0) {
                    [promptComposerData.categories[index - 1], promptComposerData.categories[index]] =
                        [promptComposerData.categories[index], promptComposerData.categories[index - 1]];
                    saveToLocalStorage();
                    renderCategories(true);
                }
            });

        const moveCategoryDownButton = $('<button>↓</button>').addClass('move-category-button')
            .hide() // Initially hide the move down button
            .click(() => {
                if (index < promptComposerData.categories.length - 1) {
                    [promptComposerData.categories[index], promptComposerData.categories[index + 1]] =
                        [promptComposerData.categories[index + 1], promptComposerData.categories[index]];
                    saveToLocalStorage();
                    renderCategories(true);
                }
            });

        const categoryManagementButtons = $('<div></div>').addClass('category-management-buttons');
        categoryManagementButtons.append(moveCategoryUpButton, moveCategoryDownButton, deleteCategoryButton);

        categoryHeader.append(label, categoryManagementButtons);
        categoryContainer.append(categoryHeader);

        const checkboxContainer = $('<div></div>').addClass('checkbox-container');
        category.tags.forEach(tag => {
            createCheckbox(tag, category.name, checkboxContainer);
        });
        categoryContainer.append(checkboxContainer);

        const addTagContainer = $('<div></div>').addClass('add-tag-container');
        const addTagInput = $('<input type="text">').addClass('add-tag-input')
            .attr('placeholder', 'Add new ' + category.name.toLowerCase() + '...');
        const addTagButton = $('<button>Add</button>').addClass('add-tag-button');

        const addNewTag = () => {
            const newTag = addTagInput.val().trim();
            if (newTag && !category.tags.some(tag => tag.name === newTag)) {
                category.tags.push({ name: newTag, active: false, weight: 1 });
                createCheckbox({ name: newTag, active: false, weight: 1 }, category.name, checkboxContainer, true);
                addTagInput.val('');
                saveToLocalStorage();
            }
        };

        addTagButton.click(addNewTag);
        addTagInput.keypress(function (e) {
            if (e.which == 13) {
                e.preventDefault();
                addNewTag();
            }
        });

        addTagContainer.append(addTagInput, addTagButton);
        categoryContainer.append(addTagContainer);

        const textArea = $('<textarea></textarea>').addClass('tag-textarea')
            .attr('placeholder', 'Enter additional ' + category.name.toLowerCase() + ' here...');

        const customTags = promptComposerData.customTags[category.name] || '';
        textArea.val(customTags);

        textArea.on('input', function () {
            const customTagsValue = $(this).val().trim();
            promptComposerData.customTags[category.name] = customTagsValue;
            saveToLocalStorage();
        });

        categoryContainer.append(textArea);

        categoriesContainer.append(categoryContainer);
    }

    function createCheckbox(tag, categoryName, container, isNew = false) {
        const checkboxLabel = $('<label></label>').addClass('checkbox-label');
        const checkbox = $('<input type="checkbox">').addClass('checkbox-input').val(tag.name);
        checkbox.prop('checked', tag.active);
        checkbox.change(function () {
            tag.active = this.checked;
            saveToLocalStorage();
        });

        // Add infotext input
        const infotextInput = $('<input type="text">').addClass('infotext-input')
            .attr('placeholder', 'Infotext...')
            .val(tag.infotext || '')
            .hide(); // Initially hidden

        infotextInput.on('input', function () {
            tag.infotext = $(this).val();
            saveToLocalStorage();
        });

        // Display infotext on mouseover
        checkboxLabel.attr('title', tag.infotext || '');

        // Create weight display
        const weightDisplay = $('<span></span>').addClass('weight-display');

        // Create weight control
        const weightControl = $('<div></div>').addClass('weight-control');
        const weightInput = $('<input type="number" step="0.05" min="0.5" max="1.5">').addClass('weight-input')
            .val(tag.weight || 1);
        const decreaseButton = $('<button>-</button>').addClass('weight-button');
        const increaseButton = $('<button>+</button>').addClass('weight-button');

        decreaseButton.click(() => updateWeight(-0.05));
        increaseButton.click(() => updateWeight(0.05));
        weightInput.on('input', () => {
            tag.weight = parseFloat(weightInput.val());
            updateWeightDisplay(tag, weightDisplay, weightControl);
            saveToLocalStorage();
        });

        function updateWeight(change) {
            let newWeight = (tag.weight || 1) + change;
            newWeight = Math.round(newWeight * 20) / 20; // Round to nearest 0.05
            newWeight = Math.max(0.5, Math.min(1.5, newWeight)); // Clamp between 0.5 and 1.5
            tag.weight = newWeight;
            weightInput.val(newWeight);
            updateWeightDisplay(tag, weightDisplay, weightControl);
            saveToLocalStorage();
        }

        weightControl.append(weightInput, decreaseButton, increaseButton);
        checkboxLabel.append(checkbox, tag.name, weightDisplay, weightControl);

        const deleteButton = $('<button>×</button>').addClass('delete-button').click(() => {
            const category = promptComposerData.categories.find(cat => cat.name === categoryName);
            category.tags = category.tags.filter(t => t.name !== tag.name);
            saveToLocalStorage();
            checkboxLabel.remove();
        });
        if (isNew) {
            console.log("isNew is true");
            deleteButton.show();
        }

        checkboxLabel.append(deleteButton, infotextInput);
        container.append(checkboxLabel);

        updateWeightDisplay(tag, weightDisplay, weightControl);
    }

    function onReady() {
        setTimeout(placeComposerButton, 5000);
    }

    function placeComposerButton() {
        let composeButton = $('<button>Compose Prompt</button>')
            .addClass('compose-prompt-button')
            .click(openPromptComposer);

        let textAreas = document.querySelectorAll("[class='ProseMirror']");
        if (textAreas.length > 0) {
            let sidebar = textAreas[0].closest('div').parentElement;
            $(sidebar).prepend(composeButton);
        }
    }

    function generatePrompt() {
        if (!isV4) {
            // Existing V3 prompt generation
            return promptComposerData.categories.map(category => {
                const activeTags = category.tags
                    .filter(tag => tag.active)
                    .map(tag => {
                        let tagText = tag.name;
                        if (category.name === "Artists" && tagText !== "realistic") {
                            tagText = "artist:" + tagText;
                        }
                        if (tag.weight > 1) {
                            const repetitions = Math.round((tag.weight - 1) / 0.05);
                            tagText = '{'.repeat(repetitions) + tagText + '}'.repeat(repetitions);
                        } else if (tag.weight < 1) {
                            const repetitions = Math.round((1 - tag.weight) / 0.05);
                            tagText = '['.repeat(repetitions) + tagText + ']'.repeat(repetitions);
                        }
                        return tagText;
                    });
                const customTags = (promptComposerData.customTags[category.name] || '')
                    .split(',')
                    .map(tag => tag.trim())
                    .filter(tag => tag !== '');
                return [...activeTags, ...customTags];
            }).flat().join(", ");
        }

        // For V4, only generate the general prompt
        const generalTags = promptComposerData.categories.map(category => {
            const activeTags = category.tags
                .filter(tag => tag.active)
                .map(tag => formatTag(tag, category.name));
                
            const customTags = (promptComposerData.customTags[category.name] || '')
                .split(',')
                .map(tag => tag.trim())
                .filter(tag => tag !== '');

            return [...activeTags, ...customTags];
        }).flat();

        return generalTags.join(", ");
    }

    function updateWeightDisplay(tag, displayElement, controlElement) {
        const weight = tag.weight || 1;
        if (weight === 1) {
            displayElement.text('');
            if (controlElement) {
                controlElement.find('input').hide();
                controlElement.find('button').show();
            }
        } else {
            if (controlElement && $('.weight-control').first().is(':visible')) {
                displayElement.text('');
                controlElement.find('input, button').show();
            } else {
                displayElement.text(`:${weight.toFixed(2)}`);
                if (controlElement) {
                    controlElement.find('input').hide();
                    controlElement.find('button').show();
                }
            }
        }
    }

    function formatTag(tag, categoryName) {
        let tagText = tag.name;
        if (categoryName === "Artists" && tagText !== "realistic") {
            tagText = "artist:" + tagText;
        }
        if (tag.weight > 1) {
            const repetitions = Math.round((tag.weight - 1) / 0.05);
            tagText = '{'.repeat(repetitions) + tagText + '}'.repeat(repetitions);
        } else if (tag.weight < 1) {
            const repetitions = Math.round((1 - tag.weight) / 0.05);
            tagText = '['.repeat(repetitions) + tagText + ']'.repeat(repetitions);
        }
        return tagText;
    }

    // Add the updateLoadSelects function at the module level
    function updateLoadSelects() {
        console.log('updateLoadSelects called, savedCharacters:', savedCharacters);
        $('.load-character-select').each(function() {
            console.log('Found load-character-select element');
            const currentValue = $(this).val();
            $(this).empty().append('<option value="">Load char...</option>');
            
            Object.entries(savedCharacters).forEach(([name, data]) => {
                console.log('Adding character option:', name, data);
                $(this).append(new Option(name, name));
            });
            
            $(this).val(currentValue);
        });
    }

    function saveCurrentPromptSet() {
        const { generalField, characterFields } = getPromptFields();
        
        // Get current state of characters
        const characters = $('.character-panel').map(function() {
            const panel = $(this);
            return {
                gender: panel.find('.gender-checkbox:checked').attr('data-tag') || null,
                tags: panel.find('.checkbox-input:checked:not(.gender-checkbox)').map(function() {
                    return $(this).attr('data-tag');
                }).get(),
                customTraits: panel.find('.custom-traits-input').val()
            };
        }).get();

        // Create prompt set object
        const promptSet = {
            generalPrompt: generalField?.innerHTML || '',
            characters: characters,
            timestamp: new Date().toISOString()
        };

        // Show save dialog
        const name = prompt('Enter a name for this prompt set:');
        if (name) {
            savedPromptSets[name] = promptSet;
            localStorage.setItem('promptComposerSets', JSON.stringify(savedPromptSets));
            updatePromptSetSelect();
        }
    }

    function loadPromptSet(name) {
        const promptSet = savedPromptSets[name];
        if (!promptSet) return;

        // Update general prompt checkboxes and UI
        promptComposerData.categories.forEach(category => {
            category.tags.forEach(tag => {
                // Reset all tags to inactive first
                tag.active = false;
            });
        });

        // Parse the general prompt and activate corresponding tags
        const generalPromptTags = promptSet.generalPrompt.split(',').map(t => t.trim());
        generalPromptTags.forEach(tagText => {
            // Remove any weight modifiers ({} or [])
            const cleanTag = tagText.replace(/[\{\}\[\]]/g, '').trim();
            // Remove artist: prefix if present
            const tagName = cleanTag.replace(/^artist:/, '');

            // Find and activate the tag
            promptComposerData.categories.forEach(category => {
                const foundTag = category.tags.find(t => t.name === tagName);
                if (foundTag) {
                    foundTag.active = true;
                }
            });

            // Handle custom tags
            promptComposerData.categories.forEach(category => {
                const customTagsArea = $(`.category-container:contains("${category.name}") .tag-textarea`);
                const customTags = promptSet.customTags?.[category.name] || '';
                customTagsArea.val(customTags);
                promptComposerData.customTags[category.name] = customTags;
            });
        });

        // Update checkboxes in the UI
        $('.checkbox-input').each(function() {
            const tagName = $(this).val();
            const isActive = promptComposerData.categories.some(category => 
                category.tags.some(tag => tag.name === tagName && tag.active)
            );
            $(this).prop('checked', isActive);
        });

        // Update the actual text field
        const { generalField } = getPromptFields();
        if (generalField) {
            generalField.innerHTML = promptSet.generalPrompt;
        }

        // Handle characters in V4 mode
        if (isV4 && promptSet.characters) {
            const characterList = $('.character-list');
            if (characterList.length) {
                characterList.empty();
                promptSet.characters.forEach((charData, index) => {
                    addCharacter(characterList, charData, index);
                });
            }
        }

        saveToLocalStorage();
    }

    function deletePromptSet(name) {
        if (confirm(`Delete saved prompt set "${name}"?`)) {
            delete savedPromptSets[name];
            localStorage.setItem('promptComposerSets', JSON.stringify(savedPromptSets));
            updatePromptSetSelect();
        }
    }

    function updatePromptSetSelect() {
        const select = $('.load-prompt-select');
        select.empty().append('<option value="">Load prompt set...</option>');
        
        Object.entries(savedPromptSets).forEach(([name, set]) => {
            const date = new Date(set.timestamp).toLocaleDateString();
            select.append(new Option(`${name} (${date})`, name));
        });
    }

    $(onReady);
})();