// ==UserScript==
// @name LibreChat Shortcuts + Token Counter
// @namespace http://tampermonkey.net/
// @version 2.6
// @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 messages
// Alt+A → Scroll up one message (.message-render)
// Alt+F → Scroll down one message (.message-render)
// Just start typing to go to input chatbox
// Paste input when not in chat box
// Alt+R → refresh cost for conversation
// Alt+U → update the token cost per million
(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;
}
}
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;
}
return document.scrollingElement || document.documentElement;
}
function checkGSAP() {
if (
typeof window.gsap !== "undefined" &&
typeof window.ScrollToPlugin !== "undefined" &&
typeof window.Observer !== "undefined" &&
typeof window.Flip !== "undefined"
) {
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: .6,
scrollTo: { y: 0 },
ease: "power4.out"
});
}
function scrollToBottom() {
const container = getScrollableContainer();
if (!container) return;
gsap.to(container, {
duration: .6,
scrollTo: { y: "max" },
ease: "power4.out"
});
}
function scrollUpOneMessage() {
const container = getScrollableContainer();
if (!container) return;
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.6,
scrollTo: { y: target?.offsetTop || 0 },
ease: "power4.out"
});
}
function scrollDownOneMessage() {
const container = getScrollableContainer();
if (!container) return;
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.6,
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();
e.stopPropagation();
switch (key) {
case 's': toggleSidebar(); break;
case 'n': openNewChat(); break;
case 't': scrollToTop(); break;
case 'z': scrollToBottom(); break;
case 'a': scrollUpOneMessage(); break;
case 'f': scrollDownOneMessage(); break;
}
}
});
console.log("✅ LibreChat shortcuts active");
}
function toggleSidebar() {
const toggleButton = document.getElementById('toggle-left-nav');
if (toggleButton) {
toggleButton.click();
console.log('🧭 Sidebar toggled');
}
}
function openNewChat() {
const newChatButton = document.querySelector('button[aria-label="New chat"]');
if (newChatButton) {
newChatButton.click();
console.log('🆕 New chat opened');
}
}
// Start loading GSAP plugins and wait for them
loadGSAPLibraries();
})();
(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() {
function removeMarkdown(text) {
return text
// Remove bold/italics
.replace(/(\*\*|__)(.*?)\1/g, "$2")
.replace(/(\*|_)(.*?)\1/g, "$2")
// Remove leading '#' from headers
.replace(/^#{1,6}\s+(.*)/gm, "$1")
// Preserve indentation for unordered list items
.replace(/^(\s*)[\*\-\+]\s+(.*)/gm, "$1- $2")
// Preserve indentation for ordered list items
.replace(/^(\s*)(\d+)\.\s+(.*)/gm, "$1$2. $3")
// Remove triple+ line breaks
.replace(/\n{3,}/g, "\n\n")
.trim();
}
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();
setTimeout(() => {
if (!navigator.clipboard) return;
navigator.clipboard.readText()
.then(textContent => navigator.clipboard.writeText(removeMarkdown(textContent)))
.then(() => console.log("Markdown removed and copied."))
.catch(() => {});
}, 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);
}
});
})();
// Existing script functionalities...
(function() {
const controlsNavId = 'controls-nav';
const chatInputId = 'prompt-textarea';
// Function to handle focusing and manually pasting into the chat input
function handlePaste(e) {
const chatInput = document.getElementById(chatInputId);
if (!chatInput) return;
// Focus the input if it is not already focused
if (document.activeElement !== chatInput) {
chatInput.focus();
}
// Use a small delay to ensure focus happens before insertion
setTimeout(() => {
// Prevent default paste action to manually handle paste
e.preventDefault();
// Obtain the pasted text
const pastedData = (e.clipboardData || window.clipboardData).getData('text') || '';
const cursorPosition = chatInput.selectionStart;
const textBefore = chatInput.value.substring(0, cursorPosition);
const textAfter = chatInput.value.substring(cursorPosition);
// Set the new value with pasted data
chatInput.value = textBefore + pastedData + textAfter;
// Move the cursor to the end of inserted data
chatInput.selectionStart = chatInput.selectionEnd = cursorPosition + pastedData.length;
// Trigger an 'input' event to ensure any form listeners react
const inputEvent = new Event('input', { bubbles: true, cancelable: true });
chatInput.dispatchEvent(inputEvent);
}, 0);
}
document.addEventListener('paste', function(e) {
const activeElement = document.activeElement;
// If currently focused on a textarea/input that is NOT our chat input, do nothing
if (
(activeElement.tagName.toLowerCase() === 'textarea' || activeElement.tagName.toLowerCase() === 'input') &&
activeElement.id !== chatInputId
) {
return;
}
// If currently within #controls-nav, do nothing
if (activeElement.closest(`#${controlsNavId}`)) {
return;
}
// Otherwise, handle the paste event
handlePaste(e);
});
})();
(function() {
const controlsNavId = 'controls-nav';
const chatInputId = 'prompt-textarea';
document.addEventListener('keydown', function(e) {
const activeElement = document.activeElement;
// If focused on any other textarea/input besides our chat input, do nothing
if (
(activeElement.tagName.toLowerCase() === 'textarea' || activeElement.tagName.toLowerCase() === 'input') &&
activeElement.id !== chatInputId
) {
return;
}
// If currently within #controls-nav, do nothing
if (activeElement.closest(`#${controlsNavId}`)) {
return;
}
// Check if the pressed key is alphanumeric and no modifier keys are pressed
const isAlphanumeric = e.key.length === 1 && /[a-zA-Z0-9]/.test(e.key);
const isModifierKeyPressed = e.altKey || e.ctrlKey || e.metaKey; // metaKey for Cmd on Mac
if (isAlphanumeric && !isModifierKeyPressed) {
const chatInput = document.getElementById(chatInputId);
if (!chatInput) return;
// If we're not already in our chat input, focus it and add the character
if (activeElement !== chatInput) {
e.preventDefault();
chatInput.focus();
chatInput.value += e.key;
}
}
});
})();
/*=============================================================
= =
= Token counter IIFE =
= =
=============================================================*/
(function(){
'use strict';
// ——— Keys & defaults ———
const COST_IN_KEY = 'costInput';
const COST_OUT_KEY = 'costOutput';
const CPT_KEY = 'charsPerToken';
let costIn = parseFloat(localStorage.getItem(COST_IN_KEY)) || 2.50;
let costOut = parseFloat(localStorage.getItem(COST_OUT_KEY)) || 10.00;
let charsPerTok = parseFloat(localStorage.getItem(CPT_KEY)) || 3.8;
const OVERHEAD = 3; // tokens per message overhead
// ——— Estimator ———
function estTok(text){
return Math.ceil((text.trim().length||0)/charsPerTok) + OVERHEAD;
}
// ——— UI: badge + refresh button ———
const badge = document.createElement('span');
badge.id = 'token-count-badge';
Object.assign(badge.style, {
fontSize:'8px', padding:'1px 0 0 6px', borderRadius:'8px',
background:'transparent', color:'#a9a9a9',
fontFamily:'monospace', userSelect:'none',
alignSelf:'center', marginTop:'16px',
display:'inline-flex', alignItems:'center'
});
const refreshBtn = document.createElement('button');
refreshBtn.textContent = '↻';
refreshBtn.title = 'Refresh token count';
Object.assign(refreshBtn.style, {
marginLeft:'6px', cursor:'pointer', fontSize:'10px',
border:'none', background:'transparent',
color:'#a9a9a9', userSelect:'none',
fontFamily:'monospace', padding:'0'
});
refreshBtn.addEventListener('click', ()=>{
flash(refreshBtn);
updateCounts();
});
badge.appendChild(refreshBtn);
function flash(el){
el.style.transition = 'transform 0.15s';
el.style.transform = 'scale(1.4)';
setTimeout(()=> el.style.transform = 'scale(1)', 150);
}
// ——— Inject badge in the “flex row” before mic button ———
function insertBadge(retries=20){
const rows = [...document.querySelectorAll('div.flex')];
const flexRow = rows.find(el =>
el.classList.contains('items-between') &&
el.classList.contains('pb-2')
);
if(!flexRow){
if(retries>0) setTimeout(()=> insertBadge(retries-1), 500);
return null;
}
if(!flexRow.querySelector('#token-count-badge')){
const mic = flexRow.querySelector('button[title="Use microphone"]');
flexRow.insertBefore(badge, mic);
}
return flexRow.parentElement;
}
// ——— Role inference ———
function inferRole(msgEl){
const wrapper = msgEl.closest('.group, .message');
if(wrapper?.classList.contains('user')) return 'user';
if(wrapper?.classList.contains('assistant')) return 'assistant';
const all = [...document.querySelectorAll('.message-render')];
return all.indexOf(msgEl)%2===0 ? 'user' : 'assistant';
}
// ——— Core update logic ———
function updateCounts(){
const msgs = [...document.querySelectorAll('.message-render')];
if(!msgs.length){
badge.textContent = '0 | 0 | ∑ 0 | $0.0000';
badge.appendChild(refreshBtn);
return;
}
const convo = msgs.map(m => ({
role: inferRole(m),
t: estTok(m.innerText||'')
}));
let inSum=0, outSum=0;
for(let i=0;i<convo.length;i++){
if(convo[i].role==='user'){
inSum += convo.slice(0,i+1).reduce((a,b)=>a+b.t,0);
const ai = convo.findIndex((c,j)=>j>i&&c.role==='assistant');
if(ai>i) outSum += convo[ai].t;
}
}
const total = inSum+outSum;
const cost = ((inSum/1e6)*costIn + (outSum/1e6)*costOut).toFixed(4);
badge.textContent = `${inSum} @ $${costIn}/M | ${outSum} @ $${costOut}/M | ∑ ${total} | $${cost}`;
badge.appendChild(refreshBtn);
}
// ——— Debounce for MutationObserver ———
let debounceTimer=null;
function scheduleUpdate(){
clearTimeout(debounceTimer);
debounceTimer = setTimeout(updateCounts, 200);
}
// ——— Hook send actions for immediate update ———
function attachSendHooks(){
const ta = document.querySelector('textarea');
if(ta && !ta.dataset.tcHooked){
ta.dataset.tcHooked = 'y';
ta.addEventListener('keydown', e=>{
if(e.key==='Enter' && !e.shiftKey && !e.altKey && !e.metaKey){
scheduleUpdate();
}
});
}
const send = document.querySelector('button[type="submit"], button[title="Send"]');
if(send && !send.dataset.tcHooked){
send.dataset.tcHooked = 'y';
send.addEventListener('click', ()=> scheduleUpdate());
}
}
// ——— Initialization ———
function init(){
const container = insertBadge();
if(!container) return;
// observe only the messages container
const msgRoot = container.querySelector('.message-render')?.parentElement || container;
new MutationObserver(scheduleUpdate)
.observe(msgRoot, { childList:true, subtree:true });
attachSendHooks();
// reattach hooks if textarea/send are re-rendered
new MutationObserver(attachSendHooks)
.observe(document.body, { childList:true, subtree:true });
updateCounts();
}
// ——— Config shortcut (Alt+U) ———
document.addEventListener('keydown', e=>{
if(e.altKey && !e.repeat && e.key.toLowerCase()==='u'){
e.preventDefault();
const resp = prompt(
'Set costs and chars/token:\ninput $/M,output $/M,chars/token',
`${costIn},${costOut},${charsPerTok}`
);
if(!resp) return;
const [ci,co,cpt] = resp.split(',').map(Number);
if([ci,co,cpt].every(v=>isFinite(v))){
costIn = ci; costOut = co; charsPerTok = cpt;
localStorage.setItem(COST_IN_KEY,ci);
localStorage.setItem(COST_OUT_KEY,co);
localStorage.setItem(CPT_KEY,cpt);
updateCounts();
} else alert('Invalid numbers');
}
});
// delay to let page render
setTimeout(init, 1000);
})();