JanitorAI Enhanced UI with CSS Toggle

Adds UI controls for JanitorAI, hides buttons on chat pages, and toggles custom CSS

当前为 2025-04-09 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name JanitorAI Enhanced UI with CSS Toggle
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.4
  5. // @description Adds UI controls for JanitorAI, hides buttons on chat pages, and toggles custom CSS
  6. // @author Fefnik
  7. // @match https://janitorai.com/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. let MIN_TOKENS = parseInt(localStorage.getItem('janitorAITokenFilter')) || 500;
  15. let isSidebarHidden = localStorage.getItem('janitorAISidebarHidden') === 'true';
  16. let isAutoScrollEnabled = localStorage.getItem('janitorAIAutoScroll') !== 'false';
  17. let isMenuVisible = localStorage.getItem('janitorAIMenuVisible') === 'true';
  18. let isCustomCssDisabled = localStorage.getItem('janitorAICustomCssDisabled') === 'true';
  19.  
  20. let sliderElement = null;
  21. let sliderContainer = null;
  22. let controlPanel = null;
  23. let controlsContainer = null;
  24. let emblaSlide = null;
  25.  
  26. const isAllowedPage = () => {
  27. const path = window.location.pathname;
  28. return path === '/' || path.startsWith('/search') || path === '/my_characters' || path.startsWith('/profiles/');
  29. };
  30.  
  31. const isChatsPage = () => window.location.pathname.startsWith('/chats');
  32.  
  33. const parseTokens = (text) => {
  34. try {
  35. text = text.replace(/<!--[\s\S]*?-->/g, '').replace('tokens', '').trim();
  36. return text.includes('k') ? parseFloat(text.replace('k', '')) * 1000 : parseInt(text, 10) || 0;
  37. } catch {
  38. return 0;
  39. }
  40. };
  41.  
  42. const filterCards = () => {
  43. document.querySelectorAll('.chakra-stack.css-1s5evre, .css-1s5evre').forEach(card => {
  44. const tokenElement = card.querySelector('.chakra-text.css-jccmq6, .css-jccmq6');
  45. if (!tokenElement) return;
  46.  
  47. const tokenCount = parseTokens(tokenElement.textContent);
  48. const parent = card.closest('.css-1sxhvxh, .css-1dbw1r8');
  49. if (parent) parent.style.display = tokenCount < MIN_TOKENS ? 'none' : '';
  50. });
  51. };
  52.  
  53. const setupPaginationScroll = () => {
  54. document.querySelectorAll('.css-kzd6o0').forEach(button => {
  55. button.removeEventListener('click', handlePaginationClick);
  56. button.addEventListener('click', handlePaginationClick);
  57. });
  58. };
  59.  
  60. const handlePaginationClick = () => {
  61. if (isAutoScrollEnabled) {
  62. setTimeout(() => window.scrollTo({ top: 0, behavior: 'smooth' }), 300);
  63. }
  64. };
  65.  
  66. const toggleSidebar = () => {
  67. const sidebar = document.querySelector('.css-h988mi');
  68. const css70qvj9 = document.querySelector('.css-70qvj9');
  69.  
  70. if (sidebar) {
  71. isSidebarHidden = !isSidebarHidden;
  72. sidebar.style.display = isSidebarHidden ? 'none' : '';
  73.  
  74. if (!emblaSlide) {
  75. emblaSlide = document.querySelector('.is-in-view.is-snapped.embla__slide');
  76. }
  77. if (emblaSlide) {
  78. emblaSlide.style.display = isSidebarHidden ? 'none' : '';
  79. }
  80.  
  81. if (css70qvj9) {
  82. css70qvj9.style.display = isSidebarHidden ? 'none' : '';
  83. }
  84.  
  85. localStorage.setItem('janitorAISidebarHidden', isSidebarHidden);
  86. updateControlText();
  87. }
  88. };
  89.  
  90. const toggleAutoScroll = () => {
  91. isAutoScrollEnabled = !isAutoScrollEnabled;
  92. localStorage.setItem('janitorAIAutoScroll', isAutoScrollEnabled);
  93. updateControlText();
  94. };
  95.  
  96. const toggleMenu = () => {
  97. isMenuVisible = !isMenuVisible;
  98. localStorage.setItem('janitorAIMenuVisible', isMenuVisible);
  99. updateElementsVisibility();
  100. updateControlText();
  101. };
  102.  
  103. const toggleCustomCss = () => {
  104. isCustomCssDisabled = !isCustomCssDisabled;
  105. localStorage.setItem('janitorAICustomCssDisabled', isCustomCssDisabled);
  106. applyCustomCssToggle();
  107. updateControlText();
  108. };
  109.  
  110. const applyCustomCssToggle = () => {
  111. if (isCustomCssDisabled) {
  112. removeCustomStyles();
  113. } else {
  114. // Если стили включены, ничего не делаем — они уже загружены страницей
  115. }
  116. };
  117.  
  118. const removeCustomStyles = () => {
  119. const styles = document.querySelectorAll('body style');
  120. styles.forEach(style => style.remove());
  121. };
  122.  
  123. const updateControlText = () => {
  124. const sidebarText = document.getElementById('sidebar-toggle-text');
  125. const scrollText = document.getElementById('auto-scroll-text');
  126. const cssToggleText = document.getElementById('css-toggle-text');
  127. if (sidebarText) {
  128. sidebarText.textContent = isSidebarHidden ? 'Show Topbar' : 'Hide Topbar';
  129. sidebarText.style.color = isSidebarHidden ? '#fff' : '#ccc';
  130. }
  131. if (scrollText) {
  132. scrollText.textContent = `Auto-Scroll: ${isAutoScrollEnabled ? 'ON' : 'OFF'}`;
  133. scrollText.style.color = isAutoScrollEnabled ? '#fff' : '#ccc';
  134. }
  135. if (cssToggleText) {
  136. cssToggleText.textContent = `Custom CSS: ${isCustomCssDisabled ? 'OFF' : 'ON'}`;
  137. cssToggleText.style.color = isCustomCssDisabled ? '#fff' : '#ccc';
  138. }
  139. };
  140.  
  141. const createControlPanel = () => {
  142. if (controlPanel) return;
  143.  
  144. controlPanel = document.createElement('div');
  145. controlPanel.id = 'janitor-control-panel';
  146. Object.assign(controlPanel.style, {
  147. position: 'fixed',
  148. top: '75px',
  149. left: '10px',
  150. zIndex: '100000',
  151. display: 'flex',
  152. flexDirection: 'column',
  153. gap: '5px',
  154. alignItems: 'flex-start'
  155. });
  156.  
  157. const settingsButton = document.createElement('button');
  158. settingsButton.id = 'token-filter-toggle';
  159. settingsButton.textContent = '⚙️';
  160. Object.assign(settingsButton.style, {
  161. width: '30px',
  162. height: '30px',
  163. padding: '0',
  164. backgroundColor: 'rgba(74, 74, 74, 0.7)',
  165. color: '#fff',
  166. border: 'none',
  167. borderRadius: '5px',
  168. cursor: 'pointer',
  169. display: 'flex',
  170. alignItems: 'center',
  171. justifyContent: 'center',
  172. fontSize: '16px',
  173. transition: 'background-color 0.2s'
  174. });
  175. settingsButton.addEventListener('click', toggleMenu);
  176.  
  177. controlsContainer = document.createElement('div');
  178. controlsContainer.id = 'controls-container';
  179. Object.assign(controlsContainer.style, {
  180. display: 'none',
  181. flexDirection: 'column',
  182. gap: '5px',
  183. backgroundColor: 'rgba(74, 74, 74, 0.7)',
  184. padding: '5px',
  185. borderRadius: '5px',
  186. zIndex: '100001'
  187. });
  188.  
  189. const sidebarText = document.createElement('span');
  190. sidebarText.id = 'sidebar-toggle-text';
  191. sidebarText.style.cursor = 'pointer';
  192. sidebarText.style.fontSize = '12px';
  193. sidebarText.addEventListener('click', toggleSidebar);
  194.  
  195. const scrollText = document.createElement('span');
  196. scrollText.id = 'auto-scroll-text';
  197. scrollText.style.cursor = 'pointer';
  198. scrollText.style.fontSize = '12px';
  199. scrollText.addEventListener('click', toggleAutoScroll);
  200.  
  201. const cssToggleText = document.createElement('span');
  202. cssToggleText.id = 'css-toggle-text';
  203. cssToggleText.style.cursor = 'pointer';
  204. cssToggleText.style.fontSize = '12px';
  205. cssToggleText.addEventListener('click', toggleCustomCss);
  206.  
  207. controlsContainer.appendChild(sidebarText);
  208. controlsContainer.appendChild(scrollText);
  209. controlsContainer.appendChild(cssToggleText);
  210. controlPanel.appendChild(settingsButton);
  211. controlPanel.appendChild(controlsContainer);
  212. document.body.appendChild(controlPanel);
  213. updateControlText();
  214. };
  215.  
  216. const createOrUpdateSlider = () => {
  217. if (sliderElement) return;
  218.  
  219. sliderContainer = document.createElement('div');
  220. sliderContainer.id = 'token-filter-container';
  221. Object.assign(sliderContainer.style, {
  222. position: 'fixed',
  223. top: '75px',
  224. left: '50px',
  225. zIndex: '100002',
  226. display: 'none',
  227. flexDirection: 'row',
  228. alignItems: 'center',
  229. gap: '10px',
  230. padding: '5px',
  231. backgroundColor: 'rgba(74, 74, 74, 0.7)',
  232. borderRadius: '5px'
  233. });
  234.  
  235. sliderElement = document.createElement('input');
  236. sliderElement.type = 'range';
  237. sliderElement.id = 'token-filter-slider';
  238. Object.assign(sliderElement, {
  239. min: '0',
  240. max: '6000',
  241. step: '100',
  242. value: MIN_TOKENS
  243. });
  244. Object.assign(sliderElement.style, {
  245. width: '150px',
  246. height: '20px',
  247. backgroundColor: '#4a4a4a',
  248. cursor: 'pointer',
  249. appearance: 'none',
  250. outline: 'none',
  251. borderRadius: '5px',
  252. padding: '0',
  253. zIndex: '100003'
  254. });
  255.  
  256. const style = document.createElement('style');
  257. style.textContent = `
  258. #token-filter-slider {
  259. -webkit-appearance: none;
  260. appearance: none;
  261. background: #4a4a4a;
  262. border-radius: 5px;
  263. z-index: 100003;
  264. }
  265. #token-filter-slider::-webkit-slider-thumb {
  266. -webkit-appearance: none;
  267. appearance: none;
  268. width: 20px;
  269. height: 20px;
  270. background: #ffffff;
  271. cursor: pointer;
  272. border-radius: 50%;
  273. border: 2px solid #000;
  274. box-shadow: 0 0 2px rgba(0,0,0,0.5);
  275. transform: translateY(-5px);
  276. z-index: 100004;
  277. }
  278. #token-filter-slider::-moz-range-thumb {
  279. width: 20px;
  280. height: 20px;
  281. background: #ffffff;
  282. cursor: pointer;
  283. border-radius: 50%;
  284. border: 2px solid #000;
  285. box-shadow: 0 0 2px rgba(0,0,0,0.5);
  286. transform: translateY(-5px);
  287. z-index: 100004;
  288. }
  289. #token-filter-slider::-webkit-slider-runnable-track {
  290. height: 10px;
  291. background: #4a4a4a;
  292. border-radius: 5px;
  293. }
  294. #token-filter-slider::-moz-range-track {
  295. height: 10px;
  296. background: #4a4a4a;
  297. border-radius: 5px;
  298. }
  299. `;
  300. document.head.appendChild(style);
  301.  
  302. const label = document.createElement('span');
  303. label.id = 'token-filter-label';
  304. label.style.color = '#fff';
  305. label.style.fontSize = '12px';
  306. label.style.minWidth = '60px';
  307. label.textContent = `${MIN_TOKENS} tokens`;
  308.  
  309. sliderElement.addEventListener('input', (e) => {
  310. MIN_TOKENS = parseInt(e.target.value);
  311. label.textContent = `${MIN_TOKENS} tokens`;
  312. localStorage.setItem('janitorAITokenFilter', MIN_TOKENS);
  313. filterCards();
  314. });
  315.  
  316. sliderContainer.appendChild(sliderElement);
  317. sliderContainer.appendChild(label);
  318. document.body.appendChild(sliderContainer);
  319. };
  320.  
  321. const updateElementsVisibility = () => {
  322. const shouldShow = isAllowedPage() && !isChatsPage();
  323. if (controlPanel) controlPanel.style.display = shouldShow ? 'flex' : 'none';
  324. if (sliderContainer) sliderContainer.style.display = shouldShow && isMenuVisible ? 'flex' : 'none';
  325. if (controlsContainer) controlsContainer.style.display = shouldShow && isMenuVisible ? 'flex' : 'none';
  326. };
  327.  
  328. const initialize = () => {
  329. createControlPanel();
  330. createOrUpdateSlider();
  331. updateElementsVisibility();
  332.  
  333. if (isAllowedPage() && !isChatsPage()) {
  334. filterCards();
  335. setupPaginationScroll();
  336. applyCustomCssToggle(); // Применяем переключатель CSS при инициализации
  337.  
  338. const sidebar = document.querySelector('.css-h988mi');
  339. if (sidebar && isSidebarHidden) {
  340. sidebar.style.display = 'none';
  341. emblaSlide = document.querySelector('.is-in-view.is-snapped.embla__slide');
  342. if (emblaSlide) emblaSlide.style.display = 'none';
  343. const css70qvj9 = document.querySelector('.css-70qvj9');
  344. if (css70qvj9) css70qvj9.style.display = 'none';
  345. }
  346.  
  347. new MutationObserver(() => {
  348. filterCards();
  349. setupPaginationScroll();
  350. applyCustomCssToggle(); // Повторно применяем при изменениях DOM
  351. }).observe(document.body, { childList: true, subtree: true });
  352.  
  353. // Перехватываем appendChild для предотвращения перемещения <style> в <head>
  354. const originalAppendChild = Element.prototype.appendChild;
  355. Element.prototype.appendChild = function(node) {
  356. if (isCustomCssDisabled && node.tagName === 'STYLE' && this.tagName === 'HEAD') {
  357. return node; // Пропускаем добавление <style> в <head>, если CSS отключены
  358. }
  359. return originalAppendChild.call(this, node);
  360. };
  361. }
  362. };
  363.  
  364. const tryInitialize = () => {
  365. if (document.body) {
  366. initialize();
  367. let lastPath = window.location.pathname;
  368. setInterval(() => {
  369. if (lastPath !== window.location.pathname) {
  370. lastPath = window.location.pathname;
  371. updateElementsVisibility();
  372. if (isAllowedPage() && !isChatsPage()) {
  373. filterCards();
  374. setupPaginationScroll();
  375. applyCustomCssToggle();
  376. }
  377. }
  378. }, 500);
  379. } else {
  380. setTimeout(tryInitialize, 1000);
  381. }
  382. };
  383.  
  384. tryInitialize();
  385. })();