lmsys-enhancer

Optimize your experience on the chat.lmsys.org with the `lmsys-enhancer` Tampermonkey script.

  1. // ==UserScript==
  2. // @name lmsys-enhancer
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.7
  5. // @description Optimize your experience on the chat.lmsys.org with the `lmsys-enhancer` Tampermonkey script.
  6. // @author joshlee
  7. // @match https://chat.lmsys.org/
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=lmsys.org
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. const scriptConfig = {
  14. model: "gpt-4-turbo",
  15. loadLatestSession: true,
  16. firstMeetSessionHash: true,
  17. firstSend: true,
  18. currentSessionHash: "",
  19. dataKey: "session_hash_history",
  20. mainColor: "#DE6B2F",
  21. drawerWidth: 250,
  22. position: {
  23. main: "#component-1",
  24. changeModel: "#model_selector_row label",
  25. directChat: ".tab-nav.scroll-hide button:nth-child(3)",
  26. maxOutputTokenInput: "//span[text()='Max output tokens']/../../input",
  27. maxOutputTokenBar: "//span[text()='Max output tokens']/../../../../input",
  28. sendBtn: '//button[text()="Send"]',
  29. unnecessaryList: ["#component-93", "#component-83"],
  30. },
  31. };
  32.  
  33. const originalSend = WebSocket.prototype.send;
  34.  
  35. var itemList;
  36.  
  37. function $x(xpathToExecute) {
  38. var result = [];
  39. var nodesSnapshot = document.evaluate(
  40. xpathToExecute,
  41. document,
  42. null,
  43. XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  44. null
  45. );
  46. for (var i = 0; i < nodesSnapshot.snapshotLength; i++) {
  47. result.push(nodesSnapshot.snapshotItem(i));
  48. }
  49. return result;
  50. }
  51.  
  52. function removeUselessElements() {
  53. // Remove unnecessary notices and components
  54. document
  55. .querySelectorAll("#notice_markdown")
  56. .forEach((elem) => elem.remove());
  57. scriptConfig.position.unnecessaryList.forEach((v) => {
  58. const componentToRemove = document.querySelector(v);
  59. if (componentToRemove) componentToRemove.remove();
  60. });
  61. }
  62.  
  63. function setMaxOutputToken(maxValue = 4096) {
  64. // Set the maximum token output to the desired value
  65. const [, , input] = $x(scriptConfig.position.maxOutputTokenInput);
  66. const [, , bar] = $x(scriptConfig.position.maxOutputTokenBar);
  67.  
  68. if (!input || !bar) {
  69. console.error("Set max output token failure!");
  70. return;
  71. }
  72.  
  73. input.value = String(maxValue);
  74. bar.max = String(maxValue);
  75. bar.value = String(maxValue);
  76.  
  77. const changeEvent = new Event("change", { bubbles: true, cancelable: true });
  78. bar.dispatchEvent(changeEvent);
  79. }
  80.  
  81. function changeModel(model) {
  82. document.querySelector(scriptConfig.position.changeModel).click();
  83. document.querySelector(`li[data-value=${model}]`).dispatchEvent(
  84. new MouseEvent("mousedown", {
  85. view: window,
  86. bubbles: true,
  87. cancelable: true,
  88. })
  89. );
  90. }
  91.  
  92. function hackWsSend(data) {
  93. let d = JSON.parse(data);
  94. // get session_hash
  95. if (d.session_hash) {
  96. // first create session
  97. if (scriptConfig.firstMeetSessionHash) {
  98. scriptConfig.currentSessionHash = d.session_hash;
  99. scriptConfig.firstMeetSessionHash = false;
  100. }
  101. if (
  102. scriptConfig.loadLatestSession &&
  103. d.fn_index === 38 &&
  104. getLastestSessionToggleBtn()
  105. ) {
  106. // skip creat new session
  107. d.fn_index = 41;
  108. d.data = [null, scriptConfig.model, ""];
  109. originalSend.call(this, data);
  110. toggleLastestSession();
  111. return;
  112. }
  113. // first send message
  114. if (scriptConfig.firstSend && d.fn_index === 39) {
  115. saveCurrentSession();
  116. scriptConfig.firstSend = false;
  117. }
  118.  
  119. // modify session_hash
  120. if (scriptConfig.currentSessionHash) {
  121. d.session_hash = scriptConfig.currentSessionHash;
  122. data = JSON.stringify(d);
  123. }
  124. }
  125. originalSend.call(this, data);
  126. }
  127.  
  128. function createSessionItem(text, sessionHash) {
  129. var itemContainer = document.createElement("div");
  130. itemContainer.style.display = "flex";
  131. itemContainer.style.justifyContent = "space-between";
  132. itemContainer.style.alignItems = "center";
  133. itemContainer.style.padding = "10px";
  134. itemContainer.style.borderBottom = "1px solid #ccc";
  135. itemContainer.className = "session-item-container";
  136. itemContainer.setAttribute("data-session-hash", sessionHash);
  137.  
  138. var itemText = document.createElement("div");
  139. itemText.textContent = text;
  140. itemContainer.appendChild(itemText);
  141.  
  142. // Create toggle session button
  143. var toggleButton = document.createElement("button");
  144. toggleButton.textContent = "⇢";
  145. toggleButton.style.marginLeft = "5px";
  146. toggleButton.style.padding = "5px 10px";
  147. toggleButton.style.border = "none";
  148. toggleButton.style.borderRadius = "4px";
  149. toggleButton.style.backgroundColor = "#4CAF50";
  150. toggleButton.style.color = "white";
  151. toggleButton.style.cursor = "pointer";
  152. // Button hover effect
  153. toggleButton.addEventListener("mouseover", function () {
  154. this.style.backgroundColor = "#45a049";
  155. });
  156. toggleButton.addEventListener("mouseout", function () {
  157. this.style.backgroundColor = "#4CAF50";
  158. });
  159. toggleButton.addEventListener("click", function () {
  160. scriptConfig.currentSessionHash = sessionHash;
  161. clickSendBtn();
  162. });
  163. itemContainer.appendChild(toggleButton);
  164.  
  165. // Create Delete button
  166. var deleteButton = document.createElement("button");
  167. deleteButton.textContent = "✖";
  168. deleteButton.style.marginLeft = "5px";
  169. deleteButton.style.padding = "5px 10px";
  170. deleteButton.style.border = "none";
  171. deleteButton.style.borderRadius = "4px";
  172. deleteButton.style.backgroundColor = "#f44336";
  173. deleteButton.style.color = "white";
  174. deleteButton.style.cursor = "pointer";
  175. // Button hover effect
  176. deleteButton.addEventListener("mouseover", function () {
  177. this.style.backgroundColor = "#e53935";
  178. });
  179. deleteButton.addEventListener("mouseout", function () {
  180. this.style.backgroundColor = "#f44336";
  181. });
  182. deleteButton.addEventListener("click", function () {
  183. removeSessionHash(sessionHash);
  184. itemContainer.remove();
  185. });
  186.  
  187. itemContainer.appendChild(deleteButton);
  188.  
  189. itemContainer.highlight = function () {
  190. itemContainer.classList.add("highlighted-session");
  191. toggleButton.classList.add("disabled-button");
  192. deleteButton.classList.add("disabled-button");
  193. };
  194.  
  195. itemContainer.unhighlight = function () {
  196. itemContainer.classList.remove("highlighted-session");
  197. toggleButton.classList.remove("disabled-button");
  198. deleteButton.classList.remove("disabled-button");
  199. };
  200.  
  201. if (sessionHash === scriptConfig.currentSessionHash) {
  202. itemContainer.highlight();
  203. }
  204.  
  205. itemList.appendChild(itemContainer);
  206. updateHighlightedSessions();
  207. }
  208.  
  209. function createHistorySessionElement() {
  210. // Create Float Button
  211. var floatButton = document.createElement("button");
  212. floatButton.textContent = "☰";
  213. floatButton.style.position = "fixed";
  214. floatButton.style.left = "20px";
  215. floatButton.style.top = "50%";
  216. floatButton.style.transform = "translateY(-50%)";
  217. floatButton.style.zIndex = "9999";
  218. floatButton.style.padding = "15px";
  219. floatButton.style.borderRadius = "100%";
  220. floatButton.style.backgroundColor = "#212936";
  221. floatButton.style.color = "white";
  222. floatButton.style.border = "none";
  223. floatButton.style.boxShadow = "0 2px 5px rgba(0,0,0,0.3)";
  224. floatButton.style.transition = "left 0.3s ease";
  225. floatButton.style.cursor = "pointer";
  226. floatButton.addEventListener("click", function () {
  227. if (drawer.style.left === `-${scriptConfig.drawerWidth}px`) {
  228. drawer.style.left = "0";
  229. floatButton.style.left = `${scriptConfig.drawerWidth}px`;
  230. } else {
  231. drawer.style.left = `-${scriptConfig.drawerWidth}px`;
  232. floatButton.style.left = "20px";
  233. }
  234. });
  235. document.body.appendChild(floatButton);
  236.  
  237. // Create drawer container
  238. var drawer = document.createElement("div");
  239. drawer.style.position = "fixed";
  240. drawer.style.left = "-250px";
  241. drawer.style.top = "0";
  242. drawer.style.width = `${scriptConfig.drawerWidth}px`;
  243. drawer.style.height = "100%";
  244. drawer.style.backgroundColor = "#212936";
  245. drawer.style.transition = "0.3s";
  246. drawer.style.zIndex = "9998";
  247. drawer.style.padding = "15px";
  248. drawer.style.boxSizing = "border-box";
  249. drawer.style.color = "white";
  250. drawer.style.overflowY = "auto";
  251. document.body.appendChild(drawer);
  252.  
  253. // Create Deawer Title
  254. var drawerTitle = document.createElement("h2");
  255. drawerTitle.textContent = "History Session";
  256. drawerTitle.style.padding = "10px";
  257. drawerTitle.style.marginTop = "0";
  258. drawerTitle.style.color = "white";
  259. drawerTitle.style.backgroundColor = "#2c3e50";
  260. drawerTitle.style.borderBottom = "1px solid #ccc";
  261. drawer.appendChild(drawerTitle);
  262.  
  263. // Create session item container
  264. itemList = document.createElement("div");
  265. drawer.appendChild(itemList);
  266.  
  267. // Click outside the drawer to turn off the drawer's function
  268. document.addEventListener("click", function (event) {
  269. // Check if the click is happening outside of the drawer or floating button
  270. if (!drawer.contains(event.target) && !floatButton.contains(event.target)) {
  271. updateHighlightedSessions();
  272. // Opened
  273. if (drawer.style.left === "0px") {
  274. drawer.style.left = `-${scriptConfig.drawerWidth}px`; // hidden
  275. floatButton.style.left = "20px"; // restore
  276. }
  277. }
  278. });
  279.  
  280. // Create New Chat Button
  281. const newChatButton = document.createElement("button");
  282. newChatButton.textContent = "New Chat";
  283. newChatButton.style.position = "absolute";
  284. newChatButton.style.bottom = "20px";
  285. newChatButton.style.left = "50%";
  286. newChatButton.style.transform = "translateX(-50%)";
  287. newChatButton.style.padding = "10px 30px";
  288. newChatButton.style.border = "none";
  289. newChatButton.style.borderRadius = "5px";
  290. newChatButton.style.backgroundColor = scriptConfig.mainColor;
  291. newChatButton.style.color = "white";
  292. newChatButton.style.cursor = "pointer";
  293. newChatButton.addEventListener("click", function () {
  294. handleNewChatClick();
  295. });
  296. drawer.appendChild(newChatButton);
  297. loadSessionHashHistory();
  298. }
  299.  
  300. function handleNewChatClick() {
  301. scriptConfig.currentSessionHash = Math.random().toString(36).substring(2);
  302. scriptConfig.firstMeetSessionHash = true;
  303. scriptConfig.firstSend = true;
  304. scriptConfig.loadLatestSession = false;
  305. clickSendBtn();
  306. }
  307.  
  308. // Update the highlighted state of the session list
  309. function updateHighlightedSessions() {
  310. const sessionItems = itemList.getElementsByClassName(
  311. "session-item-container"
  312. );
  313. for (const item of sessionItems) {
  314. item.unhighlight(); // remove all highlight state
  315. }
  316.  
  317. const newItem = itemList.querySelector(
  318. `[data-session-hash="${scriptConfig.currentSessionHash}"]`
  319. );
  320. if (newItem) {
  321. newItem.highlight();
  322. return;
  323. }
  324. }
  325.  
  326. function getLastestSessionToggleBtn() {
  327. const gotoBtn = document.querySelectorAll(
  328. ".session-item-container button"
  329. )[0];
  330. return gotoBtn;
  331. }
  332.  
  333. function toggleLastestSession() {
  334. scriptConfig.firstSend = false;
  335. scriptConfig.firstMeetSessionHash = false;
  336. getLastestSessionToggleBtn().click();
  337. }
  338.  
  339. function loadSessionHashHistory() {
  340. itemList.innerHTML = "";
  341. const list = JSON.parse(localStorage.getItem(scriptConfig.dataKey)) || [];
  342. for (let i = list.length - 1; i >= 0; i--) {
  343. let e = list[i];
  344. createSessionItem(e.time, e.session_hash);
  345. }
  346. updateHighlightedSessions();
  347. }
  348.  
  349. function getCurrentFormattedTime() {
  350. const now = new Date();
  351. const year = now.getFullYear().toString(); // year
  352. const month = (now.getMonth() + 1).toString().padStart(2, "0"); // month
  353. const date = now.getDate().toString().padStart(2, "0"); // day
  354. const hours = now.getHours().toString().padStart(2, "0"); // hout
  355. const minutes = now.getMinutes().toString().padStart(2, "0");
  356. return `${year}-${month}-${date} ${hours}:${minutes}`;
  357. }
  358.  
  359. function removeSessionHash(targetSessionHash) {
  360. const list = JSON.parse(localStorage.getItem(scriptConfig.dataKey));
  361. let filteredList = list.filter(
  362. (item) => item.session_hash !== targetSessionHash
  363. );
  364. localStorage.setItem(scriptConfig.dataKey, JSON.stringify(filteredList));
  365. }
  366.  
  367. function clickSendBtn() {
  368. $x(scriptConfig.position.sendBtn)[2].click();
  369. }
  370.  
  371. function saveCurrentSession() {
  372. const value = localStorage.getItem(scriptConfig.dataKey);
  373. let list = [];
  374. if (value) {
  375. list = JSON.parse(value);
  376. }
  377. list.push({
  378. time: getCurrentFormattedTime(),
  379. session_hash: scriptConfig.currentSessionHash,
  380. });
  381. localStorage.setItem(scriptConfig.dataKey, JSON.stringify(list));
  382. loadSessionHashHistory();
  383. }
  384.  
  385. function initialize() {
  386. window.alert = null;
  387. WebSocket.prototype.send = hackWsSend;
  388. document.querySelector(scriptConfig.position.directChat).click();
  389.  
  390. removeUselessElements();
  391. setMaxOutputToken();
  392. changeModel(scriptConfig.model);
  393. }
  394.  
  395. (function main() {
  396. // Add a style to highlight the current session
  397. const style = document.createElement("style");
  398. style.type = "text/css";
  399. style.innerHTML = `
  400. .highlighted-session {
  401. background-color: #e0e0e0;
  402. color: #333;
  403. border-left: 3px solid #ffeb3b;
  404. }
  405. .disabled-button {
  406. pointer-events: none;
  407. opacity: 0.6;
  408. }
  409. `;
  410. document.head.appendChild(style);
  411. createHistorySessionElement();
  412. // Create a MutationObserver instance and pass the callback function
  413. const observer = new MutationObserver((mutations, observer) => {
  414. // Find the target element using its ID (you may adjust the selector according to your needs)
  415. const targetElement = document.querySelector(scriptConfig.position.main);
  416. if (targetElement) {
  417. // If the target element exists, execute the optimize function and stop observing
  418. initialize();
  419. observer.disconnect(); // Stop observing
  420. }
  421. });
  422.  
  423. // Configuration for the observer:
  424. const observerConfig = { childList: true, subtree: true };
  425.  
  426. // Select the node that will be observed for mutations
  427. const targetNode = document.body; // You may specify a more specific parent node to reduce the scope of observation
  428.  
  429. // Call the observer's observe method to start observing the target node
  430. observer.observe(targetNode, observerConfig);
  431. })();