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      8.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 = {
        narrationStyle: '*', // '*' = Italics, '**' = Bold, '' = None
        removeThinkTags: true,
        removeSystemPrompt: true
    };

    // --- UNIVERSAL SELECTORS (Improved for Edge/Universal support) ---
    const EDIT_BUTTON_SELECTOR = 'button[title="Edit Message"], button[aria-label="Edit"]';
    // Robust selector: looks for specific class or style attributes broadly
    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);
        return saved ? { ...DEFAULT_SETTINGS, ...JSON.parse(saved) } : 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 ---
    function processText(text) {
        // 1. Remove tags if enabled
        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 system prompt if enabled
        if (currentSettings.removeSystemPrompt) {
            text = removeSystemPrompt(text);
        }

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

        return lines.map(line => {
            const trimmedLine = line.trim();
            if (trimmedLine === '') return '';
            const cleanLine = trimmedLine.replace(/\*/g, ''); // Strip existing asterisks

            // Regex to find quotes or code blocks
            if (cleanLine.includes('"') || cleanLine.includes('`')) {
                const fragments = cleanLine.split(/("[\s\S]*?"|`[\s\S]*?`)/);
                return fragments.map(frag => {
                    // If it's a quote or code, leave it alone
                    if ((frag.startsWith('"') && frag.endsWith('"')) || (frag.startsWith('`') && frag.endsWith('`'))) {
                        return frag;
                    }
                    // If it's narration and not empty, wrap it
                    return frag.trim() !== '' ? `${wrapper}${frag.trim()}${wrapper}` : '';
                }).filter(Boolean).join(' ');
            }
            // Entire line is narration
            return `${wrapper}${cleanLine}${wrapper}`;
        }).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();

            // Wait slightly longer for animations/modal
            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);

            // React/Vue frameworks usually require input events to recognize change
            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';

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

                <div class="setting-group">
                    <label>Narration Style:</label>
                    <select id="setting-narration">
                        <option value="*">Italics (*text*)</option>
                        <option value="**">Bold (**text**)</option>
                        <option value="">Plain Text (none)</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; tags</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);

        // Pre-select dropdown
        document.getElementById('setting-narration').value = currentSettings.narrationStyle;

        // Event Listeners
        document.getElementById('save-settings').onclick = () => {
            saveSettings({
                narrationStyle: document.getElementById('setting-narration').value,
                removeThinkTags: document.getElementById('setting-think').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);

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

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

        // Initialize Dragging (Drags the whole container)
        makeDraggable(container, formatBtn);

        // Listeners
        settingsBtn.addEventListener('click', (e) => {
            e.stopPropagation(); // Prevent drag start
            createSettingsModal();
        });
    }

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

        // Load position
        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; // Don't drag if clicking settings

            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(); // Prevent text selection
        }

        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;

            // Boundary checks
            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(`
        /* Container */
        #janitor-editor-container {
            position: fixed;
            z-index: 9999;
            display: flex;
            align-items: flex-end;
            gap: 5px;
            /* Default Position (Mobile/PC adaptive via media queries below) */
        }

        /* Buttons */
        #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;
        }

        /* Drag Visuals */
        #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); }

        /* Modal Styles */
        #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; }

        /* Screen Sizes Defaults */
        @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%; }
        }
    `);

    // --- INIT ---
    createUI();
    initKeyboardFix();
    console.log('Janitor Formatter v8.0 Loaded');

})();