Summarize with AI

Adds a button or keyboard shortcut to summarize articles, news, and similar content using the OpenAI API (gpt-4o-mini model). The summary is displayed in an overlay with enhanced styling and a loading animation.

当前为 2024-10-07 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

作者
insign
评分
0 0 0
版本
2024.10.11.1516
创建于
2024-09-19
更新于
2024-10-07
大小
24.1 KB
许可证
GPL-3.0
适用于
所有网站

// ==UserScript== // @name Summarize with AI // @namespace https://github.com/insign/summarize-with-ai // @version 2024.10.11.1517 // @description Adds a button or keyboard shortcut to summarize articles, news, and similar content using the OpenAI API (gpt-4o-mini model). The summary is displayed in an overlay with enhanced styling and a loading animation. // @author Hélio // @license GPL-3.0 // @match :///* // @grant GM.addStyle // @grant GM.xmlHttpRequest // @grant GM.setValue // @grant GM.getValue // @grant GM.getResourceURL // @connect api.openai.com // @require https://www.unpkg.com/@mozilla/[email protected]/Readability.js // ==/UserScript==

(function() { 'use strict';

/*** Constants ***/
const BUTTON_ID = 'summarize-button';
const OVERLAY_ID = 'summarize-overlay';
const CLOSE_BUTTON_ID = 'summarize-close';
const CONTENT_ID = 'summarize-content';
const ERROR_ID = 'summarize-error';
const API_URL = 'https://api.openai.com/v1/chat/completions';
const VERSION = '2024.10.11.1517';

/*** Initialization ***/

// Variable to store whether the current page is an article
let isArticle = false;
// Variables to store article title and content
let articleTitle = '';
let articleContent = '';

// Initialize the script
initialize();

/**
 * Initializes the userscript by detecting if the page is an article,
 * setting up the summarize button and keyboard shortcuts accordingly.
 */
async function initialize() {
    try {
        // Use Readability to parse the article
        const articleData = getArticleData();
        if (articleData) {
            isArticle = true;
            articleTitle = articleData.title;
            articleContent = articleData.content;
            addSummarizeButton();
            setupKeyboardShortcuts();
        } else {
            isArticle = false;
            hideSummarizeButton();
            disableKeyboardShortcuts();
            console.log('Summarize with AI: No article detected on this page.');
            return; // Disable further script functionality
        }

        // Set up event listeners to handle focus changes
        setupFocusListeners();
    } catch (error) {
        console.error('Initialization error:', error);
        showErrorNotification('Erro durante a inicialização.');
    }
}

/*** Function Definitions ***/

/**
 * Uses Mozilla's Readability to extract the article's title and content.
 * @returns {Object|null} An object containing the title and content if an article is found, otherwise null.
 */
function getArticleData() {
    try {
        const documentClone = document.cloneNode(true);
        const readability = new Readability(documentClone);
        const article = readability.parse();

        if (article && article.content && article.title) {
            return {
                title: article.title,
                content: article.textContent // Using textContent to send plain text to the API
            };
        } else {
            return null;
        }
    } catch (error) {
        console.error('Readability parsing error:', error);
        return null;
    }
}

/**
 * Adds the summarize button to the page with standardized styling.
 * The button is fixed at the bottom-right corner and has a high z-index.
 */
function addSummarizeButton() {
    // Create the button element
    const button = document.createElement('div');
    button.id = BUTTON_ID;
    button.innerText = 'S';
    document.body.appendChild(button);

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

    // Inject CSS styles using GM.addStyle to standardize them and prevent inheritance
    GM.addStyle(`
        /* Summarize Button Styling */
        #${BUTTON_ID} {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 60px;
            height: 60px;
            background-color: rgba(0, 123, 255, 0.9);
            color: white;
            font-size: 28px;
            font-weight: bold;
            text-align: center;
            line-height: 60px;
            border-radius: 50%;
            cursor: pointer;
            z-index: 100001; /* Increased z-index to ensure visibility */
            box-shadow: 0 2px 5px rgba(0,0,0,0.3);
            transition: background-color 0.3s, transform 0.3s;
            user-select: none;
            font-family: Arial, sans-serif;
        }
        /* Hover effect for the summarize button */
        #${BUTTON_ID}:hover {
            background-color: rgba(0, 123, 255, 1);
            transform: scale(1.1);
        }

        /* Summary Overlay Styling */
        #${OVERLAY_ID} {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background-color: #ffffff;
            z-index: 100002; /* Increased z-index to ensure it's above all elements */
            padding: 30px;
            box-shadow: 0 0 15px rgba(0,0,0,0.5);
            overflow: auto;
            font-size: 1.2em;
            max-width: 90%;
            max-height: 90%;
            border-radius: 10px;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            font-family: Arial, sans-serif;
        }

        /* Close Button Styling */
        #${CLOSE_BUTTON_ID} {
            position: absolute;
            top: 15px;
            right: 15px;
            cursor: pointer;
            font-size: 26px;
            transition: transform 0.2s;
            color: #555555;
        }
        /* Tooltip for Close Button on Hover */
        #${CLOSE_BUTTON_ID}:hover::after {
            content: "ESC";
            position: absolute;
            top: -12px;
            right: -10px;
            background: rgba(0, 0, 0, 0.7);
            color: white;
            padding: 2px 6px;
            border-radius: 4px;
            font-size: 12px;
            white-space: nowrap;
        }

        /* Summary Content Styling */
        #${CONTENT_ID} {
            margin-top: 40px;
            width: 100%;
            color: #333333;
        }

        /* Error Notification Styling */
        #${ERROR_ID} {
            position: fixed;
            bottom: 20px;
            left: 20px;
            background-color: rgba(255,0,0,0.8);
            color: white;
            padding: 10px 20px;
            border-radius: 5px;
            z-index: 100003; /* Higher than overlay */
            font-size: 14px;
            font-family: Arial, sans-serif;
        }

        /* Loading Text Animation */
        .glow {
            font-size: 1.5em;
            color: #333;
            text-align: center;
            animation: glow 2s ease-in-out infinite alternate;
        }
        @keyframes glow {
            from {
                color: #4b6cb7;
                text-shadow: 0 0 10px #4b6cb7, 0 0 20px #4b6cb7, 0 0 30px #4b6cb7;
            }
            to {
                color: #182848;
                text-shadow: 0 0 20px #8e2de2, 0 0 30px #8e2de2, 0 0 40px #8e2de2;
            }
        }

        /* Media Queries for Mobile Devices */
        @media (max-width: 768px) {
            #${BUTTON_ID} {
                width: 70px;
                height: 70px;
                font-size: 32px;
                line-height: 70px;
                bottom: 15px;
                right: 15px;
            }
            #${OVERLAY_ID} {
                width: 95%;
                height: 95%;
                padding: 25px;
            }
            #${ERROR_ID} {
                bottom: 15px;
                left: 15px;
                font-size: 12px;
            }
        }

        /* Additional Adjustments for Very Small Screens */
        @media (max-width: 480px) {
            #${BUTTON_ID} {
                width: 80px;
                height: 80px;
                font-size: 36px;
                line-height: 80px;
                bottom: 10px;
                right: 10px;
            }
            #${OVERLAY_ID} {
                padding: 20px;
            }
            #${ERROR_ID} {
                padding: 8px 16px;
                font-size: 11px;
            }
        }

        /* Remove Default Bullet Points from Lists */
        #${CONTENT_ID} ul {
            list-style: none;
            padding: 0;
        }
    `);
}

/**
 * Sets up keyboard shortcuts by adding a keydown event listener.
 * The 'S' key triggers the summarization unless an input element is focused.
 */
function setupKeyboardShortcuts() {
    document.addEventListener('keydown', handleKeyDown);
}

/**
 * Disables keyboard shortcuts by removing the keydown event listener.
 */
function disableKeyboardShortcuts() {
    document.removeEventListener('keydown', handleKeyDown);
}

/**
 * Handles the keydown event to trigger summarization when 'S' key is pressed.
 * Ignores the event if an input, textarea, or contenteditable element is focused.
 * @param {KeyboardEvent} e - The keyboard event.
 */
function handleKeyDown(e) {
    // Check if any input-related element is focused
    const activeElement = document.activeElement;
    const isInput = activeElement && (['INPUT', 'TEXTAREA', 'SELECT'].includes(activeElement.tagName) || activeElement.isContentEditable);
    if (isInput) {
        hideSummarizeButton();
        return;
    }

    // If 'S' or 's' is pressed, trigger summarization
    if (e.key.toLowerCase() === 's') {
        e.preventDefault(); // Prevent default behavior
        onSummarizeShortcut();
    } else {
        // Show the summarize button if 'S' is not pressed and not in input
        showSummarizeButton();
    }
}

/**
 * Sets up listeners to monitor focus changes on the page.
 * Hides the summarize button when an input element is focused and shows it otherwise.
 */
function setupFocusListeners() {
    // Listen for focusin and focusout events to handle button visibility
    document.addEventListener('focusin', handleFocusChange);
    document.addEventListener('focusout', handleFocusChange);
}

/**
 * Handles focus changes to show or hide the summarize button.
 * @param {FocusEvent} e - The focus event.
 */
function handleFocusChange(e) {
    const activeElement = document.activeElement;
    const isInput = activeElement && (['INPUT', 'TEXTAREA', 'SELECT'].includes(activeElement.tagName) || activeElement.isContentEditable);

    if (isInput) {
        hideSummarizeButton();
    } else if (isArticle) {
        showSummarizeButton();
    }
}

/**
 * Shows the summarize button by setting its display to block.
 */
function showSummarizeButton() {
    const button = document.getElementById(BUTTON_ID);
    if (button) {
        button.style.display = 'block';
    }
}

/**
 * Hides the summarize button by setting its display to none.
 */
function hideSummarizeButton() {
    const button = document.getElementById(BUTTON_ID);
    if (button) {
        button.style.display = 'none';
    }
}

/**
 * Handles the click event on the summarize button to initiate summarization.
 */
function onSummarizeClick() {
    processSummarization();
}

/**
 * Handles the keyboard shortcut for summarization when 'S' key is pressed.
 * Alerts the user if the page might not be an article but proceeds to summarize.
 */
function onSummarizeShortcut() {
    if (!isArticle) {
        alert('Esta página pode não ser um artigo. Continuando para resumir de qualquer maneira.');
    }
    processSummarization();
}

/**
 * Initiates the summarization process by obtaining the API key,
 * preparing the article data, displaying the loading overlay,
 * and sending the data to the OpenAI API.
 */
async function processSummarization() {
    try {
        const apiKey = await getApiKey();
        if (!apiKey) {
            return;
        }

        // Prepare the data to send to the API
        const payload = {
            title: articleTitle,
            content: articleContent
        };

        // Display the overlay com animação de carregamento
        showSummaryOverlay('<p class="glow">Gerando resumo...</p>');

        // Envie os dados para a API da OpenAI para resumir
        await summarizeContent(apiKey, payload);
    } catch (error) {
        showErrorNotification('Erro: Falha ao iniciar a sumarização.');
        updateSummaryOverlay('<p>Erro: Falha ao iniciar a sumarização.</p>');
        console.error('Erro no processo de sumarização:', error);
    }
}

/**
 * Handles the double-click event on the summarize button to reset the API key.
 */
function onApiKeyReset() {
    const newKey = prompt('Por favor, insira sua chave de API da OpenAI:', '');
    if (newKey) {
        GM.setValue('openai_api_key', newKey.trim()).then(function() {
            alert('Chave de API atualizada com sucesso.');
        }).catch(function(error) {
            alert('Erro ao atualizar a chave de API.');
            console.error('Erro ao atualizar a chave de API:', error);
        });
    }
}

/**
 * Retrieves the OpenAI API key from storage.
 * If not found, prompts the user to input it and saves it.
 * @returns {Promise<string|null>} Promise resolving to the API key or null if not provided.
 */
async function getApiKey() {
    try {
        let apiKey = await GM.getValue('openai_api_key');
        if (apiKey) {
            return apiKey.trim();
        } else {
            const userInput = prompt('Por favor, insira sua chave de API da OpenAI:', '');
            if (userInput) {
                apiKey = userInput.trim();
                await GM.setValue('openai_api_key', apiKey);
                return apiKey;
            } else {
                alert('É necessária uma chave de API para gerar um resumo.');
                return null;
            }
        }
    } catch (error) {
        console.error('Erro ao recuperar a chave de API:', error);
        alert('Falha ao recuperar a chave de API.');
        return null;
    }
}

/**
 * Displays the summary overlay with the provided content.
 * Adds functionality to close the overlay by clicking the close button,
 * clicking outside the content area, or pressing the 'Escape' key.
 * @param {string} content - HTML content to display inside the overlay.
 */
function showSummaryOverlay(content) {
    // Check if the overlay already exists to prevent multiple instances
    if (document.getElementById(OVERLAY_ID)) {
        updateSummaryOverlay(content);
        return;
    }

    // Create the overlay element
    const overlay = document.createElement('div');
    overlay.id = OVERLAY_ID;
    overlay.innerHTML = `
        <div id="${CLOSE_BUTTON_ID}">&times;</div>
        <div id="${CONTENT_ID}">${content}</div>
    `;
    document.body.appendChild(overlay);

    // Disable background scrolling when the overlay is open
    document.body.style.overflow = 'hidden';

    // Add event listener to the close button
    document.getElementById(CLOSE_BUTTON_ID).addEventListener('click', closeOverlay);

    // Add event listener to close the overlay when clicking outside the content area
    overlay.addEventListener('click', function(e) {
        if (e.target === overlay) {
            closeOverlay();
        }
    });

    // Add event listener for the 'Escape' key to close the overlay
    document.addEventListener('keydown', onEscapePress);

    /**
     * Handles the 'Escape' key press to close the overlay.
     * @param {KeyboardEvent} e - The keyboard event.
     */
    function onEscapePress(e) {
        if (e.key === 'Escape') {
            closeOverlay();
        }
    }

    /**
     * Closes the summary overlay and re-enables background scrolling.
     */
    function closeOverlay() {
        const existingOverlay = document.getElementById(OVERLAY_ID);
        if (existingOverlay) {
            existingOverlay.remove();
            document.body.style.overflow = '';
            document.removeEventListener('keydown', onEscapePress);
        }
    }
}

/**
 * Updates the content within the summary overlay.
 * @param {string} content - New HTML content to display.
 */
function updateSummaryOverlay(content) {
    const contentDiv = document.getElementById(CONTENT_ID);
    if (contentDiv) {
        contentDiv.innerHTML = content;
    }
}

/**
 * Displays an error notification at the bottom-left corner of the page.
 * @param {string} message - The error message to display.
 */
function showErrorNotification(message) {
    // Check if an error notification already exists
    if (document.getElementById(ERROR_ID)) {
        document.getElementById(ERROR_ID).innerText = message;
        return;
    }

    // Create the error notification element
    const errorDiv = document.createElement('div');
    errorDiv.id = ERROR_ID;
    errorDiv.innerText = message;
    document.body.appendChild(errorDiv);

    // Remove the error notification after 4 seconds
    setTimeout(function() {
        const existingError = document.getElementById(ERROR_ID);
        if (existingError) {
            existingError.remove();
        }
    }, 4000);
}

/**
 * Sends the article data to the OpenAI API to generate a summary.
 * Handles the API response and updates the overlay with the summary or error messages.
 * @param {string} apiKey - The OpenAI API key.
 * @param {Object} payload - An object containing the title and content of the article.
 */
async function summarizeContent(apiKey, payload) {
    try {
        // Prepare the API request payload
        const userLanguage = navigator.language || 'pt-BR'; // Default to Portuguese if navigator.language is undefined

        const requestData = {
            model: 'gpt-4o-mini',
            messages: [
                {
                    role: 'system',
                    content: `Você é um assistente útil que resume artigos com base no título e conteúdo fornecidos. Você deve gerar um resumo conciso que inclua uma breve introdução, seguida por uma lista de tópicos e termine com uma curta conclusão. Para os tópicos, use emojis apropriados como marcadores, e os tópicos devem consistir em títulos descritivos que resumem o assunto de cada tópico.

Você deve sempre usar tags HTML para estruturar o texto do resumo. O título deve estar envolto em tags

, e você deve sempre usar o idioma do usuário além do idioma original do artigo. O HTML gerado deve estar pronto para ser injetado no local de destino, e você nunca deve usar markdown.

Estrutura requerida:

  • Use

    para o título do resumo

  • Use parágrafos para a introdução e conclusão
  • Use emojis apropriados para os tópicos
  • Não adicione textos como "Resumo do Artigo" ou "Sumário do artigo" no resumo, nem "Introdução", "Tópicos", "Conclusão", etc.

Idioma do usuário: ${userLanguage}. Adapte o texto para ser curto, conciso e informativo. }, { role: 'user', content:Título: ${payload.title}\n\nConteúdo: ${payload.content}` } ], max_tokens: 500, temperature: 0.5, n: 1, stream: false };

        // Envie a requisição para a API da OpenAI usando GM.xmlHttpRequest
        GM.xmlHttpRequest({
            method: 'POST',
            url: API_URL,
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${apiKey}`
            },
            data: JSON.stringify(requestData),
            onload: function(response) {
                if (response && response.status === 200) {
                    try {
                        const resData = JSON.parse(response.responseText);
                        if (resData.choices && resData.choices.length > 0) {
                            const summary = resData.choices[0].message.content;

                            // Substitui quebras de linha por <br> para renderização HTML
                            const formattedSummary = summary.replace(/\n/g, '<br>');

                            // Atualiza o overlay com o resumo gerado
                            updateSummaryOverlay(formattedSummary);
                        } else {
                            showErrorNotification('Erro: Resposta inválida da API.');
                            updateSummaryOverlay('<p>Erro: Resposta inválida da API.</p>');
                        }
                    } catch (parseError) {
                        showErrorNotification('Erro: Falha ao analisar a resposta da API.');
                        updateSummaryOverlay('<p>Erro: Falha ao analisar a resposta da API.</p>');
                        console.error('Erro ao analisar a resposta da API:', parseError);
                    }
                } else if (response && response.status === undefined) {
                    // Lida com casos onde response.status está indefinido
                    showErrorNotification('Erro: Resposta inesperada da API.');
                    console.error('Resposta da API sem status:', response);
                    updateSummaryOverlay('<p>Erro: Resposta inesperada da API.</p>');
                } else if (response && response.status === 401) {
                    // Lida com acesso não autorizado (chave de API inválida)
                    showErrorNotification('Erro: Chave de API inválida.');
                    updateSummaryOverlay('<p>Erro: Chave de API inválida.</p>');
                } else {
                    // Lida com outros tipos de erros
                    showErrorNotification(`Erro: Falha ao recuperar o resumo. Status: ${response.status || 'N/A'}`);
                    updateSummaryOverlay(`<p>Erro: Falha ao recuperar o resumo. Status: ${response.status || 'N/A'}</p>`);
                }
            },
            onerror: function() {
                // Lida com erros de rede
                showErrorNotification('Erro: Problema de rede.');
                updateSummaryOverlay('<p>Erro: Problema de rede.</p>');
            },
            onabort: function() {
                // Lida com aborto da requisição
                showErrorNotification('Requisição abortada.');
                updateSummaryOverlay('<p>Requisição abortada.</p>');
            }
        });
    } catch (error) {
        showErrorNotification('Erro: Falha ao comunicar com a API.');
        updateSummaryOverlay('<p>Erro: Falha ao comunicar com a API.</p>');
        console.error('Erro de comunicação com a API:', error);
    }
}

})();