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.1422
// @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==

(async 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();
        }
    });

    // Adicionar o botão de resumir se a página for um artigo
    if (await isArticlePage()) {
        addSummarizeButton();
    }

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

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

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

        // 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)) {
            return true;
        }

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

        return false;
    }

    // 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"
    async function onSummarizeShortcut() {
        const isArticle = await isArticlePage();
        if (!isArticle) {
            alert('Esta página pode não ser um artigo. Prosseguindo para resumir de qualquer forma.');
        }
        await processSummarization();
    }

    // Função para processar a sumarização
    async function processSummarization() {
        const apiKey = await getApiKey();
        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>');

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

    // Handler para resetar a chave da API
    async function onApiKeyReset() {
        const newKey = prompt('Por favor, insira sua chave de API da OpenAI:', '');
        if (newKey) {
            try {
                await GM.setValue('openai_api_key', newKey.trim());
                alert('Chave de API atualizada com sucesso.');
            } catch (error) {
                alert('Erro ao atualizar a chave de API.');
                console.error(error);
            }
        }
    }

    // Função para obter a chave da API
    async function getApiKey() {
        try {
            let apiKey = await GM.getValue('openai_api_key');
            if (!apiKey) {
                apiKey = prompt('Por favor, insira sua chave de API da OpenAI:', '');
                if (apiKey) {
                    await GM.setValue('openai_api_key', apiKey.trim());
                } else {
                    alert('A chave de API é necessária para gerar um resumo.');
                    return null;
                }
            }
            return apiKey.trim();
        } catch (error) {
            alert('Erro ao obter a chave de API.');
            console.error(error);
            return null;
        }
    }

    // Função para exibir a sobreposição de resumo
    function showSummaryOverlay(content = '') {
        // 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() {
            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) {
        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(() => {
            errorDiv.remove();
        }, 4000);
    }

    // Função para resumir o conteúdo usando a API da OpenAI (não streaming)
    async 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
        };

        try {
            const response = await GM.xmlHttpRequest({
                method: 'POST',
                url: apiUrl,
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${apiKey}`
                },
                data: JSON.stringify(requestData)
            });

            if (response.status === 200) {
                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>');
                }
            } else if (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}`);
                updateSummaryOverlay(`<p>Erro: Falha ao recuperar o resumo. Status: ${response.status}</p>`);
            }
        } catch (error) {
            showErrorNotification('Erro: Problema de rede.');
            updateSummaryOverlay('<p>Erro: Problema de rede.</p>');
            console.error(error);
        }
    }

})();