NovelAI Prompt Composer and Tag Manager

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

  1. // ==UserScript==
  2. // @name NovelAI Prompt Composer and Tag Manager
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.0
  5. // @description Enhances NovelAI image generation with a prompt composer. Allows saving, categorizing, and quickly toggling common prompt elements.
  6. // @author ManuMonkey
  7. // @license MIT
  8. // @match http*://novelai.net/image
  9. // @require http://code.jquery.com/jquery-latest.js
  10. // ==/UserScript==
  11.  
  12. /*
  13. User Guide:
  14. 1. Click the "Compose Prompt" button to open the Prompt Composer.
  15. 2. Manage your categories and tags.
  16. 3. Use checkboxes to toggle tags on/off.
  17. 4. Click "Generate Prompt" to create your prompt.
  18. */
  19.  
  20. const promptComposerCSS = `
  21. .prompt-composer-modal {
  22. position: fixed;
  23. top: 50%;
  24. left: 50%;
  25. transform: translate(-50%, -50%);
  26. background-color: #f9f9f9;
  27. padding: 20px;
  28. border: 2px solid #333;
  29. border-radius: 10px;
  30. z-index: 9999999999;
  31. width: 600px;
  32. height: 80vh;
  33. display: flex;
  34. flex-direction: column;
  35. color: #333;
  36. }
  37.  
  38. /* Fix header and footer positions */
  39. .prompt-composer-title,
  40. .button-container,
  41. .bottom-buttons-container {
  42. flex-shrink: 0;
  43. }
  44.  
  45. /* Make middle content one scrollable block */
  46. .modal-content {
  47. flex: 1;
  48. overflow-y: auto;
  49. min-height: 0;
  50. }
  51.  
  52. /* Remove individual scroll areas */
  53. .character-list,
  54. .character-panel {
  55. max-height: none;
  56. overflow: visible;
  57. }
  58.  
  59. /* Ensure bottom buttons stay visible */
  60. .bottom-buttons-container {
  61. margin-top: 20px;
  62. padding-top: 20px;
  63. background-color: #f9f9f9;
  64. border-top: 1px solid #eee;
  65. }
  66.  
  67. @media screen and (min-width: 1200px) {
  68. .prompt-composer-modal {
  69. width: 1000px;
  70. }
  71. }
  72.  
  73. .prompt-composer-title {
  74. text-align: center;
  75. margin-bottom: 20px;
  76. color: #333;
  77. }
  78.  
  79. .button-container {
  80. display: flex;
  81. flex-wrap: wrap;
  82. gap: 10px;
  83. margin-bottom: 20px;
  84. justify-content: flex-start;
  85. }
  86.  
  87. .prompt-set-controls {
  88. display: flex;
  89. gap: 10px;
  90. margin-left: auto; /* Push to the right */
  91. flex-wrap: wrap;
  92. }
  93.  
  94. .toggle-button {
  95. position: relative;
  96. width: 32px; /* Make buttons square */
  97. height: 32px;
  98. padding: 5px;
  99. display: flex;
  100. align-items: center;
  101. justify-content: center;
  102. }
  103.  
  104. .toggle-button::before {
  105. content: attr(data-full-text);
  106. position: absolute;
  107. bottom: 100%;
  108. left: 50%;
  109. transform: translateX(-50%);
  110. background-color: #333;
  111. color: white;
  112. padding: 5px 10px;
  113. border-radius: 5px;
  114. font-size: 14px;
  115. white-space: nowrap;
  116. visibility: hidden;
  117. opacity: 0;
  118. transition: opacity 0.2s;
  119. z-index: 10000;
  120. }
  121.  
  122. .toggle-button:hover::before {
  123. visibility: visible;
  124. opacity: 1;
  125. }
  126.  
  127. .toggle-button:active {
  128. background-color: #444;
  129. }
  130.  
  131. .category-container {
  132. margin-bottom: 15px;
  133. }
  134.  
  135. .category-label {
  136. font-weight: bold;
  137. color: #333;
  138. margin-right: auto; /* Push gender selector to the right */
  139. white-space: nowrap; /* Prevent character label from wrapping */
  140. }
  141.  
  142. .checkbox-container {
  143. width: 100%; /* Take full width of section */
  144. display: flex;
  145. flex-wrap: wrap;
  146. gap: 8px;
  147. }
  148.  
  149. .checkbox-label {
  150. color: #333;
  151. display: flex;
  152. align-items: center;
  153. white-space: nowrap;
  154. margin-right: 10px;
  155. }
  156.  
  157. .checkbox-input {
  158. margin-right: 5px;
  159. }
  160.  
  161. .weight-display {
  162. font-size: 0.8em;
  163. color: #666;
  164. }
  165.  
  166. .weight-control {
  167. display: none;
  168. align-items: center;
  169. margin-left: 5px;
  170. }
  171.  
  172. .weight-input {
  173. width: 40px;
  174. margin: 0 5px;
  175. padding:2px 0px 2px 9px;
  176. }
  177.  
  178. .weight-button {
  179. padding: 0 5px;
  180. }
  181.  
  182. .delete-button {
  183. margin-left: 5px;
  184. padding: 0px 5px;
  185. border-radius: 3px;
  186. border: none;
  187. cursor: pointer;
  188. background-color: #ff4d4d;
  189. color: #fff;
  190. display: none;
  191. }
  192.  
  193. .add-tag-container {
  194. display: none;
  195. margin-top: 10px;
  196. align-items: center;
  197. }
  198.  
  199. .add-tag-input {
  200. flex-grow: 1;
  201. padding: 5px;
  202. border-radius: 5px;
  203. border: 1px solid #333;
  204. margin-right: 10px;
  205. }
  206.  
  207. .add-tag-button {
  208. padding: 5px 10px;
  209. border-radius: 5px;
  210. border: none;
  211. cursor: pointer;
  212. background-color: #333;
  213. color: #fff;
  214. white-space: nowrap;
  215. }
  216.  
  217. .tag-textarea {
  218. width: 100%;
  219. margin-top: 10px;
  220. border-radius: 5px;
  221. padding: 5px;
  222. }
  223.  
  224. .generate-button, .close-button {
  225. color: #333;
  226. background-color: rgb(108, 245, 74);
  227. font-size: medium;
  228. padding: 10px;
  229. border: none;
  230. border-radius: 5px;
  231. cursor: pointer;
  232. width: 48%; /* Make buttons take up almost half the width each */
  233. }
  234.  
  235. .generate-button:hover, .close-button:hover {
  236. /* background-color: #87e43b; */
  237. }
  238.  
  239. .close-button {
  240. background-color: #ccc;
  241. }
  242.  
  243. .compose-prompt-button {
  244. color: #333;
  245. background-color: rgb(246, 245, 244);
  246. font-size: small;
  247. padding: 5px 10px;
  248. border: none;
  249. border-radius: 5px;
  250. cursor: pointer;
  251. margin-bottom: 10px;
  252. }
  253.  
  254. .compose-prompt-button:hover {
  255. background-color: #e6e6e6;
  256. }
  257.  
  258. .add-category-container {
  259. display: flex;
  260. margin-bottom: 15px;
  261. align-items: center;
  262. }
  263.  
  264. .add-category-input {
  265. flex-grow: 1;
  266. padding: 5px;
  267. border-radius: 5px;
  268. border: 1px solid #333;
  269. margin-right: 10px;
  270. }
  271.  
  272. .add-category-button {
  273. padding: 5px 10px;
  274. border-radius: 5px;
  275. border: none;
  276. cursor: pointer;
  277. background-color: #333;
  278. color: #fff;
  279. white-space: nowrap;
  280. }
  281.  
  282. .category-header {
  283. display: flex;
  284. justify-content: space-between;
  285. align-items: center;
  286. margin-bottom: 10px;
  287. }
  288.  
  289. .category-management-buttons {
  290. display: flex;
  291. align-items: center;
  292. margin-left: 10px;
  293. }
  294.  
  295. .delete-category-button {
  296. background-color: #ff4d4d;
  297. color: white;
  298. border: none;
  299. border-radius: 5px;
  300. padding: 5px 10px;
  301. font-size: 14px;
  302. cursor: pointer;
  303. margin-left: 10px;
  304. }
  305.  
  306. .delete-category-button:hover {
  307. background-color: #ff3333;
  308. }
  309.  
  310. .delete-category-button:active {
  311. background-color: #e60000;
  312. }
  313.  
  314. .move-category-button {
  315. background-color: #4CAF50;
  316. color: white;
  317. border: none;
  318. border-radius: 50%;
  319. width: 24px;
  320. height: 24px;
  321. font-size: 16px;
  322. line-height: 1;
  323. cursor: pointer;
  324. margin-right: 5px;
  325. }
  326.  
  327. .move-category-button:hover {
  328. background-color: #45a049;
  329. }
  330.  
  331. .move-category-button {
  332. width: auto;
  333. height: auto;
  334. border-radius: 5px;
  335. padding: 5px 8px;
  336. }
  337.  
  338. .category-management {
  339. margin-bottom: 15px;
  340. border-top: 1px solid #ccc;
  341. padding-top: 15px;
  342. }
  343.  
  344. .bottom-buttons-container {
  345. display: flex;
  346. justify-content: space-between;
  347. margin-top: 20px;
  348. }
  349.  
  350. @media screen and (min-width: 1200px) {
  351. .prompt-composer-modal {
  352. width: 1000px; /* Increased width for larger screens */
  353. }
  354.  
  355. .categories-container {
  356. display: flex;
  357. flex-wrap: wrap;
  358. justify-content: space-between;
  359. }
  360.  
  361. .category-container {
  362. width: 48%; /* Slightly less than 50% to account for margins */
  363. }
  364. }
  365.  
  366. .categories-container {
  367. }
  368.  
  369. .character-label {
  370. font-weight: bold;
  371. color: #333;
  372. margin-right: auto; /* This pushes the delete button to the right */
  373. }
  374.  
  375. .character-header {
  376. display: flex;
  377. align-items: center;
  378. padding: 5px 0;
  379. margin-bottom: 10px;
  380. border-bottom: 1px solid #ccc;
  381. }
  382.  
  383. .header-content {
  384. display: flex;
  385. align-items: center;
  386. gap: 15px;
  387. flex: 1;
  388. }
  389.  
  390. .gender-selector {
  391. display: flex;
  392. gap: 12px;
  393. margin-left: auto;
  394. margin-right: 15px;
  395. white-space: nowrap; /* Prevent wrapping */
  396. flex-shrink: 0; /* Prevent the gender selector from shrinking */
  397. }
  398.  
  399. .gender-selector label {
  400. display: flex;
  401. align-items: center;
  402. gap: 4px;
  403. color: #333;
  404. cursor: pointer;
  405. white-space: nowrap; /* Ensure labels don't wrap */
  406. }
  407.  
  408. .tag-section h5 {
  409. color: #333;
  410. margin-top: 8px;
  411. font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
  412. }
  413.  
  414. .character-manager-container h4 {
  415. color: #333;
  416. font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
  417. }
  418.  
  419. .character-list {
  420. display: grid;
  421. grid-template-columns: repeat(2, 1fr);
  422. gap: 20px;
  423. margin-bottom: 20px;
  424. align-items: stretch; /* Make all panels stretch to match the tallest */
  425. }
  426.  
  427. .character-panel {
  428. background-color: #f5f5f5;
  429. border: 1px solid #ddd;
  430. border-radius: 8px;
  431. padding: 15px;
  432. display: flex;
  433. flex-direction: column;
  434. width: 100%; /* Ensure panel takes full width of its grid cell */
  435. }
  436.  
  437. .tag-sections {
  438. flex: 1; /* Allow tag sections to grow */
  439. overflow-y: auto;
  440. width: 100%; /* Take full width of panel */
  441. }
  442.  
  443. .tag-section {
  444. width: 100%; /* Take full width of container */
  445. }
  446.  
  447. .character-manager-container {
  448. margin-top: 5px;
  449. }
  450.  
  451. .add-character-button {
  452. width: 100%;
  453. padding: 10px;
  454. margin-top: 10px;
  455. background-color: #4CAF50;
  456. color: white;
  457. border: none;
  458. border-radius: 5px;
  459. cursor: pointer;
  460. }
  461.  
  462. .add-character-button:hover {
  463. background-color: #45a049;
  464. }
  465.  
  466. .character-panel .infotext-input {
  467. margin-left: 8px;
  468. padding: 2px 4px;
  469. border: 1px solid #ccc;
  470. border-radius: 3px;
  471. font-size: 12px;
  472. width: 150px;
  473. }
  474.  
  475. .character-panel .checkbox-label {
  476. display: flex;
  477. align-items: center;
  478. padding: 2px 0;
  479. }
  480.  
  481. .category-header {
  482. color: #333;
  483. margin-top: 8px;
  484. font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
  485. }
  486.  
  487. .character-actions {
  488. display: flex;
  489. gap: 8px;
  490. margin-left: auto;
  491. }
  492.  
  493. .save-character-button {
  494. background-color: #4CAF50;
  495. color: white;
  496. border: none;
  497. border-radius: 3px;
  498. padding: 2px 8px;
  499. cursor: pointer;
  500. font-size: 12px;
  501. }
  502.  
  503. .load-character-select {
  504. max-width: 120px;
  505. padding: 2px 4px;
  506. border-radius: 3px;
  507. border: 1px solid #ccc;
  508. font-size: 12px;
  509. color: #333;
  510. }
  511.  
  512. .delete-saved-button {
  513. background-color: #ff4d4d;
  514. color: white;
  515. border: none;
  516. border-radius: 3px;
  517. padding: 2px 8px;
  518. cursor: pointer;
  519. font-size: 12px;
  520. }
  521.  
  522. .delete-saved-button:hover {
  523. background-color: #ff3333;
  524. }
  525.  
  526. .load-character-container {
  527. display: flex;
  528. gap: 4px;
  529. align-items: center;
  530. }
  531.  
  532. .load-prompt-container {
  533. display: flex;
  534. align-items: center;
  535. gap: 5px;
  536. }
  537.  
  538. .load-prompt-select {
  539. width: auto; /* Override the width for the select */
  540. min-width: 80px;
  541. }
  542.  
  543. .delete-prompt-button {
  544. padding: 5px 8px;
  545. border-radius: 5px;
  546. border: none;
  547. cursor: pointer;
  548. background-color: #ff4d4d;
  549. color: #fff;
  550. }
  551.  
  552. .delete-prompt-button:hover {
  553. background-color: #ff3333;
  554. }
  555.  
  556. @media screen and (max-width: 800px) {
  557. .button-container {
  558. flex-direction: column;
  559. align-items: stretch;
  560. }
  561. .prompt-set-controls {
  562. margin-left: 0;
  563. margin-top: 10px;
  564. width: 100%;
  565. }
  566.  
  567. .toggle-button {
  568. width: 100%;
  569. height: auto;
  570. }
  571. .toggle-button::before {
  572. display: none; /* Hide tooltips on mobile */
  573. }
  574. }
  575. `;
  576.  
  577. (function () {
  578. 'use strict';
  579.  
  580. // Function to inject CSS
  581. function injectCSS() {
  582. const style = document.createElement('style');
  583. style.textContent = promptComposerCSS;
  584. document.head.appendChild(style);
  585. }
  586.  
  587. // Call the injectCSS function immediately
  588. injectCSS();
  589.  
  590. let tagArea = null;
  591. let promptComposerData = JSON.parse(localStorage.getItem('promptComposerData')) || {
  592. categories: [
  593. { name: "Artists", tags: [], pretext: "artist:" },
  594. { name: "Persons", tags: [] },
  595. { name: "Locations", tags: [] },
  596. { name: "Expressions", tags: [] }
  597. ],
  598. customTags: {},
  599. savedCharacters: {} // Initialize savedCharacters as an empty object
  600. };
  601.  
  602. let savedCharacters = JSON.parse(localStorage.getItem('promptComposerCharacters')) || {};
  603. console.log('Initial load of savedCharacters:', savedCharacters);
  604. let savedPromptSets = JSON.parse(localStorage.getItem('promptComposerSets')) || {};
  605.  
  606. function saveToLocalStorage() {
  607. localStorage.setItem('promptComposerData', JSON.stringify(promptComposerData));
  608. }
  609.  
  610. let modalDiv; // Declare modalDiv in the global scope
  611. let categoriesContainer;
  612.  
  613. let isV4 = false;
  614.  
  615. function detectVersion() {
  616. // Look for "Add Character" button which is unique to V4
  617. const addCharacterBtn = Array.from(document.querySelectorAll('button')).find(btn =>
  618. btn.textContent.trim() === 'Add Character'
  619. );
  620. isV4 = !!addCharacterBtn;
  621. console.log("Detected version:", isV4 ? "V4" : "V3");
  622. }
  623.  
  624. function getPromptFields() {
  625. const allFields = document.querySelectorAll('.ProseMirror p');
  626. // Only use the first half of the fields since the latter half are duplicates
  627. const uniqueFields = Array.from(allFields).slice(0, Math.floor(allFields.length / 2));
  628. return {
  629. generalField: uniqueFields[0], // First field is always general prompt
  630. characterFields: uniqueFields.slice(1).filter(field => field.textContent.trim() !== '') // Only get non-empty character fields
  631. };
  632. }
  633.  
  634. function getCategoryTags() {
  635. const tagCategories = {
  636. 'Artists': [],
  637. 'Activities': [],
  638. 'Participants': [],
  639. 'Body Features': [],
  640. 'Clothing': [],
  641. 'Emotions': [],
  642. 'Backgrounds': []
  643. };
  644.  
  645. // Collect tags from existing categories that match our desired categories
  646. promptComposerData.categories.forEach(category => {
  647. if (tagCategories.hasOwnProperty(category.name)) {
  648. tagCategories[category.name] = category.tags.map(tag => tag.name);
  649. }
  650. });
  651.  
  652. return tagCategories;
  653. }
  654.  
  655. // Add this function at the module level (before addCharacter)
  656. function updateCharacterPrompt(panel, characterNum) {
  657. // Get checked checkbox tags
  658. const checkedTags = panel.find('.checkbox-input:checked').map(function() {
  659. return $(this).attr('data-tag');
  660. }).get();
  661.  
  662. // Get custom traits that don't have checkboxes
  663. const customTraits = panel.find('.custom-traits-input').val()
  664. .split(',')
  665. .map(t => t.trim())
  666. .filter(t => t && !checkedTags.includes(t)); // Filter out traits that have checkboxes
  667.  
  668. const traits = [...checkedTags, ...customTraits];
  669. if (traits.length > 0) {
  670. const charPrompt = traits.join(', ');
  671. // Update the corresponding prompt field
  672. const { characterFields } = getPromptFields();
  673. if (characterFields[characterNum]) {
  674. characterFields[characterNum].innerHTML = charPrompt;
  675. }
  676. }
  677. }
  678.  
  679. function addCharacter(characterList, characterData = null, fieldIndex = null) {
  680. const characterNum = characterList.children().length;
  681. if (characterNum >= 4) return;
  682. const characterPanel = $('<div></div>').addClass('character-panel');
  683. // Character header with label, gender selector, and delete button
  684. const headerDiv = $('<div></div>').addClass('character-header');
  685. const headerContent = $('<div></div>').addClass('header-content');
  686. const characterLabel = $('<span></span>')
  687. .addClass('character-label')
  688. .text(`Character ${characterNum + 1}`);
  689.  
  690. // Create gender selector
  691. const genderSelector = $('<div></div>').addClass('gender-selector');
  692. ['girl', 'boy'].forEach(gender => {
  693. const label = $('<label></label>');
  694. const checkbox = $('<input type="checkbox">')
  695. .addClass('checkbox-input gender-checkbox')
  696. .attr('data-tag', gender)
  697. .prop('checked', characterData?.tags?.includes(gender) || false)
  698. .on('change', function() {
  699. if (this.checked) {
  700. genderSelector.find('.gender-checkbox').not(this).prop('checked', false);
  701. }
  702. updateCharacterPrompt(characterPanel, characterNum);
  703. });
  704. label.append(checkbox, gender);
  705. genderSelector.append(label);
  706. });
  707.  
  708. // Create save/load controls
  709. const characterActions = $('<div></div>').addClass('character-actions');
  710. const saveBtn = $('<button>Save</button>')
  711. .addClass('save-character-button')
  712. .click(() => {
  713. const name = prompt('Enter a name for this character:');
  714. if (name) {
  715. const characterState = {
  716. gender: characterPanel.find('.gender-checkbox:checked').attr('data-tag') || null,
  717. tags: characterPanel.find('.checkbox-input:checked:not(.gender-checkbox)').map(function() {
  718. return $(this).attr('data-tag');
  719. }).get(),
  720. customTraits: characterPanel.find('.custom-traits-input').val()
  721. };
  722. console.log('Saving character:', name, characterState);
  723. savedCharacters[name] = characterState;
  724. localStorage.setItem('promptComposerCharacters', JSON.stringify(savedCharacters));
  725. console.log('Updated savedCharacters:', savedCharacters);
  726. updateLoadSelects();
  727. }
  728. });
  729.  
  730. const loadContainer = $('<div></div>').addClass('load-character-container');
  731. const loadSelect = $('<select></select>')
  732. .addClass('load-character-select')
  733. .append('<option value="">Load char...</option>');
  734.  
  735. // Add this right after creating loadSelect
  736. Object.entries(savedCharacters).forEach(([name, data]) => {
  737. loadSelect.append(new Option(name, name));
  738. });
  739.  
  740. const deleteSavedBtn = $('<button>×</button>')
  741. .addClass('delete-saved-button')
  742. .hide()
  743. .click(() => {
  744. const selectedName = loadSelect.val();
  745. if (selectedName && confirm(`Delete saved character "${selectedName}"?`)) {
  746. delete savedCharacters[selectedName];
  747. localStorage.setItem('promptComposerCharacters', JSON.stringify(savedCharacters));
  748. updateLoadSelects();
  749. }
  750. });
  751.  
  752. loadSelect.on('change', function() {
  753. const selectedName = $(this).val();
  754. deleteSavedBtn.toggle(!!selectedName);
  755. if (selectedName) {
  756. const savedChar = savedCharacters[selectedName];
  757. characterPanel.find('.checkbox-input').prop('checked', false);
  758. if (savedChar.gender) {
  759. characterPanel.find(`.gender-checkbox[data-tag="${savedChar.gender}"]`).prop('checked', true);
  760. }
  761. savedChar.tags.forEach(tag => {
  762. characterPanel.find(`.checkbox-input[data-tag="${tag}"]`).prop('checked', true);
  763. });
  764. characterPanel.find('.custom-traits-input').val(savedChar.customTraits || '');
  765. updateCharacterPrompt(characterPanel, characterNum);
  766. }
  767. });
  768.  
  769. loadContainer.append(loadSelect, deleteSavedBtn);
  770. characterActions.append(saveBtn, loadContainer);
  771. headerContent.append(characterLabel, genderSelector, characterActions);
  772. headerDiv.append(headerContent);
  773.  
  774. const deleteCharacterBtn = $('<button>×</button>')
  775. .addClass('delete-character-button')
  776. .click(() => {
  777. if (confirm('Delete this character?')) {
  778. characterPanel.remove();
  779. $('.character-panel').each((idx, panel) => {
  780. $(panel).find('.character-label').text(`Character ${idx + 1}`);
  781. updateCharacterPrompt($(panel), idx);
  782. });
  783. }
  784. });
  785.  
  786. headerDiv.append(deleteCharacterBtn);
  787.  
  788. // Tag sections from existing categories
  789. const tagCategories = getCategoryTags();
  790. const tagSection = $('<div></div>').addClass('tag-sections');
  791.  
  792. Object.entries(tagCategories).forEach(([category, tags]) => {
  793. if (tags.length > 0) {
  794. const section = $('<div></div>').addClass('tag-section');
  795. const title = $('<h5 class="category-header"></h5>').text(category);
  796. const checkboxContainer = $('<div></div>').addClass('checkbox-container');
  797. tags.forEach(tag => {
  798. const checkboxLabel = $('<label></label>').addClass('checkbox-label');
  799. const checkbox = $('<input type="checkbox">')
  800. .addClass('checkbox-input')
  801. .attr('data-tag', tag)
  802. .prop('checked', characterData?.tags?.includes(tag) || false)
  803. .on('change', () => updateCharacterPrompt(characterPanel, characterNum));
  804.  
  805. // Add infotext input (initially hidden)
  806. const infotextInput = $('<input type="text">')
  807. .addClass('infotext-input')
  808. .attr('placeholder', 'Infotext...')
  809. .val(tag.infotext || '')
  810. .hide();
  811.  
  812. infotextInput.on('input', function() {
  813. const tagObj = promptComposerData.categories
  814. .find(cat => cat.name === category)?.tags
  815. .find(t => t.name === tag);
  816. if (tagObj) {
  817. tagObj.infotext = $(this).val();
  818. checkboxLabel.attr('title', $(this).val());
  819. saveToLocalStorage();
  820. }
  821. });
  822.  
  823. // Set initial tooltip if infotext exists
  824. const tagObj = promptComposerData.categories
  825. .find(cat => cat.name === category)?.tags
  826. .find(t => t.name === tag);
  827. if (tagObj?.infotext) {
  828. checkboxLabel.attr('title', tagObj.infotext);
  829. infotextInput.val(tagObj.infotext);
  830. }
  831. checkboxLabel.append(checkbox, tag, infotextInput);
  832. checkboxContainer.append(checkboxLabel);
  833. });
  834. section.append(title, checkboxContainer);
  835. tagSection.append(section);
  836. }
  837. });
  838.  
  839. // Custom traits input for non-checkbox tags
  840. const customTraitsInput = $('<textarea></textarea>')
  841. .addClass('custom-traits-input')
  842. .attr('placeholder', 'Add custom traits (comma-separated)...')
  843. .on('input', function() {
  844. updateCharacterPrompt(characterPanel, characterNum);
  845. });
  846.  
  847. characterPanel.append(
  848. headerDiv,
  849. tagSection,
  850. $('<div>Custom Traits:</div>').addClass('category-header'),
  851. customTraitsInput
  852. );
  853.  
  854. characterList.append(characterPanel);
  855. updateCharacterPrompt(characterPanel, characterNum);
  856. }
  857.  
  858. function createCharacterManager() {
  859. const container = $('<div></div>').addClass('character-manager-container');
  860. const characterList = $('<div></div>').addClass('character-list');
  861. // Get existing character fields
  862. const { characterFields } = getPromptFields();
  863.  
  864. // Initialize the character loader dropdowns
  865. updateLoadSelects();
  866.  
  867. // Load existing characters first
  868. characterFields.forEach((field, index) => {
  869. const existingTags = field.textContent.split(',').map(t => t.trim()).filter(t => t);
  870. addCharacter(characterList, { tags: existingTags }, index);
  871. });
  872.  
  873. // Add character button (up to 4)
  874. const addCharacterBtn = $('<button>Add Character</button>')
  875. .addClass('add-character-button')
  876. .click(() => {
  877. if (characterList.children().length < 4) {
  878. addCharacter(characterList, null, null);
  879. }
  880. });
  881. container.append(characterList, addCharacterBtn);
  882.  
  883. return container;
  884. }
  885.  
  886. function renderCategories(editMode = false) {
  887. categoriesContainer.empty();
  888. promptComposerData.categories.forEach((category, index) => {
  889. createCategorySection(category, index, categoriesContainer);
  890. });
  891. if (editMode) {
  892. $('.move-category-button, .delete-category-button').toggle();
  893. }
  894. }
  895.  
  896. function openPromptComposer() {
  897. console.log("openPromptComposer called");
  898. try {
  899. detectVersion();
  900. if ($('#promptComposerModal').length) {
  901. $('#promptComposerModal').remove();
  902. return;
  903. }
  904.  
  905. modalDiv = $('<div id="promptComposerModal"></div>').addClass('prompt-composer-modal');
  906. const title = $('<h3>Prompt Composer</h3>').addClass('prompt-composer-title');
  907. modalDiv.append(title);
  908.  
  909. const buttonContainer = $('<div></div>').addClass('button-container');
  910. const editButton = $('<button>E</button>')
  911. .addClass('toggle-button')
  912. .attr('data-full-text', 'Edit Tags')
  913. .click(() => {
  914. $('.add-tag-container, .delete-button').toggle();
  915. $('.add-tag-container:visible').css('display', 'flex');
  916. });
  917. const weightToggleButton = $('<button>W</button>')
  918. .addClass('toggle-button')
  919. .attr('data-full-text', 'Toggle Weights')
  920. .click(() => {
  921. const weightsVisible = !$('.weight-control').first().is(':visible');
  922. $('.weight-control').toggle(weightsVisible);
  923. $('.weight-display').each(function () {
  924. const label = $(this).closest('label');
  925. const weightControl = label.find('.weight-control');
  926. const tag = {
  927. weight: parseFloat(weightControl.find('input[type="number"]').val())
  928. };
  929. updateWeightDisplay(tag, $(this), weightControl);
  930. });
  931. });
  932. const categoryManagementButton = $('<button>C</button>')
  933. .addClass('toggle-button')
  934. .attr('data-full-text', 'Manage Categories')
  935. .click(() => {
  936. $('.category-management').toggle();
  937. $('.move-category-button, .delete-category-button').toggle();
  938. });
  939. const infotextToggleButton = $('<button>I</button>')
  940. .addClass('toggle-button')
  941. .attr('data-full-text', 'Manage Infotexts')
  942. .click(() => {
  943. $('.infotext-input').toggle();
  944. });
  945.  
  946. // Add prompt set management buttons
  947. const savePromptButton = $('<button>S</button>')
  948. .addClass('toggle-button')
  949. .attr('data-full-text', 'Save Prompt Set')
  950. .click(saveCurrentPromptSet);
  951.  
  952. const loadContainer = $('<div></div>').addClass('load-prompt-container');
  953. const deletePromptButton = $('<button>×</button>')
  954. .addClass('delete-prompt-button toggle-button')
  955. .attr('data-full-text', 'Delete Prompt Set')
  956. .hide()
  957. .click(() => {
  958. const selectedName = loadSelect.val();
  959. if (selectedName) {
  960. deletePromptSet(selectedName);
  961. loadSelect.val('');
  962. deletePromptButton.hide();
  963. }
  964. });
  965.  
  966. const loadSelect = $('<select></select>')
  967. .addClass('load-prompt-select toggle-button')
  968. .append('<option value="">Load...</option>')
  969. .on('change', function() {
  970. const selectedName = $(this).val();
  971. deletePromptButton.toggle(!!selectedName);
  972. if (selectedName) {
  973. loadPromptSet(selectedName);
  974. }
  975. });
  976.  
  977. loadContainer.append(loadSelect, deletePromptButton);
  978.  
  979. // Create container for prompt set controls
  980. const promptSetControls = $('<div></div>').addClass('prompt-set-controls');
  981.  
  982. // Add the regular buttons
  983. buttonContainer.append(
  984. editButton,
  985. weightToggleButton,
  986. categoryManagementButton,
  987. infotextToggleButton
  988. );
  989.  
  990. // Add the prompt set controls to their container
  991. promptSetControls.append(
  992. savePromptButton,
  993. loadContainer
  994. );
  995.  
  996. // Add the prompt set controls to the main button container
  997. buttonContainer.append(promptSetControls);
  998.  
  999. modalDiv.append(buttonContainer);
  1000.  
  1001. // Create scrollable content container
  1002. const modalContent = $('<div></div>').addClass('modal-content');
  1003. // Add category management (initially hidden)
  1004. const categoryManagement = $('<div></div>').addClass('category-management').hide();
  1005. const addCategoryContainer = $('<div></div>').addClass('add-category-container');
  1006. const addCategoryInput = $('<input type="text">').addClass('add-category-input')
  1007. .attr('placeholder', 'Add new category...');
  1008. const addCategoryButton = $('<button>Add Category</button>').addClass('add-category-button')
  1009. .click(() => {
  1010. const newCategory = addCategoryInput.val().trim();
  1011. if (newCategory && !promptComposerData.categories.some(cat => cat.name === newCategory)) {
  1012. promptComposerData.categories.push({ name: newCategory, tags: [] });
  1013. saveToLocalStorage();
  1014. addCategoryInput.val('');
  1015. renderCategories(true);
  1016. }
  1017. });
  1018. addCategoryContainer.append(addCategoryInput, addCategoryButton);
  1019. categoryManagement.append(addCategoryContainer);
  1020.  
  1021. // Create a container for categories
  1022. categoriesContainer = $('<div></div>').addClass('categories-container');
  1023.  
  1024. // Add all the middle content to modalContent
  1025. modalContent.append(categoryManagement);
  1026. modalContent.append(categoriesContainer);
  1027.  
  1028. // Render the categories
  1029. renderCategories();
  1030.  
  1031. if (isV4) {
  1032. const characterManager = createCharacterManager();
  1033. modalContent.append(characterManager);
  1034. }
  1035.  
  1036. // Create bottom buttons container
  1037. const bottomButtonsContainer = $('<div></div>').addClass('bottom-buttons-container');
  1038.  
  1039. const generateButton = $('<button>Generate Prompt</button>').addClass('generate-button')
  1040. .click(() => {
  1041. const { generalField, characterFields } = getPromptFields();
  1042. // Update general prompt field first
  1043. const generalPrompt = generatePrompt();
  1044. if (generalField) {
  1045. generalField.innerHTML = generalPrompt;
  1046. }
  1047. saveToLocalStorage();
  1048. modalDiv.remove();
  1049. });
  1050.  
  1051. const cancelButton = $('<button>Close</button>').addClass('close-button')
  1052. .click(() => {
  1053. saveToLocalStorage();
  1054. modalDiv.remove();
  1055. });
  1056.  
  1057. bottomButtonsContainer.append(generateButton, cancelButton);
  1058.  
  1059. // Add the content container to the modal
  1060. modalDiv.append(modalContent);
  1061.  
  1062. // Add bottom buttons last
  1063. modalDiv.append(bottomButtonsContainer);
  1064.  
  1065. $('body').append(modalDiv);
  1066.  
  1067. // Adjust layout based on screen size
  1068. adjustLayout();
  1069.  
  1070. $(window).on('resize', adjustLayout);
  1071.  
  1072. console.log("Modal appended to body");
  1073.  
  1074. // Add this line after creating the modal
  1075. updatePromptSetSelect();
  1076. } catch (error) {
  1077. console.error("Error in openPromptComposer:", error);
  1078. }
  1079. }
  1080.  
  1081. function adjustLayout() {
  1082. if ($(window).width() >= 1600) {
  1083. $('.category-container').css('width', '32%');
  1084. modalDiv.css('width', '1500px');
  1085. }
  1086. else if ($(window).width() >= 1200) {
  1087. $('.category-container').css('width', '48%');
  1088. modalDiv.css('width', '1000px');
  1089. } else {
  1090. $('.category-container').css('width', '100%');
  1091. modalDiv.css('width', '600px');
  1092. }
  1093. }
  1094.  
  1095. function createCategorySection(category, index, modalDiv) {
  1096. const categoryContainer = $('<div></div>').addClass('category-container');
  1097. const categoryHeader = $('<div></div>').addClass('category-header');
  1098. const label = $('<label></label>').text(category.name + ':').addClass('category-label');
  1099.  
  1100. const deleteCategoryButton = $('<button>Delete Category</button>')
  1101. .addClass('delete-category-button')
  1102. .hide() // Initially hide the delete button
  1103. .click(() => {
  1104. if (confirm(`Are you sure you want to delete the category "${category.name}" and all its tags?`)) {
  1105. promptComposerData.categories.splice(index, 1);
  1106. saveToLocalStorage();
  1107. renderCategories(true);
  1108. }
  1109. });
  1110.  
  1111. const moveCategoryUpButton = $('<button>↑</button>').addClass('move-category-button')
  1112. .hide() // Initially hide the move up button
  1113. .click(() => {
  1114. if (index > 0) {
  1115. [promptComposerData.categories[index - 1], promptComposerData.categories[index]] =
  1116. [promptComposerData.categories[index], promptComposerData.categories[index - 1]];
  1117. saveToLocalStorage();
  1118. renderCategories(true);
  1119. }
  1120. });
  1121.  
  1122. const moveCategoryDownButton = $('<button>↓</button>').addClass('move-category-button')
  1123. .hide() // Initially hide the move down button
  1124. .click(() => {
  1125. if (index < promptComposerData.categories.length - 1) {
  1126. [promptComposerData.categories[index], promptComposerData.categories[index + 1]] =
  1127. [promptComposerData.categories[index + 1], promptComposerData.categories[index]];
  1128. saveToLocalStorage();
  1129. renderCategories(true);
  1130. }
  1131. });
  1132.  
  1133. const categoryManagementButtons = $('<div></div>').addClass('category-management-buttons');
  1134. categoryManagementButtons.append(moveCategoryUpButton, moveCategoryDownButton, deleteCategoryButton);
  1135.  
  1136. categoryHeader.append(label, categoryManagementButtons);
  1137. categoryContainer.append(categoryHeader);
  1138.  
  1139. const checkboxContainer = $('<div></div>').addClass('checkbox-container');
  1140. category.tags.forEach(tag => {
  1141. createCheckbox(tag, category.name, checkboxContainer);
  1142. });
  1143. categoryContainer.append(checkboxContainer);
  1144.  
  1145. const addTagContainer = $('<div></div>').addClass('add-tag-container');
  1146. const addTagInput = $('<input type="text">').addClass('add-tag-input')
  1147. .attr('placeholder', 'Add new ' + category.name.toLowerCase() + '...');
  1148. const addTagButton = $('<button>Add</button>').addClass('add-tag-button');
  1149.  
  1150. const addNewTag = () => {
  1151. const newTag = addTagInput.val().trim();
  1152. if (newTag && !category.tags.some(tag => tag.name === newTag)) {
  1153. category.tags.push({ name: newTag, active: false, weight: 1 });
  1154. createCheckbox({ name: newTag, active: false, weight: 1 }, category.name, checkboxContainer, true);
  1155. addTagInput.val('');
  1156. saveToLocalStorage();
  1157. }
  1158. };
  1159.  
  1160. addTagButton.click(addNewTag);
  1161. addTagInput.keypress(function (e) {
  1162. if (e.which == 13) {
  1163. e.preventDefault();
  1164. addNewTag();
  1165. }
  1166. });
  1167.  
  1168. addTagContainer.append(addTagInput, addTagButton);
  1169. categoryContainer.append(addTagContainer);
  1170.  
  1171. const textArea = $('<textarea></textarea>').addClass('tag-textarea')
  1172. .attr('placeholder', 'Enter additional ' + category.name.toLowerCase() + ' here...');
  1173.  
  1174. const customTags = promptComposerData.customTags[category.name] || '';
  1175. textArea.val(customTags);
  1176.  
  1177. textArea.on('input', function () {
  1178. const customTagsValue = $(this).val().trim();
  1179. promptComposerData.customTags[category.name] = customTagsValue;
  1180. saveToLocalStorage();
  1181. });
  1182.  
  1183. categoryContainer.append(textArea);
  1184.  
  1185. categoriesContainer.append(categoryContainer);
  1186. }
  1187.  
  1188. function createCheckbox(tag, categoryName, container, isNew = false) {
  1189. const checkboxLabel = $('<label></label>').addClass('checkbox-label');
  1190. const checkbox = $('<input type="checkbox">').addClass('checkbox-input').val(tag.name);
  1191. checkbox.prop('checked', tag.active);
  1192. checkbox.change(function () {
  1193. tag.active = this.checked;
  1194. saveToLocalStorage();
  1195. });
  1196.  
  1197. // Add infotext input
  1198. const infotextInput = $('<input type="text">').addClass('infotext-input')
  1199. .attr('placeholder', 'Infotext...')
  1200. .val(tag.infotext || '')
  1201. .hide(); // Initially hidden
  1202.  
  1203. infotextInput.on('input', function () {
  1204. tag.infotext = $(this).val();
  1205. saveToLocalStorage();
  1206. });
  1207.  
  1208. // Display infotext on mouseover
  1209. checkboxLabel.attr('title', tag.infotext || '');
  1210.  
  1211. // Create weight display
  1212. const weightDisplay = $('<span></span>').addClass('weight-display');
  1213.  
  1214. // Create weight control
  1215. const weightControl = $('<div></div>').addClass('weight-control');
  1216. const weightInput = $('<input type="number" step="0.05" min="0.5" max="1.5">').addClass('weight-input')
  1217. .val(tag.weight || 1);
  1218. const decreaseButton = $('<button>-</button>').addClass('weight-button');
  1219. const increaseButton = $('<button>+</button>').addClass('weight-button');
  1220.  
  1221. decreaseButton.click(() => updateWeight(-0.05));
  1222. increaseButton.click(() => updateWeight(0.05));
  1223. weightInput.on('input', () => {
  1224. tag.weight = parseFloat(weightInput.val());
  1225. updateWeightDisplay(tag, weightDisplay, weightControl);
  1226. saveToLocalStorage();
  1227. });
  1228.  
  1229. function updateWeight(change) {
  1230. let newWeight = (tag.weight || 1) + change;
  1231. newWeight = Math.round(newWeight * 20) / 20; // Round to nearest 0.05
  1232. newWeight = Math.max(0.5, Math.min(1.5, newWeight)); // Clamp between 0.5 and 1.5
  1233. tag.weight = newWeight;
  1234. weightInput.val(newWeight);
  1235. updateWeightDisplay(tag, weightDisplay, weightControl);
  1236. saveToLocalStorage();
  1237. }
  1238.  
  1239. weightControl.append(weightInput, decreaseButton, increaseButton);
  1240. checkboxLabel.append(checkbox, tag.name, weightDisplay, weightControl);
  1241.  
  1242. const deleteButton = $('<button>×</button>').addClass('delete-button').click(() => {
  1243. const category = promptComposerData.categories.find(cat => cat.name === categoryName);
  1244. category.tags = category.tags.filter(t => t.name !== tag.name);
  1245. saveToLocalStorage();
  1246. checkboxLabel.remove();
  1247. });
  1248. if (isNew) {
  1249. console.log("isNew is true");
  1250. deleteButton.show();
  1251. }
  1252.  
  1253. checkboxLabel.append(deleteButton, infotextInput);
  1254. container.append(checkboxLabel);
  1255.  
  1256. updateWeightDisplay(tag, weightDisplay, weightControl);
  1257. }
  1258.  
  1259. function onReady() {
  1260. setTimeout(placeComposerButton, 5000);
  1261. }
  1262.  
  1263. function placeComposerButton() {
  1264. let composeButton = $('<button>Compose Prompt</button>')
  1265. .addClass('compose-prompt-button')
  1266. .click(openPromptComposer);
  1267.  
  1268. let textAreas = document.querySelectorAll("[class='ProseMirror']");
  1269. if (textAreas.length > 0) {
  1270. let sidebar = textAreas[0].closest('div').parentElement;
  1271. $(sidebar).prepend(composeButton);
  1272. }
  1273. }
  1274.  
  1275. function generatePrompt() {
  1276. if (!isV4) {
  1277. // Existing V3 prompt generation
  1278. return promptComposerData.categories.map(category => {
  1279. const activeTags = category.tags
  1280. .filter(tag => tag.active)
  1281. .map(tag => {
  1282. let tagText = tag.name;
  1283. if (category.name === "Artists" && tagText !== "realistic") {
  1284. tagText = "artist:" + tagText;
  1285. }
  1286. if (tag.weight > 1) {
  1287. const repetitions = Math.round((tag.weight - 1) / 0.05);
  1288. tagText = '{'.repeat(repetitions) + tagText + '}'.repeat(repetitions);
  1289. } else if (tag.weight < 1) {
  1290. const repetitions = Math.round((1 - tag.weight) / 0.05);
  1291. tagText = '['.repeat(repetitions) + tagText + ']'.repeat(repetitions);
  1292. }
  1293. return tagText;
  1294. });
  1295. const customTags = (promptComposerData.customTags[category.name] || '')
  1296. .split(',')
  1297. .map(tag => tag.trim())
  1298. .filter(tag => tag !== '');
  1299. return [...activeTags, ...customTags];
  1300. }).flat().join(", ");
  1301. }
  1302.  
  1303. // For V4, only generate the general prompt
  1304. const generalTags = promptComposerData.categories.map(category => {
  1305. const activeTags = category.tags
  1306. .filter(tag => tag.active)
  1307. .map(tag => formatTag(tag, category.name));
  1308. const customTags = (promptComposerData.customTags[category.name] || '')
  1309. .split(',')
  1310. .map(tag => tag.trim())
  1311. .filter(tag => tag !== '');
  1312.  
  1313. return [...activeTags, ...customTags];
  1314. }).flat();
  1315.  
  1316. return generalTags.join(", ");
  1317. }
  1318.  
  1319. function updateWeightDisplay(tag, displayElement, controlElement) {
  1320. const weight = tag.weight || 1;
  1321. if (weight === 1) {
  1322. displayElement.text('');
  1323. if (controlElement) {
  1324. controlElement.find('input').hide();
  1325. controlElement.find('button').show();
  1326. }
  1327. } else {
  1328. if (controlElement && $('.weight-control').first().is(':visible')) {
  1329. displayElement.text('');
  1330. controlElement.find('input, button').show();
  1331. } else {
  1332. displayElement.text(`:${weight.toFixed(2)}`);
  1333. if (controlElement) {
  1334. controlElement.find('input').hide();
  1335. controlElement.find('button').show();
  1336. }
  1337. }
  1338. }
  1339. }
  1340.  
  1341. function formatTag(tag, categoryName) {
  1342. let tagText = tag.name;
  1343. if (categoryName === "Artists" && tagText !== "realistic") {
  1344. tagText = "artist:" + tagText;
  1345. }
  1346. if (tag.weight > 1) {
  1347. const repetitions = Math.round((tag.weight - 1) / 0.05);
  1348. tagText = '{'.repeat(repetitions) + tagText + '}'.repeat(repetitions);
  1349. } else if (tag.weight < 1) {
  1350. const repetitions = Math.round((1 - tag.weight) / 0.05);
  1351. tagText = '['.repeat(repetitions) + tagText + ']'.repeat(repetitions);
  1352. }
  1353. return tagText;
  1354. }
  1355.  
  1356. // Add the updateLoadSelects function at the module level
  1357. function updateLoadSelects() {
  1358. console.log('updateLoadSelects called, savedCharacters:', savedCharacters);
  1359. $('.load-character-select').each(function() {
  1360. console.log('Found load-character-select element');
  1361. const currentValue = $(this).val();
  1362. $(this).empty().append('<option value="">Load char...</option>');
  1363. Object.entries(savedCharacters).forEach(([name, data]) => {
  1364. console.log('Adding character option:', name, data);
  1365. $(this).append(new Option(name, name));
  1366. });
  1367. $(this).val(currentValue);
  1368. });
  1369. }
  1370.  
  1371. function saveCurrentPromptSet() {
  1372. const { generalField, characterFields } = getPromptFields();
  1373. // Get current state of characters
  1374. const characters = $('.character-panel').map(function() {
  1375. const panel = $(this);
  1376. return {
  1377. gender: panel.find('.gender-checkbox:checked').attr('data-tag') || null,
  1378. tags: panel.find('.checkbox-input:checked:not(.gender-checkbox)').map(function() {
  1379. return $(this).attr('data-tag');
  1380. }).get(),
  1381. customTraits: panel.find('.custom-traits-input').val()
  1382. };
  1383. }).get();
  1384.  
  1385. // Create prompt set object
  1386. const promptSet = {
  1387. generalPrompt: generalField?.innerHTML || '',
  1388. characters: characters,
  1389. timestamp: new Date().toISOString()
  1390. };
  1391.  
  1392. // Show save dialog
  1393. const name = prompt('Enter a name for this prompt set:');
  1394. if (name) {
  1395. savedPromptSets[name] = promptSet;
  1396. localStorage.setItem('promptComposerSets', JSON.stringify(savedPromptSets));
  1397. updatePromptSetSelect();
  1398. }
  1399. }
  1400.  
  1401. function loadPromptSet(name) {
  1402. const promptSet = savedPromptSets[name];
  1403. if (!promptSet) return;
  1404.  
  1405. // Update general prompt checkboxes and UI
  1406. promptComposerData.categories.forEach(category => {
  1407. category.tags.forEach(tag => {
  1408. // Reset all tags to inactive first
  1409. tag.active = false;
  1410. });
  1411. });
  1412.  
  1413. // Parse the general prompt and activate corresponding tags
  1414. const generalPromptTags = promptSet.generalPrompt.split(',').map(t => t.trim());
  1415. generalPromptTags.forEach(tagText => {
  1416. // Remove any weight modifiers ({} or [])
  1417. const cleanTag = tagText.replace(/[\{\}\[\]]/g, '').trim();
  1418. // Remove artist: prefix if present
  1419. const tagName = cleanTag.replace(/^artist:/, '');
  1420.  
  1421. // Find and activate the tag
  1422. promptComposerData.categories.forEach(category => {
  1423. const foundTag = category.tags.find(t => t.name === tagName);
  1424. if (foundTag) {
  1425. foundTag.active = true;
  1426. }
  1427. });
  1428.  
  1429. // Handle custom tags
  1430. promptComposerData.categories.forEach(category => {
  1431. const customTagsArea = $(`.category-container:contains("${category.name}") .tag-textarea`);
  1432. const customTags = promptSet.customTags?.[category.name] || '';
  1433. customTagsArea.val(customTags);
  1434. promptComposerData.customTags[category.name] = customTags;
  1435. });
  1436. });
  1437.  
  1438. // Update checkboxes in the UI
  1439. $('.checkbox-input').each(function() {
  1440. const tagName = $(this).val();
  1441. const isActive = promptComposerData.categories.some(category =>
  1442. category.tags.some(tag => tag.name === tagName && tag.active)
  1443. );
  1444. $(this).prop('checked', isActive);
  1445. });
  1446.  
  1447. // Update the actual text field
  1448. const { generalField } = getPromptFields();
  1449. if (generalField) {
  1450. generalField.innerHTML = promptSet.generalPrompt;
  1451. }
  1452.  
  1453. // Handle characters in V4 mode
  1454. if (isV4 && promptSet.characters) {
  1455. const characterList = $('.character-list');
  1456. if (characterList.length) {
  1457. characterList.empty();
  1458. promptSet.characters.forEach((charData, index) => {
  1459. addCharacter(characterList, charData, index);
  1460. });
  1461. }
  1462. }
  1463.  
  1464. saveToLocalStorage();
  1465. }
  1466.  
  1467. function deletePromptSet(name) {
  1468. if (confirm(`Delete saved prompt set "${name}"?`)) {
  1469. delete savedPromptSets[name];
  1470. localStorage.setItem('promptComposerSets', JSON.stringify(savedPromptSets));
  1471. updatePromptSetSelect();
  1472. }
  1473. }
  1474.  
  1475. function updatePromptSetSelect() {
  1476. const select = $('.load-prompt-select');
  1477. select.empty().append('<option value="">Load prompt set...</option>');
  1478. Object.entries(savedPromptSets).forEach(([name, set]) => {
  1479. const date = new Date(set.timestamp).toLocaleDateString();
  1480. select.append(new Option(`${name} (${date})`, name));
  1481. });
  1482. }
  1483.  
  1484. $(onReady);
  1485. })();