Janitor AI - Automatic Message Formatting Corrector (Settings Menu)

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

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

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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

})();