Better LMarena (lmsys) Chat

make chat lmarena (lmsys) chat better and clean

目前為 2024-12-22 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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 <[email protected]>
// @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))


})()