您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Collapse messages by clicking the bottom bar. Autocollapse on open. Remembers collapse state. Add bar colors and separators. Mobile: max size for the textarea when not focused.
// ==UserScript== // @name ChatGPT Collapse Messages // @description Collapse messages by clicking the bottom bar. Autocollapse on open. Remembers collapse state. Add bar colors and separators. Mobile: max size for the textarea when not focused. // @version 1.9 // @author C89sd // @namespace https://greasyfork.org/users/1376767 // @match https://chatgpt.com/* // @grant GM_addStyle // @run-at document-start // @noframes // ==/UserScript== 'use strict'; const ALPHA = "04"; // 0xFF, controls the highlight intensity. "0F" best on mobile /* __Hierarchy__: article(maybe .text-collapsed) > .text-base > div > .text-message (main message) > .z-0 (bottom bar) > other */ GM_addStyle(` /* Mobile fix: remove the white ring on open. */ button.focus\\:ring-white { box-shadow: none !important; } @media (pointer: coarse) { /* Mobile fix: smaller header. */ :root { --header-height: calc(var(--spacing)*8) !important; } /* Mobile fix: full width messages */ :root { --user-chat-width: 90% !important; } /* Mobile fix: 10px margin */ .text-base { padding-left: 10px; padding-right: 10px; } /* Mobile fix: clicking the "Thinking mode Standard/Extended" pill closes it, raise the left side */ button.__composer-pill { z-index: 10; pointer-events: auto; } /* Mobile fix: bottom bar buttons are transparent/clickthrough */ article button { pointer-events: auto; } /* Mobile fix: minimise the textarea when not focused to save space. */ html:not(:has(input:focus, textarea:focus, [contenteditable="true"]:focus)) #prompt-textarea { max-height: 2.6em !important; margin-top: 0 !important; padding-bottom: 5px !important; } } article { padding-bottom: 10px !important; } article .text-base { padding-top: 0px; } article.text-collapsed .text-base > div > div:has(.text-message) { max-height: 6em !important; overflow: hidden; --fade: 4.5em; --mask: linear-gradient( to bottom, #000 0, #000 calc(100% - var(--fade)), transparent 100% ); -webkit-mask: var(--mask) no-repeat; mask: var(--mask) no-repeat; } article .text-base > div { border-bottom: solid 10px #8884; } article[data-turn="user"] .text-base > div { border-bottom: solid 2px #8884; } article .text-base > div > div.z-0 { background: linear-gradient(to bottom, transparent 0%, #00FF00${ALPHA} 100%); } article.text-collapsed .text-base > div > div.z-0 { background: linear-gradient(to bottom, transparent 0%, #FF0000${ALPHA} 100%); } `); const ARTICLE_SELECTOR = 'article'; const LS_KEY = 'article_state_db_v1'; const STATE = { COLLAPSED: 0, EXPANDED: 1 }; const DB_CACHE_MS = 500; let dbCache = null; let dbCacheAt = 0; // Debounce DB parse function loadStateDB() { const now = Date.now(); if (dbCache && (now - dbCacheAt) < DB_CACHE_MS) return dbCache; try { const raw = localStorage.getItem(LS_KEY); dbCache = raw ? JSON.parse(raw) : {}; if (typeof dbCache !== 'object' || Array.isArray(dbCache) || dbCache === null) dbCache = {}; } catch { dbCache = {}; } dbCacheAt = now; return dbCache; } function saveStateDB(db) { dbCache = db; dbCacheAt = Date.now(); try { localStorage.setItem(LS_KEY, JSON.stringify(db)); } catch {} } function hasId(id) { const db = loadStateDB(); return Object.prototype.hasOwnProperty.call(db, id); } function setIdState(id, stateEnum) { const db = loadStateDB(); db[id] = stateEnum; saveStateDB(db); } const scrollParent = el => { for (let e = el; e; e = e.parentElement) { const s = getComputedStyle(e); if (/(auto|scroll|overlay)/.test(s.overflowY) && e.scrollHeight > e.clientHeight) return e; } return document.scrollingElement || document.documentElement; }; // Note: 'data-turn-id' cannot be trusted for new messages so use 'convid#testid' function getId(article) { let convid = location.pathname.match(/\/c\/([\-a-zA-Z0-9]+)/)?.[1]; let testid = article.getAttribute('data-testid')?.match(/(\d+)/)?.[1]; if (convid && testid) return convid+'#'+testid; return null } function doArticle(article, onLoad) { const text = article.querySelector('.text-message'); if (!text) return; const id = getId(article); // On load: consult DB and apply initial state if (id && !id.startsWith('request-')) { const db = loadStateDB(); // console.log(article, text, id, db[id]) if (onLoad) { if (!hasId(id)) { // collapse all on open setTimeout(() => { article.classList.add('text-collapsed'); }, 200); setIdState(id, STATE.COLLAPSED); } else if (db[id] === STATE.COLLAPSED) { setTimeout(() => { article.classList.add('text-collapsed'); }, 200); } } else { // New messages const user = (article.getAttribute('data-turn') === 'user'); setIdState(id, user ? STATE.COLLAPSED : STATE.EXPANDED); } } // Click: toggle and persist new state article.addEventListener('click', (e) => { const INSIDE_BOTTOM_BAR = '.text-base > div > .z-0'; if (!(e?.target?.closest(INSIDE_BOTTOM_BAR))) return; if (e?.target?.closest('button')) return; // ignore buttons on the bar const sc = scrollParent(e.currentTarget || article); const adding = !article.classList.contains('text-collapsed'); // about to collapse const fromBottom = adding ? (sc.scrollHeight - sc.scrollTop - sc.clientHeight) : 0; article.classList.toggle('text-collapsed'); if (adding) requestAnimationFrame(() => { sc.scrollTop = Math.max(0, sc.scrollHeight - sc.clientHeight - fromBottom); }); if (id) setIdState(id, adding ? STATE.COLLAPSED : STATE.EXPANDED); e.stopPropagation(); }, true); } const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(function(node) { if (node.nodeType === Node.ELEMENT_NODE) { // Check for articles if (node.matches(ARTICLE_SELECTOR)) { // console.log("ONLOAD", node) doArticle(node, false); } node.querySelectorAll(ARTICLE_SELECTOR).forEach(descendantArticle => { // console.log("LIVE", descendantArticle) doArticle(descendantArticle, true); }); } }); } }); }); observer.observe(document.documentElement, { childList: true, subtree: true });