您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
The right arrow turns green when the next click would generate a new output.
当前为
// ==UserScript== // @name JanitorAI – Right Arrow Generate Indicator // @namespace gf:jai-green-right // @version 1.0.1 // @description The right arrow turns green when the next click would generate a new output. // @match https://janitorai.com/* // @match https://janitorai.com/chats* // @license MIT // @grant none // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const SEL = { right: 'button[aria-label="Right"]', left: 'button[aria-label="Left"]', slider: 'div[class*="_botChoicesSlider_"]', }; const style = document.createElement('style'); style.textContent = ` .jai-green-arrow { outline: 3px solid #22c55e !important; outline-offset: 2px !important; border-radius: 999px !important; } .jai-green-arrow svg { color: #22c55e !important; fill: currentColor !important; } `; document.head.appendChild(style); const $ = (s, r = document) => r.querySelector(s); const $$ = (s, r = document) => Array.from(r.querySelectorAll(s)); let ctx = { right: null, left: null, slider: null, sliderMO: null, rebinderMO: null, pulseTimer: null }; // ---------- pairing ---------- function isVisible(el) { if (!el) return false; const cs = getComputedStyle(el); if (cs.display === 'none' || cs.visibility === 'hidden') return false; const r = el.getBoundingClientRect(); return r.width > 8 && r.height > 8; } function bottomMostRight() { const candidates = $$(SEL.right).filter(isVisible); if (!candidates.length) return null; candidates.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top); return candidates[candidates.length - 1]; // lowest on the page } function findNearestSliderFrom(node) { let el = node; for (let i = 0; el && i < 12; i++, el = el.parentElement) { const s = $(SEL.slider, el); if (s && isVisible(s)) return s; } // fallback: the last visible slider on page const all = $$(SEL.slider).filter(isVisible); return all[all.length - 1] || null; } function pair() { const right = bottomMostRight(); const slider = right ? findNearestSliderFrom(right) : null; const left = (slider ? slider.parentElement : document.body)?.querySelector(SEL.left) || null; const changed = right !== ctx.right || slider !== ctx.slider || left !== ctx.left; if (!changed) return; // unhook old slider observer if (ctx.sliderMO) ctx.sliderMO.disconnect(); ctx.right = right; ctx.slider = slider; ctx.left = left; // hook clicks to pulse recalcs if (ctx.right) { ctx.right.addEventListener('click', () => pulse(3000), { capture: true }); } if (ctx.left) { ctx.left.addEventListener('click', () => pulse(2000), { capture: true }); } // watch slider for changes/movement if (ctx.slider) { ctx.sliderMO = new MutationObserver(() => { setTimeout(updateTint, 30); setTimeout(updateTint, 200); setTimeout(updateTint, 700); }); ctx.sliderMO.observe(ctx.slider, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); const vp = getViewport(ctx.slider); vp.addEventListener('scroll', updateTint, { passive: true }); vp.addEventListener('transitionend', updateTint, { passive: true }); vp.addEventListener('animationend', updateTint, { passive: true }); } updateTint(); } function getViewport(slider) { let el = slider?.parentElement; while (el && el !== document.body) { const ox = getComputedStyle(el).overflowX; if (ox && ox !== 'visible') return el; el = el.parentElement; } return slider?.parentElement || slider; } // ---------- index detection ---------- function sliderItems(slider) { return $$(':scope > *', slider).filter(isVisible); } function indexFromTransform(slider, items) { const inline = slider.style.transform || ''; let m = inline.match(/translateX\((-?\d+(?:\.\d+)?)%\)/); if (m) return Math.min(Math.max(Math.round(Math.abs(parseFloat(m[1])) / 100), 0), items.length - 1); const comp = getComputedStyle(slider).transform; if (comp && comp !== 'none') { const mm = comp.match(/matrix\(([^)]+)\)/); if (mm) { const parts = mm[1].split(',').map(s => parseFloat(s.trim())); const tx = parts[4] || 0; const w = items[0]?.getBoundingClientRect().width || 1; return Math.min(Math.max(Math.round(Math.abs(tx) / w), 0), items.length - 1); } } return null; } function indexByViewport(slider, items) { const vp = getViewport(slider).getBoundingClientRect(); const centerV = vp.left + vp.width / 2; let best = 0, bestDist = Infinity; for (let i = 0; i < items.length; i++) { const r = items[i].getBoundingClientRect(); const dist = Math.abs((r.left + r.width / 2) - centerV); if (dist < bestDist) { bestDist = dist; best = i; } } return best; } function currentIndex(slider) { const items = sliderItems(slider); if (!items.length) return 0; return indexFromTransform(slider, items) ?? indexByViewport(slider, items); } // ---------- tint logic ---------- function shouldBeGreen() { if (!ctx.right || !ctx.slider) return false; const items = sliderItems(ctx.slider); if (!items.length) return false; const idx = currentIndex(ctx.slider); return idx >= items.length - 1; // last saved variant → next click generates } function updateTint() { if (!ctx.right) return; ctx.right.classList.toggle('jai-green-arrow', shouldBeGreen()); } // ---------- keep it fresh ---------- function pulse(ms) { if (ctx.pulseTimer) clearInterval(ctx.pulseTimer); const endAt = performance.now() + ms; ctx.pulseTimer = setInterval(() => { if (performance.now() > endAt) { clearInterval(ctx.pulseTimer); ctx.pulseTimer = null; } pair(); updateTint(); }, 180); } // Re-pair when React swaps nodes function startRebinder() { if (ctx.rebinderMO) ctx.rebinderMO.disconnect(); ctx.rebinderMO = new MutationObserver(() => { if (!document.contains(ctx.right) || !document.contains(ctx.slider)) pair(); }); ctx.rebinderMO.observe(document.body, { childList: true, subtree: true }); } // Nudge after generation POSTs (function patchNetwork() { const looksGen = (url, method) => method === 'POST' && /janitorai\.com/i.test(url) && /(generate|regenerate|completion|completions|respond|messages|chat)/i.test(url); const _fetch = window.fetch; window.fetch = function(resource, init = {}) { try { const url = typeof resource === 'string' ? resource : resource.url; const method = (init?.method || 'GET').toUpperCase(); if (looksGen(url, method)) pulse(5000); } catch {} return _fetch.apply(this, arguments); }; const XO = XMLHttpRequest.prototype.open; const XS = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this._m = (method || 'GET').toUpperCase(); this._u = url || ''; return XO.apply(this, arguments); }; XMLHttpRequest.prototype.send = function(body) { try { if (looksGen(this._u || '', this._m || 'GET')) pulse(5000); } catch {} return XS.apply(this, arguments); }; })(); // SPA nav + boot function hookSpa() { const origPush = history.pushState; history.pushState = function () { const r = origPush.apply(this, arguments); setTimeout(() => { pair(); updateTint(); }, 400); return r; }; window.addEventListener('popstate', () => setTimeout(() => { pair(); updateTint(); }, 400)); window.addEventListener('resize', updateTint); } function boot() { pair(); startRebinder(); hookSpa(); // initial retries for late mount let tries = 0; const iv = setInterval(() => { pair(); if (++tries > 40 || (ctx.right && ctx.slider)) clearInterval(iv); }, 200); } boot(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址