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.6
  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 = false;
  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. updateElementsVisibility();
  99. updateControlText();
  100. };
  101.  
  102. const toggleCustomCss = () => {
  103. isCustomCssDisabled = !isCustomCssDisabled;
  104. localStorage.setItem('janitorAICustomCssDisabled', isCustomCssDisabled);
  105. applyCustomCssToggle();
  106. updateControlText();
  107. };
  108.  
  109. const applyCustomCssToggle = () => {
  110. if (isCustomCssDisabled) {
  111. removeCustomStyles();
  112. blockCustomElements();
  113. }
  114. };
  115.  
  116. const removeCustomStyles = () => {
  117. const styles = document.querySelectorAll('body style');
  118. styles.forEach(style => style.remove());
  119. };
  120.  
  121. const blockCustomElements = () => {
  122. // Блокировка элементов с классом .css-1bn1yyx
  123. document.querySelectorAll('.css-1bn1yyx').forEach(element => {
  124. element.style.display = 'none';
  125. });
  126.  
  127. // Блокировка фоновых изображений с ella.janitorai.com/background-image/
  128. document.querySelectorAll('*').forEach(element => {
  129. const style = window.getComputedStyle(element);
  130. const bgImage = style.backgroundImage;
  131. if (bgImage && bgImage.includes('ella.janitorai.com/background-image/')) {
  132. element.style.backgroundImage = 'none';
  133. }
  134. });
  135. };
  136.  
  137. const updateControlText = () => {
  138. const sidebarText = document.getElementById('sidebar-toggle-text');
  139. const scrollText = document.getElementById('auto-scroll-text');
  140. const cssToggleText = document.getElementById('css-toggle-text');
  141. if (sidebarText) {
  142. sidebarText.textContent = isSidebarHidden ? 'Topbar: OFF' : 'Topbar: ON';
  143. sidebarText.style.color = isSidebarHidden ? '#fff' : '#ccc';
  144. }
  145. if (scrollText) {
  146. scrollText.textContent = `Auto-Scroll: ${isAutoScrollEnabled ? 'ON' : 'OFF'}`;
  147. scrollText.style.color = isAutoScrollEnabled ? '#fff' : '#ccc';
  148. }
  149. if (cssToggleText) {
  150. cssToggleText.textContent = `Custom CSS: ${isCustomCssDisabled ? 'OFF' : 'ON'}`;
  151. cssToggleText.style.color = isCustomCssDisabled ? '#fff' : '#ccc';
  152. }
  153. };
  154.  
  155. const createControlPanel = () => {
  156. if (controlPanel) return;
  157.  
  158. controlPanel = document.createElement('div');
  159. controlPanel.id = 'janitor-control-panel';
  160. Object.assign(controlPanel.style, {
  161. position: 'fixed',
  162. top: '75px',
  163. left: '10px',
  164. zIndex: '100000',
  165. display: 'flex',
  166. flexDirection: 'column',
  167. gap: '5px',
  168. alignItems: 'flex-start'
  169. });
  170.  
  171. const settingsButton = document.createElement('button');
  172. settingsButton.id = 'token-filter-toggle';
  173. settingsButton.textContent = '⚙️';
  174. Object.assign(settingsButton.style, {
  175. width: '30px',
  176. height: '30px',
  177. padding: '0',
  178. backgroundColor: 'rgba(74, 74, 74, 0.7)',
  179. color: '#fff',
  180. border: 'none',
  181. borderRadius: '5px',
  182. cursor: 'pointer',
  183. display: 'flex',
  184. alignItems: 'center',
  185. justifyContent: 'center',
  186. fontSize: '16px',
  187. transition: 'background-color 0.2s'
  188. });
  189. settingsButton.addEventListener('click', toggleMenu);
  190.  
  191. controlsContainer = document.createElement('div');
  192. controlsContainer.id = 'controls-container';
  193. Object.assign(controlsContainer.style, {
  194. display: 'none',
  195. flexDirection: 'column',
  196. gap: '5px',
  197. backgroundColor: 'rgba(74, 74, 74, 0.7)',
  198. padding: '5px',
  199. borderRadius: '5px',
  200. zIndex: '100001'
  201. });
  202.  
  203. const sidebarText = document.createElement('span');
  204. sidebarText.id = 'sidebar-toggle-text';
  205. sidebarText.style.cursor = 'pointer';
  206. sidebarText.style.fontSize = '12px';
  207. sidebarText.addEventListener('click', toggleSidebar);
  208.  
  209. const scrollText = document.createElement('span');
  210. scrollText.id = 'auto-scroll-text';
  211. scrollText.style.cursor = 'pointer';
  212. scrollText.style.fontSize = '12px';
  213. scrollText.addEventListener('click', toggleAutoScroll);
  214.  
  215. const cssToggleText = document.createElement('span');
  216. cssToggleText.id = 'css-toggle-text';
  217. cssToggleText.style.cursor = 'pointer';
  218. cssToggleText.style.fontSize = '12px';
  219. cssToggleText.addEventListener('click', toggleCustomCss);
  220.  
  221. controlsContainer.appendChild(sidebarText);
  222. controlsContainer.appendChild(scrollText);
  223. controlsContainer.appendChild(cssToggleText);
  224. controlPanel.appendChild(settingsButton);
  225. controlPanel.appendChild(controlsContainer);
  226. document.body.appendChild(controlPanel);
  227. updateControlText();
  228. };
  229.  
  230. const createOrUpdateSlider = () => {
  231. if (sliderElement) return;
  232.  
  233. sliderContainer = document.createElement('div');
  234. sliderContainer.id = 'token-filter-container';
  235. Object.assign(sliderContainer.style, {
  236. position: 'fixed',
  237. top: '75px',
  238. left: '50px',
  239. zIndex: '100002',
  240. display: 'none',
  241. flexDirection: 'row',
  242. alignItems: 'center',
  243. gap: '10px',
  244. padding: '5px',
  245. backgroundColor: 'rgba(74, 74, 74, 0.7)',
  246. borderRadius: '5px'
  247. });
  248.  
  249. sliderElement = document.createElement('input');
  250. sliderElement.type = 'range';
  251. sliderElement.id = 'token-filter-slider';
  252. Object.assign(sliderElement, {
  253. min: '0',
  254. max: '6000',
  255. step: '100',
  256. value: MIN_TOKENS
  257. });
  258. Object.assign(sliderElement.style, {
  259. width: '150px',
  260. height: '20px',
  261. backgroundColor: '#4a4a4a',
  262. cursor: 'pointer',
  263. appearance: 'none',
  264. outline: 'none',
  265. borderRadius: '5px',
  266. padding: '0',
  267. zIndex: '100003'
  268. });
  269.  
  270. const style = document.createElement('style');
  271. style.textContent = `
  272. #token-filter-slider {
  273. -webkit-appearance: none;
  274. appearance: none;
  275. background: #4a4a4a;
  276. border-radius: 5px;
  277. z-index: 100003;
  278. }
  279. #token-filter-slider::-webkit-slider-thumb {
  280. -webkit-appearance: none;
  281. appearance: none;
  282. width: 10px;
  283. height: 20px;
  284. background: #ffffff;
  285. cursor: pointer;
  286. border-radius: 50%;
  287. border: 2px solid #000;
  288. box-shadow: 0 0 2px rgba(0,0,0,0.5);
  289. transform: translateY(0px);
  290. z-index: 100004;
  291. }
  292. #token-filter-slider::-moz-range-thumb {
  293. width: 20px;
  294. height: 20px;
  295. background: #ffffff;
  296. cursor: pointer;
  297. border-radius: 50%;
  298. border: 2px solid #000;
  299. box-shadow: 0 0 2px rgba(0,0,0,0.5);
  300. transform: translateY(-10px);
  301. z-index: 100004;
  302. }
  303. #token-filter-slider::-webkit-slider-runnable-track {
  304. height: '10px',
  305. background: #4a4a4a;
  306. border-radius: 5px;
  307. }
  308. #token-filter-slider::-moz-range-track {
  309. height: '10px',
  310. background: #4a4a4a;
  311. border-radius: 5px;
  312. }
  313. `;
  314. document.head.appendChild(style);
  315.  
  316. const label = document.createElement('span');
  317. label.id = 'token-filter-label';
  318. label.style.color = '#fff';
  319. label.style.fontSize = '12px';
  320. label.style.minWidth = '60px';
  321. label.textContent = `${MIN_TOKENS} tokens`;
  322.  
  323. sliderElement.addEventListener('input', (e) => {
  324. MIN_TOKENS = parseInt(e.target.value);
  325. label.textContent = `${MIN_TOKENS} tokens`;
  326. localStorage.setItem('janitorAITokenFilter', MIN_TOKENS);
  327. filterCards();
  328. });
  329.  
  330. sliderContainer.appendChild(sliderElement);
  331. sliderContainer.appendChild(label);
  332. document.body.appendChild(sliderContainer);
  333. };
  334.  
  335. const applySidebarState = () => {
  336. const sidebar = document.querySelector('.css-h988mi');
  337. const css70qvj9 = document.querySelector('.css-70qvj9');
  338. emblaSlide = document.querySelector('.is-in-view.is-snapped.embla__slide');
  339.  
  340. if (sidebar && isSidebarHidden) {
  341. sidebar.style.display = 'none';
  342. if (emblaSlide) emblaSlide.style.display = 'none';
  343. if (css70qvj9) css70qvj9.style.display = 'none';
  344. }
  345. };
  346.  
  347. const updateElementsVisibility = () => {
  348. const shouldShow = isAllowedPage() && !isChatsPage();
  349. if (controlPanel) controlPanel.style.display = shouldShow ? 'flex' : 'none';
  350. if (sliderContainer) sliderContainer.style.display = shouldShow && isMenuVisible ? 'flex' : 'none';
  351. if (controlsContainer) controlsContainer.style.display = shouldShow && isMenuVisible ? 'flex' : 'none';
  352. };
  353.  
  354. const initialize = () => {
  355. createControlPanel();
  356. createOrUpdateSlider();
  357. applySidebarState();
  358. updateElementsVisibility();
  359.  
  360. if (isAllowedPage() && !isChatsPage()) {
  361. filterCards();
  362. setupPaginationScroll();
  363. applyCustomCssToggle();
  364.  
  365. new MutationObserver(() => {
  366. filterCards();
  367. setupPaginationScroll();
  368. applyCustomCssToggle();
  369. applySidebarState();
  370. }).observe(document.body, { childList: true, subtree: true });
  371.  
  372. const originalAppendChild = Element.prototype.appendChild;
  373. Element.prototype.appendChild = function(node) {
  374. if (isCustomCssDisabled && node.tagName === 'STYLE' && this.tagName === 'HEAD') {
  375. return node;
  376. }
  377. return originalAppendChild.call(this, node);
  378. };
  379. }
  380. };
  381.  
  382. const tryInitialize = () => {
  383. if (document.body) {
  384. initialize();
  385. let lastPath = window.location.pathname;
  386. setInterval(() => {
  387. if (lastPath !== window.location.pathname) {
  388. lastPath = window.location.pathname;
  389. updateElementsVisibility();
  390. if (isAllowedPage() && !isChatsPage()) {
  391. filterCards();
  392. setupPaginationScroll();
  393. applyCustomCssToggle();
  394. applySidebarState();
  395. }
  396. }
  397. }, 500);
  398. } else {
  399. setTimeout(tryInitialize, 1000);
  400. }
  401. };
  402.  
  403. tryInitialize();
  404. })();