LibreChat Shortcuts + Token Counter

Press Alt+S to click toggle-left-nav button on localhost:3080

  1. // ==UserScript==
  2. // @name LibreChat Shortcuts + Token Counter
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.4
  5. // @description Press Alt+S to click toggle-left-nav button on localhost:3080
  6. // @author bwhurd
  7. // @match http://localhost:3080/*
  8. // @grant none
  9. // @run-at document-end
  10. // ==/UserScript==
  11.  
  12. // === Shortcut Keybindings ===
  13. // Alt+S → Toggle sidebar (clicks #toggle-left-nav)
  14. // Alt+N → New chat (clicks button[aria-label="New chat"])
  15. // Alt+T → Scroll to top of message container
  16. // Alt+Z → Scroll to bottom of message container
  17. // Alt+W → Focus Chat Input
  18. // Alt+C → Click Copy on lowest message
  19. // Alt+X → Select and copy, cycles visible messages
  20. // Alt+A → Scroll up one message (.message-render)
  21. // Alt+F → Scroll down one message (.message-render)
  22. // Just start typing to go to input chatbox
  23. // Paste input when not in chat box
  24.  
  25. // Alt+R → refresh cost for conversation
  26. // Alt+U → update the token cost per million
  27.  
  28. (function () {
  29. 'use strict';
  30.  
  31. // === Inject custom CSS to override hidden footer button color ===
  32. const style = document.createElement('style');
  33. style.textContent = `
  34. .relative.hidden.items-center.justify-center {
  35. display:none;
  36. }
  37. `;
  38. document.head.appendChild(style);
  39.  
  40. // Shared scroll state object
  41. const ScrollState = {
  42. scrollContainer: null,
  43. isAnimating: false,
  44. finalScrollPosition: 0,
  45. userInterrupted: false,
  46. };
  47.  
  48. function resetScrollState() {
  49. if (ScrollState.isAnimating) {
  50. ScrollState.isAnimating = false;
  51. ScrollState.userInterrupted = true;
  52. }
  53. ScrollState.scrollContainer = getScrollableContainer();
  54. if (ScrollState.scrollContainer) {
  55. ScrollState.finalScrollPosition = ScrollState.scrollContainer.scrollTop;
  56. }
  57. }
  58.  
  59. function getScrollableContainer() {
  60. const firstMessage = document.querySelector('.message-render');
  61. if (!firstMessage) return null;
  62.  
  63. let container = firstMessage.parentElement;
  64. while (container && container !== document.body) {
  65. const style = getComputedStyle(container);
  66. if (
  67. container.scrollHeight > container.clientHeight &&
  68. style.overflowY !== 'visible' &&
  69. style.overflowY !== 'hidden'
  70. ) {
  71. return container;
  72. }
  73. container = container.parentElement;
  74. }
  75.  
  76. return document.scrollingElement || document.documentElement;
  77. }
  78.  
  79. function checkGSAP() {
  80. if (
  81. typeof window.gsap !== "undefined" &&
  82. typeof window.ScrollToPlugin !== "undefined" &&
  83. typeof window.Observer !== "undefined" &&
  84. typeof window.Flip !== "undefined"
  85. ) {
  86. gsap.registerPlugin(ScrollToPlugin, Observer, Flip);
  87. console.log("✅ GSAP and plugins registered");
  88. initShortcuts();
  89. } else {
  90. console.warn("⏳ GSAP not ready. Retrying...");
  91. setTimeout(checkGSAP, 100);
  92. }
  93. }
  94.  
  95. function loadGSAPLibraries() {
  96. const libs = [
  97. 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.7/gsap.min.js',
  98. 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.7/ScrollToPlugin.min.js',
  99. 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.7/Observer.min.js',
  100. 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.7/Flip.min.js',
  101. ];
  102.  
  103. libs.forEach(src => {
  104. const script = document.createElement('script');
  105. script.src = src;
  106. script.async = false;
  107. document.head.appendChild(script);
  108. });
  109.  
  110. checkGSAP();
  111. }
  112.  
  113. function scrollToTop() {
  114. const container = getScrollableContainer();
  115. if (!container) return;
  116. gsap.to(container, {
  117. duration: .6,
  118. scrollTo: { y: 0 },
  119. ease: "power4.out"
  120. });
  121. }
  122.  
  123. function scrollToBottom() {
  124. const container = getScrollableContainer();
  125. if (!container) return;
  126. gsap.to(container, {
  127. duration: .6,
  128. scrollTo: { y: "max" },
  129. ease: "power4.out"
  130. });
  131. }
  132.  
  133. function scrollUpOneMessage() {
  134. const container = getScrollableContainer();
  135. if (!container) return;
  136.  
  137. const messages = [...document.querySelectorAll('.message-render')];
  138. const currentScrollTop = container.scrollTop;
  139.  
  140. let target = null;
  141. for (let i = messages.length - 1; i >= 0; i--) {
  142. if (messages[i].offsetTop < currentScrollTop - 25) {
  143. target = messages[i];
  144. break;
  145. }
  146. }
  147.  
  148. gsap.to(container, {
  149. duration: 0.6,
  150. scrollTo: { y: target?.offsetTop || 0 },
  151. ease: "power4.out"
  152. });
  153. }
  154.  
  155. function scrollDownOneMessage() {
  156. const container = getScrollableContainer();
  157. if (!container) return;
  158.  
  159. const messages = [...document.querySelectorAll('.message-render')];
  160. const currentScrollTop = container.scrollTop;
  161.  
  162. let target = null;
  163. for (let i = 0; i < messages.length; i++) {
  164. if (messages[i].offsetTop > currentScrollTop + 25) {
  165. target = messages[i];
  166. break;
  167. }
  168. }
  169.  
  170. gsap.to(container, {
  171. duration: 0.6,
  172. scrollTo: { y: target?.offsetTop || container.scrollHeight },
  173. ease: "power4.out"
  174. });
  175. }
  176.  
  177. function initShortcuts() {
  178. document.addEventListener('keydown', function (e) {
  179. if (!e.altKey || e.repeat) return;
  180.  
  181. const key = e.key.toLowerCase();
  182.  
  183. const keysToBlock = ['s', 'n', 't', 'z', 'a', 'f'];
  184. if (keysToBlock.includes(key)) {
  185. e.preventDefault();
  186. e.stopPropagation();
  187.  
  188. switch (key) {
  189. case 's': toggleSidebar(); break;
  190. case 'n': openNewChat(); break;
  191. case 't': scrollToTop(); break;
  192. case 'z': scrollToBottom(); break;
  193. case 'a': scrollUpOneMessage(); break;
  194. case 'f': scrollDownOneMessage(); break;
  195. }
  196. }
  197. });
  198.  
  199. console.log("✅ LibreChat shortcuts active");
  200. }
  201.  
  202. function toggleSidebar() {
  203. const toggleButton = document.getElementById('toggle-left-nav');
  204. if (toggleButton) {
  205. toggleButton.click();
  206. console.log('🧭 Sidebar toggled');
  207. }
  208. }
  209.  
  210. function openNewChat() {
  211. const newChatButton = document.querySelector('button[aria-label="New chat"]');
  212. if (newChatButton) {
  213. newChatButton.click();
  214. console.log('🆕 New chat opened');
  215. }
  216. }
  217.  
  218. // Start loading GSAP plugins and wait for them
  219. loadGSAPLibraries();
  220. })();
  221.  
  222.  
  223. /*=============================================================
  224. = =
  225. = IIFE #2 token counter =
  226. = =
  227. =============================================================*/
  228.  
  229. setTimeout(() => {
  230. (function () {
  231. 'use strict';
  232.  
  233. // Change from const to let so they can be updated dynamically
  234. let COST_PER_MILLION_INPUT = parseFloat(localStorage.getItem("costInput")) || 2.50;
  235. let COST_PER_MILLION_OUTPUT = parseFloat(localStorage.getItem("costOutput")) || 10.00;
  236.  
  237.  
  238. const estimateTokens = (text) => Math.ceil((text || "").trim().length / 4);
  239.  
  240. const badge = document.createElement("span");
  241. badge.id = "token-count-badge";
  242. badge.style.fontSize = "8px";
  243. badge.style.padding = "1px 0 0 6px";
  244. badge.style.borderRadius = "8px";
  245. badge.style.background = "transparent";
  246. badge.style.color = "#a9a9a9";
  247. badge.style.fontFamily = "monospace";
  248. badge.style.userSelect = "none";
  249. badge.style.alignSelf = "center";
  250. badge.style.marginTop = "16px";
  251.  
  252. const refreshBtn = document.createElement("button");
  253. refreshBtn.id = "refresh-btn";
  254. refreshBtn.title = "Refresh token count";
  255. refreshBtn.textContent = "↻";
  256. refreshBtn.style.marginLeft = "6px";
  257. refreshBtn.style.cursor = "pointer";
  258. refreshBtn.style.fontSize = "10px";
  259. refreshBtn.style.border = "none";
  260. refreshBtn.style.background = "transparent";
  261. refreshBtn.style.color = "#a9a9a9";
  262. refreshBtn.style.userSelect = "none";
  263. refreshBtn.style.fontFamily = "monospace";
  264. refreshBtn.style.transformOrigin = "center";
  265. refreshBtn.style.padding = "0";
  266.  
  267. refreshBtn.onclick = () => {
  268. refreshBtn.style.transition = 'transform 0.15s ease';
  269. refreshBtn.style.transform = 'scale(1.4)';
  270. setTimeout(() => {
  271. refreshBtn.style.transform = 'scale(1)';
  272. }, 150);
  273. updateTokenCounts();
  274. };
  275.  
  276. function insertBadgeInFlexRow() {
  277. const flexRow = [...document.querySelectorAll("div.flex")].find(el =>
  278. el.className.includes("items-between") && el.className.includes("pb-2")
  279. );
  280. if (!flexRow) return false;
  281.  
  282. if (flexRow.querySelector("#token-count-badge")) return true;
  283.  
  284. const micButton = flexRow.querySelector('button[title="Use microphone"]');
  285. if (!micButton) return false;
  286.  
  287. flexRow.insertBefore(badge, micButton);
  288. return true;
  289. }
  290.  
  291. function inferRole(msgEl) {
  292. const wrapper = msgEl.closest('.group, .message');
  293. if (wrapper?.classList.contains('user')) return 'user';
  294. if (wrapper?.classList.contains('assistant')) return 'assistant';
  295.  
  296. const label = wrapper?.querySelector('span, div')?.innerText?.toLowerCase() || '';
  297. if (label.includes("you")) return "user";
  298. if (label.includes("gpt") || label.includes("assistant")) return "assistant";
  299.  
  300. const all = Array.from(document.querySelectorAll('.message-render'));
  301. const index = all.indexOf(msgEl);
  302. return index % 2 === 0 ? "user" : "assistant";
  303. }
  304.  
  305. function updateTokenCounts() {
  306. const messages = Array.from(document.querySelectorAll('.message-render'));
  307. if (!messages.length) {
  308. badge.textContent = `0 | 0 | 0 | $0.0000`;
  309. badge.appendChild(refreshBtn);
  310. return;
  311. }
  312.  
  313. const conversation = messages.map(msg => ({
  314. role: inferRole(msg),
  315. content: msg.innerText || "",
  316. tokens: estimateTokens(msg.innerText || "")
  317. }));
  318.  
  319. let cumulativeInputTokens = 0;
  320. let cumulativeOutputTokens = 0;
  321. let priorContextTokens = 0;
  322.  
  323. for (let i = 0; i < conversation.length - 1; i++) {
  324. const current = conversation[i];
  325. const next = conversation[i + 1];
  326.  
  327. if (current.role === "user" && next?.role === "assistant") {
  328. const inputTokensThisTurn = priorContextTokens + current.tokens;
  329. const outputTokensThisTurn = next.tokens;
  330.  
  331. cumulativeInputTokens += inputTokensThisTurn;
  332. cumulativeOutputTokens += outputTokensThisTurn;
  333.  
  334. priorContextTokens += current.tokens + next.tokens;
  335. i++; // skip assistant message
  336. }
  337. }
  338.  
  339. const totalTokens = cumulativeInputTokens + cumulativeOutputTokens;
  340. const costInputCalculated = (cumulativeInputTokens / 1_000_000) * COST_PER_MILLION_INPUT;
  341. const costOutputCalculated = (cumulativeOutputTokens / 1_000_000) * COST_PER_MILLION_OUTPUT;
  342. const totalCostCalculated = costInputCalculated + costOutputCalculated;
  343.  
  344. badge.textContent = `${cumulativeInputTokens} @ $${COST_PER_MILLION_INPUT}/M | ${cumulativeOutputTokens} @ $${COST_PER_MILLION_OUTPUT}/M | ${totalTokens} | $${totalCostCalculated.toFixed(3)}`;
  345. badge.appendChild(refreshBtn);
  346. }
  347.  
  348. document.addEventListener('keydown', function(e) {
  349. if (e.altKey && e.key.toLowerCase() === 'r' && !e.repeat) {
  350. e.preventDefault();
  351. refreshBtn.style.transition = 'transform 0.15s ease';
  352. refreshBtn.style.transform = 'scale(1.4)';
  353. setTimeout(() => {
  354. refreshBtn.style.transform = 'scale(1)';
  355. }, 150);
  356. updateTokenCounts();
  357. }
  358. });
  359.  
  360. document.addEventListener('keydown', function(e) {
  361. if (e.altKey && e.key.toLowerCase() === 'u' && !e.repeat) {
  362. e.preventDefault();
  363. const newCosts = prompt(
  364. `Enter new costs for input and output per million tokens, separated by a comma.\n(Current: ${COST_PER_MILLION_INPUT}, ${COST_PER_MILLION_OUTPUT})`,
  365. `${COST_PER_MILLION_INPUT},${COST_PER_MILLION_OUTPUT}`
  366. );
  367. if (newCosts !== null) {
  368. const parts = newCosts.split(',').map(s => parseFloat(s.trim()));
  369. if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
  370. COST_PER_MILLION_INPUT = parts[0];
  371. COST_PER_MILLION_OUTPUT = parts[1];
  372. localStorage.setItem("costInput", COST_PER_MILLION_INPUT);
  373. localStorage.setItem("costOutput", COST_PER_MILLION_OUTPUT);
  374. updateTokenCounts();
  375. } else {
  376. alert("Invalid input. Please enter two valid numbers separated by a comma.");
  377. }
  378. }
  379. }
  380. });
  381.  
  382.  
  383.  
  384. function waitForLayoutAndInitialize(retries = 20) {
  385. const messagesRoot = document.querySelector('.message-render')?.parentElement;
  386. const badgeReady = insertBadgeInFlexRow();
  387.  
  388. if (!badgeReady && retries > 0) {
  389. setTimeout(() => waitForLayoutAndInitialize(retries - 1), 500);
  390. return;
  391. }
  392.  
  393. if (messagesRoot) {
  394. const observer = new MutationObserver(() => {
  395. updateTokenCounts();
  396. });
  397. observer.observe(messagesRoot, { childList: true, subtree: true });
  398. }
  399.  
  400. updateTokenCounts();
  401. }
  402.  
  403. waitForLayoutAndInitialize();
  404. })();
  405. }, 1000);
  406.  
  407.  
  408. (function() {
  409. document.addEventListener('keydown', function(e) {
  410. if (e.altKey && e.key === 'w') {
  411. e.preventDefault();
  412. const chatInput = document.querySelector('#prompt-textarea');
  413. if (chatInput) {
  414. chatInput.focus();
  415. }
  416. }
  417. });
  418. })();
  419.  
  420. (function() {
  421. document.addEventListener('keydown', function(e) {
  422. if (e.altKey && e.key === 'c') {
  423. e.preventDefault();
  424. const allButtons = Array.from(document.querySelectorAll('button'));
  425. const visibleButtons = allButtons.filter(button =>
  426. button.innerHTML.includes('M7 5a3 3 0 0 1 3-3h9a3')
  427. ).filter(button => {
  428. const rect = button.getBoundingClientRect();
  429. return (
  430. rect.top >= 0 &&
  431. rect.left >= 0 &&
  432. rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
  433. rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  434. );
  435. });
  436.  
  437. if (visibleButtons.length > 0) {
  438. visibleButtons[visibleButtons.length - 1].click();
  439.  
  440. if (window.removeMarkdownOnCopyCheckbox) {
  441. setTimeout(() => {
  442. if (!navigator.clipboard) {
  443. return; // catch error silently
  444. }
  445.  
  446. navigator.clipboard.readText()
  447. .then((textContent) => {
  448. const cleanedContent = removeMarkdown(textContent);
  449. return navigator.clipboard.writeText(cleanedContent);
  450. })
  451. .then(() => {
  452. console.log("Clipboard content cleaned and copied.");
  453. })
  454. .catch(() => {
  455. // Suppress errors for a smoother user experience
  456. });
  457. }, 500);
  458. }
  459. }
  460. }
  461. });
  462. })();
  463.  
  464. (function() {
  465. // Initialize single global store for last selection
  466. window.selectAllLowestResponseState = window.selectAllLowestResponseState || {
  467. lastSelectedIndex: -1
  468. };
  469.  
  470. document.addEventListener('keydown', function(e) {
  471. if (e.altKey && e.key === 'x') {
  472. e.preventDefault();
  473. // Delay execution to ensure DOM is fully loaded
  474. setTimeout(() => {
  475. try {
  476. const onlySelectAssistant = window.onlySelectAssistantCheckbox || false;
  477. const onlySelectUser = window.onlySelectUserCheckbox || false;
  478. const disableCopyAfterSelect = window.disableCopyAfterSelectCheckbox || false;
  479.  
  480. const allConversationTurns = (() => {
  481. try {
  482. return Array.from(document.querySelectorAll('.user-turn, .agent-turn')) || [];
  483. } catch {
  484. return [];
  485. }
  486. })();
  487.  
  488. const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
  489. const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
  490.  
  491. const composerRect = (() => {
  492. try {
  493. const composerBackground = document.getElementById('composer-background');
  494. return composerBackground ? composerBackground.getBoundingClientRect() : null;
  495. } catch {
  496. return null;
  497. }
  498. })();
  499.  
  500. const visibleTurns = allConversationTurns.filter(el => {
  501. const rect = el.getBoundingClientRect();
  502. const horizontallyInView = rect.left < viewportWidth && rect.right > 0;
  503. const verticallyInView = rect.top < viewportHeight && rect.bottom > 0;
  504. if (!horizontallyInView || !verticallyInView) return false;
  505.  
  506. if (composerRect) {
  507. if (rect.top >= composerRect.top) {
  508. return false;
  509. }
  510. }
  511.  
  512. return true;
  513. });
  514.  
  515. const filteredVisibleTurns = (() => {
  516. if (onlySelectAssistant) {
  517. return visibleTurns.filter(el =>
  518. el.querySelector('[data-message-author-role="assistant"]')
  519. );
  520. }
  521. if (onlySelectUser) {
  522. return visibleTurns.filter(el =>
  523. el.querySelector('[data-message-author-role="user"]')
  524. );
  525. }
  526. return visibleTurns;
  527. })();
  528.  
  529. if (filteredVisibleTurns.length === 0) return;
  530.  
  531. filteredVisibleTurns.sort((a, b) => {
  532. const ra = a.getBoundingClientRect();
  533. const rb = b.getBoundingClientRect();
  534. return rb.top - ra.top;
  535. });
  536.  
  537. const { lastSelectedIndex } = window.selectAllLowestResponseState;
  538. const nextIndex = (lastSelectedIndex + 1) % filteredVisibleTurns.length;
  539. const selectedTurn = filteredVisibleTurns[nextIndex];
  540. if (!selectedTurn) return;
  541.  
  542. selectAndCopyMessage(selectedTurn);
  543. window.selectAllLowestResponseState.lastSelectedIndex = nextIndex;
  544.  
  545. function selectAndCopyMessage(turnElement) {
  546. try {
  547. const userContainer = turnElement.querySelector('[data-message-author-role="user"]');
  548. const isUser = !!userContainer;
  549.  
  550. if (isUser) {
  551. if (onlySelectAssistant) return;
  552. const userTextElement = userContainer.querySelector('.whitespace-pre-wrap');
  553. if (!userTextElement) return;
  554. doSelectAndCopy(userTextElement);
  555. } else {
  556. if (onlySelectUser) return;
  557. const assistantContainer = turnElement.querySelector('[data-message-author-role="assistant"]');
  558. let textElement = null;
  559. if (assistantContainer) {
  560. textElement = assistantContainer.querySelector('.prose') || assistantContainer;
  561. } else {
  562. textElement = turnElement.querySelector('.prose') || turnElement;
  563. }
  564. if (!textElement) return;
  565. doSelectAndCopy(textElement);
  566. }
  567. } catch {
  568. // Fail silently
  569. }
  570. }
  571.  
  572. function doSelectAndCopy(el) {
  573. try {
  574. const selection = window.getSelection();
  575. if (!selection) return;
  576. selection.removeAllRanges();
  577.  
  578. const range = document.createRange();
  579. range.selectNodeContents(el);
  580. selection.addRange(range);
  581.  
  582. if (!disableCopyAfterSelect) {
  583. document.execCommand('copy');
  584. }
  585. } catch {
  586. // Fail silently
  587. }
  588. }
  589.  
  590. } catch {
  591. // Fail silently
  592. }
  593. }, 50);
  594. }
  595. });
  596. })();
  597. // Existing script functionalities...
  598. (function() {
  599. const controlsNavId = 'controls-nav';
  600. const chatInputId = 'prompt-textarea';
  601.  
  602. // Function to handle focusing and pasting into the chat input
  603. function handlePaste(e) {
  604. const chatInput = document.getElementById(chatInputId);
  605.  
  606. if (chatInput && !document.activeElement.closest(`#${controlsNavId}`)) {
  607. // Focus the input if it is not already focused
  608. if (document.activeElement !== chatInput) {
  609. chatInput.focus();
  610. }
  611.  
  612. // Use a small delay to ensure focus happens before insertion
  613. setTimeout(() => {
  614. // Prevent default paste action to manually handle paste
  615. e.preventDefault();
  616.  
  617. // Obtain the pasted text
  618. const pastedData = (e.clipboardData || window.clipboardData).getData('text') || '';
  619. // Optionally append to the current value if needed, depending on cursor position logic
  620. const cursorPosition = chatInput.selectionStart;
  621. const textBefore = chatInput.value.substring(0, cursorPosition);
  622. const textAfter = chatInput.value.substring(cursorPosition);
  623.  
  624. // Set the new value with pasted data
  625. chatInput.value = textBefore + pastedData + textAfter;
  626.  
  627. // Move the cursor to the end of inserted data
  628. chatInput.selectionStart = chatInput.selectionEnd = cursorPosition + pastedData.length;
  629.  
  630. // Trigger an 'input' event to ensure any form listeners react
  631. const inputEvent = new Event('input', { bubbles: true, cancelable: true });
  632. chatInput.dispatchEvent(inputEvent);
  633. }, 0);
  634. }
  635. }
  636.  
  637. document.addEventListener('paste', function(e) {
  638. // Only handle paste if the focused element is not within #controls-nav
  639. if (!document.activeElement.closest(`#${controlsNavId}`)) {
  640. handlePaste(e);
  641. }
  642. });
  643. })();
  644.  
  645. (function() {
  646. const controlsNavId = 'controls-nav';
  647. const chatInputId = 'prompt-textarea';
  648.  
  649. document.addEventListener('keydown', function(e) {
  650. // Check if the pressed key is alphanumeric and neither Alt nor Ctrl (or Cmd) is pressed
  651. const isAlphanumeric = e.key.length === 1 && /[a-zA-Z0-9]/.test(e.key);
  652. const isModifierKeyPressed = e.altKey || e.ctrlKey || e.metaKey; // metaKey is for Cmd on Mac
  653.  
  654. // Only handle keydown if the active element is not within #controls-nav
  655. if (!document.activeElement.closest(`#${controlsNavId}`) && isAlphanumeric && !isModifierKeyPressed) {
  656. const activeElement = document.activeElement;
  657.  
  658. if (activeElement && activeElement.id !== chatInputId) {
  659. const chatInput = document.getElementById(chatInputId);
  660. if (chatInput) {
  661. // Prevent default to avoid unwanted actions
  662. e.preventDefault();
  663.  
  664. // Focus the chat input
  665. chatInput.focus();
  666.  
  667. // Optionally, append the typed character to the chat input's value
  668. chatInput.value += e.key;
  669. }
  670. }
  671. }
  672. });
  673. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址