LibreChat Shortcuts + Token Counter

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

Version vom 31.03.2025. Aktuellste Version

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         LibreChat Shortcuts + Token Counter
// @namespace    http://tampermonkey.net/
// @version      1.9.0
// @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+A → Scroll up one message (.message-render)
// Alt+F → Scroll down one message (.message-render)

// Alt+r → refresh cost for conversation
// Alt+u → update the token cost per million

/*=============================================================
=                                                             =
=  IIFE #1 - Keyboard Shortcuts                               =
=                                                             =
=============================================================*/

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

    // Find the first `.message-render`, then climb up until we detect a scrollable ancestor
    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;
        }

        // If none found, just use the main document scroller
        return document.scrollingElement || document.documentElement;
    }

    function checkGSAP() {
        if (
            typeof window.gsap !== "undefined" &&
            typeof window.ScrollToPlugin !== "undefined" &&
            typeof window.Observer !== "undefined" &&
            typeof window.Flip !== "undefined"
        ) {
            window.gsap = gsap;
            window.ScrollToPlugin = ScrollToPlugin;
            window.Observer = Observer;
            window.Flip = Flip;

            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: 1.8,
            scrollTo: { y: 0 },
            ease: "power4.out"
        });
    }

    function scrollToBottom() {
        const container = getScrollableContainer();
        if (!container) return;
        gsap.to(container, {
            duration: 1.8,
            scrollTo: { y: "max" },
            ease: "power4.out"
        });
    }

    function scrollUpOneMessage() {
        const container = getScrollableContainer();
        if (!container) return;

        // Gather messages by .message-render
        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.8,
            scrollTo: { y: target?.offsetTop || 0 },
            ease: "power4.out"
        });
    }

    function scrollDownOneMessage() {
        const container = getScrollableContainer();
        if (!container) return;

        // Gather messages by .message-render
        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.8,
            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();    // Prevent Chrome's default shortcut
                e.stopPropagation();   // Optional: stop bubbling up

                switch (key) {
                    case 's': {
                        const toggleButton = document.getElementById('toggle-left-nav');
                        if (toggleButton) {
                            toggleButton.click();
                            console.log('🧭 Sidebar toggled');
                        }
                        break;
                    }
                    case 'n': {
                        const newChatButton = document.querySelector('button[aria-label="New chat"]');
                        if (newChatButton) {
                            newChatButton.click();
                            console.log('🆕 New chat opened');
                        }
                        break;
                    }
                    case 't':
                        scrollToTop();
                        break;
                    case 'z':
                        scrollToBottom();
                        break;
                    case 'a':
                        scrollUpOneMessage();
                        break;
                    case 'f':
                        scrollDownOneMessage();
                        break;
                }
            }
        });

        console.log("✅ LibreChat shortcuts active");
    }


    // 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 = 2.50;  // Cost per million tokens for input
        let COST_PER_MILLION_OUTPUT = 10.00; // Cost per million tokens for output


        const estimateTokens = (text) => Math.ceil((text || "").trim().length / 4);

        const badge = document.createElement("span");
        badge.id = "token-count-badge";
        badge.style.fontSize = "10px";
        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];
                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);