您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
最终集大成版。支持SPA导航,采用事件委托,按钮前置,结构清晰,性能卓越。
// ==UserScript== // @name Gemini 回复折叠器 (最终版) // @name:zh-CN Gemini 回复折叠器 (最终版) // @namespace http://tampermonkey.net/ // @version 5.4 // @description 最终集大成版。支持SPA导航,采用事件委托,按钮前置,结构清晰,性能卓越。 // @description:zh-CN 最终集大成版。支持SPA导航,采用事件委托,按钮前置,结构清晰,性能卓越。 // @author Gemini & Ma // @match https://gemini.google.com/app/* // @grant GM_addStyle // @license MIT // ==/UserScript== (function() { 'use strict'; // --- 配置中心 --- const CONFIG = { selectors: { landmark: '.restart-chat-button-scroll-placeholder', chatContainer: '.content-container', modelResponse: 'model-response', responseBody: '.response-content, .markdown', messageActions: 'message-actions', }, classes: { processed: 'collapsible-processed', collapsible: 'collapsible-response', expanded: 'expanded', preview: 'paragraph-preview', ellipsis: 'paragraph-ellipsis', originalContentWrapper: 'original-content-wrapper', button: 'mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger icon-button mat-unthemed gemini-collapser-button', // 添加了自定义类名 icon: 'mat-icon notranslate google-symbols mat-ligature-font mat-icon-no-color' }, delays: { renderSettle: 500, processResponse: 100 }, attributes: { tooltip: 'mattooltip' }, text: { collapseTooltip: '收起回复', expandTooltip: '展开回复', ellipsis: '[...]', collapseIcon: 'expand_less', expandIcon: 'expand_more' } }; // --- 样式注入 --- GM_addStyle(` .paragraph-preview { padding: 0.1em 0; } .paragraph-ellipsis { text-align: center; color: var(--text-color-secondary); font-style: italic; padding: 8px 0; letter-spacing: 4px; } .collapsible-response > .original-content-wrapper { display: none !important; } .collapsible-response > .paragraph-preview { display: block !important; } .collapsible-response.expanded > .original-content-wrapper { display: block !important; } .collapsible-response.expanded > .paragraph-preview { display: none !important; } /* 按钮前置样式 */ model-response { position: relative !important; } .gemini-collapser-button { position: absolute; top: 4px; right: 4px; z-index: 10; } `); // --- 核心功能函数 --- const createToggleButton = (responseBody, isExpandedByDefault) => { const toggleButton = document.createElement('button'); toggleButton.className = CONFIG.classes.button; const matIcon = document.createElement('mat-icon'); matIcon.className = CONFIG.classes.icon; toggleButton.appendChild(matIcon); const updateButtonState = (isExpanded) => { matIcon.textContent = isExpanded ? CONFIG.text.collapseIcon : CONFIG.text.expandIcon; toggleButton.setAttribute(CONFIG.attributes.tooltip, isExpanded ? CONFIG.text.collapseTooltip : CONFIG.text.expandTooltip); }; updateButtonState(isExpandedByDefault); return toggleButton; }; const createPreviewElement = (responseBody) => { const topLevelBlocks = Array.from(responseBody.children); if (topLevelBlocks.length <= 1) return null; const previewDiv = document.createElement('div'); previewDiv.className = CONFIG.classes.preview; const allParagraphs = Array.from(responseBody.querySelectorAll('p')); if (allParagraphs.length > 0) { previewDiv.appendChild(allParagraphs[0].cloneNode(true)); if (allParagraphs.length > 1) { const ellipsisDiv = document.createElement('div'); ellipsisDiv.className = CONFIG.classes.ellipsis; ellipsisDiv.textContent = CONFIG.text.ellipsis; previewDiv.appendChild(ellipsisDiv); previewDiv.appendChild(allParagraphs[allParagraphs.length - 1].cloneNode(true)); } } else { previewDiv.appendChild(topLevelBlocks[0].cloneNode(true)); } return previewDiv; }; const processResponse = (messageActions, isNewResponse) => { const modelResponse = messageActions.closest(CONFIG.selectors.modelResponse); if (!modelResponse || modelResponse.classList.contains(CONFIG.classes.processed)) { return; } modelResponse.classList.add(CONFIG.classes.processed); setTimeout(() => { const responseBody = modelResponse.querySelector(CONFIG.selectors.responseBody); if (!responseBody) return; const previewDiv = createPreviewElement(responseBody); if (!previewDiv) return; const contentWrapper = document.createElement('div'); contentWrapper.className = CONFIG.classes.originalContentWrapper; while (responseBody.firstChild) { contentWrapper.appendChild(responseBody.firstChild); } responseBody.appendChild(previewDiv); responseBody.appendChild(contentWrapper); responseBody.classList.add(CONFIG.classes.collapsible); if (isNewResponse) { responseBody.classList.add(CONFIG.classes.expanded); } const toggleButton = createToggleButton(responseBody, isNewResponse); modelResponse.prepend(toggleButton); }, CONFIG.delays.processResponse); }; // --- 观察者逻辑 --- let mainObserver = null; let bootstrapObserver = null; const initializeForChat = () => { if (bootstrapObserver) bootstrapObserver.disconnect(); if (mainObserver) mainObserver.disconnect(); bootstrapObserver = new MutationObserver((mutations, observer) => { const landmark = document.querySelector(CONFIG.selectors.landmark); if (landmark) { observer.disconnect(); setTimeout(() => { const chatContainer = document.querySelector(CONFIG.selectors.chatContainer); if (chatContainer) { if (chatContainer._collapserClickHandler) { chatContainer.removeEventListener('click', chatContainer._collapserClickHandler); } const clickHandler = (event) => { const toggleButton = event.target.closest('.gemini-collapser-button'); if (!toggleButton) return; const modelResponse = toggleButton.closest(CONFIG.selectors.modelResponse); if (!modelResponse) return; const responseBody = modelResponse.querySelector(CONFIG.selectors.responseBody); if (!responseBody) return; const isNowExpanded = responseBody.classList.toggle(CONFIG.classes.expanded); const matIcon = toggleButton.querySelector('mat-icon'); if (matIcon) { matIcon.textContent = isNowExpanded ? CONFIG.text.collapseIcon : CONFIG.text.expandIcon; toggleButton.setAttribute(CONFIG.attributes.tooltip, isNowExpanded ? CONFIG.text.collapseTooltip : CONFIG.text.expandTooltip); } }; chatContainer.addEventListener('click', clickHandler); chatContainer._collapserClickHandler = clickHandler; mainObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { const actions = node.matches(CONFIG.selectors.messageActions) ? [node] : node.querySelectorAll(CONFIG.selectors.messageActions); actions.forEach(action => processResponse(action, true)); } } } }); mainObserver.observe(chatContainer, { childList: true, subtree: true }); chatContainer.querySelectorAll(CONFIG.selectors.messageActions).forEach(actions => processResponse(actions, false)); } }, CONFIG.delays.renderSettle); } }); bootstrapObserver.observe(document.body, { childList: true, subtree: true }); }; // --- URL导航侦听器 --- let lastProcessedUrl = ''; const navigationObserverLoop = () => { requestAnimationFrame(() => { if (window.location.href !== lastProcessedUrl) { lastProcessedUrl = window.location.href; initializeForChat(); } navigationObserverLoop(); }); }; // --- 启动脚本 --- navigationObserverLoop(); })();