ChatGPT UwUifier

Adds a button to uwuify ChatGPT responses, preserving formatting.

// ==UserScript==
// @name         ChatGPT UwUifier
// @namespace    https://github.com/rastr1sr
// @version      1.0
// @description  Adds a button to uwuify ChatGPT responses, preserving formatting.
// @author       Rastrisr
// @match        *://chat.openai.com/*
// @match        *://chatgpt.com/*
// @grant        none
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const UWU_BUTTON_ID = 'uwuifier-button';
    const ORIGINAL_CONTENT_ATTR = 'data-original-content-uwu';
    const ASSISTANT_MESSAGE_SELECTOR = '[data-message-author-role="assistant"]';
    const CONTENT_SELECTORS = [
        '.markdown',
        '[class*="prose"]',
        'div.text-message',
        'div > p'
    ];
    const DEBOUNCE_DELAY = 300;
    const TAGS_TO_SKIP_UWUIFY = new Set(['PRE', 'CODE', 'SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'BUTTON']);

    // --- State ---
    let isUwuActive = false;
    let uwuButton = null;
    let modifiedElements = new Set();
    let initTimeoutId = null;
    let observerDebounceTimeout = null;

    // --- Core UwUify Logic (Text Only) ---
    function uwuifyText(text) {
        return text
            .replace(/(?:[rl])(?![aeiou])/gi, (match) => (match === 'r' || match === 'l' ? 'w' : 'W'))
            .replace(/[rl](?=[aeiou])/gi, 'w')
            .replace(/[RL](?=[AEIOU])/g, 'W')
            .replace(/ove/gi, 'uv')
            .replace(/O([^a-zA-Z]|$)/g, 'OwO$1')
            .replace(/n([aeiou])/gi, (match, p1) => Math.random() > 0.4 ? `ny${p1}` : match)
            .replace(/\b(Y)ou\b/g, 'Yuw').replace(/\b(y)ou\b/g, 'yuw')
            .replace(/\b(T)he\b/g, 'Da').replace(/\b(t)he\b/g, 'da')
            .replace(/\b(Y)ou're\b/g, "Yuw'we").replace(/\b(y)ou're\b/g, "yuw'we")
            // ... More text replacement rules here, reply in comments if you have any suggestions ...
            .replace(/([.!?])\s+/g, (match, p1) => {
                const random = Math.random();
                if (random < 0.05) return `${p1} uwu `;
                if (random < 0.10) return `${p1} owo `;
                if (random < 0.15) return `${p1} >w< `;
                return match;
            })
             .replace(/(!+)/g, (match) => Math.random() < 0.7 ? `${match}~` : match)
             .replace(/([.!?])($|\n)/gm, (match, p1, p2) => {
                 const random = Math.random();
                 let suffix = '';
                 if (random < 0.08) suffix = ` (✿◠‿◠)`;
                 else if (random < 0.16) suffix = ` (◕ᴗ◕✿)`;
                 else if (random < 0.24) suffix = ` (。◕‿◕。)`;
                 return `${p1}${suffix}${p2}`;
             });
    }

    // --- DOM Processing ---
    function traverseAndUwuify(node) {
        if (node.nodeType === Node.TEXT_NODE) {
            if (node.parentNode && TAGS_TO_SKIP_UWUIFY.has(node.parentNode.tagName)) {
                return;
            }
            const trimmedText = node.nodeValue.trim();
            if (trimmedText.length > 0) {
                 // Check for our placeholders - DO NOT uwuify them
                 if (!trimmedText.startsWith('[[CODE_BLOCK_') && !trimmedText.startsWith('[[INLINE_CODE_')) {
                     node.nodeValue = uwuifyText(node.nodeValue);
                 }
            }
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            if (TAGS_TO_SKIP_UWUIFY.has(node.tagName)) {
                return;
            }
            node.childNodes.forEach(traverseAndUwuify);
        }
    }

    function uwuifyHtmlContent(htmlString) {
        // Protect code blocks and inline code with placeholders
        const codeBlocks = [];
        let processedHtml = htmlString.replace(/```([\s\S]*?)```/g, (match, codeContent) => {
             const placeholder = `<pre class="uwu-placeholder-block">[[CODE_BLOCK_${codeBlocks.length}]]</pre>`;
             codeBlocks.push(match);
            return placeholder;
        });

        const inlineCode = [];
        processedHtml = processedHtml.replace(/`([^`]+?)`/g, (match, codeContent) => {
             const placeholder = `<code class="uwu-placeholder-inline">[[INLINE_CODE_${inlineCode.length}]]</code>`;
             inlineCode.push(match);
             return placeholder;
        });

        // Parse the HTML with placeholders into a DOM fragment
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = processedHtml;

        // Traverse the DOM fragment and apply uwuifyText to safe text nodes
        traverseAndUwuify(tempDiv);

        // Serialize the modified DOM fragment back to an HTML string
        let uwuifiedHtml = tempDiv.innerHTML;

        // Restore original code blocks and inline code
        uwuifiedHtml = uwuifiedHtml.replace(/<pre class="uwu-placeholder-block">\[\[CODE_BLOCK_(\d+)\]\]<\/pre>/g, (match, index) => {
            return codeBlocks[parseInt(index, 10)] || match;
        });
         uwuifiedHtml = uwuifiedHtml.replace(/<code class="uwu-placeholder-inline">\[\[INLINE_CODE_(\d+)\]\]<\/code>/g, (match, index) => {
             return inlineCode[parseInt(index, 10)] || match;
         });

        return uwuifiedHtml;
    }

    // --- DOM Manipulation ---
    function findContentElements(parentElement) {
        for (const selector of CONTENT_SELECTORS) {
            const elements = parentElement.querySelectorAll(selector);
            if (elements.length > 0) {
                 const filtered = Array.from(elements).filter(el => el.textContent.trim().length > 0 && !el.querySelector(CONTENT_SELECTORS.join(',')));
                 if (filtered.length > 0) return filtered;
                return Array.from(elements);
            }
        }
        // Fallback: Look for direct div children that aren't just simple containers
        const directDivs = parentElement.querySelectorAll(':scope > div:not(:has(button):only-child)');
        if (directDivs.length > 0) return Array.from(directDivs).filter(el => el.textContent.trim().length > 10);

        // Fallback: Use the parent itself if it has significant text and few children
        if (parentElement.textContent.trim().length > 50 && parentElement.childElementCount < 5) {
             return [parentElement];
        }
        return [];
    }

    function applyUwU() {
        modifiedElements.clear();

        const assistantMessages = document.querySelectorAll(ASSISTANT_MESSAGE_SELECTOR);
        assistantMessages.forEach(messageContainer => {
            const contentElements = findContentElements(messageContainer);
            contentElements.forEach(element => {
                 if (element.closest(ASSISTANT_MESSAGE_SELECTOR) !== messageContainer || modifiedElements.has(element)) {
                   return;
                 }
                 if (!element.hasAttribute(ORIGINAL_CONTENT_ATTR)) {
                    element.setAttribute(ORIGINAL_CONTENT_ATTR, element.innerHTML);
                 }
                 const originalContent = element.getAttribute(ORIGINAL_CONTENT_ATTR);
                 if (originalContent !== null) {
                     element.innerHTML = uwuifyHtmlContent(originalContent);
                     modifiedElements.add(element);
                 }
            });
        });
    }

    function revertUwU() {
        modifiedElements.forEach(element => {
            if (element.hasAttribute(ORIGINAL_CONTENT_ATTR)) {
                element.innerHTML = element.getAttribute(ORIGINAL_CONTENT_ATTR);
            }
        });
    }

    function toggleUwU() {
        isUwuActive = !isUwuActive;
        if (isUwuActive) {
            applyUwU();
            uwuButton.textContent = 'De-UwUify';
            uwuButton.setAttribute('data-active', 'true');
        } else {
            revertUwU();
            uwuButton.textContent = 'UwUify';
            uwuButton.setAttribute('data-active', 'false');
        }
    }

    // --- Button Creation and Injection ---
    function createUwUButton() {
        const button = document.createElement('button');
        button.id = UWU_BUTTON_ID;
        button.className = 'uwu-button flex items-center justify-center rounded-md border h-9 w-auto px-3 py-2 text-sm font-medium transition-colors';
        button.textContent = 'UwUify';
        button.setAttribute('data-active', 'false');
        button.addEventListener('click', toggleUwU);
        return button;
    }


     function addStyles() {
         const styleId = 'uwuifier-styles';
         if (document.getElementById(styleId)) return;

         const style = document.createElement('style');
         style.id = styleId;
         style.textContent = `
             #${UWU_BUTTON_ID} {
                 position: fixed; top: 60px; right: 20px; z-index: 9999;
                 cursor: pointer; box-shadow: 0 2px 5px rgba(0,0,0,0.2);
                 font-family: inherit;
                 background-color: var(--button-secondary-background, #ffffff);
                 color: var(--button-secondary-color, #374151);
                 border-color: var(--button-secondary-border-color, #d1d5db);
                 transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
             }
             #${UWU_BUTTON_ID}:hover {
                 background-color: var(--button-secondary-background-hover, #f3f4f6);
                 transform: scale(1.03);
             }
             #${UWU_BUTTON_ID}[data-active="true"] { background-color: #d8b4fe; border-color: #c084fc; color: #3b0764; }
             #${UWU_BUTTON_ID}[data-active="true"]:hover { background-color: #c084fc; }

             @media (prefers-color-scheme: dark) {
                 #${UWU_BUTTON_ID} { background-color: #374151; color: #d1d5db; border-color: #4b5563; }
                 #${UWU_BUTTON_ID}:hover { background-color: #4b5563; color: #f9fafb; }
                 #${UWU_BUTTON_ID}[data-active="true"] { background-color: #a855f7; border-color: #9333ea; color: #ffffff; }
                 #${UWU_BUTTON_ID}[data-active="true"]:hover { background-color: #9333ea; }
             }
         `;
         document.head.appendChild(style);
     }

    function initialize() {
        if (initTimeoutId) clearTimeout(initTimeoutId);

        if (document.getElementById(UWU_BUTTON_ID)) {
             if (!uwuButton) uwuButton = document.getElementById(UWU_BUTTON_ID);
             if (uwuButton && uwuButton.getAttribute('data-active') !== String(isUwuActive)) {
                 uwuButton.setAttribute('data-active', String(isUwuActive));
                 uwuButton.textContent = isUwuActive ? 'De-UwUify' : 'UwUify';
             }
             return;
        }

        const chatContainer = document.querySelector('main') || document.body;
        if (chatContainer) {
            addStyles();
            uwuButton = createUwUButton();
            document.body.appendChild(uwuButton);
             if (isUwuActive) {
                 applyUwU();
             }
        } else {
            initTimeoutId = setTimeout(initialize, 1000);
        }
    }

    // --- Mutation Observer ---
    const observer = new MutationObserver((mutations) => {
        let potentiallyNewContent = false;
        let needsButtonCheck = !document.getElementById(UWU_BUTTON_ID);

        for (const mutation of mutations) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                 if (needsButtonCheck) {
                     potentiallyNewContent = true;
                     break;
                 }
                 for (const node of mutation.addedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                       if ((node.matches && node.matches(ASSISTANT_MESSAGE_SELECTOR)) || (node.querySelector && node.querySelector(ASSISTANT_MESSAGE_SELECTOR))) {
                           potentiallyNewContent = true;
                           break;
                       }
                    }
                 }
            }
            if (potentiallyNewContent && !needsButtonCheck) break;
        }

        if (potentiallyNewContent || needsButtonCheck) {
            clearTimeout(observerDebounceTimeout);
            observerDebounceTimeout = setTimeout(() => {
                 initialize();
                 if (isUwuActive && potentiallyNewContent) {
                     applyUwU();
                 }
            }, DEBOUNCE_DELAY);
        }
    });

    // --- Start Script ---
    observer.observe(document.body, { childList: true, subtree: true });
    initialize();

})();