Janitor AI - Automatic Message Formatting Corrector (Settings Menu)

Draggable button with Settings! Select Italics/Bold/Plain text. Edge compatible. Remembers position. Formats narration & dialogues.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Janitor AI - Automatic Message Formatting Corrector (Settings Menu)
// @namespace    http://tampermonkey.net/
// @version      9.0
// @description  Draggable button with Settings! Select Italics/Bold/Plain text. Edge compatible. Remembers position. Formats narration & dialogues.
// @author       accforfaciet
// @match        *://janitorai.com/chats/*
// @grant        GM_addStyle
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- CONSTANTS & DEFAULTS ---
    const DEBUG_MODE = false;
    const POSITION_KEY = 'janitorFormatterPosition';
    const SETTINGS_KEY = 'janitorFormatterSettings';

    const DEFAULT_SETTINGS = {
        styleMode: 'italic', // 'italic', 'bold', 'none', 'bold-italic', 'bold-plain'
        removeThinkTags: true,
        removeSystemPrompt: true,
        removeGeneralTags: true
    };

    // --- UNIVERSAL SELECTORS ---
    const EDIT_BUTTON_SELECTOR = 'button[title="Edit Message"], button[aria-label="Edit"]';
    const TEXT_AREA_SELECTOR = 'textarea[class*="_autoResizeTextarea"], textarea[placeholder^="Type a message"], textarea[style*="font-size: 16px"]';
    const CONFIRM_BUTTON_SELECTOR = 'button[aria-label="Confirm"], button[aria-label="Save"], button[aria-label*="Confirm"], button[aria-label*="Save"]';

    // --- STATE MANAGEMENT ---
    let currentSettings = loadSettings();

    // --- HELPER FUNCTIONS ---
    function debugLog(...args) { if (DEBUG_MODE) console.log('[DEBUG]', ...args); }

    function loadSettings() {
        const saved = localStorage.getItem(SETTINGS_KEY);
        // Migration check: if old settings used 'narrationStyle', convert to 'styleMode'
        if (saved) {
            const parsed = JSON.parse(saved);
            if (parsed.narrationStyle !== undefined) {
                if (parsed.narrationStyle === '*') parsed.styleMode = 'italic';
                else if (parsed.narrationStyle === '**') parsed.styleMode = 'bold';
                else parsed.styleMode = 'none';
                delete parsed.narrationStyle;
            }
            return { ...DEFAULT_SETTINGS, ...parsed };
        }
        return DEFAULT_SETTINGS;
    }

    function saveSettings(newSettings) {
        localStorage.setItem(SETTINGS_KEY, JSON.stringify(newSettings));
        currentSettings = newSettings;
    }

    function waitForElement(selector, timeoutMs = 5000) {
        return new Promise(resolve => {
            let el = document.querySelector(selector);
            if (el) return resolve(el);

            const startTime = Date.now();
            const observer = new MutationObserver(() => {
                el = document.querySelector(selector);
                if (el) {
                    observer.disconnect();
                    resolve(el);
                } else if (Date.now() - startTime > timeoutMs) {
                    observer.disconnect();
                    resolve(null);
                }
            });
            observer.observe(document.body, { childList: true, subtree: true, attributes: true });
        });
    }

    // --- TEXT PROCESSING ENGINE ---
    function processText(text) {
        // 1. Remove <think> tags AND THEIR CONTENT
        if (currentSettings.removeThinkTags) {
            text = text.replace(/\n?\s*<(thought|thoughts)>[\s\S]*?<\/(thought|thoughts)>\s*\n?/g, '');
            text = text.replace(/<(system|response)>|<\/response>/g, '');
            text = text.replace(/\n?\s*<think>[\s\S]*?<\/think>\s*\n?/g, '');
            text = text.replace('</think>', '');
        }

        // 2. Remove General Tags (KEEP CONTENT) - New Feature
        // Matches <tag> or </tag> but ignores <think> logic above as that's already gone
        if (currentSettings.removeGeneralTags) {
            text = text.replace(/<\/?[a-zA-Z0-9:-]+(\s[^>]*)?>/g, '');
        }

        // 3. Remove system prompt if enabled
        if (currentSettings.removeSystemPrompt) {
            text = removeSystemPrompt(text);
        }

        // 4. Determine Formatting Wrappers
        let narrWrap = '';
        let dialWrap = '';

        switch (currentSettings.styleMode) {
            case 'italic': narrWrap = '*'; break;
            case 'bold': narrWrap = '**'; break;
            case 'bold-italic': narrWrap = '*'; dialWrap = '**'; break; // Narr=*...*, Dial=**"..."**
            case 'bold-plain': narrWrap = ''; dialWrap = '**'; break;   // Narr=..., Dial=**"..."**
            case 'none': default: break;
        }

        const normalizedText = text.replace(/[«“”„‟⹂❞❝]/g, '"');
        const lines = normalizedText.split('\n');

        const processedLines = lines.map(line => {
            let trimmedLine = line.trim();
            if (trimmedLine === '') return '';

            // Clean existing asterisks to start fresh
            const cleanLine = trimmedLine.replace(/\*/g, '');

            // 5. Noise Filter: Remove lines that are just quotes or asterisks (New Feature)
            // Example: `*"` or `**` or `"`
            if (/^["*]+$/.test(cleanLine)) return null; // Will be filtered out

            // 6. Separator Logic: Handle --- (New Feature)
            // Since we stripped asterisks in `cleanLine`, *---* becomes ---.
            if (cleanLine === '---') return '---';

            // 7. Apply Formatting
            if (cleanLine.includes('"') || cleanLine.includes('`')) {
                const fragments = cleanLine.split(/("[\s\S]*?"|`[\s\S]*?`)/);
                return fragments.map(frag => {
                    const isQuote = (frag.startsWith('"') && frag.endsWith('"')) || (frag.startsWith('`') && frag.endsWith('`'));

                    if (isQuote) {
                        return frag.trim() !== '' ? `${dialWrap}${frag}${dialWrap}` : '';
                    } else {
                        // It is narration
                        return frag.trim() !== '' ? `${narrWrap}${frag.trim()}${narrWrap}` : '';
                    }
                }).filter(Boolean).join(' ');
            }

            // Entire line is narration
            return `${narrWrap}${cleanLine}${narrWrap}`;
        });

        // Filter out null lines (from Noise Filter) and join
        return processedLines.filter(l => l !== null).join('\n');
    }

    function removeSystemPrompt(text) {
        if (!text.trim().toLowerCase().includes('theuser')) return text;
        const splitPointIndex = text.search(/[^\s\*]\*[^\s\*]/);
        if (splitPointIndex !== -1) {
            return text.substring(splitPointIndex + 1);
        }
        return text;
    }

    // --- MAIN ACTION ---
    async function executeFormat() {
        debugLog('Start Format');
        try {
            const allEditButtons = document.querySelectorAll(EDIT_BUTTON_SELECTOR);
            if (allEditButtons.length === 0) return debugLog('No edit buttons');

            const lastEditButton = allEditButtons[allEditButtons.length - 1];
            lastEditButton.click();

            await new Promise(r => setTimeout(r, 600));

            const textField = await waitForElement(TEXT_AREA_SELECTOR);
            if (!textField) return debugLog('Text field not found');

            const newText = processText(textField.value);

            const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
            nativeInputValueSetter.call(textField, newText);
            textField.dispatchEvent(new Event('input', { bubbles: true }));

            const confirmButton = await waitForElement(CONFIRM_BUTTON_SELECTOR);
            if (confirmButton) confirmButton.click();

        } catch (error) {
            console.error('JanitorFormatter Error:', error);
        }
    }

    // --- UI: SETTINGS MODAL ---
    function createSettingsModal() {
        if (document.getElementById('janitor-settings-modal')) return;

        const modalOverlay = document.createElement('div');
        modalOverlay.id = 'janitor-settings-modal';

        modalOverlay.innerHTML = `
            <div class="janitor-modal-content">
                <h3>Formatter Settings</h3>

                <div class="setting-group">
                    <label>Formatting Style:</label>
                    <select id="setting-style">
                        <option value="italic">Classic (*Narr* "Dial")</option>
                        <option value="bold">Bold Narration (**Narr** "Dial")</option>
                        <option value="bold-italic">Bold Dial + Italic Narr (*Narr* **"Dial"**)</option>
                        <option value="bold-plain">Bold Dial + Plain Narr (Narr **"Dial"**)</option>
                        <option value="none">Plain Text (No formatting)</option>
                    </select>
                </div>

                <div class="setting-group checkbox">
                    <input type="checkbox" id="setting-think" ${currentSettings.removeThinkTags ? 'checked' : ''}>
                    <label for="setting-think">Remove &lt;think&gt; content</label>
                </div>

                <div class="setting-group checkbox">
                    <input type="checkbox" id="setting-gentags" ${currentSettings.removeGeneralTags !== false ? 'checked' : ''}>
                    <label for="setting-gentags">Clean other tags (keep content)</label>
                </div>

                <div class="setting-group checkbox">
                    <input type="checkbox" id="setting-prompt" ${currentSettings.removeSystemPrompt ? 'checked' : ''}>
                    <label for="setting-prompt">Remove System Prompts</label>
                </div>

                <div class="modal-buttons">
                    <button id="save-settings">Save & Close</button>
                    <button id="cancel-settings" style="background:#555">Cancel</button>
                </div>
            </div>
        `;

        document.body.appendChild(modalOverlay);

        document.getElementById('setting-style').value = currentSettings.styleMode;

        document.getElementById('save-settings').onclick = () => {
            saveSettings({
                styleMode: document.getElementById('setting-style').value,
                removeThinkTags: document.getElementById('setting-think').checked,
                removeGeneralTags: document.getElementById('setting-gentags').checked,
                removeSystemPrompt: document.getElementById('setting-prompt').checked
            });
            modalOverlay.remove();
        };

        document.getElementById('cancel-settings').onclick = () => modalOverlay.remove();
    }

    // --- UI: MAIN BUTTONS ---
    function createUI() {
        const container = document.createElement('div');
        container.id = 'janitor-editor-container';
        document.body.appendChild(container);

        const formatBtn = document.createElement('button');
        formatBtn.innerHTML = '✏️';
        formatBtn.id = 'formatter-btn';
        formatBtn.title = 'Format (Click) / Move (Drag)';
        container.appendChild(formatBtn);

        const settingsBtn = document.createElement('button');
        settingsBtn.innerHTML = '⚙️';
        settingsBtn.id = 'settings-btn';
        settingsBtn.title = 'Configure Formatting';
        container.appendChild(settingsBtn);

        makeDraggable(container, formatBtn);

        settingsBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            createSettingsModal();
        });
    }

    // --- DRAG LOGIC ---
    function makeDraggable(container, handle) {
        let isDragging = false;
        let wasDragged = false;
        let startX, startY, initialLeft, initialTop;

        const savedPos = localStorage.getItem(POSITION_KEY);
        if (savedPos) {
            const { left, top } = JSON.parse(savedPos);
            container.style.left = left;
            container.style.top = top;
            container.style.right = 'auto';
            container.style.bottom = 'auto';
        }

        function onStart(e) {
            if (e.target.id === 'settings-btn') return;

            isDragging = true;
            wasDragged = false;
            handle.classList.add('is-dragging');

            const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
            const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;

            startX = clientX;
            startY = clientY;

            const rect = container.getBoundingClientRect();
            initialLeft = rect.left;
            initialTop = rect.top;

            document.addEventListener('mousemove', onMove);
            document.addEventListener('touchmove', onMove, { passive: false });
            document.addEventListener('mouseup', onEnd);
            document.addEventListener('touchend', onEnd);

            e.preventDefault();
        }

        function onMove(e) {
            if (!isDragging) return;
            wasDragged = true;
            e.preventDefault();

            const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
            const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;

            const dx = clientX - startX;
            const dy = clientY - startY;

            let newLeft = initialLeft + dx;
            let newTop = initialTop + dy;

            const winW = window.innerWidth;
            const winH = window.innerHeight;
            const rect = container.getBoundingClientRect();

            newLeft = Math.max(0, Math.min(newLeft, winW - rect.width));
            newTop = Math.max(0, Math.min(newTop, winH - rect.height));

            container.style.left = `${newLeft}px`;
            container.style.top = `${newTop}px`;
            container.style.right = 'auto';
            container.style.bottom = 'auto';
        }

        function onEnd() {
            isDragging = false;
            handle.classList.remove('is-dragging');
            document.removeEventListener('mousemove', onMove);
            document.removeEventListener('touchmove', onMove);
            document.removeEventListener('mouseup', onEnd);
            document.removeEventListener('touchend', onEnd);

            if (wasDragged) {
                localStorage.setItem(POSITION_KEY, JSON.stringify({
                    left: container.style.left,
                    top: container.style.top
                }));
            } else {
                executeFormat();
            }
        }

        handle.addEventListener('mousedown', onStart);
        handle.addEventListener('touchstart', onStart, { passive: false });
    }

    // --- KEYBOARD FIX ---
    async function initKeyboardFix() {
        const input = await waitForElement('textarea[placeholder^="Type a message"]');
        const container = document.getElementById('janitor-editor-container');
        if (input && container) {
            input.addEventListener('focus', () => container.style.display = 'none');
            input.addEventListener('blur', () => setTimeout(() => container.style.display = 'flex', 200));
        }
    }

    // --- STYLES ---
    GM_addStyle(`
        #janitor-editor-container { position: fixed; z-index: 9999; display: flex; align-items: flex-end; gap: 5px; }
        #janitor-editor-container button { border: none; border-radius: 50%; color: white; cursor: pointer; box-shadow: 0 4px 8px rgba(0,0,0,0.3); transition: transform 0.2s, opacity 0.2s; display: flex; justify-content: center; align-items: center; }
        #formatter-btn { background-color: #c9226e; width: 50px; height: 50px; font-size: 24px; }
        #settings-btn { background-color: #444; width: 30px; height: 30px; font-size: 16px; }
        #formatter-btn.is-dragging { transform: scale(1.1); opacity: 0.8; box-shadow: 0 8px 16px rgba(0,0,0,0.5); }
        #janitor-editor-container button:active { transform: scale(0.95); }
        #janitor-settings-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 10000; display: flex; justify-content: center; align-items: center; }
        .janitor-modal-content { background: #1f1f1f; color: white; padding: 20px; border-radius: 12px; width: 90%; max-width: 350px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); font-family: sans-serif; }
        .janitor-modal-content h3 { margin-top: 0; border-bottom: 1px solid #444; padding-bottom: 10px; }
        .setting-group { margin-bottom: 15px; display: flex; flex-direction: column; }
        .setting-group.checkbox { flex-direction: row; align-items: center; gap: 10px; }
        .setting-group select { padding: 8px; border-radius: 4px; background: #333; color: white; border: 1px solid #555; }
        .modal-buttons { display: flex; gap: 10px; margin-top: 20px; }
        .modal-buttons button { flex: 1; padding: 10px; border: none; border-radius: 4px; cursor: pointer; color: white; background: #c9226e; font-weight: bold; }
        @media (min-width: 769px) { #janitor-editor-container { right: 27%; bottom: 12%; } }
        @media (max-width: 768px) { #formatter-btn { width: 45px; height: 45px; font-size: 20px; } #janitor-editor-container { right: 5%; bottom: 20%; } }
    `);

    createUI();
    initKeyboardFix();
    console.log('Janitor Formatter v9.0 Loaded');
})();