Better LMArena (lmsys) Chat

Improves LMSYS/LMArena chat interface: cleaner look, removes clutter & startup alerts.

// ==UserScript==
// @name         Better LMArena (lmsys) Chat
// @namespace    https://github.com/insign/userscripts
// @version      202412281434
// @description  Improves LMSYS/LMArena chat interface: cleaner look, removes clutter & startup alerts.
// @match        https://lmarena.ai/*
// @match        https://chat.lmsys.org/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=lmarena.ai
// @author       Hélio <[email protected]>
// @license      WTFPL
// ==/UserScript==

(function() {
	'use strict'

	// --- Bloqueador de Alertas ---
	// Sobrescreve a função window.alert para impedir que alertas pop-up
	// interrompam o usuário, especialmente os que aparecem ao carregar a página.
	const originalAlert = window.alert // Guarda referência ao alert original (opcional)
	window.alert = function(...args) {
		console.log('Blocked alert:', args)
		// Pode-se adicionar lógica aqui, se necessário, ou apenas bloquear.
		// originalAlert.apply(window, args); // Descomente para reativar os alerts originais
	}
	console.log('Better LMArena: Alert blocker active.')

	// --- Utilitários DOM ---
	// Seletores de conveniência
	const $  = document.querySelector.bind(document)
	const $$ = document.querySelectorAll.bind(document)
	// Funções de manipulação de elementos
	const hide   = el => { if (el) el.style.display = 'none' } // Esconde o elemento
	const remove = el => { if (el) el.remove() } // Remove o elemento do DOM
	const click  = el => { if (el) el.click() } // Simula um clique no elemento
	const rename = (el, text) => { if (el) el.textContent = text } // Renomeia o texto do elemento

	/**
	 * Aplica uma função a elementos selecionados repetidamente em intervalos,
	 * mas apenas se uma condição de verificação for atendida. Útil para modificar
	 * elementos que são carregados dinamicamente ou podem mudar de estado.
	 * Otimizado para pausar quando a aba não está visível.
	 *
	 * @param {string|Element|NodeList|Array<string|Element|NodeList>} selector - Seletor(es) CSS, elemento(s) ou NodeList(s).
	 * @param {function(Element): boolean} check - Função que retorna true se a ação deve ser aplicada ao elemento.
	 * @param {function(Element): void} fn - A função a ser executada no elemento se check retornar true.
	 * @param {number} [interval=1000] - Intervalo de verificação em milissegundos.
	 */
	const perma = (selector, check, fn, interval = 1000) => {
		let intervalId = null // Armazena o ID do intervalo para poder pará-lo

		// Função que verifica e executa a ação nos elementos encontrados
		const checkAndExecute = () => {
			let elements = [] // Array para armazenar os elementos encontrados

			// Normaliza o(s) seletor(es) para um array de elementos
			const selectors = Array.isArray(selector) ? selector : [selector]
			selectors.forEach(item => {
				if (typeof item === 'string') {
					elements = elements.concat(Array.from($$(item))) // Seleciona por string CSS
				} else if (item instanceof Element) {
					elements.push(item) // Adiciona elemento diretamente
				} else if (item instanceof NodeList) {
					elements = elements.concat(Array.from(item)) // Adiciona elementos de NodeList
				}
			})

			// Itera sobre os elementos encontrados e aplica a lógica
			elements.forEach(element => {
				try {
					// Verifica a condição e executa a função se for verdadeira
					if (element && check(element)) {
						fn(element)
					}
				} catch (error) {
					console.warn(`Better LMArena: Error in perma check/fn for selector "${selector}":`, error, element)
					stopInterval() // Para o intervalo em caso de erro para evitar spam no console
				}
			})
		}

		// Inicia o intervalo de verificação
		const startInterval = () => {
			if (!intervalId) { // Evita múltiplos intervalos rodando
				checkAndExecute() // Executa imediatamente uma vez
				intervalId = setInterval(checkAndExecute, interval)
				// console.log(`Better LMArena: Perma interval started for selector "${selector}"`)
			}
		}

		// Para o intervalo de verificação
		const stopInterval = () => {
			if (intervalId) {
				clearInterval(intervalId)
				intervalId = null
				// console.log(`Better LMArena: Perma interval stopped for selector "${selector}"`)
			}
		}

		// Ouve mudanças na visibilidade da aba para pausar/retomar o intervalo
		document.addEventListener('visibilitychange', () => {
			if (document.hidden) {
				stopInterval() // Pausa quando a aba fica oculta
			} else {
				startInterval() // Retoma quando a aba fica visível
			}
		})

		// Inicia o intervalo assim que a função é chamada
		startInterval()
	}

	/**
	 * Espera que um ou mais elementos existam no DOM e então executa um callback.
	 * Usa MutationObserver para eficiência, evitando polling constante.
	 *
	 * @param {string|Element|NodeList|Array<string|Element|NodeList|function(): Element|null>} selectors - Seletor(es) CSS, elemento(s), NodeList(s) ou função(ões) que retornam um elemento.
	 * @param {function(Element): void} [callback=null] - Função a ser executada quando o primeiro elemento for encontrado.
	 * @param {number} [slow=0] - Atraso opcional (ms) antes de executar o callback.
	 * @returns {Promise<void>} - Promessa que resolve quando o elemento é encontrado e o callback executado.
	 */
	const when = (selectors = ['html'], callback = null, slow = 0) => {
		// Garante que selectors seja um array
		const selectorArray = Array.isArray(selectors) ? selectors : [selectors]

		return new Promise((resolve) => {
			// Função para executar o callback (com ou sem atraso)
			const executeCallback = (element) => {
				const execute = () => {
					if (callback) {
						try {
							callback(element)
						} catch (error) {
							console.error(`Better LMArena: Error in 'when' callback for selector "${selectors}":`, error, element)
						}
					}
					resolve() // Resolve a promessa
				}

				if (slow > 0) {
					setTimeout(execute, slow)
				} else {
					execute()
				}
			}

			// Verifica se algum dos seletores já corresponde a um elemento no DOM
			const checkSelectors = () => {
				for (const selector of selectorArray) {
					let element = null
					if (typeof selector === 'string') {
						element = $(selector) // Busca por seletor CSS
					} else if (selector instanceof Element || selector instanceof NodeList && selector.length > 0) {
						element = (selector instanceof NodeList) ? selector[0] : selector // Usa elemento ou primeiro de NodeList
					} else if (typeof selector === 'function') {
						try {
							element = selector() // Executa função para obter elemento
						} catch (error) {
							console.warn(`Better LMArena: Error executing selector function in 'when':`, error)
							continue // Pula para o próximo seletor em caso de erro na função
						}
					}

					// Se encontrou um elemento, executa o callback e retorna true
					if (element) {
						executeCallback(element)
						return true
					}
				}
				return false // Nenhum elemento encontrado ainda
			}

			// Se o elemento já existe, executa o callback e retorna
			if (checkSelectors()) {
				return
			}

			// Se não encontrou, configura um MutationObserver para observar adições ao DOM
			const observer = new MutationObserver((mutations) => {
				// Otimização: Verifica apenas se nós foram adicionados
				const nodesAdded = mutations.some(mutation => mutation.addedNodes.length > 0)
				if (nodesAdded) {
					// Se algum nó foi adicionado, verifica novamente os seletores
					if (checkSelectors()) {
						observer.disconnect() // Para de observar assim que encontrar
					}
				}
			})

			// Começa a observar o body e seus descendentes
			observer.observe(document.body || document.documentElement, { childList: true, subtree: true })
			// console.log(`Better LMArena: Waiting for selector(s):`, selectors)
		})
	}


	// --- Modificações Específicas LMArena ---

	// Renomeia os botões das abas principais para nomes mais curtos ou descritivos
	// Usa 'perma' porque os elementos podem ser recriados ou ter o texto alterado pela aplicação.
	// A função 'check' garante que a renomeação ocorra apenas uma vez por estado.
	perma('#component-18-button', el => el.textContent !== 'Battle', el => rename(el, 'Battle'), 500)
	perma('#component-63-button', el => el.textContent !== 'Side-by-Side', el => rename(el, 'Side-by-Side'), 500)
	perma('#component-107-button', el => el.textContent !== 'Chat', el => rename(el, 'Chat'), 500)
	perma('#component-108-button', el => el.textContent !== 'Vision Chat', el => rename(el, 'Vision Chat'), 500) // Pode não existir mais
	perma('#component-140-button', el => el.textContent !== 'Ranking', el => rename(el, 'Ranking'), 500)
	perma('#component-231-button', el => el.textContent !== 'About', el => rename(el, 'About'), 500)

	// Remove blocos de texto/aviso e termos de serviço que ocupam espaço inicial
	// Usa 'when' porque esses elementos geralmente aparecem uma vez ao carregar a aba.
	when([
		// Bloco de aviso no topo (o seletor pode mudar com atualizações do Gradio/LMSYS)
		() => $('gradio-app > .main > .wrap > .tabs > .tabitem > .gap > #notice_markdown'),
		// Blocos de Termos de Serviço (ToS) em diferentes abas
		() => $('#component-26 > .gap > .hide-container.block'), // ToS - Battle
		() => $('#component-139 > .gap > .hide-container.block'),// ToS - Chat? (Verificar ID)
		() => $('#component-95 > .gap > .hide-container.block'), // ToS - Side-by-Side? (Verificar ID)
		// Bloco de markdown no topo do Leaderboard
		() => $('#leaderboard_markdown > .svelte-1ed2p3z > .svelte-gq7qsu.prose'),
	], remove, 50) // Pequeno delay para garantir que o elemento exista

	// Remove outros elementos de texto/botões menos úteis (IDs podem mudar)
	// Tenta remover o botão "About" e alguns outros componentes (potencialmente spacers ou text blocks).
	when([
		'#component-151-button', // Botão "About"? Verificar se é o mesmo que #component-231
		// IDs abaixo podem corresponder a blocos de texto/markdown ou spacers. Verificar no inspetor.
		'#component-54',
		'#component-87',
		'#component-114',
		'#component-11',
	], remove, 100).then(() => {
		console.log('Better LMArena: Cleaned up initial text blocks and buttons.')

		// Ajusta o padding dos botões das abas após a remoção de outros elementos
		perma('.tab-nav button', el => el.style.padding !== 'var(--size-1) var(--size-3)', el => {
			el.style.padding = 'var(--size-1) var(--size-3)'
		}, 500)

		// Remove padding e borda dos containers das abas
		perma('.tabitem', el => el.style.padding !== '0px' || el.style.borderWidth !== '0px', el => {
			el.style.padding = '0'
			el.style.border  = '0'
		}, 500)
	})

	// Ajusta o layout principal da aplicação para ocupar mais espaço horizontal
	when('.app', el => {
		el.style.margin   = '0 auto' // Mantém centralizado
		el.style.maxWidth = '100%' // Largura total
		el.style.padding  = '0'    // Remove padding externo
	}, 50)

	// Centraliza a barra de navegação das abas
	when('.tab-nav', el => {
		el.style.display   = 'flex' // Usar flex para centralizar
		el.style.justifyContent = 'center' // Centraliza os botões horizontalmente
		el.style.gap = 'var(--spacing-lg)' // Adiciona um espaço entre os botões
	}, 50)

	// Ajusta a altura do chatbot para ocupar mais espaço vertical
	perma('#chatbot', el => el.style.height !== 'calc(80vh - 50px)', el => { // Ajuste dinâmico da altura
		el.style.height = 'calc(80vh - 50px)' // Ex: 80% da altura da viewport menos espaço para input/header
	}, 1000)

	// Reduz o espaçamento geral entre elementos (gap)
	perma('.gap', el => el.style.gap !== 'var(--spacing-sm)', el => { // Usa um espaçamento menor
		el.style.gap = 'var(--spacing-sm)' // Ex: 6px ou var(--spacing-sm)
	}, 1000)

	// Remove o arredondamento das bordas (estilo mais quadrado)
	perma(['button', 'textarea', '.gradio-textbox', '.block'], el => el.style.borderRadius !== '0px', el => {
		el.style.borderRadius = '0px'
	}, 1000)

	// Ajusta a caixa de input (remove bordas, padding, arredondamento)
	perma('#input_box', el => {
		let changed = false
		if (el.style.borderWidth !== '0px') { el.style.borderWidth = '0px'; changed = true }
		if (el.style.padding !== '0px') { el.style.padding = '0px'; changed = true }
		// Aplica ao pai também se necessário (alguns estilos podem estar no container)
		if (el.parentNode && el.parentNode.style.borderWidth !== '0px') { el.parentNode.style.borderWidth = '0px'; el.parentNode.style.borderRadius = '0px'; changed = true }
		// Aplica ao textarea filho
		const textarea = el.querySelector('textarea')
		if (textarea && textarea.style.borderRadius !== '0px') { textarea.style.borderRadius = '0px'; changed = true }
		return changed // Retorna true apenas se algo mudou
	}, el => el, 1000) // Condição de check simplificada, a lógica está na função fn

	// Renomeia e estiliza os botões de envio/regenerate/stop
	perma('.submit-button', el => el.textContent !== '⤴️', el => {
		el.style.minWidth = '40px' // Largura mínima
		el.textContent    = '⤴️'  // Ícone de envio
		el.style.padding  = 'var(--size-1) var(--size-1)' // Padding menor
	}, 500)
	// Outros botões podem ter classes diferentes (ex: .generate-button, .stop-button)
	// Adicionar 'perma' para eles se necessário.

	// Remove borda e arredondamento da área de compartilhamento
	perma('#share-region-named', el => el.style.borderWidth !== '0px', el => {
		el.style.border       = '0'
		el.style.borderRadius = '0'
	}, 1000)

	// Ajusta espaçamento em containers específicos do Svelte (se aplicável)
	perma('.svelte-15lo0d8', el => el.style.gap !== 'var(--spacing-md)', el => {
		el.style.gap = 'var(--spacing-md)'
	}, 1000)

	// Remove o link "Built with Gradio" no rodapé
	when('.built-with', remove, 1000) // Atraso maior pois pode carregar por último

	// Lógica específica: Clica automaticamente em "Direct Chat" se o pop-up "Model B" aparecer
	// O seletor '.svelte-nab2ao' pode ser específico de um componente modal que aparece.
	// É necessário verificar se esse seletor ainda é válido.
	when('.svelte-nab2ao', () => {
		console.log('Better LMArena: Detected Model B selection prompt.')
		// Espera um pouco para garantir que o botão esteja pronto e clica nele
		setTimeout(() => {
			const directChatButton = $('#component-123-button') // ID pode ter mudado
			if (directChatButton) {
				console.log('Better LMArena: Clicking "Direct Chat" button.')
				click(directChatButton)
			} else {
				console.warn('Better LMArena: "Direct Chat" button (#component-123-button) not found.')
			}
		}, 500) // Atraso para garantir que o botão esteja interativo
	}, 500)

	console.log('Better LMArena script loaded and running.')
})()