chatgpt对话时间长了特别卡顿。
油猴脚本直接粘进去就能使用
// ==UserScript==// @name ChatGPT 用户消息真·折叠(50字预览,按钮在“已折叠”前)// @namespace https://example.com/tm// @version 1.2.1// @description 折叠“您说:”的长用户消息:从DOM摘除原内容,仅保留前50字预览;顶部显示“[展开] 已折叠”;展开时再还原// @author you// @match https://chat.openai.com/*// @match https://chatgpt.com/*// @run-at document-end// @grant none// ==/UserScript==(function () { 'use strict'; const TITLE_TEXT = '您说:'; // h5 的定位文本 const THRESHOLD = 50; // 触发折叠的字数阈值 const PREVIEW_CHARS = 50; // 预览字符数 const ARTICLE_FLAG = 'data-yc-fold-processed'; // key: div.whitespace-pre-wrap -> value: { frag, bar, label, preview } const store = new WeakMap(); injectStyle(); function injectStyle() { if (document.getElementById('yc-fold-style-v3')) return; const s = document.createElement('style'); s.id = 'yc-fold-style-v3'; s.textContent = ` .yc-toggle-btn { cursor: pointer; user-select: none; font-size: 12px; padding: 2px 6px; border-radius: 6px; border: 1px solid rgba(0,0,0,.15); background: rgba(0,0,0,.04); margin-right: 8px; } .yc-toggle-btn:hover { background: rgba(0,0,0,.08); } .yc-bar { display: flex; align-items: center; gap: 6px; font-size: 12px; opacity: .8; margin-bottom: 6px; } .yc-preview { max-height: 4.2em; /* 2~3行 */ overflow: hidden; position: relative; line-height: 1.4; white-space: pre-wrap; } .yc-preview::after { content: ""; position: absolute; left:0; right:0; bottom:0; height:2em; pointer-events:none; background: linear-gradient(to bottom, rgba(255,255,255,0), var(--bg, #fff)); } @media (prefers-color-scheme: dark) { .yc-preview::after { background: linear-gradient(to bottom, rgba(0,0,0,0), var(--bg, #000)); } .yc-toggle-btn { border-color: rgba(255,255,255,.2); background: rgba(255,255,255,.06); } .yc-toggle-btn:hover { background: rgba(255,255,255,.12); } } `; document.head.appendChild(s); } function getTextLen(article) { const nodes = article.querySelectorAll('div.whitespace-pre-wrap'); let len = 0; nodes.forEach(n => len += (n.innerText || '').trim().length); return { len, nodes }; } function makePreview(text) { const preview = document.createElement('div'); preview.className = 'yc-preview'; const t = (text || '').trim(); const snippet = t.length > PREVIEW_CHARS ? (t.slice(0, PREVIEW_CHARS) + '…') : t; preview.textContent = snippet || '(空内容)'; return preview; } function ensureBar(containerDiv) { // 顶部控制条:[按钮] 已折叠 let bar = containerDiv.querySelector(':scope > .yc-bar'); if (!bar) { bar = document.createElement('div'); bar.className = 'yc-bar'; const btn = document.createElement('button'); btn.className = 'yc-toggle-btn'; btn.type = 'button'; btn.textContent = '展开'; const label = document.createElement('span'); label.textContent = '已折叠'; bar.appendChild(btn); bar.appendChild(label); containerDiv.insertBefore(bar, containerDiv.firstChild); btn.addEventListener('click', () => { if (store.has(containerDiv)) { // 处于折叠 => 展开 expand(containerDiv); btn.textContent = '收起'; label.textContent = ''; // 展开后不显示“已折叠” } else { // 处于展开 => 折叠 collapse(containerDiv); btn.textContent = '展开'; label.textContent = '已折叠'; } }); } return bar; } // —— 辅助:从一组节点提取纯文本(不含按钮区)—— function textFromNodes(nodes) { let s = ''; for (const n of nodes) { s += (n.innerText ?? n.textContent ?? ''); } return s.trim(); } // 折叠:移出原内容,仅留顶部条与 50 字预览(不包含按钮文字) function collapse(containerDiv) { if (store.has(containerDiv)) return; const frag = document.createDocumentFragment(); // 先确保有 bar,再把 bar 以外的内容移动到 frag const bar = ensureBar(containerDiv); const keep = new Set([bar]); // bar 留在 DOM const toMove = Array.from(containerDiv.childNodes).filter(n => !keep.has(n)); // 预览文本只来自将要被移走的内容 const previewText = textFromNodes(toMove); // 真正移走内容 toMove.forEach(n => frag.appendChild(n)); // 创建 50 字预览并挂上 const preview = makePreview(previewText); containerDiv.appendChild(preview); // 记录状态 store.set(containerDiv, { frag, bar, label: bar.querySelector('span'), preview }); // 更新 bar 文案 const btn = bar.querySelector('button'); const label = bar.querySelector('span'); btn.textContent = '展开'; label.textContent = '已折叠'; } // ←←← 关键:补上 collapse 的收尾大括号 // 展开:还原原内容,移除预览 function expand(containerDiv) { const rec = store.get(containerDiv); if (!rec) return; const { frag, preview } = rec; // 移除预览 preview?.remove(); // 还原原内容(追加在 bar 后面) containerDiv.appendChild(frag); store.delete(containerDiv); // 更新 bar 文案 const bar = ensureBar(containerDiv); const btn = bar.querySelector('button'); const label = bar.querySelector('span'); btn.textContent = '收起'; label.textContent = ''; } function processArticle(article) { if (article.hasAttribute(ARTICLE_FLAG)) return; const h5 = article.querySelector('h5'); if (!h5) return; if ((h5.textContent || '').trim() !== TITLE_TEXT) return; const { len, nodes } = getTextLen(article); if (!nodes.length) return; const target = nodes[0]; const shouldCollapse = len > THRESHOLD; // 为容器添加 bar(按钮始终在内容顶部;短消息隐藏按钮) const bar = ensureBar(target); const btn = bar.querySelector('button'); const label = bar.querySelector('span'); if (shouldCollapse) { collapse(target); // 会设置为“展开 / 已折叠” btn.style.display = 'inline-block'; } else { // 短内容:不折叠、不显示按钮与“已折叠” if (store.has(target)) expand(target); btn.style.display = 'none'; label.textContent = ''; } article.setAttribute(ARTICLE_FLAG, '1'); } function scan() { document.querySelectorAll('article').forEach(processArticle); } const mo = new MutationObserver(muts => { for (const m of muts) { for (const n of m.addedNodes) { if (!(n instanceof HTMLElement)) continue; if (n.tagName === 'ARTICLE') { processArticle(n); } else { const arts = n.querySelectorAll?.('article'); arts && arts.forEach(processArticle); } } } }); function init() { scan(); mo.observe(document.body, { childList: true, subtree: true }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }})();
评论 (0)