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.3
// @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
*/
// Collapse on mobile
GM_addStyle(`
@media (pointer: coarse) {
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;
}
/*
.text-base > div > div.z-0 {
border-top: dashed 1px #8884;
}
*/
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 PROMPT_TEXTAREA_SELECTOR = '#prompt-textarea';
// function doTextarea(element) {
// }
// Collapse on load
// Click on bottom bar to toggle
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;
};
function doArticle(article, onLoad) {
const text = article.querySelector('.text-message');
if (!text) return;
const id = article.getAttribute('data-turn-id');
// 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 (db[id] === STATE.COLLAPSED) {
setTimeout(() => { article.classList.add('text-collapsed'); }, 200);
}
if (!hasId(id)) {
setTimeout(() => { article.classList.add('text-collapsed'); }, 200);
setIdState(id, STATE.COLLAPSED); // collapse all on open
/*
setIdState(id, STATE.EXPANDED);
*/
}
} else {
setIdState(id, STATE.EXPANDED); // keep live messages open
// NOTE: there is no way to get the true id of new messages, I get a temp 'request-id'
// Therefore, a new message that appears EXPANDED will become COLLAPSED after reload.
// TODO: Fix observer or add timer or reapply all other article on any change (this would collapse the last one only)
}
}
// Click: toggle and persist new state
article.addEventListener('click', (e) => {
const INSIDE_BOTTOM_BAR = '.text-base > div > .z-0';
if (!(e.target && e.target.closest && e.target.closest(INSIDE_BOTTOM_BAR))) return;
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);
});
}
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);
});
// // Check for #prompt-textarea
// if (node.matches(PROMPT_TEXTAREA_SELECTOR)) {
// doTextarea(node);
// }
}
});
}
});
});
observer.observe(document.documentElement, { childList: true, subtree: true });