// ==UserScript== // @name LLM Chat Collapser // @name:zh-CN LLM 聊天内容折叠器 // @namespace https://github.com/miniyu157/llm-chat-collapser // @version 25.9.29-1 // @description Makes code blocks on Gemini/ChatGPT and long user messages on ChatGPT collapsible for a cleaner interface. // @description:zh-CN 使 Gemini 和 ChatGPT 上的代码块,以及 ChatGPT 上的长文本用户消息可折叠,以提供更整洁的界面。 // @author miniyu157 // @license MIT // @match https://gemini.google.com/* // @match https://chatgpt.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=openai.com // @grant GM_addStyle // @run-at document-end // @homepageURL https://github.com/miniyu157/llm-chat-collapser // @supportURL https://github.com/miniyu157/llm-chat-collapser/issues // @updateURL https://raw.githubusercontent.com/miniyu157/llm-chat-collapser/refs/heads/main/llm-chat-collapser.user.js // @downloadURL https://raw.githubusercontent.com/miniyu157/llm-chat-collapser/refs/heads/main/llm-chat-collapser.user.js // ==/UserScript== (function() { 'use strict'; GM_addStyle(` .ucc-collapsible-header { cursor: pointer; position: relative; user-select: none; } .ucc-arrow-indicator::before { content: '▶'; position: absolute; top: 50%; transform: translateY(-50%); font-size: 10px; color: var(--mat-sys-color-on-surface-variant, #888); transition: transform 0.2s ease-in-out; } .ucc-header-expanded::before { transform: translateY(-50%) rotate(90deg); } .gemini-header-padding { padding-left: 32px !important; } .gemini-arrow-pos::before { left: 12px; } .chatgpt-header-padding { padding-left: 24px !important; } .chatgpt-arrow-pos::before { left: 8px; } .ucc-user-text-clamp { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; } `); const processAttribute = 'data-ucc-processed'; function applyDisplayCollapse(header, content) { header.classList.add('ucc-collapsible-header', 'ucc-arrow-indicator'); content.style.display = 'none'; header.addEventListener('click', (event) => { if (event.target.closest('button, a')) return; header.classList.toggle('ucc-header-expanded'); content.style.display = content.style.display === 'none' ? '' : 'none'; }); } function applyLineClampCollapse(header, content) { header.classList.add('ucc-collapsible-header', 'ucc-arrow-indicator'); content.classList.add('ucc-user-text-clamp'); header.addEventListener('click', (event) => { if (event.target.closest('button, a')) return; header.classList.toggle('ucc-header-expanded'); content.classList.toggle('ucc-user-text-clamp'); }); } function processGeminiCodeBlock(element) { if (element.hasAttribute(processAttribute)) return; element.setAttribute(processAttribute, 'true'); const header = element.querySelector('.code-block-decoration'); const content = element.querySelector('.formatted-code-block-internal-container'); if (header && content) { header.classList.add('gemini-header-padding', 'gemini-arrow-pos'); applyDisplayCollapse(header, content); } } function processChatGPTCodeBlock(element) { if (element.hasAttribute(processAttribute)) return; element.setAttribute(processAttribute, 'true'); const header = element.querySelector('.rounded-t-2xl'); const content = element.querySelector('.p-4'); if (header && content && !header.closest('.result-streaming')) { header.classList.add('chatgpt-header-padding', 'chatgpt-arrow-pos'); applyDisplayCollapse(header, content); } } function processChatGPTUserMessage(element) { if (element.hasAttribute(processAttribute)) return; element.setAttribute(processAttribute, 'true'); const textContainer = element.querySelector('.whitespace-pre-wrap'); if (!textContainer) return; const lineCount = textContainer.textContent.split('\n').length; const isLong = lineCount > 3 || (lineCount === 1 && textContainer.textContent.length > 200); if (isLong) { const bubble = element.querySelector('.user-message-bubble-color'); if (bubble) { bubble.classList.add('chatgpt-header-padding', 'chatgpt-arrow-pos'); applyLineClampCollapse(bubble, textContainer); } } } function scanAndProcess(node) { if (!node || node.nodeType !== Node.ELEMENT_NODE) return; const host = window.location.hostname; const selectorMap = { 'gemini.google.com': { [`code-block:not([${processAttribute}])`]: processGeminiCodeBlock, }, 'chatgpt.com': { [`pre:not([${processAttribute}])`]: processChatGPTCodeBlock, [`div[data-message-author-role="user"]:not([${processAttribute}])`]: processChatGPTUserMessage, } }; for (const hostKey in selectorMap) { if (host.includes(hostKey)) { for (const selector in selectorMap[hostKey]) { const elements = node.matches(selector) ? [node] : node.querySelectorAll(selector); elements.forEach(selectorMap[hostKey][selector]); } } } } const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.addedNodes.length) { mutation.addedNodes.forEach(node => scanAndProcess(node)); } } }); observer.observe(document.body, { childList: true, subtree: true }); scanAndProcess(document.body); })();