// ==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);
})();
})();