Sync between Sexy.AI and SillyTavern optimized

Enhanced and optimized integration between SillyTavern and Sexy.AI

  1. // ==UserScript==
  2. // @name Sync between Sexy.AI and SillyTavern optimized
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.4.1
  5. // @description Enhanced and optimized integration between SillyTavern and Sexy.AI
  6. // @author You
  7. // @match https://sexy.ai/workflow*
  8. // @match https://staticui.sexy.ai/*
  9. // @match http://103.70.12.123:8000/*
  10. // @match http://127.0.0.1:8000/*
  11. // @match http://*/*:8000/*
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // @grant unsafeWindow
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. console.log("Script started on URL:", window.location.href);
  21.  
  22. const isSexyAI = window.location.href.includes('sexy.ai') || window.location.href.includes('staticui.sexy.ai');
  23. const isSillyTavern = window.location.href.includes(':8000');
  24.  
  25. // Utility Functions
  26. function createStyledButton(text, onClick) {
  27. const button = document.createElement('button');
  28. button.textContent = text;
  29. button.style.cssText = `
  30. padding: 5px 8px;
  31. background-color: #4CAF50;
  32. color: white;
  33. border: none;
  34. border-radius: 3px;
  35. cursor: pointer;
  36. font-size: 12px;
  37. opacity: 0.8;
  38. transition: all 0.3s;
  39. margin-left: 5px;
  40. `;
  41. button.addEventListener('mouseover', () => button.style.opacity = '1');
  42. button.addEventListener('mouseout', () => button.style.opacity = '0.8');
  43. button.addEventListener('click', onClick);
  44. return button;
  45. }
  46.  
  47. function showNotification(message) {
  48. const overlay = document.createElement('div');
  49. overlay.style.cssText = `
  50. position: fixed;
  51. top: 10px;
  52. right: 10px;
  53. background-color: #4CAF50;
  54. color: white;
  55. padding: 8px;
  56. border-radius: 4px;
  57. z-index: 10000;
  58. opacity: 0;
  59. transition: opacity 0.3s;
  60. `;
  61. overlay.textContent = message;
  62. document.body.appendChild(overlay);
  63.  
  64. requestAnimationFrame(() => {
  65. overlay.style.opacity = '1';
  66. setTimeout(() => {
  67. overlay.style.opacity = '0';
  68. setTimeout(() => overlay.remove(), 300);
  69. }, 1500);
  70. });
  71. }
  72.  
  73. // Extract original text from message
  74. function extractOriginalText(messageNode) {
  75. const originalText = messageNode.getAttribute('data-original-text');
  76. if (originalText) {
  77. return originalText;
  78. }
  79.  
  80. const messageText = messageNode.querySelector('.mes_text');
  81. if (!messageText) return '';
  82.  
  83. const clone = messageText.cloneNode(true);
  84. const buttonContainer = clone.querySelector('.button-container');
  85. if (buttonContainer) {
  86. buttonContainer.remove();
  87. }
  88.  
  89. return clone.textContent.trim();
  90. }
  91.  
  92. // SexyAI Implementation
  93. if (isSexyAI) {
  94. if (window.location.href.includes('staticui.sexy.ai')) {
  95. const promptButton = createStyledButton('Get Prompt', () => {
  96. const prompt = GM_getValue('st_prompt', null);
  97. if (prompt) {
  98. const positiveInput = document.querySelector('textarea') ||
  99. document.querySelector('input[type="text"]');
  100.  
  101. if (positiveInput) {
  102. positiveInput.value = prompt;
  103. const event = new Event('input', { bubbles: true });
  104. positiveInput.dispatchEvent(event);
  105. GM_setValue('st_prompt', null);
  106. promptButton.style.backgroundColor = '#2196F3';
  107. promptButton.textContent = 'Prompt Added';
  108. setTimeout(() => {
  109. promptButton.style.backgroundColor = '#4CAF50';
  110. promptButton.textContent = 'Get Prompt';
  111. }, 2000);
  112. }
  113. }
  114. });
  115. promptButton.style.cssText += 'position: fixed; right: 20px; top: 80px;';
  116. document.body.appendChild(promptButton);
  117. }
  118.  
  119. // Optimized image handling for SexyAI
  120. document.addEventListener('click', (e) => {
  121. if (e.target.tagName === 'IMG') {
  122. const allImages = document.querySelectorAll('img');
  123. const latestImages = Array.from(allImages)
  124. .slice(-1)
  125. .map(img => img.src);
  126.  
  127. GM_setValue('sexyai_images', JSON.stringify(latestImages));
  128. showNotification('Images Synced');
  129. }
  130. }, true);
  131. }
  132.  
  133. // SillyTavern Implementation
  134. else if (isSillyTavern) {
  135. function showImageModal(imageUrl) {
  136. const existingModal = document.querySelector('.image-modal-container');
  137. if (existingModal) {
  138. existingModal.remove();
  139. }
  140.  
  141. const images = JSON.parse(GM_getValue('sexyai_images', '[]'));
  142. if (images.length === 0) return;
  143.  
  144. const container = document.createElement('div');
  145. container.className = 'image-modal-container';
  146. container.style.cssText = `
  147. position: fixed;
  148. top: 0px;
  149. right: 20px;
  150. z-index: 10000;
  151. padding: 0px;
  152. border-radius: 0px;
  153. max-width: 300px;
  154. `;
  155.  
  156. const imgElement = new Image();
  157. imgElement.style.cssText = `
  158. width: 100%;
  159. height: auto;
  160. max-height: 400px;
  161. object-fit: contain;
  162. border-radius: 0px;
  163. `;
  164.  
  165. // Luôn hiển thị ảnh mới nhất
  166. imgElement.src = images[images.length - 1];
  167.  
  168. const keyHandler = (e) => {
  169. if (!container.isConnected) return;
  170. if (e.key === 'Escape') container.remove();
  171. };
  172.  
  173. document.addEventListener('keydown', keyHandler);
  174. container.addEventListener('remove', () => {
  175. document.removeEventListener('keydown', keyHandler);
  176. });
  177.  
  178. container.appendChild(imgElement);
  179. document.body.appendChild(container);
  180. }
  181. function addControlButtons() {
  182. if (document.querySelector('#show_image_button') || document.querySelector('#send_prompt_button')) {
  183. return;
  184. }
  185.  
  186. const extensionsButton = document.querySelector('#extensionsMenuButton');
  187. const optionsButton = document.querySelector('#options_button');
  188.  
  189. if (extensionsButton && optionsButton) {
  190. const showImageButton = document.createElement('div');
  191. showImageButton.id = 'show_image_button';
  192. showImageButton.className = 'fa-solid fa-eye interactable';
  193. showImageButton.title = 'Show/Hide Images';
  194. showImageButton.style.cssText = `
  195. display: flex;
  196. cursor: pointer;
  197. opacity: 0.7;
  198. margin: 0 5px;
  199. font-size: 18px;
  200. transition: all 0.3s;
  201. `;
  202. showImageButton.tabIndex = "0";
  203.  
  204. const sendPromptButton = document.createElement('div');
  205. sendPromptButton.id = 'send_prompt_button';
  206. sendPromptButton.className = 'fa-solid fa-paper-plane interactable';
  207. sendPromptButton.title = 'Send Prompt';
  208. sendPromptButton.style.cssText = `
  209. display: flex;
  210. cursor: pointer;
  211. opacity: 0.7;
  212. margin: 0 5px;
  213. font-size: 18px;
  214. transition: all 0.3s;
  215. `;
  216. sendPromptButton.tabIndex = "0";
  217.  
  218. let isShowingImages = false;
  219.  
  220. showImageButton.addEventListener('click', () => {
  221. isShowingImages = !isShowingImages;
  222. showImageButton.style.color = isShowingImages ? 'var(--accent-color, #4CAF50)' : '';
  223. showImageButton.style.opacity = isShowingImages ? '1' : '0.7';
  224.  
  225. if (isShowingImages) {
  226. const images = JSON.parse(GM_getValue('sexyai_images', '[]'));
  227. if (images.length > 0) {
  228. showImageModal(images[0]);
  229. }
  230. } else {
  231. const existingModal = document.querySelector('.image-modal-container');
  232. if (existingModal) {
  233. existingModal.remove();
  234. }
  235. }
  236. });
  237.  
  238. sendPromptButton.addEventListener('click', () => {
  239. const messages = document.getElementsByClassName('mes');
  240. const lastMessage = messages[messages.length - 1];
  241. if (lastMessage) {
  242. const text = extractOriginalText(lastMessage);
  243. const match = text.match(/image###([^#]+)###/);
  244. if (match) {
  245. const prompt = match[1].trim();
  246. GM_setValue('st_prompt', prompt);
  247. sendPromptButton.style.color = 'var(--accent-color, #4CAF50)';
  248. setTimeout(() => {
  249. sendPromptButton.style.color = '';
  250. }, 2000);
  251. }
  252. }
  253. });
  254.  
  255. optionsButton.parentNode.insertBefore(showImageButton, optionsButton.nextSibling);
  256. extensionsButton.parentNode.insertBefore(sendPromptButton, extensionsButton.nextSibling);
  257. }
  258. }
  259.  
  260. const observer = new MutationObserver((mutations) => {
  261. for (const mutation of mutations) {
  262. if (mutation.removedNodes.length > 0) {
  263. if (!document.querySelector('#show_image_button') || !document.querySelector('#send_prompt_button')) {
  264. addControlButtons();
  265. }
  266. }
  267. }
  268. });
  269.  
  270. observer.observe(document.body, {
  271. childList: true,
  272. subtree: true
  273. });
  274.  
  275. // Initial setup
  276. let checkInterval = setInterval(() => {
  277. if (document.querySelector('#options_button') && document.querySelector('#extensionsMenuButton')) {
  278. addControlButtons();
  279. if (document.querySelector('#show_image_button') && document.querySelector('#send_prompt_button')) {
  280. clearInterval(checkInterval);
  281. }
  282. }
  283. }, 1000);
  284. }
  285. })();