Greasy Fork 还支持 简体中文。

ChatGPT Tools

Various Tools for ChatGPT

  1. // ==UserScript==
  2. // @name ChatGPT Tools
  3. // @namespace ChatGPT Tools by Vishanka
  4. // @version 3.9
  5. // @description Various Tools for ChatGPT
  6. // @author Vishanka
  7. // @license Proprietary
  8. // @match https://chatgpt.com/*
  9. // @grant GM_addStyle
  10. // @supportURL https://greasyfork.org/scripts/521345-chatgpt-tools
  11. // ==/UserScript==
  12.  
  13.  
  14.  
  15. (function () {
  16. 'use strict';
  17. // =============================================================== MAIN INJECT ===============================================================
  18.  
  19. function isMobileDevice() {
  20. return /Mobi|Android/i.test(navigator.userAgent);
  21. }
  22.  
  23. if (!isMobileDevice()) {
  24. function disableEnterKey(event) {
  25. // Check if the Enter key is pressed
  26. if (event.key === "Enter") {
  27. // Allow Shift + Enter for a new paragraph
  28. if (event.shiftKey) {
  29. console.log("New paragraph created.");
  30. return; // Exit the function, allowing the default behavior
  31. }
  32. // Prevent default Enter key behavior
  33. // This stores the input text for the Lorebook, as it's executed before the other functions it only holds the input text without the injects. It could be reasonable to outsource this to the lorebook logic script, but really have to mind the timing.
  34. storeInputText();
  35. event.preventDefault();
  36. console.log("Enter key functionality disabled.");
  37. const inputPanelGPT = document.querySelector('div#prompt-textarea.ProseMirror');
  38.  
  39. // Current Time
  40. const getTimeState = localStorage.getItem('enableTime');
  41. if (getTimeState === "true") {
  42. storeCurrentTime();
  43. const currentTime = localStorage.getItem('currentTime');
  44. const newParagraph = document.createElement('p');
  45. newParagraph.textContent = currentTime;
  46. inputPanelGPT.appendChild(newParagraph);
  47. console.log('Current Time appended:', currentTime);
  48. }
  49.  
  50. // RULES
  51. const getRulesState = localStorage.getItem('enableRules');
  52. if (getRulesState === "true") {
  53. const getSelectedRuleValue = localStorage.getItem('selectedRuleValue');
  54. const newParagraph = document.createElement('p');
  55. newParagraph.textContent = getSelectedRuleValue;
  56. inputPanelGPT.appendChild(newParagraph);
  57. console.log('Next Rule appended:', getSelectedRuleValue);
  58. }
  59.  
  60.  
  61. // LOREBOOK
  62. const getLorebookState = localStorage.getItem('enableLorebook');
  63. if (getLorebookState === "true") {
  64. lorebookLogic();
  65. const getSelectedLorebookEntries = localStorage.getItem('selectedLorebookValues');
  66.  
  67. // Check if getSelectedLorebookEntries is not null, empty, or '[]'
  68. if (getSelectedLorebookEntries && getSelectedLorebookEntries !== "[]") {
  69. // Parse the JSON string back into an array
  70. const lorebookEntries = JSON.parse(getSelectedLorebookEntries);
  71.  
  72. // Iterate over each entry and append it as a separate paragraph
  73. lorebookEntries.forEach(entry => {
  74. const newParagraph = document.createElement('p');
  75. newParagraph.textContent = entry;
  76. inputPanelGPT.appendChild(newParagraph);
  77. });
  78.  
  79. console.log('Lorebook Entries appended:', lorebookEntries);
  80. }
  81. }
  82.  
  83.  
  84. // Update the selection range to the end of the container
  85. const selection = window.getSelection();
  86. selection.removeAllRanges();
  87. const newRange = document.createRange();
  88. newRange.selectNodeContents(inputPanelGPT);
  89. newRange.collapse(false);
  90. selection.addRange(newRange);
  91.  
  92. const inputEvent = new Event('input', { bubbles: true, cancelable: true });
  93. inputPanelGPT.dispatchEvent(inputEvent);
  94.  
  95. setTimeout(() => {
  96. const sendButton = document.querySelector('button[data-testid="send-button"]');
  97. if (sendButton) {
  98. sendButton.click();
  99. console.log('Send button clicked after delay.');
  100. } else {
  101. console.error('Send button not found.');
  102. }
  103. }, 100); // 10ms delay (adjust as needed)
  104.  
  105. }
  106. }
  107.  
  108. // Attach the event listener to the document
  109. document.addEventListener("keydown", disableEnterKey, true);
  110. }
  111.  
  112. if (isMobileDevice()) {
  113. let programmaticClick = false; // Flag to differentiate between user and programmatic clicks
  114.  
  115. document.body.addEventListener('click', function (event) {
  116. const sendButton1 = document.querySelector('button[aria-label="Send prompt"]');
  117.  
  118. // Check if the button exists and if the clicked target is the send button
  119. if (sendButton1 && sendButton1.contains(event.target)) {
  120. console.error('AAAAAAA');
  121.  
  122. // If this is a programmatic click, bypass the custom logic
  123. if (programmaticClick) {
  124. programmaticClick = false; // Reset the flag for subsequent clicks
  125. console.log("Programmatic click detected, skipping custom logic.");
  126. return;
  127. }
  128. // Prevent default Enter key behavior
  129. // This stores the input text for the Lorebook, as it's executed before the other functions it only holds the input text without the injects. It could be reasonable to outsource this to the lorebook logic script, but really have to mind the timing.
  130. storeInputText();
  131. event.preventDefault();
  132. event.stopImmediatePropagation();
  133. console.log("Send Button functionality temporarily disabled to execute custom logic.");
  134.  
  135. const inputPanelGPT = document.querySelector('div#prompt-textarea.ProseMirror');
  136.  
  137. // Current Time
  138. const getTimeState = localStorage.getItem('enableTime');
  139. if (getTimeState === "true") {
  140. storeCurrentTime();
  141. const currentTime = localStorage.getItem('currentTime');
  142. const newParagraph = document.createElement('p');
  143. newParagraph.textContent = currentTime;
  144. inputPanelGPT.appendChild(newParagraph);
  145. console.log('Current Time appended:', currentTime);
  146. }
  147.  
  148. // RULES
  149. const getRulesState = localStorage.getItem('enableRules');
  150. if (getRulesState === "true") {
  151. const getSelectedRuleValue = localStorage.getItem('selectedRuleValue');
  152. // Check if the rule is already appended
  153. const existingRule = Array.from(inputPanelGPT.children).find(
  154. child => child.textContent === getSelectedRuleValue
  155. );
  156. if (!existingRule) {
  157. const newParagraph = document.createElement('p');
  158. newParagraph.textContent = getSelectedRuleValue;
  159. inputPanelGPT.appendChild(newParagraph);
  160. console.log('Next Rule appended:', getSelectedRuleValue);
  161. } else {
  162. console.log('Rule already exists, not appending again.');
  163. }
  164. }
  165.  
  166. // LOREBOOK
  167. const getLorebookState = localStorage.getItem('enableLorebook');
  168. if (getLorebookState === "true") {
  169. lorebookLogic();
  170. const getSelectedLorebookEntries = localStorage.getItem('selectedLorebookValues');
  171.  
  172. // Check if getSelectedLorebookEntries is not null, empty, or '[]'
  173. if (getSelectedLorebookEntries && getSelectedLorebookEntries !== "[]") {
  174. // Parse the JSON string back into an array
  175. const lorebookEntries = JSON.parse(getSelectedLorebookEntries);
  176.  
  177. // Iterate over each entry and append it as a separate paragraph
  178. lorebookEntries.forEach(entry => {
  179. const newParagraph = document.createElement('p');
  180. newParagraph.textContent = entry;
  181. inputPanelGPT.appendChild(newParagraph);
  182. });
  183.  
  184. console.log('Lorebook Entries appended:', lorebookEntries);
  185. }
  186. }
  187.  
  188.  
  189.  
  190.  
  191. // Update the selection range to the end of the container
  192. const selection = window.getSelection();
  193. selection.removeAllRanges();
  194. const newRange = document.createRange();
  195. newRange.selectNodeContents(inputPanelGPT);
  196. newRange.collapse(false);
  197. selection.addRange(newRange);
  198.  
  199. const inputEvent = new Event('input', { bubbles: true, cancelable: true });
  200. inputPanelGPT.dispatchEvent(inputEvent);
  201.  
  202.  
  203.  
  204.  
  205. // Restore the send functionality after custom logic
  206. setTimeout(() => {
  207. console.log("Restoring Send Button functionality.");
  208. programmaticClick = true; // Set the flag to indicate programmatic click
  209. sendButton1.click(); // Programmatically trigger the send action
  210. }, 100); // Use a 0ms delay to allow custom logic to finish
  211. }
  212. });
  213.  
  214. }
  215.  
  216.  
  217.  
  218. // ================================================================ GLOBAL STYLES =================================================================
  219.  
  220. const CONFIG = {
  221. panelBackground: '#171717',
  222. // PanelButtons
  223. BUTTON_HIGHLIGHT: '#424242',
  224. BUTTON_OUTLINE: '1px solid #4E4E4E',
  225. };
  226.  
  227.  
  228.  
  229. // Black Background Overlay
  230. const overlay = document.createElement('div');
  231. overlay.style.position = 'fixed';
  232. overlay.style.top = '0';
  233. overlay.style.left = '0';
  234. overlay.style.width = '100%';
  235. overlay.style.height = '100%';
  236. overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
  237. overlay.style.zIndex = '999';
  238. overlay.style.display = 'none';
  239.  
  240. function showOverlay() {
  241. document.body.appendChild(overlay);
  242. overlay.style.display = 'block';
  243. }
  244. function hideOverlay() {
  245. overlay.style.display = 'none';
  246. }
  247.  
  248.  
  249. // ================================================================= MAIN PANEL ==================================================================
  250. function mainPanelScript() {
  251. // Create and add the arrow button to open the storage panel
  252. const arrowButton = document.createElement('div');
  253. arrowButton.innerHTML = '〈';
  254. arrowButton.style.position = 'fixed';
  255. arrowButton.style.bottom = '50%';
  256. arrowButton.style.right = '0';
  257. arrowButton.style.padding = '10px';
  258. arrowButton.style.fontSize = '24px';
  259. arrowButton.style.zIndex = '998';
  260. arrowButton.style.cursor = 'pointer';
  261. arrowButton.style.color = '#B4B4B4';
  262. arrowButton.style.borderRadius = '5px 0 0 5px';
  263. arrowButton.style.transition = 'transform 0.3s ease, right 0.3s ease';
  264.  
  265. // Making Panel available for the whole script
  266. window.mainPanel = document.createElement('div');
  267. mainPanel.style.position = 'fixed';
  268. mainPanel.style.top = '0';
  269. mainPanel.style.right = '-250px'; // Initially hidden
  270. mainPanel.style.height = '100%';
  271. mainPanel.style.width = '250px';
  272. mainPanel.style.backgroundColor = CONFIG.panelBackground;
  273. mainPanel.style.transition = 'right 0.3s ease';
  274. mainPanel.style.zIndex = '999';
  275.  
  276. // Create the header above the button
  277. const mainPanelHeader = document.createElement('div');
  278. mainPanelHeader.innerText = 'ChatGPT Tools';
  279. mainPanelHeader.style.margin = '20px';
  280. mainPanelHeader.style.padding = '10px';
  281. mainPanelHeader.style.fontSize = '19px';
  282. mainPanelHeader.style.fontWeight = '550';
  283. mainPanelHeader.style.color = '#ECECEC';
  284. mainPanelHeader.style.textAlign = 'center';
  285.  
  286. // Create the background overlay
  287. window.mainPanelOverlay = document.createElement('div');
  288. mainPanelOverlay.style.position = 'fixed';
  289. mainPanelOverlay.style.top = '0';
  290. mainPanelOverlay.style.left = '0';
  291. mainPanelOverlay.style.width = '100%';
  292. mainPanelOverlay.style.height = '100%';
  293. mainPanelOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.0)';
  294. mainPanelOverlay.style.zIndex = '997';
  295. mainPanelOverlay.style.display = 'none'; // Initially hidden
  296.  
  297. // Toggle panel visibility
  298. arrowButton.addEventListener('click', () => {
  299. if (mainPanel.style.right === '-250px') {
  300. mainPanel.style.right = '0';
  301. arrowButton.style.right = '250px';
  302. arrowButton.style.transform = 'rotate(180deg)';
  303. mainPanelOverlay.style.display = 'block'; // Show overlay
  304. } else {
  305. closePanel();
  306. }
  307. });
  308.  
  309. // Close panel and remove overlay
  310. mainPanelOverlay.addEventListener('click', () => {
  311. closePanel();
  312. });
  313.  
  314. function closePanel() {
  315. mainPanel.style.right = '-250px';
  316. arrowButton.style.right = '0';
  317. arrowButton.style.transform = 'rotate(0deg)';
  318. mainPanelOverlay.style.display = 'none'; // Hide overlay
  319. }
  320.  
  321. document.body.appendChild(arrowButton);
  322. document.body.appendChild(mainPanelOverlay);
  323. document.body.appendChild(mainPanel);
  324. mainPanel.appendChild(mainPanelHeader);
  325. }
  326.  
  327.  
  328.  
  329. // ================================================================== LOREBOOK ====================================================================
  330.  
  331. function lorebookScript() {
  332.  
  333. // Create a function to detect mobile
  334. function isMobile() {
  335. return window.innerWidth <= 768;
  336. }
  337.  
  338. // Manage Lorebook Button
  339. window.openLorebookButton = document.createElement('div');
  340. window.openLorebookButton.innerHTML = `
  341. <button id="toggle-lorebook-panel"
  342. style="
  343. display: flex;
  344. align-items: center;
  345. gap: 8px;
  346. position: relative;
  347. top: 10px;
  348. right: 0px;
  349. left: 10px;
  350. padding: 7px 15px;
  351. background: transparent;
  352. color: #b0b0b0;
  353. border: none;
  354. border-radius: 8px;
  355. font-size: 16px;
  356. text-align: left;
  357. cursor: pointer;
  358. width: 90%;
  359. transition: background-color 0.1s, color 0.1s;
  360. z-index: 1001;">
  361. <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" class="mb-[-1px]">
  362. <path d="M6 3C4.89543 3 4 3.89543 4 5V13C4 14.1046 4.89543 15 6 15L6 3Z" fill="currentColor"></path>
  363. <path d="M7 3V15H8.18037L8.4899 13.4523C8.54798 13.1619 8.69071 12.8952 8.90012 12.6858L12.2931 9.29289C12.7644 8.82153 13.3822 8.58583 14 8.58578V3.5C14 3.22386 13.7761 3 13.5 3H7Z" fill="currentColor"></path>
  364. <path d="M11.3512 15.5297L9.73505 15.8529C9.38519 15.9229 9.07673 15.6144 9.14671 15.2646L9.46993 13.6484C9.48929 13.5517 9.53687 13.4628 9.60667 13.393L12.9996 10C13.5519 9.44771 14.4473 9.44771 14.9996 10C15.5519 10.5523 15.5519 11.4477 14.9996 12L11.6067 15.393C11.5369 15.4628 11.448 15.5103 11.3512 15.5297Z" fill="currentColor"></path>
  365. </svg>
  366. Manage Lorebook
  367. </button>
  368. `;
  369.  
  370. const lorebookButtonElement = openLorebookButton.querySelector('button');
  371. lorebookButtonElement.onmouseover = () => {
  372. lorebookButtonElement.style.backgroundColor = '#212121';
  373. lorebookButtonElement.style.color = '#ffffff';
  374. };
  375. lorebookButtonElement.onmouseout = () => {
  376. lorebookButtonElement.style.backgroundColor = 'transparent';
  377. lorebookButtonElement.style.color = '#b0b0b0';
  378. };
  379.  
  380. // Create the main panel container
  381. const lorebookPanel = document.createElement('div');
  382. lorebookPanel.id = 'lorebookManagerPanel';
  383. lorebookPanel.style.position = 'fixed';
  384. lorebookPanel.style.top = '50%';
  385. lorebookPanel.style.left = '50%';
  386. lorebookPanel.style.transform = 'translate(-50%, -50%)';
  387. lorebookPanel.style.zIndex = '1000';
  388. lorebookPanel.style.display = 'none';
  389. lorebookPanel.style.backgroundColor = '#2F2F2F';
  390. lorebookPanel.style.borderRadius = '20px';
  391. lorebookPanel.style.padding = '10px';
  392.  
  393. document.body.appendChild(lorebookPanel);
  394.  
  395. // Add close button for the panel
  396. const lorebookCloseButton = document.createElement('button');
  397. lorebookCloseButton.style.position = 'absolute';
  398. lorebookCloseButton.style.borderRadius = '50%';
  399. lorebookCloseButton.style.top = '20px';
  400. lorebookCloseButton.style.right = '20px';
  401. lorebookCloseButton.style.backgroundColor = 'transparent';
  402. lorebookCloseButton.style.cursor = 'pointer';
  403. lorebookCloseButton.style.width = '32px';
  404. lorebookCloseButton.style.height = '32px';
  405. lorebookCloseButton.style.display = 'flex';
  406. lorebookCloseButton.style.alignItems = 'center';
  407. lorebookCloseButton.style.justifyContent = 'center';
  408. lorebookCloseButton.style.zIndex = '1001';
  409.  
  410. const lorebookCloseSymbol = document.createElement('span');
  411. lorebookCloseSymbol.innerText = '✕';
  412. lorebookCloseSymbol.style.color = '#FBFBFE';
  413. lorebookCloseSymbol.style.fontSize = '14px';
  414. lorebookCloseSymbol.style.fontWeight = '550';
  415. lorebookCloseSymbol.style.transform = 'translateY(-1px) translateX(0.4px)';
  416.  
  417. lorebookCloseButton.appendChild(lorebookCloseSymbol);
  418.  
  419. lorebookCloseButton.addEventListener('mouseenter', () => {
  420. lorebookCloseButton.style.backgroundColor = '#676767';
  421. });
  422. lorebookCloseButton.addEventListener('mouseleave', () => {
  423. lorebookCloseButton.style.backgroundColor = 'transparent';
  424. });
  425. const closeLorebookPanel = () => {
  426. lorebookPanel.style.display = 'none';
  427. hideOverlay();
  428. };
  429. lorebookCloseButton.addEventListener('click', closeLorebookPanel);
  430. lorebookPanel.appendChild(lorebookCloseButton);
  431.  
  432. // (Mobile) Show/Close Profiles button
  433. const showProfilesButton = document.createElement('button');
  434. showProfilesButton.innerText = 'Show Profiles';
  435. showProfilesButton.style.padding = '8px';
  436. showProfilesButton.style.border = '0.2px solid #4E4E4E';
  437. showProfilesButton.style.backgroundColor = 'transparent';
  438. showProfilesButton.style.color = '#fff';
  439. showProfilesButton.style.borderRadius = '20px';
  440. showProfilesButton.style.width = '90%';
  441. showProfilesButton.style.cursor = 'pointer';
  442. showProfilesButton.style.margin = '10px auto';
  443. showProfilesButton.style.display = 'none'; // Hidden on desktop
  444.  
  445. showProfilesButton.onmouseover = () => {
  446. showProfilesButton.style.backgroundColor = '#424242';
  447. };
  448. showProfilesButton.onmouseout = () => {
  449. showProfilesButton.style.backgroundColor = 'transparent';
  450. };
  451.  
  452. // (Mobile) Show/Close Profiles button
  453. showProfilesButton.addEventListener('click', () => {
  454. const currentlyHidden = (lorebookProfilePanel.style.display === 'none');
  455.  
  456. // Toggle the panel
  457. lorebookProfilePanel.style.display = currentlyHidden ? 'block' : 'none';
  458.  
  459. // On mobile, replace the big heading's text
  460. if (isMobile()) {
  461. lorebookEntriesTitle.innerText = currentlyHidden
  462. ? 'Profiles' // when the list is visible
  463. : 'Manage Lorebook';
  464. // Hide the smaller label altogether on mobile
  465. profileslabel.style.display = 'none';
  466. }
  467.  
  468. // Toggle button text
  469. showProfilesButton.innerText = currentlyHidden ? 'Close Profiles' : 'Show Profiles';
  470.  
  471. // Toggle the close button for mobile
  472. if (isMobile()) {
  473. lorebookCloseButton.style.display =
  474. lorebookProfilePanel.style.display === 'block'
  475. ? 'none'
  476. : 'flex';
  477. }
  478. });
  479.  
  480. // Open the Lorebook Panel
  481. openLorebookButton.addEventListener('click', () => {
  482. lorebookPanel.style.display = lorebookPanel.style.display === 'none' ? 'block' : 'none';
  483. showOverlay();
  484. loadProfileEntries();
  485. updateLayout();
  486. });
  487.  
  488. // Profiles Title
  489. // -- PROFILES LABEL (the smaller text) --
  490. const profileslabel = document.createElement('h3');
  491. profileslabel.innerText = 'Profiles';
  492. profileslabel.style.color = '#ffffff';
  493. profileslabel.style.textAlign = 'center';
  494. profileslabel.style.marginBottom = '5px';
  495. // profilesOverlay.appendChild(profilesTitleOverlay);
  496.  
  497. // Profile Management Panel
  498. const lorebookProfilePanel = document.createElement('div');
  499. lorebookProfilePanel.id = 'lorebookProfilePanel';
  500. lorebookProfilePanel.style.float = 'left';
  501. lorebookProfilePanel.style.width = '20%';
  502. lorebookProfilePanel.style.borderRight = '0.5px solid #444444';
  503. lorebookProfilePanel.style.height = '95%';
  504.  
  505. // Create the profile list container
  506. const profileList = document.createElement('div');
  507. profileList.id = 'profileList';
  508. profileList.style.height = '95%';
  509. profileList.style.color = 'white';
  510. profileList.style.overflowY = 'auto';
  511.  
  512. // Add Profiles Button
  513. const addProfileButton = document.createElement('button');
  514. addProfileButton.textContent = 'Add Profile';
  515. addProfileButton.style.margin = '-59px auto';
  516. // addProfileButton.style.align = 'center';
  517. addProfileButton.style.marginLeft = '7px';
  518. addProfileButton.style.display = 'block';
  519. addProfileButton.style.padding = '10px';
  520. addProfileButton.style.border = CONFIG.BUTTON_OUTLINE;
  521. addProfileButton.style.width = '86%';
  522. addProfileButton.style.backgroundColor = 'transparent';
  523. addProfileButton.style.borderRadius = '60px';
  524. addProfileButton.style.cursor = 'pointer';
  525.  
  526. addProfileButton.addEventListener('mouseenter', () => {
  527. addProfileButton.style.backgroundColor = CONFIG.BUTTON_HIGHLIGHT;
  528. });
  529.  
  530. addProfileButton.addEventListener('mouseleave', () => {
  531. addProfileButton.style.backgroundColor = 'transparent';
  532. });
  533.  
  534.  
  535.  
  536. addProfileButton.addEventListener('click', () => {
  537. const profileName = prompt('Enter profile name:');
  538. if (profileName) {
  539. let lorebookEntries = getAllLorebookEntries();
  540. if (!lorebookEntries[profileName]) {
  541. lorebookEntries[profileName] = [];
  542. setAllLorebookEntries(lorebookEntries);
  543. loadProfiles();
  544. } else {
  545. alert('Profile already exists.');
  546. }
  547. }
  548. });
  549.  
  550. /************************************************************
  551. * Profile Selection Functionality
  552. ************************************************************/
  553. function loadProfiles() {
  554. profileList.innerHTML = '';
  555. let lorebookEntries = getAllLorebookEntries();
  556.  
  557. Object.keys(lorebookEntries).forEach(profile => {
  558. // Entire item is clickable, text left-aligned
  559. const profileItem = document.createElement('div');
  560. profileItem.innerText = profile;
  561. profileItem.style.marginBottom = '5px';
  562. profileItem.style.cursor = 'pointer';
  563. profileItem.style.borderRadius = '5px';
  564. profileItem.style.width = '95%';
  565. profileItem.style.display = 'flex'; // ← make it a flex container
  566. profileItem.style.alignItems = 'center'; // center vertically
  567. profileItem.style.justifyContent = 'space-between'; // place removeButton on far right
  568. profileItem.style.textAlign = 'left'; // text at left
  569. // Highlight if selected
  570. profileItem.style.backgroundColor =
  571. profile === getCurrentProfile() ? '#424242' : '#2F2F2F';
  572. profileItem.style.padding = '5px 10px';
  573.  
  574. // Click entire item to set current profile
  575. profileItem.addEventListener('click', () => {
  576. setCurrentProfile(profile);
  577. loadProfiles();
  578. loadProfileEntries();
  579. });
  580.  
  581.  
  582.  
  583.  
  584. // Create the remove button
  585. const removeButton = document.createElement('button');
  586. removeButton.style.cssText = `
  587. color: #B4B4B4;
  588. background-color: transparent;
  589. cursor: pointer;
  590. border: none;
  591. font-size: 14px;
  592. padding: 0;
  593. outline: none; /* Optional: remove focus outline */
  594. display: flex; /* Optional: center-align the SVG */
  595. align-items: center; /* Optional: center-align the SVG */
  596. `;
  597.  
  598. // Add the SVG icon for the button
  599. removeButton.innerHTML = `
  600. <svg width="24" height="24" viewBox="0 0 24 24" fill="none"
  601. xmlns="http://www.w3.org/2000/svg" class="icon-sm">
  602. <path fill-rule="evenodd" clip-rule="evenodd"
  603. d="
  604. M10.5555 4 C10.099 4 9.70052 4.30906 9.58693 4.75114
  605. L9.29382 5.8919 H14.715 L14.4219 4.75114
  606. C14.3083 4.30906 13.9098 4 13.4533 4H10.5555
  607. M16.7799 5.8919 L16.3589 4.25342
  608. C16.0182 2.92719 14.8226 2 13.4533 2H10.5555
  609. C9.18616 2 7.99062 2.92719 7.64985 4.25342
  610. L7.22886 5.8919 H4
  611. C3.44772 5.8919 3 6.33961 3 6.8919
  612. C3 7.44418 3.44772 7.8919 4 7.8919H4.10069
  613. L5.31544 19.3172 C5.47763 20.8427 6.76455 22 8.29863 22H15.7014
  614. C17.2354 22 18.5224 20.8427 18.6846 19.3172 L19.8993 7.8919 H20
  615. C20.5523 7.8919 21 7.44418 21 6.8919
  616. C21 6.33961 20.5523 5.8919 20 5.8919H16.7799
  617. M17.888 7.8919 H6.11196 L7.30423 19.1057
  618. C7.3583 19.6142 7.78727 20 8.29863 20H15.7014
  619. C16.2127 20 16.6417 19.6142 16.6958 19.1057 L17.888 7.8919
  620. M10 10 C10.5523 10 11 10.4477 11 11V16
  621. C11 16.5523 10.5523 17 10 17
  622. C9.44772 17 9 16.5523 9 16V11
  623. C9 10.4477 9.44772 10 10 10
  624. M14 10 C14.5523 10 15 10.4477 15 11V16
  625. C15 16.5523 14.5523 17 14 17
  626. C13.4477 17 13 16.5523 13 16V11
  627. C13 10.4477 13.4477 10 14 10
  628. "
  629. fill="currentColor"/>
  630. </svg>
  631. `;
  632.  
  633. // Add hover color change events
  634. removeButton.addEventListener('mouseover', () => {
  635. removeButton.style.color = '#E3E3E3';
  636. });
  637.  
  638. removeButton.addEventListener('mouseout', () => {
  639. removeButton.style.color = '#B4B4B4';
  640. });
  641.  
  642. // Add click behavior to stop parent click from firing and trigger actions
  643. removeButton.addEventListener('click', (e) => {
  644. e.stopPropagation();
  645. deleteProfile(profile); // Replace with your profile deletion logic
  646. loadProfiles();
  647. loadProfileEntries();
  648. });
  649.  
  650. // Append remove button to the profile item
  651. profileItem.appendChild(removeButton);
  652.  
  653. profileList.appendChild(profileItem);
  654. });
  655. }
  656.  
  657. /************************************************************
  658. * Lorebook Entries Title (will switch to "Profiles" on mobile)
  659. ************************************************************/
  660. const lorebookEntriesTitle = document.createElement('h3');
  661. lorebookEntriesTitle.innerText = 'Manage Lorebook';
  662. lorebookEntriesTitle.style.fontWeight = '550';
  663. lorebookEntriesTitle.style.color = '#ffffff';
  664. lorebookEntriesTitle.style.textAlign = 'center';
  665. lorebookEntriesTitle.style.fontSize = '24px';
  666. lorebookEntriesTitle.style.marginTop = '10px';
  667. lorebookEntriesTitle.style.position = 'relative';
  668. //lorebookEntriesTitle.style.marginLeft = '40%';
  669.  
  670. lorebookEntriesTitle.style.marginBottom = '15px';
  671.  
  672. // -- [SEARCH BAR + SORT BUTTON] --
  673. const controlBar = document.createElement('div');
  674. //controlBar.style.display = 'flex';
  675. //controlBar.style.gap = '10px';
  676. //controlBar.style.alignItems = 'center';
  677. controlBar.style.marginLeft = '25%';
  678. controlBar.style.marginBottom = '5px';
  679. controlBar.style.width = '100%';
  680.  
  681. // Search Input
  682. const searchInput = document.createElement('input');
  683. searchInput.type = 'text';
  684. searchInput.placeholder = 'Search entries...';
  685. searchInput.style.width = '200px';
  686. searchInput.style.padding = '8px';
  687. searchInput.style.border = '1px solid #444444';
  688. searchInput.style.borderRadius = '8px';
  689. searchInput.style.backgroundColor = '#1e1e1e';
  690. searchInput.style.color = '#dddddd';
  691.  
  692. // Trigger loadProfileEntries() whenever user types
  693. searchInput.addEventListener('input', () => {
  694. loadProfileEntries();
  695. });
  696.  
  697. // Sort Button
  698. let sortAlphabetical = false;
  699. const sortButton = document.createElement('button');
  700. sortButton.innerText = 'Sort: Date';
  701. sortButton.style.padding = '8px 15px';
  702. sortButton.style.border = '0.2px solid #4E4E4E';
  703. sortButton.style.backgroundColor = 'transparent';
  704. sortButton.style.color = '#fff';
  705. sortButton.style.borderRadius = '20px';
  706. sortButton.style.cursor = 'pointer';
  707. sortButton.style.marginLeft = '5px';
  708.  
  709. sortButton.addEventListener('click', () => {
  710. sortAlphabetical = !sortAlphabetical;
  711. sortButton.innerText = sortAlphabetical ? 'Sort: A-Z' : 'Sort: Date';
  712. loadProfileEntries();
  713. });
  714.  
  715. controlBar.appendChild(searchInput);
  716. controlBar.appendChild(sortButton);
  717.  
  718. // Profile Entries List
  719. const profileEntriesList = document.createElement('div');
  720. profileEntriesList.id = 'profileEntriesList';
  721. profileEntriesList.style.marginTop = '20px';
  722. profileEntriesList.style.overflowY = 'auto';
  723.  
  724. // Header for Inputs
  725. const entrieslabel = document.createElement('div');
  726. entrieslabel.textContent = 'Enter keys and description:';
  727. entrieslabel.style.color = '#dddddd';
  728. entrieslabel.style.fontSize = '14px';
  729. entrieslabel.style.marginBottom = '5px';
  730. entrieslabel.style.marginTop = '5px';
  731. entrieslabel.style.marginLeft = '23%';
  732.  
  733. // Create key-value input fields
  734. const inputContainer = document.createElement('div');
  735. inputContainer.id = 'inputContainer';
  736. inputContainer.style.marginTop = '10px';
  737. inputContainer.style.display = 'flex';
  738. inputContainer.style.flexDirection = 'column';
  739. inputContainer.style.alignItems = 'center';
  740. inputContainer.style.margin = '0 auto';
  741.  
  742. const lorebookKeyInput = document.createElement('input');
  743. lorebookKeyInput.type = 'text';
  744. lorebookKeyInput.placeholder = 'Entry Keywords (comma-separated)';
  745. lorebookKeyInput.style.width = '90%';
  746. lorebookKeyInput.style.marginBottom = '5px';
  747. lorebookKeyInput.style.padding = '10px';
  748. lorebookKeyInput.style.border = '1px solid #444444';
  749. lorebookKeyInput.style.borderRadius = '8px';
  750. lorebookKeyInput.style.backgroundColor = '#1e1e1e';
  751. lorebookKeyInput.style.color = '#dddddd';
  752.  
  753. const lorebookValueInput = document.createElement('textarea');
  754. lorebookValueInput.placeholder = ' ';
  755. lorebookValueInput.style.width = '90%';
  756. lorebookValueInput.style.marginBottom = '5px';
  757. lorebookValueInput.style.padding = '10px';
  758. lorebookValueInput.style.border = '1px solid #444444';
  759. lorebookValueInput.style.borderRadius = '8px';
  760. lorebookValueInput.style.backgroundColor = '#1e1e1e';
  761. lorebookValueInput.style.color = '#dddddd';
  762. lorebookValueInput.style.height = '100px';
  763. lorebookValueInput.style.resize = 'vertical';
  764. lorebookValueInput.maxLength = 2000;
  765. lorebookValueInput.style.overflow = 'auto';
  766.  
  767. const charCounter = document.createElement('div');
  768. charCounter.style.color = '#dddddd';
  769. charCounter.style.fontSize = '12px';
  770. charCounter.style.marginTop = '0px';
  771. charCounter.style.marginBottom = '15px';
  772. charCounter.style.textAlign = 'right';
  773. charCounter.style.marginRight = '-87%';
  774. charCounter.style.color = 'grey';
  775. charCounter.textContent = `0/${lorebookValueInput.maxLength}`;
  776.  
  777. lorebookValueInput.addEventListener('input', () => {
  778. charCounter.textContent = `${lorebookValueInput.value.length}/${lorebookValueInput.maxLength}`;
  779. });
  780.  
  781. // Create the Save Entry button
  782. const lorebookSaveButton = document.createElement('button');
  783. lorebookSaveButton.innerText = 'Add Entry';
  784. lorebookSaveButton.style.padding = '10px 20px';
  785. lorebookSaveButton.style.border = '0.2px solid #4E4E4E';
  786. lorebookSaveButton.style.backgroundColor = '#2F2F2F';
  787. lorebookSaveButton.style.color = '#fff';
  788. lorebookSaveButton.style.borderRadius = '50px';
  789. lorebookSaveButton.style.cursor = 'pointer';
  790. lorebookSaveButton.style.flex = '1'; // Allow the button to grow equally
  791.  
  792. lorebookSaveButton.addEventListener('mouseenter', () => {
  793. lorebookSaveButton.style.backgroundColor = CONFIG.BUTTON_HIGHLIGHT;
  794. });
  795. lorebookSaveButton.addEventListener('mouseleave', () => {
  796. lorebookSaveButton.style.backgroundColor = 'transparent';
  797. });
  798.  
  799. // Create the Cancel Edit button
  800. const cancelEditButton = document.createElement('button');
  801. cancelEditButton.innerText = 'Cancel Edit';
  802. cancelEditButton.style.padding = '10px 20px';
  803. cancelEditButton.style.border = '0.2px solid #4E4E4E';
  804. cancelEditButton.style.backgroundColor = '#2F2F2F';
  805. cancelEditButton.style.color = '#fff';
  806. cancelEditButton.style.borderRadius = '50px';
  807. cancelEditButton.style.cursor = 'pointer';
  808. cancelEditButton.style.flex = '1'; // Allow equal growth
  809. cancelEditButton.style.display = 'none'; // Hidden until an edit is in progress
  810.  
  811. cancelEditButton.addEventListener('mouseenter', () => {
  812. cancelEditButton.style.backgroundColor = CONFIG.BUTTON_HIGHLIGHT;
  813. });
  814. cancelEditButton.addEventListener('mouseleave', () => {
  815. cancelEditButton.style.backgroundColor = 'transparent';
  816. });
  817.  
  818. cancelEditButton.addEventListener('click', () => {
  819. // Abandon the edit: clear inputs and reset state
  820. lorebookKeyInput.value = '';
  821. lorebookValueInput.value = '';
  822. charCounter.textContent = `0/${lorebookValueInput.maxLength}`;
  823. isEditing = false;
  824. editingIndex = -1;
  825. lorebookSaveButton.innerText = 'Add Entry';
  826. cancelEditButton.style.display = 'none';
  827. });
  828.  
  829. // Create a flex container to house both buttons side by side
  830. const buttonContainer = document.createElement('div');
  831. buttonContainer.style.display = 'flex';
  832. buttonContainer.style.justifyContent = 'space-between';
  833. buttonContainer.style.width = '95%';
  834. buttonContainer.style.marginTop = '10px';
  835.  
  836. // Add a small gap between the buttons
  837. cancelEditButton.style.marginLeft = '10px';
  838.  
  839. // Append the buttons to the flex container
  840.  
  841. // DOM structure
  842. lorebookPanel.appendChild(lorebookEntriesTitle);
  843. lorebookPanel.appendChild(showProfilesButton);
  844. lorebookPanel.appendChild(lorebookProfilePanel);
  845. lorebookProfilePanel.appendChild(profileslabel);
  846. lorebookProfilePanel.appendChild(profileList);
  847. lorebookProfilePanel.appendChild(addProfileButton);
  848. lorebookPanel.appendChild(controlBar);
  849. lorebookPanel.appendChild(profileEntriesList);
  850. lorebookPanel.appendChild(entrieslabel);
  851. lorebookPanel.appendChild(inputContainer);
  852. inputContainer.appendChild(lorebookKeyInput);
  853. inputContainer.appendChild(lorebookValueInput);
  854. inputContainer.appendChild(charCounter);
  855. buttonContainer.appendChild(lorebookSaveButton);
  856. buttonContainer.appendChild(cancelEditButton);
  857.  
  858. // Finally, append the button container to your input container
  859. inputContainer.appendChild(buttonContainer);
  860.  
  861. let isEditing = false;
  862. let editingIndex = -1; // We'll track the array index if editing
  863.  
  864. lorebookSaveButton.addEventListener('click', () => {
  865. const rawKey = lorebookKeyInput.value.trim().toLowerCase();
  866. const value = lorebookValueInput.value;
  867. const currentProfile = getCurrentProfile();
  868.  
  869. if (!currentProfile) {
  870. alert('Please select a profile.');
  871. return;
  872. }
  873.  
  874. if (!rawKey) {
  875. alert('Please enter a key for the entry.');
  876. return;
  877. }
  878.  
  879. const newKeySet = parseKeyToSet(rawKey);
  880.  
  881. let lorebookEntries = getAllLorebookEntries();
  882. if (!lorebookEntries[currentProfile]) {
  883. lorebookEntries[currentProfile] = [];
  884. }
  885.  
  886. if (!canAddKey(newKeySet, lorebookEntries[currentProfile], isEditing, editingIndex)) {
  887. return;
  888. }
  889.  
  890. const newEntry = `<[Lorebook: ${rawKey}] ${value}>`;
  891.  
  892.  
  893.  
  894.  
  895. if (isEditing && editingIndex >= 0) {
  896. lorebookEntries[currentProfile][editingIndex] = newEntry;
  897. isEditing = false;
  898. editingIndex = -1;
  899. } else {
  900. lorebookEntries[currentProfile].push(newEntry);
  901. }
  902.  
  903. setAllLorebookEntries(lorebookEntries);
  904.  
  905. // Reset the input form
  906. lorebookKeyInput.value = '';
  907. lorebookValueInput.value = '';
  908. charCounter.textContent = `0/${lorebookValueInput.maxLength}`;
  909.  
  910. // Restore initial button state
  911. lorebookSaveButton.innerText = 'Add Entry';
  912. cancelEditButton.style.display = 'none';
  913.  
  914. loadProfileEntries
  915. });
  916.  
  917. // Modified loadProfileEntries to support searching + sorting
  918. function loadProfileEntries() {
  919. profileEntriesList.innerHTML = '';
  920. profileEntriesList.style.display = 'flex';
  921. profileEntriesList.style.flexDirection = 'column';
  922. profileEntriesList.style.alignItems = 'center';
  923. profileEntriesList.style.margin = '0 auto';
  924.  
  925. const currentProfile = getCurrentProfile();
  926. if (!currentProfile) return;
  927.  
  928. let lorebookEntries = getAllLorebookEntries();
  929. let entriesArray = lorebookEntries[currentProfile] || [];
  930.  
  931. // Filter by search query
  932. const query = searchInput.value.trim().toLowerCase();
  933. if (query) {
  934. entriesArray = entriesArray.filter(entry =>
  935. entry.toLowerCase().includes(query)
  936. );
  937. }
  938.  
  939. // Sort
  940. if (sortAlphabetical) {
  941. // We'll sort by the key portion if we can parse it, fallback to the full string
  942. entriesArray.sort((a, b) => {
  943. const aMatch = a.match(/^<\[Lorebook:\s*(.*?)\]\s*(.*?)>$/);
  944. const bMatch = b.match(/^<\[Lorebook:\s*(.*?)\]\s*(.*?)>$/);
  945.  
  946. let aKey = aMatch ? aMatch[1] : a;
  947. let bKey = bMatch ? bMatch[1] : b;
  948. return aKey.localeCompare(bKey);
  949. });
  950. }
  951. // Else default is the chronological order in localStorage
  952.  
  953. entriesArray.forEach((entry) => {
  954. const entryItem = document.createElement('div');
  955. entryItem.style.padding = '10px';
  956. entryItem.style.marginBottom = '12px';
  957. entryItem.style.borderRadius = '8px';
  958. entryItem.style.backgroundColor = '#424242';
  959. entryItem.style.position = 'relative';
  960. entryItem.style.color = 'white';
  961. entryItem.style.flexDirection = 'column';
  962. entryItem.style.width = '90%';
  963.  
  964. let matched = entry.match(/^<\[Lorebook:\s*(.*?)\]\s*([\s\S]*?)>$/);
  965.  
  966. let keyPart = '';
  967. let textPart = '';
  968. if (matched) {
  969. keyPart = matched[1];
  970. textPart = matched[2];
  971. }
  972.  
  973. const keyElement = document.createElement('div');
  974. keyElement.innerText = keyPart;
  975. keyElement.style.fontWeight = 'bold';
  976. keyElement.style.marginBottom = '10px';
  977. entryItem.appendChild(keyElement);
  978.  
  979. const valueElement = document.createElement('div');
  980. valueElement.innerText = textPart;
  981. entryItem.appendChild(valueElement);
  982.  
  983. entryItem.addEventListener('click', () => {
  984. lorebookKeyInput.value = keyPart;
  985. lorebookValueInput.value = textPart;
  986. charCounter.textContent = `${textPart.length}/${lorebookValueInput.maxLength}`;
  987. isEditing = true;
  988. editingIndex = findFullIndexInStorage(entry);
  989. lorebookSaveButton.innerText = 'Save Changes';
  990. cancelEditButton.style.display = 'block';
  991. });
  992.  
  993.  
  994.  
  995. // Create button
  996. const removeButton = document.createElement('button');
  997. removeButton.style.cssText = `
  998. color: #B4B4B4;
  999. background-color: transparent;
  1000. cursor: pointer;
  1001. position: absolute;
  1002. top: 10px;
  1003. right: 10px;
  1004. border: none; /* optional: remove default button border */
  1005. outline: none; /* optional: remove focus outline */
  1006. padding: 0; /* optional: remove default button padding */
  1007. `;
  1008.  
  1009. // Insert the same SVG icon you used in deleteButton
  1010. removeButton.innerHTML = `
  1011. <svg width="24" height="24" viewBox="0 0 24 24" fill="none"
  1012. xmlns="http://www.w3.org/2000/svg" class="icon-sm">
  1013. <path fill-rule="evenodd" clip-rule="evenodd"
  1014. d="
  1015. M10.5555 4 C10.099 4 9.70052 4.30906 9.58693 4.75114
  1016. L9.29382 5.8919 H14.715 L14.4219 4.75114
  1017. C14.3083 4.30906 13.9098 4 13.4533 4H10.5555
  1018. M16.7799 5.8919 L16.3589 4.25342
  1019. C16.0182 2.92719 14.8226 2 13.4533 2H10.5555
  1020. C9.18616 2 7.99062 2.92719 7.64985 4.25342
  1021. L7.22886 5.8919 H4
  1022. C3.44772 5.8919 3 6.33961 3 6.8919
  1023. C3 7.44418 3.44772 7.8919 4 7.8919H4.10069
  1024. L5.31544 19.3172 C5.47763 20.8427 6.76455 22 8.29863 22H15.7014
  1025. C17.2354 22 18.5224 20.8427 18.6846 19.3172 L19.8993 7.8919 H20
  1026. C20.5523 7.8919 21 7.44418 21 6.8919
  1027. C21 6.33961 20.5523 5.8919 20 5.8919H16.7799
  1028. M17.888 7.8919 H6.11196 L7.30423 19.1057
  1029. C7.3583 19.6142 7.78727 20 8.29863 20H15.7014
  1030. C16.2127 20 16.6417 19.6142 16.6958 19.1057 L17.888 7.8919
  1031. M10 10 C10.5523 10 11 10.4477 11 11V16
  1032. C11 16.5523 10.5523 17 10 17
  1033. C9.44772 17 9 16.5523 9 16V11
  1034. C9 10.4477 9.44772 10 10 10
  1035. M14 10 C14.5523 10 15 10.4477 15 11V16
  1036. C15 16.5523 14.5523 17 14 17
  1037. C13.4477 17 13 16.5523 13 16V11
  1038. C13 10.4477 13.4477 10 14 10
  1039. "
  1040. fill="currentColor"/>
  1041. </svg>
  1042. `;
  1043.  
  1044. // Add hover color change events
  1045. removeButton.addEventListener('mouseover', () => {
  1046. removeButton.style.color = '#E3E3E3';
  1047. });
  1048.  
  1049. removeButton.addEventListener('mouseout', () => {
  1050. removeButton.style.color = '#B4B4B4';
  1051. });
  1052.  
  1053. // Add click behavior
  1054. removeButton.addEventListener('click', (event) => {
  1055. event.stopPropagation();
  1056. removeEntry(currentProfile, entry);
  1057. });
  1058.  
  1059. // Append to your entry item
  1060. entryItem.appendChild(removeButton);
  1061.  
  1062. entryItem.appendChild(removeButton);
  1063. profileEntriesList.appendChild(entryItem);
  1064. });
  1065. }
  1066.  
  1067. // Because we filter/sort entries before display, find the real index in localStorage
  1068. function findFullIndexInStorage(entryString) {
  1069. const currentProfile = getCurrentProfile();
  1070. let lorebookEntries = getAllLorebookEntries();
  1071. let entriesArray = lorebookEntries[currentProfile] || [];
  1072. return entriesArray.findIndex(e => e === entryString);
  1073. }
  1074.  
  1075. function removeEntry(profileName, entryString) {
  1076. let lorebookEntries = getAllLorebookEntries();
  1077. if (!lorebookEntries[profileName]) return;
  1078. let arr = lorebookEntries[profileName];
  1079. const realIndex = arr.findIndex(e => e === entryString);
  1080. if (realIndex >= 0) {
  1081. arr.splice(realIndex, 1);
  1082. lorebookEntries[profileName] = arr;
  1083. setAllLorebookEntries(lorebookEntries);
  1084. loadProfileEntries();
  1085. }
  1086. }
  1087.  
  1088. function getAllLorebookEntries() {
  1089. let data = localStorage.getItem('lorebookEntries');
  1090. if (!data) {
  1091. return {};
  1092. }
  1093. try {
  1094. return JSON.parse(data);
  1095. } catch (e) {
  1096. console.error('Error parsing lorebookEntries:', e);
  1097. return {};
  1098. }
  1099. }
  1100.  
  1101. function setAllLorebookEntries(lorebookEntries) {
  1102. localStorage.setItem('lorebookEntries', JSON.stringify(lorebookEntries));
  1103. }
  1104.  
  1105. function deleteProfile(profileName) {
  1106. let lorebookEntries = getAllLorebookEntries();
  1107. if (lorebookEntries.hasOwnProperty(profileName)) {
  1108. delete lorebookEntries[profileName];
  1109. setAllLorebookEntries(lorebookEntries);
  1110. if (getCurrentProfile() === profileName) {
  1111. localStorage.removeItem('selectedProfile.lorebook');
  1112. }
  1113. }
  1114. }
  1115.  
  1116. function getCurrentProfile() {
  1117. const selectedProfile = localStorage.getItem('selectedProfile.lorebook');
  1118. return selectedProfile ? selectedProfile : null;
  1119. }
  1120.  
  1121. function setCurrentProfile(profileName) {
  1122. localStorage.setItem('selectedProfile.lorebook', profileName);
  1123. }
  1124.  
  1125. function parseKeyToSet(keyString) {
  1126. return new Set(
  1127. keyString
  1128. .split(',')
  1129. .map(item => item.trim())
  1130. .filter(Boolean)
  1131. );
  1132. }
  1133.  
  1134. function canAddKey(newKeySet, allEntries, isEditing, editingIndex) {
  1135. for (let i = 0; i < allEntries.length; i++) {
  1136. if (isEditing && i === editingIndex) continue;
  1137. const match = allEntries[i].match(/^<\[Lorebook:\s*(.*?)\]\s*(.*?)>$/);
  1138. if (!match) continue;
  1139.  
  1140. const existingKeyString = match[1];
  1141. const existingKeySet = parseKeyToSet(existingKeyString);
  1142.  
  1143. // Check for exact match
  1144. if (setsAreEqual(newKeySet, existingKeySet)) {
  1145. alert(`Key "${existingKeyString}" already exists for this profile.`);
  1146. return false;
  1147. }
  1148. // Check for subset or superset conflict
  1149. if (isSubset(newKeySet, existingKeySet) || isSubset(existingKeySet, newKeySet)) {
  1150. alert(`Key "${existingKeyString}" is in conflict (subset/superset) with the new key.`);
  1151. return false;
  1152. }
  1153. }
  1154. return true;
  1155. }
  1156.  
  1157. function setsAreEqual(a, b) {
  1158. if (a.size !== b.size) return false;
  1159. for (let val of a) {
  1160. if (!b.has(val)) return false;
  1161. }
  1162. return true;
  1163. }
  1164.  
  1165. function isSubset(smallerSet, biggerSet) {
  1166. for (let val of smallerSet) {
  1167. if (!biggerSet.has(val)) return false;
  1168. }
  1169. return true;
  1170. }
  1171.  
  1172. // This function adjusts layout on resize or after toggling
  1173.  
  1174.  
  1175.  
  1176. // updateLayout() - handle mobile vs desktop
  1177. function updateLayout() {
  1178. if (isMobile()) {
  1179. // MOBILE LAYOUT
  1180. lorebookPanel.style.width = '90%';
  1181. lorebookPanel.style.height = '90%';
  1182.  
  1183. lorebookProfilePanel.style.display = 'none'; // hidden by default
  1184. lorebookProfilePanel.style.width = '100%';
  1185. lorebookProfilePanel.style.borderRight = 'none';
  1186. lorebookProfilePanel.style.flexDirection = 'column';
  1187. lorebookProfilePanel.style.alignItems = 'center';
  1188. lorebookProfilePanel.style.justifyContent = 'start';
  1189. lorebookProfilePanel.style.textAlign = 'center';
  1190. lorebookProfilePanel.style.marginTop = '0px';
  1191. lorebookProfilePanel.style.marginLeft = '2.5%';
  1192.  
  1193. entrieslabel.style.marginLeft = '5%';
  1194. controlBar.style.marginLeft = '5%';
  1195. addProfileButton.style.marginLeft = '0px';
  1196. profileList.style.height = '90%';
  1197. addProfileButton.style.width = '95%';
  1198. charCounter.style.marginRight = '-80%';
  1199.  
  1200. // Always hide the smaller label on mobile
  1201. profileslabel.style.display = 'none';
  1202.  
  1203. // This sets the big title back to "Manage Lorebook"
  1204. // until the user opens profiles
  1205. lorebookEntriesTitle.innerText = 'Manage Lorebook';
  1206.  
  1207. showProfilesButton.style.display = 'block';
  1208. showProfilesButton.style.margin = '10px auto';
  1209.  
  1210.  
  1211. if (lorebookProfilePanel.style.display === 'none') {
  1212. lorebookCloseButton.style.display = 'flex';
  1213. }
  1214.  
  1215. // --- BLUNT “5% STEPS” LOGIC ---
  1216. // Grab the current panel height
  1217. // (the actual offsetHeight of the container
  1218. // or you could compare window.innerHeight, etc.)
  1219. const panelHeight = lorebookPanel.offsetHeight;
  1220. let entriesPercent = 10;
  1221.  
  1222. if (panelHeight >= 500 && panelHeight < 600) {
  1223. entriesPercent = 15;
  1224. } else if (panelHeight >= 600 && panelHeight < 650) {
  1225. entriesPercent = 20;
  1226. } else if (panelHeight >= 650 && panelHeight < 700) {
  1227. entriesPercent = 30;
  1228. } else if (panelHeight >= 700 && panelHeight < 750) {
  1229. entriesPercent = 35;
  1230. } else if (panelHeight >= 750 && panelHeight < 800) {
  1231. entriesPercent = 40; // Could be higher than initial 30
  1232. } else if (panelHeight >= 800 && panelHeight < 850) {
  1233. entriesPercent = 44;
  1234. } else if (panelHeight >= 850 && panelHeight < 900) {
  1235. entriesPercent = 48;
  1236. } else if (panelHeight >= 900) {
  1237. entriesPercent = 52;
  1238. }
  1239.  
  1240. profileEntriesList.style.height = entriesPercent + '%';
  1241.  
  1242. } else {
  1243. // DESKTOP LAYOUT
  1244. lorebookPanel.style.width = '1000px';
  1245. lorebookPanel.style.height = '700px';
  1246.  
  1247. lorebookProfilePanel.style.display = 'block';
  1248. lorebookProfilePanel.style.width = '20%';
  1249. lorebookProfilePanel.style.borderRight = '0.5px solid #444444';
  1250. lorebookProfilePanel.style.marginTop = '-40px';
  1251. lorebookProfilePanel.style.marginLeft = '0px';
  1252.  
  1253. // Show the smaller label on desktop only
  1254. profileslabel.style.display = 'block';
  1255.  
  1256. lorebookEntriesTitle.innerText = 'Manage Lorebook';
  1257. profileList.style.height = '95%';
  1258. addProfileButton.style.width = '86%';
  1259. addProfileButton.style.marginLeft = '7px';
  1260.  
  1261. showProfilesButton.style.display = 'none';
  1262. lorebookCloseButton.style.display = 'flex';
  1263.  
  1264. entrieslabel.style.marginLeft = '24%';
  1265. controlBar.style.marginLeft = '24%';
  1266. profileEntriesList.style.height = '285px';
  1267. charCounter.style.marginRight = '-87%';
  1268. }
  1269. }
  1270.  
  1271. // Listen for window resize
  1272. window.addEventListener('resize', updateLayout);
  1273.  
  1274. // Initial load
  1275. loadProfiles();
  1276. }
  1277.  
  1278.  
  1279. // ==================================================================== RULES ====================================================================
  1280.  
  1281. function rulesScript() {
  1282. // Profiles object structure: { profileName: [arrayOfRules], ... }
  1283. let profiles = {};
  1284. let currentProfile = null;
  1285. let customRules = [];
  1286. let currentIndex = 0;
  1287.  
  1288. // Load profiles from localStorage
  1289. const storedRulesProfiles = localStorage.getItem('rulesProfiles');
  1290. if (storedRulesProfiles) {
  1291. profiles = JSON.parse(storedRulesProfiles);
  1292. }
  1293.  
  1294. // Check if there's a previously selected profile stored
  1295. const selectedProfileRules = localStorage.getItem('selectedProfile.Rules');
  1296.  
  1297. if (Object.keys(profiles).length === 0) {
  1298. // If no profiles exist, create a default one
  1299. profiles['Default'] = [];
  1300. saveProfiles();
  1301. currentProfile = 'Default';
  1302. } else {
  1303. // If we have a stored selected profile, use that; otherwise use first available
  1304. if (selectedProfileRules && profiles[selectedProfileRules]) {
  1305. currentProfile = selectedProfileRules;
  1306. } else {
  1307. currentProfile = Object.keys(profiles)[0];
  1308. }
  1309. }
  1310.  
  1311. // Sync customRules with currently selected profile
  1312. loadProfileRules(currentProfile);
  1313.  
  1314. function loadProfileRules(profileName) {
  1315. currentProfile = profileName;
  1316. customRules = profiles[currentProfile] || [];
  1317. // Save the selected profile so it's sticky across sessions
  1318. localStorage.setItem('selectedProfile.Rules', currentProfile);
  1319. }
  1320.  
  1321. function saveProfiles() {
  1322. localStorage.setItem('rulesProfiles', JSON.stringify(profiles));
  1323. }
  1324.  
  1325.  
  1326. // Create Button and Panel UI for Local Storage Key Management
  1327. window.manageRulesButton = document.createElement('div');
  1328. window.manageRulesButton.innerHTML = `
  1329. <button id="toggle-rules-panel"
  1330. style="
  1331. position: relative;
  1332. top: 10px;
  1333. right: 0px;
  1334. left: 10px;
  1335. padding: 7px 15px;
  1336. background: transparent;
  1337. color: #b0b0b0;
  1338. border: none;
  1339. border-radius: 8px;
  1340. font-size: 16px;
  1341. text-align: left;
  1342. cursor: pointer;
  1343. width: 90%;
  1344. display: flex;
  1345. align-items: center;
  1346. gap: 10px;
  1347. transition: background-color 0.1s, color 0.1s;
  1348. z-index: 1001;">
  1349. <svg fill="#B0B0B0" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg"
  1350. xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px"
  1351. viewBox="0 0 492.508 492.508" xml:space="preserve"
  1352. style="padding-right: 0px; margin-left: 1px;">
  1353. <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
  1354. <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
  1355. <g id="SVGRepo_iconCarrier">
  1356. <g>
  1357. <g>
  1358. <path d="M199.493,402.145c0-10.141-8.221-18.361-18.36-18.361H42.475c-10.139,0-18.36,8.221-18.36,18.361
  1359. c0,3.195,0.818,6.199,2.255,8.816H0v38.067h223.607v-38.067h-26.369C198.674,408.343,199.493,405.34,199.493,402.145z">
  1360. </path>
  1361. <path d="M175.898,88.224l117.157,74.396c9.111,4.643,20.43,1.678,26.021-7.129l5.622-8.85
  1362. c5.938-9.354,3.171-21.75-6.182-27.69L204.592,46.608c-9.352-5.939-21.748-3.172-27.688,6.182l-5.622,8.851
  1363. C165.692,70.447,167.82,81.952,175.898,88.224z">
  1364. </path>
  1365. <path d="M492.456,372.433l-0.082-1.771l-0.146-1.672c-0.075-1.143-0.235-2.159-0.375-3.204c-0.562-4.177-1.521-7.731-2.693-10.946
  1366. c-2.377-6.386-5.738-11.222-9.866-14.845c-1.027-0.913-2.126-1.714-3.218-2.528l-3.271-2.443
  1367. c-2.172-1.643-4.387-3.218-6.587-4.815c-2.196-1.606-4.419-3.169-6.644-4.729c-2.218-1.571-4.445-3.125-6.691-4.651
  1368. c-4.468-3.089-8.983-6.101-13.51-9.103l-6.812-4.464l-6.85-4.405c-4.58-2.911-9.167-5.813-13.785-8.667
  1369. c-4.611-2.865-9.24-5.703-13.896-8.496l-13.979-8.363l-14.072-8.22l-14.149-8.096l-14.219-7.987l-14.287-7.882l-14.354-7.773
  1370. c-4.802-2.566-9.599-5.137-14.433-7.653c-4.822-2.529-9.641-5.071-14.498-7.548l-4.398,6.928l-22.17-10.449l24.781-39.026
  1371. l-117.156-74.395l-60.944,95.974l117.157,74.395l24.781-39.026l18.887,15.622l-4.399,6.929c4.309,3.343,8.657,6.619,12.998,9.91
  1372. c4.331,3.305,8.698,6.553,13.062,9.808l13.14,9.686l13.211,9.577l13.275,9.474l13.346,9.361l13.422,9.242l13.514,9.095
  1373. c4.51,3.026,9.045,6.009,13.602,8.964c4.547,2.967,9.123,5.882,13.707,8.792l6.898,4.324l6.936,4.266
  1374. c4.643,2.818,9.289,5.625,13.985,8.357c2.337,1.383,4.689,2.739,7.055,4.078c2.358,1.349,4.719,2.697,7.106,4
  1375. c2.383,1.312,4.75,2.646,7.159,3.912l3.603,1.922c1.201,0.64,2.394,1.296,3.657,1.837c5.036,2.194,10.841,3.18,17.63,2.614
  1376. c3.409-0.305,7.034-0.949,11.054-2.216c1.006-0.317,1.992-0.606,3.061-1.023l1.574-0.58l1.639-0.68
  1377. c2.185-0.91,4.523-2.063,7.059-3.522C492.513,377.405,492.561,374.799,492.456,372.433z">
  1378. </path>
  1379. <path d="M67.897,261.877l113.922,72.341c9.354,5.938,21.75,3.172,27.689-6.181l5.621-8.852
  1380. c5.592-8.808,3.462-20.311-4.615-26.583L93.358,218.207c-9.111-4.642-20.43-1.678-26.022,7.13l-5.62,8.85
  1381. C55.775,243.541,58.543,255.938,67.897,261.877z">
  1382. </path>
  1383. </g>
  1384. </g>
  1385. </g>
  1386. </svg>
  1387. Manage Rules
  1388. </button>
  1389. `;
  1390. const rulesButtonElement = manageRulesButton.querySelector('button');
  1391. rulesButtonElement.onmouseover = () => {
  1392. rulesButtonElement.style.backgroundColor = '#212121';
  1393. rulesButtonElement.style.color = '#ffffff';
  1394. };
  1395. rulesButtonElement.onmouseout = () => {
  1396. rulesButtonElement.style.backgroundColor = 'transparent';
  1397. rulesButtonElement.style.color = '#b0b0b0';
  1398. };
  1399.  
  1400. const rulesPanel = document.createElement('div');
  1401. rulesPanel.style.position = 'fixed';
  1402. rulesPanel.style.top = '50%';
  1403. rulesPanel.style.left = '50%';
  1404. rulesPanel.style.transform = 'translate(-50%, -50%)';
  1405. rulesPanel.style.zIndex = 1000;
  1406. rulesPanel.style.backgroundColor = '#2f2f2f';
  1407. rulesPanel.style.padding = '20px';
  1408. rulesPanel.style.borderRadius = '20px';
  1409. rulesPanel.style.display = 'none';
  1410. document.body.appendChild(rulesPanel);
  1411.  
  1412. const isMobile = () => window.innerWidth <= 768;
  1413.  
  1414. const rulesTitle = document.createElement('h2');
  1415. rulesTitle.innerText = 'Manage Rules';
  1416. rulesTitle.style.textAlign = 'center';
  1417. rulesTitle.style.color = '#ffffff';
  1418. rulesTitle.style.fontSize = '24px';
  1419. rulesTitle.style.fontWeight = '550';
  1420. rulesTitle.style.marginBottom = '20px';
  1421. rulesPanel.appendChild(rulesTitle);
  1422.  
  1423. const rulesCloseButton = document.createElement('button');
  1424. rulesCloseButton.style.position = 'absolute';
  1425. rulesCloseButton.style.borderRadius = '50%';
  1426. rulesCloseButton.style.top = '20px';
  1427. rulesCloseButton.style.right = '20px';
  1428. rulesCloseButton.style.backgroundColor = 'transparent';
  1429. rulesCloseButton.style.cursor = 'pointer';
  1430. rulesCloseButton.style.width = '32px';
  1431. rulesCloseButton.style.height = '32px';
  1432. rulesCloseButton.style.display = 'flex';
  1433. rulesCloseButton.style.alignItems = 'center';
  1434. rulesCloseButton.style.justifyContent = 'center';
  1435.  
  1436. const rulesCloseSymbol = document.createElement('span');
  1437. rulesCloseSymbol.innerText = '✕';
  1438. rulesCloseSymbol.style.color = '#FBFBFE';
  1439. rulesCloseSymbol.style.fontSize = '14px';
  1440. rulesCloseSymbol.style.fontWeight = '550';
  1441. rulesCloseSymbol.style.transform = 'translateY(-1px) translateX(0.4px)';
  1442.  
  1443. rulesCloseButton.appendChild(rulesCloseSymbol);
  1444.  
  1445. rulesCloseButton.addEventListener('mouseenter', () => {
  1446. rulesCloseButton.style.backgroundColor = '#676767';
  1447. });
  1448.  
  1449. rulesCloseButton.addEventListener('mouseleave', () => {
  1450. rulesCloseButton.style.backgroundColor = 'transparent';
  1451. });
  1452.  
  1453. const closeRulesPanel = () => {
  1454. rulesPanel.style.display = 'none';
  1455. hideOverlay();
  1456. };
  1457.  
  1458. rulesCloseButton.addEventListener('click', closeRulesPanel);
  1459. rulesPanel.appendChild(rulesCloseButton);
  1460.  
  1461. const container = document.createElement('div');
  1462. container.style.display = 'flex';
  1463. container.style.height = 'calc(100% - 60px)';
  1464. container.style.gap = '20px';
  1465. rulesPanel.appendChild(container);
  1466.  
  1467. // Mobile: show/hide profile panel functionality
  1468. const profilesOverlay = document.createElement('div');
  1469. profilesOverlay.style.position = 'absolute';
  1470. profilesOverlay.style.top = '0';
  1471. profilesOverlay.style.left = '0';
  1472. profilesOverlay.style.width = '100%';
  1473. profilesOverlay.style.height = '100%';
  1474. profilesOverlay.style.backgroundColor = '#2f2f2f';
  1475. // profilesOverlay.style.padding = '20px';
  1476. profilesOverlay.style.display = 'none';
  1477. profilesOverlay.style.zIndex = 1100;
  1478. profilesOverlay.style.borderRadius = '20px';
  1479. profilesOverlay.style.flexDirection = 'column';
  1480. profilesOverlay.style.overflowY = 'auto';
  1481. rulesPanel.appendChild(profilesOverlay);
  1482.  
  1483. const profilesTitleOverlay = document.createElement('h3');
  1484. profilesTitleOverlay.innerText = 'Profiles';
  1485. profilesTitleOverlay.style.color = '#ffffff';
  1486. profilesTitleOverlay.style.textAlign = 'center';
  1487. profilesOverlay.appendChild(profilesTitleOverlay);
  1488.  
  1489. const profilesCloseOverlay = document.createElement('button');
  1490. profilesCloseOverlay.innerText = 'Close';
  1491. profilesCloseOverlay.style.margin = '10px auto';
  1492. profilesCloseOverlay.style.display = 'block';
  1493. profilesCloseOverlay.style.padding = '10px';
  1494. profilesCloseOverlay.style.width = '90%';
  1495. profilesCloseOverlay.style.backgroundColor = 'transparent';
  1496. profilesCloseOverlay.style.color = '#ffffff';
  1497. profilesCloseOverlay.style.border = '1px solid #444';
  1498. profilesCloseOverlay.style.borderRadius = '50px';
  1499. profilesCloseOverlay.style.cursor = 'pointer';
  1500. profilesCloseOverlay.addEventListener('mouseenter', () => {
  1501. profilesCloseOverlay.style.backgroundColor = CONFIG.BUTTON_HIGHLIGHT;
  1502. });
  1503.  
  1504. profilesCloseOverlay.addEventListener('mouseleave', () => {
  1505. profilesCloseOverlay.style.backgroundColor = 'transparent';
  1506. });
  1507.  
  1508.  
  1509. profilesCloseOverlay.addEventListener('click', () => {
  1510. profilesOverlay.style.display = 'none';
  1511. });
  1512. profilesOverlay.appendChild(profilesCloseOverlay);
  1513.  
  1514. const profilesListOverlay = document.createElement('div');
  1515. profilesListOverlay.style.flex = '1';
  1516. profilesListOverlay.style.overflowY = 'auto';
  1517. profilesOverlay.appendChild(profilesListOverlay);
  1518.  
  1519. const addProfileButtonOverlay = document.createElement('button');
  1520. addProfileButtonOverlay.textContent = 'Add Profile';
  1521. addProfileButtonOverlay.style.margin = '10px auto';
  1522. addProfileButtonOverlay.style.display = 'block';
  1523. addProfileButtonOverlay.style.padding = '10px';
  1524. addProfileButtonOverlay.style.border = CONFIG.BUTTON_OUTLINE;
  1525. addProfileButtonOverlay.style.width = '90%';
  1526. addProfileButtonOverlay.style.backgroundColor = 'transparent';
  1527. addProfileButtonOverlay.style.borderRadius = '50px';
  1528. addProfileButtonOverlay.style.cursor = 'pointer';
  1529.  
  1530. addProfileButtonOverlay.addEventListener('mouseenter', () => {
  1531. addProfileButtonOverlay.style.backgroundColor = CONFIG.BUTTON_HIGHLIGHT;
  1532. });
  1533.  
  1534. addProfileButtonOverlay.addEventListener('mouseleave', () => {
  1535. addProfileButtonOverlay.style.backgroundColor = 'transparent';
  1536. });
  1537.  
  1538. addProfileButtonOverlay.addEventListener('click', () => {
  1539. const newProfileName = prompt('Enter Profile Name:');
  1540. if (newProfileName && !profiles[newProfileName]) {
  1541. profiles[newProfileName] = [];
  1542. saveProfiles();
  1543. renderProfilesList();
  1544. }
  1545. });
  1546. profilesOverlay.appendChild(addProfileButtonOverlay);
  1547.  
  1548. // Left side (profiles)
  1549. const profilesContainer = document.createElement('div');
  1550. profilesContainer.style.width = '21.5%';
  1551. // profilesContainer.style.backgroundColor = '#1E1E1E';
  1552. profilesContainer.style.borderRight = '1px solid #444444'; // Added border width, style, and color
  1553. profilesContainer.style.marginLeft = '-20px';
  1554. profilesContainer.style.display = 'flex';
  1555. profilesContainer.style.flexDirection = 'column';
  1556. profilesContainer.style.marginTop = '-4.7%';
  1557. profilesContainer.style.padding = '10px';
  1558. profilesContainer.style.overflowY = 'auto';
  1559.  
  1560.  
  1561. const profilesTitle = document.createElement('h3');
  1562. profilesTitle.innerText = 'Profiles';
  1563. profilesTitle.style.color = '#ffffff';
  1564. profilesTitle.style.textAlign = 'center';
  1565. profilesContainer.appendChild(profilesTitle);
  1566.  
  1567. const profilesList = document.createElement('div');
  1568. profilesList.style.flex = '1';
  1569. profilesList.style.overflowY = 'auto';
  1570. profilesContainer.appendChild(profilesList);
  1571.  
  1572. const addProfileButton = document.createElement('button');
  1573. addProfileButton.textContent = 'Add Profile';
  1574. addProfileButton.style.margin = '10px auto';
  1575. addProfileButton.style.display = 'block';
  1576. addProfileButton.style.padding = '10px';
  1577. addProfileButton.style.border = CONFIG.BUTTON_OUTLINE;
  1578. addProfileButton.style.width = '90%';
  1579. addProfileButton.style.backgroundColor = 'transparent';
  1580. addProfileButton.style.borderRadius = '50px';
  1581. addProfileButton.style.cursor = 'pointer';
  1582.  
  1583. addProfileButton.addEventListener('mouseenter', () => {
  1584. addProfileButton.style.backgroundColor = CONFIG.BUTTON_HIGHLIGHT;
  1585. });
  1586.  
  1587. addProfileButton.addEventListener('mouseleave', () => {
  1588. addProfileButton.style.backgroundColor = 'transparent';
  1589. });
  1590.  
  1591. addProfileButton.addEventListener('click', () => {
  1592. const newProfileName = prompt('Enter Profile Name:');
  1593. if (newProfileName && !profiles[newProfileName]) {
  1594. profiles[newProfileName] = [];
  1595. saveProfiles();
  1596. renderProfilesList();
  1597. }
  1598. });
  1599. profilesContainer.appendChild(addProfileButton);
  1600.  
  1601. // A button to toggle profiles overlay in mobile view
  1602. const toggleProfilesButton = document.createElement('button');
  1603. toggleProfilesButton.innerText = 'Show Profiles';
  1604. toggleProfilesButton.style.display = 'none';
  1605. toggleProfilesButton.style.margin = '10px auto';
  1606. toggleProfilesButton.style.padding = '10px';
  1607. toggleProfilesButton.style.border = 'none';
  1608. toggleProfilesButton.style.width = '90%';
  1609. toggleProfilesButton.style.border = CONFIG.BUTTON_OUTLINE;
  1610. toggleProfilesButton.style.backgroundColor = 'transparent';
  1611. toggleProfilesButton.style.borderRadius = '50px';
  1612. toggleProfilesButton.style.cursor = 'pointer';
  1613. toggleProfilesButton.addEventListener('mouseenter', () => {
  1614. toggleProfilesButton.style.backgroundColor = CONFIG.BUTTON_HIGHLIGHT;
  1615. });
  1616.  
  1617. toggleProfilesButton.addEventListener('mouseleave', () => {
  1618. toggleProfilesButton.style.backgroundColor = 'transparent';
  1619. });
  1620.  
  1621. toggleProfilesButton.addEventListener('click', () => {
  1622. profilesOverlay.style.display = 'flex';
  1623. renderProfilesList(); // Update list in overlay
  1624. });
  1625.  
  1626. rulesPanel.insertBefore(toggleProfilesButton, container);
  1627.  
  1628. function renderProfilesList() {
  1629. // Decide whether to render in overlay or side panel
  1630. const targetList = isMobile() ? profilesListOverlay : profilesList;
  1631. targetList.innerHTML = '';
  1632.  
  1633. Object.keys(profiles).forEach(profileName => {
  1634. const profileItem = document.createElement('div');
  1635. profileItem.style.display = 'flex';
  1636. profileItem.style.justifyContent = 'space-between';
  1637. profileItem.style.alignItems = 'center';
  1638. profileItem.style.backgroundColor = profileName === currentProfile ? '#424242' : 'transparent';
  1639. profileItem.style.color = '#ffffff';
  1640. profileItem.style.padding = '5px 10px';
  1641. profileItem.style.borderRadius = '5px';
  1642. profileItem.style.marginBottom = '5px';
  1643. profileItem.style.cursor = 'pointer';
  1644.  
  1645. const nameSpan = document.createElement('span');
  1646. nameSpan.innerText = profileName;
  1647. nameSpan.style.flex = '1';
  1648. profileItem.appendChild(nameSpan);
  1649.  
  1650. profileItem.addEventListener('click', () => {
  1651. loadProfileRules(profileName);
  1652. if (isMobile()) {
  1653. profilesOverlay.style.display = 'none';
  1654. }
  1655. renderProfilesList();
  1656. renderPanel();
  1657. });
  1658.  
  1659. // Delete button for the profile
  1660. if (profileName !== 'Default') {
  1661. const deleteButton = document.createElement('button');
  1662.  
  1663. deleteButton.style.cssText = 'color: #B4B4B4; background-color: transparent; cursor: pointer;';
  1664. deleteButton.innerHTML = `
  1665. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon-sm">
  1666. <path fill-rule="evenodd" clip-rule="evenodd"
  1667. d="
  1668. M10.5555 4 C10.099 4 9.70052 4.30906 9.58693 4.75114 L9.29382 5.8919 H14.715 L14.4219 4.75114 C14.3083 4.30906 13.9098 4 13.4533 4H10.5555
  1669. M16.7799 5.8919 L16.3589 4.25342 C16.0182 2.92719 14.8226 2 13.4533 2H10.5555 C9.18616 2 7.99062 2.92719 7.64985 4.25342 L7.22886 5.8919 H4
  1670. C3.44772 5.8919 3 6.33961 3 6.8919 C3 7.44418 3.44772 7.8919 4 7.8919H4.10069 L5.31544 19.3172 C5.47763 20.8427 6.76455 22 8.29863 22H15.7014
  1671. C17.2354 22 18.5224 20.8427 18.6846 19.3172 L19.8993 7.8919 H20 C20.5523 7.8919 21 7.44418 21 6.8919 C21 6.33961 20.5523 5.8919 20 5.8919H16.7799
  1672. M17.888 7.8919 H6.11196 L7.30423 19.1057 C7.3583 19.6142 7.78727 20 8.29863 20H15.7014 C16.2127 20 16.6417 19.6142 16.6958 19.1057 L17.888 7.8919
  1673. M10 10 C10.5523 10 11 10.4477 11 11V16 C11 16.5523 10.5523 17 10 17 C9.44772 17 9 16.5523 9 16V11 C9 10.4477 9.44772 10 10 10 M14 10
  1674. C14.5523 10 15 10.4477 15 11V16 C15 16.5523 14.5523 17 14 17 C13.4477 17 13 16.5523 13 16V11 C13 10.4477 13.4477 10 14 10
  1675. "
  1676. fill="currentColor"/>
  1677. </svg>
  1678. `;
  1679.  
  1680.  
  1681. deleteButton.addEventListener('mouseover', () => {
  1682. deleteButton.style.color = '#E3E3E3';
  1683. });
  1684.  
  1685. deleteButton.addEventListener('mouseout', () => {
  1686. deleteButton.style.color = '#B4B4B4';
  1687. });
  1688.  
  1689.  
  1690.  
  1691. deleteButton.addEventListener('click', (event) => {
  1692. event.stopPropagation();
  1693. if (confirm(`Delete profile "${profileName}"?`)) {
  1694. delete profiles[profileName];
  1695. if (currentProfile === profileName) {
  1696. currentProfile = Object.keys(profiles)[0];
  1697. loadProfileRules(currentProfile);
  1698. }
  1699. saveProfiles();
  1700. renderProfilesList();
  1701. renderPanel();
  1702. }
  1703. });
  1704. profileItem.appendChild(deleteButton);
  1705. }
  1706.  
  1707. targetList.appendChild(profileItem);
  1708. });
  1709. }
  1710.  
  1711. // Right side rules management
  1712. const rightSideContainer = document.createElement('div');
  1713. rightSideContainer.style.flex = '1';
  1714. rightSideContainer.style.display = 'flex';
  1715. rightSideContainer.style.flexDirection = 'column';
  1716. container.appendChild(rightSideContainer);
  1717.  
  1718. const rulesListContainer = document.createElement('div');
  1719. rulesListContainer.style.overflowY = 'auto';
  1720. rulesListContainer.style.height = 'calc(100% - 81px)';
  1721. rightSideContainer.appendChild(rulesListContainer);
  1722.  
  1723. const addButton = document.createElement('button');
  1724. addButton.textContent = 'Add Rule';
  1725. addButton.style.margin = '15px auto';
  1726. addButton.style.display = 'block';
  1727. addButton.style.padding = '10px';
  1728. addButton.style.width = '90%';
  1729. addButton.style.backgroundColor = 'transparent';
  1730. addButton.style.border = CONFIG.BUTTON_OUTLINE;
  1731. addButton.style.borderRadius = '50px';
  1732. addButton.style.cursor = 'pointer';
  1733.  
  1734. addButton.addEventListener('mouseenter', () => {
  1735. addButton.style.backgroundColor = CONFIG.BUTTON_HIGHLIGHT;
  1736. });
  1737.  
  1738. addButton.addEventListener('mouseleave', () => {
  1739. addButton.style.backgroundColor = 'transparent';
  1740. });
  1741.  
  1742. addButton.addEventListener('click', () => {
  1743. const newRule = `<Rule${customRules.length + 1}: Define your rule here>`;
  1744. customRules.push(newRule);
  1745. saveCurrentProfileRules();
  1746. renderPanel();
  1747. });
  1748. rightSideContainer.appendChild(addButton);
  1749.  
  1750. function saveCurrentProfileRules() {
  1751. profiles[currentProfile] = customRules;
  1752. saveProfiles();
  1753. }
  1754.  
  1755. function renderPanel() {
  1756. rulesListContainer.innerHTML = '';
  1757.  
  1758. customRules.forEach((rule, index) => {
  1759. const ruleContainer = document.createElement('div');
  1760. ruleContainer.style.marginBottom = '15px';
  1761. ruleContainer.style.display = 'flex';
  1762. ruleContainer.style.alignItems = 'center';
  1763. ruleContainer.style.width = '95%';
  1764. ruleContainer.style.gap = '5px';
  1765. ruleContainer.style.flexWrap = 'wrap';
  1766.  
  1767. const ruleLabel = document.createElement('label');
  1768. ruleLabel.textContent = `Rule ${index + 1}:`;
  1769. ruleLabel.style.color = 'white';
  1770. ruleLabel.style.flex = '0.3';
  1771. ruleContainer.appendChild(ruleLabel);
  1772.  
  1773. const ruleInput = document.createElement('textarea');
  1774. ruleInput.value = rule.replace(/<Rule\d+: ([\s\S]*?)>/, '$1');
  1775. ruleInput.style.flex = '2';
  1776. ruleInput.style.padding = '5px';
  1777. ruleInput.style.borderRadius = '5px';
  1778. ruleInput.style.border = '0.5px solid #444444';
  1779. ruleInput.style.backgroundColor = '#1E1E1E';
  1780. ruleInput.style.color = 'gray';
  1781. ruleInput.style.overflowY = 'hidden';
  1782. ruleInput.style.maxWidth = '100%';
  1783. ruleInput.style.boxSizing = 'border-box';
  1784. ruleInput.style.resize = 'none';
  1785.  
  1786. function adjustHeight(element) {
  1787. element.style.height = 'auto';
  1788. element.style.height = `${element.scrollHeight}px`;
  1789. }
  1790.  
  1791. adjustHeight(ruleInput);
  1792.  
  1793. ruleInput.addEventListener('input', () => {
  1794. adjustHeight(ruleInput);
  1795. ruleInput.style.color = '#D16262';
  1796. });
  1797.  
  1798. ruleContainer.appendChild(ruleInput);
  1799.  
  1800. const updateButton = document.createElement('button');
  1801. updateButton.style.padding = '10px';
  1802. updateButton.style.border = 'none';
  1803. updateButton.style.color = '#B4B4B4';
  1804. updateButton.style.borderRadius = '5px';
  1805. updateButton.style.cursor = 'pointer';
  1806.  
  1807. // Adding the SVG as the innerHTML of the button
  1808. updateButton.innerHTML = `
  1809. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  1810. <path fill-rule="evenodd" clip-rule="evenodd"
  1811. d="
  1812. M18.1716 1C18.702 1 19.2107 1.21071 19.5858 1.58579L22.4142 4.41421C22.7893 4.78929 23 5.29799 23 5.82843V20C23 21.6569 21.6569
  1813. 23 20 23H4C2.34315 23 1 21.6569 1 20V4C1 2.34315 2.34315 1 4 1H18.1716ZM4 3C3.44772 3 3 3.44772 3 4V20C3 20.5523 3.44772 21 4 21L5
  1814. 21L5 15C5 13.3431 6.34315 12 8 12L16 12C17.6569 12 19 13.3431 19 15V21H20C20.5523 21 21 20.5523 21 20V6.82843C21 6.29799 20.7893
  1815. 5.78929 20.4142 5.41421L18.5858 3.58579C18.2107 3.21071 17.702 3 17.1716 3H17V5C17 6.65685 15.6569 8 14 8H10C8.34315 8 7 6.65685
  1816. 7 5V3H4ZM17 21V15C17 14.4477 16.5523 14 16 14L8 14C7.44772 14 7 14.4477 7 15L7 21L17 21ZM9 3H15V5C15 5.55228 14.5523 6 14 6H10C9.44772
  1817. 6 9 5.55228 9 5V3Z
  1818. "
  1819. fill="currentColor"/>
  1820. </svg>
  1821. `;
  1822.  
  1823. updateButton.addEventListener('mouseover', () => {
  1824. updateButton.style.color = '#E3E3E3';
  1825. });
  1826.  
  1827. updateButton.addEventListener('mouseout', () => {
  1828. updateButton.style.color = '#B4B4B4';
  1829. });
  1830.  
  1831.  
  1832.  
  1833. updateButton.addEventListener('click', () => {
  1834. customRules[index] = `<Rule${index + 1}: ${ruleInput.value}>`;
  1835. saveCurrentProfileRules();
  1836. ruleInput.style.color = 'black';
  1837. });
  1838.  
  1839. ruleContainer.appendChild(updateButton);
  1840.  
  1841. const rulesDeleteButton = document.createElement('button');
  1842. rulesDeleteButton.style.cssText = 'padding: 5px 10px; margin-right: -20px; color: #B4B4B4; background-color: transparent;';
  1843. rulesDeleteButton.innerHTML = `
  1844. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon-sm">
  1845. <path fill-rule="evenodd" clip-rule="evenodd"
  1846. d="
  1847. M10.5555 4C10.099 4 9.70052 4.30906 9.58693 4.75114L9.29382 5.8919H14.715L14.4219 4.75114C14.3083 4.30906 13.9098 4 13.4533
  1848. 4H10.5555ZM16.7799 5.8919L16.3589 4.25342C16.0182 2.92719 14.8226 2 13.4533 2H10.5555C9.18616 2 7.99062 2.92719 7.64985
  1849. 4.25342L7.22886 5.8919H4C3.44772 5.8919 3 6.33961 3 6.8919C3 7.44418 3.44772 7.8919 4 7.8919H4.10069L5.31544 19.3172C5.47763
  1850. 20.8427 6.76455 22 8.29863 22H15.7014C17.2354 22 18.5224 20.8427 18.6846 19.3172L19.8993 7.8919H20C20.5523 7.8919 21 7.44418
  1851. 21 6.8919C21 6.33961 20.5523 5.8919 20 5.8919H16.7799ZM17.888 7.8919H6.11196L7.30423 19.1057C7.3583 19.6142 7.78727 20 8.29863
  1852. 20H15.7014C16.2127 20 16.6417 19.6142 16.6958 19.1057L17.888 7.8919ZM10 10C10.5523 10 11 10.4477 11 11V16C11 16.5523 10.5523
  1853. 17 10 17C9.44772 17 9 16.5523 9 16V11C9 10.4477 9.44772 10 10 10ZM14 10C14.5523 10 15 10.4477 15 11V16C15 16.5523 14.5523 17
  1854. 14 17C13.4477 17 13 16.5523 13 16V11C13 10.4477 13.4477 10 14 10Z
  1855. "
  1856. fill="currentColor"/>
  1857. </svg>
  1858. `;
  1859.  
  1860. rulesDeleteButton.addEventListener('mouseover', () => {
  1861. rulesDeleteButton.style.color = '#E3E3E3';
  1862. });
  1863.  
  1864. rulesDeleteButton.addEventListener('mouseout', () => {
  1865. rulesDeleteButton.style.color = '#B4B4B4';
  1866. });
  1867.  
  1868. rulesDeleteButton.addEventListener('click', () => {
  1869. customRules.splice(index, 1);
  1870. saveCurrentProfileRules();
  1871. renderPanel();
  1872. });
  1873.  
  1874.  
  1875. const moveContainer = document.createElement('div');
  1876. moveContainer.style.display = 'flex';
  1877. moveContainer.style.flexDirection = 'column';
  1878. moveContainer.style.gap = '3px';
  1879.  
  1880. function reindexRules() {
  1881. customRules.forEach((rule, i) => {
  1882. // Extract content from the rule text:
  1883. // rule is like "<Rule6: your text here>"
  1884. // We capture only what's after <RuleX:
  1885. const content = rule.replace(/^<Rule\d+:\s([\s\S]*?)>$/, '$1');
  1886.  
  1887. // Now rewrite it with the correct index:
  1888. customRules[i] = `<Rule${i + 1}: ${content}>`;
  1889. });
  1890. }
  1891.  
  1892.  
  1893.  
  1894. if (index > 0) {
  1895. const upButton = document.createElement('button');
  1896. upButton.textContent = '▲';
  1897. upButton.style.padding = '3px';
  1898. upButton.style.border = 'none';
  1899. upButton.style.backgroundColor = 'transparent';
  1900. upButton.style.color = '#B4B4B4';
  1901. upButton.style.borderRadius = '5px';
  1902. upButton.style.cursor = 'pointer';
  1903. upButton.addEventListener('click', () => {
  1904. if (index > 0) {
  1905. [customRules[index - 1], customRules[index]] =
  1906. [customRules[index], customRules[index - 1]];
  1907.  
  1908. reindexRules(); // ← Re-index here
  1909. saveCurrentProfileRules();
  1910. renderPanel();
  1911. }
  1912. });
  1913.  
  1914. moveContainer.appendChild(upButton);
  1915. }
  1916.  
  1917. if (index < customRules.length - 1) {
  1918. const downButton = document.createElement('button');
  1919. downButton.textContent = '▼';
  1920. downButton.style.padding = '3px';
  1921. downButton.style.border = 'none';
  1922. downButton.style.backgroundColor = 'transparent';
  1923. downButton.style.color = '#B4B4B4';
  1924. downButton.style.borderRadius = '5px';
  1925. downButton.style.cursor = 'pointer';
  1926. downButton.addEventListener('click', () => {
  1927. if (index < customRules.length - 1) {
  1928. [customRules[index], customRules[index + 1]] =
  1929. [customRules[index + 1], customRules[index]];
  1930.  
  1931. reindexRules(); // ← Re-index here
  1932. saveCurrentProfileRules();
  1933. renderPanel();
  1934. }
  1935. });
  1936.  
  1937. moveContainer.appendChild(downButton);
  1938. }
  1939.  
  1940. ruleContainer.appendChild(moveContainer);
  1941. ruleContainer.appendChild(rulesDeleteButton);
  1942. rulesListContainer.appendChild(ruleContainer);
  1943. });
  1944.  
  1945. adjustAllHeights();
  1946. }
  1947.  
  1948. function adjustAllHeights() {
  1949. const textareas = rulesListContainer.querySelectorAll('textarea');
  1950. textareas.forEach((textarea) => {
  1951. textarea.style.height = 'auto';
  1952. textarea.style.height = `${textarea.scrollHeight}px`;
  1953. });
  1954. }
  1955.  
  1956. function updateLayout() {
  1957. if (isMobile()) {
  1958. container.style.flexDirection = 'column';
  1959. rulesPanel.style.width = '90%';
  1960. rulesPanel.style.height = '90%';
  1961. rulesListContainer.style.height = 'calc(90% - 60px)';
  1962. rulesListContainer.style.overflowY = 'auto';
  1963. rulesListContainer.style.maxHeight = `${rulesPanel.clientHeight - 150}px`; // Adjust dynamically
  1964. profilesContainer.style.display = 'none';
  1965. toggleProfilesButton.style.display = 'block';
  1966. } else {
  1967. container.style.flexDirection = 'row';
  1968. rulesPanel.style.width = '1000px';
  1969. rulesPanel.style.height = '700px';
  1970. rulesListContainer.style.height = 'calc(100% - 81px)';
  1971. rulesListContainer.style.overflowY = 'auto';
  1972. profilesContainer.style.display = 'flex';
  1973. toggleProfilesButton.style.display = 'none';
  1974.  
  1975. if (profilesOverlay.style.display === 'flex') {
  1976. profilesOverlay.style.display = 'none';
  1977. }
  1978.  
  1979. if (!container.contains(profilesContainer)) {
  1980. container.insertBefore(profilesContainer, container.firstChild);
  1981. }
  1982. }
  1983. }
  1984.  
  1985.  
  1986. window.addEventListener('resize', updateLayout);
  1987.  
  1988. function openPanel() {
  1989. rulesPanel.style.display = 'block';
  1990. showOverlay();
  1991. renderProfilesList();
  1992. renderPanel();
  1993. updateLayout();
  1994. }
  1995.  
  1996. manageRulesButton.addEventListener('click', () => {
  1997. if (rulesPanel.style.display === 'none') {
  1998. openPanel();
  1999. } else {
  2000. closeRulesPanel();
  2001. }
  2002. });
  2003.  
  2004. // Initial render
  2005. renderProfilesList();
  2006.  
  2007. updateLayout();
  2008.  
  2009. }
  2010.  
  2011. // ================================================================ INJECT TEXT ==================================================================
  2012. function injectLogicGPT() {
  2013.  
  2014. // Variable in which the next rule is stored
  2015. // const getSelectedRuleValue = localStorage.getItem('selectedRuleValue');
  2016. ////////////////////////////////////////////////////////
  2017. // 1. Detecting URL changes with a simple approach
  2018. ////////////////////////////////////////////////////////
  2019.  
  2020. let previousHref = window.location.href;
  2021.  
  2022. const observerForURL = new MutationObserver(() => {
  2023. if (previousHref !== window.location.href) {
  2024. console.log("Detected a URL change from", previousHref, "to", window.location.href);
  2025. previousHref = window.location.href;
  2026. handleConversationChange();
  2027. }
  2028. });
  2029.  
  2030. // Observe changes to the entire body (you can refine if needed)
  2031. observerForURL.observe(document.body, { childList: true, subtree: true });
  2032.  
  2033. function handleConversationChange() {
  2034. // Option 1: Clear everything right away
  2035. localStorage.removeItem('lastUserMessage');
  2036. localStorage.removeItem('lastGPTMessage');
  2037. localStorage.removeItem('lastTenMessages');
  2038. localStorage.removeItem('lastTenGPTMessages');
  2039. localStorage.removeItem('lastThreeMessages');
  2040. localStorage.removeItem('lastThreeGPTMessages');
  2041.  
  2042. // Option 2: Or check if conversation is actually empty first
  2043. clearStorageIfNoMessages();
  2044.  
  2045. // Possibly re-initialize observers or do any other steps
  2046. // setupMessageObservers(); // If needed, depends on your code’s lifecycle
  2047. }
  2048.  
  2049. ////////////////////////////////////////////////////////
  2050. // 2. A helper that clears localStorage if conversation is empty
  2051. ////////////////////////////////////////////////////////
  2052.  
  2053. function clearStorageIfNoMessages() {
  2054. const userMessages = document.querySelectorAll('.whitespace-pre-wrap');
  2055. const gptMessages = document.querySelectorAll('div.markdown.prose.w-full.break-words.dark\\:prose-invert');
  2056.  
  2057. if (userMessages.length === 0 && gptMessages.length === 0) {
  2058. console.log("Conversation is empty — clearing localStorage for last messages.");
  2059. localStorage.removeItem('lastUserMessage');
  2060. localStorage.removeItem('lastGPTMessage');
  2061. localStorage.removeItem('lastTenMessages');
  2062. localStorage.removeItem('lastTenGPTMessages');
  2063. localStorage.removeItem('lastThreeMessages');
  2064. localStorage.removeItem('lastThreeGPTMessages');
  2065. }
  2066. }
  2067.  
  2068. // ... your existing code below ...
  2069.  
  2070.  
  2071. // DETECTING INPUT PANEL
  2072. function detectInputPanelGPT(callback) {
  2073. const maxRetries = 10;
  2074. const retryInterval = 500;
  2075. let retryCount = 0;
  2076. window.inputPanelGPT = document.querySelector('div#prompt-textarea.ProseMirror');
  2077.  
  2078. if (!inputPanelGPT) {
  2079. console.error('Textarea panel not found.');
  2080. if (retryCount < maxRetries) {
  2081. retryCount++;
  2082. console.log(`Retrying... Attempt ${retryCount}`);
  2083. setTimeout(() => detectInputPanelGPT(callback), retryInterval);
  2084. } else {
  2085. console.error('Max retries reached. Cannot find the textarea.');
  2086. callback(false, null); // Pass null for inputPanelGPT on failure
  2087. }
  2088. return;
  2089. }
  2090.  
  2091. console.log('Textarea found.');
  2092. callback(true, inputPanelGPT);
  2093. }
  2094.  
  2095.  
  2096.  
  2097. detectInputPanelGPT(function(success, inputPanelGPT) {
  2098. if (success) {
  2099. console.log('Textarea is ready to listen for events.');
  2100. } else {
  2101. console.error('Cannot proceed as textarea was not found.');
  2102. }
  2103. });
  2104.  
  2105. function storeInputText() {
  2106. // Retrieve the input panel
  2107. const inputPanel = document.querySelector('div#prompt-textarea.ProseMirror');
  2108.  
  2109. // If the input panel exists
  2110. if (inputPanel) {
  2111. // Retrieve the current text from the input panel
  2112. const currentText = inputPanel.innerText || inputPanel.textContent || '';
  2113.  
  2114. // Store the text in localStorage with the key 'currentInputText'
  2115. localStorage.setItem('currentInputText', currentText);
  2116.  
  2117. console.log('Text saved to localStorage:', currentText);
  2118. } else {
  2119. console.error('Input panel not found. Cannot save text.');
  2120. }
  2121. }
  2122.  
  2123. // Attach the function to the window object to make it globally accessible
  2124. window.storeInputText = storeInputText;
  2125.  
  2126.  
  2127. // Utility function to debounce rapid function calls
  2128. function debounce(func, wait) {
  2129. let timeout;
  2130. return function(...args) {
  2131. clearTimeout(timeout);
  2132. timeout = setTimeout(() => func.apply(this, args), wait);
  2133. };
  2134. }
  2135.  
  2136. // Function to retrieve and store messages
  2137. function retrieveAndStoreMessages(type) {
  2138. const selector = type === 'user'
  2139. ? '.whitespace-pre-wrap'
  2140. : 'div.markdown.prose.w-full.break-words.dark\\:prose-invert';
  2141. const storageKey = type === 'user' ? 'lastUserMessage' : 'lastGPTMessage';
  2142. const storageKeyTen = type === 'user' ? 'lastTenMessages' : 'lastTenGPTMessages';
  2143. const storageKeyThree = type === 'user' ? 'lastThreeMessages' : 'lastThreeGPTMessages';
  2144.  
  2145. const messageElementArray = Array.from(document.querySelectorAll(selector));
  2146. if (messageElementArray.length > 0) {
  2147. const lastMessage = messageElementArray.slice(-1)[0].textContent.trim();
  2148. console.log(`Last ${type === 'user' ? 'User' : 'GPT'} Message:`, lastMessage);
  2149. localStorage.setItem(storageKey, lastMessage);
  2150.  
  2151. const lastTenMessages = messageElementArray.slice(-10).map(element => element.textContent.trim());
  2152. console.log(`Last 10 ${type === 'user' ? 'User' : 'GPT'} Messages:`, lastTenMessages);
  2153. localStorage.setItem(storageKeyTen, JSON.stringify(lastTenMessages));
  2154.  
  2155. const lastThreeMessages = messageElementArray.slice(-2).map(element => element.textContent.trim());
  2156. console.log(`Last 2 ${type === 'user' ? 'User' : 'GPT'} Messages:`, lastThreeMessages);
  2157. localStorage.setItem(storageKeyThree, JSON.stringify(lastThreeMessages));
  2158. }
  2159. }
  2160.  
  2161. // Main function to set up observers for user and GPT messages
  2162. function setupMessageObservers() {
  2163. const userSelector = '.whitespace-pre-wrap';
  2164. const gptSelector = 'div.markdown.prose.w-full.break-words.dark\\:prose-invert';
  2165.  
  2166. const observerConfig = { childList: true, subtree: true };
  2167.  
  2168. const debouncedUserHandler = debounce(() => retrieveAndStoreMessages('user'), 500);
  2169. const debouncedGPTHandler = debounce(() => retrieveAndStoreMessages('gpt'), 500);
  2170.  
  2171. const observer = new MutationObserver((mutations) => {
  2172. for (const mutation of mutations) {
  2173. if (mutation.type === 'childList') {
  2174. mutation.addedNodes.forEach(node => {
  2175. if (node.nodeType === Node.ELEMENT_NODE) {
  2176. if (node.matches(userSelector) || node.querySelector(userSelector)) {
  2177. debouncedUserHandler();
  2178. }
  2179. if (node.matches(gptSelector) || node.querySelector(gptSelector)) {
  2180. debouncedGPTHandler();
  2181. }
  2182. }
  2183. });
  2184. }
  2185. }
  2186. });
  2187.  
  2188. observer.observe(document.body, observerConfig);
  2189. console.log("MutationObserver set up for user and GPT messages.");
  2190. }
  2191.  
  2192. // Function to retrieve the last message of a given type
  2193. async function getLastMessages(type) {
  2194. const selector = type === 'user'
  2195. ? '.whitespace-pre-wrap'
  2196. : 'div.markdown.prose.w-full.break-words.dark\\:prose-invert';
  2197. const messageElementArray = Array.from(document.querySelectorAll(selector));
  2198.  
  2199. if (messageElementArray.length > 0) {
  2200. const lastMessage = messageElementArray.slice(-1)[0].textContent.trim();
  2201. return lastMessage;
  2202. } else {
  2203. // Instead of rejecting, return null to indicate absence
  2204. return null;
  2205. }
  2206. }
  2207.  
  2208. // Initialize observers
  2209. setupMessageObservers();
  2210.  
  2211. // Function to handle message retrieval and related logic
  2212. async function handleMessages() {
  2213. const lastUserMessage = await getLastMessages('user');
  2214. if (lastUserMessage) {
  2215. console.log("Retrieved last user message:", lastUserMessage);
  2216. } else {
  2217. console.info("No user messages found at this time.");
  2218. // Optionally, you can reset rules here if necessary
  2219. const getRulesState = localStorage.getItem('enableRules');
  2220. if (getRulesState === "true") {
  2221. const selectedProfileRules = localStorage.getItem('selectedProfile.Rules');
  2222. const rulesProfiles = JSON.parse(localStorage.getItem('rulesProfiles'));
  2223. const selectedRules = rulesProfiles[selectedProfileRules];
  2224. console.log(`Stored selectedRules: ${selectedRules}`);
  2225. const initNextRule = 'Rule1';
  2226. localStorage.setItem('nextRule', initNextRule);
  2227. localStorage.setItem('previousRule', '0');
  2228. const initNextRuleValue = selectedRules.find(rule => rule.startsWith(`<${initNextRule}:`));
  2229. localStorage.setItem('selectedRuleValue', initNextRuleValue);
  2230. console.log(`Initial stored selectedRuleValue: ${initNextRuleValue}`);
  2231. }
  2232. }
  2233.  
  2234. const lastGPTMessage = await getLastMessages('gpt');
  2235. if (lastGPTMessage) {
  2236. console.log("Retrieved last GPT message:", lastGPTMessage);
  2237. } else {
  2238. console.info("No GPT messages found at this time.");
  2239. }
  2240. }
  2241.  
  2242. // Optional: Remove the initial handleMessages call to prevent errors on load
  2243. // If you prefer to handle existing messages on load, you can keep it with adjusted error handling
  2244. // handleMessages();
  2245.  
  2246. // Rely on MutationObserver to handle messages as they arrive
  2247.  
  2248. // ================================================================ RULES LOGIC ==================================================================
  2249.  
  2250. function rulesLogic() {
  2251. // CHECKING IF THE MODULES ARE ENABLED
  2252.  
  2253. const selectedProfileRules = localStorage.getItem('selectedProfile.Rules');
  2254. const rulesProfiles = JSON.parse(localStorage.getItem('rulesProfiles'));
  2255. const selectedRules = rulesProfiles[selectedProfileRules];
  2256. console.log(`Stored selectedRules: ${selectedRules}`);
  2257. const initNextRule = 'Rule1';
  2258. localStorage.setItem('nextRule', initNextRule);
  2259. const initPreviousRule = localStorage.setItem('previousRule', '0');
  2260. const initNextRuleValue = selectedRules.find(rule => rule.startsWith(`<${initNextRule}:`));
  2261. localStorage.setItem('selectedRuleValue', initNextRuleValue);
  2262. console.log(`Initial stored selectedRuleValue: ${initNextRuleValue}`);
  2263. const getRulesState = localStorage.getItem('enableRules');
  2264. if (getRulesState === "true") {
  2265. // RETRIEVING THE LAST USER MESSAGE
  2266. const lastUserMessage = localStorage.getItem('lastUserMessage');
  2267.  
  2268. if (lastUserMessage) {
  2269. // MATCHING RULES IN THE MESSAGE
  2270. const ruleMatches = lastUserMessage.match(/<Rule\d+/g);
  2271. console.log('TRYING TO MATCH RULES');
  2272. if (ruleMatches) {
  2273. console.log('Found rules:', ruleMatches);
  2274.  
  2275. // STORING THE LAST MATCHED RULE AS "previousRule"
  2276. const previousRule = ruleMatches[ruleMatches.length - 1].replace('<', '');
  2277. localStorage.setItem('previousRule', previousRule);
  2278.  
  2279. // CALCULATING AND STORING THE NEXT RULE
  2280. const ruleNumber = parseInt(previousRule.replace('Rule', ''), 10);
  2281. const nextRuleNumber = (ruleNumber % selectedRules.length) + 1; // Wrap around using modulus
  2282. const nextRule = `Rule${nextRuleNumber}`;
  2283. localStorage.setItem('nextRule', nextRule);
  2284.  
  2285. console.log(`Stored previousRule: ${previousRule}`);
  2286. console.log(`Stored nextRule: ${nextRule}`);
  2287.  
  2288. const nextRuleValue = selectedRules.find(rule => rule.startsWith(`<${nextRule}:`));
  2289. localStorage.setItem('selectedRuleValue', nextRuleValue);
  2290. console.log(`Stored selectedRuleValue: ${nextRuleValue}`);
  2291. } else {
  2292.  
  2293. console.log('No rules found in the last user message.');
  2294. }
  2295. } else {
  2296. console.log('No last user message found in localStorage.');
  2297. }
  2298. } else {
  2299. console.log('Rules module is disabled.');
  2300. }
  2301. }
  2302.  
  2303. function observeLocalStorageKeyChange(key, callback) {
  2304. let previousValue = localStorage.getItem(key);
  2305.  
  2306. setInterval(() => {
  2307. const currentValue = localStorage.getItem(key);
  2308. if (currentValue !== previousValue) {
  2309. previousValue = currentValue;
  2310. callback(currentValue);
  2311. }
  2312. }, 100); // Adjust the interval as needed
  2313. }
  2314.  
  2315. // Set up the observer for 'lastUserMessage'
  2316. observeLocalStorageKeyChange('lastUserMessage', (newValue) => {
  2317. console.log(`Detected change in 'lastUserMessage': ${newValue}`);
  2318. rulesLogic();
  2319. });
  2320.  
  2321.  
  2322. rulesLogic();
  2323.  
  2324.  
  2325. // ============================================================== LOREBOOK LOGIC =================================================================
  2326. function lorebookLogic() {
  2327. // Getting the relevant lorebook entries and ensuring it's already an array
  2328. const selectedLorebookProfile = localStorage.getItem('selectedProfile.lorebook');
  2329. const lorebookEntries = JSON.parse(localStorage.getItem('lorebookEntries')) || {};
  2330.  
  2331. // Retrieve selected entries as an array
  2332. const selectedLorebookEntries = lorebookEntries[selectedLorebookProfile] || [];
  2333. console.log(`Stored selectedLorebookEntries:`, selectedLorebookEntries);
  2334.  
  2335. // Extract all possible keywords from the lorebook entries
  2336. const keywords = extractKeywords(selectedLorebookEntries);
  2337. console.log(`Extracted Keywords:`, keywords);
  2338.  
  2339. // Get current input text and last GPT message
  2340. const textareaContent = localStorage.getItem('currentInputText') || '';
  2341. const lastGPTMessage = localStorage.getItem('lastGPTMessage') || '';
  2342.  
  2343. // Retrieve the last ten messages for bracketed-exclusion scanning
  2344. const lastTenMessages = JSON.parse(localStorage.getItem('lastTenMessages')) || [];
  2345. console.log(`Last Ten Messages:`, lastTenMessages);
  2346.  
  2347. // Retrieve the last ten GPT messages if needed
  2348. const lastTenGPTMessages = JSON.parse(localStorage.getItem('lastTenGPTMessages')) || [];
  2349.  
  2350. // Retrieve the last three messages for unbracketed detection
  2351. const lastThreeMessages = JSON.parse(localStorage.getItem('lastThreeMessages')) || [];
  2352. const lastThreeGPTMessages = JSON.parse(localStorage.getItem('lastThreeGPTMessages')) || [];
  2353.  
  2354. // Combine content (textarea + last GPT message) if there's a last GPT message
  2355. let combinedContent;
  2356. if (!lastGPTMessage && lastTenMessages.length === 0) {
  2357. combinedContent = textareaContent;
  2358. } else {
  2359. combinedContent = `${textareaContent} ${lastGPTMessage}`;
  2360. }
  2361. console.log(`Combined Content: ${combinedContent}`);
  2362.  
  2363. // 1) Determine excluded keywords from the last ten messages (i.e. bracketed usage)
  2364. const excludedKeywords = parseExcludedKeywords(lastTenMessages);
  2365. console.log(`Excluded Keywords (found in <[Lorebook: ...]>):`, excludedKeywords);
  2366.  
  2367. // 2) Find matching keywords in the user+GPT combined content (partial match)
  2368. let matchingKeywords = findMatchingKeywords(keywords, combinedContent);
  2369.  
  2370. // 3) Grab any new “outside-of-brackets” keywords from the last three messages + last three GPT messages
  2371. const unbracketedKeywords = parseKeywordsOutsideBrackets(
  2372. keywords,
  2373. lastThreeMessages,
  2374. lastThreeGPTMessages,
  2375. excludedKeywords
  2376. );
  2377.  
  2378. // Merge those unbracketed keywords with the ones found in combinedContent
  2379. // but only keep those which are NOT in excludedKeywords.
  2380. const finalNewKeywords = unbracketedKeywords.filter(k => !excludedKeywords.has(k));
  2381. console.log(`Additional Unbracketed Keywords to Add:`, finalNewKeywords);
  2382.  
  2383. matchingKeywords = [...new Set([...matchingKeywords, ...finalNewKeywords])];
  2384. console.log(`All Matching Keywords Before Final Exclusion:`, matchingKeywords);
  2385.  
  2386. // 4) Exclude bracketed keywords from the final list
  2387. matchingKeywords = matchingKeywords.filter(k => !excludedKeywords.has(k));
  2388. console.log(`Matching Keywords After Exclusion:`, matchingKeywords);
  2389.  
  2390. // If we have matches, display the corresponding lorebook entries in batches of 3
  2391. if (matchingKeywords.length > 0) {
  2392. outputLorebookEntries(selectedLorebookEntries, matchingKeywords, 3);
  2393. } else {
  2394. // Clear out the localStorage entry if none remain
  2395. localStorage.setItem('selectedLorebookValues', JSON.stringify([]));
  2396. }
  2397. }
  2398.  
  2399. // -----------------------------------------------------------------------
  2400. // Function to extract keywords from the lorebook entries
  2401. function extractKeywords(lorebookEntries) {
  2402. const keywords = [];
  2403. const entryPattern = /Lorebook:\s*([^\]>]+(?:\s*,\s*[^\]>]+)*)/g;
  2404. let match;
  2405.  
  2406. lorebookEntries.forEach(entry => {
  2407. while ((match = entryPattern.exec(entry)) !== null) {
  2408. const entryKeywords = match[1].split(',').map(keyword => keyword.trim());
  2409. keywords.push(...entryKeywords);
  2410. }
  2411. });
  2412.  
  2413. return keywords;
  2414. }
  2415.  
  2416. // -----------------------------------------------------------------------
  2417. // -----------------------------------------------------------------------
  2418. // Function to parse lastTenMessages for <[Lorebook: ...]> (excluded keys)
  2419. function parseExcludedKeywords(lastTenMessages) {
  2420. const excludedKeywords = new Set();
  2421. // matches <[Lorebook: ...]>
  2422. const pattern = /<\[Lorebook:\s*([^\]]+)\]/gi;
  2423.  
  2424. lastTenMessages.forEach(message => {
  2425. let match;
  2426. while ((match = pattern.exec(message)) !== null) {
  2427. const foundKeywordString = match[1].trim();
  2428. const foundKeywords = foundKeywordString.split(',').map(k => k.trim());
  2429. foundKeywords.forEach(k => excludedKeywords.add(k));
  2430. }
  2431. });
  2432.  
  2433. return excludedKeywords;
  2434. }
  2435.  
  2436. // -----------------------------------------------------------------------
  2437. // Function to find matching keywords in the combined text (PARTIAL MATCH),
  2438. // ignoring anything inside <[ ... ]> so that "description text" is not scanned.
  2439. function findMatchingKeywords(keywords, content) {
  2440. // 1) Strip out bracketed segments
  2441. const strippedContent = content.replace(/<\[[^\]]*\][^>]*>/g, '');
  2442.  
  2443. const matchingKeywords = [];
  2444. keywords.forEach(keyword => {
  2445. // Partial, case-insensitive match
  2446. const regex = new RegExp(`\\b${keyword}\\b`, 'i');
  2447. if (regex.test(strippedContent)) {
  2448. matchingKeywords.push(keyword);
  2449. }
  2450. });
  2451. return matchingKeywords;
  2452. }
  2453.  
  2454. // -----------------------------------------------------------------------
  2455. // Same approach for the last three messages scanning outside brackets
  2456. function parseKeywordsOutsideBrackets(keywords, lastThreeMessages, lastThreeGPTMessages, excludedKeywords) {
  2457. const foundKeywords = new Set();
  2458.  
  2459. function stripBracketedSegments(msg) {
  2460. return msg.replace(/<\[[^\]]*\][^>]*>/g, '');
  2461. }
  2462.  
  2463. const allMessages = [...lastThreeMessages, ...lastThreeGPTMessages];
  2464.  
  2465. allMessages.forEach(message => {
  2466. const strippedMessage = stripBracketedSegments(message);
  2467.  
  2468. keywords.forEach(keyword => {
  2469. if (excludedKeywords.has(keyword) || foundKeywords.has(keyword)) return;
  2470. const regex = new RegExp(`\\b${keyword}\\b`, 'i');
  2471. if (regex.test(strippedMessage)) {
  2472. foundKeywords.add(keyword);
  2473. }
  2474. });
  2475. });
  2476.  
  2477. return [...foundKeywords].filter(k => !excludedKeywords.has(k));
  2478. }
  2479.  
  2480. // -----------------------------------------------------------------------
  2481. // Helper to parse the lorebook entries into structured objects
  2482. function parseLorebookEntries(lorebookEntries) {
  2483. const entryPattern = /Lorebook:\s*([^\]>]+(?:\s*,\s*[^\]>]+)*)/;
  2484.  
  2485. return lorebookEntries.map(entry => {
  2486. const match = entry.match(entryPattern);
  2487. if (!match) return { text: entry, keywords: [] };
  2488. const entryKeywords = match[1].split(',').map(k => k.trim());
  2489. return { text: entry, keywords: entryKeywords };
  2490. });
  2491. }
  2492.  
  2493. // -----------------------------------------------------------------------
  2494. // Show the matching lorebook entries in batches (limit=3).
  2495. function outputLorebookEntries(lorebookEntries, matchingKeywords, limit = 3) {
  2496. const parsedEntries = parseLorebookEntries(lorebookEntries);
  2497.  
  2498. const uniqueEntries = new Set();
  2499. const entriesToDisplay = [];
  2500.  
  2501. // For each keyword, see which parsed entries contain it
  2502. for (const keyword of matchingKeywords) {
  2503. const matches = parsedEntries.filter(e => e.keywords.includes(keyword));
  2504. for (const entryObj of matches) {
  2505. if (!uniqueEntries.has(entryObj.text)) {
  2506. uniqueEntries.add(entryObj.text);
  2507. entriesToDisplay.push(entryObj.text);
  2508. if (entriesToDisplay.length === limit) {
  2509. break;
  2510. }
  2511. }
  2512. }
  2513. if (entriesToDisplay.length === limit) {
  2514. break;
  2515. }
  2516. }
  2517.  
  2518. console.log(`Matching Lorebook Entries:`, entriesToDisplay);
  2519. localStorage.setItem('selectedLorebookValues', JSON.stringify(entriesToDisplay));
  2520. }
  2521.  
  2522. // Initialize the lorebook logic
  2523. window.lorebookLogic = lorebookLogic;
  2524. lorebookLogic();
  2525. }
  2526. // ================================================================ TEXT COLORS ==================================================================
  2527. function colorsScript() {
  2528.  
  2529. const defaultColors = {
  2530. textColor: '#A2A2AC',
  2531. emColor: '#E0DF7F',
  2532. bracketHighlight: '#737373',
  2533. whiteHighlight: '#FFFFFF'
  2534. };
  2535.  
  2536. // Load colors from localStorage or use default colors
  2537. function loadColors() {
  2538. return {
  2539. textColor: localStorage.getItem('textColor') || defaultColors.textColor,
  2540. emColor: localStorage.getItem('emColor') || defaultColors.emColor,
  2541. bracketHighlight: localStorage.getItem('bracketHighlight') || defaultColors.bracketHighlight,
  2542. whiteHighlight: localStorage.getItem('whiteHighlight') || defaultColors.whiteHighlight
  2543. };
  2544. }
  2545.  
  2546. // Inject styles based on the loaded colors
  2547. function injectStyles(colors) {
  2548. const styleId = 'colorizer-styles';
  2549. let styleElement = document.getElementById(styleId);
  2550.  
  2551. if (!styleElement) {
  2552. styleElement = document.createElement('style');
  2553. styleElement.id = styleId;
  2554. document.head.appendChild(styleElement);
  2555. }
  2556.  
  2557. styleElement.textContent = `
  2558. p, div.whitespace-pre-wrap {
  2559. color: ${colors.textColor};
  2560. }
  2561. em {
  2562. color: ${colors.emColor} !important;
  2563. font-weight: normal !important;
  2564. }
  2565. .highlight-blue { color: ${colors.bracketHighlight} !important; }
  2566. .highlight-white { color: ${colors.whiteHighlight} !important; }
  2567.  
  2568. .highlight-blue * { color: inherit !important; }
  2569. .highlight-white * { color: inherit !important; }
  2570. `;
  2571. }
  2572.  
  2573. // Create the color picker panel UI
  2574. function createColorPickerPanel() {
  2575. const colors = loadColors();
  2576.  
  2577. const colorPanel = document.createElement('div');
  2578. colorPanel.id = 'color-picker-panel';
  2579. colorPanel.style.position = 'fixed';
  2580. // Center the panel
  2581. colorPanel.style.top = '50%';
  2582. colorPanel.style.left = '50%';
  2583. colorPanel.style.transform = 'translate(-50%, -50%)';
  2584. colorPanel.style.padding = '30px';
  2585. colorPanel.style.background = '#2F2F2F';
  2586. colorPanel.style.color = '#f8f8f8';
  2587. colorPanel.style.borderRadius = '20px';
  2588. colorPanel.style.zIndex = '10000';
  2589. colorPanel.style.display = 'none';
  2590. colorPanel.style.width = '280px';
  2591.  
  2592. colorPanel.innerHTML = `
  2593. <h3 style="margin-top: 0px; margin-bottom: 20px; text-align: center; color: #f8f8f8;">Text Colors</h3>
  2594. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; width: 90%; margin-left: 10px;">
  2595. <label style="margin-right: 10px;">Standard Color:</label>
  2596. <input type="color" id="textColor" value="${colors.textColor}" style="width: 50px; height: 30px; border: none; cursor: pointer;">
  2597. </div>
  2598. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; width: 90%; margin-left: 10px;">
  2599. <label style="margin-right: 10px;">Italic:</label>
  2600. <input type="color" id="emColor" value="${colors.emColor}" style="width: 50px; height: 30px; border: none; cursor: pointer;">
  2601. </div>
  2602. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; width: 90%; margin-left: 10px;">
  2603. <label style="margin-right: 10px;">Parentheses:</label>
  2604. <input type="color" id="bracketHighlight" value="${colors.bracketHighlight}" style="width: 50px; height: 30px; border: none; cursor: pointer;">
  2605. </div>
  2606. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; width: 90%; margin-left: 10px;">
  2607. <label style="margin-right: 10px;">Quotes:</label>
  2608. <input type="color" id="whiteHighlight" value="${colors.whiteHighlight}" style="width: 50px; height: 30px; border: none; cursor: pointer;">
  2609. </div>
  2610. <div style="text-align: center; margin-top: 25px;">
  2611. <button id="saveColors" style="padding: 8px 15px; background-color: transparent; width: 100%; color: #f8f8f8; border: 1px solid #4E4E4E; border-radius: 50px; cursor: pointer;">Save</button>
  2612. </div>
  2613. `;
  2614.  
  2615. document.body.appendChild(colorPanel);
  2616.  
  2617. // Save button hover effect
  2618. const saveColorsHighlight = document.getElementById('saveColors');
  2619. saveColorsHighlight.addEventListener('mouseenter', () => {
  2620. saveColorsHighlight.style.backgroundColor = CONFIG.BUTTON_HIGHLIGHT;
  2621. });
  2622. saveColorsHighlight.addEventListener('mouseleave', () => {
  2623. saveColorsHighlight.style.backgroundColor = 'transparent';
  2624. });
  2625.  
  2626. // Close button
  2627. const colorsCloseButton = document.createElement('button');
  2628. colorsCloseButton.style.position = 'absolute';
  2629. colorsCloseButton.style.borderRadius = '50%';
  2630. colorsCloseButton.style.top = '20px';
  2631. colorsCloseButton.style.right = '20px';
  2632. colorsCloseButton.style.backgroundColor = 'transparent';
  2633. colorsCloseButton.style.cursor = 'pointer';
  2634. colorsCloseButton.style.width = '32px';
  2635. colorsCloseButton.style.height = '32px';
  2636. colorsCloseButton.style.display = 'flex';
  2637. colorsCloseButton.style.alignItems = 'center';
  2638. colorsCloseButton.style.justifyContent = 'center';
  2639.  
  2640. const colorsCloseSymbol = document.createElement('span');
  2641. colorsCloseSymbol.innerText = '✕';
  2642. colorsCloseSymbol.style.color = '#FBFBFE';
  2643. colorsCloseSymbol.style.fontSize = '14px';
  2644. colorsCloseSymbol.style.fontWeight = '550';
  2645. colorsCloseSymbol.style.transform = 'translateY(-1px) translateX(0.4px)';
  2646.  
  2647. colorsCloseButton.appendChild(colorsCloseSymbol);
  2648.  
  2649. colorsCloseButton.addEventListener('mouseenter', () => {
  2650. colorsCloseButton.style.backgroundColor = '#676767';
  2651. });
  2652. colorsCloseButton.addEventListener('mouseleave', () => {
  2653. colorsCloseButton.style.backgroundColor = 'transparent';
  2654. });
  2655.  
  2656. const closeColorsPanel = () => {
  2657. colorPanel.style.display = 'none';
  2658. hideOverlay();
  2659. };
  2660. colorsCloseButton.addEventListener('click', closeColorsPanel);
  2661. colorPanel.appendChild(colorsCloseButton);
  2662. }
  2663.  
  2664. // Create the manage colors button
  2665. window.colorButton = document.createElement('div');
  2666. window.colorButton.innerHTML = `
  2667. <button id="toggle-color-panel"
  2668. style="
  2669. position: relative;
  2670. top: 10px;
  2671. right: 0px;
  2672. left: 10px;
  2673. padding: 7px 15px;
  2674. background: transparent;
  2675. color: #b0b0b0;
  2676. border: none;
  2677. border-radius: 8px;
  2678. font-size: 16px;
  2679. text-align: left;
  2680. cursor: pointer;
  2681. width: 90%;
  2682. display: flex;
  2683. align-items: center;
  2684. gap: 10px;
  2685. transition: background-color 0.1s, color 0.1s;
  2686. z-index: 1001;">
  2687. <svg fill="#B0B0B0" viewBox="0 0 32 32" width="16px" height="16px" xmlns="http://www.w3.org/2000/svg">
  2688. <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
  2689. <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
  2690. <g id="SVGRepo_iconCarrier">
  2691. <title>brush</title>
  2692. <path d="M27.555 8.42c-1.355 1.647-5.070 6.195-8.021 9.81l-3.747-3.804c3.389-3.016 7.584-6.744 9.1-8.079 2.697-2.377 5.062-3.791
  2693. 5.576-3.213 0.322 0.32-0.533 2.396-2.908 5.286zM18.879 19.030c-1.143 1.399-2.127 2.604-2.729 3.343l-4.436-4.323c0.719-0.64
  2694. 1.916-1.705 3.304-2.939l3.861 3.919zM15.489 23.183v-0.012c-2.575 9.88-14.018 4.2-14.018 4.2s4.801 0.605 4.801-3.873c0-4.341
  2695. 4.412-4.733 4.683-4.753l4.543 4.427c0 0.001-0.009 0.011-0.009 0.011z"></path>
  2696. </g>
  2697. </svg>
  2698. Manage Colors
  2699. </button>
  2700. `;
  2701.  
  2702. const colorButtonElement = window.colorButton.querySelector('button');
  2703. colorButtonElement.onmouseover = () => {
  2704. colorButtonElement.style.backgroundColor = '#212121';
  2705. colorButtonElement.style.color = '#ffffff';
  2706. };
  2707. colorButtonElement.onmouseout = () => {
  2708. colorButtonElement.style.backgroundColor = 'transparent';
  2709. colorButtonElement.style.color = '#b0b0b0';
  2710. };
  2711.  
  2712. // Toggle color panel
  2713. window.colorButton.addEventListener('click', () => {
  2714. const colorPanel = document.getElementById('color-picker-panel');
  2715. if (colorPanel) {
  2716. showOverlay();
  2717. colorPanel.style.display = (colorPanel.style.display === 'none' ? 'block' : 'none');
  2718. }
  2719. });
  2720.  
  2721. // Save the selected colors to localStorage and update styles
  2722. function saveColors() {
  2723. const textColor = document.getElementById('textColor').value;
  2724. const emColor = document.getElementById('emColor').value;
  2725. const bracketHighlight = document.getElementById('bracketHighlight').value;
  2726. const whiteHighlight = document.getElementById('whiteHighlight').value;
  2727.  
  2728. localStorage.setItem('textColor', textColor);
  2729. localStorage.setItem('emColor', emColor);
  2730. localStorage.setItem('bracketHighlight', bracketHighlight);
  2731. localStorage.setItem('whiteHighlight', whiteHighlight);
  2732.  
  2733. injectStyles(loadColors());
  2734.  
  2735. // Re-highlight all existing messages to apply new colors
  2736. processExistingMessages();
  2737.  
  2738. showTemporaryNotification('Colors saved successfully!', 2000);
  2739. }
  2740.  
  2741. // Temporary notification
  2742. function showTemporaryNotification(message, duration) {
  2743. const notification = document.createElement('div');
  2744. notification.textContent = message;
  2745. notification.style.position = 'fixed';
  2746. notification.style.bottom = '20px';
  2747. notification.style.right = '20px';
  2748. notification.style.padding = '10px 20px';
  2749. notification.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
  2750. notification.style.color = '#fff';
  2751. notification.style.borderRadius = '5px';
  2752. notification.style.zIndex = '10001';
  2753. notification.style.opacity = '0';
  2754. notification.style.transition = 'opacity 0.5s';
  2755.  
  2756. document.body.appendChild(notification);
  2757.  
  2758. // Trigger reflow to apply transition
  2759. void notification.offsetWidth;
  2760. notification.style.opacity = '1';
  2761.  
  2762. setTimeout(() => {
  2763. notification.style.opacity = '0';
  2764. setTimeout(() => {
  2765. document.body.removeChild(notification);
  2766. }, 500);
  2767. }, duration);
  2768. }
  2769.  
  2770. // --- MAIN HIGHLIGHT LOGIC ---
  2771.  
  2772. // Replaces parentheses/quotes in all text nodes under the given container
  2773. function highlightMessageContent(container) {
  2774. // Process parentheses
  2775. safelyColorText(container, /\(([^)]+)\)/g, 'highlight-blue');
  2776. // Process quotes
  2777. safelyColorText(container, /["“](.*?)["”]/g, 'highlight-white');
  2778. }
  2779.  
  2780. function safelyColorText(container, regex, highlightClass) {
  2781. const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null, false);
  2782. let node;
  2783.  
  2784. while ((node = walker.nextNode())) {
  2785. // Skip text that’s within input or already in a highlight
  2786. if (
  2787. node.parentElement.closest('#prompt-textarea') ||
  2788. node.parentElement.closest('.prosemirror-editor-container') ||
  2789. node.parentElement.closest('#composer-background') ||
  2790. node.parentElement.closest('[contenteditable="true"]') ||
  2791. node.parentElement.closest(`.${highlightClass}`)
  2792. ) {
  2793. continue;
  2794. }
  2795.  
  2796. const text = node.textContent;
  2797. if (!text) continue;
  2798.  
  2799. // Replace curly quotes in the raw text node first
  2800. replaceQuotes(node);
  2801.  
  2802. // Now run the highlight
  2803. let newHTML = '';
  2804. let lastIndex = 0;
  2805. regex.lastIndex = 0; // always reset
  2806.  
  2807. let match;
  2808. while ((match = regex.exec(text)) !== null) {
  2809. newHTML += escapeHTML(text.substring(lastIndex, match.index));
  2810. newHTML += `<span class="${highlightClass}">${escapeHTML(match[0])}</span>`;
  2811. lastIndex = regex.lastIndex;
  2812. }
  2813. newHTML += escapeHTML(text.substring(lastIndex));
  2814.  
  2815. // If there's a difference, replace the node
  2816. if (newHTML !== escapeHTML(text)) {
  2817. const tempSpan = document.createElement('span');
  2818. tempSpan.innerHTML = newHTML;
  2819. node.parentNode.replaceChild(tempSpan, node);
  2820. }
  2821. }
  2822. }
  2823.  
  2824. function replaceQuotes(node) {
  2825. // If it's a text node, do a direct replacement
  2826. if (node.nodeType === Node.TEXT_NODE) {
  2827. node.nodeValue = node.nodeValue
  2828. .replace(/[\u2018\u2019\u201A\u201B]/g, "'")
  2829. .replace(/[\u201C\u201D\u201E\u201F]/g, '"');
  2830. }
  2831. }
  2832.  
  2833. function escapeHTML(str) {
  2834. return str
  2835. .replace(/&/g, "&amp;")
  2836. .replace(/</g, "&lt;")
  2837. .replace(/>/g, "&gt;")
  2838. .replace(/"/g, "&quot;")
  2839. .replace(/'/g, "&#039;");
  2840. }
  2841.  
  2842. // Re-highlight all relevant message blocks
  2843. function processExistingMessages() {
  2844. // Grab paragraphs, pre-wrap divs, typical markdown blocks, etc.
  2845. const messages = document.querySelectorAll(
  2846. 'p, div.whitespace-pre-wrap, div.markdown.prose.w-full.break-words.dark\\:prose-invert.dark'
  2847. );
  2848. messages.forEach(message => highlightMessageContent(message));
  2849. }
  2850.  
  2851. // MutationObserver: watch for newly added content & highlight it
  2852. let debounceTimer = null;
  2853. function handleMutations(mutationsList) {
  2854. for (const mutation of mutationsList) {
  2855. if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
  2856. if (debounceTimer) clearTimeout(debounceTimer);
  2857. // Short debounce so it doesn't run on every tiny text update
  2858. debounceTimer = setTimeout(() => {
  2859. console.log("DOM changes detected; highlighting...");
  2860. processExistingMessages();
  2861. }, 800);
  2862. break;
  2863. }
  2864. }
  2865. }
  2866.  
  2867. function initializeMutationObserver() {
  2868. const observer = new MutationObserver(handleMutations);
  2869. // Watch the entire body for new nodes
  2870. observer.observe(document.body, { childList: true, subtree: true });
  2871. console.log("MutationObserver initialized.");
  2872. }
  2873.  
  2874. // MAIN ENTRY POINT
  2875. function initialize() {
  2876. console.log('colorsScript initialized');
  2877. // Inject the user's chosen or default color styles
  2878. injectStyles(loadColors());
  2879. // Create the color-picker UI
  2880. createColorPickerPanel();
  2881. // Initial highlight pass
  2882. processExistingMessages();
  2883. // Start watching for new messages
  2884. initializeMutationObserver();
  2885. }
  2886.  
  2887. function startInitialization() {
  2888. const getColorsState = localStorage.getItem('enableColors');
  2889. if (getColorsState === "true") {
  2890. initialize();
  2891. } else {
  2892. console.log("Color customization is disabled.");
  2893. }
  2894. }
  2895.  
  2896. // Handle DOMContentLoaded
  2897. if (document.readyState === 'loading') {
  2898. document.addEventListener('DOMContentLoaded', startInitialization);
  2899. } else {
  2900. startInitialization();
  2901. }
  2902.  
  2903. // Listen for save button clicks
  2904. document.addEventListener('click', (event) => {
  2905. if (event.target && event.target.id === 'saveColors') {
  2906. saveColors();
  2907. }
  2908. });
  2909. }
  2910.  
  2911.  
  2912.  
  2913. // =================================================================== < > Hider ===================================================================
  2914.  
  2915. function hideInjectionsScript() {
  2916. // Mutation observer reference
  2917. let observer = null;
  2918.  
  2919. // Function to hide text within angle brackets
  2920. function hideBracketText(node) {
  2921. // Multi-line regex to capture text within < and >
  2922. const regex = /<[^>]*?>/gs;
  2923.  
  2924. // Ensure the node is a text node
  2925. if (node.nodeType === Node.TEXT_NODE) {
  2926. const parent = node.parentNode;
  2927.  
  2928. // Exclude nodes outside of .whitespace-pre-wrap
  2929. if (!parent.closest('.whitespace-pre-wrap')) {
  2930. return;
  2931. }
  2932.  
  2933. // Find all matches
  2934. const matches = node.nodeValue.match(regex);
  2935. if (matches) {
  2936. // Split the text node around matched portions
  2937. const parts = node.nodeValue.split(regex);
  2938.  
  2939. // Insert each segment and its matched hidden text
  2940. parts.forEach((part, index) => {
  2941. // Only insert visible text if non-empty
  2942. if (part.trim() !== "") {
  2943. const textNode = document.createTextNode(part);
  2944. parent.insertBefore(textNode, node);
  2945. }
  2946. // Create a hidden <span> if a match still exists at this index
  2947. if (index < matches.length) {
  2948. const hiddenSpan = document.createElement('span');
  2949. hiddenSpan.style.display = 'none';
  2950. hiddenSpan.textContent = matches[index];
  2951. parent.insertBefore(hiddenSpan, node);
  2952. }
  2953. });
  2954.  
  2955. // Remove the original text node
  2956. parent.removeChild(node);
  2957. }
  2958. }
  2959. }
  2960.  
  2961. // Process only visible and editable elements
  2962. function processVisibleTextNodes() {
  2963. document.querySelectorAll('*:not(script):not(style)').forEach(node => {
  2964. // Check for childNodes & whether element is visible
  2965. if (node.childNodes && node.offsetParent !== null) {
  2966. node.childNodes.forEach(child => {
  2967. // Only hide bracket text within .whitespace-pre-wrap
  2968. if (child.parentNode.closest('.whitespace-pre-wrap')) {
  2969. hideBracketText(child);
  2970. }
  2971. });
  2972. }
  2973. });
  2974. }
  2975.  
  2976. // Start observing the DOM
  2977. function startObserving() {
  2978. if (observer) return; // Avoid multiple observers
  2979.  
  2980. observer = new MutationObserver(mutations => {
  2981. mutations.forEach(mutation => {
  2982. mutation.addedNodes.forEach(addedNode => {
  2983. if (addedNode.nodeType === Node.ELEMENT_NODE) {
  2984. if (addedNode.closest('.whitespace-pre-wrap')) {
  2985. processVisibleTextNodes();
  2986. }
  2987. } else if (addedNode.nodeType === Node.TEXT_NODE) {
  2988. if (addedNode.parentNode.closest('.whitespace-pre-wrap')) {
  2989. hideBracketText(addedNode);
  2990. }
  2991. }
  2992. });
  2993. });
  2994. });
  2995.  
  2996. observer.observe(document.body, { childList: true, subtree: true });
  2997. processVisibleTextNodes(); // Process existing nodes initially
  2998. }
  2999.  
  3000. // Stop observing the DOM
  3001. function stopObserving() {
  3002. if (observer) {
  3003. observer.disconnect();
  3004. observer = null;
  3005. }
  3006. }
  3007.  
  3008. // Show hidden text
  3009. function showBracketText() {
  3010. // Reveal any spans that were previously hidden
  3011. document.querySelectorAll('span[style*="display: none"]').forEach(hiddenSpan => {
  3012. const parent = hiddenSpan.parentNode;
  3013. const textNode = document.createTextNode(hiddenSpan.textContent);
  3014. parent.insertBefore(textNode, hiddenSpan);
  3015. parent.removeChild(hiddenSpan);
  3016. });
  3017. }
  3018.  
  3019. // Monitor toggle state
  3020. function monitorHiderState() {
  3021. const state = localStorage.getItem('enableHider');
  3022. if (state === 'true') {
  3023. console.log("Hider is enabled.");
  3024. showBracketText(); // Reset to avoid duplicates
  3025. startObserving();
  3026. } else {
  3027. console.log("Hider is disabled.");
  3028. stopObserving();
  3029. showBracketText(); // Reveal hidden text
  3030. }
  3031. }
  3032.  
  3033. // Set up a timed observer that calls monitorHiderState every 2 seconds
  3034. function setupTimedObserver() {
  3035. // Initial call to ensure immediate action
  3036. monitorHiderState();
  3037.  
  3038. // Set interval to call monitorHiderState every 2000 milliseconds (2 seconds)
  3039. const intervalId = setInterval(() => {
  3040. const state = localStorage.getItem('enableHider');
  3041. if (state === 'true') {
  3042. // hideAllBracketText(); // Reapply hiding logic
  3043. }
  3044. monitorHiderState();
  3045. }, 2000);
  3046.  
  3047. // Optional: Provide a way to clear the interval if needed
  3048. // For example, to stop after a certain condition:
  3049. // setTimeout(() => clearInterval(intervalId), 60000); // Stops after 1 minute
  3050. }
  3051.  
  3052. // Optional: Provide a way to clear the interval if needed
  3053. // For example, to stop after a certain condition:
  3054. // setTimeout(() => clearInterval(intervalId), 60000); // Stops after 1 minute
  3055.  
  3056.  
  3057. // Listen for storage changes
  3058. window.addEventListener('storage', monitorHiderState);
  3059.  
  3060. // Initialize the timed observer when the script runs
  3061.  
  3062.  
  3063. // Initial setup
  3064. monitorHiderState();
  3065. setupTimedObserver();
  3066. }
  3067.  
  3068. // ============================================================= CONTENT WARNINGS =================================================================
  3069. function hideContentWarnings() {
  3070. function updateVisibility() {
  3071. const state = localStorage.getItem('enableHideWarning');
  3072. if (state === 'true') {
  3073. GM_addStyle(`
  3074. .border-orange-400\\/15,
  3075. div.text-sm.text-orange-600.border-token-border-light.bg-orange-400.bg-opacity-0.w-full.pr-5.text-right {
  3076. display: none !important;
  3077. }
  3078. `);
  3079. } else {
  3080. GM_addStyle(`
  3081. .border-orange-400\\/15,
  3082. div.text-sm.text-orange-600.border-token-border-light.bg-orange-400.bg-opacity-0.w-full.pr-5.text-right {
  3083. display: block !important;
  3084. }
  3085. `);
  3086. }
  3087. }
  3088.  
  3089. // Initial update
  3090. updateVisibility();
  3091.  
  3092. // Watch for changes in localStorage
  3093. window.addEventListener('storage', (event) => {
  3094. if (event.key === 'enableHideWarning') {
  3095. updateVisibility();
  3096. }
  3097. });
  3098.  
  3099. // Optional: Add a periodic check as a fallback
  3100. setInterval(updateVisibility, 1000);
  3101. }
  3102.  
  3103. // Call the function to initialize
  3104. hideContentWarnings();
  3105.  
  3106. // =============================================================== CURRENT TIME ===================================================================
  3107.  
  3108. function currentTime() {
  3109.  
  3110. // Function to store the current time in localStorage
  3111. function storeCurrentTime() {
  3112. const now = new Date();
  3113.  
  3114. // Format the date as '<Dayname hh:mm, dd.mm.yyyy>'
  3115. const options = { weekday: 'long' };
  3116. const dayName = new Intl.DateTimeFormat('en-US', options).format(now);
  3117. const time = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
  3118. const date = now.toLocaleDateString('en-GB').replace(/\//g, '.');
  3119.  
  3120. const formattedTime = `<${dayName} ${time}, ${date}>`;
  3121.  
  3122. // Store in localStorage
  3123. localStorage.setItem('currentTime', formattedTime);
  3124. }
  3125.  
  3126. // Make the function accessible from outside
  3127. window.storeCurrentTime = storeCurrentTime;
  3128.  
  3129. }
  3130.  
  3131. // =================================================================== BUTTONS ===================================================================
  3132.  
  3133.  
  3134. function createDivider() {
  3135. const divider = document.createElement('div');
  3136. divider.style.height = '1px';
  3137. divider.style.backgroundColor = '#212121';
  3138. divider.style.margin = '20px 20px 0px 20px';
  3139. return divider;
  3140. }
  3141. function createFooterText() {
  3142. const footerText = document.createElement('div');
  3143. footerText.textContent = '© Vishanka 2024';
  3144. footerText.style.position = 'absolute';
  3145. footerText.style.bottom = '10px';
  3146. footerText.style.left = '50%';
  3147. footerText.style.transform = 'translateX(-50%)';
  3148. footerText.style.fontSize = '12px';
  3149. footerText.style.fontWeight = '550';
  3150. footerText.style.color = '#272727';
  3151. return footerText;
  3152. }
  3153.  
  3154. function createCheckbox(label, key) {
  3155. const checkboxContainer = document.createElement('div');
  3156. checkboxContainer.style.marginTop = '5px';
  3157. checkboxContainer.style.marginLeft = '25px';
  3158.  
  3159. const checkboxLabel = document.createElement('label');
  3160. checkboxLabel.style.color = '#B0B0B0';
  3161.  
  3162. const checkbox = document.createElement('input');
  3163. checkbox.style.marginRight = '10px';
  3164. checkbox.type = 'checkbox';
  3165. checkbox.id = key;
  3166. checkboxLabel.textContent = label;
  3167. checkboxLabel.setAttribute('for', key);
  3168.  
  3169. // Retrieve saved state from localStorage
  3170. const savedState = localStorage.getItem(key);
  3171. if (savedState !== null) {
  3172. checkbox.checked = savedState === 'true';
  3173. }
  3174.  
  3175. // Listen for changes and save state to localStorage
  3176. checkbox.addEventListener('change', function() {
  3177. localStorage.setItem(key, checkbox.checked);
  3178. });
  3179.  
  3180. checkboxContainer.appendChild(checkbox);
  3181. checkboxContainer.appendChild(checkboxLabel);
  3182.  
  3183.  
  3184. return checkboxContainer;
  3185. }
  3186.  
  3187. function initializeCheckboxes() {
  3188. function checkboxDivider() {
  3189. const checkboxDivider = document.createElement('div');
  3190. checkboxDivider.style.height = '1px';
  3191. checkboxDivider.style.backgroundColor = '#212121';
  3192. checkboxDivider.style.margin = '20px 20px 20px 20px';
  3193. return checkboxDivider;
  3194. }
  3195. // Create a container for the checkboxes
  3196. const checkboxGroup = document.createElement('div');
  3197. checkboxGroup.style.marginTop = '15px'; // Add margin at the top of the group
  3198.  
  3199. // Create and append each checkbox to the group
  3200. checkboxGroup.appendChild(createCheckbox('Enable Rules', 'enableRules'));
  3201. checkboxGroup.appendChild(createCheckbox('Enable Lorebook', 'enableLorebook'));
  3202. checkboxGroup.appendChild(createCheckbox('Enable Events', 'enableEvents'));
  3203. checkboxGroup.appendChild(checkboxDivider());
  3204. checkboxGroup.appendChild(createCheckbox('Enable Colors', 'enableColors'));
  3205. checkboxGroup.appendChild(createCheckbox('Hide Injections', 'enableHider'));
  3206. checkboxGroup.appendChild(createCheckbox('Hide Content Warning', 'enableHideWarning'));
  3207. checkboxGroup.appendChild(checkboxDivider());
  3208. checkboxGroup.appendChild(createCheckbox('Enable Time', 'enableTime'));
  3209. // Append the group to the main panel
  3210. mainPanel.appendChild(checkboxGroup);
  3211. }
  3212.  
  3213.  
  3214. function initializeButton() {
  3215. mainPanel.appendChild(createDivider());
  3216. mainPanel.appendChild(openLorebookButton);
  3217. mainPanel.appendChild(manageRulesButton);
  3218. mainPanel.appendChild(createDivider());
  3219. initializeCheckboxes();
  3220. mainPanel.appendChild(createDivider());
  3221. mainPanel.appendChild(colorButton);
  3222. mainPanel.appendChild(createFooterText());
  3223. }
  3224. // ============================================================ SCRIPT LOADING ORDER ============================================================
  3225. currentTime();
  3226. colorsScript();
  3227. hideInjectionsScript();
  3228. hideContentWarnings();
  3229. mainPanelScript();
  3230. rulesScript();
  3231. lorebookScript();
  3232. initializeButton();
  3233. injectLogicGPT();
  3234. //lorebookLogic();
  3235. // Function to execute your scripts
  3236. /* function executeScripts() {
  3237. mainPanelScript();
  3238. rulesScript();
  3239. initializeButton();
  3240. injectLogicGPT();
  3241. }
  3242.  
  3243. // Initial execution
  3244. executeScripts();
  3245.  
  3246. // Monitor for URL changes
  3247. let currentUrl = location.href;
  3248. const observer = new MutationObserver(() => {
  3249. if (currentUrl !== location.href) {
  3250. currentUrl = location.href;
  3251. console.log('URL changed, re-executing scripts...');
  3252. executeScripts();
  3253. }
  3254. });
  3255.  
  3256. // Observe changes in the <body> tag
  3257. observer.observe(document.body, { childList: true, subtree: true });
  3258. */
  3259. })();