Better LMArena (lmsys) Chat

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

当前为 2025-05-01 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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.')
})()