JanitorAI - Message Formatting Corrector (PC)

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

目前為 2025-10-03 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         JanitorAI - Message Formatting Corrector (PC)
// @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 ---

    // --- DEBUGGING 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 - The 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 present. (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 the "join": [not a space and not an *] + [*] + [not a space and not an *]
        // [^\s\*] is "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 up to 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 the join point (word*word) was not found. No changes will be made.');
        return text; // If the point is not found, 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 later

        const cleanup = () => {
             if (lastHighlightedElement) highlightElement(lastHighlightedElement, true);
        };

        try {
            // 1. Find 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. Click the "Edit" button
            debugLog('2. Clicking the "Edit" button.');
            lastEditButton.click();
            await debugPause(500); // Short pause to allow the DOM to react

            // 3. Wait for and find the text area
            highlightElement(lastEditButton, true); // Remove the highlight
            debugLog('3. Waiting for 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. Process 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. Insert new text and simulate input
            debugLog('5. Inserting new text into the field.');
            textField.value = newText;
            textField.dispatchEvent(new Event('input', { bubbles: true }));
            await debugPause();

            // 6. Find and click the "Confirm" button
            highlightElement(textField, true); // Remove the highlight
            debugLog('6. Searching for 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 SUCCESSFULLY COMPLETED ---');

        } catch (error) {
            console.error('CRITICAL ERROR during the editing process:', error);
        } finally {
            cleanup(); // Remove highlight in any case
        }
    }


    /**
     * Creates and adds both buttons 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 buttons during text input.
     */
    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 these: remove "/*" from the top and "* /" from the bottom of this block,
    // and wrap the PC styles block above 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; }
    `);
    */


    // --- STARTUP ---
    createTriggerButtons();
    // The keyboard fix is activated automatically.
    // If you are on a PC, it will simply not find the input field and will exit quietly.
    initKeyboardBugFix();
    console.log('Script "Advanced Editor" (v4.0 Universal) started successfully.');
    debugLog('Script "Advanced Editor" (v4.1-debug) started successfully.');

})();