JanitorAI - Message Formatting Corrector (Mobile)

Formats narration and dialogues, and removes <think> tags.

// ==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.');

})();