ChatGPT UwUifier

Adds a button to uwuify ChatGPT responses, preserving formatting.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();

})();