Jina Reader - Copy LLM Format

Copy current page as LLM-friendly format using Jina Reader API

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Jina Reader - Copy LLM Format
// @namespace    https://github.com/kouni/jinasnap
// @version      2.1.6
// @description  Copy current page as LLM-friendly format using Jina Reader API
// @author       Kouni
// @license      GPL-2.0-or-later
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @grant        GM_registerMenuCommand
// @connect      r.jina.ai
// ==/UserScript==

(function() {
    'use strict';
    
    // Configuration
    const CONFIG = {
        API_ENDPOINT: 'https://r.jina.ai/',
        TIMEOUT: 30000,
        NOTIFICATION_DURATION: 3000,
        FADE_DURATION: 300,
        MIN_CONTENT_LENGTH: 10,
        REJECT_PATTERNS: [
            'page not found',
            'error 404',
            'error 403',
            'error 500',
            'access denied',
            'unauthorized',
            'forbidden',
            'protected by recaptcha',
            'captcha verification required',
            'please verify you are human',
            'authentication required',
            'login required'
        ]
    };
    
    // State management
    let isProcessing = false;
    let currentNotification = null;
    
    // Inject CSS styles
    function injectStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .jina-reader-notification {
                position: fixed;
                top: 20px;
                right: 20px;
                z-index: 10000;
                padding: 12px 16px;
                border-radius: 6px;
                font-size: 14px;
                font-weight: 500;
                box-shadow: 0 4px 12px rgba(0,0,0,0.2);
                transition: opacity ${CONFIG.FADE_DURATION}ms ease;
                opacity: 1;
            }
            .jina-reader-notification.jina-reader-success {
                background: #d4edda; color: #155724; border: 1px solid #c3e6cb;
            }
            .jina-reader-notification.jina-reader-error {
                background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;
            }
            .jina-reader-notification.jina-reader-warning {
                background: #fff3cd; color: #856404; border: 1px solid #ffeaa7;
            }
            .jina-reader-notification.jina-reader-info {
                background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb;
            }
        `;
        document.head.appendChild(style);
    }

    // Register menu command
    GM_registerMenuCommand('📄 Copy as LLM Format', copyCurrentPage);
    
    // Main function to copy current page
    function copyCurrentPage() {
        // Prevent multiple simultaneous requests
        if (isProcessing) {
            showNotification('⚠️ Request already in progress...', 'warning');
            return;
        }
        
        isProcessing = true;
        const currentUrl = window.top.location.href;
        
        showNotification('🔄 Converting page...', 'info');
        logDebug('Starting conversion for URL:', currentUrl);
        
        GM_xmlhttpRequest({
            method: 'GET',
            url: CONFIG.API_ENDPOINT + currentUrl,
            headers: {
                'Accept': 'text/plain',
                'User-Agent': 'Mozilla/5.0 (compatible; Jina-Reader-UserScript)'
            },
            timeout: CONFIG.TIMEOUT,
            onload: function(response) {
                isProcessing = false;
                handleApiResponse(response);
            },
            onerror: function(error) {
                isProcessing = false;
                logError('Network error:', error);
                showNotification('❌ Network error occurred', 'error');
            },
            ontimeout: function() {
                isProcessing = false;
                logError('Request timeout for URL:', currentUrl);
                showNotification('❌ Request timeout', 'error');
            }
        });
    }
    
    // Handle API response
    function handleApiResponse(response) {
        logDebug('API response status:', response.status);
        
        if (response.status === 200) {
            const content = response.responseText;
            
            // Validate content
            if (!isValidContent(content)) {
                logError('Invalid content received:', content.substring(0, 100));
                showNotification('❌ Invalid or empty response', 'error');
                return;
            }
            
            GM_setClipboard(content);
            showNotification('✅ Content copied to clipboard!', 'success');
            logDebug('Content copied successfully, length:', content.length);
        } else {
            logError('API error, status:', response.status);
            showNotification(`❌ Failed to convert page (${response.status})`, 'error');
        }
    }
    
    // Validate content
    function isValidContent(content) {
        if (!content || typeof content !== 'string') {
            return false;
        }
        
        const trimmed = content.trim();
        const lowerContent = trimmed.toLowerCase();
        
        // Check minimum length
        if (trimmed.length < CONFIG.MIN_CONTENT_LENGTH) {
            return false;
        }
        
        // Only reject obvious error/protected pages
        for (const pattern of CONFIG.REJECT_PATTERNS) {
            if (lowerContent.includes(pattern)) {
                return false;
            }
        }
        
        return true;
    }
    
    // Debug logging
    function logDebug(message, ...args) {
        console.log(`[Jina Reader] ${message}`, ...args);
    }
    
    // Error logging
    function logError(message, ...args) {
        console.error(`[Jina Reader] ${message}`, ...args);
    }
    
    // Show notification with deduplication
    function showNotification(message, type = 'info') {
        // Clear existing notification
        if (currentNotification && document.body.contains(currentNotification)) {
            document.body.removeChild(currentNotification);
        }
        
        const notification = document.createElement('div');
        notification.className = `jina-reader-notification jina-reader-${type}`;
        notification.textContent = message;
        document.body.appendChild(notification);
        
        // Store reference to current notification
        currentNotification = notification;
        
        // Auto-dismiss after configured duration
        setTimeout(() => {
            if (currentNotification === notification) {
                notification.style.opacity = '0';
                setTimeout(() => {
                    if (document.body.contains(notification)) {
                        document.body.removeChild(notification);
                    }
                    if (currentNotification === notification) {
                        currentNotification = null;
                    }
                }, CONFIG.FADE_DURATION);
            }
        }, CONFIG.NOTIFICATION_DURATION);
    }
    
    // Keyboard shortcut (Ctrl/Cmd + Shift + R)
    document.addEventListener('keydown', function(e) {
        if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'R') {
            e.preventDefault();
            copyCurrentPage();
        }
    });

    // Initialize
    injectStyles();
})();