您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhances NovelAI image generation with a prompt composer. Allows saving, categorizing, and quickly toggling common prompt elements.
// ==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); })();