// ==UserScript==
// @name GPT4Free Page Summarizer (Local Free API)
// @version 1.5
// @description Generate a summary of any webpage, selected text, YouTube video transcript using a local instance of g4f
// @author SH3LL
// @match *://*/*
// @grant GM.xmlHttpRequest
// @run-at document-end
// @namespace http://tampermonkey.net/
// ==/UserScript==
function getPageText() {
return document.body.innerText;
}
function getSelectedText() {
return window.getSelection().toString();
}
function summarizePage(textToSummarize, language, maxLines, callback) {
const apiUrl = 'http://localhost:1337/v1/chat/completions';
const prompt = `Summarize the following text in ${language} in max ${maxLines} lines,
in a clear, concise and text organised in paragraphs. Each paragraph
is logally separated by others by <br> and a title (titles are inside <b></b>).
Before each title (in the same line of the title) there si an emoji contextual to the paragraph topic.
Don't add any other sentence like "Here is the summary", write directly the summary itself.
Here is the text: ${textToSummarize}`;
const data = {
messages: [{
role: 'user',
content: prompt
}],
model: 'DeepSeek-V3',
provider: 'Blackbox'
};
GM.xmlHttpRequest({
method: 'POST',
url: apiUrl,
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(data),
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
const jsonResponse = JSON.parse(response.responseText);
if (jsonResponse.choices && jsonResponse.choices[0].message && jsonResponse.choices[0].message.content) {
callback(null, jsonResponse.choices[0].message.content, response.status);
} else {
callback('Unexpected format of the API response.', null, response.status);
}
} catch (error) {
callback('Error parsing the API response: ' + error, null, response.status);
}
} else {
callback('Error in the API call: ' + response.status + ' ' + response.statusText, null, response.status);
}
},
onerror: function(error) {
callback('Network error during the API call: ' + error, null, null);
}
});
}
function getYouTubeTranscript() {
return new Promise((resolve, reject) => {
const TRANSCRIPT_TARGET_ID = "engagement-panel-searchable-transcript";
const VISIBILITY_EXPANDED = "ENGAGEMENT_PANEL_VISIBILITY_EXPANDED";
const VISIBILITY_HIDDEN = "ENGAGEMENT_PANEL_VISIBILITY_HIDDEN";
const TRANSCRIPT_CONTENT_SELECTOR = `ytd-engagement-panel-section-list-renderer[target-id="${TRANSCRIPT_TARGET_ID}"] #content`;
const SEGMENT_SELECTOR = '.segment-text';
let transcriptPanelElement = null;
function hideTranscriptPanel() {
if (transcriptPanelElement) {
transcriptPanelElement.setAttribute("visibility", VISIBILITY_HIDDEN);
transcriptPanelElement = null; // Release the reference
}
}
function extractTranscript() {
const transcriptPanelContent = document.querySelector(TRANSCRIPT_CONTENT_SELECTOR);
if (transcriptPanelContent) {
let transcriptText = "";
transcriptPanelContent.querySelectorAll(SEGMENT_SELECTOR).forEach(segment => {
transcriptText += segment.textContent.trim() + " ";
});
hideTranscriptPanel();
resolve(transcriptText.trim());
} else {
reject("Transcript panel content not found.");
}
}
function onTranscriptPanelVisible() {
const transcriptContentObserver = new MutationObserver((mutationsList, observer) => {
const transcriptPanelContent = document.querySelector(TRANSCRIPT_CONTENT_SELECTOR);
if (transcriptPanelContent && transcriptPanelContent.children.length > 0) {
extractTranscript();
observer.disconnect(); // Stop observing once extracted
}
});
const transcriptPanel = document.querySelector(`ytd-engagement-panel-section-list-renderer[target-id="${TRANSCRIPT_TARGET_ID}"]`);
const contentTarget = transcriptPanel ? transcriptPanel.querySelector('#content') : null;
const config = { childList: true, subtree: true }; // Observe for addition of child nodes
if (contentTarget) {
transcriptContentObserver.observe(contentTarget, config);
} else {
reject("Could not find the #content element within the transcript panel.");
}
}
function showTranscriptPanelAndObserveContent() {
const transcripts = document.querySelectorAll(`[target-id="${TRANSCRIPT_TARGET_ID}"]`);
if (transcripts.length === 1) {
transcriptPanelElement = transcripts[0]; // Store panel element
transcriptPanelElement.setAttribute("visibility", VISIBILITY_EXPANDED);
onTranscriptPanelVisible();
} else {
reject('Transcript panel element not found.');
}
}
const panelObserver = new MutationObserver((mutationsList, observer) => {
const transcriptPanelContainer = document.querySelector(`ytd-engagement-panel-section-list-renderer[target-id="${TRANSCRIPT_TARGET_ID}"]`);
if (transcriptPanelContainer) {
showTranscriptPanelAndObserveContent();
observer.disconnect(); // Stop observing once found
}
});
// Start observing for the transcript panel container
const targetNode = document.body;
const config = { childList: true, subtree: true };
panelObserver.observe(targetNode, config);
// Set a timeout in case the transcript panel doesn't load
setTimeout(() => {
if (!transcriptPanelElement && !document.querySelector(TRANSCRIPT_CONTENT_SELECTOR)) {
panelObserver.disconnect(); // Ensure observer is stopped
reject("Timeout: Could not find transcript panel.");
}
}, 10000); // Adjust timeout as needed
});
}
(function() {
'use strict';
// GET LANGUAGE PAGE
const browserLanguage = navigator.language;
let selectedLanguage;
try {
const languageNames = new Intl.DisplayNames([browserLanguage], { type: 'language' });
selectedLanguage = languageNames.of(browserLanguage);
// Fallback to the raw language code if Intl.DisplayNames fails or returns undefined
if (!selectedLanguage) {
selectedLanguage = browserLanguage;
}
} catch (error) {
console.error("Error initializing Intl.DisplayNames:", error);
selectedLanguage = browserLanguage; // Fallback to raw language code
}
// GET YOUTUBE TRANSCRIPT
let ytTranscript = null;
const isYouTubeVideoPage = window.location.hostname.includes("youtube.com") && window.location.pathname === "/watch";
if (isYouTubeVideoPage) {
getYouTubeTranscript()
.then(transcript => {
ytTranscript = transcript;
console.log("YOUTUBE Transcript:", transcript);
})
.catch(error => {
console.error("Error getting YOUTUBE transcript:", error);
});
}
// CREATE DOM CONTAINER
const shadowHost = document.createElement('div');
document.body.appendChild(shadowHost);
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
// CREATE SIDEBAR DOM
const sidebar = document.createElement('div');
sidebar.style.position = 'fixed';
sidebar.style.right = '-300px';
sidebar.style.top = '0';
sidebar.style.width = '300px';
sidebar.style.height = '100vh';
sidebar.style.backgroundColor = '#000000';
sidebar.style.color = '#ffffff';
sidebar.style.padding = '20px';
sidebar.style.zIndex = '999999'; // Increased to ensure it's above all elements
sidebar.style.fontFamily = 'Arial, sans-serif';
sidebar.style.boxSizing = 'border-box';
sidebar.style.display = 'flex';
sidebar.style.flexDirection = 'column';
sidebar.style.gap = '10px';
sidebar.style.transition = 'right 0.3s ease';
shadowRoot.appendChild(sidebar);
// CREATE BUTTON HIDE/SHOW SIDEBAR
const toggleButton = document.createElement('button');
toggleButton.textContent = '<';
toggleButton.style.position = 'fixed';
toggleButton.style.right = '0';
toggleButton.style.top = '20px';
toggleButton.style.backgroundColor = '#FFC107'; // Mustard yellow
toggleButton.style.color = '#000000'; // White text
toggleButton.style.border = 'none';
toggleButton.style.padding = '10px';
toggleButton.style.cursor = 'pointer';
toggleButton.style.zIndex = '1000000'; // Higher than sidebar
toggleButton.style.fontSize = '14px';
toggleButton.style.transition = 'right 0.3s ease';
shadowRoot.appendChild(toggleButton);
// CREATE CONTAINER FOR BUTTON AND SELECTOR
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.gap = '10px';
buttonContainer.style.alignItems = 'center';
sidebar.appendChild(buttonContainer);
// CREATE SUMMARIZE BUTTON
const summarizeButton = document.createElement('button');
if (isYouTubeVideoPage && ytTranscript!==null) {
summarizeButton.textContent = 'Summarize YT';
}else{
summarizeButton.textContent = 'Summarize';
}
summarizeButton.style.backgroundColor = '#333333';
summarizeButton.style.color = '#ffffff';
summarizeButton.style.border = 'none';
summarizeButton.style.padding = '10px 20px';
summarizeButton.style.cursor = 'pointer';
summarizeButton.style.fontSize = '14px';
summarizeButton.style.flex = '1';
summarizeButton.style.transition = 'background-color 0.3s';
summarizeButton.onmouseover = () => summarizeButton.style.backgroundColor = '#4d4d4d';
summarizeButton.onmouseout = () => summarizeButton.style.backgroundColor = '#333333';
buttonContainer.appendChild(summarizeButton);
// Create the line number selector
const linesSelector = document.createElement('select');
linesSelector.style.backgroundColor = '#333333';
linesSelector.style.color = '#ffffff';
linesSelector.style.border = 'none';
linesSelector.style.padding = '10px';
linesSelector.style.fontSize = '14px';
linesSelector.style.cursor = 'pointer';
const lineOptions = [5, 10, 15, 20, 25, 30, 50, 100];
lineOptions.forEach(lines => {
const option = document.createElement('option');
option.value = lines;
option.textContent = `${lines} lines`;
if (lines === 5) option.selected = true;
linesSelector.appendChild(option);
});
buttonContainer.appendChild(linesSelector);
// Create the API status display with detected language
const statusDisplay = document.createElement('div');
statusDisplay.style.fontSize = '12px';
statusDisplay.style.color = '#888888';
statusDisplay.textContent = `Status: Idle | Lang: ${selectedLanguage}`;
sidebar.appendChild(statusDisplay);
// Create the summary container
const summaryContainer = document.createElement('div');
summaryContainer.style.fontSize = '14px';
summaryContainer.style.lineHeight = '1.5';
summaryContainer.style.display = 'none';
summaryContainer.style.overflowY = 'auto';
summaryContainer.style.maxHeight = 'calc(100vh - 130px)';
sidebar.appendChild(summaryContainer);
// Variable to track if text is selected
let isTextSelected = false;
// Function to update the button text based on text selection
function updateButtonText() {
const selectedText = getSelectedText();
if (selectedText) {
summarizeButton.textContent = `Summarize [${selectedText.substring(0, 2).trim().replace(/^\n+/, '').trim()}..]`;
summarizeButton.style.fontSize = "14px";
isTextSelected = true;
} else if (isYouTubeVideoPage && ytTranscript!==null) {
summarizeButton.textContent = 'Summarize YT';
summarizeButton.style.fontSize = "14px";
isTextSelected = false;
} else {
summarizeButton.textContent = 'Summarize';
summarizeButton.style.fontSize = "14px";
isTextSelected = false;
}
}
// Listen for text selection changes on the document
document.addEventListener('mouseup', updateButtonText);
document.addEventListener('mousedown', () => {
setTimeout(updateButtonText, 100);
});
// Handle sidebar visibility
let isSidebarVisible = false;
toggleButton.addEventListener('click', function() {
if (isSidebarVisible) {
sidebar.style.right = '-300px';
toggleButton.style.right = '0';
toggleButton.textContent = '<';
} else {
sidebar.style.right = '0';
toggleButton.style.right = '300px';
toggleButton.textContent = '>';
}
isSidebarVisible = !isSidebarVisible;
});
// Listener for the summarize button
summarizeButton.addEventListener('click', function() {
summarizeButton.disabled = true;
summarizeButton.textContent = 'Loading...';
statusDisplay.style.color = '#888888';
statusDisplay.textContent = `Status: Requesting... | Lang: ${selectedLanguage}`;
summaryContainer.style.display = 'none';
summaryContainer.style.whiteSpace = 'pre-line';
let textToSummarize = '';
if (isYouTubeVideoPage && !isTextSelected && ytTranscript) {
textToSummarize = ytTranscript;
console.log("Summarizing YouTube transcript...");
} else if (isTextSelected) {
textToSummarize = getSelectedText();
console.log("Summarizing selected text...");
} else {
textToSummarize = getPageText();
console.log("Summarizing page text...");
}
const maxLines = parseInt(linesSelector.value, 10);
summarizePage(textToSummarize, selectedLanguage, maxLines, function(error, summary, statusCode) {
summarizeButton.disabled = false;
updateButtonText();
if (error) {
summaryContainer.textContent = 'Error: ' + error;
summaryContainer.style.color = '#ff4444';
statusDisplay.textContent = `Status: Failed${statusCode ? ` (${statusCode})` : ''} | Lang: ${selectedLanguage}`;
statusDisplay.style.color = '#ff4444';
} else {
summaryContainer.innerHTML = summary;
summaryContainer.style.color = '#ffffff';
statusDisplay.textContent = `Status: Success (${statusCode}) | Lang: ${selectedLanguage}`;
statusDisplay.style.color = '#00ff00';
}
summaryContainer.style.display = 'block';
});
});
})();