// ==UserScript==
// @name JanitorAI - Message Formatting Corrector (Mobile)
// @namespace http://tampermonkey.net/
// @version 4.0
// @description Formats narration and dialogues, and removes <think> tags.
// @author accforfaciet
// @match *://janitorai.com/chats/*
// @grant GM_addStyle
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- DEBUG SETTINGS ---
const DEBUG_MODE = true; // Set to true to output messages to the console and enable pauses
const DEBUG_PAUSE_MS = 50; // Pause duration in milliseconds
// --- END OF DEBUG SETTINGS ---
// --- UNIVERSAL SELECTORS (work on both PC and mobile) ---
const EDIT_BUTTON_SELECTOR = 'button[title="Edit Message"], button[aria-label="Edit"]';
const TEXT_AREA_SELECTOR = 'textarea[style*="font-size: 16px"][style*="!important"]';
const CONFIRM_BUTTON_SELECTOR = 'button[aria-label="Confirm"], button[aria-label="Save"]';
// --- END OF SETTINGS ---
// --- DEBUG TOOLS ---
/** Logs a message to the console only if DEBUG_MODE is enabled. */
function debugLog(...args) {
if (DEBUG_MODE) {
console.log('[DEBUG]', ...args);
}
}
/** Creates a pause in execution only if DEBUG_MODE is enabled. */
function debugPause(ms = DEBUG_PAUSE_MS) {
if (DEBUG_MODE) {
debugLog(`Pausing for ${ms / 1000} sec...`);
return new Promise(resolve => setTimeout(resolve, ms));
}
return Promise.resolve();
}
/** Highlights an element with a red border for visual debugging. */
function highlightElement(element, remove = false) {
if (DEBUG_MODE && element) {
element.style.outline = remove ? '' : '3px solid red';
element.style.outlineOffset = '3px';
}
}
/**
* Asynchronous function to wait for an element to appear in the DOM.
* @param {string} selector - CSS selector for the element.
* @returns {Promise<Element>}
*/
function waitForElement(selector) {
return new Promise(resolve => {
const el = document.querySelector(selector);
if (el) return resolve(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
resolve(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
}
/**
* Function #1: Removes text inside <think> tags.
*/
function removeThinkTags(text) {
text = text.replace(/\n?\s*<thought>[\s\S]*?<\/thought>\s*\n?/g, '');
text = text.replace(/\n?\s*<thoughts>[\s\S]*?<\/thoughts>\s*\n?/g, '');
text = text.replace('<system>', '');
text = text.replace('<response>', '');
text = text.replace('</response>', '');
text = text.replace(/\n?\s*<think>[\s\S]*?<\/think>\s*\n?/g, '');
text = text.replace('</think>', '');
text = removeSystemPrompt(text);
return text;
}
/**
* Function #2: Smart text formatting (VERSION 4.0 - LINE-BY-LINE).
* Correctly handles single-line paragraphs.
*/
function formatNarrationAndDialogue(text) {
// 1. Pre-processing: remove <think> tags and normalize quotes.
text = removeThinkTags(text);
const normalizedText = text.replace(/[«“”„‟⹂❞❝]/g, '"');
const lines = normalizedText.split('\n');
const processedLines = lines.map(line => {
const trimmedLine = line.trim();
if (trimmedLine === '') return '';
const cleanLine = trimmedLine.replace(/\*/g, '');
if (cleanLine.includes('"') || cleanLine.includes('`')) {
const fragments = cleanLine.split(/("[\s\S]*?"|`[\s\S]*?`)/);
const processedFragments = fragments.map(frag => {
if ((frag.startsWith('"') && frag.endsWith('"')) || (frag.startsWith('`') && frag.endsWith('`'))) {
return frag;
} else if (frag.trim() !== '') {
return `*${frag.trim()}*`;
}
return '';
});
return processedFragments.filter(f => f).join(' ');
} else {
return `*${cleanLine}*`;
}
});
return processedLines.join('\n');
}
/**
* Function #3: Removes the system prompt, if it exists. (VERSION 3.0 - IMPROVED)
* Looks for a "join" of the "character*character" type, excluding other asterisks.
*/
function removeSystemPrompt(text) {
const trimmedText = text.replace(' ', '');
// Check if the text starts with "The user" (case-insensitive)
if (!trimmedText.toLowerCase().includes('theuser')) {
debugLog('System prompt not found (text does not start with "The user"). No changes will be made.');
return text; // If not, do nothing
}
// IMPROVED: Look for a "join": [not a space or *] + [*] + [not a space or *]
// [^\s\*] - means "any character that is not a (\s) space AND not an (\*) asterisk"
const splitPointIndex = text.search(/[^\s\*]\*[^\s\*]/);
if (splitPointIndex !== -1) {
// If the point is found, cut everything before the *
// +1 to find the position of the asterisk itself, not the character before it.
const result = text.substring(splitPointIndex + 1);
debugLog(`System prompt found. The text will be trimmed.`);
return result;
}
debugLog('Text starts with "The user", but a join point (word*word) was not found. No changes will be made.');
return text; // If the point is not found, we don't change anything, just in case
}
/**
* Main mechanism: clicks "Edit", processes the text, and saves.
*/
async function processLastMessage(textProcessor) {
debugLog('--- STARTING EDIT PROCESS ---');
let lastHighlightedElement = null; // To remove the highlight
const cleanup = () => {
if (lastHighlightedElement) highlightElement(lastHighlightedElement, true);
};
try {
// 1. Searching for the "Edit" button
debugLog('1. Searching for edit buttons with selector:', EDIT_BUTTON_SELECTOR);
const allEditButtons = document.querySelectorAll(EDIT_BUTTON_SELECTOR);
if (allEditButtons.length === 0) {
debugLog('STOP: Edit buttons not found.');
return;
}
debugLog(`Found buttons: ${allEditButtons.length}. Selecting the last one.`);
const lastEditButton = allEditButtons[allEditButtons.length - 1];
highlightElement(lastEditButton);
lastHighlightedElement = lastEditButton;
await debugPause();
// 2. Clicking the "Edit" button
debugLog('2. Clicking the "Edit" button.');
lastEditButton.click();
await debugPause(500); // Short pause to let the DOM react
// 3. Waiting for and finding the text area
highlightElement(lastEditButton, true); // Remove highlight
debugLog('3. Waiting for the text area to appear with selector:', TEXT_AREA_SELECTOR);
const textField = await waitForElement(TEXT_AREA_SELECTOR);
debugLog('Text area found!');
highlightElement(textField);
lastHighlightedElement = textField;
await debugPause();
// 4. Processing the text
const originalText = textField.value;
const newText = textProcessor(originalText);
debugLog('4. Text processed.');
if (DEBUG_MODE) {
console.groupCollapsed('[DEBUG] Text comparison (before and after)');
console.log('--- ORIGINAL TEXT ---\n', originalText);
console.log('--- NEW TEXT ---\n', newText);
console.groupEnd();
}
// 5. Inserting the new text and simulating input
debugLog('5. Inserting new text into the field.');
textField.value = newText;
textField.dispatchEvent(new Event('input', { bubbles: true }));
await debugPause();
// 6. Finding and clicking the "Confirm" button
highlightElement(textField, true); // Remove highlight
debugLog('6. Searching for the confirm button with selector:', CONFIRM_BUTTON_SELECTOR);
const confirmButton = await waitForElement(CONFIRM_BUTTON_SELECTOR);
debugLog('"Confirm" button found!');
highlightElement(confirmButton);
lastHighlightedElement = confirmButton;
await debugPause();
debugLog('7. Clicking the "Confirm" button.');
if (confirmButton) confirmButton.click();
debugLog('--- PROCESS COMPLETED SUCCESSFULLY ---');
} catch (error) {
console.error('CRITICAL ERROR during the edit process:', error);
} finally {
cleanup(); // Remove highlight in any case
}
}
/**
* Creates and adds the button to the page.
*/
function createTriggerButtons() {
const buttonContainer = document.createElement('div');
buttonContainer.id = 'janitor-editor-buttons';
document.body.appendChild(buttonContainer);
const formatButton = document.createElement('button');
formatButton.innerHTML = '✏️';
formatButton.id = 'formatterTrigger';
formatButton.title = 'Format asterisks';
formatButton.addEventListener('click', () => processLastMessage(formatNarrationAndDialogue));
buttonContainer.appendChild(formatButton);
}
/**
* Mobile keyboard fix: hides the buttons when typing.
*/
async function initKeyboardBugFix() {
try {
const mainInput = await waitForElement('textarea[placeholder^="Type a message"]');
const buttonContainer = document.getElementById('janitor-editor-buttons');
if (!mainInput || !buttonContainer) return;
mainInput.addEventListener('focus', () => { buttonContainer.style.display = 'none'; });
mainInput.addEventListener('blur', () => {
setTimeout(() => { buttonContainer.style.display = 'block'; }, 200);
});
} catch (e) {
console.log('Could not find the main input field for the keyboard bug fix (this is normal on PC).');
}
}
// --- STYLES ---
// Use the desired block and comment out the other.
// --- STYLES FOR PC (default) ---
/*
GM_addStyle(`
#janitor-editor-buttons button {
position: fixed; z-index: 9999;
width: 50px; height: 50px; color: white;
border: none; border-radius: 50%;
font-size: 24px; box-shadow: 0 4px 8px rgba(0,0,0,0.3);
cursor: pointer; transition: transform 0.2s;
}
#janitor-editor-buttons button:active { transform: scale(0.9); }
#thinkRemoverTrigger { right: 27%; bottom: 5%; background-color: #6a22c9; }
#formatterTrigger { right: 27%; bottom: 12%; background-color: #c9226e; }
`);
*/
// --- STYLES FOR MOBILE ---
// To use them: remove "/*" from the top and "*/" from the bottom of this block,
// and wrap the top block with PC styles in the same comments.
GM_addStyle(`
#janitor-editor-buttons button {
position: fixed; z-index: 9999;
width: 40px; height: 40px; color: white;
border: none; border-radius: 50%;
font-size: 16px; box-shadow: 0 4px 8px rgba(0,0,0,0.3);
cursor: pointer; transition: all 0.2s;
}
#janitor-editor-buttons button:active { transform: scale(0.9); }
#thinkRemoverTrigger { right: 14%; bottom: 20%; background-color: #6a22c9; }
#formatterTrigger { right: 28%; bottom: 20%; background-color: #c9226e; }
`);
// --- LAUNCH ---
createTriggerButtons();
// The keyboard fix is activated automatically.
// If you are on a PC, it simply won't find the input field and will exit quietly.
initKeyboardBugFix();
console.log('Script "Message Formatting Corrector (Mobile) v4.0" launched successfully.');
debugLog('Script "Message Formatting Corrector (Mobile) v4.0-debug" launched successfully.');
})();