您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Press Alt+S to click toggle-left-nav button on localhost:3080
当前为
// ==UserScript== // @name LibreChat Shortcuts + Hide Header + Token Counter // @namespace http://tampermonkey.net/ // @version 1.7 // @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) /*============================================================= = = = 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(); 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 = = = =============================================================*/ (function () { 'use strict'; // ~4 chars per token estimate const estimateTokens = (text) => Math.ceil((text || "").trim().length / 4); // Create the badge element 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"; // changed badge.style.color = "#a9a9a9"; badge.style.fontFamily = "monospace"; badge.style.userSelect = "none"; badge.style.alignSelf = "center"; badge.style.marginTop = "16px"; // moved down badge.textContent = "loading..."; // Insert the badge in the row with mic button 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 it's already inserted, skip if (flexRow.querySelector("#token-count-badge")) return true; // The mic button is our anchor const micButton = flexRow.querySelector('button[title="Use microphone"]'); if (!micButton) return false; // Insert the badge before the mic button flexRow.insertBefore(badge, micButton); return true; } // Figure out if a message is user or assistant 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"; // Fallback guess const all = Array.from(document.querySelectorAll('.message-render')); const index = all.indexOf(msgEl); return index % 2 === 0 ? "user" : "assistant"; } // Main logic: calculates turn-by-turn input and output function updateTokenCounts() { const messages = Array.from(document.querySelectorAll('.message-render')); if (!messages.length) { badge.textContent = "0 | 0 | ∑ 0 | $0.0000"; return; } // Build conversation array const conversation = messages.map(msg => ({ role: inferRole(msg), content: msg.innerText || "", tokens: estimateTokens(msg.innerText || "") })); let cumulativeInputTokens = 0; let cumulativeOutputTokens = 0; let priorContextTokens = 0; // track how many tokens are in all previous turns // Each user→assistant pair is a turn 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 userTokens = current.tokens; const assistantTokens = next.tokens; // For turn n, input = priorContextTokens + current user message const inputTokensThisTurn = priorContextTokens + userTokens; const outputTokensThisTurn = assistantTokens; cumulativeInputTokens += inputTokensThisTurn; cumulativeOutputTokens += outputTokensThisTurn; // Update prior context to include user + assistant from this turn priorContextTokens += userTokens + assistantTokens; // Skip the assistant in the loop i++; } } const totalTokens = cumulativeInputTokens + cumulativeOutputTokens; // Cost calculations // Input cost: $2.50 per 1M tokens // Output cost: $10.00 per 1M tokens const costInput = (cumulativeInputTokens / 1_000_000) * 2.50; const costOutput = (cumulativeOutputTokens / 1_000_000) * 10.00; const costTotal = costInput + costOutput; // Show: input | output | ∑ total | $X.XXXX badge.textContent = `${cumulativeInputTokens} | ${cumulativeOutputTokens} | ∑ ${totalTokens} | $${costTotal.toFixed(4)}`; } // Initialize, attach observer 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(); } // Kick off waitForLayoutAndInitialize(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址