Summarize with AI

Adiciona um botão ou atalho de teclado para resumir artigos, notícias e conteúdos similares usando a API da OpenAI (modelo gpt-4o-mini). O resumo é exibido em uma sobreposição com estilos aprimorados e animação de carregamento.

当前为 2024-09-29 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Summarize with AI
// @namespace    https://github.com/insign/summarize-with-ai
// @version      2024.10.11.1453
// @description  Adiciona um botão ou atalho de teclado para resumir artigos, notícias e conteúdos similares usando a API da OpenAI (modelo gpt-4o-mini). O resumo é exibido em uma sobreposição com estilos aprimorados e animação de carregamento.
// @author       Hélio
// @license      GPL-3.0
// @match        *://*/*
// @grant        GM.addStyle
// @grant        GM.xmlHttpRequest
// @grant        GM.setValue
// @grant        GM.getValue
// @connect      api.openai.com
// ==/UserScript==

(function() {
    'use strict';

    /*** Inicialização ***/

    // Adicionar evento de teclado para a tecla 'S' para acionar a sumarização
    document.addEventListener('keydown', function(e) {
        const activeElement = document.activeElement;
        const isInput = activeElement && (['INPUT', 'TEXTAREA'].includes(activeElement.tagName) || activeElement.isContentEditable);
        if (!isInput && (e.key === 's' || e.key === 'S')) {
            onSummarizeShortcut();
        }
    });

    // Verificar se a página é um artigo e adicionar o botão se for
    isArticlePage().then(function(isArticle) {
        if (isArticle) {
            addSummarizeButton();
        }
    }).catch(function(error) {
        console.error('Erro ao verificar se a página é um artigo:', error);
    });

    /*** Definições de Funções ***/

    // Função para determinar se a página é um artigo
    function isArticlePage() {
        return new Promise(function(resolve, reject) {
            try {
                // Verificar se existe um elemento <article>
                if (document.querySelector('article')) {
                    resolve(true);
                    return;
                }

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

                // Verificar se a URL contém termos relacionados a notícias ou artigos
                const url = window.location.href;
                if (/news|article|story|post/i.test(url)) {
                    resolve(true);
                    return;
                }

                // Verificar o conteúdo textual significativo (mais de 500 palavras)
                const bodyText = document.body.innerText || "";
                const wordCount = bodyText.split(/\s+/).length;
                if (wordCount > 500) {
                    resolve(true);
                    return;
                }

                resolve(false);
            } catch (error) {
                reject(error);
            }
        });
    }

    // Função para adicionar o botão de sumarização
    function addSummarizeButton() {
        // Criar o elemento do botão
        const button = document.createElement('div');
        button.id = 'summarize-button';
        button.innerText = 'S';
        document.body.appendChild(button);

        // Adicionar listeners de eventos
        button.addEventListener('click', onSummarizeClick);
        button.addEventListener('dblclick', onApiKeyReset);

        // Adicionar estilos via GM.addStyle
        GM.addStyle(`
            #summarize-button {
                position: fixed;
                bottom: 20px;
                right: 20px;
                width: 50px;
                height: 50px;
                background-color: rgba(0, 123, 255, 0.9);
                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);
                transition: background-color 0.3s, transform 0.3s;
            }
            #summarize-button:hover {
                background-color: rgba(0, 123, 255, 1);
                transform: scale(1.1);
            }
            #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;
                font-size: 1.1em;
                max-width: 90%;
                max-height: 90%;
                border-radius: 8px;
            }
            #summarize-overlay h2 {
                margin-top: 0;
                font-size: 1.5em;
            }
            #summarize-close {
                position: absolute;
                top: 10px;
                right: 10px;
                cursor: pointer;
                font-size: 22px;
            }
            #summarize-content {
                margin-top: 20px;
            }
            #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;
                font-size: 14px;
            }
            .glow {
                font-size: 1.2em;
                color: #fff;
                text-align: center;
                animation: glow 1.5s ease-in-out infinite alternate;
            }
            @keyframes glow {
                from {
                    text-shadow: 0 0 10px #00e6e6, 0 0 20px #00e6e6, 0 0 30px #00e6e6, 0 0 40px #00e6e6, 0 0 50px #00e6e6, 0 0 60px #00e6e6;
                }
                to {
                    text-shadow: 0 0 20px #00ffff, 0 0 30px #00ffff, 0 0 40px #00ffff, 0 0 50px #00ffff, 0 0 60px #00ffff, 0 0 70px #00ffff;
                }
            }
            /* Media Queries para dispositivos móveis */
            @media (max-width: 768px) {
                #summarize-button {
                    width: 60px;
                    height: 60px;
                    font-size: 28px;
                    line-height: 60px;
                    bottom: 15px;
                    right: 15px;
                }
                #summarize-overlay {
                    width: 95%;
                    height: 95%;
                }
                #summarize-error {
                    bottom: 15px;
                    left: 15px;
                    font-size: 12px;
                }
            }
            /* Ajustes para telas muito pequenas */
            @media (max-width: 480px) {
                #summarize-button {
                    width: 70px;
                    height: 70px;
                    font-size: 32px;
                    line-height: 70px;
                    bottom: 10px;
                    right: 10px;
                }
                #summarize-overlay {
                    padding: 15px;
                }
                #summarize-error {
                    padding: 8px 16px;
                    font-size: 11px;
                }
            }
        `);
    }

    // Handler para o clique no botão "S"
    function onSummarizeClick() {
        processSummarization();
    }

    // Handler para o atalho de teclado "S"
    function onSummarizeShortcut() {
        isArticlePage().then(function(isArticle) {
            if (!isArticle) {
                alert('Esta página pode não ser um artigo. Prosseguindo para resumir de qualquer forma.');
            }
            processSummarization();
        }).catch(function(error) {
            console.error('Erro ao verificar se a página é um artigo no atalho:', error);
            processSummarization();
        });
    }

    // Função para processar a sumarização
    function processSummarization() {
        getApiKey().then(function(apiKey) {
            if (!apiKey) {
                return;
            }

            // Capturar o conteúdo da página
            const pageContent = document.documentElement.outerHTML;

            // Mostrar sobreposição de resumo com mensagem de carregamento
            showSummaryOverlay('<p class="glow">Gerando resumo...</p>');

            // Enviar conteúdo para a API da OpenAI
            summarizeContent(apiKey, pageContent);
        }).catch(function(error) {
            showErrorNotification('Erro: Falha ao obter a chave da API.');
            updateSummaryOverlay('<p>Erro: Falha ao obter a chave da API.</p>');
            console.error('Erro ao obter a chave da API:', error);
        });
    }

    // Handler para resetar a chave da API
    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);
            });
        }
    }

    // Função para obter a chave da API
    function getApiKey() {
        return new Promise(function(resolve, reject) {
            GM.getValue('openai_api_key').then(function(apiKey) {
                if (apiKey) {
                    resolve(apiKey.trim());
                } else {
                    const userInput = prompt('Por favor, insira sua chave de API da OpenAI:', '');
                    if (userInput) {
                        GM.setValue('openai_api_key', userInput.trim()).then(function() {
                            resolve(userInput.trim());
                        }).catch(function(error) {
                            reject(error);
                        });
                    } else {
                        alert('A chave de API é necessária para gerar um resumo.');
                        resolve(null);
                    }
                }
            }).catch(function(error) {
                reject(error);
            });
        });
    }

    // Função para exibir a sobreposição de resumo
    function showSummaryOverlay(content) {
        // Verificar se a sobreposição já existe
        if (document.getElementById('summarize-overlay')) {
            updateSummaryOverlay(content);
            return;
        }

        // Criar a sobreposição
        const overlay = document.createElement('div');
        overlay.id = 'summarize-overlay';
        overlay.innerHTML = `
            <div id="summarize-close">&times;</div>
            <div id="summarize-content">${content}</div>
        `;
        document.body.appendChild(overlay);

        // Desabilitar a rolagem de fundo
        document.body.style.overflow = 'hidden';

        // Adicionar listeners para fechar a sobreposição
        document.getElementById('summarize-close').addEventListener('click', closeOverlay);
        overlay.addEventListener('click', function(e) {
            if (e.target === overlay) {
                closeOverlay();
            }
        });
        document.addEventListener('keydown', onEscapePress);

        function onEscapePress(e) {
            if (e.key === 'Escape') {
                closeOverlay();
            }
        }

        function closeOverlay() {
            if (document.getElementById('summarize-overlay')) {
                document.getElementById('summarize-overlay').remove();
                document.body.style.overflow = '';
                document.removeEventListener('keydown', onEscapePress);
            }
        }
    }

    // Função para atualizar o conteúdo da sobreposição de resumo
    function updateSummaryOverlay(content) {
        const contentDiv = document.getElementById('summarize-content');
        if (contentDiv) {
            contentDiv.innerHTML = content;
        }
    }

    // Função para exibir uma notificação de erro
    function showErrorNotification(message) {
        // Verificar se a notificação já existe
        if (document.getElementById('summarize-error')) {
            document.getElementById('summarize-error').innerText = message;
            return;
        }

        const errorDiv = document.createElement('div');
        errorDiv.id = 'summarize-error';
        errorDiv.innerText = message;
        document.body.appendChild(errorDiv);

        // Remover a notificação após 4 segundos
        setTimeout(function() {
            if (document.getElementById('summarize-error')) {
                document.getElementById('summarize-error').remove();
            }
        }, 4000);
    }

    // Função para resumir o conteúdo usando a API da OpenAI (não streaming)
    function summarizeContent(apiKey, content) {
        const userLanguage = navigator.language || 'pt-BR'; // Ajuste para português por padrão

        // Preparar a requisição para a API
        const apiUrl = 'https://api.openai.com/v1/chat/completions';
        const requestData = {
            model: 'gpt-4o-mini',
            messages: [
                {
                    role: 'system',
                    content: `Você é um assistente útil que resume artigos com base no conteúdo HTML fornecido. Você deve gerar um resumo conciso que inclua uma breve introdução, seguida por uma lista de tópicos e termine com uma breve conclusão. Para os tópicos, você deve usar emojis apropriados como marcadores, e os tópicos devem consistir em títulos descritivos resumindo o assunto do tópico.

Você deve sempre usar tags HTML para estruturar o texto do resumo. O título deve estar envolvido em tags h2, 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 destino final, e você nunca deve usar markdown.

Estrutura necessária:
- Use h2 para o título do resumo
- Use parágrafos para a introdução e conclusão
- Use emojis apropriados para 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: `Conteúdo da página:\n\n${content}` }
            ],
            max_tokens: 500,
            temperature: 0.5,
            n: 1,
            stream: false
        };

        GM.xmlHttpRequest({
            method: 'POST',
            url: apiUrl,
            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;
                            updateSummaryOverlay(summary.replace(/\n/g, '<br>'));
                        } else {
                            showErrorNotification('Erro: Resposta inválida da API.');
                            updateSummaryOverlay('<p>Erro: Resposta inválida da API.</p>');
                        }
                    } catch (parseError) {
                        showErrorNotification('Erro: Falha ao processar a resposta da API.');
                        updateSummaryOverlay('<p>Erro: Falha ao processar a resposta da API.</p>');
                        console.error('Erro ao analisar a resposta da API:', parseError);
                    }
                } else if (response && response.status === undefined) {
                    // Tratamento para caso o status esteja 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) {
                    showErrorNotification('Erro: Chave de API inválida.');
                    updateSummaryOverlay('<p>Erro: Chave de API inválida.</p>');
                } else {
                    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() {
                showErrorNotification('Erro: Problema de rede.');
                updateSummaryOverlay('<p>Erro: Problema de rede.</p>');
            },
            onabort: function() {
                showErrorNotification('Requisição cancelada.');
                updateSummaryOverlay('<p>Requisição cancelada.</p>');
            }
        });
    }

})();