Greasy Fork 还支持 简体中文。

JanitorAI Enhanced UI

Adds UI controls for JanitorAI, hides buttons on chat pages

目前為 2025-04-09 提交的版本,檢視 最新版本

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