LibreChat Shortcuts + Token Counter

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

目前为 2025-04-10 提交的版本。查看 最新版本

// ==UserScript==
// @name         LibreChat Shortcuts + Token Counter
// @namespace    http://tampermonkey.net/
// @version      2.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+w → Focus Chat Input 
// Alt+c → Click Copy on lowest message
// Alt+x → Select and copy, cycles visible essages
// 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 = 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);
        }
    });
})();

QingJ © 2025

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