- // ==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);
- })();