Better LMArena (lmsys) Chat

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

  1. // ==UserScript==
  2. // @name Better LMArena (lmsys) Chat
  3. // @namespace https://github.com/insign/userscripts
  4. // @version 202412281434
  5. // @description Improves LMSYS/LMArena chat interface: cleaner look, removes clutter & startup alerts.
  6. // @match https://lmarena.ai/*
  7. // @match https://chat.lmsys.org/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=lmarena.ai
  9. // @author Hélio <open@helio.me>
  10. // @license WTFPL
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict'
  15.  
  16. // --- Bloqueador de Alertas ---
  17. // Sobrescreve a função window.alert para impedir que alertas pop-up
  18. // interrompam o usuário, especialmente os que aparecem ao carregar a página.
  19. const originalAlert = window.alert // Guarda referência ao alert original (opcional)
  20. window.alert = function(...args) {
  21. console.log('Blocked alert:', args)
  22. // Pode-se adicionar lógica aqui, se necessário, ou apenas bloquear.
  23. // originalAlert.apply(window, args); // Descomente para reativar os alerts originais
  24. }
  25. console.log('Better LMArena: Alert blocker active.')
  26.  
  27. // --- Utilitários DOM ---
  28. // Seletores de conveniência
  29. const $ = document.querySelector.bind(document)
  30. const $$ = document.querySelectorAll.bind(document)
  31. // Funções de manipulação de elementos
  32. const hide = el => { if (el) el.style.display = 'none' } // Esconde o elemento
  33. const remove = el => { if (el) el.remove() } // Remove o elemento do DOM
  34. const click = el => { if (el) el.click() } // Simula um clique no elemento
  35. const rename = (el, text) => { if (el) el.textContent = text } // Renomeia o texto do elemento
  36.  
  37. /**
  38. * Aplica uma função a elementos selecionados repetidamente em intervalos,
  39. * mas apenas se uma condição de verificação for atendida. Útil para modificar
  40. * elementos que são carregados dinamicamente ou podem mudar de estado.
  41. * Otimizado para pausar quando a aba não está visível.
  42. *
  43. * @param {string|Element|NodeList|Array<string|Element|NodeList>} selector - Seletor(es) CSS, elemento(s) ou NodeList(s).
  44. * @param {function(Element): boolean} check - Função que retorna true se a ação deve ser aplicada ao elemento.
  45. * @param {function(Element): void} fn - A função a ser executada no elemento se check retornar true.
  46. * @param {number} [interval=1000] - Intervalo de verificação em milissegundos.
  47. */
  48. const perma = (selector, check, fn, interval = 1000) => {
  49. let intervalId = null // Armazena o ID do intervalo para poder pará-lo
  50.  
  51. // Função que verifica e executa a ação nos elementos encontrados
  52. const checkAndExecute = () => {
  53. let elements = [] // Array para armazenar os elementos encontrados
  54.  
  55. // Normaliza o(s) seletor(es) para um array de elementos
  56. const selectors = Array.isArray(selector) ? selector : [selector]
  57. selectors.forEach(item => {
  58. if (typeof item === 'string') {
  59. elements = elements.concat(Array.from($$(item))) // Seleciona por string CSS
  60. } else if (item instanceof Element) {
  61. elements.push(item) // Adiciona elemento diretamente
  62. } else if (item instanceof NodeList) {
  63. elements = elements.concat(Array.from(item)) // Adiciona elementos de NodeList
  64. }
  65. })
  66.  
  67. // Itera sobre os elementos encontrados e aplica a lógica
  68. elements.forEach(element => {
  69. try {
  70. // Verifica a condição e executa a função se for verdadeira
  71. if (element && check(element)) {
  72. fn(element)
  73. }
  74. } catch (error) {
  75. console.warn(`Better LMArena: Error in perma check/fn for selector "${selector}":`, error, element)
  76. stopInterval() // Para o intervalo em caso de erro para evitar spam no console
  77. }
  78. })
  79. }
  80.  
  81. // Inicia o intervalo de verificação
  82. const startInterval = () => {
  83. if (!intervalId) { // Evita múltiplos intervalos rodando
  84. checkAndExecute() // Executa imediatamente uma vez
  85. intervalId = setInterval(checkAndExecute, interval)
  86. // console.log(`Better LMArena: Perma interval started for selector "${selector}"`)
  87. }
  88. }
  89.  
  90. // Para o intervalo de verificação
  91. const stopInterval = () => {
  92. if (intervalId) {
  93. clearInterval(intervalId)
  94. intervalId = null
  95. // console.log(`Better LMArena: Perma interval stopped for selector "${selector}"`)
  96. }
  97. }
  98.  
  99. // Ouve mudanças na visibilidade da aba para pausar/retomar o intervalo
  100. document.addEventListener('visibilitychange', () => {
  101. if (document.hidden) {
  102. stopInterval() // Pausa quando a aba fica oculta
  103. } else {
  104. startInterval() // Retoma quando a aba fica visível
  105. }
  106. })
  107.  
  108. // Inicia o intervalo assim que a função é chamada
  109. startInterval()
  110. }
  111.  
  112. /**
  113. * Espera que um ou mais elementos existam no DOM e então executa um callback.
  114. * Usa MutationObserver para eficiência, evitando polling constante.
  115. *
  116. * @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.
  117. * @param {function(Element): void} [callback=null] - Função a ser executada quando o primeiro elemento for encontrado.
  118. * @param {number} [slow=0] - Atraso opcional (ms) antes de executar o callback.
  119. * @returns {Promise<void>} - Promessa que resolve quando o elemento é encontrado e o callback executado.
  120. */
  121. const when = (selectors = ['html'], callback = null, slow = 0) => {
  122. // Garante que selectors seja um array
  123. const selectorArray = Array.isArray(selectors) ? selectors : [selectors]
  124.  
  125. return new Promise((resolve) => {
  126. // Função para executar o callback (com ou sem atraso)
  127. const executeCallback = (element) => {
  128. const execute = () => {
  129. if (callback) {
  130. try {
  131. callback(element)
  132. } catch (error) {
  133. console.error(`Better LMArena: Error in 'when' callback for selector "${selectors}":`, error, element)
  134. }
  135. }
  136. resolve() // Resolve a promessa
  137. }
  138.  
  139. if (slow > 0) {
  140. setTimeout(execute, slow)
  141. } else {
  142. execute()
  143. }
  144. }
  145.  
  146. // Verifica se algum dos seletores já corresponde a um elemento no DOM
  147. const checkSelectors = () => {
  148. for (const selector of selectorArray) {
  149. let element = null
  150. if (typeof selector === 'string') {
  151. element = $(selector) // Busca por seletor CSS
  152. } else if (selector instanceof Element || selector instanceof NodeList && selector.length > 0) {
  153. element = (selector instanceof NodeList) ? selector[0] : selector // Usa elemento ou primeiro de NodeList
  154. } else if (typeof selector === 'function') {
  155. try {
  156. element = selector() // Executa função para obter elemento
  157. } catch (error) {
  158. console.warn(`Better LMArena: Error executing selector function in 'when':`, error)
  159. continue // Pula para o próximo seletor em caso de erro na função
  160. }
  161. }
  162.  
  163. // Se encontrou um elemento, executa o callback e retorna true
  164. if (element) {
  165. executeCallback(element)
  166. return true
  167. }
  168. }
  169. return false // Nenhum elemento encontrado ainda
  170. }
  171.  
  172. // Se o elemento já existe, executa o callback e retorna
  173. if (checkSelectors()) {
  174. return
  175. }
  176.  
  177. // Se não encontrou, configura um MutationObserver para observar adições ao DOM
  178. const observer = new MutationObserver((mutations) => {
  179. // Otimização: Verifica apenas se nós foram adicionados
  180. const nodesAdded = mutations.some(mutation => mutation.addedNodes.length > 0)
  181. if (nodesAdded) {
  182. // Se algum nó foi adicionado, verifica novamente os seletores
  183. if (checkSelectors()) {
  184. observer.disconnect() // Para de observar assim que encontrar
  185. }
  186. }
  187. })
  188.  
  189. // Começa a observar o body e seus descendentes
  190. observer.observe(document.body || document.documentElement, { childList: true, subtree: true })
  191. // console.log(`Better LMArena: Waiting for selector(s):`, selectors)
  192. })
  193. }
  194.  
  195.  
  196. // --- Modificações Específicas LMArena ---
  197.  
  198. // Renomeia os botões das abas principais para nomes mais curtos ou descritivos
  199. // Usa 'perma' porque os elementos podem ser recriados ou ter o texto alterado pela aplicação.
  200. // A função 'check' garante que a renomeação ocorra apenas uma vez por estado.
  201. perma('#component-18-button', el => el.textContent !== 'Battle', el => rename(el, 'Battle'), 500)
  202. perma('#component-63-button', el => el.textContent !== 'Side-by-Side', el => rename(el, 'Side-by-Side'), 500)
  203. perma('#component-107-button', el => el.textContent !== 'Chat', el => rename(el, 'Chat'), 500)
  204. perma('#component-108-button', el => el.textContent !== 'Vision Chat', el => rename(el, 'Vision Chat'), 500) // Pode não existir mais
  205. perma('#component-140-button', el => el.textContent !== 'Ranking', el => rename(el, 'Ranking'), 500)
  206. perma('#component-231-button', el => el.textContent !== 'About', el => rename(el, 'About'), 500)
  207.  
  208. // Remove blocos de texto/aviso e termos de serviço que ocupam espaço inicial
  209. // Usa 'when' porque esses elementos geralmente aparecem uma vez ao carregar a aba.
  210. when([
  211. // Bloco de aviso no topo (o seletor pode mudar com atualizações do Gradio/LMSYS)
  212. () => $('gradio-app > .main > .wrap > .tabs > .tabitem > .gap > #notice_markdown'),
  213. // Blocos de Termos de Serviço (ToS) em diferentes abas
  214. () => $('#component-26 > .gap > .hide-container.block'), // ToS - Battle
  215. () => $('#component-139 > .gap > .hide-container.block'),// ToS - Chat? (Verificar ID)
  216. () => $('#component-95 > .gap > .hide-container.block'), // ToS - Side-by-Side? (Verificar ID)
  217. // Bloco de markdown no topo do Leaderboard
  218. () => $('#leaderboard_markdown > .svelte-1ed2p3z > .svelte-gq7qsu.prose'),
  219. ], remove, 50) // Pequeno delay para garantir que o elemento exista
  220.  
  221. // Remove outros elementos de texto/botões menos úteis (IDs podem mudar)
  222. // Tenta remover o botão "About" e alguns outros componentes (potencialmente spacers ou text blocks).
  223. when([
  224. '#component-151-button', // Botão "About"? Verificar se é o mesmo que #component-231
  225. // IDs abaixo podem corresponder a blocos de texto/markdown ou spacers. Verificar no inspetor.
  226. '#component-54',
  227. '#component-87',
  228. '#component-114',
  229. '#component-11',
  230. ], remove, 100).then(() => {
  231. console.log('Better LMArena: Cleaned up initial text blocks and buttons.')
  232.  
  233. // Ajusta o padding dos botões das abas após a remoção de outros elementos
  234. perma('.tab-nav button', el => el.style.padding !== 'var(--size-1) var(--size-3)', el => {
  235. el.style.padding = 'var(--size-1) var(--size-3)'
  236. }, 500)
  237.  
  238. // Remove padding e borda dos containers das abas
  239. perma('.tabitem', el => el.style.padding !== '0px' || el.style.borderWidth !== '0px', el => {
  240. el.style.padding = '0'
  241. el.style.border = '0'
  242. }, 500)
  243. })
  244.  
  245. // Ajusta o layout principal da aplicação para ocupar mais espaço horizontal
  246. when('.app', el => {
  247. el.style.margin = '0 auto' // Mantém centralizado
  248. el.style.maxWidth = '100%' // Largura total
  249. el.style.padding = '0' // Remove padding externo
  250. }, 50)
  251.  
  252. // Centraliza a barra de navegação das abas
  253. when('.tab-nav', el => {
  254. el.style.display = 'flex' // Usar flex para centralizar
  255. el.style.justifyContent = 'center' // Centraliza os botões horizontalmente
  256. el.style.gap = 'var(--spacing-lg)' // Adiciona um espaço entre os botões
  257. }, 50)
  258.  
  259. // Ajusta a altura do chatbot para ocupar mais espaço vertical
  260. perma('#chatbot', el => el.style.height !== 'calc(80vh - 50px)', el => { // Ajuste dinâmico da altura
  261. el.style.height = 'calc(80vh - 50px)' // Ex: 80% da altura da viewport menos espaço para input/header
  262. }, 1000)
  263.  
  264. // Reduz o espaçamento geral entre elementos (gap)
  265. perma('.gap', el => el.style.gap !== 'var(--spacing-sm)', el => { // Usa um espaçamento menor
  266. el.style.gap = 'var(--spacing-sm)' // Ex: 6px ou var(--spacing-sm)
  267. }, 1000)
  268.  
  269. // Remove o arredondamento das bordas (estilo mais quadrado)
  270. perma(['button', 'textarea', '.gradio-textbox', '.block'], el => el.style.borderRadius !== '0px', el => {
  271. el.style.borderRadius = '0px'
  272. }, 1000)
  273.  
  274. // Ajusta a caixa de input (remove bordas, padding, arredondamento)
  275. perma('#input_box', el => {
  276. let changed = false
  277. if (el.style.borderWidth !== '0px') { el.style.borderWidth = '0px'; changed = true }
  278. if (el.style.padding !== '0px') { el.style.padding = '0px'; changed = true }
  279. // Aplica ao pai também se necessário (alguns estilos podem estar no container)
  280. if (el.parentNode && el.parentNode.style.borderWidth !== '0px') { el.parentNode.style.borderWidth = '0px'; el.parentNode.style.borderRadius = '0px'; changed = true }
  281. // Aplica ao textarea filho
  282. const textarea = el.querySelector('textarea')
  283. if (textarea && textarea.style.borderRadius !== '0px') { textarea.style.borderRadius = '0px'; changed = true }
  284. return changed // Retorna true apenas se algo mudou
  285. }, el => el, 1000) // Condição de check simplificada, a lógica está na função fn
  286.  
  287. // Renomeia e estiliza os botões de envio/regenerate/stop
  288. perma('.submit-button', el => el.textContent !== '⤴️', el => {
  289. el.style.minWidth = '40px' // Largura mínima
  290. el.textContent = '⤴️' // Ícone de envio
  291. el.style.padding = 'var(--size-1) var(--size-1)' // Padding menor
  292. }, 500)
  293. // Outros botões podem ter classes diferentes (ex: .generate-button, .stop-button)
  294. // Adicionar 'perma' para eles se necessário.
  295.  
  296. // Remove borda e arredondamento da área de compartilhamento
  297. perma('#share-region-named', el => el.style.borderWidth !== '0px', el => {
  298. el.style.border = '0'
  299. el.style.borderRadius = '0'
  300. }, 1000)
  301.  
  302. // Ajusta espaçamento em containers específicos do Svelte (se aplicável)
  303. perma('.svelte-15lo0d8', el => el.style.gap !== 'var(--spacing-md)', el => {
  304. el.style.gap = 'var(--spacing-md)'
  305. }, 1000)
  306.  
  307. // Remove o link "Built with Gradio" no rodapé
  308. when('.built-with', remove, 1000) // Atraso maior pois pode carregar por último
  309.  
  310. // Lógica específica: Clica automaticamente em "Direct Chat" se o pop-up "Model B" aparecer
  311. // O seletor '.svelte-nab2ao' pode ser específico de um componente modal que aparece.
  312. // É necessário verificar se esse seletor ainda é válido.
  313. when('.svelte-nab2ao', () => {
  314. console.log('Better LMArena: Detected Model B selection prompt.')
  315. // Espera um pouco para garantir que o botão esteja pronto e clica nele
  316. setTimeout(() => {
  317. const directChatButton = $('#component-123-button') // ID pode ter mudado
  318. if (directChatButton) {
  319. console.log('Better LMArena: Clicking "Direct Chat" button.')
  320. click(directChatButton)
  321. } else {
  322. console.warn('Better LMArena: "Direct Chat" button (#component-123-button) not found.')
  323. }
  324. }, 500) // Atraso para garantir que o botão esteja interativo
  325. }, 500)
  326.  
  327. console.log('Better LMArena script loaded and running.')
  328. })()