JanitorAI Improvements: Tokens+Proxy/Definition check+Custom emojis...

Adds a smart context-sensitive UI to JanitorAI, featuring: advanced token filter, persistent auto-pagination, proxy/definition checker, context-dependent menu (full controls on main, compact emoji menu on chat), animated emoji for 'replying...', and toggles in the settings panel to show/hide proxy & definition icons or change opacity of cards.

// ==UserScript==
// @name         JanitorAI Improvements: Tokens+Proxy/Definition check+Custom emojis...
// @namespace    http://tampermonkey.net/
// @version      2025.09.20.3
// @author       WolfgangNoir
// @description  Adds a smart context-sensitive UI to JanitorAI, featuring: advanced token filter, persistent auto-pagination, proxy/definition checker, context-dependent menu (full controls on main, compact emoji menu on chat), animated emoji for 'replying...', and toggles in the settings panel to show/hide proxy & definition icons or change opacity of cards.
// @match        https://janitorai.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @icon         https://files.catbox.moe/jer5m2.png
// ==/UserScript==

(function () {
  'use strict';
  if (location.pathname === '/my_characters') return;

  // -- PERSISTENT USER OPTION UTILS --
  function getProxyCheckEnabled() {
    return typeof GM_getValue !== "undefined" ? GM_getValue("proxyCheckEnabled", true) : true;
  }
  function setProxyCheckEnabled(val) {
    if (typeof GM_setValue !== "undefined") GM_setValue("proxyCheckEnabled", !!val);
  }
  function getDefinitionCheckEnabled() {
    return typeof GM_getValue !== "undefined" ? GM_getValue("defCheckEnabled", true) : true;
  }
  function setDefinitionCheckEnabled(val) {
    if (typeof GM_setValue !== "undefined") GM_setValue("defCheckEnabled", !!val);
  }
  function getHideOnBadProxy() {
    return typeof GM_getValue !== "undefined" ? GM_getValue("hideOnBadProxy", false) : false;
  }
  function setHideOnBadProxy(val) {
    if (typeof GM_setValue !== "undefined") GM_setValue("hideOnBadProxy", !!val);
  }
  function getHideOnHiddenDef() {
    return typeof GM_getValue !== "undefined" ? GM_getValue("hideOnHiddenDef", false) : false;
  }
  function setHideOnHiddenDef(val) {
    if (typeof GM_setValue !== "undefined") GM_setValue("hideOnHiddenDef", !!val);
  }
  function getAnimatedEmoji() {
    return typeof GM_getValue !== "undefined" ? GM_getValue("animatedEmoji", "🦈") : "🦈";
  }
  function setAnimatedEmoji(emoji) {
    if (typeof GM_setValue !== "undefined" && emoji.length > 0) GM_setValue("animatedEmoji", emoji);
  }
  function getEmojiMenuEnabled() {
    if (typeof GM_getValue !== "undefined") {
      return GM_getValue("emojiMenuEnabled", true);
    }
    return true;
  }
  function setEmojiMenuEnabled(val) {
    if (typeof GM_setValue !== "undefined") {
      GM_setValue("emojiMenuEnabled", !!val);
    }
  }

  // ------------- MINI-EMOJI-MENU EN CHAT -------------
  function insertEmojiOnlyMenuToggleable() {
    if (!getEmojiMenuEnabled()) {
      const panel = document.getElementById('janitorai-emoji-only-ui');
      if (panel) panel.remove();
      const btn = document.getElementById('janitorai-emoji-toggle-btn');
      if (btn) btn.remove();
      return;
    }
    if (document.getElementById('janitorai-emoji-toggle-btn')) return;
    const toggleBtn = document.createElement('button');
    toggleBtn.id = 'janitorai-emoji-toggle-btn';
    toggleBtn.title = 'Show/Hide emoji selector';
    toggleBtn.style.position = 'fixed';
    toggleBtn.style.top = '75px';
    toggleBtn.style.left = '10px';
    toggleBtn.style.zIndex = '100001';
    toggleBtn.style.width = '32px';
    toggleBtn.style.height = '32px';
    toggleBtn.style.fontSize = '22px';
    toggleBtn.style.background = 'rgba(48,48,48,0.92)';
    toggleBtn.style.border = 'none';
    toggleBtn.style.borderRadius = '50%';
    toggleBtn.style.display = 'flex';
    toggleBtn.style.alignItems = 'center';
    toggleBtn.style.justifyContent = 'center';
    toggleBtn.style.cursor = 'pointer';
    toggleBtn.style.boxShadow = '0 2px 8px #0007';
    toggleBtn.textContent = '👀';
    if (!document.getElementById('janitorai-emoji-only-ui')) {
      const container = document.createElement('div');
      container.id = "janitorai-emoji-only-ui";
      container.style.position = 'fixed';
      container.style.top = '118px';
      container.style.left = '10px';
      container.style.zIndex = '99999';
      container.style.background = 'rgba(40,40,60,0.98)';
      container.style.padding = '14px 18px';
      container.style.borderRadius = '14px';
      container.style.fontFamily = 'sans-serif';
      container.style.fontSize = '15px';
      container.style.color = '#fff';
      container.style.boxShadow = '0 2px 12px #111a';
      container.style.display = 'none';
      container.innerHTML = `
        <div style="margin-bottom:3px;"><b>Custom emoji for "replying..."</b></div>
        <input id="janitorai-emoji-input" type="text" maxlength="2" value="${getAnimatedEmoji()}" style="width:38px; text-align:center; font-size:18px;">
        <button id="janitorai-emoji-save" style="margin-left:5px;">Save emoji</button>
        <span style="font-size:12px;color:#aaa;">Will take effect instantly.</span>
      `;
      document.body.appendChild(container);
      document.getElementById('janitorai-emoji-save').onclick = function () {
        const em = document.getElementById('janitorai-emoji-input').value.trim();
        if (em.length > 0) {
          setAnimatedEmoji(em);
          alert("Emoji saved: " + em);
        } else {
          alert("Please enter a valid emoji.");
        }
      };
    }
    document.body.appendChild(toggleBtn);
    toggleBtn.onclick = function () {
      const panel = document.getElementById('janitorai-emoji-only-ui');
      if (panel) {
        panel.style.display = (panel.style.display === 'none') ? '' : 'none';
      }
    };
  }

  // ---------- MENÚ AVANZADO, TOKEN, PROXY, DEF HIDE, ETC ----------
  const TOKEN_FILTER_KEY = 'janitorAITokenFilter';
  const MENU_VISIBLE_KEY = 'janitorAIMenuVisible';
  const PAGINATION_KEY = 'janitorAIPaginationOn';
  const DEFAULT_MIN_TOKENS = 500;
  let minTokens = parseInt(localStorage.getItem(TOKEN_FILTER_KEY), 10) || DEFAULT_MIN_TOKENS;
  let paginationEnabled = localStorage.getItem(PAGINATION_KEY) === 'true';
  let controlPanel = null;
  function insertUI() {
    if (!document.body) return setTimeout(insertUI, 300);
    if (!document.getElementById('janitor-control-panel')) {
      controlPanel = document.createElement('div');
      controlPanel.id = 'janitor-control-panel';
      Object.assign(controlPanel.style, {
        position: 'fixed',
        top: '75px',
        left: '10px',
        zIndex: '100000',
        display: 'flex',
        flexDirection: 'column',
        gap: '5px',
        alignItems: 'flex-start'
      });
      const settingsButton = document.createElement('button');
      settingsButton.id = 'token-filter-toggle';
      settingsButton.textContent = '🛠️';
      Object.assign(settingsButton.style, {
        width: '30px',
        height: '30px',
        padding: '0',
        backgroundColor: 'rgba(74, 74, 74, 0.7)',
        color: '#fff',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        fontSize: '16px',
        transition: 'background-color 0.2s'
      });
      settingsButton.title = 'Show/hide advanced menu';
      settingsButton.onclick = function () {
        const menu = document.getElementById('janitorai-enhanced-ui');
        if (menu) {
          menu.style.display = menu.style.display === 'none' ? '' : 'none';
          localStorage.setItem(MENU_VISIBLE_KEY, menu.style.display !== 'none');
        }
      };
      controlPanel.appendChild(settingsButton);
      document.body.appendChild(controlPanel);
    }
    if (document.getElementById('janitorai-enhanced-ui')) return;
    const container = document.createElement('div');
    container.id = "janitorai-enhanced-ui";
    container.style.position = 'fixed';
    container.style.top = '38px';
    container.style.left = '50px';
    container.style.zIndex = '99999';
    container.style.background = 'rgba(40,40,60,0.98)';
    container.style.padding = '14px 18px';
    container.style.borderRadius = '14px';
    container.style.fontFamily = 'sans-serif';
    container.style.fontSize = '15px';
    container.style.color = '#fff';
    container.style.boxShadow = '0 2px 12px #111a';
    container.style.display = localStorage.getItem(MENU_VISIBLE_KEY) === 'true' ? '' : 'none';
    container.innerHTML =
      `<b>JanitorAI Improvements:</b><br/>` +
      `<label style="display:flex;align-items:center;margin-bottom:3px;">
        <input type="checkbox" id="janitorai-proxy-toggle-checkbox" style="margin-right:6px;" ${getProxyCheckEnabled() ? "checked" : ""}>
        Enable proxy status icon
      </label>` +
      `<label style="display:flex;align-items:center;margin-bottom:10px;">
        <input type="checkbox" id="janitorai-def-toggle-checkbox" style="margin-right:6px;" ${getDefinitionCheckEnabled() ? "checked" : ""}>
        Enable definition status icon
      </label>` +
      `<label style="display:flex;align-items:center;margin-bottom:3px;">
        <input type="checkbox" id="janitorai-hide-proxy-checkbox" style="margin-right:6px;" ${getHideOnBadProxy() ? "checked" : ""}>
        Change opacity if proxy is not allowed
      </label>` +
      `<label style="display:flex;align-items:center;margin-bottom:10px;">
        <input type="checkbox" id="janitorai-hide-def-checkbox" style="margin-right:6px;" ${getHideOnHiddenDef() ? "checked" : ""}>
        Change opacity if definition is hidden
      </label>` +
      `Min tokens:
      <input id="janitorai-token-input" type="number" value="${minTokens}" style="width:80px;">
      <button id="janitorai-token-save">Filter</button>
      <hr style="margin:8px 0;">
      <button id="janitorai-toggle-pagination">Auto-pagination: <span id="janitorai-pagination-state">${paginationEnabled ? 'ON' : 'OFF'}</span></button>
      <hr style="margin:8px 0;">
      <label style="display:flex;align-items:center;margin-bottom:7px;">
        <input type="checkbox" id="janitorai-emoji-toggle-checkbox" style="margin-right:6px;" ${getEmojiMenuEnabled() ? "checked" : ""}>
        Show emoji menu 👀 in chat page:
      </label>
      <div style="margin-bottom:3px;"><b>Custom emoji for "replying..."</b></div>
      <input id="janitorai-emoji-input" type="text" maxlength="2" value="${getAnimatedEmoji()}" style="width:38px; text-align:center; font-size:18px;">
      <button id="janitorai-emoji-save" style="margin-left:5px;">Save emoji</button>
      <span style="font-size:12px;color:#aaa;">Will take effect instantly.</span>
    `;
    document.body.appendChild(container);
    document.getElementById('janitorai-proxy-toggle-checkbox').onchange = function(e) {
      setProxyCheckEnabled(e.target.checked);
      document.querySelectorAll('.proxy-status-icon').forEach(ic => ic.remove());
      document.querySelectorAll('.profile-character-card-stack-link-component').forEach(card => card.removeAttribute('custom-icons-checked'));
    };
    document.getElementById('janitorai-def-toggle-checkbox').onchange = function(e) {
      setDefinitionCheckEnabled(e.target.checked);
      document.querySelectorAll('.definition-status-icon').forEach(ic => ic.remove());
      document.querySelectorAll('.profile-character-card-stack-link-component').forEach(card => card.removeAttribute('custom-icons-checked'));
    };
    document.getElementById('janitorai-hide-proxy-checkbox').onchange = function(e) {
      setHideOnBadProxy(e.target.checked);
      document.querySelectorAll('.profile-character-card-stack-link-component').forEach(card => card.removeAttribute('custom-icons-checked'));
    };
    document.getElementById('janitorai-hide-def-checkbox').onchange = function(e) {
      setHideOnHiddenDef(e.target.checked);
      document.querySelectorAll('.profile-character-card-stack-link-component').forEach(card => card.removeAttribute('custom-icons-checked'));
    };
    document.getElementById('janitorai-token-save').onclick = function () {
      const value = parseInt(document.getElementById('janitorai-token-input').value, 10);
      minTokens = isNaN(value) ? DEFAULT_MIN_TOKENS : value;
      localStorage.setItem(TOKEN_FILTER_KEY, minTokens);
      filterCards();
    };
    document.getElementById('janitorai-toggle-pagination').onclick = function () {
      paginationEnabled = !paginationEnabled;
      document.getElementById('janitorai-pagination-state').innerText = paginationEnabled ? "ON" : "OFF";
      localStorage.setItem(PAGINATION_KEY, paginationEnabled);
    };
    document.getElementById('janitorai-emoji-save').onclick = function () {
      const em = document.getElementById('janitorai-emoji-input').value.trim();
      if (em.length > 0) {
        setAnimatedEmoji(em);
        alert("Emoji saved: " + em);
      } else {
        alert("Please enter a valid emoji.");
      }
    };
    document.getElementById('janitorai-emoji-toggle-checkbox').onchange = function (e) {
      setEmojiMenuEnabled(e.target.checked);
      if (location.pathname.startsWith('/chats')) location.reload();
    };
  }
  function parseTokens(cardElement) {
    const tokenSpan = cardElement.querySelector(".chakra-text.pp-cc-tokens-count.profile-character-card-tokens-count");
    if (tokenSpan) {
      const raw = tokenSpan.textContent.replace(/\s+tokens?/i, '').trim();
      let tokens;
      if (raw.endsWith('k')) {
        tokens = parseFloat(raw) * 1000;
      } else {
        tokens = parseInt(raw.replace(/\D/g, ''), 10);
      }
      return tokens;
    }
    return null;
  }
  function filterCards() {
    const cards = document.querySelectorAll('.pp-cc-wrapper.profile-character-card-wrapper');
    cards.forEach(card => {
      const tokens = parseTokens(card);
      card.style.display = (tokens !== null && tokens >= minTokens) ? '' : 'none';
    });
  }

  // ---- AUTO PAGINACIÓN ----
  (function() {
    let isNavigating = false;
    let scrollCount = 0;
    const requiredScrolls = 3;
    const pageDelay = 2000;
    function isAtVeryBottom() {
      const scrollPosition = window.scrollY + window.innerHeight;
      const pageHeight = document.documentElement.scrollHeight;
      return pageHeight - scrollPosition <= 1;
    }
    function getNextPageElement() {
      return document.querySelector('button[aria-label="Next Page"]:not([disabled]),button.profile-pagination-next-button:not([disabled])');
    }
    window.addEventListener('wheel', function(event) {
      if (!paginationEnabled || isNavigating) return;
      if (event.deltaY > 0 && isAtVeryBottom()) {
        scrollCount++;
        if (scrollCount >= requiredScrolls) {
          const nextPage = getNextPageElement();
          if (nextPage) {
            isNavigating = true;
            nextPage.click();
            setTimeout(() => {
              isNavigating = false;
              scrollCount = 0;
            }, pageDelay);
          }
        }
      } else if (!isAtVeryBottom()) {
        scrollCount = 0;
      }
    }, { passive: true });
  })();

  const observerDom = new MutationObserver(() => {
    insertUI();
    filterCards();
  });

  // == PROXY + DEFINITION STATUS ICONS + OPACITY BEHAVIOR ==
  const proxyAllowedForKey = "paf_cache_";
  async function proxyAllowedFor(characterURL) {
    const key = proxyAllowedForKey + characterURL;
    const cache = typeof GM_getValue === "function" ? GM_getValue(key) : null;
    if (cache) {
      const { allowed, timestamp } = JSON.parse(cache);
      if (Date.now() - timestamp < 86400000) {
        return { allowed: allowed, cached: true };
      }
    }
    const response = await fetch(characterURL);
    const page = await response.text();
    const allowed = page.includes("<div>proxy allowed</div>");
    if(typeof GM_setValue === "function") {
      GM_setValue(key, JSON.stringify({ allowed: allowed, timestamp: Date.now() }));
    }
    return { allowed: allowed, cached: false };
  }
  async function definitionIsHidden(characterURL) {
    try {
      const response = await fetch(characterURL);
      const html = await response.text();
      const parser = new DOMParser();
      const doc = parser.parseFromString(html, "text/html");
      const elements = doc.querySelectorAll('h4, div, span');
      for (let el of elements) {
        if (el.textContent && el.textContent.includes("Character Definition is hidden")) {
          return true;
        }
      }
      return false;
    } catch (e) {
      return false;
    }
  }
  setInterval(async function () {
    if (location.pathname.startsWith('/chats')) return;
    const hideOnBadProxy = getHideOnBadProxy();
    const hideOnHiddenDef = getHideOnHiddenDef();
    const showProxy = getProxyCheckEnabled();
    const showDef = getDefinitionCheckEnabled();
    const characterCardElements = [...document.querySelectorAll(".profile-character-card-stack-link-component")];
    for (let i = 0; i < characterCardElements.length; i++) {
      const element = characterCardElements[i];
      if (!document.body.contains(element)) continue;
      if (!element.getAttribute("custom-icons-checked")) {
        element.setAttribute("custom-icons-checked", "yes");
        const cardWrapper = element.parentElement.parentElement;
        const titleElement = element.children[0] && element.children[0].children[0];
        let proxyRes = { allowed: true };
        let defRes = false;
        if (showProxy || hideOnBadProxy) proxyRes = await proxyAllowedFor(element.href);
        if (showDef || hideOnHiddenDef) defRes = await definitionIsHidden(element.href);
        let faded = false;
        if (hideOnBadProxy && !proxyRes.allowed) faded = true;
        if (hideOnHiddenDef && defRes) faded = true;
        if (cardWrapper) {
          cardWrapper.style.opacity = faded ? 0.25 : 1;
          cardWrapper.style.pointerEvents = faded ? "none" : "";
        }
        ['proxy-status-icon','definition-status-icon'].forEach(cls => {
          const old = titleElement && titleElement.querySelector('.'+cls);
          if (old) old.remove();
        });
        if (showDef) {
          const defIcon = document.createElement('span');
          defIcon.className = 'definition-status-icon';
          defIcon.style.marginRight = "6px";
          defIcon.style.fontSize = "1.2em";
          defIcon.textContent = defRes ? "❌" : "✅";
          defIcon.title = defRes ? "Definition is hidden" : "Definition visible";
          if (titleElement && titleElement.firstChild) titleElement.insertBefore(defIcon, titleElement.firstChild);
          else if (titleElement) titleElement.appendChild(defIcon);
        }
        if (showProxy) {
          const proxyIcon = document.createElement('span');
          proxyIcon.className = 'proxy-status-icon';
          proxyIcon.style.marginRight = "6px";
          proxyIcon.style.fontSize = "1.2em";
          proxyIcon.textContent = proxyRes.allowed ? "🟢" : "🔴";
          proxyIcon.title = proxyRes.allowed ? "Proxy OK" : "Proxy not allowed";
          if (titleElement && titleElement.firstChild) titleElement.insertBefore(proxyIcon, titleElement.firstChild);
          else if (titleElement) titleElement.appendChild(proxyIcon);
        }
      }
    }
  }, 2500);

  // ----------- CONTROLADOR DE NAVEGACIÓN SPA -----------
  let lastLocation = location.pathname;
  setInterval(() => {
    if (location.pathname !== lastLocation) {
      lastLocation = location.pathname;
      onPageChange();
    }
  }, 400);

  function onPageChange() {
    document.getElementById('janitor-control-panel')?.remove();
    document.getElementById('janitorai-enhanced-ui')?.remove();
    document.getElementById('janitorai-emoji-toggle-btn')?.remove();
    document.getElementById('janitorai-emoji-only-ui')?.remove();

    if (location.pathname.startsWith('/chats')) {
      insertEmojiOnlyMenuToggleable();
    } else {
      insertUI();
      filterCards();
      observerDom.observe(document.body, {childList: true, subtree: true});
    }
  }

  // --------- EMOJI REPLYING OVERLAY ---------
  (() => {
    const ANIMATION_FRAMES = () => {
      const emoji = getAnimatedEmoji();
      return [
        emoji,
        emoji + emoji,
        emoji + emoji + emoji
      ];
    };
    const FRAME_INTERVAL = 500;
    const seenContainers = new WeakSet();
    function createEmojiOverlay(p) {
      if (p.dataset.emojiOverlayAttached) return;
      let parent = p.parentElement;
      while (parent && getComputedStyle(parent).position === 'static') parent = parent.parentElement;
      if (!parent) return;
      if (getComputedStyle(parent).position === 'static') parent.style.position = 'relative';
      const overlay = document.createElement('div');
      overlay.style.position = 'absolute';
      overlay.style.pointerEvents = 'none';
      overlay.style.left = `${p.offsetLeft}px`;
      overlay.style.top = `${p.offsetTop}px`;
      overlay.style.font = getComputedStyle(p).font;
      overlay.style.color = getComputedStyle(p).color;
      overlay.style.zIndex = '9999';
      overlay.style.whiteSpace = 'pre';
      const shadow = overlay.attachShadow({ mode: 'open' });
      const span = document.createElement('span');
      shadow.appendChild(span);
      let frameIndex = 0;
      const interval = setInterval(() => {
        if (!document.body.contains(p)) {
          clearInterval(interval);
          overlay.remove();
          return;
        }
        if (!/^replying/i.test(p.innerText.trim())) {
          clearInterval(interval);
          overlay.remove();
          p.removeAttribute('data-emoji-overlay-attached');
          p.style.opacity = '';
          return;
        }
        span.textContent = ANIMATION_FRAMES()[frameIndex++ % ANIMATION_FRAMES().length];
      }, FRAME_INTERVAL);
      parent.appendChild(overlay);
      p.dataset.emojiOverlayAttached = 'true';
      p.style.opacity = '0';
    }
    function handleParagraph(p) {
      if (/^replying\.*/i.test(p.innerText.trim())) {
        createEmojiOverlay(p);
      }
    }
    function observeContainer(container) {
      if (seenContainers.has(container)) return;
      seenContainers.add(container);
      const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
          mutation.addedNodes.forEach(node => {
            if (node.nodeType === 1) {
              if (node.tagName === 'P') {
                handleParagraph(node);
              } else {
                node.querySelectorAll?.('p')?.forEach(handleParagraph);
              }
            }
          });
          if (mutation.type === 'characterData' && mutation.target.nodeType === 3) {
            const p = mutation.target.parentElement;
            if (p?.tagName === 'P') {
              handleParagraph(p);
            }
          }
        });
      });
      observer.observe(container, {
        childList: true,
        subtree: true,
        characterData: true,
      });
      container.querySelectorAll('p').forEach(handleParagraph);
    }
    function scanAndObserve() {
      const containers = document.querySelectorAll('div.css-zpjg');
      containers.forEach(observeContainer);
    }
    scanAndObserve();
    setInterval(scanAndObserve, 500);
  })();

  // Inicializador al cargar
  onPageChange();
(function forceTokenInputContrast() {
  const style = document.createElement('style');
  style.innerHTML = `
    #janitorai-token-input {
      background: #fff !important;
      color: #111 !important;
      border: 1px solid #aaa !important;
    }
  `;
  document.head.appendChild(style);
})();

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址