您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Explain selected text using LLM
// ==UserScript== // @name Text Explainer // @namespace http://tampermonkey.net/ // @version 0.2.14 // @description Explain selected text using LLM // @author RoCry // @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAxOTIgMTkyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiPjxjaXJjbGUgY3g9IjExNiIgY3k9Ijc2IiByPSI1NCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjEyIi8+PHBhdGggc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMTIiIGQ9Ik04Ni41IDEyMS41IDQxIDE2N2MtNC40MTggNC40MTgtMTEuNTgyIDQuNDE4LTE2IDB2MGMtNC40MTgtNC40MTgtNC40MTgtMTEuNTgyIDAtMTZsNDQuNS00NC41TTkyIDYybDEyIDMyIDEyLTMyIDEyIDMyIDEyLTMyIi8+PC9zdmc+ // @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/1549030/SmolLLM.js // @require https://update.greasyfork.org/scripts/528703/1546610/SimpleBalancer.js // @require https://update.greasyfork.org/scripts/528763/1549028/Text%20Explainer%20Settings.js // @require https://update.greasyfork.org/scripts/528822/1547803/Selection%20Context.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); }; // Create and manage floating button let floatingButton = null; let isProcessingText = false; 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 } 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; 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); // Handle button click/tap function handleButtonAction(e) { e.preventDefault(); e.stopPropagation(); // Prevent multiple clicks while processing if (isProcessingText) return; // Get selection context before clearing selection const selectionContext = GetSelectionContext(); if (!selectionContext.selectedText) { console.log('No valid selection to process'); return; } // Set processing flag isProcessingText = true; // Hide the floating button hideFloatingButton(); // Blur selection to dismiss iOS menu window.getSelection().removeAllRanges(); // Now trigger the explainer with the stored selection // 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( selectionContext.selectedText, selectionContext.paragraphText, selectionContext.textBefore, selectionContext.textAfter ); // Variable to store ongoing response text let responseText = ''; // Call LLM with progress callback 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); }) .catch(error => { console.error('Error in LLM call:', error); errorDiv.textContent = error.message || 'Error processing request'; errorDiv.style.display = 'block'; loadingDiv.style.display = 'none'; }) .finally(() => { // Reset processing flag setTimeout(() => { isProcessingText = false; }, 1000); }); } // Add click event floatingButton.addEventListener('click', handleButtonAction); // Add touch events floatingButton.addEventListener('touchstart', (e) => { e.preventDefault(); e.stopPropagation(); floatingButton.style.transform = 'scale(0.95)'; }, { passive: false }); floatingButton.addEventListener('touchend', (e) => { e.preventDefault(); e.stopPropagation(); floatingButton.style.transform = 'scale(1)'; handleButtonAction(e); }, { passive: false }); // Prevent text selection on button floatingButton.addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation(); }); } function showFloatingButton() { if (!floatingButton || !config.floatingButton.enabled || isProcessingText) return; const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) { hideFloatingButton(); return; } const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); // Calculate position near the selection const buttonSize = parseInt(floatingButton.style.width); const margin = 10; // Distance from selection // Calculate position in viewport coordinates let top = rect.bottom + margin; let left = rect.left + (rect.width / 2) - (buttonSize / 2); // If button would go off screen, try positioning above if (top + buttonSize > window.innerHeight) { top = rect.top - buttonSize - margin; } // Ensure button stays within viewport horizontally left = Math.max(10, Math.min(left, window.innerWidth - buttonSize - 10)); // Apply position (using viewport coordinates for fixed positioning) floatingButton.style.top = `${top}px`; floatingButton.style.left = `${left}px`; // Make visible and enable pointer events floatingButton.style.opacity = '1'; floatingButton.style.pointerEvents = 'auto'; } function hideFloatingButton() { if (!floatingButton) return; floatingButton.style.opacity = '0'; floatingButton.style.pointerEvents = 'none'; } // Add minimal styles for UI components GM_addStyle(` /* Base popup styles */ #explainer-popup { position: absolute; width: 450px; max-width: 90vw; max-height: 80vh; padding: 20px; z-index: 2147483647; overflow: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; /* Visual styles */ background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border-radius: 8px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); border: 1px solid rgba(0, 0, 0, 0.15); /* Text styles */ color: #111; text-shadow: 0 0 1px rgba(255, 255, 255, 0.3); /* Animations */ transition: all 0.3s ease; } /* Dark theme */ #explainer-popup.dark-theme { background: rgba(45, 45, 50, 0.85); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); color: #e0e0e0; border: 1px solid rgba(255, 255, 255, 0.15); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.4); text-shadow: 0 0 1px rgba(0, 0, 0, 0.3); } /* iOS-specific overrides */ @supports (-webkit-touch-callout: none) { #explainer-popup { background: rgba(255, 255, 255, 0.98); /* Disable backdrop-filter on iOS for better performance */ backdrop-filter: none; -webkit-backdrop-filter: none; } #explainer-popup.dark-theme { background: rgba(35, 35, 40, 0.98); } } @keyframes slideInFromTop { from { transform: translateY(-20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes slideInFromBottom { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes slideInFromLeft { from { transform: translateX(-20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideInFromRight { from { transform: translateX(20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } #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; } /* iOS-specific styles */ @supports (-webkit-touch-callout: none) { #explainer-popup { background: rgba(255, 255, 255, 0.98); box-shadow: 0 5px 25px rgba(0, 0, 0, 0.3); border: 1px solid rgba(0, 0, 0, 0.1); } /* Dark mode for iOS */ @media (prefers-color-scheme: dark) { #explainer-popup { background: rgba(35, 35, 40, 0.98); border: 1px solid rgba(255, 255, 255, 0.1); } } } /* Dark mode support - minimal */ @media (prefers-color-scheme: dark) { #explainer-popup { background: rgba(35, 35, 40, 0.85); 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); } } /* Add touch-specific styles */ @media (hover: none) and (pointer: coarse) { #explainer-popup { width: 95vw; max-height: 90vh; padding: 15px; font-size: 16px; } #explainer-popup p, #explainer-popup li { line-height: 1.6; margin-bottom: 12px; } #explainer-popup a { padding: 8px 0; } } `); // Function to detect if the page has a dark background function isPageDarkMode() { // Try to get the background color of the body or html element const bodyEl = document.body; const htmlEl = document.documentElement; // Get computed style const bodyStyle = window.getComputedStyle(bodyEl); const htmlStyle = window.getComputedStyle(htmlEl); // Extract background color const bodyBg = bodyStyle.backgroundColor; const htmlBg = htmlStyle.backgroundColor; // Parse RGB values function getRGBValues(color) { const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/); if (!match) return null; const r = parseInt(match[1], 10); const g = parseInt(match[2], 10); const b = parseInt(match[3], 10); return { r, g, b }; } // Calculate luminance (brightness) - higher values are brighter function getLuminance(color) { const rgb = getRGBValues(color); if (!rgb) return 128; // Default to middle gray if can't parse // Perceived brightness formula return (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b); } const bodyLuminance = getLuminance(bodyBg); const htmlLuminance = getLuminance(htmlBg); // If either background is dark, consider the page dark const threshold = 128; // Middle of 0-255 range // Check system preference as a fallback const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; // Page is dark if: // 1. Body background is dark, or // 2. HTML background is dark and body has no background set, or // 3. Both have no background set but system prefers dark if (bodyLuminance < threshold) { return true; } else if (bodyBg === 'rgba(0, 0, 0, 0)' && htmlLuminance < threshold) { return true; } else if (bodyBg === 'rgba(0, 0, 0, 0)' && htmlBg === 'rgba(0, 0, 0, 0)') { return prefersDark; } return false; } // Function to close the popup function closePopup() { const popup = document.getElementById('explainer-popup'); if (popup) { popup.style.animation = 'fadeOut 0.3s ease'; setTimeout(() => { popup.remove(); const overlay = document.getElementById('explainer-overlay'); if (overlay) { overlay.remove(); } }, 300); } // Always clean up the global variables from previous implementation if (window.explainerTouchTracker) { window.explainerTouchTracker = null; } } // Calculate optimal popup position based on selection function calculatePopupPosition() { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return null; // Get selection position const range = selection.getRangeAt(0); const selectionRect = range.getBoundingClientRect(); // Get scroll position to convert viewport coordinates to absolute const scrollLeft = window.scrollX || document.documentElement.scrollLeft; const scrollTop = window.scrollY || document.documentElement.scrollTop; // Get document dimensions const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // Estimate popup dimensions (will be adjusted once created) const popupWidth = 450; const popupHeight = Math.min(500, viewportHeight * 0.8); // Calculate optimal position let position = {}; // Default margin from selection const margin = 20; // Try to position below the selection if (selectionRect.bottom + margin + popupHeight <= viewportHeight) { position.top = selectionRect.bottom + scrollTop + margin; position.left = Math.min( Math.max(10 + scrollLeft, selectionRect.left + scrollLeft + (selectionRect.width / 2) - (popupWidth / 2)), viewportWidth + scrollLeft - popupWidth - 10 ); position.placement = 'below'; } // Try to position above the selection else if (selectionRect.top - margin - popupHeight >= 0) { position.top = selectionRect.top + scrollTop - margin - popupHeight; position.left = Math.min( Math.max(10 + scrollLeft, selectionRect.left + scrollLeft + (selectionRect.width / 2) - (popupWidth / 2)), viewportWidth + scrollLeft - popupWidth - 10 ); position.placement = 'above'; } // Try to position to the right else if (selectionRect.right + margin + popupWidth <= viewportWidth) { position.top = Math.max(10 + scrollTop, Math.min( selectionRect.top + scrollTop, viewportHeight + scrollTop - popupHeight - 10 )); position.left = selectionRect.right + scrollLeft + margin; position.placement = 'right'; } // Try to position to the left else if (selectionRect.left - margin - popupWidth >= 0) { position.top = Math.max(10 + scrollTop, Math.min( selectionRect.top + scrollTop, viewportHeight + scrollTop - popupHeight - 10 )); position.left = selectionRect.left + scrollLeft - margin - popupWidth; position.placement = 'left'; } // Fallback to centered position if no good placement found else { position.top = Math.max(10 + scrollTop, Math.min( selectionRect.top + selectionRect.height + scrollTop + margin, viewportHeight / 2 + scrollTop - popupHeight / 2 )); position.left = Math.max(10 + scrollLeft, Math.min( selectionRect.left + selectionRect.width / 2 + scrollLeft - popupWidth / 2, viewportWidth + scrollLeft - popupWidth - 10 )); position.placement = 'center'; } return position; } // Create popup function createPopup() { // Remove existing popup if any closePopup(); const popup = document.createElement('div'); popup.id = 'explainer-popup'; // Add dark-theme class if the page has a dark background if (isPageDarkMode()) { popup.classList.add('dark-theme'); } popup.innerHTML = ` <div id="explainer-error"></div> <div id="explainer-loading"></div> <div id="explainer-content"></div> `; document.body.appendChild(popup); // For touch devices, use fixed positioning with transform if (isTouchDevice()) { popup.style.position = 'fixed'; popup.style.top = '50%'; popup.style.left = '50%'; popup.style.transform = 'translate(-50%, -50%)'; popup.style.width = '90vw'; popup.style.maxHeight = '85vh'; } else { // Desktop positioning logic const position = calculatePopupPosition(); if (position) { popup.style.transform = 'none'; if (position.top !== undefined) popup.style.top = `${position.top}px`; if (position.bottom !== undefined) popup.style.bottom = `${position.bottom}px`; if (position.left !== undefined) popup.style.left = `${position.left}px`; if (position.right !== undefined) popup.style.right = `${position.right}px`; } else { popup.style.top = '50%'; popup.style.left = '50%'; popup.style.transform = 'translate(-50%, -50%)'; } } // Add animation popup.style.animation = 'fadeIn 0.3s ease'; // Add event listeners document.addEventListener('keydown', handleEscKey); // Use a simpler approach for touch devices - attach a click/touch handler directly if (isTouchDevice()) { // Create an overlay for capturing outside touches const overlay = document.createElement('div'); overlay.id = 'explainer-overlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: ${parseInt(popup.style.zIndex || 2147483647) - 1}; background: transparent; `; document.body.appendChild(overlay); // Handle iPad-specific touch behavior let touchStarted = false; let startX = 0; let startY = 0; // Higher threshold for iPad - more forgiving for slight movements const moveThreshold = 30; // pixels overlay.addEventListener('touchstart', (e) => { touchStarted = true; startX = e.touches[0].clientX; startY = e.touches[0].clientY; }, { passive: true }); overlay.addEventListener('touchmove', () => { // Just having a touchmove listener prevents default behavior on iOS }, { passive: true }); overlay.addEventListener('touchend', (e) => { if (!touchStarted) return; const touch = e.changedTouches[0]; const moveX = Math.abs(touch.clientX - startX); const moveY = Math.abs(touch.clientY - startY); // If user didn't move much, consider it a tap to dismiss if (moveX < moveThreshold && moveY < moveThreshold) { closePopup(); removeAllPopupListeners(); } touchStarted = false; }, { passive: true }); // Prevent popup from capturing overlay events popup.addEventListener('touchstart', (e) => { e.stopPropagation(); }, { passive: false }); } else { document.addEventListener('click', handleOutsideClick); } return popup; } // Handle Escape key to close popup function handleEscKey(e) { if (e.key === 'Escape') { closePopup(); removeAllPopupListeners(); } } // For desktop - more straightforward approach function handleOutsideClick(e) { const popup = document.getElementById('explainer-popup'); if (!popup || popup.contains(e.target)) return; closePopup(); removeAllPopupListeners(); } // Clean up all event listeners function removeAllPopupListeners() { document.removeEventListener('keydown', handleEscKey); // Only remove click listener if we're not on a touch device if (!isTouchDevice()) { document.removeEventListener('click', handleOutsideClick); } const overlay = document.getElementById('explainer-overlay'); if (overlay) { overlay.remove(); } } // 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 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(selectedText, paragraphText, textBefore, textAfter) { const wordsCount = selectedText.split(' ').length; const systemPrompt = `Respond in ${config.language} with HTML tags to improve readability. - Prioritize clarity and conciseness - Use bullet points when appropriate`; if (wordsCount >= 500) { return { prompt: `Create a structured summary in ${config.language}: - Identify key themes and concepts - Extract 3-5 main points - Use nested <ul> lists for hierarchy - Keep bullets concise for the following selected text: \n\n${selectedText} `, systemPrompt }; } // For short text that looks like a sentence, offer translation if (wordsCount >= 5) { return { prompt: `Translate exactly to ${config.language} without commentary: - Preserve technical terms and names - Maintain original punctuation - Match formal/informal tone of source for the following selected text: \n\n${selectedText} `, systemPrompt }; } const pinYinExtraPrompt = config.language === "Chinese" ? ' DO NOT add Pinyin for it.' : ''; const ipaExtraPrompt = config.language === "Chinese" ? '(with IPA if necessary)' : ''; const asciiChars = selectedText.replace(/[\s\.,\-_'"!?()]/g, '') .split('') .filter(char => char.charCodeAt(0) <= 127).length; const sampleSentenceLanguage = selectedText.length === asciiChars ? "English" : config.language; // If we have context before/after, include it in the prompt const contextPrompt = textBefore || textAfter ? `# Context: ## Before selected text: ${textBefore || 'None'} ## Selected text: ${selectedText} ## After selected text: ${textAfter || 'None'}` : paragraphText; // Explain words prompt return { prompt: `Provide an explanation for the word: "${selectedText}${ipaExtraPrompt}" in ${config.language} without commentary.${pinYinExtraPrompt} Use the context from the surrounding paragraph to inform your explanation when relevant: ${contextPrompt} # Consider these scenarios: ## Names If "${selectedText}" 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 "${selectedText}" is a technical term or jargon - give a concise definition and explain. - Some best practice of using it - Explain how it works. - No need example sentence for the technical term. 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. `, systemPrompt }; } // Main function to process selected text async function processSelectedText() { // Use the utility function instead of the local getSelectedText const { selectedText, textBefore, textAfter, paragraphText } = GetSelectionContext(); if (!selectedText) { showError('No text selected'); return; } console.log(`Selected text: '${selectedText}', 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(selectedText, paragraphText, textBefore, textAfter); // Variable to store ongoing response text let responseText = ''; 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); }); console.log('fullResponse\n', fullResponse); // 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; text = text.trim(); if (text.length === 0) { return; } try { // drop first line if it's a code block if (text.startsWith('```')) { if (text.endsWith('```')) { text = text.split('\n').slice(1, -1).join('\n'); } else { text = text.split('\n').slice(1).join('\n'); } } if (!text.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() { // Don't update button visibility if we're processing text if (isProcessingText) return; const selection = window.getSelection(); const hasSelection = selection && selection.toString().trim() !== ''; if (hasSelection && isTouchDevice() && config.floatingButton.enabled) { // Small delay to ensure selection is fully updated setTimeout(showFloatingButton, 100); } 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(); })();