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