您需要先安装一个扩展,例如 篡改猴、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.2 // @description The right arrow turns green when the next click would generate a new output. // @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, container: null, counterEl: null, sliderMO: null, counterMO: null, rebinderMO: null, pulseTimer: null }; // ---------- utils ---------- function isVisible(el) { if (!el) return false; const cs = getComputedStyle(el); if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false; const r = el.getBoundingClientRect(); return r.width > 8 && r.height > 8; } 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; } // ---------- pairing ---------- 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]; } 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; } const all = $$(SEL.slider).filter(isVisible); return all[all.length - 1] || null; } function findContainerFrom(node) { // Walk upwards to a block that contains both arrows and the slider let el = node; for (let i = 0; el && i < 12; i++, el = el.parentElement) { const hasRight = $(SEL.right, el); const hasLeft = $(SEL.left, el); const hasSlider= $(SEL.slider, el); if ((hasRight || hasLeft) && hasSlider) return el; } return null; } function findCounterEl(container) { if (!container) return null; // Look for a short text like "1/3" or "1 / 3" near the arrows const nodes = $$( 'span,div,p,small,strong,em', container ).filter(isVisible); let best = null, bestDist = Infinity; const right = $(SEL.right, container); const rbox = right ? right.getBoundingClientRect() : null; for (const n of nodes) { const t = (n.textContent || '').trim(); if (t.length > 10) continue; const m = t.match(/^(\d+)\s*\/\s*(\d+)$/); if (!m) continue; if (!rbox) { best = n; break; } const box = n.getBoundingClientRect(); const dist = Math.hypot((box.left + box.right)/2 - (rbox.left + rbox.right)/2, (box.top + box.bottom)/2 - (rbox.top + rbox.bottom)/2); if (dist < bestDist) { bestDist = dist; best = n; } } return best; } function pair() { const right = bottomMostRight(); const slider = right ? findNearestSliderFrom(right) : null; const container = right ? findContainerFrom(right) : null; const left = container ? $(SEL.left, container) : null; const counterEl = findCounterEl(container); const changed = right !== ctx.right || slider !== ctx.slider || left !== ctx.left || container !== ctx.container || counterEl !== ctx.counterEl; if (!changed) return; if (ctx.sliderMO) ctx.sliderMO.disconnect(); if (ctx.counterMO) ctx.counterMO.disconnect(); ctx.right = right; ctx.left = left; ctx.slider = slider; ctx.container = container; ctx.counterEl = counterEl; if (ctx.right) ctx.right.addEventListener('click', () => pulse(3000), { capture: true }); if (ctx.left) ctx.left.addEventListener('click', () => pulse(2000), { capture: true }); 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 }); } if (ctx.counterEl) { ctx.counterMO = new MutationObserver(() => { setTimeout(updateTint, 10); setTimeout(updateTint, 150); }); ctx.counterMO.observe(ctx.counterEl, { childList: true, subtree: true, characterData: true }); } updateTint(); } // ---------- index & counter ---------- 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 clamp(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 clamp(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); } function readCounter() { if (!ctx.counterEl) return null; const t = (ctx.counterEl.textContent || '').trim(); const m = t.match(/^(\d+)\s*\/\s*(\d+)$/); if (!m) return null; const cur = parseInt(m[1], 10); const tot = parseInt(m[2], 10); if (!isFinite(cur) || !isFinite(tot) || tot <= 0) return null; return { cur, tot }; // 1-based } function clamp(n, a, b) { return Math.max(a, Math.min(b, n)); } // ---------- tint logic ---------- function shouldBeGreen() { if (!ctx.right || !isVisible(ctx.right)) return false; // Prefer the explicit counter if available const c = readCounter(); if (c) { // Green only if we're on the last known variant (cur == tot) return c.cur >= c.tot; } // Fallback to slider math if (!ctx.slider || !isVisible(ctx.slider)) return false; const items = sliderItems(ctx.slider); if (!items.length) return false; const idx = currentIndex(ctx.slider); // 0-based return idx >= items.length - 1; } 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(); }, 160); } function startRebinder() { if (ctx.rebinderMO) ctx.rebinderMO.disconnect(); ctx.rebinderMO = new MutationObserver(() => { if (!document.contains(ctx.right) || !document.contains(ctx.slider) || (ctx.counterEl && !document.contains(ctx.counterEl))) { pair(); } }); ctx.rebinderMO.observe(document.body, { childList: true, subtree: true }); } // Pulse after generation POSTs (new variant added) (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(); let tries = 0; const iv = setInterval(() => { pair(); if (++tries > 40 || (ctx.right && ctx.slider)) clearInterval(iv); }, 200); } boot(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址