Bit of random stuff I find useful
Find a file
2026-05-04 13:59:23 +00:00
README.md Updated script 2026-05-04 13:59:23 +00:00

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);
    }
  })();
})();