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