LibreChat Shortcuts + Token Counter

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

// ==UserScript==
// @name         LibreChat Shortcuts + Token Counter
// @namespace    http://tampermonkey.net/
// @version      2.6
// @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();
})();




(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() {
    function removeMarkdown(text) {
        return text
            // Remove bold/italics
            .replace(/(\*\*|__)(.*?)\1/g, "$2")
            .replace(/(\*|_)(.*?)\1/g, "$2")
            // Remove leading '#' from headers
            .replace(/^#{1,6}\s+(.*)/gm, "$1")
            // Preserve indentation for unordered list items
            .replace(/^(\s*)[\*\-\+]\s+(.*)/gm, "$1- $2")
            // Preserve indentation for ordered list items
            .replace(/^(\s*)(\d+)\.\s+(.*)/gm, "$1$2. $3")
            // Remove triple+ line breaks
            .replace(/\n{3,}/g, "\n\n")
            .trim();
    }

    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();

                setTimeout(() => {
                    if (!navigator.clipboard) return;

                    navigator.clipboard.readText()
                        .then(textContent => navigator.clipboard.writeText(removeMarkdown(textContent)))
                        .then(() => console.log("Markdown removed and copied."))
                        .catch(() => {});
                }, 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 manually pasting into the chat input
    function handlePaste(e) {
        const chatInput = document.getElementById(chatInputId);
        if (!chatInput) return;

        // 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') || '';

            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) {
        const activeElement = document.activeElement;

        // If currently focused on a textarea/input that is NOT our chat input, do nothing
        if (
            (activeElement.tagName.toLowerCase() === 'textarea' || activeElement.tagName.toLowerCase() === 'input') &&
            activeElement.id !== chatInputId
        ) {
            return;
        }

        // If currently within #controls-nav, do nothing
        if (activeElement.closest(`#${controlsNavId}`)) {
            return;
        }

        // Otherwise, handle the paste event
        handlePaste(e);
    });
})();

(function() {
    const controlsNavId = 'controls-nav';
    const chatInputId = 'prompt-textarea';

    document.addEventListener('keydown', function(e) {
        const activeElement = document.activeElement;

        // If focused on any other textarea/input besides our chat input, do nothing
        if (
            (activeElement.tagName.toLowerCase() === 'textarea' || activeElement.tagName.toLowerCase() === 'input') &&
            activeElement.id !== chatInputId
        ) {
            return;
        }

        // If currently within #controls-nav, do nothing
        if (activeElement.closest(`#${controlsNavId}`)) {
            return;
        }

        // Check if the pressed key is alphanumeric and no modifier keys are pressed
        const isAlphanumeric = e.key.length === 1 && /[a-zA-Z0-9]/.test(e.key);
        const isModifierKeyPressed = e.altKey || e.ctrlKey || e.metaKey; // metaKey for Cmd on Mac

        if (isAlphanumeric && !isModifierKeyPressed) {
            const chatInput = document.getElementById(chatInputId);
            if (!chatInput) return;

            // If we're not already in our chat input, focus it and add the character
            if (activeElement !== chatInput) {
                e.preventDefault();
                chatInput.focus();
                chatInput.value += e.key;
            }
        }
    });
})();






/*=============================================================
=                                                             =
=  Token counter IIFE                                         =
=                                                             =
=============================================================*/
(function(){
  'use strict';

  // ——— Keys & defaults ———
  const COST_IN_KEY          = 'costInput';
  const COST_OUT_KEY         = 'costOutput';
  const CPT_KEY              = 'charsPerToken';
  let costIn       = parseFloat(localStorage.getItem(COST_IN_KEY))  || 2.50;
  let costOut      = parseFloat(localStorage.getItem(COST_OUT_KEY)) || 10.00;
  let charsPerTok  = parseFloat(localStorage.getItem(CPT_KEY))     || 3.8;
  const OVERHEAD   = 3; // tokens per message overhead

  // ——— Estimator ———
  function estTok(text){
    return Math.ceil((text.trim().length||0)/charsPerTok) + OVERHEAD;
  }

  // ——— UI: badge + refresh button ———
  const badge = document.createElement('span');
  badge.id = 'token-count-badge';
  Object.assign(badge.style, {
    fontSize:'8px', padding:'1px 0 0 6px', borderRadius:'8px',
    background:'transparent', color:'#a9a9a9',
    fontFamily:'monospace', userSelect:'none',
    alignSelf:'center', marginTop:'16px',
    display:'inline-flex', alignItems:'center'
  });

  const refreshBtn = document.createElement('button');
  refreshBtn.textContent = '↻';
  refreshBtn.title   = 'Refresh token count';
  Object.assign(refreshBtn.style, {
    marginLeft:'6px', cursor:'pointer', fontSize:'10px',
    border:'none', background:'transparent',
    color:'#a9a9a9', userSelect:'none',
    fontFamily:'monospace', padding:'0'
  });
  refreshBtn.addEventListener('click', ()=>{
    flash(refreshBtn);
    updateCounts();
  });
  badge.appendChild(refreshBtn);

  function flash(el){
    el.style.transition = 'transform 0.15s';
    el.style.transform  = 'scale(1.4)';
    setTimeout(()=> el.style.transform = 'scale(1)', 150);
  }

  // ——— Inject badge in the “flex row” before mic button ———
  function insertBadge(retries=20){
    const rows = [...document.querySelectorAll('div.flex')];
    const flexRow = rows.find(el =>
      el.classList.contains('items-between') &&
      el.classList.contains('pb-2')
    );
    if(!flexRow){
      if(retries>0) setTimeout(()=> insertBadge(retries-1), 500);
      return null;
    }
    if(!flexRow.querySelector('#token-count-badge')){
      const mic = flexRow.querySelector('button[title="Use microphone"]');
      flexRow.insertBefore(badge, mic);
    }
    return flexRow.parentElement;
  }

  // ——— Role inference ———
  function inferRole(msgEl){
    const wrapper = msgEl.closest('.group, .message');
    if(wrapper?.classList.contains('user'))      return 'user';
    if(wrapper?.classList.contains('assistant')) return 'assistant';
    const all = [...document.querySelectorAll('.message-render')];
    return all.indexOf(msgEl)%2===0 ? 'user' : 'assistant';
  }

  // ——— Core update logic ———
  function updateCounts(){
    const msgs = [...document.querySelectorAll('.message-render')];
    if(!msgs.length){
      badge.textContent = '0 | 0 | ∑ 0 | $0.0000';
      badge.appendChild(refreshBtn);
      return;
    }
    const convo = msgs.map(m => ({
      role: inferRole(m),
      t:    estTok(m.innerText||'')
    }));
    let inSum=0, outSum=0;
    for(let i=0;i<convo.length;i++){
      if(convo[i].role==='user'){
        inSum += convo.slice(0,i+1).reduce((a,b)=>a+b.t,0);
        const ai = convo.findIndex((c,j)=>j>i&&c.role==='assistant');
        if(ai>i) outSum += convo[ai].t;
      }
    }
    const total = inSum+outSum;
    const cost  = ((inSum/1e6)*costIn + (outSum/1e6)*costOut).toFixed(4);
    badge.textContent = `${inSum} @ $${costIn}/M | ${outSum} @ $${costOut}/M | ∑ ${total} | $${cost}`;
    badge.appendChild(refreshBtn);
  }

  // ——— Debounce for MutationObserver ———
  let debounceTimer=null;
  function scheduleUpdate(){
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(updateCounts, 200);
  }

  // ——— Hook send actions for immediate update ———
  function attachSendHooks(){
    const ta = document.querySelector('textarea');
    if(ta && !ta.dataset.tcHooked){
      ta.dataset.tcHooked = 'y';
      ta.addEventListener('keydown', e=>{
        if(e.key==='Enter' && !e.shiftKey && !e.altKey && !e.metaKey){
          scheduleUpdate();
        }
      });
    }
    const send = document.querySelector('button[type="submit"], button[title="Send"]');
    if(send && !send.dataset.tcHooked){
      send.dataset.tcHooked = 'y';
      send.addEventListener('click', ()=> scheduleUpdate());
    }
  }

  // ——— Initialization ———
  function init(){
    const container = insertBadge();
    if(!container) return;
    // observe only the messages container
    const msgRoot = container.querySelector('.message-render')?.parentElement || container;
    new MutationObserver(scheduleUpdate)
      .observe(msgRoot, { childList:true, subtree:true });
    attachSendHooks();
    // reattach hooks if textarea/send are re-rendered
    new MutationObserver(attachSendHooks)
      .observe(document.body, { childList:true, subtree:true });
    updateCounts();
  }

  // ——— Config shortcut (Alt+U) ———
  document.addEventListener('keydown', e=>{
    if(e.altKey && !e.repeat && e.key.toLowerCase()==='u'){
      e.preventDefault();
      const resp = prompt(
        'Set costs and chars/token:\ninput $/M,output $/M,chars/token',
        `${costIn},${costOut},${charsPerTok}`
      );
      if(!resp) return;
      const [ci,co,cpt] = resp.split(',').map(Number);
      if([ci,co,cpt].every(v=>isFinite(v))){
        costIn = ci; costOut = co; charsPerTok = cpt;
        localStorage.setItem(COST_IN_KEY,ci);
        localStorage.setItem(COST_OUT_KEY,co);
        localStorage.setItem(CPT_KEY,cpt);
        updateCounts();
      } else alert('Invalid numbers');
    }
  });

  // delay to let page render
  setTimeout(init, 1000);

})();