您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Collapse/expand user & assistant code blocks on chat.openai.com/chatgpt.com
// ==UserScript== // @name Collapse ChatGPT Code Blocks (robust) // @namespace https://github.com/you/collapse-chatgpt-code // @version 1.2.1 // @description Collapse/expand user & assistant code blocks on chat.openai.com/chatgpt.com // @author Chef D // @match https://chat.openai.com/* // @match https://chatgpt.com/* // @run-at document-end // @grant none // @noframes // @license MIT // ==/UserScript== (() => { 'use strict'; let __cgptId = 0; const nextId = () => String(++__cgptId); // small badge to confirm load (function addLoadedBadge(){ const id = 'cgpt-collapse-loaded-badge'; if (document.getElementById(id)) return; const b = document.createElement('div'); b.id = id; b.textContent = 'collapse-code: active'; b.style.cssText = [ 'position:fixed;','bottom:6px;','right:8px;', 'padding:2px 6px;','font:11px/1 system-ui,sans-serif;', 'background:rgba(0,0,0,.5);','color:#fff;','border-radius:4px;', 'z-index:999999;','pointer-events:none;','opacity:.35;' ].join(''); document.body.appendChild(b); setTimeout(()=> b.remove(), 2000); })(); // icons const TOGGLE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" style="transition:transform 120ms ease"><path d="M8.12 9.29L12 13.17l3.88-3.88a1 1 0 011.41 1.41l-4.59 4.59a1 1 0 01-1.41 0L6.71 10.7a1 1 0 111.41-1.41z" fill="currentColor"/></svg>`; const COPY_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24"><path d="M16 1H4a2 2 0 00-2 2v12h2V3h12V1zm3 4H8a2 2 0 00-2 2v14a2 2 0 002 2h11a2 2 0 002-2V7a2 2 0 00-2-2zm0 16H8V7h11v14z" fill="currentColor"/></svg>`; // utils const debounce = (fn, wait=150) => { let t; return (...a)=>{ clearTimeout(t); t=setTimeout(()=>fn(...a), wait); }; }; function createBtn({ label='', onClick, title='', style='' }) { const btn = document.createElement('button'); btn.type = 'button'; btn.title = title || label; btn.textContent = label; btn.style.cssText = [ 'font:12px/1 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;', 'padding:4px 8px;','border-radius:6px;', 'border:1px solid rgba(0,0,0,.2);', 'background:rgba(240,240,240,.9);','color:#111;', 'cursor:pointer;','user-select:none;', 'display:inline-flex;','align-items:center;','gap:6px;', 'transition:background 120ms ease,transform 60ms ease;', style ].join(''); btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(230,230,230,1)'; }); btn.addEventListener('mouseleave', () => { btn.style.background = 'rgba(240,240,240,.9)'; }); btn.addEventListener('mousedown', () => { btn.style.transform = 'translateY(1px)'; }); btn.addEventListener('mouseup', () => { btn.style.transform = 'translateY(0)'; }); if (onClick) btn.addEventListener('click', onClick); return btn; } function createIconLabelBtn({ iconSVG, label, onClick, title }) { const btn = createBtn({ label:'', onClick, title }); const span = document.createElement('span'); span.textContent = label; btn.insertAdjacentHTML('afterbegin', iconSVG); btn.appendChild(span); return btn; } // DOM helpers function findMessageBubbleFrom(pre) { let bubble = pre.closest('[data-message-author-role]'); if (bubble) return bubble; bubble = pre.closest('article,[data-message-id],[data-testid="conversation-turn"]'); return bubble || pre.parentElement; } const getRole = (bubble) => bubble?.getAttribute?.('data-message-author-role') || null; // cleanup (shared legacy cleanup only) function cleanupPreCommon(preEl) { try { const sib = preEl.previousElementSibling; if (sib && (sib.tagName==='DIV'||sib.tagName==='SECTION') && sib.childElementCount<=6 && /Copy|Show code|Hide code/i.test(sib.textContent||'')) sib.remove(); preEl.querySelectorAll('div').forEach(div=>{ if (div.dataset?.cgptControls==='bottom-left' || div.dataset?.cgptPreview==='1') return; const cs = getComputedStyle(div); if (cs.position==='absolute' && cs.top==='6px' && cs.right==='8px' && /Copy|Show|Hide/i.test(div.textContent||'')) div.remove(); }); } catch(_) {} } // extra cleanup only for user <pre> (remove any in-pre bottom-left bars from old versions) function cleanupUserPreExtras(preEl) { preEl.querySelectorAll('div[data-cgpt-controls="bottom-left"]').forEach(el => el.remove()); } // controls placement function addBottomLeftControls(containerEl, ...controls) { containerEl.style.position = containerEl.style.position || 'relative'; let ctr = containerEl.querySelector('div[data-cgpt-controls="bottom-left"]'); if (!ctr) { ctr = document.createElement('div'); ctr.dataset.cgptControls = 'bottom-left'; ctr.style.cssText = [ 'position:absolute;','bottom:6px;','left:8px;', 'display:flex;','gap:6px;','z-index:2;' ].join(''); containerEl.appendChild(ctr); } else { ctr.innerHTML = ''; } controls.forEach(c=> ctr.appendChild(c)); return ctr; } function addBelowBubbleControls(bubble, ...controls) { bubble.style.position = bubble.style.position || 'relative'; const rows = bubble.querySelectorAll('div[data-cgpt-controls="user-pre"]').length; const ctr = document.createElement('div'); ctr.dataset.cgptControls = 'user-pre'; ctr.style.cssText = [ 'position:absolute;','left:8px;',`bottom:${-32 - (rows*32)}px;`, 'display:flex;','gap:6px;','z-index:2;' ].join(''); controls.forEach(c=> ctr.appendChild(c)); bubble.appendChild(ctr); const newRows = rows + 1; const needed = 16 + newRows*32; const cur = parseInt(getComputedStyle(bubble).marginBottom||'0',10)||0; if (cur < needed) bubble.style.marginBottom = `${needed}px`; return ctr; } function createToggle(setHidden, getHidden, texts={show:'Show', hide:'Hide'}) { const btn = createIconLabelBtn({ iconSVG: TOGGLE_SVG, label: getHidden() ? texts.show : texts.hide, title: 'Toggle', onClick: (e)=>{ e.stopPropagation(); const h = !getHidden(); setHidden(h); btn.querySelector('span').textContent = h ? texts.show : texts.hide; const svg = btn.querySelector('svg'); if (svg) svg.style.transform = h ? 'rotate(-90deg)' : 'rotate(0deg)'; } }); const svg = btn.querySelector('svg'); if (svg) svg.style.transform = getHidden() ? 'rotate(-90deg)' : 'rotate(0deg)'; return btn; } function buildPreviewEl(codeEl) { const preview = document.createElement('div'); preview.dataset.cgptPreview = '1'; preview.style.cssText = [ 'font:12px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;', 'background:rgba(240,240,240,.6);','color:#444;', 'border:1px dashed rgba(0,0,0,.25);','border-radius:6px;', 'padding:6px 8px;','white-space:pre-wrap;','overflow:hidden;', 'text-overflow:ellipsis;','max-height:7.2em;','margin:6px 0 0 0;' ].join(''); const raw = (codeEl.innerText || codeEl.textContent || '').trim(); const lines = raw.split('\n').slice(0, 5); let text = lines.join('\n'); if (raw.length > text.length) text += '\n…'; preview.textContent = text || '(empty)'; return preview; } function buildMessagePreviewElFrom(node) { // Extract a compact snippet from the message body const getText = (n) => (n.innerText || n.textContent || '') .replace(/\s+/g, ' ') .trim(); let text = getText(node); if (text.length > 280) text = text.slice(0, 240) + '…'; const preview = document.createElement('div'); preview.dataset.cgptMsgPreview = '1'; preview.textContent = text || '(empty)'; preview.style.cssText = [ 'font:12px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;', 'background:rgba(240,240,240,.6);','color:#444;', 'border:1px dashed rgba(0,0,0,.25);','border-radius:6px;', 'padding:6px 8px;','white-space:normal;','overflow:hidden;', 'text-overflow:ellipsis;','max-height:7.2em;','margin:6px 0 0 0;' ].join(''); return preview; } // processors function processAssistantPre(pre) { const code = pre.querySelector('code'); if (!code) return; // skip cleanup after first time, otherwise we remove our own controls if (pre.dataset.__cgptProcessed==='assistant-pre') return; // first-time: clean legacy stuff, then add controls cleanupPreCommon(pre); let hidden = true; code.style.display = 'none'; const toggleBtn = createToggle( (h)=>{ hidden=h; code.style.display = h?'none':''; }, ()=> hidden, { show:'Show code', hide:'Hide code' } ); addBottomLeftControls(pre, toggleBtn); pre.dataset.__cgptProcessed = 'assistant-pre'; } function processUserPre(pre) { const code = pre.querySelector('code'); if (!code) return; // strong de-dupe: one control row per <pre> if (!pre.dataset.cgptId) pre.dataset.cgptId = nextId(); const preId = pre.dataset.cgptId; const bubble = findMessageBubbleFrom(pre); if (bubble.querySelector(`div[data-cgpt-controls="user-pre"][data-for-pre="${preId}"]`)) return; // clean user legacy in-pre controls/previews (safe to do each scan) cleanupPreCommon(pre); cleanupUserPreExtras(pre); pre.querySelectorAll('div[data-cgpt-preview="1"]').forEach((el, i) => { if (i > 0) el.remove(); }); // preview (build once) let preview = pre.querySelector('div[data-cgpt-preview="1"]'); if (!preview) { preview = buildPreviewEl(code); pre.appendChild(preview); } // start collapsed with preview let hidden = true; code.style.display = 'none'; preview.style.display = 'block'; const copyBtn = createIconLabelBtn({ iconSVG: COPY_SVG, label: 'Copy', title: 'Copy code', onClick: (e) => { e.stopPropagation(); const text = code.innerText || code.textContent || ''; navigator.clipboard.writeText(text).then(() => { const lbl = copyBtn.querySelector('span'); const old = lbl.textContent; lbl.textContent = 'Copied'; setTimeout(() => { lbl.textContent = old; }, 900); }).catch(() => {}); } }); const toggleBtn = createToggle( (h) => { hidden = h; code.style.display = h ? 'none' : ''; preview.style.display = h ? 'block' : 'none'; }, () => hidden, { show: 'Show', hide: 'Hide' } ); const ctr = addBelowBubbleControls(bubble, copyBtn, toggleBtn); ctr.setAttribute('data-for-pre', preId); pre.dataset.__cgptProcessed = 'user-pre'; } function processUserMessageBubble(bubble) { if (!bubble || bubble.dataset.__cgptProcessed === 'user-bubble') return; // Find main message content let content = bubble.querySelector(':scope .markdown') || bubble.querySelector(':scope [data-testid="markdown"]') || bubble.querySelector(':scope [data-message-content]') || bubble.querySelector(':scope [data-prose]'); if (!content) { const wrap = document.createElement('div'); wrap.dataset.cgptUserContent = '1'; while (bubble.firstChild) { wrap.appendChild(bubble.firstChild); } bubble.appendChild(wrap); content = wrap; } // Ensure a single preview node exists let preview = bubble.querySelector('div[data-cgpt-msg-preview="1"]'); if (!preview) { preview = buildMessagePreviewElFrom(content); preview.style.display = 'none'; // start expanded, so hide preview initially bubble.appendChild(preview); } // Toggle: when collapsing, update + show preview; when expanding, hide it let hidden = false; const msgToggle = createToggle( (h) => { hidden = h; if (hidden) { // refresh preview text in case user edited the message const fresh = buildMessagePreviewElFrom(content); preview.textContent = fresh.textContent; } content.style.display = hidden ? 'none' : ''; preview.style.display = hidden ? 'block' : 'none'; }, () => hidden, { show: 'Show message', hide: 'Hide message' } ); addBottomLeftControls(bubble, msgToggle); bubble.dataset.__cgptProcessed = 'user-bubble'; } // scanner function scan() { const pres = document.querySelectorAll('pre'); pres.forEach((pre) => { const bubble = findMessageBubbleFrom(pre); const role = getRole(bubble); if (role === 'assistant') { processAssistantPre(pre); } else if (role === 'user') { processUserPre(pre); } else { // unknown => treat as User to avoid injecting assistant controls in user bubbles processUserPre(pre); } }); document.querySelectorAll('[data-message-author-role="user"]').forEach(processUserMessageBubble); } const start = ()=> { try { scan(); } catch(e){ console.error(e); } }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start, { once:true }); } else { start(); } const mo = new MutationObserver(debounce(scan, 200)); mo.observe(document.documentElement, { childList:true, subtree:true }); window.addEventListener('resize', debounce(scan, 300)); })();