Greasy Fork is available in English.

Carrot Loading Animation for Janitor AI

Viva la carrots

// ==UserScript==
// @name         Carrot Loading Animation for Janitor AI
// @namespace    https://janitorai.com/
// @version      1.0
// @description  Viva la carrots
// @author       IWasTheSyntaxError
// @match        https://janitorai.com/chats*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  const seenContainers = new WeakSet();
  const carrotFrames = ['🥕', '🥕🥕', '🥕🥕🥕'];
  const carrotFrameInterval = 500;

  function createFloatingCarrotOverlay(p) {
    if (p.dataset.carrotOverlayAttached) return;

    // Find nearest non-static positioned parent
    let parent = p.parentElement;
    while (parent && getComputedStyle(parent).position === 'static') {
      parent = parent.parentElement;
    }
    if (!parent) return;

    // Ensure parent is relatively positioned for absolute overlay
    if (getComputedStyle(parent).position === 'static') {
      parent.style.position = 'relative';
    }

    const overlay = document.createElement('div');
    overlay.style.position = 'absolute';
    overlay.style.pointerEvents = 'none';
    overlay.style.left = `${p.offsetLeft}px`;
    overlay.style.top = `${p.offsetTop}px`;
    overlay.style.font = getComputedStyle(p).font;
    overlay.style.color = getComputedStyle(p).color;
    overlay.style.zIndex = '9999';
    overlay.style.whiteSpace = 'pre';

    const shadow = overlay.attachShadow({ mode: 'open' });
    const span = document.createElement('span');
    shadow.appendChild(span);

    let frameIndex = 0;
    const interval = setInterval(() => {
      if (!document.body.contains(p)) {
        clearInterval(interval);
        overlay.remove();
        return;
      }

      if (!/^replying/i.test(p.innerText.trim())) {
        clearInterval(interval);
        overlay.remove();
        p.removeAttribute('data-carrot-overlay-attached');
        p.style.opacity = '';
        return;
      }

      span.textContent = carrotFrames[frameIndex++ % carrotFrames.length];
    }, carrotFrameInterval);

    parent.appendChild(overlay);
    p.dataset.carrotOverlayAttached = 'true';
    p.style.opacity = '0'; // Hide the original text
  }

  function handleParagraph(p) {
    const text = p.innerText.trim();
    if (/^replying\.*/i.test(text)) {
      createFloatingCarrotOverlay(p);
    }
  }

  function observeContainer(container) {
    if (seenContainers.has(container)) return;
    seenContainers.add(container);
    console.log('[CARROT] Observing container:', container);

    const observer = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        mutation.addedNodes.forEach(node => {
          if (node.nodeType === 1) {
            if (node.tagName === 'P') {
              handleParagraph(node);
            } else {
              node.querySelectorAll?.('p')?.forEach(handleParagraph);
            }
          }
        });

        if (mutation.type === 'characterData' && mutation.target.nodeType === 3) {
          const p = mutation.target.parentElement;
          if (p?.tagName === 'P') {
            handleParagraph(p);
          }
        }
      });
    });

    observer.observe(container, {
      childList: true,
      subtree: true,
      characterData: true,
    });

    container.querySelectorAll('p').forEach(handleParagraph);
  }

  function scanAndObserve() {
    const containers = document.querySelectorAll('div.css-ijnpk1');
    const last = containers[containers.length - 1];
    if (last) observeContainer(last);
  }

  scanAndObserve();
  setInterval(scanAndObserve, 500);
})();