// ==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);
}
});
})();