您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Scrolls to the previous message sent by the user in Gemini, AI Studio, and ChatGPT.
// ==UserScript== // @name AI Chat Scroller - Scroll to Previous User Message // @version 1.3 // @namespace http://tampermonkey.net/ // Or your own unique namespace (e.g., your website, GitHub username) // @description Scrolls to the previous message sent by the user in Gemini, AI Studio, and ChatGPT. // @author WideKnotLabs // @match https://gemini.google.com/* // @match https://aistudio.google.com/* // @match https://chatgpt.com/* // @grant GM_addStyle // @license MIT // Or another open-source license you prefer (e.g., GPL-3.0-or-later) // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iY3VyclentQ29sb3IiPjxwYXRoIGQ9Ik0xMiAyQzYuNDggMiAyIDYuNDggMiAxMnM0LjQ4IDEwIDEwIDEwIDEwLTQuNDggMTAtMTBTMTcuNTIgMiAxMiAyem0wIDE4Yy00LjQxIDAtOC0zLjU5LTgtOHMzLjU5LTggOC04IDggMy41OSA4IDgtMy41OSA4LTggOHptLTEtMTFoMnYyaC0yem0wIDRoMnY2aC0yeiIvPjwvc3ZnPg== // Example icon (info icon) - optional // ==/UserScript== (function() { 'use strict'; // --- Configuration based on HTML analysis for ALL platforms --- // Selector for ALL message blocks (user and AI) on all platforms. // These should be the main containers for each distinct message entry. const allMessageBlocksSelector = [ // Gemini selectors 'span.user-query-bubble-with-background', // Gemini User 'div.response-content', // Gemini AI // AI Studio selectors 'div.user-prompt-container[data-turn-role="User"]', // AI Studio User 'div.chat-turn-container.model', // AI Studio AI (model turn) // 'div[data-turn-role="Model"]', // Alternative for AI Studio AI if data-turn-role="Model" exists // ChatGPT selectors 'div[data-message-author-role="user"]', // ChatGPT User 'div[data-message-author-role="assistant"]' // ChatGPT AI ].join(', '); // Joins into a single CSS selector string // Function to identify if a given message block is a USER message. function isUserMessage(messageElement) { // Check for Gemini user message pattern if (messageElement.matches('span.user-query-bubble-with-background')) { return true; } // Check for AI Studio user message pattern if (messageElement.matches('div.user-prompt-container[data-turn-role="User"]')) { return true; } // Check for ChatGPT user message pattern if (messageElement.matches('div[data-message-author-role="user"]')) { return true; } return false; } // --- End Configuration --- function highlightMessage(messageElement) { if (!messageElement) return; const originalOutline = messageElement.style.outline; const originalOffset = messageElement.style.outlineOffset; // Apply important to override existing styles if necessary messageElement.style.setProperty('outline', '3px dashed #007bff', 'important'); messageElement.style.setProperty('outline-offset', '2px', 'important'); setTimeout(() => { messageElement.style.outline = originalOutline; messageElement.style.outlineOffset = originalOffset; }, 2500); } function scrollToPreviousUserMessage() { const allMessages = Array.from(document.querySelectorAll(allMessageBlocksSelector)); if (!allMessages.length) { console.warn("No message blocks found with combined selector:", allMessageBlocksSelector); alert("No message blocks found. The script's selectors might need an update if the site structure changed."); return; } const userMessages = allMessages.filter(isUserMessage); if (userMessages.length === 0) { alert("No user messages found on the page based on the current HTML selectors for any platform."); return; } let targetMessage = null; // Iterate backwards through user messages to find the navigation target for (let i = userMessages.length - 1; i >= 0; i--) { const msg = userMessages[i]; const rect = msg.getBoundingClientRect(); // Is this message (msg) the one currently "active" at the top of the viewport? // "Active" means its top is within a small threshold from the viewport top. if (rect.top >= -10 && rect.top < 50 && rect.bottom > 0) { if (i > 0) { // If this "active" message is not the first user message targetMessage = userMessages[i - 1]; // Target the one before it } else { // This is the first user message and it's "active" alert("You are at your first message."); highlightMessage(msg); // Re-highlight current if it's the first msg.scrollIntoView({ behavior: 'smooth', block: 'start' }); return; // Stop further processing } break; // Found our scenario, exit loop } // If not "active", is this message the first one whose bottom is above a certain point (e.g., 10px from top of viewport)? // This means it's the highest message that is (mostly) off-screen above or just at the top edge. if (rect.bottom < 10) { targetMessage = msg; break; // This is the highest user message that is primarily off-screen above } } // Fallback logic if no message was found by the loop above // (e.g., all user messages are fully in view, or view is below all user messages) if (!targetMessage && userMessages.length > 0) { let potentialTarget = null; // Try to find the last user message whose top is above the *center* of the viewport, // giving priority to messages further up if multiple qualify. for (let i = userMessages.length - 1; i >=0; i--) { const msgRect = userMessages[i].getBoundingClientRect(); // Is the message top above the vertical midpoint of the viewport? if (msgRect.top < window.innerHeight / 2) { potentialTarget = userMessages[i]; // This message is a candidate // If this candidate is also "active" at the top, we actually want the one *before* it. if (msgRect.top >= -10 && msgRect.top < 50 && msgRect.bottom > 0 && i > 0) { potentialTarget = userMessages[i-1]; } break; // Found the highest suitable candidate by this logic } } if (potentialTarget) { targetMessage = potentialTarget; } else { // If still no target (e.g., all user messages are below the viewport center, or only one message exists) // and the first user message isn't already "active", target the first user message. const firstMsg = userMessages[0]; const firstMsgRect = firstMsg.getBoundingClientRect(); if (!(firstMsgRect.top >= -10 && firstMsgRect.top < 50 && firstMsgRect.bottom > 0) ) { targetMessage = firstMsg; } else if (userMessages.length === 1) { // Only one message, and it's active targetMessage = firstMsg; // Will trigger the "already at first message" alert or re-scroll } } } if (targetMessage) { const targetRect = targetMessage.getBoundingClientRect(); const firstUserMessageRect = userMessages[0].getBoundingClientRect(); // Check if the target is the first user message and if it's already effectively at the top if (targetMessage === userMessages[0] && targetRect.top >= -10 && targetRect.top < 50 && targetRect.bottom > 0) { alert("Already at the first user message."); } targetMessage.scrollIntoView({ behavior: 'smooth', block: 'start' }); highlightMessage(targetMessage); } else if (userMessages.length > 0) { // This is a safety net. If targetMessage is still null, but user messages exist, // it implies we are likely already at the first message. const firstMsg = userMessages[0]; const firstMsgRect = firstMsg.getBoundingClientRect(); if(firstMsgRect.top >= -10 && firstMsgRect.top < 50 && firstMsgRect.bottom > 0) { alert("Already at the first user message."); firstMsg.scrollIntoView({ behavior: 'smooth', block: 'start' }); // ensure it's perfectly at start highlightMessage(firstMsg); } else { // If something unexpected happened and no target was set, scroll to the first message. firstMsg.scrollIntoView({ behavior: 'smooth', block: 'start' }); highlightMessage(firstMsg); } } // If userMessages.length === 0, it's caught at the beginning. } // --- Button Styling and Creation --- GM_addStyle(` #universalChatScrollBtn { position: fixed !important; bottom: 20px !important; right: 20px !important; z-index: 2147483647 !important; /* Max z-index */ padding: 10px 15px !important; background-color: #1a73e8 !important; /* Google Blue */ color: white !important; border: none !important; border-radius: 8px !important; font-size: 14px !important; font-weight: bold; font-family: 'Google Sans', Roboto, Arial, sans-serif !important; cursor: pointer !important; box-shadow: 0 4px 10px rgba(0,0,0,0.25) !important; transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out !important; } #universalChatScrollBtn:hover { background-color: #1665c7 !important; /* Darker Google Blue */ } #universalChatScrollBtn:active { transform: scale(0.96) !important; background-color: #1357a9 !important; } `); const scrollButton = document.createElement('button'); scrollButton.id = 'universalChatScrollBtn'; scrollButton.textContent = '⬆️ Prev. User Msg'; scrollButton.onclick = scrollToPreviousUserMessage; // Append button when body is ready if (document.body) { document.body.appendChild(scrollButton); } else { window.addEventListener('DOMContentLoaded', () => { if(document.body) document.body.appendChild(scrollButton); }); } // Keyboard shortcut: Ctrl+Shift+ArrowUp document.addEventListener('keydown', function(e) { if (e.ctrlKey && e.shiftKey && e.key === 'ArrowUp') { e.preventDefault(); scrollToPreviousUserMessage(); } }); console.log("Universal Chat Scroller Active (Gemini, AI Studio, ChatGPT). Button added. Shortcut: Ctrl+Shift+ArrowUp"); })();