Summarize with AI

Adds a little button to summarize articles, news, and similar content using the OpenAI API (gpt-4o-mini model). The button only appears on pages detected as articles or news. The summary is displayed in a responsive overlay with a loading effect and error handling.

La data de 19-09-2024. Vezi ultima versiune.

// ==UserScript==
// @name         Summarize with AI
// @namespace    https://github.com/insign/summarize-with-ai
// @version      2024.09.19.10.50
// @description  Adds a little button to summarize articles, news, and similar content using the OpenAI API (gpt-4o-mini model). The button only appears on pages detected as articles or news. The summary is displayed in a responsive overlay with a loading effect and error handling.
// @author       Hélio <[email protected]>
// @license      WTFPL
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      api.openai.com
// ==/UserScript==

(function() {
    'use strict';

    // Check if the current page is an article or news content
    if (!isArticlePage()) {
        return;
    }

    // Add the "S" button to the page
    addSummarizeButton();

    /*** Function Definitions ***/

    // Function to determine if the page is an article
    function isArticlePage() {
        // Check for <article> element
        if (document.querySelector('article')) {
            return true;
        }

        // Check for Open Graph meta tag
        const ogType = document.querySelector('meta[property="og:type"]');
        if (ogType && ogType.content === 'article') {
            return true;
        }

        // Check for news content in the URL
        const url = window.location.href;
        if (/news|article|story|post/i.test(url)) {
            return true;
        }

        // Check for significant text content (e.g., more than 500 words)
        const bodyText = document.body.innerText || "";
        const wordCount = bodyText.split(/\s+/).length;
        if (wordCount > 500) {
            return true;
        }

        return false;
    }

    // Function to add the summarize button
    function addSummarizeButton() {
        // Create the button element
        const button = document.createElement('div');
        button.id = 'summarize-button';
        button.innerText = 'S';
        document.body.appendChild(button);

        // Add event listeners
        button.addEventListener('click', onSummarizeClick);
        button.addEventListener('dblclick', onApiKeyReset);

        // Add styles
        GM_addStyle(`
            #summarize-button {
                position: fixed;
                bottom: 20px;
                right: 20px;
                width: 50px;
                height: 50px;
                background-color: #007bff;
                color: white;
                font-size: 24px;
                font-weight: bold;
                text-align: center;
                line-height: 50px;
                border-radius: 50%;
                cursor: pointer;
                z-index: 10000;
                box-shadow: 0 2px 5px rgba(0,0,0,0.3);
            }
            #summarize-overlay {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background-color: white;
                z-index: 10001;
                padding: 20px;
                box-shadow: 0 0 10px rgba(0,0,0,0.5);
                overflow: auto;
            }
            #summarize-overlay h2 {
                margin-top: 0;
            }
            #summarize-close {
                position: absolute;
                top: 10px;
                right: 10px;
                cursor: pointer;
                font-size: 18px;
            }
            #summarize-loading {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: linear-gradient(45deg, #007bff, #00ff6a, #007bff);
                background-size: 600% 600%;
                animation: GradientAnimation 3s ease infinite;
                z-index: 10000;
                display: flex;
                align-items: center;
                justify-content: center;
                flex-direction: column;
                color: white;
                font-size: 24px;
            }
            @keyframes GradientAnimation {
                0%{background-position:0% 50%}
                50%{background-position:100% 50%}
                100%{background-position:0% 50%}
            }
            #summarize-cancel {
                margin-top: 20px;
                padding: 10px 20px;
                background-color: rgba(0,0,0,0.3);
                border: none;
                color: white;
                font-size: 18px;
                cursor: pointer;
            }
            #summarize-error {
                position: fixed;
                bottom: 20px;
                left: 20px;
                background-color: rgba(255,0,0,0.8);
                color: white;
                padding: 10px 20px;
                border-radius: 5px;
                z-index: 10002;
            }
            @media (max-width: 768px) {
                #summarize-overlay {
                    width: 90%;
                    height: 90%;
                }
            }
            @media (min-width: 769px) {
                #summarize-overlay {
                    width: 60%;
                    height: 85%;
                }
            }
        `);
    }

    // Handler for clicking the "S" button
    function onSummarizeClick() {
        const apiKey = getApiKey();
        if (!apiKey) {
            return;
        }

        // Capture page source
        const pageContent = document.documentElement.outerHTML;

        // Show loading overlay
        showLoadingOverlay();

        // Send content to OpenAI API
        summarizeContent(apiKey, pageContent);
    }

    // Handler for resetting the API key
    function onApiKeyReset() {
        const newKey = prompt('Please enter your OpenAI API key:', '');
        if (newKey) {
            localStorage.setItem('openai_api_key', newKey.trim());
            alert('API key updated successfully.');
        }
    }

    // Function to get the API key
    function getApiKey() {
        let apiKey = localStorage.getItem('openai_api_key');
        if (!apiKey) {
            apiKey = prompt('Please enter your OpenAI API key:', '');
            if (apiKey) {
                localStorage.setItem('openai_api_key', apiKey.trim());
            } else {
                alert('API key is required to generate a summary.');
                return null;
            }
        }
        return apiKey.trim();
    }

    // Function to show the loading overlay with animation
    function showLoadingOverlay() {
        // Create the loading overlay
        const loadingDiv = document.createElement('div');
        loadingDiv.id = 'summarize-loading';
        loadingDiv.innerHTML = `
            <div>Generating summary...</div>
            <button id="summarize-cancel">Cancel</button>
        `;
        document.body.appendChild(loadingDiv);

        // Add event listener for cancel button
        document.getElementById('summarize-cancel').addEventListener('click', onCancelRequest);
    }

    // Handler to cancel the API request
    function onCancelRequest() {
        if (xhrRequest) {
            xhrRequest.abort();
            removeLoadingOverlay();
        }
    }

    // Function to remove the loading overlay
    function removeLoadingOverlay() {
        const loadingDiv = document.getElementById('summarize-loading');
        if (loadingDiv) {
            loadingDiv.remove();
        }
    }

    // Function to display the summary in an overlay
    function showSummaryOverlay(summaryText) {
        // Create the overlay
        const overlay = document.createElement('div');
        overlay.id = 'summarize-overlay';
        overlay.innerHTML = `
            <div id="summarize-close">&times;</div>
            <h2>Summary</h2>
            <div>${summaryText.replace(/\n/g, '<br>')}</div>
        `;
        document.body.appendChild(overlay);

        // Add event listener for close button
        document.getElementById('summarize-close').addEventListener('click', () => {
            overlay.remove();
        });
    }

    // Function to display an error notification
    function showErrorNotification(message) {
        const errorDiv = document.createElement('div');
        errorDiv.id = 'summarize-error';
        errorDiv.innerText = message;
        document.body.appendChild(errorDiv);

        // Remove the notification after 2 seconds
        setTimeout(() => {
            errorDiv.remove();
        }, 2000);
    }

    // Variable to hold the XMLHttpRequest for cancellation
    let xhrRequest = null;

    // Function to summarize the content using OpenAI API
    function summarizeContent(apiKey, content) {
        // Prepare the API request
        const apiUrl = 'https://api.openai.com/v1/chat/completions';
        const requestData = {
            model: 'gpt-4o-mini',
            messages: [
                { role: 'system', content: 'You are a helpful assistant that summarizes articles based on the HTML content provided. And gives a concise summary of the article, add a small introduction and conclusion, in the middle list topics but instead of bullet points use the most appropriate emoji to indicate the topic. Always use HTML tags to structure the summary.' },
                { role: 'user', content: `Page content: \n\n${content}` }
            ],
            max_tokens: 500,
            temperature: 0.5,
            n: 1,
            stream: false
        };

        // Send the request using GM_xmlhttpRequest
        xhrRequest = GM_xmlhttpRequest({
            method: 'POST',
            url: apiUrl,
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${apiKey}`
            },
            data: JSON.stringify(requestData),
            onload: function(response) {
                removeLoadingOverlay();
                if (response.status === 200) {
                    const resData = JSON.parse(response.responseText);
                    const summary = resData.choices[0].message.content;
                    showSummaryOverlay(summary);
                } else {
                    showErrorNotification('Error: Failed to retrieve summary.');
                }
            },
            onerror: function() {
                removeLoadingOverlay();
                showErrorNotification('Error: Network error.');
            },
            onabort: function() {
                removeLoadingOverlay();
                showErrorNotification('Request canceled.');
            }
        });
    }

})();