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