您需要先安装一个扩展,例如 篡改猴、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.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); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址