您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
(我已经安装了用户样式管理器,让我安装!)
// ==UserScript==
// @name LibreChat Shortcuts + Hide Header + Token Counter
// @namespace http://tampermonkey.net/
// @version 1.8
// @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();
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 #3 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();
})();