GPT4Free Page Summarizer

Summarize webpage, selected text or YouTube transcript via local API

// ==UserScript==
// @name         GPT4Free Page Summarizer
// @version      1.8
// @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: 'Blackbox / gpt-4o-mini', model: 'gpt-4o-mini', provider: 'Blackbox' },
        { label: 'DDG / gpt-4o-mini', model: 'gpt-4o-mini', provider: 'DDG' },
        { label: 'Free2GPT / gemini-1.5-flash', model: 'gemini-1.5-flash', provider: 'Free2GPT' },
        { label: 'OIVSCode / gpt-4o-mini', model: 'gpt-4o-mini', provider: 'OIVSCode' },
        { label: 'PollinationsAI / gpt-4o-mini', model: 'gpt-4o-mini', provider: 'PollinationsAI' },
        { label: 'TeachAnything / gemini-1.5-flash', model: 'gemini-1.5-flash', provider: 'TeachAnything' },
        { label: 'Websim / gemini-1.5-flash', model: 'gemini-1.5-flash', provider: 'Websim' }
    ];
    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 && document.visibilityState === 'visible') {
        triggerTranscriptLoad();
    } else if (isYouTubeVideoPage) {
        document.addEventListener('visibilitychange', function handleVisibility() {
            if (document.visibilityState === 'visible') {
                document.removeEventListener('visibilitychange', handleVisibility);
                triggerTranscriptLoad();
            }
        });
    }

    function triggerTranscriptLoad() {
        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 modern/colored emoji) and "text".
                            Don't add any other sentence like "Here is the summary". Don't add any coding formatting/header like \"\`\`\`json\".
                            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.replaceAll("```json\\n", "").replaceAll("```\\n", "").replaceAll("```", ""), 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);
                    summaryContainer.textContent = ``;
                    summaryContainer.append(craftJson(json.choices[0].message.content));
                    summaryContainer.style.color = '#ffffff';
                    updateStatusDisplay(`Success (${status})`, color);
                } catch (err) {
                    if (JSON.parse(responseText).error.message) {
                        summaryContainer.textContent = 'Error: ' + JSON.parse(responseText).error.message;
                    }else{
                        summaryContainer.textContent = `Error: ${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: '3px',
            top: '20px',
            width: '40px',
            height: '40px',
            backgroundColor: '#333',
            color: '#fff',
            border: '1px solid #cccccc',
            borderTopLeftRadius: '20px',
            borderBottomLeftRadius: '20px',
            borderTopRightRadius: '0',
            borderBottomRightRadius: '0',
            boxShadow: '0 4px 12px rgba(0, 0, 0, 0.2)',
            cursor: 'pointer',
            zIndex: '1000000',
            fontSize: '18px',
            transition: 'right 0.3s ease, opacity 0.3s ease, background-color 0.3s ease',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            opacity: '0.9'
        });



        const iconSpan = document.createElement('span');
        iconSpan.textContent = '✨';
        iconSpan.style.marginLeft = '5px';
        toggleBtn.appendChild(iconSpan);


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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址