ChatGPT UwUifier

Adds a button to uwuify ChatGPT responses, preserving formatting.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

})();