PC版Google Geminiを使っていると、会話が長くなるにつれて「さっきの自分の質問を修正したい」「一番最初になんて指示したっけ?」と過去のログを遡るのが大変になりませんか?
Geminiの仕様上、古い質問の「編集アイコン」が消えてしまったり、スクロールが長大で指が疲れたりします。 そこで、チャット内の「自分の質問」だけを検知してジャンプできるUserScriptを作成しました。
【できること】
画面右端にナビゲーションボタンが追加されます。
⇈ (Top): チャット最上部へジャンプ
▲ (Prev): 一つ前の自分の質問へジャンプ
▼ (Next): 一つ次の自分の質問へジャンプ
⇊ (Bottom): チャット最下部(最新)へジャンプ
【その他の特徴】
キーボードショートカット対応: Alt + ↑ / ↓ でサクサク移動できます。
高速スクロール: スクロールアニメーションを高速化しており、キビキビ動きます。
自動検出: Geminiの仕様変更(スクロールバーの位置が変わる等)にも強いロジックを入れています。
【導入方法】
Chrome拡張機能「Tampermonkey」をインストールします。
Tampermonkeyのアイコンをクリックし、「新規スクリプトを追加」を選択。
以下のコードをすべてコピペして保存(Ctrl+S)してください。
【ソースコード】
// ==UserScript==
// @name Gemini Chat Navigator
// @namespace https://github.com/YourName/GeminiNavigator
// @version 9.0
// @description Geminiのチャット履歴を「最上部・最下部・前・次」へジャンプできるボタンを追加します。編集ボタンが消えても動作するスクロール自動検出機能付き。
// @author GekiyasuGT
// @match https://gemini.google.com/*
// @grant none
// @license MIT
// ==/UserScript==(function() {
‘use strict’;/* 【概要】
Google Gemini (PC版) のチャット画面右端にナビゲーションボタンを追加します。
長くなったチャットの「一番上」「一番下」「一つ前の質問」「一つ次の質問」へ瞬時に移動できます。【特徴】
– 4つのボタン(最上部 / 前へ / 次へ / 最下部)
– キーボードショートカット対応 (Alt + ↑/↓/Home/End)
– 高速スムーズスクロール
– スクロール対象のコンテナ(ウィンドウ or 内部div)を自動検出
*/// ▼ 設定: アニメーション速度(ミリ秒)
const SCROLL_DURATION = 300;// ▼ 設定: ボタン配置(画面右端・上下中央)
const BUTTON_STYLE = `
position: fixed;
right: 20px;
top: 50%;
transform: translateY(-50%); /* コンテナ全体を上下中央に配置 */
z-index: 9999;
display: flex;
flex-direction: column;
gap: 12px;
`;// ▼ 設定: ボタンのデザイン
const BTN_CSS = `
width: 50px;
height: 50px;
background: rgba(30, 31, 32, 0.6);
color: white;
border: 1px solid #777;
border-radius: 50%;
cursor: pointer;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-shadow: 0 4px 10px rgba(0,0,0,0.5);
user-select: none;
backdrop-filter: blur(4px);
`;let navContainer = null;
let isScrolling = false;// UI作成
function createUI() {
if (document.getElementById(‘gemini-nav-buttons’)) return;navContainer = document.createElement(‘div’);
navContainer.id = ‘gemini-nav-buttons’;
navContainer.style.cssText = BUTTON_STYLE;const topBtn = createBtn(‘⇈’, ‘最上部へ (Alt+Home)’, () => scrollToEdge(‘top’));
const prevBtn = createBtn(‘▲’, ‘前の質問 (Alt+↑)’, () => jumpToPrompt(-1));
const nextBtn = createBtn(‘▼’, ‘次の質問 (Alt+↓)’, () => jumpToPrompt(1));
const bottomBtn = createBtn(‘⇊’, ‘最下部へ (Alt+End)’, () => scrollToEdge(‘bottom’));navContainer.appendChild(topBtn);
navContainer.appendChild(prevBtn);
navContainer.appendChild(nextBtn);
navContainer.appendChild(bottomBtn);document.body.appendChild(navContainer);
}function createBtn(text, title, onClick) {
const btn = document.createElement(‘button’);
btn.innerText = text;
btn.title = title;
btn.style.cssText = BTN_CSS;btn.onclick = (e) => {
e.stopPropagation();
onClick();
btn.blur();
};// ホバーエフェクト(修正済み:位置ズレを起こさないようにscaleのみ適用)
btn.onmouseover = () => {
btn.style.background = ‘rgba(60, 64, 67, 1)’;
btn.style.transform = ‘scale(1.1)’;
btn.style.borderColor = ‘#aaa’;
};
btn.onmouseout = () => {
btn.style.background = ‘rgba(30, 31, 32, 0.6)’;
btn.style.transform = ‘scale(1)’;
btn.style.borderColor = ‘#777’;
};
return btn;
}// ▼▼▼ ユーティリティ: スクロール親要素の特定 ▼▼▼
function getScrollParent(node) {
if (!node) return document.documentElement;
let parent = node.parentElement;
while (parent) {
const style = window.getComputedStyle(parent);
if ([‘scroll’, ‘auto’].includes(style.overflowY) && parent.scrollHeight > parent.clientHeight) {
return parent;
}
parent = parent.parentElement;
}
return document.documentElement;
}function getAllPrompts() {
let els = document.querySelectorAll(‘user-query’);
if (els.length === 0) els = document.querySelectorAll(‘.user-query’);
if (els.length === 0) els = document.querySelectorAll(‘[data-test-id=”user-query”]’);
return Array.from(els);
}// ▼▼▼ 共通スクロールアニメーション関数 ▼▼▼
function animateScroll(container, targetY) {
isScrolling = true;
const isWindow = (container === document.documentElement || container === document.body);
const startY = isWindow ? window.scrollY : container.scrollTop;
const diff = targetY – startY;
const startTime = performance.now();function step(currentTime) {
const elapsed = currentTime – startTime;
let progress = Math.min(elapsed / SCROLL_DURATION, 1);// イージング (easeOutQuart)
const ease = 1 – Math.pow(1 – progress, 4);
const currentPos = startY + (diff * ease);if (isWindow) {
window.scrollTo(0, currentPos);
} else {
container.scrollTop = currentPos;
}if (progress < 1) { requestAnimationFrame(step); } else { isScrolling = false; } } requestAnimationFrame(step); } // ▼▼▼ 機能1: 最上部・最下部へジャンプ ▼▼▼ function scrollToEdge(direction) { if (isScrolling) return; const prompts = getAllPrompts(); const refEl = prompts.length > 0 ? prompts[0] : document.body;
const container = getScrollParent(refEl);let targetY = 0;
if (direction === ‘bottom’) {
targetY = container.scrollHeight;
}animateScroll(container, targetY);
}// ▼▼▼ 機能2: 前後の質問へジャンプ ▼▼▼
function jumpToPrompt(directionStep) {
if (isScrolling) return;const prompts = getAllPrompts();
if (prompts.length === 0) return;const viewportCenter = window.innerHeight / 2;
let currentIndex = 0;
let minDiff = Infinity;prompts.forEach((el, index) => {
const rect = el.getBoundingClientRect();
const elCenter = rect.top + (rect.height / 2);
const diff = Math.abs(elCenter – viewportCenter);if (diff < minDiff) { minDiff = diff; currentIndex = index; } }); let targetIndex = currentIndex + directionStep; if (targetIndex < 0) { flashButton(navContainer.children[1], 'orange'); // prevBtn return; } else if (targetIndex >= prompts.length) {
flashButton(navContainer.children[2], ‘orange’); // nextBtn
return;
}const targetEl = prompts[targetIndex];
const container = getScrollParent(targetEl);const isWindow = (container === document.documentElement || container === document.body);
const startY = isWindow ? window.scrollY : container.scrollTop;const rect = targetEl.getBoundingClientRect();
const offset = rect.top – (window.innerHeight / 2) + (rect.height / 2);
const targetY = startY + offset;animateScroll(container, targetY);
highlightElement(targetEl);
}function highlightElement(el) {
const originalTransition = el.style.transition;
const originalShadow = el.style.boxShadow;
el.style.transition = “box-shadow 0.3s”;
el.style.boxShadow = “0 0 0 4px rgba(66, 133, 244, 0.6)”;
setTimeout(() => {
el.style.boxShadow = originalShadow;
el.style.transition = originalTransition;
}, 600);
}function flashButton(btn, color) {
if (!btn) return;
const originalBorder = btn.style.borderColor;
btn.style.borderColor = color;
btn.style.borderWidth = ‘2px’;
setTimeout(() => {
btn.style.borderColor = originalBorder;
btn.style.borderWidth = ‘1px’;
}, 300);
}// キーボードショートカット
document.addEventListener(‘keydown’, (e) => {
const tag = e.target.tagName.toLowerCase();
if (tag === ‘textarea’ || tag === ‘input’ || e.target.isContentEditable) return;if (e.altKey) {
switch(e.key) {
case ‘ArrowUp’:
e.preventDefault();
jumpToPrompt(-1);
break;
case ‘ArrowDown’:
e.preventDefault();
jumpToPrompt(1);
break;
case ‘Home’:
e.preventDefault();
scrollToEdge(‘top’);
break;
case ‘End’:
e.preventDefault();
scrollToEdge(‘bottom’);
break;
}
}
});setInterval(createUI, 1500);
})();
【さいごに】 これで長い会話もストレスフリーになります。自由に改変して使ってください!
あ、あと動かない場合は chromeの拡張機能を管理 → ユーザー スクリプトを許可する をONにしてみてください。
