Bit of random stuff I find useful
| README.md | ||
GFGPT
Chat statistics script
Usage
On recent chats press F12 to open dev tools, switch to the Console tab, paste the script in and run it.
- Will attempt to scroll to the bottom of the chat list automatically, but can get stuck if it fails to load when scrolling to the bottom.
- If it fails to load the chats just scroll up a bit and then back down.
- If the script is still running it will continue.
- If it's already output the stats just run the script again.
(function autoScrollAndCountMessages() {
// -------- Config --------
const IDLE_SECONDS_TO_STOP = 15;
const TICK_MS = 1000;
const WIGGLE_EVERY_SECONDS = 5;
const WIGGLE_PX = 500;
const WIGGLE_STEPS = 8;
const WIGGLE_STEP_MS = 80;
const WIGGLE_SETTLE_MS = 200;
const BOTTOM_OFFSET_PX = 2;
const LOG_FLAGS = {
startup: false,
progress: false,
countDetails: false,
scrollDebug: false,
};
// -------- Helpers --------
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function logIf(flag, ...args) {
if (LOG_FLAGS[flag]) console.log(`[scroll-${flag}]`, ...args);
}
function isScrollable(el) {
if (!el || el === document.documentElement) return false;
const s = getComputedStyle(el);
const oy = s.overflowY;
return (oy === "auto" || oy === "scroll") && el.scrollHeight > el.clientHeight;
}
function findScrollableAncestor(el) {
let cur = el;
while (cur && cur !== document.body) {
if (isScrollable(cur)) return cur;
cur = cur.parentElement;
}
// Fallback: body/document scrolling
return document.scrollingElement || document.documentElement;
}
function getMaxScrollTop(container) {
return Math.max(0, container.scrollHeight - container.clientHeight);
}
function clampScrollTop(container, value) {
return Math.max(0, Math.min(value, getMaxScrollTop(container)));
}
function isDocumentScroller(container) {
return (
container === document.scrollingElement ||
container === document.documentElement ||
container === document.body
);
}
function dispatchScrollSignals(container) {
container.dispatchEvent(new Event("scroll", { bubbles: true }));
if (isDocumentScroller(container)) {
document.dispatchEvent(new Event("scroll", { bubbles: true }));
window.dispatchEvent(new Event("scroll"));
}
}
function setScrollTop(container, value) {
const target = clampScrollTop(container, value);
if (typeof container.scrollTo === "function") {
container.scrollTo({ top: target, left: 0, behavior: "auto" });
} else {
container.scrollTop = target;
}
if (container.scrollTop !== target) {
container.scrollTop = target;
}
dispatchScrollSignals(container);
}
function getWheelTarget(container) {
if (isDocumentScroller(container)) {
return document.elementFromPoint(window.innerWidth / 2, window.innerHeight * 0.85) || document.body;
}
const rect = container.getBoundingClientRect();
const x = Math.min(Math.max(rect.left + rect.width / 2, 0), window.innerWidth - 1);
const y = Math.min(Math.max(rect.bottom - 10, 0), window.innerHeight - 1);
return document.elementFromPoint(x, y) || container;
}
function dispatchWheelSignal(container, deltaY) {
const eventInit = {
bubbles: true,
cancelable: true,
deltaMode: 0,
deltaY,
view: window,
};
const targets = new Set([getWheelTarget(container), container, window]);
targets.forEach((target) => {
target.dispatchEvent(new WheelEvent("wheel", eventInit));
});
}
async function stepScrollBy(container, deltaY, steps = WIGGLE_STEPS) {
const start = container.scrollTop;
const end = clampScrollTop(container, start + deltaY);
const actualDelta = end - start;
if (!actualDelta) {
dispatchWheelSignal(container, deltaY || WIGGLE_PX);
return;
}
for (let i = 1; i <= steps; i++) {
const progress = i / steps;
const eased = progress < 0.5
? 2 * progress * progress
: 1 - Math.pow(-2 * progress + 2, 2) / 2;
dispatchWheelSignal(container, actualDelta / steps);
setScrollTop(container, start + actualDelta * eased);
await sleep(WIGGLE_STEP_MS);
}
}
async function scrollToBottom(container) {
setScrollTop(container, getMaxScrollTop(container));
}
async function performWiggle(container) {
const upDistance = Math.min(WIGGLE_PX, Math.max(80, container.clientHeight * 0.7));
await stepScrollBy(container, -upDistance);
await sleep(WIGGLE_SETTLE_MS);
const nearBottom = getMaxScrollTop(container) - BOTTOM_OFFSET_PX;
await stepScrollBy(container, nearBottom - container.scrollTop, WIGGLE_STEPS * 2);
await sleep(WIGGLE_SETTLE_MS);
dispatchWheelSignal(container, WIGGLE_PX);
setScrollTop(container, getMaxScrollTop(container));
}
function getBinLabel(count) {
if (count === 1) return "1 message (Queue)";
if (count >= 2 && count <= 20) return "2-20 messages";
if (count >= 21 && count <= 40) return "21-40 messages";
if (count >= 41 && count <= 60) return "41-60 messages";
if (count >= 61 && count <= 80) return "61-80 messages";
if (count >= 81 && count <= 100) return "81-100 messages";
if (count >= 101 && count <= 150) return "101-150 messages";
if (count >= 151 && count <= 180) return "151-180 messages";
if (count >= 181 && count <= 199) return "181-199 messages";
return "200+ messages";
}
function extractNumberFromIcon(icon) {
// Your markup is: <svg class="lucide-message-circle ..."></svg><span>203</span>
const span = icon.nextElementSibling;
if (!span || span.tagName !== "SPAN") return null;
const txt = span.textContent.trim().replace(/,/g, "");
if (!/^\d+$/.test(txt)) return null;
return Number(txt);
}
function getIconsWithin(container) {
return container.querySelectorAll("svg.lucide-message-circle");
}
function pickTargets() {
const main = document.getElementById("main");
const firstIconInMain = main?.querySelector("svg.lucide-message-circle") || null;
const firstIcon = firstIconInMain || document.querySelector("svg.lucide-message-circle");
if (!firstIcon) return null;
if (main) {
const inMain = main.querySelectorAll("svg.lucide-message-circle").length;
if (inMain > 0) {
return {
countRoot: main,
scrollContainer: isScrollable(main) ? main : findScrollableAncestor(firstIcon),
};
}
}
const scrollContainer = findScrollableAncestor(firstIcon);
return {
countRoot: scrollContainer,
scrollContainer,
};
}
function countMessages(container) {
const bins = {};
const icons = getIconsWithin(container);
let matched = 0;
icons.forEach((icon) => {
const num = extractNumberFromIcon(icon);
if (num == null) return;
matched++;
const label = getBinLabel(num);
bins[label] = (bins[label] || 0) + 1;
});
console.log("---- Count complete ----");
logIf("countDetails", "Icons found in container:", icons.length);
logIf("countDetails", "Icons with numeric <span>:", matched);
const labels = Object.keys(bins);
if (!labels.length) {
console.error("No message counts matched. The page markup may have changed; run scrollScriptDebug.start() or enable scrollScriptLogFlags.countDetails for more detail.");
const sample = icons[0];
logIf("countDetails", "Sample icon:", sample);
logIf("countDetails", "Sample icon nextElementSibling:", sample?.nextElementSibling);
return;
}
labels
.sort((a, b) => {
const extractMin = (label) => {
if (label === "1 message") return 1;
if (label === "200+ messages") return 2000;
return parseInt(label.match(/\d+/)[0], 10);
};
return extractMin(a) - extractMin(b);
})
.forEach((label) => console.log(`${label}: ${bins[label]}`));
const total = labels.reduce((sum, k) => sum + bins[k], 0);
console.log("TOTAL threads counted:", total);
}
function describeElement(el) {
if (el === window) return "window";
if (el === document) return "document";
if (!el || !el.tagName) return String(el);
const id = el.id ? `#${el.id}` : "";
const classes = typeof el.className === "string" && el.className.trim()
? `.${el.className.trim().split(/\s+/).slice(0, 4).join(".")}`
: "";
return `${el.tagName.toLowerCase()}${id}${classes}`;
}
function createScrollDebugger(scrollContainer, countRoot) {
let cleanup = [];
let observer = null;
let lastScrollLogAt = 0;
let lastMutationLogAt = 0;
const metrics = () => ({
scrollContainer: describeElement(scrollContainer),
countRoot: describeElement(countRoot),
scrollTop: scrollContainer.scrollTop,
scrollHeight: scrollContainer.scrollHeight,
clientHeight: scrollContainer.clientHeight,
maxScrollTop: getMaxScrollTop(scrollContainer),
distanceFromBottom: getMaxScrollTop(scrollContainer) - scrollContainer.scrollTop,
iconCount: getIconsWithin(countRoot).length,
});
const logEvent = (event) => {
if (event.type === "scroll" && Date.now() - lastScrollLogAt < 250) return;
if (event.type === "scroll") lastScrollLogAt = Date.now();
console.log("[scroll-debug]", event.type, {
trusted: event.isTrusted,
target: describeElement(event.target),
currentTarget: describeElement(event.currentTarget),
deltaY: event.deltaY,
key: event.key,
...metrics(),
});
};
return {
start() {
if (cleanup.length) return console.log("[scroll-debug] already running", metrics());
const targets = [...new Set([window, document, scrollContainer, countRoot])];
const types = ["wheel", "scroll", "touchmove", "keydown"];
targets.forEach((target) => {
types.forEach((type) => {
target.addEventListener(type, logEvent, { capture: true, passive: true });
cleanup.push(() => target.removeEventListener(type, logEvent, { capture: true }));
});
});
observer = new MutationObserver((records) => {
if (Date.now() - lastMutationLogAt < 500) return;
lastMutationLogAt = Date.now();
console.log("[scroll-debug] mutation", {
records: records.length,
addedNodes: records.reduce((sum, record) => sum + record.addedNodes.length, 0),
removedNodes: records.reduce((sum, record) => sum + record.removedNodes.length, 0),
...metrics(),
});
});
observer.observe(countRoot, { childList: true, subtree: true });
console.log("[scroll-debug] started. Use the mouse wheel once, then compare trusted wheel/scroll logs.", metrics());
},
stop() {
cleanup.forEach((remove) => remove());
cleanup = [];
if (observer) observer.disconnect();
observer = null;
console.log("[scroll-debug] stopped", metrics());
},
metrics,
wiggle: () => performWiggle(scrollContainer),
toBottom: () => scrollToBottom(scrollContainer),
};
}
// -------- Main runner (async, but invoked immediately) --------
(async () => {
// Wait until icons exist (SPA / lazy render)
let targets = null;
for (let i = 0; i < 60; i++) { // up to ~60 seconds of polling without blocking
targets = pickTargets();
if (targets) break;
await sleep(500);
}
if (!targets) {
console.error("Could not find any svg.lucide-message-circle on the page.");
return;
}
const { countRoot, scrollContainer } = targets;
window.scrollScriptDebug = createScrollDebugger(scrollContainer, countRoot);
window.scrollScriptLogFlags = LOG_FLAGS;
logIf("startup", "Using count root:", countRoot);
logIf("startup", "Using scroll container:", scrollContainer);
logIf("startup", "Scroll container isScrollable?", isScrollable(scrollContainer));
logIf("startup", "Initial icons in count root:", getIconsWithin(countRoot).length);
logIf("startup", "Debug helper available: scrollScriptDebug.start(), scrollScriptDebug.metrics(), scrollScriptDebug.wiggle(), scrollScriptDebug.stop()");
if (LOG_FLAGS.scrollDebug) {
window.scrollScriptDebug.start();
}
let lastHeight = scrollContainer.scrollHeight;
let lastIconCount = getIconsWithin(countRoot).length;
let lastChangeTime = Date.now();
let lastWiggleSecond = -1;
while (true) {
await sleep(TICK_MS);
const now = Date.now();
const secondsIdle = Math.floor((now - lastChangeTime) / 1000);
const currentHeight = scrollContainer.scrollHeight;
const currentIconCount = getIconsWithin(countRoot).length;
const changed = (currentHeight !== lastHeight) || (currentIconCount !== lastIconCount);
if (changed) {
lastHeight = currentHeight;
lastIconCount = currentIconCount;
lastChangeTime = now;
lastWiggleSecond = -1;
// Scroll to bottom to trigger more loading
logIf("progress", "Content changed; scrolling to bottom.", {
scrollHeight: currentHeight,
iconCount: currentIconCount,
});
await scrollToBottom(scrollContainer);
continue;
}
if (secondsIdle >= IDLE_SECONDS_TO_STOP) {
logIf("progress", `Idle for ${IDLE_SECONDS_TO_STOP}s. Scrolling complete. Starting count...`);
countMessages(countRoot);
return;
}
if (
secondsIdle > 0 &&
secondsIdle % WIGGLE_EVERY_SECONDS === 0 &&
secondsIdle !== lastWiggleSecond
) {
lastWiggleSecond = secondsIdle;
logIf("progress", `Idle for ${secondsIdle}s; performing wiggle.`);
await performWiggle(scrollContainer);
continue;
}
// Default: keep nudging to bottom
logIf("progress", "Nudging to bottom.", {
secondsIdle,
distanceFromBottom: getMaxScrollTop(scrollContainer) - scrollContainer.scrollTop,
});
await scrollToBottom(scrollContainer);
}
})();
})();