// ==UserScript==
// @name Gemini &Gemini & chatgpt
// @namespace https://example.com/aiHelp
// @match *://claude.ai/*
// @match *://chatgpt.com/*
// @match *://gemini.google.com/*
// @match https://gemini.google.com/app/*
// @version 1.3.2
// @author cores
// @license MIT
// @description Widescreen for Claude/ChatGPT/Gemini, Claude font fix & menu button. Gemini: code block copy & collapse buttons (header & footer). | 扩展三平台布局,Claude字体及菜单按钮。Gemini:代码块复制及头部/页脚折叠按钮。
// @grant none
// ==/UserScript==
(function() {
'use strict';
const isGemini = window.location.hostname.includes('gemini.google.com');
const isClaude = window.location.hostname.includes('claude.ai');
const isChatGPT = window.location.hostname.includes('chatgpt.com');
const style = document.createElement('style');
const commonFontStyles = `
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important;
}
`;
let platformSpecificStyles = '';
let geminiFeaturesStyles = '';
let claudeSpecificJsExecuted = false;
// --- Platform Specific Styles (保持不变) ---
if (isGemini) {
platformSpecificStyles += `/* Gemini wide screen CSS */ .chat-window, .chat-container, .conversation-container, .gemini-conversation-container { max-width: 95% !important; width: 95% !important; } .input-area-container, textarea, .prompt-textarea, .prompt-container { max-width: 95% !important; width: 95% !important; } textarea { width: 100% !important; } .max-w-3xl, .max-w-4xl, .max-w-screen-md { max-width: 95% !important; } .message-content, .user-message, .model-response { width: 100% !important; max-width: 100% !important; } .pre-fullscreen { height: auto !important; } .input-buttons-wrapper-top { right: 8px !important; }`;
} else if (isClaude) {
platformSpecificStyles += `/* Claude wide screen CSS */ .max-w-screen-md, .max-w-3xl, .max-w-4xl { max-width: 95% !important; } .w-full.max-w-3xl, .w-full.max-w-4xl { max-width: 95% !important; width: 95% !important; } .w-full.max-w-3xl textarea { width: 100% !important; } .mx-auto { max-width: 95% !important; } [data-message-author-role] { width: 100% !important; } .absolute.right-0 { right: 10px !important; } /* Claude specific font fixes */ p, h1, h2, h3, h4, h5, h6, span, div, textarea, input, button { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important; font-weight: 400 !important; } pre, code, .font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important; } [data-message-author-role] p { font-size: 16px !important; line-height: 1.5 !important; letter-spacing: normal !important; } h1, h2, h3, h4, h5, h6 { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important; }`;
} else if (isChatGPT) {
platformSpecificStyles += `/* ChatGPT wide screen CSS */ .mx-auto { max-width: 100% !important; width: auto !important; } .h-full { height: 100% !important; } .w-full { width: 100% !important; } .message-input, .input-area input, .input-area textarea { width: 100% !important; } .h-\\[116px\\] { height: auto !important; }`;
}
// --- Gemini Features (Copy Button & Collapse Buttons) ---
if (isGemini) {
const GEMINI_CODE_BLOCK_SELECTOR = 'div.code-block';
const GEMINI_CODE_CONTENT_SELECTOR = 'code[data-test-id="code-content"], pre code';
const GEMINI_CUSTOM_COPY_BUTTON_CLASS = 'gemini-custom-md-icon-copy-button'; // For custom copy button in footer
const GEMINI_FOOTER_CLASS = 'gemini-custom-code-block-footer-centered';
const ATTR_GEMINI_COPY_BUTTON_PROCESSED = 'data-gemini-copy-button-processed';
const GEMINI_CODE_HEADER_SELECTOR = 'div.code-block-decoration.header-formatted.gds-title-s';
const GEMINI_ORIGINAL_BUTTONS_CONTAINER_SELECTOR = 'div.buttons[class*="ng-star-inserted"]';
const GEMINI_COLLAPSIBLE_PANEL_SELECTOR = '.formatted-code-block-internal-container';
const ATTR_HEADER_COLLAPSE_PROCESSED = 'data-gemini-header-collapse-processed';
const ATTR_FOOTER_COLLAPSE_PROCESSED = 'data-gemini-footer-collapse-processed';
const CLASS_HEADER_COLLAPSE_BTN = 'userscript-gemini-header-collapse-btn';
const CLASS_FOOTER_COLLAPSE_BTN = 'userscript-gemini-footer-collapse-btn';
geminiFeaturesStyles = `
.${GEMINI_FOOTER_CLASS} { display: flex; justify-content: center; align-items: center; padding: 8px 0px; margin-top: 8px; gap: 8px; /* Added gap for buttons in footer */ }
.${GEMINI_CUSTOM_COPY_BUTTON_CLASS} { background-color: transparent; color: #5f6368; border: none; padding: 0; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; flex-wrap: wrap; transition: background-color 0.2s ease, color 0.2s ease, transform 0.1s ease; outline: none; }
.${GEMINI_CUSTOM_COPY_BUTTON_CLASS} .mat-icon { font-size: 24px; display: flex; align-items: center; justify-content: center; line-height: 1;flex-wrap:wrap; }
.${GEMINI_CUSTOM_COPY_BUTTON_CLASS}:hover { background-color: rgba(0, 0, 0, 0.08); color: #202124; }
.${GEMINI_CUSTOM_COPY_BUTTON_CLASS}:active { background-color: rgba(0, 0, 0, 0.12); transform: scale(0.95); }
.${CLASS_HEADER_COLLAPSE_BTN} { margin-right: 4px; } /* Space for header collapse button */
/* Footer collapse button will use GEMINI_CUSTOM_COPY_BUTTON_CLASS for base style, then its own class for identification */
.${CLASS_FOOTER_COLLAPSE_BTN}.mat-mdc-icon-button { /* Ensure it matches other footer buttons if it were generic mdc */ }
.userscript-collapsed-panel { display: none !important; }
`;
function createMaterialIconElement(iconName) {
const iconElement = document.createElement('mat-icon');
iconElement.setAttribute('role', 'img');
iconElement.setAttribute('fonticon', iconName);
iconElement.className = 'mat-icon notranslate google-symbols mat-ligature-font mat-icon-no-color';
iconElement.setAttribute('aria-hidden', 'true');
iconElement.setAttribute('data-mat-icon-type', 'font');
iconElement.setAttribute('data-mat-icon-name', iconName);
iconElement.textContent = iconName;
return iconElement;
}
function updateSingleCollapseButtonIcon(buttonElement, isPanelCollapsed) {
if (!buttonElement) return;
while (buttonElement.firstChild) buttonElement.removeChild(buttonElement.firstChild);
if (isPanelCollapsed) {
buttonElement.appendChild(createMaterialIconElement('keyboard_arrow_down'));
buttonElement.setAttribute('aria-label', '展开代码块');
buttonElement.setAttribute('mattooltip', '展开代码块');
} else {
buttonElement.appendChild(createMaterialIconElement('keyboard_arrow_up'));
buttonElement.setAttribute('aria-label', '收起代码块');
buttonElement.setAttribute('mattooltip', '收起代码块');
}
}
function syncRelatedCollapseButtons(codeBlockElement, panelIsCollapsed) {
const headerBtn = codeBlockElement.querySelector(`.${CLASS_HEADER_COLLAPSE_BTN}`);
const footerBtn = codeBlockElement.querySelector(`.${CLASS_FOOTER_COLLAPSE_BTN}`);
updateSingleCollapseButtonIcon(headerBtn, panelIsCollapsed);
updateSingleCollapseButtonIcon(footerBtn, panelIsCollapsed);
}
function addGeminiCustomCopyButton(codeBlockElement) {
if (codeBlockElement.getAttribute(ATTR_GEMINI_COPY_BUTTON_PROCESSED) === 'true') {
const existingFooter = codeBlockElement.querySelector('.' + GEMINI_FOOTER_CLASS);
const existingButton = existingFooter ? existingFooter.querySelector('.' + GEMINI_CUSTOM_COPY_BUTTON_CLASS) : null;
return { copyButton: existingButton, footerDiv: existingFooter };
}
const codeContentElement = codeBlockElement.querySelector(GEMINI_CODE_CONTENT_SELECTOR);
if (!codeContentElement) return { copyButton: null, footerDiv: null };
const copyButton = document.createElement('button');
copyButton.className = GEMINI_CUSTOM_COPY_BUTTON_CLASS;
copyButton.setAttribute('aria-label', '复制代码');
copyButton.setAttribute('title', '复制代码 (Userscript)');
copyButton.appendChild(createMaterialIconElement('content_copy'));
copyButton.addEventListener('click', async (event) => {
event.stopPropagation();
const codeText = codeContentElement.innerText;
try {
await navigator.clipboard.writeText(codeText);
while (copyButton.firstChild) copyButton.removeChild(copyButton.firstChild);
copyButton.appendChild(createMaterialIconElement('check'));
} catch (err) {
alert('无法复制代码。\nCould not copy code.');
}
setTimeout(() => {
if (copyButton.isConnected) {
while (copyButton.firstChild) copyButton.removeChild(copyButton.firstChild);
copyButton.appendChild(createMaterialIconElement('content_copy'));
}
}, 2500);
});
let footerDiv = codeBlockElement.querySelector('.' + GEMINI_FOOTER_CLASS);
if (!footerDiv) {
footerDiv = document.createElement('div');
footerDiv.className = GEMINI_FOOTER_CLASS;
codeBlockElement.appendChild(footerDiv);
}
footerDiv.appendChild(copyButton); // Append copy button first
codeBlockElement.setAttribute(ATTR_GEMINI_COPY_BUTTON_PROCESSED, 'true');
return { copyButton: copyButton, footerDiv: footerDiv };
}
function addGeminiHeaderCollapseButton(codeBlockElement, panelToCollapse) {
if (codeBlockElement.getAttribute(ATTR_HEADER_COLLAPSE_PROCESSED) === 'true' || !panelToCollapse) return;
const headerDiv = codeBlockElement.querySelector(GEMINI_CODE_HEADER_SELECTOR);
if (!headerDiv) return;
const existingButtonsDiv = headerDiv.querySelector(GEMINI_ORIGINAL_BUTTONS_CONTAINER_SELECTOR);
const collapseButton = document.createElement('button');
collapseButton.className = `mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger ${CLASS_HEADER_COLLAPSE_BTN}`;
collapseButton.setAttribute('mat-icon-button', '');
collapseButton.addEventListener('click', (event) => {
event.stopPropagation();
const isCurrentlyCollapsed = panelToCollapse.classList.toggle('userscript-collapsed-panel');
syncRelatedCollapseButtons(codeBlockElement, isCurrentlyCollapsed);
});
if (existingButtonsDiv && existingButtonsDiv.parentNode === headerDiv) {
headerDiv.insertBefore(collapseButton, existingButtonsDiv);
} else {
headerDiv.prepend(collapseButton);
}
codeBlockElement.setAttribute(ATTR_HEADER_COLLAPSE_PROCESSED, 'true');
// Initial icon will be set by a call to syncRelatedCollapseButtons later
}
function addGeminiFooterCollapseButton(codeBlockElement, panelToCollapse, footerDiv, copyButtonRef) {
if (codeBlockElement.getAttribute(ATTR_FOOTER_COLLAPSE_PROCESSED) === 'true' || !panelToCollapse || !footerDiv) return;
const collapseButton = document.createElement('button');
// Use GEMINI_CUSTOM_COPY_BUTTON_CLASS as base for footer buttons for visual consistency
collapseButton.className = `${GEMINI_CUSTOM_COPY_BUTTON_CLASS} ${CLASS_FOOTER_COLLAPSE_BTN}`;
// No need for mat-icon-button attribute if GEMINI_CUSTOM_COPY_BUTTON_CLASS doesn't imply it and handles styling
collapseButton.addEventListener('click', (event) => {
event.stopPropagation();
const isCurrentlyCollapsed = panelToCollapse.classList.toggle('userscript-collapsed-panel');
syncRelatedCollapseButtons(codeBlockElement, isCurrentlyCollapsed);
});
// Insert footer collapse button to the left of the copy button
if (copyButtonRef && copyButtonRef.parentNode === footerDiv) {
footerDiv.insertBefore(collapseButton, copyButtonRef);
} else {
footerDiv.appendChild(collapseButton); // Fallback: append if copy button isn't found in footer
}
codeBlockElement.setAttribute(ATTR_FOOTER_COLLAPSE_PROCESSED, 'true');
// Initial icon will be set by a call to syncRelatedCollapseButtons later
}
function initializeGeminiCodeBlockFeatures(codeBlockElement) {
const panelToCollapse = codeBlockElement.querySelector(GEMINI_COLLAPSIBLE_PANEL_SELECTOR);
const { copyButton, footerDiv } = addGeminiCustomCopyButton(codeBlockElement);
if (panelToCollapse) {
addGeminiHeaderCollapseButton(codeBlockElement, panelToCollapse);
if (footerDiv && copyButton) { // Ensure footer and copy button exist before adding footer collapse
addGeminiFooterCollapseButton(codeBlockElement, panelToCollapse, footerDiv, copyButton);
}
// Set initial state for all collapse buttons after they are potentially added
syncRelatedCollapseButtons(codeBlockElement, panelToCollapse.classList.contains('userscript-collapsed-panel'));
}
}
function observeGeminiCodeBlocks() {
document.querySelectorAll(GEMINI_CODE_BLOCK_SELECTOR).forEach(initializeGeminiCodeBlockFeatures);
const geminiCodeBlockObserver = new MutationObserver((mutationsList) => {
mutationsList.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches && node.matches(GEMINI_CODE_BLOCK_SELECTOR)) {
initializeGeminiCodeBlockFeatures(node);
}
node.querySelectorAll(GEMINI_CODE_BLOCK_SELECTOR).forEach(initializeGeminiCodeBlockFeatures);
}
});
}
});
});
geminiCodeBlockObserver.observe(document.body, { childList: true, subtree: true });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', observeGeminiCodeBlocks);
} else {
observeGeminiCodeBlocks();
}
}
// --- Claude Specific JavaScript (保持不变) ---
if (isClaude && !claudeSpecificJsExecuted) {
const CLAUDE_USER_MENU_BUTTON_ID = 'userscript-claude-custom-button';
const CLAUDE_SETTINGS_BUTTON_SELECTOR = 'button[data-testid="user-menu-settings"]';
function addCustomButtonToClaudeMenu() {
const settingsButton = document.querySelector(CLAUDE_SETTINGS_BUTTON_SELECTOR);
if (!settingsButton) return;
const menu = settingsButton.closest('div[role="menu"], div[data-radix-menu-content]');
if (!menu || document.getElementById(CLAUDE_USER_MENU_BUTTON_ID)) return;
const newButton = document.createElement('button');
newButton.id = CLAUDE_USER_MENU_BUTTON_ID;
newButton.className = settingsButton.className;
newButton.setAttribute('role', 'menuitem');
newButton.setAttribute('tabindex', '-1');
newButton.setAttribute('data-orientation', 'vertical');
newButton.textContent = "recents";
newButton.addEventListener('click', () => { window.location.href = 'https://claude.ai/recents'; });
if (settingsButton.parentNode) settingsButton.parentNode.insertBefore(newButton, settingsButton.nextSibling);
}
const claudeMenuObserver = new MutationObserver(addCustomButtonToClaudeMenu);
claudeMenuObserver.observe(document.body, { childList: true, subtree: true });
let claudeInputObserver;
function startClaudeInputObserver() {
if (claudeInputObserver) return;
claudeInputObserver = new MutationObserver(function() {
if (claudeInputObserver.timeoutId) return;
claudeInputObserver.timeoutId = setTimeout(function() {
document.querySelectorAll('textarea, [role="textbox"], div[contenteditable="true"]').forEach(el => {
if (el && !el.dataset.widthFixedForWide) { el.style.maxWidth = '100%'; el.dataset.widthFixedForWide = 'true'; }
});
delete claudeInputObserver.timeoutId;
}, 500);
});
const claudeForm = document.querySelector('form[enctype="multipart/form-data"], main form, body');
if (claudeForm) claudeInputObserver.observe(claudeForm, { childList: true, subtree: true, attributes: false });
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', startClaudeInputObserver);
else startClaudeInputObserver();
claudeSpecificJsExecuted = true;
}
// --- Append all styles to head ---
style.textContent = commonFontStyles + platformSpecificStyles + geminiFeaturesStyles;
document.head.appendChild(style);
// --- Widescreen Logic for inline styles (Gemini - 保持不变) ---
function applyWideModeToInlineStyles() {
if (!isGemini) return;
const elements = document.querySelectorAll('[style*="max-width"]');
elements.forEach(el => {
if (el.classList.contains('side-panel') || el.classList.contains('navigation-panel')) return;
if (typeof GEMINI_FOOTER_CLASS !== 'undefined' && el.closest(`.${GEMINI_FOOTER_CLASS}`)) return;
el.style.maxWidth = '95%';
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', applyWideModeToInlineStyles);
else applyWideModeToInlineStyles();
if (isGemini) {
const geminiWidescreenObserver = new MutationObserver(applyWideModeToInlineStyles);
geminiWidescreenObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
}
})();