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