Full suite: Title links, tags, pagination, description fixes, tooltips, and a Smart Version History Diff tool (V2+ Nested Support).
// ==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, "&").replace(/</g, "<").replace(/>/g, ">")
.replace(/"/g, """).replace(/'/g, "'").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);
})();