Collapse/expand AI responses, auto-close the model-switched popup, and keep Grounding with Google Search enabled.
// ==UserScript==
// @name AI Studio Response Collapse for Quick Navigation
// @namespace https://mekineer.com
// @version 0.5.1
// @description Collapse/expand AI responses, auto-close the model-switched popup, and keep Grounding with Google Search enabled.
// @author mekineer and Nova
// @match https://aistudio.google.com/*
// @match https://makersuite.google.com/*
// @grant none
// @license MIT
// ==/UserScript==
(() => {
"use strict";
const CFG = {
collapsedHeightPx: 120,
minHeightToCollapsePx: 220,
contentShiftPx: 90,
};
const COLLAPSED_CLASS = "aistudio-response-collapsed";
const POPUP_TEXT = "is no longer available";
const style = document.createElement("style");
style.classList.add("darkreader");
style.textContent = `
.chat-turn-container.user {
cursor: pointer !important;
position: relative !important;
border-radius: 16px !important;
border: 2px solid #ADD8E6 !important;
box-shadow:
0 0 0 1px rgba(173,216,230,.45),
0 0 14px rgba(173,216,230,.55),
0 0 44px rgba(173,216,230,.35) !important;
background: linear-gradient(90deg, rgba(173,216,230,.20), rgba(173,216,230,.06)) !important;
}
.chat-turn-container.user .turn-separator {
background: #ADD8E6 !important;
height: 2px !important;
box-shadow: 0 0 14px rgba(173,216,230,.75) !important;
}
.chat-turn-container.user .author-label {
font-weight: 700 !important;
color: #ADD8E6 !important;
text-shadow: 0 0 12px rgba(173,216,230,.60) !important;
}
.${COLLAPSED_CLASS} {
height: ${CFG.collapsedHeightPx}px !important;
overflow: hidden !important;
position: relative !important;
border-radius: 8px !important;
transition: height 0.3s ease-out;
}
.${COLLAPSED_CLASS} > div:first-child {
margin-top: -${CFG.contentShiftPx}px !important;
}
.${COLLAPSED_CLASS}::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 20px;
background: linear-gradient(to top,
#ADD8E6 5%,
rgba(173, 216, 230, 0.0) 100%
);
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
pointer-events: none;
z-index: 2;
}
.gmz-panel {
position: fixed;
right: 14px;
bottom: 0;
z-index: 999999;
display: flex;
gap: 8px;
font: 13px/1.1 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
}
.gmz-btn {
background: rgba(0,0,0,.65);
color: #fff;
border: 1px solid rgba(255,255,255,.18);
border-radius: 999px;
padding: 8px 10px;
cursor: pointer;
user-select: none;
}
.gmz-btn:hover {
filter: brightness(1.08);
}
`;
document.documentElement.appendChild(style);
let userToTargets = new WeakMap();
const collapsedTurnIds = new Set();
let debounceTimer = null;
let automationTimer = null;
function norm(s) {
return (s || "").replace(/\s+/g, " ").trim().toLowerCase();
}
function isVisible(el) {
if (!el || !el.isConnected) return false;
const cs = getComputedStyle(el);
if (cs.display === "none" || cs.visibility === "hidden") return false;
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
}
function clickEl(el) {
if (!el || !isVisible(el)) return false;
el.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }));
el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
el.dispatchEvent(new MouseEvent("pointerup", { bubbles: true }));
el.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
el.click();
return true;
}
function getChatTurns() {
return Array.from(document.querySelectorAll("ms-chat-turn"));
}
function getTurnRole(turnEl) {
const container = turnEl.querySelector(".chat-turn-container");
if (!container) return null;
if (container.classList.contains("user")) return "user";
if (container.classList.contains("model")) return "model";
const roleNode = turnEl.querySelector("[data-turn-role]");
const role = roleNode?.getAttribute("data-turn-role");
if (!role) return null;
if (role.toLowerCase() === "user") return "user";
if (role.toLowerCase() === "model") return "model";
return null;
}
function getModelCollapseTarget(turnEl) {
if (!turnEl) return null;
return (
turnEl.querySelector(".virtual-scroll-container.model-prompt-container") ||
turnEl.querySelector("[data-turn-role='Model']") ||
null
);
}
function isLongEnough(el) {
if (!el) return false;
const rectH = Math.ceil(el.getBoundingClientRect().height || 0);
const measured = Math.max(el.scrollHeight || 0, el.offsetHeight || 0, rectH);
return measured >= CFG.minHeightToCollapsePx;
}
function collapseTarget(obj) {
if (!obj?.target) return;
if (!isLongEnough(obj.target)) return;
obj.target.classList.add(COLLAPSED_CLASS);
if (obj.turnId) collapsedTurnIds.add(obj.turnId);
}
function expandTarget(obj) {
if (!obj?.target) return;
obj.target.classList.remove(COLLAPSED_CLASS);
if (obj.turnId) collapsedTurnIds.delete(obj.turnId);
}
function rebuildPairs() {
const turns = getChatTurns();
const map = new WeakMap();
for (let i = 0; i < turns.length; i++) {
const turn = turns[i];
if (getTurnRole(turn) !== "user") continue;
const targets = [];
let j = i + 1;
while (j < turns.length && getTurnRole(turns[j]) === "model") {
const target = getModelCollapseTarget(turns[j]);
if (target) {
targets.push({
turnEl: turns[j],
target,
turnId: turns[j].id || null,
});
}
j++;
}
if (targets.length) map.set(turn, targets);
}
userToTargets = map;
}
function applyCollapsedStateFromIds() {
const turns = getChatTurns();
for (const turn of turns) {
if (!turn.id || !collapsedTurnIds.has(turn.id)) continue;
const target = getModelCollapseTarget(turn);
if (target && isLongEnough(target)) target.classList.add(COLLAPSED_CLASS);
}
}
function collapseAllPass() {
rebuildPairs();
getChatTurns().forEach((turn) => {
if (getTurnRole(turn) !== "model") return;
const target = getModelCollapseTarget(turn);
if (!target) return;
collapseTarget({ turnEl: turn, target, turnId: turn.id || null });
});
}
function collapseAll() {
collapseAllPass();
requestAnimationFrame(collapseAllPass);
setTimeout(collapseAllPass, 180);
setTimeout(collapseAllPass, 500);
setTimeout(collapseAllPass, 1000);
setTimeout(collapseAllPass, 1800);
}
function expandAll() {
document.querySelectorAll(`.${COLLAPSED_CLASS}`).forEach((el) => {
el.classList.remove(COLLAPSED_CLASS);
});
collapsedTurnIds.clear();
}
function toggleFromUserTurn(userTurnEl) {
const targets = userToTargets.get(userTurnEl);
if (!targets?.length) return;
const anyCollapsed = targets.some((obj) =>
obj.target.classList.contains(COLLAPSED_CLASS)
);
if (anyCollapsed) {
targets.forEach(expandTarget);
} else {
targets.forEach(collapseTarget);
}
}
function ensurePanel() {
if (document.querySelector(".gmz-panel")) return;
const panel = document.createElement("div");
panel.className = "gmz-panel";
const btnCollapse = document.createElement("div");
btnCollapse.className = "gmz-btn";
btnCollapse.textContent = "Collapse all";
btnCollapse.title = "Collapse all long model responses";
btnCollapse.addEventListener("click", collapseAll);
const btnExpand = document.createElement("div");
btnExpand.className = "gmz-btn";
btnExpand.textContent = "Expand all";
btnExpand.title = "Expand all model responses";
btnExpand.addEventListener("click", expandAll);
panel.append(btnCollapse, btnExpand);
document.body.appendChild(panel);
}
function getGroundingButton() {
return document.querySelector(
'button[role="switch"][aria-label="Grounding with Google Search"]'
);
}
function enableGoogleGrounding() {
const btn = getGroundingButton();
if (!btn || !isVisible(btn)) return false;
if (btn.getAttribute("aria-checked") === "true") return false;
return clickEl(btn);
}
function dismissModelChangedPopup() {
const all = Array.from(document.querySelectorAll("body *")).filter(isVisible);
const popup = all.find((el) => norm(el.textContent).includes(POPUP_TEXT));
if (!popup) return false;
const root =
popup.closest('[role="dialog"], [aria-modal="true"], mat-snack-bar-container, snack-bar-container, .cdk-overlay-pane')
|| popup;
const closeBtn = root.querySelector(
'button, [role="button"], ms-button, [aria-label="Close"], [aria-label="Dismiss"]'
);
if (closeBtn && isVisible(closeBtn)) return clickEl(closeBtn);
document.dispatchEvent(
new KeyboardEvent("keydown", { key: "Escape", code: "Escape", bubbles: true })
);
return true;
}
function runAutomationPass() {
dismissModelChangedPopup();
enableGoogleGrounding();
}
function runAutomationBurst() {
clearTimeout(automationTimer);
runAutomationPass();
setTimeout(runAutomationPass, 150);
setTimeout(runAutomationPass, 500);
setTimeout(runAutomationPass, 1200);
}
function onDocumentClick(e) {
if (window.getSelection()?.toString()?.trim()) return;
if (e.target.closest("button, a, input, textarea, select, mat-icon, ms-button")) return;
const userContainer = e.target.closest(".chat-turn-container.user");
if (!userContainer) return;
const userTurn = userContainer.closest("ms-chat-turn");
if (!userTurn) return;
rebuildPairs();
toggleFromUserTurn(userTurn);
}
function scheduleUpdate() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
ensurePanel();
rebuildPairs();
applyCollapsedStateFromIds();
runAutomationBurst();
}, 120);
}
function start() {
ensurePanel();
rebuildPairs();
applyCollapsedStateFromIds();
runAutomationBurst();
document.addEventListener("click", onDocumentClick, true);
document.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) runAutomationBurst();
}, true);
const obs = new MutationObserver(scheduleUpdate);
obs.observe(document.body, { childList: true, subtree: true });
scheduleUpdate();
}
start();
})();