Chub.ai Universal Enhancer (Links, Styles, Pagination, Tooltips, Smart Diff)

Full suite: Title links, tags, pagination, description fixes, tooltips, and a Smart Version History Diff tool (V2+ Nested Support).

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Chub.ai Universal Enhancer (Links, Styles, Pagination, Tooltips, Smart Diff)
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  Full suite: Title links, tags, pagination, description fixes, tooltips, and a Smart Version History Diff tool (V2+ Nested Support).
// @author       Marcal91
// @match        https://chub.ai/*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Global State ---
    let currentProjectId = null;
    let commitHistoryCache = null;
    let diffModal = null;

    // --- Utility: Fetch Wrapper ---
    async function fetchChub(url) {
        const token = localStorage.getItem('token');
        const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' };
        if (token) headers['Authorization'] = `Bearer ${token.replace(/"/g, '')}`;
        const response = await fetch(url, { headers });
        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
        return response;
    }

    // --- Core 1: Get Project ID ---
    async function initializeProjectData() {
        if (!window.location.pathname.startsWith('/characters/')) return;
        const pathParts = window.location.pathname.split('/').slice(2);
        if (pathParts.length < 2) return;
        const characterPath = pathParts.join('/');

        try {
            const apiUrl = `https://gateway.chub.ai/api/characters/${characterPath}?full=true`;
            const response = await fetchChub(apiUrl);
            const data = await response.json();
            if (data && data.node && data.node.id) currentProjectId = data.node.id;
        } catch (e) {
            console.error("Chub Enhancer: Failed to fetch project ID", e);
        }
    }

    // --- Core 2: Fetch Commit History ---
    async function fetchCommitHistory() {
        if (!currentProjectId) return null;
        if (commitHistoryCache) return commitHistoryCache;
        try {
            const url = `https://gateway.chub.ai/api/v4/projects/${currentProjectId}/repository/commits?path=raw%2Ftavern_raw.json`;
            const response = await fetchChub(url);
            const data = await response.json();
            commitHistoryCache = data;
            return data;
        } catch (e) {
            console.error("Chub Enhancer: Failed to fetch history", e);
            return null;
        }
    }

    // --- Core 3: Fetch Raw File ---
    async function fetchVersionContent(ref) {
        const url = `https://gateway.chub.ai/api/v4/projects/${currentProjectId}/repository/files/raw%2Ftavern_raw.json/raw?ref=${ref}&response_type=blob`;
        const response = await fetchChub(url);
        return await response.json();
    }

    // --- Core 4: Word-based Diff Algorithm ---
    function diffWords(text1, text2) {
        if (text1 === undefined || text1 === null) text1 = "";
        if (text2 === undefined || text2 === null) text2 = "";
        text1 = String(text1);
        text2 = String(text2);
        
        if (text1 === text2) return escapeHtml(text1);
        
        const split1 = text1.split(/(\s+)/);
        const split2 = text2.split(/(\s+)/);

        const matrix = Array(split1.length + 1).fill(null).map(() => Array(split2.length + 1).fill(0));

        for (let i = 1; i <= split1.length; i++) {
            for (let j = 1; j <= split2.length; j++) {
                if (split1[i - 1] === split2[j - 1]) {
                    matrix[i][j] = matrix[i - 1][j - 1] + 1;
                } else {
                    matrix[i][j] = Math.max(matrix[i - 1][j], matrix[i][j - 1]);
                }
            }
        }

        let result = "";
        let i = split1.length;
        let j = split2.length;
        const changes =[];

        while (i > 0 || j > 0) {
            if (i > 0 && j > 0 && split1[i - 1] === split2[j - 1]) {
                changes.unshift({ type: 'equal', value: split1[i - 1] });
                i--; j--;
            } else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) {
                changes.unshift({ type: 'add', value: split2[j - 1] });
                j--;
            } else {
                changes.unshift({ type: 'rem', value: split1[i - 1] });
                i--;
            }
        }

        changes.forEach(change => {
            const escaped = escapeHtml(change.value);
            if (change.type === 'equal') result += escaped;
            else if (change.type === 'add') result += `<ins>${escaped}</ins>`;
            else if (change.type === 'rem') result += `<del>${escaped}</del>`;
        });

        return result;
    }

    // --- Diff Presentation Logic ---
    function generateDiffHtml(oldJson, newJson) {
        let html = '';
        let hasChanges = false;

        const oldData = oldJson.spec === 'chara_card_v2' ? oldJson.data : oldJson;
        const newData = newJson.spec === 'chara_card_v2' ? newJson.data : newJson;

        // Dynamic getters to deeply extract fields, including V2 nested extensions
        const textFields =[
            { label: 'Name', get: d => d.name },
            { label: 'Creator', get: d => d.creator },
            { label: 'Character Version', get: d => d.character_version },
            { label: 'Description', get: d => d.description },
            { label: 'Personality', get: d => d.personality },
            { label: 'Scenario', get: d => d.scenario },
            { label: 'First Message', get: d => d.first_mes },
            { label: 'Example Messages', get: d => d.mes_example },
            { label: 'Creator Notes', get: d => d.creator_notes },
            { label: 'System Prompt', get: d => d.system_prompt },
            { label: 'Post History Instructions', get: d => d.post_history_instructions },
            { label: 'Depth Prompt', get: d => {
                const dp = d.extensions?.depth_prompt || d.depth_prompt;
                if (!dp) return '';
                return `[Role: ${dp.role || 'system'} | Depth: ${dp.depth !== undefined ? dp.depth : 4}]\n${dp.prompt || ''}`;
            }},
            { label: 'Regex Scripts', get: d => {
                const rx = d.extensions?.regex || d.regex;
                if (!rx || !Array.isArray(rx) || rx.length === 0) return '';
                return rx.map(r => `Regex: ${r.regex}\nReplacement: ${r.replacement}\nPlacement: ${r.placement} | Disabled: ${!!r.disabled}`).join('\n\n');
            }}
        ];

        textFields.forEach(field => {
            const oldVal = String(field.get(oldData) || '');
            const newVal = String(field.get(newData) || '');

            if (oldVal !== newVal) {
                hasChanges = true;
                const diffOutput = diffWords(oldVal, newVal);
                html += renderDiffSection(field.label, diffOutput);
            }
        });

        // Tags Diff
        const oldTags = oldData.tags ||[];
        const newTags = newData.tags ||[];
        if (JSON.stringify(oldTags) !== JSON.stringify(newTags)) {
            hasChanges = true;
            let tagHtml = '<div class="diff-tags">';
            const allTags = new Set([...oldTags, ...newTags]);
            allTags.forEach(tag => {
                if (oldTags.includes(tag) && newTags.includes(tag)) tagHtml += `<span class="tag tag-same">${escapeHtml(tag)}</span>`;
                else if (oldTags.includes(tag)) tagHtml += `<span class="tag tag-del">${escapeHtml(tag)}</span>`;
                else tagHtml += `<span class="tag tag-add">${escapeHtml(tag)}</span>`;
            });
            tagHtml += '</div>';
            html += renderDiffSection("Tags", tagHtml);
        }

        // Alternate Greetings Diff
        const oldAlts = oldData.alternate_greetings ||[];
        const newAlts = newData.alternate_greetings ||[];
        if (JSON.stringify(oldAlts) !== JSON.stringify(newAlts)) {
            hasChanges = true;
            let altHtml = '';
            const maxLen = Math.max(oldAlts.length, newAlts.length);
            for (let i = 0; i < maxLen; i++) {
                const oldG = oldAlts[i] || "";
                const newG = newAlts[i] || "";
                if (oldG !== newG) {
                    altHtml += `<div class="alt-greet-diff"><strong>Greeting #${i + 1}:</strong><br>${diffWords(oldG, newG)}</div>`;
                }
            }
            html += renderDiffSection("Alternate Greetings", altHtml);
        }

        // Lorebook Diff (Keyed by Name/Comment instead of ID)
        const oldBook = oldData.character_book;
        const newBook = newData.character_book;

        if (oldBook || newBook) {
            const oldEntries = (oldBook && oldBook.entries) ? oldBook.entries :[];
            const newEntries = (newBook && newBook.entries) ? newBook.entries :[];
            
            const mapEntries = (entries) => {
                const map = {};
                entries.forEach(e => { map[e.comment || e.name] = e; });
                return map;
            };

            const oldMap = mapEntries(oldEntries);
            const newMap = mapEntries(newEntries);
            const allKeys = new Set([...Object.keys(oldMap), ...Object.keys(newMap)]);
            
            let loreHtml = '';
            let loreChanged = false;

            allKeys.forEach(key => {
                const oldE = oldMap[key];
                const newE = newMap[key];
                
                if (!oldE) {
                    loreChanged = true;
                    loreHtml += `
                    <details class="lore-entry entry-add">
                        <summary><strong>+ Added Entry:</strong> ${escapeHtml(key)}</summary>
                        <div class="lore-details-content">
                            <div class="sub-label">Keys:</div>${escapeHtml(newE.keys.join(', '))}<br>
                            <div class="sub-label">Content:</div>${escapeHtml(newE.content)}
                        </div>
                    </details>`;
                } else if (!newE) {
                    loreChanged = true;
                    loreHtml += `
                    <details class="lore-entry entry-del">
                        <summary><strong>- Removed Entry:</strong> ${escapeHtml(key)}</summary>
                        <div class="lore-details-content">
                            <div class="sub-label">Keys:</div>${escapeHtml(oldE.keys.join(', '))}<br>
                            <div class="sub-label">Content:</div>${escapeHtml(oldE.content)}
                        </div>
                    </details>`;
                } else {
                    let entryDiffs = '';
                    if (oldE.content !== newE.content) entryDiffs += `<div class="sub-label">Content:</div>${diffWords(oldE.content, newE.content)}<br>`;
                    if (JSON.stringify(oldE.keys) !== JSON.stringify(newE.keys)) entryDiffs += `<div class="sub-label">Keys:</div>${diffWords(oldE.keys.join(', '), newE.keys.join(', '))}<br>`;
                    
                    if (entryDiffs) {
                        loreChanged = true;
                        loreHtml += `<div class="lore-entry entry-mod"><strong>Modified Entry:</strong> ${escapeHtml(key)}<br>${entryDiffs}</div>`;
                    }
                }
            });

            if (loreChanged) {
                hasChanges = true;
                html += renderDiffSection("Lorebook (Character Book)", loreHtml);
            }
        }

        if (!hasChanges) {
            return '<div class="diff-no-change">No textual changes detected in visible fields.</div>';
        }
        return html;
    }

    function renderDiffSection(title, content) {
        return `
        <div class="diff-field">
            <h3>${title}</h3>
            <div class="diff-content">${content}</div>
        </div>`;
    }

    function escapeHtml(text) {
        if (!text) return '';
        return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
                   .replace(/"/g, "&quot;").replace(/'/g, "&#039;").replace(/\n/g, "<br>");
    }

    // --- UI: Modal ---
    function showDiffModal(content, titleText) {
        if (!diffModal) {
            diffModal = document.createElement('div');
            diffModal.id = 'chub-diff-modal';
            diffModal.innerHTML = `
                <div class="diff-modal-content">
                    <div class="diff-modal-header">
                        <h2 id="diff-modal-title">Version Comparison</h2>
                        <button class="diff-modal-close">×</button>
                    </div>
                    <div class="diff-modal-body"></div>
                </div>`;
            document.body.appendChild(diffModal);

            diffModal.querySelector('.diff-modal-close').addEventListener('click', () => diffModal.style.display = 'none');
            diffModal.addEventListener('click', (e) => { if (e.target === diffModal) diffModal.style.display = 'none'; });
        }
        document.getElementById('diff-modal-title').textContent = titleText;
        diffModal.querySelector('.diff-modal-body').innerHTML = content;
        diffModal.style.display = 'flex';
    }

    // --- UI: Inject Buttons ---
    async function processVersionHistory() {
        const commitsBlock = document.querySelector('.commits-block');
        if (!commitsBlock) return;
        if (commitsBlock.dataset.chubProcessed) return;
        
        const historyData = await fetchCommitHistory();
        if (!historyData) return;
        
        commitsBlock.dataset.chubProcessed = "true";
        const rows = Array.from(commitsBlock.querySelectorAll('.grid.grid-cols-4'));

        rows.forEach((row, index) => {
            if (row.querySelector('.chub-diff-btn')) return;
            if (index === rows.length - 1) return; 

            const shortIdDiv = row.querySelector('.commit-id');
            if (!shortIdDiv) return;
            const currentShortId = shortIdDiv.textContent.trim();

            const currentCommitObj = historyData.find(c => c.short_id === currentShortId);
            const olderRow = rows[index + 1];
            const olderShortId = olderRow.querySelector('.commit-id').textContent.trim();
            const olderCommitObj = historyData.find(c => c.short_id === olderShortId);

            if (!currentCommitObj || !olderCommitObj) return;

            const btnContainer = row.querySelector('.commit-download');
            if (!btnContainer) return;

            const diffBtn = document.createElement('button');
            diffBtn.type = 'button';
            diffBtn.textContent = 'Diff';
            diffBtn.title = `Compare ${currentShortId} with ${olderShortId}`;
            diffBtn.className = 'ant-btn css-1fxid1a ant-btn-default ant-btn-color-default ant-btn-variant-outlined chub-diff-btn';
            diffBtn.style.marginLeft = '5px';
            diffBtn.style.padding = '0 8px';
            diffBtn.style.height = '24px';
            diffBtn.style.fontSize = '12px';

            diffBtn.onclick = async function() {
                const origText = diffBtn.textContent;
                diffBtn.textContent = '...';
                diffBtn.disabled = true;
                try {
                    const [oldData, newData] = await Promise.all([
                        fetchVersionContent(olderCommitObj.id),
                        fetchVersionContent(currentCommitObj.id)
                    ]);
                    const diffHtml = generateDiffHtml(oldData, newData);
                    const modalTitle = `Changes from ${olderShortId} ➔ ${currentShortId}`;
                    showDiffModal(diffHtml, modalTitle);
                } catch (e) {
                    alert("Error: " + e.message);
                } finally {
                    diffBtn.textContent = origText;
                    diffBtn.disabled = false;
                }
            };

            const existingBtn = btnContainer.querySelector('button');
            if (existingBtn) existingBtn.parentNode.insertBefore(diffBtn, existingBtn.nextSibling);
            else btnContainer.appendChild(diffBtn);
        });
    }

    // --- Standard Enhancer Functions ---
    function processAllEnhancements() {
        document.querySelectorAll('.ant-card.char-card-class').forEach(createTitleLink);
        convertAllTagsOnPage();
        addPagination();

        const historySection = document.querySelector('.commits-block');
        if (historySection) {
            if(historySection.childElementCount !== historySection.dataset.childCount) {
                 historySection.dataset.chubProcessed = "";
                 historySection.dataset.childCount = historySection.childElementCount;
            }
            processVersionHistory();
        }
    }
    
    function createTitleLink(cardElement) {
        const titleContainer = cardElement.querySelector('.ant-card-head-title .ant-row > span');
        if (!titleContainer || titleContainer.querySelector('a.chub-card-title-link')) return;
        const titleTextSpan = titleContainer.querySelector('span');
        const cardLink = cardElement.closest('a');
        if (!titleTextSpan || !cardLink) return;
        const fullTitleText = titleTextSpan.textContent.trim();
        const cardURL = cardLink.href;
        const linkElement = document.createElement('a');
        linkElement.href = cardURL;
        linkElement.textContent = fullTitleText;
        linkElement.target = '_blank';
        linkElement.rel = 'noopener noreferrer';
        linkElement.classList.add('chub-card-title-link');
        linkElement.dataset.fullTitle = fullTitleText;
        titleTextSpan.parentNode.replaceChild(linkElement, titleTextSpan);
    }

    function convertAllTagsOnPage() {
        const tagWrappers = document.querySelectorAll('span.cursor-pointer:has(> .ant-tag)');
        tagWrappers.forEach(wrapper => {
            if (wrapper.parentElement.tagName.toLowerCase() === 'a') return;
            const tagTextSpan = wrapper.querySelector('.ant-tag > span:first-child');
            if (!tagTextSpan || !tagTextSpan.textContent) return;
            const tagName = tagTextSpan.textContent.trim();
            const tagURL = `https://chub.ai/characters?tags=${encodeURIComponent(tagName)}`;
            const linkElement = document.createElement('a');
            linkElement.href = tagURL;
            linkElement.rel = 'noopener noreferrer';
            wrapper.parentNode.insertBefore(linkElement, wrapper);
            linkElement.appendChild(wrapper);
        });
    }

    function addPagination() {
        const buttonContainer = document.querySelector('.flex.justify-between.mt-4');
        if (!buttonContainer || buttonContainer.querySelector('.pagination-container')) return;
        const currentURL = new URL(window.location.href);
        let currentPage = parseInt(currentURL.searchParams.get('page') || '1', 10);
        const paginationContainer = document.createElement('div');
        paginationContainer.classList.add('pagination-container');
        function createPageButton(pageNumber, text) {
            const pageButton = document.createElement('button');
            pageButton.type = 'button';
            pageButton.textContent = text || pageNumber.toString();
            pageButton.classList.add('ant-btn', 'css-s6hibu', 'ant-btn-default', 'pagination-link');
            if (!text && pageNumber === currentPage) pageButton.classList.add('current-page');
            pageButton.addEventListener('click', (e) => { e.preventDefault(); const u = new URL(window.location.href); u.searchParams.set('page', pageNumber); window.location.href = u.toString(); });
            return pageButton;
        }
        paginationContainer.appendChild(createPageButton(1, 'First'));
        let startPage = Math.max(1, currentPage - 2);
        let endPage = startPage + 4;
        const assumedTotalPages = 1000;
        if (endPage > assumedTotalPages) { endPage = assumedTotalPages; startPage = Math.max(1, endPage - 4); }
        startPage = Math.max(1, startPage);
        for (let i = startPage; i <= endPage; i++) paginationContainer.appendChild(createPageButton(i));
        const goToContainer = document.createElement('div');
        goToContainer.classList.add('go-to-container');
        const pageInput = document.createElement('input');
        pageInput.type = 'number'; pageInput.placeholder = 'Go...'; pageInput.min = '1'; pageInput.classList.add('pagination-input');
        const goButton = document.createElement('button');
        goButton.type = 'button'; goButton.textContent = 'Go'; goButton.classList.add('ant-btn', 'css-s6hibu', 'ant-btn-default', 'pagination-link');
        const navigateToPage = () => { if (pageInput.value >= 1) { const u = new URL(window.location.href); u.searchParams.set('page', pageInput.value); window.location.href = u.toString(); } };
        goButton.addEventListener('click', navigateToPage);
        pageInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); navigateToPage(); } });
        goToContainer.appendChild(pageInput); goToContainer.appendChild(goButton); paginationContainer.appendChild(goToContainer);
        const nextButton = buttonContainer.querySelector('button:last-child');
        buttonContainer.insertBefore(paginationContainer, nextButton);
    }
    
    function setupGlobalTooltip() {
        const tooltip = document.createElement('div');
        tooltip.id = 'chub-custom-tooltip';
        tooltip.style.display = 'none';
        document.body.appendChild(tooltip);
        document.addEventListener('mouseover', (e) => {
            const target = e.target.closest('.chub-card-title-link');
            if (target && target.dataset.fullTitle) {
                tooltip.textContent = target.dataset.fullTitle;
                tooltip.style.display = 'block';
            }
        });
        document.addEventListener('mouseout', (e) => { if (e.target.closest('.chub-card-title-link')) tooltip.style.display = 'none'; });
        document.addEventListener('mousemove', (e) => {
            if (tooltip.style.display === 'block') {
                const offset = 15;
                let top = e.clientY + offset;
                let left = e.clientX + offset;
                if (left + tooltip.offsetWidth > window.innerWidth) left = window.innerWidth - tooltip.offsetWidth - offset;
                if (top + tooltip.offsetHeight > window.innerHeight) top = e.clientY - tooltip.offsetHeight - offset;
                tooltip.style.top = `${top}px`; tooltip.style.left = `${left}px`;
            }
        });
    }

    let debounceTimer;
    const observer = new MutationObserver(() => {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(processAllEnhancements, 150);
    });
    observer.observe(document.body, { childList: true, subtree: true });
    
    let lastUrl = window.location.href;
    setInterval(() => {
        if (window.location.href !== lastUrl) {
            lastUrl = window.location.href;
            currentProjectId = null;
            commitHistoryCache = null;
            initializeProjectData();
            setTimeout(processAllEnhancements, 150);
        }
    }, 500);

    initializeProjectData();
    setTimeout(() => { processAllEnhancements(); setupGlobalTooltip(); }, 500);

    const styleElement = document.createElement('style');
    styleElement.textContent = `
        .chub-card-title-link { color: white; }
        .chub-card-title-link:visited { color: yellow; }
        #chub-custom-tooltip {
            position: fixed; background-color: #1f1f1f; color: #e5e0d8;
            border: 1px solid #5852a5; padding: 6px 12px; border-radius: 6px;
            font-size: 0.9rem; z-index: 10000; pointer-events: none;
            box-shadow: 0 4px 6px rgba(0,0,0,0.3); max-width: 400px; word-wrap: break-word;
        }
        div.ant-card-meta-description { max-height: none !important; -webkit-mask-image: none !important; mask-image: none !important; }
        .show-more-button-container { display: none !important; }
        .pagination-container { display: flex; align-items: center; gap: 0.5rem; }
        .pagination-container .pagination-link {
            background-color: #141414 !important; border: 1px solid #424242 !important;
            color: rgba(242,228,214,0.85) !important; min-width: 32px; height: 32px;
            padding: 0px 15px !important; line-height: 1.5 !important;
            display: inline-flex; align-items: center; justify-content: center;
            border-radius: 6px !important;
        }
        .pagination-container .pagination-link:not(.current-page):hover {
            background-color: #1f1f1f !important; border-color: #5852a5 !important; color: #5852a5 !important;
        }
        .pagination-container .pagination-link.current-page {
            background-color: #2e2b74 !important; border-color: #5852a5 !important; color: white !important; font-weight: bold;
        }
        .go-to-container { display: flex; gap: 0.25rem; }
        .pagination-input {
            width: 80px; height: 32px; padding: 4px 11px;
            background: #141414; border: 1px solid #424242; border-radius: 6px;
            color: rgba(242,228,214,0.85); font-size: 14px;
        }
        .pagination-input:focus { border-color: #5852a5; outline: none; }
        div.custom-scroll:hover::-webkit-scrollbar { height: 8px !important; }
        div.custom-scroll:hover::-webkit-scrollbar-thumb { background-color: #888 !important; border-radius: 4px !important; }
        div.custom-scroll:hover::-webkit-scrollbar-track { background-color: rgba(50, 50, 50, 0.2) !important; border-radius: 4px !important; }

        /* Diff Modal Styles */
        #chub-diff-modal {
            position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
            background: rgba(0,0,0,0.8); z-index: 20000;
            display: none; align-items: center; justify-content: center;
        }
        .diff-modal-content {
            background: #1f1f1f; width: 90%; max-width: 1000px; max-height: 90%;
            border-radius: 8px; border: 1px solid #424242; display: flex; flex-direction: column;
            color: #e5e0d8; font-family: monospace;
        }
        .diff-modal-header {
            padding: 16px; border-bottom: 1px solid #424242; display: flex; justify-content: space-between; align-items: center; background: #252525;
        }
        .diff-modal-close {
            background: none; border: none; color: #fff; font-size: 24px; cursor: pointer;
        }
        .diff-modal-body {
            padding: 20px; overflow-y: auto; font-size: 0.95rem; line-height: 1.5;
        }
        .diff-field { margin-bottom: 25px; border: 1px solid #444; border-radius: 6px; overflow: hidden; }
        .diff-field h3 {
            margin: 0; padding: 10px 15px; background: #2a2a2a; border-bottom: 1px solid #444;
            color: #5852a5; font-family: sans-serif; font-weight: bold;
        }
        .diff-content { padding: 15px; background: #181818; white-space: pre-wrap; word-wrap: break-word; }
        
        .diff-tags, .lore-entry { display: flex; flex-wrap: wrap; gap: 5px; padding: 10px; background: #181818; }
        .tag { padding: 2px 6px; border-radius: 4px; font-size: 0.85em; font-family: sans-serif; }
        .tag-add { background: #1d421e; color: #a3f7bf; border: 1px solid #2e6b30; }
        .tag-del { background: #5e2a2c; color: #ff9fa1; border: 1px solid #8f3e41; }
        .tag-same { background: #333; color: #888; opacity: 0.6; }
        
        /* Updated Lorebook Entries Styles */
        .lore-entry { display: block; margin-bottom: 10px; border-radius: 4px; padding: 0; border: 1px solid #444; background: #181818; }
        .lore-entry summary { padding: 10px; cursor: pointer; list-style: none; outline: none; }
        .lore-entry summary:hover { background: rgba(255,255,255,0.05); }
        .lore-details-content { padding: 10px; border-top: 1px solid #444; background: #111; white-space: pre-wrap; }
        
        .entry-add { border-left: 4px solid #78e08f; }
        .entry-add summary { color: #78e08f; background: rgba(73, 170, 25, 0.1); }
        
        .entry-del { border-left: 4px solid #ff9fa1; }
        .entry-del summary { color: #ff9fa1; background: rgba(220, 68, 70, 0.1); }

        .entry-mod { border-left: 4px solid #5852a5; padding: 10px; }
        
        .sub-label { color: #888; font-size: 0.8em; text-transform: uppercase; margin-top: 5px; }
        .alt-greet-diff { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px dashed #444; }

        ins { background-color: #1d421e; color: #a3f7bf; text-decoration: none; padding: 1px 2px; border-radius: 2px; }
        del { background-color: #5e2a2c; color: #ff9fa1; text-decoration: line-through; opacity: 0.8; padding: 1px 2px; border-radius: 2px; }
        .diff-no-change { text-align: center; padding: 20px; color: #888; font-style: italic; font-family: sans-serif; }
    `;
    document.head.appendChild(styleElement);
})();