JanitorAI - Message Formatting Corrector (Mobile)

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

Tính đến 03-10-2025. Xem phiên bản mới nhất.

Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta // @require https://update.greasyfork.org/scripts/551459/1671109/JanitorAI%20-%20Message%20Formatting%20Corrector%20%28Mobile%29.js

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

})();