// ==UserScript==
// @name GPT4Free Page Summarizer
// @version 1.6.2
// @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);
// === Init DOM UI ===
const { shadowRoot, sidebar, toggleButton, summarizeButton, wordsSelector, 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(console.error);
}
// === 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, maxWords) {
return new Promise((resolve, reject) => {
const prompt = `Summarize the following text in ${lang} using max ${maxWords} words.
Summary is organised in paragraphs.
Before each paragraph there is a title (inside <b></b>). Title and paragraph are never in the same line.
Before each title (in the same line of the title) there si an emoji contextual to the paragraph topic.
Don't add any other additional blank space or blank newline before or after each paragraph.
Never use more than one empty line (newline, breakline, \n, etc..) in sequence, every empty line must be single.
Don't add any other sentence like "Here is the summary", 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: 'DeepSeek-R1',
provider: 'Blackbox'
};
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';
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 observer = new MutationObserver(() => {
const content = document.querySelector(SELECTOR);
if (content && content.children.length > 0) {
observer.disconnect();
const transcript = Array.from(content.querySelectorAll('.segment-text'))
.map(seg => seg.textContent.trim())
.join(" ");
resolve(transcript.trim());
}
});
const panel = document.querySelector(`ytd-engagement-panel-section-list-renderer[target-id="${TRANSCRIPT_ID}"]`);
if (panel) {
panel.setAttribute("visibility", "ENGAGEMENT_PANEL_VISIBILITY_EXPANDED");
const content = panel.querySelector('#content');
if (content) observer.observe(content, { childList: true, subtree: true });
}
setTimeout(() => {
observer.disconnect();
reject("Timeout: Transcript not found.");
}, 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) {
summarizeButton.textContent = 'Summary 📹️';
isTextSelected = false;
} else {
summarizeButton.textContent = 'Summary';
isTextSelected = false;
}
}
function updateStatusDisplay(text, color) {
statusDisplay.innerHTML = `Status: <span style="color: ${color};">${text}</span> | Lang: <span style="color: #00bfff;">${selectedLanguage}</span>`;
}
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;
}
const wordLimit = parseInt(wordsSelector.value, 10);
loading = true;
summarizeButton.disabled = true;
summarizePage(content, selectedLanguage, wordLimit)
.then(({ status, responseText, color }) => {
try {
const json = JSON.parse(responseText);
const result = json.choices?.[0]?.message?.content || 'Invalid API response format.';
summaryContainer.innerHTML = 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 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 selector = document.createElement('select');
Object.assign(selector.style, {
backgroundColor: '#333',
color: '#fff',
border: '1px solid #cccccc',
borderRadius: '6px',
padding: '10px',
fontSize: '14px',
cursor: 'pointer'
});
[100, 200, 300].forEach(val => {
const opt = document.createElement('option');
opt.value = val;
opt.textContent = `${val} words`;
if (val === 100) opt.selected = true;
selector.appendChild(opt);
});
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);
container.appendChild(selector);
sidebar.appendChild(container);
sidebar.appendChild(status);
sidebar.appendChild(summary);
root.appendChild(sidebar);
root.appendChild(toggleBtn);
return {
shadowRoot: root,
sidebar,
toggleButton: toggleBtn,
summarizeButton: summarizeBtn,
wordsSelector: selector,
statusDisplay: status,
summaryContainer: summary
};
}
})();