- // ==UserScript==
- // @name LibreChat Shortcuts + Token Counter
- // @namespace http://tampermonkey.net/
- // @version 2.4
- // @description Press Alt+S to click toggle-left-nav button on localhost:3080
- // @author bwhurd
- // @match http://localhost:3080/*
- // @grant none
- // @run-at document-end
- // ==/UserScript==
-
- // === Shortcut Keybindings ===
- // Alt+S → Toggle sidebar (clicks #toggle-left-nav)
- // Alt+N → New chat (clicks button[aria-label="New chat"])
- // Alt+T → Scroll to top of message container
- // Alt+Z → Scroll to bottom of message container
- // Alt+W → Focus Chat Input
- // Alt+C → Click Copy on lowest message
- // Alt+X → Select and copy, cycles visible messages
- // Alt+A → Scroll up one message (.message-render)
- // Alt+F → Scroll down one message (.message-render)
- // Just start typing to go to input chatbox
- // Paste input when not in chat box
-
- // Alt+R → refresh cost for conversation
- // Alt+U → update the token cost per million
-
- (function () {
- 'use strict';
-
- // === Inject custom CSS to override hidden footer button color ===
- const style = document.createElement('style');
- style.textContent = `
- .relative.hidden.items-center.justify-center {
- display:none;
- }
- `;
- document.head.appendChild(style);
-
- // Shared scroll state object
- const ScrollState = {
- scrollContainer: null,
- isAnimating: false,
- finalScrollPosition: 0,
- userInterrupted: false,
- };
-
- function resetScrollState() {
- if (ScrollState.isAnimating) {
- ScrollState.isAnimating = false;
- ScrollState.userInterrupted = true;
- }
- ScrollState.scrollContainer = getScrollableContainer();
- if (ScrollState.scrollContainer) {
- ScrollState.finalScrollPosition = ScrollState.scrollContainer.scrollTop;
- }
- }
-
- function getScrollableContainer() {
- const firstMessage = document.querySelector('.message-render');
- if (!firstMessage) return null;
-
- let container = firstMessage.parentElement;
- while (container && container !== document.body) {
- const style = getComputedStyle(container);
- if (
- container.scrollHeight > container.clientHeight &&
- style.overflowY !== 'visible' &&
- style.overflowY !== 'hidden'
- ) {
- return container;
- }
- container = container.parentElement;
- }
-
- return document.scrollingElement || document.documentElement;
- }
-
- function checkGSAP() {
- if (
- typeof window.gsap !== "undefined" &&
- typeof window.ScrollToPlugin !== "undefined" &&
- typeof window.Observer !== "undefined" &&
- typeof window.Flip !== "undefined"
- ) {
- gsap.registerPlugin(ScrollToPlugin, Observer, Flip);
- console.log("✅ GSAP and plugins registered");
- initShortcuts();
- } else {
- console.warn("⏳ GSAP not ready. Retrying...");
- setTimeout(checkGSAP, 100);
- }
- }
-
- function loadGSAPLibraries() {
- const libs = [
- 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.7/gsap.min.js',
- 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.7/ScrollToPlugin.min.js',
- 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.7/Observer.min.js',
- 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.7/Flip.min.js',
- ];
-
- libs.forEach(src => {
- const script = document.createElement('script');
- script.src = src;
- script.async = false;
- document.head.appendChild(script);
- });
-
- checkGSAP();
- }
-
- function scrollToTop() {
- const container = getScrollableContainer();
- if (!container) return;
- gsap.to(container, {
- duration: .6,
- scrollTo: { y: 0 },
- ease: "power4.out"
- });
- }
-
- function scrollToBottom() {
- const container = getScrollableContainer();
- if (!container) return;
- gsap.to(container, {
- duration: .6,
- scrollTo: { y: "max" },
- ease: "power4.out"
- });
- }
-
- function scrollUpOneMessage() {
- const container = getScrollableContainer();
- if (!container) return;
-
- const messages = [...document.querySelectorAll('.message-render')];
- const currentScrollTop = container.scrollTop;
-
- let target = null;
- for (let i = messages.length - 1; i >= 0; i--) {
- if (messages[i].offsetTop < currentScrollTop - 25) {
- target = messages[i];
- break;
- }
- }
-
- gsap.to(container, {
- duration: 0.6,
- scrollTo: { y: target?.offsetTop || 0 },
- ease: "power4.out"
- });
- }
-
- function scrollDownOneMessage() {
- const container = getScrollableContainer();
- if (!container) return;
-
- const messages = [...document.querySelectorAll('.message-render')];
- const currentScrollTop = container.scrollTop;
-
- let target = null;
- for (let i = 0; i < messages.length; i++) {
- if (messages[i].offsetTop > currentScrollTop + 25) {
- target = messages[i];
- break;
- }
- }
-
- gsap.to(container, {
- duration: 0.6,
- scrollTo: { y: target?.offsetTop || container.scrollHeight },
- ease: "power4.out"
- });
- }
-
- function initShortcuts() {
- document.addEventListener('keydown', function (e) {
- if (!e.altKey || e.repeat) return;
-
- const key = e.key.toLowerCase();
-
- const keysToBlock = ['s', 'n', 't', 'z', 'a', 'f'];
- if (keysToBlock.includes(key)) {
- e.preventDefault();
- e.stopPropagation();
-
- switch (key) {
- case 's': toggleSidebar(); break;
- case 'n': openNewChat(); break;
- case 't': scrollToTop(); break;
- case 'z': scrollToBottom(); break;
- case 'a': scrollUpOneMessage(); break;
- case 'f': scrollDownOneMessage(); break;
- }
- }
- });
-
- console.log("✅ LibreChat shortcuts active");
- }
-
- function toggleSidebar() {
- const toggleButton = document.getElementById('toggle-left-nav');
- if (toggleButton) {
- toggleButton.click();
- console.log('🧭 Sidebar toggled');
- }
- }
-
- function openNewChat() {
- const newChatButton = document.querySelector('button[aria-label="New chat"]');
- if (newChatButton) {
- newChatButton.click();
- console.log('🆕 New chat opened');
- }
- }
-
- // Start loading GSAP plugins and wait for them
- loadGSAPLibraries();
- })();
-
-
- /*=============================================================
- = =
- = IIFE #2 token counter =
- = =
- =============================================================*/
-
- setTimeout(() => {
- (function () {
- 'use strict';
-
- // Change from const to let so they can be updated dynamically
- let COST_PER_MILLION_INPUT = parseFloat(localStorage.getItem("costInput")) || 2.50;
- let COST_PER_MILLION_OUTPUT = parseFloat(localStorage.getItem("costOutput")) || 10.00;
-
-
- const estimateTokens = (text) => Math.ceil((text || "").trim().length / 4);
-
- const badge = document.createElement("span");
- badge.id = "token-count-badge";
- badge.style.fontSize = "8px";
- badge.style.padding = "1px 0 0 6px";
- badge.style.borderRadius = "8px";
- badge.style.background = "transparent";
- badge.style.color = "#a9a9a9";
- badge.style.fontFamily = "monospace";
- badge.style.userSelect = "none";
- badge.style.alignSelf = "center";
- badge.style.marginTop = "16px";
-
- const refreshBtn = document.createElement("button");
- refreshBtn.id = "refresh-btn";
- refreshBtn.title = "Refresh token count";
- refreshBtn.textContent = "↻";
- refreshBtn.style.marginLeft = "6px";
- refreshBtn.style.cursor = "pointer";
- refreshBtn.style.fontSize = "10px";
- refreshBtn.style.border = "none";
- refreshBtn.style.background = "transparent";
- refreshBtn.style.color = "#a9a9a9";
- refreshBtn.style.userSelect = "none";
- refreshBtn.style.fontFamily = "monospace";
- refreshBtn.style.transformOrigin = "center";
- refreshBtn.style.padding = "0";
-
- refreshBtn.onclick = () => {
- refreshBtn.style.transition = 'transform 0.15s ease';
- refreshBtn.style.transform = 'scale(1.4)';
- setTimeout(() => {
- refreshBtn.style.transform = 'scale(1)';
- }, 150);
- updateTokenCounts();
- };
-
- function insertBadgeInFlexRow() {
- const flexRow = [...document.querySelectorAll("div.flex")].find(el =>
- el.className.includes("items-between") && el.className.includes("pb-2")
- );
- if (!flexRow) return false;
-
- if (flexRow.querySelector("#token-count-badge")) return true;
-
- const micButton = flexRow.querySelector('button[title="Use microphone"]');
- if (!micButton) return false;
-
- flexRow.insertBefore(badge, micButton);
- return true;
- }
-
- function inferRole(msgEl) {
- const wrapper = msgEl.closest('.group, .message');
- if (wrapper?.classList.contains('user')) return 'user';
- if (wrapper?.classList.contains('assistant')) return 'assistant';
-
- const label = wrapper?.querySelector('span, div')?.innerText?.toLowerCase() || '';
- if (label.includes("you")) return "user";
- if (label.includes("gpt") || label.includes("assistant")) return "assistant";
-
- const all = Array.from(document.querySelectorAll('.message-render'));
- const index = all.indexOf(msgEl);
- return index % 2 === 0 ? "user" : "assistant";
- }
-
- function updateTokenCounts() {
- const messages = Array.from(document.querySelectorAll('.message-render'));
- if (!messages.length) {
- badge.textContent = `0 | 0 | ∑ 0 | $0.0000`;
- badge.appendChild(refreshBtn);
- return;
- }
-
- const conversation = messages.map(msg => ({
- role: inferRole(msg),
- content: msg.innerText || "",
- tokens: estimateTokens(msg.innerText || "")
- }));
-
- let cumulativeInputTokens = 0;
- let cumulativeOutputTokens = 0;
- let priorContextTokens = 0;
-
- for (let i = 0; i < conversation.length - 1; i++) {
- const current = conversation[i];
- const next = conversation[i + 1];
-
- if (current.role === "user" && next?.role === "assistant") {
- const inputTokensThisTurn = priorContextTokens + current.tokens;
- const outputTokensThisTurn = next.tokens;
-
- cumulativeInputTokens += inputTokensThisTurn;
- cumulativeOutputTokens += outputTokensThisTurn;
-
- priorContextTokens += current.tokens + next.tokens;
- i++; // skip assistant message
- }
- }
-
- const totalTokens = cumulativeInputTokens + cumulativeOutputTokens;
- const costInputCalculated = (cumulativeInputTokens / 1_000_000) * COST_PER_MILLION_INPUT;
- const costOutputCalculated = (cumulativeOutputTokens / 1_000_000) * COST_PER_MILLION_OUTPUT;
- const totalCostCalculated = costInputCalculated + costOutputCalculated;
-
- badge.textContent = `${cumulativeInputTokens} @ $${COST_PER_MILLION_INPUT}/M | ${cumulativeOutputTokens} @ $${COST_PER_MILLION_OUTPUT}/M | ∑ ${totalTokens} | $${totalCostCalculated.toFixed(3)}`;
- badge.appendChild(refreshBtn);
- }
-
- document.addEventListener('keydown', function(e) {
- if (e.altKey && e.key.toLowerCase() === 'r' && !e.repeat) {
- e.preventDefault();
- refreshBtn.style.transition = 'transform 0.15s ease';
- refreshBtn.style.transform = 'scale(1.4)';
- setTimeout(() => {
- refreshBtn.style.transform = 'scale(1)';
- }, 150);
- updateTokenCounts();
- }
- });
-
- document.addEventListener('keydown', function(e) {
- if (e.altKey && e.key.toLowerCase() === 'u' && !e.repeat) {
- e.preventDefault();
- const newCosts = prompt(
- `Enter new costs for input and output per million tokens, separated by a comma.\n(Current: ${COST_PER_MILLION_INPUT}, ${COST_PER_MILLION_OUTPUT})`,
- `${COST_PER_MILLION_INPUT},${COST_PER_MILLION_OUTPUT}`
- );
- if (newCosts !== null) {
- const parts = newCosts.split(',').map(s => parseFloat(s.trim()));
- if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
- COST_PER_MILLION_INPUT = parts[0];
- COST_PER_MILLION_OUTPUT = parts[1];
- localStorage.setItem("costInput", COST_PER_MILLION_INPUT);
- localStorage.setItem("costOutput", COST_PER_MILLION_OUTPUT);
- updateTokenCounts();
- } else {
- alert("Invalid input. Please enter two valid numbers separated by a comma.");
- }
- }
- }
- });
-
-
-
- function waitForLayoutAndInitialize(retries = 20) {
- const messagesRoot = document.querySelector('.message-render')?.parentElement;
- const badgeReady = insertBadgeInFlexRow();
-
- if (!badgeReady && retries > 0) {
- setTimeout(() => waitForLayoutAndInitialize(retries - 1), 500);
- return;
- }
-
- if (messagesRoot) {
- const observer = new MutationObserver(() => {
- updateTokenCounts();
- });
- observer.observe(messagesRoot, { childList: true, subtree: true });
- }
-
- updateTokenCounts();
- }
-
- waitForLayoutAndInitialize();
- })();
- }, 1000);
-
-
- (function() {
- document.addEventListener('keydown', function(e) {
- if (e.altKey && e.key === 'w') {
- e.preventDefault();
- const chatInput = document.querySelector('#prompt-textarea');
- if (chatInput) {
- chatInput.focus();
- }
- }
- });
- })();
-
- (function() {
- document.addEventListener('keydown', function(e) {
- if (e.altKey && e.key === 'c') {
- e.preventDefault();
- const allButtons = Array.from(document.querySelectorAll('button'));
- const visibleButtons = allButtons.filter(button =>
- button.innerHTML.includes('M7 5a3 3 0 0 1 3-3h9a3')
- ).filter(button => {
- const rect = button.getBoundingClientRect();
- return (
- rect.top >= 0 &&
- rect.left >= 0 &&
- rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
- rect.right <= (window.innerWidth || document.documentElement.clientWidth)
- );
- });
-
- if (visibleButtons.length > 0) {
- visibleButtons[visibleButtons.length - 1].click();
-
- if (window.removeMarkdownOnCopyCheckbox) {
- setTimeout(() => {
- if (!navigator.clipboard) {
- return; // catch error silently
- }
-
- navigator.clipboard.readText()
- .then((textContent) => {
- const cleanedContent = removeMarkdown(textContent);
- return navigator.clipboard.writeText(cleanedContent);
- })
- .then(() => {
- console.log("Clipboard content cleaned and copied.");
- })
- .catch(() => {
- // Suppress errors for a smoother user experience
- });
- }, 500);
- }
- }
- }
- });
- })();
-
- (function() {
- // Initialize single global store for last selection
- window.selectAllLowestResponseState = window.selectAllLowestResponseState || {
- lastSelectedIndex: -1
- };
-
- document.addEventListener('keydown', function(e) {
- if (e.altKey && e.key === 'x') {
- e.preventDefault();
- // Delay execution to ensure DOM is fully loaded
- setTimeout(() => {
- try {
- const onlySelectAssistant = window.onlySelectAssistantCheckbox || false;
- const onlySelectUser = window.onlySelectUserCheckbox || false;
- const disableCopyAfterSelect = window.disableCopyAfterSelectCheckbox || false;
-
- const allConversationTurns = (() => {
- try {
- return Array.from(document.querySelectorAll('.user-turn, .agent-turn')) || [];
- } catch {
- return [];
- }
- })();
-
- const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
- const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
-
- const composerRect = (() => {
- try {
- const composerBackground = document.getElementById('composer-background');
- return composerBackground ? composerBackground.getBoundingClientRect() : null;
- } catch {
- return null;
- }
- })();
-
- const visibleTurns = allConversationTurns.filter(el => {
- const rect = el.getBoundingClientRect();
- const horizontallyInView = rect.left < viewportWidth && rect.right > 0;
- const verticallyInView = rect.top < viewportHeight && rect.bottom > 0;
- if (!horizontallyInView || !verticallyInView) return false;
-
- if (composerRect) {
- if (rect.top >= composerRect.top) {
- return false;
- }
- }
-
- return true;
- });
-
- const filteredVisibleTurns = (() => {
- if (onlySelectAssistant) {
- return visibleTurns.filter(el =>
- el.querySelector('[data-message-author-role="assistant"]')
- );
- }
- if (onlySelectUser) {
- return visibleTurns.filter(el =>
- el.querySelector('[data-message-author-role="user"]')
- );
- }
- return visibleTurns;
- })();
-
- if (filteredVisibleTurns.length === 0) return;
-
- filteredVisibleTurns.sort((a, b) => {
- const ra = a.getBoundingClientRect();
- const rb = b.getBoundingClientRect();
- return rb.top - ra.top;
- });
-
- const { lastSelectedIndex } = window.selectAllLowestResponseState;
- const nextIndex = (lastSelectedIndex + 1) % filteredVisibleTurns.length;
- const selectedTurn = filteredVisibleTurns[nextIndex];
- if (!selectedTurn) return;
-
- selectAndCopyMessage(selectedTurn);
- window.selectAllLowestResponseState.lastSelectedIndex = nextIndex;
-
- function selectAndCopyMessage(turnElement) {
- try {
- const userContainer = turnElement.querySelector('[data-message-author-role="user"]');
- const isUser = !!userContainer;
-
- if (isUser) {
- if (onlySelectAssistant) return;
- const userTextElement = userContainer.querySelector('.whitespace-pre-wrap');
- if (!userTextElement) return;
- doSelectAndCopy(userTextElement);
- } else {
- if (onlySelectUser) return;
- const assistantContainer = turnElement.querySelector('[data-message-author-role="assistant"]');
- let textElement = null;
- if (assistantContainer) {
- textElement = assistantContainer.querySelector('.prose') || assistantContainer;
- } else {
- textElement = turnElement.querySelector('.prose') || turnElement;
- }
- if (!textElement) return;
- doSelectAndCopy(textElement);
- }
- } catch {
- // Fail silently
- }
- }
-
- function doSelectAndCopy(el) {
- try {
- const selection = window.getSelection();
- if (!selection) return;
- selection.removeAllRanges();
-
- const range = document.createRange();
- range.selectNodeContents(el);
- selection.addRange(range);
-
- if (!disableCopyAfterSelect) {
- document.execCommand('copy');
- }
- } catch {
- // Fail silently
- }
- }
-
- } catch {
- // Fail silently
- }
- }, 50);
- }
- });
- })();
- // Existing script functionalities...
- (function() {
- const controlsNavId = 'controls-nav';
- const chatInputId = 'prompt-textarea';
-
- // Function to handle focusing and pasting into the chat input
- function handlePaste(e) {
- const chatInput = document.getElementById(chatInputId);
-
- if (chatInput && !document.activeElement.closest(`#${controlsNavId}`)) {
- // Focus the input if it is not already focused
- if (document.activeElement !== chatInput) {
- chatInput.focus();
- }
-
- // Use a small delay to ensure focus happens before insertion
- setTimeout(() => {
- // Prevent default paste action to manually handle paste
- e.preventDefault();
-
- // Obtain the pasted text
- const pastedData = (e.clipboardData || window.clipboardData).getData('text') || '';
- // Optionally append to the current value if needed, depending on cursor position logic
- const cursorPosition = chatInput.selectionStart;
- const textBefore = chatInput.value.substring(0, cursorPosition);
- const textAfter = chatInput.value.substring(cursorPosition);
-
- // Set the new value with pasted data
- chatInput.value = textBefore + pastedData + textAfter;
-
- // Move the cursor to the end of inserted data
- chatInput.selectionStart = chatInput.selectionEnd = cursorPosition + pastedData.length;
-
- // Trigger an 'input' event to ensure any form listeners react
- const inputEvent = new Event('input', { bubbles: true, cancelable: true });
- chatInput.dispatchEvent(inputEvent);
- }, 0);
- }
- }
-
- document.addEventListener('paste', function(e) {
- // Only handle paste if the focused element is not within #controls-nav
- if (!document.activeElement.closest(`#${controlsNavId}`)) {
- handlePaste(e);
- }
- });
- })();
-
- (function() {
- const controlsNavId = 'controls-nav';
- const chatInputId = 'prompt-textarea';
-
- document.addEventListener('keydown', function(e) {
- // Check if the pressed key is alphanumeric and neither Alt nor Ctrl (or Cmd) is pressed
- const isAlphanumeric = e.key.length === 1 && /[a-zA-Z0-9]/.test(e.key);
- const isModifierKeyPressed = e.altKey || e.ctrlKey || e.metaKey; // metaKey is for Cmd on Mac
-
- // Only handle keydown if the active element is not within #controls-nav
- if (!document.activeElement.closest(`#${controlsNavId}`) && isAlphanumeric && !isModifierKeyPressed) {
- const activeElement = document.activeElement;
-
- if (activeElement && activeElement.id !== chatInputId) {
- const chatInput = document.getElementById(chatInputId);
- if (chatInput) {
- // Prevent default to avoid unwanted actions
- e.preventDefault();
-
- // Focus the chat input
- chatInput.focus();
-
- // Optionally, append the typed character to the chat input's value
- chatInput.value += e.key;
- }
- }
- }
- });
- })();