您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Press Alt+S to click toggle-left-nav button on localhost:3080
// ==UserScript== // @name LibreChat Shortcuts + Token Counter // @namespace http://tampermonkey.net/ // @version 2.9.1 // @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, then strip markdown // 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 → Toggle header in narrow mode // alt+m toggle maximize chat area, 5 clicks baby // control+enter send message // control+backspace stop generating // control+; clicks stop then regenerate (or just regenerate if stop isn't available). // Alt+e → toggle collapse expand chat // alt+w → Open the preset menu to see the "defaults" // alt+# 1-9 to activate presets // alt+g Click right sidebar toggle with alt+g for parameter settings // other fixes // label presets with alt+1 to alt+9 // Convert <br> in tables displaying as literal <br> to line breaks // Token Counter is disabled, if enabled, Alt+U → update the token cost per million // replaces the multi-conversation and preset icons with ones that are intuitive // hides ads disguised as features (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); })(); // scrolling and toggle sidebar 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; } } 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'); setTimeout(() => { // Find the button with data-testid="close-sidebar-button" const closeSidebarButton = document.querySelector('button[data-testid="close-sidebar-button"]'); if (closeSidebarButton) { // closeSidebarButton.click(); console.log('⬅️ Sidebar closed'); } else { console.log('❌ Close sidebar button not found'); } }, 100); } } // Start loading GSAP plugins and wait for them loadGSAPLibraries(); })(); // alt+w to focus chat input. But also you can just start typing or just paste. (function() { document.addEventListener('keydown', function(e) { if (e.altKey && e.key === 'w') { e.preventDefault(); const chatInput = document.querySelector('#prompt-textarea'); if (chatInput) { chatInput.focus(); } } }); })(); // strip markdown when activating copy with alt+c (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); } } }); })(); // alt+x select text and auto-copy (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); } }); })(); // type to focus chat, paste when chat not focused (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 (() => { 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 // 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 and go to parameters (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 and some ugly style updates (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."); } } // Uncommoent next line to 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; } }); })(); // alt+m toggle maximize chat area (() => { /* ---------- utility helpers ---------- */ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); /** wait for a selector to appear (or return null after timeout) */ const waitFor = async (selector, root = document, timeout = 5000) => { const el = root.querySelector(selector); if (el) return el; return new Promise((resolve) => { const obs = new MutationObserver(() => { const found = root.querySelector(selector); if (found) { obs.disconnect(); resolve(found); } }); obs.observe(root, { childList: true, subtree: true }); setTimeout(() => { obs.disconnect(); resolve(null); }, timeout); }); }; /* click helper (fires native click + change for React) */ const clickEl = (el) => { if (!el) return; el.focus({ preventScroll: true }); // Radix Tabs checks focus el.click(); // produces a full MouseEvent }; /* robust text‑finder for dynamic IDs */ const findByText = (selector, text) => [...document.querySelectorAll(selector)].find((n) => n.textContent?.trim().toLowerCase().includes(text.toLowerCase()) ); /* ---------- main routine ---------- */ const runToggle = async () => { /* 1 — open user menu */ clickEl(await waitFor('button[data-testid="nav-user"]')); await sleep(100); /* 2 — click “Settings” in the combobox */ clickEl(findByText('div[role="option"]', 'settings')); await sleep(100); /* 3 — switch to “Chat” tab (in case another tab is open) */ const chatTabSelector = 'button[role="tab"][id$="-trigger-chat"], button[role="tab"][aria-controls$="content-chat"]'; const chatTab = await waitFor(chatTabSelector); clickEl(chatTab); await sleep(100); /* 4 — toggle Maximize‑chat‑space switch */ clickEl(await waitFor('button[data-testid="maximizeChatSpace"]')); await sleep(100); /* 5 — press Escape to close Settings */ document.dispatchEvent( new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true }) ); }; /* ---------- hot‑key binding ---------- */ document.addEventListener( 'keydown', (e) => { if (e.altKey && e.key.toLowerCase() === 'm') { e.preventDefault(); runToggle(); } }, true ); })();