最终集大成版。支持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();
})();