您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Explain selected text using LLM
当前为
// ==UserScript== // @name Text Explainer // @namespace http://tampermonkey.net/ // @version 0.2.2 // @description Explain selected text using LLM // @author RoCry // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @connect generativelanguage.googleapis.com // @connect * // @run-at document-end // @inject-into content // @require https://update.greasyfork.org/scripts/528704/1547031/SmolLLM.js // @require https://update.greasyfork.org/scripts/528703/1546610/SimpleBalancer.js // @require https://update.greasyfork.org/scripts/528763/1547395/Text%20Explainer%20Settings.js // @license MIT // ==/UserScript== (function () { 'use strict'; // Initialize settings manager with extended default config const settingsManager = new TextExplainerSettings({ model: "gemini-2.0-flash", apiKey: null, baseUrl: "https://generativelanguage.googleapis.com", provider: "gemini", language: "Chinese", // Default language shortcut: { key: "d", ctrlKey: false, altKey: true, shiftKey: false, metaKey: false }, floatingButton: { enabled: true, size: "medium", position: "bottom-right" } }); // Get current configuration let config = settingsManager.getAll(); // Initialize SmolLLM let llm; try { llm = new SmolLLM(); } catch (error) { console.error('Failed to initialize SmolLLM:', error); llm = null; } // Check if device is touch-enabled const isTouchDevice = () => { return ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0); }; const isIOS = () => { return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; }; // Create and manage floating button let floatingButton = null; function createFloatingButton() { if (floatingButton) return; floatingButton = document.createElement('div'); floatingButton.id = 'explainer-floating-button'; // Determine size based on settings let buttonSize; switch (config.floatingButton.size) { case 'small': buttonSize = '40px'; break; case 'large': buttonSize = '60px'; break; default: buttonSize = '50px'; // medium } // Position based on settings let positionCSS; switch (config.floatingButton.position) { case 'top-left': positionCSS = 'top: 20px; left: 20px;'; break; case 'top-right': positionCSS = 'top: 20px; right: 20px;'; break; case 'bottom-left': positionCSS = 'bottom: 20px; left: 20px;'; break; default: positionCSS = 'bottom: 20px; right: 20px;'; // bottom-right } floatingButton.style.cssText = ` width: ${buttonSize}; height: ${buttonSize}; border-radius: 50%; background-color: rgba(33, 150, 243, 0.8); color: white; display: flex; align-items: center; justify-content: center; position: fixed; ${positionCSS} z-index: 9999; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); cursor: pointer; font-weight: bold; font-size: ${parseInt(buttonSize) * 0.4}px; opacity: 0; transition: opacity 0.3s ease, transform 0.2s ease; pointer-events: none; touch-action: manipulation; -webkit-tap-highlight-color: transparent; `; // Add icon or text floatingButton.innerHTML = '✓'; // Add to DOM document.body.appendChild(floatingButton); // Add click event floatingButton.addEventListener('click', (e) => { e.preventDefault(); processSelectedText(); }); // Prevent text selection on button floatingButton.addEventListener('mousedown', (e) => { e.preventDefault(); }); } function showFloatingButton() { if (!floatingButton || !config.floatingButton.enabled) return; // Make visible and enable pointer events floatingButton.style.opacity = '1'; floatingButton.style.pointerEvents = 'auto'; // Add active effect for touch floatingButton.addEventListener('touchstart', () => { floatingButton.style.transform = 'scale(0.95)'; }); floatingButton.addEventListener('touchend', () => { floatingButton.style.transform = 'scale(1)'; }); } function hideFloatingButton() { if (!floatingButton) return; floatingButton.style.opacity = '0'; floatingButton.style.pointerEvents = 'none'; } // Add minimal styles for UI components GM_addStyle(` #explainer-popup { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 450px; max-width: 90vw; max-height: 80vh; background: rgba(255, 255, 255, 0.3); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); border-radius: 8px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); z-index: 10000; overflow: auto; padding: 16px; } #explainer-loading { text-align: center; padding: 20px 0; display: flex; align-items: center; justify-content: center; } #explainer-loading:after { content: ""; width: 24px; height: 24px; border: 3px solid #ddd; border-top: 3px solid #2196F3; border-radius: 50%; animation: spin 1s linear infinite; display: inline-block; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } #explainer-error { color: #d32f2f; padding: 8px; border-radius: 4px; margin-bottom: 10px; font-size: 14px; display: none; } /* Dark mode support - minimal */ @media (prefers-color-scheme: dark) { #explainer-popup { background: rgba(35, 35, 40, 0.7); color: #e0e0e0; } #explainer-error { background-color: rgba(100, 25, 25, 0.4); color: #ff8a8a; } #explainer-floating-button { background-color: rgba(33, 150, 243, 0.9); } } `); // Function to close the popup function closePopup() { const popup = document.getElementById('explainer-popup'); if (popup) { popup.remove(); } } // Create popup function createPopup() { // Remove existing popup if any closePopup(); const popup = document.createElement('div'); popup.id = 'explainer-popup'; popup.innerHTML = ` <div id="explainer-error"></div> <div id="explainer-loading"></div> <div id="explainer-content"></div> `; document.body.appendChild(popup); // Add event listener for Escape key document.addEventListener('keydown', handleEscKey); // Add event listener for clicking outside popup document.addEventListener('click', handleOutsideClick); return popup; } // Handle Escape key to close popup function handleEscKey(e) { if (e.key === 'Escape') { closePopup(); document.removeEventListener('keydown', handleEscKey); document.removeEventListener('click', handleOutsideClick); } } // Handle clicks outside popup to close it function handleOutsideClick(e) { const popup = document.getElementById('explainer-popup'); if (popup && !popup.contains(e.target)) { closePopup(); document.removeEventListener('keydown', handleEscKey); document.removeEventListener('click', handleOutsideClick); } } // Function to show an error in the popup function showError(message) { const errorDiv = document.getElementById('explainer-error'); if (errorDiv) { errorDiv.textContent = message; errorDiv.style.display = 'block'; document.getElementById('explainer-loading').style.display = 'none'; } } // Function to get text from selected element function getSelectedText() { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0 || selection.toString().trim() === '') { return { selectionText: null, paragraphText: null }; } const selectionText = selection.toString().trim(); // Get the paragraph containing the selection const range = selection.getRangeAt(0); let paragraphElement = range.commonAncestorContainer; // Navigate up to find a paragraph or meaningful content container while (paragraphElement && (paragraphElement.nodeType !== Node.ELEMENT_NODE || !['P', 'DIV', 'ARTICLE', 'SECTION', 'LI'].includes(paragraphElement.tagName))) { paragraphElement = paragraphElement.parentNode; } let paragraphText = ''; if (paragraphElement) { // Get text content but limit to a reasonable size paragraphText = paragraphElement.textContent.trim(); if (paragraphText.length > 500) { paragraphText = paragraphText.substring(0, 497) + '...'; } } return { selectionText, paragraphText }; } // Function to call the LLM using SmolLLM async function callLLM(prompt, systemPrompt, progressCallback) { if (!config.apiKey) { throw new Error("Please set up your API key in the settings."); } if (!llm) { throw new Error("SmolLLM library not initialized. Please check console for errors."); } console.log(`prompt: ${prompt}`); console.log(`systemPrompt: ${systemPrompt}`); try { return await llm.askLLM({ prompt: prompt, systemPrompt: systemPrompt, model: config.model, apiKey: config.apiKey, baseUrl: config.baseUrl, providerName: config.provider, handler: progressCallback, timeout: 60000 }); } catch (error) { console.error('LLM API error:', error); throw error; } } function getPrompt(selectionText, paragraphText) { const wordsCount = selectionText.split(' ').length; const defaultSystemPrompt = `You will response in ${config.language} with basic html format like <p> <b> <i> <a> <li> <ol> <ul> to improve readability. Do NOT wrap your response in code block.`; if (selectionText === paragraphText || wordsCount >= 500) { // Summary prompt return [ `Summarize the following text in ${config.language}, using bullet points to improve readability:\n\n${selectionText}`, defaultSystemPrompt ]; } if (wordsCount > 3) { // Translate prompt return [ `Translate the following text into ${config.language}, no extra explanation, just the translation:\n\n${selectionText}`, defaultSystemPrompt ]; } const pinYinExtraPrompt = config.language === "Chinese" ? ' DO NOT add Pinyin for it.' : ''; const ipaExtraPrompt = config.language === "Chinese" ? '(with IPA if necessary)' : ''; const asciiChars = selectionText.replace(/[\s\.,\-_'"!?()]/g, '') .split('') .filter(char => char.charCodeAt(0) <= 127).length; const sampleSentenceLanguage = selectionText.length === asciiChars ? "English" : config.language; // Explain words prompt return [ `Provide an explanation for the word: "${selectionText}${ipaExtraPrompt}" in ${config.language}.${pinYinExtraPrompt} Use the context from the surrounding paragraph to inform your explanation when relevant: "${paragraphText}" # Consider these scenarios: ## Names If "${selectionText}" is a person's name, company name, or organization name, provide a brief description (e.g., who they are or what they do). e.g. Alan Turing was a British mathematician and computer scientist. He is widely considered to be the father of theoretical computer science and artificial intelligence. His work was crucial to: • Formalizing the concepts of algorithm and computation with the Turing machine. • Breaking the German Enigma code during World War II, significantly contributing to the Allied victory. • Developing the Turing test, a benchmark for artificial intelligence. ## Technical Terms If "${selectionText}" is a technical term or jargon, give a concise definition and explain the use case or context where it is commonly used. No need example sentences. e.g. GAN → 生成对抗网络 生成对抗网络(Generative Adversarial Network),是一种深度学习框架,由Ian Goodfellow在2014年提出。GAN包含两个神经网络:生成器(Generator)和判别器(Discriminator),它们相互对抗训练。生成器尝试创建看起来真实的数据,而判别器则尝试区分真实数据和生成的假数据。通过这种"博弈"过程,生成器逐渐学会创建越来越逼真的数据。 ## Normal Words - For any other word, explain its meaning and provide 1-2 example sentences with the word in ${sampleSentenceLanguage}. e.g. jargon \\ˈdʒɑrɡən\\ → 行话,专业术语,特定领域内使用的专业词汇。在计算机科学和编程领域,指那些对外行人难以理解的专业术语和缩写。 例句: "When explaining code to beginners, try to avoid using too much technical jargon that might confuse them."(向初学者解释代码时,尽量避免使用太多可能让他们困惑的技术行话。) # Format - Output the words first, then the explanation, and then the example sentences in ${sampleSentenceLanguage} if necessary. - No extra explanation - Remember to using proper html format like <p> <b> <i> <a> <li> <ol> <ul> to improve readability. `, defaultSystemPrompt ]; } // Main function to process selected text async function processSelectedText() { const { selectionText, paragraphText } = getSelectedText(); if (!selectionText) { alert('Please select some text first.'); return; } console.log(`Selected text: '${selectionText}', Paragraph text:\n${paragraphText}`); // Create popup createPopup(); const contentDiv = document.getElementById('explainer-content'); const loadingDiv = document.getElementById('explainer-loading'); const errorDiv = document.getElementById('explainer-error'); // Reset display errorDiv.style.display = 'none'; loadingDiv.style.display = 'block'; // Assemble prompt with language preference const [prompt, systemPrompt] = getPrompt(selectionText, paragraphText); // Variable to store ongoing response text let responseText = ''; let responseStartTime = Date.now(); try { // Call LLM with progress callback and await the full response const fullResponse = await callLLM(prompt, systemPrompt, (textChunk, currentFullText) => { // Update response text with new chunk responseText = currentFullText || (responseText + textChunk); // Hide loading message if this is the first chunk if (loadingDiv.style.display !== 'none') { loadingDiv.style.display = 'none'; } // Update content with either HTML or markdown updateContentDisplay(contentDiv, responseText); }); // If we got a response if (fullResponse && fullResponse.length > 0) { responseText = fullResponse; loadingDiv.style.display = 'none'; updateContentDisplay(contentDiv, fullResponse); } // If no response was received at all else if (!fullResponse || fullResponse.length === 0) { // If we've received chunks but the final response is empty, use the accumulated text if (responseText && responseText.length > 0) { updateContentDisplay(contentDiv, responseText); } else { showError("No response received from the model. Please try again."); } } // Hide loading indicator if it's still visible if (loadingDiv.style.display !== 'none') { loadingDiv.style.display = 'none'; } } catch (error) { console.error('Error:', error); // Display error in popup showError(`Error: ${error.message}`); } } // Main function to handle keyboard shortcuts function handleKeyPress(e) { // Get shortcut configuration from settings const shortcut = config.shortcut || { key: 'd', ctrlKey: false, altKey: true, shiftKey: false, metaKey: false }; // More robust shortcut detection using both key and code properties if (isShortcutMatch(e, shortcut)) { e.preventDefault(); processSelectedText(); } } // Helper function for more robust shortcut detection function isShortcutMatch(event, shortcutConfig) { // Check all modifier keys first if (event.ctrlKey !== !!shortcutConfig.ctrlKey || event.altKey !== !!shortcutConfig.altKey || event.shiftKey !== !!shortcutConfig.shiftKey || event.metaKey !== !!shortcutConfig.metaKey) { return false; } const key = shortcutConfig.key.toLowerCase(); // Method 1: Direct key match (works for most standard keys) if (event.key.toLowerCase() === key) { return true; } // Method 2: Key code match (more reliable for letter keys) // This handles the physical key position regardless of keyboard layout if (key.length === 1 && /^[a-z]$/.test(key) && event.code === `Key${key.toUpperCase()}`) { return true; } // Method 3: Handle known special characters from Option/Alt key combinations // These are the most common mappings on macOS when using Option+key const macOptionKeyMap = { 'a': 'å', 'b': '∫', 'c': 'ç', 'd': '∂', 'e': '´', 'f': 'ƒ', 'g': '©', 'h': '˙', 'i': 'ˆ', 'j': '∆', 'k': '˚', 'l': '¬', 'm': 'µ', 'n': '˜', 'o': 'ø', 'p': 'π', 'q': 'œ', 'r': '®', 's': 'ß', 't': '†', 'u': '¨', 'v': '√', 'w': '∑', 'x': '≈', 'y': '¥', 'z': 'Ω' }; if (shortcutConfig.altKey && macOptionKeyMap[key] === event.key) { return true; } return false; } // Helper function to update content display function updateContentDisplay(contentDiv, text) { if (!text) return; try { if (!text.trim().startsWith('<')) { // fallback console.log(`Seems like the response is not HTML: ${text}`); text = `<p>${text.replace(/\n/g, '<br>')}</p>`; } contentDiv.innerHTML = text; } catch (e) { // Fallback if parsing fails console.error(`Error parsing content: ${e.message}`); contentDiv.innerHTML = `<p>${text.replace(/\n/g, '<br>')}</p>`; } } // Monitor selection changes for floating button function handleSelectionChange() { const selection = window.getSelection(); const hasSelection = selection && selection.toString().trim() !== ''; if (hasSelection && isTouchDevice() && config.floatingButton.enabled) { showFloatingButton(); } else { hideFloatingButton(); } } // Settings update callback function onSettingsChanged(updatedConfig) { config = updatedConfig; console.log('Settings updated:', config); // Recreate floating button if settings changed if (floatingButton) { floatingButton.remove(); floatingButton = null; if (isTouchDevice() && config.floatingButton.enabled) { createFloatingButton(); handleSelectionChange(); // Check if there's already a selection } } } // Initialize the script function init() { // Register settings menu in Tampermonkey GM_registerMenuCommand("Text Explainer Settings", () => { settingsManager.openDialog(onSettingsChanged); }); // Add keyboard shortcut listener document.addEventListener('keydown', handleKeyPress); // For touch devices, create floating button if (isTouchDevice() && config.floatingButton.enabled) { createFloatingButton(); // Monitor text selection document.addEventListener('selectionchange', handleSelectionChange); // Add touchend handler to show button after selection document.addEventListener('touchend', () => { // Small delay to ensure selection is updated setTimeout(handleSelectionChange, 100); }); } console.log('Text Explainer script initialized with language: ' + config.language); console.log('Touch device detected: ' + isTouchDevice()); } // Run initialization init(); })();