Janitor AI - Automatic Message Formatting Corrector (Drag & Drop button)

Draggable button with visual feedback! Remembers its position, adapts to screen size, and can't be dragged off-screen. Formats narration/dialogues.

// ==UserScript==
// @name         Janitor AI - Automatic Message Formatting Corrector (Drag & Drop button)
// @namespace    http://tampermonkey.net/
// @version      7.0
// @description  Draggable button with visual feedback! Remembers its position, adapts to screen size, and can't be dragged off-screen. Formats narration/dialogues.
// @author       accforfaciet (upgraded by professional developer)
// @match        *://janitorai.com/chats/*
// @grant        GM_addStyle
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- SCRIPT SETTINGS ---
    const DEBUG_MODE = false; // Set to true for console logs
    const BUTTON_POSITION_KEY = 'formatterButtonPosition'; // Key for saving position

    // --- UNIVERSAL SELECTORS ---
    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"]';

    // --- DEBUGGING TOOLS ---
    function debugLog(...args) { if (DEBUG_MODE) console.log('[DEBUG]', ...args); }
    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 });
        });
    }

    // --- CORE TEXT PROCESSING FUNCTIONS ---
    function removeThinkTags(text) {
        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>', '');
        return removeSystemPrompt(text);
    }

    function formatNarrationAndDialogue(text) {
        text = removeThinkTags(text);
        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, '');
            if (cleanLine.includes('"') || cleanLine.includes('`')) {
                return cleanLine.split(/("[\s\S]*?"|`[\s\S]*?`)/)
                    .map(frag => {
                        if ((frag.startsWith('"') && frag.endsWith('"')) || (frag.startsWith('`') && frag.endsWith('`'))) return frag;
                        return frag.trim() !== '' ? `*${frag.trim()}*` : '';
                    }).filter(Boolean).join(' ');
            }
            return `*${cleanLine}*`;
        }).join('\n');
    }

    function removeSystemPrompt(text) {
        if (!text.trim().toLowerCase().includes('theuser')) return text;
        const splitPointIndex = text.search(/[^\s\*]\*[^\s\*]/);
        if (splitPointIndex !== -1) {
            debugLog(`System prompt found. The text will be trimmed.`);
            return text.substring(splitPointIndex + 1);
        }
        return text;
    }

    // --- MAIN ACTION SEQUENCE ---
    async function processLastMessage(textProcessor) {
        debugLog('--- STARTING EDIT PROCESS ---');
        try {
            const allEditButtons = document.querySelectorAll(EDIT_BUTTON_SELECTOR);
            if (allEditButtons.length === 0) { debugLog('STOP: No edit buttons found.'); return; }
            const lastEditButton = allEditButtons[allEditButtons.length - 1];

            lastEditButton.click();
            await new Promise(resolve => setTimeout(resolve, 500)); // Wait for modal

            const textField = await waitForElement(TEXT_AREA_SELECTOR);
            const originalText = textField.value;
            const newText = textProcessor(originalText);

            textField.value = newText;
            textField.dispatchEvent(new Event('input', { bubbles: true }));

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

            debugLog('--- PROCESS COMPLETED SUCCESSFULLY ---');
        } catch (error) {
            console.error('CRITICAL ERROR during edit process:', error);
        }
    }

    /**
     * Makes the button draggable, handles clicks, and applies visual effects.
     * @param {HTMLElement} button The button element to make draggable.
     */
    function makeButtonDraggable(button) {
        let isDragging = false;
        let wasDragged = false;
        let offsetX, offsetY;

        // Load saved position
        const savedPosition = localStorage.getItem(BUTTON_POSITION_KEY);
        if (savedPosition) {
            const { top, left } = JSON.parse(savedPosition);
            button.style.top = top;
            button.style.left = left;
            button.style.right = 'auto';
            button.style.bottom = 'auto';
        }

        function dragStart(e) {
            e.preventDefault();
            isDragging = true;
            wasDragged = false;
            button.classList.add('is-dragging'); // NEW: Add visual effect class

            const clientX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
            const clientY = e.type === 'touchstart' ? e.touches[0].clientY : e.clientY;

            offsetX = clientX - button.getBoundingClientRect().left;
            offsetY = clientY - button.getBoundingClientRect().top;

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

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

            const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
            const clientY = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY;

            let newLeft = clientX - offsetX;
            let newTop = clientY - offsetY;

            // Constrain to viewport to prevent dragging off-screen
            const buttonRect = button.getBoundingClientRect();
            newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - buttonRect.width));
            newTop = Math.max(0, Math.min(newTop, window.innerHeight - buttonRect.height));

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

        function dragEnd() {
            if (!isDragging) return;
            isDragging = false;
            button.classList.remove('is-dragging'); // NEW: Remove visual effect class

            document.removeEventListener('mousemove', dragMove);
            document.removeEventListener('touchmove', dragMove);
            document.removeEventListener('mouseup', dragEnd);
            document.removeEventListener('touchend', dragEnd);

            if (wasDragged) {
                // Save the final position
                const pos = { top: button.style.top, left: button.style.left };
                localStorage.setItem(BUTTON_POSITION_KEY, JSON.stringify(pos));
            } else {
                // If not dragged, it's a click.
                processLastMessage(formatNarrationAndDialogue);
            }
        }

        button.addEventListener('mousedown', dragStart);
        button.addEventListener('touchstart', dragStart, { passive: false });
    }

    /**
     * Creates the main button.
     */
    function createTriggerButton() {
        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 (Click) or Move Button (Drag)';
        buttonContainer.appendChild(formatButton);

        makeButtonDraggable(formatButton);
    }

    // --- MOBILE KEYBOARD FIX ---
    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) { /* Expected to fail on PC, no error needed */ }
    }

    // --- ADAPTIVE STYLES ---
    GM_addStyle(`
        #janitor-editor-buttons button {
            position: fixed;
            z-index: 9999;
            color: white;
            border: none;
            border-radius: 50%;
            box-shadow: 0 4px 8px rgba(0,0,0,0.3);
            cursor: pointer;
            transition: transform 0.2s, opacity 0.2s, box-shadow 0.2s;
            user-select: none; /* Prevents text selection while dragging */
        }
        #janitor-editor-buttons button:active {
             /* Kept for quick click feedback, drag effect overrides it */
            transform: scale(0.95);
        }
        #formatterTrigger {
            background-color: #c9226e;
        }

        /* NEW: Visual effect for when the button is being dragged */
        #janitor-editor-buttons button.is-dragging {
            transform: scale(1.1); /* Make it slightly bigger */
            opacity: 0.8;          /* Make it semi-transparent */
            box-shadow: 0 8px 16px rgba(0,0,0,0.4); /* Enhance the shadow */
            transition: none;      /* Disable transition for smooth dragging */
        }

        /* PC STYLES (default position) */
        @media (min-width: 769px) {
            #formatterTrigger {
                width: 50px; height: 50px; font-size: 24px;
                right: 27%; bottom: 12%;
            }
        }
        /* MOBILE STYLES (default position) */
        @media (max-width: 768px) {
            #formatterTrigger {
                width: 40px; height: 40px; font-size: 16px;
                right: 28%; bottom: 20%;
            }
        }
    `);

    // --- LAUNCH SCRIPT ---
    createTriggerButton();
    initKeyboardBugFix();
    console.log('Script "Janitor AI - Automatic Message Formatting Corrector (Drag & Drop button)" (v7.0) launched successfully.');
})();