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.

이 스크립트는 직접 설치하는 용도가 아닙니다. 다른 스크립트에서 메타 지시문 // @require https://update.greatest.deepsurf.us/scripts/572546/1790490/JanitorAI%20-%20Message%20Logger%20Library.js을(를) 사용하여 포함하는 라이브러리입니다.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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