您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhances NovelAI image generation with a prompt composer. Allows saving, categorizing, and quickly toggling common prompt elements. WARNING: Beta version - manual prompt textarea interaction required after generation.
当前为
// ==UserScript== // @name NovelAI Prompt Composer and Tag Manager // @namespace http://tampermonkey.net/ // @version 0.9.0 // @description Enhances NovelAI image generation with a prompt composer. Allows saving, categorizing, and quickly toggling common prompt elements. WARNING: Beta version - manual prompt textarea interaction required after generation. // @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. 5. IMPORTANT: After generating, manually edit the prompt textarea (e.g., add a space and remove it) for the changes to take effect. */ 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; max-height: 80%; display: flex; flex-direction: column; overflow-y: auto; } .prompt-composer-title { text-align: center; margin-bottom: 20px; color: #333; } .button-container { display: flex; justify-content: flex-start; margin-bottom: 20px; } .toggle-button { padding: 5px 10px; border-radius: 5px; border: none; cursor: pointer; background-color: #666; color: #fff; margin-right: 10px; } .toggle-button:hover { background-color: #555; } .toggle-button:active { background-color: #444; } .category-container { margin-bottom: 15px; } .category-label { font-weight: bold; color: #333; } .checkbox-container { display: flex; flex-wrap: wrap; margin-top: 5px; gap: 10px; } .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; } .weight-button { padding: 0 5px; } .delete-button { margin-left: 5px; padding: 2px 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 { max-height: 60vh; /* Limit the height and enable scrolling if needed */ overflow-y: auto; } `; (function() { 'use strict'; // Display a warning message about the beta status console.warn("NovelAI Prompt Composer and Tag Manager (v0.9.0) is running in beta. Known issue: After generating a prompt, you need to manually modify the prompt textarea for it to take effect."); // 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: []}, {name: "Participants", tags: []}, {name: "Backgrounds", tags: []}, {name: "Emotions", tags: []} ], customTags: {} }; function saveToLocalStorage() { localStorage.setItem('promptComposerData', JSON.stringify(promptComposerData)); } let modalDiv; // Declare modalDiv in the global scope let categoriesContainer; function renderCategories() { categoriesContainer.empty(); promptComposerData.categories.forEach((category, index) => { createCategorySection(category, index, categoriesContainer); }); } function openPromptComposer() { console.log("openPromptComposer called"); try { 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>Edit Tags</button>').addClass('toggle-button').click(() => { $('.add-tag-container, .delete-button').toggle(); $('.add-tag-container:visible').css('display', 'flex'); }); const weightToggleButton = $('<button>Toggle Weights</button>').addClass('toggle-button').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>Manage Categories</button>').addClass('toggle-button').click(() => { $('.category-management').toggle(); $('.move-category-button, .delete-category-button').toggle(); }); buttonContainer.append(editButton, weightToggleButton, categoryManagementButton); modalDiv.append(buttonContainer); // 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(); } }); addCategoryContainer.append(addCategoryInput, addCategoryButton); categoryManagement.append(addCategoryContainer); modalDiv.append(categoryManagement); // Create a container for categories categoriesContainer = $('<div></div>').addClass('categories-container'); modalDiv.append(categoryManagement); modalDiv.append(categoriesContainer); renderCategories(); // Create a container for the bottom buttons const bottomButtonsContainer = $('<div></div>').addClass('bottom-buttons-container'); const generateButton = $('<button>Generate Prompt</button>').addClass('generate-button') .click(() => { const combinedPrompt = generatePrompt(); tagArea = document.querySelectorAll("[placeholder='Write your prompt here. Use tags to sculpt your outputs.']")[0]; tagArea.value = combinedPrompt; saveToLocalStorage(); modalDiv.remove(); }); const cancelButton = $('<button>Close</button>').addClass('close-button') .click(() => { saveToLocalStorage(); modalDiv.remove(); }); bottomButtonsContainer.append(generateButton, cancelButton); modalDiv.append(bottomButtonsContainer); $('body').append(modalDiv); // Adjust layout based on screen size adjustLayout(); $(window).on('resize', adjustLayout); console.log("Modal appended to body"); } catch (error) { console.error("Error in openPromptComposer:", error); } } function adjustLayout() { 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(); } }); 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(); } }); 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(); } }); 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: true, weight: 1 }); createCheckbox({ name: newTag, active: true, weight: 1 }, category.name, checkboxContainer); 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) { 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(); }); // 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(decreaseButton, weightInput, 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(); }); checkboxLabel.append(deleteButton); container.append(checkboxLabel); updateWeightDisplay(tag, weightDisplay, weightControl); } function onReady() { setTimeout(placeComposerButton, 3000); } function placeComposerButton() { let composeButton = $('<button>Compose Prompt</button>') .addClass('compose-prompt-button') .click(openPromptComposer); let textAreas = document.querySelectorAll("[placeholder='Write your prompt here. Use tags to sculpt your outputs.']"); if (textAreas.length > 0) { let sidebar = textAreas[0].closest('div').parentElement; $(sidebar).prepend(composeButton); } } function generatePrompt() { return promptComposerData.categories.map(category => { const activeTags = category.tags .filter(tag => tag.active) .map(tag => { let tagText = tag.name; 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(", "); } 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(); } } } } $(onReady); })();