// ==UserScript==
// @name Better LMarena (lmsys) Chat
// @namespace https://github.com/insign/better-lmsys-chat
// @version 202412221856
// @description make chat lmarena (lmsys) chat better and clean
// @match https://lmarena.ai/*
// @match https://chat.lmsys.org/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=lmarena.ai
// @author Hélio <open@helio.me>
// @license WTFPL
// ==/UserScript==
(function() {
'use strict'
const $ = document.querySelector.bind(document)
const $$ = document.querySelectorAll.bind(document)
const hide = el => el.style.display = 'none'
const remove = el => el.remove()
const click = el => el.click()
const rename = (el, text) => el.textContent = text
/**
* Executes a function on selected elements repeatedly until the condition is met.
*
* @param {string|Element|NodeList|Array<string|Element|NodeList>} selector - The CSS selector, element, NodeList, or an array containing a combination of these types to select the elements.
* @param {function(Element): boolean} check - A function that checks if the condition is met for an element. It should return `true` if the condition is met and `false` otherwise.
* @param {function(Element): void} fn - The function to be executed on each element that meets the condition.
* @param {number} [interval=50] - The interval in milliseconds between each check of the elements.
*
* @example
* // Example usage with CSS selector
* perma('.example-class', (el) => el.textContent !== 'Example', (el) => {
* el.textContent = 'Example';
* });
*
* @example
* // Example usage with element
* const element = document.querySelector('#example-id');
* perma(element, (el) => el.classList.contains('active'), (el) => {
* el.classList.add('active');
* });
*
* @example
* // Example usage with NodeList
* const elements = document.querySelectorAll('.example-class');
* perma(elements, (el) => el.dataset.status !== 'ready', (el) => {
* el.dataset.status = 'ready';
* });
*
* @example
* // Example usage with an array of selectors
* perma(['.example-class', '#example-id'], (el) => el.style.display !== 'none', (el) => {
* el.style.display = 'none';
* });
*/
const perma = (selector, check, fn, interval = 1000) => {
let intervalId = null
const checkAndExecute = () => {
let elements = []
if (Array.isArray(selector)) {
selector.forEach((item) => {
if (typeof item === 'string') {
elements = elements.concat(Array.from($$(item)))
}
else if (item instanceof Element) {
elements.push(item)
}
else if (item instanceof NodeList) {
elements = elements.concat(Array.from(item))
}
})
}
else if (typeof selector === 'string') {
elements = $$(selector)
}
else if (selector instanceof Element) {
elements = [ selector ]
}
else if (selector instanceof NodeList) {
elements = Array.from(selector)
}
elements.forEach((element) => {
if (check(element)) {
fn(element)
}
})
}
const startInterval = () => {
if (!intervalId) {
intervalId = setInterval(checkAndExecute, interval)
}
}
const stopInterval = () => {
if (intervalId) {
clearInterval(intervalId)
intervalId = null
}
}
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
stopInterval()
}
else {
startInterval()
}
})
startInterval() // Start initially
}
/**
* Waits for specific elements to be present in the DOM and executes a callback function when they are found.
*
* @param {string|Element|NodeList|Array<string|Element|NodeList|function(): Element|null>} [selectors=['html']] - The CSS selector(s), element(s), NodeList(s), or array function(s) that return an element or null. Can be a single selector, element, NodeList, array function, or an array containing a combination of these types. Defaults to ['html'] if not provided.
* @param {function(Element): void} [callback=null] - The callback function to be executed when the specified elements are found. It receives the first found element as an argument. If not provided, the function will resolve without executing a callback.
* @param {number} [slow=0] - The delay in milliseconds before executing the callback function. If set to 0 (default), the callback will be executed immediately after the elements are found.
*
* @returns {Promise<void>} A promise that resolves when the specified elements are found and the callback function (if provided) has been executed.
*
* @example
* // Example usage with a single CSS selector
* when('.example-class', (element) => {
* console.log('Element found:', element);
* });
*
* @example
* // Example usage with multiple CSS selectors
* when(['.example-class', '#example-id'], (element) => {
* console.log('Element found:', element);
* });
*
* @example
* // Example usage with an array function
* when(() => document.querySelector('.example-class'), (element) => {
* console.log('Element found:', element);
* });
*
* @example
* // Example usage with a delay before executing the callback
* when('.example-class', (element) => {
* console.log('Element found:', element);
* }, 1000);
*
* @example
* // Example usage without a callback function
* when('.example-class').then(() => {
* console.log('Element found');
* });
*/
const when = (selectors = [ 'html' ], callback = null, slow = 0) => {
if (!Array.isArray(selectors)) {
selectors = [ selectors ]
}
return new Promise((resolve) => {
const executeCallback = (element) => {
if (callback) {
if (slow > 0) {
setTimeout(() => {
callback(element)
resolve()
}, slow)
}
else {
callback(element)
resolve()
}
}
else {
resolve()
}
}
const checkSelectors = () => {
for (const selector of selectors) {
let element = null
if (typeof selector === 'string') {
element = $(selector)
}
else if (selector instanceof Element) {
element = selector
}
else if (selector instanceof NodeList) {
element = selector[0]
}
else if (typeof selector === 'function') {
element = selector()
if (element === null) {
continue
}
}
if (element) {
executeCallback(element)
return true
}
}
return false
}
if (checkSelectors()) {
return
}
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
Array.from(mutation.addedNodes).forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (checkSelectors()) {
observer.disconnect()
}
}
})
})
})
observer.observe(document.body, { childList: true, subtree: true })
})
}
// when('.prose', el => !el.closest('.wrap'), remove)
perma('#component-18-button', el => el.textContent !== 'Battle', el => rename(el, 'Battle'), 100)
perma('#component-63-button', el => el.textContent !== 'SbS', el => rename(el, 'Side-by-Side'), 100)
perma('#component-107-button', el => el.textContent !== 'Chat', el => rename(el, 'Chat'), 100)
perma('#component-108-button', el => el.textContent !== 'Vision Chat', el => rename(el, 'Vision Chat'), 100)
perma('#component-140-button', el => el.textContent !== 'Ranking', el => rename(el, 'Ranking'), 100)
perma('#component-231-button', el => el.textContent !== 'About', el => rename(el, 'About'), 100)
// ###notice_markdown > .svelte-1ed2p3z > .svelte-gq7qsu.prose > .prose.svelte-8tpqd2.md
when([
'#notice_markdown > .svelte-1ed2p3z > .svelte-gq7qsu.prose > .prose.svelte-8tpqd2.md', // top blocks of notice
'#component-26 > .gap.svelte-vt1mxs > .hide-container.padded.svelte-12cmxck.block', // ToS
'#component-139 > .gap.svelte-vt1mxs > .hide-container.padded.svelte-12cmxck.block', // ToS
'#component-95 > .gap.svelte-vt1mxs > .hide-container.padded.svelte-12cmxck.block', // ToS
'#leaderboard_markdown > .svelte-1ed2p3z > .svelte-gq7qsu.prose > .prose.svelte-8tpqd2.md', // top blocks of leaderboard
], remove)
when([
'#component-151-button', '#component-54', '#component-87', '#component-114', '#component-11',
], remove).then(() => { // all texts and about button :(
perma('.tab-nav button', el => el.style.padding !== 'var(--size-1) var(--size-3)', el => {
console.info('padding', el.style.padding)
el.style.padding = 'var(--size-1) var(--size-3)'
}, 100)
perma('.tabitem', el => el.style.padding !== '0px', el => {
console.info(el.style.padding)
el.style.padding = 0
el.style.border = 0
})
})
when('.app', el => {
el.style.margin = '0 auto'
el.style.maxWidth = '100%'
el.style.padding = 0
})
when('.tab-nav', el => {
el.style.display = 'flow'
el.style.textAlign = 'center'
})
perma('#chatbot', el => el.style.height !== '75vh', el => {
console.info('height', el.style.height)
el.style.height = '75vh'
})
perma('.gap', el => el.style.gap !== '6px', el => {
console.info('gap', el.style.gap)
el.style.gap = '6px'
})
// no-radius
perma([ 'button' ], el => el.style.borderRadius !== '0px', el => {
console.info('border-radius', el.style.borderRadius)
el.style.borderRadius = 0
})
perma('#input_box', el => el.style.border !== '0px', el => {
console.info('Found input_box parent')
el.style.border = 0
el.style.padding = 0
el.parentNode.style.border = 0
el.parentNode.style.borderRadius = 0
// run on the child textarea
el.querySelector('textarea').style.borderRadius = 0
})
// buttons send, 1123
perma([
'.submit-button',
], el => el.style.minWidth !== '35px', el => {
console.info('buttons send', el.style.minWidth)
el.style.minWidth = '35px'
el.textContent = '⤴️'
})
perma('#share-region-named', el => el.style.border !== '0px', el => {
el.style.border = 0
el.style.borderRadius = 0
})
// gapper
perma('.svelte-15lo0d8', el => el.style.gap !== 'var(--spacing-md)', el => {
console.info('gap', el.style.gap)
el.style.gap = 'var(--spacing-md)'
})
when('.built-with', remove, 1000)
// When appears "Model B" clicks on "Direct Chat" - I needed to use another setTimeout inside
when('.svelte-nab2ao', () => setTimeout(() => $('#component-123-button').click(), 1000))
})()