Perplexity UI - Premium Highlighter Theme (Definitive)

The final, definitive script for a premium Perplexity UI with a robust, persistent, multi-color highlighting tool.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Perplexity UI - Premium Highlighter Theme (Definitive)
// @namespace    http://tampermonkey.net/
// @version      13.0
// @description  The final, definitive script for a premium Perplexity UI with a robust, persistent, multi-color highlighting tool.
// @match        https://www.perplexity.ai/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    // --- Part 1: The CSS for the Premium Theme & Highlighter ---
    GM_addStyle(`
        /* --- [ IMPORTS & THEME VARIABLES ] --- */
        @import url('https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono&display=swap');

        :root {
            --main-bg: #0b0f10;
            --panel-bg: #111414;
            --text-color: #c3c3c3;
            --header-color: #f0f0f0;
            --link-color: #6ac9ff;
            --border-color: #2b3b3a;
            --border-color-focus: #555;
        }

        /* --- [ GLOBAL STYLES ] --- */
        html, body {
            font-family: 'Merriweather', serif !important;
            background-color: var(--main-bg) !important;
            color: var(--text-color) !important;
            font-size: 1.1rem !important;
        }

        /* --- [ PREMIUM TYPOGRAPHY ] --- */
        .prose, .prose p, .prose li {
            font-family: 'Merriweather', serif !important;
            font-size: 1.1rem !important;
            line-height: 1.8 !important;
            color: var(--text-color) !important;
        }
        .prose h2 {
            font-size: 1.7rem !important; margin-top: 2.5rem !important; margin-bottom: 1.2rem !important;
            color: var(--header-color) !important; font-weight: 700 !important;
        }
        .prose h3 {
            font-size: 1.3rem !important; margin-top: 2rem !important; margin-bottom: 0.5rem !important;
            color: var(--header-color) !important; font-weight: 700 !important;
        }
        a { color: var(--link-color) !important; text-decoration: none !important; }

        /* --- [ LAYOUT & UI ] --- */
        .prose {
            background: var(--panel-bg) !important; border: none !important;
            border-radius: 14px !important; padding: 2rem 2.5rem !important;
            box-shadow: 0 8px 40px rgba(0,0,0,0.5) !important;
        }
        div[cplx-follow-up-query-box="true"] .bg-offset {
            background-color: var(--panel-bg) !important; border: 1px solid var(--border-color) !important;
            border-radius: 16px !important; box-shadow: 0 8px 40px rgba(0,0,0,0.5) !important;
            transition: border-color 0.2s ease;
        }
        div[cplx-follow-up-query-box="true"] .bg-offset:focus-within {
            border-color: var(--border-color-focus) !important;
            box-shadow: 0 8px 40px rgba(0,0,0,0.5) !important; outline: none !important;
        }
        textarea {
            font-family: 'Merriweather', serif !important; font-size: 1.1rem !important;
            background-color: transparent !important;
        }
        .bg-offset, .bg-base, .bg-default, .bg-offsetPlus, .bg-raised { background-color: var(--panel-bg) !important; }

        /* --- [ HIGHLIGHTER TOOL STYLES ] --- */
        #highlighter-toolbar {
            position: absolute; background-color: #252a2b;
            border: 1px solid #444; border-radius: 8px;
            padding: 5px; box-shadow: 0 4px 15px rgba(0,0,0,0.4);
            z-index: 10000; display: none; gap: 5px;
        }
        .highlight-btn {
            width: 24px; height: 24px; border-radius: 50%;
            cursor: pointer; border: 2px solid transparent;
            transition: all 0.2s ease;
            outline: none !important;
        }
        .highlight-btn:hover { transform: scale(1.1); border-color: #fff; }
        .h-yellow { background-color: rgba(253, 224, 71, 0.5); }
        .h-pink   { background-color: rgba(244, 114, 182, 0.5); }
        .h-blue   { background-color: rgba(96, 165, 250, 0.5); }
        .h-green  { background-color: rgba(74, 222, 128, 0.5); }
        .h-clear  { background-color: #9ca3af; font-size: 12px; color: #fff; display:grid; place-items:center; }
        mark.ph-highlight {
            border-radius: 3px;
            padding: 0 2px;
            color: #FFF !important;
            background-color: var(--highlight-color);
        }

        /* --- [ FINAL BUG FIXES ] --- */
        .pb-md.border-b, .divide-y > :not([hidden]) ~ :not([hidden]) {
            border: none !important;
        }
    `);

    // --- Part 2: The JavaScript for Highlighting & Persistence ---

    function getXPath(node) {
        let path = '';
        for (; node && node.nodeType == 1; node = node.parentNode) {
            let index = 0;
            for (let sibling = node.previousSibling; sibling; sibling = sibling.previousSibling) {
                if (sibling.nodeType == 1 && sibling.nodeName == node.nodeName) index++;
            }
            const tagName = node.nodeName.toLowerCase();
            const pathIndex = (index > 0 ? `[${index + 1}]` : '');
            path = `/${tagName}${pathIndex}${path}`;
        }
        return path;
    }

    function getNodeFromXPath(path) {
        try {
            return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
        } catch (e) { return null; }
    }

    async function saveHighlights() {
        const highlights = [];
        document.querySelectorAll('.prose mark.ph-highlight').forEach(mark => {
            const parent = mark.parentNode;
            if (!parent) return;
            parent.normalize();
            let startOffset = 0;
            for (let i = 0; i < parent.childNodes.length; i++) {
                const child = parent.childNodes[i];
                if (child === mark) break;
                startOffset += child.textContent.length;
            }
            highlights.push({
                path: getXPath(parent),
                start: startOffset,
                length: mark.textContent.length,
                color: mark.style.backgroundColor
            });
        });
        const key = `highlights_${window.location.pathname}`;
        await GM_setValue(key, JSON.stringify(highlights));
    }

    async function applyHighlights() {
        const key = `highlights_${window.location.pathname}`;
        const savedHighlights = JSON.parse(await GM_getValue(key, '[]'));
        if (savedHighlights.length === 0) return;

        savedHighlights.forEach(h => {
            const parentNode = getNodeFromXPath(h.path);
            if (parentNode) {
                const textNodes = Array.from(parentNode.childNodes).filter(n => n.nodeType === Node.TEXT_NODE);
                let charCount = 0;
                for (const textNode of textNodes) {
                    const nodeLength = textNode.textContent.length;
                    if (charCount + nodeLength >= h.start) {
                        const range = document.createRange();
                        const start = h.start - charCount;
                        const end = start + h.length;
                        if (start < 0 || end > nodeLength) continue;
                        range.setStart(textNode, start);
                        range.setEnd(textNode, end);
                        const mark = document.createElement('mark');
                        mark.className = 'ph-highlight';
                        mark.style.backgroundColor = h.color;
                        try {
                            range.surroundContents(mark);
                        } catch (e) {}
                        break;
                    }
                    charCount += nodeLength;
                }
            }
        });
    }

    let selectionRange = null;
    const toolbar = document.createElement('div');
    toolbar.id = 'highlighter-toolbar';
    document.body.appendChild(toolbar);

    const colors = {
        'yellow': 'rgba(253, 224, 71, 0.5)', 'pink': 'rgba(244, 114, 182, 0.5)',
        'blue': 'rgba(96, 165, 250, 0.5)', 'green': 'rgba(74, 222, 128, 0.5)'
    };

    for (const [name, color] of Object.entries(colors)) {
        const btn = document.createElement('div');
        btn.className = `highlight-btn h-${name}`;
        btn.addEventListener('mousedown', (e) => { e.preventDefault(); highlightSelection(color); });
        toolbar.appendChild(btn);
    }

    const clearBtn = document.createElement('div');
    clearBtn.className = 'highlight-btn h-clear';
    clearBtn.innerHTML = 'X';
    clearBtn.addEventListener('mousedown', (e) => { e.preventDefault(); clearHighlight(); });
    toolbar.appendChild(clearBtn);

    function highlightSelection(color) {
        if (selectionRange) {
            const mark = document.createElement('mark');
            mark.className = 'ph-highlight';
            mark.style.backgroundColor = color;
            try {
                mark.appendChild(selectionRange.extractContents());
                selectionRange.insertNode(mark);
                saveHighlights();
            } catch (e) {}
        }
        toolbar.style.display = 'none';
        window.getSelection().removeAllRanges();
    }

    function clearHighlight() {
        if (selectionRange) {
            let parent = selectionRange.commonAncestorContainer;
            if (parent.nodeType === Node.TEXT_NODE) parent = parent.parentElement;
            if (parent.tagName === 'MARK' && parent.classList.contains('ph-highlight')) {
                const grandParent = parent.parentNode;
                grandParent.innerHTML = grandParent.innerHTML.replace(parent.outerHTML, parent.innerHTML);
                grandParent.normalize();
                saveHighlights();
            }
        }
        toolbar.style.display = 'none';
        window.getSelection().removeAllRanges();
    }

    document.addEventListener('mouseup', (e) => {
        if (!e.target.closest('.prose')) {
            if (toolbar.style.display === 'flex') toolbar.style.display = 'none';
            return;
        }
        const selection = window.getSelection();
        if (selection.toString().trim().length > 0) {
            selectionRange = selection.getRangeAt(0);
            const rect = selectionRange.getBoundingClientRect();
            toolbar.style.left = `${rect.left + window.scrollX + (rect.width / 2) - (toolbar.offsetWidth / 2)}px`;
            toolbar.style.top = `${rect.top + window.scrollY - toolbar.offsetHeight - 8}px`;
            toolbar.style.display = 'flex';
        } else {
            toolbar.style.display = 'none';
        }
    });

    document.addEventListener('mousedown', (e) => {
        if (!e.target.closest('#highlighter-toolbar')) {
            toolbar.style.display = 'none';
        }
    });

    // --- Part 3: The Bulletproof Observer Engine ---
    let observer;
    const startObserver = () => {
        const targetNode = document.querySelector('div[data-cplx-component="thread-wrapper"]');
        if (targetNode) {
            if (observer) observer.disconnect();
            applyHighlights();
            observer = new MutationObserver(() => {
                applyHighlights();
            });
            observer.observe(targetNode, { childList: true, subtree: true });
        }
    };

    // Use an interval to constantly check for the chat container, which handles SPA navigation.
    setInterval(startObserver, 500);

})();