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

Formats narration, removes <think> tags, and features a draggable button that remembers its position. Adapts for PC & Mobile.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Chub AI - Automatic Message Formatting Corrector (Drag & Drop button)
// @namespace    http://tampermonkey.net/
// @version      7.0
// @description  Formats narration, removes <think> tags, and features a draggable button that remembers its position. Adapts for PC & Mobile.
// @author       accforfaciet
// @match        *://chub.ai/chats/*
// @grant        GM_addStyle
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- SCRIPT SETTINGS ---
    const DEBUG_MODE = false;
    const BUTTON_POSITION_STORAGE_KEY = 'chubFormatterButtonPosition_v2'; // Key for saving the button's position
    // --- END OF SETTINGS ---

    // --- SELECTORS FOR CHUB.AI ---
    const EDIT_BUTTON_SELECTOR = 'button:has(span[aria-label="edit"])';
    const TEXT_AREA_SELECTOR = 'textarea.ant-input-borderless';
    const CONFIRM_BUTTON_SELECTOR = 'button:has(span[aria-label="check"])';
    const MAIN_INPUT_SELECTOR = 'textarea[placeholder*="Send a message"]';
    // --- END OF SELECTORS ---

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

    // --- DOM HELPER ---
    function waitForElement(selector) {
        return new Promise(resolve => {
            if (document.querySelector(selector)) return resolve(document.querySelector(selector));
            const observer = new MutationObserver(() => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
        });
    }

    // --- CORE FORMATTING FUNCTIONS (Unchanged) ---
    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(/\n?\s*<think>[\s\S]*?<\/think>\s*\n?/g, '');
        text = text.replace(/<system>[\s\S]*?<\/system>/g, '');
        text = text.replace(/<response>[\s\S]*?<\/response>/g, '');
        return removeSystemPrompt(text);
    }
    function removeSystemPrompt(text) {
        const trimmedText = text.trim();
        if (!trimmedText.toLowerCase().startsWith('the user')) return text;
        const splitPointIndex = text.search(/[^\s\*]\*[^\s\*]/);
        if (splitPointIndex !== -1) {
            debugLog(`System prompt found and removed.`);
            return text.substring(splitPointIndex + 1);
        }
        return text;
    }
    function formatNarrationAndDialogue(text) {
        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');
    }

    // --- MAIN SCRIPT LOGIC (Unchanged) ---
    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();
            const textField = await waitForElement(TEXT_AREA_SELECTOR);
            const nativeTextareaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
            nativeTextareaValueSetter.call(textField, textProcessor(textField.value));
            textField.dispatchEvent(new Event('input', { bubbles: true }));
            const confirmButton = await waitForElement(CONFIRM_BUTTON_SELECTOR);
            if (confirmButton) confirmButton.click();
            debugLog('--- PROCESS SUCCESSFULLY COMPLETED ---');
        } catch (error) {
            console.error('CRITICAL ERROR during the editing process:', error);
        }
    }

    // --- DRAGGABLE BUTTON LOGIC (Updated!) ---
    function makeDraggable(element) {
        let isDragging = false;
        let hasDragged = false;
        let startX, startY, initialLeft, initialTop;

        function dragStart(e) {
            isDragging = true;
            hasDragged = false;
            element.classList.add('is-dragging'); // Add visual effect class
            const clientX = e.clientX ?? e.touches[0].clientX;
            const clientY = e.clientY ?? e.touches[0].clientY;
            startX = clientX;
            startY = clientY;
            const rect = element.getBoundingClientRect();
            initialLeft = rect.left;
            initialTop = rect.top;
            window.addEventListener('mousemove', dragMove, { passive: false });
            window.addEventListener('touchmove', dragMove, { passive: false });
            window.addEventListener('mouseup', dragEnd);
            window.addEventListener('touchend', dragEnd);
        }

        function dragMove(e) {
            if (!isDragging) return;
            e.preventDefault(); // Prevent page scroll on mobile
            hasDragged = true;

            const clientX = e.clientX ?? e.touches[0].clientX;
            const clientY = e.clientY ?? e.touches[0].clientY;
            const deltaX = clientX - startX;
            const deltaY = clientY - startY;
            let newLeft = initialLeft + deltaX;
            let newTop = initialTop + deltaY;

            // --- Screen Boundary Constraints (New!) ---
            const buttonWidth = element.offsetWidth;
            const buttonHeight = element.offsetHeight;
            const screenWidth = window.innerWidth;
            const screenHeight = window.innerHeight;
            newLeft = Math.max(0, Math.min(newLeft, screenWidth - buttonWidth));
            newTop = Math.max(0, Math.min(newTop, screenHeight - buttonHeight));
            // --- End of Constraints ---

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

        function dragEnd() {
            if (!isDragging) return;
            isDragging = false;
            element.classList.remove('is-dragging'); // Remove visual effect class
            if (hasDragged) {
                const finalRect = element.getBoundingClientRect();
                savePosition({ left: finalRect.left, top: finalRect.top });
            }
            window.removeEventListener('mousemove', dragMove);
            window.removeEventListener('touchmove', dragMove);
            window.removeEventListener('mouseup', dragEnd);
            window.removeEventListener('touchend', dragEnd);
        }

        element.addEventListener('mousedown', dragStart);
        element.addEventListener('touchstart', dragStart, { passive: true });
        element.addEventListener('click', () => { if (!hasDragged) processLastMessage(formatNarrationAndDialogue); });
    }

    function savePosition(pos) {
        localStorage.setItem(BUTTON_POSITION_STORAGE_KEY, JSON.stringify(pos));
        debugLog('Button position saved:', pos);
    }

    function loadPosition(element) {
        const savedPos = localStorage.getItem(BUTTON_POSITION_STORAGE_KEY);
        if (savedPos) {
            const pos = JSON.parse(savedPos);
            element.style.right = 'auto';
            element.style.bottom = 'auto';
            element.style.left = `${pos.left}px`;
            element.style.top = `${pos.top}px`;
            debugLog('Button position loaded:', pos);
        }
    }

    // --- UI CREATION & INITIALIZATION ---
    function createTriggerButton() {
        const formatButton = document.createElement('button');
        formatButton.innerHTML = '✏️';
        formatButton.id = 'formatterTrigger';
        formatButton.title = 'Click to format message. Hold and drag to move.';
        document.body.appendChild(formatButton);
        loadPosition(formatButton);
        makeDraggable(formatButton);
    }

    async function initKeyboardBugFix() {
        try {
            const mainInput = await waitForElement(MAIN_INPUT_SELECTOR);
            const button = document.getElementById('formatterTrigger');
            if (!mainInput || !button) return;
            mainInput.addEventListener('focus', () => { button.style.display = 'none'; });
            mainInput.addEventListener('blur', () => { setTimeout(() => { button.style.display = 'block'; }, 200); });
        } catch (e) {
            console.log('Could not find main input field for keyboard fix (this is normal on PC).');
        }
    }

    // --- ADAPTIVE STYLES (Updated with drag effects) ---
    GM_addStyle(`
        #formatterTrigger {
            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; /* Smooth transitions */
            background-color: #c9226e;
            user-select: none;
        }
        #formatterTrigger:active {
            cursor: grabbing;
        }
        /* Visual feedback class for when the button is being dragged (New!) */
        #formatterTrigger.is-dragging {
            transform: scale(1.1);
            opacity: 0.8;
            box-shadow: 0 8px 16px rgba(0,0,0,0.3);
        }
        /* Default position for MOBILE (if no position is saved) */
        #formatterTrigger {
            width: 45px; height: 45px; font-size: 20px; right: 5%; bottom: 9%;
        }
        /* Default position for PC (if no position is saved) */
        @media (min-width: 769px) {
            #formatterTrigger {
                width: 50px; height: 50px; font-size: 24px; right: 18%; bottom: 8%;
            }
        }
    `);

    // --- STARTUP ---
    createTriggerButton();
    initKeyboardBugFix();
    console.log('Script "Chub AI - Automatic Message Formatting Corrector (Drag & Drop button)" (v7.0) started successfully.');
})();