GPT4Free Page Summarizer

Summarize webpage, selected text or YouTube transcript via local API

当前为 2025-04-17 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         GPT4Free Page Summarizer
// @version      1.7.3
// @description  Summarize webpage, selected text or YouTube transcript via local API
// @author       SH3LL
// @match        *://*/*
// @grant        GM.xmlHttpRequest
// @run-at       document-end
// @namespace http://tampermonkey.net/
// ==/UserScript==

(function () {
    'use strict';

    // === Globals ===
    let loading = false;
    let ytTranscript = null;
    let isSidebarVisible = false;
    let isTextSelected = false;
    let hoverTimeout;
    const isYouTubeVideoPage = window.location.hostname.includes("youtube.com") && window.location.pathname === "/watch";
    const browserLanguage = navigator.language;
    const selectedLanguage = getDisplayLanguage(browserLanguage);
    const modelProviderPairs = [
        { label: 'DDG / gpt-4o-mini', model: 'gpt-4o-mini', provider: 'DDG' },
        { label: 'Blackbox / DeepSeek-R1', model: 'DeepSeek-V3', provider: 'Blackbox' },
        { label: 'Free2GPT / gemini-1.5-flash', model: 'gemini-1.5-flash', provider: 'Free2GPT' },
        { label: 'PollinationsAI / gpt-4o-mini', model: 'gpt-4o-mini', provider: 'PollinationsAI' }
    ];
    let selectedModel = modelProviderPairs[0].model;
    let selectedProvider = modelProviderPairs[0].provider;


    // === Init DOM UI ===
    const { shadowRoot, sidebar, toggleButton, summarizeButton, statusDisplay, summaryContainer } = createSidebarUI();
    document.body.appendChild(shadowRoot.host);
    setTimeout(() => {
        toggleButton.style.opacity = '0.3';
    }, 2000);

    // === Start transcript load if on YouTube ===
    if (isYouTubeVideoPage) {
        loadYouTubeTranscript().then(transcript => {
            ytTranscript = transcript;
            console.log("YOUTUBE Transcript:", transcript);
            updateButtonText();
        }).catch(err => {
            console.warn("Transcript not found:", err);
            ytTranscript = null;
            updateButtonText();

            // Mostra messaggio arancione nel contenitore dei summary
            summaryContainer.style.display = 'block';
            summaryContainer.textContent = '⚠️ The video has no subtitles';
            summaryContainer.style.color = 'orange';
        });
    }


    // === Add Event Listeners ===
    document.addEventListener('mouseup', updateButtonText);
    document.addEventListener('mousedown', () => setTimeout(updateButtonText, 100));
    toggleButton.addEventListener('click', toggleSidebar);
    toggleButton.addEventListener('mouseover', () => {
        clearTimeout(hoverTimeout);
        toggleButton.style.opacity = '1';
    });

    toggleButton.addEventListener('mouseout', () => {
        hoverTimeout = setTimeout(() => {
            if (!isSidebarVisible) toggleButton.style.opacity = '0.3';
        }, 3000);
    });
    summarizeButton.addEventListener('click', handleSummarizeClick);

    // === Initial button text ===
    updateButtonText();
    updateStatusDisplay('Idle', '#888888');

    // === Language Utility ===
    function getDisplayLanguage(langCode) {
        try {
            const name = new Intl.DisplayNames([langCode], { type: 'language' }).of(langCode);
            return name || langCode;
        } catch {
            return langCode;
        }
    }

    // === Summarize Request ===
    function summarizePage(text, lang) {
        return new Promise((resolve, reject) => {
            const prompt = `Summarize the following text in ${lang}. The summary is organised in blocks of topics.
                            Return the result in a json list composed of dictionaries with fields "title" (the title starts with a contextual emoji) and "text".
                            Don't add any other sentence like "Here is the summary", or coding formatting like \"\`\`\`json\", write directly the summary itself.
                            Exclude from the summary any advertisement or sponsorization.
                            Here is the text: ${text}`;

            const payload = {
                messages: [{ role: 'user', content: prompt }],
                model: selectedModel,
                provider: selectedProvider
            };


            GM.xmlHttpRequest({
                method: 'POST',
                url: 'http://localhost:1337/v1/chat/completions',
                headers: { 'Content-Type': 'application/json' },
                data: JSON.stringify(payload),
                onload: response => {
                    const color = (response.status >= 200 && response.status < 300) ? '#00ff00' : '#ffcc00';
                    console.log(response.responseText);
                    resolve({ status: response.status, responseText: response.responseText, color });
                },
                onerror: err => reject({ message: 'Network error', color: '#ff4444' })
            });
        });
    }

    // === YouTube Transcript Extraction ===
    function loadYouTubeTranscript() {
        return new Promise((resolve, reject) => {
            const TRANSCRIPT_ID = "engagement-panel-searchable-transcript";
            const SELECTOR = `ytd-engagement-panel-section-list-renderer[target-id="${TRANSCRIPT_ID}"] #content`;

            const panelObserver = new MutationObserver((mutationsList, observerInstance) => {
                const panel = document.querySelector(`ytd-engagement-panel-section-list-renderer[target-id="${TRANSCRIPT_ID}"]`);
                if (panel) {
                    observerInstance.disconnect();
                    panel.setAttribute("visibility", "ENGAGEMENT_PANEL_VISIBILITY_EXPANDED");
                    const content = panel.querySelector('#content');
                    if (content) {
                        observeTranscriptContent(content, panel);
                    } else {
                        reject("Contenuto non trovato nel pannello.");
                    }
                }
            });

            panelObserver.observe(document.body, { childList: true, subtree: true });

            function observeTranscriptContent(content, panel) {
                const contentObserver = new MutationObserver((mutationsList, observerInstance) => {
                    const segments = content.querySelectorAll('.segment-text');
                    if (content.children.length > 0 && segments.length > 0) {
                        const transcript = Array.from(content.querySelectorAll('.segment-text'))
                        .map(seg => seg.textContent.trim())
                        .join(" ");

                        observerInstance.disconnect();
                        panel.setAttribute("visibility", "ENGAGEMENT_PANEL_VISIBILITY_HIDDEN");
                        resolve(transcript.trim());
                    }
                });

                contentObserver.observe(content, { childList: true, subtree: true });

                setTimeout(() => {
                    contentObserver.disconnect();
                    reject("Timeout: Trascrizione non trovata.");
                }, 10000);
            }

            setTimeout(() => {
                panelObserver.disconnect();
                reject("Timeout: Pannello di trascrizione non trovato.");
            }, 10000);
        });
    }





    // === Sidebar Toggle ===
    function toggleSidebar() {
        isSidebarVisible = !isSidebarVisible;
        sidebar.style.right = isSidebarVisible ? '0' : '-300px';
        toggleButton.style.right = isSidebarVisible ? '300px' : '0';

        setTimeout(() => {
            toggleButton.style.opacity = isSidebarVisible ? '1' : '0.3';
        }, 1000);
    }

    // === UI Updates ===
    function updateButtonText() {
        if (loading) {
            summarizeButton.textContent = 'Loading..';
            return;
        }

        const selectedText = window.getSelection().toString();
        if (selectedText) {
            summarizeButton.textContent = `Summary [${selectedText.substring(0, 2).trim()}..]`;
            isTextSelected = true;
        } else if (isYouTubeVideoPage) {
            if (ytTranscript && ytTranscript.trim().length > 0) {
                summarizeButton.textContent = 'Summary 📹️';
            } else {
                summarizeButton.textContent = 'Summary';
            }
            isTextSelected = false;
        } else {
            summarizeButton.textContent = 'Summary';
            isTextSelected = false;
        }
    }


    function updateStatusDisplay(text, color) {
        while (statusDisplay.firstChild) {
            statusDisplay.removeChild(statusDisplay.firstChild);
        }
        const statusLabel = document.createElement('span');
        statusLabel.textContent = 'Status: ';
        const statusText = document.createElement('span');
        statusText.textContent = text;
        statusText.style.color = color;
        const langLabel = document.createElement('span');
        langLabel.textContent = ' | Lang: ';
        const langText = document.createElement('span');
        langText.textContent = selectedLanguage;
        langText.style.color = '#00bfff';

        statusDisplay.appendChild(statusLabel);
        statusDisplay.appendChild(statusText);
        statusDisplay.appendChild(langLabel);
        statusDisplay.appendChild(langText);
    }

    function craftJson(jsonData) {
        const container = document.createElement('div');

        JSON.parse(jsonData).forEach(block => {
            const title = document.createElement('strong');
            title.textContent = block.title;

            const text = document.createElement('div');
            text.textContent = block.text;

            container.appendChild(title);
            container.appendChild(text);
            container.appendChild(document.createElement('br'));
        });

        return container;
    }

    function handleSummarizeClick() {
        if (loading) return;

        updateButtonText();
        updateStatusDisplay('Requesting..', '#888888');
        summaryContainer.style.display = 'none';

        let content = '';
        if (isYouTubeVideoPage && ytTranscript && isTextSelected!=true) {
            content = ytTranscript;
        } else if (isTextSelected) {
            content = window.getSelection().toString();
        } else {
            content = document.body.innerText;
        }

        loading = true;
        summarizeButton.disabled = true;

        summarizePage(content, selectedLanguage)
            .then(({ status, responseText, color }) => {
                try {
                    const json = JSON.parse(responseText);
                    const result = json.choices?.[0]?.message?.content || 'Invalid API response format.';

                    summaryContainer.append(craftJson(result));
                    summaryContainer.style.color = '#ffffff';
                    updateStatusDisplay(`Success (${status})`, color);
                } catch (err) {
                    summaryContainer.textContent = `Error parsing response: ${err}`;
                    summaryContainer.style.color = '#ff4444';
                    updateStatusDisplay(`Failed (${status})`, '#ff4444');
                }
            })
            .catch(err => {
                summaryContainer.textContent = `Error: ${err.message}`;
                summaryContainer.style.color = '#ff4444';
                updateStatusDisplay('Failed', err.color);
            })
            .finally(() => {
                loading = false;
                summarizeButton.disabled = false;
                updateButtonText();
                summaryContainer.style.display = 'block';
            });
    }

    // === UI Construction ===
    function createSidebarUI() {
        const host = document.createElement('div');
        const root = host.attachShadow({ mode: 'open' });

        const sidebar = document.createElement('div');
        Object.assign(sidebar.style, {
            position: 'fixed',
            right: '-300px',
            top: '0',
            width: '300px',
            height: '100vh',
            backgroundColor: '#000',
            color: '#fff',
            padding: '20px',
            zIndex: '999999',
            fontFamily: 'Arial, sans-serif',
            display: 'flex',
            flexDirection: 'column',
            gap: '10px',
            boxSizing: 'border-box',
            transition: 'right 0.3s ease',
            borderLeft: '1px solid #cccccc',
            borderTopLeftRadius: '5px',
            borderBottomLeftRadius: '5px'
        });

        const toggleBtn = document.createElement('button');
        Object.assign(toggleBtn.style, {
            position: 'fixed',
            right: '0',
            top: '20px',
            backgroundColor: '#333',
            color: '#000',
            border: '1px solid #cccccc',
            borderRadius: '6px',
            padding: '10px',
            cursor: 'pointer',
            zIndex: '1000000',
            fontSize: '14px',
            transition: 'right 0.3s ease, opacity 0.3s ease'
        });

        toggleBtn.textContent = '✨';

        const container = document.createElement('div');
        Object.assign(container.style, {
            display: 'flex',
            gap: '10px',
            alignItems: 'center'
        });

        const modelSelect = document.createElement('select');
        modelSelect.style.padding = '6px';
        modelSelect.style.fontSize = '14px';
        modelSelect.style.borderRadius = '4px';
        modelSelect.style.border = '1px solid #ccc';
        modelSelect.style.backgroundColor = '#222';
        modelSelect.style.color = '#fff';

        modelProviderPairs.forEach((pair, index) => {
            const option = document.createElement('option');
            option.value = index;
            option.textContent = pair.label;
            modelSelect.appendChild(option);
        });

        modelSelect.addEventListener('change', (e) => {
            const selected = modelProviderPairs[e.target.value];
            selectedModel = selected.model;
            selectedProvider = selected.provider;
        });

        const summarizeBtn = document.createElement('button');
        Object.assign(summarizeBtn.style, {
            backgroundColor: '#333',
            color: '#fff',
            border: '1px solid #cccccc',
            borderRadius: '6px',
            padding: '10px 20px',
            cursor: 'pointer',
            fontSize: '14px',
            flex: '1',
            transition: 'background-color 0.3s'
        });

        summarizeBtn.onmouseover = () => summarizeBtn.style.backgroundColor = '#4d4d4d';
        summarizeBtn.onmouseout = () => summarizeBtn.style.backgroundColor = '#333';

        const status = document.createElement('div');
        status.style.fontSize = '12px';



        const summary = document.createElement('div');
        Object.assign(summary.style, {
            fontSize: '14px',
            lineHeight: '1.5',
            display: 'none',
            overflowY: 'auto',
            maxHeight: 'calc(100vh - 130px)',
            whiteSpace: 'pre-line'
        });

        container.appendChild(summarizeBtn);
        sidebar.appendChild(modelSelect);
        sidebar.appendChild(container);
        sidebar.appendChild(status);
        sidebar.appendChild(summary);
        root.appendChild(sidebar);
        root.appendChild(toggleBtn);

        return {
            shadowRoot: root,
            sidebar,
            toggleButton: toggleBtn,
            summarizeButton: summarizeBtn,
            statusDisplay: status,
            summaryContainer: summary
        };
    }
})();