您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
The right arrow changes color when the next click would generate a new output.
// ==UserScript== // @name JanitorAI – Right Arrow Generate Indicator // @namespace gf:jai-green-right // @version 1.0.6 // @description The right arrow changes color when the next click would generate a new output. // @match https://janitorai.com/chats* // @license MIT // @grant unsafeWindow // @inject-into page // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const W = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window; const SEL = { right: 'button[aria-label="Right"]', left: 'button[aria-label="Left"]', slider: 'div[class*="_botChoicesSlider_"]', }; // Styles 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; } .jai-yellow-arrow { outline: 3px solid #f59e0b !important; outline-offset: 2px !important; border-radius: 999px !important; } .jai-yellow-arrow svg { color: #f59e0b !important; fill: currentColor !important; } `; document.documentElement.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 }; // ---------- 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 pair() { const right = bottomMostRight(); const slider = right ? findNearestSliderFrom(right) : null; const left = slider ? slider.parentElement?.querySelector(SEL.left) : null; const changed = right !== ctx.right || slider !== ctx.slider || left !== ctx.left; if (!changed) return; if (ctx.sliderMO) ctx.sliderMO.disconnect(); ctx.right = right; ctx.slider = slider; ctx.left = left; 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 }); } updateTint(); } // ---------- index detection (no :scope; use children) ---------- function sliderItems(slider) { return Array.from(slider?.children || []).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 clamp(n, a, b) { return Math.max(a, Math.min(b, n)); } // ---------- tint logic ---------- function updateTint() { if (!ctx.right || !ctx.slider) return; const items = sliderItems(ctx.slider); if (!items.length) { ctx.right.classList.remove('jai-green-arrow', 'jai-yellow-arrow'); return; } const idx = currentIndex(ctx.slider); const last = items.length - 1; const secondLast = last - 1; ctx.right.classList.remove('jai-green-arrow', 'jai-yellow-arrow'); if (idx >= last) { ctx.right.classList.add('jai-green-arrow'); // last -> next click generates } else if (idx === secondLast) { ctx.right.classList.add('jai-yellow-arrow'); // one before last } } // ---------- keep it fresh ---------- function pulse(ms) { if (ctx.pulseTimer) W.clearInterval(ctx.pulseTimer); const endAt = performance.now() + ms; ctx.pulseTimer = W.setInterval(() => { if (performance.now() > endAt) { W.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)) pair(); }); ctx.rebinderMO.observe(document.body, { childList: true, subtree: true }); } // Pulse after generation POSTs (hook in PAGE context for TM) (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 = W.fetch; try { W.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); }; } catch {} const XO = W.XMLHttpRequest && W.XMLHttpRequest.prototype.open; const XS = W.XMLHttpRequest && W.XMLHttpRequest.prototype.send; if (XO && XS) { W.XMLHttpRequest.prototype.open = function(method, url) { this._m = (method || 'GET').toUpperCase(); this._u = url || ''; return XO.apply(this, arguments); }; W.XMLHttpRequest.prototype.send = function(body) { try { if (looksGen(this._u || '', this._m || 'GET')) pulse(5000); } catch {} return XS.apply(this, arguments); }; } })(); function hookSpa() { const H = W.history; if (H && H.pushState) { const origPush = H.pushState; H.pushState = function () { const r = origPush.apply(this, arguments); W.setTimeout(() => { pair(); updateTint(); }, 400); return r; }; W.addEventListener('popstate', () => W.setTimeout(() => { pair(); updateTint(); }, 400)); } W.addEventListener('resize', updateTint); } function boot() { pair(); startRebinder(); hookSpa(); // initial retries for late-mount let tries = 0; const iv = W.setInterval(() => { pair(); if (++tries > 40 || (ctx.right && ctx.slider)) W.clearInterval(iv); }, 200); } boot(); })();