您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
makes chatgpt pretty
// ==UserScript== // @name chatgpt burrito // @namespace https://spin.rip/ // @match https://chatgpt.com/* // @grant none // @version 1.1 // @author Spinfal // @description makes chatgpt pretty // @license AGPL-3.0 // @run-at document-start // ==/UserScript== (function () { /* * official image from the chatgpt 5 launch. * in the case that this image no longer loads, you can switch to: https://cdn.spin.rip/r/1920.webp * * or you can use your own image */ const imageUrl = 'https://persistent.oaistatic.com/burrito-nux/1920.webp'; const navHeaderImageUrl = 'https://cdn.spin.rip/r/diagrainbow.png'; const hideContent = false; const css = ` /* let bg show through */ html, body { background: transparent !important; } body { position: relative !important; min-height: 100vh !important; } /* dimmed + blurred page backdrop */ body::before { content: '' !important; position: fixed !important; inset: 0 !important; background: url("${imageUrl}") center/cover no-repeat !important; filter: brightness(0.25) blur(90px) !important; transform: scale(1.03) !important; pointer-events: none !important; z-index: -1 !important; } /* kill the growing fade at the bottom area */ #thread-bottom-container.content-fade::before, #thread-bottom-container.content-fade::after, #thread-bottom-container .vertical-scroll-fade-mask::before, #thread-bottom-container .vertical-scroll-fade-mask::after, #thread-bottom-container .horizontal-scroll-fade-mask::before, #thread-bottom-container .horizontal-scroll-fade-mask::after { content: none !important; display: none !important; } #thread-bottom-container.content-fade, #thread-bottom-container .vertical-scroll-fade-mask, #thread-bottom-container .horizontal-scroll-fade-mask { -webkit-mask: none !important; mask: none !important; -webkit-mask-image: none !important; mask-image: none !important; background-image: none !important; box-shadow: none !important; } /* ---- glass composer ---- */ .spin-glass-composer { background: rgba(18,18,18,0.35) !important; -webkit-backdrop-filter: blur(16px) saturate(120%); backdrop-filter: blur(16px) saturate(120%); border: 1px solid rgba(255,255,255,0.12) !important; box-shadow: 0 10px 30px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.10); position: relative !important; overflow: visible !important; z-index: 1; transition: box-shadow .2s ease, transform .2s ease; } .spin-glass-composer:hover { box-shadow: 0 14px 40px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.12); transform: translateY(-1px); } .spin-glass-composer::before { content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.06); } .spin-glass-composer:focus-within::after { content: ''; position: absolute; inset: -6px; border-radius: inherit; pointer-events: none; box-shadow: 0 0 30px rgba(123,220,255,0.12); } /* moving border highlight that follows nearest edge to cursor */ .spin-glow-ring { position: absolute; inset: 0; padding: 2px; border-radius: inherit; pointer-events: none; --glow-color: rgba(255, 255, 255, 0.60); --mx: 50%; --my: 50%; --glow-o: 0; background: radial-gradient(160px 160px at var(--mx) var(--my), var(--glow-color) 0, rgba(123, 220, 255, 0) 70%); filter: drop-shadow(0 0 10px var(--glow-color)) drop-shadow(0 0 18px var(--glow-color)); opacity: var(--glow-o); transition: opacity .18s ease-out; -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask-composite: exclude; } /* soft ambient when focused */ .spin-glass-composer:focus-within::after { content: ''; position: absolute; inset: -6px; border-radius: inherit; pointer-events: none; box-shadow: 0 0 30px rgba(123, 220, 255, 0.12); } /* navbar, top section, and profile button */ #stage-slideover-sidebar nav::before { content: ""; position: absolute; inset: 0; background: url("${imageUrl}") center/cover no-repeat; filter: blur(90px) brightness(0.25); /* only applies to the pseudo-element image */ z-index: -1; } #stage-slideover-sidebar nav { position: relative; /* anchor the pseudo-element */ isolation: isolate; /* make z-index work as intended */ } #stage-slideover-sidebar { isolation: isolate; background-color: rgba(30,30,30,0.35) !important; /* needs some alpha for backdrop-filter */ -webkit-backdrop-filter: blur(18px) saturate(120%); backdrop-filter: blur(18px) saturate(120%); border-right: 1px solid rgba(255,255,255,0.08)!important; box-shadow: 6px 0 24px rgba(0,0,0,0.35), inset -1px 0 0 rgba(255,255,255,0.04); } [aria-label="Chat history"] > div:first-of-type { background: url("${navHeaderImageUrl}") center/cover no-repeat; } /* first aside inside the nav */ #stage-slideover-sidebar nav > aside:first-of-type { position: sticky; /* make sure it actually overlays */ z-index: 2; isolation: isolate; background-color: rgba(15,15,15,0.25) !important; -webkit-backdrop-filter:blur(24px) saturate(120%); backdrop-filter:blur(24px) saturate(120%); } /* bottom profile strip container (stronger blur) */ #stage-slideover-sidebar .sticky.bottom-0.z-30 { isolation: isolate; background-color: rgba(15,15,15,0.25) !important; -webkit-backdrop-filter: blur(32px) saturate(120%); backdrop-filter: blur(32px) saturate(120%); } ${hideContent ? ` /* HIDES YOUR PROFILE, PROJECTS, AND HISTORY HIDDEN - use the hideContent var at the top of this script */ #history, #snorlax-heading { filter: blur(15px); } #history:hover, #snorlax-heading:hover { filter: blur(0px); } #stage-slideover-sidebar .sticky.bottom-0.z-30 { filter: blur(5px); } #stage-slideover-sidebar .sticky.bottom-0.z-30:hover { filter: blur(0px); } ` : ''} /* prompt suggestions */ #thread-bottom ul { margin-top: 10px; } #thread-bottom ul li > button { background-color: rgba(15,15,15,.25) !important; -webkit-backdrop-filter: blur(32px) saturate(120%); backdrop-filter: blur(32px) saturate(120%); margin-bottom: 5px; } #thread-bottom ul li > .bg-token-border-default, #thread-bottom ul li > .bg-token-main-surface-secondary { background-color: transparent !important; } `; const style = document.createElement('style'); style.id = '__spin_bg_style'; style.textContent = css; (document.head || document.documentElement).appendChild(style); const mo = new MutationObserver(() => { if (!document.getElementById('__spin_bg_style')) { (document.head || document.documentElement).appendChild(style); } }); mo.observe(document.documentElement, { childList: true, subtree: true }); /* find the composer "shell" without hard-coding a utility class */ function findComposerShell(form) { if (!form) return null; // new grid container with overflow-clip let shell = form.querySelector('div.overflow-clip.grid'); if (shell) return shell; // fallback: anything overflow-clip + rounded shell = form.querySelector('div.overflow-clip[class*="rounded"]'); if (shell) return shell; // last resort: biggest div containing editor/send const candidates = [...form.querySelectorAll('div')] .filter(d => d.querySelector('[contenteditable="true"], textarea, [data-testid="send-button"]')) .sort((a, b) => b.getBoundingClientRect().width - a.getBoundingClientRect().width); return candidates[0] || null; } function enhanceComposer(root = document) { // be loose about the form selector in case temporary chat swaps attributes const forms = root.querySelectorAll( 'form[data-type="unified-composer"], form:has([contenteditable="true"]), form:has(textarea)' ); forms.forEach(form => { const shell = findComposerShell(form); if (!shell) { if (!form.__spinWarned) { console.warn('[spin burrito] composer shell not found; selectors may need an update'); form.__spinWarned = true; } return; } if (shell.classList.contains('spin-glass-composer')) return; shell.classList.add('spin-glass-composer'); const ring = document.createElement('div'); ring.className = 'spin-glow-ring'; shell.prepend(ring); }); } /* attach frosted class to the sidebar container or the nav itself */ function enhanceNav(root = document) { const nav = root.querySelector('nav[aria-label="Chat history"]'); if (!nav) return; const container = document.getElementById('stage-slideover-sidebar') || nav.closest('[id*="sidebar"], [data-testid*="sidebar"], aside') || null; if (container && !container.classList.contains('spin-glass-sidebar')) { container.classList.add('spin-glass-sidebar'); } if (!nav.classList.contains('spin-glass-nav')) { nav.classList.add('spin-glass-nav'); } } // retrigger enhance when the temporary chat toggle is clicked function hookTempChatToggle(root = document) { const btn = root.querySelector('button[aria-label="Turn on temporary chat"], button[aria-label="Turn off temporary chat"]'); if (!btn || btn.__spinHooked) return; btn.__spinHooked = true; btn.addEventListener('click', () => { // next frame and shortly after to cover async re-render requestAnimationFrame(() => enhanceAllSoon()); setTimeout(() => enhanceAllSoon(), 120); }, { passive: true }); } const PROXIMITY = 140; // px function onPointerMove(e) { document.querySelectorAll('.spin-glass-composer').forEach(el => { const ring = el.querySelector(':scope > .spin-glow-ring'); if (!ring) return; const r = el.getBoundingClientRect(); const px = Math.max(r.left, Math.min(e.clientX, r.right)); const py = Math.max(r.top, Math.min(e.clientY, r.bottom)); const dx = e.clientX < r.left ? r.left - e.clientX : e.clientX > r.right ? e.clientX - r.right : 0; const dy = e.clientY < r.top ? r.top - e.clientY : e.clientY > r.bottom ? e.clientY - r.bottom : 0; const dist = Math.hypot(dx, dy); const mx = ((px - r.left) / r.width) * 100; const my = ((py - r.top) / r.height) * 100; const o = Math.max(0, 1 - dist / PROXIMITY); ring.style.setProperty('--mx', `${mx}%`); ring.style.setProperty('--my', `${my}%`); ring.style.setProperty('--glow-o', o > 0 ? (0.25 + 0.75 * o) : 0); }); } function streamingTextChecker() { const DURATION_MS = 1000; // full sweep left→right const EDGE_PAD = 4; // avoid clipping at edges const OVERTRAVEL = 10; // percent beyond each edge for smoother fade const STYLE_ID = 'spin-glow-override'; const SAFETY_INTERVAL_MS = 2000; // very light fallback check // inject a hard override so hover/distance css can't kill opacity // this sets the custom property itself with !important if (!document.getElementById(STYLE_ID)) { const s = document.createElement('style'); s.id = STYLE_ID; s.textContent = ` /* keep the ring visible any time streaming-animation is present in the ancestor chain */ :where(.streaming-animation) .spin-glow-ring { --glow-o: 1 !important; opacity: 1 !important; } `; document.head.appendChild(s); } let glowEl = document.querySelector('.spin-glow-ring'); let running = false; let rafId = null; let startTime = 0; let prevPresent = null; const tick = t => { if (!running) return; if (!startTime) startTime = t; const elapsed = (t - startTime) % DURATION_MS; const pct = elapsed / DURATION_MS; const travel = 100 + OVERTRAVEL * 2; const x = -OVERTRAVEL + pct * travel; if (glowEl) { glowEl.style.setProperty('--mx', x.toFixed(2) + '%'); glowEl.style.setProperty('--my', '50%'); glowEl.style.setProperty('--glow-o', '1'); // keep forcing full opacity while streaming glowEl.style.opacity = '1'; // belt and suspenders in case the css var isn't used } rafId = requestAnimationFrame(tick); }; const start = () => { if (running) return; running = true; if (glowEl) { glowEl.style.setProperty('--glow-o', '1'); glowEl.style.opacity = '1'; } startTime = 0; rafId = requestAnimationFrame(tick); }; const stop = () => { if (!running) return; running = false; if (rafId) cancelAnimationFrame(rafId); rafId = null; if (glowEl) { glowEl.style.removeProperty('--glow-o'); // let normal behavior resume when not streaming glowEl.style.removeProperty('opacity'); } }; // re-evaluate whether we should run const reevaluate = () => { if (!glowEl || !glowEl.isConnected) glowEl = document.querySelector('.spin-glow-ring'); const hasRing = !!glowEl && glowEl.isConnected; const present = !!document.querySelector('.streaming-animation'); if (!hasRing) { stop(); prevPresent = present; return; } if (present === prevPresent) return; prevPresent = present; present ? start() : stop(); }; // avoid double observers if user calls this twice if (!window.__spinGlowObserver) { const mo = new MutationObserver(muts => { for (const m of muts) { if (m.type === 'attributes') { if (m.attributeName === 'class') { reevaluate(); return; } } else { for (const n of m.addedNodes) { if (!(n instanceof Element)) continue; if (n.matches?.('.spin-glow-ring, .streaming-animation') || n.querySelector?.('.spin-glow-ring, .streaming-animation')) { reevaluate(); return; } } for (const n of m.removedNodes) { if (!(n instanceof Element)) continue; if (n === glowEl || n.matches?.('.streaming-animation') || n.querySelector?.('.streaming-animation')) { reevaluate(); return; } } } } }); mo.observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ['class'] }); window.__spinGlowObserver = mo; } // safety net in case some dom changes slip past if (!window.__spinGlowSafetyInterval) { window.__spinGlowSafetyInterval = setInterval(reevaluate, SAFETY_INTERVAL_MS); } // initial check reevaluate(); } const ready = () => { enhanceComposer(); enhanceNav(); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', ready, { once: true }); } else { ready(); } // tiny debounce helper function debounce(fn, wait) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn.apply(null, args), wait); }; } const enhanceAllSoon = debounce(() => { enhanceComposer(document); enhanceNav(document); }, 50); const mo2 = new MutationObserver(muts => { let shouldEnhance = false; for (const m of muts) { if (m.type !== 'childList') continue; for (const n of m.addedNodes) { if (n.nodeType !== 1) continue; // element only // case 1: a new form got added if (n.matches?.('form[data-type="unified-composer"], form:has([contenteditable="true"]), form:has(textarea)')) { shouldEnhance = true; continue; } // case 2: a new shell got added inside an existing form const form = n.closest?.('form[data-type="unified-composer"], form:has([contenteditable="true"]), form:has(textarea)'); if (form) { shouldEnhance = true; continue; } // case 3: chat history/nav bits appeared if (n.matches?.('nav[aria-label="Chat history"]') || n.querySelector?.('nav[aria-label="Chat history"]')) { shouldEnhance = true; continue; } } } if (shouldEnhance) enhanceAllSoon(); }); mo2.observe(document.documentElement, { childList: true, subtree: true }); const mo3 = new MutationObserver(() => hookTempChatToggle()); mo3.observe(document.documentElement, { childList: true, subtree: true }); window.addEventListener('pointermove', onPointerMove, { passive: true }); window.addEventListener('beforeunload', () => { mo.disconnect(); mo2.disconnect(); window.removeEventListener('pointermove', onPointerMove); }); window.onload = () => { streamingTextChecker(); hookTempChatToggle(); } })();