// ==UserScript==
// @name LibreChat Shortcuts + Token Counter
// @namespace http://tampermonkey.net/
// @version 2.8.5
// @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
// Alt+e → toggle collapse expand chat
// alt+d → Open the preset menu to see the "defaults"
// alt+# 1-9 to activate presets
// Click right sidebar toggle with alt+g for game settings
// other fixes
// label presets with alt+1 to alt+9
// Convert <br> in tables displaying as literal <br> to line breaks
// toggle footer text
(function () {
/* Creates a global CSS rule that hides:
1. Anything with role="contentinfo"
2. Any <button> whose aria-label is exactly "Code Interpreter" */
const style = document.createElement('style');
style.textContent = `
[role="contentinfo"],
button[aria-label="Code Interpreter"] {
display: none !important;
}
`;
document.head.appendChild(style);
})();
(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 selectors = [
'[data-testid="close-sidebar-button"]',
'[data-testid="open-sidebar-button"]'
];
for (const selector of selectors) {
const btn = document.querySelector(selector);
if (btn) {
btn.click();
console.log(`🧭 Sidebar toggled via ${selector}`);
return;
}
}
console.warn('⚠️ No sidebar toggle button found');
}
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';
}
// ——— STORE NUMBERS FOR GSAP ANIMATIONS ———
const lastValues = {
inSum: 0,
outSum: 0,
total: 0,
cost: 0
};
// ——— Formatting helper ———
function formatBadgeText({ inSum, outSum, total, cost }) {
return `${Math.round(inSum)} @ $${costIn}/M | ${Math.round(outSum)} @ $${costOut}/M | ∑ ${Math.round(total)} | $${cost.toFixed(4)}`;
}
// ——— updateCounts WITH GSAP ———
function updateCounts() {
const msgs = [...document.querySelectorAll('.message-render')];
if (!msgs.length) {
lastValues.inSum = 0;
lastValues.outSum = 0;
lastValues.total = 0;
lastValues.cost = 0;
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;
gsap.to(lastValues, {
inSum, outSum, total, cost,
duration: 0.7,
ease: "power1.out",
onUpdate: () => {
badge.textContent = formatBadgeText(lastValues);
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);
})();
// // // // // //
// Convert <br> in tables displaying as literal <br> to line breaks
// // // // // //
(function () {
'use strict';
const BR_ENTITY_REGEX = /<br\s*\/?>/gi;
function fixBrsInMarkdown() {
document.querySelectorAll('div.markdown').forEach(container => {
container.querySelectorAll('td, th, p, li, div').forEach(el => {
if (el.innerHTML.includes('<br')) {
el.innerHTML = el.innerHTML.replace(BR_ENTITY_REGEX, '<br>');
}
});
});
}
// Run once in case content is already loaded
fixBrsInMarkdown();
// Watch for content changes
const observer = new MutationObserver(() => fixBrsInMarkdown());
observer.observe(document.body, {
childList: true,
subtree: true,
});
})();
// Alt+e → toggle collapse expand chat
(function() {
document.addEventListener('keydown', function(e) {
if (e.altKey && e.key.toLowerCase() === 'e') {
e.preventDefault();
const collapseBtn = document.querySelector('button[aria-label="Collapse Chat"]');
if (collapseBtn) {
collapseBtn.click();
return;
}
const expandBtn = document.querySelector('button[aria-label="Expand Chat"]');
if (expandBtn) expandBtn.click();
}
});
})();
// alt+1 to alt+9 to select presets
// alt+1 to alt+9 to select presets
(() => {
const synthClick = el => {
if (!el) return;
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
el.focus?.();
};
function isMenuOpen() {
// This assumes the menu creates at least one .[role="listbox"] when open, adjust selector if needed
// Also checks for at least one preset option in the DOM
return !!document.querySelector('div[role="option"][data-testid^="preset-item"]');
}
function handleAltDigit(ev) {
if (!ev.altKey || !/^Digit[1-9]$/.test(ev.code)) return;
ev.preventDefault();
ev.stopPropagation();
const idx = +ev.code.slice(-1) - 1;
const btn = document.getElementById('presets-button');
if (!btn) return console.warn('[Preset-helper] #presets-button not found');
// Only click the button if the menu is not already open
const alreadyOpen = isMenuOpen();
if (!alreadyOpen) btn.click();
// If menu was *just* opened, may need small delay for items to render
const delay = alreadyOpen ? 0 : 500;
setTimeout(() => {
const items = Array.from(
document.querySelectorAll('div[role="option"][data-testid^="preset-item"]')
);
if (items[idx]) synthClick(items[idx]);
else console.warn('[Preset-helper] Preset item not available at index', idx);
}, delay);
}
window.addEventListener('keydown', handleAltDigit, true);
})();
// Label the presets
(() => {
/* ——— simple style for the tiny label ——— */
const style = document.createElement('style');
style.textContent = `
.alt-hint {
font-size: 12px; /* small text */
opacity: .5; /* 50 % opacity */
margin-left: 4px; /* a little gap */
pointer-events: none; /* never blocks clicks */
user-select: none;
}`;
document.head.appendChild(style);
const ITEM_SELECTOR = 'div[role="option"][data-testid^="preset-item"]';
const MAX_DIGITS = 9; // Alt+1 … Alt+9
/** add the hint to each item (if not already present) */
const addHints = () => {
[...document.querySelectorAll(ITEM_SELECTOR)]
.slice(0, MAX_DIGITS)
.forEach((el, i) => {
if (el.querySelector('.alt-hint')) return; // only once
const span = document.createElement('span');
span.className = 'alt-hint';
span.textContent = `Alt+${i + 1}`;
el.appendChild(span);
});
};
/* run once right now (in case the menu is already open) */
addHints();
/* keep watching for future openings of the menu */
const mo = new MutationObserver(addHints);
mo.observe(document.body, { childList: true, subtree: true });
})();
// alt+w → Open the preset menu
(() => {
'use strict';
window.addEventListener('keydown', handleAltP, true);
const openPresetMenu = () => {
const btn = document.getElementById('presets-button');
if (!btn) {
console.log('[Preset-helper] couldn’t find #presets-button');
return false;
}
btn.click();
return true;
};
function handleAltP(e) {
if (e.altKey && e.code === 'KeyW') {
e.preventDefault();
e.stopPropagation();
openPresetMenu();
}
}
})();
// Click right sidebar toggle with alt+g for game settings
(function() {
document.addEventListener('keydown', function(e) {
// Only proceed if ALT+G is pressed
if (!e.altKey || e.key.toLowerCase() !== 'g') return;
const nav = document.querySelector('nav[aria-label="Controls"][role="navigation"]');
const width = nav ? nav.getBoundingClientRect().width : 0;
if (width > 100) {
// Panel is open: click "Hide Panel" button
const hideBtn = [...document.querySelectorAll('button')].find(
b => b.textContent.trim().toLowerCase().includes('hide panel')
);
if (hideBtn) hideBtn.click();
} else {
// Panel is closed: click toggle-right-nav, then wait for "Parameters" button to appear
const toggleBtn = document.getElementById('toggle-right-nav');
if (toggleBtn) {
toggleBtn.click();
const maxRetryTime = 5000; // how long to wait, in ms
let elapsed = 0;
const interval = 100;
const intervalId = setInterval(() => {
elapsed += interval;
// Find a button containing the text "Parameters"
const paramsBtn = [...document.querySelectorAll('button')]
.find(b => b.textContent.trim().toLowerCase().includes('parameters'));
if (paramsBtn) {
clearInterval(intervalId);
paramsBtn.click();
} else if (elapsed >= maxRetryTime) {
clearInterval(intervalId);
console.warn("Parameters button not found within time limit.");
}
}, interval);
}
}
});
})();
// Make the preset dialogue 100% viewport height
// Make the preset dialogue 100% viewport height
(function() {
const style = document.createElement('style');
style.textContent = `
[data-side="bottom"][data-align="center"][data-state="open"] {
max-height: 100% !important;
height: 90vh !important;
overflow: auto;
}
.preset-name {
font-weight: bold;
color: #f9cc87; /* electric orange */
font-size: 115% !important;
}
.preset-number {
color: #bdccff; /* baby/light blue */
margin-right: 6px;
}
`;
document.head.appendChild(style);
function highlightPresetNames(container) {
const textDivs = container.querySelectorAll('div.text-xs');
let counter = 1;
textDivs.forEach(div => {
const textNode = Array.from(div.childNodes).find(node => node.nodeType === Node.TEXT_NODE && node.nodeValue.includes(':'));
if (textNode) {
const match = textNode.nodeValue.match(/^(.*?):\s*(.*)$/s);
if (match) {
const beforeColon = match[1];
const afterColon = match[2];
div.innerHTML = `
<span class="preset-name">
<span class="preset-number">${counter}.</span>${beforeColon}
</span>(${afterColon.trim()})
`.trim();
counter++;
}
}
});
}
function runHighlight() {
const container = document.querySelector('[data-side="bottom"][data-align="center"][data-state="open"]');
if (container) {
highlightPresetNames(container);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', runHighlight);
} else {
runHighlight();
}
const observer = new MutationObserver(() => runHighlight());
observer.observe(document.body, { childList: true, subtree: true });
})();
// Insert stop dictation button
(async function () {
// 1. Inject Google Material Icons (if not already present)
if (!document.querySelector('link[href*="fonts.googleapis.com/icon?family=Material+Icons"]')) {
const link = document.createElement('link');
link.href = 'https://fonts.googleapis.com/icon?family=Material+Icons';
link.rel = 'stylesheet';
document.head.appendChild(link);
}
// 2. Dynamically import SpeechRecognition
let SpeechRecognition;
try {
SpeechRecognition = (await import('https://cdn.jsdelivr.net/npm/[email protected]/dist/index.umd.min.js')).default;
} catch (e) {
console.error('Failed to import SpeechRecognition:', e);
return;
}
// 3. Poll DOM for the microphone button
const interval = setInterval(() => {
const micBtn = document.querySelector('#audio-recorder');
if (!micBtn || document.querySelector('#stop-dictation')) return;
// 4. Create Stop Dictation button
const stopBtn = document.createElement('button');
stopBtn.id = 'stop-dictation';
stopBtn.title = 'Stop Dictation';
stopBtn.setAttribute('aria-label', 'Stop Dictation');
stopBtn.style.marginLeft = '6px';
stopBtn.className = 'cursor-pointer flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover';
stopBtn.innerHTML = `<span class="material-icons" style="font-size: 24px; color: red;">stop_circle</span>`;
// 5. Insert after mic button
micBtn.parentNode.insertBefore(stopBtn, micBtn.nextSibling);
// 6. Attach event
stopBtn.addEventListener('click', async () => {
try {
await SpeechRecognition.stopListening();
console.log('Speech recognition manually stopped.');
} catch (err) {
console.error('Failed to stop speech recognition:', err);
}
});
clearInterval(interval);
}, 500);
})();
// This script injects a CSS rule into the current page
(function() {
const style = document.createElement('style');
style.textContent = `
/* 2. make sure every normal text line looks reasonable */
.markdown.prose.message-content,
.markdown.prose.message-content p,
.markdown.prose.message-content li {
line-height: 1.45 !important; /* ≈ 23px on a 16px font */
}
/* hide the share button */
#export-menu-button {
display:none;
}
`;
document.head.appendChild(style);
})();
// alt+r toggles hiding the top bar in narrow mode
(() => {
console.log("Content script loaded: debug version");
// The CSS selector suspected to match the target
const SELECTOR = ".bg-token-main-surface-primary.sticky.top-0.z-10.flex.min-h-\\[40px\\].items-center.justify-center.bg-white.pl-1.dark\\:bg-gray-800.dark\\:text-white.md\\:hidden";
function debugQuerySelector() {
const foundElem = document.querySelector(SELECTOR);
console.log("Debug: querySelector returned:", foundElem);
return foundElem;
}
// Immediately check if the element is found at load
debugQuerySelector();
const STYLE_ID = "ext-hide-bg-token-main-surface-primary";
const cssRule = `${SELECTOR} { display: none !important; }`;
function addStyle() {
if (!document.getElementById(STYLE_ID)) {
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = cssRule;
document.head.appendChild(style);
}
}
function removeStyle() {
const existing = document.getElementById(STYLE_ID);
if (existing) {
existing.remove();
}
}
function isHidden() {
return !!document.getElementById(STYLE_ID);
}
function toggleStyle() {
console.log("Toggle requested...");
if (isHidden()) {
removeStyle();
console.log("Removed style tag, showing element again.");
} else {
addStyle();
console.log("Added style tag, hiding element.");
}
}
// Hide the element by default at page load
addStyle();
document.addEventListener("keydown", (e) => {
// Log all key presses for debugging
console.log(`Key pressed: ${e.key}, altKey = ${e.altKey}`);
// ALT+R, ignoring if user is typing in an input/textarea
if (
e.altKey &&
e.key.toLowerCase() === "r" &&
!["INPUT", "TEXTAREA"].includes(document.activeElement.tagName)
) {
e.preventDefault();
toggleStyle();
// Check again if the element is found after toggling
debugQuerySelector();
}
});
})();
// Change multi conversation split icon to an intuitive icon
(() => {
// 1. Inject Material Symbols stylesheet if not already present
if (!document.querySelector('link[href*="fonts.googleapis.com"][href*="Material+Symbols"]')) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200&icon_names=alt_route,star';
document.head.appendChild(link);
}
// 2. Replace SVG in #add-multi-conversation-button with "alt_route" icon
function replaceAddButtonIcon() {
const buttonDiv = document.querySelector('#add-multi-conversation-button');
if (buttonDiv && !buttonDiv.querySelector('.material-symbols-outlined')) {
const svg = buttonDiv.querySelector('svg');
if (svg) {
const iconSpan = document.createElement('span');
iconSpan.className = 'material-symbols-outlined';
iconSpan.textContent = 'alt_route';
iconSpan.style.fontSize = '16px';
iconSpan.style.width = '16px';
iconSpan.style.height = '16px';
iconSpan.style.display = 'inline-flex';
iconSpan.style.alignItems = 'center';
iconSpan.style.justifyContent = 'center';
svg.replaceWith(iconSpan);
}
}
}
// 3. Replace all .lucide-book-copy[aria-label="Preset Icon"] SVGs with "star" icon
function replaceBookCopyIcons() {
const svgs = document.querySelectorAll('svg.lucide-book-copy[aria-label="Preset Icon"]');
svgs.forEach(svg => {
if (svg.dataset.replacedWithMaterialSymbol) return;
const iconSpan = document.createElement('span');
iconSpan.className = 'material-symbols-outlined';
iconSpan.textContent = 'star';
iconSpan.style.fontSize = '16px';
iconSpan.style.width = '16px';
iconSpan.style.height = '16px';
iconSpan.style.display = 'inline-flex';
iconSpan.style.alignItems = 'center';
iconSpan.style.justifyContent = 'center';
svg.dataset.replacedWithMaterialSymbol = "true";
svg.replaceWith(iconSpan);
});
}
// 4. Composite function
function replaceIcons() {
replaceAddButtonIcon();
replaceBookCopyIcons();
}
// 5. Debounce utility
function debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
const debouncedReplaceIcons = debounce(replaceIcons, 100);
// 6. MutationObserver with debounce, observing entire body
const observer = new MutationObserver(debouncedReplaceIcons);
observer.observe(document.body, { subtree: true, childList: true });
// 7. Initial run on DOMContentLoaded or immediately
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", replaceIcons);
} else {
replaceIcons();
}
})();
// trigger stop with control+backspace and send with control+enter and control+; clicks stop then regenerate (or just regenerate if stop isn't available).
(function () {
// Utility: is element in viewport
function isElementInViewport(el) {
const rect = el.getBoundingClientRect();
return (
rect.width > 0 &&
rect.height > 0 &&
rect.bottom > 0 &&
rect.top < window.innerHeight &&
rect.left < window.innerWidth &&
rect.right > 0
);
}
// Utility: find lowest visible Regenerate button
function getLowestVisibleRegenerateBtn() {
const regenBtns = Array.from(document.querySelectorAll('button[title="Regenerate"]:not([disabled])'));
const visibleBtns = regenBtns.filter(isElementInViewport);
if (visibleBtns.length === 0) return null;
// Find the one with the largest .getBoundingClientRect().top
return visibleBtns.reduce((lowest, btn) => {
const currTop = btn.getBoundingClientRect().top;
const lowestTop = lowest.getBoundingClientRect().top;
return currTop > lowestTop ? btn : lowest;
}, visibleBtns[0]);
}
document.addEventListener('keydown', function (e) {
// Allow shortcuts even in input/textarea/CEs
// Ctrl+Backspace - STOP
if (e.ctrlKey && e.key === 'Backspace') {
e.preventDefault();
const stopBtn = document.querySelector('button[aria-label="Stop generating"]:not([disabled])');
if (stopBtn) stopBtn.click();
return;
}
// Ctrl+Enter - SEND
if (e.ctrlKey && (e.key === 'Enter' || e.keyCode === 13)) {
e.preventDefault();
const sendBtn = document.querySelector(
'button[aria-label="Send message"]:not([disabled]), #send-button:not([disabled]), button[data-testid="send-button"]:not([disabled])'
);
if (sendBtn) sendBtn.click();
return;
}
// Ctrl+; - STOP, then REGENERATE
if (e.ctrlKey && (e.key === ';' || e.code === 'Semicolon')) {
e.preventDefault();
const stopBtn = document.querySelector('button[aria-label="Stop generating"]:not([disabled])');
if (stopBtn) stopBtn.click();
setTimeout(function () {
const lowestRegenBtn = getLowestVisibleRegenerateBtn();
if (lowestRegenBtn) lowestRegenBtn.click();
}, 750);
return;
}
});
})();