JanitorAI - Message Logger Library

Standalone message-logging library for JanitorAI. Tracks the latest bot and user messages, their status (Complete/Streaming/Editing), swipe index, and text. Exposes a global API (window.JaiMessageLogger) for use by other scripts via @require.

Este script não deve ser instalado diretamente. Este script é uma biblioteca de outros scripts para incluir com o diretório meta // @require https://update.greatest.deepsurf.us/scripts/572546/1790490/JanitorAI%20-%20Message%20Logger%20Library.js

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         JanitorAI - Message Logger Library
// @namespace    http://tampermonkey.net/
// @license      MIT
// @version      1.0.0
// @description  Standalone message-logging library for JanitorAI. Tracks the latest bot and user messages, their status (Complete/Streaming/Editing), swipe index, and text. Exposes a global API (window.JaiMessageLogger) for use by other scripts via @require.
// @author       ZephyrThePink - @ZephyrThePink on Discord
// @match        https://janitorai.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=janitorai.com
// @grant        unsafeWindow
// ==/UserScript==

/**
 * ╔══════════════════════════════════════════════════════════════╗
 * ║              JanitorAI  Message Logger Library              ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║                                                              ║
 * ║  Detects and logs the latest bot & user messages in a        ║
 * ║  JanitorAI chat via MutationObserver.                        ║
 * ║                                                              ║
 * ║  Message index rules:                                        ║
 * ║    • Even indices (0, 2, 4, …) → bot messages                ║
 * ║    • Odd  indices (1, 3, 5, …) → user messages               ║
 * ║    • Index 0 is always the welcome/greeting message           ║
 * ║                                                              ║
 * ║  Status rules:                                               ║
 * ║    • Bot:  Complete | Streaming | Editing                    ║
 * ║    • User: Complete | Editing  (users never stream)          ║
 * ║    • Complete = has _controlPanel_ element (or index 0)      ║
 * ║    • Editing  = has _editPanel_ element                      ║
 * ║    • Streaming = bot message with neither panel              ║
 * ║                                                              ║
 * ║  Only bot messages can have swipes.                          ║
 * ║                                                              ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  USAGE (from another userscript)                             ║
 * ║                                                              ║
 * ║  // @require  <url-to-this-file>                             ║
 * ║                                                              ║
 * ║  // Wait for the library to be ready:                        ║
 * ║  function onReady(api) {                                     ║
 * ║    const bot = api.bot.get();                                ║
 * ║    console.log(bot.text, bot.status, bot.swipeIndex);        ║
 * ║  }                                                           ║
 * ║  if (window.JaiMessageLogger?.ready) {                       ║
 * ║    onReady(window.JaiMessageLogger);                         ║
 * ║  } else {                                                    ║
 * ║    window.addEventListener('jai-msglogger:ready', (e) => {   ║
 * ║      onReady(e.detail);                                      ║
 * ║    });                                                       ║
 * ║  }                                                           ║
 * ║                                                              ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  API REFERENCE  (window.JaiMessageLogger)                    ║
 * ║                                                              ║
 * ║  .ready              → true once the library has loaded      ║
 * ║  .version             → library version string               ║
 * ║                                                              ║
 * ║  ── Getters ──────────────────────────────────────────────── ║
 * ║  .bot.get()           → { text, status, swipeIndex,          ║
 * ║                           messageIndex }                     ║
 * ║  .bot.getText()       → string                               ║
 * ║  .bot.getStatus()     → "Complete"|"Streaming"|"Editing"|""  ║
 * ║  .bot.getSwipeIndex() → number (-1 if unknown)               ║
 * ║  .bot.getIndex()      → number (-1 if unknown)               ║
 * ║  .bot.getNode()       → HTMLElement | null                   ║
 * ║  .bot.isStreaming()   → boolean                               ║
 * ║  .bot.isComplete()    → boolean                               ║
 * ║  .bot.isEditing()     → boolean                               ║
 * ║                                                              ║
 * ║  .user.get()          → { text, status, messageIndex }       ║
 * ║  .user.getText()      → string                               ║
 * ║  .user.getStatus()    → "Complete"|"Editing"|""              ║
 * ║  .user.getIndex()     → number (-1 if unknown)               ║
 * ║  .user.getNode()      → HTMLElement | null                   ║
 * ║  .user.isEditing()    → boolean                               ║
 * ║  .user.isComplete()   → boolean                               ║
 * ║                                                              ║
 * ║  ── Utilities ────────────────────────────────────────────── ║
 * ║  .extractText(node)   → string  (extract text from any       ║
 * ║                          message wrapper node)               ║
 * ║  .getAllMessages()    → [ { role, text, status, index,       ║
 * ║                             swipeIndex, node } ]             ║
 * ║  .getMessageAt(idx)   → object | null (get message by        ║
 * ║                          data-index)                         ║
 * ║  .getMessageCount()  → number (total message nodes in DOM)   ║
 * ║  .isOnChatPage()      → boolean                               ║
 * ║  .poll()              → void  (force a detection pass)       ║
 * ║                                                              ║
 * ║  ── Observer Control ─────────────────────────────────────── ║
 * ║  .start()             → void  (start / restart observer)     ║
 * ║  .stop()              → void  (disconnect observer)          ║
 * ║  .reset()             → void  (clear state + stop observer)  ║
 * ║                                                              ║
 * ║  ── Callbacks ────────────────────────────────────────────── ║
 * ║  .bot.onStatusChange(cb)                                     ║
 * ║     cb(status, node, messageIndex, swipeIndex)               ║
 * ║     Returns unsubscribe function.                            ║
 * ║                                                              ║
 * ║  .bot.onSwipeChange(cb)                                      ║
 * ║     cb(status, node, messageIndex, swipeIndex)               ║
 * ║     Returns unsubscribe function.                            ║
 * ║                                                              ║
 * ║  .user.onStatusChange(cb)                                    ║
 * ║     cb(status, node, messageIndex)                           ║
 * ║     Returns unsubscribe function.                            ║
 * ║                                                              ║
 * ║  ── Logging Control ──────────────────────────────────────── ║
 * ║  .logging.enable()    → void  (turn on console logging)      ║
 * ║  .logging.disable()   → void  (turn off console logging)     ║
 * ║  .logging.isEnabled() → boolean                               ║
 * ║                                                              ║
 * ╠══════════════════════════════════════════════════════════════╣
 * ║  CONSOLE SHORTCUTS                                           ║
 * ║                                                              ║
 * ║  JaiMessageLogger.bot.get()                                  ║
 * ║  JaiMessageLogger.user.get()                                 ║
 * ║  JaiMessageLogger.getAllMessages()                            ║
 * ║  JaiMessageLogger.getMessageAt(4)                            ║
 * ║                                                              ║
 * ╚══════════════════════════════════════════════════════════════╝
 */

(function () {
    'use strict';

    // ── Selectors ───────────────────────────────────────────────
    const CHAT_CONTAINER_SELECTOR         = '[class^="_messagesMain_"]';
    const MESSAGE_CONTAINER_SELECTOR      = '[data-testid="virtuoso-item-list"] > div[data-index]';
    const LAST_MESSAGE_SWIPE_CONTAINER    = '[class^="_botChoicesContainer_"]';
    const SWIPE_SLIDER_SELECTOR           = '[class^="_botChoicesSlider_"]';
    const MESSAGE_WRAPPER_SELECTOR        = 'li[class^="_messageDisplayWrapper_"]';
    const MESSAGE_BODY_SELECTOR           = '[class^="_messageBody_"]';
    const EDIT_PANEL_SELECTOR             = '[class^="_editPanel_"]';
    const CONTROL_PANEL_SELECTOR          = '[class^="_controlPanel_"]';

    // ── Internal state ──────────────────────────────────────────
    let botState  = { text: '', status: '', swipeIndex: -1, messageIndex: -1 };
    let userState = { text: '', status: '', messageIndex: -1 };
    let botNode   = null;   // active bot wrapper DOM node
    let userNode  = null;   // active user wrapper DOM node
    let observer  = null;
    let loggingEnabled = true;

    // ── Callback registries ─────────────────────────────────────
    const botStatusCallbacks   = [];
    const botSwipeCallbacks    = [];
    const userStatusCallbacks  = [];

    /** Helper: subscribe to a callback array and return an unsubscribe fn */
    function subscribe(arr, cb) {
        arr.push(cb);
        return function unsubscribe() {
            const idx = arr.indexOf(cb);
            if (idx !== -1) arr.splice(idx, 1);
        };
    }

    // ── Text extraction ─────────────────────────────────────────

    /**
     * Extract readable text from a message wrapper node, preserving
     * basic formatting markers (*italic*, **bold**, `code`).
     */
    function extractMessageText(messageNode) {
        const messageBody = messageNode.querySelector(MESSAGE_BODY_SELECTOR);
        if (!messageBody) return '[No text found]';

        // Find the main text container (skip name, think, rating wrappers)
        let textContainer = null;
        for (const child of messageBody.children) {
            if (
                child.tagName === 'DIV' &&
                !child.className.match(/_nameContainer_/) &&
                !child.className.match(/_thinkTagContainer_/) &&
                !child.className.match(/_ratingWrapper_/)
            ) {
                textContainer = child;
                break;
            }
        }
        if (!textContainer) {
            // Editing mode fallback – textarea
            const textarea = messageBody.querySelector('textarea[class^="_autoResizeTextarea_"]');
            if (textarea) return textarea.value.trim() || '[No text found]';
            return '[No text found]';
        }

        function extractParagraphText(p) {
            let line = '';
            p.childNodes.forEach(child => {
                if (child.nodeType === Node.ELEMENT_NODE) {
                    if (child.tagName === 'EM')     line += '*' + child.textContent + '*';
                    else if (child.tagName === 'STRONG') line += '**' + child.textContent + '**';
                    else if (child.tagName === 'CODE')   line += '`' + child.textContent + '`';
                    else line += child.textContent;
                } else if (child.nodeType === Node.TEXT_NODE) {
                    line += child.textContent;
                }
            });
            return line.trim();
        }

        const result = [];
        const blocks = textContainer.querySelectorAll('[class^="css-"]');

        if (blocks.length > 0) {
            blocks.forEach(block => {
                if (block.closest('[class*="_thinkTagContainer_"]')) return;
                const p = block.querySelector('p');
                if (p) { const l = extractParagraphText(p); if (l) result.push(l); return; }
                const ul = block.querySelector('ul');
                if (ul) { ul.querySelectorAll('li').forEach(li => result.push('• ' + li.textContent.trim())); return; }
                const code = block.querySelector('code');
                if (code && !p) { result.push('`' + code.textContent.trim() + '`'); return; }
                if (!block.textContent.trim()) return;
                result.push(block.textContent.trim());
            });
        }

        if (result.length === 0) {
            const paragraphs = textContainer.querySelectorAll('p');
            paragraphs.forEach(p => {
                if (p.closest('[class*="_thinkTagContainer_"]')) return;
                const line = extractParagraphText(p);
                if (line) result.push(line);
            });
        }

        if (result.length === 0) {
            const clone = textContainer.cloneNode(true);
            clone.querySelectorAll('[class*="_thinkTagContainer_"], [class*="_thinkContent_"]').forEach(el => el.remove());
            const text = clone.textContent.trim();
            if (text) result.push(text);
        }

        return result.length ? result.join('\n') : '[No text found]';
    }

    // ── Wrapper resolution ──────────────────────────────────────

    /**
     * For a given message container node identify the visible wrapper,
     * its current swipe index (bot only), and whether it's being edited.
     */
    function resolveMessageWrapper(node, isBot) {
        let candidateNode = null;
        let swipeIndex = 0;

        if (isBot) {
            const swipeContainer = node.querySelector(LAST_MESSAGE_SWIPE_CONTAINER);
            if (swipeContainer) {
                const slider = swipeContainer.querySelector(SWIPE_SLIDER_SELECTOR);
                if (!slider) return null;
                const transform = slider.style.transform;
                const translateX = transform
                    ? parseFloat(transform.match(/translateX\(([-0-9.]+)%\)/)?.[1] || '0')
                    : 0;
                swipeIndex = Math.round(Math.abs(translateX) / 100);
                const allSwipes = slider.querySelectorAll(MESSAGE_WRAPPER_SELECTOR);
                if (allSwipes.length <= swipeIndex) return null;
                candidateNode = allSwipes[swipeIndex];
            } else {
                candidateNode = node.querySelector(MESSAGE_WRAPPER_SELECTOR);
            }
        } else {
            candidateNode = node.querySelector(MESSAGE_WRAPPER_SELECTOR);
        }

        if (!candidateNode) return null;
        const isBeingEdited = !!candidateNode.querySelector(EDIT_PANEL_SELECTOR);
        return { wrapperNode: candidateNode, swipeIndex, isBeingEdited };
    }

    // ── Core detection loop ─────────────────────────────────────

    function detectMessages() {
        const allMessageNodes = document.querySelectorAll(MESSAGE_CONTAINER_SELECTOR);
        if (allMessageNodes.length === 0) return;

        let foundBot = false;
        let foundUser = false;

        for (let i = allMessageNodes.length - 1; i >= 0; i--) {
            if (foundBot && foundUser) break;

            const node = allMessageNodes[i];
            const dataIndex = parseInt(node.dataset.index, 10);
            if (isNaN(dataIndex)) continue;

            const isBot = dataIndex % 2 === 0;
            if (isBot && foundBot) continue;
            if (!isBot && foundUser) continue;

            const resolved = resolveMessageWrapper(node, isBot);
            if (!resolved) continue;

            const { wrapperNode, swipeIndex, isBeingEdited } = resolved;

            // ── Status detection ──
            let status;
            if (!isBot) {
                status = isBeingEdited ? 'Editing' : 'Complete';
            } else if (isBeingEdited) {
                status = 'Editing';
            } else {
                const hasControlPanel = !!wrapperNode.querySelector(CONTROL_PANEL_SELECTOR);
                status = (hasControlPanel || dataIndex === 0) ? 'Complete' : 'Streaming';
            }

            const messageText = extractMessageText(wrapperNode);

            // ── Bot ──
            if (isBot) {
                const prev = botState;
                const statusChanged = status !== prev.status;
                const swipeChanged  = swipeIndex !== prev.swipeIndex;
                const indexChanged  = dataIndex !== prev.messageIndex;
                const textChanged   = messageText !== prev.text;

                if (textChanged) botState = { ...botState, text: messageText };

                const shouldNotify = statusChanged || swipeChanged || indexChanged;

                if (shouldNotify) {
                    botState = { text: messageText, status, swipeIndex, messageIndex: dataIndex };

                    if (loggingEnabled) {
                        const ts = new Date().toLocaleTimeString();
                        console.group(`📨 [MessageLogger:Bot] ${ts}`);
                        console.log(`📍 Message Index: ${dataIndex}`);
                        console.log(`🔄 Swipe: ${swipeIndex + 1} (0-indexed: ${swipeIndex})`);
                        console.log(`📊 Status: ${status}`);
                        console.log(`📝 Text (${messageText.length} chars):`);
                        console.log(messageText);
                        const changes = [];
                        if (statusChanged) changes.push(`status: ${prev.status || 'none'} → ${status}`);
                        if (swipeChanged)  changes.push(`swipe: ${prev.swipeIndex} → ${swipeIndex}`);
                        if (indexChanged)  changes.push(`index: ${prev.messageIndex} → ${dataIndex}`);
                        if (changes.length) console.log(`🔔 Changes: ${changes.join(', ')}`);
                        console.groupEnd();
                    }

                    if (statusChanged) {
                        for (const cb of botStatusCallbacks) {
                            try { cb(status, wrapperNode, dataIndex, swipeIndex); } catch (e) { console.error('[MessageLogger] bot status callback error:', e); }
                        }
                    }
                    if (swipeChanged || indexChanged) {
                        for (const cb of botSwipeCallbacks) {
                            try { cb(status, wrapperNode, dataIndex, swipeIndex); } catch (e) { console.error('[MessageLogger] bot swipe callback error:', e); }
                        }
                    }
                }

                botNode = wrapperNode;
                foundBot = true;

            // ── User ──
            } else {
                const prev = userState;
                const statusChanged = status !== prev.status;
                const indexChanged  = dataIndex !== prev.messageIndex;
                const textChanged   = messageText !== prev.text;

                if (textChanged) userState = { ...userState, text: messageText };

                const shouldNotify = statusChanged || indexChanged;

                if (shouldNotify) {
                    userState = { text: messageText, status, messageIndex: dataIndex };

                    if (loggingEnabled) {
                        const ts = new Date().toLocaleTimeString();
                        console.group(`👤 [MessageLogger:User] ${ts}`);
                        console.log(`📍 Message Index: ${dataIndex}`);
                        console.log(`📊 Status: ${status}`);
                        console.log(`📝 Text (${messageText.length} chars):`);
                        console.log(messageText);
                        const changes = [];
                        if (statusChanged) changes.push(`status: ${prev.status || 'none'} → ${status}`);
                        if (indexChanged)  changes.push(`index: ${prev.messageIndex} → ${dataIndex}`);
                        if (changes.length) console.log(`🔔 Changes: ${changes.join(', ')}`);
                        console.groupEnd();
                    }

                    if (userStatusCallbacks.length > 0) {
                        for (const cb of userStatusCallbacks) {
                            try { cb(status, wrapperNode, dataIndex); } catch (e) { console.error('[MessageLogger] user status callback error:', e); }
                        }
                    }
                }

                userNode = wrapperNode;
                foundUser = true;
            }
        }
    }

    // ── Helpers for the public utilities ─────────────────────────

    /**
     * Build a message descriptor for a given container node.
     */
    function describeMessage(node) {
        const dataIndex = parseInt(node.dataset.index, 10);
        if (isNaN(dataIndex)) return null;
        const isBot = dataIndex % 2 === 0;
        const resolved = resolveMessageWrapper(node, isBot);
        if (!resolved) return null;

        const { wrapperNode, swipeIndex, isBeingEdited } = resolved;
        let status;
        if (!isBot) {
            status = isBeingEdited ? 'Editing' : 'Complete';
        } else if (isBeingEdited) {
            status = 'Editing';
        } else {
            const hasControlPanel = !!wrapperNode.querySelector(CONTROL_PANEL_SELECTOR);
            status = (hasControlPanel || dataIndex === 0) ? 'Complete' : 'Streaming';
        }

        return {
            role: isBot ? 'bot' : 'user',
            text: extractMessageText(wrapperNode),
            status,
            index: dataIndex,
            swipeIndex: isBot ? swipeIndex : null,
            node: wrapperNode
        };
    }

    // ── Observer management ─────────────────────────────────────

    function startObserver() {
        if (observer) return; // already running
        const container = document.querySelector(CHAT_CONTAINER_SELECTOR);
        if (!container) {
            setTimeout(startObserver, 1000);
            return;
        }

        observer = new MutationObserver(() => detectMessages());
        observer.observe(container, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['style'],
            characterData: true
        });

        detectMessages();
        console.log('[MessageLogger] 🟢 Observer started');
    }

    function stopObserver() {
        if (observer) {
            observer.disconnect();
            observer = null;
            console.log('[MessageLogger] 🔴 Observer stopped');
        }
    }

    function resetState() {
        stopObserver();
        botState  = { text: '', status: '', swipeIndex: -1, messageIndex: -1 };
        userState = { text: '', status: '', messageIndex: -1 };
        botNode   = null;
        userNode  = null;
        console.log('[MessageLogger] 🔄 State reset');
    }

    // ── Public API object ───────────────────────────────────────

    const API = {
        ready: true,
        version: '1.0.0',

        // ── Bot sub-API ──
        bot: {
            get()          { return { ...botState }; },
            getText()      { return botState.text; },
            getStatus()    { return botState.status; },
            getSwipeIndex(){ return botState.swipeIndex; },
            getIndex()     { return botState.messageIndex; },
            getNode()      { return botNode; },
            isStreaming()  { return botState.status === 'Streaming'; },
            isComplete()   { return botState.status === 'Complete'; },
            isEditing()    { return botState.status === 'Editing'; },
            /** Register a callback for bot status changes. Returns unsubscribe fn. */
            onStatusChange(cb) { return subscribe(botStatusCallbacks, cb); },
            /** Register a callback for swipe / message-index changes. Returns unsubscribe fn. */
            onSwipeChange(cb)  { return subscribe(botSwipeCallbacks, cb); }
        },

        // ── User sub-API ──
        user: {
            get()        { return { ...userState }; },
            getText()    { return userState.text; },
            getStatus()  { return userState.status; },
            getIndex()   { return userState.messageIndex; },
            getNode()    { return userNode; },
            isEditing()  { return userState.status === 'Editing'; },
            isComplete() { return userState.status === 'Complete'; },
            /** Register a callback for user status changes. Returns unsubscribe fn. */
            onStatusChange(cb) { return subscribe(userStatusCallbacks, cb); }
        },

        // ── Utilities ──
        extractText: extractMessageText,

        /** Return descriptors for every message node currently in the DOM. */
        getAllMessages() {
            const nodes = document.querySelectorAll(MESSAGE_CONTAINER_SELECTOR);
            const out = [];
            nodes.forEach(n => {
                const desc = describeMessage(n);
                if (desc) out.push(desc);
            });
            return out;
        },

        /** Get a single message by its data-index value. */
        getMessageAt(idx) {
            const node = document.querySelector(`[data-testid="virtuoso-item-list"] > div[data-index="${idx}"]`);
            if (!node) return null;
            return describeMessage(node);
        },

        /** Number of message container nodes in the DOM right now. */
        getMessageCount() {
            return document.querySelectorAll(MESSAGE_CONTAINER_SELECTOR).length;
        },

        /** Whether the current page is a chat page (where messages exist). */
        isOnChatPage() {
            return window.location.pathname.startsWith('/chats/');
        },

        /** Force a detection pass right now. */
        poll() { detectMessages(); },

        // ── Observer control ──
        start: startObserver,
        stop:  stopObserver,
        reset: resetState,

        // ── Logging control ──
        logging: {
            enable()    { loggingEnabled = true; },
            disable()   { loggingEnabled = false; },
            isEnabled() { return loggingEnabled; }
        }
    };

    // ── Expose globally ─────────────────────────────────────────
    const target = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
    target.JaiMessageLogger = API;

    // Fire a custom event so scripts that loaded before us can detect readiness
    window.dispatchEvent(new CustomEvent('jai-msglogger:ready', { detail: API }));

    // ── Auto-start on chat pages ────────────────────────────────
    function boot() {
        if (!API.isOnChatPage()) return;
        console.log('[MessageLogger] 📝 JanitorAI Message Logger Library v' + API.version);
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', startObserver);
        } else {
            startObserver();
        }
    }

    boot();

    // Re-check on SPA navigation (JanitorAI is a single-page app)
    let lastPath = window.location.pathname;
    const navCheck = setInterval(() => {
        if (window.location.pathname !== lastPath) {
            lastPath = window.location.pathname;
            if (API.isOnChatPage()) {
                if (!observer) {
                    console.log('[MessageLogger] 🔄 Navigated to chat page – starting observer');
                    startObserver();
                }
            } else {
                if (observer) {
                    resetState();
                }
            }
        }
    }, 1000);
})();