Xoul AI Notepad (Ready)

Adds a notepad functionality, specific to each chat

目前为 2024-12-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Xoul AI Notepad (Ready)
  3. // @namespace Xoul AI
  4. // @match https://xoul.ai/*
  5. // @grant none
  6. // @license MIT
  7. // @version 1.0
  8. // @description Adds a notepad functionality, specific to each chat
  9. // @icon https://i.imgur.com/REqi6Iw.png
  10. // @author LuxTallis
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. let currentChatKey = null;
  17. let currentNotepad = null;
  18. let buttonAdded = false; // Track if the button has been added
  19.  
  20. // Create a style element and append it to the head to avoid CSP issues
  21. const style = document.createElement('style');
  22. style.textContent = `
  23. .notepad {
  24. position: fixed;
  25. top: 0;
  26. right: 0;
  27. width: 300px;
  28. height: calc(100vh - 10px);
  29. background-color: #0a0a0a;
  30. color: #ffffff;
  31. padding: 10px;
  32. box-shadow: 0 0 0px rgba(0, 0, 0, 0.5);
  33. transform: translateX(100%);
  34. transition: transform 0.3s ease-in-out;
  35. z-index: 10000;
  36. overflow-y: auto;
  37. }
  38. .toggle-button {
  39. position: relative;
  40. background-color: #4a4a4a;
  41. color: #ffffff;
  42. border: none;
  43. padding: 10px;
  44. cursor: pointer;
  45. }
  46. `;
  47. document.head.appendChild(style);
  48.  
  49. // Function to get the current chat ID from the URL
  50. function getChatId() {
  51. const url = window.location.href;
  52. const chatId = url.match(/\/chats\/([a-f0-9\-]+)/);
  53. return chatId ? chatId[1] : null;
  54. }
  55.  
  56. // Function to initialize or reinitialize the notepad
  57. function initializeNotepad() {
  58. const chatKey = getChatId();
  59. if (!chatKey || chatKey === currentChatKey) return; // Don't reinitialize if the chat ID is the same
  60.  
  61. currentChatKey = chatKey;
  62.  
  63. // If a notepad already exists, remove it
  64. if (currentNotepad) {
  65. currentNotepad.remove();
  66. }
  67.  
  68. // Create the notepad container
  69. currentNotepad = document.createElement('div');
  70. currentNotepad.className = 'notepad';
  71. currentNotepad.contentEditable = true; // Make it editable
  72.  
  73. // Load saved content from localStorage based on the chat ID
  74. const savedContent = localStorage.getItem(`notepadContent-${chatKey}`);
  75. currentNotepad.textContent = savedContent ? savedContent : 'Start typing...'; // Default text
  76.  
  77. // Event listener to save content on each change
  78. currentNotepad.addEventListener('input', () => {
  79. localStorage.setItem(`notepadContent-${chatKey}`, currentNotepad.textContent);
  80. });
  81.  
  82. // Add the notepad to the page
  83. document.body.appendChild(currentNotepad);
  84. }
  85.  
  86. // Add the new button under a given element
  87. function addButtonUnder(targetElement) {
  88. // Check if the button already exists, to avoid duplicates
  89. if (!document.querySelector('#newButton')) {
  90. const button = document.createElement('button');
  91. button.id = 'newButton'; // Add an ID to easily reference and avoid duplicates
  92. button.textContent = '◙';
  93. button.style.cssText = 'margin: 10px 0; padding: 5px 10px; background-color: #28a74500; color: white; border: none; border-radius: 5px; cursor: pointer; display: block;';
  94.  
  95. button.addEventListener('click', (e) => {
  96. e.stopPropagation(); // Prevent click from propagating to the document
  97. if (!currentNotepad) {
  98. initializeNotepad();
  99. }
  100. // Toggle the notepad visibility
  101. if (currentNotepad.style.transform === 'translateX(100%)') {
  102. currentNotepad.style.transform = 'translateX(0%)'; // Open the notepad
  103. } else {
  104. currentNotepad.style.transform = 'translateX(100%)'; // Close the notepad
  105. }
  106. });
  107.  
  108. targetElement.insertAdjacentElement('afterend', button);
  109. }
  110. }
  111.  
  112. // Wait for an element to load and execute the callback when it's found
  113. function waitForElement(selector, callback) {
  114. const observer = new MutationObserver(() => {
  115. const element = document.querySelector(selector);
  116. if (element) {
  117. observer.disconnect();
  118. callback(element);
  119. }
  120. });
  121.  
  122. observer.observe(document.body, { childList: true, subtree: true });
  123. }
  124.  
  125. // Main function to decide where to place the new button
  126. function placeButton() {
  127. const firstButton = document.querySelector('#customButton');
  128. if (firstButton) {
  129. // If the first button exists, place the new button below it
  130. addButtonUnder(firstButton);
  131. } else {
  132. // If the first button doesn't exist, place the new button under the fallback element
  133. waitForElement('a.Sidebar_link__0EvG_:nth-child(6)', addButtonUnder);
  134. }
  135. }
  136.  
  137. // Use MutationObserver to recheck the DOM periodically
  138. const observer = new MutationObserver(placeButton);
  139. observer.observe(document.body, { childList: true, subtree: true });
  140.  
  141. // Observer to detect URL changes and update the notepad accordingly
  142. const urlObserver = new MutationObserver(() => {
  143. const chatId = getChatId();
  144. if (chatId !== currentChatKey) {
  145. initializeNotepad();
  146. }
  147. });
  148. urlObserver.observe(document, { childList: true, subtree: true });
  149.  
  150. // Close notepad when clicking outside of it
  151. function closeNotepadOnClickOutside(event) {
  152. if (currentNotepad && !currentNotepad.contains(event.target)) {
  153. currentNotepad.style.transform = 'translateX(100%)'; // Close the notepad
  154. }
  155. }
  156.  
  157. // Attach the click event listener to the document to detect clicks outside
  158. document.addEventListener('click', closeNotepadOnClickOutside);
  159.  
  160. // Initial call to place the button when the page is loaded
  161. placeButton();
  162. })();