- // ==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.greatest.deepsurf.us/scripts/528704/1549030/SmolLLM.js
- // @require https://update.greatest.deepsurf.us/scripts/528703/1546610/SimpleBalancer.js
- // @require https://update.greatest.deepsurf.us/scripts/528763/1549028/Text%20Explainer%20Settings.js
- // @require https://update.greatest.deepsurf.us/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();
- })();